@dfosco/storyboard-react 2.8.0 → 3.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.
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard-react",
3
- "version": "2.8.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@dfosco/storyboard-core": "2.8.0",
6
+ "@dfosco/storyboard-core": "3.0.0",
7
+ "@dfosco/tiny-canvas": "file:../tiny-canvas",
8
+ "@neodrag/react": "^2.3.1",
7
9
  "glob": "^11.0.0",
8
10
  "jsonc-parser": "^3.3.1"
9
11
  },
@@ -24,6 +26,7 @@
24
26
  "exports": {
25
27
  ".": "./src/index.js",
26
28
  "./vite": "./src/vite/data-plugin.js",
27
- "./hash-preserver": "./src/hashPreserver.js"
29
+ "./hash-preserver": "./src/hashPreserver.js",
30
+ "./canvas/CanvasPage": "./src/canvas/CanvasPage.jsx"
28
31
  }
29
32
  }
@@ -1,5 +1,5 @@
1
1
 
2
- import { useRef, useEffect } from 'react'
2
+ import { useRef, useEffect, useMemo } from 'react'
3
3
 
4
4
  /**
5
5
  * Viewfinder — thin React wrapper around the Svelte Viewfinder component.
@@ -23,9 +23,10 @@ export default function Viewfinder({ pageModules = {}, basePath, title = 'Storyb
23
23
 
24
24
  const shouldHideDefault = hideDefaultFlow ?? hideDefaultScene
25
25
 
26
- const knownRoutes = Object.keys(pageModules)
26
+ const knownRoutes = useMemo(() => Object.keys(pageModules)
27
27
  .map(p => p.replace('/src/prototypes/', '').replace('.jsx', ''))
28
- .filter(n => !n.startsWith('_') && n !== 'index' && n !== 'viewfinder')
28
+ .filter(n => !n.startsWith('_') && n !== 'index' && n !== 'viewfinder'),
29
+ [pageModules])
29
30
 
30
31
  useEffect(() => {
31
32
  if (!containerRef.current) return
@@ -53,7 +54,7 @@ export default function Viewfinder({ pageModules = {}, basePath, title = 'Storyb
53
54
  handleRef.current = null
54
55
  }
55
56
  }
56
- }, [title, subtitle, basePath, showThumbnails, shouldHideDefault])
57
+ }, [title, subtitle, basePath, knownRoutes, showThumbnails, shouldHideDefault])
57
58
 
58
59
  return <div ref={containerRef} style={{ minHeight: '100vh' }} />
59
60
  }
@@ -0,0 +1,145 @@
1
+ import { createElement, useCallback, useRef, useState } from 'react'
2
+ import { Canvas } from '@dfosco/tiny-canvas'
3
+ import { useCanvas } from './useCanvas.js'
4
+ import { getWidgetComponent } from './widgets/index.js'
5
+ import ComponentWidget from './widgets/ComponentWidget.jsx'
6
+ import CanvasToolbar from './CanvasToolbar.jsx'
7
+ import { updateCanvas, removeWidget as removeWidgetApi } from './canvasApi.js'
8
+ import styles from './CanvasPage.module.css'
9
+
10
+ /**
11
+ * Debounce helper — returns a function that delays invocation.
12
+ */
13
+ function debounce(fn, ms) {
14
+ let timer
15
+ return (...args) => {
16
+ clearTimeout(timer)
17
+ timer = setTimeout(() => fn(...args), ms)
18
+ }
19
+ }
20
+
21
+ /** Renders a single JSON-defined widget by type lookup. */
22
+ function WidgetRenderer({ widget, onUpdate, onRemove }) {
23
+ const Component = getWidgetComponent(widget.type)
24
+ if (!Component) {
25
+ console.warn(`[canvas] Unknown widget type: ${widget.type}`)
26
+ return null
27
+ }
28
+ return createElement(Component, {
29
+ id: widget.id,
30
+ props: widget.props,
31
+ onUpdate,
32
+ onRemove,
33
+ })
34
+ }
35
+
36
+ /**
37
+ * Generic canvas page component.
38
+ * Reads canvas data from the index and renders all widgets on a draggable surface.
39
+ *
40
+ * @param {{ name: string }} props - Canvas name as indexed by the data plugin
41
+ */
42
+ export default function CanvasPage({ name }) {
43
+ const { canvas, jsxExports, loading } = useCanvas(name)
44
+
45
+ // Local mutable copy of widgets for instant UI updates
46
+ const [localWidgets, setLocalWidgets] = useState(canvas?.widgets ?? null)
47
+ const [trackedCanvas, setTrackedCanvas] = useState(canvas)
48
+ if (canvas !== trackedCanvas) {
49
+ setTrackedCanvas(canvas)
50
+ setLocalWidgets(canvas?.widgets ?? null)
51
+ }
52
+
53
+ // Debounced save to server
54
+ const debouncedSave = useRef(
55
+ debounce((canvasName, widgets) => {
56
+ updateCanvas(canvasName, { widgets }).catch((err) =>
57
+ console.error('[canvas] Failed to save:', err)
58
+ )
59
+ }, 2000)
60
+ ).current
61
+
62
+ const handleWidgetUpdate = useCallback((widgetId, updates) => {
63
+ setLocalWidgets((prev) => {
64
+ if (!prev) return prev
65
+ const next = prev.map((w) =>
66
+ w.id === widgetId ? { ...w, props: { ...w.props, ...updates } } : w
67
+ )
68
+ debouncedSave(name, next)
69
+ return next
70
+ })
71
+ }, [name, debouncedSave])
72
+
73
+ const handleWidgetRemove = useCallback((widgetId) => {
74
+ setLocalWidgets((prev) => prev ? prev.filter((w) => w.id !== widgetId) : prev)
75
+ removeWidgetApi(name, widgetId).catch((err) =>
76
+ console.error('[canvas] Failed to remove widget:', err)
77
+ )
78
+ }, [name])
79
+
80
+ if (!canvas) {
81
+ return (
82
+ <div className={styles.empty}>
83
+ <p>Canvas &ldquo;{name}&rdquo; not found</p>
84
+ </div>
85
+ )
86
+ }
87
+
88
+ if (loading) {
89
+ return (
90
+ <div className={styles.loading}>
91
+ <p>Loading canvas…</p>
92
+ </div>
93
+ )
94
+ }
95
+
96
+ const canvasProps = {
97
+ centered: canvas.centered ?? false,
98
+ dotted: canvas.dotted ?? false,
99
+ grid: canvas.grid ?? false,
100
+ gridSize: canvas.gridSize ?? 18,
101
+ colorMode: canvas.colorMode ?? 'auto',
102
+ }
103
+
104
+ // Merge JSX-sourced widgets (from .canvas.jsx) and JSON widgets
105
+ const allChildren = []
106
+
107
+ // 1. JSX-sourced component widgets
108
+ if (jsxExports) {
109
+ for (const [exportName, Component] of Object.entries(jsxExports)) {
110
+ allChildren.push(
111
+ <div key={`jsx-${exportName}`} id={`jsx-${exportName}`}>
112
+ <ComponentWidget component={Component} />
113
+ </div>
114
+ )
115
+ }
116
+ }
117
+
118
+ // 2. JSON-defined mutable widgets
119
+ for (const widget of (localWidgets ?? [])) {
120
+ allChildren.push(
121
+ <div key={widget.id} id={widget.id}>
122
+ <WidgetRenderer
123
+ widget={widget}
124
+ onUpdate={(updates) => handleWidgetUpdate(widget.id, updates)}
125
+ onRemove={() => handleWidgetRemove(widget.id)}
126
+ />
127
+ </div>
128
+ )
129
+ }
130
+
131
+ return (
132
+ <>
133
+ <Canvas {...canvasProps}>
134
+ {allChildren}
135
+ </Canvas>
136
+ <CanvasToolbar
137
+ canvasName={name}
138
+ onWidgetAdded={() => {
139
+ // Reload the page to pick up the new widget from the updated .canvas.json
140
+ window.location.reload()
141
+ }}
142
+ />
143
+ </>
144
+ )
145
+ }
@@ -0,0 +1,15 @@
1
+ .empty,
2
+ .loading {
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ min-height: 100vh;
7
+ font-family: var(--tc-font-stack, system-ui, -apple-system, sans-serif);
8
+ color: var(--fgColor-muted, #656d76);
9
+ font-size: 16px;
10
+ }
11
+
12
+ .empty p,
13
+ .loading p {
14
+ margin: 0;
15
+ }
@@ -0,0 +1,76 @@
1
+ import { useState } from 'react'
2
+ import { addWidget as addWidgetApi } from './canvasApi.js'
3
+ import { schemas, getDefaults } from './widgets/widgetProps.js'
4
+ import styles from './CanvasToolbar.module.css'
5
+
6
+ const WIDGET_TYPES = [
7
+ { type: 'sticky-note', label: 'Sticky Note', icon: '📝' },
8
+ { type: 'markdown', label: 'Markdown', icon: '📄' },
9
+ { type: 'prototype', label: 'Prototype', icon: '🖥️' },
10
+ ]
11
+
12
+ /**
13
+ * Floating toolbar for adding widgets to a canvas.
14
+ */
15
+ export default function CanvasToolbar({ canvasName, onWidgetAdded }) {
16
+ const [open, setOpen] = useState(false)
17
+ const [adding, setAdding] = useState(false)
18
+
19
+ async function handleAdd(type) {
20
+ if (adding) return
21
+ setAdding(true)
22
+ try {
23
+ const defaultProps = schemas[type] ? getDefaults(schemas[type]) : {}
24
+ const result = await addWidgetApi(canvasName, {
25
+ type,
26
+ props: defaultProps,
27
+ position: { x: 0, y: 0 },
28
+ })
29
+ if (result.success) {
30
+ onWidgetAdded?.(result.widget)
31
+ setOpen(false)
32
+ }
33
+ } catch (err) {
34
+ console.error('[canvas] Failed to add widget:', err)
35
+ } finally {
36
+ setAdding(false)
37
+ }
38
+ }
39
+
40
+ return (
41
+ <nav className={styles.toolbar}>
42
+ {open ? (
43
+ <div className={styles.menu}>
44
+ <header className={styles.menuHeader}>
45
+ <span>Add widget</span>
46
+ <button
47
+ className={styles.closeBtn}
48
+ onClick={() => setOpen(false)}
49
+ aria-label="Close"
50
+ >×</button>
51
+ </header>
52
+ {WIDGET_TYPES.map(({ type, label, icon }) => (
53
+ <button
54
+ key={type}
55
+ className={styles.menuItem}
56
+ onClick={() => handleAdd(type)}
57
+ disabled={adding}
58
+ >
59
+ <span className={styles.menuIcon}>{icon}</span>
60
+ {label}
61
+ </button>
62
+ ))}
63
+ </div>
64
+ ) : (
65
+ <button
66
+ className={styles.addBtn}
67
+ onClick={() => setOpen(true)}
68
+ title="Add widget"
69
+ aria-label="Add widget"
70
+ >
71
+ +
72
+ </button>
73
+ )}
74
+ </nav>
75
+ )
76
+ }
@@ -0,0 +1,92 @@
1
+ .toolbar {
2
+ position: fixed;
3
+ bottom: 80px;
4
+ left: 24px;
5
+ z-index: 1000;
6
+ font-family: var(--tc-font-stack, system-ui, -apple-system, sans-serif);
7
+ }
8
+
9
+ .addBtn {
10
+ all: unset;
11
+ cursor: pointer;
12
+ width: 48px;
13
+ height: 48px;
14
+ border-radius: 50%;
15
+ background: var(--bgColor-accent-emphasis, #2f81f7);
16
+ color: var(--fgColor-onEmphasis, #ffffff);
17
+ font-size: 28px;
18
+ font-weight: 300;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
23
+ transition: transform 150ms, background 150ms;
24
+ }
25
+
26
+ .addBtn:hover {
27
+ transform: scale(1.08);
28
+ background: var(--bgColor-accent-emphasis, #388bfd);
29
+ }
30
+
31
+ .menu {
32
+ background: var(--bgColor-default, #ffffff);
33
+ border: 1px solid var(--borderColor-muted, rgba(0, 0, 0, 0.15));
34
+ border-radius: 12px;
35
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
36
+ overflow: hidden;
37
+ min-width: 180px;
38
+ }
39
+
40
+ .menuHeader {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ padding: 10px 14px;
45
+ font-size: 12px;
46
+ font-weight: 600;
47
+ text-transform: uppercase;
48
+ letter-spacing: 0.5px;
49
+ color: var(--fgColor-muted, #656d76);
50
+ border-bottom: 1px solid var(--borderColor-muted, rgba(0, 0, 0, 0.1));
51
+ }
52
+
53
+ .closeBtn {
54
+ all: unset;
55
+ cursor: pointer;
56
+ font-size: 16px;
57
+ line-height: 1;
58
+ color: var(--fgColor-muted, #656d76);
59
+ padding: 0 2px;
60
+ border-radius: 4px;
61
+ }
62
+
63
+ .closeBtn:hover {
64
+ color: var(--fgColor-default, #1f2328);
65
+ }
66
+
67
+ .menuItem {
68
+ all: unset;
69
+ cursor: pointer;
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 10px;
73
+ padding: 10px 14px;
74
+ font-size: 14px;
75
+ color: var(--fgColor-default, #1f2328);
76
+ width: 100%;
77
+ box-sizing: border-box;
78
+ transition: background 100ms;
79
+ }
80
+
81
+ .menuItem:hover {
82
+ background: var(--bgColor-muted, #f6f8fa);
83
+ }
84
+
85
+ .menuItem:disabled {
86
+ opacity: 0.5;
87
+ cursor: default;
88
+ }
89
+
90
+ .menuIcon {
91
+ font-size: 18px;
92
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Client-side API for canvas CRUD operations.
3
+ * Calls the /_storyboard/canvas/ server endpoints.
4
+ */
5
+
6
+ const BASE = '/_storyboard/canvas'
7
+
8
+ function getApiBase() {
9
+ const base = (import.meta.env?.BASE_URL || '/').replace(/\/$/, '')
10
+ return base + BASE
11
+ }
12
+
13
+ async function request(path, method, body) {
14
+ const url = getApiBase() + path
15
+ const res = await fetch(url, {
16
+ method,
17
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
18
+ body: body ? JSON.stringify(body) : undefined,
19
+ })
20
+ return res.json()
21
+ }
22
+
23
+ export function listCanvases() {
24
+ return request('/list', 'GET')
25
+ }
26
+
27
+ export function createCanvas(data) {
28
+ return request('/create', 'POST', data)
29
+ }
30
+
31
+ export function updateCanvas(name, { widgets, sources, settings }) {
32
+ return request('/update', 'PUT', { name, widgets, sources, settings })
33
+ }
34
+
35
+ export function addWidget(name, { type, props, position }) {
36
+ return request('/widget', 'POST', { name, type, props, position })
37
+ }
38
+
39
+ export function removeWidget(name, widgetId) {
40
+ return request('/widget', 'DELETE', { name, widgetId })
41
+ }
@@ -0,0 +1,74 @@
1
+ import { useState, useEffect, useMemo } from 'react'
2
+ import { getCanvasData } from '@dfosco/storyboard-core'
3
+
4
+ /**
5
+ * Fetch fresh canvas data from the server's .canvas.json file.
6
+ * Falls back to build-time data if the server is unavailable.
7
+ */
8
+ async function fetchCanvasFromServer(name) {
9
+ try {
10
+ const base = (import.meta.env?.BASE_URL || '/').replace(/\/$/, '')
11
+ const res = await fetch(`${base}/_storyboard/canvas/read?name=${encodeURIComponent(name)}`)
12
+ if (res.ok) return res.json()
13
+ } catch { /* fall back to build-time data */ }
14
+ return null
15
+ }
16
+
17
+ /**
18
+ * Hook to load canvas data by name.
19
+ * Uses build-time data for static config (routes, JSX path), but fetches
20
+ * fresh widget data from the server to pick up persisted edits.
21
+ *
22
+ * @param {string} name - Canvas name as indexed by the data plugin
23
+ * @returns {{ canvas: object|null, jsxExports: object|null, loading: boolean }}
24
+ */
25
+ export function useCanvas(name) {
26
+ const buildTimeCanvas = useMemo(() => getCanvasData(name), [name])
27
+ const [canvas, setCanvas] = useState(buildTimeCanvas)
28
+ const [jsxExports, setJsxExports] = useState(null)
29
+ const [loading, setLoading] = useState(true)
30
+
31
+ // Fetch fresh data from server on mount
32
+ useEffect(() => {
33
+ if (!buildTimeCanvas) {
34
+ setCanvas(null)
35
+ setLoading(false)
36
+ return
37
+ }
38
+
39
+ setLoading(true)
40
+ fetchCanvasFromServer(name).then((fresh) => {
41
+ if (fresh) {
42
+ // Merge: use server data for widgets/sources, keep build-time for _route/_jsxModule
43
+ setCanvas({ ...buildTimeCanvas, ...fresh })
44
+ } else {
45
+ setCanvas(buildTimeCanvas)
46
+ }
47
+ setLoading(false)
48
+ })
49
+ }, [name, buildTimeCanvas])
50
+
51
+ useEffect(() => {
52
+ if (!canvas?._jsxModule) {
53
+ setJsxExports(null)
54
+ return
55
+ }
56
+
57
+ import(/* @vite-ignore */ canvas._jsxModule)
58
+ .then((mod) => {
59
+ const exports = {}
60
+ for (const [key, value] of Object.entries(mod)) {
61
+ if (key !== 'default' && typeof value === 'function') {
62
+ exports[key] = value
63
+ }
64
+ }
65
+ setJsxExports(exports)
66
+ })
67
+ .catch((err) => {
68
+ console.error(`[storyboard] Failed to load canvas JSX module: ${canvas._jsxModule}`, err)
69
+ setJsxExports(null)
70
+ })
71
+ }, [canvas?._jsxModule])
72
+
73
+ return { canvas, jsxExports, loading }
74
+ }
@@ -0,0 +1,15 @@
1
+ import WidgetWrapper from './WidgetWrapper.jsx'
2
+
3
+ /**
4
+ * Renders a live JSX export from a .canvas.jsx companion file.
5
+ * Content is read-only (re-renders on HMR), only position is mutable.
6
+ */
7
+ export default function ComponentWidget({ component: Component }) {
8
+ if (!Component) return null
9
+
10
+ return (
11
+ <WidgetWrapper>
12
+ <Component />
13
+ </WidgetWrapper>
14
+ )
15
+ }
@@ -0,0 +1,87 @@
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import WidgetWrapper from './WidgetWrapper.jsx'
3
+ import { readProp, markdownSchema } from './widgetProps.js'
4
+ import styles from './MarkdownBlock.module.css'
5
+
6
+ /**
7
+ * Renders markdown as plain HTML using a minimal built-in converter.
8
+ */
9
+ function renderMarkdown(text) {
10
+ if (!text) return ''
11
+ return text
12
+ .replace(/^### (.+)$/gm, '<h3>$1</h3>')
13
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
14
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
15
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
16
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
17
+ .replace(/`(.+?)`/g, '<code>$1</code>')
18
+ .replace(/^- (.+)$/gm, '<li>$1</li>')
19
+ .replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
20
+ .replace(/\n\n/g, '</p><p>')
21
+ .replace(/\n/g, '<br>')
22
+ .replace(/^(.+)$/gm, (line) => {
23
+ if (line.startsWith('<')) return line
24
+ return `<p>${line}</p>`
25
+ })
26
+ }
27
+
28
+ export default function MarkdownBlock({ props, onUpdate, onRemove }) {
29
+ const content = readProp(props, 'content', markdownSchema)
30
+ const width = readProp(props, 'width', markdownSchema)
31
+ const [editing, setEditing] = useState(false)
32
+ const textareaRef = useRef(null)
33
+ const blockRef = useRef(null)
34
+ const [editHeight, setEditHeight] = useState(null)
35
+
36
+ useEffect(() => {
37
+ if (editing) {
38
+ // Capture the preview height before switching to editor
39
+ if (blockRef.current && !editHeight) {
40
+ setEditHeight(blockRef.current.offsetHeight)
41
+ }
42
+ if (textareaRef.current) {
43
+ textareaRef.current.focus()
44
+ }
45
+ } else {
46
+ setEditHeight(null)
47
+ }
48
+ }, [editing, editHeight])
49
+
50
+ return (
51
+ <WidgetWrapper onRemove={onRemove}>
52
+ <div
53
+ ref={blockRef}
54
+ className={styles.block}
55
+ style={{ width, minHeight: editHeight || undefined }}
56
+ >
57
+ {editing ? (
58
+ <textarea
59
+ ref={textareaRef}
60
+ className={styles.editor}
61
+ style={{ minHeight: editHeight ? editHeight - 2 : undefined }}
62
+ value={content}
63
+ onChange={(e) => onUpdate?.({ content: e.target.value })}
64
+ onBlur={() => setEditing(false)}
65
+ onMouseDown={(e) => e.stopPropagation()}
66
+ onPointerDown={(e) => e.stopPropagation()}
67
+ onKeyDown={(e) => {
68
+ if (e.key === 'Escape') setEditing(false)
69
+ }}
70
+ placeholder="Write markdown…"
71
+ />
72
+ ) : (
73
+ <div
74
+ className={styles.preview}
75
+ onDoubleClick={() => setEditing(true)}
76
+ role="button"
77
+ tabIndex={0}
78
+ onKeyDown={(e) => { if (e.key === 'Enter') setEditing(true) }}
79
+ dangerouslySetInnerHTML={{
80
+ __html: renderMarkdown(content) || '<p class="placeholder">Double-click to edit…</p>',
81
+ }}
82
+ />
83
+ )}
84
+ </div>
85
+ </WidgetWrapper>
86
+ )
87
+ }
@@ -0,0 +1,78 @@
1
+ .block {
2
+ min-height: 80px;
3
+ background: var(--bgColor-default, #ffffff);
4
+ font-family: var(--tc-font-stack, system-ui, -apple-system, sans-serif);
5
+ }
6
+
7
+ .preview {
8
+ padding: 16px 20px;
9
+ font-size: 14px;
10
+ line-height: 1.6;
11
+ color: var(--fgColor-default, #1f2328);
12
+ cursor: text;
13
+ min-height: 60px;
14
+ }
15
+
16
+ .preview h1 {
17
+ font-size: 20px;
18
+ font-weight: 700;
19
+ margin: 0 0 8px;
20
+ line-height: 1.3;
21
+ }
22
+
23
+ .preview h2 {
24
+ font-size: 17px;
25
+ font-weight: 600;
26
+ margin: 0 0 6px;
27
+ line-height: 1.3;
28
+ }
29
+
30
+ .preview h3 {
31
+ font-size: 15px;
32
+ font-weight: 600;
33
+ margin: 0 0 4px;
34
+ line-height: 1.3;
35
+ }
36
+
37
+ .preview p {
38
+ margin: 0 0 8px;
39
+ }
40
+
41
+ .preview code {
42
+ background: var(--bgColor-neutral-muted, #afb8c133);
43
+ padding: 2px 5px;
44
+ border-radius: 4px;
45
+ font-size: 13px;
46
+ font-family: ui-monospace, monospace;
47
+ }
48
+
49
+ .preview ul {
50
+ margin: 0 0 8px;
51
+ padding-left: 20px;
52
+ }
53
+
54
+ .preview li {
55
+ margin: 0 0 2px;
56
+ }
57
+
58
+ .preview :global(.placeholder) {
59
+ color: var(--fgColor-muted, #656d76);
60
+ font-style: italic;
61
+ }
62
+
63
+ .editor {
64
+ display: block;
65
+ width: 100%;
66
+ height: 100%;
67
+ box-sizing: border-box;
68
+ min-height: 120px;
69
+ padding: 16px 20px;
70
+ border: none;
71
+ outline: none;
72
+ background: var(--bgColor-default, #ffffff);
73
+ font-family: ui-monospace, SFMono-Regular, monospace;
74
+ font-size: 13px;
75
+ line-height: 1.5;
76
+ color: var(--fgColor-default, #1f2328);
77
+ resize: none;
78
+ }