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