@dfosco/storyboard-react 4.0.0-beta.9 → 4.0.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 (75) hide show
  1. package/package.json +6 -3
  2. package/src/AuthModal/AuthModal.jsx +134 -0
  3. package/src/AuthModal/AuthModal.module.css +221 -0
  4. package/src/BranchBar/BranchBar.jsx +56 -0
  5. package/src/BranchBar/BranchBar.module.css +230 -0
  6. package/src/BranchBar/useBranches.js +79 -0
  7. package/src/CommandPalette/CommandPalette.jsx +936 -0
  8. package/src/CommandPalette/CreateDialog.jsx +219 -0
  9. package/src/CommandPalette/command-palette.css +111 -0
  10. package/src/Icon.jsx +180 -0
  11. package/src/Viewfinder.jsx +1104 -57
  12. package/src/Viewfinder.module.css +1107 -149
  13. package/src/canvas/CanvasControls.jsx +51 -2
  14. package/src/canvas/CanvasControls.module.css +31 -0
  15. package/src/canvas/CanvasPage.bridge.test.jsx +142 -19
  16. package/src/canvas/CanvasPage.dragdrop.test.jsx +346 -0
  17. package/src/canvas/CanvasPage.jsx +807 -251
  18. package/src/canvas/CanvasPage.module.css +98 -50
  19. package/src/canvas/CanvasPage.multiselect.test.jsx +13 -11
  20. package/src/canvas/CanvasToolbar.jsx +2 -2
  21. package/src/canvas/MarqueeOverlay.jsx +20 -0
  22. package/src/canvas/PageSelector.jsx +239 -0
  23. package/src/canvas/PageSelector.module.css +165 -0
  24. package/src/canvas/PageSelector.test.jsx +104 -0
  25. package/src/canvas/canvasApi.js +22 -8
  26. package/src/canvas/canvasTheme.js +96 -52
  27. package/src/canvas/componentIsolate.jsx +33 -7
  28. package/src/canvas/useCanvas.js +9 -8
  29. package/src/canvas/useCanvas.test.js +4 -4
  30. package/src/canvas/useMarqueeSelect.js +187 -0
  31. package/src/canvas/useMarqueeSelect.test.js +78 -0
  32. package/src/canvas/widgets/CodePenEmbed.jsx +292 -0
  33. package/src/canvas/widgets/CodePenEmbed.module.css +161 -0
  34. package/src/canvas/widgets/ComponentWidget.jsx +42 -10
  35. package/src/canvas/widgets/ComponentWidget.module.css +6 -5
  36. package/src/canvas/widgets/FigmaEmbed.jsx +110 -24
  37. package/src/canvas/widgets/FigmaEmbed.module.css +21 -7
  38. package/src/canvas/widgets/LinkPreview.jsx +297 -11
  39. package/src/canvas/widgets/LinkPreview.module.css +386 -18
  40. package/src/canvas/widgets/LinkPreview.test.jsx +193 -0
  41. package/src/canvas/widgets/MarkdownBlock.jsx +86 -5
  42. package/src/canvas/widgets/MarkdownBlock.module.css +64 -15
  43. package/src/canvas/widgets/PrototypeEmbed.jsx +96 -145
  44. package/src/canvas/widgets/PrototypeEmbed.module.css +74 -4
  45. package/src/canvas/widgets/StickyNote.module.css +5 -0
  46. package/src/canvas/widgets/StickyNote.test.jsx +9 -9
  47. package/src/canvas/widgets/StoryWidget.jsx +277 -0
  48. package/src/canvas/widgets/StoryWidget.module.css +211 -0
  49. package/src/canvas/widgets/WidgetChrome.jsx +76 -20
  50. package/src/canvas/widgets/WidgetChrome.module.css +2 -6
  51. package/src/canvas/widgets/WidgetWrapper.module.css +2 -0
  52. package/src/canvas/widgets/codepenUrl.js +75 -0
  53. package/src/canvas/widgets/codepenUrl.test.js +76 -0
  54. package/src/canvas/widgets/embedInteraction.test.jsx +235 -0
  55. package/src/canvas/widgets/embedOverlay.module.css +35 -0
  56. package/src/canvas/widgets/embedTheme.js +138 -39
  57. package/src/canvas/widgets/githubUrl.js +82 -0
  58. package/src/canvas/widgets/githubUrl.test.js +74 -0
  59. package/src/canvas/widgets/iframeDevLogs.js +49 -0
  60. package/src/canvas/widgets/iframeDevLogs.test.jsx +81 -0
  61. package/src/canvas/widgets/index.js +4 -0
  62. package/src/canvas/widgets/pasteRules.js +295 -0
  63. package/src/canvas/widgets/pasteRules.test.js +474 -0
  64. package/src/canvas/widgets/snapshotDisplay.test.jsx +259 -0
  65. package/src/canvas/widgets/widgetConfig.js +16 -5
  66. package/src/canvas/widgets/widgetConfig.test.js +34 -12
  67. package/src/context.jsx +145 -16
  68. package/src/hooks/useSceneData.js +4 -2
  69. package/src/hooks/useThemeState.js +61 -0
  70. package/src/hooks/useThemeState.test.js +66 -0
  71. package/src/index.js +10 -0
  72. package/src/story/StoryPage.jsx +117 -0
  73. package/src/story/StoryPage.module.css +18 -0
  74. package/src/vite/data-plugin.js +348 -66
  75. package/src/vite/data-plugin.test.js +405 -5
