@marianmeres/stuic 3.173.0 → 3.175.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,254 @@
1
+ # DescriptionList
2
+
3
+ The term-and-value list — the read-only block on every detail page, drawer and back-office
4
+ panel. Renders the HTML the spec intends: `<dl>` › `<div>` per pair › `<dt>` + `<dd>`, with
5
+ an optional second `<dd>` qualifying the value. No ARIA is added or needed — a `<dl>`
6
+ already has list semantics, which is the reason to render one instead of nested `<div>`s.
7
+
8
+ Two ways to feed it, both first-class: `items` for flat lists, `children` for markup-heavy
9
+ ones. The CSS is written on the _structure_ (`dl > div > dt`), so a hand-written
10
+ `<div><dt/><dd/></div>` renders identically to a generated row.
11
+
12
+ The default layout (`"auto"`) puts the label **over** the value while the list is narrow
13
+ and **beside** it once the list itself is wide enough — a container query, not a media
14
+ query, so a narrow list inside a wide page stays stacked.
15
+
16
+ Not an editor (that is `FieldKeyValues`), not a table. A run of numbers that should be a
17
+ table is a table.
18
+
19
+ ## Props
20
+
21
+ | Prop | Type | Default | Description |
22
+ | ------------------ | -------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------- |
23
+ | `items` | `DescriptionListItem[]` | - | The rows, in order. Data-driven form |
24
+ | `children` | `Snippet` | - | Compositional form: rendered inside the `<dl>` _instead of_ `items` |
25
+ | `layout` | `"auto" \| "stacked" \| "columns"` | `"auto"` | Label over value, label beside value, or the former until the list is `columnsFrom` wide |
26
+ | `columnsFrom` | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"sm"` | The **list's own** width at which `"auto"` flips: 20 / 24 / 28 / 32 / 36rem. Only for `"auto"` |
27
+ | `divide` | `"none" \| "inside" \| "outside"` | `"inside"` | Hairlines between rows only, around the whole list as well, or none |
28
+ | `wrap` | `"anywhere" \| "truncate" \| "normal"` | `"anywhere"` | How a long value behaves. Per-row override via `item.wrap` |
29
+ | `valueAlign` | `"start" \| "end"` | `"start"` | Text alignment of the value column (`"end"` is the totals shape) |
30
+ | `emptyValue` | `THC` | `"—"` | Rendered when an item's `value` is `undefined`, `null` or `""`. Pass `""` for an empty cell |
31
+ | `renderLabel` | `Snippet<[DescriptionListSnippetArg]>` | - | Override the `<dt>` content for every row |
32
+ | `renderValue` | `Snippet<[DescriptionListSnippetArg]>` | - | Override the value `<dd>` content for every row |
33
+ | `renderItem` | `Snippet<[DescriptionListSnippetArg]>` | - | Override the whole row _content_ (the `<div>` stays — it is what the grid is on) |
34
+ | `unstyled` | `boolean` | `false` | Skip all default styling (no classes, no data attributes) |
35
+ | `class` | `string` | - | Additional CSS classes on the `<dl>` (merged via twMerge) |
36
+ | `classItem` | `string` | - | Class for every row `<div>` |
37
+ | `classLabel` | `string` | - | Class for every `<dt>` |
38
+ | `classValue` | `string` | - | Class for every value `<dd>` |
39
+ | `classDescription` | `string` | - | Class for every description `<dd>` |
40
+ | `el` | `HTMLDListElement` | - | Element reference (bindable) |
41
+
42
+ Any other attribute (`aria-label`, `data-*`, `style`, `lang`, …) is passed to the `<dl>`.
43
+
44
+ Empty `items` with no `children` renders **nothing** — not even the `<dl>`: an empty list
45
+ with `divide="outside"` would otherwise draw two rules around a void.
46
+
47
+ ### `DescriptionListItem`
48
+
49
+ | Field | Type | Description |
50
+ | ------------------ | --------------------- | ------------------------------------------------------------------------------------------------------- |
51
+ | `key` | `string \| number` | Keyed `{#each}` identity. Falls back to the index |
52
+ | `label` | `THC` | The term (`<dt>`). Required |
53
+ | `value` | `THC \| number` | The details (`<dd>`). A number is `String()`-ed; missing → `emptyValue` |
54
+ | `description` | `THC` | A second `<dd>` under the value — a unit, a qualifier. In the columns state it sits under the value |
55
+ | `href` | `string` | Wraps the value in `<a href>`. Plain link only; anything more is the snippet / `children` form |
56
+ | `title` | `string` | `title` on the value `<dd>`. Auto-filled from a plain-string value under `wrap="truncate"` unless given |
57
+ | `labelLang` | `string` | `lang` on the `<dt>` |
58
+ | `valueLang` | `string` | `lang` on the value `<dd>` |
59
+ | `emphasis` | `boolean` | `data-emphasis` on the row: full-strength label color and semibold value — the _Total_ row |
60
+ | `wrap` | `DescriptionListWrap` | Per-row override of the list's `wrap` |
61
+ | `class` | `string` | Class for this row, merged after `classItem` |
62
+ | `classLabel` | `string` | Class for this `<dt>`, merged after `classLabel` |
63
+ | `classValue` | `string` | Class for this value `<dd>`, merged after `classValue` |
64
+ | `classDescription` | `string` | Class for this description `<dd>`, merged after `classDescription` |
65
+
66
+ ### Snippet Props
67
+
68
+ All three receive `DescriptionListSnippetArg` = `{ item, index }`, as in `Timeline`.
69
+
70
+ ## Usage
71
+
72
+ ### Basic
73
+
74
+ ```svelte
75
+ <script lang="ts">
76
+ import { DescriptionList } from "@marianmeres/stuic";
77
+ </script>
78
+
79
+ <DescriptionList
80
+ items={[
81
+ { label: "Reference", value: "REF-2026-0042" },
82
+ { label: "Created", value: "Sep 1, 2026" },
83
+ { label: "Owner", value: null }, // renders the em dash
84
+ ]}
85
+ />
86
+ ```
87
+
88
+ ### A detail block (the reference shape)
89
+
90
+ Stacked while narrow, two columns once the list itself has room, interior hairlines, a
91
+ truncated URL that stays reachable through its `title`, and a qualifier line under a count:
92
+
93
+ ```svelte
94
+ <DescriptionList
95
+ class="mt-3 text-sm"
96
+ items={[
97
+ { label: "Reference", value: doc.reference, classValue: "font-mono font-semibold" },
98
+ {
99
+ label: "Share link",
100
+ value: shareUrl(doc),
101
+ href: shareUrl(doc),
102
+ wrap: "truncate",
103
+ },
104
+ {
105
+ label: "Downloads",
106
+ value: downloadCount,
107
+ description: "in the last 30 days",
108
+ classValue: "tabular-nums",
109
+ },
110
+ ]}
111
+ />
112
+ ```
113
+
114
+ ### Totals
115
+
116
+ ```svelte
117
+ <DescriptionList
118
+ layout="columns"
119
+ valueAlign="end"
120
+ divide="none"
121
+ style="--stuic-description-list-label-width: 1fr;"
122
+ items={[
123
+ { label: "Subtotal", value: "$120.00" },
124
+ { label: "Tax", value: "$25.20" },
125
+ { label: "Total", value: "$145.20", emphasis: true, class: "border-t mt-1 pt-2" },
126
+ ]}
127
+ />
128
+ ```
129
+
130
+ ### The `children` form
131
+
132
+ For rows the data form cannot express — a link with `target`/`rel`, a conditional row, a
133
+ button inside a value. The structural CSS styles it identically, so **no layout utility is
134
+ needed on the consumer side**; `data-wrap` and `data-emphasis` on a hand-written row are
135
+ honoured by the same selectors the data form uses.
136
+
137
+ ```svelte
138
+ <DescriptionList class="mt-3 text-sm">
139
+ <div>
140
+ <dt>Reference</dt>
141
+ <dd class="font-mono text-base font-semibold">{doc.reference}</dd>
142
+ </div>
143
+ <div data-wrap="truncate">
144
+ <dt>Share link</dt>
145
+ <dd title={shareUrl(doc)}>
146
+ <a href={shareUrl(doc)} target="_blank" rel="noopener noreferrer">
147
+ {shareUrl(doc)}
148
+ </a>
149
+ </dd>
150
+ </div>
151
+ {#if downloadCount !== null}
152
+ <div>
153
+ <dt>Downloads</dt>
154
+ <dd class="tabular-nums">{downloadCount}</dd>
155
+ <dd>in the last 30 days</dd>
156
+ </div>
157
+ {/if}
158
+ </DescriptionList>
159
+ ```
160
+
161
+ One `<div>` per pair is the contract: it is what the spec provides for grouping a term with
162
+ its details, and it is what lets each row be a grid without subgrid. Bare `<dt>`/`<dd>`
163
+ children of the `<dl>` are valid HTML but get no row styling.
164
+
165
+ ### Snippet overrides
166
+
167
+ ```svelte
168
+ <DescriptionList {items}>
169
+ {#snippet renderValue({ item })}
170
+ <Pill label={item.value} intent="success" />
171
+ {/snippet}
172
+ </DescriptionList>
173
+ ```
174
+
175
+ ## Layout
176
+
177
+ `layout="auto"` makes the `<dl>` an inline-size container (named
178
+ `stuic-description-list`) and switches the **rows** to a two-column grid at `columnsFrom` —
179
+ an element cannot query itself, so the query is on the list and the styling is on its rows.
180
+
181
+ Two consequences worth knowing:
182
+
183
+ - **The list measures itself, not the viewport.** A 200px-wide list on a 1400px page stays
184
+ stacked. That is the whole point: `sm:` would have flipped it while it was still 200px.
185
+ - **`container-type: inline-size` gives the `<dl>` inline-axis size containment**, so its
186
+ min-content contribution to a flex parent is zero — the list can sit beside a fixed-width
187
+ element and shrink to whatever is left. `min-w-0 flex-1` on the consumer side is already
188
+ there (the base rule sets `min-width: 0` in the static layouts too).
189
+
190
+ The breakpoint is a prop on a fixed scale rather than a token because
191
+ `@container (min-width: var(--x))` is not valid CSS — size queries take literals only. The
192
+ scale is Tailwind's own `@xs`…`@xl`, so a consumer who would have typed `@min-[24rem]`
193
+ writes `columnsFrom="sm"`.
194
+
195
+ ## Dividers
196
+
197
+ `divide="inside"` (default) draws `n − 1` rules — a list inside a card, drawer or panel,
198
+ where the box supplies the outer edge; the first and last rows sit flush with it.
199
+ `divide="outside"` draws `n + 1` and keeps the outer padding — a list loose on a page,
200
+ where the top and bottom lines are what close it. `divide="none"` draws none.
201
+
202
+ ## Wrapping
203
+
204
+ `wrap="anywhere"` (default) never lets a value push the page sideways. It is
205
+ `overflow-wrap: anywhere` and not `break-word` on purpose: only `anywhere` is counted in
206
+ min-content sizing, so an inline-block inside a value cannot overflow the column it was
207
+ meant to respect.
208
+
209
+ `wrap="truncate"` clips to one line with an ellipsis — for a value that is chrome (an ID, a
210
+ URL that is also a link). The `title` is auto-filled from a plain-string value so the
211
+ clipped text stays reachable; for an html/component/snippet value there is no string to put
212
+ there, so pass `item.title`. A `href` value must stay `display: inline` for the `<dd>`'s
213
+ `text-overflow` to clip it — do not make the link `inline-block`.
214
+
215
+ `wrap="normal"` leaves the browser default. Any of the three can be set per row via
216
+ `item.wrap` (or `data-wrap` on a hand-written row), which wins over the list-level value.
217
+
218
+ ## CSS Variables
219
+
220
+ | Variable | Default | Description |
221
+ | ----------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------ |
222
+ | `--stuic-description-list-label-width` | `minmax(7rem, 10rem)` | Label track (any `grid-template-columns` value; not declared — fallback at the usage site) |
223
+ | `--stuic-description-list-gap-x` | `1.5rem` | Label ↔ value, columns state |
224
+ | `--stuic-description-list-gap-y` | `0.125rem` | Label ↕ value, stacked state |
225
+ | `--stuic-description-list-item-padding-y` | `0.5rem` | Row padding, both states |
226
+ | `--stuic-description-list-rule-color` | `var(--stuic-color-border)` | Hairline color |
227
+ | `--stuic-description-list-rule-width` | `1px` | Hairline thickness |
228
+ | `--stuic-description-list-label-font-size` | `var(--text-sm)` | Label size |
229
+ | `--stuic-description-list-label-font-weight` | `var(--font-weight-medium)` | Label weight |
230
+ | `--stuic-description-list-label-text` | `var(--stuic-color-muted-foreground)` | Label color |
231
+ | `--stuic-description-list-value-text` | `var(--stuic-color-foreground)` | Value color (size is inherited) |
232
+ | `--stuic-description-list-description-font-size` | `var(--text-sm)` | Description size |
233
+ | `--stuic-description-list-description-text` | `var(--stuic-color-muted-foreground)` | Description color |
234
+ | `--stuic-description-list-label-text-emphasis` | `var(--stuic-color-foreground)` | Label color on an `emphasis` row |
235
+ | `--stuic-description-list-value-font-weight-emphasis` | `var(--font-weight-semibold)` | Value weight on an `emphasis` row |
236
+
237
+ The value deliberately has no font-size token: it inherits, so `class="text-sm"` on the
238
+ `<dl>` scales the list. The rule tokens are the component's own (like `Separator`) rather
239
+ than the `--stuic-border-width` tier, so a theme that zeroes box borders keeps its
240
+ hairlines.
241
+
242
+ ## Data Attributes
243
+
244
+ On the `<dl>`: `data-layout`, `data-columns-from` (only when `layout="auto"`),
245
+ `data-divide`, `data-wrap`, `data-value-align`. On each row `<div>`: `data-wrap` (when
246
+ `item.wrap` is set) and `data-emphasis` (empty attribute). `unstyled` removes all of them
247
+ along with the classes.
248
+
249
+ ## Accessibility
250
+
251
+ The rendered `<dl>` carries list semantics natively — no `role` is added. One term per row:
252
+ a second `<dt>` in the same group is valid HTML but lands in the value column; use two rows
253
+ instead. `lang` per row (`labelLang` / `valueLang`) is for a localized label or a
254
+ fallback-language value inside a page that declares another language.
@@ -0,0 +1,264 @@
1
+ /* ============================================================================
2
+ DESCRIPTION LIST COMPONENT TOKENS
3
+ Override globally: :root { --stuic-description-list-label-width: auto; }
4
+ Override locally: <DescriptionList style="--stuic-description-list-gap-x: 1rem;">
5
+ ============================================================================ */
6
+
7
+ :root {
8
+ /* Layout */
9
+ --stuic-description-list-gap-x: 1.5rem; /* label <-> value, columns state */
10
+ --stuic-description-list-gap-y: 0.125rem; /* label <-> value, stacked state */
11
+ --stuic-description-list-item-padding-y: 0.5rem; /* row padding, both states */
12
+
13
+ /* `--stuic-description-list-label-width` is deliberately NOT declared: it is read
14
+ as a fallback argument at the usage site (`minmax(7rem, 10rem)`) so a scoped
15
+ override on any ancestor works — see "Fallback Pattern" in docs/conventions.md. */
16
+
17
+ /* Rules */
18
+ --stuic-description-list-rule-color: var(--stuic-color-border);
19
+ --stuic-description-list-rule-width: 1px;
20
+
21
+ /* Label */
22
+ --stuic-description-list-label-font-size: var(--text-sm);
23
+ --stuic-description-list-label-font-weight: var(--font-weight-medium);
24
+ --stuic-description-list-label-text: var(--stuic-color-muted-foreground);
25
+
26
+ /* Value — inherits font-size, so `class="text-sm"` on the <dl> scales the whole list */
27
+ --stuic-description-list-value-text: var(--stuic-color-foreground);
28
+
29
+ /* Description (the second <dd>) */
30
+ --stuic-description-list-description-font-size: var(--text-sm);
31
+ --stuic-description-list-description-text: var(--stuic-color-muted-foreground);
32
+
33
+ /* Emphasis row */
34
+ --stuic-description-list-label-text-emphasis: var(--stuic-color-foreground);
35
+ --stuic-description-list-value-font-weight-emphasis: var(--font-weight-semibold);
36
+ }
37
+
38
+ @layer components {
39
+ /* ============================================================================
40
+ BASE — selected STRUCTURALLY (`dl > div > dt`), not by class, so a hand-written
41
+ `<div><dt/><dd/></div>` child renders identically to a generated `items` row.
42
+ The per-element classes exist only as merge targets for `classLabel` & co.
43
+ ============================================================================ */
44
+
45
+ .stuic-description-list {
46
+ /* UA gives <dl> a 1em block margin and <dd> a 40px inline start margin */
47
+ margin: 0;
48
+ /* A list in a flex/grid parent shrinks to its share instead of pushing it wider.
49
+ In `layout="auto"` the container-type below already implies this; declaring it
50
+ here means the static layouts behave the same way. */
51
+ min-width: 0;
52
+ }
53
+
54
+ /* Stacked is the base state: label over value. */
55
+ .stuic-description-list > div {
56
+ display: flex;
57
+ flex-direction: column;
58
+ row-gap: var(--stuic-description-list-gap-y);
59
+ padding-block: var(--stuic-description-list-item-padding-y);
60
+ min-width: 0;
61
+ }
62
+
63
+ .stuic-description-list > div > dt {
64
+ min-width: 0;
65
+ font-size: var(--stuic-description-list-label-font-size);
66
+ font-weight: var(--stuic-description-list-label-font-weight);
67
+ color: var(--stuic-description-list-label-text);
68
+ }
69
+
70
+ .stuic-description-list > div > dd {
71
+ margin: 0;
72
+ min-width: 0;
73
+ color: var(--stuic-description-list-value-text);
74
+ }
75
+
76
+ /* The second <dd> on the same term is the qualifier line. */
77
+ .stuic-description-list > div > dd + dd {
78
+ font-size: var(--stuic-description-list-description-font-size);
79
+ color: var(--stuic-description-list-description-text);
80
+ }
81
+
82
+ /* ============================================================================
83
+ LAYOUT — "columns" is static; "auto" is the same row grid behind a container
84
+ query on the LIST's own inline size (an element cannot query itself, so the
85
+ container is the <dl> and the query styles its rows).
86
+ ============================================================================ */
87
+
88
+ .stuic-description-list[data-layout="columns"] > div {
89
+ display: grid;
90
+ grid-template-columns:
91
+ var(--stuic-description-list-label-width, minmax(7rem, 10rem))
92
+ /* minmax(0, 1fr) and not 1fr: a grid item's automatic minimum size refuses
93
+ to shrink below its content, so `wrap="truncate"` would overflow instead
94
+ of clipping. */
95
+ minmax(0, 1fr);
96
+ column-gap: var(--stuic-description-list-gap-x);
97
+ align-items: baseline;
98
+ }
99
+
100
+ .stuic-description-list[data-layout="columns"] > div > dd + dd {
101
+ grid-column: 2;
102
+ }
103
+
104
+ .stuic-description-list[data-layout="auto"] {
105
+ /* Named so the query below can only ever resolve against this <dl>, never
106
+ against a consumer's own @container ancestor. */
107
+ container-name: stuic-description-list;
108
+ container-type: inline-size;
109
+ }
110
+
111
+ /* The breakpoint cannot be a token: `@container (min-width: var(--x))` is not valid
112
+ CSS — size queries take literals only. Hence a prop on a fixed scale mapped to a
113
+ data attribute, and one block per step. Values are Tailwind's @xs…@xl. */
114
+
115
+ @container stuic-description-list (min-width: 20rem) {
116
+ .stuic-description-list[data-layout="auto"][data-columns-from="xs"] > div {
117
+ display: grid;
118
+ grid-template-columns:
119
+ var(--stuic-description-list-label-width, minmax(7rem, 10rem))
120
+ minmax(0, 1fr);
121
+ column-gap: var(--stuic-description-list-gap-x);
122
+ align-items: baseline;
123
+ }
124
+ .stuic-description-list[data-layout="auto"][data-columns-from="xs"] > div > dd + dd {
125
+ grid-column: 2;
126
+ }
127
+ }
128
+
129
+ @container stuic-description-list (min-width: 24rem) {
130
+ .stuic-description-list[data-layout="auto"][data-columns-from="sm"] > div {
131
+ display: grid;
132
+ grid-template-columns:
133
+ var(--stuic-description-list-label-width, minmax(7rem, 10rem))
134
+ minmax(0, 1fr);
135
+ column-gap: var(--stuic-description-list-gap-x);
136
+ align-items: baseline;
137
+ }
138
+ .stuic-description-list[data-layout="auto"][data-columns-from="sm"] > div > dd + dd {
139
+ grid-column: 2;
140
+ }
141
+ }
142
+
143
+ @container stuic-description-list (min-width: 28rem) {
144
+ .stuic-description-list[data-layout="auto"][data-columns-from="md"] > div {
145
+ display: grid;
146
+ grid-template-columns:
147
+ var(--stuic-description-list-label-width, minmax(7rem, 10rem))
148
+ minmax(0, 1fr);
149
+ column-gap: var(--stuic-description-list-gap-x);
150
+ align-items: baseline;
151
+ }
152
+ .stuic-description-list[data-layout="auto"][data-columns-from="md"] > div > dd + dd {
153
+ grid-column: 2;
154
+ }
155
+ }
156
+
157
+ @container stuic-description-list (min-width: 32rem) {
158
+ .stuic-description-list[data-layout="auto"][data-columns-from="lg"] > div {
159
+ display: grid;
160
+ grid-template-columns:
161
+ var(--stuic-description-list-label-width, minmax(7rem, 10rem))
162
+ minmax(0, 1fr);
163
+ column-gap: var(--stuic-description-list-gap-x);
164
+ align-items: baseline;
165
+ }
166
+ .stuic-description-list[data-layout="auto"][data-columns-from="lg"] > div > dd + dd {
167
+ grid-column: 2;
168
+ }
169
+ }
170
+
171
+ @container stuic-description-list (min-width: 36rem) {
172
+ .stuic-description-list[data-layout="auto"][data-columns-from="xl"] > div {
173
+ display: grid;
174
+ grid-template-columns:
175
+ var(--stuic-description-list-label-width, minmax(7rem, 10rem))
176
+ minmax(0, 1fr);
177
+ column-gap: var(--stuic-description-list-gap-x);
178
+ align-items: baseline;
179
+ }
180
+ .stuic-description-list[data-layout="auto"][data-columns-from="xl"] > div > dd + dd {
181
+ grid-column: 2;
182
+ }
183
+ }
184
+
185
+ /* ============================================================================
186
+ DIVIDERS
187
+ ============================================================================ */
188
+
189
+ .stuic-description-list[data-divide="inside"] > div + div,
190
+ .stuic-description-list[data-divide="outside"] > div {
191
+ border-top: var(--stuic-description-list-rule-width) solid
192
+ var(--stuic-description-list-rule-color);
193
+ }
194
+
195
+ .stuic-description-list[data-divide="outside"] > div:last-child {
196
+ border-bottom: var(--stuic-description-list-rule-width) solid
197
+ var(--stuic-description-list-rule-color);
198
+ }
199
+
200
+ /* Inside a box, the first and last rows sit flush with the box's own edges. With
201
+ OUTSIDE rules the padding is what keeps the text off the rule, so it stays. */
202
+ .stuic-description-list:not([data-divide="outside"]) > div:first-child {
203
+ padding-top: 0;
204
+ }
205
+
206
+ .stuic-description-list:not([data-divide="outside"]) > div:last-child {
207
+ padding-bottom: 0;
208
+ }
209
+
210
+ /* ============================================================================
211
+ WRAPPING — `anywhere` and not `break-word` on purpose: only `anywhere` counts in
212
+ min-content sizing, so an inline-block inside the value cannot overflow the
213
+ column it was meant to respect.
214
+ ============================================================================ */
215
+
216
+ .stuic-description-list[data-wrap="anywhere"] > div > dd {
217
+ overflow-wrap: anywhere;
218
+ }
219
+
220
+ .stuic-description-list[data-wrap="truncate"] > div > dd {
221
+ overflow: hidden;
222
+ text-overflow: ellipsis;
223
+ white-space: nowrap;
224
+ }
225
+
226
+ /* Row-level override (`item.wrap`, or `data-wrap` on a hand-written row). Same
227
+ specificity as the list-level rules above — one class, one attribute, two
228
+ elements — so it wins by source order. This block MUST stay after that one. */
229
+ .stuic-description-list > div[data-wrap="normal"] > dd {
230
+ overflow-wrap: normal;
231
+ white-space: normal;
232
+ overflow: visible;
233
+ text-overflow: clip;
234
+ }
235
+
236
+ .stuic-description-list > div[data-wrap="anywhere"] > dd {
237
+ overflow-wrap: anywhere;
238
+ white-space: normal;
239
+ overflow: visible;
240
+ text-overflow: clip;
241
+ }
242
+
243
+ .stuic-description-list > div[data-wrap="truncate"] > dd {
244
+ overflow: hidden;
245
+ text-overflow: ellipsis;
246
+ white-space: nowrap;
247
+ }
248
+
249
+ /* ============================================================================
250
+ VALUE ALIGNMENT + EMPHASIS (the totals shape)
251
+ ============================================================================ */
252
+
253
+ .stuic-description-list[data-value-align="end"] > div > dd {
254
+ text-align: end;
255
+ }
256
+
257
+ .stuic-description-list > div[data-emphasis] > dt {
258
+ color: var(--stuic-description-list-label-text-emphasis);
259
+ }
260
+
261
+ .stuic-description-list > div[data-emphasis] > dd {
262
+ font-weight: var(--stuic-description-list-value-font-weight-emphasis);
263
+ }
264
+ }
@@ -0,0 +1 @@
1
+ export { default as DescriptionList, type Props as DescriptionListProps, type DescriptionListItem, type DescriptionListSnippetArg, type DescriptionListLayout, type DescriptionListColumnsFrom, type DescriptionListDivide, type DescriptionListWrap, type DescriptionListValueAlign, } from "./DescriptionList.svelte";
@@ -0,0 +1 @@
1
+ export { default as DescriptionList, } from "./DescriptionList.svelte";
@@ -177,6 +177,11 @@
177
177
  let _classTiers = $derived(
178
178
  unstyled ? classTiersProp : twMerge("stuic-pricing-table-tiers", classTiersProp)
179
179
  );
180
+ // The toggle's pill look lives in index.css behind --stuic-pricing-table-toggle-*
181
+ // tokens (an inline declaration here would outrank any :root override).
182
+ let _classToggle = $derived(
183
+ unstyled ? classToggleProp : twMerge("stuic-pricing-table-toggle", classToggleProp)
184
+ );
180
185
  </script>
181
186
 
182
187
  <div bind:this={el} class={_class} {style} {...rest}>
@@ -195,20 +200,9 @@
195
200
  onButtonClick={(i) => {
196
201
  billingPeriod = i === 0 ? "monthly" : "annual";
197
202
  }}
198
- class={classToggleProp}
199
- style="
200
- width: auto;
201
- --stuic-button-group-radius: 9999px;
202
- --stuic-button-group-padding: 0.25rem;
203
- --stuic-button-group-bg: var(--stuic-color-muted);
204
- --stuic-button-group-border-width: 0;
205
- --stuic-button-group-button-bg-active: var(--stuic-color-background);
206
- --stuic-button-group-button-text-active: var(--stuic-color-foreground);
207
- --stuic-button-group-button-bg-active-hover: var(--stuic-color-background);
208
- --stuic-button-group-button-text-active-hover: var(--stuic-color-foreground);
209
- {styleToggle ?? ''}
210
- "
211
- classButtonActive="shadow"
203
+ class={_classToggle}
204
+ style={styleToggle}
205
+ classButtonActive={unstyled ? undefined : "shadow"}
212
206
  />
213
207
  {/if}
214
208
  {/if}
@@ -161,12 +161,35 @@ A data-driven pricing tiers component for displaying plan comparisons with featu
161
161
 
162
162
  ### Toggle
163
163
 
164
- | Variable | Default | Description |
165
- | ------------------------------------------- | ---------------- | ------------------------ |
166
- | `--stuic-pricing-table-toggle-bg` | muted | Toggle track background |
167
- | `--stuic-pricing-table-toggle-bg-active` | background | Active option background |
168
- | `--stuic-pricing-table-toggle-color` | muted-foreground | Option text color |
169
- | `--stuic-pricing-table-toggle-color-active` | foreground | Active option text color |
164
+ The billing toggle is an internal `ButtonGroupRadio`. Its pill look is expressed through the
165
+ tokens below, so a theme can reach it from `:root` like any other part of the component:
166
+
167
+ ```css
168
+ :root {
169
+ /* square toggle to match a flat theme */
170
+ --stuic-pricing-table-toggle-radius: var(--stuic-radius-button);
171
+ }
172
+ ```
173
+
174
+ | Variable | Default | Description |
175
+ | ------------------------------------------------- | ---------------- | ------------------------------------------- |
176
+ | `--stuic-pricing-table-toggle-radius` | `9999px` | Track and option border radius |
177
+ | `--stuic-pricing-table-toggle-bg` | muted | Toggle track background |
178
+ | `--stuic-pricing-table-toggle-border-width` | `0` | Toggle track border width |
179
+ | `--stuic-pricing-table-toggle-padding-x` | `0.25rem` | Track horizontal padding around the options |
180
+ | `--stuic-pricing-table-toggle-padding-y` | `0.25rem` | Track vertical padding around the options |
181
+ | `--stuic-pricing-table-toggle-color` | muted-foreground | Inactive option text color |
182
+ | `--stuic-pricing-table-toggle-color-hover` | foreground | Inactive option text color on hover |
183
+ | `--stuic-pricing-table-toggle-bg-active` | background | Active option background |
184
+ | `--stuic-pricing-table-toggle-color-active` | foreground | Active option text color |
185
+ | `--stuic-pricing-table-toggle-bg-active-hover` | `-bg-active` | Active option background on hover |
186
+ | `--stuic-pricing-table-toggle-color-active-hover` | `-color-active` | Active option text color on hover |
187
+
188
+ For anything these do not cover, `styleToggle` sets inline styles on the group (including raw
189
+ `--stuic-button-group-*` tokens) and wins over all of the above; `classToggle` adds classes.
190
+ Note that `--stuic-button-group-*` set at `:root` does **not** reach the pricing toggle for the
191
+ properties listed above — the component declares them on the element, so use the
192
+ `--stuic-pricing-table-toggle-*` tokens instead.
170
193
 
171
194
  ### Typography
172
195
 
@@ -17,6 +17,60 @@
17
17
  width: 100%;
18
18
  }
