@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
@@ -0,0 +1,217 @@
1
+ /* ============================================================================
2
+ STAT COMPONENT TOKENS
3
+ Override globally: :root { --stuic-stat-radius: 0; }
4
+ Override locally: <Stat style="--stuic-stat-value-font-size: 3rem;">
5
+ ============================================================================ */
6
+
7
+ :root {
8
+ --stuic-stat-bg: var(--stuic-color-card, var(--stuic-color-background));
9
+ --stuic-stat-bg-hover: var(--stuic-color-card-hover, var(--stuic-color-muted));
10
+ --stuic-stat-border: var(--stuic-color-border);
11
+ --stuic-stat-border-hover: var(--stuic-color-border-hover);
12
+
13
+ /* Layout */
14
+ --stuic-stat-padding: 1rem;
15
+ --stuic-stat-gap: 0.375rem;
16
+ --stuic-stat-meta-gap: 0.5rem;
17
+ --stuic-stat-delta-gap: 0.25rem;
18
+
19
+ /* Label */
20
+ --stuic-stat-label-font-size: var(--text-sm);
21
+ --stuic-stat-label-font-weight: var(--font-weight-medium);
22
+ --stuic-stat-label-text: var(--stuic-color-muted-foreground);
23
+
24
+ /* Value */
25
+ --stuic-stat-value-font-size: var(--text-2xl);
26
+ --stuic-stat-value-font-weight: var(--font-weight-semibold);
27
+ --stuic-stat-value-line-height: 1.2;
28
+ --stuic-stat-value-text: var(--stuic-color-foreground);
29
+
30
+ /* Delta */
31
+ --stuic-stat-delta-font-size: var(--text-sm);
32
+ --stuic-stat-delta-font-weight: var(--font-weight-medium);
33
+ --stuic-stat-delta-text-success: var(--stuic-color-success);
34
+ --stuic-stat-delta-text-destructive: var(--stuic-color-destructive);
35
+ --stuic-stat-delta-text-warning: var(--stuic-color-warning);
36
+ --stuic-stat-delta-text-neutral: var(--stuic-color-muted-foreground);
37
+
38
+ /* Hint */
39
+ --stuic-stat-hint-font-size: var(--text-sm);
40
+ --stuic-stat-hint-text: var(--stuic-color-muted-foreground);
41
+
42
+ /* Icon */
43
+ --stuic-stat-icon-text: var(--stuic-color-muted-foreground);
44
+
45
+ /* Focus ring */
46
+ --stuic-stat-ring-width: 3px;
47
+ --stuic-stat-ring-color: var(--stuic-color-ring);
48
+
49
+ /* Disabled */
50
+ --stuic-stat-opacity-disabled: 0.5;
51
+ }
52
+
53
+ @layer components {
54
+ /* ============================================================================
55
+ BASE STYLES
56
+ ============================================================================ */
57
+
58
+ .stuic-stat {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: var(--stuic-stat-gap);
62
+ width: 100%;
63
+ height: 100%;
64
+ padding: var(--stuic-stat-padding);
65
+ background: var(--stuic-stat-bg);
66
+ border: var(--stuic-stat-border-width, var(--stuic-border-width)) solid
67
+ var(--stuic-stat-border);
68
+ border-radius: var(--stuic-stat-radius, var(--stuic-radius-container));
69
+ box-shadow: var(--stuic-stat-shadow, var(--stuic-shadow));
70
+ transition:
71
+ background var(--stuic-stat-transition, var(--stuic-transition)),
72
+ border-color var(--stuic-stat-transition, var(--stuic-transition)),
73
+ box-shadow var(--stuic-stat-transition, var(--stuic-transition));
74
+ text-decoration: none;
75
+ color: inherit;
76
+ text-align: start;
77
+ }
78
+
79
+ /* Reset button styling */
80
+ button.stuic-stat {
81
+ font: inherit;
82
+ cursor: pointer;
83
+ -webkit-tap-highlight-color: transparent;
84
+ }
85
+
86
+ /* ============================================================================
87
+ INTERACTIVE HOVER (only when href or onclick)
88
+ ============================================================================ */
89
+
90
+ .stuic-stat[data-interactive]:hover:not([data-disabled]) {
91
+ background: var(--stuic-stat-bg-hover);
92
+ border-color: var(--stuic-stat-border-hover);
93
+ box-shadow: var(--stuic-stat-shadow-hover, var(--stuic-shadow-hover));
94
+ cursor: pointer;
95
+ }
96
+
97
+ /* ============================================================================
98
+ FOCUS
99
+ ============================================================================ */
100
+
101
+ .stuic-stat:focus-visible {
102
+ outline: var(--stuic-stat-ring-width) solid var(--stuic-stat-ring-color);
103
+ outline-offset: 0px;
104
+ }
105
+
106
+ /* ============================================================================
107
+ DISABLED
108
+ ============================================================================ */
109
+
110
+ .stuic-stat[data-disabled] {
111
+ opacity: var(--stuic-stat-opacity-disabled);
112
+ cursor: not-allowed;
113
+ pointer-events: none;
114
+ }
115
+
116
+ /* ============================================================================
117
+ HEADER (label + icon)
118
+ ============================================================================ */
119
+
120
+ .stuic-stat-header {
121
+ display: flex;
122
+ align-items: flex-start;
123
+ justify-content: space-between;
124
+ gap: 0.5rem;
125
+ }
126
+
127
+ .stuic-stat-label {
128
+ font-size: var(--stuic-stat-label-font-size);
129
+ font-weight: var(--stuic-stat-label-font-weight);
130
+ color: var(--stuic-stat-label-text);
131
+ }
132
+
133
+ .stuic-stat-icon {
134
+ color: var(--stuic-stat-icon-text);
135
+ flex-shrink: 0;
136
+ }
137
+
138
+ /* ============================================================================
139
+ VALUE
140
+ ============================================================================ */
141
+
142
+ .stuic-stat-value {
143
+ font-size: var(--stuic-stat-value-font-size);
144
+ font-weight: var(--stuic-stat-value-font-weight);
145
+ line-height: var(--stuic-stat-value-line-height);
146
+ color: var(--stuic-stat-value-text);
147
+ font-variant-numeric: tabular-nums;
148
+ }
149
+
150
+ /* ============================================================================
151
+ META (delta + hint)
152
+ ============================================================================ */
153
+
154
+ .stuic-stat-meta {
155
+ display: flex;
156
+ align-items: baseline;
157
+ flex-wrap: wrap;
158
+ gap: var(--stuic-stat-meta-gap);
159
+ }
160
+
161
+ /* Deliberately NOT a flex container. A flex box exports its FIRST item's
162
+ baseline, which here is the arrow — and a replaced <svg> has no text
163
+ baseline, so it synthesizes one from its bottom edge. Centered on the line
164
+ that edge sits ~0.2em below the delta text's baseline, and the row's
165
+ baseline alignment then pushed the hint down by that much. Inline layout
166
+ exports the text baseline, so delta text and hint sit on the same line. */
167
+ .stuic-stat-delta {
168
+ font-size: var(--stuic-stat-delta-font-size);
169
+ font-weight: var(--stuic-stat-delta-font-weight);
170
+ }
171
+
172
+ /* Delta color by resolved trend intent (root data attribute) */
173
+ .stuic-stat[data-trend-intent="success"] .stuic-stat-delta {
174
+ color: var(--stuic-stat-delta-text-success);
175
+ }
176
+
177
+ .stuic-stat[data-trend-intent="destructive"] .stuic-stat-delta {
178
+ color: var(--stuic-stat-delta-text-destructive);
179
+ }
180
+
181
+ .stuic-stat[data-trend-intent="warning"] .stuic-stat-delta {
182
+ color: var(--stuic-stat-delta-text-warning);
183
+ }
184
+
185
+ .stuic-stat[data-trend-intent="neutral"] .stuic-stat-delta {
186
+ color: var(--stuic-stat-delta-text-neutral);
187
+ }
188
+
189
+ .stuic-stat-trend-arrow {
190
+ display: inline-block;
191
+ margin-inline-end: var(--stuic-stat-delta-gap);
192
+ /* Optical centering: a 1em icon reads as centered on the text when its
193
+ box hangs slightly below the baseline. */
194
+ vertical-align: -0.125em;
195
+ }
196
+
197
+ /* display:block so the icon defines no inner line box — the wrapper's
198
+ baseline stays its bottom edge, which is what vertical-align shifts. */
199
+ .stuic-stat-trend-arrow svg {
200
+ display: block;
201
+ width: 1em;
202
+ height: 1em;
203
+ }
204
+
205
+ .stuic-stat-hint {
206
+ font-size: var(--stuic-stat-hint-font-size);
207
+ color: var(--stuic-stat-hint-text);
208
+ }
209
+
210
+ /* ============================================================================
211
+ FOOTER
212
+ ============================================================================ */
213
+
214
+ .stuic-stat-footer {
215
+ margin-top: auto;
216
+ }
217
+ }
@@ -0,0 +1 @@
1
+ export { default as Stat, type Props as StatProps, type StatTrend, type StatTrendIntent, } from "./Stat.svelte";
@@ -0,0 +1 @@
1
+ export { default as Stat, } from "./Stat.svelte";
@@ -0,0 +1,181 @@
1
+ # Stepper
2
+
3
+ Numbered multi-step progress header for wizard/checkout flows. Renders the steps as
4
+ an accessible `<nav>` landmark with an ordered list: completed steps get a check
5
+ mark and filled connector, the current step is highlighted (and marked
6
+ `aria-current="step"`), upcoming steps stay muted. Display-only by default; pass
7
+ `onSelect` to make steps clickable (completed-only by default — the "go back and
8
+ revisit" wizard rule).
9
+
10
+ Pairs naturally with [`@marianmeres/wizard`](https://github.com/marianmeres/wizard)
11
+ — `current` is a zero-based index, exactly the wizard's `step.index` (see below).
12
+
13
+ ## Props
14
+
15
+ | Prop | Type | Default | Description |
16
+ | ------------------ | -------------------------------- | -------------- | -------------------------------------------------------------------------------------------- |
17
+ | `steps` | `(StepperStep \| string)[]` | required | The steps; plain strings are shorthand for `{ label }` |
18
+ | `current` | `number` | `0` | Zero-based current step index; `steps.length` = all completed (wizard done); clamped |
19
+ | `onSelect` | `(index, step) => void` | - | Navigation callback; when present, steps render as buttons |
20
+ | `clickable` | `"none" \| "completed" \| "all"` | `"completed"` | Which steps may be clicked when `onSelect` is present (the current step never fires) |
21
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Layout direction |
22
+ | `labelPosition` | `"end" \| "below"` | `"end"` | Horizontal only: labels beside the indicator, or centered below it (classic checkout header) |
23
+ | `disabled` | `boolean` | `false` | Disable all interaction (e.g. while a step is submitting) |
24
+ | `t` | `TranslateFn` | English | i18n translate function (see below) |
25
+ | `unstyled` | `boolean` | `false` | Skip all default styling |
26
+ | `class` | `string` | - | Additional CSS classes (merged via twMerge) |
27
+ | `classStep` | `string` | - | Class for every step item (`li`) |
28
+ | `classIndicator` | `string` | - | Class for the indicator bubble |
29
+ | `classLabel` | `string` | - | Class for the label |
30
+ | `classDescription` | `string` | - | Class for the description |
31
+ | `classConnector` | `string` | - | Class for the connector line between steps |
32
+ | `el` | `HTMLElement` | - | Element reference (bindable) |
33
+
34
+ ### `StepperStep`
35
+
36
+ | Field | Type | Description |
37
+ | ------------- | --------- | ------------------------------------------------------------------------------------------------- |
38
+ | `label` | `THC` | Step label (text, html, or component — see `Thc`) |
39
+ | `description` | `THC` | Optional secondary line under the label |
40
+ | `icon` | `string` | Raw svg/html string rendered in the indicator instead of the number (completed/error markers win) |
41
+ | `error` | `boolean` | Marks the step as failed — destructive coloring + "×" indicator |
42
+ | `disabled` | `boolean` | Never clickable, regardless of the `clickable` policy |
43
+
44
+ ## Snippet Props
45
+
46
+ | Snippet | Description |
47
+ | ----------------- | -------------------------------------------------------------- |
48
+ | `renderIndicator` | Override the bubble content; receives `{ index, step, state }` |
49
+
50
+ ## Usage
51
+
52
+ ### Basic
53
+
54
+ ```svelte
55
+ <script lang="ts">
56
+ import { Stepper } from "@marianmeres/stuic";
57
+
58
+ let current = $state(1);
59
+ </script>
60
+
61
+ <Stepper steps={["Account", "Payment", "Confirm"]} {current} />
62
+ ```
63
+
64
+ ### Clickable (go back to a completed step)
65
+
66
+ ```svelte
67
+ <Stepper
68
+ steps={["Account", "Payment", "Confirm"]}
69
+ {current}
70
+ onSelect={(index) => (current = index)}
71
+ />
72
+
73
+ <!-- free navigation to any non-disabled step -->
74
+ <Stepper {steps} {current} clickable="all" onSelect={(index) => (current = index)} />
75
+ ```
76
+
77
+ ### Descriptions, error state
78
+
79
+ ```svelte
80
+ <Stepper
81
+ current={2}
82
+ steps={[
83
+ { label: "Account", description: "Your credentials" },
84
+ { label: "Payment", description: "Card details", error: true },
85
+ { label: "Confirm", description: "Review the order" },
86
+ ]}
87
+ />
88
+ ```
89
+
90
+ ### Vertical / labels below
91
+
92
+ ```svelte
93
+ <Stepper {steps} {current} orientation="vertical" />
94
+ <Stepper {steps} {current} labelPosition="below" />
95
+ ```
96
+
97
+ ### With `@marianmeres/wizard`
98
+
99
+ `current` is a zero-based index, exactly the wizard's `step.index`:
100
+
101
+ ```svelte
102
+ <script lang="ts">
103
+ import { Stepper } from "@marianmeres/stuic";
104
+ import { createWizard } from "@marianmeres/wizard";
105
+
106
+ const wizard = createWizard("checkout", {
107
+ steps: [{ label: "Account" }, { label: "Payment" }, { label: "Confirm" }],
108
+ });
109
+ </script>
110
+
111
+ <Stepper
112
+ steps={$wizard.steps.map((s) => ({ label: s.label, error: !!s.error }))}
113
+ current={$wizard.isDone ? $wizard.steps.length : $wizard.step.index}
114
+ />
115
+ ```
116
+
117
+ ### i18n
118
+
119
+ Built-in English; bundled Slovak (`STEPPER_MESSAGES_SK`) is opt-in. Only the `<nav>`
120
+ landmark label and the screen-reader-only step announcements are localized — the
121
+ step labels are yours.
122
+
123
+ ```svelte
124
+ <script lang="ts">
125
+ import { Stepper, createStepperT, STEPPER_MESSAGES_SK } from "@marianmeres/stuic";
126
+ const t = createStepperT(STEPPER_MESSAGES_SK);
127
+ </script>
128
+
129
+ <Stepper {steps} {current} {t} />
130
+ ```
131
+
132
+ ### Custom indicator
133
+
134
+ ```svelte
135
+ <Stepper {steps} {current}>
136
+ {#snippet renderIndicator({ index, state })}
137
+ {state === "completed" ? "✓" : index + 1}
138
+ {/snippet}
139
+ </Stepper>
140
+ ```
141
+
142
+ ## Accessibility
143
+
144
+ - Renders a `<nav aria-label="Progress">` landmark (label localized via `t`) with an
145
+ ordered list; the current step is marked `aria-current="step"`.
146
+ - The indicator bubble is `aria-hidden` — a visually hidden text announces
147
+ "Step X of Y" plus the completed/failed state instead, so icon-only information is
148
+ never lost to screen readers.
149
+ - Clickable steps are real `<button>`s; ineligible ones are disabled.
150
+
151
+ ## CSS Variables
152
+
153
+ | Variable | Default | Description |
154
+ | ---------------------------------------------------------------------- | ---------------------------------- | ---------------------------------------- |
155
+ | `--stuic-stepper-gap` | `0.75rem` | Indicator↔label gap + step↔connector gap |
156
+ | `--stuic-stepper-gap-vertical` | `1.75rem` | Vertical: distance between steps |
157
+ | `--stuic-stepper-connector-min-length` | `1.5rem` | Horizontal: shortest connector |
158
+ | `--stuic-stepper-indicator-size` | `2rem` | Bubble diameter |
159
+ | `--stuic-stepper-indicator-border-width` | `2px` | Bubble border width |
160
+ | `--stuic-stepper-indicator-radius` | full circle | Bubble radius |
161
+ | `--stuic-stepper-indicator-font-size` | `--text-sm` | Bubble number font size |
162
+ | `--stuic-stepper-indicator-bg` / `-text` / `-border` | upcoming colors | Bubble, upcoming state |
163
+ | `--stuic-stepper-indicator-bg-current` / `-text-current` / … | `--stuic-color-primary` accents | Bubble, current state |
164
+ | `--stuic-stepper-indicator-bg-completed` / `-text-completed` / … | `--stuic-color-primary` filled | Bubble, completed state |
165
+ | `--stuic-stepper-indicator-bg-error` / `-text-error` / `-border-error` | `--stuic-color-destructive` filled | Bubble, error state |
166
+ | `--stuic-stepper-label-font-size` | `--text-base` | Label font size |
167
+ | `--stuic-stepper-label-text` / `-text-current` / `-text-completed` / … | muted / foreground / destructive | Label color per state |
168
+ | `--stuic-stepper-description-font-size` | `--text-sm` | Description font size |
169
+ | `--stuic-stepper-description-text` | `--stuic-color-muted-foreground` | Description color |
170
+ | `--stuic-stepper-connector-thickness` | `2px` | Connector line thickness |
171
+ | `--stuic-stepper-connector-bg` / `-bg-completed` | border / primary | Connector color |
172
+ | `--stuic-stepper-ring-width` / `--stuic-stepper-ring-color` | `3px` / `--stuic-color-ring` | Focus ring of clickable steps |
173
+ | `--stuic-stepper-opacity-disabled` | `0.5` | Disabled opacity |
174
+
175
+ ## Data Attributes
176
+
177
+ - `data-orientation` - `"horizontal" | "vertical"` (root)
178
+ - `data-label-position` - `"end" | "below"` (root, horizontal only)
179
+ - `data-disabled` - empty attribute (root when `disabled`; step `li` when `step.disabled`)
180
+ - `data-state` - `"completed" | "current" | "upcoming"` (step `li`); `"completed"` on filled connectors
181
+ - `data-error` - empty attribute (step `li` when `step.error`)
@@ -0,0 +1,240 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { Snippet } from "svelte";
4
+ import type { THC } from "../Thc/Thc.svelte";
5
+ import type { TranslateFn } from "../../types.js";
6
+
7
+ export type StepperOrientation = "horizontal" | "vertical";
8
+
9
+ export type StepperLabelPosition = "end" | "below";
10
+
11
+ export type StepperStepState = "completed" | "current" | "upcoming";
12
+
13
+ export type StepperClickable = "none" | "completed" | "all";
14
+
15
+ export interface StepperStep {
16
+ /** Step label */
17
+ label: THC;
18
+ /** Optional secondary line under the label */
19
+ description?: THC;
20
+ /**
21
+ * Raw svg/html string rendered inside the indicator instead of the step number
22
+ * (e.g. an `@marianmeres/icons-fns` render result). Completed/error markers
23
+ * still take precedence.
24
+ */
25
+ icon?: string;
26
+ /** Marks the step as failed — destructive coloring + "×" indicator */
27
+ error?: boolean;
28
+ /** Never clickable, regardless of the `clickable` policy */
29
+ disabled?: boolean;
30
+ }
31
+
32
+ export interface Props extends Omit<HTMLAttributes<HTMLElement>, "children"> {
33
+ /** The steps to render. Plain strings are shorthand for `{ label }`. */
34
+ steps: (StepperStep | string)[];
35
+ /**
36
+ * Zero-based index of the current step (default 0) — aligns with
37
+ * `@marianmeres/wizard`'s `step.index`. Steps before it render as completed,
38
+ * steps after it as upcoming. Pass `steps.length` to mark every step
39
+ * completed (wizard done). Out-of-range values are clamped.
40
+ */
41
+ current?: number;
42
+ /**
43
+ * Navigation callback. When provided (and `clickable` is not "none"), steps
44
+ * render as buttons and eligible ones fire this on click. The current step
45
+ * never fires.
46
+ */
47
+ onSelect?: (index: number, step: StepperStep) => void;
48
+ /**
49
+ * Which steps may be clicked when `onSelect` is present. `"completed"`
50
+ * (default): only completed steps — the "go back and revisit" wizard rule.
51
+ * `"all"`: any non-disabled step. `"none"`: display-only.
52
+ */
53
+ clickable?: StepperClickable;
54
+ /** Layout direction (default "horizontal") */
55
+ orientation?: StepperOrientation;
56
+ /**
57
+ * Horizontal orientation only: labels to the right of the indicator
58
+ * (`"end"`, default) or centered below it (`"below"` — the classic
59
+ * checkout header).
60
+ */
61
+ labelPosition?: StepperLabelPosition;
62
+ /** Disable all interaction (e.g. while a step is submitting) */
63
+ disabled?: boolean;
64
+ /** i18n translate function (see `createStepperT`) */
65
+ t?: TranslateFn;
66
+ /** Override the indicator bubble content */
67
+ renderIndicator?: Snippet<
68
+ [{ index: number; step: StepperStep; state: StepperStepState }]
69
+ >;
70
+ /** Skip all default styling */
71
+ unstyled?: boolean;
72
+ /** Additional CSS classes */
73
+ class?: string;
74
+ /** Class for every step item (`li`) */
75
+ classStep?: string;
76
+ /** Class for the indicator bubble */
77
+ classIndicator?: string;
78
+ /** Class for the label */
79
+ classLabel?: string;
80
+ /** Class for the description */
81
+ classDescription?: string;
82
+ /** Class for the connector line between steps */
83
+ classConnector?: string;
84
+ /** Bindable element reference */
85
+ el?: HTMLElement;
86
+ }
87
+ </script>
88
+
89
+ <script lang="ts">
90
+ import { twMerge } from "../../utils/tw-merge.js";
91
+ import Thc from "../Thc/Thc.svelte";
92
+ import { iconCheck, iconX } from "../../icons/index.js";
93
+ import { t_default } from "./i18n.js";
94
+
95
+ let {
96
+ steps,
97
+ current = 0,
98
+ onSelect,
99
+ clickable = "completed",
100
+ orientation = "horizontal",
101
+ labelPosition = "end",
102
+ disabled = false,
103
+ t = t_default,
104
+ renderIndicator,
105
+ unstyled = false,
106
+ class: classProp,
107
+ classStep: classStepProp,
108
+ classIndicator: classIndicatorProp,
109
+ classLabel: classLabelProp,
110
+ classDescription: classDescriptionProp,
111
+ classConnector: classConnectorProp,
112
+ el = $bindable(),
113
+ ...rest
114
+ }: Props = $props();
115
+
116
+ let _steps: StepperStep[] = $derived(
117
+ steps.map((s) => (typeof s === "string" ? { label: s } : s))
118
+ );
119
+
120
+ // clamped to [0, steps.length] — the extra slot means "all completed"
121
+ let _current = $derived(Math.min(Math.max(0, Math.floor(current)), _steps.length));
122
+
123
+ let _interactive = $derived(!!onSelect && clickable !== "none");
124
+
125
+ let _class = $derived(unstyled ? classProp : twMerge("stuic-stepper", classProp));
126
+ let _classStep = $derived(
127
+ unstyled ? classStepProp : twMerge("stuic-stepper-item", classStepProp)
128
+ );
129
+ let _classIndicator = $derived(
130
+ unstyled ? classIndicatorProp : twMerge("stuic-stepper-indicator", classIndicatorProp)
131
+ );
132
+ let _classLabel = $derived(
133
+ unstyled ? classLabelProp : twMerge("stuic-stepper-label", classLabelProp)
134
+ );
135
+ let _classDescription = $derived(
136
+ unstyled
137
+ ? classDescriptionProp
138
+ : twMerge("stuic-stepper-description", classDescriptionProp)
139
+ );
140
+ let _classConnector = $derived(
141
+ unstyled ? classConnectorProp : twMerge("stuic-stepper-connector", classConnectorProp)
142
+ );
143
+
144
+ function stateOf(i: number): StepperStepState {
145
+ if (i < _current) return "completed";
146
+ if (i === _current) return "current";
147
+ return "upcoming";
148
+ }
149
+
150
+ function isEligible(step: StepperStep, state: StepperStepState): boolean {
151
+ if (disabled || step.disabled || state === "current") return false;
152
+ return clickable === "all" || state === "completed";
153
+ }
154
+
155
+ function select(i: number) {
156
+ const step = _steps[i];
157
+ if (!_interactive || !isEligible(step, stateOf(i))) return;
158
+ onSelect?.(i, step);
159
+ }
160
+
161
+ // icon-only state info for screen readers ("Step 2 of 4, Completed")
162
+ function srText(i: number, step: StepperStep, state: StepperStepState): string {
163
+ const parts = [t("step_x_of_y", { step: i + 1, stepCount: _steps.length })];
164
+ if (step.error) parts.push(t("failed"));
165
+ else if (state === "completed") parts.push(t("completed"));
166
+ return parts.join(", ");
167
+ }
168
+ </script>
169
+
170
+ {#snippet stepInner(i: number, step: StepperStep, state: StepperStepState)}
171
+ <span class={_classIndicator} aria-hidden="true">
172
+ {#if renderIndicator}
173
+ {@render renderIndicator({ index: i, step, state })}
174
+ {:else if step.error}
175
+ {@html iconX()}
176
+ {:else if state === "completed"}
177
+ {@html iconCheck()}
178
+ {:else if step.icon}
179
+ {@html step.icon}
180
+ {:else}
181
+ {i + 1}
182
+ {/if}
183
+ </span>
184
+ <span class="sr-only">{srText(i, step, state)}</span>
185
+ <span class={unstyled ? undefined : "stuic-stepper-text"}>
186
+ <span class={_classLabel}><Thc thc={step.label} /></span>
187
+ {#if step.description}
188
+ <span class={_classDescription}><Thc thc={step.description} /></span>
189
+ {/if}
190
+ </span>
191
+ {/snippet}
192
+
193
+ {#if _steps.length}
194
+ <nav
195
+ bind:this={el}
196
+ class={_class}
197
+ data-orientation={!unstyled ? orientation : undefined}
198
+ data-label-position={!unstyled && orientation === "horizontal"
199
+ ? labelPosition
200
+ : undefined}
201
+ data-disabled={!unstyled && disabled ? "" : undefined}
202
+ aria-label={t("stepper", null, "Progress")}
203
+ {...rest}
204
+ >
205
+ <ol class={unstyled ? undefined : "stuic-stepper-list"}>
206
+ {#each _steps as step, i (i)}
207
+ {@const state = stateOf(i)}
208
+ <li
209
+ class={_classStep}
210
+ data-state={!unstyled ? state : undefined}
211
+ data-error={!unstyled && step.error ? "" : undefined}
212
+ data-disabled={!unstyled && step.disabled ? "" : undefined}
213
+ aria-current={state === "current" ? "step" : undefined}
214
+ >
215
+ {#if _interactive}
216
+ <button
217
+ type="button"
218
+ class={unstyled ? undefined : "stuic-stepper-trigger"}
219
+ disabled={!isEligible(step, state)}
220
+ onclick={() => select(i)}
221
+ >
222
+ {@render stepInner(i, step, state)}
223
+ </button>
224
+ {:else}
225
+ <div class={unstyled ? undefined : "stuic-stepper-trigger"}>
226
+ {@render stepInner(i, step, state)}
227
+ </div>
228
+ {/if}
229
+ {#if i < _steps.length - 1}
230
+ <span
231
+ class={_classConnector}
232
+ data-state={!unstyled && state === "completed" ? "completed" : undefined}
233
+ aria-hidden="true"
234
+ ></span>
235
+ {/if}
236
+ </li>
237
+ {/each}
238
+ </ol>
239
+ </nav>
240
+ {/if}