@open-slide/core 1.1.0 → 1.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/dist/{build-DSqSio-T.js → build-6BeQ3cxb.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-KdiYeWtK.js → config-AxZ5OE1u.js} +673 -211
- package/dist/{config-C7vMYzFD.d.ts → config-CtT8K4VF.d.ts} +1 -1
- package/dist/{dev-B_GVbr11.js → dev-C9eLmUEq.js} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +96 -20
- package/dist/{preview-D_mxhj7w.js → preview-Cunm-f4i.js} +1 -1
- package/dist/{types-DYgVpIGo.d.ts → types-CRHIeoNq.d.ts} +28 -4
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +48 -1
- package/src/app/components/inspector/image-crop-dialog.tsx +64 -20
- package/src/app/components/inspector/inspector-panel.tsx +44 -13
- package/src/app/components/inspector/inspector-provider.tsx +60 -7
- package/src/app/components/notes-drawer.tsx +117 -0
- package/src/app/components/player.tsx +11 -7
- package/src/app/components/present/overview-grid.tsx +2 -2
- package/src/app/components/thumbnail-rail.tsx +119 -24
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/routes/home.tsx +34 -12
- package/src/app/routes/slide.tsx +209 -74
- package/src/locale/en.ts +26 -4
- package/src/locale/ja.ts +26 -4
- package/src/locale/types.ts +29 -4
- package/src/locale/zh-cn.ts +26 -4
- package/src/locale/zh-tw.ts +26 -4
|
@@ -13,11 +13,9 @@ import {
|
|
|
13
13
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
14
14
|
import { useLocale } from '@/lib/use-locale';
|
|
15
15
|
|
|
16
|
-
export type
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
y: number;
|
|
20
|
-
};
|
|
16
|
+
export type ImageCropRect = { x: number; y: number; width: number; height: number };
|
|
17
|
+
|
|
18
|
+
export type ImageCropResult = { fit: 'contain' } | { fit: 'cover'; rect: ImageCropRect };
|
|
21
19
|
|
|
22
20
|
export function ImageCropDialog({
|
|
23
21
|
src,
|
|
@@ -25,6 +23,7 @@ export function ImageCropDialog({
|
|
|
25
23
|
targetHeight,
|
|
26
24
|
initialFit,
|
|
27
25
|
initialPosition,
|
|
26
|
+
initialRect,
|
|
28
27
|
onClose,
|
|
29
28
|
onApply,
|
|
30
29
|
}: {
|
|
@@ -33,6 +32,7 @@ export function ImageCropDialog({
|
|
|
33
32
|
targetHeight: number;
|
|
34
33
|
initialFit: 'cover' | 'contain';
|
|
35
34
|
initialPosition: { x: number; y: number };
|
|
35
|
+
initialRect: ImageCropRect | null;
|
|
36
36
|
onClose: () => void;
|
|
37
37
|
onApply: (result: ImageCropResult) => void;
|
|
38
38
|
}) {
|
|
@@ -44,26 +44,30 @@ export function ImageCropDialog({
|
|
|
44
44
|
|
|
45
45
|
const onImageLoad = (e: SyntheticEvent<HTMLImageElement>) => {
|
|
46
46
|
const im = e.currentTarget;
|
|
47
|
-
setCrop(
|
|
47
|
+
setCrop(initialCrop(im.naturalWidth, im.naturalHeight, aspect, initialRect, initialPosition));
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
useEffect(() => {
|
|
51
51
|
const im = imgRef.current;
|
|
52
|
-
if (!im
|
|
52
|
+
if (!im?.complete || !im.naturalWidth || !im.naturalHeight) return;
|
|
53
53
|
setCrop((prev) => {
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
if (prev && prev.unit === '%') {
|
|
55
|
+
return clampToAspect(prev as PercentCrop, aspect, im.naturalWidth, im.naturalHeight);
|
|
56
|
+
}
|
|
57
|
+
return initialCrop(im.naturalWidth, im.naturalHeight, aspect, initialRect, initialPosition);
|
|
56
58
|
});
|
|
57
|
-
}, [aspect, initialPosition]);
|
|
59
|
+
}, [aspect, initialPosition, initialRect]);
|
|
58
60
|
|
|
59
61
|
const onApplyClick = () => {
|
|
60
62
|
if (fit === 'contain') {
|
|
61
|
-
onApply({ fit
|
|
63
|
+
onApply({ fit });
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
|
-
const
|
|
65
|
-
crop && crop.unit === '%'
|
|
66
|
-
|
|
66
|
+
const rect =
|
|
67
|
+
crop && crop.unit === '%'
|
|
68
|
+
? roundRect(crop as PercentCrop)
|
|
69
|
+
: { x: 0, y: 0, width: 100, height: 100 };
|
|
70
|
+
onApply({ fit, rect });
|
|
67
71
|
};
|
|
68
72
|
|
|
69
73
|
return (
|
|
@@ -98,7 +102,6 @@ export function ImageCropDialog({
|
|
|
98
102
|
onChange={(_, percentCrop) => setCrop(percentCrop)}
|
|
99
103
|
aspect={aspect}
|
|
100
104
|
keepSelection
|
|
101
|
-
locked
|
|
102
105
|
className="max-h-full"
|
|
103
106
|
>
|
|
104
107
|
<img
|
|
@@ -124,6 +127,19 @@ export function ImageCropDialog({
|
|
|
124
127
|
);
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
function initialCrop(
|
|
131
|
+
naturalW: number,
|
|
132
|
+
naturalH: number,
|
|
133
|
+
aspect: number,
|
|
134
|
+
rect: ImageCropRect | null,
|
|
135
|
+
position: { x: number; y: number },
|
|
136
|
+
): PercentCrop {
|
|
137
|
+
if (rect) {
|
|
138
|
+
return clampToAspect({ unit: '%', ...rect }, aspect, naturalW, naturalH);
|
|
139
|
+
}
|
|
140
|
+
return makeMaxSizeCrop(naturalW, naturalH, aspect, position);
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
function makeMaxSizeCrop(
|
|
128
144
|
naturalW: number,
|
|
129
145
|
naturalH: number,
|
|
@@ -150,12 +166,40 @@ function makeMaxSizeCrop(
|
|
|
150
166
|
return { unit: '%', x, y, width, height };
|
|
151
167
|
}
|
|
152
168
|
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
function clampToAspect(
|
|
170
|
+
crop: PercentCrop,
|
|
171
|
+
aspect: number,
|
|
172
|
+
naturalW: number,
|
|
173
|
+
naturalH: number,
|
|
174
|
+
): PercentCrop {
|
|
175
|
+
const cx = crop.x + crop.width / 2;
|
|
176
|
+
const cy = crop.y + crop.height / 2;
|
|
177
|
+
let width = crop.width;
|
|
178
|
+
let height = crop.height;
|
|
179
|
+
const targetPctRatio = naturalW > 0 && naturalH > 0 ? (aspect * naturalH) / naturalW : aspect;
|
|
180
|
+
const currentRatio = height > 0 ? width / height : targetPctRatio;
|
|
181
|
+
if (Math.abs(currentRatio - targetPctRatio) > 0.0001) {
|
|
182
|
+
if (currentRatio > targetPctRatio) {
|
|
183
|
+
height = width / targetPctRatio;
|
|
184
|
+
} else {
|
|
185
|
+
width = height * targetPctRatio;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
width = clamp(width, 1, 100);
|
|
189
|
+
height = clamp(height, 1, 100);
|
|
190
|
+
let x = cx - width / 2;
|
|
191
|
+
let y = cy - height / 2;
|
|
192
|
+
x = clamp(x, 0, 100 - width);
|
|
193
|
+
y = clamp(y, 0, 100 - height);
|
|
194
|
+
return { unit: '%', x, y, width, height };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function roundRect(crop: PercentCrop): ImageCropRect {
|
|
156
198
|
return {
|
|
157
|
-
x:
|
|
158
|
-
y:
|
|
199
|
+
x: round2(crop.x),
|
|
200
|
+
y: round2(crop.y),
|
|
201
|
+
width: round2(crop.width),
|
|
202
|
+
height: round2(crop.height),
|
|
159
203
|
};
|
|
160
204
|
}
|
|
161
205
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
AlignLeft,
|
|
5
5
|
AlignRight,
|
|
6
6
|
Bold,
|
|
7
|
+
Crop,
|
|
7
8
|
ImageIcon,
|
|
8
9
|
Italic,
|
|
9
10
|
X,
|
|
@@ -32,6 +33,7 @@ import { Slider } from '@/components/ui/slider';
|
|
|
32
33
|
import { Textarea } from '@/components/ui/textarea';
|
|
33
34
|
import { Toggle } from '@/components/ui/toggle';
|
|
34
35
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
36
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
35
37
|
import { type AssetEntry, useAssets } from '@/lib/assets';
|
|
36
38
|
import { findSlideSource } from '@/lib/inspector/fiber';
|
|
37
39
|
import type { EditOp } from '@/lib/inspector/use-editor';
|
|
@@ -149,15 +151,18 @@ export function InspectorPanel() {
|
|
|
149
151
|
<{pinSelected.anchor.tagName.toLowerCase()}>
|
|
150
152
|
</span>
|
|
151
153
|
</div>
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
<div className="flex items-center gap-1.5">
|
|
155
|
+
<AgentWatchingBadge />
|
|
156
|
+
<Button
|
|
157
|
+
variant="ghost"
|
|
158
|
+
size="icon-sm"
|
|
159
|
+
className="text-muted-foreground hover:text-foreground"
|
|
160
|
+
onClick={() => setSelected(null)}
|
|
161
|
+
aria-label={t.inspector.deselect}
|
|
162
|
+
>
|
|
163
|
+
<X className="size-3.5" />
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
161
166
|
</>
|
|
162
167
|
}
|
|
163
168
|
footer={<CommentsSection selected={pinSelected} onAdd={add} />}
|
|
@@ -637,6 +642,7 @@ function ImageField({
|
|
|
637
642
|
className="flex-1"
|
|
638
643
|
onClick={() => openCrop(anchor as HTMLImageElement)}
|
|
639
644
|
>
|
|
645
|
+
<Crop className="size-3.5" />
|
|
640
646
|
{t.inspector.crop}
|
|
641
647
|
</Button>
|
|
642
648
|
)}
|
|
@@ -800,6 +806,31 @@ function AssetPickerDialog({
|
|
|
800
806
|
);
|
|
801
807
|
}
|
|
802
808
|
|
|
809
|
+
function AgentWatchingBadge() {
|
|
810
|
+
const t = useLocale();
|
|
811
|
+
return (
|
|
812
|
+
<TooltipProvider delayDuration={200}>
|
|
813
|
+
<Tooltip>
|
|
814
|
+
<TooltipTrigger asChild>
|
|
815
|
+
<button
|
|
816
|
+
type="button"
|
|
817
|
+
className="flex shrink-0 cursor-help items-center gap-1.5 rounded-[3px] border border-hairline bg-card px-1.5 py-px text-[10.5px] text-foreground/85 outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
818
|
+
>
|
|
819
|
+
<span aria-hidden className="relative flex size-1.5 items-center justify-center">
|
|
820
|
+
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-500 opacity-60" />
|
|
821
|
+
<span className="relative inline-flex size-1.5 rounded-full bg-emerald-500" />
|
|
822
|
+
</span>
|
|
823
|
+
{t.inspector.agentWatching}
|
|
824
|
+
</button>
|
|
825
|
+
</TooltipTrigger>
|
|
826
|
+
<TooltipContent side="bottom" align="end" className="max-w-[260px] leading-relaxed">
|
|
827
|
+
{t.inspector.agentWatchingTooltip}
|
|
828
|
+
</TooltipContent>
|
|
829
|
+
</Tooltip>
|
|
830
|
+
</TooltipProvider>
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
803
834
|
function CommentsSection({
|
|
804
835
|
selected,
|
|
805
836
|
onAdd,
|
|
@@ -824,7 +855,7 @@ function CommentsSection({
|
|
|
824
855
|
};
|
|
825
856
|
|
|
826
857
|
return (
|
|
827
|
-
<Section title={t.inspector.
|
|
858
|
+
<Section title={t.inspector.leaveComment}>
|
|
828
859
|
<div className="flex flex-col gap-2">
|
|
829
860
|
<div className="comment-cue rounded-[6px]">
|
|
830
861
|
<Textarea
|
|
@@ -836,16 +867,16 @@ function CommentsSection({
|
|
|
836
867
|
submit();
|
|
837
868
|
}
|
|
838
869
|
}}
|
|
839
|
-
placeholder={t.inspector.
|
|
870
|
+
placeholder={t.inspector.commentPlaceholder}
|
|
840
871
|
className="min-h-16 resize-none text-[12px]"
|
|
841
872
|
/>
|
|
842
873
|
</div>
|
|
843
874
|
<div className="flex items-center justify-between gap-2">
|
|
844
875
|
<span className="font-mono text-[10.5px] text-muted-foreground/70">
|
|
845
|
-
{t.inspector.
|
|
876
|
+
{t.inspector.commentShortcutHint}
|
|
846
877
|
</span>
|
|
847
878
|
<Button size="sm" variant="brand" disabled={submitting || !draft.trim()} onClick={submit}>
|
|
848
|
-
{t.inspector.
|
|
879
|
+
{t.inspector.addComment}
|
|
849
880
|
</Button>
|
|
850
881
|
</div>
|
|
851
882
|
</div>
|
|
@@ -15,7 +15,7 @@ import { Button } from '@/components/ui/button';
|
|
|
15
15
|
import { type SlideComment, useComments } from '@/lib/inspector/use-comments';
|
|
16
16
|
import { type Edit, type EditOp, type EditResult, useEditor } from '@/lib/inspector/use-editor';
|
|
17
17
|
import { useLocale } from '@/lib/use-locale';
|
|
18
|
-
import { ImageCropDialog } from './image-crop-dialog';
|
|
18
|
+
import { ImageCropDialog, type ImageCropRect } from './image-crop-dialog';
|
|
19
19
|
|
|
20
20
|
export type SelectedTarget = {
|
|
21
21
|
line: number;
|
|
@@ -101,6 +101,7 @@ export function InspectorProvider({ slideId, children }: { slideId: string; chil
|
|
|
101
101
|
targetHeight: number;
|
|
102
102
|
initialFit: 'cover' | 'contain';
|
|
103
103
|
initialPosition: { x: number; y: number };
|
|
104
|
+
initialRect: ImageCropRect | null;
|
|
104
105
|
} | null>(null);
|
|
105
106
|
const t = useLocale();
|
|
106
107
|
|
|
@@ -558,6 +559,7 @@ export function InspectorProvider({ slideId, children }: { slideId: string; chil
|
|
|
558
559
|
targetHeight: anchor.offsetHeight || anchor.getBoundingClientRect().height,
|
|
559
560
|
initialFit: cs.objectFit === 'contain' ? 'contain' : 'cover',
|
|
560
561
|
initialPosition: parseObjectPosition(cs.objectPosition),
|
|
562
|
+
initialRect: parseObjectViewBox(cs.getPropertyValue('object-view-box')),
|
|
561
563
|
});
|
|
562
564
|
}, []);
|
|
563
565
|
|
|
@@ -615,18 +617,30 @@ export function InspectorProvider({ slideId, children }: { slideId: string; chil
|
|
|
615
617
|
targetHeight={cropTarget.targetHeight}
|
|
616
618
|
initialFit={cropTarget.initialFit}
|
|
617
619
|
initialPosition={cropTarget.initialPosition}
|
|
620
|
+
initialRect={cropTarget.initialRect}
|
|
618
621
|
onClose={() => setCropTarget(null)}
|
|
619
622
|
onApply={(result) => {
|
|
620
623
|
const { line, column, anchor } = cropTarget;
|
|
621
624
|
if (anchor.isConnected) {
|
|
622
|
-
|
|
625
|
+
const ops: EditOp[] = [
|
|
623
626
|
{ kind: 'set-style', key: 'objectFit', value: result.fit },
|
|
624
|
-
{
|
|
627
|
+
{ kind: 'set-style', key: 'objectPosition', value: '50% 50%' },
|
|
628
|
+
];
|
|
629
|
+
if (result.fit === 'cover') {
|
|
630
|
+
const { x, y, width, height } = result.rect;
|
|
631
|
+
const top = round2(y);
|
|
632
|
+
const left = round2(x);
|
|
633
|
+
const right = round2(100 - x - width);
|
|
634
|
+
const bottom = round2(100 - y - height);
|
|
635
|
+
ops.push({
|
|
625
636
|
kind: 'set-style',
|
|
626
|
-
key: '
|
|
627
|
-
value:
|
|
628
|
-
}
|
|
629
|
-
|
|
637
|
+
key: 'objectViewBox',
|
|
638
|
+
value: `inset(${top}% ${right}% ${bottom}% ${left}%)`,
|
|
639
|
+
});
|
|
640
|
+
} else {
|
|
641
|
+
ops.push({ kind: 'set-style', key: 'objectViewBox', value: null });
|
|
642
|
+
}
|
|
643
|
+
bufferOps(line, column, anchor, ops);
|
|
630
644
|
}
|
|
631
645
|
setCropTarget(null);
|
|
632
646
|
}}
|
|
@@ -640,6 +654,45 @@ function round2(n: number): number {
|
|
|
640
654
|
return Math.round(n * 100) / 100;
|
|
641
655
|
}
|
|
642
656
|
|
|
657
|
+
function parseObjectViewBox(value: string): ImageCropRect | null {
|
|
658
|
+
const v = value?.trim();
|
|
659
|
+
if (!v || v === 'none') return null;
|
|
660
|
+
const m = v.match(/^inset\(([^)]+)\)$/);
|
|
661
|
+
if (!m?.[1]) return null;
|
|
662
|
+
const nums = m[1]
|
|
663
|
+
.trim()
|
|
664
|
+
.split(/\s+/)
|
|
665
|
+
.map((p) => {
|
|
666
|
+
const n = p.match(/^(-?\d+(?:\.\d+)?)%$/);
|
|
667
|
+
return n?.[1] ? Number(n[1]) : null;
|
|
668
|
+
});
|
|
669
|
+
if (nums.some((n) => n === null)) return null;
|
|
670
|
+
let top: number, right: number, bottom: number, left: number;
|
|
671
|
+
if (nums.length === 1) {
|
|
672
|
+
top = right = bottom = left = nums[0] as number;
|
|
673
|
+
} else if (nums.length === 2) {
|
|
674
|
+
top = bottom = nums[0] as number;
|
|
675
|
+
right = left = nums[1] as number;
|
|
676
|
+
} else if (nums.length === 3) {
|
|
677
|
+
top = nums[0] as number;
|
|
678
|
+
right = left = nums[1] as number;
|
|
679
|
+
bottom = nums[2] as number;
|
|
680
|
+
} else if (nums.length === 4) {
|
|
681
|
+
top = nums[0] as number;
|
|
682
|
+
right = nums[1] as number;
|
|
683
|
+
bottom = nums[2] as number;
|
|
684
|
+
left = nums[3] as number;
|
|
685
|
+
} else {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
const x = left;
|
|
689
|
+
const y = top;
|
|
690
|
+
const width = 100 - left - right;
|
|
691
|
+
const height = 100 - top - bottom;
|
|
692
|
+
if (width <= 0 || height <= 0) return null;
|
|
693
|
+
return { x, y, width, height };
|
|
694
|
+
}
|
|
695
|
+
|
|
643
696
|
function parseObjectPosition(value: string): { x: number; y: number } {
|
|
644
697
|
const parts = value.trim().split(/\s+/);
|
|
645
698
|
const xRaw = parts[0] ?? '50%';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ChevronDown, ChevronUp, NotebookPen } from 'lucide-react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { PANEL_TRANSITION_MS, usePanelMount } from '@/components/panel/panel-shell';
|
|
4
|
+
import { useNotes } from '@/lib/inspector/use-notes';
|
|
5
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
const STORAGE_KEY = 'open-slide:notes-drawer-open';
|
|
9
|
+
const DRAWER_CONTENT_H = 166;
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
slideId: string;
|
|
13
|
+
index: number;
|
|
14
|
+
total: number;
|
|
15
|
+
initial: string | undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function NotesDrawer({ slideId, index, total, initial }: Props) {
|
|
19
|
+
const t = useLocale();
|
|
20
|
+
const [open, setOpen] = useState(() => {
|
|
21
|
+
if (typeof window === 'undefined') return false;
|
|
22
|
+
return window.localStorage.getItem(STORAGE_KEY) === '1';
|
|
23
|
+
});
|
|
24
|
+
const { value, setValue, status, flush } = useNotes(slideId, index, initial);
|
|
25
|
+
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
26
|
+
const { mounted, animVisible } = usePanelMount(open);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (typeof window === 'undefined') return;
|
|
30
|
+
window.localStorage.setItem(STORAGE_KEY, open ? '1' : '0');
|
|
31
|
+
}, [open]);
|
|
32
|
+
|
|
33
|
+
const statusLabel = (() => {
|
|
34
|
+
switch (status.kind) {
|
|
35
|
+
case 'saving':
|
|
36
|
+
return t.notesDrawer.statusSaving;
|
|
37
|
+
case 'saved':
|
|
38
|
+
return t.notesDrawer.statusSaved;
|
|
39
|
+
case 'error':
|
|
40
|
+
return format(t.notesDrawer.statusError, { msg: status.message });
|
|
41
|
+
default:
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<aside
|
|
48
|
+
data-notes-drawer
|
|
49
|
+
className="hidden shrink-0 border-t border-hairline bg-sidebar/85 backdrop-blur md:block"
|
|
50
|
+
>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => {
|
|
54
|
+
setOpen((o) => {
|
|
55
|
+
if (o) void flush();
|
|
56
|
+
return !o;
|
|
57
|
+
});
|
|
58
|
+
}}
|
|
59
|
+
className="flex h-9 w-full items-center gap-2 px-3 text-[12px] text-foreground/80 hover:bg-muted/40"
|
|
60
|
+
aria-expanded={open}
|
|
61
|
+
>
|
|
62
|
+
<NotebookPen className="size-3.5 text-muted-foreground" />
|
|
63
|
+
<span className="font-medium">{t.notesDrawer.toggle}</span>
|
|
64
|
+
<span className="font-mono text-[11px] text-muted-foreground">
|
|
65
|
+
{format(t.notesDrawer.pageLabel, { n: index + 1, total })}
|
|
66
|
+
</span>
|
|
67
|
+
<span
|
|
68
|
+
className={cn(
|
|
69
|
+
'ml-auto truncate text-[11px]',
|
|
70
|
+
status.kind === 'error' ? 'text-destructive' : 'text-muted-foreground',
|
|
71
|
+
)}
|
|
72
|
+
aria-live="polite"
|
|
73
|
+
>
|
|
74
|
+
{statusLabel}
|
|
75
|
+
</span>
|
|
76
|
+
{open ? (
|
|
77
|
+
<ChevronDown className="size-3.5 text-muted-foreground" />
|
|
78
|
+
) : (
|
|
79
|
+
<ChevronUp className="size-3.5 text-muted-foreground" />
|
|
80
|
+
)}
|
|
81
|
+
</button>
|
|
82
|
+
{mounted && (
|
|
83
|
+
<div
|
|
84
|
+
className="overflow-hidden border-t border-hairline transition-[height] ease-out"
|
|
85
|
+
style={{
|
|
86
|
+
height: animVisible ? DRAWER_CONTENT_H : 0,
|
|
87
|
+
transitionDuration: `${PANEL_TRANSITION_MS}ms`,
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<div className="px-3 py-2">
|
|
91
|
+
<textarea
|
|
92
|
+
ref={textareaRef}
|
|
93
|
+
value={value}
|
|
94
|
+
onChange={(e) => setValue(e.target.value)}
|
|
95
|
+
onBlur={() => {
|
|
96
|
+
void flush();
|
|
97
|
+
}}
|
|
98
|
+
onKeyDown={(e) => {
|
|
99
|
+
if (e.key === 'Escape') {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
textareaRef.current?.blur();
|
|
102
|
+
} else if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
void flush();
|
|
105
|
+
}
|
|
106
|
+
}}
|
|
107
|
+
placeholder={t.notesDrawer.placeholder}
|
|
108
|
+
rows={6}
|
|
109
|
+
spellCheck
|
|
110
|
+
className="block h-[150px] w-full resize-none rounded-[6px] border border-border bg-card px-3 py-2 text-[13px] leading-relaxed text-card-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</aside>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -61,6 +61,9 @@ export function Player({
|
|
|
61
61
|
const [keyboardDriven, setKeyboardDriven] = useState(false);
|
|
62
62
|
const [startedAt] = useState(() => Date.now());
|
|
63
63
|
|
|
64
|
+
const canPrev = index > 0;
|
|
65
|
+
const canNext = index < pages.length - 1;
|
|
66
|
+
|
|
64
67
|
const goPrev = useCallback(() => {
|
|
65
68
|
if (index > 0) onIndexChange(index - 1);
|
|
66
69
|
}, [index, onIndexChange]);
|
|
@@ -73,8 +76,8 @@ export function Player({
|
|
|
73
76
|
useWheelPageNavigation({
|
|
74
77
|
ref: rootRef,
|
|
75
78
|
enabled: !overlayActive,
|
|
76
|
-
canPrev
|
|
77
|
-
canNext
|
|
79
|
+
canPrev,
|
|
80
|
+
canNext,
|
|
78
81
|
onPrev: goPrev,
|
|
79
82
|
onNext: goNext,
|
|
80
83
|
});
|
|
@@ -269,7 +272,8 @@ export function Player({
|
|
|
269
272
|
ref={setRoot}
|
|
270
273
|
className={cn(
|
|
271
274
|
'relative flex h-dvh w-screen items-center justify-center bg-black',
|
|
272
|
-
|
|
275
|
+
controls && 'select-none',
|
|
276
|
+
controls && (hideCursor ? 'cursor-none' : 'cursor-default'),
|
|
273
277
|
)}
|
|
274
278
|
>
|
|
275
279
|
<SlideCanvas flat design={design}>
|
|
@@ -280,15 +284,15 @@ export function Player({
|
|
|
280
284
|
type="button"
|
|
281
285
|
aria-label="Previous page"
|
|
282
286
|
onClick={goPrev}
|
|
283
|
-
disabled={
|
|
284
|
-
className=
|
|
287
|
+
disabled={!canPrev}
|
|
288
|
+
className={cn('absolute inset-y-0 left-0 z-10 w-[30%]', hideCursor && 'cursor-none')}
|
|
285
289
|
/>
|
|
286
290
|
<button
|
|
287
291
|
type="button"
|
|
288
292
|
aria-label="Next page"
|
|
289
293
|
onClick={goNext}
|
|
290
|
-
disabled={
|
|
291
|
-
className=
|
|
294
|
+
disabled={!canNext}
|
|
295
|
+
className={cn('absolute inset-y-0 right-0 z-10 w-[30%]', hideCursor && 'cursor-none')}
|
|
292
296
|
/>
|
|
293
297
|
|
|
294
298
|
{controls && (
|
|
@@ -96,9 +96,9 @@ export function PresentOverviewGrid({ pages, design, open, current, onClose, onS
|
|
|
96
96
|
</div>
|
|
97
97
|
<div ref={gridRef} className="min-h-0 flex-1 overflow-auto px-8 pb-8">
|
|
98
98
|
<div
|
|
99
|
-
className="grid gap-5"
|
|
99
|
+
className="grid justify-center gap-5"
|
|
100
100
|
style={{
|
|
101
|
-
gridTemplateColumns: `repeat(auto-fill,
|
|
101
|
+
gridTemplateColumns: `repeat(auto-fill, ${THUMB_W}px)`,
|
|
102
102
|
}}
|
|
103
103
|
>
|
|
104
104
|
{pages.map((PageComp, i) => {
|