fix(card): reduce internal spacing and bump footer button size

Refactor card to use flex column with gap-based spacing instead of
per-section padding, tightening the space between header, body, and
footer from size-6 to size-3. Add .card-footer .btn override to
render buttons at font-base size.
This commit is contained in:
Florian Schroedl
2026-03-03 17:08:29 +01:00
parent e4ee7b750e
commit 0dd8a9c8bf
4 changed files with 69 additions and 49 deletions

View File

@@ -3,9 +3,8 @@
(defn accordion-class-list (defn accordion-class-list
"Generate a vector of CSS class strings for an accordion item." "Generate a vector of CSS class strings for an accordion item."
[{:keys [open]}] [_opts]
(cond-> ["accordion"] ["accordion"])
open (conj "accordion--open")))
(defn accordion-classes (defn accordion-classes
"Generate CSS class string for an accordion." "Generate CSS class string for an accordion."
@@ -13,40 +12,40 @@
(str/join " " (accordion-class-list opts))) (str/join " " (accordion-class-list opts)))
(defn accordion (defn accordion
"Render an accordion (collapsible) item. "Render an accordion (collapsible) item using native <details>/<summary>.
Props: Props:
:title - trigger text :title - trigger text
:open - boolean, whether expanded :open - boolean, whether initially expanded
:class - additional CSS classes :class - additional CSS classes
:attrs - additional HTML attributes" :attrs - additional HTML attributes"
[{:keys [title open class attrs] :as _props} & children] [{:keys [title open class attrs] :as _props} & children]
#?(:squint #?(:squint
(let [classes (cond-> (accordion-classes {:open open}) (let [classes (cond-> (accordion-classes {})
class (str " " class)) class (str " " class))
base-attrs (merge {:class classes} attrs)] base-attrs (cond-> (merge {:class classes} attrs)
(into [:div base-attrs open (assoc :open true))]
[:div {:class "accordion-trigger"} title]] (into [:details base-attrs
(when open [:summary {:class "accordion-trigger"} title]
[[:div {:class "accordion-content"} [:div {:class "accordion-content"}]]
(into [:div] children)]]))) children))
:cljs :cljs
(let [cls (accordion-class-list {:open open}) (let [cls (accordion-class-list {})
classes (cond-> cls class (conj class)) classes (cond-> cls class (conj class))
base-attrs (merge {:class classes} attrs)] base-attrs (cond-> (merge {:class classes} attrs)
(into [:div base-attrs open (assoc :open true))]
[:div {:class ["accordion-trigger"]} title]] (into [:details base-attrs
(when open [:summary {:class ["accordion-trigger"]} title]
[[:div {:class ["accordion-content"]} [:div {:class ["accordion-content"]}]]
(into [:div] children)]]))) children))
:clj :clj
(let [classes (cond-> (accordion-classes {:open open}) (let [classes (cond-> (accordion-classes {})
class (str " " class)) class (str " " class))
base-attrs (merge {:class classes} attrs)] base-attrs (cond-> (merge {:class classes} attrs)
(into [:div base-attrs open (assoc :open true))]
[:div {:class "accordion-trigger"} title]] (into [:details base-attrs
(when open [:summary {:class "accordion-trigger"} title]
[[:div {:class "accordion-content"} [:div {:class "accordion-content"}]]
(into [:div] children)]]))))) children))))

View File

@@ -31,6 +31,14 @@
user-select: none; user-select: none;
color: var(--fg-0); color: var(--fg-0);
transition: background-color 150ms ease; transition: background-color 150ms ease;
list-style: none;
}
/* Hide default marker across browsers */
.accordion-trigger::-webkit-details-marker,
.accordion-trigger::marker {
display: none;
content: "";
} }
.accordion-trigger:hover { .accordion-trigger:hover {
@@ -49,11 +57,11 @@
transition: transform 150ms ease; transition: transform 150ms ease;
} }
.accordion--open > .accordion-trigger { .accordion[open] > .accordion-trigger {
border-bottom: var(--border-0); border-bottom: var(--border-0);
} }
.accordion--open > .accordion-trigger::after { .accordion[open] > .accordion-trigger::after {
transform: rotate(180deg); transform: rotate(180deg);
} }

