@nqmcreative/ui 0.2.1 → 0.2.3

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 (175) hide show
  1. package/README.md +7 -5
  2. package/dist/core/index.d.ts +1 -1
  3. package/dist/core/index.js +1 -1
  4. package/dist/core/trigger.d.ts +6 -0
  5. package/dist/core/trigger.js +13 -0
  6. package/dist/styles/matte/Button.svelte +4 -4
  7. package/dist/styles/matte/Card.svelte +3 -3
  8. package/dist/styles/matte/Combobox.svelte +2 -2
  9. package/dist/styles/matte/CommandPalette.svelte +32 -7
  10. package/dist/styles/matte/CommandPalette.svelte.d.ts +5 -0
  11. package/dist/styles/matte/DatePicker.svelte +2 -2
  12. package/dist/styles/matte/Drawer.svelte +2 -2
  13. package/dist/styles/matte/Dropzone.svelte +1 -1
  14. package/dist/styles/matte/FeatureCard.svelte +1 -1
  15. package/dist/styles/matte/Input.svelte +4 -4
  16. package/dist/styles/matte/MenuItem.svelte +1 -1
  17. package/dist/styles/matte/Modal.svelte +3 -3
  18. package/dist/styles/matte/NumberInput.svelte +2 -2
  19. package/dist/styles/matte/PasswordInput.svelte +2 -2
  20. package/dist/styles/matte/PricingCard.svelte +3 -3
  21. package/dist/styles/matte/SegmentedControl.svelte +1 -1
  22. package/dist/styles/matte/Select.svelte +1 -1
  23. package/dist/styles/matte/Sidebar.svelte +75 -26
  24. package/dist/styles/matte/Sidebar.svelte.d.ts +15 -1
  25. package/dist/styles/matte/StatsBand.svelte +1 -1
  26. package/dist/styles/matte/Table.svelte +3 -3
  27. package/dist/styles/matte/Testimonial.svelte +2 -2
  28. package/dist/styles/matte/Textarea.svelte +1 -1
  29. package/dist/styles/matte/index.d.ts +1 -1
  30. package/dist/styles/paper/CommandPalette.svelte +25 -2
  31. package/dist/styles/paper/CommandPalette.svelte.d.ts +5 -0
  32. package/dist/styles/paper/Modal.svelte +9 -1
  33. package/dist/styles/paper/Sidebar.svelte +75 -26
  34. package/dist/styles/paper/Sidebar.svelte.d.ts +15 -1
  35. package/dist/styles/paper/fonts.css +34 -7
  36. package/dist/styles/paper/index.d.ts +1 -1
  37. package/dist/styles/paper/theme.css +11 -8
  38. package/dist/styles/sprout/Accordion.svelte +17 -0
  39. package/dist/styles/sprout/Accordion.svelte.d.ts +8 -0
  40. package/dist/styles/sprout/AccordionItem.svelte +46 -0
  41. package/dist/styles/sprout/AccordionItem.svelte.d.ts +14 -0
  42. package/dist/styles/sprout/Alert.svelte +100 -0
  43. package/dist/styles/sprout/Alert.svelte.d.ts +18 -0
  44. package/dist/styles/sprout/Avatar.svelte +60 -0
  45. package/dist/styles/sprout/Avatar.svelte.d.ts +16 -0
  46. package/dist/styles/sprout/AvatarGroup.svelte +17 -0
  47. package/dist/styles/sprout/AvatarGroup.svelte.d.ts +8 -0
  48. package/dist/styles/sprout/Badge.svelte +58 -0
  49. package/dist/styles/sprout/Badge.svelte.d.ts +16 -0
  50. package/dist/styles/sprout/Breadcrumb.svelte +49 -0
  51. package/dist/styles/sprout/Breadcrumb.svelte.d.ts +14 -0
  52. package/dist/styles/sprout/Button.svelte +105 -0
  53. package/dist/styles/sprout/Button.svelte.d.ts +21 -0
  54. package/dist/styles/sprout/CTASection.svelte +77 -0
  55. package/dist/styles/sprout/CTASection.svelte.d.ts +18 -0
  56. package/dist/styles/sprout/Calendar.svelte +202 -0
  57. package/dist/styles/sprout/Calendar.svelte.d.ts +22 -0
  58. package/dist/styles/sprout/Card.svelte +82 -0
  59. package/dist/styles/sprout/Card.svelte.d.ts +21 -0
  60. package/dist/styles/sprout/Checkbox.svelte +76 -0
  61. package/dist/styles/sprout/Checkbox.svelte.d.ts +15 -0
  62. package/dist/styles/sprout/CheckboxGroup.svelte +112 -0
  63. package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +30 -0
  64. package/dist/styles/sprout/Combobox.svelte +267 -0
  65. package/dist/styles/sprout/Combobox.svelte.d.ts +29 -0
  66. package/dist/styles/sprout/CommandPalette.svelte +233 -0
  67. package/dist/styles/sprout/CommandPalette.svelte.d.ts +37 -0
  68. package/dist/styles/sprout/ConfirmDialog.svelte +97 -0
  69. package/dist/styles/sprout/ConfirmDialog.svelte.d.ts +22 -0
  70. package/dist/styles/sprout/ContextMenu.svelte +89 -0
  71. package/dist/styles/sprout/ContextMenu.svelte.d.ts +15 -0
  72. package/dist/styles/sprout/DatePicker.svelte +193 -0
  73. package/dist/styles/sprout/DatePicker.svelte.d.ts +23 -0
  74. package/dist/styles/sprout/Divider.svelte +30 -0
  75. package/dist/styles/sprout/Divider.svelte.d.ts +10 -0
  76. package/dist/styles/sprout/Drawer.svelte +166 -0
  77. package/dist/styles/sprout/Drawer.svelte.d.ts +20 -0
  78. package/dist/styles/sprout/Dropdown.svelte +95 -0
  79. package/dist/styles/sprout/Dropdown.svelte.d.ts +23 -0
  80. package/dist/styles/sprout/Dropzone.svelte +211 -0
  81. package/dist/styles/sprout/Dropzone.svelte.d.ts +29 -0
  82. package/dist/styles/sprout/EmptyState.svelte +71 -0
  83. package/dist/styles/sprout/EmptyState.svelte.d.ts +18 -0
  84. package/dist/styles/sprout/FeatureCard.svelte +61 -0
  85. package/dist/styles/sprout/FeatureCard.svelte.d.ts +16 -0
  86. package/dist/styles/sprout/FeatureGrid.svelte +62 -0
  87. package/dist/styles/sprout/FeatureGrid.svelte.d.ts +16 -0
  88. package/dist/styles/sprout/Field.svelte +42 -0
  89. package/dist/styles/sprout/Field.svelte.d.ts +16 -0
  90. package/dist/styles/sprout/Footer.svelte +91 -0
  91. package/dist/styles/sprout/Footer.svelte.d.ts +25 -0
  92. package/dist/styles/sprout/HeroSection.svelte +86 -0
  93. package/dist/styles/sprout/HeroSection.svelte.d.ts +21 -0
  94. package/dist/styles/sprout/Input.svelte +61 -0
  95. package/dist/styles/sprout/Input.svelte.d.ts +18 -0
  96. package/dist/styles/sprout/InputGroup.svelte +20 -0
  97. package/dist/styles/sprout/InputGroup.svelte.d.ts +11 -0
  98. package/dist/styles/sprout/Kbd.svelte +18 -0
  99. package/dist/styles/sprout/Kbd.svelte.d.ts +8 -0
  100. package/dist/styles/sprout/Label.svelte +16 -0
  101. package/dist/styles/sprout/Label.svelte.d.ts +9 -0
  102. package/dist/styles/sprout/Link.svelte +44 -0
  103. package/dist/styles/sprout/Link.svelte.d.ts +16 -0
  104. package/dist/styles/sprout/LocaleProvider.svelte +24 -0
  105. package/dist/styles/sprout/LocaleProvider.svelte.d.ts +10 -0
  106. package/dist/styles/sprout/LogoCloud.svelte +61 -0
  107. package/dist/styles/sprout/LogoCloud.svelte.d.ts +19 -0
  108. package/dist/styles/sprout/MenuItem.svelte +64 -0
  109. package/dist/styles/sprout/MenuItem.svelte.d.ts +18 -0
  110. package/dist/styles/sprout/MenuSeparator.svelte +22 -0
  111. package/dist/styles/sprout/MenuSeparator.svelte.d.ts +8 -0
  112. package/dist/styles/sprout/Modal.svelte +127 -0
  113. package/dist/styles/sprout/Modal.svelte.d.ts +21 -0
  114. package/dist/styles/sprout/MultiSelect.svelte +288 -0
  115. package/dist/styles/sprout/MultiSelect.svelte.d.ts +31 -0
  116. package/dist/styles/sprout/Navbar.svelte +201 -0
  117. package/dist/styles/sprout/Navbar.svelte.d.ts +33 -0
  118. package/dist/styles/sprout/NumberInput.svelte +97 -0
  119. package/dist/styles/sprout/NumberInput.svelte.d.ts +15 -0
  120. package/dist/styles/sprout/Pagination.svelte +84 -0
  121. package/dist/styles/sprout/Pagination.svelte.d.ts +15 -0
  122. package/dist/styles/sprout/PasswordInput.svelte +101 -0
  123. package/dist/styles/sprout/PasswordInput.svelte.d.ts +12 -0
  124. package/dist/styles/sprout/Popover.svelte +90 -0
  125. package/dist/styles/sprout/Popover.svelte.d.ts +19 -0
  126. package/dist/styles/sprout/PricingCard.svelte +109 -0
  127. package/dist/styles/sprout/PricingCard.svelte.d.ts +25 -0
  128. package/dist/styles/sprout/Progress.svelte +59 -0
  129. package/dist/styles/sprout/Progress.svelte.d.ts +17 -0
  130. package/dist/styles/sprout/Radio.svelte +56 -0
  131. package/dist/styles/sprout/Radio.svelte.d.ts +15 -0
  132. package/dist/styles/sprout/RadioGroup.svelte +96 -0
  133. package/dist/styles/sprout/RadioGroup.svelte.d.ts +27 -0
  134. package/dist/styles/sprout/SegmentedControl.svelte +89 -0
  135. package/dist/styles/sprout/SegmentedControl.svelte.d.ts +25 -0
  136. package/dist/styles/sprout/Select.svelte +62 -0
  137. package/dist/styles/sprout/Select.svelte.d.ts +20 -0
  138. package/dist/styles/sprout/Sidebar.svelte +255 -0
  139. package/dist/styles/sprout/Sidebar.svelte.d.ts +50 -0
  140. package/dist/styles/sprout/Skeleton.svelte +46 -0
  141. package/dist/styles/sprout/Skeleton.svelte.d.ts +13 -0
  142. package/dist/styles/sprout/Slider.svelte +80 -0
  143. package/dist/styles/sprout/Slider.svelte.d.ts +19 -0
  144. package/dist/styles/sprout/Spinner.svelte +34 -0
  145. package/dist/styles/sprout/Spinner.svelte.d.ts +13 -0
  146. package/dist/styles/sprout/Stat.svelte +51 -0
  147. package/dist/styles/sprout/Stat.svelte.d.ts +16 -0
  148. package/dist/styles/sprout/StatsBand.svelte +64 -0
  149. package/dist/styles/sprout/StatsBand.svelte.d.ts +20 -0
  150. package/dist/styles/sprout/Steps.svelte +143 -0
  151. package/dist/styles/sprout/Steps.svelte.d.ts +24 -0
  152. package/dist/styles/sprout/Switch.svelte +65 -0
  153. package/dist/styles/sprout/Switch.svelte.d.ts +14 -0
  154. package/dist/styles/sprout/Table.svelte +218 -0
  155. package/dist/styles/sprout/Table.svelte.d.ts +66 -0
  156. package/dist/styles/sprout/Tabs.svelte +103 -0
  157. package/dist/styles/sprout/Tabs.svelte.d.ts +23 -0
  158. package/dist/styles/sprout/Testimonial.svelte +65 -0
  159. package/dist/styles/sprout/Testimonial.svelte.d.ts +19 -0
  160. package/dist/styles/sprout/Textarea.svelte +55 -0
  161. package/dist/styles/sprout/Textarea.svelte.d.ts +12 -0
  162. package/dist/styles/sprout/ThemeToggle.svelte +131 -0
  163. package/dist/styles/sprout/ThemeToggle.svelte.d.ts +16 -0
  164. package/dist/styles/sprout/Toaster.svelte +133 -0
  165. package/dist/styles/sprout/Toaster.svelte.d.ts +8 -0
  166. package/dist/styles/sprout/Tooltip.svelte +34 -0
  167. package/dist/styles/sprout/Tooltip.svelte.d.ts +12 -0
  168. package/dist/styles/sprout/fonts.css +77 -0
  169. package/dist/styles/sprout/index.d.ts +109 -0
  170. package/dist/styles/sprout/index.js +83 -0
  171. package/dist/styles/sprout/lift.d.ts +40 -0
  172. package/dist/styles/sprout/lift.js +36 -0
  173. package/dist/styles/sprout/theme.css +255 -0
  174. package/package.json +269 -3
  175. package/registry.json +139 -66