19
19
 
20
+ /* ============================================================================
21
+ BILLING TOGGLE (an internal ButtonGroupRadio)
22
+
23
+ The opinionated pill look lives here rather than in the component's inline
24
+ `style` attribute, because an inline custom property declaration outranks any
25
+ `:root` declaration for that element and its subtree -- which made the toggle
26
+ the one part of this component a theme could not reach. Each value is a
27
+ never-declared `--stuic-pricing-table-toggle-*` component token, so
28
+ `:root { --stuic-pricing-table-toggle-radius: 0; }` works like every other
29
+ token here. `styleToggle` still wins over all of it (inline).
30
+ ============================================================================ */
31
+
32
+ .stuic-pricing-table-toggle {
33
+ /* hug the options instead of spanning the row (the group defaults to 100%) */
34
+ width: auto;
35
+
36
+ --stuic-button-group-radius: var(--stuic-pricing-table-toggle-radius, 9999px);
37
+ --stuic-button-group-padding-x: var(--stuic-pricing-table-toggle-padding-x, 0.25rem);
38
+ --stuic-button-group-padding-y: var(--stuic-pricing-table-toggle-padding-y, 0.25rem);
39
+ --stuic-button-group-bg: var(
40
+ --stuic-pricing-table-toggle-bg,
41
+ var(--stuic-color-muted)
42
+ );
43
+ --stuic-button-group-border-width: var(--stuic-pricing-table-toggle-border-width, 0);
44
+
45
+ /* Inactive option */
46
+ --stuic-button-group-button-text: var(
47
+ --stuic-pricing-table-toggle-color,
48
+ var(--stuic-color-muted-foreground)
49
+ );
50
+ --stuic-button-group-button-text-hover: var(
51
+ --stuic-pricing-table-toggle-color-hover,
52
+ var(--stuic-color-foreground)
53
+ );
54
+
55
+ /* Active option (hover falls back to the resting value = no hover shift) */
56
+ --stuic-button-group-button-bg-active: var(
57
+ --stuic-pricing-table-toggle-bg-active,
58
+ var(--stuic-color-background)
59
+ );
60
+ --stuic-button-group-button-text-active: var(
61
+ --stuic-pricing-table-toggle-color-active,
62
+ var(--stuic-color-foreground)
63
+ );
64
+ --stuic-button-group-button-bg-active-hover: var(
65
+ --stuic-pricing-table-toggle-bg-active-hover,
66
+ var(--stuic-pricing-table-toggle-bg-active, var(--stuic-color-background))
67
+ );
68
+ --stuic-button-group-button-text-active-hover: var(
69
+ --stuic-pricing-table-toggle-color-active-hover,
70
+ var(--stuic-pricing-table-toggle-color-active, var(--stuic-color-foreground))
71
+ );
72
+ }
73
+
20
74
  /* ============================================================================
21
75
  TIERS GRID
22
76
  ============================================================================ */