@heyramzi/ui 2.22.1 → 2.24.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.
Files changed (163) hide show
  1. package/COMPONENTS.md +3 -6
  2. package/dist/ai/confirmation/index.js +3 -6
  3. package/dist/ai/index.js +1 -5
  4. package/dist/canvas/editor/rebase.js +26 -2
  5. package/dist/charts/dashboard/index.js +1 -3
  6. package/dist/charts/primitives/chart-utils.js +14 -0
  7. package/dist/components/chip/index.js +1 -0
  8. package/dist/components/language-flag/flags.js +5 -0
  9. package/dist/components/language-flag/index.js +1 -0
  10. package/dist/components/list-page/index.js +1 -3
  11. package/dist/components/work-parts/index.js +1 -1
  12. package/dist/editor/doc-blocks.js +2 -1
  13. package/dist/editor/safe-url.js +26 -0
  14. package/dist/editor/serializer.js +10 -4
  15. package/dist/editor/slash-query.js +10 -0
  16. package/dist/effects/animated-beam/path-calculator.svelte.js +13 -6
  17. package/dist/lib/dates.js +41 -0
  18. package/dist/lib/panel.js +2 -2
  19. package/dist/lib/tone.js +13 -0
  20. package/dist/record/format.js +2 -18
  21. package/dist/record/logic.js +3 -6
  22. package/dist/status/index.js +1 -1
  23. package/dist/triage/index.js +4 -0
  24. package/dist/triage/types.js +12 -0
  25. package/dist/work/dates.js +2 -3
  26. package/dist/work/detail/detail-types.js +0 -8
  27. package/dist/work/docs/edited.js +8 -7
  28. package/dist/work/inbox/inbox-logic.js +5 -20
  29. package/dist/work/index.js +0 -2
  30. package/dist/work/time/duration.js +2 -3
  31. package/package.json +7 -21
  32. package/scripts/sync-shadcn.mjs +4 -6
  33. package/src/ai/confirmation/Confirmation.svelte +2 -2
  34. package/src/ai/confirmation/{ConfirmationRequest.svelte → ConfirmationState.svelte} +6 -3
  35. package/src/ai/confirmation/confirmation-context.svelte.ts +2 -2
  36. package/src/ai/confirmation/confirmation-parts.test.svelte +17 -0
  37. package/src/ai/confirmation/confirmation-parts.test.ts +23 -0
  38. package/src/ai/confirmation/index.ts +6 -21
  39. package/src/ai/index.ts +3 -13
  40. package/src/ai/typing-dots/TypingDots.svelte +32 -12
  41. package/src/ai/typing-dots/typing-dots.test.ts +36 -0
  42. package/src/animations/modal.css +4 -1
  43. package/src/canvas/editor/rebase.test.ts +37 -0
  44. package/src/canvas/editor/rebase.ts +33 -1
  45. package/src/canvas/ui/canvas-ui.css +44 -1
  46. package/src/charts/README.md +2 -2
  47. package/src/charts/area-chart.svelte +5 -12
  48. package/src/charts/bar-chart.svelte +6 -14
  49. package/src/charts/chart-tokens.css +19 -0
  50. package/src/charts/dashboard/data-table.svelte +21 -21
  51. package/src/charts/dashboard/index.ts +1 -3
  52. package/src/charts/dashboard/rating-chip.svelte +9 -11
  53. package/src/charts/dashboard/stat-tile.svelte +4 -5
  54. package/src/charts/donut-chart.svelte +1 -1
  55. package/src/charts/gauge-chart.svelte +1 -1
  56. package/src/charts/line-chart.svelte +5 -12
  57. package/src/charts/number-chart.svelte +1 -1
  58. package/src/charts/primitives/chart-utils.test.ts +17 -0
  59. package/src/charts/primitives/chart-utils.ts +16 -0
  60. package/src/charts/ring-chart.svelte +2 -2
  61. package/src/charts/sparkline.svelte +1 -1
  62. package/src/charts/stat-card.svelte +1 -1
  63. package/src/charts/time-series-chart.svelte +9 -5
  64. package/src/components/chip/count-chip.svelte +35 -0
  65. package/src/components/chip/index.ts +1 -0
  66. package/src/components/data-table/data-table.ts +1 -1
  67. package/src/components/data-table/row-context-menu.svelte +3 -4
  68. package/src/components/data-table/sortable-head.svelte +45 -10
  69. package/src/components/language-flag/flags.ts +5 -0
  70. package/src/components/language-flag/index.ts +1 -0
  71. package/src/components/language-flag/language-flag.svelte +3 -11
  72. package/src/components/list-page/empty-state.svelte +90 -12
  73. package/src/components/list-page/filter-popover.svelte +2 -5
  74. package/src/components/list-page/grouped-list.svelte +2 -4
  75. package/src/components/list-page/index.ts +1 -3
  76. package/src/components/section-header/section-header.svelte +2 -6
  77. package/src/components/segmented/segment.svelte +13 -19
  78. package/src/components/segmented/segmented-filter.svelte +1 -2
  79. package/src/components/table-view/table-view.svelte +31 -45
  80. package/src/components/table-view/table-view.test.ts +6 -3
  81. package/src/components/work-parts/group-head.svelte +1 -1
  82. package/src/components/work-parts/index.ts +1 -1
  83. package/src/editor/doc-blocks.ts +2 -1
  84. package/src/editor/editor.svelte +7 -5
  85. package/src/editor/safe-url.ts +32 -0
  86. package/src/editor/serializer.test.ts +58 -0
  87. package/src/editor/serializer.ts +9 -6
  88. package/src/editor/slash-query.test.ts +19 -0
  89. package/src/editor/slash-query.ts +11 -0
  90. package/src/effects/animated-beam/path-calculator.svelte.ts +19 -5
  91. package/src/effects/marquee/Marquee.css +6 -3
  92. package/src/effects/marquee/Marquee.svelte +6 -2
  93. package/src/inbox/thread-inbox.svelte +6 -4
  94. package/src/lib/dates.test.ts +63 -0
  95. package/src/lib/dates.ts +63 -0
  96. package/src/lib/panel.svelte +116 -0
  97. package/src/lib/panel.ts +2 -2
  98. package/src/lib/tone.ts +15 -0
  99. package/src/modules.test.ts +13 -1
  100. package/src/panel/side-panel.svelte +4 -4
  101. package/src/record/format.ts +3 -15
  102. package/src/record/logic.ts +2 -6
  103. package/src/record/record-stats.svelte +4 -3
  104. package/src/record/steps-rail.svelte +7 -4
  105. package/src/shadcn/alert-dialog/alert-dialog-overlay.svelte +1 -1
  106. package/src/shadcn/button/button.svelte +1 -1
  107. package/src/shadcn/button/button.test.ts +2 -2
  108. package/src/shadcn/command/command-dialog.svelte +5 -10
  109. package/src/shadcn/dialog/dialog-overlay.svelte +1 -1
  110. package/src/shadcn/input/input.svelte +2 -2
  111. package/src/shadcn/sheet/sheet-overlay.svelte +1 -1
  112. package/src/status/index.ts +1 -1
  113. package/src/status/status-pill.svelte +2 -8
  114. package/src/thread/thread.svelte +3 -6
  115. package/src/tokens/craft.css +30 -0
  116. package/src/triage/README.md +28 -0
  117. package/src/triage/index.ts +10 -0
  118. package/src/triage/triage-plan.svelte +132 -0
  119. package/src/triage/triage-queue.svelte +48 -0
  120. package/src/triage/triage-task.svelte +95 -0
  121. package/src/triage/types.ts +35 -0
  122. package/src/work/README.md +0 -1
  123. package/src/work/dates.ts +3 -3
  124. package/src/work/demo.svelte +9 -1
  125. package/src/work/detail/detail-demo.svelte +30 -27
  126. package/src/work/detail/detail-types.ts +1 -29
  127. package/src/work/detail/task-detail.svelte +18 -17
  128. package/src/work/docs/doc-menu.svelte +2 -1
  129. package/src/work/docs/edited.ts +4 -7
  130. package/src/work/inbox/inbox-card.svelte +4 -9
  131. package/src/work/inbox/inbox-logic.ts +4 -16
  132. package/src/work/index.ts +1 -3
  133. package/src/work/kanban-column.svelte +5 -3
  134. package/src/work/time/duration.ts +2 -3
  135. package/src/work/time/time-chip.svelte +1 -3
  136. package/dist/ai/thinking-dots/index.js +0 -3
  137. package/dist/components/icon/index.js +0 -1
  138. package/dist/components/locale-switch/index.js +0 -1
  139. package/dist/icons/trades.js +0 -49
  140. package/dist/status/pill-tint.js +0 -13
  141. package/dist/work/presets/sales.js +0 -39
  142. package/src/ai/confirmation/ConfirmationAccepted.svelte +0 -23
  143. package/src/ai/confirmation/ConfirmationFailed.svelte +0 -23
  144. package/src/ai/confirmation/ConfirmationRejected.svelte +0 -23
  145. package/src/ai/thinking-dots/ThinkingDots.svelte +0 -42
  146. package/src/ai/thinking-dots/index.ts +0 -5
  147. package/src/charts/dashboard/dashboard-empty.svelte +0 -55
  148. package/src/charts/dashboard/data-panel.svelte +0 -81
  149. package/src/charts/dashboard/data-sort-head.svelte +0 -102
  150. package/src/components/icon/icon.svelte +0 -23
  151. package/src/components/icon/index.ts +0 -1
  152. package/src/components/list-page/detail-empty.svelte +0 -45
  153. package/src/components/list-page/detail-panel.svelte +0 -82
  154. package/src/components/list-page/rail-empty.svelte +0 -44
  155. package/src/components/locale-switch/index.ts +0 -1
  156. package/src/components/locale-switch/locale-switch.svelte +0 -59
  157. package/src/components/work-parts/count-chip.svelte +0 -20
  158. package/src/icons/TradeIcon.svelte +0 -16
  159. package/src/icons/trades.ts +0 -89
  160. package/src/status/pill-tint.ts +0 -16
  161. package/src/work/detail/task-property.svelte +0 -17
  162. package/src/work/detail/task-thread.svelte +0 -221
  163. package/src/work/presets/sales.ts +0 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyramzi/ui",