View File

@@ -1,18 +1,20 @@
.card { .card {
display: flex;
flex-direction: column;
gap: var(--size-3);
background: var(--bg-1); background: var(--bg-1);
color: var(--fg-0); color: var(--fg-0);
border: var(--border-0); border: var(--border-0);
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: var(--shadow-0); box-shadow: var(--shadow-0);
overflow: hidden; overflow: hidden;
padding: var(--size-6);
} }
.card-header { .card-header {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--size-1); gap: var(--size-1);
padding: var(--size-6);
padding-bottom: 0;
} }
.card-header h1, .card-header h1,
@@ -31,13 +33,16 @@
} }
.card-body { .card-body {
padding: var(--size-6);
} }
.card-footer { .card-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: var(--size-2); gap: var(--size-2);
padding: var(--size-6); }
padding-top: 0;
.card-footer .btn {
padding: var(--size-2) var(--size-4);
font-size: var(--font-base);
line-height: var(--size-5);
} }

View File

@@ -3,29 +3,37 @@
[ui.accordion :as accordion])) [ui.accordion :as accordion]))
(deftest accordion-class-list-test (deftest accordion-class-list-test
(testing "closed accordion" (testing "always returns base class"
(is (= ["accordion"] (accordion/accordion-class-list {}))) (is (= ["accordion"] (accordion/accordion-class-list {})))
(is (= ["accordion"] (accordion/accordion-class-list {:open false})))) (is (= ["accordion"] (accordion/accordion-class-list {:open true})))))
(testing "open accordion"
(is (= ["accordion" "accordion--open"] (accordion/accordion-class-list {:open true})))))
(deftest accordion-classes-test (deftest accordion-classes-test
(testing "space-joined output" (testing "space-joined output"
(is (= "accordion" (accordion/accordion-classes {}))) (is (= "accordion" (accordion/accordion-classes {})))
(is (= "accordion accordion--open" (accordion/accordion-classes {:open true}))))) (is (= "accordion" (accordion/accordion-classes {:open true})))))
(deftest accordion-component-test (deftest accordion-component-test
(testing "closed accordion renders trigger only" (testing "closed accordion uses details/summary"
(let [result (accordion/accordion {:title "Question?"} "Answer.")] (let [result (accordion/accordion {:title "Question?"} "Answer.")]
(is (= :div (first result))) (is (= :details (first result)))
(is (= "accordion" (get-in result [1 :class]))) (is (= "accordion" (get-in result [1 :class])))
;; trigger is present (is (nil? (get-in result [1 :open])))
(is (= "accordion-trigger" (get-in result [2 1 :class]))))) ;; summary trigger is present
(is (= :summary (get-in result [2 0])))
(testing "open accordion includes content" (is (= "accordion-trigger" (get-in result [2 1 :class])))
(let [result (accordion/accordion {:title "Q?" :open true} "A.")] ;; content div is always present
(is (= "accordion accordion--open" (get-in result [1 :class])))
;; should contain accordion-content div
(is (some #(and (vector? %) (= "accordion-content" (get-in % [1 :class]))) (is (some #(and (vector? %) (= "accordion-content" (get-in % [1 :class])))
(rest (rest result))))))) (rest (rest result))))))
(testing "open accordion has open attribute"
(let [result (accordion/accordion {:title "Q?" :open true} "A.")]
(is (= :details (first result)))
(is (true? (get-in result [1 :open])))))
(testing "children are inside content div"
(let [result (accordion/accordion {:title "T"} "Child1" "Child2")]
;; find the content div
(let [content-div (first (filter #(and (vector? %)
(= "accordion-content" (get-in % [1 :class])))
(rest (rest result))))]
(is (some? content-div))))))