diff --git a/dev/hiccup/src/dev/hiccup.clj b/dev/hiccup/src/dev/hiccup.clj index da6a4b9..cc51b21 100644 --- a/dev/hiccup/src/dev/hiccup.clj +++ b/dev/hiccup/src/dev/hiccup.clj @@ -98,7 +98,19 @@ (button/button {:variant :primary :href "#"} "Link primary") (button/button {:variant :secondary :href "#"} "Link secondary") (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 [] (section "Alert" @@ -116,7 +128,12 @@ (badge/badge {:variant :outline} "Outline") (badge/badge {:variant :success} "Success") (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 [] (section "Card" @@ -271,6 +288,10 @@ (form/form-input {:type :datetime-local})) (form/form-field {:label "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-radio-group {:label "Preference" :radio-name "pref" diff --git a/dev/replicant/src/dev/replicant.cljs b/dev/replicant/src/dev/replicant.cljs index c2c4e89..1d5e055 100644 --- a/dev/replicant/src/dev/replicant.cljs +++ b/dev/replicant/src/dev/replicant.cljs @@ -60,7 +60,19 @@ (button/button {:variant :primary :href "#"} "Link primary") (button/button {:variant :secondary :href "#"} "Link secondary") (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 [] (section "Alert" @@ -78,7 +90,12 @@ (badge/badge {:variant :outline} "Outline") (badge/badge {:variant :success} "Success") (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 [] (section "Card" @@ -235,6 +252,10 @@ (form/form-input {:type :datetime-local})) (form/form-field {:label "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-radio-group {:label "Preference" :radio-name "pref" diff --git a/dev/squint/src/dev/squint.cljs b/dev/squint/src/dev/squint.cljs index 5492dd1..58df889 100644 --- a/dev/squint/src/dev/squint.cljs +++ b/dev/squint/src/dev/squint.cljs @@ -78,7 +78,19 @@ [(button/button {:variant "primary" :href "#"} "Link primary") (button/button {:variant "secondary" :href "#"} "Link secondary") (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 [] (section "Alert" @@ -96,7 +108,12 @@ (badge/badge {:variant "outline"} "Outline") (badge/badge {:variant "success"} "Success") (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 [] (section "Card" @@ -253,6 +270,10 @@ (form/form-input {:type "datetime-local"})) (form/form-field {:label "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-radio-group {:label "Preference" :radio-name "pref" diff --git a/src/ui/alert.cljc b/src/ui/alert.cljc index b795c17..dcf1e6b 100644 --- a/src/ui/alert.cljc +++ b/src/ui/alert.cljc @@ -1,5 +1,6 @@ (ns ui.alert - (:require [clojure.string :as str])) + (:require [clojure.string :as str] + [ui.icon :as icon])) #?(:squint (defn- kw-name [s] s) :cljs (defn- kw-name [s] (name s)) @@ -17,38 +18,61 @@ [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 "Render an alert element. Props: - :variant - :success, :warning, :danger, :info (nil for neutral) - :title - optional title string - :class - additional CSS classes - :attrs - additional HTML attributes" - [{:keys [variant title class attrs] :as _props} & children] - #?(:squint - (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))))) + :variant - :success, :warning, :danger, :info (nil for neutral) + :title - optional title string + :icon-name - override icon (nil uses variant default, false to suppress) + :class - additional CSS classes + :attrs - additional HTML attributes" + [{:keys [variant title icon-name class attrs] :as _props} & children] + (let [v (some-> variant kw-name) + iname (cond + (false? icon-name) nil ;; explicitly suppressed + icon-name icon-name ;; explicit override + v (get variant-icons v))] + #?(:squint + (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 - (let [cls (alert-class-list {:variant variant}) - classes (cond-> cls class (conj 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))))) + :cljs + (let [cls (alert-class-list {:variant variant}) + classes (cond-> cls class (conj 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-> [] + 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))))))) diff --git a/src/ui/alert.css b/src/ui/alert.css index 39910b1..523cfd1 100644 --- a/src/ui/alert.css +++ b/src/ui/alert.css @@ -2,8 +2,7 @@ container-type: inline-size; position: relative; display: flex; - flex-wrap: wrap; - align-items: center; + align-items: flex-start; gap: var(--size-3); padding: var(--size-4) var(--size-6); background: var(--bg-1); @@ -12,6 +11,18 @@ 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 { font-weight: 600; margin: 0; diff --git a/src/ui/badge.cljc b/src/ui/badge.cljc index 50aff0f..2910392 100644 --- a/src/ui/badge.cljc +++ b/src/ui/badge.cljc @@ -1,5 +1,6 @@ (ns ui.badge - (:require [clojure.string :as str])) + (:require [clojure.string :as str] + [ui.icon :as icon])) #?(:squint (defn- kw-name [s] s) :cljs (defn- kw-name [s] (name s)) @@ -25,24 +26,34 @@ "Render a badge element. Props: - :variant - :primary, :secondary, :outline, :success, :warning, :danger - :class - additional CSS classes - :attrs - additional HTML attributes" - [{:keys [variant class attrs] :as _props} & children] + :variant - :primary, :secondary, :outline, :success, :warning, :danger + :icon-name - optional leading icon (e.g. :check, :star) + :class - additional CSS classes + :attrs - additional HTML attributes" + [{:keys [variant icon-name class attrs] :as _props} & children] #?(:squint (let [classes (cond-> (badge-classes {:variant variant}) class (str " " class)) 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 (let [cls (badge-class-list {:variant variant}) classes (cond-> cls class (conj class)) 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 (let [classes (cond-> (badge-classes {:variant variant}) class (str " " class)) 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)))))) diff --git a/src/ui/badge.css b/src/ui/badge.css index 922b165..5d72863 100644 --- a/src/ui/badge.css +++ b/src/ui/badge.css @@ -11,6 +11,12 @@ color: var(--fg-on-accent); } +.badge-icon { + width: 0.875em; + height: 0.875em; + flex-shrink: 0; +} + .badge-secondary { background: var(--bg-2); color: var(--fg-0); diff --git a/src/ui/button.cljc b/src/ui/button.cljc index ac46c9b..98dfa79 100644 --- a/src/ui/button.cljc +++ b/src/ui/button.cljc @@ -1,5 +1,6 @@ (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 #?(:squint (defn- kw-name [s] s) @@ -10,13 +11,15 @@ (def default-size "md") (defn button-class-list - "Generate a vector of CSS class strings for a button given variant and size. - Returns e.g. [\"btn\" \"btn-primary\" \"btn-lg\"]." - [{:keys [variant 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\"]. + When :icon is provided (icon-only mode), adds \"btn-icon\"." + [{:keys [variant size icon]}] (let [v (or (some-> variant kw-name) default-variant) s (or (some-> size kw-name) default-size)] (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 "Generate CSS class string for a button. Returns a space-joined string." @@ -28,42 +31,68 @@ When :href is provided, renders as instead of