@dfosco/storyboard 0.10.2 → 0.10.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Storyboard prototyping framework — core engine, React integration, and canvas",
@@ -117,7 +117,7 @@
117
117
  "remark-gfm": "^4.0.1",
118
118
  "remark-html": "^16.0.1",
119
119
  "tailwind-merge": "^3.5.0",
120
- "ws": "^8.0.0",
120
+ "ws": "^8.21.0",
121
121
  "zod": "^3.23.8"
122
122
  },
123
123
  "peerDependencies": {
@@ -194,7 +194,7 @@
194
194
  "tailwindcss": "^4.2.2",
195
195
  "tw-animate-css": "^1.4.0",
196
196
  "typescript": "^5.6.0",
197
- "vite": "^7.3.1"
197
+ "vite": "^7.3.5"
198
198
  },
199
199
  "optionalDependencies": {
200
200
  "node-pty": "^1.0.0"
@@ -329,7 +329,7 @@ export const configDefaults = {
329
329
  surfaces: {
330
330
  prototype: { label: 'Prototype', sync: true },
331
331
  canvas: { label: 'Canvas', sync: true },
332
- toolbar: { label: 'Tools', sync: false },
332
+ toolbar: { label: 'Tools', sync: true },
333
333
  codeBoxes: { label: 'Code boxes', sync: true },
334
334
  },
335
335
  // Default global theme. "system" follows OS prefers-color-scheme.
@@ -64,6 +64,35 @@ function zoomLimits() {
64
64
  return { ZOOM_MIN: z.min, ZOOM_MAX: z.max, ZOOM_STEP: z.step }
65
65
  }
66
66
 
67
+ /**
68
+ * Compute the .canvasZoom wrapper's layout width/height for a given scale,
69
+ * honoring `canvas.surface.{width,height}`.
70
+ *
71
+ * Default ("auto"): `max(10000, 100 / scale) <axis-unit>` — keeps the
72
+ * surface huge so users can pan into an infinite-feeling canvas; layout
73
+ * shrinks with zoom-in but the 10000-unit floor keeps scroll available.
74
+ *
75
+ * Viewport: the surface clamps to 100<axis-unit> at scale = 1, then grows
76
+ * proportionally with scale on zoom-in (`100 * scale <axis-unit>`) so the
77
+ * scroller still produces horizontal/vertical scroll above 100% zoom.
78
+ * At scale < 1 the layout stays at 100<axis-unit> (zoom-out doesn't add
79
+ * extra empty space).
80
+ */
81
+ function getZoomLayoutDim(scale, surfaceAxis, axisUnit) {
82
+ if (surfaceAxis === 'viewport') {
83
+ return `${Math.max(100, 100 * scale)}${axisUnit}`
84
+ }
85
+ return `${Math.max(10000, 100 / scale)}${axisUnit}`
86
+ }
87
+
88
+ function getZoomLayoutWidth(scale, surface) {
89
+ return getZoomLayoutDim(scale, surface?.width, 'vw')
90
+ }
91
+
92
+ function getZoomLayoutHeight(scale, surface) {
93
+ return getZoomLayoutDim(scale, surface?.height, 'vh')
94
+ }
95
+
67
96
  /** Saved viewport state older than this is considered stale — zoom-to-fit instead. */
68
97
  const VIEWPORT_TTL_MS = 15 * 60 * 1000
69
98
 
@@ -135,7 +164,7 @@ function getConnectedComponent(widgetId, connectors) {
135
164
 
136
165
  function resolveCanvasThemeFromStorage() {
137
166
  if (typeof localStorage === 'undefined') return 'light'
138
- let sync = { prototype: true, toolbar: false, codeBoxes: true, canvas: true }
167
+ let sync = { prototype: true, toolbar: true, codeBoxes: true, canvas: true }
139
168
  try {
140
169
  const rawSync = localStorage.getItem('sb-theme-sync')
141
170
  if (rawSync) sync = { ...sync, ...JSON.parse(rawSync) }
@@ -2058,8 +2087,8 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
2058
2087
  const zoomEl = zoomElRef.current
2059
2088
  if (zoomEl) {
2060
2089
  zoomEl.style.transform = `scale(${newScale})`
2061
- zoomEl.style.width = `${Math.max(10000, 100 / newScale)}vw`
2062
- zoomEl.style.height = `${Math.max(10000, 100 / newScale)}vh`
2090
+ zoomEl.style.width = getZoomLayoutWidth(newScale, interactionRef.current?.surface)
2091
+ zoomEl.style.height = getZoomLayoutHeight(newScale, interactionRef.current?.surface)
2063
2092
  }
2064
2093
  setZoom(fitZoom)
2065
2094
  el.scrollLeft = (bounds.minX - FIT_PADDING) * newScale
@@ -2281,8 +2310,8 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
2281
2310
  // Imperative DOM update — no React re-render
2282
2311
  zoomRef.current = clampedZoom
2283
2312
  zoomEl.style.transform = `scale(${newScale})`
2284
- zoomEl.style.width = `${Math.max(10000, 100 / newScale)}vw`
2285
- zoomEl.style.height = `${Math.max(10000, 100 / newScale)}vh`
2313
+ zoomEl.style.width = getZoomLayoutWidth(newScale, interactionRef.current?.surface)
2314
+ zoomEl.style.height = getZoomLayoutHeight(newScale, interactionRef.current?.surface)
2286
2315
 
2287
2316
  // Hint GPU compositing during active zoom
2288
2317
  zoomEl.dataset.zooming = ''
@@ -2618,8 +2647,8 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
2618
2647
  const zoomEl = zoomElRef.current
2619
2648
  if (zoomEl) {
2620
2649
  zoomEl.style.transform = `scale(${newScale})`
2621
- zoomEl.style.width = `${Math.max(10000, 100 / newScale)}vw`
2622
- zoomEl.style.height = `${Math.max(10000, 100 / newScale)}vh`
2650
+ zoomEl.style.width = getZoomLayoutWidth(newScale, interactionRef.current?.surface)
2651
+ zoomEl.style.height = getZoomLayoutHeight(newScale, interactionRef.current?.surface)
2623
2652
  }
2624
2653
  setZoom(fitZoom)
2625
2654
 
@@ -2852,8 +2881,8 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
2852
2881
  const zoomEl = zoomElRef.current
2853
2882
  if (zoomEl) {
2854
2883
  zoomEl.style.transform = `scale(${newScale})`
2855
- zoomEl.style.width = `${Math.max(10000, 100 / newScale)}vw`
2856
- zoomEl.style.height = `${Math.max(10000, 100 / newScale)}vh`
2884
+ zoomEl.style.width = getZoomLayoutWidth(newScale, interactionRef.current?.surface)
2885
+ zoomEl.style.height = getZoomLayoutHeight(newScale, interactionRef.current?.surface)
2857
2886
  }
2858
2887
  setZoom(targetZoom)
2859
2888
  }
@@ -4098,8 +4127,8 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
4098
4127
  style={{
4099
4128
  transform: `scale(${scale})`,
4100
4129
  transformOrigin: '0 0',
4101
- width: `${Math.max(10000, 100 / scale)}vw`,
4102
- height: `${Math.max(10000, 100 / scale)}vh`,
4130
+ width: getZoomLayoutWidth(scale, interactionRef.current?.surface),
4131
+ height: getZoomLayoutHeight(scale, interactionRef.current?.surface),
4103
4132
  ...(spaceHeld ? { pointerEvents: 'none' } : {}),
4104
4133
  }}
4105
4134
  >
@@ -41,23 +41,6 @@
41
41
  will-change: transform;
42
42
  }
43
43
 
44
- /* When the consumer sets `canvas.surface.width/height` to "viewport"
45
- (see canvasInteractionStore.js), the canvas surface clamps to 100vw/100vh
46
- — but the .canvasZoom wrapper still has its inline `max(10000, 100/scale)vw`
47
- sizing, which would leave a huge stretch of empty wrapper to the right /
48
- below the canvas and let the user scroll into it. Clamp the wrapper to
49
- the viewport too. !important is required to beat the inline style set
50
- by applyZoom(). Each axis is independent — only the configured axis is
51
- constrained. */
52
- :global(html[data-canvas-surface-width="viewport"]) .canvasZoom {
53
- width: 100vw !important;
54
- min-width: 100vw !important;
55
- }
56
- :global(html[data-canvas-surface-height="viewport"]) .canvasZoom {
57
- height: 100vh !important;
58
- min-height: 100vh !important;
59
- }
60
-
61
44
  /* Selection outline is now handled by WidgetChrome.module.css (.widgetSlotSelected) */
62
45
 
63
46
  .canvasLogo {
@@ -28,6 +28,22 @@
28
28
  transition: opacity 0.2s ease;
29
29
  }
30
30
 
31
+ /* When the consumer sets `canvas.surface.{width,height}` to "viewport"
32
+ (see canvasInteractionStore.js), the canvas surface clamps to 100vw/100vh.
33
+ The SVG layers below are siblings of .tc-canvas inside .canvasZoom and
34
+ have a hardcoded `width: 100000px; height: 100000px` inline style — so
35
+ without clamping them too, they grow the scroll container even though
36
+ .tc-canvas is viewport-sized. Override with !important to beat the
37
+ inline width/height. Each axis is independent. */
38
+ :global(html[data-canvas-surface-width="viewport"]) .connectorLayer,
39
+ :global(html[data-canvas-surface-width="viewport"]) .endpointLayer {
40
+ width: 100vw !important;
41
+ }
42
+ :global(html[data-canvas-surface-height="viewport"]) .connectorLayer,
43
+ :global(html[data-canvas-surface-height="viewport"]) .endpointLayer {
44
+ height: 100vh !important;
45
+ }
46
+
31
47
  .connectorPath {
32
48
  fill: none;
33
49
  stroke: var(--connector-stroke, var(--color-foreground-accent, #0969da));
@@ -31,7 +31,7 @@ function formatName(name) {
31
31
 
32
32
  function resolveCanvasThemeFromStorage() {
33
33
  if (typeof localStorage === 'undefined') return 'light'
34
- let sync = { prototype: true, toolbar: false, codeBoxes: true, canvas: false }
34
+ let sync = { prototype: true, toolbar: true, codeBoxes: true, canvas: false }
35
35
  try {
36
36
  const rawSync = localStorage.getItem('sb-theme-sync')
37
37
  if (rawSync) sync = { ...sync, ...JSON.parse(rawSync) }
@@ -49,7 +49,7 @@ describe('useThemeSyncTargets', () => {
49
49
  it('returns default sync targets', () => {
50
50
  const { result } = renderHook(() => useThemeSyncTargets())
51
51
  expect(result.current.prototype).toBe(true)
52
- expect(result.current.toolbar).toBe(false)
52
+ expect(result.current.toolbar).toBe(true)
53
53
  expect(result.current.codeBoxes).toBe(true)
54
54
  expect(result.current.canvas).toBe(true)
55
55
  })