@marianmeres/stuic 3.166.0 → 3.168.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 (105) hide show
  1. package/AGENTS.md +6 -6
  2. package/API.md +14 -13
  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 +58 -36
  45. package/dist/components/DataTable/DataTable.svelte.d.ts +12 -0
  46. package/dist/components/DataTable/README.md +108 -33
  47. package/dist/components/DataTable/i18n-sk.js +1 -0
  48. package/dist/components/DataTable/i18n.d.ts +1 -0
  49. package/dist/components/DataTable/i18n.js +3 -0
  50. package/dist/components/DataTable/index.css +41 -3
  51. package/dist/components/EmptyState/EmptyState.svelte +121 -0
  52. package/dist/components/EmptyState/EmptyState.svelte.d.ts +40 -0
  53. package/dist/components/EmptyState/README.md +188 -0
  54. package/dist/components/EmptyState/index.css +154 -0
  55. package/dist/components/EmptyState/index.d.ts +1 -0
  56. package/dist/components/EmptyState/index.js +1 -0
  57. package/dist/components/Pagination/Pagination.svelte +250 -0
  58. package/dist/components/Pagination/Pagination.svelte.d.ts +69 -0
  59. package/dist/components/Pagination/README.md +152 -0
  60. package/dist/components/Pagination/i18n-sk.d.ts +17 -0
  61. package/dist/components/Pagination/i18n-sk.js +24 -0
  62. package/dist/components/Pagination/i18n.d.ts +41 -0
  63. package/dist/components/Pagination/i18n.js +49 -0
  64. package/dist/components/Pagination/index.css +50 -0
  65. package/dist/components/Pagination/index.d.ts +4 -0
  66. package/dist/components/Pagination/index.js +4 -0
  67. package/dist/components/Pagination/pagination-range.d.ts +21 -0
  68. package/dist/components/Pagination/pagination-range.js +41 -0
  69. package/dist/components/Stat/README.md +157 -0
  70. package/dist/components/Stat/Stat.svelte +226 -0
  71. package/dist/components/Stat/Stat.svelte.d.ts +54 -0
  72. package/dist/components/Stat/index.css +217 -0
  73. package/dist/components/Stat/index.d.ts +1 -0
  74. package/dist/components/Stat/index.js +1 -0
  75. package/dist/components/Stepper/README.md +181 -0
  76. package/dist/components/Stepper/Stepper.svelte +240 -0
  77. package/dist/components/Stepper/Stepper.svelte.d.ts +86 -0
  78. package/dist/components/Stepper/i18n-sk.d.ts +17 -0
  79. package/dist/components/Stepper/i18n-sk.js +21 -0
  80. package/dist/components/Stepper/i18n.d.ts +35 -0
  81. package/dist/components/Stepper/i18n.js +43 -0
  82. package/dist/components/Stepper/index.css +302 -0
  83. package/dist/components/Stepper/index.d.ts +3 -0
  84. package/dist/components/Stepper/index.js +3 -0
  85. package/dist/components/TrendChart/README.md +123 -0
  86. package/dist/components/TrendChart/TrendChart.svelte +114 -0
  87. package/dist/components/TrendChart/TrendChart.svelte.d.ts +36 -0
  88. package/dist/components/TrendChart/index.css +51 -0
  89. package/dist/components/TrendChart/index.d.ts +1 -0
  90. package/dist/components/TrendChart/index.js +1 -0
  91. package/dist/icons/index.d.ts +4 -0
  92. package/dist/icons/index.js +4 -0
  93. package/dist/index.css +7 -0
  94. package/dist/index.d.ts +7 -0
  95. package/dist/index.js +12 -0
  96. package/dist/utils/copy-to-clipboard.d.ts +43 -0
  97. package/dist/utils/copy-to-clipboard.js +102 -0
  98. package/dist/utils/index.d.ts +1 -0
  99. package/dist/utils/index.js +1 -0
  100. package/docs/architecture.md +1 -1
  101. package/docs/domains/attachments.md +41 -4
  102. package/docs/domains/components.md +280 -13
  103. package/docs/domains/utils.md +8 -6
  104. package/docs/maybe-todo.md +111 -0
  105. package/package.json +15 -5
