@dfosco/storyboard-react 3.0.0 → 3.1.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.
- package/package.json +3 -3
- package/src/canvas/CanvasControls.jsx +123 -0
- package/src/canvas/CanvasControls.module.css +133 -0
- package/src/canvas/CanvasPage.jsx +306 -18
- package/src/canvas/CanvasPage.module.css +58 -0
- package/src/canvas/widgets/LinkPreview.jsx +34 -0
- package/src/canvas/widgets/LinkPreview.module.css +51 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +8 -4
- package/src/canvas/widgets/PrototypeEmbed.jsx +160 -14
- package/src/canvas/widgets/PrototypeEmbed.module.css +217 -1
- package/src/canvas/widgets/StickyNote.jsx +12 -20
- package/src/canvas/widgets/StickyNote.module.css +14 -39
- package/src/canvas/widgets/WidgetWrapper.jsx +2 -15
- package/src/canvas/widgets/WidgetWrapper.module.css +0 -30
- package/src/canvas/widgets/index.js +3 -1
- package/src/canvas/widgets/widgetProps.js +7 -0
- package/src/vite/data-plugin.js +9 -7
|
@@ -13,3 +13,61 @@
|
|
|
13
13
|
.loading p {
|
|
14
14
|
margin: 0;
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
.canvasScroll {
|
|
18
|
+
width: 100vw;
|
|
19
|
+
height: 100vh;
|
|
20
|
+
overflow: auto;
|
|
21
|
+
background-color: var(--bgColor-muted, #f6f8fa);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@media (prefers-color-scheme: dark) {
|
|
25
|
+
.canvasScroll {
|
|
26
|
+
background-color: var(--bgColor-muted, #161b22);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.canvasZoom {
|
|
31
|
+
min-width: 100%;
|
|
32
|
+
min-height: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.selected {
|
|
36
|
+
outline: 2px solid var(--bgColor-accent-emphasis, #2f81f7);
|
|
37
|
+
outline-offset: 2px;
|
|
38
|
+
border-radius: 4px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.canvasTitle {
|
|
42
|
+
position: fixed;
|
|
43
|
+
top: 12px;
|
|
44
|
+
left: 16px;
|
|
45
|
+
z-index: 10;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.canvasTitleInput {
|
|
49
|
+
font-family: var(--tc-font-stack, system-ui, -apple-system, sans-serif);
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
color: var(--fgColor-muted, #656d76);
|
|
53
|
+
background: transparent;
|
|
54
|
+
border: 1px solid transparent;
|
|
55
|
+
border-radius: 6px;
|
|
56
|
+
padding: 4px 8px;
|
|
57
|
+
outline: none;
|
|
58
|
+
min-width: 80px;
|
|
59
|
+
max-width: 300px;
|
|
60
|
+
transition: border-color 150ms, background-color 150ms, color 150ms;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.canvasTitleInput:hover {
|
|
64
|
+
color: var(--fgColor-default, #1f2328);
|
|
65
|
+
border-color: var(--borderColor-default, #d1d9e0);
|
|
66
|
+
background: var(--bgColor-default, #ffffff);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.canvasTitleInput:focus {
|
|
70
|
+
color: var(--fgColor-default, #1f2328);
|
|
71
|
+
border-color: var(--bgColor-accent-emphasis, #2f81f7);
|
|
72
|
+
background: var(--bgColor-default, #ffffff);
|
|
73
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
2
|
+
import { readProp, linkPreviewSchema } from './widgetProps.js'
|
|
3
|
+
import styles from './LinkPreview.module.css'
|
|
4
|
+
|
|
5
|
+
export default function LinkPreview({ props }) {
|
|
6
|
+
const url = readProp(props, 'url', linkPreviewSchema)
|
|
7
|
+
const title = readProp(props, 'title', linkPreviewSchema)
|
|
8
|
+
|
|
9
|
+
let hostname = ''
|
|
10
|
+
try {
|
|
11
|
+
hostname = new URL(url).hostname
|
|
12
|
+
} catch { /* invalid URL */ }
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<WidgetWrapper>
|
|
16
|
+
<div className={styles.card}>
|
|
17
|
+
<span className={styles.icon}>🔗</span>
|
|
18
|
+
<div className={styles.text}>
|
|
19
|
+
{title && <p className={styles.title}>{title}</p>}
|
|
20
|
+
<a
|
|
21
|
+
href={url}
|
|
22
|
+
target="_blank"
|
|
23
|
+
rel="noopener noreferrer"
|
|
24
|
+
className={styles.url}
|
|
25
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
26
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
27
|
+
>
|
|
28
|
+
{hostname || url}
|
|
29
|
+
</a>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</WidgetWrapper>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
.card {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 12px;
|
|
5
|
+
padding: 14px 16px;
|
|
6
|
+
text-decoration: none;
|
|
7
|
+
color: inherit;
|
|
8
|
+
min-width: 240px;
|
|
9
|
+
transition: background 150ms;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.card:hover {
|
|
13
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.icon {
|
|
17
|
+
font-size: 20px;
|
|
18
|
+
flex-shrink: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.text {
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
min-width: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.title {
|
|
27
|
+
margin: 0;
|
|
28
|
+
font-size: 14px;
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
line-height: 1.4;
|
|
31
|
+
color: var(--fgColor-default, #1f2328);
|
|
32
|
+
white-space: nowrap;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
text-overflow: ellipsis;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.url {
|
|
38
|
+
margin: 0;
|
|
39
|
+
font-size: 12px;
|
|
40
|
+
color: var(--fgColor-muted, #656d76);
|
|
41
|
+
white-space: nowrap;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
text-overflow: ellipsis;
|
|
44
|
+
text-decoration: none;
|
|
45
|
+
display: block;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.url:hover {
|
|
49
|
+
text-decoration: underline;
|
|
50
|
+
color: var(--fgColor-accent, #0969da);
|
|
51
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from 'react'
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
2
2
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
3
3
|
import { readProp, markdownSchema } from './widgetProps.js'
|
|
4
4
|
import styles from './MarkdownBlock.module.css'
|
|
@@ -25,7 +25,7 @@ function renderMarkdown(text) {
|
|
|
25
25
|
})
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export default function MarkdownBlock({ props, onUpdate
|
|
28
|
+
export default function MarkdownBlock({ props, onUpdate }) {
|
|
29
29
|
const content = readProp(props, 'content', markdownSchema)
|
|
30
30
|
const width = readProp(props, 'width', markdownSchema)
|
|
31
31
|
const [editing, setEditing] = useState(false)
|
|
@@ -33,6 +33,10 @@ export default function MarkdownBlock({ props, onUpdate, onRemove }) {
|
|
|
33
33
|
const blockRef = useRef(null)
|
|
34
34
|
const [editHeight, setEditHeight] = useState(null)
|
|
35
35
|
|
|
36
|
+
const handleContentChange = useCallback((e) => {
|
|
37
|
+
onUpdate?.({ content: e.target.value })
|
|
38
|
+
}, [onUpdate])
|
|
39
|
+
|
|
36
40
|
useEffect(() => {
|
|
37
41
|
if (editing) {
|
|
38
42
|
// Capture the preview height before switching to editor
|
|
@@ -48,7 +52,7 @@ export default function MarkdownBlock({ props, onUpdate, onRemove }) {
|
|
|
48
52
|
}, [editing, editHeight])
|
|
49
53
|
|
|
50
54
|
return (
|
|
51
|
-
<WidgetWrapper
|
|
55
|
+
<WidgetWrapper>
|
|
52
56
|
<div
|
|
53
57
|
ref={blockRef}
|
|
54
58
|
className={styles.block}
|
|
@@ -60,7 +64,7 @@ export default function MarkdownBlock({ props, onUpdate, onRemove }) {
|
|
|
60
64
|
className={styles.editor}
|
|
61
65
|
style={{ minHeight: editHeight ? editHeight - 2 : undefined }}
|
|
62
66
|
value={content}
|
|
63
|
-
onChange={
|
|
67
|
+
onChange={handleContentChange}
|
|
64
68
|
onBlur={() => setEditing(false)}
|
|
65
69
|
onMouseDown={(e) => e.stopPropagation()}
|
|
66
70
|
onPointerDown={(e) => e.stopPropagation()}
|
|
@@ -1,33 +1,179 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
1
2
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
2
3
|
import { readProp, prototypeEmbedSchema } from './widgetProps.js'
|
|
3
4
|
import styles from './PrototypeEmbed.module.css'
|
|
4
5
|
|
|
5
|
-
export default function PrototypeEmbed({ props,
|
|
6
|
+
export default function PrototypeEmbed({ props, onUpdate }) {
|
|
6
7
|
const src = readProp(props, 'src', prototypeEmbedSchema)
|
|
7
8
|
const width = readProp(props, 'width', prototypeEmbedSchema)
|
|
8
9
|
const height = readProp(props, 'height', prototypeEmbedSchema)
|
|
10
|
+
const zoom = readProp(props, 'zoom', prototypeEmbedSchema)
|
|
9
11
|
const label = readProp(props, 'label', prototypeEmbedSchema) || src
|
|
10
12
|
|
|
11
|
-
// Build the full iframe URL using the app's base path
|
|
12
13
|
const basePath = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
|
|
13
|
-
const
|
|
14
|
+
const rawSrc = src ? `${basePath}${src}` : ''
|
|
15
|
+
const iframeSrc = rawSrc ? `${rawSrc}${rawSrc.includes('?') ? '&' : '?'}_sb_embed` : ''
|
|
16
|
+
|
|
17
|
+
const scale = zoom / 100
|
|
18
|
+
|
|
19
|
+
const [editing, setEditing] = useState(false)
|
|
20
|
+
const [interactive, setInteractive] = useState(false)
|
|
21
|
+
const inputRef = useRef(null)
|
|
22
|
+
const embedRef = useRef(null)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (editing && inputRef.current) {
|
|
26
|
+
inputRef.current.focus()
|
|
27
|
+
inputRef.current.select()
|
|
28
|
+
}
|
|
29
|
+
}, [editing])
|
|
30
|
+
|
|
31
|
+
// Exit interactive mode when clicking outside the embed
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!interactive) return
|
|
34
|
+
function handlePointerDown(e) {
|
|
35
|
+
if (embedRef.current && !embedRef.current.contains(e.target)) {
|
|
36
|
+
setInteractive(false)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
document.addEventListener('pointerdown', handlePointerDown)
|
|
40
|
+
return () => document.removeEventListener('pointerdown', handlePointerDown)
|
|
41
|
+
}, [interactive])
|
|
42
|
+
|
|
43
|
+
const enterInteractive = useCallback(() => setInteractive(true), [])
|
|
44
|
+
|
|
45
|
+
function handleSubmit(e) {
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
const value = inputRef.current?.value?.trim() || ''
|
|
48
|
+
onUpdate?.({ src: value })
|
|
49
|
+
setEditing(false)
|
|
50
|
+
}
|
|
14
51
|
|
|
15
52
|
return (
|
|
16
|
-
<WidgetWrapper
|
|
17
|
-
<div
|
|
18
|
-
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
53
|
+
<WidgetWrapper>
|
|
54
|
+
<div
|
|
55
|
+
ref={embedRef}
|
|
56
|
+
className={styles.embed}
|
|
57
|
+
style={{ width, height }}
|
|
58
|
+
>
|
|
59
|
+
{editing ? (
|
|
60
|
+
<form
|
|
61
|
+
className={styles.urlForm}
|
|
62
|
+
onSubmit={handleSubmit}
|
|
63
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
64
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
65
|
+
>
|
|
66
|
+
<label className={styles.urlLabel}>Prototype URL path</label>
|
|
67
|
+
<input
|
|
68
|
+
ref={inputRef}
|
|
69
|
+
className={styles.urlInput}
|
|
70
|
+
type="text"
|
|
71
|
+
defaultValue={src}
|
|
72
|
+
placeholder="/MyPrototype/page"
|
|
73
|
+
onKeyDown={(e) => { if (e.key === 'Escape') setEditing(false) }}
|
|
74
|
+
/>
|
|
75
|
+
<div className={styles.urlActions}>
|
|
76
|
+
<button type="submit" className={styles.urlSave}>Save</button>
|
|
77
|
+
<button type="button" className={styles.urlCancel} onClick={() => setEditing(false)}>Cancel</button>
|
|
78
|
+
</div>
|
|
79
|
+
</form>
|
|
80
|
+
) : iframeSrc ? (
|
|
81
|
+
<>
|
|
82
|
+
<div className={styles.iframeContainer}>
|
|
83
|
+
<iframe
|
|
84
|
+
src={iframeSrc}
|
|
85
|
+
className={styles.iframe}
|
|
86
|
+
style={{
|
|
87
|
+
width: width / scale,
|
|
88
|
+
height: height / scale,
|
|
89
|
+
transform: `scale(${scale})`,
|
|
90
|
+
transformOrigin: '0 0',
|
|
91
|
+
}}
|
|
92
|
+
title={label || 'Prototype embed'}
|
|
93
|
+
sandbox="allow-same-origin allow-scripts allow-forms allow-popups"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
{!interactive && (
|
|
97
|
+
<div
|
|
98
|
+
className={styles.dragOverlay}
|
|
99
|
+
onDoubleClick={enterInteractive}
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
</>
|
|
25
103
|
) : (
|
|
26
|
-
<div
|
|
27
|
-
|
|
104
|
+
<div
|
|
105
|
+
className={styles.empty}
|
|
106
|
+
onDoubleClick={() => setEditing(true)}
|
|
107
|
+
role="button"
|
|
108
|
+
tabIndex={0}
|
|
109
|
+
onKeyDown={(e) => { if (e.key === 'Enter') setEditing(true) }}
|
|
110
|
+
>
|
|
111
|
+
<p>Double-click to set prototype URL</p>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
{iframeSrc && !editing && (
|
|
115
|
+
<button
|
|
116
|
+
className={styles.editBtn}
|
|
117
|
+
onClick={(e) => { e.stopPropagation(); setEditing(true) }}
|
|
118
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
119
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
120
|
+
title="Edit URL"
|
|
121
|
+
aria-label="Edit prototype URL"
|
|
122
|
+
>
|
|
123
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z"/></svg>
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
126
|
+
{iframeSrc && !editing && (
|
|
127
|
+
<div
|
|
128
|
+
className={styles.zoomBar}
|
|
129
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
130
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
131
|
+
>
|
|
132
|
+
<button
|
|
133
|
+
className={styles.zoomBtn}
|
|
134
|
+
onClick={() => {
|
|
135
|
+
const step = zoom <= 75 ? 5 : 25
|
|
136
|
+
onUpdate?.({ zoom: Math.max(25, zoom - step) })
|
|
137
|
+
}}
|
|
138
|
+
disabled={zoom <= 25}
|
|
139
|
+
aria-label="Zoom out"
|
|
140
|
+
>−</button>
|
|
141
|
+
<span className={styles.zoomLabel}>{zoom}%</span>
|
|
142
|
+
<button
|
|
143
|
+
className={styles.zoomBtn}
|
|
144
|
+
onClick={() => {
|
|
145
|
+
const step = zoom < 75 ? 5 : 25
|
|
146
|
+
onUpdate?.({ zoom: Math.min(200, zoom + step) })
|
|
147
|
+
}}
|
|
148
|
+
disabled={zoom >= 200}
|
|
149
|
+
aria-label="Zoom in"
|
|
150
|
+
>+</button>
|
|
28
151
|
</div>
|
|
29
152
|
)}
|
|
30
153
|
</div>
|
|
154
|
+
<div
|
|
155
|
+
className={styles.resizeHandle}
|
|
156
|
+
onMouseDown={(e) => {
|
|
157
|
+
e.stopPropagation()
|
|
158
|
+
e.preventDefault()
|
|
159
|
+
const startX = e.clientX
|
|
160
|
+
const startY = e.clientY
|
|
161
|
+
const startW = width
|
|
162
|
+
const startH = height
|
|
163
|
+
function onMove(ev) {
|
|
164
|
+
const newW = Math.max(200, startW + ev.clientX - startX)
|
|
165
|
+
const newH = Math.max(150, startH + ev.clientY - startY)
|
|
166
|
+
onUpdate?.({ width: newW, height: newH })
|
|
167
|
+
}
|
|
168
|
+
function onUp() {
|
|
169
|
+
document.removeEventListener('mousemove', onMove)
|
|
170
|
+
document.removeEventListener('mouseup', onUp)
|
|
171
|
+
}
|
|
172
|
+
document.addEventListener('mousemove', onMove)
|
|
173
|
+
document.addEventListener('mouseup', onUp)
|
|
174
|
+
}}
|
|
175
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
176
|
+
/>
|
|
31
177
|
</WidgetWrapper>
|
|
32
178
|
)
|
|
33
179
|
}
|
|
@@ -4,13 +4,24 @@
|
|
|
4
4
|
background: var(--bgColor-default, #ffffff);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
.
|
|
7
|
+
.iframeContainer {
|
|
8
8
|
width: 100%;
|
|
9
9
|
height: 100%;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.iframe {
|
|
10
14
|
border: none;
|
|
11
15
|
display: block;
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
.dragOverlay {
|
|
19
|
+
position: absolute;
|
|
20
|
+
inset: 0;
|
|
21
|
+
z-index: 1;
|
|
22
|
+
cursor: grab;
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
.empty {
|
|
15
26
|
display: flex;
|
|
16
27
|
align-items: center;
|
|
@@ -19,8 +30,213 @@
|
|
|
19
30
|
color: var(--fgColor-muted, #656d76);
|
|
20
31
|
font-size: 14px;
|
|
21
32
|
font-style: italic;
|
|
33
|
+
cursor: pointer;
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
.empty p {
|
|
25
37
|
margin: 0;
|
|
26
38
|
}
|
|
39
|
+
|
|
40
|
+
.editBtn {
|
|
41
|
+
all: unset;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
position: absolute;
|
|
44
|
+
top: 8px;
|
|
45
|
+
right: 8px;
|
|
46
|
+
width: 28px;
|
|
47
|
+
height: 28px;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
border-radius: 6px;
|
|
52
|
+
background: rgba(255, 255, 255, 0.92);
|
|
53
|
+
backdrop-filter: blur(12px);
|
|
54
|
+
-webkit-backdrop-filter: blur(12px);
|
|
55
|
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
56
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
opacity: 0;
|
|
59
|
+
transition: opacity 150ms;
|
|
60
|
+
z-index: 2;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.embed:hover .editBtn {
|
|
64
|
+
opacity: 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.editBtn:hover {
|
|
68
|
+
background: rgba(255, 255, 255, 0.98);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@media (prefers-color-scheme: dark) {
|
|
72
|
+
.editBtn {
|
|
73
|
+
background: rgba(22, 27, 34, 0.88);
|
|
74
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.editBtn:hover {
|
|
78
|
+
background: rgba(30, 37, 46, 0.95);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.urlForm {
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-direction: column;
|
|
85
|
+
gap: 8px;
|
|
86
|
+
padding: 24px;
|
|
87
|
+
height: 100%;
|
|
88
|
+
box-sizing: border-box;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.urlLabel {
|
|
93
|
+
font-size: 12px;
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
color: var(--fgColor-muted, #656d76);
|
|
96
|
+
text-transform: uppercase;
|
|
97
|
+
letter-spacing: 0.5px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.urlInput {
|
|
101
|
+
all: unset;
|
|
102
|
+
padding: 8px 10px;
|
|
103
|
+
font-size: 14px;
|
|
104
|
+
font-family: ui-monospace, SFMono-Regular, monospace;
|
|
105
|
+
border: 1px solid var(--borderColor-default, #d0d7de);
|
|
106
|
+
border-radius: 6px;
|
|
107
|
+
background: var(--bgColor-default, #ffffff);
|
|
108
|
+
color: var(--fgColor-default, #1f2328);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.urlInput:focus {
|
|
112
|
+
border-color: var(--bgColor-accent-emphasis, #2f81f7);
|
|
113
|
+
box-shadow: 0 0 0 2px rgba(47, 129, 247, 0.3);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.urlActions {
|
|
117
|
+
display: flex;
|
|
118
|
+
gap: 8px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.urlSave,
|
|
122
|
+
.urlCancel {
|
|
123
|
+
all: unset;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
padding: 6px 14px;
|
|
126
|
+
font-size: 13px;
|
|
127
|
+
font-weight: 500;
|
|
128
|
+
border-radius: 6px;
|
|
129
|
+
text-align: center;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.urlSave {
|
|
133
|
+
background: var(--bgColor-accent-emphasis, #2f81f7);
|
|
134
|
+
color: var(--fgColor-onEmphasis, #ffffff);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.urlSave:hover {
|
|
138
|
+
background: var(--bgColor-accent-emphasis, #388bfd);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.urlCancel {
|
|
142
|
+
color: var(--fgColor-muted, #656d76);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.urlCancel:hover {
|
|
146
|
+
color: var(--fgColor-default, #1f2328);
|
|
147
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.resizeHandle {
|
|
151
|
+
position: absolute;
|
|
152
|
+
bottom: 0;
|
|
153
|
+
right: 0;
|
|
154
|
+
width: 16px;
|
|
155
|
+
height: 16px;
|
|
156
|
+
cursor: nwse-resize;
|
|
157
|
+
background: linear-gradient(
|
|
158
|
+
135deg,
|
|
159
|
+
transparent 40%,
|
|
160
|
+
var(--borderColor-muted, rgba(0, 0, 0, 0.15)) 40%,
|
|
161
|
+
var(--borderColor-muted, rgba(0, 0, 0, 0.15)) 50%,
|
|
162
|
+
transparent 50%,
|
|
163
|
+
transparent 65%,
|
|
164
|
+
var(--borderColor-muted, rgba(0, 0, 0, 0.15)) 65%,
|
|
165
|
+
var(--borderColor-muted, rgba(0, 0, 0, 0.15)) 75%,
|
|
166
|
+
transparent 75%
|
|
167
|
+
);
|
|
168
|
+
opacity: 0;
|
|
169
|
+
transition: opacity 150ms;
|
|
170
|
+
z-index: 2;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.embed:hover ~ .resizeHandle,
|
|
174
|
+
.resizeHandle:hover {
|
|
175
|
+
opacity: 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.zoomBar {
|
|
179
|
+
position: absolute;
|
|
180
|
+
bottom: 8px;
|
|
181
|
+
left: 50%;
|
|
182
|
+
transform: translateX(-50%);
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
border-radius: 10px;
|
|
186
|
+
border: 1.5px solid var(--trigger-border, var(--borderColor-muted, #d0d7de));
|
|
187
|
+
background: var(--trigger-bg, var(--bgColor-muted, #f6f8fa));
|
|
188
|
+
opacity: 0;
|
|
189
|
+
transition: opacity 150ms;
|
|
190
|
+
z-index: 3;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.embed:hover .zoomBar {
|
|
194
|
+
opacity: 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.zoomBtn {
|
|
198
|
+
all: unset;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
width: 36px;
|
|
201
|
+
height: 32px;
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: center;
|
|
205
|
+
font-size: 16px;
|
|
206
|
+
font-weight: 600;
|
|
207
|
+
color: var(--trigger-text, var(--fgColor-default, #1f2328));
|
|
208
|
+
transition: background 120ms;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.zoomBtn:first-child {
|
|
212
|
+
border-radius: 7px 0 0 7px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.zoomBtn:last-child {
|
|
216
|
+
border-radius: 0 7px 7px 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.zoomBtn:hover:not(:disabled) {
|
|
220
|
+
background: var(--trigger-bg-hover, var(--bgColor-neutral-muted, #eaeef2));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.zoomBtn:disabled {
|
|
224
|
+
opacity: 0.3;
|
|
225
|
+
cursor: default;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.zoomLabel {
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
justify-content: center;
|
|
232
|
+
min-width: 48px;
|
|
233
|
+
height: 32px;
|
|
234
|
+
padding: 0 4px;
|
|
235
|
+
font-size: 11px;
|
|
236
|
+
font-weight: 600;
|
|
237
|
+
font-variant-numeric: tabular-nums;
|
|
238
|
+
color: var(--trigger-text, var(--fgColor-default, #1f2328));
|
|
239
|
+
border-left: 1.5px solid var(--trigger-border, var(--borderColor-muted, #d0d7de));
|
|
240
|
+
border-right: 1.5px solid var(--trigger-border, var(--borderColor-muted, #d0d7de));
|
|
241
|
+
user-select: none;
|
|
242
|
+
}
|
|
@@ -11,7 +11,7 @@ const COLORS = {
|
|
|
11
11
|
orange: { bg: '#fff1e5', border: '#d18616', dot: '#e8a844' },
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export default function StickyNote({ props, onUpdate
|
|
14
|
+
export default function StickyNote({ props, onUpdate }) {
|
|
15
15
|
const text = readProp(props, 'text', stickyNoteSchema)
|
|
16
16
|
const color = readProp(props, 'color', stickyNoteSchema)
|
|
17
17
|
const palette = COLORS[color] ?? COLORS.yellow
|
|
@@ -39,15 +39,17 @@ export default function StickyNote({ props, onUpdate, onRemove }) {
|
|
|
39
39
|
className={styles.sticky}
|
|
40
40
|
style={{ '--sticky-bg': palette.bg, '--sticky-border': palette.border }}
|
|
41
41
|
>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
<p
|
|
43
|
+
className={styles.text}
|
|
44
|
+
style={editing ? { visibility: 'hidden' } : undefined}
|
|
45
|
+
onDoubleClick={() => setEditing(true)}
|
|
46
|
+
role="button"
|
|
47
|
+
tabIndex={0}
|
|
48
|
+
onKeyDown={(e) => { if (e.key === 'Enter') setEditing(true) }}
|
|
49
|
+
>
|
|
50
|
+
{text || 'Double-click to edit…'}
|
|
51
|
+
</p>
|
|
52
|
+
{editing && (
|
|
51
53
|
<textarea
|
|
52
54
|
ref={textareaRef}
|
|
53
55
|
className={styles.textarea}
|
|
@@ -61,16 +63,6 @@ export default function StickyNote({ props, onUpdate, onRemove }) {
|
|
|
61
63
|
}}
|
|
62
64
|
placeholder="Type here…"
|
|
63
65
|
/>
|
|
64
|
-
) : (
|
|
65
|
-
<p
|
|
66
|
-
className={styles.text}
|
|
67
|
-
onDoubleClick={() => setEditing(true)}
|
|
68
|
-
role="button"
|
|
69
|
-
tabIndex={0}
|
|
70
|
-
onKeyDown={(e) => { if (e.key === 'Enter') setEditing(true) }}
|
|
71
|
-
>
|
|
72
|
-
{text || 'Double-click to edit…'}
|
|
73
|
-
</p>
|
|
74
66
|
)}
|
|
75
67
|
</article>
|
|
76
68
|
|