@@ -0,0 +1,56 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLInputAttributes } from 'svelte/elements';
4
+ import { peerFocusRing, tonePeerChecked, tonePeerFocus, type Tone } from '../../core/tones.js';
5
+ import { edge } from './lift.js';
6
+
7
+ interface Props extends Omit<HTMLInputAttributes, 'type' | 'size'> {
8
+ /** Bind the same variable across a set of radios. */
9
+ group?: string;
10
+ value: string;
11
+ label?: string;
12
+ description?: string;
13
+ tone?: Tone;
14
+ children?: Snippet;
15
+ }
16
+
17
+ let {
18
+ group = $bindable(''),
19
+ value,
20
+ label,
21
+ description,
22
+ tone = 'brand',
23
+ disabled = false,
24
+ class: className = '',
25
+ children,
26
+ ...rest
27
+ }: Props = $props();
28
+ </script>
29
+
30
+ <label
31
+ class="group inline-flex items-start gap-2.5 font-sans text-sm
32
+ {disabled ? 'pointer-events-none opacity-50' : 'cursor-pointer'} {className}"
33
+ >
34
+ <input type="radio" bind:group {value} {disabled} class="peer sr-only" {...rest} />
35
+ <span
36
+ class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded-full border border-hairline-strong bg-bg {edge} transition-colors duration-150 ease-brand-out
37
+ {tonePeerChecked[tone]} {peerFocusRing} {tonePeerFocus[tone]}"
38
+ aria-hidden="true"
39
+ >
40
+ <span
41
+ class="size-1.5 rounded-full bg-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
42
+ ></span>
43
+ </span>
44
+ {#if label || description || children}
45
+ <span class="flex flex-col gap-0.5">
46
+ {#if children}
47
+ {@render children()}
48
+ {:else if label}
49
+ <span class="leading-snug text-text">{label}</span>
50
+ {/if}
51
+ {#if description}
52
+ <span class="text-[13px] leading-snug text-text-muted">{description}</span>
53
+ {/if}
54
+ </span>
55
+ {/if}
56
+ </label>
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../../core/tones.js';
4
+ interface Props extends Omit<HTMLInputAttributes, 'type' | 'size'> {
5
+ /** Bind the same variable across a set of radios. */
6
+ group?: string;
7
+ value: string;
8
+ label?: string;
9
+ description?: string;
10
+ tone?: Tone;
11
+ children?: Snippet;
12
+ }
13
+ declare const Radio: import("svelte").Component<Props, {}, "group">;
14
+ type Radio = ReturnType<typeof Radio>;
15
+ export default Radio;
@@ -0,0 +1,96 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLFieldsetAttributes } from 'svelte/elements';
4
+ import Radio from './Radio.svelte';
5
+ import { toneBorder, toneSurface, type Tone } from '../../core/tones.js';
6
+ import { edge } from './lift.js';
7
+
8
+ export interface RadioOption {
9
+ value: string;
10
+ label: string;
11
+ description?: string;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ interface Props extends HTMLFieldsetAttributes {
16
+ /** Bindable — the selected option's `value`. */
17
+ value?: string;
18
+ options?: RadioOption[];
19
+ legend?: string;
20
+ hint?: string;
21
+ error?: string;
22
+ required?: boolean;
23
+ tone?: Tone;
24
+ orientation?: 'vertical' | 'horizontal';
25
+ /** Wraps each option in a bordered, clickable card. */
26
+ boxed?: boolean;
27
+ /** Pass your own `Radio`s instead of `options`. */
28
+ children?: Snippet;
29
+ }
30
+
31
+ let {
32
+ value = $bindable(''),
33
+ options,
34
+ legend,
35
+ hint,
36
+ error,
37
+ required = false,
38
+ tone = 'brand',
39
+ orientation = 'vertical',
40
+ boxed = false,
41
+ class: className = '',
42
+ children,
43
+ ...rest
44
+ }: Props = $props();
45
+ </script>
46
+
47
+ <fieldset class="flex flex-col gap-3 border-0 p-0 {className}" {...rest}>
48
+ {#if legend}
49
+ <legend class="mb-1 font-sans text-sm font-medium text-text">
50
+ {legend}
51
+ {#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span>{/if}
52
+ </legend>
53
+ {/if}
54
+
55
+ <div class="flex gap-2.5 {orientation === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col'}">
56
+ {#if options}
57
+ {#each options as option (option.value)}
58
+ {#if boxed}
59
+ <label
60
+ class="flex flex-1 cursor-pointer items-start gap-3 rounded-[20px] border p-4 {edge} transition-colors duration-150 ease-brand-out
61
+ {value === option.value
62
+ ? `${toneBorder[tone]} ${toneSurface[tone]}`
63
+ : 'border-hairline bg-bg hover:border-hairline-strong'}
64
+ {option.disabled ? 'pointer-events-none opacity-50' : ''}"
65
+ >
66
+ <Radio
67
+ bind:group={value}
68
+ value={option.value}
69
+ label={option.label}
70
+ description={option.description}
71
+ disabled={option.disabled}
72
+ {tone}
73
+ />
74
+ </label>
75
+ {:else}
76
+ <Radio
77
+ bind:group={value}
78
+ value={option.value}
79
+ label={option.label}
80
+ description={option.description}
81
+ disabled={option.disabled}
82
+ {tone}
83
+ />
84
+ {/if}
85
+ {/each}
86
+ {:else if children}
87
+ {@render children()}
88
+ {/if}
89
+ </div>
90
+
91
+ {#if error}
92
+ <p class="font-sans text-[13px] text-danger">{error}</p>
93
+ {:else if hint}
94
+ <p class="font-sans text-[13px] text-text-muted">{hint}</p>
95
+ {/if}
96
+ </fieldset>
@@ -0,0 +1,27 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLFieldsetAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../../core/tones.js';
4
+ export interface RadioOption {
5
+ value: string;
6
+ label: string;
7
+ description?: string;
8
+ disabled?: boolean;
9
+ }
10
+ interface Props extends HTMLFieldsetAttributes {
11
+ /** Bindable — the selected option's `value`. */
12
+ value?: string;
13
+ options?: RadioOption[];
14
+ legend?: string;
15
+ hint?: string;
16
+ error?: string;
17
+ required?: boolean;
18
+ tone?: Tone;
19
+ orientation?: 'vertical' | 'horizontal';
20
+ /** Wraps each option in a bordered, clickable card. */
21
+ boxed?: boolean;
22
+ /** Pass your own `Radio`s instead of `options`. */
23
+ children?: Snippet;
24
+ }
25
+ declare const RadioGroup: import("svelte").Component<Props, {}, "value">;
26
+ type RadioGroup = ReturnType<typeof RadioGroup>;
27
+ export default RadioGroup;
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { focusRing, toneRing, type Tone } from '../../core/tones.js';
4
+ import { edge } from './lift.js';
5
+
6
+ export interface SegmentOption {
7
+ value: string;
8
+ label: string;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ export type SegmentedSize = 'sm' | 'md';
13
+
14
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange'> {
15
+ /** Bindable — the selected option's `value`. */
16
+ value?: string;
17
+ options: SegmentOption[];
18
+ /** Submits with a form when set. */
19
+ name?: string;
20
+ size?: SegmentedSize;
21
+ tone?: Tone;
22
+ /** Spread the segments evenly across the full width. */
23
+ fullWidth?: boolean;
24
+ label?: string;
25
+ disabled?: boolean;
26
+ onchange?: (value: string) => void;
27
+ }
28
+
29
+ let {
30
+ value = $bindable(''),
31
+ options,
32
+ name,
33
+ size = 'md',
34
+ tone = 'brand',
35
+ fullWidth = false,
36
+ label,
37
+ disabled = false,
38
+ class: className = '',
39
+ onchange,
40
+ ...rest
41
+ }: Props = $props();
42
+
43
+ const sizes: Record<SegmentedSize, string> = {
44
+ sm: 'h-7 px-2.5 text-[13px]',
45
+ md: 'h-8 px-3.5 text-sm'
46
+ };
47
+
48
+ function select(option: SegmentOption) {
49
+ if (option.disabled || disabled) return;
50
+ value = option.value;
51
+ onchange?.(option.value);
52
+ }
53
+
54
+ function onKeydown(event: KeyboardEvent) {
55
+ const step = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
56
+ if (!step) return;
57
+ event.preventDefault();
58
+ const usable = options.filter((option) => !option.disabled);
59
+ const index = usable.findIndex((option) => option.value === value);
60
+ select(usable[(index + step + usable.length) % usable.length]);
61
+ }
62
+ </script>
63
+
64
+ <div
65
+ role="radiogroup"
66
+ aria-label={label}
67
+ onkeydown={onKeydown}
68
+ class="inline-flex items-center gap-1 rounded-2xl border border-hairline bg-bg-inset p-1
69
+ {fullWidth ? 'flex w-full *:flex-1' : ''}
70
+ {disabled ? 'pointer-events-none opacity-50' : ''} {className}"
71
+ {...rest}
72
+ >
73
+ {#each options as option (option.value)}
74
+ <button
75
+ type="button"
76
+ role="radio"
77
+ aria-checked={value === option.value}
78
+ disabled={option.disabled || disabled}
79
+ tabindex={value === option.value ? 0 : -1}
80
+ onclick={() => select(option)}
81
+ class="inline-flex items-center justify-center rounded-xl font-sans font-medium whitespace-nowrap transition-all duration-150 ease-brand-out disabled:pointer-events-none disabled:opacity-40
82
+ {sizes[size]} {focusRing} {toneRing[tone]}
83
+ {value === option.value ? `bg-bg text-text ${edge}` : 'text-text-muted hover:text-text'}"
84
+ >
85
+ {option.label}
86
+ </button>
87
+ {/each}
88
+ {#if name}<input type="hidden" {name} {value} />{/if}
89
+ </div>
@@ -0,0 +1,25 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../../core/tones.js';
3
+ export interface SegmentOption {
4
+ value: string;
5
+ label: string;
6
+ disabled?: boolean;
7
+ }
8
+ export type SegmentedSize = 'sm' | 'md';
9
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange'> {
10
+ /** Bindable — the selected option's `value`. */
11
+ value?: string;
12
+ options: SegmentOption[];
13
+ /** Submits with a form when set. */
14
+ name?: string;
15
+ size?: SegmentedSize;
16
+ tone?: Tone;
17
+ /** Spread the segments evenly across the full width. */
18
+ fullWidth?: boolean;
19
+ label?: string;
20
+ disabled?: boolean;
21
+ onchange?: (value: string) => void;
22
+ }
23
+ declare const SegmentedControl: import("svelte").Component<Props, {}, "value">;
24
+ type SegmentedControl = ReturnType<typeof SegmentedControl>;
25
+ export default SegmentedControl;
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLSelectAttributes } from 'svelte/elements';
4
+ import { toneFocusBorder, type Tone } from '../../core/tones.js';
5
+ import { edge } from './lift.js';
6
+
7
+ export interface SelectOption {
8
+ value: string;
9
+ label: string;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ interface Props extends HTMLSelectAttributes {
14
+ value?: string;
15
+ /** Data-driven options. Omit and pass `children` to write `<option>`s yourself. */
16
+ options?: SelectOption[];
17
+ placeholder?: string;
18
+ tone?: Tone;
19
+ invalid?: boolean;
20
+ children?: Snippet;
21
+ }
22
+
23
+ let {
24
+ value = $bindable(''),
25
+ options,
26
+ placeholder,
27
+ tone = 'brand',
28
+ invalid = false,
29
+ class: className = '',
30
+ children,
31
+ ...rest
32
+ }: Props = $props();
33
+ </script>
34
+
35
+ <span class="relative inline-flex w-full items-center">
36
+ <select
37
+ bind:value
38
+ aria-invalid={invalid ? 'true' : undefined}
39
+ class="h-10 w-full appearance-none rounded-xl border bg-bg py-0 pr-9 pl-3 font-sans text-sm text-text {edge} transition-colors duration-150 ease-brand-out focus:outline-none disabled:pointer-events-none disabled:opacity-50
40
+ {invalid ? 'border-danger' : `border-hairline-strong ${toneFocusBorder[tone]}`} {className}"
41
+ {...rest}
42
+ >
43
+ {#if placeholder}
44
+ <option value="" disabled>{placeholder}</option>
45
+ {/if}
46
+ {#if options}
47
+ {#each options as option (option.value)}
48
+ <option value={option.value} disabled={option.disabled}>{option.label}</option>
49
+ {/each}
50
+ {:else if children}
51
+ {@render children()}
52
+ {/if}
53
+ </select>
54
+ <svg
55
+ class="pointer-events-none absolute right-3 size-4 text-text-muted"
56
+ viewBox="0 0 16 16"
57
+ fill="none"
58
+ aria-hidden="true"
59
+ >
60
+ <path d="m4 6 4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
61
+ </svg>
62
+ </span>
@@ -0,0 +1,20 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLSelectAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../../core/tones.js';
4
+ export interface SelectOption {
5
+ value: string;
6
+ label: string;
7
+ disabled?: boolean;
8
+ }
9
+ interface Props extends HTMLSelectAttributes {
10
+ value?: string;
11
+ /** Data-driven options. Omit and pass `children` to write `<option>`s yourself. */
12
+ options?: SelectOption[];
13
+ placeholder?: string;
14
+ tone?: Tone;
15
+ invalid?: boolean;
16
+ children?: Snippet;
17
+ }
18
+ declare const Select: import("svelte").Component<Props, {}, "value">;
19
+ type Select = ReturnType<typeof Select>;
20
+ export default Select;
@@ -0,0 +1,255 @@
1
+ <script module lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ export type SidebarVariant = 'plain' | 'filled' | 'floating';
5
+
6
+ export interface SidebarItem {
7
+ /** Stable id used for the active state. Falls back to `href`. */
8
+ id?: string;
9
+ label: string;
10
+ href?: string;
11
+ disabled?: boolean;
12
+ badge?: string | number;
13
+ /**
14
+ * Leading glyph, 16px. The one thing a collapsed rail still shows, so
15
+ * an item that has to survive `collapsed` needs one.
16
+ */
17
+ icon?: Snippet;
18
+ /** Nested links — the parent becomes a collapsible group. */
19
+ items?: SidebarItem[];
20
+ /** Open the group on first render. */
21
+ open?: boolean;
22
+ /**
23
+ * `false` pins a group open: the parent stops being a button and the
24
+ * children are always on show. For navigation a reader should not be
25
+ * able to lose.
26
+ */
27
+ collapsible?: boolean;
28
+ }
29
+
30
+ export interface SidebarSection {
31
+ /** Small heading above a run of items. */
32
+ label?: string;
33
+ items: SidebarItem[];
34
+ }
35
+ </script>
36
+
37
+ <script lang="ts">
38
+ import { useLocale } from '../../core/locale.svelte.js';
39
+ import { focusRing, toneRing, toneSoft, type Tone } from '../../core/tones.js';
40
+
41
+ interface Props {
42
+ sections: SidebarSection[];
43
+ /** Bindable — the `id` (or `href`) of the current item. */
44
+ value?: string;
45
+ /** Bindable — narrow rail mode. */
46
+ collapsed?: boolean;
47
+ /** Show the collapse button. */
48
+ collapsible?: boolean;
49
+ /** Surface treatment: bare, the framed default, or a detached card. */
50
+ variant?: SidebarVariant;
51
+ tone?: Tone;
52
+ header?: Snippet;
53
+ footer?: Snippet;
54
+ onnavigate?: (item: SidebarItem) => void;
55
+ class?: string;
56
+ }
57
+
58
+ let {
59
+ sections,
60
+ value = $bindable(''),
61
+ collapsed = $bindable(false),
62
+ collapsible = true,
63
+ variant = 'filled',
64
+ tone = 'brand',
65
+ header,
66
+ footer,
67
+ onnavigate,
68
+ class: className = ''
69
+ }: Props = $props();
70
+
71
+ const t = useLocale();
72
+
73
+ const keyOf = (item: SidebarItem) => item.id ?? item.href ?? item.label;
74
+
75
+ /** Groups start open when asked, or when they contain the active item. */
76
+ let opened = $state<Record<string, boolean>>({});
77
+
78
+ const isOpen = (item: SidebarItem) =>
79
+ item.collapsible === false ||
80
+ (opened[keyOf(item)] ?? (item.open || item.items!.some((child) => keyOf(child) === value)));
81
+
82
+ function toggle(item: SidebarItem) {
83
+ opened = { ...opened, [keyOf(item)]: !isOpen(item) };
84
+ }
85
+
86
+ function go(item: SidebarItem) {
87
+ if (item.disabled) return;
88
+ value = keyOf(item);
89
+ onnavigate?.(item);
90
+ }
91
+
92
+ const surfaces: Record<SidebarVariant, string> = {
93
+ plain: 'h-full bg-transparent',
94
+ filled: 'h-full border-r border-hairline bg-bg-alt',
95
+ floating: 'm-3 h-[calc(100%-1.5rem)] rounded-2xl border border-hairline bg-bg-alt shadow-sm'
96
+ };
97
+
98
+ const row =
99
+ 'flex w-full items-center gap-3 rounded-xl px-2.5 py-2 text-left font-sans text-sm transition-colors duration-150 ease-brand-out disabled:pointer-events-none disabled:opacity-40';
100
+ </script>
101
+
102
+ <!-- Nested links. One copy, rendered by both kinds of group. -->
103
+ {#snippet subitems(item: SidebarItem)}
104
+ <div class="ml-4 flex flex-col gap-0.5 border-l border-hairline pl-2">
105
+ {#each item.items as child (keyOf(child))}
106
+ <a
107
+ href={child.href}
108
+ onclick={() => go(child)}
109
+ aria-current={value === keyOf(child) ? 'page' : undefined}
110
+ class="{row} {focusRing} {toneRing[tone]} {value === keyOf(child)
111
+ ? `${toneSoft[tone]} font-medium`
112
+ : 'text-text-muted hover:bg-bg-inset hover:text-text'} {child.disabled
113
+ ? 'pointer-events-none opacity-40'
114
+ : ''}"
115
+ >
116
+ {@render glyph(child)}
117
+ <span class="min-w-0 flex-1 truncate">{child.label}</span>
118
+ {#if child.badge !== undefined}
119
+ <span class="shrink-0 text-[11px] tabular-nums">{child.badge}</span>
120
+ {/if}
121
+ </a>
122
+ {/each}
123
+ </div>
124
+ {/snippet}
125
+
126
+ <!-- One box for every glyph, so rows line up whether or not an item has one. -->
127
+ {#snippet glyph(item: SidebarItem)}
128
+ {#if item.icon}
129
+ <span class="grid size-4 shrink-0 place-items-center *:size-4">{@render item.icon()}</span>
130
+ {/if}
131
+ {/snippet}
132
+
133
+ <aside
134
+ class="flex shrink-0 flex-col transition-[width] duration-200 ease-brand-out
135
+ {surfaces[variant]} {collapsed ? 'w-16' : 'w-60'} {className}"
136
+ >
137
+ {#if header}
138
+ <div class="flex h-16 shrink-0 items-center gap-3 border-b border-hairline px-3">
139
+ {@render header()}
140
+ </div>
141
+ {/if}
142
+
143
+ <nav aria-label={t.current.navigation} class="flex-1 overflow-y-auto p-2">
144
+ {#each sections as section, i (section.label ?? i)}
145
+ <div class="flex flex-col gap-0.5 {i > 0 ? 'mt-4' : ''}">
146
+ {#if section.label && !collapsed}
147
+ <p class="px-2.5 pb-1 font-sans text-[11px] font-semibold text-text-muted">
148
+ {section.label}
149
+ </p>
150
+ {:else if section.label && collapsed && i > 0}
151
+ <div class="mx-2 mb-1 h-px bg-hairline" role="separator"></div>
152
+ {/if}
153
+
154
+ {#each section.items as item (keyOf(item))}
155
+ {#if item.items?.length && item.collapsible === false}
156
+ <div class="{row} {collapsed ? 'justify-center' : ''} font-medium text-text-muted">
157
+ {@render glyph(item)}
158
+ <span class="min-w-0 flex-1 truncate {collapsed ? 'sr-only' : ''}">{item.label}</span>
159
+ </div>
160
+ {#if isOpen(item) && !collapsed}
161
+ {@render subitems(item)}
162
+ {/if}
163
+ {:else if item.items?.length}
164
+ <button
165
+ type="button"
166
+ onclick={() => toggle(item)}
167
+ aria-expanded={isOpen(item)}
168
+ title={collapsed ? item.label : undefined}
169
+ class="{row} {collapsed ? 'justify-center' : ''} {focusRing} {toneRing[
170
+ tone
171
+ ]} text-text-secondary hover:bg-bg-inset hover:text-text"
172
+ >
173
+ {@render glyph(item)}
174
+ <span class="min-w-0 flex-1 truncate {collapsed ? 'sr-only' : ''}">{item.label}</span>
175
+ {#if !collapsed}
176
+ <svg
177
+ class="size-3.5 shrink-0 transition-transform duration-200 {isOpen(item)
178
+ ? 'rotate-90'
179
+ : ''}"
180
+ viewBox="0 0 16 16"
181
+ fill="none"
182
+ aria-hidden="true"
183
+ >
184
+ <path
185
+ d="m6 4 4 4-4 4"
186
+ stroke="currentColor"
187
+ stroke-width="1.6"
188
+ stroke-linecap="round"
189
+ />
190
+ </svg>
191
+ {/if}
192
+ </button>
193
+
194
+ {#if isOpen(item) && !collapsed}
195
+ {@render subitems(item)}
196
+ {/if}
197
+ {:else}
198
+ <a
199
+ href={item.href}
200
+ onclick={() => go(item)}
201
+ aria-current={value === keyOf(item) ? 'page' : undefined}
202
+ title={collapsed ? item.label : undefined}
203
+ class="{row} {collapsed ? 'justify-center' : ''} {focusRing} {toneRing[
204
+ tone
205
+ ]} {value === keyOf(item)
206
+ ? `${toneSoft[tone]} font-medium`
207
+ : 'text-text-secondary hover:bg-bg-inset hover:text-text'} {item.disabled
208
+ ? 'pointer-events-none opacity-40'
209
+ : ''}"
210
+ >
211
+ {@render glyph(item)}
212
+ <span class="min-w-0 flex-1 truncate {collapsed ? 'sr-only' : ''}">{item.label}</span>
213
+ {#if item.badge !== undefined && !collapsed}
214
+ <span class="shrink-0 text-[11px] text-text-muted tabular-nums">{item.badge}</span>
215
+ {/if}
216
+ </a>
217
+ {/if}
218
+ {/each}
219
+ </div>
220
+ {/each}
221
+ </nav>
222
+
223
+ {#if footer || collapsible}
224
+ <div class="flex shrink-0 flex-col gap-2 border-t border-hairline p-2">
225
+ {#if footer}
226
+ {@render footer()}
227
+ {/if}
228
+ {#if collapsible}
229
+ <button
230
+ type="button"
231
+ onclick={() => (collapsed = !collapsed)}
232
+ aria-label={collapsed ? t.current.expandSidebar : t.current.collapseSidebar}
233
+ class="inline-flex h-8 items-center justify-center gap-2 rounded-xl font-sans text-xs text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text {focusRing} {toneRing[
234
+ tone
235
+ ]}"
236
+ >
237
+ <svg
238
+ class="size-4 transition-transform duration-200 {collapsed ? 'rotate-180' : ''}"
239
+ viewBox="0 0 16 16"
240
+ fill="none"
241
+ aria-hidden="true"
242
+ >
243
+ <path
244
+ d="m10 4-4 4 4 4"
245
+ stroke="currentColor"
246
+ stroke-width="1.6"
247
+ stroke-linecap="round"
248
+ />
249
+ </svg>
250
+ {#if !collapsed}<span>{t.current.collapseSidebar}</span>{/if}
251
+ </button>
252
+ {/if}
253
+ </div>
254
+ {/if}
255
+ </aside>