@heyramzi/ui 2.15.0 → 2.16.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
@@ -136,6 +136,7 @@ Generic pieces built on the primitives, imported from `@heyramzi/ui/components/<
136
136
  ## Record (`src/record`)
137
137
 
138
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
+ - `record-rail` — RecordRail, RecordRailTab. A record's side rail split into tabs (fields in one, a comment thread in the next), each with an optional count. A single tab draws no strip
139
140
 
140
141
  ---
141
142
 
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Motion tokens for code that needs a number: a Svelte `flip`, `slide` or `fade`, a timer. The
3
+ * CSS in `tokens/craft.css` owns the values; these mirror it for the server render and for a
4
+ * page that never loaded the tokens.
5
+ */
6
+ export const DURATION_MS = {
7
+ quick: 150,
8
+ fast: 250,
9
+ slow: 300
10
+ };
11
+ /** A duration token in ms, read off a mounted element; both `ms` and `s` parse. */
12
+ export function tokenMs(el, name, fallback) {
13
+ const raw = getComputedStyle(el).getPropertyValue(name).trim();
14
+ const n = Number.parseFloat(raw);
15
+ if (Number.isNaN(n)) return fallback;
16
+ return raw.endsWith("ms") ? n : n * 1e3;
17
+ }
18
+ /** `--duration-<token>` in ms off the root, so a token edit retimes the JS motion too. */
19
+ export function durationMs(token) {
20
+ if (typeof document === "undefined") return DURATION_MS[token];
21
+ return tokenMs(document.documentElement, `--duration-${token}`, DURATION_MS[token]);
22
+ }
@@ -8,6 +8,7 @@ export { default as RecordTimeline } from "../../src/record/record-timeline.svel
8
8
  export { default as ProjectList } from "../../src/record/project-list.svelte";
9
9
  export { default as FieldSection } from "../../src/record/field-section.svelte";
10
10
  export { default as InfoList } from "../../src/record/info-list.svelte";
11
+ export { default as RecordRail } from "../../src/record/record-rail.svelte";
11
12
  export { DEFAULT_RECORD_FILTERS, DEFAULT_RECORD_KINDS } from "./kinds.js";
12
13
  export { DEFAULT_RECORD_LABELS, resolveLabels } from "./types.js";
13
14
  export { activeProjects, dayRelation, eventsForFilter, filterCounts, foldGroups, groupEventsByDay, groupSteps, latestWhereWeAre, localDay, mergeClientEvents, nextAcrossProjects, nudgeStep, reorderIds, rollupProjects, stepMarkers, stepProgress } from "./logic.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyramzi/ui",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "Svelte 5 UI for apps: primitives, effects and animations, plus work (tasks and docs), charts, slides, an editor, onboarding and a canvas as subpaths.",
5
5
  "homepage": "https://ui.heyramzi.com",
6
6
  "license": "MIT",
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Motion tokens for code that needs a number: a Svelte `flip`, `slide` or `fade`, a timer. The
3
+ * CSS in `tokens/craft.css` owns the values; these mirror it for the server render and for a
4
+ * page that never loaded the tokens.
5
+ */
6
+ export const DURATION_MS = {
7
+ quick: 150,
8
+ fast: 250,
9
+ slow: 300,
10
+ } as const;
11
+
12
+ export type DurationToken = keyof typeof DURATION_MS;
13
+
14
+ /** A duration token in ms, read off a mounted element; both `ms` and `s` parse. */
15
+ export function tokenMs(el: Element, name: string, fallback: number): number {
16
+ const raw = getComputedStyle(el).getPropertyValue(name).trim();
17
+ const n = Number.parseFloat(raw);
18
+ if (Number.isNaN(n)) return fallback;
19
+ return raw.endsWith("ms") ? n : n * 1000;
20
+ }
21
+
22
+ /** `--duration-<token>` in ms off the root, so a token edit retimes the JS motion too. */
23
+ export function durationMs(token: DurationToken): number {
24
+ if (typeof document === "undefined") return DURATION_MS[token];
25
+ return tokenMs(document.documentElement, `--duration-${token}`, DURATION_MS[token]);
26
+ }
@@ -8,6 +8,8 @@ export { default as RecordTimeline } from "./record-timeline.svelte";
8
8
  export { default as ProjectList } from "./project-list.svelte";
9
9
  export { default as FieldSection } from "./field-section.svelte";
10
10
  export { default as InfoList } from "./info-list.svelte";
11
+ export { default as RecordRail } from "./record-rail.svelte";
12
+ export type { RecordRailTab } from "./record-rail.svelte";
11
13
 
12
14
  export { DEFAULT_RECORD_FILTERS, DEFAULT_RECORD_KINDS } from "./kinds.js";
13
15
  export { DEFAULT_RECORD_LABELS, resolveLabels } from "./types.js";
@@ -0,0 +1,74 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import type { Snippet } from "svelte";
6
+ import { cn } from "../lib/utils.js";
7
+ import * as Tabs from "../shadcn/tabs/index.js";
8
+
9
+ // =========================
10
+ // TYPES
11
+ // =========================
12
+ export interface RecordRailTab {
13
+ id: string;
14
+ label: string;
15
+ /** Drawn beside the label, such as the number of comments. Hidden at 0 or when left out. */
16
+ count?: number;
17
+ content: Snippet;
18
+ }
19
+
20
+ interface Props {
21
+ tabs: RecordRailTab[];
22
+ /** The open tab's id. Defaults to the first tab. */
23
+ value?: string;
24
+ /** Names the tab list for a screen reader, such as "Task sidebar". */
25
+ label: string;
26
+ class?: string;
27
+ /** Classes for the strip that holds the tab list. */
28
+ listClass?: string;
29
+ /** Classes for each tab's panel. */
30
+ contentClass?: string;
31
+ }
32
+
33
+ // =========================
34
+ // PROPS
35
+ // =========================
36
+ let {
37
+ tabs,
38
+ value = $bindable(),
39
+ label,
40
+ class: className,
41
+ listClass,
42
+ contentClass,
43
+ }: Props = $props();
44
+
45
+ // =========================
46
+ // DERIVED
47
+ // =========================
48
+ // WHY a fallback and not a write: the caller may hand over a new tab set where the open id is gone.
49
+ let open = $derived(tabs.some((tab) => tab.id === value) ? value : tabs[0]?.id);
50
+ </script>
51
+
52
+ <!-- A record's side rail with tabs: the fields in one, a thread or a log in the next,
53
+ so the thread sits beside the record and not a screen below it. One tab draws no strip. -->
54
+ <Tabs.Root value={open} onValueChange={(next) => (value = next)} class={cn("min-w-0 gap-0", className)}>
55
+ {#if tabs.length > 1}
56
+ <div class={cn("pb-3", listClass)}>
57
+ <Tabs.List size="sm" class="w-full" aria-label={label}>
58
+ {#each tabs as tab (tab.id)}
59
+ <Tabs.Trigger value={tab.id}>
60
+ {tab.label}
61
+ {#if tab.count}
62
+ <span class="text-xs tabular-nums text-muted-foreground">{tab.count}</span>
63
+ {/if}
64
+ </Tabs.Trigger>
65
+ {/each}
66
+ </Tabs.List>
67
+ </div>
68
+ {/if}
69
+ {#each tabs as tab (tab.id)}
70
+ <Tabs.Content value={tab.id} class={cn("min-w-0", contentClass)}>
71
+ {@render tab.content()}
72
+ </Tabs.Content>
73
+ {/each}
74
+ </Tabs.Root>
@@ -366,8 +366,11 @@
366
366
  opacity: 1;
367
367
  }
368
368
 
369
- /* clip, not hidden: the margin lets a lifted card's shadow out while the fold stays shut. */
369
+ /* clip, not hidden: the margin lets a lifted card's shadow out while the fold stays shut.
370
+ relative: a card's sr-only labels are absolute, and clip only cuts what this box contains.
371
+ Without it they hang below a shut fold and the page scrolls past its last lane (TSK-1617). */
370
372
  .kanban-fold-inner {
373
+ position: relative;
371
374
  display: flex;
372
375
  flex-direction: column;
373
376
  gap: 0.375rem;