@bendyline/squisq-editor-react 1.5.3 → 1.6.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/dist/index.d.ts +757 -20
- package/dist/index.js +16875 -6883
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/BlockCardView.tsx +121 -0
- package/src/BlockPreviewPanel.tsx +69 -0
- package/src/BlockPropertiesPopover.tsx +191 -0
- package/src/EditorContext.tsx +143 -0
- package/src/EditorShell.tsx +200 -120
- package/src/FolderView.tsx +131 -0
- package/src/Icon.tsx +26 -0
- package/src/ImageEditor.tsx +69 -22
- package/src/OutlinePanel.tsx +38 -3
- package/src/PlainHtmlPreview.tsx +30 -3
- package/src/PreviewControls.tsx +180 -8
- package/src/RawEditor.tsx +169 -13
- package/src/RecorderEntry.tsx +9 -16
- package/src/TemplateAnnotation.ts +44 -0
- package/src/TemplatePicker.tsx +329 -54
- package/src/ThemeCustomizerPanel.tsx +30 -336
- package/src/ThemePicker.tsx +112 -3
- package/src/TimelineBlockPreview.tsx +37 -0
- package/src/TimelineTrack.tsx +671 -0
- package/src/Toolbar.tsx +528 -174
- package/src/Tooltip.tsx +22 -4
- package/src/TransitionPicker.tsx +351 -0
- package/src/VersionHistoryPanel.tsx +2 -14
- package/src/ViewMenuPanel.tsx +17 -14
- package/src/WysiwygEditor.tsx +161 -65
- package/src/__tests__/blockProperties.test.ts +92 -0
- package/src/__tests__/blockRange.test.ts +105 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +73 -0
- package/src/__tests__/createShapeLayer.test.ts +46 -0
- package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
- package/src/__tests__/embeddedMedia.test.ts +48 -0
- package/src/__tests__/headingTransition.test.ts +138 -0
- package/src/__tests__/layoutChildRoundTrip.test.ts +71 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +10 -8
- package/src/__tests__/recorderMediaInsert.test.ts +86 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +18 -0
- package/src/__tests__/templatePickerMetadata.test.ts +32 -0
- package/src/__tests__/timelineSource.test.ts +134 -0
- package/src/__tests__/tiptapBridge.test.ts +92 -0
- package/src/__tests__/tiptapBridgeConformance.test.ts +47 -0
- package/src/__tests__/tooltip.test.tsx +72 -0
- package/src/__tests__/transitionCatalog.test.ts +64 -0
- package/src/__tests__/useBlockNavigator.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +24 -0
- package/src/__tests__/useTimelineClock.test.ts +21 -0
- package/src/blockProperties.ts +88 -0
- package/src/blockRange.ts +132 -0
- package/src/buildPreviewDoc.ts +98 -9
- package/src/customTemplates/AddBin.tsx +126 -0
- package/src/customTemplates/CustomLayoutManager.tsx +233 -0
- package/src/customTemplates/CustomTemplateContext.tsx +182 -0
- package/src/customTemplates/LayerToolbar.tsx +580 -0
- package/src/customTemplates/ShapeGlyph.tsx +47 -0
- package/src/customTemplates/TemplateDesigner.tsx +430 -0
- package/src/customTemplates/__tests__/library.test.ts +88 -0
- package/src/customTemplates/__tests__/normalizePositions.test.ts +109 -0
- package/src/customTemplates/__tests__/shapeDefs.test.ts +49 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +95 -0
- package/src/customTemplates/designer.css +673 -0
- package/src/customTemplates/index.ts +31 -0
- package/src/customTemplates/library.ts +97 -0
- package/src/customTemplates/normalizePositions.ts +75 -0
- package/src/customTemplates/shapeDefs.ts +131 -0
- package/src/customTemplates/thumbnail.tsx +63 -0
- package/src/customTemplates/tokenDefs.ts +60 -0
- package/src/customTemplates/useDocCustomTemplates.ts +52 -0
- package/src/customTemplates/useMemoryLayerAdapter.ts +123 -0
- package/src/customThemes/CustomThemeContext.tsx +179 -0
- package/src/customThemes/CustomThemeDialog.tsx +286 -0
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +64 -0
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +47 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +51 -0
- package/src/customThemes/customThemeLibrary.ts +97 -0
- package/src/customThemes/index.ts +31 -0
- package/src/customThemes/themeControls.tsx +229 -0
- package/src/customThemes/themeDraft.ts +272 -0
- package/src/customThemes/useDocCustomThemes.ts +49 -0
- package/src/diagram/DiagramCanvas.tsx +240 -0
- package/src/diagram/DiagramExtension.ts +209 -0
- package/src/diagram/DiagramMaximizedOverlay.tsx +46 -0
- package/src/diagram/DiagramWidget.tsx +270 -0
- package/src/diagram/diagramCommands.ts +604 -0
- package/src/diagram/diagramConstants.ts +17 -0
- package/src/diagram/useDiagramData.ts +126 -0
- package/src/embeddedMedia.ts +78 -0
- package/src/frontmatter.ts +29 -0
- package/src/headingTransition.ts +231 -0
- package/src/imageEditor/CanvasSurface.tsx +383 -88
- package/src/imageEditor/PropertiesPanel.tsx +47 -1
- package/src/imageEditor/Toolbar.tsx +229 -16
- package/src/imageEditor/createShapeLayer.ts +280 -0
- package/src/imageEditor/icons.tsx +34 -114
- package/src/imageEditor/image-editor.css +54 -5
- package/src/imageEditor/state.ts +23 -3
- package/src/index.ts +77 -0
- package/src/recorder/RecorderModal.tsx +120 -53
- package/src/recorder/RecorderPanel.tsx +2 -26
- package/src/recorder/hooks/useMediaRecorder.ts +8 -1
- package/src/recorder/insertMediaBlock.ts +30 -0
- package/src/resolveBlockVisual.ts +33 -0
- package/src/scene/Scene.tsx +540 -0
- package/src/scene/SceneBlockExtension.ts +198 -0
- package/src/scene/SceneBlockToolbar.tsx +201 -0
- package/src/scene/SceneBlockWidget.tsx +434 -0
- package/src/scene/ScenePropsBar.tsx +85 -0
- package/src/scene/SceneSelection.tsx +107 -0
- package/src/scene/SceneViewport.tsx +102 -0
- package/src/scene/ShapePalette.tsx +181 -0
- package/src/scene/__tests__/DiagramAdapter.test.ts +56 -0
- package/src/scene/__tests__/bezierEdit.test.ts +85 -0
- package/src/scene/__tests__/blockLayers.test.ts +57 -0
- package/src/scene/__tests__/shapeLayers.test.ts +106 -0
- package/src/scene/__tests__/useSceneHitTest.test.ts +90 -0
- package/src/scene/__tests__/useScenePanZoom.test.ts +103 -0
- package/src/scene/adapters/DiagramAdapter.ts +168 -0
- package/src/scene/adapters/DrawingAdapter.ts +415 -0
- package/src/scene/adapters/LayoutAdapter.ts +310 -0
- package/src/scene/adapters/blockLayers.ts +159 -0
- package/src/scene/commands/SceneCommand.ts +70 -0
- package/src/scene/commands/drawingCommands.ts +318 -0
- package/src/scene/commands/layoutCommands.ts +301 -0
- package/src/scene/hooks/useSceneHitTest.ts +105 -0
- package/src/scene/hooks/useScenePanZoom.ts +147 -0
- package/src/scene/hooks/useSceneSelection.ts +62 -0
- package/src/scene/index.ts +95 -0
- package/src/scene/layers/DiagramEdges.tsx +127 -0
- package/src/scene/layers/edgeGeometry.ts +77 -0
- package/src/scene/layers/nodeCard.tsx +145 -0
- package/src/scene/layers/renderLayer.tsx +70 -0
- package/src/scene/layers/shapeLayers.ts +201 -0
- package/src/scene/paths/bezierEdit.ts +208 -0
- package/src/scene/scene.css +649 -0
- package/src/scene/text/SceneTextOverlay.tsx +161 -0
- package/src/scene/text/sceneTextChannel.ts +40 -0
- package/src/scene/text/sceneTextConfig.ts +27 -0
- package/src/scene/text/sceneTiptap.ts +36 -0
- package/src/scene/text/useSceneTextEditing.ts +39 -0
- package/src/scene/tools/ConnectTool.ts +111 -0
- package/src/scene/tools/DrawingConnectTool.ts +161 -0
- package/src/scene/tools/PathTool.ts +158 -0
- package/src/scene/tools/PlaceTool.ts +47 -0
- package/src/scene/tools/SceneTool.ts +75 -0
- package/src/scene/tools/SelectTool.ts +284 -0
- package/src/scene/tools/ShapeTool.ts +144 -0
- package/src/scene/tools/TextTool.ts +72 -0
- package/src/scene/tools/TokenTool.ts +95 -0
- package/src/scene/tools/createDrawShapeTool.ts +82 -0
- package/src/styles/diagram.css +183 -0
- package/src/styles/editor.css +1615 -203
- package/src/styles/folder-view.css +210 -0
- package/src/styles/image-edit-affordance.css +2 -2
- package/src/styles/index.css +4 -0
- package/src/timelineSource.ts +244 -0
- package/src/tiptapBridge.ts +115 -34
- package/src/tooltipPlacement.ts +13 -0
- package/src/transitionCatalog.ts +159 -0
- package/src/types/monaco-shims.d.ts +10 -0
- package/src/useBlockNavigator.ts +153 -0
- package/src/useMonacoLoader.ts +23 -1
- package/src/useTimelineClock.ts +76 -0
- package/src/utils/dropUtils.ts +1 -1
package/src/Tooltip.tsx
CHANGED
|
@@ -7,19 +7,21 @@
|
|
|
7
7
|
* immediate. Mount once near the root of the editor shell.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { useEffect, useRef, useState } from 'react';
|
|
10
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
11
11
|
import { createPortal } from 'react-dom';
|
|
12
|
+
import { clampTooltipLeft } from './tooltipPlacement';
|
|
12
13
|
|
|
13
14
|
const SHOW_DELAY_MS = 180;
|
|
14
15
|
|
|
15
16
|
interface TooltipState {
|
|
16
17
|
label: string;
|
|
17
18
|
top: number;
|
|
18
|
-
|
|
19
|
+
anchorX: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function TooltipLayer() {
|
|
22
23
|
const [state, setState] = useState<TooltipState | null>(null);
|
|
24
|
+
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
|
23
25
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
24
26
|
const currentTargetRef = useRef<HTMLElement | null>(null);
|
|
25
27
|
// Visibility tracked in a ref so `handleOver` can decide whether to swap
|
|
@@ -27,6 +29,20 @@ export function TooltipLayer() {
|
|
|
27
29
|
// the effect to re-run (and re-register all listeners) on every change.
|
|
28
30
|
const visibleRef = useRef(false);
|
|
29
31
|
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
if (!state) return;
|
|
34
|
+
const node = tooltipRef.current;
|
|
35
|
+
if (!node) return;
|
|
36
|
+
|
|
37
|
+
const rect = node.getBoundingClientRect();
|
|
38
|
+
const tooltipWidth = rect.width || node.offsetWidth;
|
|
39
|
+
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
40
|
+
const left = clampTooltipLeft(state.anchorX, tooltipWidth, viewportWidth);
|
|
41
|
+
|
|
42
|
+
node.style.left = `${left}px`;
|
|
43
|
+
node.style.visibility = 'visible';
|
|
44
|
+
}, [state]);
|
|
45
|
+
|
|
30
46
|
useEffect(() => {
|
|
31
47
|
const clearTimer = () => {
|
|
32
48
|
if (timerRef.current) {
|
|
@@ -48,7 +64,7 @@ export function TooltipLayer() {
|
|
|
48
64
|
setState({
|
|
49
65
|
label,
|
|
50
66
|
top: rect.bottom + 6,
|
|
51
|
-
|
|
67
|
+
anchorX: rect.left + rect.width / 2,
|
|
52
68
|
});
|
|
53
69
|
};
|
|
54
70
|
|
|
@@ -111,10 +127,12 @@ export function TooltipLayer() {
|
|
|
111
127
|
<div
|
|
112
128
|
role="tooltip"
|
|
113
129
|
className="squisq-tooltip"
|
|
130
|
+
ref={tooltipRef}
|
|
114
131
|
style={{
|
|
115
132
|
position: 'fixed',
|
|
116
133
|
top: state.top,
|
|
117
|
-
left: state.
|
|
134
|
+
left: state.anchorX,
|
|
135
|
+
visibility: 'hidden',
|
|
118
136
|
}}
|
|
119
137
|
>
|
|
120
138
|
{state.label}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TransitionPicker
|
|
3
|
+
*
|
|
4
|
+
* Toolbar control shown when a block (heading) is active. A trigger button
|
|
5
|
+
* opens a portal flyout with a curated, grouped, searchable menu of block
|
|
6
|
+
* transitions, plus direction and duration sub-controls. Selecting a
|
|
7
|
+
* transition writes `transition=` (and optionally `transitionDirection=` /
|
|
8
|
+
* `transitionDuration=`) onto the block — the parent does the actual edit
|
|
9
|
+
* via `onChange`; this component is purely presentational.
|
|
10
|
+
*
|
|
11
|
+
* Positioning/portal/outside-click logic mirrors `TemplatePicker` so the two
|
|
12
|
+
* toolbar popovers behave identically.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
16
|
+
import { createPortal } from 'react-dom';
|
|
17
|
+
import { getTransitionClass } from '@bendyline/squisq-react';
|
|
18
|
+
import type { TransitionDirection, TransitionType } from '@bendyline/squisq/schemas';
|
|
19
|
+
import type { TransitionFields } from './headingTransition';
|
|
20
|
+
import { EMPTY_TRANSITION } from './headingTransition';
|
|
21
|
+
import {
|
|
22
|
+
TRANSITION_GROUPS,
|
|
23
|
+
TRANSITION_ENTRIES,
|
|
24
|
+
DIRECTION_OPTIONS,
|
|
25
|
+
findTransitionEntry,
|
|
26
|
+
transitionLabel,
|
|
27
|
+
type TransitionCatalogEntry,
|
|
28
|
+
} from './transitionCatalog';
|
|
29
|
+
|
|
30
|
+
export interface TransitionPickerProps {
|
|
31
|
+
value: TransitionFields;
|
|
32
|
+
onChange: (next: TransitionFields) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const FLYOUT_ID = 'squisq-transition-flyout-portal';
|
|
36
|
+
|
|
37
|
+
export function TransitionPicker({ value, onChange }: TransitionPickerProps) {
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
const [style, setStyle] = useState<React.CSSProperties>({});
|
|
40
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
41
|
+
|
|
42
|
+
const updatePosition = () => {
|
|
43
|
+
if (!triggerRef.current) return;
|
|
44
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
45
|
+
const flyout = document.getElementById(FLYOUT_ID);
|
|
46
|
+
const flyoutRect = flyout?.getBoundingClientRect();
|
|
47
|
+
const width = flyoutRect?.width ?? 460;
|
|
48
|
+
// Natural content height (the list scrolls inside, so scrollHeight is the
|
|
49
|
+
// full unclamped height we'd like to show).
|
|
50
|
+
const contentHeight = flyout?.scrollHeight ?? 520;
|
|
51
|
+
const margin = 8;
|
|
52
|
+
const gap = 6;
|
|
53
|
+
const vw = window.innerWidth;
|
|
54
|
+
const vh = window.innerHeight;
|
|
55
|
+
|
|
56
|
+
const left = Math.min(Math.max(margin, rect.left), Math.max(margin, vw - width - margin));
|
|
57
|
+
|
|
58
|
+
// Prefer opening below the trigger; flip above when there's clearly more
|
|
59
|
+
// room there. Either way cap the height to the available space so the
|
|
60
|
+
// panel never runs off the viewport — the list scrolls within it.
|
|
61
|
+
const spaceBelow = vh - rect.bottom - gap - margin;
|
|
62
|
+
const spaceAbove = rect.top - gap - margin;
|
|
63
|
+
const openDown = spaceBelow >= contentHeight || spaceBelow >= spaceAbove;
|
|
64
|
+
const top = openDown
|
|
65
|
+
? rect.bottom + gap
|
|
66
|
+
: Math.max(margin, rect.top - gap - Math.min(contentHeight, spaceAbove));
|
|
67
|
+
const maxHeight = Math.max(140, openDown ? spaceBelow : spaceAbove);
|
|
68
|
+
|
|
69
|
+
setStyle({ position: 'fixed', top, left, maxHeight, zIndex: 9999 });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleOpen = () => {
|
|
73
|
+
updatePosition();
|
|
74
|
+
setOpen((v) => !v);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Close on outside click
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!open) return;
|
|
80
|
+
const handler = (e: MouseEvent) => {
|
|
81
|
+
const target = e.target as Node;
|
|
82
|
+
const inTrigger = triggerRef.current?.contains(target);
|
|
83
|
+
const inFlyout = document.getElementById(FLYOUT_ID)?.contains(target);
|
|
84
|
+
if (!inTrigger && !inFlyout) setOpen(false);
|
|
85
|
+
};
|
|
86
|
+
document.addEventListener('mousedown', handler);
|
|
87
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
88
|
+
}, [open]);
|
|
89
|
+
|
|
90
|
+
// Close on Escape
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!open) return;
|
|
93
|
+
const handler = (e: KeyboardEvent) => {
|
|
94
|
+
if (e.key === 'Escape') setOpen(false);
|
|
95
|
+
};
|
|
96
|
+
document.addEventListener('keydown', handler);
|
|
97
|
+
return () => document.removeEventListener('keydown', handler);
|
|
98
|
+
}, [open]);
|
|
99
|
+
|
|
100
|
+
// Reposition on scroll/resize while open (and once after mount for width)
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!open) return;
|
|
103
|
+
requestAnimationFrame(updatePosition);
|
|
104
|
+
const handler = () => updatePosition();
|
|
105
|
+
window.addEventListener('scroll', handler, true);
|
|
106
|
+
window.addEventListener('resize', handler);
|
|
107
|
+
return () => {
|
|
108
|
+
window.removeEventListener('scroll', handler, true);
|
|
109
|
+
window.removeEventListener('resize', handler);
|
|
110
|
+
};
|
|
111
|
+
}, [open]);
|
|
112
|
+
|
|
113
|
+
const flyout = open
|
|
114
|
+
? createPortal(
|
|
115
|
+
<TransitionFlyout style={style} value={value} onChange={onChange} />,
|
|
116
|
+
document.body,
|
|
117
|
+
)
|
|
118
|
+
: null;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="squisq-transition-picker">
|
|
122
|
+
<button
|
|
123
|
+
ref={triggerRef}
|
|
124
|
+
type="button"
|
|
125
|
+
className={`squisq-transition-picker-trigger${open ? ' squisq-transition-picker-trigger--open' : ''}`}
|
|
126
|
+
onClick={handleOpen}
|
|
127
|
+
aria-haspopup="menu"
|
|
128
|
+
aria-expanded={open}
|
|
129
|
+
title="Block transition"
|
|
130
|
+
>
|
|
131
|
+
<span className="squisq-transition-picker-trigger-label">Transition:</span>
|
|
132
|
+
<span className="squisq-transition-picker-trigger-value">
|
|
133
|
+
{transitionLabel(value.type)}
|
|
134
|
+
</span>
|
|
135
|
+
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
136
|
+
<path
|
|
137
|
+
d="M2 3.5l3 3 3-3"
|
|
138
|
+
stroke="currentColor"
|
|
139
|
+
strokeWidth="1.5"
|
|
140
|
+
strokeLinecap="round"
|
|
141
|
+
fill="none"
|
|
142
|
+
/>
|
|
143
|
+
</svg>
|
|
144
|
+
</button>
|
|
145
|
+
{flyout}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function TransitionFlyout({
|
|
151
|
+
style,
|
|
152
|
+
value,
|
|
153
|
+
onChange,
|
|
154
|
+
}: {
|
|
155
|
+
style: React.CSSProperties;
|
|
156
|
+
value: TransitionFields;
|
|
157
|
+
onChange: (next: TransitionFields) => void;
|
|
158
|
+
}) {
|
|
159
|
+
const [query, setQuery] = useState('');
|
|
160
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
searchRef.current?.focus();
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const trimmed = query.trim().toLowerCase();
|
|
167
|
+
const hasQuery = trimmed.length > 0;
|
|
168
|
+
|
|
169
|
+
// Flat, filtered list when searching; grouped otherwise.
|
|
170
|
+
const filtered = useMemo(
|
|
171
|
+
() =>
|
|
172
|
+
hasQuery
|
|
173
|
+
? TRANSITION_ENTRIES.filter(
|
|
174
|
+
(e) =>
|
|
175
|
+
e.label.toLowerCase().includes(trimmed) || e.value.toLowerCase().includes(trimmed),
|
|
176
|
+
)
|
|
177
|
+
: null,
|
|
178
|
+
[hasQuery, trimmed],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const activeEntry = findTransitionEntry(value.type);
|
|
182
|
+
|
|
183
|
+
// Selecting a transition keeps the duration but drops a now-meaningless
|
|
184
|
+
// direction (only directional transitions carry one).
|
|
185
|
+
const selectType = (next: TransitionCatalogEntry | null) => {
|
|
186
|
+
if (!next) {
|
|
187
|
+
onChange({ ...EMPTY_TRANSITION });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
onChange({
|
|
191
|
+
type: next.value,
|
|
192
|
+
direction: next.direction ? value.direction : '',
|
|
193
|
+
duration: value.duration,
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const selectDirection = (dir: string) => {
|
|
198
|
+
// Toggle off when re-clicking the active direction.
|
|
199
|
+
onChange({ ...value, direction: value.direction === dir ? '' : dir });
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const setDuration = (raw: string) => {
|
|
203
|
+
onChange({ ...value, duration: raw.trim() });
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div id={FLYOUT_ID} className="squisq-transition-flyout" role="menu" style={style}>
|
|
208
|
+
<div className="squisq-transition-flyout-search">
|
|
209
|
+
<input
|
|
210
|
+
ref={searchRef}
|
|
211
|
+
type="search"
|
|
212
|
+
className="squisq-transition-flyout-search-input"
|
|
213
|
+
placeholder="Search transitions…"
|
|
214
|
+
value={query}
|
|
215
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
216
|
+
aria-label="Search transitions"
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div className="squisq-transition-flyout-list">
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
role="menuitemradio"
|
|
224
|
+
aria-checked={value.type === ''}
|
|
225
|
+
className={`squisq-transition-option${value.type === '' ? ' squisq-transition-option--selected' : ''}`}
|
|
226
|
+
onClick={() => selectType(null)}
|
|
227
|
+
>
|
|
228
|
+
None
|
|
229
|
+
</button>
|
|
230
|
+
|
|
231
|
+
{hasQuery ? (
|
|
232
|
+
filtered && filtered.length > 0 ? (
|
|
233
|
+
<div className="squisq-transition-flyout-grid">
|
|
234
|
+
{filtered.map((entry) => (
|
|
235
|
+
<TransitionOption
|
|
236
|
+
key={entry.value}
|
|
237
|
+
entry={entry}
|
|
238
|
+
selected={value.type === entry.value}
|
|
239
|
+
direction={value.type === entry.value ? value.direction : undefined}
|
|
240
|
+
onSelect={() => selectType(entry)}
|
|
241
|
+
/>
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
) : (
|
|
245
|
+
<div className="squisq-transition-flyout-empty">No transitions match “{query}”.</div>
|
|
246
|
+
)
|
|
247
|
+
) : (
|
|
248
|
+
TRANSITION_GROUPS.map((group) => (
|
|
249
|
+
<div key={group.title} className="squisq-transition-flyout-group">
|
|
250
|
+
<h4 className="squisq-transition-flyout-group-title">{group.title}</h4>
|
|
251
|
+
<div className="squisq-transition-flyout-grid">
|
|
252
|
+
{group.entries.map((entry) => (
|
|
253
|
+
<TransitionOption
|
|
254
|
+
key={entry.value}
|
|
255
|
+
entry={entry}
|
|
256
|
+
selected={value.type === entry.value}
|
|
257
|
+
direction={value.type === entry.value ? value.direction : undefined}
|
|
258
|
+
onSelect={() => selectType(entry)}
|
|
259
|
+
/>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
))
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* Direction + duration only apply once a transition is chosen. */}
|
|
268
|
+
{value.type && (
|
|
269
|
+
<div className="squisq-transition-flyout-controls">
|
|
270
|
+
{activeEntry?.direction && (
|
|
271
|
+
<div className="squisq-transition-flyout-control">
|
|
272
|
+
<span className="squisq-transition-flyout-control-label">Direction</span>
|
|
273
|
+
<div className="squisq-transition-direction-row">
|
|
274
|
+
{DIRECTION_OPTIONS[activeEntry.direction].map((opt) => (
|
|
275
|
+
<button
|
|
276
|
+
key={opt.value}
|
|
277
|
+
type="button"
|
|
278
|
+
className={`squisq-transition-direction-button${value.direction === opt.value ? ' squisq-transition-direction-button--selected' : ''}`}
|
|
279
|
+
onClick={() => selectDirection(opt.value)}
|
|
280
|
+
>
|
|
281
|
+
{opt.label}
|
|
282
|
+
</button>
|
|
283
|
+
))}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
<div className="squisq-transition-flyout-control">
|
|
288
|
+
<span className="squisq-transition-flyout-control-label">Duration</span>
|
|
289
|
+
<div className="squisq-transition-duration-row">
|
|
290
|
+
<input
|
|
291
|
+
type="number"
|
|
292
|
+
min="0"
|
|
293
|
+
step="0.1"
|
|
294
|
+
className="squisq-transition-duration-input"
|
|
295
|
+
placeholder="0.7"
|
|
296
|
+
value={value.duration}
|
|
297
|
+
onChange={(e) => setDuration(e.target.value)}
|
|
298
|
+
aria-label="Transition duration in seconds"
|
|
299
|
+
/>
|
|
300
|
+
<span className="squisq-transition-duration-unit">sec</span>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function TransitionOption({
|
|
310
|
+
entry,
|
|
311
|
+
selected,
|
|
312
|
+
direction,
|
|
313
|
+
onSelect,
|
|
314
|
+
}: {
|
|
315
|
+
entry: TransitionCatalogEntry;
|
|
316
|
+
selected: boolean;
|
|
317
|
+
/** Direction to preview (for the active entry); falls back to the default. */
|
|
318
|
+
direction?: string;
|
|
319
|
+
onSelect: () => void;
|
|
320
|
+
}) {
|
|
321
|
+
// Play the real entering transition on the label while hovered. Local
|
|
322
|
+
// state (rather than a flyout-wide hover map) keeps the rerender to the
|
|
323
|
+
// single option, and toggling the class off on leave lets the animation
|
|
324
|
+
// restart cleanly on the next hover. `getTransitionClass` is the same
|
|
325
|
+
// mapping the player uses; if a consumer hasn't loaded the transition CSS
|
|
326
|
+
// the class simply matches no rule and the label stays static.
|
|
327
|
+
const [previewing, setPreviewing] = useState(false);
|
|
328
|
+
// `entry.value` is a real `TransitionType` (guarded by transitionCatalog.test).
|
|
329
|
+
const animClass =
|
|
330
|
+
previewing && entry.value
|
|
331
|
+
? getTransitionClass(entry.value as TransitionType, true, normalizeDir(direction))
|
|
332
|
+
: '';
|
|
333
|
+
return (
|
|
334
|
+
<button
|
|
335
|
+
type="button"
|
|
336
|
+
role="menuitemradio"
|
|
337
|
+
aria-checked={selected}
|
|
338
|
+
className={`squisq-transition-option${selected ? ' squisq-transition-option--selected' : ''}`}
|
|
339
|
+
onClick={onSelect}
|
|
340
|
+
onMouseEnter={() => setPreviewing(true)}
|
|
341
|
+
onMouseLeave={() => setPreviewing(false)}
|
|
342
|
+
>
|
|
343
|
+
<span className={`squisq-transition-option-label ${animClass}`}>{entry.label}</span>
|
|
344
|
+
</button>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** `getTransitionClass` wants a `TransitionDirection | undefined`; '' → undefined. */
|
|
349
|
+
function normalizeDir(direction: string | undefined): TransitionDirection | undefined {
|
|
350
|
+
return direction ? (direction as TransitionDirection) : undefined;
|
|
351
|
+
}
|
|
@@ -16,6 +16,7 @@ import { DiffEditor } from '@monaco-editor/react';
|
|
|
16
16
|
import type { Version } from '@bendyline/squisq/versions';
|
|
17
17
|
import { useEditorContext } from './EditorContext';
|
|
18
18
|
import { useMonacoLoader } from './useMonacoLoader';
|
|
19
|
+
import { Icon } from './Icon';
|
|
19
20
|
|
|
20
21
|
interface LazyDiffEditorProps {
|
|
21
22
|
original: string;
|
|
@@ -229,20 +230,7 @@ export function VersionHistoryPanel() {
|
|
|
229
230
|
aria-expanded={open}
|
|
230
231
|
onClick={() => setOpen((v) => !v)}
|
|
231
232
|
>
|
|
232
|
-
<
|
|
233
|
-
width="16"
|
|
234
|
-
height="16"
|
|
235
|
-
viewBox="0 0 16 16"
|
|
236
|
-
fill="none"
|
|
237
|
-
stroke="currentColor"
|
|
238
|
-
strokeWidth="1.5"
|
|
239
|
-
strokeLinecap="round"
|
|
240
|
-
strokeLinejoin="round"
|
|
241
|
-
>
|
|
242
|
-
<path d="M2.5 8a5.5 5.5 0 1 0 1.6-3.9" />
|
|
243
|
-
<polyline points="2.5,2.5 2.5,4.5 4.5,4.5" />
|
|
244
|
-
<polyline points="8,4.5 8,8 11,9.5" />
|
|
245
|
-
</svg>
|
|
233
|
+
<Icon icon="fa-solid fa-clock-rotate-left" />
|
|
246
234
|
</button>
|
|
247
235
|
{open && (
|
|
248
236
|
<div
|
package/src/ViewMenuPanel.tsx
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
14
|
-
import { useEditorContext, type ThemeInheritance } from './EditorContext';
|
|
14
|
+
import { useEditorContext, type ThemeInheritance, type LayoutMode } from './EditorContext';
|
|
15
|
+
import { Icon } from './Icon';
|
|
15
16
|
|
|
16
17
|
export function ViewMenuPanel() {
|
|
17
18
|
const {
|
|
@@ -25,6 +26,8 @@ export function ViewMenuPanel() {
|
|
|
25
26
|
setBlockTagsVisible,
|
|
26
27
|
themeInheritance,
|
|
27
28
|
setThemeInheritance,
|
|
29
|
+
layoutMode,
|
|
30
|
+
setLayoutMode,
|
|
28
31
|
} = useEditorContext();
|
|
29
32
|
const [open, setOpen] = useState(false);
|
|
30
33
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -70,22 +73,22 @@ export function ViewMenuPanel() {
|
|
|
70
73
|
aria-haspopup="menu"
|
|
71
74
|
onClick={() => setOpen((v) => !v)}
|
|
72
75
|
>
|
|
73
|
-
<
|
|
74
|
-
width="16"
|
|
75
|
-
height="16"
|
|
76
|
-
viewBox="0 0 16 16"
|
|
77
|
-
fill="none"
|
|
78
|
-
stroke="currentColor"
|
|
79
|
-
strokeWidth="1.4"
|
|
80
|
-
strokeLinecap="round"
|
|
81
|
-
strokeLinejoin="round"
|
|
82
|
-
>
|
|
83
|
-
<path d="M1.5 8 C 3 4.5 5.5 3 8 3 S 13 4.5 14.5 8 C 13 11.5 10.5 13 8 13 S 3 11.5 1.5 8 Z" />
|
|
84
|
-
<circle cx="8" cy="8" r="2.2" />
|
|
85
|
-
</svg>
|
|
76
|
+
<Icon icon="fa-solid fa-eye" />
|
|
86
77
|
</button>
|
|
87
78
|
{open && (
|
|
88
79
|
<div className="squisq-view-menu-popover" role="menu" aria-label="View options">
|
|
80
|
+
<MenuRadioGroup
|
|
81
|
+
label="View"
|
|
82
|
+
name="layout-mode"
|
|
83
|
+
value={layoutMode}
|
|
84
|
+
onChange={(v) => setLayoutMode(v as LayoutMode)}
|
|
85
|
+
options={[
|
|
86
|
+
{ value: 'document', label: 'Document' },
|
|
87
|
+
{ value: 'block', label: 'Block-at-a-time' },
|
|
88
|
+
{ value: 'timeline', label: 'Timeline' },
|
|
89
|
+
]}
|
|
90
|
+
/>
|
|
91
|
+
<div className="squisq-view-menu-separator" role="separator" />
|
|
89
92
|
<MenuToggle label="Show outline" checked={outlineVisible} onChange={toggleOutline} />
|
|
90
93
|
<MenuToggle
|
|
91
94
|
label="Show block previews"
|