@heyramzi/ui 3.0.0 → 3.2.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 (160) hide show
  1. package/dist/charts/dual-axis.js +38 -0
  2. package/dist/lib/sliding-pill.js +2 -2
  3. package/dist/record/measure-clamp.js +23 -0
  4. package/dist/shadcn/button-group/index.js +6 -0
  5. package/dist/shadcn/checkbox/checkbox-box.js +3 -0
  6. package/dist/shadcn/input-group/index.js +9 -0
  7. package/dist/shadcn/item/index.js +13 -0
  8. package/dist/shadcn/native-select/index.js +6 -0
  9. package/dist/shadcn/pagination/index.js +12 -0
  10. package/dist/slides/deck-controller.svelte.js +62 -15
  11. package/dist/slides/layout.js +3 -2
  12. package/package.json +9 -2
  13. package/scripts/svelte-dts.mjs +3 -0
  14. package/scripts/sync-shadcn.mjs +0 -5
  15. package/src/ai/generation-progress/GenerationProgress.svelte +14 -12
  16. package/src/canvas/pen/PenOverlay.svelte +1 -30
  17. package/src/canvas/screen/PartView.svelte +13 -8
  18. package/src/canvas/screen/ScreenBody.svelte +18 -11
  19. package/src/canvas/ui/BarTooltip.svelte +46 -0
  20. package/src/canvas/ui/BarTooltip.svelte.d.ts +17 -0
  21. package/src/canvas/ui/CanvasToolbar.svelte +170 -156
  22. package/src/canvas/ui/StylePanel.svelte +179 -102
  23. package/src/canvas/ui/canvas-ui.css +12 -76
  24. package/src/charts/area-chart.css +6 -0
  25. package/src/charts/area-chart.svelte +115 -17
  26. package/src/charts/area-chart.svelte.d.ts +11 -2
  27. package/src/charts/chart-tokens.css +4 -3
  28. package/src/charts/dashboard/data-table.svelte +80 -80
  29. package/src/charts/dashboard/hint.svelte +34 -0
  30. package/src/charts/dashboard/hint.svelte.d.ts +10 -0
  31. package/src/charts/dashboard/rating-chip.svelte +4 -3
  32. package/src/charts/dual-axis.test.ts +40 -0
  33. package/src/charts/dual-axis.ts +43 -0
  34. package/src/charts/legend.css +0 -8
  35. package/src/charts/legend.svelte +26 -30
  36. package/src/components/copy/copy-button.svelte +10 -2
  37. package/src/components/copy/copy-button.test.ts +7 -0
  38. package/src/components/data-table/table-skeleton.svelte +15 -7
  39. package/src/components/data-table/table-skeleton.svelte.d.ts +4 -0
  40. package/src/components/docs/on-this-page.svelte +2 -2
  41. package/src/components/hold-to-confirm/hold-to-confirm.svelte +2 -1
  42. package/src/components/list-page/filter-item.svelte +20 -7
  43. package/src/components/list-page/filter-popover.svelte +41 -16
  44. package/src/components/list-page/filter-popover.svelte.d.ts +1 -1
  45. package/src/components/list-page/grouped-list.svelte +41 -23
  46. package/src/components/picker/picker.svelte +8 -3
  47. package/src/components/picker/picker.svelte.d.ts +2 -1
  48. package/src/components/segmented/segmented-filter.svelte +19 -10
  49. package/src/components/segmented/segmented-track.svelte +11 -3
  50. package/src/components/table-view/table-view.svelte +22 -6
  51. package/src/components/tags-input/tags-input.svelte +94 -77
  52. package/src/components/toolbar/toolbar.svelte +2 -1
  53. package/src/components/toolbar/toolbar.test.ts +5 -4
  54. package/src/components/work-parts/group-head.svelte +28 -9
  55. package/src/components/work-parts/group-head.svelte.d.ts +5 -2
  56. package/src/editor/editor-toolbar.svelte +231 -193
  57. package/src/editor/editor.svelte +57 -39
  58. package/src/editor/image-toolbar.svelte +99 -37
  59. package/src/editor/image-toolbar.svelte.d.ts +4 -5
  60. package/src/editor/url-dialog.svelte +96 -0
  61. package/src/editor/url-dialog.svelte.d.ts +15 -0
  62. package/src/effects/theme-toggle/ThemeToggle.css +8 -8
  63. package/src/effects/theme-toggle/ThemeToggle.svelte +20 -6
  64. package/src/inbox/thread-inbox.svelte +9 -1
  65. package/src/lib/live-region.svelte +12 -4
  66. package/src/lib/live-region.svelte.d.ts +5 -1
  67. package/src/lib/sliding-pill.ts +3 -2
  68. package/src/record/info-list.svelte +33 -59
  69. package/src/record/measure-clamp.ts +31 -0
  70. package/src/record/record-shell.svelte +38 -39
  71. package/src/record/record-timeline.svelte +10 -5
  72. package/src/record/steps-rail.svelte +50 -36
  73. package/src/record/where-we-are.svelte +5 -19
  74. package/src/shadcn/button-group/button-group-separator.svelte +23 -0
  75. package/src/shadcn/button-group/button-group-separator.svelte.d.ts +13 -0
  76. package/src/shadcn/button-group/button-group-text.svelte +28 -0
  77. package/src/shadcn/button-group/button-group-text.svelte.d.ts +11 -0
  78. package/src/shadcn/button-group/button-group.svelte +46 -0
  79. package/src/shadcn/button-group/button-group.svelte.d.ts +16 -0
  80. package/src/shadcn/button-group/index.ts +15 -0
  81. package/src/shadcn/checkbox/checkbox-box.ts +4 -0
  82. package/src/shadcn/checkbox/checkbox.svelte +2 -4
  83. package/src/shadcn/input-group/index.ts +22 -0
  84. package/src/shadcn/input-group/input-group-addon.svelte +51 -0
  85. package/src/shadcn/input-group/input-group-addon.svelte.d.ts +18 -0
  86. package/src/shadcn/input-group/input-group-button.svelte +49 -0
  87. package/src/shadcn/input-group/input-group-button.svelte.d.ts +18 -0
  88. package/src/shadcn/input-group/input-group-input.svelte +20 -0
  89. package/src/shadcn/input-group/input-group-input.svelte.d.ts +11 -0
  90. package/src/shadcn/input-group/input-group-text.svelte +19 -0
  91. package/src/shadcn/input-group/input-group-text.svelte.d.ts +5 -0
  92. package/src/shadcn/input-group/input-group-textarea.svelte +20 -0
  93. package/src/shadcn/input-group/input-group-textarea.svelte.d.ts +3 -0
  94. package/src/shadcn/input-group/input-group.svelte +24 -0
  95. package/src/shadcn/input-group/input-group.svelte.d.ts +5 -0
  96. package/src/shadcn/item/index.ts +34 -0
  97. package/src/shadcn/item/item-actions.svelte +20 -0
  98. package/src/shadcn/item/item-actions.svelte.d.ts +5 -0
  99. package/src/shadcn/item/item-content.svelte +23 -0
  100. package/src/shadcn/item/item-content.svelte.d.ts +5 -0
  101. package/src/shadcn/item/item-description.svelte +23 -0
  102. package/src/shadcn/item/item-description.svelte.d.ts +5 -0
  103. package/src/shadcn/item/item-footer.svelte +20 -0
  104. package/src/shadcn/item/item-footer.svelte.d.ts +5 -0
  105. package/src/shadcn/item/item-group.svelte +21 -0
  106. package/src/shadcn/item/item-group.svelte.d.ts +5 -0
  107. package/src/shadcn/item/item-header.svelte +20 -0
  108. package/src/shadcn/item/item-header.svelte.d.ts +5 -0
  109. package/src/shadcn/item/item-media.svelte +42 -0
  110. package/src/shadcn/item/item-media.svelte.d.ts +17 -0
  111. package/src/shadcn/item/item-separator.svelte +19 -0
  112. package/src/shadcn/item/item-separator.svelte.d.ts +13 -0
  113. package/src/shadcn/item/item-title.svelte +20 -0
  114. package/src/shadcn/item/item-title.svelte.d.ts +5 -0
  115. package/src/shadcn/item/item.svelte +61 -0
  116. package/src/shadcn/item/item.svelte.d.ts +28 -0
  117. package/src/shadcn/native-select/index.ts +13 -0
  118. package/src/shadcn/native-select/native-select-opt-group.svelte +20 -0
  119. package/src/shadcn/native-select/native-select-opt-group.svelte.d.ts +5 -0
  120. package/src/shadcn/native-select/native-select-option.svelte +20 -0
  121. package/src/shadcn/native-select/native-select-option.svelte.d.ts +5 -0
  122. package/src/shadcn/native-select/native-select.svelte +45 -0
  123. package/src/shadcn/native-select/native-select.svelte.d.ts +8 -0
  124. package/src/shadcn/pagination/index.ts +31 -0
  125. package/src/shadcn/pagination/pagination-content.svelte +20 -0
  126. package/src/shadcn/pagination/pagination-content.svelte.d.ts +5 -0
  127. package/src/shadcn/pagination/pagination-ellipsis.svelte +22 -0
  128. package/src/shadcn/pagination/pagination-ellipsis.svelte.d.ts +5 -0
  129. package/src/shadcn/pagination/pagination-item.svelte +14 -0
  130. package/src/shadcn/pagination/pagination-item.svelte.d.ts +5 -0
  131. package/src/shadcn/pagination/pagination-link.svelte +41 -0
  132. package/src/shadcn/pagination/pagination-link.svelte.d.ts +9 -0
  133. package/src/shadcn/pagination/pagination-next-button.svelte +34 -0
  134. package/src/shadcn/pagination/pagination-next-button.svelte.d.ts +4 -0
  135. package/src/shadcn/pagination/pagination-next.svelte +26 -0
  136. package/src/shadcn/pagination/pagination-next.svelte.d.ts +4 -0
  137. package/src/shadcn/pagination/pagination-prev-button.svelte +34 -0
  138. package/src/shadcn/pagination/pagination-prev-button.svelte.d.ts +4 -0
  139. package/src/shadcn/pagination/pagination-previous.svelte +30 -0
  140. package/src/shadcn/pagination/pagination-previous.svelte.d.ts +4 -0
  141. package/src/shadcn/pagination/pagination.svelte +27 -0
  142. package/src/shadcn/pagination/pagination.svelte.d.ts +4 -0
  143. package/src/slides/Controls.svelte +39 -36
  144. package/src/slides/Deck.svelte +23 -10
  145. package/src/slides/Overview.svelte +10 -4
  146. package/src/slides/README.md +1 -1
  147. package/src/slides/Slide.svelte +17 -11
  148. package/src/slides/deck-controller.svelte.ts +62 -16
  149. package/src/slides/layout.ts +3 -2
  150. package/src/slides/theme.css +34 -5
  151. package/src/thread/thread.svelte +15 -8
  152. package/src/work/detail/task-detail.svelte +1 -1
  153. package/src/work/gantt/gantt-task-row.svelte +3 -6
  154. package/src/work/inbox/inbox-card.svelte +7 -5
  155. package/src/work/inbox/inbox-trace.svelte +14 -10
  156. package/src/work/kanban-board.svelte +40 -25
  157. package/src/work/kanban-hidden-rail.svelte +67 -65
  158. package/src/work/task-list.svelte +145 -124
  159. package/src/work/task-list.svelte.test.ts +5 -2
  160. package/src/work/time/time-tracker.svelte +35 -10
