@nqmcreative/ui 0.2.1 → 0.2.2

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 (139) hide show
  1. package/README.md +7 -5
  2. package/dist/styles/sprout/Accordion.svelte +17 -0
  3. package/dist/styles/sprout/Accordion.svelte.d.ts +8 -0
  4. package/dist/styles/sprout/AccordionItem.svelte +46 -0
  5. package/dist/styles/sprout/AccordionItem.svelte.d.ts +14 -0
  6. package/dist/styles/sprout/Alert.svelte +100 -0
  7. package/dist/styles/sprout/Alert.svelte.d.ts +18 -0
  8. package/dist/styles/sprout/Avatar.svelte +60 -0
  9. package/dist/styles/sprout/Avatar.svelte.d.ts +16 -0
  10. package/dist/styles/sprout/AvatarGroup.svelte +17 -0
  11. package/dist/styles/sprout/AvatarGroup.svelte.d.ts +8 -0
  12. package/dist/styles/sprout/Badge.svelte +58 -0
  13. package/dist/styles/sprout/Badge.svelte.d.ts +16 -0
  14. package/dist/styles/sprout/Breadcrumb.svelte +49 -0
  15. package/dist/styles/sprout/Breadcrumb.svelte.d.ts +14 -0
  16. package/dist/styles/sprout/Button.svelte +105 -0
  17. package/dist/styles/sprout/Button.svelte.d.ts +21 -0
  18. package/dist/styles/sprout/CTASection.svelte +77 -0
  19. package/dist/styles/sprout/CTASection.svelte.d.ts +18 -0
  20. package/dist/styles/sprout/Calendar.svelte +202 -0
  21. package/dist/styles/sprout/Calendar.svelte.d.ts +22 -0
  22. package/dist/styles/sprout/Card.svelte +82 -0
  23. package/dist/styles/sprout/Card.svelte.d.ts +21 -0
  24. package/dist/styles/sprout/Checkbox.svelte +76 -0
  25. package/dist/styles/sprout/Checkbox.svelte.d.ts +15 -0
  26. package/dist/styles/sprout/CheckboxGroup.svelte +112 -0
  27. package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +30 -0
  28. package/dist/styles/sprout/Combobox.svelte +267 -0
  29. package/dist/styles/sprout/Combobox.svelte.d.ts +29 -0
  30. package/dist/styles/sprout/CommandPalette.svelte +210 -0
  31. package/dist/styles/sprout/CommandPalette.svelte.d.ts +32 -0
  32. package/dist/styles/sprout/ConfirmDialog.svelte +97 -0
  33. package/dist/styles/sprout/ConfirmDialog.svelte.d.ts +22 -0
  34. package/dist/styles/sprout/ContextMenu.svelte +89 -0
  35. package/dist/styles/sprout/ContextMenu.svelte.d.ts +15 -0
  36. package/dist/styles/sprout/DatePicker.svelte +193 -0
  37. package/dist/styles/sprout/DatePicker.svelte.d.ts +23 -0
  38. package/dist/styles/sprout/Divider.svelte +30 -0
  39. package/dist/styles/sprout/Divider.svelte.d.ts +10 -0
  40. package/dist/styles/sprout/Drawer.svelte +166 -0
  41. package/dist/styles/sprout/Drawer.svelte.d.ts +20 -0
  42. package/dist/styles/sprout/Dropdown.svelte +95 -0
  43. package/dist/styles/sprout/Dropdown.svelte.d.ts +23 -0
  44. package/dist/styles/sprout/Dropzone.svelte +211 -0
  45. package/dist/styles/sprout/Dropzone.svelte.d.ts +29 -0
  46. package/dist/styles/sprout/EmptyState.svelte +71 -0
  47. package/dist/styles/sprout/EmptyState.svelte.d.ts +18 -0
  48. package/dist/styles/sprout/FeatureCard.svelte +61 -0
  49. package/dist/styles/sprout/FeatureCard.svelte.d.ts +16 -0
  50. package/dist/styles/sprout/FeatureGrid.svelte +62 -0
  51. package/dist/styles/sprout/FeatureGrid.svelte.d.ts +16 -0
  52. package/dist/styles/sprout/Field.svelte +42 -0
  53. package/dist/styles/sprout/Field.svelte.d.ts +16 -0
  54. package/dist/styles/sprout/Footer.svelte +91 -0
  55. package/dist/styles/sprout/Footer.svelte.d.ts +25 -0
  56. package/dist/styles/sprout/HeroSection.svelte +86 -0
  57. package/dist/styles/sprout/HeroSection.svelte.d.ts +21 -0
  58. package/dist/styles/sprout/Input.svelte +61 -0
  59. package/dist/styles/sprout/Input.svelte.d.ts +18 -0
  60. package/dist/styles/sprout/InputGroup.svelte +20 -0
  61. package/dist/styles/sprout/InputGroup.svelte.d.ts +11 -0
  62. package/dist/styles/sprout/Kbd.svelte +18 -0
  63. package/dist/styles/sprout/Kbd.svelte.d.ts +8 -0
  64. package/dist/styles/sprout/Label.svelte +16 -0
  65. package/dist/styles/sprout/Label.svelte.d.ts +9 -0
  66. package/dist/styles/sprout/Link.svelte +44 -0
  67. package/dist/styles/sprout/Link.svelte.d.ts +16 -0
  68. package/dist/styles/sprout/LocaleProvider.svelte +24 -0
  69. package/dist/styles/sprout/LocaleProvider.svelte.d.ts +10 -0
  70. package/dist/styles/sprout/LogoCloud.svelte +61 -0
  71. package/dist/styles/sprout/LogoCloud.svelte.d.ts +19 -0
  72. package/dist/styles/sprout/MenuItem.svelte +64 -0
  73. package/dist/styles/sprout/MenuItem.svelte.d.ts +18 -0
  74. package/dist/styles/sprout/MenuSeparator.svelte +22 -0
  75. package/dist/styles/sprout/MenuSeparator.svelte.d.ts +8 -0
  76. package/dist/styles/sprout/Modal.svelte +119 -0
  77. package/dist/styles/sprout/Modal.svelte.d.ts +21 -0
  78. package/dist/styles/sprout/MultiSelect.svelte +288 -0
  79. package/dist/styles/sprout/MultiSelect.svelte.d.ts +31 -0
  80. package/dist/styles/sprout/Navbar.svelte +201 -0
  81. package/dist/styles/sprout/Navbar.svelte.d.ts +33 -0
  82. package/dist/styles/sprout/NumberInput.svelte +97 -0
  83. package/dist/styles/sprout/NumberInput.svelte.d.ts +15 -0
  84. package/dist/styles/sprout/Pagination.svelte +84 -0
  85. package/dist/styles/sprout/Pagination.svelte.d.ts +15 -0
  86. package/dist/styles/sprout/PasswordInput.svelte +101 -0
  87. package/dist/styles/sprout/PasswordInput.svelte.d.ts +12 -0
  88. package/dist/styles/sprout/Popover.svelte +90 -0
  89. package/dist/styles/sprout/Popover.svelte.d.ts +19 -0
  90. package/dist/styles/sprout/PricingCard.svelte +109 -0
  91. package/dist/styles/sprout/PricingCard.svelte.d.ts +25 -0
  92. package/dist/styles/sprout/Progress.svelte +59 -0
  93. package/dist/styles/sprout/Progress.svelte.d.ts +17 -0
  94. package/dist/styles/sprout/Radio.svelte +56 -0
  95. package/dist/styles/sprout/Radio.svelte.d.ts +15 -0
  96. package/dist/styles/sprout/RadioGroup.svelte +96 -0
  97. package/dist/styles/sprout/RadioGroup.svelte.d.ts +27 -0
  98. package/dist/styles/sprout/SegmentedControl.svelte +89 -0
  99. package/dist/styles/sprout/SegmentedControl.svelte.d.ts +25 -0
  100. package/dist/styles/sprout/Select.svelte +62 -0
  101. package/dist/styles/sprout/Select.svelte.d.ts +20 -0
  102. package/dist/styles/sprout/Sidebar.svelte +206 -0
  103. package/dist/styles/sprout/Sidebar.svelte.d.ts +36 -0
  104. package/dist/styles/sprout/Skeleton.svelte +46 -0
  105. package/dist/styles/sprout/Skeleton.svelte.d.ts +13 -0
  106. package/dist/styles/sprout/Slider.svelte +80 -0
  107. package/dist/styles/sprout/Slider.svelte.d.ts +19 -0
  108. package/dist/styles/sprout/Spinner.svelte +34 -0
  109. package/dist/styles/sprout/Spinner.svelte.d.ts +13 -0
  110. package/dist/styles/sprout/Stat.svelte +51 -0
  111. package/dist/styles/sprout/Stat.svelte.d.ts +16 -0
  112. package/dist/styles/sprout/StatsBand.svelte +64 -0
  113. package/dist/styles/sprout/StatsBand.svelte.d.ts +20 -0
  114. package/dist/styles/sprout/Steps.svelte +143 -0
  115. package/dist/styles/sprout/Steps.svelte.d.ts +24 -0
  116. package/dist/styles/sprout/Switch.svelte +65 -0
  117. package/dist/styles/sprout/Switch.svelte.d.ts +14 -0
  118. package/dist/styles/sprout/Table.svelte +218 -0
  119. package/dist/styles/sprout/Table.svelte.d.ts +66 -0
  120. package/dist/styles/sprout/Tabs.svelte +103 -0
  121. package/dist/styles/sprout/Tabs.svelte.d.ts +23 -0
  122. package/dist/styles/sprout/Testimonial.svelte +65 -0
  123. package/dist/styles/sprout/Testimonial.svelte.d.ts +19 -0
  124. package/dist/styles/sprout/Textarea.svelte +55 -0
  125. package/dist/styles/sprout/Textarea.svelte.d.ts +12 -0
  126. package/dist/styles/sprout/ThemeToggle.svelte +131 -0
  127. package/dist/styles/sprout/ThemeToggle.svelte.d.ts +16 -0
  128. package/dist/styles/sprout/Toaster.svelte +133 -0
  129. package/dist/styles/sprout/Toaster.svelte.d.ts +8 -0
  130. package/dist/styles/sprout/Tooltip.svelte +34 -0
  131. package/dist/styles/sprout/Tooltip.svelte.d.ts +12 -0
  132. package/dist/styles/sprout/fonts.css +78 -0
  133. package/dist/styles/sprout/index.d.ts +109 -0
  134. package/dist/styles/sprout/index.js +83 -0
  135. package/dist/styles/sprout/lift.d.ts +40 -0
  136. package/dist/styles/sprout/lift.js +36 -0
  137. package/dist/styles/sprout/theme.css +255 -0
  138. package/package.json +269 -3
  139. package/registry.json +137 -65
