@dfosco/storyboard 0.10.0 → 0.10.2
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/dist/storyboard-ui.js +1 -1
- package/dist/storyboard-ui.js.map +1 -1
- package/dist/tiny-canvas.css +1 -1
- package/package.json +1 -1
- package/src/canvas/style.css +24 -0
- package/src/core/mountStoryboardCore.js +4 -2
- package/src/core/stores/canvasInteractionStore.js +92 -11
- package/src/core/stores/canvasInteractionStore.test.js +106 -14
- package/src/core/stores/configSchema.js +11 -0
- package/src/core/stores/configSchema.test.js +1 -0
- package/src/internals/canvas/CanvasPage.module.css +17 -0
- package/toolbar.config.json +2 -2
package/dist/tiny-canvas.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--tc-border-radius: var(--radius, 12px);--tc-shadow-rest: var(--shadow-resting, 0 1px 3px rgba(0, 0, 0, .12));--tc-shadow-float: var(--shadow-floating, 0 4px 12px rgba(0, 0, 0, .15));--tc-border-color: var(--color-border-muted, rgba(0, 0, 0, .15));--tc-bg-muted: var(--color-background-muted, #f6f8fa);--tc-dot-color: var(--color-overlay-backdrop, rgba(0, 0, 0, .08));--tc-grid-size: var(--golden-number, 36px);--tc-grid-offset: calc(var(--tc-grid-size) / -2);--tc-dot-radius: 2px;--tc-font-stack: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif}.tc-canvas{min-width:max(100vw,10000px);min-height:max(100vh,10000px);width:100%;height:100%;margin:0;font-family:var(--tc-font-stack);position:relative;background-color:var(--tc-bg-muted);color-scheme:light dark}.tc-canvas[data-color-mode=light]{color-scheme:light}.tc-canvas[data-color-mode=dark]{color-scheme:dark}.tc-canvas[data-dotted]{background-image:radial-gradient(var(--tc-dot-color) var(--tc-dot-radius),transparent var(--tc-dot-radius));background-size:var(--tc-grid-size) var(--tc-grid-size);background-position:var(--tc-grid-offset) var(--tc-grid-offset)}@media(prefers-color-scheme:dark){.tc-canvas:not([data-color-mode=light]){--tc-bg-muted: var(--color-background-muted, #161b22);--tc-dot-color: var(--color-overlay-backdrop, rgba(255, 255, 255, .1));--tc-border-color: var(--color-border-muted, rgba(255, 255, 255, .15));--tc-shadow-rest: var(--shadow-resting, 0 1px 3px rgba(0, 0, 0, .3));--tc-shadow-float: var(--shadow-floating, 0 4px 12px rgba(0, 0, 0, .4))}}.tc-canvas[data-color-mode=dark]{--tc-bg-muted: var(--color-background-muted, #161b22);--tc-dot-color: var(--color-overlay-backdrop, rgba(255, 255, 255, .1));--tc-border-color: var(--color-border-muted, rgba(255, 255, 255, .15));--tc-shadow-rest: var(--shadow-resting, 0 1px 3px rgba(0, 0, 0, .3));--tc-shadow-float: var(--shadow-floating, 0 4px 12px rgba(0, 0, 0, .4))}body .tc-draggable-inner:not(:active){touch-action:none}.tc-on-translation{transition:transform calc(var(--tc-translation-ms, .15s) + var(--tc-translation-delay-ms, 50ms)) cubic-bezier(.78,.1,.51,.78) var(--tc-translation-delay-ms, 50ms)}.tc-drag{position:absolute;width:fit-content}.tc-draggable-inner{position:relative;width:fit-content;height:min-content}.tc-draggable-inner:after{content:"";position:absolute;z-index:-1;width:100%;height:100%;top:0;border-radius:var(--tc-border-radius);box-shadow:var(--tc-shadow-float);opacity:0;transition:opacity .3s ease-in-out}.tc-on-translation .tc-draggable-inner:after,.tc-draggable-inner:active:after,.tc-draggable-inner:hover:after{opacity:0}.tc-on-translation .tc-draggable-inner,.tc-draggable-inner:active,.tc-draggable-inner:hover{outline:none}.tc-canvas[data-locked] *{cursor:default!important}
|
|
1
|
+
:root{--tc-border-radius: var(--radius, 12px);--tc-shadow-rest: var(--shadow-resting, 0 1px 3px rgba(0, 0, 0, .12));--tc-shadow-float: var(--shadow-floating, 0 4px 12px rgba(0, 0, 0, .15));--tc-border-color: var(--color-border-muted, rgba(0, 0, 0, .15));--tc-bg-muted: var(--color-background-muted, #f6f8fa);--tc-dot-color: var(--color-overlay-backdrop, rgba(0, 0, 0, .08));--tc-grid-size: var(--golden-number, 36px);--tc-grid-offset: calc(var(--tc-grid-size) / -2);--tc-dot-radius: 2px;--tc-font-stack: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif}.tc-canvas{min-width:max(100vw,10000px);min-height:max(100vh,10000px);width:100%;height:100%;margin:0;font-family:var(--tc-font-stack);position:relative;background-color:var(--tc-bg-muted);color-scheme:light dark}html[data-canvas-surface-width=viewport] .tc-canvas{min-width:100vw;max-width:100vw;overflow-x:hidden}html[data-canvas-surface-height=viewport] .tc-canvas{min-height:100vh;max-height:100vh;overflow-y:hidden}.tc-canvas[data-color-mode=light]{color-scheme:light}.tc-canvas[data-color-mode=dark]{color-scheme:dark}.tc-canvas[data-dotted]{background-image:radial-gradient(var(--tc-dot-color) var(--tc-dot-radius),transparent var(--tc-dot-radius));background-size:var(--tc-grid-size) var(--tc-grid-size);background-position:var(--tc-grid-offset) var(--tc-grid-offset)}@media(prefers-color-scheme:dark){.tc-canvas:not([data-color-mode=light]){--tc-bg-muted: var(--color-background-muted, #161b22);--tc-dot-color: var(--color-overlay-backdrop, rgba(255, 255, 255, .1));--tc-border-color: var(--color-border-muted, rgba(255, 255, 255, .15));--tc-shadow-rest: var(--shadow-resting, 0 1px 3px rgba(0, 0, 0, .3));--tc-shadow-float: var(--shadow-floating, 0 4px 12px rgba(0, 0, 0, .4))}}.tc-canvas[data-color-mode=dark]{--tc-bg-muted: var(--color-background-muted, #161b22);--tc-dot-color: var(--color-overlay-backdrop, rgba(255, 255, 255, .1));--tc-border-color: var(--color-border-muted, rgba(255, 255, 255, .15));--tc-shadow-rest: var(--shadow-resting, 0 1px 3px rgba(0, 0, 0, .3));--tc-shadow-float: var(--shadow-floating, 0 4px 12px rgba(0, 0, 0, .4))}body .tc-draggable-inner:not(:active){touch-action:none}.tc-on-translation{transition:transform calc(var(--tc-translation-ms, .15s) + var(--tc-translation-delay-ms, 50ms)) cubic-bezier(.78,.1,.51,.78) var(--tc-translation-delay-ms, 50ms)}.tc-drag{position:absolute;width:fit-content}.tc-draggable-inner{position:relative;width:fit-content;height:min-content}.tc-draggable-inner:after{content:"";position:absolute;z-index:-1;width:100%;height:100%;top:0;border-radius:var(--tc-border-radius);box-shadow:var(--tc-shadow-float);opacity:0;transition:opacity .3s ease-in-out}.tc-on-translation .tc-draggable-inner:after,.tc-draggable-inner:active:after,.tc-draggable-inner:hover:after{opacity:0}.tc-on-translation .tc-draggable-inner,.tc-draggable-inner:active,.tc-draggable-inner:hover{outline:none}.tc-canvas[data-locked] *{cursor:default!important}
|
package/package.json
CHANGED
package/src/canvas/style.css
CHANGED
|
@@ -28,6 +28,30 @@
|
|
|
28
28
|
color-scheme: light dark;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* Canvas surface sizing — consumer-controlled via `canvas.surface.*` in
|
|
32
|
+
* storyboard.config.json (see canvasInteractionStore.js). The store
|
|
33
|
+
* toggles these `<html>` data attrs; CSS clamps each axis independently
|
|
34
|
+
* so e.g. width can be viewport-locked while height stays "auto" with
|
|
35
|
+
* the 10000px pannable floor.
|
|
36
|
+
*
|
|
37
|
+
* `overflow-{x,y}: hidden` is required on the constrained axis so that
|
|
38
|
+
* widgets positioned past the surface bounds (absolute-positioned
|
|
39
|
+
* Draggables) don't contribute to the scroll container's scrollWidth /
|
|
40
|
+
* scrollHeight at 100% zoom. CSS transforms (the zoom scaling on
|
|
41
|
+
* `.canvasZoom`) ink-overflow past `overflow: hidden`, so zoom-in still
|
|
42
|
+
* produces real scroll on the scroller — only the in-bounds layout is
|
|
43
|
+
* clipped. */
|
|
44
|
+
html[data-canvas-surface-width="viewport"] .tc-canvas {
|
|
45
|
+
min-width: 100vw;
|
|
46
|
+
max-width: 100vw;
|
|
47
|
+
overflow-x: hidden;
|
|
48
|
+
}
|
|
49
|
+
html[data-canvas-surface-height="viewport"] .tc-canvas {
|
|
50
|
+
min-height: 100vh;
|
|
51
|
+
max-height: 100vh;
|
|
52
|
+
overflow-y: hidden;
|
|
53
|
+
}
|
|
54
|
+
|
|
31
55
|
.tc-canvas[data-color-mode="light"] {
|
|
32
56
|
color-scheme: light;
|
|
33
57
|
}
|
|
@@ -485,13 +485,15 @@ export async function mountStoryboardCore(config = {}, options = {}) {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
// Seed canvas-interaction store from the same canvas config block. Reads
|
|
488
|
-
// `canvas.scroll.axis
|
|
489
|
-
// initCanvasConfig so the
|
|
488
|
+
// `canvas.scroll.axis`, `canvas.zoom.{gestures,origin}`, and
|
|
489
|
+
// `canvas.surface.{width,height}`. Runs after initCanvasConfig so the
|
|
490
|
+
// unified-store path is preferred when present.
|
|
490
491
|
const canvasCfg = (uc.canvas && Object.keys(uc.canvas).length > 0) ? uc.canvas : (config.canvas || {})
|
|
491
492
|
initCanvasInteraction({
|
|
492
493
|
scrollAxis: canvasCfg.scroll?.axis,
|
|
493
494
|
zoomGestures: canvasCfg.zoom?.gestures,
|
|
494
495
|
zoomOrigin: canvasCfg.zoom?.origin,
|
|
496
|
+
surface: canvasCfg.surface,
|
|
495
497
|
})
|
|
496
498
|
|
|
497
499
|
// Load and merge command palette config.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Canvas Interaction Store — runtime gates for canvas scroll + zoom behavior.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Four knobs that go *beyond* presentation (which is purely visual):
|
|
5
5
|
*
|
|
6
6
|
* - `scrollAxis` — restricts the native scroll axes of the canvas
|
|
7
7
|
* scroll container. Useful when a canvas is rendered as a "webpage"
|
|
@@ -25,9 +25,20 @@
|
|
|
25
25
|
* Pointer-driven zoom (wheel/pinch) is unaffected — it always
|
|
26
26
|
* anchors on the cursor.
|
|
27
27
|
*
|
|
28
|
+
* - `surface` — sizes the underlying canvas surface (`.tc-canvas`)
|
|
29
|
+
* per axis. `{ width, height }` each accept `"auto"` (default,
|
|
30
|
+
* keeps the `max(100vw, 10000px)` floor so users can pan into a
|
|
31
|
+
* large workspace) or `"viewport"` (sizes to exactly `100vw` /
|
|
32
|
+
* `100vh`, so the unscaled surface matches the visible window
|
|
33
|
+
* exactly and zooming >100% surfaces real scroll). The store
|
|
34
|
+
* toggles `data-canvas-surface-width="viewport"` and
|
|
35
|
+
* `data-canvas-surface-height="viewport"` on `<html>` so CSS can
|
|
36
|
+
* pick up the override without React having to re-render.
|
|
37
|
+
*
|
|
28
38
|
* Defaults are seeded from `canvas.scroll.axis`, `canvas.zoom.gestures`,
|
|
29
|
-
*
|
|
30
|
-
* can be applied at any
|
|
39
|
+
* `canvas.zoom.origin`, and `canvas.surface.{width,height}` in
|
|
40
|
+
* `storyboard.config.json`. Runtime overrides can be applied at any
|
|
41
|
+
* time via `setCanvasInteraction(partial)`.
|
|
31
42
|
*
|
|
32
43
|
* This is an **advanced, undocumented** extension surface — see
|
|
33
44
|
* `core/stores/presentationStore.js` for the sibling system.
|
|
@@ -37,30 +48,72 @@
|
|
|
37
48
|
|
|
38
49
|
const VALID_AXES = new Set(['both', 'vertical', 'horizontal', 'none'])
|
|
39
50
|
const VALID_ZOOM_ORIGINS = new Set(['center', 'top-left'])
|
|
51
|
+
const VALID_SURFACE_SIZES = new Set(['auto', 'viewport'])
|
|
40
52
|
|
|
41
53
|
const _defaults = {
|
|
42
54
|
scrollAxis: 'both',
|
|
43
55
|
zoomGestures: true,
|
|
44
56
|
zoomOrigin: 'center',
|
|
57
|
+
surface: { width: 'auto', height: 'auto' },
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
let _state = {
|
|
60
|
+
let _state = {
|
|
61
|
+
..._defaults,
|
|
62
|
+
surface: { ..._defaults.surface },
|
|
63
|
+
}
|
|
48
64
|
|
|
49
65
|
/** @type {Set<Function>} */
|
|
50
66
|
const _listeners = new Set()
|
|
51
67
|
|
|
52
68
|
let _snapshotVersion = 0
|
|
53
69
|
|
|
70
|
+
function _applySurfaceAttrs() {
|
|
71
|
+
if (typeof document === 'undefined') return
|
|
72
|
+
const root = document.documentElement
|
|
73
|
+
if (_state.surface.width === 'viewport') {
|
|
74
|
+
root.dataset.canvasSurfaceWidth = 'viewport'
|
|
75
|
+
} else {
|
|
76
|
+
delete root.dataset.canvasSurfaceWidth
|
|
77
|
+
}
|
|
78
|
+
if (_state.surface.height === 'viewport') {
|
|
79
|
+
root.dataset.canvasSurfaceHeight = 'viewport'
|
|
80
|
+
} else {
|
|
81
|
+
delete root.dataset.canvasSurfaceHeight
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _readSurfaceFromConfig(input) {
|
|
86
|
+
// Accept the nested shape ({ width, height }) directly, and ignore
|
|
87
|
+
// anything malformed. Each axis is validated independently.
|
|
88
|
+
if (!input || typeof input !== 'object') return null
|
|
89
|
+
const next = {}
|
|
90
|
+
if (typeof input.width === 'string' && VALID_SURFACE_SIZES.has(input.width)) {
|
|
91
|
+
next.width = input.width
|
|
92
|
+
}
|
|
93
|
+
if (typeof input.height === 'string' && VALID_SURFACE_SIZES.has(input.height)) {
|
|
94
|
+
next.height = input.height
|
|
95
|
+
}
|
|
96
|
+
return Object.keys(next).length ? next : null
|
|
97
|
+
}
|
|
98
|
+
|
|
54
99
|
/**
|
|
55
100
|
* Seed the store from a partial config (typically from
|
|
56
101
|
* `storyboard.config.json` → `canvas.scroll.axis`, `canvas.zoom.gestures`,
|
|
57
|
-
*
|
|
58
|
-
* at app startup.
|
|
102
|
+
* `canvas.zoom.origin`, and `canvas.surface`). Replaces any previous
|
|
103
|
+
* overrides. Called once at app startup.
|
|
59
104
|
*
|
|
60
|
-
* @param {{
|
|
105
|
+
* @param {{
|
|
106
|
+
* scrollAxis?: string,
|
|
107
|
+
* zoomGestures?: boolean,
|
|
108
|
+
* zoomOrigin?: string,
|
|
109
|
+
* surface?: { width?: string, height?: string },
|
|
110
|
+
* }} [config]
|
|
61
111
|
*/
|
|
62
112
|
export function initCanvasInteraction(config) {
|
|
63
|
-
_state = {
|
|
113
|
+
_state = {
|
|
114
|
+
..._defaults,
|
|
115
|
+
surface: { ..._defaults.surface },
|
|
116
|
+
}
|
|
64
117
|
if (config && typeof config === 'object') {
|
|
65
118
|
if (typeof config.scrollAxis === 'string' && VALID_AXES.has(config.scrollAxis)) {
|
|
66
119
|
_state.scrollAxis = config.scrollAxis
|
|
@@ -71,13 +124,23 @@ export function initCanvasInteraction(config) {
|
|
|
71
124
|
if (typeof config.zoomOrigin === 'string' && VALID_ZOOM_ORIGINS.has(config.zoomOrigin)) {
|
|
72
125
|
_state.zoomOrigin = config.zoomOrigin
|
|
73
126
|
}
|
|
127
|
+
const surfaceUpdate = _readSurfaceFromConfig(config.surface)
|
|
128
|
+
if (surfaceUpdate) {
|
|
129
|
+
_state.surface = { ..._state.surface, ...surfaceUpdate }
|
|
130
|
+
}
|
|
74
131
|
}
|
|
132
|
+
_applySurfaceAttrs()
|
|
75
133
|
_notify()
|
|
76
134
|
}
|
|
77
135
|
|
|
78
136
|
/**
|
|
79
137
|
* Apply a partial runtime override. Unspecified keys keep their current value.
|
|
80
|
-
* @param {{
|
|
138
|
+
* @param {{
|
|
139
|
+
* scrollAxis?: string,
|
|
140
|
+
* zoomGestures?: boolean,
|
|
141
|
+
* zoomOrigin?: string,
|
|
142
|
+
* surface?: { width?: string, height?: string },
|
|
143
|
+
* }} partial
|
|
81
144
|
*/
|
|
82
145
|
export function setCanvasInteraction(partial) {
|
|
83
146
|
if (!partial || typeof partial !== 'object') return
|
|
@@ -94,11 +157,25 @@ export function setCanvasInteraction(partial) {
|
|
|
94
157
|
_state.zoomOrigin = partial.zoomOrigin
|
|
95
158
|
changed = true
|
|
96
159
|
}
|
|
160
|
+
const surfaceUpdate = _readSurfaceFromConfig(partial.surface)
|
|
161
|
+
if (surfaceUpdate) {
|
|
162
|
+
const merged = { ..._state.surface, ...surfaceUpdate }
|
|
163
|
+
if (merged.width !== _state.surface.width || merged.height !== _state.surface.height) {
|
|
164
|
+
_state.surface = merged
|
|
165
|
+
_applySurfaceAttrs()
|
|
166
|
+
changed = true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
97
169
|
if (changed) _notify()
|
|
98
170
|
}
|
|
99
171
|
|
|
100
172
|
/**
|
|
101
|
-
* @returns {{
|
|
173
|
+
* @returns {{
|
|
174
|
+
* scrollAxis: 'both'|'vertical'|'horizontal'|'none',
|
|
175
|
+
* zoomGestures: boolean,
|
|
176
|
+
* zoomOrigin: 'center'|'top-left',
|
|
177
|
+
* surface: { width: 'auto'|'viewport', height: 'auto'|'viewport' },
|
|
178
|
+
* }}
|
|
102
179
|
*/
|
|
103
180
|
export function getCanvasInteraction() {
|
|
104
181
|
return _state
|
|
@@ -134,7 +211,11 @@ function _notify() {
|
|
|
134
211
|
|
|
135
212
|
/** Reset all state. Only for tests. */
|
|
136
213
|
export function _resetCanvasInteraction() {
|
|
137
|
-
_state = {
|
|
214
|
+
_state = {
|
|
215
|
+
..._defaults,
|
|
216
|
+
surface: { ..._defaults.surface },
|
|
217
|
+
}
|
|
218
|
+
_applySurfaceAttrs()
|
|
138
219
|
_listeners.clear()
|
|
139
220
|
_snapshotVersion = 0
|
|
140
221
|
}
|
|
@@ -14,23 +14,58 @@ describe('canvasInteractionStore', () => {
|
|
|
14
14
|
_resetCanvasInteraction()
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
it('starts with defaults: scrollAxis=both, zoomGestures=true, zoomOrigin=center', () => {
|
|
18
|
-
expect(getCanvasInteraction()).toEqual({
|
|
17
|
+
it('starts with defaults: scrollAxis=both, zoomGestures=true, zoomOrigin=center, surface=auto/auto', () => {
|
|
18
|
+
expect(getCanvasInteraction()).toEqual({
|
|
19
|
+
scrollAxis: 'both',
|
|
20
|
+
zoomGestures: true,
|
|
21
|
+
zoomOrigin: 'center',
|
|
22
|
+
surface: { width: 'auto', height: 'auto' },
|
|
23
|
+
})
|
|
19
24
|
})
|
|
20
25
|
|
|
21
26
|
it('initCanvasInteraction seeds from config', () => {
|
|
22
|
-
initCanvasInteraction({
|
|
23
|
-
|
|
27
|
+
initCanvasInteraction({
|
|
28
|
+
scrollAxis: 'vertical',
|
|
29
|
+
zoomGestures: false,
|
|
30
|
+
zoomOrigin: 'top-left',
|
|
31
|
+
surface: { width: 'viewport', height: 'viewport' },
|
|
32
|
+
})
|
|
33
|
+
expect(getCanvasInteraction()).toEqual({
|
|
34
|
+
scrollAxis: 'vertical',
|
|
35
|
+
zoomGestures: false,
|
|
36
|
+
zoomOrigin: 'top-left',
|
|
37
|
+
surface: { width: 'viewport', height: 'viewport' },
|
|
38
|
+
})
|
|
24
39
|
})
|
|
25
40
|
|
|
26
41
|
it('initCanvasInteraction with no args / empty / invalid resets to defaults', () => {
|
|
27
|
-
initCanvasInteraction({
|
|
42
|
+
initCanvasInteraction({
|
|
43
|
+
scrollAxis: 'vertical',
|
|
44
|
+
zoomGestures: false,
|
|
45
|
+
zoomOrigin: 'top-left',
|
|
46
|
+
surface: { width: 'viewport', height: 'viewport' },
|
|
47
|
+
})
|
|
28
48
|
initCanvasInteraction()
|
|
29
|
-
expect(getCanvasInteraction()).toEqual({
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
expect(getCanvasInteraction()).toEqual({
|
|
50
|
+
scrollAxis: 'both',
|
|
51
|
+
zoomGestures: true,
|
|
52
|
+
zoomOrigin: 'center',
|
|
53
|
+
surface: { width: 'auto', height: 'auto' },
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
initCanvasInteraction({
|
|
57
|
+
scrollAxis: 'vertical',
|
|
58
|
+
zoomGestures: false,
|
|
59
|
+
zoomOrigin: 'top-left',
|
|
60
|
+
surface: { width: 'viewport', height: 'viewport' },
|
|
61
|
+
})
|
|
32
62
|
initCanvasInteraction({})
|
|
33
|
-
expect(getCanvasInteraction()).toEqual({
|
|
63
|
+
expect(getCanvasInteraction()).toEqual({
|
|
64
|
+
scrollAxis: 'both',
|
|
65
|
+
zoomGestures: true,
|
|
66
|
+
zoomOrigin: 'center',
|
|
67
|
+
surface: { width: 'auto', height: 'auto' },
|
|
68
|
+
})
|
|
34
69
|
})
|
|
35
70
|
|
|
36
71
|
it('initCanvasInteraction ignores invalid scrollAxis values', () => {
|
|
@@ -77,23 +112,75 @@ describe('canvasInteractionStore', () => {
|
|
|
77
112
|
}
|
|
78
113
|
})
|
|
79
114
|
|
|
115
|
+
it('initCanvasInteraction accepts surface.{width,height} independently', () => {
|
|
116
|
+
initCanvasInteraction({ surface: { width: 'viewport' } })
|
|
117
|
+
expect(getCanvasInteraction().surface).toEqual({ width: 'viewport', height: 'auto' })
|
|
118
|
+
|
|
119
|
+
initCanvasInteraction({ surface: { height: 'viewport' } })
|
|
120
|
+
expect(getCanvasInteraction().surface).toEqual({ width: 'auto', height: 'viewport' })
|
|
121
|
+
|
|
122
|
+
initCanvasInteraction({ surface: { width: 'viewport', height: 'viewport' } })
|
|
123
|
+
expect(getCanvasInteraction().surface).toEqual({ width: 'viewport', height: 'viewport' })
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('initCanvasInteraction ignores invalid surface values', () => {
|
|
127
|
+
initCanvasInteraction({ surface: { width: 'fluid' } })
|
|
128
|
+
expect(getCanvasInteraction().surface).toEqual({ width: 'auto', height: 'auto' })
|
|
129
|
+
|
|
130
|
+
initCanvasInteraction({ surface: { width: 42 } })
|
|
131
|
+
expect(getCanvasInteraction().surface).toEqual({ width: 'auto', height: 'auto' })
|
|
132
|
+
|
|
133
|
+
initCanvasInteraction({ surface: null })
|
|
134
|
+
expect(getCanvasInteraction().surface).toEqual({ width: 'auto', height: 'auto' })
|
|
135
|
+
|
|
136
|
+
initCanvasInteraction({ surface: 'viewport' })
|
|
137
|
+
expect(getCanvasInteraction().surface).toEqual({ width: 'auto', height: 'auto' })
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('initCanvasInteraction writes data-canvas-surface-* attrs on <html>', () => {
|
|
141
|
+
const root = document.documentElement
|
|
142
|
+
initCanvasInteraction({ surface: { width: 'viewport' } })
|
|
143
|
+
expect(root.dataset.canvasSurfaceWidth).toBe('viewport')
|
|
144
|
+
expect(root.dataset.canvasSurfaceHeight).toBeUndefined()
|
|
145
|
+
|
|
146
|
+
initCanvasInteraction({ surface: { width: 'viewport', height: 'viewport' } })
|
|
147
|
+
expect(root.dataset.canvasSurfaceWidth).toBe('viewport')
|
|
148
|
+
expect(root.dataset.canvasSurfaceHeight).toBe('viewport')
|
|
149
|
+
|
|
150
|
+
initCanvasInteraction() // resets to default
|
|
151
|
+
expect(root.dataset.canvasSurfaceWidth).toBeUndefined()
|
|
152
|
+
expect(root.dataset.canvasSurfaceHeight).toBeUndefined()
|
|
153
|
+
})
|
|
154
|
+
|
|
80
155
|
it('setCanvasInteraction patches partial state without resetting other keys', () => {
|
|
81
|
-
initCanvasInteraction({
|
|
156
|
+
initCanvasInteraction({
|
|
157
|
+
scrollAxis: 'vertical',
|
|
158
|
+
zoomGestures: false,
|
|
159
|
+
zoomOrigin: 'top-left',
|
|
160
|
+
surface: { width: 'viewport' },
|
|
161
|
+
})
|
|
82
162
|
|
|
83
163
|
setCanvasInteraction({ zoomGestures: true })
|
|
84
|
-
expect(getCanvasInteraction()).toEqual({
|
|
164
|
+
expect(getCanvasInteraction()).toEqual({
|
|
165
|
+
scrollAxis: 'vertical',
|
|
166
|
+
zoomGestures: true,
|
|
167
|
+
zoomOrigin: 'top-left',
|
|
168
|
+
surface: { width: 'viewport', height: 'auto' },
|
|
169
|
+
})
|
|
85
170
|
|
|
86
171
|
setCanvasInteraction({ scrollAxis: 'horizontal' })
|
|
87
|
-
expect(getCanvasInteraction()).
|
|
172
|
+
expect(getCanvasInteraction().scrollAxis).toBe('horizontal')
|
|
173
|
+
|
|
174
|
+
setCanvasInteraction({ surface: { height: 'viewport' } })
|
|
175
|
+
expect(getCanvasInteraction().surface).toEqual({ width: 'viewport', height: 'viewport' })
|
|
88
176
|
|
|
89
177
|
setCanvasInteraction({ zoomOrigin: 'center' })
|
|
90
|
-
expect(getCanvasInteraction()).
|
|
178
|
+
expect(getCanvasInteraction().zoomOrigin).toBe('center')
|
|
91
179
|
})
|
|
92
180
|
|
|
93
181
|
it('setCanvasInteraction with no-op patch does not notify subscribers', () => {
|
|
94
182
|
const cb = vi.fn()
|
|
95
183
|
subscribeToCanvasInteraction(cb)
|
|
96
|
-
// Reset count: subscribers fire on initial reset_+ subscribe etc.
|
|
97
184
|
cb.mockClear()
|
|
98
185
|
|
|
99
186
|
setCanvasInteraction({}) // no keys
|
|
@@ -102,6 +189,8 @@ describe('canvasInteractionStore', () => {
|
|
|
102
189
|
setCanvasInteraction({ zoomGestures: true }) // same as default
|
|
103
190
|
setCanvasInteraction({ zoomOrigin: 'center' }) // same as default
|
|
104
191
|
setCanvasInteraction({ zoomOrigin: 'invalid' }) // ignored
|
|
192
|
+
setCanvasInteraction({ surface: { width: 'auto' } }) // same as default
|
|
193
|
+
setCanvasInteraction({ surface: { width: 'fluid' } }) // ignored
|
|
105
194
|
expect(cb).not.toHaveBeenCalled()
|
|
106
195
|
|
|
107
196
|
setCanvasInteraction({ zoomGestures: false }) // real change
|
|
@@ -109,6 +198,9 @@ describe('canvasInteractionStore', () => {
|
|
|
109
198
|
|
|
110
199
|
setCanvasInteraction({ zoomOrigin: 'top-left' }) // real change
|
|
111
200
|
expect(cb).toHaveBeenCalledTimes(2)
|
|
201
|
+
|
|
202
|
+
setCanvasInteraction({ surface: { width: 'viewport' } }) // real change
|
|
203
|
+
expect(cb).toHaveBeenCalledTimes(3)
|
|
112
204
|
})
|
|
113
205
|
|
|
114
206
|
it('subscribers are notified on init and meaningful set; unsubscribe stops further notifications', () => {
|
|
@@ -257,6 +257,17 @@ export const configDefaults = {
|
|
|
257
257
|
scroll: {
|
|
258
258
|
axis: 'both', // "both" | "vertical" | "horizontal" | "none"
|
|
259
259
|
},
|
|
260
|
+
surface: {
|
|
261
|
+
// Sizing of the underlying canvas surface (`.tc-canvas`). Independent
|
|
262
|
+
// per axis. "auto" preserves the default `max(100vw, 10000px)` floor
|
|
263
|
+
// so users can pan into a large workspace. "viewport" sizes the
|
|
264
|
+
// surface to exactly `100vw` / `100vh`, which is what landing-style
|
|
265
|
+
// canvases want: the unscaled canvas matches the visible window
|
|
266
|
+
// exactly, while zooming above 100% legitimately overflows and
|
|
267
|
+
// surfaces horizontal/vertical scroll.
|
|
268
|
+
width: 'auto', // "auto" | "viewport"
|
|
269
|
+
height: 'auto', // "auto" | "viewport"
|
|
270
|
+
},
|
|
260
271
|
terminal: {
|
|
261
272
|
resizable: false,
|
|
262
273
|
defaultWidth: 800,
|
|
@@ -27,6 +27,7 @@ describe('configSchema', () => {
|
|
|
27
27
|
expect(c.canvas.zoom.gestures).toBe(true)
|
|
28
28
|
expect(c.canvas.zoom.origin).toBe('center')
|
|
29
29
|
expect(c.canvas.scroll.axis).toBe('both')
|
|
30
|
+
expect(c.canvas.surface).toEqual({ width: 'auto', height: 'auto' })
|
|
30
31
|
expect(c.canvas.production).toEqual({
|
|
31
32
|
move: false,
|
|
32
33
|
resize: false,
|
|
@@ -41,6 +41,23 @@
|
|
|
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
|
+
|
|
44
61
|
/* Selection outline is now handled by WidgetChrome.module.css (.widgetSlotSelected) */
|
|
45
62
|
|
|
46
63
|
.canvasLogo {
|
package/toolbar.config.json
CHANGED
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"ariaLabel": "Zoom to objects",
|
|
166
166
|
"label": "Zoom to objects",
|
|
167
167
|
"icon": "iconoir/square-3d-three-points",
|
|
168
|
-
"meta": { "strokeWeight":
|
|
168
|
+
"meta": { "strokeWeight": 2, "scale": 1 },
|
|
169
169
|
"render": "canvas-zoom-to-fit",
|
|
170
170
|
"surface": "canvas-toolbar",
|
|
171
171
|
"handler": "core:canvas-toolbar",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
"labelOn": "Snap to grid (on)",
|
|
180
180
|
"labelOff": "Snap to grid (off)",
|
|
181
181
|
"icon": "primer/apps",
|
|
182
|
-
"meta": { "strokeWeight":
|
|
182
|
+
"meta": { "strokeWeight": 0.9, "scale": 1.1 },
|
|
183
183
|
"render": "canvas-snap",
|
|
184
184
|
"surface": "canvas-toolbar",
|
|
185
185
|
"handler": "core:canvas-toolbar",
|