feat: add icon support to button, alert, badge, and form input
Button: - :icon-left, :icon-right props for buttons with leading/trailing icons - :icon prop for icon-only buttons (square padding via .btn-icon class) - Icon size scales with button size (sm→sm, md/lg→sm/md) Alert: - Auto-assigns variant-specific icons (circle-check, alert-triangle, alert-circle, info) per variant - :icon-name prop to override default, false to suppress - Layout restructured with .alert-icon + .alert-content wrapper Badge: - :icon-name prop adds a leading icon before text - .badge-icon CSS scales icon to match badge font size Form input: - :icon-left and :icon-right props on form-input - Wraps input in .form-input-wrap with absolutely-positioned icon spans - Padding adjusts automatically via .form-input--icon-left/right All three dev targets (hiccup, replicant, squint) updated with demos.
This commit is contained in:
@@ -98,7 +98,19 @@
|
|||||||
(button/button {:variant :primary :href "#"} "Link primary")
|
(button/button {:variant :primary :href "#"} "Link primary")
|
||||||
(button/button {:variant :secondary :href "#"} "Link secondary")
|
(button/button {:variant :secondary :href "#"} "Link secondary")
|
||||||
(button/button {:variant :link} "Link button")
|
(button/button {:variant :link} "Link button")
|
||||||
(button/button {:variant :link :href "https://example.com"} "Link with href")]))
|
(button/button {:variant :link :href "https://example.com"} "Link with href")]
|
||||||
|
[:div {:style "display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center;"}
|
||||||
|
(button/button {:variant :primary :icon-left :plus} "Add item")
|
||||||
|
(button/button {:variant :secondary :icon-right :arrow-right} "Next")
|
||||||
|
(button/button {:variant :primary :icon-left :download :icon-right :arrow-down} "Download")
|
||||||
|
(button/button {:variant :ghost :icon-left :edit} "Edit")]
|
||||||
|
[:div {:style "display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center;"}
|
||||||
|
(button/button {:variant :primary :icon :plus})
|
||||||
|
(button/button {:variant :secondary :icon :search})
|
||||||
|
(button/button {:variant :ghost :icon :settings})
|
||||||
|
(button/button {:variant :danger :icon :trash})
|
||||||
|
(button/button {:variant :primary :icon :plus :size :sm})
|
||||||
|
(button/button {:variant :primary :icon :plus :size :lg})]))
|
||||||
|
|
||||||
(defn alert-demo []
|
(defn alert-demo []
|
||||||
(section "Alert"
|
(section "Alert"
|
||||||
@@ -116,7 +128,12 @@
|
|||||||
(badge/badge {:variant :outline} "Outline")
|
(badge/badge {:variant :outline} "Outline")
|
||||||
(badge/badge {:variant :success} "Success")
|
(badge/badge {:variant :success} "Success")
|
||||||
(badge/badge {:variant :warning} "Warning")
|
(badge/badge {:variant :warning} "Warning")
|
||||||
(badge/badge {:variant :danger} "Danger")]))
|
(badge/badge {:variant :danger} "Danger")]
|
||||||
|
[:div {:style "display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;"}
|
||||||
|
(badge/badge {:icon-name :check :variant :success} "Verified")
|
||||||
|
(badge/badge {:icon-name :star} "Featured")
|
||||||
|
(badge/badge {:icon-name :alert-triangle :variant :warning} "Caution")
|
||||||
|
(badge/badge {:icon-name :clock :variant :secondary} "Pending")]))
|
||||||
|
|
||||||
(defn card-demo []
|
(defn card-demo []
|
||||||
(section "Card"
|
(section "Card"
|
||||||
@@ -271,6 +288,10 @@
|
|||||||
(form/form-input {:type :datetime-local}))
|
(form/form-input {:type :datetime-local}))
|
||||||
(form/form-field {:label "Date"}
|
(form/form-field {:label "Date"}
|
||||||
(form/form-input {:type :date}))
|
(form/form-input {:type :date}))
|
||||||
|
(form/form-field {:label "Search (icon left)"}
|
||||||
|
(form/form-input {:type :text :placeholder "Search..." :icon-left :search}))
|
||||||
|
(form/form-field {:label "URL (both icons)"}
|
||||||
|
(form/form-input {:type :text :placeholder "example.com" :icon-left :globe :icon-right :check}))
|
||||||
(form/form-checkbox {:label "I agree to the terms"})
|
(form/form-checkbox {:label "I agree to the terms"})
|
||||||
(form/form-radio-group {:label "Preference"
|
(form/form-radio-group {:label "Preference"
|
||||||
:radio-name "pref"
|
:radio-name "pref"
|
||||||
|
|||||||
@@ -60,7 +60,19 @@
|
|||||||
(button/button {:variant :primary :href "#"} "Link primary")
|
(button/button {:variant :primary :href "#"} "Link primary")
|
||||||
(button/button {:variant :secondary :href "#"} "Link secondary")
|
(button/button {:variant :secondary :href "#"} "Link secondary")
|
||||||
(button/button {:variant :link} "Link button")
|
(button/button {:variant :link} "Link button")
|
||||||
(button/button {:variant :link :href "https://example.com"} "Link with href")]))
|
(button/button {:variant :link :href "https://example.com"} "Link with href")]
|
||||||
|
[:div {:style {:display "flex" :gap "0.75rem" :flex-wrap "wrap" :align-items "center"}}
|
||||||
|
(button/button {:variant :primary :icon-left :plus} "Add item")
|
||||||
|
(button/button {:variant :secondary :icon-right :arrow-right} "Next")
|
||||||
|
(button/button {:variant :primary :icon-left :download :icon-right :arrow-down} "Download")
|
||||||
|
(button/button {:variant :ghost :icon-left :edit} "Edit")]
|
||||||
|
[:div {:style {:display "flex" :gap "0.75rem" :flex-wrap "wrap" :align-items "center"}}
|
||||||
|
(button/button {:variant :primary :icon :plus})
|
||||||
|
(button/button {:variant :secondary :icon :search})
|
||||||
|
(button/button {:variant :ghost :icon :settings})
|
||||||
|
(button/button {:variant :danger :icon :trash})
|
||||||
|
(button/button {:variant :primary :icon :plus :size :sm})
|
||||||
|
(button/button {:variant :primary :icon :plus :size :lg})]))
|
||||||
|
|
||||||
(defn alert-demo []
|
(defn alert-demo []
|
||||||
(section "Alert"
|
(section "Alert"
|
||||||
@@ -78,7 +90,12 @@
|
|||||||
(badge/badge {:variant :outline} "Outline")
|
(badge/badge {:variant :outline} "Outline")
|
||||||
(badge/badge {:variant :success} "Success")
|
(badge/badge {:variant :success} "Success")
|
||||||
(badge/badge {:variant :warning} "Warning")
|
(badge/badge {:variant :warning} "Warning")
|
||||||
(badge/badge {:variant :danger} "Danger")]))
|
(badge/badge {:variant :danger} "Danger")]
|
||||||
|
[:div {:style {:display "flex" :gap "0.5rem" :flex-wrap "wrap" :align-items "center"}}
|
||||||
|
(badge/badge {:icon-name :check :variant :success} "Verified")
|
||||||
|
(badge/badge {:icon-name :star} "Featured")
|
||||||
|
(badge/badge {:icon-name :alert-triangle :variant :warning} "Caution")
|
||||||
|
(badge/badge {:icon-name :clock :variant :secondary} "Pending")]))
|
||||||
|
|
||||||
(defn card-demo []
|
(defn card-demo []
|
||||||
(section "Card"
|
(section "Card"
|
||||||
@@ -235,6 +252,10 @@
|
|||||||
(form/form-input {:type :datetime-local}))
|
(form/form-input {:type :datetime-local}))
|
||||||
(form/form-field {:label "Date"}
|
(form/form-field {:label "Date"}
|
||||||
(form/form-input {:type :date}))
|
(form/form-input {:type :date}))
|
||||||
|
(form/form-field {:label "Search (icon left)"}
|
||||||
|
(form/form-input {:type :text :placeholder "Search..." :icon-left :search}))
|
||||||
|
(form/form-field {:label "URL (both icons)"}
|
||||||
|
(form/form-input {:type :text :placeholder "example.com" :icon-left :globe :icon-right :check}))
|
||||||
(form/form-checkbox {:label "I agree to the terms"})
|
(form/form-checkbox {:label "I agree to the terms"})
|
||||||
(form/form-radio-group {:label "Preference"
|
(form/form-radio-group {:label "Preference"
|
||||||
:radio-name "pref"
|
:radio-name "pref"
|
||||||
|
|||||||
@@ -78,7 +78,19 @@
|
|||||||
[(button/button {:variant "primary" :href "#"} "Link primary")
|
[(button/button {:variant "primary" :href "#"} "Link primary")
|
||||||
(button/button {:variant "secondary" :href "#"} "Link secondary")
|
(button/button {:variant "secondary" :href "#"} "Link secondary")
|
||||||
(button/button {:variant "link"} "Link button")
|
(button/button {:variant "link"} "Link button")
|
||||||
(button/button {:variant "link" :href "https://example.com"} "Link with href")])))
|
(button/button {:variant "link" :href "https://example.com"} "Link with href")])
|
||||||
|
(into [:div {:style {"display" "flex" "gap" "0.75rem" "flex-wrap" "wrap" "align-items" "center"}}]
|
||||||
|
[(button/button {:variant "primary" :icon-left "plus"} "Add item")
|
||||||
|
(button/button {:variant "secondary" :icon-right "arrow-right"} "Next")
|
||||||
|
(button/button {:variant "primary" :icon-left "download" :icon-right "arrow-down"} "Download")
|
||||||
|
(button/button {:variant "ghost" :icon-left "edit"} "Edit")])
|
||||||
|
(into [:div {:style {"display" "flex" "gap" "0.75rem" "flex-wrap" "wrap" "align-items" "center"}}]
|
||||||
|
[(button/button {:variant "primary" :icon "plus"})
|
||||||
|
(button/button {:variant "secondary" :icon "search"})
|
||||||
|
(button/button {:variant "ghost" :icon "settings"})
|
||||||
|
(button/button {:variant "danger" :icon "trash"})
|
||||||
|
(button/button {:variant "primary" :icon "plus" :size "sm"})
|
||||||
|
(button/button {:variant "primary" :icon "plus" :size "lg"})])))
|
||||||
|
|
||||||
(defn alert-demo []
|
(defn alert-demo []
|
||||||
(section "Alert"
|
(section "Alert"
|
||||||
@@ -96,7 +108,12 @@
|
|||||||
(badge/badge {:variant "outline"} "Outline")
|
(badge/badge {:variant "outline"} "Outline")
|
||||||
(badge/badge {:variant "success"} "Success")
|
(badge/badge {:variant "success"} "Success")
|
||||||
(badge/badge {:variant "warning"} "Warning")
|
(badge/badge {:variant "warning"} "Warning")
|
||||||
(badge/badge {:variant "danger"} "Danger")])))
|
(badge/badge {:variant "danger"} "Danger")])
|
||||||
|
(into [:div {:style {"display" "flex" "gap" "0.5rem" "flex-wrap" "wrap" "align-items" "center"}}]
|
||||||
|
[(badge/badge {:icon-name "check" :variant "success"} "Verified")
|
||||||
|
(badge/badge {:icon-name "star"} "Featured")
|
||||||
|
(badge/badge {:icon-name "alert-triangle" :variant "warning"} "Caution")
|
||||||
|
(badge/badge {:icon-name "clock" :variant "secondary"} "Pending")])))
|
||||||
|
|
||||||
(defn card-demo []
|
(defn card-demo []
|
||||||
(section "Card"
|
(section "Card"
|
||||||
@@ -253,6 +270,10 @@
|
|||||||
(form/form-input {:type "datetime-local"}))
|
(form/form-input {:type "datetime-local"}))
|
||||||
(form/form-field {:label "Date"}
|
(form/form-field {:label "Date"}
|
||||||
(form/form-input {:type "date"}))
|
(form/form-input {:type "date"}))
|
||||||
|
(form/form-field {:label "Search (icon left)"}
|
||||||
|
(form/form-input {:type "text" :placeholder "Search..." :icon-left "search"}))
|
||||||
|
(form/form-field {:label "URL (both icons)"}
|
||||||
|
(form/form-input {:type "text" :placeholder "example.com" :icon-left "globe" :icon-right "check"}))
|
||||||
(form/form-checkbox {:label "I agree to the terms"})
|
(form/form-checkbox {:label "I agree to the terms"})
|
||||||
(form/form-radio-group {:label "Preference"
|
(form/form-radio-group {:label "Preference"
|
||||||
:radio-name "pref"
|
:radio-name "pref"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
(ns ui.alert
|
(ns ui.alert
|
||||||
(:require [clojure.string :as str]))
|
(:require [clojure.string :as str]
|
||||||
|
[ui.icon :as icon]))
|
||||||
|
|
||||||
#?(:squint (defn- kw-name [s] s)
|
#?(:squint (defn- kw-name [s] s)
|
||||||
:cljs (defn- kw-name [s] (name s))
|
:cljs (defn- kw-name [s] (name s))
|
||||||
@@ -17,38 +18,61 @@
|
|||||||
[opts]
|
[opts]
|
||||||
(str/join " " (alert-class-list opts)))
|
(str/join " " (alert-class-list opts)))
|
||||||
|
|
||||||
|
(def ^:private variant-icons
|
||||||
|
"Default icon names per alert variant."
|
||||||
|
{"success" :circle-check
|
||||||
|
"warning" :alert-triangle
|
||||||
|
"danger" :alert-circle
|
||||||
|
"info" :info})
|
||||||
|
|
||||||
(defn alert
|
(defn alert
|
||||||
"Render an alert element.
|
"Render an alert element.
|
||||||
|
|
||||||
Props:
|
Props:
|
||||||
:variant - :success, :warning, :danger, :info (nil for neutral)
|
:variant - :success, :warning, :danger, :info (nil for neutral)
|
||||||
:title - optional title string
|
:title - optional title string
|
||||||
:class - additional CSS classes
|
:icon-name - override icon (nil uses variant default, false to suppress)
|
||||||
:attrs - additional HTML attributes"
|
:class - additional CSS classes
|
||||||
[{:keys [variant title class attrs] :as _props} & children]
|
:attrs - additional HTML attributes"
|
||||||
#?(:squint
|
[{:keys [variant title icon-name class attrs] :as _props} & children]
|
||||||
(let [classes (cond-> (alert-classes {:variant variant})
|
(let [v (some-> variant kw-name)
|
||||||
class (str " " class))
|
iname (cond
|
||||||
base-attrs (merge {:class classes :role "alert"} attrs)]
|
(false? icon-name) nil ;; explicitly suppressed
|
||||||
(into [:div base-attrs]
|
icon-name icon-name ;; explicit override
|
||||||
(cond-> []
|
v (get variant-icons v))]
|
||||||
title (conj [:p {:class "alert-title"} title])
|
#?(:squint
|
||||||
:always (into (map (fn [c] [:p {:class "alert-body"} c]) children)))))
|
(let [classes (cond-> (alert-classes {:variant variant})
|
||||||
|
class (str " " class))
|
||||||
|
base-attrs (merge {:class classes :role "alert"} attrs)]
|
||||||
|
(into [:div base-attrs]
|
||||||
|
(cond-> []
|
||||||
|
iname (conj [:span {:class "alert-icon"} (icon/icon {:icon-name iname :size :sm})])
|
||||||
|
true (conj (into [:div {:class "alert-content"}]
|
||||||
|
(cond-> []
|
||||||
|
title (conj [:p {:class "alert-title"} title])
|
||||||
|
:always (into (map (fn [c] [:p {:class "alert-body"} c]) children))))))))
|
||||||
|
|
||||||
:cljs
|
:cljs
|
||||||
(let [cls (alert-class-list {:variant variant})
|
(let [cls (alert-class-list {:variant variant})
|
||||||
classes (cond-> cls class (conj class))
|
classes (cond-> cls class (conj class))
|
||||||
base-attrs (merge {:class classes :role "alert"} attrs)]
|
base-attrs (merge {:class classes :role "alert"} attrs)]
|
||||||
(into [:div base-attrs]
|
(into [:div base-attrs]
|
||||||
(cond-> []
|
(cond-> []
|
||||||
title (conj [:p {:class ["alert-title"]} title])
|
iname (conj [:span {:class ["alert-icon"]} (icon/icon {:icon-name iname :size :sm})])
|
||||||
:always (into (map (fn [c] [:p {:class ["alert-body"]} c]) children)))))
|
true (conj (into [:div {:class ["alert-content"]}]
|
||||||
|
(cond-> []
|
||||||
|
title (conj [:p {:class ["alert-title"]} title])
|
||||||
|
:always (into (map (fn [c] [:p {:class ["alert-body"]} c]) children))))))))
|
||||||
|
|
||||||
|
:clj
|
||||||
|
(let [classes (cond-> (alert-classes {:variant variant})
|
||||||
|
class (str " " class))
|
||||||
|
base-attrs (merge {:class classes :role "alert"} attrs)]
|
||||||
|
(into [:div base-attrs]
|
||||||
|
(cond-> []
|
||||||
|
iname (conj [:span {:class "alert-icon"} (icon/icon {:icon-name iname :size :sm})])
|
||||||
|
true (conj (into [:div {:class "alert-content"}]
|
||||||
|
(cond-> []
|
||||||
|
title (conj [:p {:class "alert-title"} title])
|
||||||
|
:always (into (map (fn [c] [:p {:class "alert-body"} c]) children)))))))))))
|
||||||
|
|
||||||
:clj
|
|
||||||
(let [classes (cond-> (alert-classes {:variant variant})
|
|
||||||
class (str " " class))
|
|
||||||
base-attrs (merge {:class classes :role "alert"} attrs)]
|
|
||||||
(into [:div base-attrs]
|
|
||||||
(cond-> []
|
|
||||||
title (conj [:p {:class "alert-title"} title])
|
|
||||||
:always (into (map (fn [c] [:p {:class "alert-body"} c]) children)))))))
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: flex-start;
|
||||||
align-items: center;
|
|
||||||
gap: var(--size-3);
|
gap: var(--size-3);
|
||||||
padding: var(--size-4) var(--size-6);
|
padding: var(--size-4) var(--size-6);
|
||||||
background: var(--bg-1);
|
background: var(--bg-1);
|
||||||
@@ -12,6 +11,18 @@
|
|||||||
font-size: var(--font-sm);
|
font-size: var(--font-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: var(--size-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-title {
|
.alert-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
(ns ui.badge
|
(ns ui.badge
|
||||||
(:require [clojure.string :as str]))
|
(:require [clojure.string :as str]
|
||||||
|
[ui.icon :as icon]))
|
||||||
|
|
||||||
#?(:squint (defn- kw-name [s] s)
|
#?(:squint (defn- kw-name [s] s)
|
||||||
:cljs (defn- kw-name [s] (name s))
|
:cljs (defn- kw-name [s] (name s))
|
||||||
@@ -25,24 +26,34 @@
|
|||||||
"Render a badge element.
|
"Render a badge element.
|
||||||
|
|
||||||
Props:
|
Props:
|
||||||
:variant - :primary, :secondary, :outline, :success, :warning, :danger
|
:variant - :primary, :secondary, :outline, :success, :warning, :danger
|
||||||
:class - additional CSS classes
|
:icon-name - optional leading icon (e.g. :check, :star)
|
||||||
:attrs - additional HTML attributes"
|
:class - additional CSS classes
|
||||||
[{:keys [variant class attrs] :as _props} & children]
|
:attrs - additional HTML attributes"
|
||||||
|
[{:keys [variant icon-name class attrs] :as _props} & children]
|
||||||
#?(:squint
|
#?(:squint
|
||||||
(let [classes (cond-> (badge-classes {:variant variant})
|
(let [classes (cond-> (badge-classes {:variant variant})
|
||||||
class (str " " class))
|
class (str " " class))
|
||||||
base-attrs (merge {:class classes} attrs)]
|
base-attrs (merge {:class classes} attrs)]
|
||||||
(into [:span base-attrs] children))
|
(into [:span base-attrs]
|
||||||
|
(cond-> []
|
||||||
|
icon-name (conj (icon/icon {:icon-name icon-name :size :sm :class "badge-icon"}))
|
||||||
|
true (into children))))
|
||||||
|
|
||||||
:cljs
|
:cljs
|
||||||
(let [cls (badge-class-list {:variant variant})
|
(let [cls (badge-class-list {:variant variant})
|
||||||
classes (cond-> cls class (conj class))
|
classes (cond-> cls class (conj class))
|
||||||
base-attrs (merge {:class classes} attrs)]
|
base-attrs (merge {:class classes} attrs)]
|
||||||
(into [:span base-attrs] children))
|
(into [:span base-attrs]
|
||||||
|
(cond-> []
|
||||||
|
icon-name (conj (icon/icon {:icon-name icon-name :size :sm :class "badge-icon"}))
|
||||||
|
true (into children))))
|
||||||
|
|
||||||
:clj
|
:clj
|
||||||
(let [classes (cond-> (badge-classes {:variant variant})
|
(let [classes (cond-> (badge-classes {:variant variant})
|
||||||
class (str " " class))
|
class (str " " class))
|
||||||
base-attrs (merge {:class classes} attrs)]
|
base-attrs (merge {:class classes} attrs)]
|
||||||
(into [:span base-attrs] children))))
|
(into [:span base-attrs]
|
||||||
|
(cond-> []
|
||||||
|
icon-name (conj (icon/icon {:icon-name icon-name :size :sm :class "badge-icon"}))
|
||||||
|
true (into children))))))
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
color: var(--fg-on-accent);
|
color: var(--fg-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-icon {
|
||||||
|
width: 0.875em;
|
||||||
|
height: 0.875em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.badge-secondary {
|
.badge-secondary {
|
||||||
background: var(--bg-2);
|
background: var(--bg-2);
|
||||||
color: var(--fg-0);
|
color: var(--fg-0);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
(ns ui.button
|
(ns ui.button
|
||||||
(:require [clojure.string :as str]))
|
(:require [clojure.string :as str]
|
||||||
|
[ui.icon :as icon]))
|
||||||
|
|
||||||
;; In squint, keywords are strings — name is identity
|
;; In squint, keywords are strings — name is identity
|
||||||
#?(:squint (defn- kw-name [s] s)
|
#?(:squint (defn- kw-name [s] s)
|
||||||
@@ -10,13 +11,15 @@
|
|||||||
(def default-size "md")
|
(def default-size "md")
|
||||||
|
|
||||||
(defn button-class-list
|
(defn button-class-list
|
||||||
"Generate a vector of CSS class strings for a button given variant and size.
|
"Generate a vector of CSS class strings for a button given variant, size, and icon mode.
|
||||||
Returns e.g. [\"btn\" \"btn-primary\" \"btn-lg\"]."
|
Returns e.g. [\"btn\" \"btn-primary\" \"btn-lg\"].
|
||||||
[{:keys [variant size]}]
|
When :icon is provided (icon-only mode), adds \"btn-icon\"."
|
||||||
|
[{:keys [variant size icon]}]
|
||||||
(let [v (or (some-> variant kw-name) default-variant)
|
(let [v (or (some-> variant kw-name) default-variant)
|
||||||
s (or (some-> size kw-name) default-size)]
|
s (or (some-> size kw-name) default-size)]
|
||||||
(cond-> ["btn" (str "btn-" v)]
|
(cond-> ["btn" (str "btn-" v)]
|
||||||
(not= s "md") (conj (str "btn-" s)))))
|
(not= s "md") (conj (str "btn-" s))
|
||||||
|
icon (conj "btn-icon"))))
|
||||||
|
|
||||||
(defn button-classes
|
(defn button-classes
|
||||||
"Generate CSS class string for a button. Returns a space-joined string."
|
"Generate CSS class string for a button. Returns a space-joined string."
|
||||||
@@ -28,42 +31,68 @@
|
|||||||
When :href is provided, renders as <a> instead of <button>.
|
When :href is provided, renders as <a> instead of <button>.
|
||||||
|
|
||||||
Props:
|
Props:
|
||||||
:variant - :primary, :secondary, :ghost, :danger, :link
|
:variant - :primary, :secondary, :ghost, :danger, :link
|
||||||
:size - :sm, :md, :lg
|
:size - :sm, :md, :lg
|
||||||
:href - URL string; when set, renders as <a> tag
|
:href - URL string; when set, renders as <a> tag
|
||||||
:on-click - click handler (ignored in :clj target)
|
:on-click - click handler (ignored in :clj target)
|
||||||
:disabled - boolean
|
:disabled - boolean
|
||||||
:class - additional CSS classes (string or vector)
|
:icon-left - icon name keyword for left icon (e.g. :plus)
|
||||||
:attrs - additional HTML attributes map"
|
:icon-right - icon name keyword for right icon (e.g. :arrow-right)
|
||||||
[{:keys [variant size href on-click disabled class attrs] :as _props} & children]
|
:icon - icon name keyword for icon-only button (no text children)
|
||||||
#?(:squint
|
:class - additional CSS classes (string or vector)
|
||||||
(let [tag (if href :a :button)
|
:attrs - additional HTML attributes map"
|
||||||
classes (cond-> (button-classes {:variant variant :size size})
|
[{:keys [variant size href on-click disabled icon-left icon-right icon class attrs] :as _props} & children]
|
||||||
class (str " " class))
|
(let [icon-size (case (kw-name (or size default-size))
|
||||||
base-attrs (cond-> (merge {:class classes} attrs)
|
"sm" :sm
|
||||||
href (assoc :href href)
|
"lg" :md
|
||||||
disabled (assoc :disabled true))]
|
#?(:squint "sm" :cljs :sm :clj :sm))]
|
||||||
(into [tag (cond-> base-attrs
|
#?(:squint
|
||||||
on-click (assoc :on-click on-click))]
|
(let [tag (if href :a :button)
|
||||||
children))
|
classes (cond-> (button-classes {:variant variant :size size :icon icon})
|
||||||
|
class (str " " class))
|
||||||
|
base-attrs (cond-> (merge {:class classes} attrs)
|
||||||
|
href (assoc :href href)
|
||||||
|
disabled (assoc :disabled true))
|
||||||
|
base-attrs (cond-> base-attrs
|
||||||
|
on-click (assoc :on-click on-click))]
|
||||||
|
(if icon
|
||||||
|
[tag base-attrs (icon/icon {:icon-name icon :size icon-size})]
|
||||||
|
(into [tag base-attrs]
|
||||||
|
(cond-> []
|
||||||
|
icon-left (conj (icon/icon {:icon-name icon-left :size icon-size}))
|
||||||
|
true (into children)
|
||||||
|
icon-right (conj (icon/icon {:icon-name icon-right :size icon-size}))))))
|
||||||
|
|
||||||
:cljs
|
:cljs
|
||||||
(let [tag (if href :a :button)
|
(let [tag (if href :a :button)
|
||||||
cls (button-class-list {:variant variant :size size})
|
cls (button-class-list {:variant variant :size size :icon icon})
|
||||||
classes (cond-> cls
|
classes (cond-> cls
|
||||||
class (conj class))
|
class (conj class))
|
||||||
base-attrs (cond-> (merge {:class classes} attrs)
|
base-attrs (cond-> (merge {:class classes} attrs)
|
||||||
href (assoc :href href)
|
href (assoc :href href)
|
||||||
disabled (assoc :disabled true))]
|
disabled (assoc :disabled true))
|
||||||
(into [tag (cond-> base-attrs
|
base-attrs (cond-> base-attrs
|
||||||
on-click (assoc-in [:on :click] on-click))]
|
on-click (assoc-in [:on :click] on-click))]
|
||||||
children))
|
(if icon
|
||||||
|
[tag base-attrs (icon/icon {:icon-name icon :size icon-size})]
|
||||||
|
(into [tag base-attrs]
|
||||||
|
(cond-> []
|
||||||
|
icon-left (conj (icon/icon {:icon-name icon-left :size icon-size}))
|
||||||
|
true (into children)
|
||||||
|
icon-right (conj (icon/icon {:icon-name icon-right :size icon-size}))))))
|
||||||
|
|
||||||
|
:clj
|
||||||
|
(let [tag (if href :a :button)
|
||||||
|
classes (cond-> (button-classes {:variant variant :size size :icon icon})
|
||||||
|
class (str " " class))
|
||||||
|
base-attrs (cond-> (merge {:class classes} attrs)
|
||||||
|
href (assoc :href href)
|
||||||
|
disabled (assoc :disabled true))]
|
||||||
|
(if icon
|
||||||
|
[tag base-attrs (icon/icon {:icon-name icon :size icon-size})]
|
||||||
|
(into [tag base-attrs]
|
||||||
|
(cond-> []
|
||||||
|
icon-left (conj (icon/icon {:icon-name icon-left :size icon-size}))
|
||||||
|
true (into children)
|
||||||
|
icon-right (conj (icon/icon {:icon-name icon-right :size icon-size})))))))))
|
||||||
|
|
||||||
:clj
|
|
||||||
(let [tag (if href :a :button)
|
|
||||||
classes (cond-> (button-classes {:variant variant :size size})
|
|
||||||
class (str " " class))
|
|
||||||
base-attrs (cond-> (merge {:class classes} attrs)
|
|
||||||
href (assoc :href href)
|
|
||||||
disabled (assoc :disabled true))]
|
|
||||||
(into [tag base-attrs] children))))
|
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ a.btn-link {
|
|||||||
line-height: var(--size-6);
|
line-height: var(--size-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Icon-only button ────────────────────────────────────────── */
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
padding: var(--size-2);
|
padding: var(--size-2);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@@ -96,6 +98,8 @@ a.btn-link {
|
|||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Disabled ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@@ -93,7 +93,8 @@
|
|||||||
(str/join " " (form-input-class-list opts)))
|
(str/join " " (form-input-class-list opts)))
|
||||||
|
|
||||||
(defn form-input
|
(defn form-input
|
||||||
"Render a text input element.
|
"Render a text input element. When :icon-left or :icon-right is provided,
|
||||||
|
wraps in a .form-input-wrap div with absolutely-positioned icons.
|
||||||
|
|
||||||
Props:
|
Props:
|
||||||
:type - :text, :email, :password, :date, :datetime-local, etc.
|
:type - :text, :email, :password, :date, :datetime-local, etc.
|
||||||
@@ -101,36 +102,67 @@
|
|||||||
:value - input value
|
:value - input value
|
||||||
:disabled - boolean
|
:disabled - boolean
|
||||||
:error - boolean, adds error styling
|
:error - boolean, adds error styling
|
||||||
|
:icon-left - icon name keyword for left icon (e.g. :search)
|
||||||
|
:icon-right - icon name keyword for right icon (e.g. :check)
|
||||||
:on-change - change handler (ignored in :clj target)
|
:on-change - change handler (ignored in :clj target)
|
||||||
:class - additional CSS classes
|
:class - additional CSS classes
|
||||||
:attrs - additional HTML attributes"
|
:attrs - additional HTML attributes"
|
||||||
[{:keys [type placeholder value disabled error on-change class attrs] :as _props}]
|
[{:keys [type placeholder value disabled error icon-left icon-right on-change class attrs] :as _props}]
|
||||||
(let [input-type (or (some-> type kw-name) "text")]
|
(let [input-type (or (some-> type kw-name) "text")
|
||||||
|
has-icons (or icon-left icon-right)]
|
||||||
#?(:squint
|
#?(:squint
|
||||||
(let [classes (cond-> (form-input-classes {:error error})
|
(let [input-cls (cond-> (form-input-classes {:error error})
|
||||||
class (str " " class))]
|
class (str " " class)
|
||||||
[:input (cond-> (merge {:class classes :type input-type} attrs)
|
icon-left (str " form-input--icon-left")
|
||||||
placeholder (assoc :placeholder placeholder)
|
icon-right (str " form-input--icon-right"))
|
||||||
value (assoc :value value)
|
input-el [:input (cond-> (merge {:class input-cls :type input-type} attrs)
|
||||||
disabled (assoc :disabled true)
|
placeholder (assoc :placeholder placeholder)
|
||||||
on-change (assoc :on-change on-change))])
|
value (assoc :value value)
|
||||||
|
disabled (assoc :disabled true)
|
||||||
|
on-change (assoc :on-change on-change))]]
|
||||||
|
(if has-icons
|
||||||
|
(into [:div {:class "form-input-wrap"}]
|
||||||
|
(cond-> []
|
||||||
|
icon-left (conj [:span {:class "form-input-icon form-input-icon--left"} (icon/icon {:icon-name icon-left :size :sm})])
|
||||||
|
true (conj input-el)
|
||||||
|
icon-right (conj [:span {:class "form-input-icon form-input-icon--right"} (icon/icon {:icon-name icon-right :size :sm})])))
|
||||||
|
input-el))
|
||||||
|
|
||||||
:cljs
|
:cljs
|
||||||
(let [cls (form-input-class-list {:error error})
|
(let [cls (form-input-class-list {:error error})
|
||||||
classes (cond-> cls class (conj class))]
|
input-cls (cond-> cls
|
||||||
[:input (cond-> (merge {:class classes :type input-type} attrs)
|
class (conj class)
|
||||||
placeholder (assoc :placeholder placeholder)
|
icon-left (conj "form-input--icon-left")
|
||||||
value (assoc :value value)
|
icon-right (conj "form-input--icon-right"))
|
||||||
disabled (assoc :disabled true)
|
input-el [:input (cond-> (merge {:class input-cls :type input-type} attrs)
|
||||||
on-change (assoc-in [:on :change] on-change))])
|
placeholder (assoc :placeholder placeholder)
|
||||||
|
value (assoc :value value)
|
||||||
|
disabled (assoc :disabled true)
|
||||||
|
on-change (assoc-in [:on :change] on-change))]]
|
||||||
|
(if has-icons
|
||||||
|
(into [:div {:class ["form-input-wrap"]}]
|
||||||
|
(cond-> []
|
||||||
|
icon-left (conj [:span {:class ["form-input-icon" "form-input-icon--left"]} (icon/icon {:icon-name icon-left :size :sm})])
|
||||||
|
true (conj input-el)
|
||||||
|
icon-right (conj [:span {:class ["form-input-icon" "form-input-icon--right"]} (icon/icon {:icon-name icon-right :size :sm})])))
|
||||||
|
input-el))
|
||||||
|
|
||||||
:clj
|
:clj
|
||||||
(let [classes (cond-> (form-input-classes {:error error})
|
(let [input-cls (cond-> (form-input-classes {:error error})
|
||||||
class (str " " class))]
|
class (str " " class)
|
||||||
[:input (cond-> (merge {:class classes :type input-type} attrs)
|
icon-left (str " form-input--icon-left")
|
||||||
placeholder (assoc :placeholder placeholder)
|
icon-right (str " form-input--icon-right"))
|
||||||
value (assoc :value value)
|
input-el [:input (cond-> (merge {:class input-cls :type input-type} attrs)
|
||||||
disabled (assoc :disabled true))]))))
|
placeholder (assoc :placeholder placeholder)
|
||||||
|
value (assoc :value value)
|
||||||
|
disabled (assoc :disabled true))]]
|
||||||
|
(if has-icons
|
||||||
|
(into [:div {:class "form-input-wrap"}]
|
||||||
|
(cond-> []
|
||||||
|
icon-left (conj [:span {:class "form-input-icon form-input-icon--left"} (icon/icon {:icon-name icon-left :size :sm})])
|
||||||
|
true (conj input-el)
|
||||||
|
icon-right (conj [:span {:class "form-input-icon form-input-icon--right"} (icon/icon {:icon-name icon-right :size :sm})])))
|
||||||
|
input-el)))))
|
||||||
|
|
||||||
;; ── Textarea ────────────────────────────────────────────────────────
|
;; ── Textarea ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,39 @@
|
|||||||
outline-color: var(--danger);
|
outline-color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Input with icons ──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.form-input-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--fg-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input-icon--left {
|
||||||
|
left: var(--size-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input-icon--right {
|
||||||
|
right: var(--size-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input--icon-left {
|
||||||
|
padding-left: var(--size-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input--icon-right {
|
||||||
|
padding-right: var(--size-8);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Textarea ──────────────────────────────────────────────────── */
|
/* ── Textarea ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.form-textarea {
|
.form-textarea {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
(ns ui.alert-test
|
(ns ui.alert-test
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
[ui.alert :as alert]))
|
[ui.alert :as alert]
|
||||||
|
[ui.icon :as icon]))
|
||||||
|
|
||||||
(deftest alert-class-list-test
|
(deftest alert-class-list-test
|
||||||
(testing "neutral (no variant)"
|
(testing "neutral (no variant)"
|
||||||
@@ -24,12 +25,29 @@
|
|||||||
(is (= "alert alert-success" (get-in result [1 :class])))
|
(is (= "alert alert-success" (get-in result [1 :class])))
|
||||||
(is (= "alert" (get-in result [1 :role])))))
|
(is (= "alert" (get-in result [1 :role])))))
|
||||||
|
|
||||||
(testing "alert with title includes title paragraph"
|
(testing "variant alert includes default icon"
|
||||||
(let [result (alert/alert {:title "Title"} "Body")]
|
(let [result (alert/alert {:variant :success :title "Done!"} "Saved.")
|
||||||
(is (some #(and (vector? %) (= "alert-title" (get-in % [1 :class])))
|
icon-span (nth result 2)]
|
||||||
(rest (rest result))))))
|
(is (= :span (first icon-span)))
|
||||||
|
(is (= "alert-icon" (get-in icon-span [1 :class])))))
|
||||||
|
|
||||||
(testing "alert without title has no title paragraph"
|
(testing "neutral alert has no icon"
|
||||||
(let [result (alert/alert {} "Body")]
|
(let [result (alert/alert {:title "Title"} "Body")]
|
||||||
(is (not (some #(and (vector? %) (= "alert-title" (get-in % [1 :class])))
|
(is (not (some #(and (vector? %) (= "alert-icon" (get-in % [1 :class])))
|
||||||
(rest (rest result))))))))
|
(rest (rest result)))))))
|
||||||
|
|
||||||
|
(testing "icon-name false suppresses icon"
|
||||||
|
(let [result (alert/alert {:variant :success :icon-name false} "Body")]
|
||||||
|
(is (not (some #(and (vector? %) (= "alert-icon" (get-in % [1 :class])))
|
||||||
|
(rest (rest result)))))))
|
||||||
|
|
||||||
|
(testing "icon-name overrides default"
|
||||||
|
(let [result (alert/alert {:variant :success :icon-name :star} "Body")
|
||||||
|
icon-span (nth result 2)]
|
||||||
|
(is (= "alert-icon" (get-in icon-span [1 :class])))))
|
||||||
|
|
||||||
|
(testing "alert content is wrapped in alert-content div"
|
||||||
|
(let [result (alert/alert {:variant :info :title "Title"} "Body")
|
||||||
|
content-div (last result)]
|
||||||
|
(is (= :div (first content-div)))
|
||||||
|
(is (= "alert-content" (get-in content-div [1 :class]))))))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
(ns ui.badge-test
|
(ns ui.badge-test
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
[ui.badge :as badge]))
|
[ui.badge :as badge]
|
||||||
|
[ui.icon :as icon]))
|
||||||
|
|
||||||
(deftest badge-class-list-test
|
(deftest badge-class-list-test
|
||||||
(testing "default variant (primary)"
|
(testing "default variant (primary)"
|
||||||
@@ -29,3 +30,14 @@
|
|||||||
(testing "extra class gets appended"
|
(testing "extra class gets appended"
|
||||||
(let [result (badge/badge {:class "extra"} "X")]
|
(let [result (badge/badge {:class "extra"} "X")]
|
||||||
(is (= "badge extra" (get-in result [1 :class]))))))
|
(is (= "badge extra" (get-in result [1 :class]))))))
|
||||||
|
|
||||||
|
(deftest badge-icon-test
|
||||||
|
(testing "badge with icon-name renders icon before text"
|
||||||
|
(let [result (badge/badge {:icon-name :check} "Done")]
|
||||||
|
(is (= :span (first result)))
|
||||||
|
(is (= :svg (first (nth result 2)))) ;; icon is first child
|
||||||
|
(is (= "Done" (nth result 3))))) ;; text is second child
|
||||||
|
|
||||||
|
(testing "badge without icon-name has no icon"
|
||||||
|
(let [result (badge/badge {} "Plain")]
|
||||||
|
(is (= "Plain" (nth result 2))))))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
(ns ui.button-test
|
(ns ui.button-test
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
[ui.button :as button]))
|
[ui.button :as button]
|
||||||
|
[ui.icon :as icon]))
|
||||||
|
|
||||||
(deftest button-class-list-test
|
(deftest button-class-list-test
|
||||||
(testing "default variant and size"
|
(testing "default variant and size"
|
||||||
@@ -97,3 +98,39 @@
|
|||||||
(is (= :a (first result)))
|
(is (= :a (first result)))
|
||||||
(is (= "https://example.com" (get-in result [1 :href])))
|
(is (= "https://example.com" (get-in result [1 :href])))
|
||||||
(is (= "btn btn-link" (get-in result [1 :class]))))))
|
(is (= "btn btn-link" (get-in result [1 :class]))))))
|
||||||
|
|
||||||
|
(deftest button-icon-class-list-test
|
||||||
|
(testing "icon-only adds btn-icon class"
|
||||||
|
(is (= ["btn" "btn-secondary" "btn-icon"] (button/button-class-list {:icon :plus})))
|
||||||
|
(is (= ["btn" "btn-primary" "btn-icon"] (button/button-class-list {:variant :primary :icon :plus}))))
|
||||||
|
|
||||||
|
(testing "icon-only with size"
|
||||||
|
(is (= ["btn" "btn-primary" "btn-sm" "btn-icon"]
|
||||||
|
(button/button-class-list {:variant :primary :size :sm :icon :plus})))))
|
||||||
|
|
||||||
|
(deftest button-icon-component-test
|
||||||
|
(testing "icon-only button renders icon child"
|
||||||
|
(let [result (button/button {:variant :primary :icon :plus})]
|
||||||
|
(is (= :button (first result)))
|
||||||
|
(is (= "btn btn-primary btn-icon" (get-in result [1 :class])))
|
||||||
|
;; third element should be the SVG icon
|
||||||
|
(is (= :svg (first (nth result 2))))))
|
||||||
|
|
||||||
|
(testing "icon-left renders icon before text"
|
||||||
|
(let [result (button/button {:variant :primary :icon-left :plus} "Add")]
|
||||||
|
(is (= :button (first result)))
|
||||||
|
(is (= :svg (first (nth result 2)))) ;; icon is first child
|
||||||
|
(is (= "Add" (nth result 3))))) ;; text is second child
|
||||||
|
|
||||||
|
(testing "icon-right renders icon after text"
|
||||||
|
(let [result (button/button {:variant :primary :icon-right :arrow-right} "Next")]
|
||||||
|
(is (= :button (first result)))
|
||||||
|
(is (= "Next" (nth result 2))) ;; text is first child
|
||||||
|
(is (= :svg (first (nth result 3)))))) ;; icon is second child
|
||||||
|
|
||||||
|
(testing "icon-left and icon-right together"
|
||||||
|
(let [result (button/button {:variant :primary :icon-left :plus :icon-right :arrow-right} "Add")]
|
||||||
|
(is (= :svg (first (nth result 2)))) ;; left icon
|
||||||
|
(is (= "Add" (nth result 3))) ;; text
|
||||||
|
(is (= :svg (first (nth result 4)))))) ;; right icon
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
(ns ui.form-test
|
(ns ui.form-test
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
[ui.form :as form]))
|
[ui.form :as form]
|
||||||
|
[ui.icon :as icon]))
|
||||||
|
|
||||||
;; ── form-field ──────────────────────────────────────────────────────
|
;; ── form-field ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -102,6 +103,41 @@
|
|||||||
(let [result (form/form-input {:error true})]
|
(let [result (form/form-input {:error true})]
|
||||||
(is (= "form-input form-input--error" (get-in result [1 :class]))))))
|
(is (= "form-input form-input--error" (get-in result [1 :class]))))))
|
||||||
|
|
||||||
|
(deftest form-input-icon-test
|
||||||
|
(testing "input with icon-left wraps in form-input-wrap"
|
||||||
|
(let [result (form/form-input {:icon-left :search :placeholder "Search..."})]
|
||||||
|
(is (= :div (first result)))
|
||||||
|
(is (= "form-input-wrap" (get-in result [1 :class])))
|
||||||
|
;; first child: left icon span
|
||||||
|
(let [icon-span (nth result 2)]
|
||||||
|
(is (= :span (first icon-span)))
|
||||||
|
(is (= "form-input-icon form-input-icon--left" (get-in icon-span [1 :class]))))
|
||||||
|
;; second child: the actual input
|
||||||
|
(let [input (nth result 3)]
|
||||||
|
(is (= :input (first input)))
|
||||||
|
(is (clojure.string/includes? (get-in input [1 :class]) "form-input--icon-left")))))
|
||||||
|
|
||||||
|
(testing "input with icon-right wraps in form-input-wrap"
|
||||||
|
(let [result (form/form-input {:icon-right :check})]
|
||||||
|
(is (= :div (first result)))
|
||||||
|
;; second child: the input
|
||||||
|
(let [input (nth result 2)]
|
||||||
|
(is (= :input (first input)))
|
||||||
|
(is (clojure.string/includes? (get-in input [1 :class]) "form-input--icon-right")))
|
||||||
|
;; third child: right icon span
|
||||||
|
(let [icon-span (nth result 3)]
|
||||||
|
(is (= :span (first icon-span)))
|
||||||
|
(is (= "form-input-icon form-input-icon--right" (get-in icon-span [1 :class]))))))
|
||||||
|
|
||||||
|
(testing "input with both icons"
|
||||||
|
(let [result (form/form-input {:icon-left :search :icon-right :x})]
|
||||||
|
(is (= :div (first result)))
|
||||||
|
(is (= 5 (count result))))) ;; div + attrs + left-icon + input + right-icon
|
||||||
|
|
||||||
|
(testing "input without icons returns bare input"
|
||||||
|
(let [result (form/form-input {:placeholder "Name"})]
|
||||||
|
(is (= :input (first result))))))
|
||||||
|
|
||||||
;; ── form-textarea ───────────────────────────────────────────────────
|
;; ── form-textarea ───────────────────────────────────────────────────
|
||||||
|
|
||||||
(deftest form-textarea-class-list-test
|
(deftest form-textarea-class-list-test
|
||||||
|
|||||||
Reference in New Issue
Block a user