@heyramzi/ui 2.5.0 → 2.7.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 +4 -0
- package/package.json +1 -1
- package/src/brand/index.ts +12 -0
- package/src/canvas/headless/canvas-cleanup.test.ts +1 -1
- package/src/canvas/ops/ops.ts +1 -1
- package/src/icons/trades.ts +1 -1
- package/src/record/info-list.svelte +87 -7
- package/src/record/types.ts +7 -0
- package/src/shadcn/card/card-title.svelte +5 -1
- package/src/tokens/palette.css +10 -4
package/COMPONENTS.md
CHANGED
|
@@ -133,6 +133,10 @@ Generic pieces built on the primitives, imported from `@heyramzi/ui/components/<
|
|
|
133
133
|
- `typing-dots` — TypingDots
|
|
134
134
|
- `voice-waveform` — VoiceWaveform
|
|
135
135
|
|
|
136
|
+
## Record (`src/record`)
|
|
137
|
+
|
|
138
|
+
- `info-list` — InfoList. An editable label and value `<dl>`; `layout="row"` makes it a read-only band of items that wrap and truncate, with `term`, `content` and `action` snippets per row and a `trailing` snippet after the list
|
|
139
|
+
|
|
136
140
|
---
|
|
137
141
|
|
|
138
142
|
Skipped on purpose: brand assets (logos, marks, brand renderer), kanban, list, gantt and status (they live in `@heyramzi/ui/tasks`), and charts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyramzi/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Svelte 5 UI for apps: primitives, effects and animations, plus tasks, charts, slides, an editor, onboarding and a canvas as subpaths.",
|
|
5
5
|
"homepage": "https://ui.heyramzi.com",
|
|
6
6
|
"license": "MIT",
|
package/src/brand/index.ts
CHANGED
|
@@ -29,6 +29,14 @@ export type BrandColors = {
|
|
|
29
29
|
surfaceLight?: CssColor;
|
|
30
30
|
/** Borders on a light page. */
|
|
31
31
|
lineLight?: CssColor;
|
|
32
|
+
/** The key button on a light page. Falls back to the near-black neutral. */
|
|
33
|
+
primary?: CssColor;
|
|
34
|
+
/** Text on the key button on a light page. */
|
|
35
|
+
primaryForeground?: CssColor;
|
|
36
|
+
/** The key button on a dark page. Falls back to paper. */
|
|
37
|
+
primaryOnDark?: CssColor;
|
|
38
|
+
/** Text on the key button on a dark page. */
|
|
39
|
+
primaryForegroundOnDark?: CssColor;
|
|
32
40
|
};
|
|
33
41
|
|
|
34
42
|
export type BrandFonts = {
|
|
@@ -61,6 +69,10 @@ const COLOR_VARS = {
|
|
|
61
69
|
paper: "--brand-paper",
|
|
62
70
|
surfaceLight: "--brand-surface-light",
|
|
63
71
|
lineLight: "--brand-line-light",
|
|
72
|
+
primary: "--brand-primary",
|
|
73
|
+
primaryForeground: "--brand-primary-foreground",
|
|
74
|
+
primaryOnDark: "--brand-primary-on-dark",
|
|
75
|
+
primaryForegroundOnDark: "--brand-primary-foreground-on-dark",
|
|
64
76
|
} as const satisfies Record<keyof BrandColors, string>;
|
|
65
77
|
|
|
66
78
|
const FONT_VARS = {
|
|
@@ -3,7 +3,7 @@ import { sortParentsFirst, stripCallbacks } from "./canvas-cleanup.js";
|
|
|
3
3
|
import { makeNode } from "../test-utils.js";
|
|
4
4
|
import type { CanvasNode } from "../types.js";
|
|
5
5
|
|
|
6
|
-
/** Read serialized data as a loose record: these tests exercise
|
|
6
|
+
/** Read serialized data as a loose record: these tests exercise legacy fields
|
|
7
7
|
* that are no longer part of any per-type shape but must survive the denylist. */
|
|
8
8
|
function loose(node: { data: unknown }): Record<string, unknown> {
|
|
9
9
|
return node.data as Record<string, unknown>;
|
package/src/canvas/ops/ops.ts
CHANGED
|
@@ -36,7 +36,7 @@ export const patchNodeOp = z.object({
|
|
|
36
36
|
kind: z.literal("patchNode"),
|
|
37
37
|
nodeId: z.string().min(1),
|
|
38
38
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
39
|
-
// Merged into the node's style object. A CSS-string style
|
|
39
|
+
// Merged into the node's style object. A legacy CSS-string style is replaced
|
|
40
40
|
// wholesale (there is nothing sane to merge into).
|
|
41
41
|
style: z.record(z.string(), z.unknown()).optional(),
|
|
42
42
|
width: z.number().optional(),
|
package/src/icons/trades.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/*
|
|
18
|
-
* car-garage, hammer / wave-saw-tool, helmet, trowel, grid-dots, leaf, car, crane. Svelte 5 port is ` @tabler/icons-svelte-runes` (runes, not
|
|
18
|
+
* car-garage, hammer / wave-saw-tool, helmet, trowel, grid-dots, leaf, car, crane. Svelte 5 port is ` @tabler/icons-svelte-runes` (runes, not legacy ` @tabler/icons-svelte`).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// =========================
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
5
6
|
import { Check, CaretUpDown } from "phosphor-svelte";
|
|
6
7
|
import { cn } from "../lib/utils.js";
|
|
7
8
|
import { Button } from "../shadcn/button/index.js";
|
|
@@ -23,9 +24,32 @@
|
|
|
23
24
|
/** A heading over the list. Left out, nothing is drawn. */
|
|
24
25
|
title?: string | null;
|
|
25
26
|
labels?: Partial<RecordLabels>;
|
|
27
|
+
/**
|
|
28
|
+
* `list` is the editable label and value grid. `row` is a read-only band of items that
|
|
29
|
+
* wrap, with values that truncate. Rows ignore `onSave` there and keep their empty ones.
|
|
30
|
+
*/
|
|
31
|
+
layout?: "list" | "row";
|
|
32
|
+
/** `row` only. Where the label sits against its value. */
|
|
33
|
+
labelPosition?: "above" | "beside";
|
|
34
|
+
/** `row` only. Classes for the `<dl>`, to set a grid or a gap. */
|
|
35
|
+
listClass?: string;
|
|
36
|
+
/** `row` only. Classes for each item, to cap its width. */
|
|
37
|
+
itemClass?: string;
|
|
38
|
+
/** `row` only. Sits after the list on the same line, a button for the whole band. */
|
|
39
|
+
trailing?: Snippet;
|
|
26
40
|
class?: string;
|
|
27
41
|
}
|
|
28
|
-
let {
|
|
42
|
+
let {
|
|
43
|
+
rows,
|
|
44
|
+
title = null,
|
|
45
|
+
labels,
|
|
46
|
+
layout = "list",
|
|
47
|
+
labelPosition = "above",
|
|
48
|
+
listClass,
|
|
49
|
+
itemClass,
|
|
50
|
+
trailing,
|
|
51
|
+
class: className,
|
|
52
|
+
}: Props = $props();
|
|
29
53
|
|
|
30
54
|
// =========================
|
|
31
55
|
// STATE
|
|
@@ -42,9 +66,13 @@
|
|
|
42
66
|
let t = $derived(resolveLabels(labels));
|
|
43
67
|
// A read-only row with nothing in it is noise. An editable one stays, as the way to fill it.
|
|
44
68
|
let shown = $derived(
|
|
45
|
-
rows.filter(
|
|
69
|
+
(layout === "row" ? rows.filter((r) => !r.divider) : rows).filter(
|
|
46
70
|
(r) =>
|
|
47
|
-
|
|
71
|
+
layout === "row" ||
|
|
72
|
+
r.divider ||
|
|
73
|
+
r.onSave ||
|
|
74
|
+
(r.value != null && r.value !== "") ||
|
|
75
|
+
(r.chips?.length ?? 0) > 0,
|
|
48
76
|
),
|
|
49
77
|
);
|
|
50
78
|
// Past this many options a plain list is slower than typing.
|
|
@@ -135,7 +163,7 @@
|
|
|
135
163
|
<!-- =========================== -->
|
|
136
164
|
<!-- MARKUP -->
|
|
137
165
|
<!-- =========================== -->
|
|
138
|
-
{#snippet display(row: InfoRow)}
|
|
166
|
+
{#snippet display(row: InfoRow, clamp = false)}
|
|
139
167
|
{#if row.chips?.length}
|
|
140
168
|
<span class="flex flex-wrap items-center gap-1">
|
|
141
169
|
{#each row.chips as chip (chip.label)}
|
|
@@ -147,12 +175,17 @@
|
|
|
147
175
|
{:else if row.href && row.value}
|
|
148
176
|
<a
|
|
149
177
|
href={row.href}
|
|
150
|
-
class=
|
|
178
|
+
class={cn(
|
|
179
|
+
"min-w-0 truncate text-sm font-medium underline-offset-2 hover:underline",
|
|
180
|
+
clamp &&
|
|
181
|
+
"rounded-xs focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
|
182
|
+
)}>{row.value}</a
|
|
151
183
|
>
|
|
152
184
|
{:else if row.value}
|
|
153
185
|
<span
|
|
154
186
|
class={cn(
|
|
155
|
-
"min-w-0 text-sm font-medium tabular-nums
|
|
187
|
+
"min-w-0 text-sm font-medium tabular-nums",
|
|
188
|
+
clamp ? "truncate" : "wrap-anywhere whitespace-pre-line",
|
|
156
189
|
row.muted && "text-muted-foreground",
|
|
157
190
|
)}>{row.value}</span
|
|
158
191
|
>
|
|
@@ -215,7 +248,54 @@
|
|
|
215
248
|
</Popover.Root>
|
|
216
249
|
{/snippet}
|
|
217
250
|
|
|
218
|
-
{#if shown.length > 0}
|
|
251
|
+
{#if layout === "row" && (shown.length > 0 || trailing)}
|
|
252
|
+
<div class={cn("flex items-start justify-between gap-4", className)}>
|
|
253
|
+
<dl class={cn("flex min-w-0 flex-1 flex-wrap gap-x-8 gap-y-2", listClass)}>
|
|
254
|
+
{#each shown as row (keyOf(row))}
|
|
255
|
+
<div
|
|
256
|
+
class={cn(
|
|
257
|
+
"min-w-0",
|
|
258
|
+
labelPosition === "beside" && "flex items-baseline gap-2",
|
|
259
|
+
itemClass,
|
|
260
|
+
)}
|
|
261
|
+
>
|
|
262
|
+
<dt
|
|
263
|
+
class={cn(
|
|
264
|
+
"flex items-center gap-1.5 text-xs text-muted-foreground",
|
|
265
|
+
labelPosition === "beside" && "shrink-0",
|
|
266
|
+
)}
|
|
267
|
+
>
|
|
268
|
+
{#if row.term}
|
|
269
|
+
{@render row.term()}
|
|
270
|
+
{:else}
|
|
271
|
+
{#if row.icon}
|
|
272
|
+
{@const Icon = row.icon}
|
|
273
|
+
<Icon class="size-3.5 shrink-0" aria-hidden="true" />
|
|
274
|
+
{/if}
|
|
275
|
+
{row.label}
|
|
276
|
+
{/if}
|
|
277
|
+
</dt>
|
|
278
|
+
<dd
|
|
279
|
+
class="flex min-w-0 items-center gap-1.5"
|
|
280
|
+
title={row.content ? undefined : (row.value ?? undefined)}
|
|
281
|
+
>
|
|
282
|
+
{#if row.content}
|
|
283
|
+
{@render row.content()}
|
|
284
|
+
{:else if isEmpty(row)}
|
|
285
|
+
<span class="min-w-0 truncate text-sm text-muted-foreground" title={row.placeholder}
|
|
286
|
+
>{row.placeholder ?? "-"}</span
|
|
287
|
+
>
|
|
288
|
+
{:else}
|
|
289
|
+
{@render display(row, true)}
|
|
290
|
+
{/if}
|
|
291
|
+
{@render row.action?.()}
|
|
292
|
+
</dd>
|
|
293
|
+
</div>
|
|
294
|
+
{/each}
|
|
295
|
+
</dl>
|
|
296
|
+
{@render trailing?.()}
|
|
297
|
+
</div>
|
|
298
|
+
{:else if shown.length > 0}
|
|
219
299
|
<div class={cn("flex flex-col gap-2", className)}>
|
|
220
300
|
{#if title}
|
|
221
301
|
<h2 class="text-xs font-medium tracking-wide text-muted-foreground uppercase">{title}</h2>
|
package/src/record/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The view types of a client or project record. Plain data in, nothing loaded:
|
|
2
2
|
// the host maps its own rows onto these and passes them down.
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
3
4
|
import type { PhosphorIcon } from "../lib/icon-types.js";
|
|
4
5
|
import type { Tone } from "../lib/tone.js";
|
|
5
6
|
|
|
@@ -186,6 +187,12 @@ export type InfoRow = {
|
|
|
186
187
|
/** Without it the row is read-only. A multi row passes the picked keys. */
|
|
187
188
|
onSave?: (value: string | string[]) => void | Promise<void>;
|
|
188
189
|
divider?: boolean;
|
|
190
|
+
/** `layout="row"` only. Replaces the label text inside the `<dt>` (key chips, say). `label` still names the row. */
|
|
191
|
+
term?: Snippet;
|
|
192
|
+
/** `layout="row"` only. Replaces the drawn value with arbitrary content. A row with one is never hidden for lacking a value. */
|
|
193
|
+
content?: Snippet;
|
|
194
|
+
/** `layout="row"` only. Draws after the value, inside the `<dd>`. */
|
|
195
|
+
action?: Snippet;
|
|
189
196
|
};
|
|
190
197
|
|
|
191
198
|
/** A kind of event: how its filter chip and its default row read. */
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
import type { HTMLAttributes } from "svelte/elements";
|
|
3
3
|
import { cn, type WithElementRef } from "../../lib/utils.js";
|
|
4
4
|
|
|
5
|
-
// WHY: card titles
|
|
5
|
+
// WHY: card titles render as a div by default for backwards compatibility,
|
|
6
|
+
|
|
7
|
+
// but a page whose sections are all cards ends up with no headings below the h1, so screen readers cannot navigate it by heading (WCAG 2.4.6).
|
|
8
|
+
|
|
9
|
+
// Pass `as="h2"` on real section titles.
|
|
6
10
|
let {
|
|
7
11
|
ref = $bindable(null),
|
|
8
12
|
class: className,
|
package/src/tokens/palette.css
CHANGED
|
@@ -38,6 +38,12 @@
|
|
|
38
38
|
--brand-surface-light: oklch(0.99 0 0); /* Cards on a light page */
|
|
39
39
|
--brand-line-light: oklch(0.91 0 0); /* Borders on a light page */
|
|
40
40
|
|
|
41
|
+
/* The key button. Each theme has its own pair, since a hue lifted for dark needs dark text. */
|
|
42
|
+
--brand-primary: var(--gray-900);
|
|
43
|
+
--brand-primary-foreground: var(--gray-50);
|
|
44
|
+
--brand-primary-on-dark: var(--brand-paper);
|
|
45
|
+
--brand-primary-foreground-on-dark: var(--gray-900);
|
|
46
|
+
|
|
41
47
|
/* === Tint scales ===
|
|
42
48
|
Mixed from the three hues so `--brand` alone yields 100 to 800; 400 is the colour lifted
|
|
43
49
|
for dark, 600 the colour itself. */
|
|
@@ -220,8 +226,8 @@
|
|
|
220
226
|
--color-popover: var(--brand-surface-light);
|
|
221
227
|
--color-popover-foreground: var(--gray-900);
|
|
222
228
|
|
|
223
|
-
--color-primary: var(--
|
|
224
|
-
--color-primary-foreground: var(--
|
|
229
|
+
--color-primary: var(--brand-primary);
|
|
230
|
+
--color-primary-foreground: var(--brand-primary-foreground);
|
|
225
231
|
|
|
226
232
|
--color-secondary: var(--gray-100);
|
|
227
233
|
--color-secondary-foreground: var(--gray-900);
|
|
@@ -320,8 +326,8 @@
|
|
|
320
326
|
--color-popover: var(--brand-surface);
|
|
321
327
|
--color-popover-foreground: var(--brand-paper);
|
|
322
328
|
|
|
323
|
-
--color-primary: var(--brand-
|
|
324
|
-
--color-primary-foreground: var(--
|
|
329
|
+
--color-primary: var(--brand-primary-on-dark);
|
|
330
|
+
--color-primary-foreground: var(--brand-primary-foreground-on-dark);
|
|
325
331
|
|
|
326
332
|
--color-secondary: var(--brand-raised);
|
|
327
333
|
--color-secondary-foreground: var(--brand-paper);
|