@marianmeres/stuic 3.174.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 +2 -2
- package/dist/components/DescriptionList/DescriptionList.svelte +270 -0
- package/dist/components/DescriptionList/DescriptionList.svelte.d.ts +127 -0
- package/dist/components/DescriptionList/README.md +254 -0
- package/dist/components/DescriptionList/index.css +264 -0
- package/dist/components/DescriptionList/index.d.ts +1 -0
- package/dist/components/DescriptionList/index.js +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/docs/domains/components.md +51 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
src/lib/
|
|
26
|
-
├── components/ #
|
|
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)
|
|
@@ -146,7 +146,7 @@ so it is the only confusable pair — do not "fix" one into the other.
|
|
|
146
146
|
|
|
147
147
|
### Domain Docs
|
|
148
148
|
|
|
149
|
-
- [Components](./docs/domains/components.md) —
|
|
149
|
+
- [Components](./docs/domains/components.md) — 79 component directories, Props pattern, snippets
|
|
150
150
|
- [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
|
|
151
151
|
- [CSS presets](./docs/domains/css-presets.md) — ratio-locked frame (letterbox), safe-area, scrollbar
|
|
152
152
|
- [Actions](./docs/domains/actions.md) — 16 Svelte directives
|
|
@@ -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;
|
|
@@ -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";
|
package/dist/index.css
CHANGED
|
@@ -95,6 +95,7 @@ In practice:
|
|
|
95
95
|
@import "./components/CopyButton/index.css";
|
|
96
96
|
@import "./components/CronInput/index.css";
|
|
97
97
|
@import "./components/DataTable/index.css";
|
|
98
|
+
@import "./components/DescriptionList/index.css";
|
|
98
99
|
@import "./components/DismissibleMessage/index.css";
|
|
99
100
|
@import "./components/DropdownMenu/index.css";
|
|
100
101
|
@import "./components/EmailVerifyForm/index.css";
|
package/dist/index.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export * from "./components/ContextMenu/index.js";
|
|
|
46
46
|
export * from "./components/CopyButton/index.js";
|
|
47
47
|
export * from "./components/CronInput/index.js";
|
|
48
48
|
export * from "./components/DataTable/index.js";
|
|
49
|
+
export * from "./components/DescriptionList/index.js";
|
|
49
50
|
export * from "./components/DismissibleMessage/index.js";
|
|
50
51
|
export * from "./components/Drawer/index.js";
|
|
51
52
|
export * from "./components/DropdownMenu/index.js";
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,7 @@ export * from "./components/ContextMenu/index.js";
|
|
|
52
52
|
export * from "./components/CopyButton/index.js";
|
|
53
53
|
export * from "./components/CronInput/index.js";
|
|
54
54
|
export * from "./components/DataTable/index.js";
|
|
55
|
+
export * from "./components/DescriptionList/index.js";
|
|
55
56
|
export * from "./components/DismissibleMessage/index.js";
|
|
56
57
|
export * from "./components/Drawer/index.js";
|
|
57
58
|
export * from "./components/DropdownMenu/index.js";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
79 Svelte 5 component directories with consistent API patterns. All use runes-based reactivity.
|
|
6
6
|
|
|
7
7
|
## Component Categories
|
|
8
8
|
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
| Thc | Flexible renderer for text, HTML, components, or snippets |
|
|
106
106
|
| Card | Flexible card with image, title, footer; vertical/horizontal layout |
|
|
107
107
|
| Stat | KPI/stat card: label + value + delta with trend arrow and semantic coloring |
|
|
108
|
+
| DescriptionList | Term/value list (`<dl>`): stacked or two-column by container query, hairlines, truncation, totals |
|
|
108
109
|
| Timeline | Vertical event list on a rail: dot/icon/custom markers, inline or opposite time, alternate layout |
|
|
109
110
|
| TrendChart | Svelte wrapper for `@marianmeres/trend-chart` (subpath-only: `@marianmeres/stuic/trend-chart`) |
|
|
110
111
|
| Tree | Hierarchical tree view with keyboard nav and drag-and-drop |
|
|
@@ -970,6 +971,55 @@ Prefix: `--stuic-copy-button-*` (the surface themes via `--stuic-button-*`)
|
|
|
970
971
|
|
|
971
972
|
---
|
|
972
973
|
|
|
974
|
+
## DescriptionList
|
|
975
|
+
|
|
976
|
+
The term-and-value list — the read-only block on every detail page, drawer and back-office panel. Renders `<dl>` › `<div>` per pair › `<dt>` + `<dd>` (plus an optional second `<dd>` qualifying the value), which is what the spec provides for grouping a term with its details and what lets each row be a grid without subgrid. No ARIA is added: a `<dl>` already has list semantics. Two equal ways to feed it — `items` for flat lists, `children` for markup-heavy ones — because the CSS selects **structurally** (`dl > div > dt`, `> div > dd + dd`), so a hand-written row renders identically to a generated one. Not an editor (that is `FieldKeyValues`) and not a table.
|
|
977
|
+
|
|
978
|
+
### Exports
|
|
979
|
+
|
|
980
|
+
| Export | Kind | Description |
|
|
981
|
+
| ---------------------------- | --------- | -------------------------------------------------------------------------------------------------------- |
|
|
982
|
+
| `DescriptionList` | component | Main component |
|
|
983
|
+
| `DescriptionListProps` | type | Props type |
|
|
984
|
+
| `DescriptionListItem` | type | `{ key?, label, value?, description?, href?, title?, labelLang?, valueLang?, emphasis?, wrap?, class* }` |
|
|
985
|
+
| `DescriptionListLayout` | type | `"auto" \| "stacked" \| "columns"` |
|
|
986
|
+
| `DescriptionListColumnsFrom` | type | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` (20 / 24 / 28 / 32 / 36rem) |
|
|
987
|
+
| `DescriptionListDivide` | type | `"none" \| "inside" \| "outside"` |
|
|
988
|
+
| `DescriptionListWrap` | type | `"anywhere" \| "truncate" \| "normal"` |
|
|
989
|
+
| `DescriptionListValueAlign` | type | `"start" \| "end"` |
|
|
990
|
+
| `DescriptionListSnippetArg` | type | `{ item, index }` — argument of all three snippets |
|
|
991
|
+
|
|
992
|
+
### Key Props
|
|
993
|
+
|
|
994
|
+
| Prop | Type | Default | Description |
|
|
995
|
+
| ------------- | -------------------------------------- | ------------ | ----------------------------------------------------------------------------------- |
|
|
996
|
+
| `items` | `DescriptionListItem[]` | — | The rows, in order (label/value/description are THC) |
|
|
997
|
+
| `children` | `Snippet` | — | Rendered inside the `<dl>` _instead of_ `items` |
|
|
998
|
+
| `layout` | `"auto" \| "stacked" \| "columns"` | `"auto"` | Label over value, beside it, or the former until the list is `columnsFrom` wide |
|
|
999
|
+
| `columnsFrom` | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"sm"` | The **list's own** width at which `"auto"` flips. Dropped unless `layout="auto"` |
|
|
1000
|
+
| `divide` | `"none" \| "inside" \| "outside"` | `"inside"` | n−1 rules (list in a box), n+1 (list loose on a page), or none |
|
|
1001
|
+
| `wrap` | `"anywhere" \| "truncate" \| "normal"` | `"anywhere"` | Long-value behavior; per-row override via `item.wrap` |
|
|
1002
|
+
| `valueAlign` | `"start" \| "end"` | `"start"` | `"end"` is the totals shape (pair with `--stuic-description-list-label-width: 1fr`) |
|
|
1003
|
+
| `emptyValue` | `THC` | `"—"` | Used when `value` is `undefined`/`null`/`""` — `0` is a value, not empty |
|
|
1004
|
+
|
|
1005
|
+
Snippets (all receive `{ item, index }`): `renderLabel`, `renderValue`, `renderItem` (the row `<div>` stays — it is what the grid is on). Class slots: `class`, `classItem`, `classLabel`, `classValue`, `classDescription`, each also available per item. Empty `items` with no `children` renders **nothing**, not an empty `<dl>`.
|
|
1006
|
+
|
|
1007
|
+
### Why a container query
|
|
1008
|
+
|
|
1009
|
+
`layout="auto"` makes the `<dl>` a named inline-size container (`stuic-description-list`) and flips its **rows** to a two-column grid at `columnsFrom` — an element cannot query itself. The viewport answers a different question: a 200px-wide list inside a 1400px page must stay stacked, and `sm:` would have flipped it. The breakpoint is a prop on a fixed scale (Tailwind's `@xs`…`@xl`) and not a token because `@container (min-width: var(--x))` is not valid CSS — size queries take literals. Side effect worth knowing: `container-type: inline-size` gives the list inline-axis containment, so its min-content contribution to a flex parent is zero (the list can sit beside a fixed-width element and shrink into what is left; `min-w-0` is already there in the static layouts too).
|
|
1010
|
+
|
|
1011
|
+
### Row-level contract (works in both forms)
|
|
1012
|
+
|
|
1013
|
+
`data-wrap="anywhere|truncate|normal"` and `data-emphasis` on a row `<div>` are honoured by the same selectors the data form uses, so `children` rows opt into per-row wrapping and the _Total_ look without a prop. Under `wrap="truncate"` a plain-string value auto-fills the `<dd>`'s `title` (a clipped value must stay reachable); pass `item.title` for html/component/snippet values.
|
|
1014
|
+
|
|
1015
|
+
### CSS Tokens
|
|
1016
|
+
|
|
1017
|
+
Prefix: `--stuic-description-list-*`
|
|
1018
|
+
|
|
1019
|
+
`label-width` (not declared — fallback `minmax(7rem, 10rem)` at the usage site), `gap-x`, `gap-y`, `item-padding-y`, `rule-color`, `rule-width`, `label-font-size`, `label-font-weight`, `label-text`, `value-text`, `description-font-size`, `description-text`, `label-text-emphasis`, `value-font-weight-emphasis`
|
|
1020
|
+
|
|
1021
|
+
---
|
|
1022
|
+
|
|
973
1023
|
## EmptyState
|
|
974
1024
|
|
|
975
1025
|
Icon + title + description + CTA placeholder for empty lists, tables, and search results. Centered, non-interactive; the CTA area is a snippet filled with consumer `Button`s/links. Pairs naturally with `DataTable` ("no rows") and search UIs ("no results").
|