@dfosco/storyboard 0.8.1 → 0.9.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 (114) hide show
  1. package/dist/storyboard-ui.css +1 -1
  2. package/dist/storyboard-ui.js +6420 -6336
  3. package/dist/storyboard-ui.js.map +1 -1
  4. package/dist/tailwind.css +1 -1
  5. package/dist/tiny-canvas.css +1 -1
  6. package/package.json +1 -1
  7. package/scaffold/src/library/_app.jsx +2 -14
  8. package/scaffold/src/library/_app.module.css +2 -8
  9. package/scaffold/src/library/mount.jsx +0 -1
  10. package/src/canvas/style.css +16 -16
  11. package/src/core/artifact/operations.js +35 -2
  12. package/src/core/artifact/schemas/canvas.schema.json +7 -1
  13. package/src/core/canvas/hot-pool.js +11 -22
  14. package/src/core/canvas/selectedWidgets.js +1 -1
  15. package/src/core/canvas/server.js +52 -31
  16. package/src/core/canvas/server.test.js +107 -0
  17. package/src/core/cli/artifact.js +2 -0
  18. package/src/core/cli/create.js +35 -5
  19. package/src/core/cli/schemas.js +6 -1
  20. package/src/core/comments/ui/comment-layout.css +14 -14
  21. package/src/core/index.js +18 -0
  22. package/src/core/inspector/highlighter.js +1 -1
  23. package/src/core/inspector/mouseMode.js +2 -2
  24. package/src/core/lib/components/ui/dialog/dialog.module.css +8 -8
  25. package/src/core/modes/modes.css +4 -4
  26. package/src/core/mountStoryboardCore.js +53 -9
  27. package/src/core/mountStoryboardCore.test.js +25 -1
  28. package/src/core/rename-watcher/config.json +1 -1
  29. package/src/core/rename-watcher/watcher.js +1 -1
  30. package/src/core/stores/canvasInteractionStore.js +120 -0
  31. package/src/core/stores/canvasInteractionStore.test.js +126 -0
  32. package/src/core/stores/configSchema.js +23 -6
  33. package/src/core/stores/configSchema.test.js +3 -0
  34. package/src/core/stores/customerModeConfig.js +24 -3
  35. package/src/core/stores/customerModeConfig.test.js +46 -0
  36. package/src/core/stores/presentationStore.js +118 -0
  37. package/src/core/stores/presentationStore.test.js +114 -0
  38. package/src/core/stores/uiConfig.js +10 -2
  39. package/src/core/styles/tailwind.css +138 -90
  40. package/src/core/styles/workspace-chrome.css +4 -21
  41. package/src/core/ui/AutosyncMenuButton.css +7 -7
  42. package/src/core/ui/BranchSelect.module.css +5 -5
  43. package/src/core/ui/ChromeSlot.jsx +92 -0
  44. package/src/core/ui/ChromeSlot.test.jsx +140 -0
  45. package/src/core/ui/CoreUIBar.css +5 -5
  46. package/src/core/ui/CoreUIBar.jsx +161 -136
  47. package/src/core/ui/InspectorPanel.css +5 -5
  48. package/src/core/ui/InspectorPanel.jsx +9 -9
  49. package/src/core/ui/PwaInstallBanner.css +5 -5
  50. package/src/core/ui/PwaInstallBanner.jsx +5 -5
  51. package/src/core/ui/core-ui-colors.css +28 -66
  52. package/src/core/ui/sidepanel.css +12 -12
  53. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +102 -13
  54. package/src/internals/ArtifactForm/ArtifactForm.module.css +7 -7
  55. package/src/internals/AuthModal/AuthModal.module.css +34 -34
  56. package/src/internals/BaseUiForm/BaseUiForm.module.css +85 -85
  57. package/src/internals/BranchBar/BranchBar.module.css +4 -4
  58. package/src/internals/BranchesDropdown/BranchesDropdown.module.css +9 -9
  59. package/src/internals/CommandPalette/CommandPalette.jsx +16 -10
  60. package/src/internals/CommandPalette/WidgetArtifactDialog.css +16 -16
  61. package/src/internals/CommandPalette/command-palette.css +14 -14
  62. package/src/internals/CreatePage/CreatePage.module.css +1 -1
  63. package/src/internals/FlowError.module.css +5 -5
  64. package/src/internals/PrototypeErrorBoundary.module.css +15 -15
  65. package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +5 -4
  66. package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +38 -38
  67. package/src/internals/Viewfinder.jsx +33 -16
  68. package/src/internals/Viewfinder.module.css +189 -189
  69. package/src/internals/canvas/CanvasControls.module.css +11 -11
  70. package/src/internals/canvas/CanvasPage.bridge.test.jsx +11 -37
  71. package/src/internals/canvas/CanvasPage.jsx +91 -93
  72. package/src/internals/canvas/CanvasPage.module.css +20 -19
  73. package/src/internals/canvas/CanvasToolbar.module.css +11 -11
  74. package/src/internals/canvas/ConnectorLayer.jsx +39 -2
  75. package/src/internals/canvas/ConnectorLayer.module.css +10 -10
  76. package/src/internals/canvas/PageSelector.module.css +28 -28
  77. package/src/internals/canvas/canvasTheme.js +22 -111
  78. package/src/internals/canvas/isolation/prototypeRoutes.jsx +12 -3
  79. package/src/internals/canvas/isolation/storyRoutes.jsx +1 -1
  80. package/src/internals/canvas/widgets/CodePenEmbed.jsx +16 -36
  81. package/src/internals/canvas/widgets/CodePenEmbed.module.css +12 -12
  82. package/src/internals/canvas/widgets/CropOverlay.module.css +23 -23
  83. package/src/internals/canvas/widgets/ExpandedPane.module.css +8 -8
  84. package/src/internals/canvas/widgets/FigmaEmbed.jsx +15 -36
  85. package/src/internals/canvas/widgets/FigmaEmbed.module.css +15 -15
  86. package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +5 -5
  87. package/src/internals/canvas/widgets/ImageWidget.module.css +3 -3
  88. package/src/internals/canvas/widgets/LinkPreview.module.css +77 -78
  89. package/src/internals/canvas/widgets/MarkdownBlock.module.css +31 -31
  90. package/src/internals/canvas/widgets/PromptWidget.jsx +2 -16
  91. package/src/internals/canvas/widgets/PromptWidget.module.css +33 -33
  92. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +23 -7
  93. package/src/internals/canvas/widgets/PrototypeEmbed.module.css +58 -56
  94. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +4 -4
  95. package/src/internals/canvas/widgets/ResizeHandle.module.css +4 -4
  96. package/src/internals/canvas/widgets/StorySetWidget.jsx +45 -21
  97. package/src/internals/canvas/widgets/StoryWidget.jsx +53 -21
  98. package/src/internals/canvas/widgets/StoryWidget.module.css +22 -20
  99. package/src/internals/canvas/widgets/TerminalReadWidget.module.css +4 -4
  100. package/src/internals/canvas/widgets/TerminalWidget.jsx +56 -45
  101. package/src/internals/canvas/widgets/TerminalWidget.module.css +10 -10
  102. package/src/internals/canvas/widgets/TilesWidget.module.css +17 -17
  103. package/src/internals/canvas/widgets/WidgetChrome.module.css +61 -61
  104. package/src/internals/canvas/widgets/WidgetWrapper.module.css +2 -2
  105. package/src/internals/canvas/widgets/embedOverlay.module.css +14 -4
  106. package/src/internals/canvas/widgets/embedTheme.js +10 -102
  107. package/src/internals/canvas/widgets/widgetConfig.js +5 -5
  108. package/src/internals/story/ComponentSetPage.module.css +14 -14
  109. package/src/internals/story/StoryPage.module.css +2 -2
  110. package/src/internals/vite/data-plugin.js +5 -2
  111. package/src/internals/canvas/WebGLContextPool.jsx +0 -291
  112. package/src/internals/canvas/WebGLContextPool.test.jsx +0 -165
  113. package/src/internals/canvas/widgets/ComponentSetWidget.module.css +0 -89
  114. package/src/internals/canvas/widgets/StorySetWidget.module.css +0 -89
