@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.
Files changed (115) hide show
  1. package/AGENTS.md +6 -6
  2. package/dist/README.md +1 -1
  3. package/dist/attachments/index.d.ts +1 -0
  4. package/dist/attachments/index.js +1 -0
  5. package/dist/attachments/long-press.d.ts +43 -0
  6. package/dist/attachments/long-press.js +107 -0
  7. package/dist/components/Breadcrumbs/Breadcrumbs.svelte +198 -0
  8. package/dist/components/Breadcrumbs/Breadcrumbs.svelte.d.ts +60 -0
  9. package/dist/components/Breadcrumbs/README.md +174 -0
  10. package/dist/components/Breadcrumbs/i18n-sk.d.ts +17 -0
  11. package/dist/components/Breadcrumbs/i18n-sk.js +19 -0
  12. package/dist/components/Breadcrumbs/i18n.d.ts +31 -0
  13. package/dist/components/Breadcrumbs/i18n.js +40 -0
  14. package/dist/components/Breadcrumbs/index.css +84 -0
  15. package/dist/components/Breadcrumbs/index.d.ts +4 -0
  16. package/dist/components/Breadcrumbs/index.js +4 -0
  17. package/dist/components/Breadcrumbs/json-ld.d.ts +58 -0
  18. package/dist/components/Breadcrumbs/json-ld.js +56 -0
  19. package/dist/components/Collapsible/Collapsible.svelte +84 -15
  20. package/dist/components/Collapsible/Collapsible.svelte.d.ts +29 -2
  21. package/dist/components/Collapsible/README.md +82 -23
  22. package/dist/components/Collapsible/index.css +38 -0
  23. package/dist/components/Collapsible/index.d.ts +1 -1
  24. package/dist/components/ContextMenu/ContextMenu.svelte +302 -0
  25. package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +94 -0
  26. package/dist/components/ContextMenu/README.md +159 -0
  27. package/dist/components/ContextMenu/i18n-sk.d.ts +17 -0
  28. package/dist/components/ContextMenu/i18n-sk.js +18 -0
  29. package/dist/components/ContextMenu/i18n.d.ts +30 -0
  30. package/dist/components/ContextMenu/i18n.js +38 -0
  31. package/dist/components/ContextMenu/index.css +26 -0
  32. package/dist/components/ContextMenu/index.d.ts +3 -0
  33. package/dist/components/ContextMenu/index.js +3 -0
  34. package/dist/components/CopyButton/CopyButton.svelte +278 -0
  35. package/dist/components/CopyButton/CopyButton.svelte.d.ts +76 -0
  36. package/dist/components/CopyButton/README.md +241 -0
  37. package/dist/components/CopyButton/i18n-sk.d.ts +17 -0
  38. package/dist/components/CopyButton/i18n-sk.js +20 -0
  39. package/dist/components/CopyButton/i18n.d.ts +35 -0
  40. package/dist/components/CopyButton/i18n.js +43 -0
  41. package/dist/components/CopyButton/index.css +67 -0
  42. package/dist/components/CopyButton/index.d.ts +3 -0
  43. package/dist/components/CopyButton/index.js +3 -0
  44. package/dist/components/DataTable/DataTable.svelte +41 -34
  45. package/dist/components/DataTable/README.md +45 -0
  46. package/dist/components/DataTable/i18n-sk.js +1 -0
  47. package/dist/components/DataTable/i18n.d.ts +1 -0
  48. package/dist/components/DataTable/i18n.js +3 -0
  49. package/dist/components/DataTable/index.css +41 -3
  50. package/dist/components/EmptyState/EmptyState.svelte +121 -0
  51. package/dist/components/EmptyState/EmptyState.svelte.d.ts +40 -0
  52. package/dist/components/EmptyState/README.md +188 -0
  53. package/dist/components/EmptyState/index.css +154 -0
  54. package/dist/components/EmptyState/index.d.ts +1 -0
  55. package/dist/components/EmptyState/index.js +1 -0
  56. package/dist/components/Input/FieldOptions.svelte +92 -2
  57. package/dist/components/Input/FieldOptions.svelte.d.ts +13 -0
  58. package/dist/components/Input/README.md +66 -31
  59. package/dist/components/Input/_internal/FieldLikeChips.svelte +286 -0
  60. package/dist/components/Input/_internal/FieldLikeChips.svelte.d.ts +65 -0
  61. package/dist/components/Input/index.css +58 -0
  62. package/dist/components/Pagination/Pagination.svelte +250 -0
  63. package/dist/components/Pagination/Pagination.svelte.d.ts +69 -0
  64. package/dist/components/Pagination/README.md +152 -0
  65. package/dist/components/Pagination/i18n-sk.d.ts +17 -0
  66. package/dist/components/Pagination/i18n-sk.js +24 -0
  67. package/dist/components/Pagination/i18n.d.ts +41 -0
  68. package/dist/components/Pagination/i18n.js +49 -0
  69. package/dist/components/Pagination/index.css +50 -0
  70. package/dist/components/Pagination/index.d.ts +4 -0
  71. package/dist/components/Pagination/index.js +4 -0
  72. package/dist/components/Pagination/pagination-range.d.ts +21 -0
  73. package/dist/components/Pagination/pagination-range.js +41 -0
  74. package/dist/components/Pill/Pill.svelte +8 -2
  75. package/dist/components/Pill/Pill.svelte.d.ts +2 -0
  76. package/dist/components/Pill/README.md +35 -22
  77. package/dist/components/Pill/index.css +57 -18
  78. package/dist/components/Stat/README.md +157 -0
  79. package/dist/components/Stat/Stat.svelte +226 -0
  80. package/dist/components/Stat/Stat.svelte.d.ts +54 -0
  81. package/dist/components/Stat/index.css +217 -0
  82. package/dist/components/Stat/index.d.ts +1 -0
  83. package/dist/components/Stat/index.js +1 -0
  84. package/dist/components/Stepper/README.md +181 -0
  85. package/dist/components/Stepper/Stepper.svelte +240 -0
  86. package/dist/components/Stepper/Stepper.svelte.d.ts +86 -0
  87. package/dist/components/Stepper/i18n-sk.d.ts +17 -0
  88. package/dist/components/Stepper/i18n-sk.js +21 -0
  89. package/dist/components/Stepper/i18n.d.ts +35 -0
  90. package/dist/components/Stepper/i18n.js +43 -0
  91. package/dist/components/Stepper/index.css +302 -0
  92. package/dist/components/Stepper/index.d.ts +3 -0
  93. package/dist/components/Stepper/index.js +3 -0
  94. package/dist/components/TrendChart/README.md +123 -0
  95. package/dist/components/TrendChart/TrendChart.svelte +114 -0
  96. package/dist/components/TrendChart/TrendChart.svelte.d.ts +36 -0
  97. package/dist/components/TrendChart/index.css +51 -0
  98. package/dist/components/TrendChart/index.d.ts +1 -0
  99. package/dist/components/TrendChart/index.js +1 -0
  100. package/dist/icons/index.d.ts +4 -0
  101. package/dist/icons/index.js +4 -0
  102. package/dist/index.css +7 -0
  103. package/dist/index.d.ts +7 -0
  104. package/dist/index.js +12 -0
  105. package/dist/utils/copy-to-clipboard.d.ts +43 -0
  106. package/dist/utils/copy-to-clipboard.js +102 -0
  107. package/dist/utils/index.d.ts +1 -0
  108. package/dist/utils/index.js +1 -0
  109. package/docs/architecture.md +1 -1
  110. package/docs/domains/attachments.md +41 -4
  111. package/docs/domains/components.md +280 -13
  112. package/docs/domains/utils.md +8 -6
  113. package/docs/maybe-todo.md +132 -0
  114. package/docs/upgrading.md +6 -0
  115. 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 already provides
149
- visual height, extra padding-y just makes them oversized. */
150
- .stuic-pill[data-dismissible="true"][data-size="sm"],
151
- .stuic-pill[data-dismissible="true"][data-size="md"],
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: opacity var(--stuic-pill-transition, var(--stuic-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: 1px;
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
- .stuic-pill-dismiss svg {
484
- width: 1em;
485
- height: 1em;
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;