@crewlethq/ui 0.31.0 → 0.32.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 (47) hide show
  1. package/README.md +25 -1
  2. package/dist/Canvas/Canvas.d.ts +48 -2
  3. package/dist/Canvas/ShortcutTip.d.ts +22 -0
  4. package/dist/Canvas/geometry.d.ts +13 -3
  5. package/dist/Canvas/index.d.ts +3 -1
  6. package/dist/Canvas/index.js +3 -1
  7. package/dist/{Canvas-YVXHMWLN.css → Canvas-LRVNW7XN.css} +39 -3
  8. package/dist/CanvasBoard/CanvasBoard.d.ts +75 -12
  9. package/dist/CanvasBoard/index.d.ts +2 -2
  10. package/dist/CanvasBoard/index.js +6 -2
  11. package/dist/{CanvasBoard-ZLPDINSM.css → CanvasBoard-MBZVBWDO.css} +135 -30
  12. package/dist/CanvasToolbar/CanvasToolbar.d.ts +48 -0
  13. package/dist/CanvasToolbar/index.d.ts +3 -0
  14. package/dist/CanvasToolbar/index.js +12 -0
  15. package/dist/CanvasToolbar-AE5MXH7T.css +32 -0
  16. package/dist/DataView/index.js +3 -3
  17. package/dist/EditingStatus/index.js +1 -1
  18. package/dist/{EditingStatus-NFADEPSX.css → EditingStatus-L6THUAN7.css} +21 -0
  19. package/dist/PresenterView/index.js +2 -2
  20. package/dist/RichEditor/index.js +1 -1
  21. package/dist/SheetGrid/index.js +3 -3
  22. package/dist/SlideCompare/index.js +2 -2
  23. package/dist/SlideFilmstrip/index.js +2 -2
  24. package/dist/SlideGrid/index.js +2 -2
  25. package/dist/SlideStage/SlideStage.d.ts +6 -1
  26. package/dist/SlideStage/index.js +1 -1
  27. package/dist/TreeCanvas/index.js +4 -2
  28. package/dist/{chunk-ALMSY7RL.js → chunk-CRKFL423.js} +1 -1
  29. package/dist/{chunk-O63MWA4G.js → chunk-E6CR5PEV.js} +2 -1
  30. package/dist/chunk-E6EWK5YY.js +33 -0
  31. package/dist/{chunk-3DPIU2Q7.js → chunk-FPS3JIPQ.js} +1 -1
  32. package/dist/{chunk-OGAAHVF6.js → chunk-MBNFJSZS.js} +1 -1
  33. package/dist/{chunk-OJ45PPCV.js → chunk-OTN35CI4.js} +113 -30
  34. package/dist/{chunk-JODJS7MR.js → chunk-PJD2KFHX.js} +243 -39
  35. package/dist/{chunk-LR5QTFOE.js → chunk-RV7SZAHR.js} +1 -1
  36. package/dist/{chunk-VOX3YRFO.js → chunk-V5IGUXMZ.js} +1 -1
  37. package/dist/{chunk-3C3BRIMY.js → chunk-VS3KOOOL.js} +1 -1
  38. package/dist/{chunk-S6VG5ZFZ.js → chunk-WTNUVQVI.js} +1 -1
  39. package/dist/generated/index.d.ts +1 -0
  40. package/dist/generated/index.js +49 -48
  41. package/dist/index.js +49 -48
  42. package/dist/styles.css +233 -36
  43. package/package.json +3 -3
  44. package/dist/{Editor-YZ7KG2RD.js → Editor-YWBUJAQU.js} +1 -1
  45. package/dist/{chunk-UVJ5FF5V.js → chunk-3ZAFJ4GK.js} +2 -2
  46. package/dist/{chunk-CT4EZF62.js → chunk-LWXRD6L6.js} +2 -2
  47. /package/dist/{chunk-OC46246T.js → chunk-TF2P3OPS.js} +0 -0
