@ruckc/truss-ui 0.0.0-stage → 1.0.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 (155) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +91 -2
  3. package/dist/color.d.ts +21 -0
  4. package/dist/color.js +32 -0
  5. package/dist/components/Accordion.svelte +70 -0
  6. package/dist/components/Accordion.svelte.d.ts +15 -0
  7. package/dist/components/Alert.svelte +46 -0
  8. package/dist/components/Alert.svelte.d.ts +15 -0
  9. package/dist/components/Avatar.svelte +32 -0
  10. package/dist/components/Avatar.svelte.d.ts +9 -0
  11. package/dist/components/AvatarGroup.svelte +20 -0
  12. package/dist/components/AvatarGroup.svelte.d.ts +11 -0
  13. package/dist/components/Badge.svelte +27 -0
  14. package/dist/components/Badge.svelte.d.ts +9 -0
  15. package/dist/components/Breadcrumbs.svelte +38 -0
  16. package/dist/components/Breadcrumbs.svelte.d.ts +12 -0
  17. package/dist/components/Button.svelte +119 -0
  18. package/dist/components/Button.svelte.d.ts +16 -0
  19. package/dist/components/ButtonGroup.svelte +23 -0
  20. package/dist/components/ButtonGroup.svelte.d.ts +9 -0
  21. package/dist/components/Calendar.svelte +166 -0
  22. package/dist/components/Calendar.svelte.d.ts +19 -0
  23. package/dist/components/Callout.svelte +50 -0
  24. package/dist/components/Callout.svelte.d.ts +14 -0
  25. package/dist/components/Card.svelte +50 -0
  26. package/dist/components/Card.svelte.d.ts +15 -0
  27. package/dist/components/Checkbox.svelte +37 -0
  28. package/dist/components/Checkbox.svelte.d.ts +16 -0
  29. package/dist/components/ColorPicker.svelte +164 -0
  30. package/dist/components/ColorPicker.svelte.d.ts +14 -0
  31. package/dist/components/Combobox.svelte +131 -0
  32. package/dist/components/Combobox.svelte.d.ts +19 -0
  33. package/dist/components/ContextMenu.svelte +51 -0
  34. package/dist/components/ContextMenu.svelte.d.ts +10 -0
  35. package/dist/components/DataTable.svelte +657 -0
  36. package/dist/components/DataTable.svelte.d.ts +76 -0
  37. package/dist/components/DatePicker.svelte +70 -0
  38. package/dist/components/DatePicker.svelte.d.ts +15 -0
  39. package/dist/components/DateRangePicker.svelte +92 -0
  40. package/dist/components/DateRangePicker.svelte.d.ts +15 -0
  41. package/dist/components/DateTimePicker.svelte +62 -0
  42. package/dist/components/DateTimePicker.svelte.d.ts +19 -0
  43. package/dist/components/DateTimeRangePicker.svelte +167 -0
  44. package/dist/components/DateTimeRangePicker.svelte.d.ts +22 -0
  45. package/dist/components/Dialog.svelte +70 -0
  46. package/dist/components/Dialog.svelte.d.ts +14 -0
  47. package/dist/components/Divider.svelte +16 -0
  48. package/dist/components/Divider.svelte.d.ts +8 -0
  49. package/dist/components/Drawer.svelte +64 -0
  50. package/dist/components/Drawer.svelte.d.ts +13 -0
  51. package/dist/components/DurationInput.svelte +112 -0
  52. package/dist/components/DurationInput.svelte.d.ts +19 -0
  53. package/dist/components/EditableText.svelte +73 -0
  54. package/dist/components/EditableText.svelte.d.ts +14 -0
  55. package/dist/components/EmptyState.svelte +30 -0
  56. package/dist/components/EmptyState.svelte.d.ts +12 -0
  57. package/dist/components/FileUpload.svelte +143 -0
  58. package/dist/components/FileUpload.svelte.d.ts +18 -0
  59. package/dist/components/FormGroup.svelte +27 -0
  60. package/dist/components/FormGroup.svelte.d.ts +13 -0
  61. package/dist/components/Input.svelte +62 -0
  62. package/dist/components/Input.svelte.d.ts +15 -0
  63. package/dist/components/InputAddon.svelte +21 -0
  64. package/dist/components/InputAddon.svelte.d.ts +9 -0
  65. package/dist/components/InputGroup.svelte +70 -0
  66. package/dist/components/InputGroup.svelte.d.ts +16 -0
  67. package/dist/components/Menu.svelte +95 -0
  68. package/dist/components/Menu.svelte.d.ts +15 -0
  69. package/dist/components/MenuButton.svelte +45 -0
  70. package/dist/components/MenuButton.svelte.d.ts +16 -0
  71. package/dist/components/MultiSelect.svelte +129 -0
  72. package/dist/components/MultiSelect.svelte.d.ts +18 -0
  73. package/dist/components/Navbar.svelte +17 -0
  74. package/dist/components/Navbar.svelte.d.ts +10 -0
  75. package/dist/components/NumberInput.svelte +66 -0
  76. package/dist/components/NumberInput.svelte.d.ts +16 -0
  77. package/dist/components/OptionList.svelte +74 -0
  78. package/dist/components/OptionList.svelte.d.ts +15 -0
  79. package/dist/components/Popover.svelte +87 -0
  80. package/dist/components/Popover.svelte.d.ts +14 -0
  81. package/dist/components/ProgressBar.svelte +35 -0
  82. package/dist/components/ProgressBar.svelte.d.ts +10 -0
  83. package/dist/components/RadioGroup.svelte +39 -0
  84. package/dist/components/RadioGroup.svelte.d.ts +14 -0
  85. package/dist/components/Section.svelte +56 -0
  86. package/dist/components/Section.svelte.d.ts +15 -0
  87. package/dist/components/SegmentedControl.svelte +56 -0
  88. package/dist/components/SegmentedControl.svelte.d.ts +17 -0
  89. package/dist/components/Select.svelte +204 -0
  90. package/dist/components/Select.svelte.d.ts +41 -0
  91. package/dist/components/Shell.svelte +143 -0
  92. package/dist/components/Shell.svelte.d.ts +36 -0
  93. package/dist/components/Sidebar.svelte +84 -0
  94. package/dist/components/Sidebar.svelte.d.ts +24 -0
  95. package/dist/components/Skeleton.svelte +25 -0
  96. package/dist/components/Skeleton.svelte.d.ts +9 -0
  97. package/dist/components/Slider.svelte +121 -0
  98. package/dist/components/Slider.svelte.d.ts +16 -0
  99. package/dist/components/Spinner.svelte +30 -0
  100. package/dist/components/Spinner.svelte.d.ts +9 -0
  101. package/dist/components/SplitButton.svelte +113 -0
  102. package/dist/components/SplitButton.svelte.d.ts +14 -0
  103. package/dist/components/SplitPane.svelte +106 -0
  104. package/dist/components/SplitPane.svelte.d.ts +16 -0
  105. package/dist/components/Stepper.svelte +73 -0
  106. package/dist/components/Stepper.svelte.d.ts +26 -0
  107. package/dist/components/Switch.svelte +21 -0
  108. package/dist/components/Switch.svelte.d.ts +10 -0
  109. package/dist/components/Tabs.svelte +74 -0
  110. package/dist/components/Tabs.svelte.d.ts +18 -0
  111. package/dist/components/Tag.svelte +49 -0
  112. package/dist/components/Tag.svelte.d.ts +16 -0
  113. package/dist/components/TagInput.svelte +72 -0
  114. package/dist/components/TagInput.svelte.d.ts +17 -0
  115. package/dist/components/Textarea.svelte +37 -0
  116. package/dist/components/Textarea.svelte.d.ts +11 -0
  117. package/dist/components/TimeList.svelte +76 -0
  118. package/dist/components/TimeList.svelte.d.ts +10 -0
  119. package/dist/components/TimePicker.svelte +79 -0
  120. package/dist/components/TimePicker.svelte.d.ts +15 -0
  121. package/dist/components/TimeRangePicker.svelte +44 -0
  122. package/dist/components/TimeRangePicker.svelte.d.ts +17 -0
  123. package/dist/components/Timeline.svelte +38 -0
  124. package/dist/components/Timeline.svelte.d.ts +17 -0
  125. package/dist/components/Toaster.svelte +35 -0
  126. package/dist/components/Toaster.svelte.d.ts +18 -0
  127. package/dist/components/Tooltip.svelte +66 -0
  128. package/dist/components/Tooltip.svelte.d.ts +11 -0
  129. package/dist/components/Tree.svelte +147 -0
  130. package/dist/components/Tree.svelte.d.ts +16 -0
  131. package/dist/date.d.ts +19 -0
  132. package/dist/date.js +43 -0
  133. package/dist/duration.d.ts +11 -0
  134. package/dist/duration.js +44 -0
  135. package/dist/fonts/LICENSE-Inter.txt +93 -0
  136. package/dist/fonts/LICENSE-JetBrainsMono.txt +93 -0
  137. package/dist/fonts/inter-latin-wght-normal.woff2 +0 -0
  138. package/dist/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  139. package/dist/icons/Icon.svelte +30 -0
  140. package/dist/icons/Icon.svelte.d.ts +9 -0
  141. package/dist/icons/paths.d.ts +39 -0
  142. package/dist/icons/paths.js +43 -0
  143. package/dist/index.d.ts +69 -0
  144. package/dist/index.js +65 -0
  145. package/dist/styles/base.css +71 -0
  146. package/dist/styles/fonts.css +21 -0
  147. package/dist/styles/index.css +3 -0
  148. package/dist/styles/tokens.css +143 -0
  149. package/dist/time.d.ts +7 -0
  150. package/dist/time.js +30 -0
  151. package/dist/toast.svelte.d.ts +22 -0
  152. package/dist/toast.svelte.js +19 -0
  153. package/dist/types.d.ts +70 -0
  154. package/dist/types.js +1 -0
  155. package/package.json +76 -5
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ let { vertical = false, fill = false, children }: { vertical?: boolean; fill?: boolean; children: Snippet } = $props();
4
+ </script>
5
+
6
+ <div class="bg" role="group" data-vertical={vertical} data-fill={fill}>{@render children()}</div>
7
+
8
+ <style>
9
+ .bg { display: inline-flex; }
10
+ .bg[data-fill='true'] { display: flex; }
11
+ .bg[data-fill='true'] > :global(.tr-btn) { flex: 1; }
12
+ .bg > :global(.tr-btn) { border-radius: 0; }
13
+ .bg:not([data-vertical='true']) > :global(.tr-btn:first-child) { border-start-start-radius: var(--tr-radius-sm); border-end-start-radius: var(--tr-radius-sm); }
14
+ .bg:not([data-vertical='true']) > :global(.tr-btn:last-child) { border-start-end-radius: var(--tr-radius-sm); border-end-end-radius: var(--tr-radius-sm); }
15
+ .bg:not([data-vertical='true']) > :global(.tr-btn + .tr-btn) { margin-left: -1px; }
16
+ .bg[data-vertical='true'] { flex-direction: column; }
17
+ .bg[data-vertical='true'] > :global(.tr-btn:first-child) { border-start-start-radius: var(--tr-radius-sm); border-start-end-radius: var(--tr-radius-sm); }
18
+ .bg[data-vertical='true'] > :global(.tr-btn:last-child) { border-end-start-radius: var(--tr-radius-sm); border-end-end-radius: var(--tr-radius-sm); }
19
+ .bg[data-vertical='true'] > :global(.tr-btn + .tr-btn) { margin-top: -1px; }
20
+ /* hairline seam between joined filled/tonal buttons */
21
+ .bg > :global(.tr-btn:hover), .bg > :global(.tr-btn:focus-visible) { z-index: 1; }
22
+ .bg > :global(.tr-btn[data-variant='filled'] + .tr-btn[data-variant='filled']) { box-shadow: inset 1px 0 0 color-mix(in srgb, var(--tr-surface) 35%, transparent), var(--tr-elevation-1); }
23
+ </style>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ vertical?: boolean;
4
+ fill?: boolean;
5
+ children: Snippet;
6
+ };
7
+ declare const ButtonGroup: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type ButtonGroup = ReturnType<typeof ButtonGroup>;
9
+ export default ButtonGroup;
@@ -0,0 +1,166 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import Button from './Button.svelte';
4
+ import { addDays, addMonths, monthGrid, parseISO, startOfMonth, toISO, today } from '../date.js';
5
+ import type { DateRange } from '../types.js';
6
+
7
+ let {
8
+ mode = 'single',
9
+ value = $bindable(),
10
+ range = $bindable({}),
11
+ min,
12
+ max,
13
+ weekStart = 0,
14
+ locale = 'en-US',
15
+ disabledDate,
16
+ onpick,
17
+ onrange
18
+ }: {
19
+ mode?: 'single' | 'range';
20
+ value?: string;
21
+ range?: DateRange;
22
+ min?: string;
23
+ max?: string;
24
+ /** 0 = Sunday … 6 = Saturday */
25
+ weekStart?: number;
26
+ locale?: string;
27
+ disabledDate?: (iso: string) => boolean;
28
+ /** Fires when a selection is complete (single pick, or range end chosen). */
29
+ onpick?: () => void;
30
+ /** Called with every range change (including the in-progress first click), for controlled use. */
31
+ onrange?: (r: DateRange) => void;
32
+ } = $props();
33
+
34
+ const seed = () => parseISO((mode === 'range' ? range.start : value) ?? '') ?? new Date();
35
+ let view = $state(startOfMonth(seed()));
36
+ let focused = $state(toISO(seed()));
37
+ let pending = $state<string>();
38
+ let hover = $state<string>();
39
+ let grid: HTMLDivElement;
40
+
41
+ // Follow external changes (typed input, presets)
42
+ $effect(() => {
43
+ const d = parseISO((mode === 'range' ? range.start : value) ?? '');
44
+ if (d) { view = startOfMonth(d); focused = toISO(d); }
45
+ });
46
+
47
+ const weeks = $derived(monthGrid(view.getFullYear(), view.getMonth(), weekStart));
48
+ const monthLabel = $derived(new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(view));
49
+ const dayLabel = $derived(new Intl.DateTimeFormat(locale, { dateStyle: 'full' }));
50
+ const weekdays = $derived(
51
+ Array.from({ length: 7 }, (_, i) =>
52
+ new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(new Date(2023, 0, 1 + ((weekStart + i) % 7)))
53
+ )
54
+ );
55
+ const now = today();
56
+
57
+ const bounds = $derived.by(() => {
58
+ if (mode !== 'range') return { lo: undefined, hi: undefined };
59
+ if (pending) {
60
+ const o = hover ?? pending;
61
+ return o < pending ? { lo: o, hi: pending } : { lo: pending, hi: o };
62
+ }
63
+ return { lo: range.start, hi: range.end };
64
+ });
65
+ const isOff = (iso: string) => (!!min && iso < min) || (!!max && iso > max) || !!disabledDate?.(iso);
66
+
67
+ function pick(iso: string) {
68
+ if (isOff(iso)) return;
69
+ focused = iso;
70
+ if (mode === 'single') { value = iso; onpick?.(); return; }
71
+ if (!pending) { pending = iso; range = { start: iso }; onrange?.(range); return; }
72
+ const [s, e] = iso < pending ? [iso, pending] : [pending, iso];
73
+ range = { start: s, end: e };
74
+ onrange?.(range);
75
+ pending = hover = undefined;
76
+ onpick?.();
77
+ }
78
+
79
+ function jump(next: string, focus = false) {
80
+ focused = next;
81
+ view = startOfMonth(parseISO(next)!);
82
+ if (focus) tick().then(() => grid.querySelector<HTMLElement>(`[data-iso="${next}"]`)?.focus());
83
+ }
84
+ const shift = (months: number) => jump(toISO(addMonths(parseISO(focused)!, months)));
85
+
86
+ function onKey(e: KeyboardEvent) {
87
+ const d = parseISO(focused)!;
88
+ const weekOffset = (d.getDay() - weekStart + 7) % 7;
89
+ const moves: Record<string, Date> = {
90
+ ArrowLeft: addDays(d, -1), ArrowRight: addDays(d, 1), ArrowUp: addDays(d, -7), ArrowDown: addDays(d, 7),
91
+ Home: addDays(d, -weekOffset), End: addDays(d, 6 - weekOffset),
92
+ PageUp: addMonths(d, e.shiftKey ? -12 : -1), PageDown: addMonths(d, e.shiftKey ? 12 : 1)
93
+ };
94
+ if (!(e.key in moves)) return;
95
+ e.preventDefault();
96
+ jump(toISO(moves[e.key]), true);
97
+ }
98
+ </script>
99
+
100
+ <div class="cal">
101
+ <div class="head">
102
+ <Button variant="minimal" intent="neutral" size="sm" icon="chevrons-left" aria-label="Previous year" onclick={() => shift(-12)} />
103
+ <Button variant="minimal" intent="neutral" size="sm" icon="chevron-left" aria-label="Previous month" onclick={() => shift(-1)} />
104
+ <div class="title" aria-live="polite">{monthLabel}</div>
105
+ <Button variant="minimal" intent="neutral" size="sm" icon="chevron-right" aria-label="Next month" onclick={() => shift(1)} />
106
+ <Button variant="minimal" intent="neutral" size="sm" icon="chevrons-right" aria-label="Next year" onclick={() => shift(12)} />
107
+ </div>
108
+
109
+ <div class="grid" role="grid" aria-label={monthLabel} bind:this={grid} onkeydown={onKey} tabindex="-1">
110
+ <div class="row" role="row">
111
+ {#each weekdays as w}<div class="dow" role="columnheader">{w}</div>{/each}
112
+ </div>
113
+ {#each weeks as week}
114
+ <div class="row" role="row">
115
+ {#each week as iso (iso)}
116
+ {@const inRange = !!bounds.lo && !!bounds.hi && iso >= bounds.lo && iso <= bounds.hi}
117
+ {@const edge = iso === bounds.lo || iso === bounds.hi}
118
+ {@const sel = mode === 'single' ? iso === value : edge}
119
+ <div role="gridcell" aria-selected={sel}>
120
+ <button
121
+ type="button"
122
+ class="day"
123
+ data-iso={iso}
124
+ data-outside={iso.slice(0, 7) !== toISO(view).slice(0, 7)}
125
+ data-today={iso === now}
126
+ data-selected={sel}
127
+ data-range={inRange && !edge}
128
+ tabindex={iso === focused ? 0 : -1}
129
+ disabled={isOff(iso)}
130
+ aria-label={dayLabel.format(parseISO(iso)!)}
131
+ aria-current={iso === now ? 'date' : undefined}
132
+ onclick={() => pick(iso)}
133
+ onpointerenter={() => pending && (hover = iso)}
134
+ >{+iso.slice(8)}</button>
135
+ </div>
136
+ {/each}
137
+ </div>
138
+ {/each}
139
+ </div>
140
+
141
+ <div class="foot">
142
+ <Button variant="minimal" size="sm" onclick={() => jump(now, true)}>Today</Button>
143
+ </div>
144
+ </div>
145
+
146
+ <style>
147
+ .cal { width: 252px; padding: 8px; }
148
+ .head { display: flex; align-items: center; gap: 2px; margin-bottom: 6px; }
149
+ .title { flex: 1; text-align: center; font-weight: 600; }
150
+ .grid { outline: none; }
151
+ .row { display: grid; grid-template-columns: repeat(7, 1fr); }
152
+ .dow { height: 24px; display: grid; place-items: center; font-size: var(--tr-text-xs); font-weight: 700; text-transform: uppercase; color: var(--tr-on-surface-variant); }
153
+ [role='gridcell'] { display: contents; }
154
+ .day {
155
+ height: 32px; border: 0; background: none; color: var(--tr-on-surface); font: inherit; font-variant-numeric: tabular-nums;
156
+ border-radius: var(--tr-radius-sm); cursor: pointer; position: relative;
157
+ }
158
+ .day:hover:not(:disabled) { background: var(--tr-surface-container-highest); }
159
+ .day[data-outside='true'] { color: var(--tr-placeholder); }
160
+ .day[data-today='true'] { box-shadow: inset 0 0 0 1px var(--tr-outline); }
161
+ .day[data-range='true'] { background: var(--tr-primary-container); color: var(--tr-on-primary-container); border-radius: 0; }
162
+ .day[data-selected='true'] { background: var(--tr-primary); color: var(--tr-on-primary); font-weight: 700; }
163
+ .day:focus-visible { outline: none; box-shadow: var(--tr-focus-ring); z-index: 1; }
164
+ .day:disabled { opacity: 0.35; cursor: not-allowed; }
165
+ .foot { display: flex; justify-content: flex-end; margin-top: 4px; border-top: 1px solid var(--tr-outline-variant); padding-top: 6px; }
166
+ </style>
@@ -0,0 +1,19 @@
1
+ import type { DateRange } from '../types.js';
2
+ type $$ComponentProps = {
3
+ mode?: 'single' | 'range';
4
+ value?: string;
5
+ range?: DateRange;
6
+ min?: string;
7
+ max?: string;
8
+ /** 0 = Sunday … 6 = Saturday */
9
+ weekStart?: number;
10
+ locale?: string;
11
+ disabledDate?: (iso: string) => boolean;
12
+ /** Fires when a selection is complete (single pick, or range end chosen). */
13
+ onpick?: () => void;
14
+ /** Called with every range change (including the in-progress first click), for controlled use. */
15
+ onrange?: (r: DateRange) => void;
16
+ };
17
+ declare const Calendar: import("svelte").Component<$$ComponentProps, {}, "value" | "range">;
18
+ type Calendar = ReturnType<typeof Calendar>;
19
+ export default Calendar;
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Icon from '../icons/Icon.svelte';
4
+ import Button from './Button.svelte';
5
+ import type { IconName } from '../icons/paths.js';
6
+
7
+ let {
8
+ intent = 'primary',
9
+ title,
10
+ icon,
11
+ onclose,
12
+ children,
13
+ actions
14
+ }: {
15
+ intent?: 'primary' | 'success' | 'warning' | 'danger' | 'neutral';
16
+ title?: string;
17
+ icon?: IconName;
18
+ /** Shows a dismiss button. */
19
+ onclose?: () => void;
20
+ children?: Snippet;
21
+ actions?: Snippet;
22
+ } = $props();
23
+ const glyph = { primary: 'info', success: 'success', warning: 'warning', danger: 'error', neutral: 'info' } as const;
24
+ </script>
25
+
26
+ <div class="co" data-intent={intent} role={intent === 'danger' || intent === 'warning' ? 'alert' : 'note'}>
27
+ <span class="ic"><Icon name={icon ?? glyph[intent]} size={20} /></span>
28
+ <div class="body">
29
+ {#if title}<div class="title">{title}</div>{/if}
30
+ {#if children}<div class="text">{@render children()}</div>{/if}
31
+ {#if actions}<div class="actions">{@render actions()}</div>{/if}
32
+ </div>
33
+ {#if onclose}<Button variant="minimal" intent="neutral" size="sm" icon="cross" aria-label="Dismiss" onclick={onclose} />{/if}
34
+ </div>
35
+
36
+ <style>
37
+ .co { --c: var(--tr-primary); --cc: var(--tr-primary-container); --on: var(--tr-on-primary-container); display: flex; gap: 10px; padding: 10px 10px 10px 14px; position: relative; background: var(--cc); color: var(--on); border-radius: var(--tr-radius-md); overflow: hidden; }
38
+ .co::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--c); }
39
+ .co[data-intent='success'] { --c: var(--tr-success); --cc: var(--tr-success-container); --on: var(--tr-on-success-container); }
40
+ .co[data-intent='warning'] { --c: var(--tr-warning); --cc: var(--tr-warning-container); --on: var(--tr-on-warning-container); }
41
+ .co[data-intent='danger'] { --c: var(--tr-danger); --cc: var(--tr-danger-container); --on: var(--tr-on-danger-container); }
42
+ .co[data-intent='neutral'] { --c: var(--tr-outline); --cc: var(--tr-surface-container-high); --on: var(--tr-on-surface); }
43
+ .ic { flex: none; margin-top: 1px; color: var(--c); }
44
+ .co[data-intent='warning'] .ic, .co[data-intent='success'] .ic, .co[data-intent='danger'] .ic { color: var(--on); }
45
+ .body { flex: 1; min-width: 0; }
46
+ .title { font-weight: 700; }
47
+ .text { line-height: 1.45; }
48
+ .title + .text { margin-top: 2px; }
49
+ .actions { display: flex; gap: 8px; margin-top: 8px; }
50
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { IconName } from '../icons/paths.js';
3
+ type $$ComponentProps = {
4
+ intent?: 'primary' | 'success' | 'warning' | 'danger' | 'neutral';
5
+ title?: string;
6
+ icon?: IconName;
7
+ /** Shows a dismiss button. */
8
+ onclose?: () => void;
9
+ children?: Snippet;
10
+ actions?: Snippet;
11
+ };
12
+ declare const Callout: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type Callout = ReturnType<typeof Callout>;
14
+ export default Callout;
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ let {
5
+ padding = 'md',
6
+ elevation = 1,
7
+ href,
8
+ onclick,
9
+ header,
10
+ footer,
11
+ children
12
+ }: {
13
+ padding?: 'none' | 'sm' | 'md';
14
+ elevation?: 0 | 1 | 2 | 3;
15
+ /** Makes the card a link. */
16
+ href?: string;
17
+ /** Makes the card a button. */
18
+ onclick?: (e: MouseEvent) => void;
19
+ header?: Snippet;
20
+ footer?: Snippet;
21
+ children?: Snippet;
22
+ } = $props();
23
+ const interactive = $derived(!!href || !!onclick);
24
+ </script>
25
+
26
+ {#snippet inner()}
27
+ {#if header}<div class="hd">{@render header()}</div>{/if}
28
+ {#if children}<div class="bd" data-padding={padding}>{@render children()}</div>{/if}
29
+ {#if footer}<div class="ft">{@render footer()}</div>{/if}
30
+ {/snippet}
31
+
32
+ {#if href}<a class="card" {href} data-interactive="true" data-elev={elevation}>{@render inner()}</a>
33
+ {:else if onclick}<button type="button" class="card" {onclick} data-interactive="true" data-elev={elevation}>{@render inner()}</button>
34
+ {:else}<div class="card" data-elev={elevation}>{@render inner()}</div>{/if}
35
+
36
+ <style>
37
+ .card { display: block; width: 100%; text-align: left; color: inherit; text-decoration: none; font: inherit; background: var(--tr-surface-container-low); border: 1px solid var(--tr-outline-variant); border-radius: var(--tr-radius-md); overflow: hidden; transition: box-shadow var(--tr-dur) var(--tr-ease), border-color var(--tr-dur) var(--tr-ease), transform var(--tr-dur-fast) var(--tr-ease); }
38
+ .card[data-elev='1'] { box-shadow: var(--tr-elevation-1); }
39
+ .card[data-elev='2'] { box-shadow: var(--tr-elevation-2); }
40
+ .card[data-elev='3'] { box-shadow: var(--tr-elevation-3); }
41
+ .card[data-interactive='true'] { cursor: pointer; }
42
+ .card[data-interactive='true']:hover { border-color: var(--tr-outline); box-shadow: var(--tr-elevation-2); }
43
+ .card[data-interactive='true']:active { transform: scale(0.995); }
44
+ .card[data-interactive='true']:focus-visible { outline: none; box-shadow: var(--tr-focus-ring); border-color: var(--tr-primary); }
45
+ .hd, .ft { padding: 8px 12px; background: var(--tr-surface-container); }
46
+ .hd { border-bottom: 1px solid var(--tr-outline-variant); font-weight: 700; }
47
+ .ft { border-top: 1px solid var(--tr-outline-variant); }
48
+ .bd[data-padding='md'] { padding: 16px; }
49
+ .bd[data-padding='sm'] { padding: 8px 12px; }
50
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ padding?: 'none' | 'sm' | 'md';
4
+ elevation?: 0 | 1 | 2 | 3;
5
+ /** Makes the card a link. */
6
+ href?: string;
7
+ /** Makes the card a button. */
8
+ onclick?: (e: MouseEvent) => void;
9
+ header?: Snippet;
10
+ footer?: Snippet;
11
+ children?: Snippet;
12
+ };
13
+ declare const Card: import("svelte").Component<$$ComponentProps, {}, "">;
14
+ type Card = ReturnType<typeof Card>;
15
+ export default Card;
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Icon from '../icons/Icon.svelte';
4
+
5
+ let {
6
+ checked = $bindable(false),
7
+ indeterminate = false,
8
+ disabled = false,
9
+ hint,
10
+ onchange,
11
+ children,
12
+ ...rest
13
+ }: { checked?: boolean; indeterminate?: boolean; disabled?: boolean; hint?: string; onchange?: (e: Event & { currentTarget: HTMLInputElement }) => void; children?: Snippet; 'aria-label'?: string; tabindex?: number } = $props();
14
+
15
+ let el: HTMLInputElement;
16
+ $effect(() => { if (el) el.indeterminate = indeterminate; });
17
+ </script>
18
+
19
+ <label class="cb" data-disabled={disabled}>
20
+ <input bind:this={el} type="checkbox" class="tr-visually-hidden" bind:checked {disabled} {onchange} {...rest} />
21
+ <span class="box" data-on={checked || indeterminate}>
22
+ {#if indeterminate}<Icon name="minus" />{:else if checked}<Icon name="tick" />{/if}
23
+ </span>
24
+ {#if children}<span class="text">{@render children()}{#if hint}<small>{hint}</small>{/if}</span>{/if}
25
+ </label>
26
+
27
+ <style>
28
+ .cb { display: inline-flex; align-items: flex-start; gap: 8px; min-height: 24px; cursor: pointer; }
29
+ .cb[data-disabled='true'] { opacity: 0.5; cursor: not-allowed; }
30
+ .box { flex: none; display: grid; place-items: center; width: 18px; height: 18px; margin-top: 3px; border: 1px solid var(--tr-outline); border-radius: var(--tr-radius-sm); background: var(--tr-surface-container-low); color: var(--tr-on-primary); transition: background-color var(--tr-dur) var(--tr-ease), box-shadow var(--tr-dur) var(--tr-ease); }
31
+ .box :global(svg) { width: 14px; height: 14px; }
32
+ .cb:hover .box { background: var(--tr-surface-container-high); }
33
+ .box[data-on='true'] { background: var(--tr-primary); border-color: var(--tr-primary); }
34
+ input:focus-visible + .box { box-shadow: var(--tr-focus-ring); }
35
+ .text { display: flex; flex-direction: column; padding-top: 3px; }
36
+ small { color: var(--tr-on-surface-variant); font-size: var(--tr-text-sm); }
37
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ checked?: boolean;
4
+ indeterminate?: boolean;
5
+ disabled?: boolean;
6
+ hint?: string;
7
+ onchange?: (e: Event & {
8
+ currentTarget: HTMLInputElement;
9
+ }) => void;
10
+ children?: Snippet;
11
+ 'aria-label'?: string;
12
+ tabindex?: number;
13
+ };
14
+ declare const Checkbox: import("svelte").Component<$$ComponentProps, {}, "checked">;
15
+ type Checkbox = ReturnType<typeof Checkbox>;
16
+ export default Checkbox;
@@ -0,0 +1,164 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import Button from './Button.svelte';
4
+ import Popover from './Popover.svelte';
5
+ import { clamp, hsvToRgb, parseHex, rgbToHsv, toHex, type HSV } from '../color.js';
6
+
7
+ let {
8
+ value = $bindable('#1f5fbf'),
9
+ label,
10
+ ariaLabel,
11
+ alpha = false,
12
+ swatches = ['#1f5fbf', '#2a78d6', '#1baf7a', '#008300', '#eda100', '#eb6834', '#e34948', '#e87ba4', '#6250d6', '#1a1c22', '#7a7f8e', '#ffffff'],
13
+ disabled = false,
14
+ error
15
+ }: {
16
+ /** #rrggbb, or #rrggbbaa when `alpha` is on and opacity < 1. */
17
+ value?: string;
18
+ label?: string;
19
+ /** Accessible name when no visible `label` is shown. */
20
+ ariaLabel?: string;
21
+ alpha?: boolean;
22
+ swatches?: string[];
23
+ disabled?: boolean;
24
+ error?: string;
25
+ } = $props();
26
+
27
+ const id = $props.id();
28
+ let open = $state(false);
29
+ let hsv = $state<HSV>({ h: 210, s: 0.7, v: 0.75 });
30
+ let a = $state(1);
31
+ let text = $state('');
32
+ let bad = $state(false);
33
+ let input: HTMLInputElement;
34
+ let sv = $state<HTMLDivElement>();
35
+ let hueEl = $state<HTMLDivElement>();
36
+ let alphaEl = $state<HTMLDivElement>();
37
+
38
+ const serialize = (h: HSV = hsv, al = a) => toHex(hsvToRgb(h), alpha ? al : undefined);
39
+ const rgb = $derived(hsvToRgb(hsv));
40
+ const solid = $derived(`rgb(${rgb.r} ${rgb.g} ${rgb.b})`);
41
+
42
+ // Follow external value changes without clobbering hue when s/v collapse (grey, black)
43
+ $effect(() => {
44
+ const v = value;
45
+ untrack(() => {
46
+ if (v && v.toLowerCase() === serialize().toLowerCase()) { text = v; return; }
47
+ const p = v ? parseHex(v) : null;
48
+ if (p) { const n = rgbToHsv(p); hsv = { h: n.s === 0 || n.v === 0 ? hsv.h : n.h, s: n.s, v: n.v }; a = p.a; text = v; bad = false; }
49
+ });
50
+ });
51
+ function commit(h = hsv, al = a) { hsv = h; a = al; value = serialize(h, al); text = value; bad = false; }
52
+
53
+ function drag(el: HTMLElement | undefined, e: PointerEvent, apply: (fx: number, fy: number) => void) {
54
+ if (!el || disabled) return;
55
+ el.setPointerCapture(e.pointerId);
56
+ const go = (ev: PointerEvent) => {
57
+ const r = el.getBoundingClientRect();
58
+ apply(clamp((ev.clientX - r.left) / r.width), clamp((ev.clientY - r.top) / r.height));
59
+ };
60
+ go(e);
61
+ const up = () => { el.removeEventListener('pointermove', go); el.removeEventListener('pointerup', up); };
62
+ el.addEventListener('pointermove', go);
63
+ el.addEventListener('pointerup', up);
64
+ }
65
+ const step = (e: KeyboardEvent, small: number) => (e.shiftKey ? small * 10 : small);
66
+
67
+ function svKey(e: KeyboardEvent) {
68
+ const d = step(e, 0.01);
69
+ const m: Record<string, [number, number]> = { ArrowLeft: [-d, 0], ArrowRight: [d, 0], ArrowUp: [0, d], ArrowDown: [0, -d] };
70
+ if (!(e.key in m)) return;
71
+ e.preventDefault();
72
+ commit({ ...hsv, s: clamp(hsv.s + m[e.key][0]), v: clamp(hsv.v + m[e.key][1]) });
73
+ }
74
+ function railKey(e: KeyboardEvent, cur: number, max: number, small: number, set: (n: number) => void) {
75
+ const d = e.key === 'ArrowRight' || e.key === 'ArrowUp' ? step(e, small) : e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? -step(e, small) : e.key === 'Home' ? -max : e.key === 'End' ? max : 0;
76
+ if (!d) return;
77
+ e.preventDefault();
78
+ set(clamp(cur + d, 0, max));
79
+ }
80
+ function commitText() {
81
+ const p = parseHex(text);
82
+ if (!p) { bad = !!text.trim(); return; }
83
+ const n = rgbToHsv(p);
84
+ commit({ h: n.s === 0 || n.v === 0 ? hsv.h : n.h, s: n.s, v: n.v }, alpha ? p.a : 1);
85
+ }
86
+ const hasDropper = typeof window !== 'undefined' && 'EyeDropper' in window;
87
+ async function pipette() {
88
+ try {
89
+ const r = await new (window as any).EyeDropper().open();
90
+ const p = parseHex(r.sRGBHex);
91
+ if (p) { const n = rgbToHsv(p); commit({ h: n.s === 0 || n.v === 0 ? hsv.h : n.h, s: n.s, v: n.v }); }
92
+ } catch { /* cancelled */ }
93
+ }
94
+ </script>
95
+
96
+ <div class="tr-field">
97
+ {#if label}<label class="tr-label" for={id}>{label}</label>{/if}
98
+ <div class="tr-anchor">
99
+ <div class="tr-control" data-invalid={!!error || bad} data-disabled={disabled}>
100
+ <button type="button" class="sw" {disabled} aria-label="Open color picker" aria-haspopup="dialog" aria-expanded={open} onclick={() => (open = !open)}>
101
+ <span class="chk"></span><span class="col" style:background={value}></span>
102
+ </button>
103
+ <input class="tr-bare mono" {id} aria-label={label ? undefined : ariaLabel} bind:this={input} bind:value={text} {disabled} spellcheck="false" autocomplete="off" maxlength="9"
104
+ aria-invalid={bad || undefined} onblur={commitText} onkeydown={(e) => e.key === 'Enter' && commitText()} />
105
+ </div>
106
+ <Popover bind:open onclose={(r) => r && input.focus()}>
107
+ <div class="panel">
108
+ <div
109
+ class="sv" bind:this={sv} role="slider" tabindex="0" aria-label="Saturation and brightness"
110
+ aria-valuetext="saturation {Math.round(hsv.s * 100)}%, brightness {Math.round(hsv.v * 100)}%" aria-valuenow={Math.round(hsv.s * 100)}
111
+ style:background-color="hsl({hsv.h} 100% 50%)" onpointerdown={(e) => drag(sv, e, (x, y) => commit({ ...hsv, s: x, v: 1 - y }))} onkeydown={svKey}
112
+ >
113
+ <span class="thumb" style:left="{hsv.s * 100}%" style:top="{(1 - hsv.v) * 100}%" style:background={solid}></span>
114
+ </div>
115
+ <div
116
+ class="rail hue" bind:this={hueEl} role="slider" tabindex="0" aria-label="Hue" aria-valuemin="0" aria-valuemax="360" aria-valuenow={Math.round(hsv.h)}
117
+ onpointerdown={(e) => drag(hueEl, e, (x) => commit({ ...hsv, h: x * 360 }))} onkeydown={(e) => railKey(e, hsv.h, 360, 1, (h) => commit({ ...hsv, h }))}
118
+ ><span class="thumb" style:left="{(hsv.h / 360) * 100}%" style:background="hsl({hsv.h} 100% 50%)"></span></div>
119
+ {#if alpha}
120
+ <div
121
+ class="rail alpha" bind:this={alphaEl} role="slider" tabindex="0" aria-label="Opacity" aria-valuemin="0" aria-valuemax="100" aria-valuenow={Math.round(a * 100)}
122
+ style:--c={solid} onpointerdown={(e) => drag(alphaEl, e, (x) => commit(hsv, Math.round(x * 100) / 100))} onkeydown={(e) => railKey(e, a, 1, 0.01, (n) => commit(hsv, n))}
123
+ ><span class="thumb" style:left="{a * 100}%" style:background={solid}></span></div>
124
+ {/if}
125
+ <div class="row">
126
+ <div class="prev"><span class="chk"></span><span class="col" style:background={value}></span></div>
127
+ <div class="rgb">{rgb.r}, {rgb.g}, {rgb.b}{alpha ? `, ${a.toFixed(2)}` : ''}</div>
128
+ {#if hasDropper}<Button size="sm" variant="minimal" intent="neutral" icon="dropper" aria-label="Pick color from screen" onclick={pipette} />{/if}
129
+ </div>
130
+ {#if swatches.length}
131
+ <div class="swatches" role="group" aria-label="Swatches">
132
+ {#each swatches as s}
133
+ <button type="button" class="s" style:background={s} aria-label={s} aria-pressed={value?.toLowerCase().slice(0, 7) === s.toLowerCase()} onclick={() => { const p = parseHex(s)!; const n = rgbToHsv(p); commit({ h: n.s === 0 || n.v === 0 ? hsv.h : n.h, s: n.s, v: n.v }); }}></button>
134
+ {/each}
135
+ </div>
136
+ {/if}
137
+ </div>
138
+ </Popover>
139
+ </div>
140
+ {#if error || bad}<div class="tr-hint" data-error="true">{error ?? 'Enter a hex color like #1f5fbf'}</div>{/if}
141
+ </div>
142
+
143
+ <style>
144
+ .chk { position: absolute; inset: 0; background: repeating-conic-gradient(var(--tr-outline-variant) 0 25%, transparent 0 50%) 0 0 / 8px 8px; }
145
+ .col { position: absolute; inset: 0; }
146
+ .sw { position: relative; flex: none; width: 20px; height: 20px; padding: 0; border: 1px solid var(--tr-outline); border-radius: var(--tr-radius-sm); overflow: hidden; cursor: pointer; }
147
+ .sw:focus-visible { outline: none; box-shadow: var(--tr-focus-ring); }
148
+ .mono { font-family: var(--tr-font-mono); text-transform: lowercase; }
149
+ .panel { width: 248px; padding: 10px; display: grid; gap: 10px; }
150
+ .sv { position: relative; height: 150px; border-radius: var(--tr-radius-sm); cursor: crosshair; touch-action: none; outline: none; background-image: linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent); }
151
+ .rail { position: relative; height: 12px; border-radius: 6px; cursor: pointer; touch-action: none; outline: none; }
152
+ .hue { background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); }
153
+ .alpha { background: linear-gradient(to right, transparent, var(--c)), repeating-conic-gradient(var(--tr-outline-variant) 0 25%, transparent 0 50%) 0 0 / 8px 8px; }
154
+ .thumb { position: absolute; width: 16px; height: 16px; margin: -8px 0 0 -8px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px rgb(0 0 0 / 0.45), var(--tr-elevation-1); pointer-events: none; }
155
+ .rail .thumb { top: 50%; }
156
+ .sv:focus-visible, .rail:focus-visible { box-shadow: var(--tr-focus-ring); }
157
+ .row { display: flex; align-items: center; gap: 8px; }
158
+ .prev { position: relative; width: 28px; height: 28px; border: 1px solid var(--tr-outline); border-radius: var(--tr-radius-sm); overflow: hidden; }
159
+ .rgb { flex: 1; font: var(--tr-text-sm) var(--tr-font-mono); color: var(--tr-on-surface-variant); }
160
+ .swatches { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; }
161
+ .s { aspect-ratio: 1; border: 1px solid var(--tr-outline-variant); border-radius: var(--tr-radius-sm); cursor: pointer; }
162
+ .s[aria-pressed='true'] { box-shadow: 0 0 0 2px var(--tr-surface), 0 0 0 4px var(--tr-primary); }
163
+ .s:focus-visible { outline: none; box-shadow: var(--tr-focus-ring); }
164
+ </style>
@@ -0,0 +1,14 @@
1
+ type $$ComponentProps = {
2
+ /** #rrggbb, or #rrggbbaa when `alpha` is on and opacity < 1. */
3
+ value?: string;
4
+ label?: string;
5
+ /** Accessible name when no visible `label` is shown. */
6
+ ariaLabel?: string;
7
+ alpha?: boolean;
8
+ swatches?: string[];
9
+ disabled?: boolean;
10
+ error?: string;
11
+ };
12
+ declare const ColorPicker: import("svelte").Component<$$ComponentProps, {}, "value">;
13
+ type ColorPicker = ReturnType<typeof ColorPicker>;
14
+ export default ColorPicker;