@nqmcreative/ui 0.2.0 → 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 (141) hide show
  1. package/README.md +7 -5
  2. package/cli/index.mjs +16 -3
  3. package/dist/styles/matte/index.d.ts +1 -1
  4. package/dist/styles/sprout/Accordion.svelte +17 -0
  5. package/dist/styles/sprout/Accordion.svelte.d.ts +8 -0
  6. package/dist/styles/sprout/AccordionItem.svelte +46 -0
  7. package/dist/styles/sprout/AccordionItem.svelte.d.ts +14 -0
  8. package/dist/styles/sprout/Alert.svelte +100 -0
  9. package/dist/styles/sprout/Alert.svelte.d.ts +18 -0
  10. package/dist/styles/sprout/Avatar.svelte +60 -0
  11. package/dist/styles/sprout/Avatar.svelte.d.ts +16 -0
  12. package/dist/styles/sprout/AvatarGroup.svelte +17 -0
  13. package/dist/styles/sprout/AvatarGroup.svelte.d.ts +8 -0
  14. package/dist/styles/sprout/Badge.svelte +58 -0
  15. package/dist/styles/sprout/Badge.svelte.d.ts +16 -0
  16. package/dist/styles/sprout/Breadcrumb.svelte +49 -0
  17. package/dist/styles/sprout/Breadcrumb.svelte.d.ts +14 -0
  18. package/dist/styles/sprout/Button.svelte +105 -0
  19. package/dist/styles/sprout/Button.svelte.d.ts +21 -0
  20. package/dist/styles/sprout/CTASection.svelte +77 -0
  21. package/dist/styles/sprout/CTASection.svelte.d.ts +18 -0
  22. package/dist/styles/sprout/Calendar.svelte +202 -0
  23. package/dist/styles/sprout/Calendar.svelte.d.ts +22 -0
  24. package/dist/styles/sprout/Card.svelte +82 -0
  25. package/dist/styles/sprout/Card.svelte.d.ts +21 -0
  26. package/dist/styles/sprout/Checkbox.svelte +76 -0
  27. package/dist/styles/sprout/Checkbox.svelte.d.ts +15 -0
  28. package/dist/styles/sprout/CheckboxGroup.svelte +112 -0
  29. package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +30 -0
  30. package/dist/styles/sprout/Combobox.svelte +267 -0
  31. package/dist/styles/sprout/Combobox.svelte.d.ts +29 -0
  32. package/dist/styles/sprout/CommandPalette.svelte +210 -0
  33. package/dist/styles/sprout/CommandPalette.svelte.d.ts +32 -0
  34. package/dist/styles/sprout/ConfirmDialog.svelte +97 -0
  35. package/dist/styles/sprout/ConfirmDialog.svelte.d.ts +22 -0
  36. package/dist/styles/sprout/ContextMenu.svelte +89 -0
  37. package/dist/styles/sprout/ContextMenu.svelte.d.ts +15 -0
  38. package/dist/styles/sprout/DatePicker.svelte +193 -0
  39. package/dist/styles/sprout/DatePicker.svelte.d.ts +23 -0
  40. package/dist/styles/sprout/Divider.svelte +30 -0
  41. package/dist/styles/sprout/Divider.svelte.d.ts +10 -0
  42. package/dist/styles/sprout/Drawer.svelte +166 -0
  43. package/dist/styles/sprout/Drawer.svelte.d.ts +20 -0
  44. package/dist/styles/sprout/Dropdown.svelte +95 -0
  45. package/dist/styles/sprout/Dropdown.svelte.d.ts +23 -0
  46. package/dist/styles/sprout/Dropzone.svelte +211 -0
  47. package/dist/styles/sprout/Dropzone.svelte.d.ts +29 -0
  48. package/dist/styles/sprout/EmptyState.svelte +71 -0
  49. package/dist/styles/sprout/EmptyState.svelte.d.ts +18 -0
  50. package/dist/styles/sprout/FeatureCard.svelte +61 -0
  51. package/dist/styles/sprout/FeatureCard.svelte.d.ts +16 -0
  52. package/dist/styles/sprout/FeatureGrid.svelte +62 -0
  53. package/dist/styles/sprout/FeatureGrid.svelte.d.ts +16 -0
  54. package/dist/styles/sprout/Field.svelte +42 -0
  55. package/dist/styles/sprout/Field.svelte.d.ts +16 -0
  56. package/dist/styles/sprout/Footer.svelte +91 -0
  57. package/dist/styles/sprout/Footer.svelte.d.ts +25 -0
  58. package/dist/styles/sprout/HeroSection.svelte +86 -0
  59. package/dist/styles/sprout/HeroSection.svelte.d.ts +21 -0
  60. package/dist/styles/sprout/Input.svelte +61 -0
  61. package/dist/styles/sprout/Input.svelte.d.ts +18 -0
  62. package/dist/styles/sprout/InputGroup.svelte +20 -0
  63. package/dist/styles/sprout/InputGroup.svelte.d.ts +11 -0
  64. package/dist/styles/sprout/Kbd.svelte +18 -0
  65. package/dist/styles/sprout/Kbd.svelte.d.ts +8 -0
  66. package/dist/styles/sprout/Label.svelte +16 -0
  67. package/dist/styles/sprout/Label.svelte.d.ts +9 -0
  68. package/dist/styles/sprout/Link.svelte +44 -0
  69. package/dist/styles/sprout/Link.svelte.d.ts +16 -0
  70. package/dist/styles/sprout/LocaleProvider.svelte +24 -0
  71. package/dist/styles/sprout/LocaleProvider.svelte.d.ts +10 -0
  72. package/dist/styles/sprout/LogoCloud.svelte +61 -0
  73. package/dist/styles/sprout/LogoCloud.svelte.d.ts +19 -0
  74. package/dist/styles/sprout/MenuItem.svelte +64 -0
  75. package/dist/styles/sprout/MenuItem.svelte.d.ts +18 -0
  76. package/dist/styles/sprout/MenuSeparator.svelte +22 -0
  77. package/dist/styles/sprout/MenuSeparator.svelte.d.ts +8 -0
  78. package/dist/styles/sprout/Modal.svelte +119 -0
  79. package/dist/styles/sprout/Modal.svelte.d.ts +21 -0
  80. package/dist/styles/sprout/MultiSelect.svelte +288 -0
  81. package/dist/styles/sprout/MultiSelect.svelte.d.ts +31 -0
  82. package/dist/styles/sprout/Navbar.svelte +201 -0
  83. package/dist/styles/sprout/Navbar.svelte.d.ts +33 -0
  84. package/dist/styles/sprout/NumberInput.svelte +97 -0
  85. package/dist/styles/sprout/NumberInput.svelte.d.ts +15 -0
  86. package/dist/styles/sprout/Pagination.svelte +84 -0
  87. package/dist/styles/sprout/Pagination.svelte.d.ts +15 -0
  88. package/dist/styles/sprout/PasswordInput.svelte +101 -0
  89. package/dist/styles/sprout/PasswordInput.svelte.d.ts +12 -0
  90. package/dist/styles/sprout/Popover.svelte +90 -0
  91. package/dist/styles/sprout/Popover.svelte.d.ts +19 -0
  92. package/dist/styles/sprout/PricingCard.svelte +109 -0
  93. package/dist/styles/sprout/PricingCard.svelte.d.ts +25 -0
  94. package/dist/styles/sprout/Progress.svelte +59 -0
  95. package/dist/styles/sprout/Progress.svelte.d.ts +17 -0
  96. package/dist/styles/sprout/Radio.svelte +56 -0
  97. package/dist/styles/sprout/Radio.svelte.d.ts +15 -0
  98. package/dist/styles/sprout/RadioGroup.svelte +96 -0
  99. package/dist/styles/sprout/RadioGroup.svelte.d.ts +27 -0
  100. package/dist/styles/sprout/SegmentedControl.svelte +89 -0
  101. package/dist/styles/sprout/SegmentedControl.svelte.d.ts +25 -0
  102. package/dist/styles/sprout/Select.svelte +62 -0
  103. package/dist/styles/sprout/Select.svelte.d.ts +20 -0
  104. package/dist/styles/sprout/Sidebar.svelte +206 -0
  105. package/dist/styles/sprout/Sidebar.svelte.d.ts +36 -0
  106. package/dist/styles/sprout/Skeleton.svelte +46 -0
  107. package/dist/styles/sprout/Skeleton.svelte.d.ts +13 -0
  108. package/dist/styles/sprout/Slider.svelte +80 -0
  109. package/dist/styles/sprout/Slider.svelte.d.ts +19 -0
  110. package/dist/styles/sprout/Spinner.svelte +34 -0
  111. package/dist/styles/sprout/Spinner.svelte.d.ts +13 -0
  112. package/dist/styles/sprout/Stat.svelte +51 -0
  113. package/dist/styles/sprout/Stat.svelte.d.ts +16 -0
  114. package/dist/styles/sprout/StatsBand.svelte +64 -0
  115. package/dist/styles/sprout/StatsBand.svelte.d.ts +20 -0
  116. package/dist/styles/sprout/Steps.svelte +143 -0
  117. package/dist/styles/sprout/Steps.svelte.d.ts +24 -0
  118. package/dist/styles/sprout/Switch.svelte +65 -0
  119. package/dist/styles/sprout/Switch.svelte.d.ts +14 -0
  120. package/dist/styles/sprout/Table.svelte +218 -0
  121. package/dist/styles/sprout/Table.svelte.d.ts +66 -0
  122. package/dist/styles/sprout/Tabs.svelte +103 -0
  123. package/dist/styles/sprout/Tabs.svelte.d.ts +23 -0
  124. package/dist/styles/sprout/Testimonial.svelte +65 -0
  125. package/dist/styles/sprout/Testimonial.svelte.d.ts +19 -0
  126. package/dist/styles/sprout/Textarea.svelte +55 -0
  127. package/dist/styles/sprout/Textarea.svelte.d.ts +12 -0
  128. package/dist/styles/sprout/ThemeToggle.svelte +131 -0
  129. package/dist/styles/sprout/ThemeToggle.svelte.d.ts +16 -0
  130. package/dist/styles/sprout/Toaster.svelte +133 -0
  131. package/dist/styles/sprout/Toaster.svelte.d.ts +8 -0
  132. package/dist/styles/sprout/Tooltip.svelte +34 -0
  133. package/dist/styles/sprout/Tooltip.svelte.d.ts +12 -0
  134. package/dist/styles/sprout/fonts.css +78 -0
  135. package/dist/styles/sprout/index.d.ts +109 -0
  136. package/dist/styles/sprout/index.js +83 -0
  137. package/dist/styles/sprout/lift.d.ts +40 -0
  138. package/dist/styles/sprout/lift.js +36 -0
  139. package/dist/styles/sprout/theme.css +255 -0
  140. package/package.json +269 -3
  141. package/registry.json +137 -65