3
- "version": "2.22.1",
3
+ "version": "2.24.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",
@@ -126,16 +126,6 @@
126
126
  "node": "./dist/icons/sf-symbols.js",
127
127
  "default": "./src/icons/sf-symbols.ts"
128
128
  },
129
- "./icons/trades": {
130
- "svelte": "./src/icons/trades.ts",
131
- "types": "./src/icons/trades.ts",
132
- "node": "./dist/icons/trades.js",
133
- "default": "./src/icons/trades.ts"
134
- },
135
- "./icons/TradeIcon": {
136
- "svelte": "./src/icons/TradeIcon.svelte",
137
- "default": "./src/icons/TradeIcon.svelte"
138
- },
139
129
  "./design": {
140
130
  "style": "./src/design/index.css",
141
131
  "default": "./src/design/index.css"
@@ -225,6 +215,12 @@
225
215
  "node": "./dist/inbox/index.js",
226
216
  "default": "./src/inbox/index.ts"
227
217
  },
218
+ "./triage": {
219
+ "svelte": "./src/triage/index.ts",
220
+ "types": "./src/triage/index.ts",
221
+ "node": "./dist/triage/index.js",
222
+ "default": "./src/triage/index.ts"
223
+ },
228
224
  "./work": {
229
225
  "svelte": "./src/work/index.ts",
230
226
  "types": "./src/work/index.ts",
@@ -257,12 +253,6 @@
257
253
  "node": "./dist/work/inbox/index.js",
258
254
  "default": "./src/work/inbox/index.ts"
259
255
  },