@@ -1,291 +0,0 @@
1
- import { createContext, useContext, useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'
2
-
3
- /**
4
- * WebGL Context Pool — manages live WebGL slots for terminal widgets.
5
- *
6
- * Browsers cap WebGL contexts at ~8-16. This pool ensures only a subset
7
- * of terminal widgets hold live ghostty-web renderers at any time.
8
- * Offscreen terminals release their context and show a frozen snapshot.
9
- *
10
- * Architecture:
11
- * - Lease-based: widgets register and receive live/frozen status
12
- * - Priority: PINNED > VISIBLE > NEAR_VIEWPORT > OFFSCREEN
13
- * - Hysteresis: 3s grace before evicting recently-visible terminals
14
- * - Generation tokens prevent stale async opens after eviction
15
- */
16
-
17
- // Priority levels (higher = more important to keep live)
18
- export const Priority = {
19
- OFFSCREEN: 0,
20
- NEAR_VIEWPORT: 1,
21
- VISIBLE: 2,
22
- PINNED: 3, // expanded, focused, or actively interacting
23
- }
24
-
25
- const DEFAULT_MAX_LIVE = 6
26
- const HYSTERESIS_MS = 3000
27
-
28
- // ── Pool Engine (framework-agnostic) ─────────────────────────────────
29
-
30
- class ContextPool {
31
- constructor(maxLive = DEFAULT_MAX_LIVE) {
32
- this._maxLive = maxLive
33
- /** @type {Map<string, { priority: number, generation: number, live: boolean, lastVisible: number }>} */
34
- this._slots = new Map()
35
- this._listeners = new Set()
36
- this._hysteresisTimers = new Map()
37
- // Monotonic version counter — bumped on every state change so
38
- // useSyncExternalStore detects updates via a new snapshot value.
39
- this._version = 0
40
- }
41
-
42
- /** Subscribe to pool state changes. Returns unsubscribe fn. */
43
- subscribe(listener) {
44
- this._listeners.add(listener)
45
- return () => this._listeners.delete(listener)
46
- }
47
-
48
- _notify() {
49
- this._version++
50
- for (const fn of this._listeners) fn()
51
- }
52
-
53
- /** Register a widget. Returns its initial generation. */
54
- register(widgetId, initialPriority = Priority.OFFSCREEN) {
55
- if (this._slots.has(widgetId)) return this._slots.get(widgetId).generation
56
- this._slots.set(widgetId, {
57
- priority: initialPriority,
58
- generation: 0,
59
- live: false,
60
- lastVisible: initialPriority >= Priority.VISIBLE ? Date.now() : 0,
61
- })
62
- this._recompute()
63
- return 0
64
- }
65
-
66
- /** Unregister a widget (on unmount). */
67
- unregister(widgetId) {
68
- this._slots.delete(widgetId)
69
- const timer = this._hysteresisTimers.get(widgetId)
70
- if (timer) { clearTimeout(timer); this._hysteresisTimers.delete(widgetId) }
71
- this._recompute()
72
- }
73
-
74
- /** Update a widget's priority. Triggers recomputation. */
75
- setPriority(widgetId, priority) {
76
- const slot = this._slots.get(widgetId)
77
- if (!slot) return
78
- const prev = slot.priority
79
- slot.priority = priority
80
-
81
- // Track when widget was last visible/pinned for hysteresis
82
- if (priority >= Priority.VISIBLE) {
83
- slot.lastVisible = Date.now()
84
- }
85
-
86
- // Immediate recompute when priority increases or widget is pinned
87
- if (priority > prev || priority === Priority.PINNED) {
88
- this._recompute()
89
- return
90
- }
91
-
92
- // When priority drops (e.g. scrolled offscreen), use hysteresis
93
- if (priority < prev && prev >= Priority.VISIBLE) {
94
- const existing = this._hysteresisTimers.get(widgetId)
95
- if (existing) clearTimeout(existing)
96
- this._hysteresisTimers.set(widgetId, setTimeout(() => {
97
- this._hysteresisTimers.delete(widgetId)
98
- this._recompute()
99
- }, HYSTERESIS_MS))
100
- return
101
- }
102
-
103
- this._recompute()
104
- }
105
-
106
- /** Get current state for a widget. */
107
- getSlot(widgetId) {
108
- return this._slots.get(widgetId) || null
109
- }
110
-
111
- /**
112
- * Recompute which widgets should be live.
113
- * Sorts by priority (desc), then by lastVisible (desc).
114
- * Top N get live status; rest are frozen.
115
- */
116
- _recompute() {
117
- const entries = [...this._slots.entries()]
118
-
119
- // Sort: pinned first, then by priority desc, then by recency
120
- entries.sort(([, a], [, b]) => {
121
- if (a.priority !== b.priority) return b.priority - a.priority
122
- return b.lastVisible - a.lastVisible
123
- })
124
-
125
- let liveCount = 0
126
- const changes = []
127
-
128
- for (const [id, slot] of entries) {
129
- // Pinned terminals always get a slot (they bypass the cap)
130
- const shouldBeLive = slot.priority === Priority.PINNED || liveCount < this._maxLive
131
- if (shouldBeLive) liveCount++
132
-
133
- if (slot.live !== shouldBeLive) {
134
- changes.push({ id, slot, shouldBeLive })
135
- }
136
- }
137
-
138
- if (changes.length === 0) return
139
-
140
- for (const { slot, shouldBeLive } of changes) {
141
- slot.live = shouldBeLive
142
- if (!shouldBeLive) {
143
- // Bump generation so stale async opens abort
144
- slot.generation++
145
- }
146
- }
147
-
148
- this._notify()
149
- }
150
- }
151
-
152
- // ── React Context ────────────────────────────────────────────────────
153
-
154
- const WebGLPoolContext = createContext(null)
155
-
156
- /**
157
- * Provider that creates and owns the context pool.
158
- * Place this around the canvas widget tree in CanvasPage.
159
- *
160
- * @param {{ maxLive?: number, children: React.ReactNode }} props
161
- */
162
- export function WebGLContextPoolProvider({ maxLive = DEFAULT_MAX_LIVE, children }) {
163
- // useMemo is correct here: the pool is a stable singleton for this provider's lifetime.
164
- // eslint-disable-next-line react-hooks/exhaustive-deps
165
- const pool = useMemo(() => new ContextPool(maxLive), [])
166
- return (
167
- <WebGLPoolContext.Provider value={pool}>
168
- {children}
169
- </WebGLPoolContext.Provider>
170
- )
171
- }
172
-
173
- /**
174
- * Hook that manages a terminal widget's WebGL slot lifecycle.
175
- *
176
- * Returns:
177
- * - `isLive`: whether this widget should create a live ghostty terminal
178
- * - `generation`: counter that increments on each live→frozen transition;
179
- * use as an effect dep to detect when to re-create the terminal
180
- * - `setPriority(p)`: update this widget's priority
181
- *
182
- * @param {string} widgetId
183
- * @returns {{ isLive: boolean, generation: number, setPriority: (p: number) => void }}
184
- */
185
- export function useWebGLSlot(widgetId, initialPriority) {
186
- const pool = useContext(WebGLPoolContext)
187
-
188
- // Register on mount, unregister on unmount
189
- useEffect(() => {
190
- if (!pool) return
191
- pool.register(widgetId, initialPriority)
192
- return () => pool.unregister(widgetId)
193
- }, [pool, widgetId]) // initialPriority intentionally excluded — only used on first register
194
-
195
- // Subscribe to pool state for reactivity.
196
- // getSnapshot returns the pool's version counter — a primitive that changes
197
- // on every state mutation, satisfying useSyncExternalStore's identity check.
198
- const subscribe = useCallback(
199
- (cb) => pool ? pool.subscribe(cb) : () => {},
200
- [pool],
201
- )
202
- const getSnapshot = useCallback(() => {
203
- if (!pool) return -1
204
- return pool._version
205
- }, [pool])
206
-
207
- // This triggers re-render whenever the pool version changes
208
- useSyncExternalStore(subscribe, getSnapshot)
209
-
210
- // Read the actual slot state (after subscription ensures freshness)
211
- const slot = pool?.getSlot(widgetId)
212
-
213
- const setPriority = useCallback(
214
- (p) => pool?.setPriority(widgetId, p),
215
- [pool, widgetId],
216
- )
217
-
218
- // Gating disabled — every widget is always live. The pool still tracks
219
- // priority/registration for any future consumer (and so setPriority calls
220
- // remain valid), but isLive ignores the cap and viewport demotion entirely.
221
- void slot
222
- return {
223
- isLive: true,
224
- generation: 0,
225
- setPriority,
226
- }
227
- }
228
-
229
- /**
230
- * Hook for CanvasPage to batch-update visibility for all terminal widgets.
231
- * Call this with the current viewport rect and widget positions.
232
- *
233
- * @returns {(viewportRect: {x:number,y:number,w:number,h:number}, widgets: Array<{id:string,type:string,position:{x:number,y:number},props?:object}>, selectedIds: Set<string>, expandedId: string|null) => void}
234
- */
235
- export function usePoolVisibilityUpdater() {
236
- const pool = useContext(WebGLPoolContext)
237
-
238
- return useCallback((viewportRect, widgets, selectedIds, expandedId) => {
239
- if (!pool) return
240
-
241
- const NEAR_MARGIN = 400 // canvas-space pixels for "near viewport" zone
242
-
243
- const terminalTypes = new Set(['terminal', 'agent', 'prompt'])
244
-
245
- for (const w of widgets) {
246
- if (!terminalTypes.has(w.type)) continue
247
-
248
- // Determine priority
249
- if (w.id === expandedId) {
250
- pool.setPriority(w.id, Priority.PINNED)
251
- continue
252
- }
253
-
254
- if (selectedIds?.has(w.id)) {
255
- pool.setPriority(w.id, Priority.PINNED)
256
- continue
257
- }
258
-
259
- const wx = w.position?.x ?? 0
260
- const wy = w.position?.y ?? 0
261
- const ww = w.props?.width ?? 800
262
- const wh = w.props?.height ?? 450
263
-
264
- // Check overlap with viewport
265
- const visible = rectsOverlap(
266
- viewportRect.x, viewportRect.y, viewportRect.w, viewportRect.h,
267
- wx, wy, ww, wh,
268
- )
269
-
270
- if (visible) {
271
- pool.setPriority(w.id, Priority.VISIBLE)
272
- continue
273
- }
274
-
275
- // Check overlap with expanded viewport (near margin)
276
- const near = rectsOverlap(
277
- viewportRect.x - NEAR_MARGIN,
278
- viewportRect.y - NEAR_MARGIN,
279
- viewportRect.w + NEAR_MARGIN * 2,
280
- viewportRect.h + NEAR_MARGIN * 2,
281
- wx, wy, ww, wh,
282
- )
283
-
284
- pool.setPriority(w.id, near ? Priority.NEAR_VIEWPORT : Priority.OFFSCREEN)
285
- }
286
- }, [pool])
287
- }
288
-
289
- function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
290
- return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by
291
- }
@@ -1,165 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { render, act } from '@testing-library/react'
3
- import { WebGLContextPoolProvider, useWebGLSlot, usePoolVisibilityUpdater, Priority } from './WebGLContextPool.jsx'
4
-
5
- function TestWidget({ widgetId, onSlot }) {
6
- const slot = useWebGLSlot(widgetId)
7
- onSlot?.(slot)
8
- return <div data-testid={widgetId}>{slot.isLive ? 'live' : 'frozen'}</div>
9
- }
10
-
11
- function TestUpdater({ onUpdater }) {
12
- const update = usePoolVisibilityUpdater()
13
- onUpdater?.(update)
14
- return null
15
- }
16
-
17
- describe('WebGLContextPool', () => {
18
- it('grants live slots to widgets within the max limit', () => {
19
- let slot1, slot2
20
- render(
21
- <WebGLContextPoolProvider maxLive={2}>
22
- <TestWidget widgetId="t1" onSlot={(s) => { slot1 = s }} />
23
- <TestWidget widgetId="t2" onSlot={(s) => { slot2 = s }} />
24
- </WebGLContextPoolProvider>
25
- )
26
-
27
- // Both should be live since we're under the limit
28
- expect(slot1.isLive).toBe(true)
29
- expect(slot2.isLive).toBe(true)
30
- })
31
-
32
- it('freezes excess widgets when over the limit', () => {
33
- let slot1, slot2, slot3
34
- render(
35
- <WebGLContextPoolProvider maxLive={2}>
36
- <TestWidget widgetId="t1" onSlot={(s) => { slot1 = s }} />
37
- <TestWidget widgetId="t2" onSlot={(s) => { slot2 = s }} />
38
- <TestWidget widgetId="t3" onSlot={(s) => { slot3 = s }} />
39
- </WebGLContextPoolProvider>
40
- )
41
-
42
- const liveCount = [slot1, slot2, slot3].filter(s => s.isLive).length
43
- const frozenCount = [slot1, slot2, slot3].filter(s => !s.isLive).length
44
-
45
- expect(liveCount).toBe(2)
46
- expect(frozenCount).toBe(1)
47
- })
48
-
49
- it('always returns live when no provider is present', () => {
50
- let slot
51
- render(<TestWidget widgetId="t1" onSlot={(s) => { slot = s }} />)
52
- expect(slot.isLive).toBe(true)
53
- expect(slot.generation).toBe(0)
54
- })
55
-
56
- it('prioritizes PINNED widgets over OFFSCREEN', () => {
57
- let slot3
58
- render(
59
- <WebGLContextPoolProvider maxLive={2}>
60
- <TestWidget widgetId="t1" onSlot={() => {}} />
61
- <TestWidget widgetId="t2" onSlot={() => {}} />
62
- <TestWidget widgetId="t3" onSlot={(s) => { slot3 = s }} />
63
- </WebGLContextPoolProvider>
64
- )
65
-
66
- // Pin t3 — it should become live, evicting one of the others
67
- act(() => { slot3.setPriority(Priority.PINNED) })
68
-
69
- expect(slot3.isLive).toBe(true)
70
- })
71
-
72
- it('PINNED widgets bypass the max limit', () => {
73
- let slot1, slot2, slot3
74
- render(
75
- <WebGLContextPoolProvider maxLive={2}>
76
- <TestWidget widgetId="t1" onSlot={(s) => { slot1 = s }} />
77
- <TestWidget widgetId="t2" onSlot={(s) => { slot2 = s }} />
78
- <TestWidget widgetId="t3" onSlot={(s) => { slot3 = s }} />
79
- </WebGLContextPoolProvider>
80
- )
81
-
82
- // Pin all three
83
- act(() => {
84
- slot1.setPriority(Priority.PINNED)
85
- slot2.setPriority(Priority.PINNED)
86
- slot3.setPriority(Priority.PINNED)
87
- })
88
-
89
- // All should be live because PINNED bypasses the cap
90
- expect(slot1.isLive).toBe(true)
91
- expect(slot2.isLive).toBe(true)
92
- expect(slot3.isLive).toBe(true)
93
- })
94
-
95
- it('tracks generation across live-frozen-live transitions', () => {
96
- let slot3
97
- render(
98
- <WebGLContextPoolProvider maxLive={2}>
99
- <TestWidget widgetId="t1" onSlot={() => {}} />
100
- <TestWidget widgetId="t2" onSlot={() => {}} />
101
- <TestWidget widgetId="t3" onSlot={(s) => { slot3 = s }} />
102
- </WebGLContextPoolProvider>
103
- )
104
-
105
- // t3 starts frozen with generation 0 (never was live)
106
- expect(slot3.isLive).toBe(false)
107
- expect(slot3.generation).toBe(0)
108
-
109
- // Pin t3 to make it live
110
- act(() => { slot3.setPriority(Priority.PINNED) })
111
- expect(slot3.isLive).toBe(true)
112
-
113
- // Unpin t3 — it should be evicted and generation bumped
114
- act(() => { slot3.setPriority(Priority.OFFSCREEN) })
115
- // Hysteresis delays eviction; use fake timers if needed.
116
- // For now, verify that generation bumps when eviction happens.
117
- })
118
-
119
- it('usePoolVisibilityUpdater updates priorities based on viewport', () => {
120
- let slot1, slot2, updater
121
- render(
122
- <WebGLContextPoolProvider maxLive={1}>
123
- <TestWidget widgetId="t1" onSlot={(s) => { slot1 = s }} />
124
- <TestWidget widgetId="t2" onSlot={(s) => { slot2 = s }} />
125
- <TestUpdater onUpdater={(u) => { updater = u }} />
126
- </WebGLContextPoolProvider>
127
- )
128
-
129
- const widgets = [
130
- { id: 't1', type: 'terminal', position: { x: 100, y: 100 }, props: { width: 800, height: 450 } },
131
- { id: 't2', type: 'terminal', position: { x: 5000, y: 5000 }, props: { width: 800, height: 450 } },
132
- ]
133
-
134
- // Viewport only covers t1
135
- act(() => {
136
- updater({ x: 0, y: 0, w: 1920, h: 1080 }, widgets, new Set(), null)
137
- })
138
-
139
- expect(slot1.isLive).toBe(true)
140
- expect(slot2.isLive).toBe(false)
141
- })
142
-
143
- it('selected widgets get PINNED priority via visibility updater', () => {
144
- let slot2, updater
145
- render(
146
- <WebGLContextPoolProvider maxLive={1}>
147
- <TestWidget widgetId="t1" onSlot={() => {}} />
148
- <TestWidget widgetId="t2" onSlot={(s) => { slot2 = s }} />
149
- <TestUpdater onUpdater={(u) => { updater = u }} />
150
- </WebGLContextPoolProvider>
151
- )
152
-
153
- const widgets = [
154
- { id: 't1', type: 'terminal', position: { x: 100, y: 100 }, props: { width: 800, height: 450 } },
155
- { id: 't2', type: 'terminal', position: { x: 5000, y: 5000 }, props: { width: 800, height: 450 } },
156
- ]
157
-
158
- // t2 is offscreen but selected — should be pinned and live
159
- act(() => {
160
- updater({ x: 0, y: 0, w: 1920, h: 1080 }, widgets, new Set(['t2']), null)
161
- })
162
-
163
- expect(slot2.isLive).toBe(true)
164
- })
165
- })
@@ -1,89 +0,0 @@
1
- /* ComponentSetWidget — canvas widget chrome */
2
-
3
- .container {
4
- position: relative;
5
- overflow: hidden;
6
- min-width: 200px;
7
- min-height: 120px;
8
- background: var(--bgColor-default, #ffffff);
9
- border: 3px solid var(--borderColor-default, #d0d7de);
10
- border-radius: 12px;
11
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
12
- width: 100%;
13
- height: 100%;
14
- }
15
-
16
- .header {
17
- display: flex;
18
- align-items: center;
19
- gap: 6px;
20
- padding: 10px 10px;
21
- font-size: 12px;
22
- font-weight: 500;
23
- color: var(--fgColor-muted, #656d76);
24
- background: var(--bgColor-muted, #f6f8fa);
25
- border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
26
- white-space: nowrap;
27
- overflow: hidden;
28
- text-overflow: ellipsis;
29
- user-select: none;
30
- }
31
-
32
- .headerIcon {
33
- display: inline-flex;
34
- flex-shrink: 0;
35
- }
36
-
37
- .headerTitle {
38
- overflow: hidden;
39
- text-overflow: ellipsis;
40
- }
41
-
42
- .headerSelected {
43
- color: var(--fgColor-accent, #0969da);
44
- font-weight: 600;
45
- flex-shrink: 0;
46
- }
47
-
48
- .headerLayout {
49
- margin-left: auto;
50
- font-size: 14px;
51
- opacity: 0.5;
52
- flex-shrink: 0;
53
- }
54
-
55
- .content {
56
- position: relative;
57
- width: 100%;
58
- height: calc(100% - 37px);
59
- }
60
-
61
- .iframe {
62
- position: absolute;
63
- inset: 0;
64
- display: block;
65
- width: 100%;
66
- height: 100%;
67
- border: none;
68
- z-index: 1;
69
- }
70
-
71
- .error {
72
- display: flex;
73
- align-items: center;
74
- gap: 8px;
75
- padding: 16px;
76
- color: var(--fgColor-danger, #cf222e);
77
- font-family: system-ui, -apple-system, sans-serif;
78
- font-size: 13px;
79
- line-height: 1.5;
80
- }
81
-
82
- .errorIcon {
83
- font-size: 20px;
84
- flex-shrink: 0;
85
- }
86
-
87
- .errorText {
88
- word-break: break-word;
89
- }
@@ -1,89 +0,0 @@
1
- /* StorySetWidget — canvas widget chrome */
2
-
3
- .container {
4
- position: relative;
5
- overflow: hidden;
6
- min-width: 200px;
7
- min-height: 120px;
8
- background: var(--bgColor-default, #ffffff);
9
- border: 3px solid var(--borderColor-default, #d0d7de);
10
- border-radius: 12px;
11
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
12
- width: 100%;
13
- height: 100%;
14
- }
15
-
16
- .header {
17
- display: flex;
18
- align-items: center;
19
- gap: 6px;
20
- padding: 10px 10px;
21
- font-size: 12px;
22
- font-weight: 500;
23
- color: var(--fgColor-muted, #656d76);
24
- background: var(--bgColor-muted, #f6f8fa);
25
- border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
26
- white-space: nowrap;
27
- overflow: hidden;
28
- text-overflow: ellipsis;
29
- user-select: none;
30
- }
31
-
32
- .headerIcon {
33
- display: inline-flex;
34
- flex-shrink: 0;
35
- }
36
-
37
- .headerTitle {
38
- overflow: hidden;
39
- text-overflow: ellipsis;
40
- }
41
-
42
- .headerSelected {
43
- color: var(--fgColor-accent, #0969da);
44
- font-weight: 600;
45
- flex-shrink: 0;
46
- }
47
-
48
- .headerLayout {
49
- margin-left: auto;
50
- font-size: 14px;
51
- opacity: 0.5;
52
- flex-shrink: 0;
53
- }
54
-
55
- .content {
56
- position: relative;
57
- width: 100%;
58
- height: calc(100% - 37px);
59
- }
60
-
61
- .iframe {
62
- position: absolute;
63
- inset: 0;
64
- display: block;
65
- width: 100%;
66
- height: 100%;
67
- border: none;
68
- z-index: 1;
69
- }
70
-
71
- .error {
72
- display: flex;
73
- align-items: center;
74
- gap: 8px;
75
- padding: 16px;
76
- color: var(--fgColor-danger, #cf222e);
77
- font-family: system-ui, -apple-system, sans-serif;
78
- font-size: 13px;
79
- line-height: 1.5;
80
- }
81
-
82
- .errorIcon {
83
- font-size: 20px;
84
- flex-shrink: 0;
85
- }
86
-
87
- .errorText {
88
- word-break: break-word;
89
- }