package/README.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # @nqmcreative/ui
2
2
 
3
- Svelte 5 (runes) + Tailwind CSS v4 component library. One package, **two
3
+ Svelte 5 (runes) + Tailwind CSS v4 component library. One package, **three
4
4
  styles**, 65 components each — the same props, the same behaviour, a different
5
5
  look. Eight tones, light and dark, no runtime dependencies.
6
6
 
7
- | style | import | character |
8
- | ------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
9
- | `matte` | `@nqmcreative/ui/matte` | Flat and sharp-cornered — no radius, no shadow, no gloss. Hairline rules and mono micro-labels do the work borders usually do. |
10
- | `paper` | `@nqmcreative/ui/paper` | The neutral one. Start here if unsure: soft corners, light shadows, generous spacing, and nothing that fights the content. |
7
+ | style | import | character |
8
+ | -------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
9
+ | `matte` | `@nqmcreative/ui/matte` | Flat and sharp-cornered — no radius, no shadow, no gloss. Hairline rules and mono micro-labels do the work borders usually do. |
10
+ | `paper` | `@nqmcreative/ui/paper` | The neutral one. Start here if unsure: soft corners, light shadows, generous spacing, and nothing that fights the content. |
11
+ | `sprout` | `@nqmcreative/ui/sprout` | Warm and fully rounded — leaf green on cream, pill buttons, and a round display face. For school, community and family sites. |
11
12
 
