feat: add separator component
Horizontal and vertical separator (divider) with CSS classes, ARIA role="none", and data-orientation attribute. Includes dark mode support, unit tests, and demos in all three dev targets.
This commit is contained in:
2
bb.edn
2
bb.edn
@@ -39,6 +39,7 @@
|
||||
[ui.icon-test]
|
||||
[ui.sidebar-test]
|
||||
[ui.chip-test]
|
||||
[ui.separator-test]
|
||||
[ui.theme-test])
|
||||
:task (let [{:keys [fail error]} (t/run-tests
|
||||
'ui.button-test
|
||||
@@ -59,6 +60,7 @@
|
||||
'ui.icon-test
|
||||
'ui.sidebar-test
|
||||
'ui.chip-test
|
||||
'ui.separator-test
|
||||
'ui.theme-test)]
|
||||
(when (pos? (+ fail error))
|
||||
(System/exit 1)))}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
[ui.pagination :as pagination]
|
||||
[ui.form :as form]
|
||||
[ui.sidebar :as sidebar]
|
||||
[ui.icon :as icon]))
|
||||
[ui.icon :as icon]
|
||||
[ui.separator :as separator]))
|
||||
|
||||
;; ── Query Params ────────────────────────────────────────────────────
|
||||
|
||||
@@ -228,6 +229,24 @@
|
||||
(pagination/pagination {:current 3 :total 5
|
||||
:href-fn (fn [p] (str "#page-" p))})))
|
||||
|
||||
(defn separator-demo []
|
||||
(section "Separator"
|
||||
;; Basic horizontal
|
||||
[:div {:style "max-width: 24rem;"}
|
||||
[:div {:style "display: flex; flex-direction: column; gap: 0.375rem;"}
|
||||
[:div {:style "font-weight: 500; line-height: 1;"} "Clojure UI"]
|
||||
[:div {:style "color: var(--fg-2); font-size: var(--font-sm);"} "A cross-target component library"]]
|
||||
[:div {:style "margin: 1rem 0;"}
|
||||
(separator/separator {})]
|
||||
[:p {:style "font-size: var(--font-sm);"} "Build once, render everywhere — Hiccup, Replicant, and Squint."]]
|
||||
;; Vertical separator
|
||||
[:div {:style "display: flex; align-items: center; gap: 1rem; height: 1.25rem;"}
|
||||
[:span {:style "font-size: var(--font-sm);"} "Blog"]
|
||||
(separator/separator {:orientation :vertical})
|
||||
[:span {:style "font-size: var(--font-sm);"} "Docs"]
|
||||
(separator/separator {:orientation :vertical})
|
||||
[:span {:style "font-size: var(--font-sm);"} "Source"]]))
|
||||
|
||||
(defn form-demo []
|
||||
(section "Form"
|
||||
[:form {:style "max-width: 480px;"}
|
||||
@@ -297,6 +316,7 @@
|
||||
(tooltip-demo)
|
||||
(breadcrumb-demo)
|
||||
(pagination-demo)
|
||||
(separator-demo)
|
||||
(form-demo)])
|
||||
|
||||
(def icon-categories
|
||||
@@ -403,6 +423,8 @@
|
||||
{:label "Spinner" :anchor "spinner"}
|
||||
{:label "Skeleton" :anchor "skeleton"}
|
||||
{:label "Tooltip" :anchor "tooltip"}]}
|
||||
{:title "Layout"
|
||||
:items [{:label "Separator" :anchor "separator"}]}
|
||||
{:title "Navigation"
|
||||
:items [{:label "Breadcrumb" :anchor "breadcrumb"}
|
||||
{:label "Pagination" :anchor "pagination"}]}])
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
[ui.pagination :as pagination]
|
||||
[ui.form :as form]
|
||||
[ui.sidebar :as sidebar]
|
||||
[ui.icon :as icon]))
|
||||
[ui.icon :as icon]
|
||||
[ui.separator :as separator]))
|
||||
|
||||
;; ── State ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -192,6 +193,24 @@
|
||||
(pagination/pagination {:current 3 :total 5
|
||||
:on-click (fn [p] (js/console.log (str "Page: " p)))})))
|
||||
|
||||
(defn separator-demo []
|
||||
(section "Separator"
|
||||
;; Basic horizontal
|
||||
[:div {:style {:max-width "24rem"}}
|
||||
[:div {:style {:display "flex" :flex-direction "column" :gap "0.375rem"}}
|
||||
[:div {:style {:font-weight "500" :line-height "1"}} "Clojure UI"]
|
||||
[:div {:style {:color "var(--fg-2)" :font-size "var(--font-sm)"}} "A cross-target component library"]]
|
||||
[:div {:style {:margin "1rem 0"}}
|
||||
(separator/separator {})]
|
||||
[:p {:style {:font-size "var(--font-sm)"}} "Build once, render everywhere — Hiccup, Replicant, and Squint."]]
|
||||
;; Vertical separator
|
||||
[:div {:style {:display "flex" :align-items "center" :gap "1rem" :height "1.25rem"}}
|
||||
[:span {:style {:font-size "var(--font-sm)"}} "Blog"]
|
||||
(separator/separator {:orientation :vertical})
|
||||
[:span {:style {:font-size "var(--font-sm)"}} "Docs"]
|
||||
(separator/separator {:orientation :vertical})
|
||||
[:span {:style {:font-size "var(--font-sm)"}} "Source"]]))
|
||||
|
||||
(defn form-demo []
|
||||
(section "Form"
|
||||
[:form {:style {:max-width "480px"}}
|
||||
@@ -255,6 +274,7 @@
|
||||
(tooltip-demo)
|
||||
(breadcrumb-demo)
|
||||
(pagination-demo)
|
||||
(separator-demo)
|
||||
(form-demo)])
|
||||
|
||||
(def icon-categories
|
||||
@@ -360,6 +380,8 @@
|
||||
{:label "Spinner" :anchor "spinner"}
|
||||
{:label "Skeleton" :anchor "skeleton"}
|
||||
{:label "Tooltip" :anchor "tooltip"}]}
|
||||
{:title "Layout"
|
||||
:items [{:label "Separator" :anchor "separator"}]}
|
||||
{:title "Navigation"
|
||||
:items [{:label "Breadcrumb" :anchor "breadcrumb"}
|
||||
{:label "Pagination" :anchor "pagination"}]}])
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
[ui.pagination :as pagination]
|
||||
[ui.form :as form]
|
||||
[ui.sidebar :as sidebar]
|
||||
[ui.icon :as icon]))
|
||||
[ui.icon :as icon]
|
||||
[ui.separator :as separator]))
|
||||
|
||||
;; ── State ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -210,6 +211,24 @@
|
||||
(pagination/pagination {:current 3 :total 5
|
||||
:on-click (fn [p] (js/console.log (str "Page: " p)))})))
|
||||
|
||||
(defn separator-demo []
|
||||
(section "Separator"
|
||||
;; Basic horizontal
|
||||
[:div {:style {"max-width" "24rem"}}
|
||||
[:div {:style {"display" "flex" "flex-direction" "column" "gap" "0.375rem"}}
|
||||
[:div {:style {"font-weight" "500" "line-height" "1"}} "Clojure UI"]
|
||||
[:div {:style {"color" "var(--fg-2)" "font-size" "var(--font-sm)"}} "A cross-target component library"]]
|
||||
[:div {:style {"margin" "1rem 0"}}
|
||||
(separator/separator {})]
|
||||
[:p {:style {"font-size" "var(--font-sm)"}} "Build once, render everywhere — Hiccup, Replicant, and Squint."]]
|
||||
;; Vertical separator
|
||||
[:div {:style {"display" "flex" "align-items" "center" "gap" "1rem" "height" "1.25rem"}}
|
||||
[:span {:style {"font-size" "var(--font-sm)"}} "Blog"]
|
||||
(separator/separator {:orientation "vertical"})
|
||||
[:span {:style {"font-size" "var(--font-sm)"}} "Docs"]
|
||||
(separator/separator {:orientation "vertical"})
|
||||
[:span {:style {"font-size" "var(--font-sm)"}} "Source"]]))
|
||||
|
||||
(defn form-demo []
|
||||
(section "Form"
|
||||
[:form {:style {"max-width" "480px"}}
|
||||
@@ -273,6 +292,7 @@
|
||||
(tooltip-demo)
|
||||
(breadcrumb-demo)
|
||||
(pagination-demo)
|
||||
(separator-demo)
|
||||
(form-demo)])
|
||||
|
||||
(def icon-categories
|
||||
@@ -386,6 +406,8 @@
|
||||
{:label "Spinner" :anchor "spinner"}
|
||||
{:label "Skeleton" :anchor "skeleton"}
|
||||
{:label "Tooltip" :anchor "tooltip"}]}
|
||||
{:title "Layout"
|
||||
:items [{:label "Separator" :anchor "separator"}]}
|
||||
{:title "Navigation"
|
||||
:items [{:label "Breadcrumb" :anchor "breadcrumb"}
|
||||
{:label "Pagination" :anchor "pagination"}]}])
|
||||
|
||||
53
src/ui/separator.cljc
Normal file
53
src/ui/separator.cljc
Normal file
@@ -0,0 +1,53 @@
|
||||
(ns ui.separator
|
||||
(:require [clojure.string :as str]))
|
||||
|
||||
#?(:squint (defn- kw-name [s] s)
|
||||
:cljs (defn- kw-name [s] (name s))
|
||||
:clj (defn- kw-name [s] (name s)))
|
||||
|
||||
(defn separator-class-list
|
||||
"Generate a vector of CSS class strings for a separator.
|
||||
Orientation: :horizontal (default), :vertical."
|
||||
[{:keys [orientation]}]
|
||||
(let [o (or (some-> orientation kw-name) "horizontal")]
|
||||
["separator" (str "separator-" o)]))
|
||||
|
||||
(defn separator-classes
|
||||
"Generate CSS class string for a separator."
|
||||
[opts]
|
||||
(str/join " " (separator-class-list opts)))
|
||||
|
||||
(defn separator
|
||||
"Render a separator element. Works across all targets via reader conditionals.
|
||||
|
||||
Props:
|
||||
:orientation - :horizontal (default), :vertical
|
||||
:class - additional CSS classes
|
||||
:attrs - additional HTML attributes map"
|
||||
[{:keys [orientation class attrs] :as _props}]
|
||||
#?(:squint
|
||||
(let [classes (cond-> (separator-classes {:orientation orientation})
|
||||
class (str " " class))
|
||||
base-attrs (merge {:class classes
|
||||
:role "none"
|
||||
:data-orientation (or (some-> orientation kw-name) "horizontal")}
|
||||
attrs)]
|
||||
[:div base-attrs])
|
||||
|
||||
:cljs
|
||||
(let [cls (separator-class-list {:orientation orientation})
|
||||
classes (cond-> cls class (conj class))
|
||||
base-attrs (merge {:class classes
|
||||
:role "none"
|
||||
:data-orientation (or (some-> orientation kw-name) "horizontal")}
|
||||
attrs)]
|
||||
[:div base-attrs])
|
||||
|
||||
:clj
|
||||
(let [classes (cond-> (separator-classes {:orientation orientation})
|
||||
class (str " " class))
|
||||
base-attrs (merge {:class classes
|
||||
:role "none"
|
||||
:data-orientation (or (some-> orientation kw-name) "horizontal")}
|
||||
attrs)]
|
||||
[:div base-attrs])))
|
||||
27
src/ui/separator.css
Normal file
27
src/ui/separator.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/* Separator — visually or semantically separates content */
|
||||
|
||||
.separator {
|
||||
flex-shrink: 0;
|
||||
background: var(--gray-200);
|
||||
}
|
||||
|
||||
.separator-horizontal {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.separator-vertical {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
/* Dark mode: use a lighter border shade */
|
||||
[data-theme="dark"] .separator {
|
||||
background: var(--gray-800);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .separator {
|
||||
background: var(--gray-800);
|
||||
}
|
||||
}
|
||||
39
test/ui/separator_test.clj
Normal file
39
test/ui/separator_test.clj
Normal file
@@ -0,0 +1,39 @@
|
||||
(ns ui.separator-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.separator :as separator]))
|
||||
|
||||
(deftest separator-class-list-test
|
||||
(testing "default (horizontal)"
|
||||
(is (= ["separator" "separator-horizontal"] (separator/separator-class-list {}))))
|
||||
|
||||
(testing "explicit horizontal"
|
||||
(is (= ["separator" "separator-horizontal"] (separator/separator-class-list {:orientation :horizontal}))))
|
||||
|
||||
(testing "vertical"
|
||||
(is (= ["separator" "separator-vertical"] (separator/separator-class-list {:orientation :vertical})))))
|
||||
|
||||
(deftest separator-classes-test
|
||||
(testing "returns space-joined string"
|
||||
(is (= "separator separator-horizontal" (separator/separator-classes {})))
|
||||
(is (= "separator separator-vertical" (separator/separator-classes {:orientation :vertical})))))
|
||||
|
||||
(deftest separator-component-test
|
||||
(testing "renders a div with role=none and data-orientation"
|
||||
(let [result (separator/separator {})]
|
||||
(is (= :div (first result)))
|
||||
(is (= "separator separator-horizontal" (get-in result [1 :class])))
|
||||
(is (= "none" (get-in result [1 :role])))
|
||||
(is (= "horizontal" (get-in result [1 :data-orientation])))))
|
||||
|
||||
(testing "vertical orientation"
|
||||
(let [result (separator/separator {:orientation :vertical})]
|
||||
(is (= "separator separator-vertical" (get-in result [1 :class])))
|
||||
(is (= "vertical" (get-in result [1 :data-orientation])))))
|
||||
|
||||
(testing "extra class is appended"
|
||||
(let [result (separator/separator {:class "my-sep"})]
|
||||
(is (= "separator separator-horizontal my-sep" (get-in result [1 :class])))))
|
||||
|
||||
(testing "attrs are merged"
|
||||
(let [result (separator/separator {:attrs {:id "sep-1"}})]
|
||||
(is (= "sep-1" (get-in result [1 :id]))))))
|
||||
Reference in New Issue
Block a user