@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.
package/AGENTS.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  ```
25
25
  src/lib/
26
- ├── components/ # 78 component directories
26
+ ├── components/ # 79 component directories
27
27
  ├── actions/ # 16 Svelte actions (use: directives)
28
28
  ├── attachments/ # Svelte attachments ({@attach} — preferred for new DOM helpers)
29
29
  ├── utils/ # 55 utility modules (48 on the barrel)
@@ -101,12 +101,27 @@ Global tokens that control cross-component visual properties. Defined in `src/li
101
101
  }
102
102
  ```
103
103
 
104
- **Element vs Container classification:**
104
+ **Element vs Button vs Container classification:**
105
105
 
106
106
  - **Element** (`--stuic-radius`): inputs, badges, list items, checkboxes, tabs — interactive controls
107
- - **Button** (`--stuic-radius-button`): buttons, button groups — allows rounded buttons even with flat elements
107
+ - **Button** (`--stuic-radius-button`): buttons, split buttons, button groups — allows rounded buttons even with flat elements
108
108
  - **Container** (`--stuic-radius-container`): cards, modals, dropdowns, notifications, accordions — content wrappers
109
109
 
110
+ **Tier tokens vs component tokens — the mirrored names.** Tier tokens (the table above) are
111
+ `--stuic-{property}-{tier}` and ARE declared in `src/lib/index.css`. Component tokens are the
112
+ transposition, `--stuic-{component}-{property}`, and are NEVER declared — they exist only as
113
+ the first argument of a `var()` fallback:
114
+
115
+ ```css
116
+ border-radius: var(--stuic-button-radius, var(--stuic-radius-button));
117
+ /* ^ component token (3) ^ tier token (2) */
118
+ ```
119
+
120
+ So `--stuic-radius-button` applies to Button, SplitButton and ButtonGroupRadio, while
121
+ `--stuic-button-radius` applies to Button alone. Same for `--stuic-border-width-button` vs
122
+ `--stuic-button-border-width`. `button` is the only tier name that is also a component name,
123
+ so it is the only confusable pair — do not "fix" one into the other.
124
+
110
125
  ---
111
126
 
112
127
  ## Before Making Changes
@@ -131,7 +146,7 @@ Global tokens that control cross-component visual properties. Defined in `src/li
131
146
 
132
147
  ### Domain Docs
133
148
 