@@ -1,13 +1,29 @@
1
1
  import { LayerNodeBridge } from './chunk-VYOKRN2I.js';
2
+ import { Kbd } from './chunk-SMBKXWML.js';
3
+ import { Tooltip } from './chunk-QWOPX2MN.js';
2
4
  import { LAYER_REPOSITION_EVENT, LayerHost } from './chunk-HMPFGIWM.js';
3
5
  import { Button } from './chunk-OG6CL3EB.js';
4
6
  import { IconButton } from './chunk-4FQT6ZAY.js';
5
7
  import { VisuallyHidden } from './chunk-BZFWDIV6.js';
6
8
  import { cx } from './chunk-ODEQTPFM.js';
7
- import './Canvas-YVXHMWLN.css';
9
+ import './Canvas-LRVNW7XN.css';
8
10
  import { useId, useRef, useState, useCallback, useLayoutEffect, useImperativeHandle, useEffect } from 'react';
9
- import { ZoomOutGlyph, ZoomInGlyph, FullscreenGlyph } from '@crewlethq/icons/glyphs';
10
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
11
+ import { CURSORS, ZoomOutGlyph, ZoomInGlyph, FullscreenGlyph } from '@crewlethq/icons/glyphs';
12
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
13
+
14
+ function ShortcutTip({ label, keys, placement = "top", children }) {
15
+ return /* @__PURE__ */ jsx(
16
+ Tooltip,
17
+ {
18
+ placement,
19
+ content: /* @__PURE__ */ jsxs("span", { className: "crewlet-shortcut-tip", children: [
20
+ label,
21
+ /* @__PURE__ */ jsx(Kbd, { keys, subtle: true })
22
+ ] }),
23
+ children
24
+ }
25
+ );
26
+ }
11
27
 
12
28
  // src/Canvas/geometry.ts
13
29
  var IDENTITY_VIEW = { x: 0, y: 0, k: 1 };
@@ -26,9 +42,9 @@ var CANVAS_REVEAL_PADDING = 24;
26
42
  var CANVAS_PAN_STEP = 64;
27
43
  var CANVAS_TAP_SLOP = { mouse: 4, pen: 4, touch: 10 };
28
44
  var CANVAS_ZOOM_LIMITS = { min: CANVAS_MIN_ZOOM, max: CANVAS_MAX_ZOOM };
