feat(card): add card-list component with full and inset dividers
Adds `card-list` and `card-list-item` to the card module for rendering a list of items inside a single card background with border separators. The `:divider` prop controls separator style: - `:full` (default) — borders go edge-to-edge - `:inset` — borders are inset from edges by `--size-4`h
This commit is contained in:
@@ -124,7 +124,21 @@
|
||||
(card/card-body {} [:p "This is the card content. It can contain any HTML."])
|
||||
(card/card-footer {}
|
||||
(button/button {:variant :secondary :size :sm} "Cancel")
|
||||
(button/button {:variant :primary :size :sm} "Save")))))
|
||||
(button/button {:variant :primary :size :sm} "Save")))
|
||||
|
||||
[:h5 "Card List (full dividers)"]
|
||||
(card/card-list {}
|
||||
(card/card-list-item {} "Notifications")
|
||||
(card/card-list-item {} "Privacy")
|
||||
(card/card-list-item {} "Appearance")
|
||||
(card/card-list-item {} "Accessibility"))
|
||||
|
||||
[:h5 "Card List (inset dividers)"]
|
||||
(card/card-list {:divider :inset}
|
||||
(card/card-list-item {} "Notifications")
|
||||
(card/card-list-item {} "Privacy")
|
||||
(card/card-list-item {} "Appearance")
|
||||
(card/card-list-item {} "Accessibility"))))
|
||||
|
||||
(defn accordion-demo []
|
||||
(section "Accordion"
|
||||
|
||||
@@ -86,7 +86,21 @@
|
||||
(card/card-body {} [:p "This is the card content. It can contain any HTML."])
|
||||
(card/card-footer {}
|
||||
(button/button {:variant :secondary :size :sm} "Cancel")
|
||||
(button/button {:variant :primary :size :sm} "Save")))))
|
||||
(button/button {:variant :primary :size :sm} "Save")))
|
||||
|
||||
[:h5 "Card List (full dividers)"]
|
||||
(card/card-list {}
|
||||
(card/card-list-item {} "Notifications")
|
||||
(card/card-list-item {} "Privacy")
|
||||
(card/card-list-item {} "Appearance")
|
||||
(card/card-list-item {} "Accessibility"))
|
||||
|
||||
[:h5 "Card List (inset dividers)"]
|
||||
(card/card-list {:divider :inset}
|
||||
(card/card-list-item {} "Notifications")
|
||||
(card/card-list-item {} "Privacy")
|
||||
(card/card-list-item {} "Appearance")
|
||||
(card/card-list-item {} "Accessibility"))))
|
||||
|
||||
(defn accordion-demo []
|
||||
(section "Accordion"
|
||||
|
||||
@@ -104,7 +104,21 @@
|
||||
(card/card-body {} [:p "This is the card content. It can contain any HTML."])
|
||||
(card/card-footer {}
|
||||
(button/button {:variant "secondary" :size "sm"} "Cancel")
|
||||
(button/button {:variant "primary" :size "sm"} "Save")))))
|
||||
(button/button {:variant "primary" :size "sm"} "Save")))
|
||||
|
||||
[:h5 "Card List (full dividers)"]
|
||||
(card/card-list {}
|
||||
(card/card-list-item {} "Notifications")
|
||||
(card/card-list-item {} "Privacy")
|
||||
(card/card-list-item {} "Appearance")
|
||||
(card/card-list-item {} "Accessibility"))
|
||||
|
||||
[:h5 "Card List (inset dividers)"]
|
||||
(card/card-list {:divider "inset"}
|
||||
(card/card-list-item {} "Notifications")
|
||||
(card/card-list-item {} "Privacy")
|
||||
(card/card-list-item {} "Appearance")
|
||||
(card/card-list-item {} "Accessibility"))))
|
||||
|
||||
(defn accordion-demo []
|
||||
(section "Accordion"
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
(ns ui.card
|
||||
(:require [clojure.string :as str]))
|
||||
|
||||
;; In squint, keywords are strings — name is identity
|
||||
#?(:squint (defn- kw-name [s] s)
|
||||
:cljs (defn- kw-name [s] (name s))
|
||||
:clj (defn- kw-name [s] (name s)))
|
||||
|
||||
(defn card-class-list
|
||||
"Generate a vector of CSS class strings for a card."
|
||||
[_opts]
|
||||
@@ -65,3 +70,62 @@
|
||||
(into [:footer (merge {:class (cond-> ["card-footer"] class (conj class))} attrs)] children)
|
||||
:clj
|
||||
(into [:footer (merge {:class (cond-> "card-footer" class (str " " class))} attrs)] children)))
|
||||
|
||||
;; ─── Card List ─────────────────────────────────────────────────────
|
||||
|
||||
(defn card-list-class-list
|
||||
"Generate a vector of CSS class strings for a card-list.
|
||||
Options:
|
||||
:divider - :full (default) or :inset (borders have horizontal spacing from edges)"
|
||||
[{:keys [divider]}]
|
||||
(let [d (or (some-> divider kw-name) "full")]
|
||||
(cond-> ["card-list"]
|
||||
(= d "inset") (conj "card-list-inset"))))
|
||||
|
||||
(defn card-list-classes
|
||||
"Generate CSS class string for a card-list."
|
||||
[opts]
|
||||
(str/join " " (card-list-class-list opts)))
|
||||
|
||||
(defn card-list
|
||||
"Render a card-list container — a card background with items separated by borders.
|
||||
|
||||
Props:
|
||||
:divider - :full (default, borders go edge-to-edge) or :inset (borders inset from edges)
|
||||
:class - additional CSS classes
|
||||
:attrs - additional HTML attributes"
|
||||
[{:keys [divider class attrs] :as _props} & children]
|
||||
#?(:squint
|
||||
(let [classes (cond-> (card-list-classes {:divider divider})
|
||||
class (str " " class))
|
||||
base-attrs (merge {:class classes :role "list"} attrs)]
|
||||
(into [:div base-attrs] children))
|
||||
|
||||
:cljs
|
||||
(let [cls (card-list-class-list {:divider divider})
|
||||
classes (cond-> cls class (conj class))
|
||||
base-attrs (merge {:class classes :role "list"} attrs)]
|
||||
(into [:div base-attrs] children))
|
||||
|
||||
:clj
|
||||
(let [classes (cond-> (card-list-classes {:divider divider})
|
||||
class (str " " class))
|
||||
base-attrs (merge {:class classes :role "list"} attrs)]
|
||||
(into [:div base-attrs] children))))
|
||||
|
||||
(defn card-list-item
|
||||
"Render an item inside a card-list.
|
||||
|
||||
Props:
|
||||
:class - additional CSS classes
|
||||
:attrs - additional HTML attributes"
|
||||
[{:keys [class attrs] :as _props} & children]
|
||||
#?(:squint
|
||||
(into [:div (merge {:class (cond-> "card-list-item" class (str " " class))
|
||||
:role "listitem"} attrs)] children)
|
||||
:cljs
|
||||
(into [:div (merge {:class (cond-> ["card-list-item"] class (conj class))
|
||||
:role "listitem"} attrs)] children)
|
||||
:clj
|
||||
(into [:div (merge {:class (cond-> "card-list-item" class (str " " class))
|
||||
:role "listitem"} attrs)] children)))
|
||||
|
||||
@@ -63,3 +63,39 @@
|
||||
font-size: var(--font-base);
|
||||
line-height: var(--size-5);
|
||||
}
|
||||
|
||||
/* ─── Card List ───────────────────────────────────────────────── */
|
||||
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-1);
|
||||
color: var(--fg-0);
|
||||
border: var(--border-0);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-list-item {
|
||||
padding: var(--size-3) var(--size-4);
|
||||
}
|
||||
|
||||
/* Full-width dividers (default) */
|
||||
.card-list-item + .card-list-item {
|
||||
border-top: var(--border-0);
|
||||
}
|
||||
|
||||
/* Inset dividers — border is inset from edges via pseudo-element */
|
||||
.card-list-inset .card-list-item + .card-list-item {
|
||||
border-top: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-list-inset .card-list-item + .card-list-item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: var(--size-4);
|
||||
right: var(--size-4);
|
||||
border-top: var(--border-0);
|
||||
}
|
||||
|
||||
@@ -34,3 +34,38 @@
|
||||
(let [result (card/card-footer {} "Actions")]
|
||||
(is (= :footer (first result)))
|
||||
(is (= "card-footer" (get-in result [1 :class]))))))
|
||||
|
||||
;; ─── Card List ─────────────────────────────────────────────────
|
||||
|
||||
(deftest card-list-class-list-test
|
||||
(testing "default divider is full"
|
||||
(is (= ["card-list"] (card/card-list-class-list {}))))
|
||||
(testing "explicit full divider"
|
||||
(is (= ["card-list"] (card/card-list-class-list {:divider :full}))))
|
||||
(testing "inset divider adds modifier"
|
||||
(is (= ["card-list" "card-list-inset"] (card/card-list-class-list {:divider :inset})))))
|
||||
|
||||
(deftest card-list-test
|
||||
(testing "renders a div with role list"
|
||||
(let [result (card/card-list {} "Item")]
|
||||
(is (= :div (first result)))
|
||||
(is (= "card-list" (get-in result [1 :class])))
|
||||
(is (= "list" (get-in result [1 :role])))
|
||||
(is (= "Item" (nth result 2)))))
|
||||
(testing "inset divider class"
|
||||
(let [result (card/card-list {:divider :inset} "Item")]
|
||||
(is (= "card-list card-list-inset" (get-in result [1 :class])))))
|
||||
(testing "extra class gets appended"
|
||||
(let [result (card/card-list {:class "extra"} "Item")]
|
||||
(is (= "card-list extra" (get-in result [1 :class]))))))
|
||||
|
||||
(deftest card-list-item-test
|
||||
(testing "renders a div with role listitem"
|
||||
(let [result (card/card-list-item {} "Content")]
|
||||
(is (= :div (first result)))
|
||||
(is (= "card-list-item" (get-in result [1 :class])))
|
||||
(is (= "listitem" (get-in result [1 :role])))
|
||||
(is (= "Content" (nth result 2)))))
|
||||
(testing "extra class gets appended"
|
||||
(let [result (card/card-list-item {:class "extra"} "Content")]
|
||||
(is (= "card-list-item extra" (get-in result [1 :class]))))))
|
||||
|
||||
Reference in New Issue
Block a user