@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.
- package/COMPONENTS.md +6 -0
- package/package.json +2 -2
- package/scripts/lib/sync-core.mjs +1 -1
- package/scripts/sync-shadcn.mjs +11 -11
- package/scripts/sync-transitions.mjs +7 -7
- package/src/agents/agent-avatar.svelte +14 -15
- package/src/agents/agent-cast.svelte +2 -2
- package/src/agents/demo.svelte +1 -1
- package/src/agents/helpers.ts +10 -1
- package/src/agents/index.ts +2 -2
- package/src/agents/page-state.ts +1 -1
- package/src/agents/types.ts +3 -3
- package/src/components/copy/copy-button.test.ts +3 -1
- package/src/components/data-table/infinite-scroll.svelte +1 -1
- package/src/components/data-table/pagination.svelte +1 -1
- package/src/components/data-table/row-arrow.svelte +1 -1
- package/src/components/data-table/row-context-menu.svelte +1 -1
- package/src/components/data-table/sortable-head.svelte +2 -2
- package/src/components/data-table/table-skeleton.svelte +1 -1
- package/src/components/docs/doc-page.svelte +161 -0
- package/src/components/docs/index.ts +12 -0
- package/src/components/docs/on-this-page.svelte +217 -0
- package/src/components/docs/scroll-spy.svelte.ts +97 -0
- package/src/components/field-row/field-row.svelte +57 -0
- package/src/components/field-row/index.ts +1 -0
- package/src/components/hold-to-confirm/hold-to-confirm.svelte +2 -2
- package/src/components/icon/icon.svelte +1 -1
- package/src/components/list-page/detail-empty.svelte +1 -1
- package/src/components/list-page/detail-header.svelte +1 -1
- package/src/components/list-page/detail-panel.svelte +1 -1
- package/src/components/list-page/detail-row.svelte +1 -1
- package/src/components/list-page/detail-stack.svelte +1 -1
- package/src/components/list-page/filter-item.svelte +1 -1
- package/src/components/list-page/filter-popover.svelte +1 -1
- package/src/components/list-page/grouped-list.svelte +1 -1
- package/src/components/list-page/list-page-shell.svelte +1 -1
- package/src/components/list-page/master-detail.svelte +1 -1
- package/src/components/list-page/prev-next.svelte +1 -1
- package/src/components/list-page/rail-body.svelte +1 -1
- package/src/components/list-page/rail-head.svelte +1 -1
- package/src/components/list-page/rail-row.svelte +1 -1
- package/src/components/list-page/section-label.svelte +1 -1
- package/src/components/list-page/toolbar-search.svelte +2 -2
- package/src/components/list-page/toolbar-trigger.ts +1 -1
- package/src/components/locale-switch/locale-switch.svelte +1 -1
- package/src/components/mark/mark.svelte +1 -1
- package/src/components/picker/mark-picker.svelte +1 -1
- package/src/components/picker/picker.svelte +1 -1
- package/src/components/picker/trigger.ts +2 -2
- package/src/components/section-card/index.ts +1 -0
- package/src/components/section-card/section-card.svelte +43 -0
- package/src/components/section-header/index.ts +1 -0
- package/src/components/section-header/section-header.svelte +54 -0
- package/src/components/section-header/section-header.test.ts +25 -0
- package/src/components/segmented/animated-tabs.svelte +2 -2
- package/src/components/segmented/collapse.test.ts +10 -0
- package/src/components/segmented/collapse.ts +5 -0
- package/src/components/segmented/segment.svelte +2 -2
- package/src/components/segmented/segmented-filter.svelte +6 -5
- package/src/components/segmented/segmented-nav.svelte +1 -1
- package/src/components/segmented/segmented-overflow.svelte +1 -1
- package/src/components/segmented/segmented-track.svelte +29 -4
- package/src/components/segmented/segmented.test.ts +1 -1
- package/src/components/segmented/segmented.ts +2 -2
- package/src/components/star-rating-input/star-rating-input.svelte +11 -4
- package/src/components/table-view/index.ts +3 -0
- package/src/components/table-view/table-view.svelte +103 -0
- package/src/components/table-view/table-view.test.ts +53 -0
- package/src/components/table-view/types.ts +14 -0
- package/src/components/theme/index.ts +17 -0
- package/src/components/theme/theme-provider.svelte +47 -0
- package/src/components/theme/theme-provider.test.ts +61 -0
- package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
- package/src/effects/animated-beam/path-calculator.svelte.ts +1 -1
- package/src/effects/border-beam/types.ts +1 -1
- package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +1 -1
- package/src/effects/shader-gradient/shaders.ts +1 -1
- package/src/effects/spotlight/Spotlight.svelte +1 -2
- package/src/icons/trades.ts +3 -3
- package/src/lib/sliding-pill.ts +3 -3
- package/src/lib/tone.test.ts +10 -0
- package/src/lib/tone.ts +1 -1
- package/src/shadcn/badge/badge.svelte +1 -1
- package/src/shadcn/button/button.svelte +10 -1
- package/src/shadcn/button/button.test.ts +19 -0
- package/src/shadcn/checkbox/checkbox.svelte +1 -1
- package/src/shadcn/context-menu/context-menu-separator.svelte +4 -1
- package/src/shadcn/dropdown-menu/dropdown-menu-separator.svelte +4 -1
- package/src/shadcn/input/input.svelte +2 -2
- package/src/shadcn/progress/progress.svelte +8 -2
- package/src/shadcn/progress/tone.test.ts +13 -0
- package/src/shadcn/progress/tone.ts +15 -0
- package/src/shadcn/sidebar/sidebar-group-label.svelte +1 -1
- package/src/shadcn/table/table-cell.svelte +21 -1
- package/src/shadcn/table/table-cell.test.ts +16 -0
- package/src/tokens/base.css +183 -0
- package/src/tokens/craft.css +179 -0
- package/src/tokens/index.css +19 -0
- package/src/tokens/palette.css +410 -0
- package/src/tokens/primitives.css +223 -0
- package/scripts/relative-imports.mjs +0 -39
- 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
|
-
|
|
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
|
-
}
|
|
22
|
+
}
|
|
23
23
|
|
|
24
24
|
// ========================= PROPS
|
|
25
25
|
let {
|
|
@@ -9,7 +9,7 @@ import type { PhosphorIcon, IconWeight } from "../../lib/icon-types.js";
|
|
|
9
9
|
// =========================
|
|
10
10
|
// TYPES
|
|
11
11
|
// =========================
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -12,7 +12,7 @@ import { MASTER_DETAIL_PANE, type MasterDetailPane } from "./queue-rail.js";
|
|
|
12
12
|
// =========================
|
|
13
13
|
// TYPES
|
|
14
14
|
// =========================
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
29
|
-
default: "h-7 ps-1.5 pe-2.5 any-pointer-coarse:h-
|
|
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";
|