@dfosco/storyboard-react 4.1.0 → 4.2.0-beta.1

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.
@@ -4,6 +4,78 @@
4
4
  position: relative;
5
5
  }
6
6
 
7
+ /* Connector anchor ports — positioned at widget edge centers */
8
+ .anchorPort {
9
+ position: absolute;
10
+ width: 12px;
11
+ height: 12px;
12
+ border-radius: 50%;
13
+ background: var(--bgColor-accent-emphasis, #2f81f7);
14
+ border: 3px solid var(--bgColor-default, #fff);
15
+ opacity: 0;
16
+ transition: opacity 0.15s ease, width 0.1s ease, height 0.1s ease, margin 0.1s ease;
17
+ cursor: crosshair;
18
+ z-index: 100;
19
+ pointer-events: auto;
20
+ }
21
+
22
+ .chromeContainer:hover .anchorPort {
23
+ opacity: 0.6;
24
+ }
25
+
26
+ .anchorPort:hover {
27
+ opacity: 1;
28
+ width: 18px;
29
+ height: 18px;
30
+ }
31
+
32
+ .anchorPortTop {
33
+ top: -6px;
34
+ left: 50%;
35
+ margin-left: -6px;
36
+ }
37
+ .anchorPortTop:hover {
38
+ margin-left: -9px;
39
+ top: -9px;
40
+ }
41
+
42
+ .anchorPortBottom {
43
+ bottom: -6px;
44
+ left: 50%;
45
+ margin-left: -6px;
46
+ }
47
+ .anchorPortBottom:hover {
48
+ margin-left: -9px;
49
+ bottom: -9px;
50
+ }
51
+
52
+ .anchorPortLeft {
53
+ left: -6px;
54
+ top: 50%;
55
+ margin-top: -6px;
56
+ }
57
+ .anchorPortLeft:hover {
58
+ margin-top: -9px;
59
+ left: -9px;
60
+ }
61
+
62
+ .anchorPortRight {
63
+ right: -6px;
64
+ top: 50%;
65
+ margin-top: -6px;
66
+ }
67
+ .anchorPortRight:hover {
68
+ margin-top: -9px;
69
+ right: -9px;
70
+ }
71
+
72
+ .anchorPortDisabled {
73
+ background: var(--fgColor-muted, #8b949e);
74
+ opacity: 0;
75
+ cursor: not-allowed;
76
+ pointer-events: none;
77
+ }
78
+
7
79
  /* Widget slot — contains the actual widget; selection outline targets this */
8
80
  .widgetSlot {
9
81
  position: relative;
@@ -6,6 +6,7 @@ import ImageWidget from './ImageWidget.jsx'
6
6
  import FigmaEmbed from './FigmaEmbed.jsx'
7
7
  import CodePenEmbed from './CodePenEmbed.jsx'
8
8
  import StoryWidget from './StoryWidget.jsx'
9
+ import TerminalWidget from './TerminalWidget.jsx'
9
10
 
10
11
  /**
11
12
  * Maps widget type strings to their React components.
@@ -20,6 +21,7 @@ export const widgetRegistry = {
20
21
  'figma-embed': FigmaEmbed,
21
22
  'codepen-embed': CodePenEmbed,
22
23
  'story': StoryWidget,
24
+ 'terminal': TerminalWidget,
23
25
  }
24
26
 
25
27
  /**
@@ -151,6 +151,19 @@ export function getWidgetMeta(type) {
151
151
  return { label: def.label, icon: def.icon }
152
152
  }
153
153
 
154
+ /**
155
+ * Get the interact gate config for a widget type.
156
+ * @returns {{ enabled: boolean, label: string }}
157
+ */
158
+ export function getInteractGate(type) {
159
+ const def = widgetTypes[type]
160
+ if (!def || !def.interactGate) return { enabled: false, label: 'Click to interact' }
161
+ return {
162
+ enabled: true,
163
+ label: def.interactGateLabel || 'Click to interact',
164
+ }
165
+ }
166
+
154
167
  /**
155
168
  * Get all widget types as an array of { type, label, icon } for menus.
156
169
  * Excludes link-preview, image, and figma-embed which are created via paste only.
@@ -160,3 +173,68 @@ export function getMenuWidgetTypes() {
160
173
  .filter(([type]) => type !== 'link-preview' && type !== 'image' && type !== 'figma-embed' && type !== 'codepen-embed' && type !== 'story')
161
174
  .map(([type, def]) => ({ type, label: def.label, icon: def.icon }))
162
175
  }
176
+
177
+ /**
178
+ * Get the connector configuration for a widget type.
179
+ * @param {string} type — widget type string
180
+ * @returns {{ anchors: Record<string, string>, accept: string[], exclude: string[], defaults: Object|undefined }}
181
+ */
182
+ export function getConnectorConfig(type) {
183
+ const def = widgetTypes[type]?.connectors
184
+ return {
185
+ anchors: def?.anchors ?? { top: 'available', bottom: 'available', left: 'available', right: 'available' },
186
+ accept: def?.accept ?? ['*'],
187
+ exclude: def?.exclude ?? [],
188
+ defaults: def?.defaults,
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Check if a specific anchor is available on a widget type.
194
+ * @param {string} type — widget type string
195
+ * @param {string} anchor — anchor name (top/bottom/left/right)
196
+ * @returns {'available' | 'disabled' | 'unavailable'}
197
+ */
198
+ export function getAnchorState(type, anchor) {
199
+ const config = getConnectorConfig(type)
200
+ return config.anchors[anchor] ?? 'available'
201
+ }
202
+
203
+ /**
204
+ * Get the connector styling defaults from config.
205
+ * @returns {Object} connector default styles
206
+ */
207
+ export function getConnectorDefaults() {
208
+ const defaults = widgetsConfig.connectorDefaults ?? {}
209
+ return {
210
+ controlOffset: defaults.controlOffset ?? 80,
211
+ stroke: defaults.stroke ?? 'var(--fgColor-accent, #0969da)',
212
+ strokeWidth: defaults.strokeWidth ?? 4,
213
+ hoverStroke: defaults.hoverStroke ?? 'var(--fgColor-danger, #cf222e)',
214
+ hoverStrokeWidth: defaults.hoverStrokeWidth ?? 5,
215
+ endpointRadius: defaults.endpointRadius ?? 6,
216
+ endpointFill: defaults.endpointFill ?? 'var(--fgColor-accent, #0969da)',
217
+ endpointStroke: defaults.endpointStroke ?? 'var(--bgColor-default, #ffffff)',
218
+ endpointStrokeWidth: defaults.endpointStrokeWidth ?? 3,
219
+ hitAreaStrokeWidth: defaults.hitAreaStrokeWidth ?? 16,
220
+ dragStroke: defaults.dragStroke ?? 'var(--fgColor-accent, #0969da)',
221
+ dragStrokeWidth: defaults.dragStrokeWidth ?? 2,
222
+ dragDasharray: defaults.dragDasharray ?? '6 4',
223
+ dragOpacity: defaults.dragOpacity ?? 0.7,
224
+ startEndpoint: defaults.startEndpoint ?? 'circle',
225
+ endEndpoint: defaults.endEndpoint ?? 'circle',
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Check if a connection from sourceType to targetType is allowed.
231
+ * @param {string} targetType — widget type receiving the connection
232
+ * @param {string} sourceType — widget type initiating the connection
233
+ * @returns {boolean}
234
+ */
235
+ export function canAcceptConnection(targetType, sourceType) {
236
+ const config = getConnectorConfig(targetType)
237
+ if (config.exclude.includes(sourceType)) return false
238
+ if (config.accept.includes('*')) return true
239
+ return config.accept.includes(sourceType)
240
+ }
@@ -129,3 +129,4 @@ export const prototypeEmbedSchema = schemas['prototype']
129
129
  export const linkPreviewSchema = schemas['link-preview']
130
130
  export const imageSchema = schemas['image']
131
131
  export const figmaEmbedSchema = schemas['figma-embed']
132
+ export const terminalSchema = schemas['terminal']
package/src/context.jsx CHANGED
@@ -30,6 +30,21 @@ for (const [name, data] of Object.entries(canvases || {})) {
30
30
  })
31
31
  }
32
32
  }
33
+ // Sort each group's pages by pageOrder from .meta.json (if available)
34
+ for (const [, pages] of canvasGroupMap) {
35
+ const pageOrder = pages[0]?._canvasMeta?.pageOrder
36
+ if (Array.isArray(pageOrder)) {
37
+ const orderMap = new Map()
38
+ pageOrder.forEach((entry, idx) => {
39
+ if (typeof entry === 'string' && !entry.startsWith('sep-')) orderMap.set(entry, idx)
40
+ })
41
+ pages.sort((a, b) => {
42
+ const ai = orderMap.has(a.name) ? orderMap.get(a.name) : Infinity
43
+ const bi = orderMap.has(b.name) ? orderMap.get(b.name) : Infinity
44
+ return ai - bi
45
+ })
46
+ }
47
+ }
33
48
 
34
49
  // Build a map from story route paths → story names at module load time
35
50
  const storyRouteMap = new Map()