@marianmeres/stuic 3.167.0 → 3.169.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 +6 -6
- package/dist/README.md +1 -1
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/attachments/long-press.d.ts +43 -0
- package/dist/attachments/long-press.js +107 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte +198 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte.d.ts +60 -0
- package/dist/components/Breadcrumbs/README.md +174 -0
- package/dist/components/Breadcrumbs/i18n-sk.d.ts +17 -0
- package/dist/components/Breadcrumbs/i18n-sk.js +19 -0
- package/dist/components/Breadcrumbs/i18n.d.ts +31 -0
- package/dist/components/Breadcrumbs/i18n.js +40 -0
- package/dist/components/Breadcrumbs/index.css +84 -0
- package/dist/components/Breadcrumbs/index.d.ts +4 -0
- package/dist/components/Breadcrumbs/index.js +4 -0
- package/dist/components/Breadcrumbs/json-ld.d.ts +58 -0
- package/dist/components/Breadcrumbs/json-ld.js +56 -0
- package/dist/components/Collapsible/Collapsible.svelte +84 -15
- package/dist/components/Collapsible/Collapsible.svelte.d.ts +29 -2
- package/dist/components/Collapsible/README.md +82 -23
- package/dist/components/Collapsible/index.css +38 -0
- package/dist/components/Collapsible/index.d.ts +1 -1
- package/dist/components/ContextMenu/ContextMenu.svelte +302 -0
- package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +94 -0
- package/dist/components/ContextMenu/README.md +159 -0
- package/dist/components/ContextMenu/i18n-sk.d.ts +17 -0
- package/dist/components/ContextMenu/i18n-sk.js +18 -0
- package/dist/components/ContextMenu/i18n.d.ts +30 -0
- package/dist/components/ContextMenu/i18n.js +38 -0
- package/dist/components/ContextMenu/index.css +26 -0
- package/dist/components/ContextMenu/index.d.ts +3 -0
- package/dist/components/ContextMenu/index.js +3 -0
- package/dist/components/CopyButton/CopyButton.svelte +278 -0
- package/dist/components/CopyButton/CopyButton.svelte.d.ts +76 -0
- package/dist/components/CopyButton/README.md +241 -0
- package/dist/components/CopyButton/i18n-sk.d.ts +17 -0
- package/dist/components/CopyButton/i18n-sk.js +20 -0
- package/dist/components/CopyButton/i18n.d.ts +35 -0
- package/dist/components/CopyButton/i18n.js +43 -0
- package/dist/components/CopyButton/index.css +67 -0
- package/dist/components/CopyButton/index.d.ts +3 -0
- package/dist/components/CopyButton/index.js +3 -0
- package/dist/components/DataTable/DataTable.svelte +41 -34
- package/dist/components/DataTable/README.md +45 -0
- package/dist/components/DataTable/i18n-sk.js +1 -0
- package/dist/components/DataTable/i18n.d.ts +1 -0
- package/dist/components/DataTable/i18n.js +3 -0
- package/dist/components/DataTable/index.css +41 -3
- package/dist/components/EmptyState/EmptyState.svelte +121 -0
- package/dist/components/EmptyState/EmptyState.svelte.d.ts +40 -0
- package/dist/components/EmptyState/README.md +188 -0
- package/dist/components/EmptyState/index.css +154 -0
- package/dist/components/EmptyState/index.d.ts +1 -0
- package/dist/components/EmptyState/index.js +1 -0
- package/dist/components/Input/FieldOptions.svelte +92 -2
- package/dist/components/Input/FieldOptions.svelte.d.ts +13 -0
- package/dist/components/Input/README.md +66 -31
- package/dist/components/Input/_internal/FieldLikeChips.svelte +286 -0
- package/dist/components/Input/_internal/FieldLikeChips.svelte.d.ts +65 -0
- package/dist/components/Input/index.css +58 -0
- package/dist/components/Pagination/Pagination.svelte +250 -0
- package/dist/components/Pagination/Pagination.svelte.d.ts +69 -0
- package/dist/components/Pagination/README.md +152 -0
- package/dist/components/Pagination/i18n-sk.d.ts +17 -0
- package/dist/components/Pagination/i18n-sk.js +24 -0
- package/dist/components/Pagination/i18n.d.ts +41 -0
- package/dist/components/Pagination/i18n.js +49 -0
- package/dist/components/Pagination/index.css +50 -0
- package/dist/components/Pagination/index.d.ts +4 -0
- package/dist/components/Pagination/index.js +4 -0
- package/dist/components/Pagination/pagination-range.d.ts +21 -0
- package/dist/components/Pagination/pagination-range.js +41 -0
- package/dist/components/Pill/Pill.svelte +8 -2
- package/dist/components/Pill/Pill.svelte.d.ts +2 -0
- package/dist/components/Pill/README.md +35 -22
- package/dist/components/Pill/index.css +57 -18
- package/dist/components/Stat/README.md +157 -0
- package/dist/components/Stat/Stat.svelte +226 -0
- package/dist/components/Stat/Stat.svelte.d.ts +54 -0
- package/dist/components/Stat/index.css +217 -0
- package/dist/components/Stat/index.d.ts +1 -0
- package/dist/components/Stat/index.js +1 -0
- package/dist/components/Stepper/README.md +181 -0
- package/dist/components/Stepper/Stepper.svelte +240 -0
- package/dist/components/Stepper/Stepper.svelte.d.ts +86 -0
- package/dist/components/Stepper/i18n-sk.d.ts +17 -0
- package/dist/components/Stepper/i18n-sk.js +21 -0
- package/dist/components/Stepper/i18n.d.ts +35 -0
- package/dist/components/Stepper/i18n.js +43 -0
- package/dist/components/Stepper/index.css +302 -0
- package/dist/components/Stepper/index.d.ts +3 -0
- package/dist/components/Stepper/index.js +3 -0
- package/dist/components/TrendChart/README.md +123 -0
- package/dist/components/TrendChart/TrendChart.svelte +114 -0
- package/dist/components/TrendChart/TrendChart.svelte.d.ts +36 -0
- package/dist/components/TrendChart/index.css +51 -0
- package/dist/components/TrendChart/index.d.ts +1 -0
- package/dist/components/TrendChart/index.js +1 -0
- package/dist/icons/index.d.ts +4 -0
- package/dist/icons/index.js +4 -0
- package/dist/index.css +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/copy-to-clipboard.d.ts +43 -0
- package/dist/utils/copy-to-clipboard.js +102 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/docs/architecture.md +1 -1
- package/docs/domains/attachments.md +41 -4
- package/docs/domains/components.md +280 -13
- package/docs/domains/utils.md +8 -6
- package/docs/maybe-todo.md +132 -0
- package/docs/upgrading.md +6 -0
- package/package.json +11 -1
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
/* Status dot */
|
|
22
22
|
--stuic-pill-dot-size: 0.5rem;
|
|
23
23
|
|
|
24
|
+
/* Dismiss (X) button: a full-height square flush with the pill's end edge */
|
|
25
|
+
--stuic-pill-dismiss-min-size: 1.5rem; /* touch target floor (24px), whatever the pill size */
|
|
26
|
+
--stuic-pill-dismiss-icon-size: 1.125em; /* the X glyph box, relative to the pill font size */
|
|
27
|
+
--stuic-pill-dismiss-bg-hover: color-mix(in srgb, currentColor 12%, transparent);
|
|
28
|
+
--stuic-pill-dismiss-bg-active: color-mix(in srgb, currentColor 20%, transparent);
|
|
29
|
+
|
|
24
30
|
/* Size: sm */
|
|
25
31
|
--stuic-pill-padding-x-sm: 0.5rem;
|
|
26
32
|
--stuic-pill-padding-y-sm: 0.125rem;
|
|
@@ -145,13 +151,36 @@
|
|
|
145
151
|
min-height: var(--stuic-pill-min-height-lg);
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
/* Dismissible pills: drop vertical padding — the X button
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.stuic-pill[data-dismissible="true"]
|
|
152
|
-
.stuic-pill[data-dismissible="true"][data-size="lg"] {
|
|
154
|
+
/* Dismissible pills: drop vertical + end padding — the X button spans the full
|
|
155
|
+
height and sits flush with the end edge (its hover surface IS the end cap),
|
|
156
|
+
so any padding there would only make the pill oversized. */
|
|
157
|
+
.stuic-pill[data-dismissible="true"] {
|
|
153
158
|
padding-top: 0;
|
|
154
159
|
padding-bottom: 0;
|
|
160
|
+
padding-inline-end: 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* The X button is a square whose side follows the pill's min-height, but never
|
|
164
|
+
drops below the touch-target floor (so sm pills keep a 24px X). */
|
|
165
|
+
.stuic-pill[data-dismissible="true"][data-size="sm"] {
|
|
166
|
+
--_dismiss-size: max(
|
|
167
|
+
var(--stuic-pill-min-height-sm),
|
|
168
|
+
var(--stuic-pill-dismiss-min-size)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.stuic-pill[data-dismissible="true"][data-size="md"] {
|
|
173
|
+
--_dismiss-size: max(
|
|
174
|
+
var(--stuic-pill-min-height-md),
|
|
175
|
+
var(--stuic-pill-dismiss-min-size)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.stuic-pill[data-dismissible="true"][data-size="lg"] {
|
|
180
|
+
--_dismiss-size: max(
|
|
181
|
+
var(--stuic-pill-min-height-lg),
|
|
182
|
+
var(--stuic-pill-dismiss-min-size)
|
|
183
|
+
);
|
|
155
184
|
}
|
|
156
185
|
|
|
157
186
|
/* ============================================================================
|
|
@@ -448,31 +477,45 @@
|
|
|
448
477
|
cursor: not-allowed;
|
|
449
478
|
}
|
|
450
479
|
|
|
451
|
-
/* Dismiss X button
|
|
480
|
+
/* Dismiss X button: a full-height square segment flush with the pill's end edge.
|
|
481
|
+
The whole square (not just the glyph) is the hover/press surface — the same
|
|
482
|
+
treatment as SplitButton's secondary trigger. Radius is inherited from the pill,
|
|
483
|
+
so on rounded-full pills the hover surface coincides with the end cap. */
|
|
452
484
|
.stuic-pill-dismiss {
|
|
453
485
|
display: inline-flex;
|
|
454
486
|
align-items: center;
|
|
455
487
|
justify-content: center;
|
|
488
|
+
align-self: stretch;
|
|
489
|
+
min-width: var(--_dismiss-size, var(--stuic-pill-dismiss-min-size));
|
|
490
|
+
min-height: var(--_dismiss-size, var(--stuic-pill-dismiss-min-size));
|
|
456
491
|
appearance: none;
|
|
457
492
|
background: transparent;
|
|
458
493
|
border: 0;
|
|
494
|
+
border-radius: inherit;
|
|
459
495
|
padding: 0;
|
|
460
496
|
margin: 0;
|
|
461
497
|
color: inherit;
|
|
462
498
|
cursor: pointer;
|
|
463
499
|
opacity: 0.7;
|
|
464
|
-
transition:
|
|
500
|
+
transition:
|
|
501
|
+
opacity var(--stuic-pill-transition, var(--stuic-transition)),
|
|
502
|
+
background var(--stuic-pill-transition, var(--stuic-transition));
|
|
465
503
|
flex-shrink: 0;
|
|
466
504
|
}
|
|
467
505
|
|
|
468
506
|
.stuic-pill-dismiss:hover:not(:disabled) {
|
|
469
507
|
opacity: 1;
|
|
508
|
+
background: var(--stuic-pill-dismiss-bg-hover);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.stuic-pill-dismiss:active:not(:disabled) {
|
|
512
|
+
background: var(--stuic-pill-dismiss-bg-active);
|
|
470
513
|
}
|
|
471
514
|
|
|
515
|
+
/* Flush with the pill edge, so the ring goes inside the square */
|
|
472
516
|
.stuic-pill-dismiss:focus-visible {
|
|
473
517
|
outline: var(--stuic-pill-ring-width) solid var(--_ring, var(--stuic-pill-ring-color));
|
|
474
|
-
outline-offset:
|
|
475
|
-
border-radius: 9999px;
|
|
518
|
+
outline-offset: calc(-1 * var(--stuic-pill-ring-width));
|
|
476
519
|
}
|
|
477
520
|
|
|
478
521
|
.stuic-pill-dismiss:disabled {
|
|
@@ -480,14 +523,10 @@
|
|
|
480
523
|
cursor: not-allowed;
|
|
481
524
|
}
|
|
482
525
|
|
|
483
|
-
.
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
/* Tighten dismiss icon size per pill size */
|
|
489
|
-
.stuic-pill[data-size="sm"] .stuic-pill-dismiss svg {
|
|
490
|
-
width: 0.875em;
|
|
491
|
-
height: 0.875em;
|
|
526
|
+
/* The glyph box — deliberately smaller than the button, which keeps the target. */
|
|
527
|
+
.stuic-pill-dismiss-icon {
|
|
528
|
+
display: inline-flex;
|
|
529
|
+
width: var(--stuic-pill-dismiss-icon-size);
|
|
530
|
+
height: var(--stuic-pill-dismiss-icon-size);
|
|
492
531
|
}
|
|
493
532
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Stat
|
|
2
|
+
|
|
3
|
+
A KPI/stat card: a metric label, a prominent value, and an optional delta with trend
|
|
4
|
+
arrow and color-coded semantics (up/down/flat), plus an optional hint, corner icon, and
|
|
5
|
+
footer area (e.g. for a sparkline). Renders as a `<div>`, or as an `<a>`/`<button>` when
|
|
6
|
+
`href`/`onclick` is provided.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```svelte
|
|
11
|
+
<script>
|
|
12
|
+
import { Stat } from "@marianmeres/stuic";
|
|
13
|
+
// optional, for the sparkline footer example below (see TrendChart's README):
|
|
14
|
+
import { TrendChart } from "@marianmeres/stuic/trend-chart";
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<!-- Basic -->
|
|
18
|
+
<Stat
|
|
19
|
+
label="Total Revenue"
|
|
20
|
+
value="$45,231.89"
|
|
21
|
+
delta="+20.1%"
|
|
22
|
+
trend="up"
|
|
23
|
+
hint="vs. last month"
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<!-- Trend down (auto-colored destructive) -->
|
|
27
|
+
<Stat
|
|
28
|
+
label="Bounce Rate"
|
|
29
|
+
value="47.3%"
|
|
30
|
+
delta="+2.4%"
|
|
31
|
+
trend="up"
|
|
32
|
+
trendIntent="destructive"
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
<!-- Down is good here: override the auto color mapping -->
|
|
36
|
+
<Stat label="Churn" value="2.1%" delta="-0.4%" trend="down" trendIntent="success" />
|
|
37
|
+
|
|
38
|
+
<!-- With a corner icon -->
|
|
39
|
+
<Stat label="Active Users" value="2,338" delta="+180" trend="up">
|
|
40
|
+
{#snippet renderIcon()}
|
|
41
|
+
{@html iconUser({ size: 20 })}
|
|
42
|
+
{/snippet}
|
|
43
|
+
</Stat>
|
|
44
|
+
|
|
45
|
+
<!-- Clickable (renders as <a>) -->
|
|
46
|
+
<Stat
|
|
47
|
+
href="/analytics/revenue"
|
|
48
|
+
label="Revenue"
|
|
49
|
+
value="$45,231"
|
|
50
|
+
delta="+20.1%"
|
|
51
|
+
trend="up"
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
<!-- Footer (sparkline, actions, ...) — first-class pairing with the TrendChart
|
|
55
|
+
wrapper (subpath import; optional @marianmeres/trend-chart peer dep) -->
|
|
56
|
+
<Stat label="Requests" value="1.2M" delta="+8.4%" trend="up">
|
|
57
|
+
{#snippet renderFooter()}
|
|
58
|
+
<TrendChart data={[42, 51, 48, 63, 58, 71, 84]} sparkline />
|
|
59
|
+
{/snippet}
|
|
60
|
+
</Stat>
|
|
61
|
+
|
|
62
|
+
<!-- Full override -->
|
|
63
|
+
<Stat>
|
|
64
|
+
<div class="p-4">Completely custom content</div>
|
|
65
|
+
</Stat>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Props
|
|
69
|
+
|
|
70
|
+
| Prop | Type | Default | Description |
|
|
71
|
+
| ---------------- | ---------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
72
|
+
| `label` | `THC` | - | Small label above the value (metric name) |
|
|
73
|
+
| `value` | `THC \| number` | - | The primary metric value, preformatted (e.g. `"$45,231.89"`) |
|
|
74
|
+
| `delta` | `THC \| number` | - | Delta/change indicator, preformatted (e.g. `"+20.1%"`) |
|
|
75
|
+
| `trend` | `"up" \| "down" \| "flat"` | - | Trend direction — drives the arrow and (via `trendIntent="auto"`) the delta color |
|
|
76
|
+
| `trendIntent` | `"auto" \| "success" \| "destructive" \| "warning" \| "neutral"` | `"auto"` | Delta color semantics. `"auto"` maps up→success, down→destructive, flat→neutral. Set explicitly for metrics where down is good (churn, costs). |
|
|
77
|
+
| `showTrendArrow` | `boolean` | `true` | Show the trend direction arrow next to the delta |
|
|
78
|
+
| `hint` | `THC` | - | Secondary text next to the delta (e.g. "vs. last month") |
|
|
79
|
+
| `href` | `string` | - | Renders stat as `<a>` |
|
|
80
|
+
| `onclick` | `(e: MouseEvent) => void` | - | Renders stat as `<button>` |
|
|
81
|
+
| `disabled` | `boolean` | `false` | Disabled state (reduced opacity, no interaction) |
|
|
82
|
+
| `unstyled` | `boolean` | `false` | Skip all default styling |
|
|
83
|
+
| `class` | `string` | - | Additional CSS classes for the stat shell |
|
|
84
|
+
| `classLabel` | `string` | - | Additional CSS classes for the label |
|
|
85
|
+
| `classValue` | `string` | - | Additional CSS classes for the value |
|
|
86
|
+
| `classDelta` | `string` | - | Additional CSS classes for the delta |
|
|
87
|
+
| `classHint` | `string` | - | Additional CSS classes for the hint |
|
|
88
|
+
| `classFooter` | `string` | - | Additional CSS classes for the footer |
|
|
89
|
+
| `el` | `HTMLElement` | - | Bindable element reference |
|
|
90
|
+
|
|
91
|
+
## Snippets
|
|
92
|
+
|
|
93
|
+
| Snippet | Parameters | Description |
|
|
94
|
+
| -------------- | ---------- | ------------------------------------------------- |
|
|
95
|
+
| `children` | - | Overrides the entire stat body |
|
|
96
|
+
| `renderIcon` | - | Icon area in the top-right corner (next to label) |
|
|
97
|
+
| `renderFooter` | - | Footer area (sparkline, actions, metadata) |
|
|
98
|
+
|
|
99
|
+
## CSS Variables
|
|
100
|
+
|
|
101
|
+
| Variable | Default | Description |
|
|
102
|
+
| ------------------------------------- | --------------------------------------------------------- | ----------------------------------- |
|
|
103
|
+
| `--stuic-stat-bg` | `var(--stuic-color-card, var(--stuic-color-background))` | Background color |
|
|
104
|
+
| `--stuic-stat-bg-hover` | `var(--stuic-color-card-hover, var(--stuic-color-muted))` | Hover background (interactive only) |
|
|
105
|
+
| `--stuic-stat-border-width` | `1px` | Border width |
|
|
106
|
+
| `--stuic-stat-border` | `var(--stuic-color-border)` | Border color |
|
|
107
|
+
| `--stuic-stat-border-hover` | `var(--stuic-color-border-hover)` | Hover border color |
|
|
108
|
+
| `--stuic-stat-radius` | `var(--radius-lg)` | Border radius |
|
|
109
|
+
| `--stuic-stat-shadow` | `var(--shadow-sm)` | Box shadow |
|
|
110
|
+
| `--stuic-stat-shadow-hover` | `var(--shadow-md)` | Hover box shadow |
|
|
111
|
+
| `--stuic-stat-transition` | `150ms` | Transition duration |
|
|
112
|
+
| `--stuic-stat-padding` | `1rem` | Padding |
|
|
113
|
+
| `--stuic-stat-gap` | `0.375rem` | Vertical gap between rows |
|
|
114
|
+
| `--stuic-stat-meta-gap` | `0.5rem` | Gap between delta and hint |
|
|
115
|
+
| `--stuic-stat-delta-gap` | `0.25rem` | Gap between arrow and delta text |
|
|
116
|
+
| `--stuic-stat-label-font-size` | `var(--text-sm)` | Label font size |
|
|
117
|
+
| `--stuic-stat-label-font-weight` | `var(--font-weight-medium)` | Label font weight |
|
|
118
|
+
| `--stuic-stat-label-text` | `var(--stuic-color-muted-foreground)` | Label text color |
|
|
119
|
+
| `--stuic-stat-value-font-size` | `var(--text-2xl)` | Value font size |
|
|
120
|
+
| `--stuic-stat-value-font-weight` | `var(--font-weight-semibold)` | Value font weight |
|
|
121
|
+
| `--stuic-stat-value-line-height` | `1.2` | Value line height |
|
|
122
|
+
| `--stuic-stat-value-text` | `var(--stuic-color-foreground)` | Value text color |
|
|
123
|
+
| `--stuic-stat-delta-font-size` | `var(--text-sm)` | Delta font size |
|
|
124
|
+
| `--stuic-stat-delta-font-weight` | `var(--font-weight-medium)` | Delta font weight |
|
|
125
|
+
| `--stuic-stat-delta-text-success` | `var(--stuic-color-success)` | Delta color (success intent) |
|
|
126
|
+
| `--stuic-stat-delta-text-destructive` | `var(--stuic-color-destructive)` | Delta color (destructive intent) |
|
|
127
|
+
| `--stuic-stat-delta-text-warning` | `var(--stuic-color-warning)` | Delta color (warning intent) |
|
|
128
|
+
| `--stuic-stat-delta-text-neutral` | `var(--stuic-color-muted-foreground)` | Delta color (neutral intent) |
|
|
129
|
+
| `--stuic-stat-hint-font-size` | `var(--text-sm)` | Hint font size |
|
|
130
|
+
| `--stuic-stat-hint-text` | `var(--stuic-color-muted-foreground)` | Hint text color |
|
|
131
|
+
| `--stuic-stat-icon-text` | `var(--stuic-color-muted-foreground)` | Corner icon color |
|
|
132
|
+
| `--stuic-stat-ring-width` | `3px` | Focus ring width |
|
|
133
|
+
| `--stuic-stat-ring-color` | `var(--stuic-color-ring)` | Focus ring color |
|
|
134
|
+
| `--stuic-stat-opacity-disabled` | `0.5` | Disabled opacity |
|
|
135
|
+
|
|
136
|
+
## Notes
|
|
137
|
+
|
|
138
|
+
- The stat fills its parent container (`width: 100%; height: 100%`). Control sizing via
|
|
139
|
+
the parent element (typically a CSS grid of KPI cards).
|
|
140
|
+
- The arrow shows the **direction** (`trend`), the color shows the **sentiment**
|
|
141
|
+
(`trendIntent`). They are deliberately independent: a downward trend can be colored
|
|
142
|
+
as success (churn, costs, error rate) via `trendIntent="success"`.
|
|
143
|
+
- Values and deltas are rendered as-is — format numbers/percentages/currencies yourself
|
|
144
|
+
(e.g. via `Intl.NumberFormat`). Bare `number` values are stringified verbatim.
|
|
145
|
+
- The trend arrow is `aria-hidden`; make sure the delta text itself carries the sign
|
|
146
|
+
(`"+20.1%"`, `"-3%"`) so the direction survives for screen readers.
|
|
147
|
+
- All content props (`label`, `value`, `delta`, `hint`) accept the THC type (plain
|
|
148
|
+
string, HTML, component, or snippet).
|
|
149
|
+
- When using `href` or `onclick`, the entire stat becomes interactive with hover
|
|
150
|
+
effects. Avoid placing interactive elements inside `renderFooter` in this case, as it
|
|
151
|
+
creates invalid nested interactive HTML.
|
|
152
|
+
- Footer sticks to the bottom of the stat via `margin-top: auto` (relevant in
|
|
153
|
+
equal-height grids).
|
|
154
|
+
- For a sparkline footer, the first-class pairing is the `TrendChart` wrapper
|
|
155
|
+
(`import { TrendChart } from "@marianmeres/stuic/trend-chart"`) — a subpath
|
|
156
|
+
export whose `@marianmeres/trend-chart` peer dependency stays opt-in (nothing
|
|
157
|
+
is bundled unless you use it). See `src/lib/components/TrendChart/README.md`.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLAttributes, HTMLAnchorAttributes } from "svelte/elements";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
5
|
+
|
|
6
|
+
export type StatTrend = "up" | "down" | "flat";
|
|
7
|
+
|
|
8
|
+
export type StatTrendIntent =
|
|
9
|
+
"auto" | "success" | "destructive" | "warning" | "neutral";
|
|
10
|
+
|
|
11
|
+
export interface Props extends Omit<
|
|
12
|
+
HTMLAttributes<HTMLDivElement>,
|
|
13
|
+
"children" | "title"
|
|
14
|
+
> {
|
|
15
|
+
/** Small label above the value (metric name, e.g. "Revenue") */
|
|
16
|
+
label?: THC;
|
|
17
|
+
/** The primary metric value, preformatted (e.g. "$45,231.89") */
|
|
18
|
+
value?: THC | number;
|
|
19
|
+
/** Delta/change indicator, preformatted (e.g. "+20.1%") */
|
|
20
|
+
delta?: THC | number;
|
|
21
|
+
/** Trend direction — drives the arrow and (via `trendIntent="auto"`) the delta color */
|
|
22
|
+
trend?: StatTrend;
|
|
23
|
+
/**
|
|
24
|
+
* Delta color semantics. "auto" (default) maps up→success, down→destructive,
|
|
25
|
+
* flat→neutral. Set explicitly for metrics where down is good (churn, costs):
|
|
26
|
+
* e.g. `trend="down" trendIntent="success"`.
|
|
27
|
+
*/
|
|
28
|
+
trendIntent?: StatTrendIntent;
|
|
29
|
+
/** Show the trend direction arrow next to the delta */
|
|
30
|
+
showTrendArrow?: boolean;
|
|
31
|
+
/** Secondary text next to the delta (e.g. "vs. last month") */
|
|
32
|
+
hint?: THC;
|
|
33
|
+
/** When provided, the stat renders as <a> */
|
|
34
|
+
href?: string;
|
|
35
|
+
/** Disabled state */
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
/** Override the entire stat body */
|
|
38
|
+
children?: Snippet;
|
|
39
|
+
/** Icon area in the top-right corner (next to the label) */
|
|
40
|
+
renderIcon?: Snippet;
|
|
41
|
+
/** Footer area (sparkline, actions, metadata, etc.) */
|
|
42
|
+
renderFooter?: Snippet;
|
|
43
|
+
/** Skip all default styling */
|
|
44
|
+
unstyled?: boolean;
|
|
45
|
+
/** Additional CSS classes */
|
|
46
|
+
class?: string;
|
|
47
|
+
/** Class for the label */
|
|
48
|
+
classLabel?: string;
|
|
49
|
+
/** Class for the value */
|
|
50
|
+
classValue?: string;
|
|
51
|
+
/** Class for the delta */
|
|
52
|
+
classDelta?: string;
|
|
53
|
+
/** Class for the hint */
|
|
54
|
+
classHint?: string;
|
|
55
|
+
/** Class for the footer area */
|
|
56
|
+
classFooter?: string;
|
|
57
|
+
/** Bindable element reference */
|
|
58
|
+
el?: HTMLElement;
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<script lang="ts">
|
|
63
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
64
|
+
import Thc from "../Thc/Thc.svelte";
|
|
65
|
+
import { iconTrendingUp, iconTrendingDown, iconMinus } from "../../icons/index.js";
|
|
66
|
+
|
|
67
|
+
let {
|
|
68
|
+
label,
|
|
69
|
+
value,
|
|
70
|
+
delta,
|
|
71
|
+
trend,
|
|
72
|
+
trendIntent = "auto",
|
|
73
|
+
showTrendArrow = true,
|
|
74
|
+
hint,
|
|
75
|
+
href,
|
|
76
|
+
disabled = false,
|
|
77
|
+
children,
|
|
78
|
+
renderIcon,
|
|
79
|
+
renderFooter,
|
|
80
|
+
unstyled = false,
|
|
81
|
+
class: classProp,
|
|
82
|
+
classLabel: classLabelProp,
|
|
83
|
+
classValue: classValueProp,
|
|
84
|
+
classDelta: classDeltaProp,
|
|
85
|
+
classHint: classHintProp,
|
|
86
|
+
classFooter: classFooterProp,
|
|
87
|
+
el = $bindable(),
|
|
88
|
+
onclick,
|
|
89
|
+
...rest
|
|
90
|
+
}: Props = $props();
|
|
91
|
+
|
|
92
|
+
const TREND_ICONS: Record<StatTrend, () => string> = {
|
|
93
|
+
up: () => iconTrendingUp(),
|
|
94
|
+
down: () => iconTrendingDown(),
|
|
95
|
+
flat: () => iconMinus(),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
let _value = $derived(typeof value === "number" ? String(value) : value);
|
|
99
|
+
let _delta = $derived(typeof delta === "number" ? String(delta) : delta);
|
|
100
|
+
|
|
101
|
+
let _resolvedTrendIntent = $derived.by(() => {
|
|
102
|
+
if (trendIntent !== "auto") return trendIntent;
|
|
103
|
+
if (trend === "up") return "success";
|
|
104
|
+
if (trend === "down") return "destructive";
|
|
105
|
+
return "neutral";
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
let _showArrow = $derived(showTrendArrow && !!trend);
|
|
109
|
+
let _hasDelta = $derived((delta !== undefined && delta !== null) || _showArrow);
|
|
110
|
+
let _hasHeader = $derived(!!(label || renderIcon));
|
|
111
|
+
let _hasMeta = $derived(_hasDelta || !!hint);
|
|
112
|
+
|
|
113
|
+
let _class = $derived(unstyled ? classProp : twMerge("stuic-stat", classProp));
|
|
114
|
+
let _classLabel = $derived(
|
|
115
|
+
unstyled ? classLabelProp : twMerge("stuic-stat-label", classLabelProp)
|
|
116
|
+
);
|
|
117
|
+
let _classValue = $derived(
|
|
118
|
+
unstyled ? classValueProp : twMerge("stuic-stat-value", classValueProp)
|
|
119
|
+
);
|
|
120
|
+
let _classDelta = $derived(
|
|
121
|
+
unstyled ? classDeltaProp : twMerge("stuic-stat-delta", classDeltaProp)
|
|
122
|
+
);
|
|
123
|
+
let _classHint = $derived(
|
|
124
|
+
unstyled ? classHintProp : twMerge("stuic-stat-hint", classHintProp)
|
|
125
|
+
);
|
|
126
|
+
let _classFooter = $derived(
|
|
127
|
+
unstyled ? classFooterProp : twMerge("stuic-stat-footer", classFooterProp)
|
|
128
|
+
);
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
{#snippet statInner()}
|
|
132
|
+
{#if children}
|
|
133
|
+
{@render children()}
|
|
134
|
+
{:else}
|
|
135
|
+
{#if _hasHeader}
|
|
136
|
+
<div class={unstyled ? undefined : "stuic-stat-header"}>
|
|
137
|
+
{#if label}
|
|
138
|
+
<div class={_classLabel}>
|
|
139
|
+
<Thc thc={label} />
|
|
140
|
+
</div>
|
|
141
|
+
{/if}
|
|
142
|
+
{#if renderIcon}
|
|
143
|
+
<div class={unstyled ? undefined : "stuic-stat-icon"}>
|
|
144
|
+
{@render renderIcon()}
|
|
145
|
+
</div>
|
|
146
|
+
{/if}
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
149
|
+
{#if _value !== undefined && _value !== null}
|
|
150
|
+
<div class={_classValue}>
|
|
151
|
+
<Thc thc={_value} />
|
|
152
|
+
</div>
|
|
153
|
+
{/if}
|
|
154
|
+
{#if _hasMeta}
|
|
155
|
+
<div class={unstyled ? undefined : "stuic-stat-meta"}>
|
|
156
|
+
{#if _hasDelta}
|
|
157
|
+
<span class={_classDelta}>
|
|
158
|
+
{#if _showArrow && trend}
|
|
159
|
+
<span
|
|
160
|
+
class={unstyled ? undefined : "stuic-stat-trend-arrow"}
|
|
161
|
+
aria-hidden="true"
|
|
162
|
+
>
|
|
163
|
+
{@html TREND_ICONS[trend]()}
|
|
164
|
+
</span>
|
|
165
|
+
{/if}
|
|
166
|
+
{#if _delta !== undefined && _delta !== null}
|
|
167
|
+
<Thc thc={_delta} />
|
|
168
|
+
{/if}
|
|
169
|
+
</span>
|
|
170
|
+
{/if}
|
|
171
|
+
{#if hint}
|
|
172
|
+
<span class={_classHint}>
|
|
173
|
+
<Thc thc={hint} />
|
|
174
|
+
</span>
|
|
175
|
+
{/if}
|
|
176
|
+
</div>
|
|
177
|
+
{/if}
|
|
178
|
+
{#if renderFooter}
|
|
179
|
+
<div class={_classFooter}>
|
|
180
|
+
{@render renderFooter()}
|
|
181
|
+
</div>
|
|
182
|
+
{/if}
|
|
183
|
+
{/if}
|
|
184
|
+
{/snippet}
|
|
185
|
+
|
|
186
|
+
{#if href}
|
|
187
|
+
<a
|
|
188
|
+
{href}
|
|
189
|
+
bind:this={el}
|
|
190
|
+
class={_class}
|
|
191
|
+
data-trend={!unstyled && trend ? trend : undefined}
|
|
192
|
+
data-trend-intent={!unstyled && _hasDelta ? _resolvedTrendIntent : undefined}
|
|
193
|
+
data-interactive={!unstyled ? "" : undefined}
|
|
194
|
+
data-disabled={!unstyled && disabled ? "" : undefined}
|
|
195
|
+
aria-disabled={disabled ? "true" : undefined}
|
|
196
|
+
{...rest as HTMLAnchorAttributes}
|
|
197
|
+
>
|
|
198
|
+
{@render statInner()}
|
|
199
|
+
</a>
|
|
200
|
+
{:else if onclick}
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
bind:this={el}
|
|
204
|
+
class={_class}
|
|
205
|
+
data-trend={!unstyled && trend ? trend : undefined}
|
|
206
|
+
data-trend-intent={!unstyled && _hasDelta ? _resolvedTrendIntent : undefined}
|
|
207
|
+
data-interactive={!unstyled ? "" : undefined}
|
|
208
|
+
data-disabled={!unstyled && disabled ? "" : undefined}
|
|
209
|
+
{disabled}
|
|
210
|
+
{onclick}
|
|
211
|
+
{...rest as any}
|
|
212
|
+
>
|
|
213
|
+
{@render statInner()}
|
|
214
|
+
</button>
|
|
215
|
+
{:else}
|
|
216
|
+
<div
|
|
217
|
+
bind:this={el}
|
|
218
|
+
class={_class}
|
|
219
|
+
data-trend={!unstyled && trend ? trend : undefined}
|
|
220
|
+
data-trend-intent={!unstyled && _hasDelta ? _resolvedTrendIntent : undefined}
|
|
221
|
+
data-disabled={!unstyled && disabled ? "" : undefined}
|
|
222
|
+
{...rest}
|
|
223
|
+
>
|
|
224
|
+
{@render statInner()}
|
|
225
|
+
</div>
|
|
226
|
+
{/if}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
4
|
+
export type StatTrend = "up" | "down" | "flat";
|
|
5
|
+
export type StatTrendIntent = "auto" | "success" | "destructive" | "warning" | "neutral";
|
|
6
|
+
export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "title"> {
|
|
7
|
+
/** Small label above the value (metric name, e.g. "Revenue") */
|
|
8
|
+
label?: THC;
|
|
9
|
+
/** The primary metric value, preformatted (e.g. "$45,231.89") */
|
|
10
|
+
value?: THC | number;
|
|
11
|
+
/** Delta/change indicator, preformatted (e.g. "+20.1%") */
|
|
12
|
+
delta?: THC | number;
|
|
13
|
+
/** Trend direction — drives the arrow and (via `trendIntent="auto"`) the delta color */
|
|
14
|
+
trend?: StatTrend;
|
|
15
|
+
/**
|
|
16
|
+
* Delta color semantics. "auto" (default) maps up→success, down→destructive,
|
|
17
|
+
* flat→neutral. Set explicitly for metrics where down is good (churn, costs):
|
|
18
|
+
* e.g. `trend="down" trendIntent="success"`.
|
|
19
|
+
*/
|
|
20
|
+
trendIntent?: StatTrendIntent;
|
|
21
|
+
/** Show the trend direction arrow next to the delta */
|
|
22
|
+
showTrendArrow?: boolean;
|
|
23
|
+
/** Secondary text next to the delta (e.g. "vs. last month") */
|
|
24
|
+
hint?: THC;
|
|
25
|
+
/** When provided, the stat renders as <a> */
|
|
26
|
+
href?: string;
|
|
27
|
+
/** Disabled state */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** Override the entire stat body */
|
|
30
|
+
children?: Snippet;
|
|
31
|
+
/** Icon area in the top-right corner (next to the label) */
|
|
32
|
+
renderIcon?: Snippet;
|
|
33
|
+
/** Footer area (sparkline, actions, metadata, etc.) */
|
|
34
|
+
renderFooter?: Snippet;
|
|
35
|
+
/** Skip all default styling */
|
|
36
|
+
unstyled?: boolean;
|
|
37
|
+
/** Additional CSS classes */
|
|
38
|
+
class?: string;
|
|
39
|
+
/** Class for the label */
|
|
40
|
+
classLabel?: string;
|
|
41
|
+
/** Class for the value */
|
|
42
|
+
classValue?: string;
|
|
43
|
+
/** Class for the delta */
|
|
44
|
+
classDelta?: string;
|
|
45
|
+
/** Class for the hint */
|
|
46
|
+
classHint?: string;
|
|
47
|
+
/** Class for the footer area */
|
|
48
|
+
classFooter?: string;
|
|
49
|
+
/** Bindable element reference */
|
|
50
|
+
el?: HTMLElement;
|
|
51
|
+
}
|
|
52
|
+
declare const Stat: import("svelte").Component<Props, {}, "el">;
|
|
53
|
+
type Stat = ReturnType<typeof Stat>;
|
|
54
|
+
export default Stat;
|