29
- function zoomLimits(content, viewport, largestItem, padding = CANVAS_FIT_PADDING) {
45
+ function zoomLimits(content, viewport, largestItem, padding = CANVAS_FIT_PADDING, floor = CANVAS_MIN_ZOOM) {
30
46
  const measured = content !== null && viewport.width > 0 && viewport.height > 0;
31
- const min = measured ? fitView(content, viewport, padding).k : CANVAS_MIN_ZOOM;
47
+ const min = measured ? fitView(content, viewport, padding, floor).k : floor;
32
48
  const room = viewport.width - 2 * padding;
33
49
  const wanted = largestItem !== void 0 && largestItem > 0 && room > 0 ? room / largestItem : CANVAS_MAX_ZOOM;
34
50
  const max = Math.min(CANVAS_ZOOM_CEILING, Math.max(CANVAS_MAX_ZOOM, wanted));
@@ -73,13 +89,13 @@ function clampPan(view, content, viewport, margin = CANVAS_PAN_MARGIN) {
73
89
  k: view.k
74
90
  };
75
91
  }
76
- function fitView(content, viewport, padding = CANVAS_FIT_PADDING) {
92
+ function fitView(content, viewport, padding = CANVAS_FIT_PADDING, floor = CANVAS_MIN_ZOOM) {
77
93
  if (viewport.width <= 0 || viewport.height <= 0) return IDENTITY_VIEW;
78
94
  const roomX = Math.max(0, viewport.width - 2 * padding);
79
95
  const roomY = Math.max(0, viewport.height - 2 * padding);
80
96
  const scaleX = content.width > 0 ? roomX / content.width : Infinity;
81
97
  const scaleY = content.height > 0 ? roomY / content.height : Infinity;
82
- const k = Math.max(CANVAS_MIN_ZOOM, Math.min(CANVAS_FIT_MAX_ZOOM, scaleX, scaleY));
98
+ const k = Math.max(floor, Math.min(CANVAS_FIT_MAX_ZOOM, scaleX, scaleY));
83
99
  const drawnHeight = content.height * k;
84
100
  const x = (viewport.width - content.width * k) / 2 - content.x * k;
85
101
  const y = drawnHeight <= roomY ? (viewport.height - drawnHeight) / 2 - content.y * k : padding - content.y * k;
@@ -145,10 +161,11 @@ var LABELS = {
145
161
  zoomIn: "Zoom in",
146
162
  zoomOut: "Zoom out",
147
163
  fit: "Fit to view",
164
+ actualSize: "Zoom to 100%",
148
165
  zoom: (percent) => `Zoom is ${percent} percent. Set a zoom level`,
149
166
  zoomLevel: "Zoom level, percent",
150
167
  done: "Done",
151
- keys: "Plus and minus zoom this canvas and zero fits everything in view. With the canvas itself focused, the arrow keys pan it."
168
+ keys: "Plus and minus zoom this canvas, zero or Shift 1 fits everything in view, and Shift 0 shows it at actual size. With the canvas itself focused, the arrow keys pan it."
152
169
  };
153
170
  var INTERACTIVE = "button, a[href], input, select, textarea, [contenteditable='true'], label";
154
171
  var TYPING = "input, textarea, select, [contenteditable='true']";
@@ -160,11 +177,15 @@ function Canvas({
160
177
  controls = true,
161
178
  controlsPlacement = "bottom-right",
162
179
  controlsExtra,
180
+ controlsActualSize = false,
163
181
  controlsBelow,
164
182
  hint,
165
183
  largestItemWidth,
166
184
  dimmed = false,
167
185
  ground = "plain",
186
+ minZoom = CANVAS_MIN_ZOOM,
187
+ cursor = "hand",
188
+ refitOnResize = false,
168
189
  touch = true,
169
190
  labels,
170
191
  onReady,
@@ -190,10 +211,17 @@ function Canvas({
190
211
  contentNow.current = content;
191
212
  touchNow.current = touchActive;
192
213
  sizeNow.current = size;
193
- const limits = zoomLimits(content, size, largestItemWidth);
214
+ const limits = zoomLimits(content, size, largestItemWidth, CANVAS_FIT_PADDING, minZoom);
194
215
  const limitsNow = useRef(limits);
195
216
  limitsNow.current = limits;
217
+ const floorNow = useRef(minZoom);
218
+ floorNow.current = minZoom;
219
+ const fitOf = useCallback(
220
+ (bounds, viewportSize = sizeNow.current) => fitView(bounds, viewportSize, CANVAS_FIT_PADDING, floorNow.current),
221
+ []
222
+ );
196
223
  const left = useRef(null);
224
+ const fitted = useRef(null);
197
225
  const apply = useCallback((next, eased) => {
198
226
  const bounds = contentNow.current;
199
227
  const clamped = bounds ? clampPan(next, bounds, sizeNow.current) : next;
@@ -203,6 +231,13 @@ function Canvas({
203
231
  viewNow.current = clamped;
204
232
  setView(clamped);
205
233
  }, []);
234
+ const fitNow = useCallback(
235
+ (bounds) => {
236
+ apply(fitOf(bounds), "near");
237
+ fitted.current = viewNow.current;
238
+ },
239
+ [apply, fitOf]
240
+ );
206
241
  useLayoutEffect(() => {
207
242
  const el = viewport.current;
208
243
  if (!el || typeof ResizeObserver === "undefined") return;
@@ -217,15 +252,29 @@ function Canvas({
217
252
  announceReady.current = onReady;
218
253
  useLayoutEffect(() => {
219
254
  if (ready || !content || size.width <= 0 || size.height <= 0) return;
220
- const first = fitView(content, size);
255
+ const first = fitOf(content, size);
221
256
  viewNow.current = first;
257
+ fitted.current = first;
222
258
  setView(first);
223
259
  setReady(true);
224
260
  announceReady.current?.();
225
- }, [ready, content, size]);
261
+ }, [ready, content, size, fitOf]);
262
+ const sizeSeen = useRef(size);
226
263
  useLayoutEffect(() => {
227
- if (ready) apply(viewNow.current);
228
- }, [ready, size, content, apply]);
264
+ if (!ready) return;
265
+ const resized = sizeSeen.current.width !== size.width || sizeSeen.current.height !== size.height;
266
+ sizeSeen.current = size;
267
+ const was = fitted.current;
268
+ const now = viewNow.current;
269
+ const untouched = was !== null && was.x === now.x && was.y === now.y && was.k === now.k;
270
+ if (refitOnResize && resized && untouched && content) {
271
+ apply(fitOf(content), false);
272
+ fitted.current = viewNow.current;
273
+ } else {
274
+ apply(now);
275
+ if (untouched) fitted.current = viewNow.current;
276
+ }
277
+ }, [ready, size, content, apply, fitOf, refitOnResize]);
229
278
  const reportView = useRef(onViewChange);
230
279
  reportView.current = onViewChange;
231
280
  useLayoutEffect(() => {
@@ -239,17 +288,26 @@ function Canvas({
239
288
  setAnimate(false);
240
289
  layer?.dispatchEvent(new CustomEvent(LAYER_REPOSITION_EVENT, { detail: viewNow.current }));
241
290
  };
291
+ const local = useCallback((event) => {
292
+ const rect = viewport.current?.getBoundingClientRect();
293
+ return { x: event.clientX - (rect?.left ?? 0), y: event.clientY - (rect?.top ?? 0) };
294
+ }, []);
242
295
  useImperativeHandle(
243
296
  ref,
244
297
  () => ({
245
298
  fit: () => {
246
299
  const bounds = contentNow.current;
247
- if (bounds) apply(fitView(bounds, sizeNow.current), "near");
300
+ if (bounds) fitNow(bounds);
248
301
  },
249
302
  zoomBy: (factor) => {
250
303
  const { width, height } = sizeNow.current;
251
304
  apply(zoomAt(viewNow.current, factor, { x: width / 2, y: height / 2 }, limitsNow.current), "near");
252
305
  },
306
+ zoomTo: (scale) => {
307
+ const { width, height } = sizeNow.current;
308
+ const factor = scale / viewNow.current.k;
309
+ apply(zoomAt(viewNow.current, factor, { x: width / 2, y: height / 2 }, limitsNow.current), "near");
310
+ },
253
311
  reveal: (target) => {
254
312
  const next = revealView(viewNow.current, target, sizeNow.current);
255
313
  if (next.x !== viewNow.current.x || next.y !== viewNow.current.y) apply(next, "near");
@@ -268,9 +326,10 @@ function Canvas({
268
326
  left.current = null;
269
327
  apply(was, "travel");
270
328
  },
271
- screenRect: (target) => screenRect(viewNow.current, target)
329
+ screenRect: (target) => screenRect(viewNow.current, target),
330
+ worldAt: (client) => toWorld(viewNow.current, local({ clientX: client.x, clientY: client.y }))
272
331
  }),
273
- [apply]
332
+ [apply, fitNow, local]
274
333
  );
275
334
  useEffect(() => {
276
335
  const el = viewport.current;
@@ -294,10 +353,6 @@ function Canvas({
294
353
  }, [apply]);
295
354
  const pointers = useRef(/* @__PURE__ */ new Map());
296
355
  const gesture = useRef(null);
297
- const local = useCallback((event) => {
298
- const rect = viewport.current?.getBoundingClientRect();
299
- return { x: event.clientX - (rect?.left ?? 0), y: event.clientY - (rect?.top ?? 0) };
300
- }, []);
301
356
  const endGesture = useCallback(() => {
302
357
  gesture.current = null;
303
358
  setPanning(false);
@@ -411,12 +466,17 @@ function Canvas({
411
466
  const { width, height } = sizeNow.current;
412
467
  const centre = { x: width / 2, y: height / 2 };
413
468
  let handled = true;
414
- if (event.key === "+" || mod && event.key === "=") {
469
+ const shiftDigit = event.shiftKey && !mod && !event.altKey ? event.code : "";
470
+ if (shiftDigit === "Digit0") {
471
+ centreZoom(1 / viewNow.current.k);
472
+ } else if (shiftDigit === "Digit1") {
473
+ if (content) fitNow(content);
474
+ } else if (event.key === "+" || mod && event.key === "=") {
415
475
  apply(zoomAt(viewNow.current, CANVAS_ZOOM_STEP, centre, limitsNow.current), "near");
416
476
  } else if (event.key === "-") {
417
477
  apply(zoomAt(viewNow.current, 1 / CANVAS_ZOOM_STEP, centre, limitsNow.current), "near");
418
478
  } else if (event.key === "0") {
419
- if (content) apply(fitView(content, sizeNow.current), "near");
479
+ if (content) fitNow(content);
420
480
  } else if (!onViewport) {
421
481
  handled = false;
422
482
  } else if (!mod && !event.altKey && event.key.startsWith("Arrow")) {
@@ -455,6 +515,8 @@ function Canvas({
455
515
  {
456
516
  className: cx("crewlet-canvas", className),
457
517
  ref: root,
518
+ style: CURSOR_PROPERTIES,
519
+ "data-cursor": cursor,
458
520
  "data-dimmed": dimmed,
459
521
  "data-ground": ground,
460
522
  "data-ready": ready,
@@ -501,16 +563,17 @@ function Canvas({
501
563
  (controls || touchActive) && /* @__PURE__ */ jsxs("div", { className: "crewlet-canvas__bar", children: [
502
564
  touchActive && /* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", onClick: () => setTouchActive(false), children: text.done }),
503
565
  controls && /* @__PURE__ */ jsxs(Fragment, { children: [
504
- /* @__PURE__ */ jsx(
566
+ /* @__PURE__ */ jsx(ShortcutTip, { label: text.zoomOut, keys: ["-"], children: /* @__PURE__ */ jsx(
505
567
  IconButton,
506
568
  {
507
569
  size: "sm",
508
570
  label: text.zoomOut,
571
+ title: "",
509
572
  icon: /* @__PURE__ */ jsx(ZoomOutGlyph, {}),
510
573
  onClick: () => centreZoom(1 / CANVAS_ZOOM_STEP),
511
574
  disabled: !ready
512
575
  }
513
- ),
576
+ ) }),
514
577
  /* @__PURE__ */ jsx(
515
578
  ZoomReadout,
516
579
  {
@@ -521,26 +584,39 @@ function Canvas({
521
584
  disabled: !ready
522
585
  }
523
586
  ),
524
- /* @__PURE__ */ jsx(
587
+ /* @__PURE__ */ jsx(ShortcutTip, { label: text.zoomIn, keys: ["+"], children: /* @__PURE__ */ jsx(
525
588
  IconButton,
526
589
  {
527
590
  size: "sm",
528
591
  label: text.zoomIn,
592
+ title: "",
529
593
  icon: /* @__PURE__ */ jsx(ZoomInGlyph, {}),
530
594
  onClick: () => centreZoom(CANVAS_ZOOM_STEP),
531
595
  disabled: !ready
532
596
  }
533
- ),
534
- /* @__PURE__ */ jsx(
597
+ ) }),
598
+ controlsActualSize && /* @__PURE__ */ jsx(ShortcutTip, { label: text.actualSize, keys: ["Shift", "0"], children: /* @__PURE__ */ jsx(
599
+ "button",
600
+ {
601
+ type: "button",
602
+ className: "crewlet-canvas__zoom crewlet-canvas__actual",
603
+ "aria-label": text.actualSize,
604
+ onClick: () => centreZoom(1 / viewNow.current.k),
605
+ disabled: !ready,
606
+ children: "1:1"
607
+ }
608
+ ) }),
609
+ /* @__PURE__ */ jsx(ShortcutTip, { label: text.fit, keys: ["Shift", "1"], children: /* @__PURE__ */ jsx(
535
610
  IconButton,
536
611
  {
537
612
  size: "sm",
538
613
  label: text.fit,
614
+ title: "",
539
615
  icon: /* @__PURE__ */ jsx(FullscreenGlyph, {}),
540
- onClick: () => content && apply(fitView(content, sizeNow.current), "near"),
616
+ onClick: () => content && fitNow(content),
541
617
  disabled: !ready
542
618
  }
543
- ),
619
+ ) }),
544
620
  controlsExtra
545
621
  ] })
546
622
  ] }),
@@ -552,6 +628,13 @@ function Canvas({
552
628
  }
553
629
  );
554
630
  }
631
+ var CURSOR_PROPERTIES = {
632
+ "--crewlet-canvas-cursor-pointer": CURSORS.pointer,
633
+ "--crewlet-canvas-cursor-hand": CURSORS.hand,
634
+ "--crewlet-canvas-cursor-grabbing": CURSORS.grabbing,
635
+ "--crewlet-canvas-cursor-crosshair": CURSORS.crosshair,
636
+ "--crewlet-canvas-cursor-text": CURSORS.text
637
+ };
555
638
  function ZoomReadout({
556
639
  zoom,
557
640
  onZoom,
@@ -633,4 +716,4 @@ function swallowNextClick(within) {
633
716
  setTimeout(() => window.removeEventListener("click", swallow, { capture: true }), 0);
634
717
  }
635
718
 
636
- export { CANVAS_COMPOSE_CONTEXT, CANVAS_FIT_MAX_ZOOM, CANVAS_FIT_PADDING, CANVAS_FOCUS_CONTEXT, CANVAS_MAX_ZOOM, CANVAS_MIN_ZOOM, CANVAS_PAN_MARGIN, CANVAS_PAN_STEP, CANVAS_REVEAL_PADDING, CANVAS_TAP_SLOP, CANVAS_WHEEL_LINE_PX, CANVAS_WHEEL_ZOOM_PER_PIXEL, CANVAS_ZOOM_CEILING, CANVAS_ZOOM_LIMITS, CANVAS_ZOOM_STEP, Canvas, IDENTITY_VIEW, anchorView, beyondSlop, clampPan, clampZoom, fitView, panBy, pinchView, regionView, revealView, screenRect, toScreen, toWorld, wheelPixels, wheelZoomFactor, zoomAt, zoomLimits };
719
+ export { CANVAS_COMPOSE_CONTEXT, CANVAS_FIT_MAX_ZOOM, CANVAS_FIT_PADDING, CANVAS_FOCUS_CONTEXT, CANVAS_MAX_ZOOM, CANVAS_MIN_ZOOM, CANVAS_PAN_MARGIN, CANVAS_PAN_STEP, CANVAS_REVEAL_PADDING, CANVAS_TAP_SLOP, CANVAS_WHEEL_LINE_PX, CANVAS_WHEEL_ZOOM_PER_PIXEL, CANVAS_ZOOM_CEILING, CANVAS_ZOOM_LIMITS, CANVAS_ZOOM_STEP, Canvas, IDENTITY_VIEW, ShortcutTip, anchorView, beyondSlop, clampPan, clampZoom, fitView, panBy, pinchView, regionView, revealView, screenRect, toScreen, toWorld, wheelPixels, wheelZoomFactor, zoomAt, zoomLimits };