@nqmcreative/ui 0.1.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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +507 -0
  3. package/cli/index.mjs +410 -0
  4. package/dist/actions/anchor.d.ts +29 -0
  5. package/dist/actions/anchor.js +122 -0
  6. package/dist/actions/dismissable.d.ts +44 -0
  7. package/dist/actions/dismissable.js +147 -0
  8. package/dist/components/Accordion.svelte +14 -0
  9. package/dist/components/Accordion.svelte.d.ts +8 -0
  10. package/dist/components/AccordionItem.svelte +46 -0
  11. package/dist/components/AccordionItem.svelte.d.ts +14 -0
  12. package/dist/components/Alert.svelte +88 -0
  13. package/dist/components/Alert.svelte.d.ts +18 -0
  14. package/dist/components/Avatar.svelte +58 -0
  15. package/dist/components/Avatar.svelte.d.ts +16 -0
  16. package/dist/components/AvatarGroup.svelte +17 -0
  17. package/dist/components/AvatarGroup.svelte.d.ts +8 -0
  18. package/dist/components/Badge.svelte +53 -0
  19. package/dist/components/Badge.svelte.d.ts +18 -0
  20. package/dist/components/Breadcrumb.svelte +46 -0
  21. package/dist/components/Breadcrumb.svelte.d.ts +14 -0
  22. package/dist/components/Button.svelte +94 -0
  23. package/dist/components/Button.svelte.d.ts +21 -0
  24. package/dist/components/Calendar.svelte +206 -0
  25. package/dist/components/Calendar.svelte.d.ts +22 -0
  26. package/dist/components/Card.svelte +76 -0
  27. package/dist/components/Card.svelte.d.ts +21 -0
  28. package/dist/components/Checkbox.svelte +69 -0
  29. package/dist/components/Checkbox.svelte.d.ts +15 -0
  30. package/dist/components/CheckboxGroup.svelte +109 -0
  31. package/dist/components/CheckboxGroup.svelte.d.ts +30 -0
  32. package/dist/components/Combobox.svelte +268 -0
  33. package/dist/components/Combobox.svelte.d.ts +29 -0
  34. package/dist/components/CommandPalette.svelte +226 -0
  35. package/dist/components/CommandPalette.svelte.d.ts +32 -0
  36. package/dist/components/ConfirmDialog.svelte +97 -0
  37. package/dist/components/ConfirmDialog.svelte.d.ts +22 -0
  38. package/dist/components/ContextMenu.svelte +90 -0
  39. package/dist/components/ContextMenu.svelte.d.ts +15 -0
  40. package/dist/components/DatePicker.svelte +186 -0
  41. package/dist/components/DatePicker.svelte.d.ts +23 -0
  42. package/dist/components/Divider.svelte +30 -0
  43. package/dist/components/Divider.svelte.d.ts +10 -0
  44. package/dist/components/Drawer.svelte +161 -0
  45. package/dist/components/Drawer.svelte.d.ts +20 -0
  46. package/dist/components/Dropdown.svelte +112 -0
  47. package/dist/components/Dropdown.svelte.d.ts +23 -0
  48. package/dist/components/Dropzone.svelte +226 -0
  49. package/dist/components/Dropzone.svelte.d.ts +32 -0
  50. package/dist/components/EmptyState.svelte +57 -0
  51. package/dist/components/EmptyState.svelte.d.ts +18 -0
  52. package/dist/components/Field.svelte +42 -0
  53. package/dist/components/Field.svelte.d.ts +16 -0
  54. package/dist/components/Footer.svelte +93 -0
  55. package/dist/components/Footer.svelte.d.ts +25 -0
  56. package/dist/components/Input.svelte +60 -0
  57. package/dist/components/Input.svelte.d.ts +18 -0
  58. package/dist/components/InputGroup.svelte +19 -0
  59. package/dist/components/InputGroup.svelte.d.ts +10 -0
  60. package/dist/components/Kbd.svelte +17 -0
  61. package/dist/components/Kbd.svelte.d.ts +8 -0
  62. package/dist/components/Label.svelte +16 -0
  63. package/dist/components/Label.svelte.d.ts +9 -0
  64. package/dist/components/Link.svelte +44 -0
  65. package/dist/components/Link.svelte.d.ts +16 -0
  66. package/dist/components/LocaleProvider.svelte +24 -0
  67. package/dist/components/LocaleProvider.svelte.d.ts +10 -0
  68. package/dist/components/MenuItem.svelte +58 -0
  69. package/dist/components/MenuItem.svelte.d.ts +18 -0
  70. package/dist/components/MenuSeparator.svelte +22 -0
  71. package/dist/components/MenuSeparator.svelte.d.ts +8 -0
  72. package/dist/components/Modal.svelte +114 -0
  73. package/dist/components/Modal.svelte.d.ts +21 -0
  74. package/dist/components/MultiSelect.svelte +292 -0
  75. package/dist/components/MultiSelect.svelte.d.ts +31 -0
  76. package/dist/components/Navbar.svelte +190 -0
  77. package/dist/components/Navbar.svelte.d.ts +33 -0
  78. package/dist/components/NumberInput.svelte +100 -0
  79. package/dist/components/NumberInput.svelte.d.ts +15 -0
  80. package/dist/components/Pagination.svelte +90 -0
  81. package/dist/components/Pagination.svelte.d.ts +15 -0
  82. package/dist/components/PasswordInput.svelte +104 -0
  83. package/dist/components/PasswordInput.svelte.d.ts +12 -0
  84. package/dist/components/Popover.svelte +114 -0
  85. package/dist/components/Popover.svelte.d.ts +19 -0
  86. package/dist/components/Progress.svelte +56 -0
  87. package/dist/components/Progress.svelte.d.ts +17 -0
  88. package/dist/components/Radio.svelte +55 -0
  89. package/dist/components/Radio.svelte.d.ts +15 -0
  90. package/dist/components/RadioGroup.svelte +93 -0
  91. package/dist/components/RadioGroup.svelte.d.ts +27 -0
  92. package/dist/components/SegmentedControl.svelte +88 -0
  93. package/dist/components/SegmentedControl.svelte.d.ts +25 -0
  94. package/dist/components/Select.svelte +61 -0
  95. package/dist/components/Select.svelte.d.ts +20 -0
  96. package/dist/components/Sidebar.svelte +206 -0
  97. package/dist/components/Sidebar.svelte.d.ts +36 -0
  98. package/dist/components/Skeleton.svelte +46 -0
  99. package/dist/components/Skeleton.svelte.d.ts +13 -0
  100. package/dist/components/Slider.svelte +79 -0
  101. package/dist/components/Slider.svelte.d.ts +19 -0
  102. package/dist/components/Spinner.svelte +34 -0
  103. package/dist/components/Spinner.svelte.d.ts +13 -0
  104. package/dist/components/Stat.svelte +49 -0
  105. package/dist/components/Stat.svelte.d.ts +16 -0
  106. package/dist/components/Steps.svelte +136 -0
  107. package/dist/components/Steps.svelte.d.ts +24 -0
  108. package/dist/components/Switch.svelte +64 -0
  109. package/dist/components/Switch.svelte.d.ts +14 -0
  110. package/dist/components/Table.svelte +236 -0
  111. package/dist/components/Table.svelte.d.ts +70 -0
  112. package/dist/components/Tabs.svelte +91 -0
  113. package/dist/components/Tabs.svelte.d.ts +23 -0
  114. package/dist/components/Textarea.svelte +54 -0
  115. package/dist/components/Textarea.svelte.d.ts +12 -0
  116. package/dist/components/ThemeToggle.svelte +133 -0
  117. package/dist/components/ThemeToggle.svelte.d.ts +20 -0
  118. package/dist/components/Toaster.svelte +110 -0
  119. package/dist/components/Toaster.svelte.d.ts +8 -0
  120. package/dist/components/Tooltip.svelte +33 -0
  121. package/dist/components/Tooltip.svelte.d.ts +12 -0
  122. package/dist/date.d.ts +35 -0
  123. package/dist/date.js +108 -0
  124. package/dist/fonts.css +193 -0
  125. package/dist/index.d.ts +105 -0
  126. package/dist/index.js +77 -0
  127. package/dist/locale.svelte.d.ts +93 -0
  128. package/dist/locale.svelte.js +153 -0
  129. package/dist/theme.css +210 -0
  130. package/dist/toast.svelte.d.ts +47 -0
  131. package/dist/toast.svelte.js +60 -0
  132. package/dist/tones.d.ts +48 -0
  133. package/dist/tones.js +209 -0
  134. package/package.json +349 -0
  135. package/registry.json +669 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Behaviour shared by every dismissable overlay: click outside, Escape, focus
