@marianmeres/stuic 3.189.0 → 3.190.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/API.md +37 -0
- package/dist/components/ListGroup/ListGroup.fixture.svelte +91 -0
- package/dist/components/ListGroup/ListGroup.fixture.svelte.d.ts +12 -0
- package/dist/components/ListGroup/ListGroup.svelte +232 -0
- package/dist/components/ListGroup/ListGroup.svelte.d.ts +116 -0
- package/dist/components/ListGroup/README.md +299 -0
- package/dist/components/ListGroup/index.css +279 -0
- package/dist/components/ListGroup/index.d.ts +1 -0
- package/dist/components/ListGroup/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 +55 -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/ # 82 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) — 82 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
|
package/API.md
CHANGED
|
@@ -1273,6 +1273,43 @@ Horizontal schedule chart — project plans (task per row, progress, milestones)
|
|
|
1273
1273
|
|
|
1274
1274
|
The geometry is exported separately for axis-aligned overlays: `buildGanttAxis`, `placeRange`, `placePoint`, `dayToFraction`, `boundsOf`.
|
|
1275
1275
|
|
|
1276
|
+
#### `ListGroup`
|
|
1277
|
+
|
|
1278
|
+
Bordered, rounded box of rows split by hairlines, with an optional header (title + aside) and footer. Each row is one wrapping flex line of parts; `data-grow` on the part that should absorb the slack. Generic over the item type.
|
|
1279
|
+
|
|
1280
|
+
| Prop | Type | Default | Description |
|
|
1281
|
+
| ------------ | -------------------------------------- | ------- | ---------------------------------------------------------------------- |
|
|
1282
|
+
| `items` | `T[]` | — | The rows (without `renderItem`, each renders as `THC`) |
|
|
1283
|
+
| `renderItem` | `Snippet<[{ item, index }]>` | — | Row content |
|
|
1284
|
+
| `children` | `Snippet` | — | Hand-written `<li>`s instead of `items` (styled identically) |
|
|
1285
|
+
| `getItemId` | `(item, index) => string \| number` | index | Keyed identity |
|
|
1286
|
+
| `itemProps` | `(item, index) => ListGroupItemProps` | — | Attributes (`data-*`, `class`) on each `<li>` |
|
|
1287
|
+
| `itemHref` | `(item, index) => string \| undefined` | — | Makes the whole row a link |
|
|
1288
|
+
| `title` | `THC` | — | Header start side; labels the list |
|
|
1289
|
+
| `titleLevel` | `1…6` | — | Render the title as `<hN>` (semantics only) |
|
|
1290
|
+
| `aside` | `THC` | — | Header end side |
|
|
1291
|
+
| `footer` | `THC` | — | A line under the rows |
|
|
1292
|
+
| `empty` | `THC` | — | Replaces the list when there are no rows; without it, nothing renders |
|
|
1293
|
+
| `listProps` | `ListGroupListProps` | — | Attributes for the `<ul>` (e.g. `aria-label` when there is no `title`) |
|
|
1294
|
+
|
|
1295
|
+
```svelte
|
|
1296
|
+
<ListGroup
|
|
1297
|
+
class="text-sm"
|
|
1298
|
+
items={lines}
|
|
1299
|
+
getItemId={(l) => l.id}
|
|
1300
|
+
itemProps={(l) => ({ "data-line": l.id })}
|
|
1301
|
+
title="Loose items"
|
|
1302
|
+
aside="70 pc"
|
|
1303
|
+
empty="Nothing booked yet."
|
|
1304
|
+
>
|
|
1305
|
+
{#snippet renderItem({ item })}
|
|
1306
|
+
<span class="font-mono">{item.code}</span>
|
|
1307
|
+
<span data-grow>{item.name}</span>
|
|
1308
|
+
<span>×{item.qty}</span>
|
|
1309
|
+
{/snippet}
|
|
1310
|
+
</ListGroup>
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1276
1313
|
#### `ImageCycler`
|
|
1277
1314
|
|
|
1278
1315
|
Auto-cycling image carousel with fade transitions. Preloads next image before displaying. Supports custom title/description snippets.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Layout fixture: rows whose parts are DIRECT children of the <li> (a raw snippet can
|
|
3
|
+
// only render one root element), plus a hand-written `children` form to compare.
|
|
4
|
+
import ListGroup from "./ListGroup.svelte";
|
|
5
|
+
|
|
6
|
+
interface Line {
|
|
7
|
+
id: number;
|
|
8
|
+
code: string;
|
|
9
|
+
name: string;
|
|
10
|
+
qty: number;
|
|
11
|
+
location: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
width = 1000,
|
|
16
|
+
mode = "items",
|
|
17
|
+
linked = false,
|
|
18
|
+
showRow = false,
|
|
19
|
+
style,
|
|
20
|
+
title = "Loose items",
|
|
21
|
+
}: {
|
|
22
|
+
width?: number;
|
|
23
|
+
mode?: "items" | "children" | "children-empty";
|
|
24
|
+
linked?: boolean;
|
|
25
|
+
showRow?: boolean;
|
|
26
|
+
style?: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
} = $props();
|
|
29
|
+
|
|
30
|
+
const LINES: Line[] = [
|
|
31
|
+
{
|
|
32
|
+
id: 1,
|
|
33
|
+
code: "7QMET2GD",
|
|
34
|
+
name: "Folding chair",
|
|
35
|
+
qty: 40,
|
|
36
|
+
location: "hall-a.floor-a",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 2,
|
|
40
|
+
code: "89A6KMHY",
|
|
41
|
+
name: "Carpet tape, double-sided",
|
|
42
|
+
qty: 3,
|
|
43
|
+
location: "hall-a-raca.rack-a2.shelf-a2-2.consumables-bin",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<!-- the theme is not loaded in browser tests: `--stuic-color-border` / `-ring` are undefined, which
|
|
49
|
+
would make the rule and ring declarations invalid at computed-value time -->
|
|
50
|
+
<div
|
|
51
|
+
data-testid="frame"
|
|
52
|
+
style="width: {width}px; --stuic-list-group-rule-color: currentColor; --stuic-list-group-item-ring-color: currentColor;"
|
|
53
|
+
>
|
|
54
|
+
{#if mode === "items"}
|
|
55
|
+
<ListGroup
|
|
56
|
+
items={LINES}
|
|
57
|
+
getItemId={(l) => l.id}
|
|
58
|
+
itemHref={linked ? (l) => `#/line/${l.id}` : undefined}
|
|
59
|
+
{title}
|
|
60
|
+
{style}
|
|
61
|
+
>
|
|
62
|
+
{#snippet renderItem({ item })}
|
|
63
|
+
<span data-part="code">{item.code}</span>
|
|
64
|
+
<span data-part="name" data-grow>{item.name}</span>
|
|
65
|
+
<span data-part="qty">×{item.qty}</span>
|
|
66
|
+
<span data-part="picked">picked {item.qty}/{item.qty}</span>
|
|
67
|
+
<span data-part="location">{item.location}</span>
|
|
68
|
+
{/snippet}
|
|
69
|
+
</ListGroup>
|
|
70
|
+
{:else if mode === "children"}
|
|
71
|
+
<ListGroup {title} {style}>
|
|
72
|
+
<li data-row="plain">
|
|
73
|
+
<span data-part="code">7QMET2GD</span>
|
|
74
|
+
<span data-part="name" data-grow>Folding chair</span>
|
|
75
|
+
</li>
|
|
76
|
+
<li data-row="link">
|
|
77
|
+
<a href="#/search"><span data-part="name" data-grow>Search</span><kbd>/</kbd></a>
|
|
78
|
+
</li>
|
|
79
|
+
<li data-row="button">
|
|
80
|
+
<button type="button"><span>A button row</span></button>
|
|
81
|
+
</li>
|
|
82
|
+
<li data-row="stuic">
|
|
83
|
+
<button type="button" class="stuic-button">Kept</button>
|
|
84
|
+
</li>
|
|
85
|
+
</ListGroup>
|
|
86
|
+
{:else}
|
|
87
|
+
<ListGroup {title} footer="The footer">
|
|
88
|
+
{#if showRow}<li>A row</li>{/if}
|
|
89
|
+
</ListGroup>
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import ListGroup from "./ListGroup.svelte";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
width?: number;
|
|
4
|
+
mode?: "items" | "children" | "children-empty";
|
|
5
|
+
linked?: boolean;
|
|
6
|
+
showRow?: boolean;
|
|
7
|
+
style?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
};
|
|
10
|
+
declare const ListGroup: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type ListGroup = ReturnType<typeof ListGroup>;
|
|
12
|
+
export default ListGroup;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLAttributes, HTMLLiAttributes } from "svelte/elements";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
5
|
+
|
|
6
|
+
/** Renders the title as `<h1>`…`<h6>`. Semantics only — the look never changes. */
|
|
7
|
+
export type ListGroupTitleLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
8
|
+
|
|
9
|
+
export interface ListGroupSnippetArg<T = unknown> {
|
|
10
|
+
item: T;
|
|
11
|
+
index: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What `itemProps` may put on a row `<li>` — typically the `data-*` hooks a consumer's
|
|
16
|
+
* tests select on. `class` is a plain string so it can be merged after `classItem`.
|
|
17
|
+
*/
|
|
18
|
+
export type ListGroupItemProps = Omit<HTMLLiAttributes, "children" | "class"> & {
|
|
19
|
+
class?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Attributes for the `<ul>` itself (`class` is `classList`, `role` is fixed) */
|
|
23
|
+
export type ListGroupListProps = Omit<
|
|
24
|
+
HTMLAttributes<HTMLUListElement>,
|
|
25
|
+
"children" | "class" | "role"
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
export interface Props<T = unknown> extends Omit<
|
|
29
|
+
HTMLAttributes<HTMLDivElement>,
|
|
30
|
+
"children" | "title"
|
|
31
|
+
> {
|
|
32
|
+
/** The rows, in order. Data-driven form. */
|
|
33
|
+
items?: T[];
|
|
34
|
+
/**
|
|
35
|
+
* A row's content, rendered inside the `<li>` (or inside the `<a>` when `itemHref`
|
|
36
|
+
* returns a href). Without it an item is rendered as `THC`, so `items={["a", "b"]}`
|
|
37
|
+
* works as is.
|
|
38
|
+
*/
|
|
39
|
+
renderItem?: Snippet<[ListGroupSnippetArg<T>]>;
|
|
40
|
+
/**
|
|
41
|
+
* Compositional form: rendered inside the `<ul>` *instead of* `items`. Write `<li>`s;
|
|
42
|
+
* the structural CSS styles them exactly like generated rows.
|
|
43
|
+
*/
|
|
44
|
+
children?: Snippet;
|
|
45
|
+
/** Keyed `{#each}` identity. Defaults to the index. */
|
|
46
|
+
getItemId?: (item: T, index: number) => string | number;
|
|
47
|
+
/**
|
|
48
|
+
* Attributes spread onto each `<li>` — the `data-*` hooks consumers test against.
|
|
49
|
+
* A returned `class` is merged after `classItem`. `null`/`undefined` values omit the
|
|
50
|
+
* attribute. Identity comes from `getItemId`, never from here.
|
|
51
|
+
*/
|
|
52
|
+
itemProps?: (item: T, index: number) => ListGroupItemProps | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* When it returns a href, the row's content is wrapped in
|
|
55
|
+
* `<a class="stuic-list-group-item-link">` and the anchor becomes the row box
|
|
56
|
+
* (padding, hover, focus ring). Falsy → a plain row. A linked row must not contain
|
|
57
|
+
* other interactive content.
|
|
58
|
+
*/
|
|
59
|
+
itemHref?: (item: T, index: number) => string | undefined | null;
|
|
60
|
+
/** The header's start side. Labels the list (`aria-labelledby`). */
|
|
61
|
+
title?: THC;
|
|
62
|
+
/** Render the title as `<hN>` instead of a `<div>`. Semantics only. */
|
|
63
|
+
titleLevel?: ListGroupTitleLevel;
|
|
64
|
+
/**
|
|
65
|
+
* The header's end side: a sum, a count, a shortfall, an action. Never an automatic
|
|
66
|
+
* row count — say what the figure is.
|
|
67
|
+
*/
|
|
68
|
+
aside?: THC;
|
|
69
|
+
/** A line inside the box, under the rows, above a rule. */
|
|
70
|
+
footer?: THC;
|
|
71
|
+
/**
|
|
72
|
+
* Rendered in place of the `<ul>` when `items` is empty (or absent) and there are no
|
|
73
|
+
* `children`. Without it, a group with no rows renders nothing at all.
|
|
74
|
+
*/
|
|
75
|
+
empty?: THC;
|
|
76
|
+
/**
|
|
77
|
+
* Attributes for the `<ul>`. The escape hatch for labelling a list that has no
|
|
78
|
+
* `title` (`aria-label`, or `aria-labelledby` pointing at a caption outside the box)
|
|
79
|
+
* — an `aria-label` on the root `<div>` would name nothing.
|
|
80
|
+
*/
|
|
81
|
+
listProps?: ListGroupListProps;
|
|
82
|
+
/** Skip all default styling */
|
|
83
|
+
unstyled?: boolean;
|
|
84
|
+
/** Additional CSS classes for the root */
|
|
85
|
+
class?: string;
|
|
86
|
+
/** Class for the header row */
|
|
87
|
+
classHeader?: string;
|
|
88
|
+
/** Class for the title */
|
|
89
|
+
classTitle?: string;
|
|
90
|
+
/** Class for the aside */
|
|
91
|
+
classAside?: string;
|
|
92
|
+
/** Class for the `<ul>` */
|
|
93
|
+
classList?: string;
|
|
94
|
+
/** Class for every generated `<li>` */
|
|
95
|
+
classItem?: string;
|
|
96
|
+
/** Class for every generated `<a>` (`itemHref`) */
|
|
97
|
+
classItemLink?: string;
|
|
98
|
+
/** Class for the empty state */
|
|
99
|
+
classEmpty?: string;
|
|
100
|
+
/** Class for the footer */
|
|
101
|
+
classFooter?: string;
|
|
102
|
+
/** Bindable root element reference */
|
|
103
|
+
el?: HTMLDivElement;
|
|
104
|
+
}
|
|
105
|
+
</script>
|
|
106
|
+
|
|
107
|
+
<script lang="ts" generics="T = unknown">
|
|
108
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
109
|
+
import { getId } from "../../utils/get-id.js";
|
|
110
|
+
import Thc, { isTHCNotEmpty } from "../Thc/Thc.svelte";
|
|
111
|
+
|
|
112
|
+
let {
|
|
113
|
+
items,
|
|
114
|
+
renderItem,
|
|
115
|
+
children,
|
|
116
|
+
getItemId = (_item: T, index: number) => index,
|
|
117
|
+
itemProps,
|
|
118
|
+
itemHref,
|
|
119
|
+
title,
|
|
120
|
+
titleLevel,
|
|
121
|
+
aside,
|
|
122
|
+
footer,
|
|
123
|
+
empty,
|
|
124
|
+
listProps,
|
|
125
|
+
unstyled = false,
|
|
126
|
+
class: classProp,
|
|
127
|
+
classHeader: classHeaderProp,
|
|
128
|
+
classTitle: classTitleProp,
|
|
129
|
+
classAside: classAsideProp,
|
|
130
|
+
classList: classListProp,
|
|
131
|
+
classItem: classItemProp,
|
|
132
|
+
classItemLink: classItemLinkProp,
|
|
133
|
+
classEmpty: classEmptyProp,
|
|
134
|
+
classFooter: classFooterProp,
|
|
135
|
+
el = $bindable(),
|
|
136
|
+
...rest
|
|
137
|
+
}: Props<T> = $props();
|
|
138
|
+
|
|
139
|
+
const titleId = getId("stuic-list-group-title-");
|
|
140
|
+
|
|
141
|
+
let rows = $derived(items ?? []);
|
|
142
|
+
let hasRows = $derived(!!children || rows.length > 0);
|
|
143
|
+
let hasTitle = $derived(isTHCNotEmpty(title));
|
|
144
|
+
let hasAside = $derived(isTHCNotEmpty(aside));
|
|
145
|
+
let hasFooter = $derived(isTHCNotEmpty(footer));
|
|
146
|
+
let hasEmpty = $derived(isTHCNotEmpty(empty));
|
|
147
|
+
|
|
148
|
+
let _titleTag = $derived(
|
|
149
|
+
titleLevel && titleLevel >= 1 && titleLevel <= 6 ? `h${titleLevel}` : "div"
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Under `unstyled` a part keeps only the consumer's classes — `undefined` rather than
|
|
153
|
+
// `class=""` when there are none.
|
|
154
|
+
const _cls = (base: string, ...extra: (string | undefined)[]) =>
|
|
155
|
+
(unstyled ? twMerge(...extra) : twMerge(base, ...extra)) || undefined;
|
|
156
|
+
|
|
157
|
+
let _class = $derived(unstyled ? classProp : twMerge("stuic-list-group", classProp));
|
|
158
|
+
let _classHeader = $derived(_cls("stuic-list-group-header", classHeaderProp));
|
|
159
|
+
let _classTitle = $derived(_cls("stuic-list-group-title", classTitleProp));
|
|
160
|
+
let _classAside = $derived(_cls("stuic-list-group-aside", classAsideProp));
|
|
161
|
+
let _classList = $derived(_cls("stuic-list-group-list", classListProp));
|
|
162
|
+
let _classItemLink = $derived(_cls("stuic-list-group-item-link", classItemLinkProp));
|
|
163
|
+
let _classEmpty = $derived(_cls("stuic-list-group-empty", classEmptyProp));
|
|
164
|
+
let _classFooter = $derived(_cls("stuic-list-group-footer", classFooterProp));
|
|
165
|
+
|
|
166
|
+
/** `itemProps` split into the attributes to spread and the class to merge */
|
|
167
|
+
const _itemAttrs = (item: T, index: number) => {
|
|
168
|
+
const { class: itemClass, ...attrs }: ListGroupItemProps =
|
|
169
|
+
itemProps?.(item, index) ?? {};
|
|
170
|
+
return { attrs, class: _cls("stuic-list-group-item", classItemProp, itemClass) };
|
|
171
|
+
};
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
{#snippet content(item: T, index: number)}
|
|
175
|
+
{#if renderItem}
|
|
176
|
+
{@render renderItem({ item, index })}
|
|
177
|
+
{:else}
|
|
178
|
+
<Thc thc={item as THC} />
|
|
179
|
+
{/if}
|
|
180
|
+
{/snippet}
|
|
181
|
+
|
|
182
|
+
{#if hasRows || hasEmpty}
|
|
183
|
+
<div bind:this={el} class={_class} {...rest}>
|
|
184
|
+
<!-- A <div>, not a <header>: outside <main>/sectioning content (a drawer, a
|
|
185
|
+
dialog) a <header> is a `banner` landmark, one per group. -->
|
|
186
|
+
{#if hasTitle || hasAside}
|
|
187
|
+
<div class={_classHeader}>
|
|
188
|
+
{#if hasTitle}
|
|
189
|
+
<svelte:element this={_titleTag} id={titleId} class={_classTitle}>
|
|
190
|
+
<Thc thc={title!} />
|
|
191
|
+
</svelte:element>
|
|
192
|
+
{/if}
|
|
193
|
+
{#if hasAside}
|
|
194
|
+
<div class={_classAside}><Thc thc={aside!} /></div>
|
|
195
|
+
{/if}
|
|
196
|
+
</div>
|
|
197
|
+
{/if}
|
|
198
|
+
|
|
199
|
+
{#if hasRows}
|
|
200
|
+
<!-- WebKit drops list semantics from a `list-style: none` <ul> without it -->
|
|
201
|
+
<ul
|
|
202
|
+
role="list"
|
|
203
|
+
aria-labelledby={hasTitle ? titleId : undefined}
|
|
204
|
+
{...listProps}
|
|
205
|
+
class={_classList}
|
|
206
|
+
>
|
|
207
|
+
{#if children}
|
|
208
|
+
{@render children()}
|
|
209
|
+
{:else}
|
|
210
|
+
{#each rows as item, index (getItemId(item, index))}
|
|
211
|
+
{@const li = _itemAttrs(item, index)}
|
|
212
|
+
{@const href = itemHref?.(item, index)}
|
|
213
|
+
<li {...li.attrs} class={li.class}>
|
|
214
|
+
{#if href}
|
|
215
|
+
<a {href} class={_classItemLink}>{@render content(item, index)}</a>
|
|
216
|
+
{:else}
|
|
217
|
+
{@render content(item, index)}
|
|
218
|
+
{/if}
|
|
219
|
+
</li>
|
|
220
|
+
{/each}
|
|
221
|
+
{/if}
|
|
222
|
+
</ul>
|
|
223
|
+
{:else}
|
|
224
|
+
<div class={_classEmpty}><Thc thc={empty!} /></div>
|
|
225
|
+
{/if}
|
|
226
|
+
|
|
227
|
+
<!-- A <div>, not a <footer>: same landmark reason as the header -->
|
|
228
|
+
{#if hasFooter}
|
|
229
|
+
<div class={_classFooter}><Thc thc={footer!} /></div>
|
|
230
|
+
{/if}
|
|
231
|
+
</div>
|
|
232
|
+
{/if}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { HTMLAttributes, HTMLLiAttributes } from "svelte/elements";
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
4
|
+
/** Renders the title as `<h1>`…`<h6>`. Semantics only — the look never changes. */
|
|
5
|
+
export type ListGroupTitleLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
6
|
+
export interface ListGroupSnippetArg<T = unknown> {
|
|
7
|
+
item: T;
|
|
8
|
+
index: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* What `itemProps` may put on a row `<li>` — typically the `data-*` hooks a consumer's
|
|
12
|
+
* tests select on. `class` is a plain string so it can be merged after `classItem`.
|
|
13
|
+
*/
|
|
14
|
+
export type ListGroupItemProps = Omit<HTMLLiAttributes, "children" | "class"> & {
|
|
15
|
+
class?: string;
|
|
16
|
+
};
|
|
17
|
+
/** Attributes for the `<ul>` itself (`class` is `classList`, `role` is fixed) */
|
|
18
|
+
export type ListGroupListProps = Omit<HTMLAttributes<HTMLUListElement>, "children" | "class" | "role">;
|
|
19
|
+
export interface Props<T = unknown> extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "title"> {
|
|
20
|
+
/** The rows, in order. Data-driven form. */
|
|
21
|
+
items?: T[];
|
|
22
|
+
/**
|
|
23
|
+
* A row's content, rendered inside the `<li>` (or inside the `<a>` when `itemHref`
|
|
24
|
+
* returns a href). Without it an item is rendered as `THC`, so `items={["a", "b"]}`
|
|
25
|
+
* works as is.
|
|
26
|
+
*/
|
|
27
|
+
renderItem?: Snippet<[ListGroupSnippetArg<T>]>;
|
|
28
|
+
/**
|
|
29
|
+
* Compositional form: rendered inside the `<ul>` *instead of* `items`. Write `<li>`s;
|
|
30
|
+
* the structural CSS styles them exactly like generated rows.
|
|
31
|
+
*/
|
|
32
|
+
children?: Snippet;
|
|
33
|
+
/** Keyed `{#each}` identity. Defaults to the index. */
|
|
34
|
+
getItemId?: (item: T, index: number) => string | number;
|
|
35
|
+
/**
|
|
36
|
+
* Attributes spread onto each `<li>` — the `data-*` hooks consumers test against.
|
|
37
|
+
* A returned `class` is merged after `classItem`. `null`/`undefined` values omit the
|
|
38
|
+
* attribute. Identity comes from `getItemId`, never from here.
|
|
39
|
+
*/
|
|
40
|
+
itemProps?: (item: T, index: number) => ListGroupItemProps | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* When it returns a href, the row's content is wrapped in
|
|
43
|
+
* `<a class="stuic-list-group-item-link">` and the anchor becomes the row box
|
|
44
|
+
* (padding, hover, focus ring). Falsy → a plain row. A linked row must not contain
|
|
45
|
+
* other interactive content.
|
|
46
|
+
*/
|
|
47
|
+
itemHref?: (item: T, index: number) => string | undefined | null;
|
|
48
|
+
/** The header's start side. Labels the list (`aria-labelledby`). */
|
|
49
|
+
title?: THC;
|
|
50
|
+
/** Render the title as `<hN>` instead of a `<div>`. Semantics only. */
|
|
51
|
+
titleLevel?: ListGroupTitleLevel;
|
|
52
|
+
/**
|
|
53
|
+
* The header's end side: a sum, a count, a shortfall, an action. Never an automatic
|
|
54
|
+
* row count — say what the figure is.
|
|
55
|
+
*/
|
|
56
|
+
aside?: THC;
|
|
57
|
+
/** A line inside the box, under the rows, above a rule. */
|
|
58
|
+
footer?: THC;
|
|
59
|
+
/**
|
|
60
|
+
* Rendered in place of the `<ul>` when `items` is empty (or absent) and there are no
|
|
61
|
+
* `children`. Without it, a group with no rows renders nothing at all.
|
|
62
|
+
*/
|
|
63
|
+
empty?: THC;
|
|
64
|
+
/**
|
|
65
|
+
* Attributes for the `<ul>`. The escape hatch for labelling a list that has no
|
|
66
|
+
* `title` (`aria-label`, or `aria-labelledby` pointing at a caption outside the box)
|
|
67
|
+
* — an `aria-label` on the root `<div>` would name nothing.
|
|
68
|
+
*/
|
|
69
|
+
listProps?: ListGroupListProps;
|
|
70
|
+
/** Skip all default styling */
|
|
71
|
+
unstyled?: boolean;
|
|
72
|
+
/** Additional CSS classes for the root */
|
|
73
|
+
class?: string;
|
|
74
|
+
/** Class for the header row */
|
|
75
|
+
classHeader?: string;
|
|
76
|
+
/** Class for the title */
|
|
77
|
+
classTitle?: string;
|
|
78
|
+
/** Class for the aside */
|
|
79
|
+
classAside?: string;
|
|
80
|
+
/** Class for the `<ul>` */
|
|
81
|
+
classList?: string;
|
|
82
|
+
/** Class for every generated `<li>` */
|
|
83
|
+
classItem?: string;
|
|
84
|
+
/** Class for every generated `<a>` (`itemHref`) */
|
|
85
|
+
classItemLink?: string;
|
|
86
|
+
/** Class for the empty state */
|
|
87
|
+
classEmpty?: string;
|
|
88
|
+
/** Class for the footer */
|
|
89
|
+
classFooter?: string;
|
|
90
|
+
/** Bindable root element reference */
|
|
91
|
+
el?: HTMLDivElement;
|
|
92
|
+
}
|
|
93
|
+
declare function $$render<T = unknown>(): {
|
|
94
|
+
props: Props<T>;
|
|
95
|
+
exports: {};
|
|
96
|
+
bindings: "el";
|
|
97
|
+
slots: {};
|
|
98
|
+
events: {};
|
|
99
|
+
};
|
|
100
|
+
declare class __sveltets_Render<T = unknown> {
|
|
101
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
102
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
103
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
104
|
+
bindings(): "el";
|
|
105
|
+
exports(): {};
|
|
106
|
+
}
|
|
107
|
+
interface $$IsomorphicComponent {
|
|
108
|
+
new <T = unknown>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
109
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
110
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
111
|
+
<T = unknown>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
112
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
113
|
+
}
|
|
114
|
+
declare const ListGroup: $$IsomorphicComponent;
|
|
115
|
+
type ListGroup<T = unknown> = InstanceType<typeof ListGroup<T>>;
|
|
116
|
+
export default ListGroup;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# ListGroup
|
|
2
|
+
|
|
3
|
+
A rounded, bordered box of rows split by hairlines, with an optional header (a title on the
|
|
4
|
+
start side, a figure on the end side) and an optional footer line — the block a back office
|
|
5
|
+
keeps drawing by hand. Each row is **one wrapping flex line** of parts: a code, a name that
|
|
6
|
+
takes the leftover space, a run of facts. Nothing lines up across rows, on purpose: a row
|
|
7
|
+
that carries an extra part does not push its siblings, and in a narrow drawer the trailing
|
|
8
|
+
facts drop under the name one by one.
|
|
9
|
+
|
|
10
|
+
Renders `<div>` › optional header `<div>` › `<ul role="list">` › `<li>` per row › optional
|
|
11
|
+
footer `<div>`. The list is labelled by the title (`aria-labelledby`), so a screen reader
|
|
12
|
+
announces "Loose items, list, 8 items".
|
|
13
|
+
|
|
14
|
+
Not a `DataTable` (the rows share no columns — no alignment, sorting, paging or selection),
|
|
15
|
+
not a `DescriptionList` (each row is a different thing, not a property of one thing), not a
|
|
16
|
+
`Card` (no shadow, no padded body). `ListItemButton` is not its row.
|
|
17
|
+
|
|
18
|
+
## Props
|
|
19
|
+
|
|
20
|
+
| Prop | Type | Default | Description |
|
|
21
|
+
| --------------- | --------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
22
|
+
| `items` | `T[]` | - | The rows, in order. Data-driven form |
|
|
23
|
+
| `renderItem` | `Snippet<[ListGroupSnippetArg<T>]>` | - | A row's content, inside the `<li>` (or inside the `<a>` with `itemHref`). Without it an item renders as `THC` |
|
|
24
|
+
| `children` | `Snippet` | - | Compositional form: `<li>`s rendered inside the `<ul>` _instead of_ `items` |
|
|
25
|
+
| `getItemId` | `(item: T, index: number) => string \| number` | the index | Keyed `{#each}` identity (same name and default as `DataTable`'s `getRowId`) |
|
|
26
|
+
| `itemProps` | `(item: T, index: number) => ListGroupItemProps` | - | Attributes spread onto each `<li>` — the `data-*` hooks tests select on. A returned `class` merges after `classItem` |
|
|
27
|
+
| `itemHref` | `(item: T, index: number) => string \| null \| undefined` | - | When it returns a href, the row content is wrapped in `<a class="stuic-list-group-item-link">`, which becomes the row box |
|
|
28
|
+
| `title` | `THC` | - | The header's start side; labels the list |
|
|
29
|
+
| `titleLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | - | Render the title as `<hN>` instead of a `<div>`. Semantics only — the look never changes |
|
|
30
|
+
| `aside` | `THC` | - | The header's end side: a sum, a shortfall, an action. Pushed to the inline end, `tabular-nums` |
|
|
31
|
+
| `footer` | `THC` | - | A line inside the box, under the rows, above a rule |
|
|
32
|
+
| `empty` | `THC` | - | Rendered **in place of** the `<ul>` when there are no rows. Without it, a group with no rows renders nothing at all |
|
|
33
|
+
| `listProps` | `ListGroupListProps` | - | Attributes for the `<ul>` — `aria-label`, or `aria-labelledby` pointing at a caption outside the box |
|
|
34
|
+
| `unstyled` | `boolean` | `false` | Skip all default styling (no `stuic-*` classes) |
|
|
35
|
+
| `class` | `string` | - | Additional CSS classes on the root (merged via twMerge) |
|
|
36
|
+
| `classHeader` | `string` | - | The header row |
|
|
37
|
+
| `classTitle` | `string` | - | The title |
|
|
38
|
+
| `classAside` | `string` | - | The aside |
|
|
39
|
+
| `classList` | `string` | - | The `<ul>` |
|
|
40
|
+
| `classItem` | `string` | - | Every generated `<li>` |
|
|
41
|
+
| `classItemLink` | `string` | - | Every generated `<a>` (`itemHref`) |
|
|
42
|
+
| `classEmpty` | `string` | - | The empty state |
|
|
43
|
+
| `classFooter` | `string` | - | The footer |
|
|
44
|
+
| `el` | `HTMLDivElement` | - | Root element reference (bindable) |
|
|
45
|
+
|
|
46
|
+
Any other attribute (`data-*`, `style`, `id`, …) goes to the root `<div>`. `title` is
|
|
47
|
+
omitted from the root's HTML attributes because the prop is a `THC`, not the tooltip — as in
|
|
48
|
+
`Card`. An `aria-label` on the root would name nothing (a `<div>` has no role); label a
|
|
49
|
+
title-less list through `listProps`.
|
|
50
|
+
|
|
51
|
+
The component is generic (`T = unknown`), so `renderItem`, `getItemId`, `itemProps` and
|
|
52
|
+
`itemHref` are typed by your `items`.
|
|
53
|
+
|
|
54
|
+
### Types
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface ListGroupSnippetArg<T> {
|
|
58
|
+
item: T;
|
|
59
|
+
index: number;
|
|
60
|
+
}
|
|
61
|
+
type ListGroupTitleLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
62
|
+
/** `class` is a string, merged after `classItem` */
|
|
63
|
+
type ListGroupItemProps = Omit<HTMLLiAttributes, "children" | "class"> & {
|
|
64
|
+
class?: string;
|
|
65
|
+
};
|
|
66
|
+
type ListGroupListProps = Omit<
|
|
67
|
+
HTMLAttributes<HTMLUListElement>,
|
|
68
|
+
"children" | "class" | "role"
|
|
69
|
+
>;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
### A kit group (the reference shape)
|
|
75
|
+
|
|
76
|
+
```svelte
|
|
77
|
+
<script lang="ts">
|
|
78
|
+
import { ListGroup } from "@marianmeres/stuic";
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<ListGroup
|
|
82
|
+
class="text-sm"
|
|
83
|
+
items={group.lines}
|
|
84
|
+
getItemId={(line) => line.booking_id}
|
|
85
|
+
itemProps={(line) => ({
|
|
86
|
+
"data-line": "",
|
|
87
|
+
"data-booking-id": line.booking_id,
|
|
88
|
+
"data-short": line.short || null,
|
|
89
|
+
})}
|
|
90
|
+
titleLevel={3}
|
|
91
|
+
data-kit-group
|
|
92
|
+
data-kit={group.key}
|
|
93
|
+
>
|
|
94
|
+
{#snippet title()}
|
|
95
|
+
{group.label}
|
|
96
|
+
{#if group.optional}
|
|
97
|
+
<span class="text-muted-foreground font-normal">optional</span>
|
|
98
|
+
{/if}
|
|
99
|
+
{/snippet}
|
|
100
|
+
{#snippet aside()}
|
|
101
|
+
<span>{group.requested} pc</span>
|
|
102
|
+
{#if group.short > 0}
|
|
103
|
+
<span class="text-destructive">−{group.short}</span>
|
|
104
|
+
{/if}
|
|
105
|
+
{/snippet}
|
|
106
|
+
{#snippet renderItem({ item: line })}
|
|
107
|
+
<span class="font-mono">{line.equipment_code}</span>
|
|
108
|
+
<span data-grow>{line.equipment_name}</span>
|
|
109
|
+
<span>×{line.requested}</span>
|
|
110
|
+
<span class="text-muted-foreground">picked {line.picked}/{line.requested}</span>
|
|
111
|
+
<span class="text-muted-foreground">{line.location}</span>
|
|
112
|
+
{/snippet}
|
|
113
|
+
</ListGroup>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
What is left on the consumer side is content styling only — mono, muted, a state colour.
|
|
117
|
+
No `flex`, `gap`, `px-3 py-2`, `divide-y` or `rounded-md border`.
|
|
118
|
+
|
|
119
|
+
### Linked rows and a footer
|
|
120
|
+
|
|
121
|
+
```svelte
|
|
122
|
+
<ListGroup
|
|
123
|
+
items={hits}
|
|
124
|
+
getItemId={(hit) => hit.id}
|
|
125
|
+
itemHref={(hit) => `#/find/${hit.kind}/${hit.id}`}
|
|
126
|
+
title="Equipment"
|
|
127
|
+
aside="3 of 15"
|
|
128
|
+
footer="+12 more — narrow the search."
|
|
129
|
+
>
|
|
130
|
+
{#snippet renderItem({ item: hit })}
|
|
131
|
+
<span class="font-mono">{hit.id}</span>
|
|
132
|
+
<span data-grow>{hit.label}</span>
|
|
133
|
+
<span class="text-muted-foreground">{hit.meta}</span>
|
|
134
|
+
{/snippet}
|
|
135
|
+
</ListGroup>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### An empty state
|
|
139
|
+
|
|
140
|
+
```svelte
|
|
141
|
+
<ListGroup items={crew} title="Crew" empty="Nobody and nothing booked for this job yet.">
|
|
142
|
+
{#snippet renderItem({ item })}…{/snippet}
|
|
143
|
+
</ListGroup>
|
|
144
|
+
|
|
145
|
+
<!-- or any THC, e.g. an EmptyState in a snippet -->
|
|
146
|
+
<ListGroup items={scanned} title="Scanned">
|
|
147
|
+
{#snippet empty()}
|
|
148
|
+
<EmptyState size="sm" title="Nothing scanned yet" />
|
|
149
|
+
{/snippet}
|
|
150
|
+
{#snippet renderItem({ item })}…{/snippet}
|
|
151
|
+
</ListGroup>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Plain strings
|
|
155
|
+
|
|
156
|
+
```svelte
|
|
157
|
+
<ListGroup items={["Alpha", "Beta", "Gamma"]} title="Strings" />
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### The `children` form
|
|
161
|
+
|
|
162
|
+
For rows that don't come from one array, or a static list. The CSS selects structurally, so
|
|
163
|
+
hand-written rows render exactly like generated ones — including a row whose only child is
|
|
164
|
+
a link or a button:
|
|
165
|
+
|
|
166
|
+
```svelte
|
|
167
|
+
<ListGroup title="Shortcuts" class="text-sm">
|
|
168
|
+
<li><span data-grow>Open the scan hub</span><kbd>S</kbd></li>
|
|
169
|
+
<li><a href="#/find/search"><span data-grow>Search</span><kbd>/</kbd></a></li>
|
|
170
|
+
<li><button type="button" onclick={toggle}><span data-grow>Toggle</span></button></li>
|
|
171
|
+
</ListGroup>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`empty` is an `items` feature: the component cannot count what a snippet renders, so
|
|
175
|
+
`children` always renders the `<ul>` it is given. (A `children` list whose rows all render
|
|
176
|
+
away collapses — no stray rule under the header.)
|
|
177
|
+
|
|
178
|
+
## The row
|
|
179
|
+
|
|
180
|
+
The row box is the `<li>` — or, on a linked row, the `<a>` (or a `<button>`) that is its only
|
|
181
|
+
child. It is a wrapping flex line: `column-gap` / `row-gap`, `align-items` from a token
|
|
182
|
+
(`center` by default; `baseline` for rows that align on text rather than on pills and
|
|
183
|
+
buttons), and `overflow-wrap: anywhere`, so a long dotted path such as
|
|
184
|
+
`hall-a.rack-a2.shelf-a2-2.bin` breaks instead of pushing a narrow drawer sideways.
|
|
185
|
+
|
|
186
|
+
### `data-grow` — the part that absorbs the slack
|
|
187
|
+
|
|
188
|
+
Put `data-grow` on the one part that should take the leftover space and push everything
|
|
189
|
+
after it to the inline end (the name). It gets `flex: 1 1 var(--stuic-list-group-grow-basis)`
|
|
190
|
+
(10rem), `min-width: 0` and a one-line truncation. The **basis**, not a `min-width`, carries
|
|
191
|
+
"claim 10rem before anything wraps": flex line-breaking uses the basis, so the trailing parts
|
|
192
|
+
wrap to the next line once the name would get less than that — and in a container narrower
|
|
193
|
+
than 10rem the name still shrinks and truncates instead of overflowing. A row whose primary
|
|
194
|
+
part should _wrap_ rather than truncate uses `class="flex-1"` and skips `data-grow`.
|
|
195
|
+
|
|
196
|
+
### Second lines
|
|
197
|
+
|
|
198
|
+
A child with `basis-full` (or `w-full`) forces a new line inside the same row; the row gap
|
|
199
|
+
spaces it. No attribute is needed:
|
|
200
|
+
|
|
201
|
+
```svelte
|
|
202
|
+
{#snippet renderItem({ item: c })}
|
|
203
|
+
<span data-grow class="font-medium">{c.name}</span>
|
|
204
|
+
<span class="text-muted-foreground">{c.role}</span>
|
|
205
|
+
{#if c.clash}
|
|
206
|
+
<span class="text-destructive basis-full">{c.clash}</span>
|
|
207
|
+
{/if}
|
|
208
|
+
{/snippet}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Linked rows
|
|
212
|
+
|
|
213
|
+
`itemHref` (or a hand-written `<li><a href>…</a></li>`) makes the **whole row** the anchor:
|
|
214
|
+
the hit area, the hover background, and an inset focus ring that stays visible inside the
|
|
215
|
+
clipped box, first and last row included. The anchor's accessible name is the whole row's
|
|
216
|
+
text. **A linked row must not contain other interactive content** — a row that needs both a
|
|
217
|
+
link and a button puts the `<a>` on its name in `renderItem`. There is deliberately no
|
|
218
|
+
`onItemClick`: a real link beats a click handler (tab order, `Enter`, middle/cmd-click,
|
|
219
|
+
"copy link address"). A `<button>` alone in a hand-written row gets the same row styling.
|
|
220
|
+
|
|
221
|
+
A child that carries a stuic component class — a lone `Button`, `Pill` or `ListItemButton`
|
|
222
|
+
— is **not** flattened into the row: it keeps its own box, and the row keeps its padding.
|
|
223
|
+
|
|
224
|
+
## Empty
|
|
225
|
+
|
|
226
|
+
- No rows and no `empty` → **nothing renders**, header included (the same rule as
|
|
227
|
+
`DescriptionList` and `Timeline`), so no `{#if items.length}` wrapper is needed.
|
|
228
|
+
- No rows with `empty` → header, the empty part, footer. The empty part **replaces** the
|
|
229
|
+
`<ul>` rather than sitting in an `<li>`: a list whose only item says there are no items
|
|
230
|
+
announces "list, 1 item".
|
|
231
|
+
|
|
232
|
+
## CSS Variables
|
|
233
|
+
|
|
234
|
+
| Variable | Default | Description |
|
|
235
|
+
| -------------------------------------- | ------------------------------------- | ------------------------------------------------------------------ |
|
|
236
|
+
| `--stuic-list-group-bg` | `transparent` | Box background (the surface behind shows through by default) |
|
|
237
|
+
| `--stuic-list-group-border-color` | `var(--stuic-color-border)` | Box border color |
|
|
238
|
+
| `--stuic-list-group-border-width` | `var(--stuic-border-width)` | Box border width (not declared — tier fallback at the usage site) |
|
|
239
|
+
| `--stuic-list-group-radius` | `var(--stuic-radius-container)` | Box radius (not declared — tier fallback at the usage site) |
|
|
240
|
+
| `--stuic-list-group-rule-color` | `var(--stuic-color-border)` | Hairlines between header, rows and footer |
|
|
241
|
+
| `--stuic-list-group-rule-width` | `1px` | Hairline width |
|
|
242
|
+
| `--stuic-list-group-item-padding-x` | `0.75rem` | Row inline padding — also the header, empty and footer padding |
|
|
243
|
+
| `--stuic-list-group-item-padding-y` | `0.5rem` | Row block padding — also the default header, empty, footer padding |
|
|
244
|
+
| `--stuic-list-group-item-gap-x` | `0.75rem` | Gap between row parts (and header parts) |
|
|
245
|
+
| `--stuic-list-group-item-gap-y` | `0.25rem` | Gap between wrapped lines of a row |
|
|
246
|
+
| `--stuic-list-group-item-align` | `center` | Row `align-items` (`baseline` for text-aligned rows) |
|
|
247
|
+
| `--stuic-list-group-grow-basis` | `10rem` | What a `data-grow` part claims before its neighbours wrap |
|
|
248
|
+
| `--stuic-list-group-item-bg-hover` | `var(--stuic-color-muted)` | Linked row hover background |
|
|
249
|
+
| `--stuic-list-group-item-ring-width` | `2px` | Linked row focus ring width (drawn inset) |
|
|
250
|
+
| `--stuic-list-group-item-ring-color` | `var(--stuic-color-ring)` | Linked row focus ring color |
|
|
251
|
+
| `--stuic-list-group-transition` | `var(--stuic-transition)` | Hover transition (not declared — tier fallback at the usage site) |
|
|
252
|
+
| `--stuic-list-group-header-padding-y` | `--stuic-list-group-item-padding-y` | Header block padding (not declared — fallback at the usage site) |
|
|
253
|
+
| `--stuic-list-group-header-bg` | `transparent` | Header background |
|
|
254
|
+
| `--stuic-list-group-title-font-weight` | `var(--font-weight-medium)` | Title weight |
|
|
255
|
+
| `--stuic-list-group-title-text` | `var(--stuic-color-foreground)` | Title color |
|
|
256
|
+
| `--stuic-list-group-aside-text` | `var(--stuic-color-muted-foreground)` | Aside color |
|
|
257
|
+
| `--stuic-list-group-empty-text` | `var(--stuic-color-muted-foreground)` | Empty state color |
|
|
258
|
+
| `--stuic-list-group-footer-text` | `var(--stuic-color-muted-foreground)` | Footer color |
|
|
259
|
+
|
|
260
|
+
**No part declares a font size.** Rows, title, aside, empty and footer all inherit, so
|
|
261
|
+
`class="text-sm"` on the root scales the whole box, and no part can default below the size
|
|
262
|
+
you set. The title differs only in weight, the aside only in colour.
|
|
263
|
+
|
|
264
|
+
**The header shares the rows' inline padding by construction** — there is no header
|
|
265
|
+
`padding-x` token — so the header text and the row text always start at the same x.
|
|
266
|
+
`--stuic-list-group-header-padding-y` is read as a fallback argument, so a scoped
|
|
267
|
+
`--stuic-list-group-item-padding-y` on one group reaches its header too.
|
|
268
|
+
|
|
269
|
+
The rules have their own width token (like `DescriptionList` and `Separator`), so a theme
|
|
270
|
+
that zeroes box borders keeps its hairlines.
|
|
271
|
+
|
|
272
|
+
A denser, muted-header variant is a few overrides:
|
|
273
|
+
|
|
274
|
+
```svelte
|
|
275
|
+
<ListGroup
|
|
276
|
+
style="--stuic-list-group-item-padding-x: 1rem;
|
|
277
|
+
--stuic-list-group-item-padding-y: 0.75rem;
|
|
278
|
+
--stuic-list-group-bg: var(--stuic-color-background);
|
|
279
|
+
--stuic-list-group-header-bg: var(--stuic-color-muted);
|
|
280
|
+
--stuic-list-group-title-font-weight: var(--font-weight-semibold);"
|
|
281
|
+
{items}
|
|
282
|
+
title="Before the job"
|
|
283
|
+
/>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Accessibility
|
|
287
|
+
|
|
288
|
+
- The root is a `<div>`, not a `<section>`: a labelled `<section>` is a `region` landmark, and
|
|
289
|
+
a drawer holding six groups would add six. The header and footer are `<div>`s for the same
|
|
290
|
+
reason — outside `<main>` or sectioning content (a drawer, a dialog), a `<header>` is a
|
|
291
|
+
`banner` landmark and a `<footer>` a `contentinfo` one.
|
|
292
|
+
- `role="list"` is set explicitly on the `<ul>`: WebKit/VoiceOver drops list semantics from a
|
|
293
|
+
`list-style: none` list.
|
|
294
|
+
- The title labels the list via `aria-labelledby`. With `titleLevel`, it is also a heading.
|
|
295
|
+
- `unstyled` removes the classes but keeps `role`, `aria-labelledby` and the `itemHref`
|
|
296
|
+
anchor — they are semantics, not styling.
|
|
297
|
+
- The box is `overflow: clip` (so header and hover backgrounds follow the rounded corners).
|
|
298
|
+
`clip` creates no scroll container, so sticky content still sticks; stuic's tooltip and
|
|
299
|
+
popover are `position: fixed` and are not clipped.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
LIST GROUP COMPONENT TOKENS
|
|
3
|
+
Override globally: :root { --stuic-list-group-item-padding-x: 1rem; }
|
|
4
|
+
Override locally: <ListGroup style="--stuic-list-group-header-bg: var(--stuic-color-muted);">
|
|
5
|
+
============================================================================ */
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
/* Box — radius and border width are read with a tier fallback at the usage site
|
|
9
|
+
(`--stuic-list-group-radius` → `--stuic-radius-container`,
|
|
10
|
+
`--stuic-list-group-border-width` → `--stuic-border-width`), never declared here. */
|
|
11
|
+
--stuic-list-group-bg: transparent;
|
|
12
|
+
--stuic-list-group-border-color: var(--stuic-color-border);
|
|
13
|
+
|
|
14
|
+
/* Rules between header, rows and footer. The component's own width (like
|
|
15
|
+
DescriptionList and Separator), so a theme that zeroes box borders keeps them. */
|
|
16
|
+
--stuic-list-group-rule-color: var(--stuic-color-border);
|
|
17
|
+
--stuic-list-group-rule-width: 1px;
|
|
18
|
+
|
|
19
|
+
/* Rows */
|
|
20
|
+
--stuic-list-group-item-padding-x: 0.75rem;
|
|
21
|
+
--stuic-list-group-item-padding-y: 0.5rem;
|
|
22
|
+
--stuic-list-group-item-gap-x: 0.75rem;
|
|
23
|
+
--stuic-list-group-item-gap-y: 0.25rem;
|
|
24
|
+
--stuic-list-group-item-align: center;
|
|
25
|
+
--stuic-list-group-grow-basis: 10rem; /* what `data-grow` claims before neighbours wrap */
|
|
26
|
+
|
|
27
|
+
/* Linked rows */
|
|
28
|
+
--stuic-list-group-item-bg-hover: var(--stuic-color-muted);
|
|
29
|
+
--stuic-list-group-item-ring-width: 2px;
|
|
30
|
+
--stuic-list-group-item-ring-color: var(--stuic-color-ring);
|
|
31
|
+
|
|
32
|
+
/* Header. `--stuic-list-group-header-padding-y` is deliberately NOT declared: it is
|
|
33
|
+
read as a fallback argument at the usage site, so a scoped override of
|
|
34
|
+
`--stuic-list-group-item-padding-y` reaches the header too (declared here, it would
|
|
35
|
+
resolve once at :root and ignore every scoped override). */
|
|
36
|
+
--stuic-list-group-header-bg: transparent;
|
|
37
|
+
--stuic-list-group-title-font-weight: var(--font-weight-medium);
|
|
38
|
+
--stuic-list-group-title-text: var(--stuic-color-foreground);
|
|
39
|
+
--stuic-list-group-aside-text: var(--stuic-color-muted-foreground);
|
|
40
|
+
|
|
41
|
+
/* Empty + footer */
|
|
42
|
+
--stuic-list-group-empty-text: var(--stuic-color-muted-foreground);
|
|
43
|
+
--stuic-list-group-footer-text: var(--stuic-color-muted-foreground);
|
|
44
|
+
|
|
45
|
+
/* Typography: no part declares a size — everything inherits, so `class="text-sm"` on
|
|
46
|
+
the root scales the whole box. */
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@layer components {
|
|
50
|
+
/* ============================================================================
|
|
51
|
+
BOX
|
|
52
|
+
============================================================================ */
|
|
53
|
+
|
|
54
|
+
.stuic-list-group {
|
|
55
|
+
min-width: 0;
|
|
56
|
+
background: var(--stuic-list-group-bg);
|
|
57
|
+
border: var(--stuic-list-group-border-width, var(--stuic-border-width)) solid
|
|
58
|
+
var(--stuic-list-group-border-color);
|
|
59
|
+
border-radius: var(--stuic-list-group-radius, var(--stuic-radius-container));
|
|
60
|
+
/* The header background and a linked row's hover background must follow the
|
|
61
|
+
rounded corners. `clip`, not `hidden`: it creates no scroll container, so a
|
|
62
|
+
sticky element inside still sticks (`hidden` stays as the fallback for browsers
|
|
63
|
+
without `clip`). stuic's tooltip and popover are `position: fixed`, which an
|
|
64
|
+
overflow-clipped ancestor does not clip. */
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
overflow: clip;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* One rule between consecutive parts (header › list | empty › footer). On the part
|
|
70
|
+
that FOLLOWS rather than under the header, so a header over a `children` list that
|
|
71
|
+
rendered no rows (hidden below) does not draw a second line against the box. */
|
|
72
|
+
.stuic-list-group > * + * {
|
|
73
|
+
border-top: var(--stuic-list-group-rule-width) solid
|
|
74
|
+
var(--stuic-list-group-rule-color);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ============================================================================
|
|
78
|
+
HEADER — shares the rows' inline padding BY CONSTRUCTION (not a separate
|
|
79
|
+
token), so the header text and the row text always start at the same x.
|
|
80
|
+
============================================================================ */
|
|
81
|
+
|
|
82
|
+
.stuic-list-group-header {
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-wrap: wrap;
|
|
85
|
+
align-items: center;
|
|
86
|
+
column-gap: var(--stuic-list-group-item-gap-x);
|
|
87
|
+
row-gap: var(--stuic-list-group-item-gap-y);
|
|
88
|
+
padding-block: var(
|
|
89
|
+
--stuic-list-group-header-padding-y,
|
|
90
|
+
var(--stuic-list-group-item-padding-y)
|
|
91
|
+
);
|
|
92
|
+
padding-inline: var(--stuic-list-group-item-padding-x);
|
|
93
|
+
min-width: 0;
|
|
94
|
+
overflow-wrap: anywhere;
|
|
95
|
+
background: var(--stuic-list-group-header-bg);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.stuic-list-group-title {
|
|
99
|
+
/* `titleLevel` is semantics only: undo the UA heading margin and size. `font`
|
|
100
|
+
(not `font-size`) so the size is inherited, never declared. */
|
|
101
|
+
margin: 0;
|
|
102
|
+
font: inherit;
|
|
103
|
+
font-weight: var(--stuic-list-group-title-font-weight);
|
|
104
|
+
color: var(--stuic-list-group-title-text);
|
|
105
|
+
min-width: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.stuic-list-group-aside {
|
|
109
|
+
margin-inline-start: auto;
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-wrap: wrap;
|
|
112
|
+
align-items: center;
|
|
113
|
+
column-gap: var(--stuic-list-group-item-gap-x);
|
|
114
|
+
row-gap: var(--stuic-list-group-item-gap-y);
|
|
115
|
+
color: var(--stuic-list-group-aside-text);
|
|
116
|
+
font-variant-numeric: tabular-nums;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ============================================================================
|
|
120
|
+
LIST + ROWS — selected STRUCTURALLY (`> li`), not by class, so a hand-written
|
|
121
|
+
`children` row renders exactly like a generated one. The per-part classes exist
|
|
122
|
+
only as merge targets for `classItem` / `classItemLink`.
|
|
123
|
+
|
|
124
|
+
The ROW BOX is the <li>, or — when its only child is a link or a button — that
|
|
125
|
+
child. A child carrying a stuic component class (a `Button`, a `Pill`, a
|
|
126
|
+
`ListItemButton`) is left alone: it has its own box, and flattening it into the
|
|
127
|
+
row would strip its background and border. The generated `itemHref` anchor is
|
|
128
|
+
matched by its own class for the same reason.
|
|
129
|
+
============================================================================ */
|
|
130
|
+
|
|
131
|
+
.stuic-list-group-list {
|
|
132
|
+
margin: 0;
|
|
133
|
+
padding: 0;
|
|
134
|
+
list-style: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* A `children` list whose rows all rendered away (Svelte's anchors are comments) */
|
|
138
|
+
.stuic-list-group-list:empty {
|
|
139
|
+
display: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.stuic-list-group-list > li,
|
|
143
|
+
.stuic-list-group-list
|
|
144
|
+
> li
|
|
145
|
+
> :is(
|
|
146
|
+
.stuic-list-group-item-link,
|
|
147
|
+
:is(a, button):only-child:not([class^="stuic-"], [class*=" stuic-"])
|
|
148
|
+
) {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-wrap: wrap;
|
|
151
|
+
align-items: var(--stuic-list-group-item-align);
|
|
152
|
+
column-gap: var(--stuic-list-group-item-gap-x);
|
|
153
|
+
row-gap: var(--stuic-list-group-item-gap-y);
|
|
154
|
+
padding-block: var(--stuic-list-group-item-padding-y);
|
|
155
|
+
padding-inline: var(--stuic-list-group-item-padding-x);
|
|
156
|
+
min-width: 0;
|
|
157
|
+
/* A dotted location path ("hall-a.rack-a2.shelf-a2-2.bin") is one unbreakable
|
|
158
|
+
word. Without this its min-content pushes a 360px drawer sideways. `anywhere`
|
|
159
|
+
and not `break-word`: only `anywhere` counts in min-content sizing. */
|
|
160
|
+
overflow-wrap: anywhere;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.stuic-list-group-list > li + li {
|
|
164
|
+
border-top: var(--stuic-list-group-rule-width) solid
|
|
165
|
+
var(--stuic-list-group-rule-color);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* A row whose only child is a link or a button hands the box to that child */
|
|
169
|
+
.stuic-list-group-list
|
|
170
|
+
> li:has(
|
|
171
|
+
> :is(
|
|
172
|
+
.stuic-list-group-item-link,
|
|
173
|
+
:is(a, button):only-child:not([class^="stuic-"], [class*=" stuic-"])
|
|
174
|
+
)
|
|
175
|
+
) {
|
|
176
|
+
display: block;
|
|
177
|
+
padding: 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ============================================================================
|
|
181
|
+
LINKED ROWS
|
|
182
|
+
============================================================================ */
|
|
183
|
+
|
|
184
|
+
.stuic-list-group-list
|
|
185
|
+
> li
|
|
186
|
+
> :is(
|
|
187
|
+
.stuic-list-group-item-link,
|
|
188
|
+
:is(a, button):only-child:not([class^="stuic-"], [class*=" stuic-"])
|
|
189
|
+
) {
|
|
190
|
+
/* a <button> is shrink-to-fit even as a flex container */
|
|
191
|
+
width: 100%;
|
|
192
|
+
box-sizing: border-box;
|
|
193
|
+
margin: 0;
|
|
194
|
+
color: inherit;
|
|
195
|
+
/* for <button> rows */
|
|
196
|
+
font: inherit;
|
|
197
|
+
text-align: start;
|
|
198
|
+
text-decoration: none;
|
|
199
|
+
background: none;
|
|
200
|
+
border: 0;
|
|
201
|
+
border-radius: 0;
|
|
202
|
+
cursor: pointer;
|
|
203
|
+
-webkit-tap-highlight-color: transparent;
|
|
204
|
+
transition: background-color
|
|
205
|
+
var(--stuic-list-group-transition, var(--stuic-transition));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.stuic-list-group-list
|
|
209
|
+
> li
|
|
210
|
+
> :is(
|
|
211
|
+
.stuic-list-group-item-link,
|
|
212
|
+
:is(a, button):only-child:not([class^="stuic-"], [class*=" stuic-"])
|
|
213
|
+
):hover:not(:disabled) {
|
|
214
|
+
background: var(--stuic-list-group-item-bg-hover);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.stuic-list-group-list
|
|
218
|
+
> li
|
|
219
|
+
> :is(
|
|
220
|
+
.stuic-list-group-item-link,
|
|
221
|
+
:is(a, button):only-child:not([class^="stuic-"], [class*=" stuic-"])
|
|
222
|
+
):focus-visible {
|
|
223
|
+
outline: var(--stuic-list-group-item-ring-width) solid
|
|
224
|
+
var(--stuic-list-group-item-ring-color);
|
|
225
|
+
/* inset: the box clips, and the first/last row sit against its edge */
|
|
226
|
+
outline-offset: calc(-1 * var(--stuic-list-group-item-ring-width));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.stuic-list-group-list
|
|
230
|
+
> li
|
|
231
|
+
> :is(
|
|
232
|
+
.stuic-list-group-item-link,
|
|
233
|
+
:is(a, button):only-child:not([class^="stuic-"], [class*=" stuic-"])
|
|
234
|
+
):disabled {
|
|
235
|
+
cursor: default;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ============================================================================
|
|
239
|
+
ROW CONTENT CONVENTION — `data-grow` on the one part that takes the leftover
|
|
240
|
+
space. The BASIS, not a min-width, carries "claim 10rem before anything wraps":
|
|
241
|
+
flex line-breaking uses the hypothetical size, so the trailing parts wrap once
|
|
242
|
+
the name would get less than that — and in a container narrower than the basis
|
|
243
|
+
the part still shrinks and truncates instead of overflowing.
|
|
244
|
+
============================================================================ */
|
|
245
|
+
|
|
246
|
+
.stuic-list-group-list > li > [data-grow],
|
|
247
|
+
.stuic-list-group-list
|
|
248
|
+
> li
|
|
249
|
+
> :is(
|
|
250
|
+
.stuic-list-group-item-link,
|
|
251
|
+
:is(a, button):only-child:not([class^="stuic-"], [class*=" stuic-"])
|
|
252
|
+
)
|
|
253
|
+
> [data-grow] {
|
|
254
|
+
flex: 1 1 var(--stuic-list-group-grow-basis);
|
|
255
|
+
min-width: 0;
|
|
256
|
+
overflow: hidden;
|
|
257
|
+
text-overflow: ellipsis;
|
|
258
|
+
white-space: nowrap;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* ============================================================================
|
|
262
|
+
EMPTY + FOOTER
|
|
263
|
+
============================================================================ */
|
|
264
|
+
|
|
265
|
+
.stuic-list-group-empty,
|
|
266
|
+
.stuic-list-group-footer {
|
|
267
|
+
padding-block: var(--stuic-list-group-item-padding-y);
|
|
268
|
+
padding-inline: var(--stuic-list-group-item-padding-x);
|
|
269
|
+
overflow-wrap: anywhere;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.stuic-list-group-empty {
|
|
273
|
+
color: var(--stuic-list-group-empty-text);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.stuic-list-group-footer {
|
|
277
|
+
color: var(--stuic-list-group-footer-text);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ListGroup, type Props as ListGroupProps, type ListGroupSnippetArg, type ListGroupTitleLevel, type ListGroupItemProps, type ListGroupListProps, } from "./ListGroup.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ListGroup, } from "./ListGroup.svelte";
|
package/dist/index.css
CHANGED
|
@@ -110,6 +110,7 @@ In practice:
|
|
|
110
110
|
@import "./components/IconSwap/index.css";
|
|
111
111
|
@import "./components/Input/index.css";
|
|
112
112
|
@import "./components/KbdShortcut/index.css";
|
|
113
|
+
@import "./components/ListGroup/index.css";
|
|
113
114
|
@import "./components/ListItemButton/index.css";
|
|
114
115
|
@import "./components/Modal/index.css";
|
|
115
116
|
@import "./components/ModalDialog/index.css";
|
package/dist/index.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export * from "./components/KbdShortcut/index.js";
|
|
|
66
66
|
export * from "./components/LoginForm/index.js";
|
|
67
67
|
export * from "./components/RegisterForm/index.js";
|
|
68
68
|
export * from "./components/LoginOrRegisterForm/index.js";
|
|
69
|
+
export * from "./components/ListGroup/index.js";
|
|
69
70
|
export * from "./components/ListItemButton/index.js";
|
|
70
71
|
export * from "./components/Modal/index.js";
|
|
71
72
|
export * from "./components/ModalDialog/index.js";
|
package/dist/index.js
CHANGED
|
@@ -72,6 +72,7 @@ export * from "./components/KbdShortcut/index.js";
|
|
|
72
72
|
export * from "./components/LoginForm/index.js";
|
|
73
73
|
export * from "./components/RegisterForm/index.js";
|
|
74
74
|
export * from "./components/LoginOrRegisterForm/index.js";
|
|
75
|
+
export * from "./components/ListGroup/index.js";
|
|
75
76
|
export * from "./components/ListItemButton/index.js";
|
|
76
77
|
export * from "./components/Modal/index.js";
|
|
77
78
|
export * from "./components/ModalDialog/index.js";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
82 Svelte 5 component directories with consistent API patterns. All use runes-based reactivity.
|
|
6
6
|
|
|
7
7
|
## Component Categories
|
|
8
8
|
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
| Card | Flexible card with image, title, footer; vertical/horizontal layout |
|
|
111
111
|
| Stat | KPI/stat card: label + value + delta with trend arrow and semantic coloring |
|
|
112
112
|
| DescriptionList | Term/value list (`<dl>`): stacked or two-column by container query, hairlines, truncation, totals |
|
|
113
|
+
| ListGroup | Bordered box of wrapping rows (`<ul>`): header title/aside, hairlines, linked rows, empty, footer |
|
|
113
114
|
| Timeline | Vertical event list on a rail: dot/icon/custom markers, inline or opposite time, alternate layout |
|
|
114
115
|
| Gantt | Horizontal schedule chart: day/week/month axis, bars, milestones, progress, today line |
|
|
115
116
|
| TrendChart | Svelte wrapper for `@marianmeres/trend-chart` (subpath-only: `@marianmeres/stuic/trend-chart`) |
|
|
@@ -1088,6 +1089,59 @@ Prefix: `--stuic-description-list-*`
|
|
|
1088
1089
|
|
|
1089
1090
|
---
|
|
1090
1091
|
|
|
1092
|
+
## ListGroup
|
|
1093
|
+
|
|
1094
|
+
A rounded, bordered box of rows split by hairlines — optional header (title start, figure end), optional footer line — the block back offices keep hand-rolling. Renders `<div>` › header `<div>` › `<ul role="list">` › `<li>` per row › footer `<div>`. Each row is **one wrapping flex line** of parts (a code, a name that takes the slack, a run of facts), not columns: nothing lines up across rows, and in a narrow drawer the trailing facts drop under the name. Not a `DataTable` (no column model), not a `DescriptionList` (many things, not properties of one), not a `Card` (no shadow, no padded body). Generic over `T`.
|
|
1095
|
+
|
|
1096
|
+
### Exports
|
|
1097
|
+
|
|
1098
|
+
| Export | Kind | Description |
|
|
1099
|
+
| --------------------- | --------- | ------------------------------------------------------------------------------------- |
|
|
1100
|
+
| `ListGroup` | component | Main component |
|
|
1101
|
+
| `ListGroupProps` | type | Props type (`Props<T>`) |
|
|
1102
|
+
| `ListGroupSnippetArg` | type | `{ item, index }` — `renderItem`'s argument |
|
|
1103
|
+
| `ListGroupTitleLevel` | type | `1 \| 2 \| 3 \| 4 \| 5 \| 6` |
|
|
1104
|
+
| `ListGroupItemProps` | type | `HTMLLiAttributes` minus `children`, with `class?: string` — what `itemProps` returns |
|
|
1105
|
+
| `ListGroupListProps` | type | `HTMLAttributes<HTMLUListElement>` minus `children`/`class`/`role` — `listProps` |
|
|
1106
|
+
|
|
1107
|
+
### Key Props
|
|
1108
|
+
|
|
1109
|
+
| Prop | Type | Default | Description |
|
|
1110
|
+
| ------------ | -------------------------------------- | ------- | ----------------------------------------------------------------------------------------- |
|
|
1111
|
+
| `items` | `T[]` | — | The rows; without `renderItem` an item renders as `THC` |
|
|
1112
|
+
| `renderItem` | `Snippet<[{ item, index }]>` | — | Row content, inside the `<li>` (or the `itemHref` anchor) |
|
|
1113
|
+
| `children` | `Snippet` | — | `<li>`s rendered inside the `<ul>` _instead of_ `items` |
|
|
1114
|
+
| `getItemId` | `(item, index) => string \| number` | index | Keyed `{#each}` identity |
|
|
1115
|
+
| `itemProps` | `(item, index) => ListGroupItemProps` | — | Attributes on each `<li>` (the `data-*` hooks tests select on); `class` merges last |
|
|
1116
|
+
| `itemHref` | `(item, index) => string \| undefined` | — | Wraps the row in `a.stuic-list-group-item-link`, which becomes the row box |
|
|
1117
|
+
| `title` | `THC` | — | Header start side; labels the `<ul>` (`aria-labelledby`) |
|
|
1118
|
+
| `titleLevel` | `1…6` | — | `<hN>` instead of `<div>` — semantics only, look unchanged |
|
|
1119
|
+
| `aside` | `THC` | — | Header end side (`tabular-nums`, muted). Never an automatic row count |
|
|
1120
|
+
| `footer` | `THC` | — | A line inside the box under the rows |
|
|
1121
|
+
| `empty` | `THC` | — | Replaces the `<ul>` when there are no rows; without it an empty group renders **nothing** |
|
|
1122
|
+
| `listProps` | `ListGroupListProps` | — | Attributes for the `<ul>` — how a title-less list gets `aria-label`/`aria-labelledby` |
|
|
1123
|
+
|
|
1124
|
+
Class slots: `class`, `classHeader`, `classTitle`, `classAside`, `classList`, `classItem`, `classItemLink`, `classEmpty`, `classFooter`. Rest props go to the root `<div>`.
|
|
1125
|
+
|
|
1126
|
+
### Row contract (works in both forms)
|
|
1127
|
+
|
|
1128
|
+
The CSS selects **structurally** (`ul > li`, `> li > :is(a, button):only-child`), so a hand-written `children` row renders exactly like a generated one. A row whose only child is a plain `<a>`/`<button>` hands the row box (padding, hover, inset focus ring) to it; a child carrying a stuic component class (a lone `Button`, `Pill`, `ListItemButton`) is left alone so it keeps its own box. `data-grow` on one part gives it `flex: 1 1 var(--stuic-list-group-grow-basis)` (10rem) + truncation — the **basis**, not a min-width, is what makes the neighbours wrap once the name would get less than 10rem, while a container narrower than that still shrinks it instead of overflowing. A second line is a child with `basis-full`.
|
|
1129
|
+
|
|
1130
|
+
### Deliberate choices
|
|
1131
|
+
|
|
1132
|
+
- Root, header and footer are `<div>`s: a labelled `<section>` is a `region` landmark, and outside `<main>`/sectioning content (a drawer, a dialog) a `<header>`/`<footer>` is a `banner`/`contentinfo` landmark — one per group.
|
|
1133
|
+
- `role="list"` is explicit (WebKit drops list semantics from `list-style: none`).
|
|
1134
|
+
- No part declares a `font-size` — `class="text-sm"` on the root scales the box. The header has no own `padding-x`: it shares the rows', so header and row text always start at the same x. `--stuic-list-group-header-padding-y` is a usage-site fallback, never declared, so a scoped `--stuic-list-group-item-padding-y` reaches the header.
|
|
1135
|
+
- `overflow: clip` (not `hidden`) on the box so header/hover backgrounds follow the radius without creating a scroll container.
|
|
1136
|
+
|
|
1137
|
+
### CSS Tokens
|
|
1138
|
+
|
|
1139
|
+
Prefix: `--stuic-list-group-*`
|
|
1140
|
+
|
|
1141
|
+
`bg`, `border-color`, `rule-color`, `rule-width`, `item-padding-x`, `item-padding-y`, `item-gap-x`, `item-gap-y`, `item-align`, `grow-basis`, `item-bg-hover`, `item-ring-width`, `item-ring-color`, `header-bg`, `title-font-weight`, `title-text`, `aside-text`, `empty-text`, `footer-text`. Not declared (usage-site fallbacks): `radius` (→ `--stuic-radius-container`), `border-width` (→ `--stuic-border-width`), `transition` (→ `--stuic-transition`), `header-padding-y` (→ `item-padding-y`).
|
|
1142
|
+
|
|
1143
|
+
---
|
|
1144
|
+
|
|
1091
1145
|
## EmptyState
|
|
1092
1146
|
|
|
1093
1147
|
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").
|