260
- "./work/presets/sales": {
261
- "svelte": "./src/work/presets/sales.ts",
262
- "types": "./src/work/presets/sales.ts",
263
- "node": "./dist/work/presets/sales.js",
264
- "default": "./src/work/presets/sales.ts"
265
- },
266
256
  "./work/detail": {
267
257
  "svelte": "./src/work/detail/task-detail.svelte",
268
258
  "types": "./src/work/detail/task-detail.svelte",
@@ -430,7 +420,6 @@
430
420
  },
431
421
  "peerDependencies": {
432
422
  "@internationalized/date": "^3.12.4",
433
- "@tabler/icons-svelte-runes": "^3.46.0",
434
423
  "@xyflow/svelte": "^1.7.0",
435
424
  "bits-ui": "^2.19.5",
436
425
  "mode-watcher": "^1.1.0",
@@ -447,9 +436,6 @@
447
436
  "@internationalized/date": {
448
437
  "optional": true
449
438
  },
450
- "@tabler/icons-svelte-runes": {
451
- "optional": true
452
- },
453
439
  "@xyflow/svelte": {
454
440
  "optional": true
455
441
  }
@@ -141,24 +141,22 @@ const publicImports = (demo) =>
141
141
  .join("\n");
142
142
 
143
143
  /* The section's list, generated: a primitive reaches the page by being synced,
144
- * never by being typed into an array. */
144
+ * never by being typed into an array. Names and words only: the site's navigation reads this
145
+ * list on every page, and each demo loads from `./<name>.svelte` on its own page. */
145
146
  async function writeManifest(demos) {
146
147
  const rows = demos.map(
147
148
  (d) =>
148
- ` { name: ${JSON.stringify(d.name)}, title: ${JSON.stringify(d.title)}, note: ${JSON.stringify(d.note)}, path: ${JSON.stringify(d.imports)}, Comp: ${pascal(d.name)}Demo },`,
149
+ ` { name: ${JSON.stringify(d.name)}, title: ${JSON.stringify(d.title)}, note: ${JSON.stringify(d.note)}, path: ${JSON.stringify(d.imports)} },`,
149
150
  );
150
151
  const text = [
151
152
  "/* Generated by scripts/sync-shadcn.mjs. Do not edit: the demos are upstream's",
152
- " * own, and the sync rewrites this list. */",
153
- 'import type { Component } from "svelte";',
154
- ...demos.map((d) => `import ${pascal(d.name)}Demo from "./${d.name}.svelte";`),
153
+ " * own, and the sync rewrites this list. Each demo loads from `./<name>.svelte`. */",
155
154
  "",
156
155
  "export type ShadcnDemo = {",
157
156
  " name: string;",
158
157
  " title: string;",
159
158
  " note: string;",
160
159
  " path: string;",
161
- " Comp: Component;",
162
160
  "};",
163
161
  "",
164
162
  "export const SHADCN_DEMOS: ShadcnDemo[] = [",
@@ -1,10 +1,10 @@
1
1
  <script lang="ts" module>
2
2
  import type { Snippet } from "svelte";
3
3
  import type { HTMLAttributes } from "svelte/elements";
4
- import type { ConfirmationState } from "./confirmation-context.svelte.js";
4
+ import type { ConfirmationKind } from "./confirmation-context.svelte.js";
5
5
 
6
6
  export interface ConfirmationProps extends HTMLAttributes<HTMLElement> {
7
- kind: ConfirmationState;
7
+ kind: ConfirmationKind;
8
8
  executing?: boolean;
9
9
  class?: string;
10
10
  children?: Snippet;
@@ -1,7 +1,10 @@
1
1
  <script lang="ts" module>
2
2
  import type { Snippet } from "svelte";
3
+ import type { ConfirmationKind } from "./confirmation-context.svelte.js";
3
4
 
4
- export interface ConfirmationRequestProps {
5
+ export interface ConfirmationStateProps {
6
+ /** The state whose content this shows. */
7
+ when: Exclude<ConfirmationKind, "idle">;
5
8
  children?: Snippet;
6
9
  }
7
10
  </script>
@@ -9,12 +12,12 @@
9
12
  <script lang="ts">
10
13
  import { getConfirmationContext } from "./confirmation-context.svelte.js";
11
14
 
12
- let { children }: ConfirmationRequestProps = $props();
15
+ let { when, children }: ConfirmationStateProps = $props();
13
16
 
14
17
  const ctx = getConfirmationContext();
15
18
  </script>
16
19
 
17
- {#if ctx.kind === "requested"}
20
+ {#if ctx.kind === when}
18
21
  <!-- WHY: the wrapper mirrors the parent section's stack so the state swap can
19
22
  animate as one block without changing the children's layout. -->
20
23
  <div
@@ -2,10 +2,10 @@ import { getContext, setContext } from "svelte";
2
2
 
3
3
  const CONFIRMATION_CONTEXT_KEY = Symbol("confirmation-context");
4
4
 
5
- export type ConfirmationState = "idle" | "requested" | "accepted" | "rejected" | "failed";
5
+ export type ConfirmationKind = "idle" | "requested" | "accepted" | "rejected" | "failed";
6
6
 
7
7
  export interface ConfirmationContextValue {
8
- readonly kind: ConfirmationState;
8
+ readonly kind: ConfirmationKind;
9
9
  readonly executing: boolean;
10
10
  }
11
11
 
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ // Every state part under one Confirmation, for the state-swap test.
3
+ import {
4
+ Confirmation,
5
+ ConfirmationState,
6
+ type ConfirmationKind,
7
+ } from "./index.js";
8
+
9
+ let { kind }: { kind: ConfirmationKind } = $props();
10
+ </script>
11
+
12
+ <Confirmation {kind}>
13
+ <ConfirmationState when="requested">request</ConfirmationState>
14
+ <ConfirmationState when="accepted">accepted</ConfirmationState>
15
+ <ConfirmationState when="rejected">rejected</ConfirmationState>
16
+ <ConfirmationState when="failed">failed</ConfirmationState>
17
+ </Confirmation>
@@ -0,0 +1,23 @@
1
+ import { flushSync, mount, unmount } from "svelte";
2
+ import { describe, expect, it } from "vite-plus/test";
3
+ import type { ConfirmationKind } from "./index.js";
4
+ import Parts from "./confirmation-parts.test.svelte";
5
+
6
+ function shown(kind: ConfirmationKind): string {
7
+ const target = document.createElement("div");
8
+ const app = mount(Parts, { target, props: { kind } });
9
+ flushSync();
10
+ const text = target.textContent?.replace(/\s+/g, " ").trim() ?? "";
11
+ void unmount(app);
12
+ return text;
13
+ }
14
+
15
+ describe("ConfirmationState", () => {
16
+ it("shows only the part whose `when` matches the kind", () => {
17
+ expect(shown("requested")).toBe("request");
18
+ expect(shown("accepted")).toBe("accepted");
19
+ expect(shown("rejected")).toBe("rejected");
20
+ expect(shown("failed")).toBe("failed");
21
+ expect(shown("idle")).toBe("");
22
+ });
23
+ });
@@ -1,50 +1,35 @@
1
1
  import Confirmation from "./Confirmation.svelte";
2
2
  import ConfirmationTitle from "./ConfirmationTitle.svelte";
3
- import ConfirmationRequest from "./ConfirmationRequest.svelte";
4
- import ConfirmationAccepted from "./ConfirmationAccepted.svelte";
5
- import ConfirmationRejected from "./ConfirmationRejected.svelte";
6
- import ConfirmationFailed from "./ConfirmationFailed.svelte";
3
+ import ConfirmationState from "./ConfirmationState.svelte";
7
4
  import ConfirmationActions from "./ConfirmationActions.svelte";
8
5
  import ConfirmationAction from "./ConfirmationAction.svelte";
9
6
 
10
7
  import type { ConfirmationProps } from "./Confirmation.svelte";
11
8
  import type { ConfirmationTitleProps } from "./ConfirmationTitle.svelte";
12
- import type { ConfirmationRequestProps } from "./ConfirmationRequest.svelte";
13
- import type { ConfirmationAcceptedProps } from "./ConfirmationAccepted.svelte";
14
- import type { ConfirmationRejectedProps } from "./ConfirmationRejected.svelte";
15
- import type { ConfirmationFailedProps } from "./ConfirmationFailed.svelte";
9
+ import type { ConfirmationStateProps } from "./ConfirmationState.svelte";
16
10
  import type { ConfirmationActionsProps } from "./ConfirmationActions.svelte";
17
11
  import type { ConfirmationActionProps } from "./ConfirmationAction.svelte";
18
12
 
19
13
  export type {
20
14
  ConfirmationContextValue,
21
- ConfirmationState,
15
+ ConfirmationKind,
22
16
  } from "./confirmation-context.svelte.js";
23
17
 
24
18
  export {
25
19
  Confirmation,
26
20
  ConfirmationTitle,
27
- ConfirmationRequest,
28
- ConfirmationAccepted,
29
- ConfirmationRejected,
30
- ConfirmationFailed,
21
+ ConfirmationState,
31
22
  ConfirmationActions,
32
23
  ConfirmationAction,
33
24
  type ConfirmationProps,
34
25
  type ConfirmationTitleProps,
35
- type ConfirmationRequestProps,
36
- type ConfirmationAcceptedProps,
37
- type ConfirmationRejectedProps,
38
- type ConfirmationFailedProps,
26
+ type ConfirmationStateProps,
39
27
  type ConfirmationActionsProps,
40
28
  type ConfirmationActionProps,
41
29
  //
42
30
  Confirmation as Root,
43
31
  ConfirmationTitle as Title,
44
- ConfirmationRequest as Request,
45
- ConfirmationAccepted as Accepted,
46
- ConfirmationRejected as Rejected,
47
- ConfirmationFailed as Failed,
32
+ ConfirmationState as State,
48
33
  ConfirmationActions as Actions,
49
34
  ConfirmationAction as Action,
50
35
  };
package/src/ai/index.ts CHANGED
@@ -46,28 +46,18 @@ export {
46
46
  export {
47
47
  Confirmation,
48
48
  ConfirmationTitle,
49
- ConfirmationRequest,
50
- ConfirmationAccepted,
51
- ConfirmationRejected,
49
+ ConfirmationState,
52
50
  ConfirmationActions,
53
51
  ConfirmationAction,
54
52
  type ConfirmationProps,
55
53
  type ConfirmationTitleProps,
56
- type ConfirmationRequestProps,
57
- type ConfirmationAcceptedProps,
58
- type ConfirmationRejectedProps,
54
+ type ConfirmationStateProps,
59
55
  type ConfirmationActionsProps,
60
56
  type ConfirmationActionProps,
61
57
  type ConfirmationContextValue,
62
- type ConfirmationState,
58
+ type ConfirmationKind,
63
59
  } from "./confirmation/index.js";
64
60
 
65
- // =========================
66
- // THINKING DOTS
67
- // =========================
68
-
69
- export { ThinkingDots, type ThinkingDotsProps } from "./thinking-dots/index.js";
70
-
71
61
  // =========================
72
62
  // VOICE WAVEFORM
73
63
  // =========================
@@ -1,9 +1,11 @@
1
1
  <script lang="ts" module>
2
2
  import type { HTMLAttributes } from "svelte/elements";
3
3
 
4
- export interface TypingDotsProps extends HTMLAttributes<HTMLSpanElement> {
4
+ export interface TypingDotsProps extends HTMLAttributes<HTMLElement> {
5
5
  /** Dot diameter in px. */
6
6
  size?: number;
7
+ /** Set, even to "", the dots become a `role="status"` line; the text shows when non-empty. */
8
+ label?: string;
7
9
  class?: string;
8
10
  }
9
11
  </script>
@@ -12,16 +14,34 @@
12
14
  import "./TypingDots.css";
13
15
  import { cn } from "../../lib/utils.js";
14
16
 
15
- let { size = 4, class: className, ...rest }: TypingDotsProps = $props();
17
+ let { size = 4, label, class: className, ...rest }: TypingDotsProps = $props();
16
18
  </script>
17
19
 
18
- <span
19
- class={cn("typing-dots", className)}
20
- style:--typing-dot-size="{size}px"
21
- aria-hidden="true"
22
- {...rest}
23
- >
24
- <span class="typing-dot"></span>
25
- <span class="typing-dot"></span>
26
- <span class="typing-dot"></span>
27
- </span>
20
+ {#snippet dots(extra?: HTMLAttributes<HTMLElement>, dotsClass?: string)}
21
+ <span
22
+ class={cn("typing-dots", dotsClass)}
23
+ style:--typing-dot-size="{size}px"
24
+ aria-hidden="true"
25
+ {...extra}
26
+ >
27
+ <span class="typing-dot"></span>
28
+ <span class="typing-dot"></span>
29
+ <span class="typing-dot"></span>
30
+ </span>
31
+ {/snippet}
32
+
33
+ {#if label === undefined}
34
+ {@render dots(rest, className)}
35
+ {:else}
36
+ <div
37
+ class={cn(
38
+ "inline-flex items-center gap-2 text-sm text-[color:var(--ai-thinking-fg,inherit)]",
39
+ className,
40
+ )}
41
+ role="status"
42
+ {...rest}
43
+ >
44
+ {#if label}<span>{label}</span>{/if}
45
+ {@render dots()}
46
+ </div>
47
+ {/if}
@@ -0,0 +1,36 @@
1
+ import { flushSync, mount, unmount } from "svelte";
2
+ import { describe, expect, it } from "vite-plus/test";
3
+ import TypingDots, { type TypingDotsProps } from "./TypingDots.svelte";
4
+
5
+ function render(props: TypingDotsProps): HTMLElement {
6
+ const target = document.createElement("div");
7
+ const app = mount(TypingDots, { target, props });
8
+ flushSync();
9
+ const root = target.firstElementChild as HTMLElement;
10
+ void unmount(app);
11
+ return root;
12
+ }
13
+
14
+ describe("TypingDots", () => {
15
+ it("is three hidden dots with no label", () => {
16
+ const root = render({});
17
+ expect(root.getAttribute("aria-hidden")).toBe("true");
18
+ expect(root.getAttribute("role")).toBeNull();
19
+ expect(root.querySelectorAll(".typing-dot")).toHaveLength(3);
20
+ });
21
+
22
+ it("becomes a status line with its text when labelled", () => {
23
+ const root = render({ label: "Thinking", class: "extra" });
24
+ expect(root.getAttribute("role")).toBe("status");
25
+ expect(root.textContent?.trim()).toBe("Thinking");
26
+ expect(root.classList.contains("extra")).toBe(true);
27
+ expect(root.querySelector(".typing-dots")?.getAttribute("aria-hidden")).toBe("true");
28
+ });
29
+
30
+ it("keeps the status role and the caller's aria-label for an empty label", () => {
31
+ const root = render({ label: "", "aria-label": "Thinking" });
32
+ expect(root.getAttribute("role")).toBe("status");
33
+ expect(root.getAttribute("aria-label")).toBe("Thinking");
34
+ expect(root.textContent?.trim()).toBe("");
35
+ });
36
+ });
@@ -7,7 +7,8 @@
7
7
  --modal-scale: var(--scale-large, 0.96);
8
8
  --modal-scale-close: var(--scale-large, 0.96);
9
9
  --modal-ease: var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
10
- --modal-backdrop-color: rgb(0 0 0 / 0.5);
10
+ --modal-backdrop-color: var(--scrim, rgb(0 0 0 / 0.32));
11
+ --modal-backdrop-blur: var(--scrim-blur, 2px);
11
12
  }
12
13
 
13
14
  /* === Upstream: class-based toggle === */
@@ -54,6 +55,8 @@
54
55
  /* WHY: pair .t-modal-backdrop with the modal so the overlay fades in sync. */
55
56
  .t-modal-backdrop {
56
57
  background: var(--modal-backdrop-color);
58
+ -webkit-backdrop-filter: blur(var(--modal-backdrop-blur));
59
+ backdrop-filter: blur(var(--modal-backdrop-blur));
57
60
  opacity: 0;
58
61
  transition: opacity var(--modal-open-dur) var(--modal-ease);
59
62
  }
@@ -62,6 +62,15 @@ describe("diffToOps", () => {
62
62
  const replayed = applyOps({ nodes: [], edges: [] }, diffToOps(base, current));
63
63
  expect(replayed.nodes).toHaveLength(2);
64
64
  });
65
+
66
+ it("replays the delete of a connected node", () => {
67
+ const base = doc([n("a"), n("b")], [{ id: "e1", source: "a", target: "b" }]);
68
+ const current = doc([n("a")]);
69
+
70
+ const replayed = applyOps({ nodes: base.nodes, edges: base.edges }, diffToOps(base, current));
71
+ expect(replayed.nodes).toHaveLength(1);
72
+ expect(replayed.edges).toHaveLength(0);
73
+ });
65
74
  });
66
75
 
67
76
  describe("rebaseDiagram", () => {
@@ -186,6 +195,34 @@ describe("rebaseDiagram", () => {
186
195
  const local = doc([n("a"), n("b", { data: { label: "edited" } })]);
187
196
  expect(rebaseDiagram(serverDoc, lastSaved, local)).toBeNull();
188
197
  });
198
+
199
+ describe("deletes", () => {
200
+ const connected = doc([n("a"), n("b")], [{ id: "e1", source: "a", target: "b" }]);
201
+
202
+ it("merges a local delete of a connected node", () => {
203
+ const server = doc([n("a"), n("b"), n("c")], [{ id: "e1", source: "a", target: "b" }]);
204
+ const local = doc([n("a")]);
205
+
206
+ const rebased = expectDefined(rebaseDiagram(server, connected, local), "rebased doc");
207
+ expect(rebased.nodes.map((node) => node.id)).toEqual(["a", "c"]);
208
+ expect(rebased.edges).toEqual([]);
209
+ });
210
+
211
+ it("merges when both sides deleted the same node", () => {
212
+ const server = doc([n("a"), n("c")]);
213
+ const local = doc([n("a")]);
214
+
215
+ const rebased = expectDefined(rebaseDiagram(server, connected, local), "rebased doc");
216
+ expect(rebased.nodes.map((node) => node.id)).toEqual(["a", "c"]);
217
+ });
218
+
219
+ it("merges when both sides deleted the same edge", () => {
220
+ const both = doc([n("a"), n("b")]);
221
+
222
+ const rebased = expectDefined(rebaseDiagram(both, connected, both), "rebased doc");
223
+ expect(rebased.edges).toHaveLength(0);
224
+ });
225
+ });
189
226
  });
190
227
 
191
228
  describe("a new edge keeps everything it was drawn with", () => {
@@ -164,8 +164,14 @@ function patchEdgeOps(before: DocEdge, edge: DocEdge): CanvasOp[] {
164
164
  function edgeOps(base: DiagramDoc, current: DiagramDoc): CanvasOp[] {
165
165
  const baseEdges = new Map(base.edges.map((e) => [e.id, e]));
166
166
  const currentIds = new Set(current.edges.map((e) => e.id));
167
+ const currentNodeIds = new Set(current.nodes.map((n) => n.id));
168
+ const removedNodeIds = new Set(
169
+ base.nodes.filter((n) => !currentNodeIds.has(n.id)).map((n) => n.id),
170
+ );
171
+ // WHY: removeNode already takes the node's edges, and removeEdge throws on a missing id.
167
172
  const removed: CanvasOp[] = base.edges
168
173
  .filter((e) => !currentIds.has(e.id))
174
+ .filter((e) => !removedNodeIds.has(String(e.source)) && !removedNodeIds.has(String(e.target)))
169
175
  .map((e) => ({ kind: "removeEdge", edgeId: e.id }));
170
176
  const upserted = current.edges.flatMap((edge) => {
171
177
  const before = baseEdges.get(edge.id);
@@ -255,6 +261,32 @@ function parentsFirst(nodes: Record<string, unknown>[]): Record<string, unknown>
255
261
  return sortParentsFirst(wrapped).map(({ node }) => node);
256
262
  }
257
263
 
264
+ /**
265
+ * Drops removals the server already made, so a delete on both sides merges. Only
266
+ * removeNode and removeEdge: an edit to a node the server deleted must still throw.
267
+ */
268
+ function withoutServerRemovals(serverDoc: DiagramDoc, ops: CanvasOp[]): CanvasOp[] {
269
+ const serverNodeIds = new Set(serverDoc.nodes.map((n) => n.id));
270
+ const serverEdges = new Map(serverDoc.edges.map((e) => [e.id, e]));
271
+ const removedNodeIds = new Set<string>();
272
+ return ops.filter((op) => {
273
+ if (op.kind === "removeNode") {
274
+ if (!serverNodeIds.has(op.nodeId)) return false;
275
+ removedNodeIds.add(op.nodeId);
276
+ return true;
277
+ }
278
+ if (op.kind === "removeEdge") {
279
+ const edge = serverEdges.get(op.edgeId);
280
+ return (
281
+ edge !== undefined &&
282
+ !removedNodeIds.has(String(edge.source)) &&
283
+ !removedNodeIds.has(String(edge.target))
284
+ );
285
+ }
286
+ return true;
287
+ });
288
+ }
289
+
258
290
  /**
259
291
  * Replay local changes (lastSaved -> local) on the server document. Returns
260
292
  * null when impossible, like a patched node deleted, so the save error shows.
@@ -264,7 +296,7 @@ export function rebaseDiagram(
264
296
  lastSaved: DiagramDoc,
265
297
  local: DiagramDoc,
266
298
  ): RawCanvasData | null {
267
- const ops = diffToOps(lastSaved, local);
299
+ const ops = withoutServerRemovals(serverDoc, diffToOps(lastSaved, local));
268
300
  const res = tryCatch(() => applyOps({ nodes: serverDoc.nodes, edges: serverDoc.edges }, ops));
269
301
  if (!res.ok) return null;
270
302
  const nodes = applyNodeFieldPatches(res.value.nodes, nodeFieldPatches(lastSaved, local));
@@ -43,6 +43,49 @@
43
43
  transform: translateX(-50%);
44
44
  }
45
45
 
46
+ /* A phone has no room for a side rail: at 390px a rail on the left covers the
47
+ board xyflow just fitted. The rail lies flat along the bottom edge instead,
48
+ with its style bar stacked above it. */
49
+ @media (max-width: 639px) {
50
+ .svelte-flow .svelte-flow__panel.cv-dock.left.center {
51
+ top: auto;
52
+ bottom: 0;
53
+ left: 50%;
54
+ transform: translateX(-50%);
55
+ }
56
+
57
+ .cv-dock > .cv-row {
58
+ flex-direction: column-reverse;
59
+ }
60
+
61
+ .cv-dock .cv-rail {
62
+ flex-direction: row;
63
+ }
64
+ }
65
+
66
+ /* xyflow's credit is #999 on a half-white wash, which turns grey on grey on a
67
+ dark board. It takes the chrome's surface and muted ink in both themes. */
68
+ .svelte-flow .svelte-flow__attribution {
69
+ border-top-left-radius: var(--radius-sm, 4px);
70
+ background: var(--cv-surface);
71
+ }
72
+
73
+ /* On a phone the rail runs along the bottom edge and the credit would sit under
74
+ it, in the frame's rounded corner. It moves to the top right, inset off the
75
+ corner's curve. */
76
+ @media (max-width: 639px) {
77
+ .svelte-flow .svelte-flow__panel.svelte-flow__attribution {
78
+ top: 0;
79
+ bottom: auto;
80
+ margin: 6px 8px;
81
+ border-radius: var(--radius-sm, 4px);
82
+ }
83
+ }
84
+
85
+ .svelte-flow .svelte-flow__attribution a {
86
+ color: var(--cv-ink-muted);
87
+ }
88
+
46
89
  /* Beside the selection: StylePanel writes the spot, and a phone keeps its own row above the pill. */
47
90
  @media (min-width: 640px) {
48
91
  .svelte-flow .svelte-flow__panel.cv-dock.cv-style-dock.is-placed {
@@ -115,7 +158,7 @@
115
158
  }
116
159
 
117
160
  .cv-btn:active:not(:disabled) {
118
- scale: 0.94;
161
+ scale: 0.96;
119
162
  }
120
163
 
121
164
  /* One active style for a tool, a size, a brush and an open popover. */
@@ -71,8 +71,8 @@ The `@source` line matters: the chart markup carries Tailwind classes, and Tailw
71
71
  | --- | --- |
72
72
  | `DashboardSection`, `PanelGrid`, `StatBand` | A named area, a row of panels side by side, a row of number cards |
73
73
  | `StatTile` | A number card on `NumberChart`: a delta, a share track, a trend shape behind the figure, an optional link |
74
- | `DataPanel`, `DashboardEmpty` | A titled panel for a table or chart, and the dashed empty state |
75
- | `DataTable`, `DataSortHead`, `RatingChip` | The one data table: sortable heads, a row that opens, columns that hide on a phone, share bars, loading and empty states. An A to E grade chip |
74
+ | `DataPanel` | A titled panel for a table or chart. The dashed empty state is `EmptyState surface="dashed"` from `@heyramzi/ui/components/list-page` |
75
+ | `DataTable`, `RatingChip` | The one data table: sortable heads (bind `sort`, the `SortState` TableView takes; a new column starts descending), a row that opens, columns that hide on a phone, share bars, loading and empty states. An A to E grade chip |
76
76
  | `PeriodFilter`, `DAY_WINDOWS` | The 7, 30 and 90 day window, as a filter or as links |
77
77
  | `CHART_HEIGHT`, `dataTableClass` and friends | The one chart height and the table skin |
78
78
 
@@ -12,6 +12,7 @@
12
12
  import Figure from "./figure.svelte";
13
13
  import HoverFocus from "./hover-focus.svelte";
14
14
  import { DOMAIN_MOTION, TOOLTIP_GAP, TOOLTIP_TOP } from "./motion.js";
15
+ import { valueTick } from "./primitives/chart-utils.js";
15
16
  import Reveal from "./reveal.svelte";
16
17
  import ReferenceLine, { type ChartReference } from "./reference-line.svelte";
17
18
  import {
@@ -70,7 +71,7 @@
70
71
  xKey,
71
72
  yKey,
72
73
  series: seriesInput,
73
- color = "var(--chart-1)",
74
+ color = "var(--chart-series-1)",
74
75
  height = 240,
75
76
  stacked = false,
76
77
  smooth = true,
@@ -119,7 +120,7 @@
119
120
  ? seriesInput.map((s, i) => ({
120
121
  key: s.key,
121
122
  label: s.label ?? s.key,
122
- color: s.color ?? `var(--chart-${(i % 5) + 1})`,
123
+ color: s.color ?? `var(--chart-series-${(i % 6) + 1})`,
123
124
  }))
124
125
  : yKey
125
126
  ? [{ key: yKey, label: yKey, color }]
@@ -153,15 +154,7 @@
153
154
  const fallback = $derived(data.length > 0 ? data[data.length - 1] : null);
154
155
  const display = $derived(active ?? fallback);
155
156
 
156
- // A long tick label ("1,000,000") falls back to "1M" so the left gutter holds it.
157
- const compact = new Intl.NumberFormat(undefined, {
158
- notation: "compact",
159
- maximumFractionDigits: 1,
160
- });
161
- function valueTick(v: number): string {
162
- const full = formatValue(v);
163
- return full.length > 6 && Number.isFinite(v) ? compact.format(v) : full;
164
- }
157
+ const tick = $derived(valueTick(formatValue));
165
158
  const figureRows = $derived(
166
159
  display
167
160
  ? series.map((s) => ({
@@ -212,7 +205,7 @@
212
205
  motion={DOMAIN_MOTION}
213
206
  >
214
207
  <Svg>
215
- <Axis placement="left" format={(v) => valueTick(Number(v))} ticks={4} />
208
+ <Axis placement="left" format={(v) => tick(Number(v))} ticks={4} />
216
209
  <Axis placement="bottom" format={(v) => formatX(v)} ticks={ticksForWidth(xTicks)} />
217
210
  <g class="hr-area-grid">
218
211
  <Axis placement="left" grid rule={false} ticks={4} format={() => ""} />