134
- - [Components](./docs/domains/components.md) — 78 component directories, Props pattern, snippets
149
+ - [Components](./docs/domains/components.md) — 79 component directories, Props pattern, snippets
135
150
  - [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
136
151
  - [CSS presets](./docs/domains/css-presets.md) — ratio-locked frame (letterbox), safe-area, scrollbar
137
152
  - [Actions](./docs/domains/actions.md) — 16 Svelte directives
package/README.md CHANGED
@@ -57,7 +57,7 @@ STUIC uses a 4-layer CSS variable token system:
57
57
  ```
58
58
  Layer 1: Theme Tokens (--stuic-color-*)
59
59
 
60
- Layer 2: Structural Tokens (--stuic-radius, --stuic-shadow, --stuic-border-width, --stuic-transition)
60
+ Layer 2: Structural Tokens (--stuic-radius, --stuic-radius-button, --stuic-shadow, ...)
61
61
  ↓ (used as fallback defaults)
62
62
  Layer 3: Component Tokens (--stuic-button-radius, --stuic-input-accent, etc.)
63
63
  ↓ (Tailwind utility class references)
@@ -87,16 +87,20 @@ Override shared structural tokens to change the entire library's visual characte
87
87
  /* Brutalist — sharp, flat, borderless */
88
88
  :root {
89
89
  --stuic-radius: 0;
90
+ --stuic-radius-button: 0;
90
91
  --stuic-radius-container: 0;
91
92
  --stuic-shadow: none;
92
93
  --stuic-shadow-hover: none;
93
94
  --stuic-shadow-overlay: none;
94
95
  --stuic-shadow-dialog: none;
95
96
  --stuic-border-width: 0;
97
+ --stuic-border-width-button: 0;
96
98
  }
97
99
  ```
98
100
 
99
- Available tokens: `--stuic-radius`, `--stuic-radius-container`, `--stuic-shadow`, `--stuic-shadow-hover`, `--stuic-shadow-overlay`, `--stuic-shadow-dialog`, `--stuic-border-width`, `--stuic-transition`.
101
+ Available tokens: `--stuic-radius`, `--stuic-radius-button`, `--stuic-radius-container`, `--stuic-shadow`, `--stuic-shadow-hover`, `--stuic-shadow-overlay`, `--stuic-shadow-dialog`, `--stuic-border-width`, `--stuic-border-width-button`, `--stuic-transition`.
102
+
103
+ Radius and border-width come in three tiers — elements, **buttons**, containers — so you can flatten inputs while keeping pill buttons, or the reverse.
100
104
 
101
105
  ### Per-Component Customization
102
106
 
@@ -104,7 +108,7 @@ Override specific component tokens:
104
108
 
105
109
  ```css
106
110
  :root {
107
- --stuic-button-radius: 9999px; /* Pill buttons — overrides the shared --stuic-radius */
111
+ --stuic-button-radius: 9999px; /* Pill buttons — overrides the shared --stuic-radius-button */
108
112
  --stuic-switch-accent: #10b981; /* Green switches */
109
113
  }
110
114
  ```
@@ -174,16 +174,17 @@ Notes:
174
174
 
175
175
  ### Component Tokens
176
176
 
177
- | Variable | Default | Description |
178
- | ------------------------------ | ---------------------- | ------------------- |
179
- | `--stuic-button-radius` | `--radius-md` | Border radius |
180
- | `--stuic-button-font-family` | `--font-sans` | Font family |
181
- | `--stuic-button-font-weight` | `--font-weight-medium` | Font weight |
182
- | `--stuic-button-transition` | `100ms` | Transition duration |
183
- | `--stuic-button-ring-width` | `3px` | Focus ring width |
184
- | `--stuic-button-ring-color` | `--stuic-color-ring` | Focus ring color |
185
- | `--stuic-button-raised-offset` | `2px` | 3D effect offset |
186
- | `--stuic-button-raised-color` | `rgb(0 0 0 / 0.8)` | 3D shadow color |
177
+ | Variable | Default | Description |
178
+ | ------------------------------ | ------------------------------ | ------------------- |
179
+ | `--stuic-button-radius` | `--stuic-radius-button` | Border radius |
180
+ | `--stuic-button-border-width` | `--stuic-border-width-button` | Border width |
181
+ | `--stuic-button-font-family` | `--font-sans` | Font family |
182
+ | `--stuic-button-font-weight` | `--font-weight-medium` | Font weight |
183
+ | `--stuic-button-transition` | `--stuic-transition` (`150ms`) | Transition duration |
184
+ | `--stuic-button-ring-width` | `3px` | Focus ring width |
185
+ | `--stuic-button-ring-color` | `--stuic-color-ring` | Focus ring color |
186
+ | `--stuic-button-raised-offset` | `2px` | 3D effect offset |
187
+ | `--stuic-button-raised-color` | `rgb(0 0 0 / 0.8)` | 3D shadow color |
187
188
 
188
189
  ### Size Tokens
189
190
 
@@ -129,34 +129,63 @@ disable the whole group instead, use the top-level `disabled` prop.
129
129
 
130
130
  ## CSS Variables
131
131
 
132
+ All tokens below are **declared** in the component's `:root`. That means a `:root` override
133
+ or a `style` prop override both work, but the `var(--token, <fallback>)` fallbacks visible in
134
+ `index.css` never fire — the defaults are the `:root` values listed here.
135
+
132
136
  ### Component Tokens
133
137
 
134
- | Variable | Default | Description |
135
- | ---------------------------------------- | ------------------------- | --------------------------------------- |
136
- | `--stuic-button-group-radius` | `var(--radius-md)` | Border radius for container and buttons |
137
- | `--stuic-button-group-padding` | `0.375rem` | Container padding |
138
- | `--stuic-button-group-gap` | `0.25rem` | Gap between buttons |
139
- | `--stuic-button-group-border-width` | `1px` | Container border width |
140
- | `--stuic-button-group-transition` | `150ms` | Transition duration |
141
- | `--stuic-button-group-ring-width` | `3px` | Focus ring width |
142
- | `--stuic-button-group-ring-color` | `var(--stuic-color-ring)` | Focus ring color |
143
- | `--stuic-button-group-button-padding-x` | `0.75rem` | Button horizontal padding |
144
- | `--stuic-button-group-button-padding-y` | `0.5rem` | Button vertical padding |
145
- | `--stuic-button-group-button-min-height` | `2.75rem` | Button min height (44px touch target) |
138
+ | Variable | Default | Description |
139
+ | ---------------------------------------- | ------------------------- | ---------------------------------------------- |
140
+ | `--stuic-button-group-radius` | `9999px` | Border radius for container and buttons (pill) |
141
+ | `--stuic-button-group-padding-x` | `4px` | Container horizontal padding |
142
+ | `--stuic-button-group-padding-y` | `3px` | Container vertical padding |
143
+ | `--stuic-button-group-gap` | `0.25rem` | Gap between buttons |
144
+ | `--stuic-button-group-border-width` | `1px` | Container border width |
145
+ | `--stuic-button-group-ring-width` | `4px` | Focus ring width |
146
+ | `--stuic-button-group-ring-color` | `var(--stuic-color-ring)` | Focus ring color |
147
+ | `--stuic-button-group-button-padding-x` | `0.75rem` | Button horizontal padding |
148
+ | `--stuic-button-group-button-padding-y` | `0.375rem` | Button vertical padding |
149
+ | `--stuic-button-group-button-min-height` | `2.25rem` | Button min height (36px) |
150
+ | `--stuic-button-group-transition` | `var(--stuic-transition)` | Transition duration (not declared — fallback) |
151
+
152
+ `--stuic-button-group-transition` is the one exception: it is _not_ declared, so it resolves
153
+ through its usage-site fallback to `--stuic-transition`.
146
154
 
147
155
  ### Color Tokens
148
156
 
149
- | Variable | Default | Description |
150
- | --------------------------------------------- | --------------------------------------- | -------------------------------- |
151
- | `--stuic-button-group-bg` | `var(--stuic-color-surface)` | Container background |
152
- | `--stuic-button-group-text` | `var(--stuic-color-foreground)` | Container text color |
153
- | `--stuic-button-group-border` | `var(--stuic-color-border)` | Container border color |
154
- | `--stuic-button-group-button-bg` | `transparent` | Inactive button background |
155
- | `--stuic-button-group-button-text` | `var(--stuic-color-foreground)` | Inactive button text |
156
- | `--stuic-button-group-button-bg-hover` | `var(--stuic-color-muted)` | Inactive button hover background |
157
- | `--stuic-button-group-button-bg-active` | `var(--stuic-color-primary)` | Active button background |
158
- | `--stuic-button-group-button-text-active` | `var(--stuic-color-primary-foreground)` | Active button text |
159
- | `--stuic-button-group-button-bg-active-hover` | `var(--stuic-color-primary-hover)` | Active button hover background |
157
+ | Variable | Default | Description |
158
+ | ----------------------------------------------- | ----------------------------------------- | -------------------------------- |
159
+ | `--stuic-button-group-bg` | `var(--stuic-color-muted)` | Container background |
160
+ | `--stuic-button-group-text` | `var(--stuic-color-muted-foreground)` | Container text color |
161
+ | `--stuic-button-group-border` | `var(--stuic-input-border)` | Container border color |
162
+ | `--stuic-button-group-border-focus` | `var(--stuic-color-primary)` | Container border on focus-within |
163
+ | `--stuic-button-group-button-bg` | `transparent` | Inactive button background |
164
+ | `--stuic-button-group-button-text` | `var(--stuic-color-muted-foreground)` | Inactive button text |
165
+ | `--stuic-button-group-button-bg-hover` | `transparent` | Inactive button hover background |
166
+ | `--stuic-button-group-button-text-hover` | `var(--stuic-color-foreground)` | Inactive button hover text |
167
+ | `--stuic-button-group-button-bg-active` | `var(--stuic-color-surface-1)` | Active button background |
168
+ | `--stuic-button-group-button-text-active` | `var(--stuic-color-surface-1-foreground)` | Active button text |
169
+ | `--stuic-button-group-button-bg-active-hover` | `var(--stuic-color-surface-1)` | Active button hover background |
170
+ | `--stuic-button-group-button-text-active-hover` | `var(--stuic-color-surface-1-foreground)` | Active button hover text |
171
+
172
+ ### Size Variants
173
+
174
+ `size` sets `data-size` on the container, which re-declares a subset of the tokens above.
175
+ `md` is the `:root` default; `sm` and `lg` override these:
176
+
177
+ | Token | `sm` | `md` (default) | `lg` |
178
+ | ---------------------------------------- | ---------- | -------------- | ---------- |
179
+ | `--stuic-button-group-padding-x` | `3px` | `4px` | `8px` |
180
+ | `--stuic-button-group-padding-y` | `2px` | `3px` | `6px` |
181
+ | `--stuic-button-group-gap` | `0.125rem` | `0.25rem` | `0.375rem` |
182
+ | `--stuic-button-group-button-padding-x` | `0.5rem` | `0.75rem` | `1rem` |
183
+ | `--stuic-button-group-button-padding-y` | `0.375rem` | `0.375rem` | `0.625rem` |
184
+ | `--stuic-button-group-button-min-height` | `2.25rem` | `2.25rem` | `3rem` |
185
+ | overall height | 42px | 44px | 62px |
186
+
187
+ Because the size variants declare these on the container itself, they beat the inherited
188
+ `:root` values — but an inline `style` on the same element still wins over both.
160
189
 
161
190
  ### Customization Examples
162
191
 
@@ -78,7 +78,9 @@
78
78
  ============================================================================ */
79
79
 
80
80
  .stuic-button-group[data-size="sm"] {
81
- --stuic-button-group-padding: 0.25rem;
81
+ /* overall height = 36px + 2×2px + 2×1px = 42px */
82
+ --stuic-button-group-padding-x: 3px;
83
+ --stuic-button-group-padding-y: 2px;
82
84
  --stuic-button-group-gap: 0.125rem;
83
85
  --stuic-button-group-button-padding-x: 0.5rem;
84
86
  --stuic-button-group-button-padding-y: 0.375rem;
@@ -86,7 +88,9 @@
86
88
  }
87
89
 
88
90
  .stuic-button-group[data-size="lg"] {
89
- --stuic-button-group-padding: 0.5rem;
91
+ /* overall height = 48px + 2×6px + 2×1px = 62px */
92
+ --stuic-button-group-padding-x: 8px;
93
+ --stuic-button-group-padding-y: 6px;
90
94
  --stuic-button-group-gap: 0.375rem;
91
95
  --stuic-button-group-button-padding-x: 1rem;
92
96
  --stuic-button-group-button-padding-y: 0.625rem;
@@ -0,0 +1,270 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { Snippet } from "svelte";
4
+ import type { THC } from "../Thc/Thc.svelte";
5
+
6
+ /** Row shape: label above value, label beside value, or the former until there is room */
7
+ export type DescriptionListLayout = "auto" | "stacked" | "columns";
8
+
9
+ /**
10
+ * Where `layout="auto"` switches to columns, measured on the list's OWN inline size.
11
+ * The names and numbers of Tailwind's `@xs`…`@xl` container variants.
12
+ */
13
+ export type DescriptionListColumnsFrom = "xs" | "sm" | "md" | "lg" | "xl";
14
+
15
+ /** Hairlines: between rows only, around the whole list, or none */
16
+ export type DescriptionListDivide = "none" | "inside" | "outside";
17
+
18
+ /** How a long value behaves */
19
+ export type DescriptionListWrap = "anywhere" | "truncate" | "normal";
20
+
21
+ /** Text alignment of the value column */
22
+ export type DescriptionListValueAlign = "start" | "end";
23
+
24
+ export interface DescriptionListItem {
25
+ /** Keyed `{#each}` identity. Falls back to the index. */
26
+ key?: string | number;
27
+ /** The term (`<dt>`) */
28
+ label: THC;
29
+ /**
30
+ * The details (`<dd>`). A number is `String()`-ed. `undefined`, `null` or `""`
31
+ * → `emptyValue` (`null` is accepted because that is what an API row holds).
32
+ */
33
+ value?: THC | number | null;
34
+ /**
35
+ * A second `<dd>` under the value — a unit, a qualifier, a "vs. last month".
36
+ * In the columns state it sits under the value column, never under the label.
37
+ */
38
+ description?: THC | null;
39
+ /**
40
+ * Wraps the value in `<a href>`. Plain link only; anything more (`target`, `rel`,
41
+ * `onclick`) is the snippet / `children` form — same rule as `Stat` and `Timeline`.
42
+ */
43
+ href?: string | null;
44
+ /**
45
+ * `title` attribute on the value `<dd>`. When the effective wrap is `"truncate"`
46
+ * and `value` is a non-empty plain string, it defaults to that value — a clipped
47
+ * value must stay reachable.
48
+ */
49
+ title?: string;
50
+ /** `lang` on the `<dt>` */
51
+ labelLang?: string;
52
+ /** `lang` on the value `<dd>` */
53
+ valueLang?: string;
54
+ /** `data-emphasis` on the row: full-strength label color and semibold value — the *Total* row */
55
+ emphasis?: boolean;
56
+ /** Per-row override of the list's `wrap` */
57
+ wrap?: DescriptionListWrap;
58
+ /** Class for this row (`div`), merged after `classItem` */
59
+ class?: string;
60
+ /** Class for this row's `<dt>`, merged after `classLabel` */
61
+ classLabel?: string;
62
+ /** Class for this row's value `<dd>`, merged after `classValue` */
63
+ classValue?: string;
64
+ /** Class for this row's description `<dd>`, merged after `classDescription` */
65
+ classDescription?: string;
66
+ }
67
+
68
+ export interface DescriptionListSnippetArg {
69
+ item: DescriptionListItem;
70
+ index: number;
71
+ }
72
+
73
+ export interface Props extends Omit<HTMLAttributes<HTMLDListElement>, "children"> {
74
+ /** The rows, in order. Data-driven form. */
75
+ items?: DescriptionListItem[];
76
+ /**
77
+ * Compositional form: rendered inside the `<dl>` *instead of* `items`. Write
78
+ * `<div><dt>…</dt><dd>…</dd></div>` per row; the structural CSS styles it identically.
79
+ */
80
+ children?: Snippet;
81
+ /**
82
+ * `"stacked"`: label above value, always. `"columns"`: label beside value, always.
83
+ * `"auto"` (default): stacked until the list itself is `columnsFrom` wide, then columns.
84
+ */
85
+ layout?: DescriptionListLayout;
86
+ /**
87
+ * The **list's own** inline size at which `"auto"` switches to columns:
88
+ * 20 / 24 / 28 / 32 / 36rem. Ignored unless `layout="auto"`.
89
+ */
90
+ columnsFrom?: DescriptionListColumnsFrom;
91
+ /**
92
+ * `"inside"` (default): a hairline *between* rows only (a list inside a card, drawer
93
+ * or panel — the box supplies the outer edge). `"outside"`: plus one above the first
94
+ * and below the last (a list loose on a page). `"none"`: no rules.
95
+ */
96
+ divide?: DescriptionListDivide;
97
+ /**
98
+ * How a long value behaves. `"anywhere"` (default) never lets a value push the page
99
+ * sideways. `"truncate"` clips to one line with an ellipsis. `"normal"` leaves the
100
+ * browser default. Per-item override via `item.wrap`.
101
+ */
102
+ wrap?: DescriptionListWrap;
103
+ /**
104
+ * Text alignment of the value column. `"end"` is the totals shape — pair it with
105
+ * `--stuic-description-list-label-width: 1fr`.
106
+ */
107
+ valueAlign?: DescriptionListValueAlign;
108
+ /**
109
+ * Rendered in the `<dd>` when an item's `value` is `undefined`, `null` or `""`.
110
+ * Pass `""` to render an empty cell.
111
+ */
112
+ emptyValue?: THC;
113
+ /** Override the `<dt>` content for every row */
114
+ renderLabel?: Snippet<[DescriptionListSnippetArg]>;
115
+ /** Override the value `<dd>` content for every row */
116
+ renderValue?: Snippet<[DescriptionListSnippetArg]>;
117
+ /** Override the whole row *content* (the `<div>` stays — it is what the grid is on) */
118
+ renderItem?: Snippet<[DescriptionListSnippetArg]>;
119
+ /** Skip all default styling */
120
+ unstyled?: boolean;
121
+ /** Additional CSS classes */
122
+ class?: string;
123
+ /** Class for every row (`div`) */
124
+ classItem?: string;
125
+ /** Class for every `<dt>` */
126
+ classLabel?: string;
127
+ /** Class for every value `<dd>` */
128
+ classValue?: string;
129
+ /** Class for every description `<dd>` */
130
+ classDescription?: string;
131
+ /** Bindable element reference */
132
+ el?: HTMLDListElement;
133
+ }
134
+ </script>
135
+
136
+ <script lang="ts">
137
+ import { twMerge } from "../../utils/tw-merge.js";
138
+ import Thc, { isTHCNotEmpty } from "../Thc/Thc.svelte";
139
+
140
+ let {
141
+ items,
142
+ children,
143
+ layout = "auto",
144
+ columnsFrom = "sm",
145
+ divide = "inside",
146
+ wrap = "anywhere",
147
+ valueAlign = "start",
148
+ emptyValue = "—",
149
+ renderLabel,
150
+ renderValue,
151
+ renderItem,
152
+ unstyled = false,
153
+ class: classProp,
154
+ classItem: classItemProp,
155
+ classLabel: classLabelProp,
156
+ classValue: classValueProp,
157
+ classDescription: classDescriptionProp,
158
+ el = $bindable(),
159
+ ...rest
160
+ }: Props = $props();
161
+
162
+ // `isTHCNotEmpty` only knows string/text/html/component — a snippet (bare or
163
+ // `{ snippet }`) is renderable content it reports as empty, which would silently
164
+ // drop a snippet description. Widen the test here.
165
+ const _hasContent = (thc: THC | undefined | null): boolean =>
166
+ typeof thc === "function" ||
167
+ !!(thc && typeof thc === "object" && "snippet" in thc) ||
168
+ isTHCNotEmpty(thc);
169
+
170
+ /** Emptiness of a VALUE is deliberately not `isTHCNotEmpty`: `0` is a value, not empty. */
171
+ const _isEmptyValue = (v: THC | number | undefined | null): boolean =>
172
+ v === undefined || v === null || v === "";
173
+
174
+ const _value = (item: DescriptionListItem): THC => {
175
+ if (_isEmptyValue(item.value)) return emptyValue;
176
+ return typeof item.value === "number" ? String(item.value) : (item.value as THC);
177
+ };
178
+
179
+ const _wrap = (item: DescriptionListItem): DescriptionListWrap => item.wrap ?? wrap;
180
+
181
+ // A clipped value must stay reachable. Only a plain string can become a title —
182
+ // html/component/snippet values have no string to put there (pass `item.title`).
183
+ const _title = (item: DescriptionListItem): string | undefined => {
184
+ if (item.title !== undefined) return item.title;
185
+ if (_wrap(item) !== "truncate") return undefined;
186
+ return typeof item.value === "string" && item.value !== "" ? item.value : undefined;
187
+ };
188
+
189
+ let _class = $derived(
190
+ unstyled ? classProp : twMerge("stuic-description-list", classProp)
191
+ );
192
+
193
+ const _classItem = (item: DescriptionListItem) =>
194
+ unstyled
195
+ ? twMerge(classItemProp, item.class)
196
+ : twMerge("stuic-description-list-item", classItemProp, item.class);
197
+
198
+ const _classLabel = (item: DescriptionListItem) =>
199
+ unstyled
200
+ ? twMerge(classLabelProp, item.classLabel)
201
+ : twMerge("stuic-description-list-label", classLabelProp, item.classLabel);
202
+
203
+ const _classValue = (item: DescriptionListItem) =>
204
+ unstyled
205
+ ? twMerge(classValueProp, item.classValue)
206
+ : twMerge("stuic-description-list-value", classValueProp, item.classValue);
207
+
208
+ const _classDescription = (item: DescriptionListItem) =>
209
+ unstyled
210
+ ? twMerge(classDescriptionProp, item.classDescription)
211
+ : twMerge(
212
+ "stuic-description-list-description",
213
+ classDescriptionProp,
214
+ item.classDescription
215
+ );
216
+
217
+ // An empty list with `divide="outside"` would draw two rules around a void.
218
+ let _render = $derived(!!children || !!items?.length);
219
+ </script>
220
+
221
+ {#if _render}
222
+ <dl
223
+ bind:this={el}
224
+ class={_class}
225
+ data-layout={!unstyled ? layout : undefined}
226
+ data-columns-from={!unstyled && layout === "auto" ? columnsFrom : undefined}
227
+ data-divide={!unstyled ? divide : undefined}
228
+ data-wrap={!unstyled ? wrap : undefined}
229
+ data-value-align={!unstyled ? valueAlign : undefined}
230
+ {...rest}
231
+ >
232
+ {#if children}
233
+ {@render children()}
234
+ {:else}
235
+ {#each items ?? [] as item, index (item.key ?? index)}
236
+ <div
237
+ class={_classItem(item)}
238
+ data-emphasis={!unstyled && item.emphasis ? "" : undefined}
239
+ data-wrap={!unstyled ? item.wrap : undefined}
240
+ >
241
+ {#if renderItem}
242
+ {@render renderItem({ item, index })}
243
+ {:else}
244
+ <dt class={_classLabel(item)} lang={item.labelLang}>
245
+ {#if renderLabel}
246
+ {@render renderLabel({ item, index })}
247
+ {:else}
248
+ <Thc thc={item.label} />
249
+ {/if}
250
+ </dt>
251
+ <dd class={_classValue(item)} lang={item.valueLang} title={_title(item)}>
252
+ {#if renderValue}
253
+ {@render renderValue({ item, index })}
254
+ {:else if item.href}
255
+ <a href={item.href}><Thc thc={_value(item)} /></a>
256
+ {:else}
257
+ <Thc thc={_value(item)} />
258
+ {/if}
259
+ </dd>
260
+ {#if _hasContent(item.description)}
261
+ <dd class={_classDescription(item)}>
262
+ <Thc thc={item.description!} />
263
+ </dd>
264
+ {/if}
265
+ {/if}
266
+ </div>
267
+ {/each}
268
+ {/if}
269
+ </dl>
270
+ {/if}
@@ -0,0 +1,127 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ import type { Snippet } from "svelte";
3
+ import type { THC } from "../Thc/Thc.svelte";
4
+ /** Row shape: label above value, label beside value, or the former until there is room */
5
+ export type DescriptionListLayout = "auto" | "stacked" | "columns";
6
+ /**
7
+ * Where `layout="auto"` switches to columns, measured on the list's OWN inline size.
8
+ * The names and numbers of Tailwind's `@xs`…`@xl` container variants.
9
+ */
10
+ export type DescriptionListColumnsFrom = "xs" | "sm" | "md" | "lg" | "xl";
11
+ /** Hairlines: between rows only, around the whole list, or none */
12
+ export type DescriptionListDivide = "none" | "inside" | "outside";
13
+ /** How a long value behaves */
14
+ export type DescriptionListWrap = "anywhere" | "truncate" | "normal";
15
+ /** Text alignment of the value column */
16
+ export type DescriptionListValueAlign = "start" | "end";
17
+ export interface DescriptionListItem {
18
+ /** Keyed `{#each}` identity. Falls back to the index. */
19
+ key?: string | number;
20
+ /** The term (`<dt>`) */
21
+ label: THC;
22
+ /**
23
+ * The details (`<dd>`). A number is `String()`-ed. `undefined`, `null` or `""`
24
+ * → `emptyValue` (`null` is accepted because that is what an API row holds).
25
+ */
26
+ value?: THC | number | null;
27
+ /**
28
+ * A second `<dd>` under the value — a unit, a qualifier, a "vs. last month".
29
+ * In the columns state it sits under the value column, never under the label.
30
+ */
31
+ description?: THC | null;
32
+ /**
33
+ * Wraps the value in `<a href>`. Plain link only; anything more (`target`, `rel`,
34
+ * `onclick`) is the snippet / `children` form — same rule as `Stat` and `Timeline`.
35
+ */
36
+ href?: string | null;
37
+ /**
38
+ * `title` attribute on the value `<dd>`. When the effective wrap is `"truncate"`
39
+ * and `value` is a non-empty plain string, it defaults to that value — a clipped
40
+ * value must stay reachable.
41
+ */
42
+ title?: string;
43
+ /** `lang` on the `<dt>` */
44
+ labelLang?: string;
45
+ /** `lang` on the value `<dd>` */
46
+ valueLang?: string;
47
+ /** `data-emphasis` on the row: full-strength label color and semibold value — the *Total* row */
48
+ emphasis?: boolean;
49
+ /** Per-row override of the list's `wrap` */
50
+ wrap?: DescriptionListWrap;
51
+ /** Class for this row (`div`), merged after `classItem` */
52
+ class?: string;
53
+ /** Class for this row's `<dt>`, merged after `classLabel` */
54
+ classLabel?: string;
55
+ /** Class for this row's value `<dd>`, merged after `classValue` */
56
+ classValue?: string;
57
+ /** Class for this row's description `<dd>`, merged after `classDescription` */
58
+ classDescription?: string;
59
+ }
60
+ export interface DescriptionListSnippetArg {
61
+ item: DescriptionListItem;
62
+ index: number;
63
+ }
64
+ export interface Props extends Omit<HTMLAttributes<HTMLDListElement>, "children"> {
65
+ /** The rows, in order. Data-driven form. */
66
+ items?: DescriptionListItem[];
67
+ /**
68
+ * Compositional form: rendered inside the `<dl>` *instead of* `items`. Write
69
+ * `<div><dt>…</dt><dd>…</dd></div>` per row; the structural CSS styles it identically.
70
+ */
71
+ children?: Snippet;
72
+ /**
73
+ * `"stacked"`: label above value, always. `"columns"`: label beside value, always.
74
+ * `"auto"` (default): stacked until the list itself is `columnsFrom` wide, then columns.
75
+ */
76
+ layout?: DescriptionListLayout;
77
+ /**
78
+ * The **list's own** inline size at which `"auto"` switches to columns:
79
+ * 20 / 24 / 28 / 32 / 36rem. Ignored unless `layout="auto"`.
80
+ */
81
+ columnsFrom?: DescriptionListColumnsFrom;
82
+ /**
83
+ * `"inside"` (default): a hairline *between* rows only (a list inside a card, drawer
84
+ * or panel — the box supplies the outer edge). `"outside"`: plus one above the first
85
+ * and below the last (a list loose on a page). `"none"`: no rules.
86
+ */
87
+ divide?: DescriptionListDivide;
88
+ /**
89
+ * How a long value behaves. `"anywhere"` (default) never lets a value push the page
90
+ * sideways. `"truncate"` clips to one line with an ellipsis. `"normal"` leaves the
91
+ * browser default. Per-item override via `item.wrap`.
92
+ */
93
+ wrap?: DescriptionListWrap;
94
+ /**
95
+ * Text alignment of the value column. `"end"` is the totals shape — pair it with
96
+ * `--stuic-description-list-label-width: 1fr`.
97
+ */
98
+ valueAlign?: DescriptionListValueAlign;
99
+ /**
100
+ * Rendered in the `<dd>` when an item's `value` is `undefined`, `null` or `""`.
101
+ * Pass `""` to render an empty cell.
102
+ */
103
+ emptyValue?: THC;
104
+ /** Override the `<dt>` content for every row */
105
+ renderLabel?: Snippet<[DescriptionListSnippetArg]>;
106
+ /** Override the value `<dd>` content for every row */
107
+ renderValue?: Snippet<[DescriptionListSnippetArg]>;
108
+ /** Override the whole row *content* (the `<div>` stays — it is what the grid is on) */
109
+ renderItem?: Snippet<[DescriptionListSnippetArg]>;
110
+ /** Skip all default styling */
111
+ unstyled?: boolean;
112
+ /** Additional CSS classes */
113
+ class?: string;
114
+ /** Class for every row (`div`) */
115
+ classItem?: string;
116
+ /** Class for every `<dt>` */
117
+ classLabel?: string;
118
+ /** Class for every value `<dd>` */
119
+ classValue?: string;
120
+ /** Class for every description `<dd>` */
121
+ classDescription?: string;
122
+ /** Bindable element reference */
123
+ el?: HTMLDListElement;
124
+ }
125
+ declare const DescriptionList: import("svelte").Component<Props, {}, "el">;
126
+ type DescriptionList = ReturnType<typeof DescriptionList>;
127
+ export default DescriptionList;