@@ -0,0 +1,13 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ export type SkeletonVariant = 'text' | 'block' | 'circle';
3
+ interface Props extends HTMLAttributes<HTMLDivElement> {
4
+ variant?: SkeletonVariant;
5
+ /** `text` only — number of stacked lines, the last one runs short. */
6
+ lines?: number;
7
+ /** Any CSS length, e.g. `'12rem'` or `'60%'`. */
8
+ width?: string;
9
+ height?: string;
10
+ }
11
+ declare const Skeleton: import("svelte").Component<Props, {}, "">;
12
+ type Skeleton = ReturnType<typeof Skeleton>;
13
+ export default Skeleton;
@@ -0,0 +1,80 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import { toneText, type Tone } from '../../core/tones.js';
4
+ import { percentOf } from '../../core/number.js';
5
+
6
+ interface Props extends Omit<HTMLInputAttributes, 'size' | 'type' | 'value' | 'min' | 'max'> {
7
+ value?: number;
8
+ min?: number;
9
+ max?: number;
10
+ step?: number;
11
+ tone?: Tone;
12
+ label?: string;
13
+ /** Prints the current value on the right of the label row. */
14
+ showValue?: boolean;
15
+ /** Appended to the printed value, e.g. `'%'`. */
16
+ unit?: string;
17
+ /** Tick labels under the track — usually just `[min, max]`. */
18
+ marks?: (number | string)[];
19
+ }
20
+
21
+ let {
22
+ value = $bindable(50),
23
+ min = 0,
24
+ max = 100,
25
+ step = 1,
26
+ tone = 'brand',
27
+ label,
28
+ showValue = false,
29
+ unit = '',
30
+ marks,
31
+ disabled = false,
32
+ class: className = '',
33
+ ...rest
34
+ }: Props = $props();
35
+
36
+ const pct = $derived(percentOf(value, min, max));
37
+
38
+ // The filled part of the track and the thumb both paint with `currentColor`,
39
+ // so a single tone class on the input drives every vendor pseudo-element.
40
+ // The gradient classes below are spelled out twice on purpose: Tailwind only
41
+ // sees literal class strings, so a shared JS constant would produce nothing.
42
+ </script>
43
+
44
+ <div class="flex w-full flex-col gap-2 {disabled ? 'opacity-50' : ''} {className}">
45
+ {#if label || showValue}
46
+ <div class="flex items-baseline justify-between gap-4 font-sans text-sm">
47
+ {#if label}<span class="text-text-secondary">{label}</span>{/if}
48
+ {#if showValue}
49
+ <span class="text-sm font-medium tabular-nums {toneText[tone]}">{value}{unit}</span>
50
+ {/if}
51
+ </div>
52
+ {/if}
53
+
54
+ <input
55
+ type="range"
56
+ bind:value
57
+ {min}
58
+ {max}
59
+ {step}
60
+ {disabled}
61
+ aria-label={label}
62
+ style="--pct:{pct}%"
63
+ class="h-5 w-full cursor-pointer appearance-none bg-transparent focus:outline-none disabled:cursor-not-allowed
64
+ {toneText[tone]}
65
+ focus-visible:outline-2 focus-visible:outline-offset-4
66
+ [&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:rounded-full
67
+ [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-bg [&::-moz-range-thumb]:bg-current [&::-moz-range-thumb]:shadow-[0_2px_0_0_rgba(20,83,45,0.25)] [&::-moz-range-track]:h-1.5 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,currentColor_var(--pct),var(--color-bg-inset)_var(--pct))] [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,currentColor_var(--pct),var(--color-bg-inset)_var(--pct))]
68
+ [&::-webkit-slider-thumb]:-mt-[5px] [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2
69
+ [&::-webkit-slider-thumb]:border-bg [&::-webkit-slider-thumb]:bg-current [&::-webkit-slider-thumb]:shadow-[0_2px_0_0_rgba(20,83,45,0.25)]"
70
+ {...rest}
71
+ />
72
+
73
+ {#if marks?.length}
74
+ <div class="flex justify-between text-[11px] text-text-muted tabular-nums">
75
+ {#each marks as mark (mark)}
76
+ <span>{mark}</span>
77
+ {/each}
78
+ </div>
79
+ {/if}
80
+ </div>
@@ -0,0 +1,19 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../../core/tones.js';
3
+ interface Props extends Omit<HTMLInputAttributes, 'size' | 'type' | 'value' | 'min' | 'max'> {
4
+ value?: number;
5
+ min?: number;
6
+ max?: number;
7
+ step?: number;
8
+ tone?: Tone;
9
+ label?: string;
10
+ /** Prints the current value on the right of the label row. */
11
+ showValue?: boolean;
12
+ /** Appended to the printed value, e.g. `'%'`. */
13
+ unit?: string;
14
+ /** Tick labels under the track — usually just `[min, max]`. */
15
+ marks?: (number | string)[];
16
+ }
17
+ declare const Slider: import("svelte").Component<Props, {}, "value">;
18
+ type Slider = ReturnType<typeof Slider>;
19
+ export default Slider;
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { toneText, type Tone } from '../../core/tones.js';
4
+ import { useLocale } from '../../core/locale.svelte.js';
5
+
6
+ export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg';
7
+
8
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
9
+ size?: SpinnerSize;
10
+ /** Omit to inherit the parent's `currentColor` (e.g. inside a Button). */
11
+ tone?: Tone;
12
+ /** Defaults to the locale's loading string; pass `''` to hide it. */
13
+ label?: string;
14
+ }
15
+
16
+ const t = useLocale();
17
+
18
+ let { size = 'md', tone, label, class: className = '', ...rest }: Props = $props();
19
+
20
+ const sizes: Record<SpinnerSize, string> = {
21
+ xs: 'size-3 border-[1.5px]',
22
+ sm: 'size-4 border-2',
23
+ md: 'size-5 border-2',
24
+ lg: 'size-8 border-[3px]'
25
+ };
26
+ </script>
27
+
28
+ <span
29
+ role="status"
30
+ aria-label={label ?? t.current.loading}
31
+ class="inline-block shrink-0 animate-spin rounded-full border-current/25 border-t-current
32
+ {sizes[size]} {tone ? toneText[tone] : ''} {className}"
33
+ {...rest}
34
+ ></span>
@@ -0,0 +1,13 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../../core/tones.js';
3
+ export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg';
4
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
5
+ size?: SpinnerSize;
6
+ /** Omit to inherit the parent's `currentColor` (e.g. inside a Button). */
7
+ tone?: Tone;
8
+ /** Defaults to the locale's loading string; pass `''` to hide it. */
9
+ label?: string;
10
+ }
11
+ declare const Spinner: import("svelte").Component<Props, {}, "">;
12
+ type Spinner = ReturnType<typeof Spinner>;
13
+ export default Spinner;
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { toneText, type Tone } from '../../core/tones.js';
4
+
5
+ export type StatTrend = 'up' | 'down' | 'flat';
6
+
7
+ interface Props extends HTMLAttributes<HTMLDivElement> {
8
+ label: string;
9
+ value: string | number;
10
+ /** Secondary figure, e.g. `'+12.4%'`. */
11
+ delta?: string;
12
+ trend?: StatTrend;
13
+ hint?: string;
14
+ /** Colour of the value itself. Leave unset for plain `text`. */
15
+ tone?: Tone;
16
+ }
17
+
18
+ let {
19
+ label,
20
+ value,
21
+ delta,
22
+ trend = 'flat',
23
+ hint,
24
+ tone,
25
+ class: className = '',
26
+ ...rest
27
+ }: Props = $props();
28
+
29
+ const trendTone: Record<StatTrend, Tone> = { up: 'success', down: 'danger', flat: 'neutral' };
30
+ const trendGlyph: Record<StatTrend, string> = { up: '↑', down: '↓', flat: '→' };
31
+ </script>
32
+
33
+ <div class="flex flex-col gap-1 font-sans {className}" {...rest}>
34
+ <span class="text-sm font-medium text-text-muted">{label}</span>
35
+ <div class="flex items-baseline gap-2">
36
+ <span
37
+ class="font-heading text-3xl font-semibold tracking-tight tabular-nums {tone
38
+ ? toneText[tone]
39
+ : 'text-text'}"
40
+ >
41
+ {value}
42
+ </span>
43
+ {#if delta}
44
+ <span class="text-sm font-medium {toneText[trendTone[trend]]}">
45
+ {trendGlyph[trend]}
46
+ {delta}
47
+ </span>
48
+ {/if}
49
+ </div>
50
+ {#if hint}<span class="text-sm text-text-muted">{hint}</span>{/if}
51
+ </div>
@@ -0,0 +1,16 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../../core/tones.js';
3
+ export type StatTrend = 'up' | 'down' | 'flat';
4
+ interface Props extends HTMLAttributes<HTMLDivElement> {
5
+ label: string;
6
+ value: string | number;
7
+ /** Secondary figure, e.g. `'+12.4%'`. */
8
+ delta?: string;
9
+ trend?: StatTrend;
10
+ hint?: string;
11
+ /** Colour of the value itself. Leave unset for plain `text`. */
12
+ tone?: Tone;
13
+ }
14
+ declare const Stat: import("svelte").Component<Props, {}, "">;
15
+ type Stat = ReturnType<typeof Stat>;
16
+ export default Stat;
@@ -0,0 +1,64 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import Stat from './Stat.svelte';
4
+ import type { StatTrend } from './Stat.svelte';
5
+ import type { Tone } from '../../core/tones.js';
6
+ import { edge } from './lift.js';
7
+
8
+ export interface BandStat {
9
+ label: string;
10
+ value: string | number;
11
+ delta?: string;
12
+ trend?: StatTrend;
13
+ hint?: string;
14
+ tone?: Tone;
15
+ }
16
+
17
+ interface Props extends HTMLAttributes<HTMLElement> {
18
+ stats: BandStat[];
19
+ title?: string;
20
+ /** Figures in one bordered card rather than free-standing. */
21
+ bordered?: boolean;
22
+ }
23
+
24
+ let { stats, title, bordered = true, class: className = '', ...rest }: Props = $props();
25
+
26
+ const cols = $derived(
27
+ stats.length === 2
28
+ ? 'sm:grid-cols-2'
29
+ : stats.length === 4
30
+ ? 'sm:grid-cols-2 lg:grid-cols-4'
31
+ : 'sm:grid-cols-3'
32
+ );
33
+ </script>
34
+
35
+ <section class="w-full px-6 py-16 {className}" {...rest}>
36
+ <div class="mx-auto flex w-full max-w-6xl flex-col gap-8">
37
+ {#if title}
38
+ <h2
39
+ class="max-w-xl font-heading text-3xl font-semibold tracking-tight text-balance text-text"
40
+ >
41
+ {title}
42
+ </h2>
43
+ {/if}
44
+
45
+ <div
46
+ class="grid {cols} {bordered
47
+ ? `gap-px overflow-hidden rounded-[28px] border border-hairline bg-hairline ${edge}`
48
+ : 'gap-8'}"
49
+ >
50
+ {#each stats as stat (stat.label)}
51
+ <div class={bordered ? 'bg-bg p-6' : ''}>
52
+ <Stat
53
+ label={stat.label}
54
+ value={stat.value}
55
+ delta={stat.delta}
56
+ trend={stat.trend}
57
+ hint={stat.hint}
58
+ tone={stat.tone}
59
+ />
60
+ </div>
61
+ {/each}
62
+ </div>
63
+ </div>
64
+ </section>
@@ -0,0 +1,20 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { StatTrend } from './Stat.svelte';
3
+ import type { Tone } from '../../core/tones.js';
4
+ export interface BandStat {
5
+ label: string;
6
+ value: string | number;
7
+ delta?: string;
8
+ trend?: StatTrend;
9
+ hint?: string;
10
+ tone?: Tone;
11
+ }
12
+ interface Props extends HTMLAttributes<HTMLElement> {
13
+ stats: BandStat[];
14
+ title?: string;
15
+ /** Figures in one bordered card rather than free-standing. */
16
+ bordered?: boolean;
17
+ }
18
+ declare const StatsBand: import("svelte").Component<Props, {}, "">;
19
+ type StatsBand = ReturnType<typeof StatsBand>;
20
+ export default StatsBand;
@@ -0,0 +1,143 @@
1
+ <script module lang="ts">
2
+ export interface StepItem {
3
+ label: string;
4
+ description?: string;
5
+ /** Marks the step as failed — shown in `danger` regardless of position. */
6
+ error?: boolean;
7
+ disabled?: boolean;
8
+ }
9
+ </script>
10
+
11
+ <script lang="ts">
12
+ import type { HTMLAttributes } from 'svelte/elements';
13
+ import { focusRing, toneFill, toneRing, toneText, type Tone } from '../../core/tones.js';
14
+ import { liftSm } from './lift.js';
15
+
16
+ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'onchange'> {
17
+ items: StepItem[];
18
+ /** Bindable, zero-based. Steps before it count as done. */
19
+ current?: number;
20
+ orientation?: 'horizontal' | 'vertical';
21
+ tone?: Tone;
22
+ /** Let the user jump to a completed step by clicking it. */
23
+ clickable?: boolean;
24
+ /** Hide the connecting rule between steps. */
25
+ bare?: boolean;
26
+ onchange?: (index: number) => void;
27
+ }
28
+
29
+ let {
30
+ items,
31
+ current = $bindable(0),
32
+ orientation = 'horizontal',
33
+ tone = 'brand',
34
+ clickable = false,
35
+ bare = false,
36
+ class: className = '',
37
+ onchange,
38
+ ...rest
39
+ }: Props = $props();
40
+
41
+ function status(index: number): 'done' | 'current' | 'todo' {
42
+ if (index < current) return 'done';
43
+ if (index === current) return 'current';
44
+ return 'todo';
45
+ }
46
+
47
+ const marker =
48
+ 'flex size-8 shrink-0 items-center justify-center rounded-full border font-sans text-xs font-semibold transition-colors duration-150 ease-brand-out';
49
+
50
+ function markerTone(state: 'done' | 'current' | 'todo', item: StepItem) {
51
+ if (item.error) return 'border-danger bg-danger-light text-danger';
52
+ if (state === 'done')
53
+ return `${toneFill[tone]} border-transparent text-text-inverse ${liftSm[tone]}`;
54
+ if (state === 'current') return `border-2 border-current bg-bg ${toneText[tone]}`;
55
+ return 'border-hairline-strong bg-bg text-text-muted';
56
+ }
57
+
58
+ function go(index: number, item: StepItem) {
59
+ if (!clickable || item.disabled || index > current) return;
60
+ current = index;
61
+ onchange?.(index);
62
+ }
63
+ </script>
64
+
65
+ {#snippet glyph(index: number, state: 'done' | 'current' | 'todo', item: StepItem)}
66
+ {#if item.error}
67
+ !
68
+ {:else if state === 'done'}
69
+ <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
70
+ <path
71
+ d="m2 7 3.2 3.2L12 3.6"
72
+ stroke="currentColor"
73
+ stroke-width="2"
74
+ stroke-linecap="round"
75
+ stroke-linejoin="round"
76
+ />
77
+ </svg>
78
+ {:else}
79
+ {index + 1}
80
+ {/if}
81
+ {/snippet}
82
+
83
+ <nav
84
+ aria-label="Progress"
85
+ class="flex w-full {orientation === 'vertical' ? 'flex-col gap-0' : 'flex-row'} {className}"
86
+ {...rest}
87
+ >
88
+ {#each items as item, i (item.label)}
89
+ {@const state = status(i)}
90
+ <div
91
+ class="flex min-w-0 {orientation === 'vertical' ? 'flex-row gap-3' : 'flex-1 flex-col gap-2'}"
92
+ >
93
+ <!-- marker + rule -->
94
+ <div
95
+ class="flex shrink-0 items-center {orientation === 'vertical'
96
+ ? 'flex-col self-stretch'
97
+ : 'w-full flex-row'}"
98
+ >
99
+ {#if clickable && state !== 'todo' && !item.disabled}
100
+ <button
101
+ type="button"
102
+ onclick={() => go(i, item)}
103
+ aria-current={state === 'current' ? 'step' : undefined}
104
+ class="{marker} cursor-pointer {markerTone(state, item)} {focusRing} {toneRing[tone]}"
105
+ >
106
+ {@render glyph(i, state, item)}
107
+ </button>
108
+ {:else}
109
+ <div
110
+ aria-current={state === 'current' ? 'step' : undefined}
111
+ class="{marker} {markerTone(state, item)}"
112
+ >
113
+ {@render glyph(i, state, item)}
114
+ </div>
115
+ {/if}
116
+
117
+ {#if !bare && i < items.length - 1}
118
+ <span
119
+ class="rounded-full {orientation === 'vertical'
120
+ ? 'my-1 w-0.5 flex-1'
121
+ : 'mx-3 h-0.5 flex-1'} {i < current ? toneFill[tone] : 'bg-hairline'}"
122
+ ></span>
123
+ {/if}
124
+ </div>
125
+
126
+ <!-- copy -->
127
+ <div class="flex min-w-0 flex-col gap-0.5 {orientation === 'vertical' ? 'pb-6' : 'pr-3'}">
128
+ <span
129
+ class="font-sans text-sm leading-snug break-words {state === 'todo'
130
+ ? 'text-text-muted'
131
+ : 'font-medium text-text'}"
132
+ >
133
+ {item.label}
134
+ </span>
135
+ {#if item.description}
136
+ <span class="font-sans text-[13px] leading-snug break-words text-text-muted">
137
+ {item.description}
138
+ </span>
139
+ {/if}
140
+ </div>
141
+ </div>
142
+ {/each}
143
+ </nav>
@@ -0,0 +1,24 @@
1
+ export interface StepItem {
2
+ label: string;
3
+ description?: string;
4
+ /** Marks the step as failed — shown in `danger` regardless of position. */
5
+ error?: boolean;
6
+ disabled?: boolean;
7
+ }
8
+ import type { HTMLAttributes } from 'svelte/elements';
9
+ import { type Tone } from '../../core/tones.js';
10
+ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'onchange'> {
11
+ items: StepItem[];
12
+ /** Bindable, zero-based. Steps before it count as done. */
13
+ current?: number;
14
+ orientation?: 'horizontal' | 'vertical';
15
+ tone?: Tone;
16
+ /** Let the user jump to a completed step by clicking it. */
17
+ clickable?: boolean;
18
+ /** Hide the connecting rule between steps. */
19
+ bare?: boolean;
20
+ onchange?: (index: number) => void;
21
+ }
22
+ declare const Steps: import("svelte").Component<Props, {}, "current">;
23
+ type Steps = ReturnType<typeof Steps>;
24
+ export default Steps;
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ import { focusRing, toneFill, toneRing, type Tone } from '../../core/tones.js';
4
+ import { edge } from './lift.js';
5
+
6
+ export type SwitchSize = 'sm' | 'md';
7
+
8
+ interface Props extends Omit<HTMLButtonAttributes, 'onclick' | 'onchange'> {
9
+ checked?: boolean;
10
+ label?: string;
11
+ description?: string;
12
+ tone?: Tone;
13
+ size?: SwitchSize;
14
+ onchange?: (checked: boolean) => void;
15
+ }
16
+
17
+ let {
18
+ checked = $bindable(false),
19
+ label,
20
+ description,
21
+ tone = 'brand',
22
+ size = 'md',
23
+ disabled = false,
24
+ onchange,
25
+ class: className = '',
26
+ ...rest
27
+ }: Props = $props();
28
+
29
+ const sizes: Record<SwitchSize, { track: string; thumb: string; travel: string }> = {
30
+ sm: { track: 'h-5 w-9', thumb: 'size-4', travel: 'translate-x-4' },
31
+ md: { track: 'h-6 w-11', thumb: 'size-5', travel: 'translate-x-5' }
32
+ };
33
+
34
+ function toggle() {
35
+ checked = !checked;
36
+ onchange?.(checked);
37
+ }
38
+ </script>
39
+
40
+ <div class="inline-flex items-start gap-3 {disabled ? 'opacity-50' : ''} {className}">
41
+ <button
42
+ type="button"
43
+ role="switch"
44
+ aria-checked={checked}
45
+ aria-label={label}
46
+ {disabled}
47
+ onclick={toggle}
48
+ class="relative mt-0.5 inline-flex shrink-0 items-center rounded-full p-0.5 transition-colors duration-200 ease-brand-out disabled:pointer-events-none
49
+ {sizes[size].track} {checked ? toneFill[tone] : 'bg-hairline-strong'} {focusRing} {toneRing[tone]}"
50
+ {...rest}
51
+ >
52
+ <span
53
+ class="rounded-full bg-bg {edge} transition-transform duration-200 ease-brand-out
54
+ {sizes[size].thumb} {checked ? sizes[size].travel : 'translate-x-0'}"
55
+ ></span>
56
+ </button>
57
+ {#if label || description}
58
+ <span class="flex flex-col gap-0.5 font-sans text-sm">
59
+ {#if label}<span class="leading-snug text-text">{label}</span>{/if}
60
+ {#if description}
61
+ <span class="text-[13px] leading-snug text-text-muted">{description}</span>
62
+ {/if}
63
+ </span>
64
+ {/if}
65
+ </div>
@@ -0,0 +1,14 @@
1
+ import type { HTMLButtonAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../../core/tones.js';
3
+ export type SwitchSize = 'sm' | 'md';
4
+ interface Props extends Omit<HTMLButtonAttributes, 'onclick' | 'onchange'> {
5
+ checked?: boolean;
6
+ label?: string;
7
+ description?: string;
8
+ tone?: Tone;
9
+ size?: SwitchSize;
10
+ onchange?: (checked: boolean) => void;
11
+ }
12
+ declare const Switch: import("svelte").Component<Props, {}, "checked">;
13
+ type Switch = ReturnType<typeof Switch>;
14
+ export default Switch;