@lemonriver/core 0.1.0 → 0.2.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/LICENSE +21 -0
- package/dist/{build-f-fM2ZY4.js → build-Df_xwIpg.js} +1 -1
- package/dist/cli/bin.js +4 -4
- package/dist/{config-DO45GU3e.js → config-BjWCLoMQ.js} +2343 -363
- package/dist/{config-B6JaKJM4.d.ts → config-rMGCkz6i.d.ts} +6 -2
- package/dist/{dev-CqS2xpgE.js → dev-Ctsx9IeO.js} +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -3
- package/dist/locale/index.d.ts +5 -2
- package/dist/locale/index.js +2 -2
- package/dist/{preview-BihgMlEl.js → preview-CJgcqsPk.js} +1 -1
- package/dist/{types-BjW4glip.d.ts → types-8VUyA2bu.d.ts} +133 -5
- package/dist/{update-C_R0ycVC.js → update-BKOA3Z6o.js} +153 -21
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/{zh-tw-Ci0jK8eg.js → zh-tw-CeLrYwRE.js} +1178 -25
- package/package.json +21 -15
- package/skills/apply-comments/SKILL.md +10 -6
- package/skills/create-slide/SKILL.md +2 -0
- package/skills/current-slide/SKILL.md +1 -0
- package/skills/slide-authoring/SKILL.md +31 -1
- package/src/agent/prompt.ts +141 -0
- package/src/agent/provider.ts +123 -0
- package/src/agent/types.ts +151 -0
- package/src/app/app.tsx +2 -0
- package/src/app/assets/open-slide.svg +39 -0
- package/src/app/components/agent/agent-dock.tsx +35 -0
- package/src/app/components/agent/agent-markdown.tsx +45 -0
- package/src/app/components/agent/agent-panel.tsx +692 -0
- package/src/app/components/agent/agent-presence.tsx +74 -0
- package/src/app/components/agent/composer.tsx +120 -0
- package/src/app/components/agent/connect-view.tsx +184 -0
- package/src/app/components/agent/history-view.tsx +135 -0
- package/src/app/components/agent/provider-logo.tsx +34 -0
- package/src/app/components/agent/transcript.tsx +609 -0
- package/src/app/components/board/board-canvas.tsx +143 -12
- package/src/app/components/board/board.css +51 -0
- package/src/app/components/board/layout.test.ts +53 -1
- package/src/app/components/board/layout.ts +50 -2
- package/src/app/components/board/page-node.tsx +287 -13
- package/src/app/components/board/use-board-cards.ts +210 -0
- package/src/app/components/board/use-page-order.ts +17 -9
- package/src/app/components/command/slide-command-menu.tsx +10 -0
- package/src/app/components/icon-switcher-indicator.tsx +1 -1
- package/src/app/components/inspector/comment-layer.tsx +1011 -0
- package/src/app/components/inspector/image-crop-dialog.tsx +1 -5
- package/src/app/components/inspector/inspect-overlay.tsx +2 -39
- package/src/app/components/inspector/inspector-panel.tsx +10 -93
- package/src/app/components/inspector/inspector-provider.tsx +121 -47
- package/src/app/components/panel/editor-sidebar.tsx +85 -16
- package/src/app/components/panel/panel-shell.tsx +2 -2
- package/src/app/components/panel/resize-handle.tsx +129 -0
- package/src/app/components/player.tsx +0 -1
- package/src/app/components/preview-step-host.tsx +16 -0
- package/src/app/components/sidebar/sidebar.tsx +2 -2
- package/src/app/components/style-panel/style-panel.tsx +3 -2
- package/src/app/components/thumbnail-rail.tsx +76 -1
- package/src/app/favicon.ico +0 -0
- package/src/app/favicon.svg +39 -0
- package/src/app/index.html +2 -1
- package/src/app/lib/agent/use-agent.ts +543 -0
- package/src/app/lib/inspector/freeze-motion.ts +61 -0
- package/src/app/lib/inspector/use-comments.ts +32 -3
- package/src/app/lib/locale-store.ts +6 -1
- package/src/app/lib/print-ready.ts +9 -6
- package/src/app/lib/use-auto-grow.ts +15 -0
- package/src/app/lib/utils.ts +4 -0
- package/src/app/routes/board.tsx +48 -11
- package/src/app/routes/home.tsx +43 -7
- package/src/app/routes/presenter.tsx +1 -16
- package/src/app/routes/preview.tsx +169 -0
- package/src/app/routes/slide.tsx +221 -173
- package/src/app/styles.css +234 -80
- package/src/editing/placeholder-id.ts +1 -0
- package/src/locale/en.ts +127 -5
- package/src/locale/es.ts +692 -0
- package/src/locale/index.ts +1 -0
- package/src/locale/ja.ts +120 -6
- package/src/locale/types.ts +132 -5
- package/src/locale/zh-cn.ts +117 -5
- package/src/locale/zh-tw.ts +117 -5
- package/src/app/assets/open-slide.png +0 -0
- package/src/app/components/inspector/comment-widget.tsx +0 -119
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type SyntheticEvent, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import ReactCrop, { type Crop, type PercentCrop } from 'react-image-crop';
|
|
3
|
-
import { round2 } from '@/lib/utils';
|
|
3
|
+
import { clamp, round2 } from '@/lib/utils';
|
|
4
4
|
import 'react-image-crop/dist/ReactCrop.css';
|
|
5
5
|
import { Button } from '@/components/ui/button';
|
|
6
6
|
import {
|
|
@@ -209,7 +209,3 @@ function roundRect(crop: PercentCrop): ImageCropRect {
|
|
|
209
209
|
height: round2(crop.height),
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
|
-
|
|
213
|
-
function clamp(v: number, lo: number, hi: number) {
|
|
214
|
-
return v < lo ? lo : v > hi ? hi : v;
|
|
215
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Crop, ImageIcon } from 'lucide-react';
|
|
2
2
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { findSlideSource } from '@/lib/inspector/fiber';
|
|
4
|
+
import { useFreezeSlideMotion } from '@/lib/inspector/freeze-motion';
|
|
4
5
|
import {
|
|
5
6
|
type Guide,
|
|
6
7
|
type Point,
|
|
@@ -98,19 +99,7 @@ export function InspectOverlay() {
|
|
|
98
99
|
const [scale, setScale] = useState(1);
|
|
99
100
|
const displayed = localTargets ?? selection;
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
if (!active) return;
|
|
103
|
-
const root = document.querySelector<HTMLElement>('[data-inspector-root]');
|
|
104
|
-
if (!root) return;
|
|
105
|
-
const style = document.createElement('style');
|
|
106
|
-
style.textContent = EDITING_FREEZE_CSS;
|
|
107
|
-
document.head.appendChild(style);
|
|
108
|
-
root.dataset.inspectorEditing = 'true';
|
|
109
|
-
return () => {
|
|
110
|
-
style.remove();
|
|
111
|
-
delete root.dataset.inspectorEditing;
|
|
112
|
-
};
|
|
113
|
-
}, [active]);
|
|
102
|
+
useFreezeSlideMotion(active ? 'edit' : null);
|
|
114
103
|
|
|
115
104
|
useLayoutEffect(() => {
|
|
116
105
|
if (!active) return;
|
|
@@ -656,29 +645,3 @@ export function InspectOverlay() {
|
|
|
656
645
|
</div>
|
|
657
646
|
);
|
|
658
647
|
}
|
|
659
|
-
|
|
660
|
-
const EDITING_FREEZE_CSS = `
|
|
661
|
-
[data-inspector-editing] { touch-action: none; }
|
|
662
|
-
[data-inspector-editing] *:not([data-inspector-ui], [data-inspector-ui] *),
|
|
663
|
-
[data-inspector-editing] *:not([data-inspector-ui], [data-inspector-ui] *)::before,
|
|
664
|
-
[data-inspector-editing] *:not([data-inspector-ui], [data-inspector-ui] *)::after {
|
|
665
|
-
animation-duration: 1ms !important;
|
|
666
|
-
animation-delay: 0s !important;
|
|
667
|
-
animation-iteration-count: 1 !important;
|
|
668
|
-
animation-fill-mode: forwards !important;
|
|
669
|
-
transition: none !important;
|
|
670
|
-
view-transition-name: none !important;
|
|
671
|
-
user-select: none;
|
|
672
|
-
}
|
|
673
|
-
[data-inspector-editing] *:not([data-inspector-ui], [data-inspector-ui] *, [data-visual-gesture], [data-visual-gesture] *),
|
|
674
|
-
[data-inspector-editing] *:not([data-inspector-ui], [data-inspector-ui] *, [data-visual-gesture], [data-visual-gesture] *)::before,
|
|
675
|
-
[data-inspector-editing] *:not([data-inspector-ui], [data-inspector-ui] *, [data-visual-gesture], [data-visual-gesture] *)::after {
|
|
676
|
-
cursor: default !important;
|
|
677
|
-
}
|
|
678
|
-
[data-inspector-editing] [data-visual-gesture],
|
|
679
|
-
[data-inspector-editing] [data-visual-gesture] *,
|
|
680
|
-
[data-inspector-editing] [data-visual-gesture] *::before,
|
|
681
|
-
[data-inspector-editing] [data-visual-gesture] *::after {
|
|
682
|
-
cursor: var(--osd-gesture-cursor, default) !important;
|
|
683
|
-
}
|
|
684
|
-
`;
|
|
@@ -46,7 +46,7 @@ import { styleContext } from '@/lib/inspector/text-selection';
|
|
|
46
46
|
import type { EditOp } from '@/lib/inspector/use-editor';
|
|
47
47
|
import { useAgentSocketConnected } from '@/lib/use-agent-socket';
|
|
48
48
|
import { format, useLocale } from '@/lib/use-locale';
|
|
49
|
-
import {
|
|
49
|
+
import { round2 } from '@/lib/utils';
|
|
50
50
|
import type { Locale } from '../../../locale/types';
|
|
51
51
|
import { ArrangePanel } from './arrange-panel';
|
|
52
52
|
import { AssetPickerDialog } from './asset-picker-dialog';
|
|
@@ -107,7 +107,6 @@ export function InspectorPanel({
|
|
|
107
107
|
bufferOps,
|
|
108
108
|
pendingCount,
|
|
109
109
|
opsVersion,
|
|
110
|
-
add,
|
|
111
110
|
applyEdit,
|
|
112
111
|
} = useInspector();
|
|
113
112
|
const [snapshot, setSnapshot] = useState<ElementSnapshot | null>(null);
|
|
@@ -256,19 +255,14 @@ export function InspectorPanel({
|
|
|
256
255
|
);
|
|
257
256
|
const footer =
|
|
258
257
|
selected && snapshot && !multiple ? (
|
|
259
|
-
|
|
260
|
-
<
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
</span>
|
|
268
|
-
<AgentWatchingBadge />
|
|
269
|
-
</div>
|
|
270
|
-
</CollapsibleSection>
|
|
271
|
-
</>
|
|
258
|
+
<CollapsibleSection title={t.inspector.sourceSection}>
|
|
259
|
+
<div className="flex items-center justify-between gap-2">
|
|
260
|
+
<span className="font-mono text-[10.5px] text-muted-foreground">
|
|
261
|
+
<{selected.anchor.tagName.toLowerCase()}> · {selected.line}:{selected.column}
|
|
262
|
+
</span>
|
|
263
|
+
<AgentWatchingBadge />
|
|
264
|
+
</div>
|
|
265
|
+
</CollapsibleSection>
|
|
272
266
|
) : undefined;
|
|
273
267
|
|
|
274
268
|
return (
|
|
@@ -278,7 +272,7 @@ export function InspectorPanel({
|
|
|
278
272
|
if (value === 'arrange') stopInlineEdit();
|
|
279
273
|
if (value === 'format' || value === 'arrange') onTabChange(value);
|
|
280
274
|
}}
|
|
281
|
-
className="h-full shrink-0 gap-0"
|
|
275
|
+
className="h-full w-full min-w-0 shrink-0 gap-0"
|
|
282
276
|
onPointerDownCapture={(event) => {
|
|
283
277
|
if (!inlineEdit || !(event.target instanceof Element)) return;
|
|
284
278
|
const button = event.target.closest('button');
|
|
@@ -894,83 +888,6 @@ function AgentWatchingBadge() {
|
|
|
894
888
|
);
|
|
895
889
|
}
|
|
896
890
|
|
|
897
|
-
// The cue animation re-mounts with every element selection; without this
|
|
898
|
-
// guard it replays each time instead of once per inspector session.
|
|
899
|
-
let commentCuePlayed = false;
|
|
900
|
-
|
|
901
|
-
function CommentsSection({
|
|
902
|
-
selected,
|
|
903
|
-
onAdd,
|
|
904
|
-
}: {
|
|
905
|
-
selected: { line: number; column: number };
|
|
906
|
-
onAdd: (line: number, column: number, text: string) => Promise<void>;
|
|
907
|
-
}) {
|
|
908
|
-
const [draft, setDraft] = useState('');
|
|
909
|
-
const [submitting, setSubmitting] = useState(false);
|
|
910
|
-
const [showCue] = useState(() => !commentCuePlayed);
|
|
911
|
-
const wrapRef = useRef<HTMLDivElement>(null);
|
|
912
|
-
const t = useLocale();
|
|
913
|
-
|
|
914
|
-
useEffect(() => {
|
|
915
|
-
commentCuePlayed = true;
|
|
916
|
-
}, []);
|
|
917
|
-
|
|
918
|
-
useEffect(() => {
|
|
919
|
-
const onKey = (e: KeyboardEvent) => {
|
|
920
|
-
if (e.key !== '/') return;
|
|
921
|
-
if (!(e.metaKey || e.ctrlKey)) return;
|
|
922
|
-
if (e.altKey || e.shiftKey) return;
|
|
923
|
-
const ta = wrapRef.current?.querySelector('textarea');
|
|
924
|
-
if (!ta) return;
|
|
925
|
-
e.preventDefault();
|
|
926
|
-
const details = ta.closest('details');
|
|
927
|
-
if (details) details.open = true;
|
|
928
|
-
ta.focus({ preventScroll: true });
|
|
929
|
-
};
|
|
930
|
-
window.addEventListener('keydown', onKey);
|
|
931
|
-
return () => window.removeEventListener('keydown', onKey);
|
|
932
|
-
}, []);
|
|
933
|
-
|
|
934
|
-
const submit = async () => {
|
|
935
|
-
const trimmed = draft.trim();
|
|
936
|
-
if (!trimmed) return;
|
|
937
|
-
setSubmitting(true);
|
|
938
|
-
try {
|
|
939
|
-
await onAdd(selected.line, selected.column, trimmed);
|
|
940
|
-
setDraft('');
|
|
941
|
-
} finally {
|
|
942
|
-
setSubmitting(false);
|
|
943
|
-
}
|
|
944
|
-
};
|
|
945
|
-
|
|
946
|
-
return (
|
|
947
|
-
<>
|
|
948
|
-
<div ref={wrapRef} className={cn('rounded-[6px]', showCue && 'comment-cue')}>
|
|
949
|
-
<Textarea
|
|
950
|
-
value={draft}
|
|
951
|
-
onChange={(e) => setDraft(e.target.value)}
|
|
952
|
-
onKeyDown={(e) => {
|
|
953
|
-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
954
|
-
e.preventDefault();
|
|
955
|
-
submit();
|
|
956
|
-
}
|
|
957
|
-
}}
|
|
958
|
-
placeholder={t.inspector.commentPlaceholder}
|
|
959
|
-
className="min-h-16 resize-none text-[12px]"
|
|
960
|
-
/>
|
|
961
|
-
</div>
|
|
962
|
-
<div className="flex items-center justify-between gap-2">
|
|
963
|
-
<span className="font-mono text-[10.5px] text-muted-foreground/70">
|
|
964
|
-
{t.inspector.commentShortcutHint}
|
|
965
|
-
</span>
|
|
966
|
-
<Button size="sm" variant="brand" disabled={submitting || !draft.trim()} onClick={submit}>
|
|
967
|
-
{t.inspector.addComment}
|
|
968
|
-
</Button>
|
|
969
|
-
</div>
|
|
970
|
-
</>
|
|
971
|
-
);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
891
|
function readSnapshot(el: HTMLElement): ElementSnapshot {
|
|
975
892
|
const cs = getComputedStyle(el);
|
|
976
893
|
const text =
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Eye, PanelRight, Pencil } from 'lucide-react';
|
|
1
|
+
import { Eye, MessageSquare, PanelRight, Pencil } from 'lucide-react';
|
|
2
2
|
import {
|
|
3
3
|
createContext,
|
|
4
4
|
type ReactNode,
|
|
@@ -12,20 +12,22 @@ import {
|
|
|
12
12
|
import { toast } from 'sonner';
|
|
13
13
|
import { useHistory } from '@/components/history-provider';
|
|
14
14
|
import { IconSwitcherIndicator } from '@/components/icon-switcher-indicator';
|
|
15
|
+
import { IconTooltip } from '@/components/icon-tooltip';
|
|
15
16
|
import { Button } from '@/components/ui/button';
|
|
16
17
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
18
|
+
import { TooltipTrigger } from '@/components/ui/tooltip';
|
|
17
19
|
import { findSlideSource } from '@/lib/inspector/fiber';
|
|
18
20
|
import {
|
|
19
21
|
appendTextEdit,
|
|
20
22
|
type TextEditOp,
|
|
21
23
|
type TextEditStep,
|
|
22
24
|
} from '@/lib/inspector/text-edit-timeline';
|
|
23
|
-
import { type SlideComment, useComments } from '@/lib/inspector/use-comments';
|
|
25
|
+
import { type AddCommentInput, type SlideComment, useComments } from '@/lib/inspector/use-comments';
|
|
24
26
|
import { type Edit, type EditOp, useEditor } from '@/lib/inspector/use-editor';
|
|
25
27
|
import { useVisualEditor, type VisualEdit } from '@/lib/inspector/use-visual-editor';
|
|
26
28
|
import { isShortcutControlTarget, isTypingTarget } from '@/lib/keys';
|
|
27
29
|
import { textDiff } from '@/lib/text-diff';
|
|
28
|
-
import { useLocale } from '@/lib/use-locale';
|
|
30
|
+
import { format, plural, useLocale } from '@/lib/use-locale';
|
|
29
31
|
import { round2 } from '@/lib/utils';
|
|
30
32
|
import { AssetPickerDialog } from './asset-picker-dialog';
|
|
31
33
|
import { ImageCropDialog, type ImageCropRect } from './image-crop-dialog';
|
|
@@ -367,9 +369,17 @@ function replayDomTextEdits(el: HTMLElement, snapshot: TextDomSnapshot, steps: T
|
|
|
367
369
|
if (el.innerHTML !== next) el.innerHTML = next;
|
|
368
370
|
}
|
|
369
371
|
|
|
372
|
+
export type InspectorMode = 'preview' | 'edit' | 'comment';
|
|
373
|
+
|
|
370
374
|
type InspectorCtx = {
|
|
371
375
|
slideId: string;
|
|
376
|
+
pageIndex: number;
|
|
377
|
+
mode: InspectorMode;
|
|
378
|
+
setMode: (mode: InspectorMode) => void;
|
|
379
|
+
// True while the visual editor owns the canvas (`mode === 'edit'`).
|
|
372
380
|
active: boolean;
|
|
381
|
+
commentMode: boolean;
|
|
382
|
+
exitCommentMode: () => void;
|
|
373
383
|
toggle: () => void;
|
|
374
384
|
panelOpen: boolean;
|
|
375
385
|
panelHidden: boolean;
|
|
@@ -377,7 +387,8 @@ type InspectorCtx = {
|
|
|
377
387
|
cancel: () => void;
|
|
378
388
|
comments: SlideComment[];
|
|
379
389
|
error: string | null;
|
|
380
|
-
add: (
|
|
390
|
+
add: (input: AddCommentInput) => Promise<void>;
|
|
391
|
+
update: (id: string, text: string) => Promise<void>;
|
|
381
392
|
remove: (id: string) => Promise<void>;
|
|
382
393
|
selection: SelectedTarget[];
|
|
383
394
|
setSelection: (targets: SelectedTarget[]) => void;
|
|
@@ -416,6 +427,10 @@ export function useInspector(): InspectorCtx {
|
|
|
416
427
|
return v;
|
|
417
428
|
}
|
|
418
429
|
|
|
430
|
+
export function useOptionalInspector(): InspectorCtx | null {
|
|
431
|
+
return useContext(Ctx);
|
|
432
|
+
}
|
|
433
|
+
|
|
419
434
|
export function InspectorProvider({
|
|
420
435
|
slideId,
|
|
421
436
|
pageIndex,
|
|
@@ -429,7 +444,11 @@ export function InspectorProvider({
|
|
|
429
444
|
onPanelOpen?: () => void;
|
|
430
445
|
children: ReactNode;
|
|
431
446
|
}) {
|
|
432
|
-
const [
|
|
447
|
+
const [mode, setModeState] = useState<InspectorMode>(import.meta.env.DEV ? 'edit' : 'preview');
|
|
448
|
+
const active = mode === 'edit';
|
|
449
|
+
const commentMode = mode === 'comment';
|
|
450
|
+
// Where Esc / toggling the comment tool off returns the user to.
|
|
451
|
+
const modeBeforeCommentRef = useRef<Exclude<InspectorMode, 'comment'>>('preview');
|
|
433
452
|
const [panelOpen, setPanelOpen] = useState(
|
|
434
453
|
() => typeof window !== 'undefined' && window.innerWidth >= 1024,
|
|
435
454
|
);
|
|
@@ -475,7 +494,7 @@ export function InspectorProvider({
|
|
|
475
494
|
[],
|
|
476
495
|
);
|
|
477
496
|
const [opsVersion, setOpsVersion] = useState(0);
|
|
478
|
-
const { comments, error, add, remove } = useComments(slideId);
|
|
497
|
+
const { comments, error, add, update, remove } = useComments(slideId);
|
|
479
498
|
const { applyEdit, applyEdits } = useEditor(slideId);
|
|
480
499
|
const history = useHistory();
|
|
481
500
|
|
|
@@ -1252,19 +1271,32 @@ export function InspectorProvider({
|
|
|
1252
1271
|
return () => observer.disconnect();
|
|
1253
1272
|
}, [selection, setSelection, slideId]);
|
|
1254
1273
|
|
|
1274
|
+
const setMode = useCallback(
|
|
1275
|
+
(next: InspectorMode) => {
|
|
1276
|
+
if (next === mode) return;
|
|
1277
|
+
if (next === 'comment' && mode !== 'comment') modeBeforeCommentRef.current = mode;
|
|
1278
|
+
if (mode === 'edit') {
|
|
1279
|
+
setSelected(null);
|
|
1280
|
+
setInlineEdit(null);
|
|
1281
|
+
}
|
|
1282
|
+
setModeState(next);
|
|
1283
|
+
},
|
|
1284
|
+
[mode, setSelected],
|
|
1285
|
+
);
|
|
1286
|
+
|
|
1255
1287
|
const toggle = useCallback(() => {
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
}, [
|
|
1288
|
+
setMode(active ? 'preview' : 'edit');
|
|
1289
|
+
}, [active, setMode]);
|
|
1290
|
+
|
|
1291
|
+
const exitCommentMode = useCallback(() => {
|
|
1292
|
+
if (commentMode) setMode(modeBeforeCommentRef.current);
|
|
1293
|
+
}, [commentMode, setMode]);
|
|
1262
1294
|
|
|
1263
1295
|
const togglePanel = useCallback(() => {
|
|
1264
1296
|
setPanelOpen(!active || panelHidden || !panelOpen);
|
|
1265
|
-
|
|
1297
|
+
setMode('edit');
|
|
1266
1298
|
onPanelOpen?.();
|
|
1267
|
-
}, [active, panelHidden, panelOpen, onPanelOpen]);
|
|
1299
|
+
}, [active, panelHidden, panelOpen, onPanelOpen, setMode]);
|
|
1268
1300
|
|
|
1269
1301
|
const cancel = useCallback(() => {
|
|
1270
1302
|
setSelected(null);
|
|
@@ -1320,13 +1352,24 @@ export function InspectorProvider({
|
|
|
1320
1352
|
e.altKey
|
|
1321
1353
|
)
|
|
1322
1354
|
return;
|
|
1323
|
-
if (e.key
|
|
1324
|
-
|
|
1325
|
-
|
|
1355
|
+
if (e.key === 'i' || e.key === 'I') {
|
|
1356
|
+
e.preventDefault();
|
|
1357
|
+
togglePanel();
|
|
1358
|
+
} else if (e.key === 'c' || e.key === 'C') {
|
|
1359
|
+
e.preventDefault();
|
|
1360
|
+
if (commentMode) exitCommentMode();
|
|
1361
|
+
else setMode('comment');
|
|
1362
|
+
} else if (e.key === 'v' || e.key === 'V') {
|
|
1363
|
+
e.preventDefault();
|
|
1364
|
+
setMode('preview');
|
|
1365
|
+
} else if (e.key === 'e' || e.key === 'E') {
|
|
1366
|
+
e.preventDefault();
|
|
1367
|
+
setMode('edit');
|
|
1368
|
+
}
|
|
1326
1369
|
};
|
|
1327
1370
|
window.addEventListener('keydown', onKey);
|
|
1328
1371
|
return () => window.removeEventListener('keydown', onKey);
|
|
1329
|
-
}, [togglePanel]);
|
|
1372
|
+
}, [togglePanel, commentMode, exitCommentMode, setMode]);
|
|
1330
1373
|
|
|
1331
1374
|
const openCrop = useCallback((anchor: HTMLImageElement) => {
|
|
1332
1375
|
const loc = anchor.dataset.slideLoc;
|
|
@@ -1352,7 +1395,12 @@ export function InspectorProvider({
|
|
|
1352
1395
|
const value = useMemo<InspectorCtx>(
|
|
1353
1396
|
() => ({
|
|
1354
1397
|
slideId,
|
|
1398
|
+
pageIndex,
|
|
1399
|
+
mode,
|
|
1400
|
+
setMode,
|
|
1355
1401
|
active,
|
|
1402
|
+
commentMode,
|
|
1403
|
+
exitCommentMode,
|
|
1356
1404
|
toggle,
|
|
1357
1405
|
panelOpen,
|
|
1358
1406
|
panelHidden,
|
|
@@ -1361,6 +1409,7 @@ export function InspectorProvider({
|
|
|
1361
1409
|
comments,
|
|
1362
1410
|
error,
|
|
1363
1411
|
add,
|
|
1412
|
+
update,
|
|
1364
1413
|
remove,
|
|
1365
1414
|
selection,
|
|
1366
1415
|
setSelection,
|
|
@@ -1387,7 +1436,12 @@ export function InspectorProvider({
|
|
|
1387
1436
|
}),
|
|
1388
1437
|
[
|
|
1389
1438
|
slideId,
|
|
1439
|
+
pageIndex,
|
|
1440
|
+
mode,
|
|
1441
|
+
setMode,
|
|
1390
1442
|
active,
|
|
1443
|
+
commentMode,
|
|
1444
|
+
exitCommentMode,
|
|
1391
1445
|
toggle,
|
|
1392
1446
|
panelOpen,
|
|
1393
1447
|
panelHidden,
|
|
@@ -1396,6 +1450,7 @@ export function InspectorProvider({
|
|
|
1396
1450
|
comments,
|
|
1397
1451
|
error,
|
|
1398
1452
|
add,
|
|
1453
|
+
update,
|
|
1399
1454
|
remove,
|
|
1400
1455
|
selection,
|
|
1401
1456
|
setSelection,
|
|
@@ -1552,44 +1607,63 @@ function parsePercent(s: string, fallback: number): number {
|
|
|
1552
1607
|
|
|
1553
1608
|
export function InspectModeSwitcher() {
|
|
1554
1609
|
const t = useLocale();
|
|
1555
|
-
const {
|
|
1610
|
+
const { mode, setMode, comments } = useInspector();
|
|
1556
1611
|
if (import.meta.env.PROD) return null;
|
|
1612
|
+
const items: { value: InspectorMode; label: string; shortcut: string; icon: ReactNode }[] = [
|
|
1613
|
+
{ value: 'preview', label: t.inspector.previewMode, shortcut: 'V', icon: <Eye /> },
|
|
1614
|
+
{ value: 'edit', label: t.inspector.editMode, shortcut: 'E', icon: <Pencil /> },
|
|
1615
|
+
{ value: 'comment', label: t.inspector.commentMode, shortcut: 'C', icon: <MessageSquare /> },
|
|
1616
|
+
];
|
|
1617
|
+
const index = Math.max(
|
|
1618
|
+
0,
|
|
1619
|
+
items.findIndex((item) => item.value === mode),
|
|
1620
|
+
);
|
|
1621
|
+
const pending = comments.length;
|
|
1622
|
+
const accessibleName = (item: (typeof items)[number]) =>
|
|
1623
|
+
item.value === 'comment' && pending > 0
|
|
1624
|
+
? `${item.label} · ${format(plural(pending, t.inspector.commentsCount), { count: pending })}`
|
|
1625
|
+
: item.label;
|
|
1557
1626
|
return (
|
|
1558
1627
|
<ToggleGroup
|
|
1559
1628
|
size="sm"
|
|
1560
1629
|
spacing={1}
|
|
1561
|
-
value={[
|
|
1630
|
+
value={[mode]}
|
|
1562
1631
|
onValueChange={(value) => {
|
|
1563
|
-
const next = value[0];
|
|
1564
|
-
if (next &&
|
|
1632
|
+
const next = value[0] as InspectorMode | undefined;
|
|
1633
|
+
if (next && next !== mode) setMode(next);
|
|
1565
1634
|
}}
|
|
1566
|
-
aria-label={`${t.inspector.previewMode} / ${t.inspector.editMode}`}
|
|
1635
|
+
aria-label={`${t.inspector.previewMode} / ${t.inspector.editMode} / ${t.inspector.commentMode}`}
|
|
1567
1636
|
data-inspector-ui
|
|
1568
1637
|
className="relative isolate h-8 gap-0 rounded-lg border border-border/70 bg-muted/70 p-0.5"
|
|
1569
1638
|
>
|
|
1570
|
-
<IconSwitcherIndicator index={
|
|
1571
|
-
|
|
1572
|
-
value=
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1639
|
+
<IconSwitcherIndicator index={index} />
|
|
1640
|
+
{items.map((item) => (
|
|
1641
|
+
<IconTooltip key={item.value} label={item.label} shortcut={item.shortcut}>
|
|
1642
|
+
<TooltipTrigger
|
|
1643
|
+
render={
|
|
1644
|
+
<ToggleGroupItem
|
|
1645
|
+
value={item.value}
|
|
1646
|
+
aria-label={accessibleName(item)}
|
|
1647
|
+
data-inspector-mode={item.value}
|
|
1648
|
+
onClick={(event) => {
|
|
1649
|
+
if (event.detail > 0) event.currentTarget.blur();
|
|
1650
|
+
}}
|
|
1651
|
+
className="relative z-10 h-full w-8 rounded-md px-0 text-muted-foreground hover:bg-transparent data-pressed:bg-transparent data-pressed:text-foreground data-pressed:shadow-none"
|
|
1652
|
+
/>
|
|
1653
|
+
}
|
|
1654
|
+
>
|
|
1655
|
+
{item.icon}
|
|
1656
|
+
{item.value === 'comment' && pending > 0 && (
|
|
1657
|
+
<span
|
|
1658
|
+
aria-hidden
|
|
1659
|
+
className="pointer-events-none absolute -top-1.5 -right-1.5 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-brand px-1 font-mono text-[9px] font-semibold leading-none text-brand-foreground nums"
|
|
1660
|
+
>
|
|
1661
|
+
{pending > 99 ? '99+' : pending}
|
|
1662
|
+
</span>
|
|
1663
|
+
)}
|
|
1664
|
+
</TooltipTrigger>
|
|
1665
|
+
</IconTooltip>
|
|
1666
|
+
))}
|
|
1593
1667
|
</ToggleGroup>
|
|
1594
1668
|
);
|
|
1595
1669
|
}
|
|
@@ -1610,7 +1684,7 @@ export function InspectPanelButton() {
|
|
|
1610
1684
|
data-inspector-ui
|
|
1611
1685
|
>
|
|
1612
1686
|
<PanelRight className="size-3.5" />
|
|
1613
|
-
<span className="hidden
|
|
1687
|
+
<span className="hidden lg:inline">{t.inspector.format}</span>
|
|
1614
1688
|
</Button>
|
|
1615
1689
|
);
|
|
1616
1690
|
}
|
|
@@ -1,49 +1,118 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { type ReactNode, useState } from 'react';
|
|
2
|
+
import { AGENT_DOCK_W } from '@/components/agent/agent-dock';
|
|
2
3
|
import { InspectorPanel } from '@/components/inspector/inspector-panel';
|
|
3
4
|
import { useInspector } from '@/components/inspector/inspector-provider';
|
|
4
5
|
import { useDesignPanelState } from '@/components/style-panel/design-provider';
|
|
5
6
|
import { DesignPanel } from '@/components/style-panel/style-panel';
|
|
7
|
+
import { useLocale } from '@/lib/use-locale';
|
|
6
8
|
import { PANEL_TRANSITION_MS, PANEL_W, usePanelMount } from './panel-shell';
|
|
9
|
+
import { ResizeHandle, useStoredWidth } from './resize-handle';
|
|
7
10
|
|
|
11
|
+
type Mode = 'format' | 'design' | 'agent';
|
|
12
|
+
|
|
13
|
+
const AGENT_MIN_W = 300;
|
|
14
|
+
const EDITOR_MAX_W = 480;
|
|
15
|
+
const AGENT_MAX_W = 720;
|
|
16
|
+
|
|
17
|
+
// The agent shares this container with Format and Design so switching between
|
|
18
|
+
// them only retargets one width transition instead of collapsing one column
|
|
19
|
+
// while another grows.
|
|
8
20
|
export function EditorSidebar({
|
|
9
21
|
designOpen,
|
|
10
22
|
onCloseDesign,
|
|
23
|
+
agentOpen = false,
|
|
24
|
+
agent,
|
|
11
25
|
}: {
|
|
12
26
|
designOpen: boolean;
|
|
13
27
|
onCloseDesign: () => void;
|
|
28
|
+
agentOpen?: boolean;
|
|
29
|
+
agent?: ReactNode;
|
|
14
30
|
}) {
|
|
31
|
+
const t = useLocale();
|
|
15
32
|
const { active, panelOpen } = useInspector();
|
|
33
|
+
const [editorWidth, setEditorWidth] = useStoredWidth(
|
|
34
|
+
'open-slide:editor-panel-width',
|
|
35
|
+
PANEL_W,
|
|
36
|
+
PANEL_W,
|
|
37
|
+
EDITOR_MAX_W,
|
|
38
|
+
);
|
|
39
|
+
const [agentWidth, setAgentWidth] = useStoredWidth(
|
|
40
|
+
'open-slide:agent-panel-width',
|
|
41
|
+
AGENT_DOCK_W,
|
|
42
|
+
AGENT_MIN_W,
|
|
43
|
+
AGENT_MAX_W,
|
|
44
|
+
);
|
|
45
|
+
const [resizing, setResizing] = useState(false);
|
|
16
46
|
const { loaded, draft } = useDesignPanelState();
|
|
17
47
|
const [preferredTab, setPreferredTab] = useState<'format' | 'arrange'>('format');
|
|
18
|
-
const mode =
|
|
19
|
-
|
|
20
|
-
? '
|
|
21
|
-
:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
const mode: Mode | null =
|
|
49
|
+
agentOpen && agent
|
|
50
|
+
? 'agent'
|
|
51
|
+
: designOpen
|
|
52
|
+
? loaded && draft
|
|
53
|
+
? 'design'
|
|
54
|
+
: null
|
|
55
|
+
: active && panelOpen
|
|
56
|
+
? 'format'
|
|
57
|
+
: null;
|
|
58
|
+
const [lastMode, setLastMode] = useState<Mode>('format');
|
|
26
59
|
if (mode && mode !== lastMode) setLastMode(mode);
|
|
60
|
+
const shown = mode ?? lastMode;
|
|
61
|
+
const isAgent = shown === 'agent';
|
|
62
|
+
const [lastEditor, setLastEditor] = useState<'format' | 'design'>('format');
|
|
63
|
+
if (!isAgent && shown !== lastEditor) setLastEditor(shown);
|
|
64
|
+
const [agentMounted, setAgentMounted] = useState(false);
|
|
65
|
+
if (isAgent && !agentMounted) setAgentMounted(true);
|
|
27
66
|
const open = mode !== null;
|
|
28
67
|
const { mounted, animVisible } = usePanelMount(open);
|
|
29
68
|
|
|
30
69
|
if (!mounted) return null;
|
|
31
70
|
|
|
71
|
+
const width = isAgent ? agentWidth : editorWidth;
|
|
72
|
+
|
|
32
73
|
return (
|
|
33
74
|
<div
|
|
34
75
|
data-editor-sidebar
|
|
76
|
+
data-agent-dock={isAgent ? '' : undefined}
|
|
35
77
|
inert={!open}
|
|
36
78
|
aria-hidden={!open}
|
|
37
|
-
className="flex h-full shrink-0 justify-end overflow-hidden bg-sidebar transition-[width] ease-swift motion-reduce:transition-none"
|
|
79
|
+
className="relative flex h-full max-h-1/2 shrink-0 justify-end overflow-hidden bg-sidebar transition-[width] ease-swift motion-reduce:transition-none md:max-h-none"
|
|
38
80
|
style={{
|
|
39
|
-
width: animVisible ?
|
|
40
|
-
transitionDuration: `${PANEL_TRANSITION_MS}ms`,
|
|
81
|
+
width: animVisible ? width : 0,
|
|
82
|
+
transitionDuration: resizing ? '0ms' : `${PANEL_TRANSITION_MS}ms`,
|
|
41
83
|
}}
|
|
42
84
|
>
|
|
43
|
-
{
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
85
|
+
{open && (
|
|
86
|
+
<ResizeHandle
|
|
87
|
+
key={isAgent ? 'agent' : 'editor'}
|
|
88
|
+
width={width}
|
|
89
|
+
min={isAgent ? AGENT_MIN_W : PANEL_W}
|
|
90
|
+
max={isAgent ? AGENT_MAX_W : EDITOR_MAX_W}
|
|
91
|
+
defaultWidth={isAgent ? AGENT_DOCK_W : PANEL_W}
|
|
92
|
+
edge="left"
|
|
93
|
+
label={t.slide.resizeSidePanel}
|
|
94
|
+
onResize={isAgent ? setAgentWidth : setEditorWidth}
|
|
95
|
+
onResizingChange={setResizing}
|
|
96
|
+
className="left-0 hidden md:block"
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
{agentMounted && (
|
|
100
|
+
<div
|
|
101
|
+
className="sidebar-swap h-full shrink-0"
|
|
102
|
+
style={{ width: agentWidth }}
|
|
103
|
+
hidden={!isAgent}
|
|
104
|
+
>
|
|
105
|
+
{agent}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
{!isAgent && (
|
|
109
|
+
<div className="sidebar-swap flex h-full shrink-0" style={{ width: editorWidth }}>
|
|
110
|
+
{lastEditor === 'design' ? (
|
|
111
|
+
<DesignPanel onClose={onCloseDesign} />
|
|
112
|
+
) : (
|
|
113
|
+
<InspectorPanel preferredTab={preferredTab} onTabChange={setPreferredTab} />
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
47
116
|
)}
|
|
48
117
|
</div>
|
|
49
118
|
);
|
|
@@ -45,8 +45,8 @@ type PanelShellProps = {
|
|
|
45
45
|
export function PanelShell({ uiAttr, header, banner, footer, children }: PanelShellProps) {
|
|
46
46
|
const dataAttrs = uiAttr === 'inspector' ? { 'data-inspector-ui': '' } : { 'data-design-ui': '' };
|
|
47
47
|
return (
|
|
48
|
-
<aside {...dataAttrs} className="flex h-full
|
|
49
|
-
<div
|
|
48
|
+
<aside {...dataAttrs} className="flex h-full w-full min-w-0 bg-sidebar">
|
|
49
|
+
<div className="flex h-full w-full min-w-0 flex-col">
|
|
50
50
|
<header className="flex h-9 shrink-0 items-center justify-between gap-2 border-b border-hairline px-3">
|
|
51
51
|
{header}
|
|
52
52
|
</header>
|