@@ -0,0 +1,38 @@
1
+ const NICE = [
2
+ 1,
3
+ 2,
4
+ 2.5,
5
+ 5,
6
+ 10
7
+ ];
8
+ /** The smallest 1, 2, 2.5 or 5 times a power of ten that is at least `raw`. */
9
+ export function niceStep(raw) {
10
+ if (!(raw > 0) || !Number.isFinite(raw)) return 1;
11
+ const power = 10 ** Math.floor(Math.log10(raw));
12
+ // WHY the epsilon: 0.3 / 0.1 is 2.9999999999999996, which would skip 3 x 0.1 for the next step.
13
+ const multiple = NICE.find((m) => m * power >= raw * (1 - 1e-9)) ?? 10;
14
+ return multiple * power;
15
+ }
16
+ function ticksOf(step, count) {
17
+ // Rounded so 0.1 x 3 reads 0.3 on the axis, not 0.30000000000000004.
18
+ return Array.from({ length: count + 1 }, (_, i) => Number((i * step).toPrecision(12)));
19
+ }
20
+ /**
21
+ * Left and right domains from 0, with the same tick count so both axes sit on one grid.
22
+ * `target` is the number of intervals wanted; the left data may round it to one more or less.
23
+ */
24
+ export function dualAxis(leftMax, rightMax, target = 4) {
25
+ const leftStep = niceStep(Math.max(0, leftMax) / target);
26
+ const count = Math.max(1, Math.ceil(Math.max(0, leftMax) / leftStep - 1e-9));
27
+ const rightStep = niceStep(Math.max(0, rightMax) / count);
28
+ return {
29
+ left: {
30
+ max: leftStep * count,
31
+ ticks: ticksOf(leftStep, count)
32
+ },
33
+ right: {
34
+ max: rightStep * count,
35
+ ticks: ticksOf(rightStep, count)
36
+ }
37
+ };
38
+ }
@@ -2,10 +2,10 @@
2
2
  // WHY attach to the pill, not the list: the pill finds its list via
