@dfosco/storyboard-react 4.1.0 → 4.2.0-alpha.11
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 +5 -4
- package/src/CommandPalette/CommandPalette.jsx +69 -5
- package/src/CommandPalette/command-palette.css +5 -0
- package/src/canvas/CanvasPage.jsx +333 -10
- package/src/canvas/CanvasPage.module.css +26 -4
- package/src/canvas/ConnectorLayer.jsx +252 -0
- package/src/canvas/ConnectorLayer.module.css +60 -0
- package/src/canvas/PageSelector.jsx +376 -37
- package/src/canvas/PageSelector.module.css +93 -6
- package/src/canvas/canvasApi.js +35 -0
- package/src/canvas/useCanvas.js +1 -1
- package/src/canvas/widgets/ActionWidget.jsx +193 -0
- package/src/canvas/widgets/ActionWidget.module.css +122 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +27 -6
- package/src/canvas/widgets/MarkdownBlock.module.css +11 -1
- package/src/canvas/widgets/StickyNote.module.css +3 -0
- package/src/canvas/widgets/TerminalWidget.jsx +280 -0
- package/src/canvas/widgets/TerminalWidget.module.css +158 -0
- package/src/canvas/widgets/WidgetChrome.jsx +86 -1
- package/src/canvas/widgets/WidgetChrome.module.css +82 -0
- package/src/canvas/widgets/index.js +4 -0
- package/src/canvas/widgets/widgetConfig.js +78 -0
- package/src/canvas/widgets/widgetProps.js +1 -0
- package/src/context.jsx +15 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
position: relative;
|
|
3
|
+
padding-bottom: 0;
|
|
4
|
+
border-radius: var(--base-size-16, 16px);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/* Match selection outline border-radius to terminal's rounded corners */
|
|
8
|
+
:global(.tc-drag-surface):has(.container) {
|
|
9
|
+
border-radius: 16px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.titleBar {
|
|
13
|
+
position: absolute;
|
|
14
|
+
top: -28px;
|
|
15
|
+
left: 4px;
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
17
|
+
font-size: 11px;
|
|
18
|
+
color: #8b949e;
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
user-select: none;
|
|
21
|
+
white-space: nowrap;
|
|
22
|
+
z-index: 2;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[data-widget-selected] .titleBar {
|
|
26
|
+
color: var(--borderColor-accent-emphasis, #0969da);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.terminal {
|
|
30
|
+
position: relative;
|
|
31
|
+
border-radius: var(--base-size-16, 16px);
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
background: #0d1117;
|
|
34
|
+
border: 1px solid var(--borderColor-default, #30363d);
|
|
35
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.24);
|
|
36
|
+
padding: 8px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.xtermContainer {
|
|
40
|
+
width: 100%;
|
|
41
|
+
height: 100%;
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ghostty-web / xterm.js container overrides */
|
|
46
|
+
.xtermContainer :global(.xterm) {
|
|
47
|
+
width: 100%;
|
|
48
|
+
height: 100%;
|
|
49
|
+
padding: 12px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.xtermContainer :global(.xterm-viewport) {
|
|
53
|
+
overflow-y: auto;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.xtermContainer :global(.xterm-viewport::-webkit-scrollbar) {
|
|
57
|
+
width: 6px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.xtermContainer :global(.xterm-viewport::-webkit-scrollbar-thumb) {
|
|
61
|
+
background: rgba(255, 255, 255, 0.15);
|
|
62
|
+
border-radius: 3px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.xtermContainer :global(.xterm-viewport::-webkit-scrollbar-track) {
|
|
66
|
+
background: transparent;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.loading {
|
|
70
|
+
position: absolute;
|
|
71
|
+
inset: 0;
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
color: #8b949e;
|
|
76
|
+
font-size: 13px;
|
|
77
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
78
|
+
background: #0d1117;
|
|
79
|
+
z-index: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.error {
|
|
83
|
+
position: absolute;
|
|
84
|
+
inset: 0;
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
justify-content: center;
|
|
88
|
+
color: #f85149;
|
|
89
|
+
font-size: 13px;
|
|
90
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
91
|
+
background: #0d1117;
|
|
92
|
+
z-index: 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.mutedPrompt {
|
|
96
|
+
color: #484f58;
|
|
97
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, monospace;
|
|
98
|
+
font-size: 16px;
|
|
99
|
+
position: absolute;
|
|
100
|
+
top: 12px;
|
|
101
|
+
left: 16px;
|
|
102
|
+
pointer-events: none;
|
|
103
|
+
user-select: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ── Terminal Zzz Animation ── */
|
|
107
|
+
|
|
108
|
+
.buddyZzz {
|
|
109
|
+
display: flex;
|
|
110
|
+
gap: 8px;
|
|
111
|
+
align-items: baseline;
|
|
112
|
+
margin-bottom: 16px;
|
|
113
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, monospace;
|
|
114
|
+
pointer-events: none;
|
|
115
|
+
user-select: none;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.z1 {
|
|
119
|
+
font-size: 18px;
|
|
120
|
+
color: rgba(255, 255, 255, 0.6);
|
|
121
|
+
animation: zFloat 2.4s ease-in-out infinite;
|
|
122
|
+
animation-delay: 0s;
|
|
123
|
+
opacity: 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.z2 {
|
|
127
|
+
font-size: 22px;
|
|
128
|
+
color: rgba(255, 255, 255, 0.5);
|
|
129
|
+
animation: zFloat 2.4s ease-in-out infinite;
|
|
130
|
+
animation-delay: 0.8s;
|
|
131
|
+
opacity: 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.z3 {
|
|
135
|
+
font-size: 28px;
|
|
136
|
+
font-weight: 600;
|
|
137
|
+
color: rgba(255, 255, 255, 0.4);
|
|
138
|
+
animation: zFloat 2.4s ease-in-out infinite;
|
|
139
|
+
animation-delay: 1.6s;
|
|
140
|
+
opacity: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@keyframes zFloat {
|
|
144
|
+
0% {
|
|
145
|
+
opacity: 0;
|
|
146
|
+
transform: translateY(0);
|
|
147
|
+
}
|
|
148
|
+
15% {
|
|
149
|
+
opacity: 1;
|
|
150
|
+
}
|
|
151
|
+
70% {
|
|
152
|
+
opacity: 0.4;
|
|
153
|
+
}
|
|
154
|
+
100% {
|
|
155
|
+
opacity: 0;
|
|
156
|
+
transform: translateY(-24px);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect, useSyncExternalStore } from 'react'
|
|
2
2
|
import { Tooltip } from '@primer/react'
|
|
3
3
|
import { EyeIcon as OcticonEye, EyeClosedIcon as OcticonEyeClosed, CodeIcon as OcticonCode, UnwrapIcon as OcticonUnwrap, ImageIcon as OcticonImage, UnfoldIcon as OcticonUnfold, FoldIcon as OcticonFold } from '@primer/octicons-react'
|
|
4
|
+
import { getConnectorConfig, getInteractGate } from './widgetConfig.js'
|
|
4
5
|
import styles from './WidgetChrome.module.css'
|
|
6
|
+
import overlayStyles from './embedOverlay.module.css'
|
|
5
7
|
|
|
6
8
|
const STICKY_NOTE_COLORS = {
|
|
7
9
|
yellow: { bg: '#fff8c5', border: '#d4a72c', dot: '#e8c846' },
|
|
@@ -393,6 +395,7 @@ function ColorPickerFeature({ currentColor, options, onColorChange }) {
|
|
|
393
395
|
*/
|
|
394
396
|
export default function WidgetChrome({
|
|
395
397
|
widgetId,
|
|
398
|
+
widgetType,
|
|
396
399
|
features = [],
|
|
397
400
|
selected = false,
|
|
398
401
|
multiSelected = false,
|
|
@@ -402,6 +405,7 @@ export default function WidgetChrome({
|
|
|
402
405
|
onDeselect, // eslint-disable-line no-unused-vars
|
|
403
406
|
onAction,
|
|
404
407
|
onUpdate,
|
|
408
|
+
onConnectorDragStart,
|
|
405
409
|
children,
|
|
406
410
|
readOnly = false,
|
|
407
411
|
}) {
|
|
@@ -452,6 +456,55 @@ export default function WidgetChrome({
|
|
|
452
456
|
const showFeatures = showToolbar && !multiSelected
|
|
453
457
|
const menuFeatures = features.filter((f) => f.menu)
|
|
454
458
|
|
|
459
|
+
// Interact gate — declarative overlay from widgets.config.json
|
|
460
|
+
const gate = widgetType ? getInteractGate(widgetType) : { enabled: false }
|
|
461
|
+
const [interacting, setInteracting] = useState(false)
|
|
462
|
+
const slotRef = useRef(null)
|
|
463
|
+
|
|
464
|
+
// Exit interact mode on click outside or double-Escape
|
|
465
|
+
const lastEscapeRef = useRef(0)
|
|
466
|
+
useEffect(() => {
|
|
467
|
+
if (!gate.enabled || !interacting) return
|
|
468
|
+
const handleMouseDown = (e) => {
|
|
469
|
+
if (slotRef.current && !slotRef.current.contains(e.target)) {
|
|
470
|
+
setInteracting(false)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const handleKeyDown = (e) => {
|
|
474
|
+
if (e.key === 'Escape') {
|
|
475
|
+
const now = Date.now()
|
|
476
|
+
if (now - lastEscapeRef.current < 500) {
|
|
477
|
+
// Double-Escape: exit interact mode but keep widget selected
|
|
478
|
+
e.stopPropagation()
|
|
479
|
+
e.preventDefault()
|
|
480
|
+
setInteracting(false)
|
|
481
|
+
lastEscapeRef.current = 0
|
|
482
|
+
} else {
|
|
483
|
+
// First Escape: let it pass to widget, record timestamp
|
|
484
|
+
lastEscapeRef.current = now
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
document.addEventListener('mousedown', handleMouseDown, true)
|
|
489
|
+
document.addEventListener('keydown', handleKeyDown, true)
|
|
490
|
+
return () => {
|
|
491
|
+
document.removeEventListener('mousedown', handleMouseDown, true)
|
|
492
|
+
document.removeEventListener('keydown', handleKeyDown, true)
|
|
493
|
+
}
|
|
494
|
+
}, [gate.enabled, interacting])
|
|
495
|
+
|
|
496
|
+
// Exit interact mode when deselected
|
|
497
|
+
useEffect(() => {
|
|
498
|
+
if (!selected && !hovered && interacting) setInteracting(false)
|
|
499
|
+
}, [selected, hovered, interacting])
|
|
500
|
+
|
|
501
|
+
const handleGateClick = useCallback((e) => {
|
|
502
|
+
e.stopPropagation()
|
|
503
|
+
setInteracting(true)
|
|
504
|
+
// Also trigger selection so the widget gets selected
|
|
505
|
+
onSelect?.()
|
|
506
|
+
}, [onSelect])
|
|
507
|
+
|
|
455
508
|
return (
|
|
456
509
|
<div
|
|
457
510
|
className={styles.chromeContainer}
|
|
@@ -460,9 +513,41 @@ export default function WidgetChrome({
|
|
|
460
513
|
onMouseEnter={(readOnly && !hasFeatures) ? undefined : handleMouseEnter}
|
|
461
514
|
onMouseLeave={(readOnly && !hasFeatures) ? undefined : handleMouseLeave}
|
|
462
515
|
>
|
|
463
|
-
<div className={`tc-drag-surface ${styles.widgetSlot} ${selected ? styles.widgetSlotSelected : ''} ${multiSelected ? styles.widgetSlotMultiSelected : ''}`} data-widget-selected={selected || undefined}>
|
|
516
|
+
<div ref={slotRef} className={`tc-drag-surface ${styles.widgetSlot} ${selected ? styles.widgetSlotSelected : ''} ${multiSelected ? styles.widgetSlotMultiSelected : ''}`} data-widget-selected={selected || undefined} data-widget-interacting={interacting || undefined}>
|
|
464
517
|
{children}
|
|
518
|
+
{gate.enabled && !interacting && (
|
|
519
|
+
<div
|
|
520
|
+
className={overlayStyles.interactOverlay}
|
|
521
|
+
onClick={handleGateClick}
|
|
522
|
+
role="button"
|
|
523
|
+
tabIndex={0}
|
|
524
|
+
aria-label={gate.label}
|
|
525
|
+
>
|
|
526
|
+
<span className={overlayStyles.interactHint}>{gate.label}</span>
|
|
527
|
+
</div>
|
|
528
|
+
)}
|
|
465
529
|
</div>
|
|
530
|
+
{!readOnly && onConnectorDragStart && (() => {
|
|
531
|
+
const connConfig = widgetType ? getConnectorConfig(widgetType) : null
|
|
532
|
+
return ['top', 'bottom', 'left', 'right']
|
|
533
|
+
.filter((a) => !connConfig || connConfig.anchors[a] !== 'unavailable')
|
|
534
|
+
.map((anchor) => {
|
|
535
|
+
const disabled = connConfig?.anchors[anchor] === 'disabled'
|
|
536
|
+
return (
|
|
537
|
+
<div
|
|
538
|
+
key={anchor}
|
|
539
|
+
className={`${styles.anchorPort} ${styles[`anchorPort${anchor[0].toUpperCase()}${anchor.slice(1)}`]} ${disabled ? styles.anchorPortDisabled : ''}`}
|
|
540
|
+
onPointerDown={disabled ? undefined : (e) => {
|
|
541
|
+
e.stopPropagation()
|
|
542
|
+
e.nativeEvent?.stopImmediatePropagation?.()
|
|
543
|
+
e.preventDefault()
|
|
544
|
+
onConnectorDragStart(widgetId, anchor, e)
|
|
545
|
+
}}
|
|
546
|
+
data-anchor={anchor}
|
|
547
|
+
/>
|
|
548
|
+
)
|
|
549
|
+
})
|
|
550
|
+
})()}
|
|
466
551
|
<div
|
|
467
552
|
className={styles.toolbar}
|
|
468
553
|
>
|
|
@@ -4,6 +4,88 @@
|
|
|
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
|
+
/* Invisible expanded hit area — 15px padding around the visible dot */
|
|
23
|
+
.anchorPort::before {
|
|
24
|
+
content: '';
|
|
25
|
+
position: absolute;
|
|
26
|
+
top: -15px;
|
|
27
|
+
left: -15px;
|
|
28
|
+
right: -15px;
|
|
29
|
+
bottom: -15px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.chromeContainer:hover .anchorPort {
|
|
33
|
+
opacity: 0.6;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.anchorPort:hover {
|
|
37
|
+
opacity: 1;
|
|
38
|
+
width: 18px;
|
|
39
|
+
height: 18px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.anchorPortTop {
|
|
43
|
+
top: -6px;
|
|
44
|
+
left: 50%;
|
|
45
|
+
margin-left: -6px;
|
|
46
|
+
}
|
|
47
|
+
.anchorPortTop:hover {
|
|
48
|
+
margin-left: -9px;
|
|
49
|
+
top: -9px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.anchorPortBottom {
|
|
53
|
+
bottom: -6px;
|
|
54
|
+
left: 50%;
|
|
55
|
+
margin-left: -6px;
|
|
56
|
+
}
|
|
57
|
+
.anchorPortBottom:hover {
|
|
58
|
+
margin-left: -9px;
|
|
59
|
+
bottom: -9px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.anchorPortLeft {
|
|
63
|
+
left: -6px;
|
|
64
|
+
top: 50%;
|
|
65
|
+
margin-top: -6px;
|
|
66
|
+
}
|
|
67
|
+
.anchorPortLeft:hover {
|
|
68
|
+
margin-top: -9px;
|
|
69
|
+
left: -9px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.anchorPortRight {
|
|
73
|
+
right: -6px;
|
|
74
|
+
top: 50%;
|
|
75
|
+
margin-top: -6px;
|
|
76
|
+
}
|
|
77
|
+
.anchorPortRight:hover {
|
|
78
|
+
margin-top: -9px;
|
|
79
|
+
right: -9px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.anchorPortDisabled {
|
|
83
|
+
background: var(--fgColor-muted, #8b949e);
|
|
84
|
+
opacity: 0;
|
|
85
|
+
cursor: not-allowed;
|
|
86
|
+
pointer-events: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
7
89
|
/* Widget slot — contains the actual widget; selection outline targets this */
|
|
8
90
|
.widgetSlot {
|
|
9
91
|
position: relative;
|
|
@@ -6,6 +6,8 @@ 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'
|
|
10
|
+
import ActionWidget from './ActionWidget.jsx'
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Maps widget type strings to their React components.
|
|
@@ -20,6 +22,8 @@ export const widgetRegistry = {
|
|
|
20
22
|
'figma-embed': FigmaEmbed,
|
|
21
23
|
'codepen-embed': CodePenEmbed,
|
|
22
24
|
'story': StoryWidget,
|
|
25
|
+
'terminal': TerminalWidget,
|
|
26
|
+
'action': ActionWidget,
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
/**
|
|
@@ -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()
|