@heyramzi/ui 1.53.0 → 1.54.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 (102) hide show
  1. package/COMPONENTS.md +6 -0
  2. package/package.json +2 -2
  3. package/scripts/lib/sync-core.mjs +1 -1
  4. package/scripts/sync-shadcn.mjs +11 -11
  5. package/scripts/sync-transitions.mjs +7 -7
  6. package/src/agents/agent-avatar.svelte +14 -15
  7. package/src/agents/agent-cast.svelte +2 -2
  8. package/src/agents/demo.svelte +1 -1
  9. package/src/agents/helpers.ts +10 -1
  10. package/src/agents/index.ts +2 -2
  11. package/src/agents/page-state.ts +1 -1
  12. package/src/agents/types.ts +3 -3
  13. package/src/components/copy/copy-button.test.ts +3 -1
  14. package/src/components/data-table/infinite-scroll.svelte +1 -1
  15. package/src/components/data-table/pagination.svelte +1 -1
  16. package/src/components/data-table/row-arrow.svelte +1 -1
  17. package/src/components/data-table/row-context-menu.svelte +1 -1
  18. package/src/components/data-table/sortable-head.svelte +2 -2
  19. package/src/components/data-table/table-skeleton.svelte +1 -1
  20. package/src/components/docs/doc-page.svelte +161 -0
  21. package/src/components/docs/index.ts +12 -0
  22. package/src/components/docs/on-this-page.svelte +217 -0
  23. package/src/components/docs/scroll-spy.svelte.ts +97 -0
  24. package/src/components/field-row/field-row.svelte +57 -0
  25. package/src/components/field-row/index.ts +1 -0
  26. package/src/components/hold-to-confirm/hold-to-confirm.svelte +2 -2
  27. package/src/components/icon/icon.svelte +1 -1
  28. package/src/components/list-page/detail-empty.svelte +1 -1
  29. package/src/components/list-page/detail-header.svelte +1 -1
  30. package/src/components/list-page/detail-panel.svelte +1 -1
  31. package/src/components/list-page/detail-row.svelte +1 -1
  32. package/src/components/list-page/detail-stack.svelte +1 -1
  33. package/src/components/list-page/filter-item.svelte +1 -1
  34. package/src/components/list-page/filter-popover.svelte +1 -1
  35. package/src/components/list-page/grouped-list.svelte +1 -1
  36. package/src/components/list-page/list-page-shell.svelte +1 -1
  37. package/src/components/list-page/master-detail.svelte +1 -1
  38. package/src/components/list-page/prev-next.svelte +1 -1
  39. package/src/components/list-page/rail-body.svelte +1 -1
  40. package/src/components/list-page/rail-head.svelte +1 -1
  41. package/src/components/list-page/rail-row.svelte +1 -1
  42. package/src/components/list-page/section-label.svelte +1 -1
  43. package/src/components/list-page/toolbar-search.svelte +2 -2
  44. package/src/components/list-page/toolbar-trigger.ts +1 -1
  45. package/src/components/locale-switch/locale-switch.svelte +1 -1
  46. package/src/components/mark/mark.svelte +1 -1
  47. package/src/components/picker/mark-picker.svelte +1 -1
  48. package/src/components/picker/picker.svelte +1 -1
  49. package/src/components/picker/trigger.ts +2 -2
  50. package/src/components/section-card/index.ts +1 -0
  51. package/src/components/section-card/section-card.svelte +43 -0
  52. package/src/components/section-header/index.ts +1 -0
  53. package/src/components/section-header/section-header.svelte +54 -0
  54. package/src/components/section-header/section-header.test.ts +25 -0
  55. package/src/components/segmented/animated-tabs.svelte +2 -2
  56. package/src/components/segmented/collapse.test.ts +10 -0
  57. package/src/components/segmented/collapse.ts +5 -0
  58. package/src/components/segmented/segment.svelte +2 -2
  59. package/src/components/segmented/segmented-filter.svelte +6 -5
  60. package/src/components/segmented/segmented-nav.svelte +1 -1
  61. package/src/components/segmented/segmented-overflow.svelte +1 -1
  62. package/src/components/segmented/segmented-track.svelte +29 -4
  63. package/src/components/segmented/segmented.test.ts +1 -1
  64. package/src/components/segmented/segmented.ts +2 -2
  65. package/src/components/star-rating-input/star-rating-input.svelte +11 -4
  66. package/src/components/table-view/index.ts +3 -0
  67. package/src/components/table-view/table-view.svelte +103 -0
  68. package/src/components/table-view/table-view.test.ts +53 -0
  69. package/src/components/table-view/types.ts +14 -0
  70. package/src/components/theme/index.ts +17 -0
  71. package/src/components/theme/theme-provider.svelte +47 -0
  72. package/src/components/theme/theme-provider.test.ts +61 -0
  73. package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
  74. package/src/effects/animated-beam/path-calculator.svelte.ts +1 -1
  75. package/src/effects/border-beam/types.ts +1 -1
  76. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +1 -1
  77. package/src/effects/shader-gradient/shaders.ts +1 -1
  78. package/src/effects/spotlight/Spotlight.svelte +1 -2
  79. package/src/icons/trades.ts +3 -3
  80. package/src/lib/sliding-pill.ts +3 -3
  81. package/src/lib/tone.test.ts +10 -0
  82. package/src/lib/tone.ts +1 -1
  83. package/src/shadcn/badge/badge.svelte +1 -1
  84. package/src/shadcn/button/button.svelte +10 -1
  85. package/src/shadcn/button/button.test.ts +19 -0
  86. package/src/shadcn/checkbox/checkbox.svelte +1 -1
  87. package/src/shadcn/context-menu/context-menu-separator.svelte +4 -1
  88. package/src/shadcn/dropdown-menu/dropdown-menu-separator.svelte +4 -1
  89. package/src/shadcn/input/input.svelte +2 -2
  90. package/src/shadcn/progress/progress.svelte +8 -2
  91. package/src/shadcn/progress/tone.test.ts +13 -0
  92. package/src/shadcn/progress/tone.ts +15 -0
  93. package/src/shadcn/sidebar/sidebar-group-label.svelte +1 -1
  94. package/src/shadcn/table/table-cell.svelte +21 -1
  95. package/src/shadcn/table/table-cell.test.ts +16 -0
  96. package/src/tokens/base.css +183 -0
  97. package/src/tokens/craft.css +179 -0
  98. package/src/tokens/index.css +19 -0
  99. package/src/tokens/palette.css +410 -0
  100. package/src/tokens/primitives.css +223 -0
  101. package/scripts/relative-imports.mjs +0 -39
  102. package/src/tokens.css +0 -993