3
+ * containment, and returning focus where it came from.
4
+ *
5
+ * They live in one file because overlays always need them together, and the
6
+ * ordering between them matters — Escape must beat click-outside, and focus is
7
+ * only returned once, on teardown.
8
+ */
9
+ export function clickOutside(node, options = {}) {
10
+ let opts = options;
11
+ const handle = (event) => {
12
+ if (opts.enabled === false)
13
+ return;
14
+ const target = event.target;
15
+ if (!target || node.contains(target))
16
+ return;
17
+ if (opts.ignore?.some((el) => el?.contains(target)))
18
+ return;
19
+ opts.onoutside?.();
20
+ };
21
+ // `pointerdown` rather than `click`, so a press that starts outside and ends
22
+ // inside (a drag off a scrollbar, say) still counts as leaving.
23
+ document.addEventListener('pointerdown', handle, true);
24
+ return {
25
+ update(next) {
26
+ opts = next;
27
+ },
28
+ destroy() {
29
+ document.removeEventListener('pointerdown', handle, true);
30
+ }
31
+ };
32
+ }
33
+ const FOCUSABLE = [
34
+ 'a[href]',
35
+ 'button:not([disabled])',
36
+ 'input:not([disabled]):not([type="hidden"])',
37
+ 'select:not([disabled])',
38
+ 'textarea:not([disabled])',
39
+ '[tabindex]:not([tabindex="-1"])'
40
+ ].join(',');
41
+ /** Every focusable descendant, in tab order, skipping hidden ones. */
42
+ export function focusable(container) {
43
+ return [...container.querySelectorAll(FOCUSABLE)].filter((el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement);
44
+ }
45
+ export function focusTrap(node, options = {}) {
46
+ let opts = { autofocus: true, restore: true, enabled: true, ...options };
47
+ const previous = document.activeElement;
48
+ const handle = (event) => {
49
+ if (!opts.enabled || event.key !== 'Tab')
50
+ return;
51
+ const items = focusable(node);
52
+ if (items.length === 0) {
53
+ event.preventDefault();
54
+ return;
55
+ }
56
+ const first = items[0];
57
+ const last = items[items.length - 1];
58
+ const active = document.activeElement;
59
+ if (event.shiftKey && (active === first || !node.contains(active))) {
60
+ event.preventDefault();
61
+ last.focus();
62
+ }
63
+ else if (!event.shiftKey && active === last) {
64
+ event.preventDefault();
65
+ first.focus();
66
+ }
67
+ };
68
+ if (opts.enabled && opts.autofocus) {
69
+ queueMicrotask(() => {
70
+ if (node.contains(document.activeElement))
71
+ return;
72
+ (focusable(node)[0] ?? node).focus();
73
+ });
74
+ }
75
+ node.addEventListener('keydown', handle);
76
+ return {
77
+ update(next) {
78
+ opts = { autofocus: true, restore: true, enabled: true, ...next };
79
+ },
80
+ destroy() {
81
+ node.removeEventListener('keydown', handle);
82
+ if (opts.restore && previous?.isConnected)
83
+ previous.focus();
84
+ }
85
+ };
86
+ }
87
+ /** Moves the node to `document.body` (or any selector/element) on mount. */
88
+ export function portal(node, target = 'body') {
89
+ let mount = null;
90
+ const attach = (to) => {
91
+ mount = typeof to === 'string' ? document.querySelector(to) : to;
92
+ mount?.appendChild(node);
93
+ };
94
+ attach(target);
95
+ return {
96
+ update: attach,
97
+ destroy() {
98
+ node.remove();
99
+ }
100
+ };
101
+ }
102
+ /**
103
+ * Roving focus for menus and listboxes: Arrow keys move, Home/End jump,
104
+ * printable characters type-ahead. Returns whether the key was handled.
105
+ */
106
+ export function navigateList(event, items, options = {}) {
107
+ if (items.length === 0)
108
+ return false;
109
+ const loop = options.loop ?? true;
110
+ const index = items.indexOf(document.activeElement);
111
+ const focusAt = (next) => {
112
+ const clamped = loop
113
+ ? (next + items.length) % items.length
114
+ : Math.min(Math.max(0, next), items.length - 1);
115
+ items[clamped]?.focus();
116
+ };
117
+ switch (event.key) {
118
+ case 'ArrowDown':
119
+ event.preventDefault();
120
+ focusAt(index + 1);
121
+ return true;
122
+ case 'ArrowUp':
123
+ event.preventDefault();
124
+ focusAt(index - 1);
125
+ return true;
126
+ case 'Home':
127
+ event.preventDefault();
128
+ focusAt(0);
129
+ return true;
130
+ case 'End':
131
+ event.preventDefault();
132
+ focusAt(items.length - 1);
133
+ return true;
134
+ }
135
+ if (event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey) {
136
+ const needle = event.key.toLowerCase();
137
+ const from = index + 1;
138
+ const ordered = [...items.slice(from), ...items.slice(0, from)];
139
+ const match = ordered.find((el) => el.textContent?.trim().toLowerCase().startsWith(needle));
140
+ if (match) {
141
+ event.preventDefault();
142
+ match.focus();
143
+ return true;
144
+ }
145
+ }
146
+ return false;
147
+ }
@@ -0,0 +1,14 @@
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 class="flex flex-col border-t border-hairline {className}" {...rest}>
13
+ {@render children()}
14
+ </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 '../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 {className}" {...rest}>
27
+ <summary
28
+ class="flex cursor-pointer list-none items-center gap-4 py-4 font-sans text-[15px] text-text transition-colors duration-150 ease-brand-out select-none hover:text-text-secondary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current [&::-webkit-details-marker]:hidden"
29
+ >
30
+ <span class="flex-1 font-medium">{title}</span>
31
+ {#if meta}<span class="font-mono text-xs text-text-muted">{meta}</span>{/if}
32
+ <svg
33
+ class="size-4 shrink-0 transition-transform duration-200 ease-brand-out group-open:rotate-45 {toneText[
34
+ tone
35
+ ]}"
36
+ viewBox="0 0 16 16"
37
+ fill="none"
38
+ aria-hidden="true"
39
+ >
40
+ <path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" />
41
+ </svg>
42
+ </summary>
43
+ <div class="pb-5 font-sans text-[15px] 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 '../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,88 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { toneBorder, toneBorderSoft, toneSurface, toneText, type Tone } from '../tones.js';
5
+ import { useLocale } from '../locale.svelte.js';
6
+
7
+ export type AlertVariant = 'soft' | 'outline' | 'accent';
8
+
9
+ interface Props extends HTMLAttributes<HTMLDivElement> {
10
+ tone?: Tone;
11
+ variant?: AlertVariant;
12
+ title?: string;
13
+ /** Shows a close button and fires `ondismiss`. */
14
+ dismissible?: boolean;
15
+ ondismiss?: () => void;
16
+ /** Replaces the built-in status glyph. */
17
+ icon?: Snippet;
18
+ children?: Snippet;
19
+ }
20
+
21
+ let {
22
+ tone = 'info',
23
+ variant = 'soft',
24
+ title,
25
+ dismissible = false,
26
+ ondismiss,
27
+ icon,
28
+ class: className = '',
29
+ children,
30
+ ...rest
31
+ }: Props = $props();
32
+
33
+ const t = useLocale();
34
+ let open = $state(true);
35
+
36
+ const variants = $derived({
37
+ soft: `${toneSurface[tone]} border ${toneBorderSoft[tone]}`,
38
+ outline: `bg-bg border ${toneBorderSoft[tone]}`,
39
+ accent: `${toneSurface[tone]} border-l-2 ${toneBorder[tone]}`
40
+ });
41
+
42
+ function dismiss() {
43
+ open = false;
44
+ ondismiss?.();
45
+ }
46
+ </script>
47
+
48
+ {#if open}
49
+ <div
50
+ role={tone === 'danger' ? 'alert' : 'status'}
51
+ class="flex items-start gap-3 p-4 font-sans {variants[variant]} {className}"
52
+ {...rest}
53
+ >
54
+ <span class="mt-0.5 shrink-0 {toneText[tone]}">
55
+ {#if icon}
56
+ {@render icon()}
57
+ {:else}
58
+ <svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
59
+ <circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
60
+ <path d="M8 4.5v4.5" stroke="currentColor" stroke-width="1.5" />
61
+ <circle cx="8" cy="11.5" r="0.9" fill="currentColor" />
62
+ </svg>
63
+ {/if}
64
+ </span>
65
+
66
+ <div class="flex min-w-0 flex-1 flex-col gap-1">
67
+ {#if title}
68
+ <p class="text-[15px] leading-snug font-medium {toneText[tone]}">{title}</p>
69
+ {/if}
70
+ {#if children}
71
+ <div class="text-sm leading-relaxed text-text-secondary">{@render children()}</div>
72
+ {/if}
73
+ </div>
74
+
75
+ {#if dismissible}
76
+ <button
77
+ type="button"
78
+ onclick={dismiss}
79
+ aria-label={t.current.dismiss}
80
+ class="-m-1 shrink-0 p-1 text-text-muted transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
81
+ >
82
+ <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
83
+ <path d="m2 2 10 10M12 2 2 12" stroke="currentColor" stroke-width="1.5" />
84
+ </svg>
85
+ </button>
86
+ {/if}
87
+ </div>
88
+ {/if}
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../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,58 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { toneSoft, type Tone } from '../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
+ /** 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-mono font-medium select-none
50
+ {squared ? '' : 'rounded-full'} {sizes[size]} {src ? 'bg-bg-inset' : toneSoft[tone]} {className}"
51
+ {...rest}
52
+ >
53
+ {#if src}
54
+ <img {src} alt={alt ?? name} class="size-full object-cover" />
55
+ {:else}
56
+ {initials}
57
+ {/if}
58
+ </span>
@@ -0,0 +1,16 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../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
+ /** 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,53 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { toneBorderSoft, toneFill, toneSoft, toneSolid, toneText, type Tone } from '../tones.js';
5
+
6
+ /** @deprecated use `Tone` — kept so existing `tone="brand" | "neutral"` still type-checks. */
7
+ export type BadgeTone = Tone;
8
+ export type BadgeVariant = 'soft' | 'solid' | 'outline';
9
+ export type BadgeSize = 'sm' | 'md';
10
+
11
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
12
+ tone?: Tone;
13
+ variant?: BadgeVariant;
14
+ size?: BadgeSize;
15
+ /** Small filled circle before the label. */
16
+ dot?: boolean;
17
+ children: Snippet;
18
+ }
19
+
20
+ let {
21
+ tone = 'brand',
22
+ variant = 'soft',
23
+ size = 'md',
24
+ dot = false,
25
+ class: className = '',
26
+ children,
27
+ ...rest
28
+ }: Props = $props();
29
+
30
+ const variants: Record<BadgeVariant, string> = $derived({
31
+ soft: toneSoft[tone],
32
+ solid: toneSolid[tone],
33
+ outline: `border ${toneBorderSoft[tone]} ${toneText[tone]} bg-bg`
34
+ });
35
+
36
+ const sizes: Record<BadgeSize, string> = {
37
+ sm: 'px-2.5 py-0.5 text-[10px]',
38
+ md: 'px-3 py-1 text-xs'
39
+ };
40
+ </script>
41
+
42
+ <span
43
+ class="inline-flex items-center gap-1.5 rounded-full font-mono font-medium tracking-wide
44
+ {variants[variant]} {sizes[size]} {className}"
45
+ {...rest}
46
+ >
47
+ {#if dot}
48
+ <span
49
+ class="size-1.5 shrink-0 rounded-full {variant === 'solid' ? 'bg-current' : toneFill[tone]}"
50
+ ></span>
51
+ {/if}
52
+ {@render children()}
53
+ </span>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../tones.js';
4
+ /** @deprecated use `Tone` — kept so existing `tone="brand" | "neutral"` still type-checks. */
5
+ export type BadgeTone = Tone;
6
+ export type BadgeVariant = 'soft' | 'solid' | 'outline';
7
+ export type BadgeSize = 'sm' | 'md';
8
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
9
+ tone?: Tone;
10
+ variant?: BadgeVariant;
11
+ size?: BadgeSize;
12
+ /** Small filled circle before the label. */
13
+ dot?: boolean;
14
+ children: Snippet;
15
+ }
16
+ declare const Badge: import("svelte").Component<Props, {}, "">;
17
+ type Badge = ReturnType<typeof Badge>;
18
+ export default Badge;
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { toneHoverText, type Tone } from '../tones.js';
4
+ import { useLocale } from '../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-2">
24
+ {#each items as item, i (item.label)}
25
+ <li class="flex items-center gap-2">
26
+ {#if i > 0}
27
+ <span class="text-text-muted 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="text-text-secondary transition-colors duration-150 ease-brand-out {toneHoverText[
33
+ tone
34
+ ]}"
35
+ >
36
+ {item.label}
37
+ </a>
38
+ {:else}
39
+ <span class="text-text" aria-current={i === items.length - 1 ? 'page' : undefined}>
40
+ {item.label}
41
+ </span>
42
+ {/if}
43
+ </li>
44
+ {/each}
45
+ </ol>
46
+ </nav>
@@ -0,0 +1,14 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../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,94 @@
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
+ toneBorder,
8
+ toneRing,
9
+ toneSoft,
10
+ toneSoftHover,
11
+ toneSolid,
12
+ toneSolidHover,
13
+ toneText,
14
+ type Tone
15
+ } from '../tones.js';
16
+
17
+ export type ButtonVariant = 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'primary';
18
+ export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
19
+
20
+ interface Props extends HTMLButtonAttributes {
21
+ variant?: ButtonVariant;
22
+ tone?: Tone;
23
+ size?: ButtonSize;
24
+ /** Renders an `<a>` instead of a `<button>`. */
25
+ href?: string;
26
+ target?: string;
27
+ rel?: string;
28
+ loading?: boolean;
29
+ /** Stretch to the container's full width. */
30
+ block?: boolean;
31
+ children: Snippet;
32
+ }
33
+
34
+ let {
35
+ variant = 'solid',
36
+ tone = 'brand',
37
+ size = 'md',
38
+ href,
39
+ loading = false,
40
+ block = false,
41
+ disabled = false,
42
+ type = 'button',
43
+ class: className = '',
44
+ children,
45
+ ...rest
46
+ }: Props = $props();
47
+
48
+ const base =
49
+ 'inline-flex items-center justify-center gap-2 border font-sans font-medium whitespace-nowrap transition-all duration-150 ease-brand-out disabled:opacity-50 disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:pointer-events-none';
50
+
51
+ const sizes: Record<ButtonSize, string> = {
52
+ sm: 'h-9 px-4 text-[13px]',
53
+ md: 'h-11 px-7 text-[15px]',
54
+ lg: 'h-12 px-8 text-base',
55
+ xl: 'h-14 px-10 text-base'
56
+ };
57
+
58
+ const variantClass = $derived.by(() => {
59
+ switch (variant) {
60
+ case 'primary':
61
+ case 'solid':
62
+ return `border-transparent ${toneSolid[tone]} ${toneSolidHover[tone]}`;
63
+ case 'soft':
64
+ return `border-transparent ${toneSoft[tone]} ${toneSoftHover[tone]}`;
65
+ case 'outline':
66
+ return `bg-transparent ${toneText[tone]} ${toneBorder[tone]} ${toneSolidHover[tone]} hover:text-text-inverse`;
67
+ case 'ghost':
68
+ return `border-transparent bg-transparent ${toneText[tone]} ${toneSoftHover[tone]}`;
69
+ case 'link':
70
+ return `border-transparent ${toneText[tone]} h-auto p-0 text-sm hover:gap-3`;
71
+ }
72
+ });
73
+
74
+ const isLink = $derived(variant === 'link');
75
+ const inert = $derived(disabled || loading);
76
+ </script>
77
+
78
+ <svelte:element
79
+ this={href ? 'a' : 'button'}
80
+ {href}
81
+ type={href ? undefined : type}
82
+ disabled={href ? undefined : inert}
83
+ aria-disabled={href && inert ? 'true' : undefined}
84
+ aria-busy={loading ? 'true' : undefined}
85
+ role={href ? 'button' : undefined}
86
+ class="{base} {focusRing} {toneRing[tone]} {variantClass} {isLink ? '' : sizes[size]}
87
+ {block ? 'w-full' : ''} {className}"
88
+ {...rest}
89
+ >
90
+ {#if loading}
91
+ <Spinner size={size === 'sm' ? 'xs' : 'sm'} label="" />
92
+ {/if}
93
+ {@render children()}
94
+ </svelte:element>