@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 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.5.0",
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",
@@ -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 old fields
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>;
@@ -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 from an old save is replaced
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(),
@@ -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 the Svelte 4 ` @tabler/icons-svelte`).
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 { rows, title = null, labels, class: className }: Props = $props();
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
- r.divider || r.onSave || (r.value != null && r.value !== "") || (r.chips?.length ?? 0) > 0,
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="min-w-0 truncate text-sm font-medium underline-offset-2 hover:underline">{row.value}</a
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 wrap-anywhere whitespace-pre-line",
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>
@@ -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 default to a div, so pass `as="h2"` on real section titles or a page of cards has no headings for screen readers (WCAG 2.4.6).
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,
@@ -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(--gray-900);
224
- --color-primary-foreground: var(--gray-50);
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-paper);
324
- --color-primary-foreground: var(--gray-900);
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);