@@ -0,0 +1,217 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { tick } from "svelte";
6
+ import { CaretDown, List } from "phosphor-svelte";
7
+ import type { DocHeading } from "./scroll-spy.svelte.js";
8
+
9
+ // =========================
10
+ // TYPES
11
+ // =========================
12
+ interface Props {
13
+ items: DocHeading[];
14
+ /** The id the reader is in. */
15
+ active: string;
16
+ /** What the rail is called: "On this page". Passed in so the library holds no copy. */
17
+ label: string;
18
+ onselect: (id: string) => void;
19
+ /**
20
+ * `rail` is the column beside the document. `bar` is the strip that takes
21
+ * its place on a narrow pane: it names where you are and opens the list.
22
+ */
23
+ variant?: "rail" | "bar";
24
+ }
25
+
26
+ // =========================
27
+ // PROPS
28
+ // =========================
29
+ let { items, active, label, onselect, variant = "rail" }: Props = $props();
30
+
31
+ // =========================
32
+ // STATE
33
+ // =========================
34
+ let list = $state<HTMLElement | null>(null);
35
+ let open = $state(false);
36
+ let marker = $state({ y: 0, h: 0, ready: false });
37
+ // Plain, not state: the effect below writes `marker`, so it can't also read it.
38
+ let placed = false;
39
+ let scrollBox = $state<HTMLElement | null>(null);
40
+ let overflowing = $state(false);
41
+
42
+ // =========================
43
+ // DERIVED
44
+ // =========================
45
+ let activeIndex = $derived(Math.max(0, items.findIndex((item) => item.id === active)));
46
+ let activeLabel = $derived(items.find((item) => item.id === active)?.label ?? items[0]?.label ?? "");
47
+ // How far through the document: the bar's hairline, and the number a reader
48
+ // reads at a glance.
49
+ let progress = $derived(items.length > 1 ? activeIndex / (items.length - 1) : 0);
50
+
51
+ // =========================
52
+ // EFFECTS
53
+ // =========================
54
+ // One marker that slides to the active row, rather than a border that blinks
55
+ // from one row to the next.
56
+ $effect(() => {
57
+ void active;
58
+ void items;
59
+ if (variant !== "rail" || !list) return;
60
+ overflowing = !!scrollBox && scrollBox.scrollHeight > scrollBox.clientHeight + 1;
61
+ const row = list.querySelector<HTMLElement>("[data-current='true']");
62
+ if (!row) return;
63
+ const y = row.offsetTop;
64
+ const h = row.offsetHeight;
65
+ // First placement lands with no slide, or it would glide in from the top.
66
+ const wasPlaced = placed;
67
+ placed = true;
68
+ marker = { y, h, ready: wasPlaced };
69
+ if (!wasPlaced) return;
70
+ // A long list scrolls inside the rail, so keep the current row in view.
71
+ const box = list.parentElement;
72
+ if (box && (y < box.scrollTop || y + h > box.scrollTop + box.clientHeight)) {
73
+ box.scrollTo({ top: y - box.clientHeight / 2, behavior: "auto" });
74
+ }
75
+ });
76
+
77
+ function pick(id: string) {
78
+ open = false;
79
+ onselect(id);
80
+ void tick();
81
+ }
82
+ </script>
83
+
84
+ <svelte:window
85
+ onkeydown={(event) => {
86
+ if (event.key === "Escape") open = false;
87
+ }}
88
+ onpointerdown={(event) => {
89
+ if (!open) return;
90
+ if (!(event.target as HTMLElement).closest("[data-otp-bar]")) open = false;
91
+ }}
92
+ />
93
+
94
+ <!-- =========================== -->
95
+ <!-- MARKUP -->
96
+ <!-- =========================== -->
97
+ {#if variant === "rail"}
98
+ <nav aria-label={label} class="flex max-h-[calc(100svh-8rem)] flex-col gap-3">
99
+ <!-- Set like the map's own headings on the left, so the two rails read as one. -->
100
+ <p class="font-heading text-xs font-semibold uppercase tracking-wider text-muted-foreground">
101
+ {label}
102
+ </p>
103
+ <div
104
+ bind:this={scrollBox}
105
+ class="thin-scrollbar -mr-2 min-h-0 overflow-y-auto pr-2"
106
+ class:otp-fade={overflowing}
107
+ >
108
+ <div bind:this={list} class="relative border-l border-border">
109
+ <!-- The marker sits on the track's own line, so the active row reads as
110
+ the part of the line you're standing on. -->
111
+ <span
112
+ aria-hidden="true"
113
+ class="otp-marker absolute -left-px top-0 w-0.5 rounded-full bg-brand-indigo dark:bg-brand-indigo-light"
114
+ class:is-ready={marker.ready}
115
+ style="transform: translateY({marker.y}px); height: {marker.h}px;"
116
+ ></span>
117
+ <ul class="flex flex-col">
118
+ {#each items as item (item.id)}
119
+ {@const current = item.id === active}
120
+ <li>
121
+ <button
122
+ type="button"
123
+ data-current={current}
124
+ aria-current={current ? "location" : undefined}
125
+ onclick={() => pick(item.id)}
126
+ class="otp-row block w-full py-1.5 pl-3 pr-1 text-left text-[13px] leading-snug transition-colors duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] any-pointer-coarse:py-2.5 {current
127
+ ? 'font-medium text-foreground'
128
+ : 'text-muted-foreground hover:text-foreground'}"
129
+ >
130
+ {item.label}
131
+ </button>
132
+ </li>
133
+ {/each}
134
+ </ul>
135
+ </div>
136
+ </div>
137
+ </nav>
138
+ {:else}
139
+ <div data-otp-bar class="relative">
140
+ <button
141
+ type="button"
142
+ aria-expanded={open}
143
+ aria-label={label}
144
+ onclick={() => (open = !open)}
145
+ class="flex h-11 w-full items-center gap-2.5 px-4 text-left transition-colors duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] hover:bg-muted/50"
146
+ >
147
+ <List size={16} class="shrink-0 text-muted-foreground" aria-hidden="true" />
148
+ <span class="shrink-0 text-xs font-medium text-muted-foreground">{label}</span>
149
+ <span class="min-w-0 flex-1 truncate text-sm font-medium text-foreground">{activeLabel}</span>
150
+ <CaretDown
151
+ size={14}
152
+ class="shrink-0 text-muted-foreground transition-transform duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] {open
153
+ ? 'rotate-180'
154
+ : ''}"
155
+ />
156
+ </button>
157
+ <!-- How far through the reader is. The only motion here, and it is the state. -->
158
+ <span aria-hidden="true" class="absolute inset-x-0 bottom-0 h-px bg-border">
159
+ <span
160
+ class="otp-progress block h-full origin-left bg-brand-indigo dark:bg-brand-indigo-light"
161
+ style="transform: scaleX({progress});"
162
+ ></span>
163
+ </span>
164
+
165
+ {#if open}
166
+ <nav
167
+ aria-label={label}
168
+ class="thin-scrollbar absolute inset-x-2 top-full z-20 mt-1 max-h-[60svh] overflow-y-auto rounded-xl border border-border bg-popover p-1.5 shadow-lg"
169
+ >
170
+ <ul class="flex flex-col">
171
+ {#each items as item (item.id)}
172
+ {@const current = item.id === active}
173
+ <li>
174
+ <button
175
+ type="button"
176
+ aria-current={current ? "location" : undefined}
177
+ onclick={() => pick(item.id)}
178
+ class="block w-full rounded-lg px-3 py-2.5 text-left text-sm leading-snug transition-colors duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] {current
179
+ ? 'bg-muted font-medium text-foreground'
180
+ : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'}"
181
+ >
182
+ {item.label}
183
+ </button>
184
+ </li>
185
+ {/each}
186
+ </ul>
187
+ </nav>
188
+ {/if}
189
+ </div>
190
+ {/if}
191
+
192
+ <!-- =========================== -->
193
+ <!-- STYLE -->
194
+ <!-- =========================== -->
195
+ <style>
196
+ .otp-marker.is-ready {
197
+ transition:
198
+ transform var(--duration-medium) var(--ease-smooth-out),
199
+ height var(--duration-medium) var(--ease-smooth-out);
200
+ }
201
+
202
+ .otp-progress {
203
+ transition: transform var(--duration-medium) var(--ease-smooth-out);
204
+ }
205
+
206
+ /* A long list fades into its edges instead of ending on a hard cut. */
207
+ .otp-fade {
208
+ mask-image: linear-gradient(to bottom, transparent, #000 0.5rem, #000 calc(100% - 1.5rem), transparent);
209
+ }
210
+
211
+ @media (prefers-reduced-motion: reduce) {
212
+ .otp-marker.is-ready,
213
+ .otp-progress {
214
+ transition: none;
215
+ }
216
+ }
217
+ </style>
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Which heading the reader is in, for a box that scrolls. One spy feeds the
3
+ * side rail and the compact bar, so the two can never disagree.
4
+ */
5
+
6
+ export type DocHeading = { id: string; label: string };
7
+
8
+ /**
9
+ * Where a heading sits inside the scroller, in scroller content coordinates.
10
+ * The default reads the live DOM by id. A framed document supplies its own,
11
+ * since its headings live in another document.
12
+ */
13
+ export type LocateHeading = (id: string) => number | null;
14
+
15
+ /** How far below the scroller's top edge a heading counts as "reached" and lands. */
16
+ export const READ_LINE = 80;
17
+
18
+ export class ScrollSpy {
19
+ active = $state("");
20
+
21
+ #scroller: HTMLElement | null = null;
22
+ #items: DocHeading[] = [];
23
+ #locate: LocateHeading | undefined;
24
+ #frame = 0;
25
+
26
+ /** Point the spy at a scroller and a list. Returns the cleanup. */
27
+ attach(scroller: HTMLElement | null, items: DocHeading[], locate?: LocateHeading) {
28
+ this.#scroller = scroller;
29
+ this.#items = items;
30
+ this.#locate = locate;
31
+ if (!scroller || items.length === 0) {
32
+ this.active = "";
33
+ return () => {};
34
+ }
35
+
36
+ const schedule = () => {
37
+ this.#frame ||= requestAnimationFrame(() => {
38
+ this.#frame = 0;
39
+ this.update();
40
+ });
41
+ };
42
+ scroller.addEventListener("scroll", schedule, { passive: true });
43
+ const observer = new ResizeObserver(schedule);
44
+ observer.observe(scroller);
45
+ // A document that grows after load (a diagram, a chart) moves every
46
+ // heading below it, so watch the content too.
47
+ for (const child of scroller.children) observer.observe(child);
48
+ this.update();
49
+
50
+ return () => {
51
+ scroller.removeEventListener("scroll", schedule);
52
+ observer.disconnect();
53
+ if (this.#frame) cancelAnimationFrame(this.#frame);
54
+ this.#frame = 0;
55
+ };
56
+ }
57
+
58
+ topOf(id: string): number | null {
59
+ const scroller = this.#scroller;
60
+ if (!scroller) return null;
61
+ if (this.#locate) return this.#locate(id);
62
+ const el = scroller.querySelector<HTMLElement>(`#${CSS.escape(id)}`);
63
+ if (!el) return null;
64
+ return (
65
+ el.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop
66
+ );
67
+ }
68
+
69
+ update() {
70
+ const scroller = this.#scroller;
71
+ if (!scroller || this.#items.length === 0) return;
72
+ // At the very bottom the last heading wins, or a short final section could
73
+ // never be the current one.
74
+ const atEnd = scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 2;
75
+ if (atEnd && scroller.scrollTop > 0) {
76
+ this.active = this.#items[this.#items.length - 1].id;
77
+ return;
78
+ }
79
+ const line = scroller.scrollTop + READ_LINE + 1;
80
+ let current = this.#items[0].id;
81
+ for (const item of this.#items) {
82
+ const top = this.topOf(item.id);
83
+ if (top !== null && top <= line) current = item.id;
84
+ }
85
+ this.active = current;
86
+ }
87
+
88
+ /** Scroll so the heading lands on the read line, instant under reduced motion. */
89
+ jump(id: string) {
90
+ const scroller = this.#scroller;
91
+ const top = this.topOf(id);
92
+ if (!scroller || top === null) return;
93
+ const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
94
+ this.active = id;
95
+ scroller.scrollTo({ top: Math.max(0, top - READ_LINE), behavior: reduced ? "auto" : "smooth" });
96
+ }
97
+ }
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import type { Snippet } from "svelte";
6
+ import { cn } from "@heyramzi/utils";
7
+ import * as Field from "../../shadcn/field/index.js";
8
+
9
+ // =========================
10
+ // TYPES
11
+ // =========================
12
+ interface Props {
13
+ label: string;
14
+ /** Id of the control, so the label focuses it. */
15
+ for: string;
16
+ description?: string;
17
+ error?: string;
18
+ class?: string;
19
+ /** The control: an Input, a Select trigger. It takes the width the action leaves. */
20
+ children: Snippet;
21
+ /** The button that acts on the control. Same height as the control, bottom of the row. */
22
+ action?: Snippet;
23
+ }
24
+
25
+ // =========================
26
+ // PROPS
27
+ // =========================
28
+ let { label, for: htmlFor, description, error, class: className, children, action }: Props = $props();
29
+ </script>
30
+
31
+ <!-- =========================== -->
32
+ <!-- MARKUP -->
33
+ <!-- WHY a measure of its own: a settings card fills the page, but a text field
34
+ stretched over 1200px is hard to read and to aim at. 36rem, left aligned. -->
35
+ <!-- =========================== -->
36
+ <Field.Simple
37
+ {label}
38
+ {description}
39
+ {error}
40
+ for={htmlFor}
41
+ class={cn("max-w-xl", className)}
42
+ >
43
+ <div
44
+ class="flex items-start gap-2"
45
+ data-slot="field-row"
46
+ >
47
+ <div class="min-w-0 flex-1">{@render children()}</div>
48
+ {#if action}
49
+ <!-- A disabled action reads as a muted button, not a half-faded one. -->
50
+ <div
51
+ class="shrink-0 [&_button:disabled]:bg-muted [&_button:disabled]:text-muted-foreground [&_button:disabled]:opacity-100 [&_button:disabled]:shadow-none"
52
+ >
53
+ {@render action()}
54
+ </div>
55
+ {/if}
56
+ </div>
57
+ </Field.Simple>
@@ -0,0 +1 @@
1
+ export { default as FieldRow } from "./field-row.svelte";
@@ -6,7 +6,7 @@
6
6
  import type { HTMLButtonAttributes } from "svelte/elements";
7
7
 
8
8
  // ========================= TYPES
9
- type Props = {
9
+ interface Props extends Omit<HTMLButtonAttributes, "class"> {
10
10
  duration?: number;
11
11
  onConfirm: () => void;
12
12
  disabled?: boolean;
@@ -19,7 +19,7 @@
19
19
  class?: string;
20
20
  idle: Snippet;
21
21
  holding: Snippet;
22
- } & HTMLButtonAttributes;
22
+ }
23
23
 
24
24
  // ========================= PROPS
25
25
  let {
@@ -4,7 +4,7 @@ import type { PhosphorIcon, IconWeight } from "../../lib/icon-types.js";
4
4
  // =========================
5
5
  // TYPES
6
6
  // =========================
7
- type Props = {
7
+ interface Props {
8
8
  icon: PhosphorIcon;
9
9
  size?: number;
10
10
  class?: string;
@@ -9,7 +9,7 @@ import type { PhosphorIcon, IconWeight } from "../../lib/icon-types.js";
9
9
  // =========================
10
10
  // TYPES
11
11
  // =========================
12
- type Props = {
12
+ interface Props {
13
13
  // Shown when nothing is picked or the board is empty. Both mean "content
14
14
  // goes here", so both get a dashed `border-2` edge.
15
15
  icon?: PhosphorIcon;
@@ -8,7 +8,7 @@ import { cn } from "@heyramzi/utils";
8
8
  // =========================
9
9
  // TYPES
10
10
  // =========================
11
- type Props = {
11
+ interface Props {
12
12
  // Answers three questions in one order: what is this, what state is it in,
13
13
  // what can I do with it. Each queue page used to order them differently.
14
14
  title: string;
@@ -11,7 +11,7 @@ import type { PhosphorIcon } from "../../lib/icon-types.js";
11
11
  // =========================
12
12
  // TYPES
13
13
  // =========================
14
- type Props = {
14
+ interface Props {
15
15
  // One panel per idea in a detail pane. Hand-rolled copies drifted between
16
16
  // 8px and 12px corners, so rounded-xl lives here once.
17
17
  label?: string;
@@ -8,7 +8,7 @@ import { cn } from "@heyramzi/utils";
8
8
  // =========================
9
9
  // TYPES
10
10
  // =========================
11
- type Props = {
11
+ interface Props {
12
12
  // A hairline band inside `DetailHeader` or `DetailPanel` for the record's
13
13
  // own facts, so the page never nests cards inside cards.
14
14
  muted?: boolean;
@@ -8,7 +8,7 @@ import { cn } from "@heyramzi/utils";
8
8
  // =========================
9
9
  // TYPES
10
10
  // =========================
11
- type Props = {
11
+ interface Props {
12
12
  // A flat stack of panels at one gap. `xl:pr-1` keeps the top panel's
13
13
  // corner clear of the shell's scrollbar.
14
14
 
@@ -10,7 +10,7 @@ import { cn } from "@heyramzi/utils";
10
10
  // =========================
11
11
  // TYPES
12
12
  // =========================
13
- type Props = {
13
+ interface Props {
14
14
  checked: boolean;
15
15
  onclick: () => void;
16
16
  variant?: "checkbox" | "radio";
@@ -17,7 +17,7 @@ const DefaultFunnelIcon = FunnelIcon as unknown as PhosphorIcon;
17
17
  // =========================
18
18
  // TYPES
19
19
  // =========================
20
- type Props = {
20
+ interface Props {
21
21
  label: string;
22
22
  icon?: PhosphorIcon;
23
23
  count?: number;
@@ -10,7 +10,7 @@
10
10
  // =========================
11
11
  // TYPES
12
12
  // =========================
13
- type Props = {
13
+ interface Props {
14
14
  groups: TGroup[];
15
15
  getKey: (group: TGroup) => string;
16
16
  defaultCollapsed?: string[];
@@ -23,7 +23,7 @@
23
23
  actionHref?: string;
24
24
  };
25
25
 
26
- type Props = {
26
+ interface Props {
27
27
  isEmpty?: boolean;
28
28
  emptyState?: EmptyConfig;
29
29
  hideToolbarWhenEmpty?: boolean;
@@ -12,7 +12,7 @@ import { MASTER_DETAIL_PANE, type MasterDetailPane } from "./queue-rail.js";
12
12
  // =========================
13
13
  // TYPES
14
14
  // =========================
15
- type Props = {
15
+ interface Props {
16
16
  // The list rail (an <aside>) and the content pane. Pages style both; this
17
17
  // shell owns the two columns and the min-h-0/min-w-0 scroll containment.
18
18
  rail: Snippet;
@@ -10,7 +10,7 @@ import { stepTo } from "./step.js";
10
10
  // =========================
11
11
  // TYPES
12
12
  // =========================
13
- type Props = {
13
+ interface Props {
14
14
  /** Ids in the order the list shows them. */
15
15
  ids: readonly string[];
16
16
  current: string | null | undefined;
@@ -8,7 +8,7 @@ import { cn } from "@heyramzi/utils";
8
8
  // =========================
9
9
  // TYPES
10
10
  // =========================
11
- type Props = {
11
+ interface Props {
12
12
  // The scroller under `RailHead`. `min-h-0` makes a flex child scroll instead
13
13
  // of growing its parent, and it's the line hand-rolled rails forgot.
14
14
  children: Snippet;
@@ -9,7 +9,7 @@ import { cn } from "@heyramzi/utils";
9
9
  // =========================
10
10
  // TYPES
11
11
  // =========================
12
- type Props = {
12
+ interface Props {
13
13
  // The fixed top of a queue rail: label, filter, search. A fixed height, so
14
14
  // rows start on the same line on every queue page.
15
15
  label?: string;
@@ -10,7 +10,7 @@ import { MASTER_DETAIL_PANE, type MasterDetailPane } from "./queue-rail.js";
10
10
  // =========================
11
11
  // TYPES
12
12
  // =========================
13
- type Props = {
13
+ interface Props {
14
14
  // One row shape for every queue rail, in a fixed order: identity, preview,
15
15
  // facts. Three pages used to draw three different rows.
16
16
  title: string;
@@ -9,7 +9,7 @@ import type { PhosphorIcon } from "../../lib/icon-types.js";
9
9
  // =========================
10
10
  // TYPES
11
11
  // =========================
12
- type Props = {
12
+ interface Props {
13
13
  // The one label style for panel, rail group and sub-section headers. Retyped
14
14
  // copies drifted to a different tracking value.
15
15
  icon?: PhosphorIcon;
@@ -9,7 +9,7 @@ import { cn } from "@heyramzi/utils";
9
9
  // =========================
10
10
  // TYPES
11
11
  // =========================
12
- type Props = {
12
+ interface Props {
13
13
  value: string;
14
14
  placeholder?: string;
15
15
  size?: "sm" | "default";
@@ -57,7 +57,7 @@ let {
57
57
  class={cn(
58
58
  "w-full",
59
59
  size === "sm" ? "h-7 pl-7 pr-2.5 text-xs" : "h-7 pl-7 pr-2.5 text-xs",
60
- "any-pointer-coarse:h-10 any-pointer-coarse:text-base",
60
+ "any-pointer-coarse:h-11 any-pointer-coarse:text-base",
61
61
  )}
62
62
  />
63
63
  </div>
@@ -16,7 +16,7 @@ export function toolbarTriggerClass({
16
16
  }: ToolbarTriggerOpts = {}): string {
17
17
  return cn(
18
18
  "inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-border font-medium transition-colors active:translate-y-px",
19
- "h-7 px-2.5 text-xs any-pointer-coarse:h-10 any-pointer-coarse:px-3",
19
+ "h-7 px-2.5 text-xs any-pointer-coarse:h-11 any-pointer-coarse:px-3",
20
20
  active ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground",
21
21
  className,
22
22
  );
@@ -13,7 +13,7 @@ type LocaleOption = {
13
13
  flag: string;
14
14
  };
15
15
 
16
- type Props = {
16
+ interface Props {
17
17
  // Wraps `SegmentedFilter` so it shares one track with every other strip. It
18
18
  // only adds each language's flag.
19
19
  value: string;
@@ -12,7 +12,7 @@ import { cn } from "@heyramzi/utils";
12
12
  * A brand's mark at glyph size: a kit icon, a channel avatar, a client logo.
13
13
  * One renderer, because three places each handled a missing file their own way.
14
14
  */
15
- type Props = {
15
+ interface Props {
16
16
  /** The file. A missing or broken one falls back to the label's initial. */
17
17
  src?: string;
18
18
  /** Read for the fallback letter. The mark itself is decorative. */
@@ -18,7 +18,7 @@ import type { PickerOption } from "./types.js";
18
18
  * wraps `Picker` because the kit strip and the channel row wrote the same two
19
19
  * snippets.
20
20
  */
21
- type Props = {
21
+ interface Props {
22
22
  options: PickerOption<V>[];
23
23
  value: V | null;
24
24
  onSelect: (value: V) => void;
@@ -12,7 +12,7 @@
12
12
  // It replaced four pickers in one sidebar, each built differently. Only one
13
13
  // had search, only one had arrow keys, and the check mark sat in three places.
14
14
 
15
- type Props = {
15
+ interface Props {
16
16
  /** Flat list. Mutually exclusive with `groups`. */
17
17
  options?: PickerOption<V>[];
18
18
  /** Titled runs, when the menu has a shape worth naming. */
@@ -25,8 +25,8 @@ export const pickerTriggerClass = tv({
25
25
  // Heights match `segmentedTrackClass`, so a strip and a picker line up.
26
26
  // Padding is 6px leading, 10px trailing: an even 8px shunted icon rows right.
27
27
  size: {
28
- sm: "h-6 ps-1.5 pe-2 any-pointer-coarse:h-9 any-pointer-coarse:pe-2.5",
29
- default: "h-7 ps-1.5 pe-2.5 any-pointer-coarse:h-10",
28
+ sm: "h-6 ps-1.5 pe-2 any-pointer-coarse:h-11 any-pointer-coarse:pe-2.5",
29
+ default: "h-7 ps-1.5 pe-2.5 any-pointer-coarse:h-11",
30
30
  },
31
31
  /**
32
32
  * Fill the row. Off by default: Linear's property rows sit at natural width
@@ -0,0 +1 @@
1
+ export { default as SectionCard } from "./section-card.svelte";