@@ -0,0 +1,21 @@
1
+ /** One rendered slot of the "numbers" variant: a page number or an ellipsis gap. */
2
+ export type PaginationRangeItem = number | "ellipsis";
3
+ export interface PaginationRangeOptions {
4
+ /** How many pages to always show around the current page (default 1). */
5
+ siblingCount?: number;
6
+ /** How many pages to always show at the start and at the end (default 1). */
7
+ boundaryCount?: number;
8
+ }
9
+ /**
10
+ * Computes the windowed page list of the "numbers" variant: boundary pages at both
11
+ * ends, siblings around the current page, and `"ellipsis"` tokens for the collapsed
12
+ * gaps. The window is stable — for a given `pageCount` and options, every
13
+ * `currentPage` yields the same number of slots (no layout jumping while paging).
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * paginationRange(5, 10); // [1, "ellipsis", 4, 5, 6, "ellipsis", 10]
18
+ * paginationRange(1, 5); // [1, 2, 3, 4, 5]
19
+ * ```
20
+ */
21
+ export declare function paginationRange(currentPage: number, pageCount: number, options?: PaginationRangeOptions): PaginationRangeItem[];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Computes the windowed page list of the "numbers" variant: boundary pages at both
3
+ * ends, siblings around the current page, and `"ellipsis"` tokens for the collapsed
4
+ * gaps. The window is stable — for a given `pageCount` and options, every
5
+ * `currentPage` yields the same number of slots (no layout jumping while paging).
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * paginationRange(5, 10); // [1, "ellipsis", 4, 5, 6, "ellipsis", 10]
10
+ * paginationRange(1, 5); // [1, 2, 3, 4, 5]
11
+ * ```
12
+ */
13
+ export function paginationRange(currentPage, pageCount, options = {}) {
14
+ const siblingCount = Math.max(0, Math.floor(options.siblingCount ?? 1));
15
+ const boundaryCount = Math.max(0, Math.floor(options.boundaryCount ?? 1));
16
+ if (!Number.isFinite(pageCount) || pageCount <= 0)
17
+ return [];
18
+ const page = Math.min(Math.max(1, Math.floor(currentPage) || 1), pageCount);
19
+ const range = (start, end) => Array.from({ length: end - start + 1 }, (_, i) => start + i);
20
+ const startPages = range(1, Math.min(boundaryCount, pageCount));
21
+ const endPages = range(Math.max(pageCount - boundaryCount + 1, boundaryCount + 1), pageCount);
22
+ const siblingsStart = Math.max(Math.min(page - siblingCount, pageCount - boundaryCount - siblingCount * 2 - 1), boundaryCount + 2);
23
+ const siblingsEnd = Math.min(Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2), endPages.length > 0 ? endPages[0] - 2 : pageCount - 1);
24
+ return [
25
+ ...startPages,
26
+ // start gap: ellipsis, the single page it would hide, or nothing
27
+ ...(siblingsStart > boundaryCount + 2
28
+ ? ["ellipsis"]
29
+ : boundaryCount + 1 < pageCount - boundaryCount
30
+ ? [boundaryCount + 1]
31
+ : []),
32
+ ...range(siblingsStart, siblingsEnd),
33
+ // end gap: ellipsis, the single page it would hide, or nothing
34
+ ...(siblingsEnd < pageCount - boundaryCount - 1
35
+ ? ["ellipsis"]
36
+ : pageCount - boundaryCount > boundaryCount
37
+ ? [pageCount - boundaryCount]
38
+ : []),
39
+ ...endPages,
40
+ ];
41
+ }
@@ -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;