3
3
  // `parentElement`, so any strip that renders one span gets the motion.
4
4
  // WHY measure: the active item's width is its label. The selector covers the
5
- // three ways a strip marks it: a tab, a nav link, a toggle.
5
+ // ways a strip marks it: a tab, a nav link, a pressed button, a bits-ui toggle group item.
6
6
  // Per transitions.dev `tabs-sliding`: first paint and resizes write the
7
7
  // position with the transition off, so the pill snaps in instead of flying.
8
- const ACTIVE = "[data-state=\"active\"],[aria-current=\"page\"],[aria-pressed=\"true\"]";
8
+ const ACTIVE = "[data-state=\"active\"],[aria-current=\"page\"],[aria-pressed=\"true\"],[data-state=\"on\"]";
9
9
  export const slidingPill = (pill) => {
10
10
  const list = pill.parentElement;
11
11
  if (!list) return;
@@ -0,0 +1,23 @@
1
+ import { untrack } from "svelte";
2
+ /** Reports whether a line-clamped box cuts its text, so "More" shows only then. */
3
+ // WHY measure: a note that fits two lines at 1280px overflows at 390px.
4
+ // WHY untracked: an open box keeps its last answer, so a fold never remounts the toggle under focus.
5
+ export function measureClamp(expanded, onCut) {
6
+ return (el) => {
7
+ const measure = () => untrack(() => {
8
+ if (!expanded()) onCut(el.scrollHeight > el.clientHeight + 1);
9
+ });
10
+ measure();
11
+ const observer = new ResizeObserver(measure);
12
+ observer.observe(el);
13
+ // A web font landing rewraps the text in a box that keeps its height, and the observer stays quiet.
14
+ let live = true;
15
+ void document.fonts?.ready.then(() => live && measure());
16
+ document.fonts?.addEventListener("loadingdone", measure);
17
+ return () => {
18
+ live = false;
19
+ observer.disconnect();
20
+ document.fonts?.removeEventListener("loadingdone", measure);
21
+ };
22
+ };
23
+ }
@@ -0,0 +1,6 @@
1
+ import Separator from "../../../src/shadcn/button-group/button-group-separator.svelte";
2
+ import Text from "../../../src/shadcn/button-group/button-group-text.svelte";
3
+ import Root, { buttonGroupVariants } from "../../../src/shadcn/button-group/button-group.svelte";
4
+ export { Root, Text, Separator, buttonGroupVariants,
5
+ //
6
+ Root as ButtonGroup, Text as ButtonGroupText, Separator as ButtonGroupSeparator };
@@ -0,0 +1,3 @@
1
+ // The checkbox's square, shared with rows that draw it as a picture inside a menu item,
2
+ // where a real checkbox would put a button inside a button.
3
+ export const checkboxBoxClass = "border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 not-data-checked:enabled:hover:border-foreground/30 focus-visible:border-ring focus-visible:shadow-(--ring-focus) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-sm border shadow-btn transition-[background-color,border-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] group-has-disabled/field:opacity-50 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 any-pointer-coarse:after:-inset-[15px] disabled:cursor-not-allowed disabled:opacity-50";
@@ -0,0 +1,9 @@
1
+ import Addon from "../../../src/shadcn/input-group/input-group-addon.svelte";
2
+ import Button from "../../../src/shadcn/input-group/input-group-button.svelte";
3
+ import Input from "../../../src/shadcn/input-group/input-group-input.svelte";
4
+ import Text from "../../../src/shadcn/input-group/input-group-text.svelte";
5
+ import Textarea from "../../../src/shadcn/input-group/input-group-textarea.svelte";
6
+ import Root from "../../../src/shadcn/input-group/input-group.svelte";
7
+ export { Root, Addon, Button, Input, Text, Textarea,
8
+ //
9
+ Root as InputGroup, Addon as InputGroupAddon, Button as InputGroupButton, Input as InputGroupInput, Text as InputGroupText, Textarea as InputGroupTextarea };
@@ -0,0 +1,13 @@
1
+ import Actions from "../../../src/shadcn/item/item-actions.svelte";
2
+ import Content from "../../../src/shadcn/item/item-content.svelte";
3
+ import Description from "../../../src/shadcn/item/item-description.svelte";
4
+ import Footer from "../../../src/shadcn/item/item-footer.svelte";
5
+ import Group from "../../../src/shadcn/item/item-group.svelte";
6
+ import Header from "../../../src/shadcn/item/item-header.svelte";
7
+ import Media from "../../../src/shadcn/item/item-media.svelte";
8
+ import Separator from "../../../src/shadcn/item/item-separator.svelte";
9
+ import Title from "../../../src/shadcn/item/item-title.svelte";
10
+ import Root from "../../../src/shadcn/item/item.svelte";
11
+ export { Root, Group, Separator, Header, Footer, Content, Title, Description, Actions, Media,
12
+ //
13
+ Root as Item, Group as ItemGroup, Separator as ItemSeparator, Header as ItemHeader, Footer as ItemFooter, Content as ItemContent, Title as ItemTitle, Description as ItemDescription, Actions as ItemActions, Media as ItemMedia };
@@ -0,0 +1,6 @@
1
+ import Root from "../../../src/shadcn/native-select/native-select.svelte";
2
+ import Option from "../../../src/shadcn/native-select/native-select-option.svelte";
3
+ import OptGroup from "../../../src/shadcn/native-select/native-select-opt-group.svelte";
4
+ export { Root, Option, OptGroup,
5
+ //
6
+ Root as NativeSelect, Option as NativeSelectOption, OptGroup as NativeSelectOptGroup };
@@ -0,0 +1,12 @@
1
+ import Content from "../../../src/shadcn/pagination/pagination-content.svelte";
2
+ import Ellipsis from "../../../src/shadcn/pagination/pagination-ellipsis.svelte";
3
+ import Item from "../../../src/shadcn/pagination/pagination-item.svelte";
4
+ import Link from "../../../src/shadcn/pagination/pagination-link.svelte";
5
+ import NextButton from "../../../src/shadcn/pagination/pagination-next-button.svelte";
6
+ import Next from "../../../src/shadcn/pagination/pagination-next.svelte";
7
+ import PrevButton from "../../../src/shadcn/pagination/pagination-prev-button.svelte";
8
+ import Previous from "../../../src/shadcn/pagination/pagination-previous.svelte";
9
+ import Root from "../../../src/shadcn/pagination/pagination.svelte";
10
+ export { Root, Content, Item, Link, PrevButton, NextButton, Ellipsis, Previous, Next,
11
+ //
12
+ Root as Pagination, Content as PaginationContent, Item as PaginationItem, Link as PaginationLink, PrevButton as PaginationPrevButton, NextButton as PaginationNextButton, Ellipsis as PaginationEllipsis, Previous as PaginationPrevious, Next as PaginationNext };
@@ -5,9 +5,19 @@ import { resolveLabels } from "./labels.js";
5
5
  import { DEFAULT_CANVAS } from "./types.js";
6
6
  /** Height the controls bar takes at the bottom of the overview, in px. */
7
7
  const CONTROLS_ROOM = 56;
8
- /** How long the overview morph flag stays up, a little past the longest stagger. */
9
- const MORPH_MS = 900;
10
8
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
9
+ /**
10
+ * Where `cell` lands when the camera moves `target` onto `fit`. Every box shares one scale and
11
+ * shift, and a transform tween is linear in both, so the grid stays rigid while it zooms.
12
+ */
13
+ function frame(cell, target, fit) {
14
+ const k = fit.scale / target.scale;
15
+ return {
16
+ x: fit.x + (cell.x - target.x) * k,
17
+ y: fit.y + (cell.y - target.y) * k,
18
+ scale: cell.scale * k
19
+ };
20
+ }
11
21
  function isEditable(el) {
12
22
  return el.isContentEditable || [
13
23
  "INPUT",
@@ -22,7 +32,7 @@ export class DeckController {
22
32
  /** The highlighted thumbnail in the overview, as an index among the visible slides. */
23
33
  cursor = $state(0);
24
34
  direction = $state(1);
25
- morphing = $state(false);
35
+ zoom = $state(null);
26
36
  size = $state({
27
37
  w: 0,
28
38
  h: 0
@@ -30,6 +40,7 @@ export class DeckController {
30
40
  options;
31
41
  #listeners = new Set();
32
42
  #timer;
43
+ #frame = 0;
33
44
  constructor(options) {
34
45
  this.options = options;
35
46
  }
@@ -61,8 +72,13 @@ export class DeckController {
61
72
  }, this.canvas, this.visible.length));
62
73
  /** Where slide `index` sits right now: centred to fit, or in its overview cell. */
63
74
  boxOf(index) {
64
- if (!this.overview) return this.fit;
65
- return this.grid.boxes[this.visible.indexOf(index)];
75
+ const zoom = this.zoom;
76
+ const cell = this.grid.boxes[this.visible.indexOf(index)];
77
+ if (zoom?.framed) {
78
+ const target = this.grid.boxes[this.visible.indexOf(zoom.index)];
79
+ return cell && target ? frame(cell, target, this.fit) : this.fit;
80
+ }
81
+ return this.overview || zoom ? cell : this.fit;
66
82
  }
67
83
  register(entry) {
68
84
  untrack(() => {
@@ -78,7 +94,7 @@ export class DeckController {
78
94
  return () => this.#listeners.delete(listener);
79
95
  }
80
96
  mount() {
81
- return () => clearTimeout(this.#timer);
97
+ return () => this.#stopZoom();
82
98
  }
83
99
  /** Which deck hears a key is the page's call: Deck passes only keys from focus inside it. */
84
100
  get listening() {
@@ -88,14 +104,15 @@ export class DeckController {
88
104
  this.options.index = position.index;
89
105
  this.options.step = position.step;
90
106
  }
91
- commit(target, source = "local") {
107
+ /** `animate: false` skips the slide transition, for a move the overview zoom already shows. */
108
+ commit(target, source = "local", animate = true) {
92
109
  if (!target) return;
93
110
  const from = this.position;
94
111
  const to = settle(this.slides, target);
95
112
  if (to.index === from.index && to.step === from.step) return;
96
113
  this.direction = to.index >= from.index ? 1 : -1;
97
114
  const apply = () => this.#write(to);
98
- if (to.index !== from.index && this.options.transition) this.options.transition(apply);
115
+ if (animate && to.index !== from.index && this.options.transition) this.options.transition(apply);
99
116
  else apply();
100
117
  for (const listener of this.#listeners) listener(to, source);
101
118
  }
@@ -111,23 +128,53 @@ export class DeckController {
111
128
  step
112
129
  }, source);
113
130
  }
131
+ /**
132
+ * Opening zooms the camera out from the current slide to the whole grid. Closing zooms into
133
+ * the current slide, so `select` moves there first.
134
+ */
114
135
  toggleOverview(force) {
115
136
  const open = force ?? !this.overview;
116
137
  if (open === this.overview) return;
117
- if (open) this.cursor = Math.max(0, this.visible.indexOf(this.position.index));
138
+ const index = this.position.index;
139
+ if (open) this.cursor = Math.max(0, this.visible.indexOf(index));
118
140
  this.view = open ? "overview" : "slide";
119
- this.#morph();
141
+ this.#stopZoom();
142
+ const ms = this.options.zoomMs?.() ?? 0;
143
+ if (ms <= 0 || !this.visible.includes(index)) return;
144
+ /*
145
+ * Each way paints its start state first and moves on a later frame. Moved in the same
146
+ * flush, a style read in between (the overview handing focus back) commits the boxes
147
+ * before the transition rule is on, and the zoom jumps to its end.
148
+ */
149
+ this.zoom = {
150
+ index,
151
+ framed: open,
152
+ running: !open
153
+ };
154
+ this.#frame = requestAnimationFrame(() => {
155
+ this.#frame = requestAnimationFrame(() => {
156
+ this.zoom = {
157
+ index,
158
+ framed: !open,
159
+ running: true
160
+ };
161
+ this.#timer = setTimeout(() => this.#stopZoom(), ms);
162
+ });
163
+ });
120
164
  }
121
165
  /** Leaves the overview on slide `index`. Picking the current slide keeps its step. */
122
166
  select(index) {
123
- this.toggleOverview(false);
124
167
  const keep = index === this.position.index;
125
- this.go(index, keep ? this.position.step : 0);
168
+ this.commit({
169
+ index,
170
+ step: keep ? this.position.step : 0
171
+ }, "local", false);
172
+ this.toggleOverview(false);
126
173
  }
127
- #morph() {
128
- this.morphing = true;
174
+ #stopZoom() {
129
175
  clearTimeout(this.#timer);
130
- this.#timer = setTimeout(() => this.morphing = false, MORPH_MS);
176
+ cancelAnimationFrame(this.#frame);
177
+ this.zoom = null;
131
178
  }
132
179
  #moveCursor(to) {
133
180
  this.cursor = clamp(to, 0, this.visible.length - 1);
@@ -10,7 +10,8 @@ export function fitBox(view, canvas) {
10
10
  function scaleFor(cols, count, view, canvas, pad, gap) {
11
11
  const rows = Math.ceil(count / cols);
12
12
  const byWidth = (view.w - 2 * pad - (cols - 1) * gap) / cols / canvas.w;
13
- const byHeight = (view.h - 2 * pad - (rows - 1) * gap) / rows / canvas.h;
13
+ /* Every row, the last too, keeps a gap under it for its numbers. */
14
+ const byHeight = (view.h - 2 * pad - rows * gap) / rows / canvas.h;
14
15
  return Math.min(byWidth, byHeight);
15
16
  }
16
17
  /** Lays `count` canvases in the roomiest grid that shows them all, centred. */
@@ -32,7 +33,7 @@ export function gridBoxes(view, canvas, count) {
32
33
  const cellH = canvas.h * scale;
33
34
  const rows = Math.ceil(count / cols);
34
35
  const left = (view.w - (cols * cellW + (cols - 1) * gap)) / 2;
35
- const top = (view.h - (rows * cellH + (rows - 1) * gap)) / 2;
36
+ const top = (view.h - rows * (cellH + gap)) / 2;
36
37
  const boxes = Array.from({ length: count }, (_, i) => ({
37
38
  x: left + i % cols * (cellW + gap),
38
39
  y: top + Math.floor(i / cols) * (cellH + gap),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyramzi/ui",
3
- "version": "3.0.0",
3
+ "version": "3.2.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",
@@ -271,6 +271,10 @@
271
271
  "default": "./src/editor/block-ops.ts"
272
272
  },
273
273
  "./editor/inline-editor": {
274
+ "types": [
275
+ "./src/editor/inline-editor.svelte.d.ts",
276
+ "./src/editor/inline-editor.svelte"
277
+ ],
274
278
  "svelte": "./src/editor/inline-editor.svelte",
275
279
  "default": "./src/editor/inline-editor.svelte"
276
280
  },
@@ -291,7 +295,10 @@
291
295
  "default": "./src/canvas/index.ts"
292
296
  },
293
297
  "./canvas/*.svelte": {
294
- "types": "./src/canvas/*.svelte",
298
+ "types": [
299
+ "./src/canvas/*.svelte.d.ts",
300
+ "./src/canvas/*.svelte"
301
+ ],
295
302
  "svelte": "./src/canvas/*.svelte",
296
303
  "default": "./src/canvas/*.svelte"
297
304
  },
@@ -3,6 +3,9 @@
3
3
  // `.svelte` under node_modules, so without them every component types as `any`.
4
4
 
5
5
  // prepack writes them, postpack deletes them (`--clean`): left in src, they'd shadow the components.
6
+ // An export whose target is a `.svelte` file lists its `types` as [`x.svelte.d.ts`, `x.svelte`]:
7
+ // from a `.svelte` target TypeScript only looks for `x.d.svelte.ts`, and svelte-check stops
8
+ // faking that file once a real `x.svelte.d.ts` sits beside it, so the import found nothing (3.0.0).
6
9
  import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
10
  import { createRequire } from "node:module";
8
11
  import { tmpdir } from "node:os";
@@ -76,19 +76,14 @@ const DEMO_ALIASES = [
76
76
  /* Registry primitives this package does not ship: no product imports them. Skipped, so a sync does not bring them back. */
77
77
  const NOT_SHIPPED = new Set([
78
78
  "aspect-ratio",
79
- "button-group",
80
79
  "carousel",
81
80
  "chart",
82
81
  "data-table",
83
82
  "drawer",
84
83
  "form",
85
- "input-group",
86
- "item",
87
84
  "kbd",
88
85
  "menubar",
89
- "native-select",
90
86
  "navigation-menu",
91
- "pagination",
92
87
  "range-calendar",
93
88
  "resizable",
94
89
  // Its `pascalToKebab` has no importer, in the registry or here.
@@ -19,6 +19,7 @@
19
19
  // =========================
20
20
  // IMPORTS
21
21
  // =========================
22
+ import { Progress as ProgressPrimitive } from "bits-ui";
22
23
  import { cn } from "../../lib/utils.js";
23
24
 
24
25
  // =========================
@@ -104,21 +105,22 @@
104
105
  </span>
105
106
  </div>
106
107
 
107
- <div
108
- class="gen-track"
109
- data-indeterminate={progress ? undefined : true}
110
- role="progressbar"
108
+ <!-- Bits' root, not the shadcn bar: that one has no indeterminate look and fills with primary. -->
109
+ <ProgressPrimitive.Root
110
+ value={progress ? progress.completed : null}
111
+ max={progress?.total}
111
112
  aria-label={heading}
112
- aria-valuemin={progress ? 0 : undefined}
113
- aria-valuemax={progress?.total}
114
- aria-valuenow={progress?.completed}
115
113
  aria-valuetext={progress ? `${progress.completed} of ${progress.total}` : undefined}
116
114
  >
117
- <div
118
- class="gen-fill"
119
- style:transform={progress ? `scaleX(${percent / 100})` : undefined}
120
- ></div>
121
- </div>
115
+ {#snippet child({ props })}
116
+ <div {...props} class="gen-track">
117
+ <div
118
+ class="gen-fill"
119
+ style:transform={progress ? `scaleX(${percent / 100})` : undefined}
120
+ ></div>
121
+ </div>
122
+ {/snippet}
123
+ </ProgressPrimitive.Root>
122
124
 
123
125
  {#if progress?.currentStep}
124
126
  {#key progress.currentStep}
@@ -8,6 +8,7 @@
8
8
  import type { PenOp } from "./ops.js";
9
9
  import { createPenSession, type PenPreview } from "./pen-session.js";
10
10
  import type { PenColor, PenTool } from "./types.js";
11
+ import { forwardWheel } from "../ui/forward-wheel.js";
11
12
 
12
13
  // =========================
13
14
  // TYPES
@@ -106,36 +107,6 @@
106
107
  if (event.key === "Escape") session.cancel();
107
108
  }
108
109
 
109
- // The overlay sits above xyflow's zoom pane, so a wheel or a trackpad pinch would
110
- // never reach it. Hand those to the pane so you can zoom and pan mid-drawing.
111
- function forwardWheel(node: HTMLElement) {
112
- const handler = (event: WheelEvent) => {
113
- const zoomPane = node.closest(".svelte-flow")?.querySelector(".svelte-flow__zoom");
114
- if (!zoomPane) return;
115
- event.preventDefault();
116
- zoomPane.dispatchEvent(
117
- new WheelEvent("wheel", {
118
- deltaX: event.deltaX,
119
- deltaY: event.deltaY,
120
- deltaZ: event.deltaZ,
121
- deltaMode: event.deltaMode,
122
- clientX: event.clientX,
123
- clientY: event.clientY,
124
- screenX: event.screenX,
125
- screenY: event.screenY,
126
- ctrlKey: event.ctrlKey,
127
- shiftKey: event.shiftKey,
128
- altKey: event.altKey,
129
- metaKey: event.metaKey,
130
- bubbles: true,
131
- cancelable: true,
132
- }),
133
- );
134
- };
135
- node.addEventListener("wheel", handler, { passive: false });
136
- return () => node.removeEventListener("wheel", handler);
137
- }
138
-
139
110
  function cursorFor(current: PenTool): string {
140
111
  if (current === "text") return "text";
141
112
  return "crosshair";
@@ -3,6 +3,7 @@
3
3
  // IMPORTS
4
4
  // =========================
5
5
  import { tick } from "svelte";
6
+ import { Toggle } from "../../shadcn/toggle/index.js";
6
7
  import GlyphIcon from "./GlyphIcon.svelte";
7
8
  import { addItem, removeItem, setItem } from "./parts.js";
8
9
  import type { ScreenLabels } from "./labels.js";
@@ -206,15 +207,19 @@
206
207
  {#if !readonly}
207
208
  <div class="cv-tools nodrag">
208
209
  {#if part.kind === "button"}
209
- <button
210
- type="button"
211
- class="cv-tool is-text"
212
- aria-pressed={part.variant !== "secondary"}
213
- onclick={() =>
214
- onCommit({ variant: part.variant === "secondary" ? "primary" : "secondary" }, false)}
210
+ <!-- The button is drawn here, so the card's own classes replace the toggle's utilities. -->
211
+ <Toggle
212
+ bind:pressed={
213
+ () => part.variant !== "secondary",
214
+ (primary) => onCommit({ variant: primary ? "primary" : "secondary" }, false)
215
+ }
215
216
  >
216
- {part.variant === "secondary" ? labels.secondary : labels.primary}
217
- </button>
217
+ {#snippet child({ props })}
218
+ <button {...props} class="cv-tool is-text">
219
+ {part.variant === "secondary" ? labels.secondary : labels.primary}
220
+ </button>
221
+ {/snippet}
222
+ </Toggle>
218
223
  {/if}
219
224
  <button
220
225
  type="button"
@@ -2,6 +2,7 @@
2
2
  // =========================
3
3
  // IMPORTS
4
4
  // =========================
5
+ import { RadioGroup } from "bits-ui";
5
6
  import { getDiagramEditorContext } from "../editor/editor-context.js";
6
7
  import { isLocked } from "../editor/lock.js";
7
8
  import { DEFAULT_SCREEN_LABELS, type ScreenLabels } from "./labels.js";
@@ -71,6 +72,13 @@
71
72
  if (mode !== screen.mode) write({ mode });
72
73
  }
73
74
 
75
+ // Svelte Flow nudges a selected card on an arrow, so the mode switch keeps the keys it moves by.
76
+ function keepArrows(event: KeyboardEvent) {
77
+ if (event.key.startsWith("Arrow") || event.key === "Home" || event.key === "End") {
78
+ event.stopPropagation();
79
+ }
80
+ }
81
+
74
82
  function add(kind: PartKind) {
75
83
  write({ parts: addPart(screen.parts, kind) });
76
84
  tray = false;
@@ -104,20 +112,19 @@
104
112
  }}
105
113
  />
106
114
  {#if !readonly}
107
- <div class="cv-switch nodrag" role="radiogroup" aria-label={labels.mode}>
115
+ <RadioGroup.Root
116
+ class="cv-switch nodrag"
117
+ orientation="horizontal"
118
+ aria-label={labels.mode}
119
+ bind:value={() => screen.mode, (mode) => setMode(mode as ScreenMode)}
120
+ onkeydown={keepArrows}
121
+ >
108
122
  {#each SCREEN_MODES as mode (mode)}
109
- <button
110
- type="button"
111
- role="radio"
112
- class="cv-seg"
113
- aria-checked={screen.mode === mode}
114
- data-mode-option={mode}
115
- onclick={() => setMode(mode)}
116
- >
123
+ <RadioGroup.Item value={mode} class="cv-seg" data-mode-option={mode}>
117
124
  {labels.modes[mode]}
118
- </button>
125
+ </RadioGroup.Item>
119
126
  {/each}
120
- </div>
127
+ </RadioGroup.Root>
121
128
  {/if}
122
129
  </div>
123
130
 
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import type { Snippet } from "svelte";
6
+ import { Kbd } from "../../shadcn/kbd/index.js";
7
+ import * as Tooltip from "../../shadcn/tooltip/index.js";
8
+
9
+ // =========================
10
+ // TYPES
11
+ // =========================
12
+ interface Props {
13
+ /** Shown in the tooltip, and the button's name unless `name` is set. */
14
+ label: string;
15
+ /** The name a screen reader reads, when it needs more than the tooltip, like "Fill: Red". */
16
+ name?: string;
17
+ /** The single key that picks this tool, shown beside the label and set as aria-keyshortcuts. */
18
+ shortcut?: string;
19
+ side?: "top" | "right" | "bottom" | "left";
20
+ /** The bar control. Spread `props` on it, so the tooltip opens from it and names it. */
21
+ child: Snippet<[{ props: Record<string, unknown> }]>;
22
+ }
23
+
24
+ // =========================
25
+ // PROPS
26
+ // =========================
27
+ // Renamed, because the trigger's own `child` snippet below would shadow it.
28
+ let { label, name = label, shortcut, side = "top", child: control }: Props = $props();
29
+ </script>
30
+
31
+ <!-- =========================== -->
32
+ <!-- MARKUP -->
33
+ <!-- =========================== -->
34
+ <Tooltip.Root>
35
+ <Tooltip.Trigger>
36
+ {#snippet child({ props })}
37
+ {@render control({
38
+ props: { ...props, "aria-label": name, "aria-keyshortcuts": shortcut || undefined },
39
+ })}
40
+ {/snippet}
41
+ </Tooltip.Trigger>
42
+ <Tooltip.Content {side} sideOffset={10} class="flex items-center gap-2">
43
+ {label}
44
+ {#if shortcut}<Kbd>{shortcut}</Kbd>{/if}
45
+ </Tooltip.Content>
46
+ </Tooltip.Root>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ /** Shown in the tooltip, and the button's name unless `name` is set. */
4
+ label: string;
5
+ /** The name a screen reader reads, when it needs more than the tooltip, like "Fill: Red". */
6
+ name?: string;
7
+ /** The single key that picks this tool, shown beside the label and set as aria-keyshortcuts. */
8
+ shortcut?: string;
9
+ side?: "top" | "right" | "bottom" | "left";
10
+ /** The bar control. Spread `props` on it, so the tooltip opens from it and names it. */
11
+ child: Snippet<[{
12
+ props: Record<string, unknown>;
13
+ }]>;
14
+ }
15
+ declare const $$Component: import("svelte").Component<Props, {}, "">;
16
+ type $$Component = ReturnType<typeof $$Component>;
17
+ export default $$Component;