@@ -3,9 +3,13 @@ import { remark } from 'remark'
3
3
  import remarkGfm from 'remark-gfm'
4
4
  import remarkHtml from 'remark-html'
5
5
  import WidgetWrapper from './WidgetWrapper.jsx'
6
- import { readProp, markdownSchema } from './widgetProps.js'
6
+ import ResizeHandle from './ResizeHandle.jsx'
7
+ import { readProp } from './widgetProps.js'
8
+ import { schemas } from './widgetConfig.js'
7
9
  import styles from './MarkdownBlock.module.css'
8
10
 
11
+ const markdownSchema = schemas['markdown']
12
+
9
13
  /**
10
14
  * Renders markdown to HTML using remark with GitHub Flavored Markdown support.
11
15
  */
@@ -15,12 +19,55 @@ function renderMarkdown(text) {
15
19
  .use(remarkGfm)
16
20
  .use(remarkHtml, { sanitize: false })
17
21
  .processSync(text)
18
- return String(result)
22
+ // Open all links in new tabs
23
+ return String(result).replace(/<a\s/g, '<a target="_blank" rel="noopener noreferrer" ')
24
+ }
25
+
26
+ /**
27
+ * Post-process rendered HTML to syntax-highlight fenced code blocks.
28
+ * remark-html outputs <pre><code class="language-xxx">...</code></pre>.
29
+ * We replace the code content with highlight.js output.
30
+ */
31
+ let hljsPromise = null
32
+ function getHljs() {
33
+ if (!hljsPromise) {
34
+ hljsPromise = import('@dfosco/storyboard-core/inspector/highlighter').then((mod) => mod)
35
+ }
36
+ return hljsPromise
37
+ }
38
+
39
+ async function highlightCodeBlocks(html) {
40
+ if (!html.includes('<code class="language-')) return html
41
+ const { createInspectorHighlighter } = await getHljs()
42
+ const hl = await createInspectorHighlighter()
43
+ return html.replace(
44
+ /<pre><code class="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g,
45
+ (match, lang, code) => {
46
+ try {
47
+ // Decode all HTML entities that remark-html may produce
48
+ const decoded = code
49
+ .replace(/&#x3C;/gi, '<')
50
+ .replace(/&#x3E;/gi, '>')
51
+ .replace(/&#x26;/gi, '&')
52
+ .replace(/&#x22;/gi, '"')
53
+ .replace(/&#x27;/gi, "'")
54
+ .replace(/&lt;/g, '<')
55
+ .replace(/&gt;/g, '>')
56
+ .replace(/&quot;/g, '"')
57
+ .replace(/&amp;/g, '&')
58
+ // codeToHtml returns a full <pre style="bg;fg"><code>...</code></pre>
59
+ return hl.codeToHtml(decoded, { lang })
60
+ } catch {
61
+ return match
62
+ }
63
+ }
64
+ )
19
65
  }
20
66
 
21
- export default function MarkdownBlock({ props, onUpdate }) {
67
+ export default function MarkdownBlock({ props, onUpdate, resizable }) {
22
68
  const content = readProp(props, 'content', markdownSchema)
23
69
  const width = readProp(props, 'width', markdownSchema)
70
+ const height = props?.height
24
71
  const canEdit = typeof onUpdate === 'function'
25
72
  const [editing, setEditing] = useState(false)
26
73
  const editingActive = canEdit && editing
@@ -28,6 +75,32 @@ export default function MarkdownBlock({ props, onUpdate }) {
28
75
  const blockRef = useRef(null)
29
76
  const [editHeight, setEditHeight] = useState(null)
30
77
 
78
+ const handleResize = useCallback((w, h) => {
79
+ onUpdate?.({ width: w, height: h })
80
+ }, [onUpdate])
81
+
82
+ const rawHtml = useMemo(() => renderMarkdown(content), [content])
83
+ const [renderedHtml, setRenderedHtml] = useState(rawHtml)
84
+
85
+ // Re-highlight when theme changes
86
+ const [themeKey, setThemeKey] = useState(0)
87
+ useEffect(() => {
88
+ function onThemeChanged() { setThemeKey((k) => k + 1) }
89
+ document.addEventListener('storyboard:theme:changed', onThemeChanged)
90
+ return () => document.removeEventListener('storyboard:theme:changed', onThemeChanged)
91
+ }, [])
92
+
93
+ // Async-highlight code blocks after initial render or theme change
94
+ useEffect(() => {
95
+ setRenderedHtml(rawHtml)
96
+ if (!rawHtml.includes('<code class="language-')) return
97
+ let cancelled = false
98
+ highlightCodeBlocks(rawHtml).then((highlighted) => {
99
+ if (!cancelled) setRenderedHtml(highlighted)
100
+ })
101
+ return () => { cancelled = true }
102
+ }, [rawHtml, themeKey])
103
+
31
104
  const handleContentChange = useCallback((e) => {
32
105
  onUpdate?.({ content: e.target.value })
33
106
  }, [onUpdate])
@@ -60,7 +133,7 @@ export default function MarkdownBlock({ props, onUpdate }) {
60
133
  <div
61
134
  ref={blockRef}
62
135
  className={styles.block}
63
- style={{ width, minHeight: editHeight || undefined }}
136
+ style={{ width, ...(height ? { height, overflow: 'auto' } : {}), minHeight: editHeight || undefined }}
64
137
  >
65
138
  {editingActive ? (
66
139
  <textarea
@@ -90,12 +163,20 @@ export default function MarkdownBlock({ props, onUpdate }) {
90
163
  tabIndex={canEdit ? 0 : undefined}
91
164
  onKeyDown={canEdit ? (e) => { if (e.key === 'Enter') setEditing(true) } : undefined}
92
165
  dangerouslySetInnerHTML={{
93
- __html: renderMarkdown(content) || (canEdit
166
+ __html: renderedHtml || (canEdit
94
167
  ? '<p class="placeholder">Double-click to edit…</p>'
95
168
  : '<p class="placeholder">No content</p>'),
96
169
  }}
97
170
  />
98
171
  )}
172
+ {resizable && (
173
+ <ResizeHandle
174
+ targetRef={blockRef}
175
+ minWidth={200}
176
+ minHeight={60}
177
+ onResize={handleResize}
178
+ />
179
+ )}
99
180
  </div>
100
181
  </WidgetWrapper>
101
182
  )
@@ -1,4 +1,5 @@
1
1
  .block {
2
+ position: relative;
2
3
  min-height: 80px;
3
4
  --sb--markdown-bg: var(--bgColor-default, #ffffff);
4
5
  --sb--markdown-fg: var(--fgColor-default, #1f2328);
@@ -23,6 +24,38 @@
23
24
  pointer-events: none;
24
25
  }
25
26
 
27
+ .preview a {
28
+ color: var(--sb--markdown-accent);
29
+ text-decoration: none;
30
+ pointer-events: auto;
31
+ cursor: pointer;
32
+ }
33
+
34
+ .preview a:hover {
35
+ text-decoration: underline;
36
+ }
37
+
38
+ .preview img {
39
+ max-width: 100%;
40
+ height: auto;
41
+ border-radius: 6px;
42
+ border: 1px solid var(--borderColor-default, #d0d7de);
43
+ margin: 8px 0;
44
+ display: block;
45
+ pointer-events: auto;
46
+ }
47
+
48
+ .preview video {
49
+ max-width: 100%;
50
+ height: auto;
51
+ border-radius: 6px;
52
+ border: 1px solid var(--borderColor-default, #d0d7de);
53
+ margin: 8px 0;
54
+ display: block;
55
+ pointer-events: auto;
56
+ background: var(--bgColor-muted, #f6f8fa);
57
+ }
58
+
26
59
  .preview h1 {
27
60
  font-size: 20px;
28
61
  font-weight: 700;
@@ -52,28 +85,33 @@
52
85
  background: var(--bgColor-neutral-muted, #afb8c133);
53
86
  padding: 2px 5px;
54
87
  border-radius: 4px;
55
- font-size: 13px;
56
- font-family: ui-monospace, monospace;
88
+ font-size: 12px;
89
+ font-weight: 400;
90
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
57
91
  }
58
92
 
59
93
  .preview ul {
60
94
  margin: 0 0 8px;
61
95
  padding-left: 20px;
96
+ list-style-type: disc;
62
97
  }
63
98
 
64
99
  .preview li {
65
100
  margin: 0 0 2px;
101
+ display: list-item;
66
102
  }
67
103
 
68
104
  .preview ol {
69
105
  margin: 0 0 8px;
70
106
  padding-left: 20px;
107
+ list-style-type: decimal;
71
108
  }
72
109
 
73
- /* GFM: Task lists */
110
+ /* GFM: Task lists — accent-colored checkboxes */
74
111
  .preview input[type="checkbox"] {
75
112
  margin-right: 6px;
76
113
  pointer-events: none;
114
+ accent-color: var(--sb--markdown-accent);
77
115
  }
78
116
 
79
117
  .preview li:has(input[type="checkbox"]) {
@@ -107,21 +145,21 @@
107
145
  font-weight: 600;
108
146
  }
109
147
 
110
- /* GFM: Autolinks */
111
- .preview a {
112
- color: var(--sb--markdown-accent);
113
- text-decoration: none;
114
- }
115
-
116
- .preview a:hover {
117
- text-decoration: underline;
118
- }
119
-
120
148
  /* Code blocks */
121
149
  .preview pre {
122
- background: var(--bgColor-neutral-muted, #afb8c133);
123
150
  padding: 12px 16px;
124
151
  border-radius: 6px;
152
+ border: 1px solid var(--borderColor-muted, #d8dee4);
153
+ overflow-x: auto;
154
+ margin: 8px 0;
155
+ background: var(--bgColor-neutral-muted, #afb8c133);
156
+ line-height: 1.4;
157
+ }
158
+
159
+ /* When inspector highlighter sets inline background, let it through */
160
+ .preview pre[style] {
161
+ border-radius: 6px;
162
+ border: 1px solid var(--borderColor-muted, #d8dee4);
125
163
  overflow-x: auto;
126
164
  margin: 8px 0;
127
165
  }
@@ -129,7 +167,18 @@
129
167
  .preview pre code {
130
168
  background: none;
131
169
  padding: 0;
132
- font-size: 13px;
170
+ font-size: 12px;
171
+ font-weight: 400;
172
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
173
+ white-space: pre;
174
+ word-break: normal;
175
+ overflow-wrap: normal;
176
+ display: block;
177
+ }
178
+
179
+ /* Suppress line numbers from inspector highlighter's .line spans */
180
+ .preview :global(.line::before) {
181
+ display: none !important;
133
182
  }
134
183
 
135
184
  /* Blockquotes */