@lyeve-labs/ui-kit 0.11.2 → 0.13.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/README.md +1 -1
- package/dist/components/AccordionItem.svelte +1 -1
- package/dist/components/Autocomplete.svelte +191 -125
- package/dist/components/Autocomplete.svelte.d.ts +29 -8
- package/dist/components/Button.svelte +26 -4
- package/dist/components/Card.svelte +61 -3
- package/dist/components/Card.svelte.d.ts +24 -2
- package/dist/components/Checkbox.svelte +174 -59
- package/dist/components/Checkbox.svelte.d.ts +20 -3
- package/dist/components/CheckboxGroup.svelte +162 -0
- package/dist/components/CheckboxGroup.svelte.d.ts +51 -0
- package/dist/components/Collapsible.svelte +142 -0
- package/dist/components/Collapsible.svelte.d.ts +32 -0
- package/dist/components/CopyButton.svelte +126 -0
- package/dist/components/CopyButton.svelte.d.ts +14 -0
- package/dist/components/DatePicker.svelte +48 -6
- package/dist/components/DateTimePicker.svelte +337 -0
- package/dist/components/DateTimePicker.svelte.d.ts +26 -0
- package/dist/components/DescriptionList.svelte +78 -0
- package/dist/components/DescriptionList.svelte.d.ts +34 -0
- package/dist/components/Drawer.svelte +15 -4
- package/dist/components/Field.svelte +104 -0
- package/dist/components/Field.svelte.d.ts +46 -0
- package/dist/components/FileInput.svelte +5 -2
- package/dist/components/FormMessage.svelte +85 -0
- package/dist/components/FormMessage.svelte.d.ts +11 -0
- package/dist/components/Input.svelte +1 -1
- package/dist/components/Label.svelte +7 -1
- package/dist/components/Label.svelte.d.ts +6 -0
- package/dist/components/Modal.svelte +25 -8
- package/dist/components/MultiSelect.svelte +199 -109
- package/dist/components/MultiSelect.svelte.d.ts +22 -9
- package/dist/components/NumberInput.svelte +8 -4
- package/dist/components/PageHeader.svelte +37 -4
- package/dist/components/PageHeader.svelte.d.ts +15 -0
- package/dist/components/PageShell.svelte +85 -0
- package/dist/components/PageShell.svelte.d.ts +38 -0
- package/dist/components/Pagination.svelte +58 -17
- package/dist/components/Panel.svelte +101 -0
- package/dist/components/Panel.svelte.d.ts +39 -0
- package/dist/components/PasswordInput.svelte +139 -0
- package/dist/components/PasswordInput.svelte.d.ts +29 -0
- package/dist/components/Radio.svelte +152 -32
- package/dist/components/Radio.svelte.d.ts +16 -1
- package/dist/components/RadioGroup.svelte +118 -71
- package/dist/components/RadioGroup.svelte.d.ts +39 -9
- package/dist/components/SectionHeading.svelte +39 -0
- package/dist/components/SectionHeading.svelte.d.ts +21 -0
- package/dist/components/SegmentedControl.svelte +194 -0
- package/dist/components/SegmentedControl.svelte.d.ts +55 -0
- package/dist/components/Select.svelte +471 -46
- package/dist/components/Select.svelte.d.ts +95 -6
- package/dist/components/SidebarNav.svelte +259 -0
- package/dist/components/SidebarNav.svelte.d.ts +17 -0
- package/dist/components/Stat.svelte +53 -2
- package/dist/components/Stat.svelte.d.ts +31 -0
- package/dist/components/Textarea.svelte +1 -1
- package/dist/components/TimePicker.svelte +480 -0
- package/dist/components/TimePicker.svelte.d.ts +23 -0
- package/dist/components/Toaster.svelte +9 -2
- package/dist/components/Toggle.svelte +5 -1
- package/dist/components/Toggle.svelte.d.ts +2 -0
- package/dist/components/Toolbar.svelte +39 -0
- package/dist/components/Toolbar.svelte.d.ts +26 -0
- package/dist/components/Tooltip.svelte +48 -12
- package/dist/components/TreeView.svelte +339 -0
- package/dist/components/TreeView.svelte.d.ts +37 -0
- package/dist/components/dialog/Dialog.svelte +15 -58
- package/dist/components/dialog/dialog-manager.svelte.d.ts +2 -2
- package/dist/components/dialog/dialog-manager.svelte.js +21 -21
- package/dist/index.d.ts +25 -1
- package/dist/index.js +20 -1
- package/dist/internal/calendar.d.ts +119 -0
- package/dist/internal/calendar.js +225 -0
- package/dist/internal/choice.d.ts +136 -0
- package/dist/internal/choice.js +179 -0
- package/dist/internal/field.d.ts +31 -0
- package/dist/internal/field.js +42 -1
- package/dist/internal/filter.d.ts +80 -0
- package/dist/internal/filter.js +80 -0
- package/dist/internal/layout.d.ts +119 -0
- package/dist/internal/layout.js +132 -0
- package/dist/internal/listbox.svelte.d.ts +77 -0
- package/dist/internal/listbox.svelte.js +438 -0
- package/dist/internal/nav-expansion.svelte.d.ts +36 -0
- package/dist/internal/nav-expansion.svelte.js +144 -0
- package/dist/internal/nav-tree.d.ts +68 -0
- package/dist/internal/nav-tree.js +102 -0
- package/dist/internal/overlay.d.ts +25 -0
- package/dist/internal/overlay.js +92 -0
- package/dist/internal/panel.d.ts +100 -0
- package/dist/internal/panel.js +109 -0
- package/dist/internal/rollup.d.ts +52 -0
- package/dist/internal/rollup.js +67 -0
- package/dist/internal/time.d.ts +103 -0
- package/dist/internal/time.js +166 -0
- package/dist/internal/tree.d.ts +86 -0
- package/dist/internal/tree.js +111 -0
- package/dist/styles/theme.css +66 -25
- package/package.json +4 -2
- package/src/lib/styles/theme.css +66 -25
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { activeTrail, type NavNode, type NavTree } from '../internal/nav-tree.js';
|
|
3
|
+
import { createNavExpansion } from '../internal/nav-expansion.svelte.js';
|
|
4
|
+
import type { AccentTone } from '../internal/tone.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
items: NavTree;
|
|
8
|
+
/** Pathname only, no query, no hash. Passed in rather than read, because the kit cannot import an app router. */
|
|
9
|
+
activePath: string;
|
|
10
|
+
/** The nav landmark's accessible name. */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Icon-only rail. */
|
|
13
|
+
collapsed?: boolean;
|
|
14
|
+
expandActive?: boolean;
|
|
15
|
+
exclusive?: boolean;
|
|
16
|
+
storageKey?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
items,
|
|
22
|
+
activePath,
|
|
23
|
+
label = 'Primary',
|
|
24
|
+
collapsed = false,
|
|
25
|
+
expandActive = true,
|
|
26
|
+
exclusive = false,
|
|
27
|
+
storageKey = undefined,
|
|
28
|
+
class: klass = '',
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
// $props.id() and not a random string: a random id differs between the server
|
|
32
|
+
// render and hydration, so every aria-controls built from it points at an
|
|
33
|
+
// element that no longer carries that id once the client takes over.
|
|
34
|
+
const uid = $props.id();
|
|
35
|
+
|
|
36
|
+
const expansion = createNavExpansion({
|
|
37
|
+
items: () => items,
|
|
38
|
+
activePath: () => activePath,
|
|
39
|
+
expandActive: () => expandActive,
|
|
40
|
+
exclusive: () => exclusive,
|
|
41
|
+
storageKey: () => storageKey,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const trail = $derived(activeTrail(items, activePath));
|
|
45
|
+
const onTrail = $derived(new Set(trail));
|
|
46
|
+
/** The tail of the trail is the page itself. Everything above it is an ancestor. */
|
|
47
|
+
const currentId = $derived(trail.length > 0 ? trail[trail.length - 1] : undefined);
|
|
48
|
+
|
|
49
|
+
function listId(id: string): string {
|
|
50
|
+
return `${uid}-${id}-group`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The row, without the colours any one state paints.
|
|
55
|
+
*
|
|
56
|
+
* The rail is a start border on every row, not one added when the branch is
|
|
57
|
+
* active, so marking a branch cannot shift its rows two pixels sideways. Its
|
|
58
|
+
* colour is a ternary rather than a second border utility: two colour
|
|
59
|
+
* utilities on one element resolve in the order Tailwind emits them, not the
|
|
60
|
+
* order they are written.
|
|
61
|
+
*
|
|
62
|
+
* The focus ring is inset and stated once, outside every state branch. A
|
|
63
|
+
* sidebar scrolls, and a scrolling box crops the 2px the global outline sits
|
|
64
|
+
* outside its element, so an outer ring on a row near the edge shows as a
|
|
65
|
+
* stray line or as nothing at all.
|
|
66
|
+
*/
|
|
67
|
+
const ROW =
|
|
68
|
+
'flex w-full items-center gap-2.5 rounded-lg border-s-2 py-2 pe-2 text-sm text-start ' +
|
|
69
|
+
'transition-colors duration-150 outline-none ' +
|
|
70
|
+
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand';
|
|
71
|
+
|
|
72
|
+
/** Exactly one border colour, one background and one text colour per state. */
|
|
73
|
+
function rowPaint(state: 'current' | 'ancestor' | 'rest'): string {
|
|
74
|
+
if (state === 'current') return 'border-brand bg-surface-2 font-medium text-brand';
|
|
75
|
+
if (state === 'ancestor') return 'border-brand text-fg hover:bg-surface-2';
|
|
76
|
+
return 'border-transparent text-muted hover:bg-surface-2 hover:text-fg';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const ROW_DISABLED = 'cursor-not-allowed border-transparent text-muted opacity-50';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The disclosure beside a link, for a node that is both a destination and a
|
|
83
|
+
* section. It is a separate control because one control cannot know whether
|
|
84
|
+
* a click meant "go there" or "show me what is under it", and guessing gets
|
|
85
|
+
* it wrong half the time.
|
|
86
|
+
*/
|
|
87
|
+
const DISCLOSURE =
|
|
88
|
+
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-muted ' +
|
|
89
|
+
'transition-colors duration-150 outline-none hover:bg-surface-2 hover:text-fg ' +
|
|
90
|
+
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand';
|
|
91
|
+
|
|
92
|
+
const BADGE_TONE: Record<AccentTone, string> = {
|
|
93
|
+
neutral: 'bg-surface-2 text-faint',
|
|
94
|
+
brand: 'bg-brand/15 text-brand',
|
|
95
|
+
success: 'bg-success/15 text-success',
|
|
96
|
+
warn: 'bg-warn/15 text-warn',
|
|
97
|
+
danger: 'bg-danger/15 text-danger',
|
|
98
|
+
violet: 'bg-violet/15 text-violet',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const BADGE = 'ms-auto shrink-0 rounded-full px-1.5 py-0.5 text-xs font-medium';
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<!--
|
|
105
|
+
A nav and nothing else. The app shell already owns the aside around this, and
|
|
106
|
+
a second landmark of any kind changes what a landmark query answers with, so
|
|
107
|
+
a test looking for the one navigation region finds two and a screen reader
|
|
108
|
+
user hears the sidebar announced twice.
|
|
109
|
+
-->
|
|
110
|
+
<nav
|
|
111
|
+
aria-label={label}
|
|
112
|
+
class="{collapsed
|
|
113
|
+
? 'w-nav-rail'
|
|
114
|
+
: 'w-sidebar'} shrink-0 overflow-y-auto overscroll-contain py-2 {klass}"
|
|
115
|
+
>
|
|
116
|
+
<ul class="flex flex-col gap-0.5">
|
|
117
|
+
{#each items as node (node.id)}
|
|
118
|
+
{@render row(node, 0)}
|
|
119
|
+
{/each}
|
|
120
|
+
</ul>
|
|
121
|
+
</nav>
|
|
122
|
+
|
|
123
|
+
{#snippet row(node: NavNode, depth: number)}
|
|
124
|
+
{@const children = node.children ?? []}
|
|
125
|
+
{@const branch = children.length > 0}
|
|
126
|
+
{@const current = node.id === currentId}
|
|
127
|
+
{@const ancestor = onTrail.has(node.id) && !current}
|
|
128
|
+
{@const paint = rowPaint(current ? 'current' : ancestor ? 'ancestor' : 'rest')}
|
|
129
|
+
{@const open = expansion.isExpanded(node.id)}
|
|
130
|
+
<!--
|
|
131
|
+
Indentation reads a token through a style attribute. A class built from the
|
|
132
|
+
depth, pl-{depth * 4}, matches no candidate in Tailwind's scan of the
|
|
133
|
+
source, so no rule is generated and every level renders flush against the
|
|
134
|
+
edge. A collapsed rail has no room to indent, so its levels all start at
|
|
135
|
+
the same inset.
|
|
136
|
+
-->
|
|
137
|
+
{@const level = collapsed ? 0 : depth}
|
|
138
|
+
<li>
|
|
139
|
+
{#if node.disabled}
|
|
140
|
+
<!--
|
|
141
|
+
Not a link. An anchor without an href is not focusable and an anchor
|
|
142
|
+
with one still navigates, so a disabled entry that keeps its anchor is
|
|
143
|
+
either invisible to the keyboard or not disabled at all.
|
|
144
|
+
-->
|
|
145
|
+
<span
|
|
146
|
+
class="{ROW} {ROW_DISABLED}"
|
|
147
|
+
style="padding-inline-start: calc(var(--spacing-nav-indent) * {level} + var(--spacing-inline))"
|
|
148
|
+
aria-disabled="true"
|
|
149
|
+
>
|
|
150
|
+
{@render body(node)}
|
|
151
|
+
</span>
|
|
152
|
+
{:else if node.href && branch}
|
|
153
|
+
<div class="flex items-center gap-1">
|
|
154
|
+
<a
|
|
155
|
+
href={node.href}
|
|
156
|
+
aria-current={current ? 'page' : undefined}
|
|
157
|
+
class="{ROW} {paint} min-w-0 flex-1"
|
|
158
|
+
style="padding-inline-start: calc(var(--spacing-nav-indent) * {level} + var(--spacing-inline))"
|
|
159
|
+
>
|
|
160
|
+
{@render body(node)}
|
|
161
|
+
</a>
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
class={DISCLOSURE}
|
|
165
|
+
aria-expanded={open}
|
|
166
|
+
aria-controls={listId(node.id)}
|
|
167
|
+
aria-current={ancestor ? 'true' : undefined}
|
|
168
|
+
aria-label="{open ? 'Collapse' : 'Expand'} {node.label}"
|
|
169
|
+
onclick={() => expansion.toggle(node.id)}
|
|
170
|
+
>
|
|
171
|
+
{@render chevron(open)}
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
{:else if branch}
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
class="{ROW} {paint}"
|
|
178
|
+
style="padding-inline-start: calc(var(--spacing-nav-indent) * {level} + var(--spacing-inline))"
|
|
179
|
+
aria-expanded={open}
|
|
180
|
+
aria-controls={listId(node.id)}
|
|
181
|
+
aria-current={ancestor ? 'true' : undefined}
|
|
182
|
+
onclick={() => expansion.toggle(node.id)}
|
|
183
|
+
>
|
|
184
|
+
{@render body(node)}
|
|
185
|
+
{@render chevron(open)}
|
|
186
|
+
</button>
|
|
187
|
+
{:else if node.href}
|
|
188
|
+
<a
|
|
189
|
+
href={node.href}
|
|
190
|
+
aria-current={current ? 'page' : undefined}
|
|
191
|
+
class="{ROW} {paint}"
|
|
192
|
+
style="padding-inline-start: calc(var(--spacing-nav-indent) * {level} + var(--spacing-inline))"
|
|
193
|
+
>
|
|
194
|
+
{@render body(node)}
|
|
195
|
+
</a>
|
|
196
|
+
{:else}
|
|
197
|
+
<span
|
|
198
|
+
class="{ROW} {paint}"
|
|
199
|
+
style="padding-inline-start: calc(var(--spacing-nav-indent) * {level} + var(--spacing-inline))"
|
|
200
|
+
>
|
|
201
|
+
{@render body(node)}
|
|
202
|
+
</span>
|
|
203
|
+
{/if}
|
|
204
|
+
|
|
205
|
+
{#if branch}
|
|
206
|
+
<!--
|
|
207
|
+
Rendered whether or not it is open, and hidden with a display utility.
|
|
208
|
+
aria-controls has to name an element that exists; pointing it at markup
|
|
209
|
+
that appears only once the group is open leaves the reference dangling
|
|
210
|
+
in the one state where a reader needs it to tell them what the button
|
|
211
|
+
will reveal.
|
|
212
|
+
-->
|
|
213
|
+
<ul
|
|
214
|
+
id={listId(node.id)}
|
|
215
|
+
role="group"
|
|
216
|
+
class="{open && !node.disabled ? 'flex' : 'hidden'} mt-0.5 flex-col gap-0.5"
|
|
217
|
+
>
|
|
218
|
+
{#each children as child (child.id)}
|
|
219
|
+
{@render row(child, depth + 1)}
|
|
220
|
+
{/each}
|
|
221
|
+
</ul>
|
|
222
|
+
{/if}
|
|
223
|
+
</li>
|
|
224
|
+
{/snippet}
|
|
225
|
+
|
|
226
|
+
{#snippet body(node: NavNode)}
|
|
227
|
+
{#if node.icon}
|
|
228
|
+
{@const Icon = node.icon}
|
|
229
|
+
<Icon size={16} class="shrink-0" />
|
|
230
|
+
{/if}
|
|
231
|
+
<!--
|
|
232
|
+
The label stays in the accessible name on a collapsed rail. Dropping it
|
|
233
|
+
leaves a row whose only content is an icon, which a screen reader announces
|
|
234
|
+
as an empty link.
|
|
235
|
+
-->
|
|
236
|
+
<span class={collapsed ? 'sr-only' : 'min-w-0 flex-1 truncate'}>{node.label}</span>
|
|
237
|
+
{#if node.badge !== undefined && !collapsed}
|
|
238
|
+
<span class="{BADGE} {BADGE_TONE[node.badgeTone ?? 'neutral']}">{node.badge}</span>
|
|
239
|
+
{/if}
|
|
240
|
+
{/snippet}
|
|
241
|
+
|
|
242
|
+
{#snippet chevron(open: boolean)}
|
|
243
|
+
<!-- Stroked SVG, not a Unicode arrow: a glyph lands at whatever weight the
|
|
244
|
+
reader's font gives it, visibly apart from every other icon here. -->
|
|
245
|
+
<svg
|
|
246
|
+
width="14"
|
|
247
|
+
height="14"
|
|
248
|
+
viewBox="0 0 24 24"
|
|
249
|
+
fill="none"
|
|
250
|
+
stroke="currentColor"
|
|
251
|
+
stroke-width="2"
|
|
252
|
+
stroke-linecap="round"
|
|
253
|
+
stroke-linejoin="round"
|
|
254
|
+
aria-hidden="true"
|
|
255
|
+
class="shrink-0 transition-transform duration-150 {open ? 'rotate-90' : ''}"
|
|
256
|
+
>
|
|
257
|
+
<path d="M9 18l6-6-6-6" />
|
|
258
|
+
</svg>
|
|
259
|
+
{/snippet}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type NavTree } from '../internal/nav-tree.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
items: NavTree;
|
|
4
|
+
/** Pathname only, no query, no hash. Passed in rather than read, because the kit cannot import an app router. */
|
|
5
|
+
activePath: string;
|
|
6
|
+
/** The nav landmark's accessible name. */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Icon-only rail. */
|
|
9
|
+
collapsed?: boolean;
|
|
10
|
+
expandActive?: boolean;
|
|
11
|
+
exclusive?: boolean;
|
|
12
|
+
storageKey?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
declare const SidebarNav: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type SidebarNav = ReturnType<typeof SidebarNav>;
|
|
17
|
+
export default SidebarNav;
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* One measured number with its label.
|
|
4
|
+
*
|
|
5
|
+
* Two apps each declared a local Stat snippet for the same job and the two
|
|
6
|
+
* were visually unrelated: one a bordered card with a mono value, the other a
|
|
7
|
+
* bare div at body size. `size`, `tone` and `mono` are what let both of them
|
|
8
|
+
* use this one instead.
|
|
9
|
+
*/
|
|
10
|
+
import type { StatusTone } from '../internal/tone.js';
|
|
11
|
+
|
|
2
12
|
type Trend = 'up' | 'down' | 'flat';
|
|
3
13
|
type Accent = 'brand' | 'violet' | 'success' | 'neutral';
|
|
14
|
+
type Size = 'sm' | 'md';
|
|
4
15
|
|
|
5
16
|
interface Props {
|
|
6
17
|
label: string;
|
|
@@ -9,6 +20,27 @@
|
|
|
9
20
|
trend?: Trend;
|
|
10
21
|
change?: string;
|
|
11
22
|
accent?: Accent;
|
|
23
|
+
/** `sm` for a dense row of figures, `md` for a card of its own. */
|
|
24
|
+
size?: Size;
|
|
25
|
+
/**
|
|
26
|
+
* The status the figure reports, which colours the value.
|
|
27
|
+
*
|
|
28
|
+
* Wins over `accent` when it is set. `accent` is the decorative vocabulary
|
|
29
|
+
* and has no warn or danger, so a number that is over its quota could only
|
|
30
|
+
* be stated in prose beside it. Only the canonical spellings are accepted:
|
|
31
|
+
* the tone vocabulary keeps `info` as an alias for the components that
|
|
32
|
+
* already shipped it, and this prop never shipped anything to keep working.
|
|
33
|
+
*/
|
|
34
|
+
tone?: StatusTone;
|
|
35
|
+
/**
|
|
36
|
+
* Tabular figures for a value that changes.
|
|
37
|
+
*
|
|
38
|
+
* A proportional 1 is narrower than a proportional 0, so a counter that
|
|
39
|
+
* ticks changes width on nearly every update and shoves whatever sits
|
|
40
|
+
* beside it sideways. The mono family and tabular figures together hold
|
|
41
|
+
* every digit on the same advance.
|
|
42
|
+
*/
|
|
43
|
+
mono?: boolean;
|
|
12
44
|
class?: string;
|
|
13
45
|
}
|
|
14
46
|
|
|
@@ -19,6 +51,9 @@
|
|
|
19
51
|
trend = undefined,
|
|
20
52
|
change = undefined,
|
|
21
53
|
accent = 'brand',
|
|
54
|
+
size = 'md',
|
|
55
|
+
tone = undefined,
|
|
56
|
+
mono = false,
|
|
22
57
|
class: klass = '',
|
|
23
58
|
}: Props = $props();
|
|
24
59
|
|
|
@@ -29,13 +64,29 @@
|
|
|
29
64
|
neutral: 'text-fg',
|
|
30
65
|
} as const;
|
|
31
66
|
|
|
67
|
+
const tones: Record<StatusTone, string> = {
|
|
68
|
+
neutral: 'text-fg',
|
|
69
|
+
brand: 'text-brand',
|
|
70
|
+
success: 'text-success',
|
|
71
|
+
warn: 'text-warn',
|
|
72
|
+
danger: 'text-danger',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const pads: Record<Size, string> = { sm: 'p-4', md: 'p-5' };
|
|
76
|
+
const values: Record<Size, string> = {
|
|
77
|
+
sm: 'mt-1 text-lg font-semibold',
|
|
78
|
+
md: 'mt-2 text-2xl font-bold',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const valueColor = $derived(tone ? tones[tone] : accents[accent]);
|
|
82
|
+
|
|
32
83
|
const trendColor = { up: 'text-success', down: 'text-danger', flat: 'text-muted' } as const;
|
|
33
84
|
const trendMark = { up: '↑', down: '↓', flat: '→' } as const;
|
|
34
85
|
</script>
|
|
35
86
|
|
|
36
|
-
<div class="rounded-xl border border-line bg-surface
|
|
87
|
+
<div class="rounded-xl border border-line bg-surface {pads[size]} {klass}">
|
|
37
88
|
<p class="text-xs font-medium uppercase tracking-wide text-faint">{label}</p>
|
|
38
|
-
<p class="
|
|
89
|
+
<p class="{values[size]} {valueColor} {mono ? 'font-mono tabular-nums' : ''}">{value}</p>
|
|
39
90
|
<div class="mt-1 flex items-center gap-2">
|
|
40
91
|
{#if trend && change}
|
|
41
92
|
<span class="text-xs font-medium {trendColor[trend]}">{trendMark[trend]} {change}</span>
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One measured number with its label.
|
|
3
|
+
*
|
|
4
|
+
* Two apps each declared a local Stat snippet for the same job and the two
|
|
5
|
+
* were visually unrelated: one a bordered card with a mono value, the other a
|
|
6
|
+
* bare div at body size. `size`, `tone` and `mono` are what let both of them
|
|
7
|
+
* use this one instead.
|
|
8
|
+
*/
|
|
9
|
+
import type { StatusTone } from '../internal/tone.js';
|
|
1
10
|
type Trend = 'up' | 'down' | 'flat';
|
|
2
11
|
type Accent = 'brand' | 'violet' | 'success' | 'neutral';
|
|
12
|
+
type Size = 'sm' | 'md';
|
|
3
13
|
interface Props {
|
|
4
14
|
label: string;
|
|
5
15
|
value: string | number;
|
|
@@ -7,6 +17,27 @@ interface Props {
|
|
|
7
17
|
trend?: Trend;
|
|
8
18
|
change?: string;
|
|
9
19
|
accent?: Accent;
|
|
20
|
+
/** `sm` for a dense row of figures, `md` for a card of its own. */
|
|
21
|
+
size?: Size;
|
|
22
|
+
/**
|
|
23
|
+
* The status the figure reports, which colours the value.
|
|
24
|
+
*
|
|
25
|
+
* Wins over `accent` when it is set. `accent` is the decorative vocabulary
|
|
26
|
+
* and has no warn or danger, so a number that is over its quota could only
|
|
27
|
+
* be stated in prose beside it. Only the canonical spellings are accepted:
|
|
28
|
+
* the tone vocabulary keeps `info` as an alias for the components that
|
|
29
|
+
* already shipped it, and this prop never shipped anything to keep working.
|
|
30
|
+
*/
|
|
31
|
+
tone?: StatusTone;
|
|
32
|
+
/**
|
|
33
|
+
* Tabular figures for a value that changes.
|
|
34
|
+
*
|
|
35
|
+
* A proportional 1 is narrower than a proportional 0, so a counter that
|
|
36
|
+
* ticks changes width on nearly every update and shoves whatever sits
|
|
37
|
+
* beside it sideways. The mono family and tabular figures together hold
|
|
38
|
+
* every digit on the same advance.
|
|
39
|
+
*/
|
|
40
|
+
mono?: boolean;
|
|
10
41
|
class?: string;
|
|
11
42
|
}
|
|
12
43
|
declare const Stat: import("svelte").Component<Props, {}, "">;
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
<div class="{FIELD_WRAP} {cls}">
|
|
55
55
|
{#if label}
|
|
56
56
|
<label for={fieldId} class={FIELD_LABEL}>
|
|
57
|
-
{label}{#if required}<span class="text-danger ml-0.5" aria-
|
|
57
|
+
{label}{#if required}<span class="text-danger ml-0.5" aria-hidden="true">*</span>{/if}
|
|
58
58
|
</label>
|
|
59
59
|
{/if}
|
|
60
60
|
|