@heyramzi/ui 2.3.0 → 2.5.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
@@ -76,7 +76,7 @@ Generic pieces built on the primitives, imported from `@heyramzi/ui/components/<
76
76
  - `section-card` — SectionCard, a card with a fixed-height header, an h3 title, optional actions and a line under it
77
77
  - `section-header` — SectionHeader, a section label with a count chip, a line of description and actions
78
78
  - `field-row` — FieldRow, a labelled control with its action button at the control's height
79
- - `table-view` — TableView, a read-only table from columns and rows: numeric columns, footer row, custom cell; opt-in: `bare`, sortable heads (`column.sortable`, bind `sort`; the caller orders `rows`), `rowHref` (a real link over the row), `onRowClick`, `selectable` + bind `selected`, `rowMenu` (context-menu items), `group` + `groupHeader` + `collapsible`, `subRow` + `expanded`, `loading` (skeleton rows in the frame), `empty`, `trailing`, `head`, and per-column `hideBelow`, `sticky`, `interactive`, `class`
79
+ - `table-view` — TableView, a read-only table from columns and rows: numeric columns, footer row, custom cell; opt-in: `bare`, sortable heads (`column.sortable`, bind `sort`; the caller orders `rows`), `rowHref` (a real link over the row; `onRowLink` opens in place on a plain click), `onRowClick`, `selectable` + bind `selected`, `rowMenu` (context-menu items), `group` (a `null` key sits with no header) + `groupHeader` + `collapsible`, `subRow` + `expanded`, `loading` (skeleton rows in the frame), `empty`, `trailing`, `head`, and per-column `hideBelow`, `sticky`, `interactive`, `class`
80
80
  - `segmented` — AnimatedTabs (pill or underline), SegmentedFilter, SegmentedNav, SectionRuns, one bordered track with a sliding pill, a badge note and an overflow menu
81
81
  - `theme` — ThemeProvider, mode, userPrefersMode, setMode, toggleMode, resetMode: the one light and dark system (mode-watcher, optional peer). Mount `<ThemeProvider initial onChange themeColors modeStorageKey />` once; flip with `toggleMode()`, read `mode.current`. Pair with `theme-toggle`'s `revealTheme(event, toggleMode)` for the wipe
82
82
  - `star-rating-input` — StarRatingInput
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyramzi/ui",
3
- "version": "2.3.0",
3
+ "version": "2.5.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" />
@@ -43,6 +43,8 @@
43
43
  sort?: SortState<Row>;
44
44
  /** Where a row goes. Draws a real link over the row, so middle-click and cmd-click work. */
45
45
  rowHref?: (row: Row) => string | undefined;
46
+ /** Runs on a plain left click of the row link. Call `event.preventDefault()` to open in place; a modified click still follows `href`. */
47
+ onRowLink?: (row: Row, event: MouseEvent) => void;
46
48
  /** The link's accessible name. Defaults to the row's first column text. */
47
49
  rowLabel?: (row: Row) => string;
48
50
  /** Adds a checkbox column with a select-all in the head. */
@@ -53,8 +55,8 @@
53
55
  selectRowLabel?: (row: Row) => string;
54
56
  /** The items of a right-click menu for one row: `ContextMenu.Item`s. */
55
57
  rowMenu?: Snippet<[Row]>;
56
- /** Rows that return the same key sit under one header row. Order `rows` so a group is contiguous. */
57
- group?: (row: Row) => string;
58
+ /** Rows that return the same key sit under one header row, and `null` rows sit with no header. Order `rows` so a group is contiguous. */
59
+ group?: (row: Row) => string | null;
58
60
  /** Draws a group's header. Prints the key when left out. */
59
61
  groupHeader?: Snippet<[TableGroup<Row>]>;
60
62
  /** Makes each group header a button that folds its rows. */
@@ -94,6 +96,7 @@
94
96
  class: className,
95
97
  sort = $bindable(null),
96
98
  rowHref,
99
+ onRowLink,
97
100
  rowLabel,
98
101
  selectable = false,
99
102
  selected = $bindable(new Set<string>()),
@@ -127,15 +130,16 @@
127
130
  align: column.numeric ? ("right" as const) : ("left" as const),
128
131
  })),
129
132
  ]);
130
- let entries = $derived.by<{ group: TableGroup<Row> }[]>(() => {
133
+ type Run = { id: string; key: string | null; rows: Row[] };
134
+ let entries = $derived.by<Run[]>(() => {
131
135
  if (!group) return [];
132
- const out: { group: TableGroup<Row> }[] = [];
133
- let current: TableGroup<Row> | undefined;
136
+ const out: Run[] = [];
137
+ let current: Run | undefined;
134
138
  for (const row of rows) {
135
139
  const groupKey = group(row);
136
140
  if (!current || current.key !== groupKey) {
137
- current = { key: groupKey, rows: [] };
138
- out.push({ group: current });
141
+ current = { id: groupKey ?? `\u0000${out.length}`, key: groupKey, rows: [] };
142
+ out.push(current);
139
143
  }
140
144
  current.rows.push(row);
141
145
  }
@@ -204,6 +208,12 @@
204
208
  onRowClick?.(row);
205
209
  }
206
210
 
211
+ function linkClick(event: MouseEvent, row: Row) {
212
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
213
+ return;
214
+ onRowLink?.(row, event);
215
+ }
216
+
207
217
  function linkName(row: Row): string {
208
218
  return rowLabel?.(row) ?? (columns[0] ? textOf(columns[0], row) : label);
209
219
  }
@@ -288,6 +298,7 @@
288
298
  <a
289
299
  {href}
290
300
  aria-label={linkName(row)}
301
+ onclick={onRowLink ? (event: MouseEvent) => linkClick(event, row) : undefined}
291
302
  class="absolute inset-0 z-0 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
292
303
  ></a>
293
304
  </td>
@@ -370,40 +381,45 @@
370
381
  <TableSkeleton rows={skeletonRows} columns={skeletonColumns} />
371
382
  {:else}
372
383
  {#if group}
373
- {#each entries as entry (entry.group.key)}
374
- {@const folded = collapsible && collapsed.has(entry.group.key)}
384
+ {#each entries as entry (entry.id)}
385
+ {@const folded = entry.key !== null && collapsible && collapsed.has(entry.key)}
386
+ {@const tableGroup = { key: entry.key ?? "", rows: entry.rows }}
375
387
  <Table.Body>
376
- <Table.Row class="bg-muted/40 hover:bg-muted/40 hover:translate-y-0 hover:shadow-none">
377
- <Table.Head
378
- colspan={span + (rowHref ? 1 : 0)}
379
- scope="colgroup"
380
- class="text-foreground h-10 p-0"
388
+ {#if entry.key !== null}
389
+ <Table.Row
390
+ class="bg-muted/40 hover:bg-muted/40 hover:translate-y-0 hover:shadow-none"
381
391
  >
382
- {#if collapsible}
383
- <button
384
- type="button"
385
- aria-expanded={!folded}
386
- class="t-press focus-visible:ring-ring flex h-10 w-full items-center gap-2 px-2 text-start outline-none focus-visible:ring-2 focus-visible:ring-inset"
387
- onclick={() => toggleGroup(entry.group.key)}
388
- >
389
- <CaretDown
390
- class={cn(
391
- "size-3.5 shrink-0 transition-transform duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] motion-reduce:transition-none",
392
- folded && "-rotate-90",
393
- )}
394
- aria-hidden="true"
395
- />
396
- {#if groupHeader}{@render groupHeader(entry.group)}{:else}{entry.group.key}{/if}
397
- </button>
398
- {:else}
399
- <span class="flex h-10 items-center px-2">
400
- {#if groupHeader}{@render groupHeader(entry.group)}{:else}{entry.group.key}{/if}
401
- </span>
402
- {/if}
403
- </Table.Head>
404
- </Table.Row>
392
+ <Table.Head
393
+ colspan={span + (rowHref ? 1 : 0)}
394
+ scope="colgroup"
395
+ class="text-foreground h-10 p-0"
396
+ >
397
+ {#if collapsible}
398
+ <button
399
+ type="button"
400
+ aria-expanded={!folded}
401
+ class="t-press focus-visible:ring-ring flex h-10 w-full items-center gap-2 px-2 text-start outline-none focus-visible:ring-2 focus-visible:ring-inset"
402
+ onclick={() => toggleGroup(tableGroup.key)}
403
+ >
404
+ <CaretDown
405
+ class={cn(
406
+ "size-3.5 shrink-0 transition-transform duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] motion-reduce:transition-none",
407
+ folded && "-rotate-90",
408
+ )}
409
+ aria-hidden="true"
410
+ />
411
+ {#if groupHeader}{@render groupHeader(tableGroup)}{:else}{tableGroup.key}{/if}
412
+ </button>
413
+ {:else}
414
+ <span class="flex h-10 items-center px-2">
415
+ {#if groupHeader}{@render groupHeader(tableGroup)}{:else}{tableGroup.key}{/if}
416
+ </span>
417
+ {/if}
418
+ </Table.Head>
419
+ </Table.Row>
420
+ {/if}
405
421
  {#if !folded}
406
- {#each entry.group.rows as row (rowKey(row))}
422
+ {#each entry.rows as row (rowKey(row))}
407
423
  {@render rowOrMenu(row)}
408
424
  {/each}
409
425
  {/if}