feat: add 13 components adapted from Oat UI
Components (.cljc + .css + tests): - Alert (success/warning/danger/info variants) - Badge (primary/secondary/outline/success/warning/danger) - Card (card/card-header/card-body/card-footer) - Accordion (collapsible with open/closed state) - Table (headers/rows, striped/bordered variants) - Dialog (modal with header/body/footer sections) - Breadcrumb (nav with active item) - Pagination (current/total with prev/next) - Progress (value bar with color variants) - Spinner (sm/md/lg sizes) - Skeleton (line/box/circle/heading placeholders) - Switch (toggle with checked/disabled states) - Tooltip (hover text via data-tooltip attr) CSS-only additions: - Form elements (inputs, selects, checkboxes, radios, range, groups) - Grid (12-column system with offsets, responsive) - Utilities (flex, spacing, alignment, sr-only) Also adds warning/fg-on-warning tokens to light and dark themes. All 3 dev targets updated with full component showcase. 40 tests, 213 assertions, all passing.
This commit is contained in:
31
test/ui/accordion_test.clj
Normal file
31
test/ui/accordion_test.clj
Normal file
@@ -0,0 +1,31 @@
|
||||
(ns ui.accordion-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.accordion :as accordion]))
|
||||
|
||||
(deftest accordion-class-list-test
|
||||
(testing "closed accordion"
|
||||
(is (= ["accordion"] (accordion/accordion-class-list {})))
|
||||
(is (= ["accordion"] (accordion/accordion-class-list {:open false}))))
|
||||
|
||||
(testing "open accordion"
|
||||
(is (= ["accordion" "accordion--open"] (accordion/accordion-class-list {:open true})))))
|
||||
|
||||
(deftest accordion-classes-test
|
||||
(testing "space-joined output"
|
||||
(is (= "accordion" (accordion/accordion-classes {})))
|
||||
(is (= "accordion accordion--open" (accordion/accordion-classes {:open true})))))
|
||||
|
||||
(deftest accordion-component-test
|
||||
(testing "closed accordion renders trigger only"
|
||||
(let [result (accordion/accordion {:title "Question?"} "Answer.")]
|
||||
(is (= :div (first result)))
|
||||
(is (= "accordion" (get-in result [1 :class])))
|
||||
;; trigger is present
|
||||
(is (= "accordion-trigger" (get-in result [2 1 :class])))))
|
||||
|
||||
(testing "open accordion includes content"
|
||||
(let [result (accordion/accordion {:title "Q?" :open true} "A.")]
|
||||
(is (= "accordion accordion--open" (get-in result [1 :class])))
|
||||
;; should contain accordion-content div
|
||||
(is (some #(and (vector? %) (= "accordion-content" (get-in % [1 :class])))
|
||||
(rest (rest result)))))))
|
||||
35
test/ui/alert_test.clj
Normal file
35
test/ui/alert_test.clj
Normal file
@@ -0,0 +1,35 @@
|
||||
(ns ui.alert-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.alert :as alert]))
|
||||
|
||||
(deftest alert-class-list-test
|
||||
(testing "neutral (no variant)"
|
||||
(is (= ["alert"] (alert/alert-class-list {}))))
|
||||
|
||||
(testing "explicit variants"
|
||||
(is (= ["alert" "alert-success"] (alert/alert-class-list {:variant :success})))
|
||||
(is (= ["alert" "alert-warning"] (alert/alert-class-list {:variant :warning})))
|
||||
(is (= ["alert" "alert-danger"] (alert/alert-class-list {:variant :danger})))
|
||||
(is (= ["alert" "alert-info"] (alert/alert-class-list {:variant :info})))))
|
||||
|
||||
(deftest alert-classes-test
|
||||
(testing "space-joined output"
|
||||
(is (= "alert" (alert/alert-classes {})))
|
||||
(is (= "alert alert-success" (alert/alert-classes {:variant :success})))))
|
||||
|
||||
(deftest alert-component-test
|
||||
(testing "basic alert renders correct hiccup"
|
||||
(let [result (alert/alert {:variant :success :title "Done!"} "Saved.")]
|
||||
(is (= :div (first result)))
|
||||
(is (= "alert alert-success" (get-in result [1 :class])))
|
||||
(is (= "alert" (get-in result [1 :role])))))
|
||||
|
||||
(testing "alert with title includes title paragraph"
|
||||
(let [result (alert/alert {:title "Title"} "Body")]
|
||||
(is (some #(and (vector? %) (= "alert-title" (get-in % [1 :class])))
|
||||
(rest (rest result))))))
|
||||
|
||||
(testing "alert without title has no title paragraph"
|
||||
(let [result (alert/alert {} "Body")]
|
||||
(is (not (some #(and (vector? %) (= "alert-title" (get-in % [1 :class])))
|
||||
(rest (rest result))))))))
|
||||
31
test/ui/badge_test.clj
Normal file
31
test/ui/badge_test.clj
Normal file
@@ -0,0 +1,31 @@
|
||||
(ns ui.badge-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.badge :as badge]))
|
||||
|
||||
(deftest badge-class-list-test
|
||||
(testing "default variant (primary)"
|
||||
(is (= ["badge"] (badge/badge-class-list {}))))
|
||||
|
||||
(testing "explicit variants"
|
||||
(is (= ["badge"] (badge/badge-class-list {:variant :primary})))
|
||||
(is (= ["badge" "badge-secondary"] (badge/badge-class-list {:variant :secondary})))
|
||||
(is (= ["badge" "badge-outline"] (badge/badge-class-list {:variant :outline})))
|
||||
(is (= ["badge" "badge-success"] (badge/badge-class-list {:variant :success})))
|
||||
(is (= ["badge" "badge-warning"] (badge/badge-class-list {:variant :warning})))
|
||||
(is (= ["badge" "badge-danger"] (badge/badge-class-list {:variant :danger})))))
|
||||
|
||||
(deftest badge-classes-test
|
||||
(testing "space-joined output"
|
||||
(is (= "badge" (badge/badge-classes {})))
|
||||
(is (= "badge badge-danger" (badge/badge-classes {:variant :danger})))))
|
||||
|
||||
(deftest badge-component-test
|
||||
(testing "renders a span"
|
||||
(let [result (badge/badge {} "New")]
|
||||
(is (= :span (first result)))
|
||||
(is (= "badge" (get-in result [1 :class])))
|
||||
(is (= "New" (nth result 2)))))
|
||||
|
||||
(testing "extra class gets appended"
|
||||
(let [result (badge/badge {:class "extra"} "X")]
|
||||
(is (= "badge extra" (get-in result [1 :class]))))))
|
||||
37
test/ui/breadcrumb_test.clj
Normal file
37
test/ui/breadcrumb_test.clj
Normal file
@@ -0,0 +1,37 @@
|
||||
(ns ui.breadcrumb-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.breadcrumb :as breadcrumb]))
|
||||
|
||||
(deftest breadcrumb-component-test
|
||||
(testing "renders a nav with breadcrumb list"
|
||||
(let [result (breadcrumb/breadcrumb
|
||||
{:items [{:label "Home" :href "/"}
|
||||
{:label "Projects" :href "/projects"}
|
||||
{:label "Current"}]})]
|
||||
(is (= :nav (first result)))
|
||||
(is (= "Breadcrumb" (get-in result [1 :aria-label])))
|
||||
;; ol is inside
|
||||
(let [ol (nth result 2)]
|
||||
(is (= :ol (first ol)))
|
||||
(is (= "breadcrumb" (get-in ol [1 :class])))
|
||||
;; 3 items
|
||||
(is (= 3 (count (drop 2 ol)))))))
|
||||
|
||||
(testing "last item is active"
|
||||
(let [result (breadcrumb/breadcrumb
|
||||
{:items [{:label "Home" :href "/"}
|
||||
{:label "Active"}]})
|
||||
ol (nth result 2)
|
||||
items (drop 2 ol)
|
||||
last-item (last items)]
|
||||
(is (clojure.string/includes? (get-in last-item [1 :class]) "breadcrumb-item--active")))))
|
||||
|
||||
(deftest breadcrumb-links-test
|
||||
(testing "non-active items have links"
|
||||
(let [result (breadcrumb/breadcrumb
|
||||
{:items [{:label "Home" :href "/"}
|
||||
{:label "End"}]})
|
||||
ol (nth result 2)
|
||||
first-item (nth ol 2)]
|
||||
;; first item should have an anchor child
|
||||
(is (= :a (first (nth first-item 2)))))))
|
||||
36
test/ui/card_test.clj
Normal file
36
test/ui/card_test.clj
Normal file
@@ -0,0 +1,36 @@
|
||||
(ns ui.card-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.card :as card]))
|
||||
|
||||
(deftest card-class-list-test
|
||||
(testing "always returns card class"
|
||||
(is (= ["card"] (card/card-class-list {})))))
|
||||
|
||||
(deftest card-component-test
|
||||
(testing "renders an article"
|
||||
(let [result (card/card {} "Content")]
|
||||
(is (= :article (first result)))
|
||||
(is (= "card" (get-in result [1 :class])))
|
||||
(is (= "Content" (nth result 2)))))
|
||||
|
||||
(testing "extra class gets appended"
|
||||
(let [result (card/card {:class "extra"} "X")]
|
||||
(is (= "card extra" (get-in result [1 :class]))))))
|
||||
|
||||
(deftest card-header-test
|
||||
(testing "renders header"
|
||||
(let [result (card/card-header {} [:h3 "Title"])]
|
||||
(is (= :header (first result)))
|
||||
(is (= "card-header" (get-in result [1 :class]))))))
|
||||
|
||||
(deftest card-body-test
|
||||
(testing "renders body"
|
||||
(let [result (card/card-body {} "Content")]
|
||||
(is (= :div (first result)))
|
||||
(is (= "card-body" (get-in result [1 :class]))))))
|
||||
|
||||
(deftest card-footer-test
|
||||
(testing "renders footer"
|
||||
(let [result (card/card-footer {} "Actions")]
|
||||
(is (= :footer (first result)))
|
||||
(is (= "card-footer" (get-in result [1 :class]))))))
|
||||
34
test/ui/dialog_test.clj
Normal file
34
test/ui/dialog_test.clj
Normal file
@@ -0,0 +1,34 @@
|
||||
(ns ui.dialog-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.dialog :as dialog]))
|
||||
|
||||
(deftest dialog-class-list-test
|
||||
(testing "always returns dialog class"
|
||||
(is (= ["dialog"] (dialog/dialog-class-list {})))))
|
||||
|
||||
(deftest dialog-component-test
|
||||
(testing "renders a dialog element"
|
||||
(let [result (dialog/dialog {:id "my-dialog"} "Content")]
|
||||
(is (= :dialog (first result)))
|
||||
(is (= "dialog" (get-in result [1 :class])))
|
||||
(is (= "my-dialog" (get-in result [1 :id])))))
|
||||
|
||||
(testing "open dialog has open attr"
|
||||
(let [result (dialog/dialog {:open true} "Content")]
|
||||
(is (true? (get-in result [1 :open]))))))
|
||||
|
||||
(deftest dialog-sections-test
|
||||
(testing "dialog-header renders header"
|
||||
(let [result (dialog/dialog-header {} [:h3 "Title"])]
|
||||
(is (= :header (first result)))
|
||||
(is (= "dialog-header" (get-in result [1 :class])))))
|
||||
|
||||
(testing "dialog-body renders div"
|
||||
(let [result (dialog/dialog-body {} "Body")]
|
||||
(is (= :div (first result)))
|
||||
(is (= "dialog-body" (get-in result [1 :class])))))
|
||||
|
||||
(testing "dialog-footer renders footer"
|
||||
(let [result (dialog/dialog-footer {} "Footer")]
|
||||
(is (= :footer (first result)))
|
||||
(is (= "dialog-footer" (get-in result [1 :class]))))))
|
||||
30
test/ui/pagination_test.clj
Normal file
30
test/ui/pagination_test.clj
Normal file
@@ -0,0 +1,30 @@
|
||||
(ns ui.pagination-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.pagination :as pagination]))
|
||||
|
||||
(deftest pagination-item-class-list-test
|
||||
(testing "default item"
|
||||
(is (= ["pagination-item"] (pagination/pagination-item-class-list {}))))
|
||||
|
||||
(testing "active item"
|
||||
(is (= ["pagination-item" "pagination-item--active"]
|
||||
(pagination/pagination-item-class-list {:active true}))))
|
||||
|
||||
(testing "disabled item"
|
||||
(is (= ["pagination-item" "pagination-item--disabled"]
|
||||
(pagination/pagination-item-class-list {:disabled true}))))
|
||||
|
||||
(testing "active + disabled"
|
||||
(is (= ["pagination-item" "pagination-item--active" "pagination-item--disabled"]
|
||||
(pagination/pagination-item-class-list {:active true :disabled true})))))
|
||||
|
||||
(deftest pagination-component-test
|
||||
(testing "renders nav with pagination list"
|
||||
(let [result (pagination/pagination {:current 2 :total 5})]
|
||||
(is (= :nav (first result)))
|
||||
(is (= "Pagination" (get-in result [1 :aria-label])))
|
||||
(let [ol (nth result 2)]
|
||||
(is (= :ol (first ol)))
|
||||
(is (= "pagination" (get-in ol [1 :class])))
|
||||
;; 5 page items + prev + next = 7
|
||||
(is (= 7 (count (drop 2 ol))))))))
|
||||
24
test/ui/progress_test.clj
Normal file
24
test/ui/progress_test.clj
Normal file
@@ -0,0 +1,24 @@
|
||||
(ns ui.progress-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.progress :as progress]))
|
||||
|
||||
(deftest progress-bar-class-list-test
|
||||
(testing "default (no variant)"
|
||||
(is (= ["progress-bar"] (progress/progress-bar-class-list {}))))
|
||||
|
||||
(testing "explicit variants"
|
||||
(is (= ["progress-bar" "progress-bar--success"] (progress/progress-bar-class-list {:variant :success})))
|
||||
(is (= ["progress-bar" "progress-bar--warning"] (progress/progress-bar-class-list {:variant :warning})))
|
||||
(is (= ["progress-bar" "progress-bar--danger"] (progress/progress-bar-class-list {:variant :danger})))))
|
||||
|
||||
(deftest progress-component-test
|
||||
(testing "renders progress with correct structure"
|
||||
(let [result (progress/progress {:value 60})]
|
||||
(is (= :div (first result)))
|
||||
(is (= "progress" (get-in result [1 :class])))
|
||||
(is (= "progressbar" (get-in result [1 :role])))
|
||||
;; inner bar
|
||||
(let [bar (nth result 2)]
|
||||
(is (= :div (first bar)))
|
||||
(is (= "progress-bar" (get-in bar [1 :class])))
|
||||
(is (= "width: 60%" (get-in bar [1 :style])))))))
|
||||
20
test/ui/skeleton_test.clj
Normal file
20
test/ui/skeleton_test.clj
Normal file
@@ -0,0 +1,20 @@
|
||||
(ns ui.skeleton-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.skeleton :as skeleton]))
|
||||
|
||||
(deftest skeleton-class-list-test
|
||||
(testing "default (no variant)"
|
||||
(is (= ["skeleton"] (skeleton/skeleton-class-list {}))))
|
||||
|
||||
(testing "explicit variants"
|
||||
(is (= ["skeleton" "skeleton--line"] (skeleton/skeleton-class-list {:variant :line})))
|
||||
(is (= ["skeleton" "skeleton--box"] (skeleton/skeleton-class-list {:variant :box})))
|
||||
(is (= ["skeleton" "skeleton--circle"] (skeleton/skeleton-class-list {:variant :circle})))
|
||||
(is (= ["skeleton" "skeleton--heading"] (skeleton/skeleton-class-list {:variant :heading})))))
|
||||
|
||||
(deftest skeleton-component-test
|
||||
(testing "renders a div with role=status"
|
||||
(let [result (skeleton/skeleton {:variant :line})]
|
||||
(is (= :div (first result)))
|
||||
(is (= "skeleton skeleton--line" (get-in result [1 :class])))
|
||||
(is (= "status" (get-in result [1 :role]))))))
|
||||
21
test/ui/spinner_test.clj
Normal file
21
test/ui/spinner_test.clj
Normal file
@@ -0,0 +1,21 @@
|
||||
(ns ui.spinner-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.spinner :as spinner]))
|
||||
|
||||
(deftest spinner-class-list-test
|
||||
(testing "default size (md)"
|
||||
(is (= ["spinner"] (spinner/spinner-class-list {})))
|
||||
(is (= ["spinner"] (spinner/spinner-class-list {:size :md}))))
|
||||
|
||||
(testing "small size"
|
||||
(is (= ["spinner" "spinner-sm"] (spinner/spinner-class-list {:size :sm}))))
|
||||
|
||||
(testing "large size"
|
||||
(is (= ["spinner" "spinner-lg"] (spinner/spinner-class-list {:size :lg})))))
|
||||
|
||||
(deftest spinner-component-test
|
||||
(testing "renders a span"
|
||||
(let [result (spinner/spinner {})]
|
||||
(is (= :span (first result)))
|
||||
(is (= "spinner" (get-in result [1 :class])))
|
||||
(is (= "status" (get-in result [1 :role]))))))
|
||||
27
test/ui/switch_test.clj
Normal file
27
test/ui/switch_test.clj
Normal file
@@ -0,0 +1,27 @@
|
||||
(ns ui.switch-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.switch :as switch]))
|
||||
|
||||
(deftest switch-class-list-test
|
||||
(testing "default switch"
|
||||
(is (= ["switch"] (switch/switch-class-list {}))))
|
||||
|
||||
(testing "disabled switch"
|
||||
(is (= ["switch" "switch--disabled"] (switch/switch-class-list {:disabled true})))))
|
||||
|
||||
(deftest switch-component-test
|
||||
(testing "renders a label"
|
||||
(let [result (switch/switch-toggle {:label "Notifications"})]
|
||||
(is (= :label (first result)))
|
||||
(is (= "switch" (get-in result [1 :class])))))
|
||||
|
||||
(testing "checked switch has checked track class"
|
||||
(let [result (switch/switch-toggle {:checked true :label "On"})]
|
||||
;; Find the track span
|
||||
(let [track (nth result 3)]
|
||||
(is (= "switch-track switch-track--checked" (get-in track [1 :class]))))))
|
||||
|
||||
(testing "unchecked switch has no checked track class"
|
||||
(let [result (switch/switch-toggle {:checked false :label "Off"})]
|
||||
(let [track (nth result 3)]
|
||||
(is (= "switch-track" (get-in track [1 :class])))))))
|
||||
31
test/ui/table_test.clj
Normal file
31
test/ui/table_test.clj
Normal file
@@ -0,0 +1,31 @@
|
||||
(ns ui.table-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.table :as table]))
|
||||
|
||||
(deftest table-class-list-test
|
||||
(testing "default table"
|
||||
(is (= ["table"] (table/table-class-list {}))))
|
||||
|
||||
(testing "striped variant"
|
||||
(is (= ["table" "table--striped"] (table/table-class-list {:variant :striped}))))
|
||||
|
||||
(testing "bordered variant"
|
||||
(is (= ["table" "table--bordered"] (table/table-class-list {:variant :bordered})))))
|
||||
|
||||
(deftest table-classes-test
|
||||
(testing "space-joined output"
|
||||
(is (= "table" (table/table-classes {})))
|
||||
(is (= "table table--striped" (table/table-classes {:variant :striped})))))
|
||||
|
||||
(deftest table-component-test
|
||||
(testing "renders a table with headers and rows"
|
||||
(let [result (table/table {:headers ["Name" "Email"]
|
||||
:rows [["Alice" "alice@example.com"]
|
||||
["Bob" "bob@example.com"]]})]
|
||||
;; outer div.table-wrapper
|
||||
(is (= :div (first result)))
|
||||
(is (= "table-wrapper" (get-in result [1 :class])))
|
||||
;; table element inside
|
||||
(let [tbl (nth result 2)]
|
||||
(is (= :table (first tbl)))
|
||||
(is (= "table" (get-in tbl [1 :class])))))))
|
||||
15
test/ui/tooltip_test.clj
Normal file
15
test/ui/tooltip_test.clj
Normal file
@@ -0,0 +1,15 @@
|
||||
(ns ui.tooltip-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[ui.tooltip :as tooltip]))
|
||||
|
||||
(deftest tooltip-component-test
|
||||
(testing "renders a span with data-tooltip attr"
|
||||
(let [result (tooltip/tooltip {:text "Hello"} "Hover me")]
|
||||
(is (= :span (first result)))
|
||||
(is (= "tooltip" (get-in result [1 :class])))
|
||||
(is (= "Hello" (get-in result [1 :data-tooltip])))
|
||||
(is (= "Hover me" (nth result 2)))))
|
||||
|
||||
(testing "extra class appended"
|
||||
(let [result (tooltip/tooltip {:text "Tip" :class "extra"} "X")]
|
||||
(is (= "tooltip extra" (get-in result [1 :class]))))))
|
||||
Reference in New Issue
Block a user