12
13
  The docs site lives in this repo — every component with a live demo, plus
13
14
  theming, locale and contributing guides. `bun run dev` to read it.
@@ -132,6 +133,7 @@ Under SSR use `<LocaleProvider>` instead, so the locale is scoped to a request.
132
133
  src/lib/core/ behaviour, a11y, types — no markup, no classes
133
134
  src/lib/styles/matte/ 65 components
134
135
  src/lib/styles/paper/ 65 components
136
+ src/lib/styles/sprout/ 65 components
135
137
  scripts/catalogue.mjs the list every style must implement
136
138
  ```
137
139
 
package/cli/index.mjs CHANGED
@@ -191,9 +191,22 @@ async function init() {
191
191
  if (css.includes(`@nqmcreative/ui/${style.name}/theme.css`)) {
192
192
  skip(`${project.cssRel} already imports the theme`);
193
193
  } else {
194
- const body = css.trim()
195
- ? `${lines.join('\n')}\n\n${css.replace(/@import 'tailwindcss';\n?/, '').trim()}\n`
196
- : `${lines.join('\n')}\n`;
194
+ // Drop whatever this package left here before — an older layout's
195
+ // `@nqmcreative/ui/theme.css`, another style's, and the `@source` that
196
+ // went with them. Keeping those would either fight the new tokens or fail
197
+ // to resolve, so a rewrite is cleaner than an append.
198
+ const stale =
199
+ /^@import ['"]@nqmcreative\/ui\/[^'"]+\.css['"];$|^@source ['"][^'"]*@nqmcreative\/ui[^'"]*['"];$/;
200
+
201
+ const kept = css
202
+ .split('\n')
203
+ .filter((line) => !stale.test(line.trim()))
204
+ .join('\n')
205
+ .replace(/@import ['"]tailwindcss['"];\n?/, '')
206
+ .replace(/\n{3,}/g, '\n\n')
207
+ .trim();
208
+
209
+ const body = kept ? `${lines.join('\n')}\n\n${kept}\n` : `${lines.join('\n')}\n`;
197
210
  await write(project.css, body, project.cssRel);
198
211
  }
199
212
 
@@ -40,7 +40,7 @@ export { default as DatePicker } from './DatePicker.svelte';
40
40
  export { default as Calendar } from './Calendar.svelte';
41
41
  export { default as Dropzone } from './Dropzone.svelte';
42
42
  export { default as Badge } from './Badge.svelte';
43
- export type { BadgeTone, BadgeVariant, BadgeSize } from './Badge.svelte';
43
+ export type { BadgeVariant, BadgeSize } from './Badge.svelte';
44
44
  export { default as Avatar } from './Avatar.svelte';
45
45
  export type { AvatarSize } from './Avatar.svelte';
46
46
  export { default as AvatarGroup } from './AvatarGroup.svelte';
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ interface Props extends HTMLAttributes<HTMLDivElement> {
6
+ children: Snippet;
7
+ }
8
+
9
+ let { class: className = '', children, ...rest }: Props = $props();
10
+ </script>
11
+
12
+ <div
13
+ class="flex flex-col overflow-hidden rounded-[20px] border border-hairline bg-bg {className}"
14
+ {...rest}
15
+ >
16
+ {@render children()}
17
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLAttributes<HTMLDivElement> {
4
+ children: Snippet;
5
+ }
6
+ declare const Accordion: import("svelte").Component<Props, {}, "">;
7
+ type Accordion = ReturnType<typeof Accordion>;
8
+ export default Accordion;
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { toneText, type Tone } from '../../core/tones.js';
5
+
6
+ interface Props extends HTMLAttributes<HTMLDetailsElement> {
7
+ title: string;
8
+ open?: boolean;
9
+ tone?: Tone;
10
+ /** Short text on the right of the summary row. */
11
+ meta?: string;
12
+ children: Snippet;
13
+ }
14
+
15
+ let {
16
+ title,
17
+ open = $bindable(false),
18
+ tone = 'brand',
19
+ meta,
20
+ class: className = '',
21
+ children,
22
+ ...rest
23
+ }: Props = $props();
24
+ </script>
25
+
26
+ <details bind:open class="group border-b border-hairline last:border-b-0 {className}" {...rest}>
27
+ <summary
28
+ class="flex cursor-pointer list-none items-center gap-4 px-4 py-3.5 font-sans text-sm font-medium text-text transition-colors duration-150 ease-brand-out select-none hover:bg-bg-alt focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-current [&::-webkit-details-marker]:hidden"
29
+ >
30
+ <span class="flex-1">{title}</span>
31
+ {#if meta}<span class="text-[13px] font-normal text-text-muted">{meta}</span>{/if}
32
+ <svg
33
+ class="size-4 shrink-0 transition-transform duration-200 ease-brand-out group-open:rotate-180 {toneText[
34
+ tone
35
+ ]}"
36
+ viewBox="0 0 16 16"
37
+ fill="none"
38
+ aria-hidden="true"
39
+ >
40
+ <path d="m4 6 4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
41
+ </svg>
42
+ </summary>
43
+ <div class="px-4 pb-4 font-sans text-sm leading-relaxed text-text-secondary">
44
+ {@render children()}
45
+ </div>
46
+ </details>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../../core/tones.js';
4
+ interface Props extends HTMLAttributes<HTMLDetailsElement> {
5
+ title: string;
6
+ open?: boolean;
7
+ tone?: Tone;
8
+ /** Short text on the right of the summary row. */
9
+ meta?: string;
10
+ children: Snippet;
11
+ }
12
+ declare const AccordionItem: import("svelte").Component<Props, {}, "open">;
13
+ type AccordionItem = ReturnType<typeof AccordionItem>;
14
+ export default AccordionItem;
@@ -0,0 +1,100 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import {
5
+ toneBorder,
6
+ toneBorderSoft,
7
+ toneSurface,
8
+ toneText,
9
+ type Tone
10
+ } from '../../core/tones.js';
11
+ import { useLocale } from '../../core/locale.svelte.js';
12
+ import { edge } from './lift.js';
13
+
14
+ export type AlertVariant = 'soft' | 'outline' | 'accent';
15
+
16
+ interface Props extends HTMLAttributes<HTMLDivElement> {
17
+ tone?: Tone;
18
+ variant?: AlertVariant;
19
+ title?: string;
20
+ /** Shows a close button and fires `ondismiss`. */
21
+ dismissible?: boolean;
22
+ ondismiss?: () => void;
23
+ /** Replaces the built-in status glyph. */
24
+ icon?: Snippet;
25
+ children?: Snippet;
26
+ }
27
+
28
+ let {
29
+ tone = 'info',
30
+ variant = 'soft',
31
+ title,
32
+ dismissible = false,
33
+ ondismiss,
34
+ icon,
35
+ class: className = '',
36
+ children,
37
+ ...rest
38
+ }: Props = $props();
39
+
40
+ const t = useLocale();
41
+ let open = $state(true);
42
+
43
+ const variants = $derived({
44
+ soft: `${toneSurface[tone]} border ${toneBorderSoft[tone]}`,
45
+ outline: `bg-bg ${edge} border ${toneBorderSoft[tone]}`,
46
+ accent: `${toneSurface[tone]} border-l-4 ${toneBorder[tone]}`
47
+ });
48
+
49
+ function dismiss() {
50
+ open = false;
51
+ ondismiss?.();
52
+ }
53
+ </script>
54
+
55
+ {#if open}
56
+ <div
57
+ role={tone === 'danger' ? 'alert' : 'status'}
58
+ class="flex items-start gap-3 rounded-[20px] p-4 font-sans {variants[variant]} {className}"
59
+ {...rest}
60
+ >
61
+ <span class="mt-0.5 shrink-0 {toneText[tone]}">
62
+ {#if icon}
63
+ {@render icon()}
64
+ {:else}
65
+ <svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
66
+ <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5" />
67
+ <path d="M8 4.75v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
68
+ <circle cx="8" cy="11.25" r="0.85" fill="currentColor" />
69
+ </svg>
70
+ {/if}
71
+ </span>
72
+
73
+ <div class="flex min-w-0 flex-1 flex-col gap-1">
74
+ {#if title}
75
+ <p class="text-sm leading-snug font-semibold {toneText[tone]}">{title}</p>
76
+ {/if}
77
+ {#if children}
78
+ <div class="text-[13px] leading-relaxed text-text-secondary">{@render children()}</div>
79
+ {/if}
80
+ </div>
81
+
82
+ {#if dismissible}
83
+ <button
84
+ type="button"
85
+ onclick={dismiss}
86
+ aria-label={t.current.dismiss}
87
+ class="-m-1 shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg/60 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
88
+ >
89
+ <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
90
+ <path
91
+ d="m2 2 10 10M12 2 2 12"
92
+ stroke="currentColor"
93
+ stroke-width="1.6"
94
+ stroke-linecap="round"
95
+ />
96
+ </svg>
97
+ </button>
98
+ {/if}
99
+ </div>
100
+ {/if}
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../../core/tones.js';
4
+ export type AlertVariant = 'soft' | 'outline' | 'accent';
5
+ interface Props extends HTMLAttributes<HTMLDivElement> {
6
+ tone?: Tone;
7
+ variant?: AlertVariant;
8
+ title?: string;
9
+ /** Shows a close button and fires `ondismiss`. */
10
+ dismissible?: boolean;
11
+ ondismiss?: () => void;
12
+ /** Replaces the built-in status glyph. */
13
+ icon?: Snippet;
14
+ children?: Snippet;
15
+ }
16
+ declare const Alert: import("svelte").Component<Props, {}, "">;
17
+ type Alert = ReturnType<typeof Alert>;
18
+ export default Alert;
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { toneSoft, type Tone } from '../../core/tones.js';
4
+
5
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
6
+
7
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
8
+ src?: string;
9
+ alt?: string;
10
+ /** Falls back to the initials of this name when there is no `src`. */
11
+ name?: string;
12
+ size?: AvatarSize;
13
+ tone?: Tone;
14
+ /** Rounded square instead of the default circle. */
15
+ squared?: boolean;
16
+ }
17
+
18
+ let {
19
+ src,
20
+ alt,
21
+ name = '',
22
+ size = 'md',
23
+ tone = 'brand',
24
+ squared = false,
25
+ class: className = '',
26
+ ...rest
27
+ }: Props = $props();
28
+
29
+ const sizes: Record<AvatarSize, string> = {
30
+ xs: 'size-6 text-[10px]',
31
+ sm: 'size-8 text-xs',
32
+ md: 'size-10 text-sm',
33
+ lg: 'size-12 text-base',
34
+ xl: 'size-16 text-lg'
35
+ };
36
+
37
+ const initials = $derived(
38
+ name
39
+ .trim()
40
+ .split(/\s+/)
41
+ .slice(0, 2)
42
+ .map((part) => part[0] ?? '')
43
+ .join('')
44
+ .toUpperCase()
45
+ );
46
+ </script>
47
+
48
+ <span
49
+ class="inline-flex shrink-0 items-center justify-center overflow-hidden font-sans font-semibold select-none
50
+ {squared ? 'rounded-2xl' : 'rounded-full'} {sizes[size]} {src
51
+ ? 'bg-bg-inset'
52
+ : toneSoft[tone]} {className}"
53
+ {...rest}
54
+ >
55
+ {#if src}
56
+ <img {src} alt={alt ?? name} class="size-full object-cover" />
57
+ {:else}
58
+ {initials}
59
+ {/if}
60
+ </span>
@@ -0,0 +1,16 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../../core/tones.js';
3
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
4
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
5
+ src?: string;
6
+ alt?: string;
7
+ /** Falls back to the initials of this name when there is no `src`. */
8
+ name?: string;
9
+ size?: AvatarSize;
10
+ tone?: Tone;
11
+ /** Rounded square instead of the default circle. */
12
+ squared?: boolean;
13
+ }
14
+ declare const Avatar: import("svelte").Component<Props, {}, "">;
15
+ type Avatar = ReturnType<typeof Avatar>;
16
+ export default Avatar;
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ interface Props extends HTMLAttributes<HTMLDivElement> {
6
+ children: Snippet;
7
+ }
8
+
9
+ let { class: className = '', children, ...rest }: Props = $props();
10
+ </script>
11
+
12
+ <div
13
+ class="flex items-center *:-ml-2 *:ring-2 *:ring-bg [&>*:first-child]:ml-0 {className}"
14
+ {...rest}
15
+ >
16
+ {@render children()}
17
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLAttributes<HTMLDivElement> {
4
+ children: Snippet;
5
+ }
6
+ declare const AvatarGroup: import("svelte").Component<Props, {}, "">;
7
+ type AvatarGroup = ReturnType<typeof AvatarGroup>;
8
+ export default AvatarGroup;
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import {
5
+ toneBorderSoft,
6
+ toneFill,
7
+ toneSoft,
8
+ toneSolid,
9
+ toneText,
10
+ type Tone
11
+ } from '../../core/tones.js';
12
+
13
+ export type BadgeVariant = 'soft' | 'solid' | 'outline';
14
+ export type BadgeSize = 'sm' | 'md';
15
+
16
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
17
+ tone?: Tone;
18
+ variant?: BadgeVariant;
19
+ size?: BadgeSize;
20
+ /** Small filled circle before the label. */
21
+ dot?: boolean;
22
+ children: Snippet;
23
+ }
24
+
25
+ let {
26
+ tone = 'brand',
27
+ variant = 'soft',
28
+ size = 'md',
29
+ dot = false,
30
+ class: className = '',
31
+ children,
32
+ ...rest
33
+ }: Props = $props();
34
+
35
+ const variants: Record<BadgeVariant, string> = $derived({
36
+ soft: toneSoft[tone],
37
+ solid: toneSolid[tone],
38
+ outline: `border ${toneBorderSoft[tone]} ${toneText[tone]} bg-bg`
39
+ });
40
+
41
+ const sizes: Record<BadgeSize, string> = {
42
+ sm: 'px-2.5 py-0.5 text-[11px]',
43
+ md: 'px-3 py-1 text-xs'
44
+ };
45
+ </script>
46
+
47
+ <span
48
+ class="inline-flex items-center gap-1.5 rounded-full font-heading font-medium tracking-[0.04em]
49
+ {variants[variant]} {sizes[size]} {className}"
50
+ {...rest}
51
+ >
52
+ {#if dot}
53
+ <span
54
+ class="size-1.5 shrink-0 rounded-full {variant === 'solid' ? 'bg-current' : toneFill[tone]}"
55
+ ></span>
56
+ {/if}
57
+ {@render children()}
58
+ </span>
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../../core/tones.js';
4
+ export type BadgeVariant = 'soft' | 'solid' | 'outline';
5
+ export type BadgeSize = 'sm' | 'md';
6
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
7
+ tone?: Tone;
8
+ variant?: BadgeVariant;
9
+ size?: BadgeSize;
10
+ /** Small filled circle before the label. */
11
+ dot?: boolean;
12
+ children: Snippet;
13
+ }
14
+ declare const Badge: import("svelte").Component<Props, {}, "">;
15
+ type Badge = ReturnType<typeof Badge>;
16
+ export default Badge;
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { toneHoverText, type Tone } from '../../core/tones.js';
4
+ import { useLocale } from '../../core/locale.svelte.js';
5
+
6
+ export interface BreadcrumbItem {
7
+ label: string;
8
+ href?: string;
9
+ }
10
+
11
+ interface Props extends HTMLAttributes<HTMLElement> {
12
+ items: BreadcrumbItem[];
13
+ separator?: string;
14
+ tone?: Tone;
15
+ }
16
+
17
+ let { items, separator = '/', tone = 'brand', class: className = '', ...rest }: Props = $props();
18
+
19
+ const t = useLocale();
20
+ </script>
21
+
22
+ <nav aria-label={t.current.breadcrumb} class="font-sans text-sm {className}" {...rest}>
23
+ <ol class="flex flex-wrap items-center gap-1.5">
24
+ {#each items as item, i (item.label)}
25
+ <li class="flex items-center gap-1.5">
26
+ {#if i > 0}
27
+ <span class="text-hairline-strong select-none" aria-hidden="true">{separator}</span>
28
+ {/if}
29
+ {#if item.href && i < items.length - 1}
30
+ <a
31
+ href={item.href}
32
+ class="rounded px-1 py-0.5 text-text-muted transition-colors duration-150 ease-brand-out hover:bg-bg-inset {toneHoverText[
33
+ tone
34
+ ]}"
35
+ >
36
+ {item.label}
37
+ </a>
38
+ {:else}
39
+ <span
40
+ class="px-1 py-0.5 font-medium text-text"
41
+ aria-current={i === items.length - 1 ? 'page' : undefined}
42
+ >
43
+ {item.label}
44
+ </span>
45
+ {/if}
46
+ </li>
47
+ {/each}
48
+ </ol>
49
+ </nav>
@@ -0,0 +1,14 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../../core/tones.js';
3
+ export interface BreadcrumbItem {
4
+ label: string;
5
+ href?: string;
6
+ }
7
+ interface Props extends HTMLAttributes<HTMLElement> {
8
+ items: BreadcrumbItem[];
9
+ separator?: string;
10
+ tone?: Tone;
11
+ }
12
+ declare const Breadcrumb: import("svelte").Component<Props, {}, "">;
13
+ type Breadcrumb = ReturnType<typeof Breadcrumb>;
14
+ export default Breadcrumb;
@@ -0,0 +1,105 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLButtonAttributes } from 'svelte/elements';
4
+ import Spinner from './Spinner.svelte';
5
+ import {
6
+ focusRing,
7
+ toneBorderSoft,
8
+ toneRing,
9
+ toneSoft,
10
+ toneSoftHover,
11
+ toneSolid,
12
+ toneSolidHover,
13
+ toneText,
14
+ type Tone
15
+ } from '../../core/tones.js';
16
+ import { edge, lift } from './lift.js';
17
+
18
+ export type ButtonVariant = 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'primary';
19
+ export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
20
+
21
+ interface Props extends HTMLButtonAttributes {
22
+ variant?: ButtonVariant;
23
+ tone?: Tone;
24
+ size?: ButtonSize;
25
+ /** Renders an `<a>` instead of a `<button>`. */
26
+ href?: string;
27
+ target?: string;
28
+ rel?: string;
29
+ loading?: boolean;
30
+ /** Stretch to the container's full width. */
31
+ block?: boolean;
32
+ children: Snippet;
33
+ }
34
+
35
+ let {
36
+ variant = 'solid',
37
+ tone = 'brand',
38
+ size = 'md',
39
+ href,
40
+ loading = false,
41
+ block = false,
42
+ disabled = false,
43
+ type = 'button',
44
+ class: className = '',
45
+ children,
46
+ ...rest
47
+ }: Props = $props();
48
+
49
+ // Fully round, in the heading face, and standing on a solid lip it sinks
50
+ // into when pressed: the three things that make a sprout button read as
51
+ // something to touch rather than something to click. The 3px it travels is
52
+ // exactly what the lip loses, so the top face lands where the shadow was.
53
+ const base =
54
+ 'inline-flex items-center justify-center gap-2 rounded-full border font-heading font-semibold whitespace-nowrap transition-[color,background-color,border-color,box-shadow,transform] duration-150 ease-brand-out active:translate-y-[3px] disabled:opacity-50 disabled:pointer-events-none disabled:active:translate-y-0 aria-disabled:opacity-50 aria-disabled:pointer-events-none';
55
+
56
+ // Roomier than the other styles' — a pill needs horizontal padding to keep its label
57
+ // off the curve, and the extra width suits a touch target.
58
+ const sizes: Record<ButtonSize, string> = {
59
+ sm: 'h-9 px-4 text-[13px]',
60
+ md: 'h-11 px-5 text-sm',
61
+ lg: 'h-12 px-6 text-[15px]',
62
+ xl: 'h-14 px-8 text-base'
63
+ };
64
+
65
+ const variantClass = $derived.by(() => {
66
+ switch (variant) {
67
+ case 'primary':
68
+ case 'solid':
69
+ return `border-transparent ${toneSolid[tone]} ${toneSolidHover[tone]} ${lift[tone]}`;
70
+ case 'soft':
71
+ return `border-transparent ${toneSoft[tone]} ${toneSoftHover[tone]} ${edge}`;
72
+ // Outline keeps its own colours on hover — swapping to a solid fill is
73
+ // matte's move, and it fights sprout's soft surfaces.
74
+ case 'outline':
75
+ return `bg-bg ${edge} ${toneText[tone]} ${toneBorderSoft[tone]} hover:bg-bg-alt`;
76
+ // Nothing raised, so nothing to sink: ghost stays flat on the page.
77
+ case 'ghost':
78
+ return `border-transparent bg-transparent ${toneText[tone]} ${toneSoftHover[tone]} active:translate-y-0`;
79
+ // No pill and no dip: this variant is a sentence, not a target.
80
+ case 'link':
81
+ return `border-transparent ${toneText[tone]} h-auto p-0 text-sm underline-offset-4 hover:underline active:translate-y-0`;
82
+ }
83
+ });
84
+
85
+ const isLink = $derived(variant === 'link');
86
+ const inert = $derived(disabled || loading);
87
+ </script>
88
+
89
+ <svelte:element
90
+ this={href ? 'a' : 'button'}
91
+ {href}
92
+ type={href ? undefined : type}
93
+ disabled={href ? undefined : inert}
94
+ aria-disabled={href && inert ? 'true' : undefined}
95
+ aria-busy={loading ? 'true' : undefined}
96
+ role={href ? 'button' : undefined}
97
+ class="{base} {focusRing} {toneRing[tone]} {variantClass} {isLink ? '' : sizes[size]}
98
+ {block ? 'w-full' : ''} {className}"
99
+ {...rest}
100
+ >
101
+ {#if loading}
102
+ <Spinner size={size === 'sm' ? 'xs' : 'sm'} label="" />
103
+ {/if}
104
+ {@render children()}
105
+ </svelte:element>
@@ -0,0 +1,21 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../../core/tones.js';
4
+ export type ButtonVariant = 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'primary';
5
+ export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
6
+ interface Props extends HTMLButtonAttributes {
7
+ variant?: ButtonVariant;
8
+ tone?: Tone;
9
+ size?: ButtonSize;
10
+ /** Renders an `<a>` instead of a `<button>`. */
11
+ href?: string;
12
+ target?: string;
13
+ rel?: string;
14
+ loading?: boolean;
15
+ /** Stretch to the container's full width. */
16
+ block?: boolean;
17
+ children: Snippet;
18
+ }
19
+ declare const Button: import("svelte").Component<Props, {}, "">;
20
+ type Button = ReturnType<typeof Button>;
21
+ export default Button;