@heyramzi/ui 2.4.0 → 2.6.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.4.0",
3
+ "version": "2.6.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",
@@ -45,6 +45,8 @@
45
45
  xTicks?: number;
46
46
  /** A target drawn across the plot, like the deal a cost runs up to. */
47
47
  reference?: ChartReference;
48
+ /** The headline while no point is hovered, like a period total. Defaults to the last point. */
49
+ resting?: { value: number; label: string };
48
50
  class?: string;
49
51
  }
50
52
 
@@ -65,6 +67,7 @@
65
67
  formatX = (v) => String(v),
66
68
  xTicks = 6,
67
69
  reference,
70
+ resting,
68
71
  class: className,
69
72
  }: Props = $props();
70
73
 
@@ -127,7 +130,14 @@
127
130
  aria-label="Area chart"
128
131
  onpointerleave={() => (active = null)}
129
132
  >
130
- {#if figureRows && display}
133
+ {#if resting && !active && series[0]}
134
+ <Figure
135
+ series={[{ value: resting.value, label: series[0].label, color: series[0].color }]}
136
+ eyebrow={resting.label}
137
+ format={formatValue}
138
+ size="md"
139
+ />
140
+ {:else if figureRows && display}
131
141
  <Figure series={figureRows} eyebrow={formatX(display[xKey])} format={formatValue} size="md" />
132
142
  {:else}
133
143
  <Figure value="—" eyebrow="Latest" size="md" />
@@ -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. */