promptslide 0.3.18 → 0.3.19
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/CHANGELOG.md +7 -0
- package/README.md +76 -0
- package/dist/index.d.ts +29 -4
- package/dist/index.js +1336 -339
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/build.mjs +4 -1
- package/src/commands/clone.mjs +5 -2
- package/src/commands/create.mjs +45 -14
- package/src/commands/pull.mjs +6 -3
- package/src/core/annotations/annotation-panel.tsx +18 -12
- package/src/core/index.ts +11 -5
- package/src/core/inspector.tsx +155 -0
- package/src/core/loop/config.d.mts +13 -0
- package/src/core/loop/config.mjs +65 -0
- package/src/core/loop/context.tsx +20 -0
- package/src/core/loop/loop-settings.tsx +292 -0
- package/src/core/loop/use-loop-playback.ts +115 -0
- package/src/core/slide-deck.tsx +398 -146
- package/src/core/slide-renderer.tsx +7 -1
- package/src/core/toolbar-action.tsx +38 -0
- package/src/core/types.ts +14 -0
- package/src/core/use-slide-navigation.ts +27 -0
- package/src/core/view-menu.tsx +130 -0
- package/src/editor/README.md +11 -1
- package/src/editor/plugin.mjs +1 -1
- package/src/editor/studio.css +1 -27
- package/src/editor/studio.tsx +45 -29
- package/src/utils/deck-config.mjs +6 -0
- package/src/utils/loop-config.mjs +70 -0
- package/src/vite/plugin.mjs +18 -2
- package/templates/default/src/deck-config.ts +2 -2
package/dist/index.js
CHANGED
|
@@ -503,6 +503,26 @@ function useSlideNavigation({
|
|
|
503
503
|
},
|
|
504
504
|
[currentSlide, slides.length, onSlideChange]
|
|
505
505
|
);
|
|
506
|
+
const goToStep = useCallback(
|
|
507
|
+
(step) => {
|
|
508
|
+
if (!Number.isInteger(step)) return;
|
|
509
|
+
setQueuedAction(null);
|
|
510
|
+
setAnimationStep(Math.max(0, Math.min(step, slides[currentSlide]?.steps ?? 0)));
|
|
511
|
+
setNavState((previous) => ({ ...previous, direction: 0 }));
|
|
512
|
+
},
|
|
513
|
+
[slides, currentSlide]
|
|
514
|
+
);
|
|
515
|
+
const restartAtSlide = useCallback(
|
|
516
|
+
(index) => {
|
|
517
|
+
if (index < 0 || index >= slides.length) return;
|
|
518
|
+
setQueuedAction(null);
|
|
519
|
+
setAnimationStep(0);
|
|
520
|
+
setCurrentSlide(index);
|
|
521
|
+
setNavState({ status: index === currentSlide ? "idle" : "transitioning", direction: 1 });
|
|
522
|
+
onSlideChange?.(index);
|
|
523
|
+
},
|
|
524
|
+
[slides.length, currentSlide, onSlideChange]
|
|
525
|
+
);
|
|
506
526
|
useEffect(() => {
|
|
507
527
|
if (navState.status === "idle" && queuedAction !== null) {
|
|
508
528
|
setQueuedAction(null);
|
|
@@ -523,6 +543,8 @@ function useSlideNavigation({
|
|
|
523
543
|
advance,
|
|
524
544
|
goBack,
|
|
525
545
|
goToSlide,
|
|
546
|
+
goToStep,
|
|
547
|
+
restartAtSlide,
|
|
526
548
|
onTransitionComplete
|
|
527
549
|
};
|
|
528
550
|
}
|
|
@@ -595,6 +617,7 @@ var SlideErrorBoundary = class extends React.Component {
|
|
|
595
617
|
// src/core/slide-renderer.tsx
|
|
596
618
|
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
597
619
|
function SlideRenderer({
|
|
620
|
+
playbackKey,
|
|
598
621
|
slides,
|
|
599
622
|
currentSlide,
|
|
600
623
|
animationStep,
|
|
@@ -634,7 +657,14 @@ function SlideRenderer({
|
|
|
634
657
|
currentStep: animationStep,
|
|
635
658
|
totalSteps,
|
|
636
659
|
showAllAnimations,
|
|
637
|
-
children: /* @__PURE__ */ jsx6(SlideErrorBoundary, { slideIndex: currentSlide, slideTitle: slides[currentSlide]?.title, children: /* @__PURE__ */ jsx6(
|
|
660
|
+
children: /* @__PURE__ */ jsx6(SlideErrorBoundary, { slideIndex: currentSlide, slideTitle: slides[currentSlide]?.title, children: /* @__PURE__ */ jsx6(
|
|
661
|
+
CurrentSlideComponent,
|
|
662
|
+
{
|
|
663
|
+
slideNumber: currentSlide + 1,
|
|
664
|
+
totalSlides: slides.length
|
|
665
|
+
},
|
|
666
|
+
playbackKey
|
|
667
|
+
) })
|
|
638
668
|
}
|
|
639
669
|
)
|
|
640
670
|
},
|
|
@@ -741,18 +771,17 @@ function SlideEmbed({ slides, transition, directionalTransition }) {
|
|
|
741
771
|
// src/core/slide-deck.tsx
|
|
742
772
|
import { LayoutGroup } from "framer-motion";
|
|
743
773
|
import {
|
|
774
|
+
ChevronDown as ChevronDown2,
|
|
744
775
|
ChevronLeft,
|
|
745
776
|
ChevronRight,
|
|
746
|
-
Download,
|
|
747
|
-
Grid3X3,
|
|
748
|
-
List,
|
|
777
|
+
Download as Download2,
|
|
749
778
|
MessageCircle,
|
|
750
|
-
Monitor,
|
|
751
779
|
PanelLeftClose,
|
|
752
780
|
PanelLeftOpen,
|
|
753
|
-
Play
|
|
781
|
+
Play as Play2,
|
|
782
|
+
Settings2
|
|
754
783
|
} from "lucide-react";
|
|
755
|
-
import { useCallback as
|
|
784
|
+
import { useCallback as useCallback6, useEffect as useEffect9, useLayoutEffect as useLayoutEffect2, useMemo as useMemo5, useRef as useRef8, useState as useState10 } from "react";
|
|
756
785
|
|
|
757
786
|
// src/core/annotations/annotation-overlay.tsx
|
|
758
787
|
import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
|
|
@@ -1093,6 +1122,7 @@ import { Check, Pencil, PanelRightClose, RotateCcw, Trash2 } from "lucide-react"
|
|
|
1093
1122
|
import { useEffect as useEffect5, useRef as useRef4, useState as useState5 } from "react";
|
|
1094
1123
|
import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1095
1124
|
function AnnotationPanel({
|
|
1125
|
+
embedded = false,
|
|
1096
1126
|
annotations,
|
|
1097
1127
|
unlinkedAnnotations = [],
|
|
1098
1128
|
selectedId,
|
|
@@ -1106,82 +1136,88 @@ function AnnotationPanel({
|
|
|
1106
1136
|
const resolved = annotations.filter((a) => a.status === "resolved");
|
|
1107
1137
|
const total = annotations.length + unlinkedAnnotations.length;
|
|
1108
1138
|
const numberFor = (annotation) => annotations.findIndex((a) => a.id === annotation.id) + 1;
|
|
1109
|
-
return /* @__PURE__ */ jsxs5(
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
className: "rounded p-2 text-neutral-400 hover:bg-neutral-800 hover:text-white",
|
|
1116
|
-
"aria-label": "Hide comments",
|
|
1117
|
-
title: "Hide comments (C)",
|
|
1118
|
-
children: /* @__PURE__ */ jsx11(PanelRightClose, { className: "h-4 w-4" })
|
|
1119
|
-
}
|
|
1120
|
-
),
|
|
1121
|
-
/* @__PURE__ */ jsxs5("span", { className: "text-right text-sm font-semibold", children: [
|
|
1122
|
-
"Comments (",
|
|
1123
|
-
total,
|
|
1124
|
-
")"
|
|
1125
|
-
] })
|
|
1126
|
-
] }),
|
|
1127
|
-
/* @__PURE__ */ jsxs5("div", { className: "min-h-0 flex-1 overflow-y-auto p-3", children: [
|
|
1128
|
-
annotations.length === 0 && unlinkedAnnotations.length === 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-4 px-3 py-6", children: /* @__PURE__ */ jsx11(WorkflowHint, {}) }),
|
|
1129
|
-
open.length > 0 && /* @__PURE__ */ jsxs5("div", { children: [
|
|
1130
|
-
/* @__PURE__ */ jsx11("div", { className: "mb-1.5 px-2 pt-1 text-[11px] font-medium tracking-wider text-neutral-500 uppercase", children: "Open" }),
|
|
1131
|
-
open.map((a) => /* @__PURE__ */ jsx11(
|
|
1132
|
-
AnnotationItem,
|
|
1133
|
-
{
|
|
1134
|
-
annotation: a,
|
|
1135
|
-
number: numberFor(a),
|
|
1136
|
-
isSelected: a.id === selectedId,
|
|
1137
|
-
onSelect: () => onSelect(a.id),
|
|
1138
|
-
onHover: (hovered) => onHover(hovered ? a.id : null),
|
|
1139
|
-
onDelete: () => onDelete(a.id),
|
|
1140
|
-
onUpdate: (patch) => onUpdate(a.id, patch)
|
|
1141
|
-
},
|
|
1142
|
-
a.id
|
|
1143
|
-
))
|
|
1144
|
-
] }),
|
|
1145
|
-
resolved.length > 0 && /* @__PURE__ */ jsxs5("div", { className: open.length > 0 ? "mt-3" : "", children: [
|
|
1146
|
-
/* @__PURE__ */ jsx11("div", { className: "mb-1.5 px-2 pt-1 text-[11px] font-medium tracking-wider text-neutral-500 uppercase", children: "Resolved" }),
|
|
1147
|
-
resolved.map((a) => /* @__PURE__ */ jsx11(
|
|
1148
|
-
AnnotationItem,
|
|
1149
|
-
{
|
|
1150
|
-
annotation: a,
|
|
1151
|
-
number: numberFor(a),
|
|
1152
|
-
isSelected: a.id === selectedId,
|
|
1153
|
-
onSelect: () => onSelect(a.id),
|
|
1154
|
-
onHover: (hovered) => onHover(hovered ? a.id : null),
|
|
1155
|
-
onDelete: () => onDelete(a.id),
|
|
1156
|
-
onUpdate: (patch) => onUpdate(a.id, patch)
|
|
1157
|
-
},
|
|
1158
|
-
a.id
|
|
1159
|
-
))
|
|
1160
|
-
] }),
|
|
1161
|
-
unlinkedAnnotations.length > 0 && /* @__PURE__ */ jsxs5("div", { className: "mt-3", children: [
|
|
1162
|
-
/* @__PURE__ */ jsx11("div", { className: "mb-1.5 px-2 pt-1 text-[11px] font-medium tracking-wider text-neutral-500 uppercase", children: "Removed slides" }),
|
|
1163
|
-
unlinkedAnnotations.map((a, i) => /* @__PURE__ */ jsxs5("div", { children: [
|
|
1164
|
-
/* @__PURE__ */ jsx11("p", { className: "px-2 text-[11px] text-neutral-500", children: a.slideTitle || a.slideId }),
|
|
1139
|
+
return /* @__PURE__ */ jsxs5(
|
|
1140
|
+
"div",
|
|
1141
|
+
{
|
|
1142
|
+
className: `flex h-full max-w-[90vw] flex-shrink-0 flex-col bg-neutral-950 text-white ${embedded ? "w-full" : "w-80 border-l border-neutral-800"}`,
|
|
1143
|
+
children: [
|
|
1144
|
+
!embedded && /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between border-b border-neutral-800 px-3 py-2", children: [
|
|
1165
1145
|
/* @__PURE__ */ jsx11(
|
|
1166
|
-
|
|
1146
|
+
"button",
|
|
1167
1147
|
{
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
onDelete: () => onDelete(a.id),
|
|
1174
|
-
onUpdate: (patch) => onUpdate(a.id, patch)
|
|
1148
|
+
onClick: onClose,
|
|
1149
|
+
className: "rounded p-2 text-neutral-400 hover:bg-neutral-800 hover:text-white",
|
|
1150
|
+
"aria-label": "Hide comments",
|
|
1151
|
+
title: "Hide comments (C)",
|
|
1152
|
+
children: /* @__PURE__ */ jsx11(PanelRightClose, { className: "h-4 w-4" })
|
|
1175
1153
|
}
|
|
1176
|
-
)
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1154
|
+
),
|
|
1155
|
+
/* @__PURE__ */ jsxs5("span", { className: "text-right text-sm font-semibold", children: [
|
|
1156
|
+
"Comments (",
|
|
1157
|
+
total,
|
|
1158
|
+
")"
|
|
1159
|
+
] })
|
|
1160
|
+
] }),
|
|
1161
|
+
/* @__PURE__ */ jsxs5("div", { className: "min-h-0 flex-1 overflow-y-auto p-3", children: [
|
|
1162
|
+
annotations.length === 0 && unlinkedAnnotations.length === 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-4 px-3 py-6", children: /* @__PURE__ */ jsx11(WorkflowHint, {}) }),
|
|
1163
|
+
open.length > 0 && /* @__PURE__ */ jsxs5("div", { children: [
|
|
1164
|
+
/* @__PURE__ */ jsx11("div", { className: "mb-1.5 px-2 pt-1 text-[11px] font-medium tracking-wider text-neutral-500 uppercase", children: "Open" }),
|
|
1165
|
+
open.map((a) => /* @__PURE__ */ jsx11(
|
|
1166
|
+
AnnotationItem,
|
|
1167
|
+
{
|
|
1168
|
+
annotation: a,
|
|
1169
|
+
number: numberFor(a),
|
|
1170
|
+
isSelected: a.id === selectedId,
|
|
1171
|
+
onSelect: () => onSelect(a.id),
|
|
1172
|
+
onHover: (hovered) => onHover(hovered ? a.id : null),
|
|
1173
|
+
onDelete: () => onDelete(a.id),
|
|
1174
|
+
onUpdate: (patch) => onUpdate(a.id, patch)
|
|
1175
|
+
},
|
|
1176
|
+
a.id
|
|
1177
|
+
))
|
|
1178
|
+
] }),
|
|
1179
|
+
resolved.length > 0 && /* @__PURE__ */ jsxs5("div", { className: open.length > 0 ? "mt-3" : "", children: [
|
|
1180
|
+
/* @__PURE__ */ jsx11("div", { className: "mb-1.5 px-2 pt-1 text-[11px] font-medium tracking-wider text-neutral-500 uppercase", children: "Resolved" }),
|
|
1181
|
+
resolved.map((a) => /* @__PURE__ */ jsx11(
|
|
1182
|
+
AnnotationItem,
|
|
1183
|
+
{
|
|
1184
|
+
annotation: a,
|
|
1185
|
+
number: numberFor(a),
|
|
1186
|
+
isSelected: a.id === selectedId,
|
|
1187
|
+
onSelect: () => onSelect(a.id),
|
|
1188
|
+
onHover: (hovered) => onHover(hovered ? a.id : null),
|
|
1189
|
+
onDelete: () => onDelete(a.id),
|
|
1190
|
+
onUpdate: (patch) => onUpdate(a.id, patch)
|
|
1191
|
+
},
|
|
1192
|
+
a.id
|
|
1193
|
+
))
|
|
1194
|
+
] }),
|
|
1195
|
+
unlinkedAnnotations.length > 0 && /* @__PURE__ */ jsxs5("div", { className: "mt-3", children: [
|
|
1196
|
+
/* @__PURE__ */ jsx11("div", { className: "mb-1.5 px-2 pt-1 text-[11px] font-medium tracking-wider text-neutral-500 uppercase", children: "Removed slides" }),
|
|
1197
|
+
unlinkedAnnotations.map((a, i) => /* @__PURE__ */ jsxs5("div", { children: [
|
|
1198
|
+
/* @__PURE__ */ jsx11("p", { className: "px-2 text-[11px] text-neutral-500", children: a.slideTitle || a.slideId }),
|
|
1199
|
+
/* @__PURE__ */ jsx11(
|
|
1200
|
+
AnnotationItem,
|
|
1201
|
+
{
|
|
1202
|
+
annotation: a,
|
|
1203
|
+
number: i + 1,
|
|
1204
|
+
isSelected: a.id === selectedId,
|
|
1205
|
+
onSelect: () => onSelect(a.id),
|
|
1206
|
+
onHover: (hovered) => onHover(hovered ? a.id : null),
|
|
1207
|
+
onDelete: () => onDelete(a.id),
|
|
1208
|
+
onUpdate: (patch) => onUpdate(a.id, patch)
|
|
1209
|
+
}
|
|
1210
|
+
)
|
|
1211
|
+
] }, a.id))
|
|
1212
|
+
] })
|
|
1213
|
+
] }),
|
|
1214
|
+
annotations.length + unlinkedAnnotations.length > 0 && /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
1215
|
+
/* @__PURE__ */ jsx11("div", { className: "border-t border-neutral-800" }),
|
|
1216
|
+
/* @__PURE__ */ jsx11("div", { className: "px-3 py-3", children: /* @__PURE__ */ jsx11(WorkflowHint, {}) })
|
|
1217
|
+
] })
|
|
1218
|
+
]
|
|
1219
|
+
}
|
|
1220
|
+
);
|
|
1185
1221
|
}
|
|
1186
1222
|
function WorkflowHint() {
|
|
1187
1223
|
return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
|
|
@@ -1468,6 +1504,10 @@ function useAnnotations(slidesOrAdapter, adapter) {
|
|
|
1468
1504
|
};
|
|
1469
1505
|
}
|
|
1470
1506
|
|
|
1507
|
+
// src/core/inspector.tsx
|
|
1508
|
+
import { X as X2 } from "lucide-react";
|
|
1509
|
+
import { useId } from "react";
|
|
1510
|
+
|
|
1471
1511
|
// src/core/utils.ts
|
|
1472
1512
|
import { clsx } from "clsx";
|
|
1473
1513
|
import { twMerge } from "tailwind-merge";
|
|
@@ -1475,22 +1515,752 @@ function cn(...inputs) {
|
|
|
1475
1515
|
return twMerge(clsx(inputs));
|
|
1476
1516
|
}
|
|
1477
1517
|
|
|
1518
|
+
// src/core/inspector.tsx
|
|
1519
|
+
import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1520
|
+
function Inspector({
|
|
1521
|
+
mode,
|
|
1522
|
+
visible,
|
|
1523
|
+
editorAvailable,
|
|
1524
|
+
width,
|
|
1525
|
+
overlay,
|
|
1526
|
+
onSelect,
|
|
1527
|
+
onClose,
|
|
1528
|
+
slideSettings,
|
|
1529
|
+
comments
|
|
1530
|
+
}) {
|
|
1531
|
+
const prefix = useId();
|
|
1532
|
+
const tabs = [
|
|
1533
|
+
{ id: "slide", label: "Slide" },
|
|
1534
|
+
...editorAvailable ? [{ id: "editor", label: "Elements" }] : [],
|
|
1535
|
+
{ id: "comments", label: "Comments" }
|
|
1536
|
+
];
|
|
1537
|
+
const closeLabel = mode === "editor" ? "Done editing" : mode === "comments" ? "Hide comments" : "Close slide settings";
|
|
1538
|
+
return /* @__PURE__ */ jsx12(
|
|
1539
|
+
"aside",
|
|
1540
|
+
{
|
|
1541
|
+
"data-ps-inspector": true,
|
|
1542
|
+
"aria-label": "Inspector",
|
|
1543
|
+
"aria-hidden": !visible,
|
|
1544
|
+
inert: !visible,
|
|
1545
|
+
className: cn(
|
|
1546
|
+
"h-full shrink-0 overflow-hidden bg-neutral-950 text-white transition-[width,transform] duration-200 ease-out motion-reduce:transition-none print:hidden",
|
|
1547
|
+
overlay && "fixed inset-y-0 right-0 z-[60]",
|
|
1548
|
+
overlay && !visible && "translate-x-full"
|
|
1549
|
+
),
|
|
1550
|
+
style: { width: visible ? width : 0, maxWidth: overlay ? "90vw" : void 0 },
|
|
1551
|
+
onKeyDown: (event) => {
|
|
1552
|
+
if ((event.key === " " || event.key === "Enter") && event.target.closest("button"))
|
|
1553
|
+
event.stopPropagation();
|
|
1554
|
+
},
|
|
1555
|
+
children: /* @__PURE__ */ jsxs6("div", { className: "flex h-full min-h-0 w-full flex-col border-l border-neutral-800", children: [
|
|
1556
|
+
/* @__PURE__ */ jsxs6(
|
|
1557
|
+
"div",
|
|
1558
|
+
{
|
|
1559
|
+
"data-ps-inspector-header": true,
|
|
1560
|
+
className: "flex shrink-0 items-center gap-1 border-b border-neutral-800 px-2 py-2",
|
|
1561
|
+
children: [
|
|
1562
|
+
/* @__PURE__ */ jsx12(
|
|
1563
|
+
"div",
|
|
1564
|
+
{
|
|
1565
|
+
role: "tablist",
|
|
1566
|
+
"aria-label": "Inspector sections",
|
|
1567
|
+
className: "flex min-w-0 flex-1 gap-1",
|
|
1568
|
+
onKeyDown: (event) => {
|
|
1569
|
+
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
|
|
1570
|
+
event.preventDefault();
|
|
1571
|
+
event.stopPropagation();
|
|
1572
|
+
const buttons = Array.from(
|
|
1573
|
+
event.currentTarget.querySelectorAll("button")
|
|
1574
|
+
);
|
|
1575
|
+
const index = buttons.indexOf(event.target);
|
|
1576
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === "ArrowRight" ? 1 : -1) + buttons.length) % buttons.length;
|
|
1577
|
+
buttons[next]?.focus();
|
|
1578
|
+
},
|
|
1579
|
+
children: tabs.map((tab) => /* @__PURE__ */ jsx12(
|
|
1580
|
+
"button",
|
|
1581
|
+
{
|
|
1582
|
+
id: `${prefix}-tab-${tab.id}`,
|
|
1583
|
+
role: "tab",
|
|
1584
|
+
"aria-selected": mode === tab.id,
|
|
1585
|
+
"aria-controls": `${prefix}-panel-${tab.id}`,
|
|
1586
|
+
tabIndex: mode === tab.id ? 0 : -1,
|
|
1587
|
+
className: cn(
|
|
1588
|
+
"rounded-md px-2 py-1.5 text-xs font-medium hover:bg-neutral-800 focus-visible:outline-2 focus-visible:outline-orange-500",
|
|
1589
|
+
mode === tab.id ? "bg-neutral-800 text-white" : "text-neutral-400"
|
|
1590
|
+
),
|
|
1591
|
+
onClick: () => onSelect(tab.id),
|
|
1592
|
+
children: tab.label
|
|
1593
|
+
},
|
|
1594
|
+
tab.id
|
|
1595
|
+
))
|
|
1596
|
+
}
|
|
1597
|
+
),
|
|
1598
|
+
/* @__PURE__ */ jsx12(
|
|
1599
|
+
"button",
|
|
1600
|
+
{
|
|
1601
|
+
onClick: onClose,
|
|
1602
|
+
"aria-label": closeLabel,
|
|
1603
|
+
title: closeLabel,
|
|
1604
|
+
className: "shrink-0 rounded-md p-1.5 text-neutral-400 hover:bg-neutral-800 hover:text-white focus-visible:outline-2 focus-visible:outline-orange-500",
|
|
1605
|
+
children: /* @__PURE__ */ jsx12(X2, { size: 16 })
|
|
1606
|
+
}
|
|
1607
|
+
)
|
|
1608
|
+
]
|
|
1609
|
+
}
|
|
1610
|
+
),
|
|
1611
|
+
/* @__PURE__ */ jsx12(
|
|
1612
|
+
"div",
|
|
1613
|
+
{
|
|
1614
|
+
id: `${prefix}-panel-slide`,
|
|
1615
|
+
role: "tabpanel",
|
|
1616
|
+
"aria-labelledby": `${prefix}-tab-slide`,
|
|
1617
|
+
hidden: mode !== "slide",
|
|
1618
|
+
className: "min-h-0 flex-1",
|
|
1619
|
+
children: slideSettings
|
|
1620
|
+
}
|
|
1621
|
+
),
|
|
1622
|
+
/* @__PURE__ */ jsx12(
|
|
1623
|
+
"div",
|
|
1624
|
+
{
|
|
1625
|
+
id: `${prefix}-panel-editor`,
|
|
1626
|
+
role: "tabpanel",
|
|
1627
|
+
"aria-labelledby": `${prefix}-tab-editor`,
|
|
1628
|
+
hidden: mode !== "editor",
|
|
1629
|
+
className: "min-h-0 flex-1",
|
|
1630
|
+
children: /* @__PURE__ */ jsx12(
|
|
1631
|
+
"div",
|
|
1632
|
+
{
|
|
1633
|
+
"data-ps-editor-panel": true,
|
|
1634
|
+
"data-open": mode === "editor" && visible,
|
|
1635
|
+
"aria-hidden": mode !== "editor" || !visible,
|
|
1636
|
+
inert: mode !== "editor" || !visible,
|
|
1637
|
+
className: "h-full w-full"
|
|
1638
|
+
}
|
|
1639
|
+
)
|
|
1640
|
+
}
|
|
1641
|
+
),
|
|
1642
|
+
/* @__PURE__ */ jsx12(
|
|
1643
|
+
"div",
|
|
1644
|
+
{
|
|
1645
|
+
id: `${prefix}-panel-comments`,
|
|
1646
|
+
role: "tabpanel",
|
|
1647
|
+
"aria-labelledby": `${prefix}-tab-comments`,
|
|
1648
|
+
hidden: mode !== "comments",
|
|
1649
|
+
className: "min-h-0 flex-1",
|
|
1650
|
+
children: comments
|
|
1651
|
+
}
|
|
1652
|
+
)
|
|
1653
|
+
] })
|
|
1654
|
+
}
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// src/core/loop/config.mjs
|
|
1659
|
+
var DEFAULT_LOOP_CONFIG = { version: 1, defaultDuration: 5, slides: {} };
|
|
1660
|
+
function validateLoopConfig(value) {
|
|
1661
|
+
if (!value || value.version !== 1 || !validDuration(value.defaultDuration) || !value.slides || typeof value.slides !== "object" || Array.isArray(value.slides)) {
|
|
1662
|
+
throw new Error("Invalid loop settings");
|
|
1663
|
+
}
|
|
1664
|
+
const slides = /* @__PURE__ */ Object.create(null);
|
|
1665
|
+
if (Object.keys(value.slides).length > 1e3) throw new Error("Too many slides");
|
|
1666
|
+
for (const [id, item] of Object.entries(value.slides)) {
|
|
1667
|
+
if (!id || id.length > 256 || !item || typeof item !== "object" || item.excluded !== void 0 && typeof item.excluded !== "boolean" || item.steps !== void 0 && (!Number.isInteger(item.steps) || item.steps < 0 || item.steps > 1e3) || item.dwell !== void 0 && (!Array.isArray(item.dwell) || item.dwell.length > 1001 || !item.dwell.every((duration) => duration === null || validDuration(duration))))
|
|
1668
|
+
throw new Error(`Invalid timing for ${id}`);
|
|
1669
|
+
slides[id] = {
|
|
1670
|
+
...item.excluded !== void 0 ? { excluded: item.excluded } : {},
|
|
1671
|
+
...item.steps !== void 0 ? { steps: item.steps } : {},
|
|
1672
|
+
...item.dwell !== void 0 ? { dwell: [...item.dwell] } : {}
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
return { version: 1, defaultDuration: value.defaultDuration, slides };
|
|
1676
|
+
}
|
|
1677
|
+
function validDuration(value) {
|
|
1678
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0.1 && value <= 3600;
|
|
1679
|
+
}
|
|
1680
|
+
function loopSlideKey(slide, index) {
|
|
1681
|
+
return slide.id || String(index + 1);
|
|
1682
|
+
}
|
|
1683
|
+
function stateDuration(config, slide, index, step) {
|
|
1684
|
+
return config.slides[loopSlideKey(slide, index)]?.dwell?.[step] ?? config.defaultDuration;
|
|
1685
|
+
}
|
|
1686
|
+
function includedSlides(config, slides) {
|
|
1687
|
+
return slides.flatMap(
|
|
1688
|
+
(slide, index) => config.slides[loopSlideKey(slide, index)]?.excluded ? [] : [index]
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
function loopDuration(config, slides) {
|
|
1692
|
+
return includedSlides(config, slides).reduce((total, index) => {
|
|
1693
|
+
const slide = slides[index];
|
|
1694
|
+
for (let step = 0; step <= slide.steps; step++)
|
|
1695
|
+
total += stateDuration(config, slide, index, step);
|
|
1696
|
+
return total;
|
|
1697
|
+
}, 0);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/core/loop/context.tsx
|
|
1701
|
+
import { createContext as createContext3, useContext as useContext3 } from "react";
|
|
1702
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
1703
|
+
var LoopContext = createContext3(DEFAULT_LOOP_CONFIG);
|
|
1704
|
+
function LoopConfigProvider({
|
|
1705
|
+
config,
|
|
1706
|
+
children
|
|
1707
|
+
}) {
|
|
1708
|
+
return /* @__PURE__ */ jsx13(LoopContext.Provider, { value: config, children });
|
|
1709
|
+
}
|
|
1710
|
+
var useDefaultLoopConfig = () => useContext3(LoopContext);
|
|
1711
|
+
|
|
1712
|
+
// src/core/loop/loop-settings.tsx
|
|
1713
|
+
import { Check as Check2, Download, Play, RotateCcw as RotateCcw2 } from "lucide-react";
|
|
1714
|
+
import { useId as useId2, useState as useState7 } from "react";
|
|
1715
|
+
import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1716
|
+
var button = "rounded-md border border-neutral-700 px-2.5 py-2 text-xs hover:bg-neutral-800 disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-orange-500";
|
|
1717
|
+
function DurationInput({
|
|
1718
|
+
value,
|
|
1719
|
+
label,
|
|
1720
|
+
onCommit,
|
|
1721
|
+
onFocus,
|
|
1722
|
+
id
|
|
1723
|
+
}) {
|
|
1724
|
+
return /* @__PURE__ */ jsxs7("span", { className: "inline-flex w-20 shrink-0 items-center gap-1 rounded-md border border-neutral-700 bg-neutral-950 px-2.5 focus-within:border-orange-500 focus-within:outline-2 focus-within:outline-orange-500/30", children: [
|
|
1725
|
+
/* @__PURE__ */ jsx14(
|
|
1726
|
+
"input",
|
|
1727
|
+
{
|
|
1728
|
+
id,
|
|
1729
|
+
"aria-label": label,
|
|
1730
|
+
className: "min-w-0 w-full bg-transparent py-2 text-right text-sm tabular-nums text-white outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
|
|
1731
|
+
style: { colorScheme: "dark" },
|
|
1732
|
+
type: "number",
|
|
1733
|
+
min: "0.1",
|
|
1734
|
+
max: "3600",
|
|
1735
|
+
step: "0.1",
|
|
1736
|
+
defaultValue: value,
|
|
1737
|
+
onFocus,
|
|
1738
|
+
onBlur: (event) => {
|
|
1739
|
+
const next = event.target.valueAsNumber;
|
|
1740
|
+
if (Number.isFinite(next) && next >= 0.1 && next <= 3600) {
|
|
1741
|
+
if (next !== value) onCommit(next);
|
|
1742
|
+
} else event.target.value = String(value);
|
|
1743
|
+
}
|
|
1744
|
+
},
|
|
1745
|
+
value
|
|
1746
|
+
),
|
|
1747
|
+
/* @__PURE__ */ jsx14("span", { "aria-hidden": "true", className: "text-xs text-neutral-500", children: "s" })
|
|
1748
|
+
] });
|
|
1749
|
+
}
|
|
1750
|
+
function LoopSettings({
|
|
1751
|
+
slides,
|
|
1752
|
+
currentSlide,
|
|
1753
|
+
animationStep,
|
|
1754
|
+
config,
|
|
1755
|
+
onChange,
|
|
1756
|
+
onStepChange,
|
|
1757
|
+
onSave,
|
|
1758
|
+
onStart,
|
|
1759
|
+
onRevert,
|
|
1760
|
+
dirty
|
|
1761
|
+
}) {
|
|
1762
|
+
const defaultDurationId = useId2();
|
|
1763
|
+
const [message, setMessage] = useState7("");
|
|
1764
|
+
const [saving, setSaving] = useState7(false);
|
|
1765
|
+
const slide = slides[currentSlide];
|
|
1766
|
+
const key = slide ? loopSlideKey(slide, currentSlide) : "";
|
|
1767
|
+
const settings = config.slides[key];
|
|
1768
|
+
const changedSteps = settings?.steps !== void 0 && settings.steps !== slide?.steps;
|
|
1769
|
+
const updateDuration = (step, value) => {
|
|
1770
|
+
if (!slide) return;
|
|
1771
|
+
const dwell = Array.from({ length: slide.steps + 1 }, (_, i) => settings?.dwell?.[i] ?? null);
|
|
1772
|
+
dwell[step] = value;
|
|
1773
|
+
onChange({
|
|
1774
|
+
...config,
|
|
1775
|
+
slides: { ...config.slides, [key]: { ...settings, steps: slide.steps, dwell } }
|
|
1776
|
+
});
|
|
1777
|
+
};
|
|
1778
|
+
const save = async () => {
|
|
1779
|
+
setSaving(true);
|
|
1780
|
+
try {
|
|
1781
|
+
setMessage(await onSave(validateLoopConfig(config)));
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
setMessage(error instanceof Error ? error.message : "Could not save loop settings");
|
|
1784
|
+
} finally {
|
|
1785
|
+
setSaving(false);
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
const download = () => {
|
|
1789
|
+
const url = URL.createObjectURL(
|
|
1790
|
+
new Blob([JSON.stringify(config, null, 2) + "\n"], { type: "application/json" })
|
|
1791
|
+
);
|
|
1792
|
+
const a = document.createElement("a");
|
|
1793
|
+
a.href = url;
|
|
1794
|
+
a.download = "loop-config.json";
|
|
1795
|
+
a.click();
|
|
1796
|
+
URL.revokeObjectURL(url);
|
|
1797
|
+
setMessage(
|
|
1798
|
+
"Place loop-config.json in the project folder to include these settings in builds and publishing."
|
|
1799
|
+
);
|
|
1800
|
+
};
|
|
1801
|
+
return /* @__PURE__ */ jsxs7(
|
|
1802
|
+
"section",
|
|
1803
|
+
{
|
|
1804
|
+
"aria-label": "Slide settings",
|
|
1805
|
+
className: "flex h-full min-h-0 flex-col text-sm text-white",
|
|
1806
|
+
children: [
|
|
1807
|
+
/* @__PURE__ */ jsxs7("div", { className: "min-h-0 flex-1 overflow-y-auto p-4", children: [
|
|
1808
|
+
slide && /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
1809
|
+
/* @__PURE__ */ jsxs7("p", { className: "text-[10px] font-semibold uppercase tracking-wider text-neutral-500", children: [
|
|
1810
|
+
"Slide ",
|
|
1811
|
+
currentSlide + 1
|
|
1812
|
+
] }),
|
|
1813
|
+
/* @__PURE__ */ jsx14("h2", { className: "mt-1 truncate text-base font-semibold", children: slide.title || slide.id || `Slide ${currentSlide + 1}` }),
|
|
1814
|
+
/* @__PURE__ */ jsx14("p", { className: "mt-1 text-xs text-neutral-400", children: "Select slides in the thumbnail rail. Select a state below to preview it on the canvas." }),
|
|
1815
|
+
/* @__PURE__ */ jsxs7("label", { className: "mt-5 flex items-center justify-between gap-3 border-b border-neutral-800 pb-4", children: [
|
|
1816
|
+
"Include in loop",
|
|
1817
|
+
/* @__PURE__ */ jsxs7("span", { className: "relative flex h-5 w-5 shrink-0 items-center justify-center", children: [
|
|
1818
|
+
/* @__PURE__ */ jsx14(
|
|
1819
|
+
"input",
|
|
1820
|
+
{
|
|
1821
|
+
"aria-label": "Include current slide in loop",
|
|
1822
|
+
className: "peer h-5 w-5 cursor-pointer appearance-none rounded border border-neutral-600 bg-neutral-950 transition-colors checked:border-orange-500 checked:bg-orange-500 hover:border-neutral-400 checked:hover:border-orange-400 checked:hover:bg-orange-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-500",
|
|
1823
|
+
type: "checkbox",
|
|
1824
|
+
checked: !settings?.excluded,
|
|
1825
|
+
onChange: (event) => onChange({
|
|
1826
|
+
...config,
|
|
1827
|
+
slides: {
|
|
1828
|
+
...config.slides,
|
|
1829
|
+
[key]: { ...settings, excluded: !event.target.checked }
|
|
1830
|
+
}
|
|
1831
|
+
})
|
|
1832
|
+
}
|
|
1833
|
+
),
|
|
1834
|
+
/* @__PURE__ */ jsx14(
|
|
1835
|
+
Check2,
|
|
1836
|
+
{
|
|
1837
|
+
"aria-hidden": "true",
|
|
1838
|
+
size: 14,
|
|
1839
|
+
strokeWidth: 3,
|
|
1840
|
+
className: "pointer-events-none absolute text-white opacity-0 peer-checked:opacity-100"
|
|
1841
|
+
}
|
|
1842
|
+
)
|
|
1843
|
+
] })
|
|
1844
|
+
] }),
|
|
1845
|
+
/* @__PURE__ */ jsxs7("h3", { className: "mb-2 mt-4 text-xs font-semibold text-neutral-300", children: [
|
|
1846
|
+
"State timings ",
|
|
1847
|
+
/* @__PURE__ */ jsx14("span", { className: "font-normal text-neutral-500", children: "\xB7 seconds" })
|
|
1848
|
+
] }),
|
|
1849
|
+
changedSteps && /* @__PURE__ */ jsx14("p", { role: "status", className: "mb-3 text-xs text-amber-300", children: "This slide's step count changed. Review its timings; new states use the default." }),
|
|
1850
|
+
/* @__PURE__ */ jsx14("div", { className: "space-y-1.5", "aria-label": "Slide states", children: Array.from({ length: slide.steps + 1 }, (_, step) => {
|
|
1851
|
+
const label = step === 0 ? "Initial view" : `Step ${step}`;
|
|
1852
|
+
const inherited = settings?.dwell?.[step] == null;
|
|
1853
|
+
return /* @__PURE__ */ jsxs7(
|
|
1854
|
+
"div",
|
|
1855
|
+
{
|
|
1856
|
+
className: `flex items-center gap-1 rounded-md border p-1 ${animationStep === step ? "border-orange-500/50 bg-orange-500/5" : "border-transparent"}`,
|
|
1857
|
+
children: [
|
|
1858
|
+
/* @__PURE__ */ jsxs7(
|
|
1859
|
+
"button",
|
|
1860
|
+
{
|
|
1861
|
+
"aria-label": label,
|
|
1862
|
+
"aria-pressed": animationStep === step,
|
|
1863
|
+
className: "min-w-0 flex-1 rounded px-1 py-1.5 text-left text-xs hover:bg-neutral-800",
|
|
1864
|
+
onClick: () => onStepChange(step),
|
|
1865
|
+
children: [
|
|
1866
|
+
label,
|
|
1867
|
+
/* @__PURE__ */ jsx14("span", { className: "mt-0.5 block text-[10px] text-neutral-500", children: inherited ? "Default duration" : "Custom duration" })
|
|
1868
|
+
]
|
|
1869
|
+
}
|
|
1870
|
+
),
|
|
1871
|
+
/* @__PURE__ */ jsx14(
|
|
1872
|
+
DurationInput,
|
|
1873
|
+
{
|
|
1874
|
+
value: stateDuration(config, slide, currentSlide, step),
|
|
1875
|
+
label: `${label} duration in seconds`,
|
|
1876
|
+
onFocus: () => onStepChange(step),
|
|
1877
|
+
onCommit: (value) => updateDuration(step, value)
|
|
1878
|
+
}
|
|
1879
|
+
),
|
|
1880
|
+
/* @__PURE__ */ jsx14(
|
|
1881
|
+
"button",
|
|
1882
|
+
{
|
|
1883
|
+
className: "rounded p-1 text-neutral-500 hover:text-white disabled:opacity-30",
|
|
1884
|
+
disabled: inherited,
|
|
1885
|
+
"aria-label": `Use default for ${label.toLowerCase()}`,
|
|
1886
|
+
title: "Use default duration",
|
|
1887
|
+
onClick: () => updateDuration(step, null),
|
|
1888
|
+
children: /* @__PURE__ */ jsx14(RotateCcw2, { size: 13 })
|
|
1889
|
+
}
|
|
1890
|
+
)
|
|
1891
|
+
]
|
|
1892
|
+
},
|
|
1893
|
+
`${key}:${step}`
|
|
1894
|
+
);
|
|
1895
|
+
}) }),
|
|
1896
|
+
/* @__PURE__ */ jsx14("p", { className: "mt-3 text-xs leading-relaxed text-neutral-500", children: "Durations include reveal animations. Allow enough time for videos and animations that play by themselves." }),
|
|
1897
|
+
/* @__PURE__ */ jsx14(
|
|
1898
|
+
"button",
|
|
1899
|
+
{
|
|
1900
|
+
className: `${button} mt-3 w-full`,
|
|
1901
|
+
onClick: () => {
|
|
1902
|
+
const rest = { ...settings };
|
|
1903
|
+
delete rest.dwell;
|
|
1904
|
+
delete rest.steps;
|
|
1905
|
+
onChange({ ...config, slides: { ...config.slides, [key]: rest } });
|
|
1906
|
+
},
|
|
1907
|
+
children: "Use default for this slide"
|
|
1908
|
+
}
|
|
1909
|
+
)
|
|
1910
|
+
] }),
|
|
1911
|
+
/* @__PURE__ */ jsxs7("details", { className: "mt-5 border-t border-neutral-800 pt-4", open: true, children: [
|
|
1912
|
+
/* @__PURE__ */ jsx14("summary", { className: "cursor-pointer text-xs font-semibold text-neutral-300", children: "Loop defaults" }),
|
|
1913
|
+
/* @__PURE__ */ jsxs7(
|
|
1914
|
+
"label",
|
|
1915
|
+
{
|
|
1916
|
+
htmlFor: defaultDurationId,
|
|
1917
|
+
className: "mt-3 flex items-center justify-between gap-2 text-xs text-neutral-400",
|
|
1918
|
+
children: [
|
|
1919
|
+
"Seconds per state",
|
|
1920
|
+
/* @__PURE__ */ jsx14(
|
|
1921
|
+
DurationInput,
|
|
1922
|
+
{
|
|
1923
|
+
id: defaultDurationId,
|
|
1924
|
+
value: config.defaultDuration,
|
|
1925
|
+
label: "Default duration in seconds",
|
|
1926
|
+
onCommit: (value) => onChange({ ...config, defaultDuration: value })
|
|
1927
|
+
}
|
|
1928
|
+
)
|
|
1929
|
+
]
|
|
1930
|
+
}
|
|
1931
|
+
),
|
|
1932
|
+
/* @__PURE__ */ jsxs7("p", { className: "mt-3 text-xs text-neutral-500", children: [
|
|
1933
|
+
includedSlides(config, slides).length,
|
|
1934
|
+
" slides \xB7",
|
|
1935
|
+
" ",
|
|
1936
|
+
loopDuration(config, slides).toFixed(1),
|
|
1937
|
+
"s + slide transitions"
|
|
1938
|
+
] })
|
|
1939
|
+
] })
|
|
1940
|
+
] }),
|
|
1941
|
+
/* @__PURE__ */ jsxs7("footer", { className: "shrink-0 space-y-2 border-t border-neutral-800 p-3", children: [
|
|
1942
|
+
/* @__PURE__ */ jsx14("p", { role: "status", className: "text-xs text-neutral-400", children: dirty ? "Unsaved loop settings" : message || "Loop settings saved" }),
|
|
1943
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex gap-2", children: [
|
|
1944
|
+
/* @__PURE__ */ jsx14(
|
|
1945
|
+
"button",
|
|
1946
|
+
{
|
|
1947
|
+
className: `${button} flex-1`,
|
|
1948
|
+
disabled: !dirty || saving,
|
|
1949
|
+
onClick: () => {
|
|
1950
|
+
onRevert();
|
|
1951
|
+
setMessage("");
|
|
1952
|
+
},
|
|
1953
|
+
children: "Revert"
|
|
1954
|
+
}
|
|
1955
|
+
),
|
|
1956
|
+
/* @__PURE__ */ jsx14(
|
|
1957
|
+
"button",
|
|
1958
|
+
{
|
|
1959
|
+
className: `${button} flex-1 border-orange-500/50 text-orange-400`,
|
|
1960
|
+
disabled: saving,
|
|
1961
|
+
onClick: () => void save(),
|
|
1962
|
+
children: saving ? "Saving\u2026" : "Save settings"
|
|
1963
|
+
}
|
|
1964
|
+
)
|
|
1965
|
+
] }),
|
|
1966
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex gap-2", children: [
|
|
1967
|
+
/* @__PURE__ */ jsx14(
|
|
1968
|
+
"button",
|
|
1969
|
+
{
|
|
1970
|
+
className: `${button} flex items-center justify-center gap-1.5`,
|
|
1971
|
+
"aria-label": "Download loop settings",
|
|
1972
|
+
onClick: download,
|
|
1973
|
+
children: /* @__PURE__ */ jsx14(Download, { size: 14 })
|
|
1974
|
+
}
|
|
1975
|
+
),
|
|
1976
|
+
/* @__PURE__ */ jsxs7(
|
|
1977
|
+
"button",
|
|
1978
|
+
{
|
|
1979
|
+
className: `${button} flex flex-1 items-center justify-center gap-1.5`,
|
|
1980
|
+
disabled: !includedSlides(config, slides).length || saving,
|
|
1981
|
+
onClick: () => onStart(validateLoopConfig(config)),
|
|
1982
|
+
children: [
|
|
1983
|
+
/* @__PURE__ */ jsx14(Play, { size: 14 }),
|
|
1984
|
+
" Start loop"
|
|
1985
|
+
]
|
|
1986
|
+
}
|
|
1987
|
+
)
|
|
1988
|
+
] })
|
|
1989
|
+
] })
|
|
1990
|
+
]
|
|
1991
|
+
}
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// src/core/loop/use-loop-playback.ts
|
|
1996
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef6, useState as useState8 } from "react";
|
|
1997
|
+
function useLoopPlayback({
|
|
1998
|
+
config,
|
|
1999
|
+
slides,
|
|
2000
|
+
currentSlide,
|
|
2001
|
+
animationStep,
|
|
2002
|
+
isTransitioning,
|
|
2003
|
+
enabled,
|
|
2004
|
+
advance,
|
|
2005
|
+
goBack,
|
|
2006
|
+
restartAtSlide
|
|
2007
|
+
}) {
|
|
2008
|
+
const [paused, setPaused] = useState8(false);
|
|
2009
|
+
const [hidden, setHidden] = useState8(() => typeof document !== "undefined" && document.hidden);
|
|
2010
|
+
const [epoch, setEpoch] = useState8(0);
|
|
2011
|
+
const [revision, setRevision] = useState8(0);
|
|
2012
|
+
const remaining = useRef6(void 0);
|
|
2013
|
+
const identity = useRef6("");
|
|
2014
|
+
const included = useMemo4(() => includedSlides(config, slides), [config, slides]);
|
|
2015
|
+
useEffect7(() => {
|
|
2016
|
+
const onVisibility = () => setHidden(document.hidden);
|
|
2017
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
2018
|
+
return () => document.removeEventListener("visibilitychange", onVisibility);
|
|
2019
|
+
}, []);
|
|
2020
|
+
const restart = useCallback5(
|
|
2021
|
+
(index) => {
|
|
2022
|
+
setEpoch((value) => value + 1);
|
|
2023
|
+
remaining.current = void 0;
|
|
2024
|
+
restartAtSlide(index);
|
|
2025
|
+
},
|
|
2026
|
+
[restartAtSlide]
|
|
2027
|
+
);
|
|
2028
|
+
const resetTimer = useCallback5(() => {
|
|
2029
|
+
remaining.current = void 0;
|
|
2030
|
+
setRevision((value) => value + 1);
|
|
2031
|
+
}, []);
|
|
2032
|
+
const move = (direction) => {
|
|
2033
|
+
resetTimer();
|
|
2034
|
+
if (isTransitioning || included.length === 0) return;
|
|
2035
|
+
if (direction === 1 && animationStep < (slides[currentSlide]?.steps ?? 0)) advance();
|
|
2036
|
+
else if (direction === -1 && animationStep > 0) goBack();
|
|
2037
|
+
else {
|
|
2038
|
+
const position = included.indexOf(currentSlide);
|
|
2039
|
+
restart(included[(position + direction + included.length) % included.length]);
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
useEffect7(() => {
|
|
2043
|
+
if (!enabled) {
|
|
2044
|
+
remaining.current = void 0;
|
|
2045
|
+
setPaused(false);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
const key = `${currentSlide}:${animationStep}:${epoch}:${revision}`;
|
|
2049
|
+
if (identity.current !== key) {
|
|
2050
|
+
identity.current = key;
|
|
2051
|
+
remaining.current = void 0;
|
|
2052
|
+
}
|
|
2053
|
+
if (paused || hidden || isTransitioning || included.length === 0) return;
|
|
2054
|
+
const slide = slides[currentSlide];
|
|
2055
|
+
if (!slide) return;
|
|
2056
|
+
const duration = remaining.current ?? stateDuration(config, slide, currentSlide, animationStep) * 1e3;
|
|
2057
|
+
const deadline = performance.now() + duration;
|
|
2058
|
+
const timer = window.setTimeout(() => {
|
|
2059
|
+
remaining.current = void 0;
|
|
2060
|
+
if (animationStep < slide.steps) advance();
|
|
2061
|
+
else {
|
|
2062
|
+
const position = included.indexOf(currentSlide);
|
|
2063
|
+
restart(included[(position + 1) % included.length]);
|
|
2064
|
+
}
|
|
2065
|
+
}, duration);
|
|
2066
|
+
return () => {
|
|
2067
|
+
window.clearTimeout(timer);
|
|
2068
|
+
remaining.current = Math.max(0, deadline - performance.now());
|
|
2069
|
+
};
|
|
2070
|
+
}, [
|
|
2071
|
+
enabled,
|
|
2072
|
+
paused,
|
|
2073
|
+
hidden,
|
|
2074
|
+
isTransitioning,
|
|
2075
|
+
currentSlide,
|
|
2076
|
+
animationStep,
|
|
2077
|
+
epoch,
|
|
2078
|
+
revision,
|
|
2079
|
+
config,
|
|
2080
|
+
slides,
|
|
2081
|
+
advance,
|
|
2082
|
+
restart,
|
|
2083
|
+
included
|
|
2084
|
+
]);
|
|
2085
|
+
return { paused, setPaused, epoch, restart, move };
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/core/toolbar-action.tsx
|
|
2089
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
2090
|
+
function ToolbarAction({
|
|
2091
|
+
label,
|
|
2092
|
+
selected,
|
|
2093
|
+
title = label,
|
|
2094
|
+
children,
|
|
2095
|
+
...props
|
|
2096
|
+
}) {
|
|
2097
|
+
return /* @__PURE__ */ jsx15(
|
|
2098
|
+
"button",
|
|
2099
|
+
{
|
|
2100
|
+
...props,
|
|
2101
|
+
type: "button",
|
|
2102
|
+
"aria-label": label,
|
|
2103
|
+
"aria-pressed": selected,
|
|
2104
|
+
title,
|
|
2105
|
+
className: cn(
|
|
2106
|
+
"relative inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-transparent transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#FF6B35] disabled:cursor-default disabled:opacity-35 [&>svg]:h-4 [&>svg]:w-4 [&>svg]:shrink-0",
|
|
2107
|
+
selected ? "border-[#FF6B35]/50 bg-[#FF6B35]/15 text-[#FF6B35] enabled:hover:border-[#FF6B35] enabled:hover:bg-[#FF6B35]/25" : "text-neutral-400 enabled:hover:bg-neutral-800 enabled:hover:text-white"
|
|
2108
|
+
),
|
|
2109
|
+
children
|
|
2110
|
+
}
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// src/core/view-menu.tsx
|
|
2115
|
+
import { Check as Check3, ChevronDown, Grid3X3, List, Monitor } from "lucide-react";
|
|
2116
|
+
import { useEffect as useEffect8, useId as useId3, useLayoutEffect, useRef as useRef7, useState as useState9 } from "react";
|
|
2117
|
+
import { jsx as jsx16, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2118
|
+
var views = [
|
|
2119
|
+
{ mode: "slide", label: "Presentation view", shortcut: "V", icon: Monitor },
|
|
2120
|
+
{ mode: "list", label: "List view", shortcut: "L", icon: List },
|
|
2121
|
+
{ mode: "grid", label: "Grid view", shortcut: "G", icon: Grid3X3 }
|
|
2122
|
+
];
|
|
2123
|
+
function ViewMenu({
|
|
2124
|
+
value,
|
|
2125
|
+
onChange,
|
|
2126
|
+
disabled
|
|
2127
|
+
}) {
|
|
2128
|
+
const [open, setOpen] = useState9(false);
|
|
2129
|
+
const id = useId3();
|
|
2130
|
+
const root = useRef7(null);
|
|
2131
|
+
const trigger = useRef7(null);
|
|
2132
|
+
const active = views.find((view) => view.mode === value);
|
|
2133
|
+
const Icon = active.icon;
|
|
2134
|
+
useLayoutEffect(() => {
|
|
2135
|
+
if (open) root.current?.querySelector('[aria-checked="true"]')?.focus();
|
|
2136
|
+
}, [open]);
|
|
2137
|
+
useEffect8(() => {
|
|
2138
|
+
if (!open) return;
|
|
2139
|
+
const close = (event) => {
|
|
2140
|
+
if (!root.current?.contains(event.target)) setOpen(false);
|
|
2141
|
+
};
|
|
2142
|
+
document.addEventListener("pointerdown", close);
|
|
2143
|
+
return () => document.removeEventListener("pointerdown", close);
|
|
2144
|
+
}, [open]);
|
|
2145
|
+
return /* @__PURE__ */ jsxs8(
|
|
2146
|
+
"div",
|
|
2147
|
+
{
|
|
2148
|
+
role: "group",
|
|
2149
|
+
"aria-label": "View selector",
|
|
2150
|
+
ref: root,
|
|
2151
|
+
className: "relative flex",
|
|
2152
|
+
onBlur: (event) => {
|
|
2153
|
+
const next = event.relatedTarget;
|
|
2154
|
+
if (next && !event.currentTarget.contains(next)) setOpen(false);
|
|
2155
|
+
},
|
|
2156
|
+
onKeyDown: (event) => {
|
|
2157
|
+
event.stopPropagation();
|
|
2158
|
+
if (event.key === "Escape") {
|
|
2159
|
+
setOpen(false);
|
|
2160
|
+
trigger.current?.focus();
|
|
2161
|
+
} else if (open && event.key === "Tab") {
|
|
2162
|
+
trigger.current?.focus();
|
|
2163
|
+
setOpen(false);
|
|
2164
|
+
} else if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
|
|
2165
|
+
event.preventDefault();
|
|
2166
|
+
if (!open) {
|
|
2167
|
+
setOpen(true);
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
const items = Array.from(
|
|
2171
|
+
root.current.querySelectorAll('[role="menuitemradio"]')
|
|
2172
|
+
);
|
|
2173
|
+
const index = items.indexOf(document.activeElement);
|
|
2174
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length;
|
|
2175
|
+
items[next]?.focus();
|
|
2176
|
+
}
|
|
2177
|
+
},
|
|
2178
|
+
children: [
|
|
2179
|
+
/* @__PURE__ */ jsxs8(
|
|
2180
|
+
"button",
|
|
2181
|
+
{
|
|
2182
|
+
ref: trigger,
|
|
2183
|
+
disabled,
|
|
2184
|
+
"aria-label": "View options",
|
|
2185
|
+
title: `View options \xB7 ${active.label}`,
|
|
2186
|
+
"aria-haspopup": "menu",
|
|
2187
|
+
"aria-expanded": open,
|
|
2188
|
+
"aria-controls": open ? id : void 0,
|
|
2189
|
+
className: "inline-flex h-8 items-center gap-1.5 rounded-md bg-neutral-800 px-2 text-white hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-orange-500",
|
|
2190
|
+
onClick: () => setOpen((previous) => !previous),
|
|
2191
|
+
children: [
|
|
2192
|
+
/* @__PURE__ */ jsx16(Icon, { size: 16 }),
|
|
2193
|
+
/* @__PURE__ */ jsx16(ChevronDown, { size: 12 })
|
|
2194
|
+
]
|
|
2195
|
+
}
|
|
2196
|
+
),
|
|
2197
|
+
open && /* @__PURE__ */ jsx16(
|
|
2198
|
+
"div",
|
|
2199
|
+
{
|
|
2200
|
+
id,
|
|
2201
|
+
role: "menu",
|
|
2202
|
+
"aria-label": "View options",
|
|
2203
|
+
className: "absolute left-0 top-10 w-56 rounded-lg border border-neutral-700 bg-neutral-950 p-1 text-sm text-white",
|
|
2204
|
+
children: views.map(({ mode, label, shortcut, icon: OptionIcon }) => /* @__PURE__ */ jsxs8(
|
|
2205
|
+
"button",
|
|
2206
|
+
{
|
|
2207
|
+
role: "menuitemradio",
|
|
2208
|
+
"aria-checked": mode === value,
|
|
2209
|
+
tabIndex: -1,
|
|
2210
|
+
className: cn(
|
|
2211
|
+
"flex w-full items-center gap-2 rounded px-2 py-2 text-left text-neutral-300 hover:bg-neutral-800 focus:bg-neutral-800 focus:outline-none",
|
|
2212
|
+
mode === value && "text-white"
|
|
2213
|
+
),
|
|
2214
|
+
onClick: () => {
|
|
2215
|
+
onChange(mode);
|
|
2216
|
+
setOpen(false);
|
|
2217
|
+
trigger.current?.focus();
|
|
2218
|
+
},
|
|
2219
|
+
children: [
|
|
2220
|
+
/* @__PURE__ */ jsx16(OptionIcon, { size: 16 }),
|
|
2221
|
+
/* @__PURE__ */ jsx16("span", { className: "flex-1", children: label }),
|
|
2222
|
+
/* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "text-xs text-neutral-500", children: shortcut }),
|
|
2223
|
+
/* @__PURE__ */ jsx16(Check3, { size: 14, className: mode === value ? "text-orange-400" : "invisible" })
|
|
2224
|
+
]
|
|
2225
|
+
},
|
|
2226
|
+
mode
|
|
2227
|
+
))
|
|
2228
|
+
}
|
|
2229
|
+
)
|
|
2230
|
+
]
|
|
2231
|
+
}
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
1478
2235
|
// src/core/slide-deck.tsx
|
|
1479
|
-
import { Fragment as
|
|
2236
|
+
import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1480
2237
|
var NARROW_VIEWPORT_WIDTH = 768;
|
|
1481
2238
|
var DEFAULT_THUMBNAIL_WIDTH = 240;
|
|
1482
2239
|
var MIN_THUMBNAIL_WIDTH = 176;
|
|
1483
2240
|
var MAX_THUMBNAIL_WIDTH = 480;
|
|
1484
2241
|
var THUMBNAIL_WIDTH_STORAGE_KEY = "promptslide:thumbnail-width";
|
|
2242
|
+
function loadLoopConfig(base, storageKey) {
|
|
2243
|
+
let fallback = DEFAULT_LOOP_CONFIG;
|
|
2244
|
+
try {
|
|
2245
|
+
fallback = validateLoopConfig(base);
|
|
2246
|
+
} catch {
|
|
2247
|
+
}
|
|
2248
|
+
try {
|
|
2249
|
+
const saved = JSON.parse(localStorage.getItem(storageKey) || "null");
|
|
2250
|
+
if (saved?.base === JSON.stringify(base)) return validateLoopConfig(saved.config);
|
|
2251
|
+
} catch {
|
|
2252
|
+
}
|
|
2253
|
+
return fallback;
|
|
2254
|
+
}
|
|
1485
2255
|
function GridThumbnail({
|
|
1486
2256
|
slide,
|
|
1487
2257
|
index,
|
|
1488
2258
|
total
|
|
1489
2259
|
}) {
|
|
1490
|
-
const viewportRef =
|
|
1491
|
-
const canvasRef =
|
|
2260
|
+
const viewportRef = useRef8(null);
|
|
2261
|
+
const canvasRef = useRef8(null);
|
|
1492
2262
|
const SlideComponent = slide.component;
|
|
1493
|
-
|
|
2263
|
+
useLayoutEffect2(() => {
|
|
1494
2264
|
const viewport = viewportRef.current;
|
|
1495
2265
|
const canvas = canvasRef.current;
|
|
1496
2266
|
if (!viewport || !canvas) return;
|
|
@@ -1502,21 +2272,21 @@ function GridThumbnail({
|
|
|
1502
2272
|
observer.observe(viewport);
|
|
1503
2273
|
return () => observer.disconnect();
|
|
1504
2274
|
}, []);
|
|
1505
|
-
return /* @__PURE__ */
|
|
2275
|
+
return /* @__PURE__ */ jsx17("div", { ref: viewportRef, className: "absolute inset-0 overflow-hidden", children: /* @__PURE__ */ jsx17(
|
|
1506
2276
|
"div",
|
|
1507
2277
|
{
|
|
1508
2278
|
ref: canvasRef,
|
|
1509
2279
|
className: "absolute top-0 left-0 origin-top-left",
|
|
1510
2280
|
style: { width: SLIDE_DIMENSIONS.width, height: SLIDE_DIMENSIONS.height },
|
|
1511
|
-
children: /* @__PURE__ */
|
|
2281
|
+
children: /* @__PURE__ */ jsx17(AnimationProvider, { currentStep: slide.steps, totalSteps: slide.steps, showAllAnimations: true, children: /* @__PURE__ */ jsx17(SlideErrorBoundary, { slideIndex: index, slideTitle: slide.title, children: /* @__PURE__ */ jsx17(SlideComponent, { slideNumber: index + 1, totalSlides: total }) }) })
|
|
1512
2282
|
}
|
|
1513
2283
|
) });
|
|
1514
2284
|
}
|
|
1515
2285
|
function ListSlide({ slide, index, total }) {
|
|
1516
|
-
const viewportRef =
|
|
1517
|
-
const canvasRef =
|
|
2286
|
+
const viewportRef = useRef8(null);
|
|
2287
|
+
const canvasRef = useRef8(null);
|
|
1518
2288
|
const SlideComponent = slide.component;
|
|
1519
|
-
|
|
2289
|
+
useLayoutEffect2(() => {
|
|
1520
2290
|
const viewport = viewportRef.current;
|
|
1521
2291
|
const canvas = canvasRef.current;
|
|
1522
2292
|
if (!viewport || !canvas) return;
|
|
@@ -1528,29 +2298,29 @@ function ListSlide({ slide, index, total }) {
|
|
|
1528
2298
|
observer.observe(viewport);
|
|
1529
2299
|
return () => observer.disconnect();
|
|
1530
2300
|
}, []);
|
|
1531
|
-
return /* @__PURE__ */
|
|
2301
|
+
return /* @__PURE__ */ jsx17(
|
|
1532
2302
|
"div",
|
|
1533
2303
|
{
|
|
1534
2304
|
ref: viewportRef,
|
|
1535
2305
|
className: "relative aspect-video w-full overflow-hidden rounded-xl border border-neutral-800 bg-black shadow-sm print:m-0 print:h-[1080px] print:w-[1920px] print:break-after-page print:rounded-none print:border-0 print:shadow-none",
|
|
1536
|
-
children: /* @__PURE__ */
|
|
2306
|
+
children: /* @__PURE__ */ jsx17(
|
|
1537
2307
|
"div",
|
|
1538
2308
|
{
|
|
1539
2309
|
ref: canvasRef,
|
|
1540
2310
|
className: "list-slide-canvas absolute top-0 left-0 origin-top-left",
|
|
1541
2311
|
style: { width: SLIDE_DIMENSIONS.width, height: SLIDE_DIMENSIONS.height },
|
|
1542
|
-
children: /* @__PURE__ */
|
|
2312
|
+
children: /* @__PURE__ */ jsx17(AnimationProvider, { currentStep: slide.steps, totalSteps: slide.steps, showAllAnimations: true, children: /* @__PURE__ */ jsx17(SlideErrorBoundary, { slideIndex: index, slideTitle: slide.title, children: /* @__PURE__ */ jsx17(SlideComponent, { slideNumber: index + 1, totalSlides: total }) }) })
|
|
1543
2313
|
}
|
|
1544
2314
|
)
|
|
1545
2315
|
}
|
|
1546
2316
|
);
|
|
1547
2317
|
}
|
|
1548
2318
|
function SlideExportView({ slides, slideIndex }) {
|
|
1549
|
-
const [ready, setReady] =
|
|
2319
|
+
const [ready, setReady] = useState10(false);
|
|
1550
2320
|
const clampedIndex = Math.max(0, Math.min(slideIndex, slides.length - 1));
|
|
1551
2321
|
const slideConfig = slides[clampedIndex];
|
|
1552
2322
|
const SlideComponent = slideConfig.component;
|
|
1553
|
-
|
|
2323
|
+
useEffect9(() => {
|
|
1554
2324
|
let cancelled = false;
|
|
1555
2325
|
const markReady = async () => {
|
|
1556
2326
|
await document.fonts.ready;
|
|
@@ -1567,7 +2337,7 @@ function SlideExportView({ slides, slideIndex }) {
|
|
|
1567
2337
|
cancelled = true;
|
|
1568
2338
|
};
|
|
1569
2339
|
}, []);
|
|
1570
|
-
return /* @__PURE__ */
|
|
2340
|
+
return /* @__PURE__ */ jsx17(
|
|
1571
2341
|
"div",
|
|
1572
2342
|
{
|
|
1573
2343
|
"data-export-ready": ready ? "true" : void 0,
|
|
@@ -1578,13 +2348,13 @@ function SlideExportView({ slides, slideIndex }) {
|
|
|
1578
2348
|
position: "relative",
|
|
1579
2349
|
background: "black"
|
|
1580
2350
|
},
|
|
1581
|
-
children: /* @__PURE__ */
|
|
2351
|
+
children: /* @__PURE__ */ jsx17(
|
|
1582
2352
|
AnimationProvider,
|
|
1583
2353
|
{
|
|
1584
2354
|
currentStep: slideConfig.steps,
|
|
1585
2355
|
totalSteps: slideConfig.steps,
|
|
1586
2356
|
showAllAnimations: true,
|
|
1587
|
-
children: /* @__PURE__ */
|
|
2357
|
+
children: /* @__PURE__ */ jsx17(SlideErrorBoundary, { slideIndex: clampedIndex, slideTitle: slideConfig.title, children: /* @__PURE__ */ jsx17(SlideComponent, { slideNumber: clampedIndex + 1, totalSlides: slides.length }) })
|
|
1588
2358
|
}
|
|
1589
2359
|
)
|
|
1590
2360
|
}
|
|
@@ -1594,19 +2364,21 @@ function SlideDeck({
|
|
|
1594
2364
|
slides,
|
|
1595
2365
|
transition,
|
|
1596
2366
|
directionalTransition,
|
|
2367
|
+
loopConfig: suppliedLoopConfig,
|
|
2368
|
+
onLoopConfigChange,
|
|
1597
2369
|
annotations,
|
|
1598
2370
|
onAnnotationAdd,
|
|
1599
2371
|
onAnnotationDelete,
|
|
1600
2372
|
onAnnotationUpdate
|
|
1601
2373
|
}) {
|
|
1602
|
-
const [exportParams] =
|
|
2374
|
+
const [exportParams] = useState10(() => {
|
|
1603
2375
|
if (typeof window === "undefined") return null;
|
|
1604
2376
|
const params = new URLSearchParams(window.location.search);
|
|
1605
2377
|
if (params.get("export") !== "true") return null;
|
|
1606
2378
|
return { slideIndex: parseInt(params.get("slide") || "0", 10) };
|
|
1607
2379
|
});
|
|
1608
2380
|
if (exportParams) {
|
|
1609
|
-
return /* @__PURE__ */
|
|
2381
|
+
return /* @__PURE__ */ jsx17(SlideExportView, { slides, slideIndex: exportParams.slideIndex });
|
|
1610
2382
|
}
|
|
1611
2383
|
const internal = useAnnotations(slides);
|
|
1612
2384
|
const isExternallyManaged = annotations !== void 0;
|
|
@@ -1614,101 +2386,179 @@ function SlideDeck({
|
|
|
1614
2386
|
const effectiveAdd = isExternallyManaged ? onAnnotationAdd : internal.addAnnotation;
|
|
1615
2387
|
const effectiveDelete = isExternallyManaged ? onAnnotationDelete : internal.deleteAnnotation;
|
|
1616
2388
|
const effectiveUpdate = isExternallyManaged ? onAnnotationUpdate : internal.updateAnnotation;
|
|
1617
|
-
const openCount =
|
|
2389
|
+
const openCount = useMemo5(
|
|
1618
2390
|
() => effectiveAnnotations.filter((a) => a.status === "open").length,
|
|
1619
2391
|
[effectiveAnnotations]
|
|
1620
2392
|
);
|
|
1621
|
-
const getSlideAnnotations =
|
|
2393
|
+
const getSlideAnnotations = useCallback6(
|
|
1622
2394
|
(slideIndex) => effectiveAnnotations.filter(
|
|
1623
2395
|
(a) => a.slideId ? a.slideId === slides[slideIndex]?.id : a.slideIndex === slideIndex
|
|
1624
2396
|
),
|
|
1625
2397
|
[effectiveAnnotations, slides]
|
|
1626
2398
|
);
|
|
1627
|
-
const unlinkedAnnotations =
|
|
2399
|
+
const unlinkedAnnotations = useMemo5(
|
|
1628
2400
|
() => effectiveAnnotations.filter((a) => a.slideId && !slides.some((slide) => slide.id === a.slideId)),
|
|
1629
2401
|
[effectiveAnnotations, slides]
|
|
1630
2402
|
);
|
|
1631
|
-
const [viewMode, setViewMode] =
|
|
1632
|
-
const [isEditorOpen, setIsEditorOpen] =
|
|
1633
|
-
const
|
|
1634
|
-
|
|
2403
|
+
const [viewMode, setViewMode] = useState10("slide");
|
|
2404
|
+
const [isEditorOpen, setIsEditorOpen] = useState10(false);
|
|
2405
|
+
const editorOpenRef = useRef8(false);
|
|
2406
|
+
const [editorAvailable, setEditorAvailable] = useState10(false);
|
|
2407
|
+
const [inspectorMode, setInspectorMode] = useState10(null);
|
|
2408
|
+
const pendingInspector = useRef8(void 0);
|
|
2409
|
+
const [editorRevision, setEditorRevision] = useState10(0);
|
|
2410
|
+
const activateInspector = useCallback6((mode) => {
|
|
2411
|
+
setInspectorMode(mode);
|
|
2412
|
+
setLoopMenuOpen(false);
|
|
2413
|
+
if (mode) setViewMode("slide");
|
|
2414
|
+
if (mode !== "comments") {
|
|
2415
|
+
setSelectedAnnotationId(null);
|
|
2416
|
+
setHoveredAnnotationId(null);
|
|
2417
|
+
}
|
|
2418
|
+
}, []);
|
|
2419
|
+
const requestInspector = useCallback6(
|
|
2420
|
+
(mode) => {
|
|
2421
|
+
if (isEditorOpen) {
|
|
2422
|
+
if (mode === "editor") return;
|
|
2423
|
+
pendingInspector.current = mode;
|
|
2424
|
+
window.dispatchEvent(
|
|
2425
|
+
new CustomEvent("promptslide:editor-request", { detail: { open: false } })
|
|
2426
|
+
);
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
if (mode === "editor") {
|
|
2430
|
+
if (editorAvailable)
|
|
2431
|
+
window.dispatchEvent(
|
|
2432
|
+
new CustomEvent("promptslide:editor-request", { detail: { open: true } })
|
|
2433
|
+
);
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
activateInspector(mode);
|
|
2437
|
+
},
|
|
2438
|
+
[isEditorOpen, editorAvailable, activateInspector]
|
|
2439
|
+
);
|
|
2440
|
+
useEffect9(() => {
|
|
1635
2441
|
const onEditorState = (event) => {
|
|
1636
2442
|
const open = Boolean(event.detail?.open);
|
|
2443
|
+
const changed = editorOpenRef.current !== open;
|
|
2444
|
+
editorOpenRef.current = open;
|
|
1637
2445
|
setIsEditorOpen(open);
|
|
1638
2446
|
if (open) {
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2447
|
+
if (changed) pendingInspector.current = void 0;
|
|
2448
|
+
activateInspector("editor");
|
|
2449
|
+
} else if (changed) {
|
|
2450
|
+
const next = pendingInspector.current;
|
|
2451
|
+
pendingInspector.current = void 0;
|
|
2452
|
+
activateInspector(next === void 0 ? null : next);
|
|
1642
2453
|
}
|
|
1643
2454
|
};
|
|
2455
|
+
const onEditorReady = (event) => setEditorAvailable(Boolean(event.detail?.available));
|
|
2456
|
+
const onCloseCancelled = () => {
|
|
2457
|
+
pendingInspector.current = void 0;
|
|
2458
|
+
};
|
|
1644
2459
|
const refreshEditor = () => setEditorRevision((value) => value + 1);
|
|
1645
2460
|
window.addEventListener("promptslide:editor-state", onEditorState);
|
|
2461
|
+
window.addEventListener("promptslide:editor-ready", onEditorReady);
|
|
2462
|
+
window.addEventListener("promptslide:editor-close-cancelled", onCloseCancelled);
|
|
1646
2463
|
window.addEventListener("promptslide:editor-refresh", refreshEditor);
|
|
1647
2464
|
return () => {
|
|
1648
2465
|
window.removeEventListener("promptslide:editor-state", onEditorState);
|
|
2466
|
+
window.removeEventListener("promptslide:editor-ready", onEditorReady);
|
|
2467
|
+
window.removeEventListener("promptslide:editor-close-cancelled", onCloseCancelled);
|
|
1649
2468
|
window.removeEventListener("promptslide:editor-refresh", refreshEditor);
|
|
1650
2469
|
};
|
|
1651
|
-
}, []);
|
|
1652
|
-
const [isPresentationMode, setIsPresentationMode] =
|
|
1653
|
-
const
|
|
2470
|
+
}, [activateInspector]);
|
|
2471
|
+
const [isPresentationMode, setIsPresentationMode] = useState10(false);
|
|
2472
|
+
const providedLoopConfig = useDefaultLoopConfig();
|
|
2473
|
+
const defaultLoopKey = JSON.stringify(suppliedLoopConfig ?? providedLoopConfig);
|
|
2474
|
+
const defaultLoopConfig = useMemo5(() => JSON.parse(defaultLoopKey), [defaultLoopKey]);
|
|
2475
|
+
const storageKey = `promptslide:loop:${typeof location === "undefined" ? "" : location.pathname}:${JSON.stringify(slides.map(loopSlideKey))}`;
|
|
2476
|
+
const [loopConfig, setLoopConfig] = useState10(
|
|
2477
|
+
() => loadLoopConfig(defaultLoopConfig, storageKey)
|
|
2478
|
+
);
|
|
2479
|
+
const [loopDraft, setLoopDraft] = useState10(
|
|
2480
|
+
() => loadLoopConfig(defaultLoopConfig, storageKey)
|
|
2481
|
+
);
|
|
2482
|
+
const [playbackConfig, setPlaybackConfig] = useState10(loopConfig);
|
|
2483
|
+
const [isLooping, setIsLooping] = useState10(false);
|
|
2484
|
+
const [loopMenuOpen, setLoopMenuOpen] = useState10(false);
|
|
2485
|
+
const [loopError, setLoopError] = useState10("");
|
|
2486
|
+
const [loopStartScreen, setLoopStartScreen] = useState10(
|
|
2487
|
+
() => typeof window !== "undefined" && new URLSearchParams(window.location.search).has("loop")
|
|
2488
|
+
);
|
|
2489
|
+
const loopMenuRef = useRef8(null);
|
|
2490
|
+
useEffect9(() => {
|
|
2491
|
+
const config = loadLoopConfig(defaultLoopConfig, storageKey);
|
|
2492
|
+
setLoopConfig(config);
|
|
2493
|
+
setLoopDraft(config);
|
|
2494
|
+
setPlaybackConfig(config);
|
|
2495
|
+
setIsLooping(false);
|
|
2496
|
+
}, [defaultLoopConfig, storageKey]);
|
|
2497
|
+
useEffect9(() => {
|
|
2498
|
+
if (!loopMenuOpen) return;
|
|
2499
|
+
const close = (event) => {
|
|
2500
|
+
if (!loopMenuRef.current?.contains(event.target)) setLoopMenuOpen(false);
|
|
2501
|
+
};
|
|
2502
|
+
document.addEventListener("pointerdown", close);
|
|
2503
|
+
return () => document.removeEventListener("pointerdown", close);
|
|
2504
|
+
}, [loopMenuOpen]);
|
|
2505
|
+
const [viewportWidth, setViewportWidth] = useState10(
|
|
1654
2506
|
() => typeof window === "undefined" ? 1280 : window.innerWidth
|
|
1655
2507
|
);
|
|
1656
|
-
const [showThumbnailRail, setShowThumbnailRail] =
|
|
2508
|
+
const [showThumbnailRail, setShowThumbnailRail] = useState10(
|
|
1657
2509
|
() => typeof window === "undefined" ? true : window.innerWidth >= NARROW_VIEWPORT_WIDTH
|
|
1658
2510
|
);
|
|
1659
|
-
const [thumbnailWidth, setThumbnailWidth] =
|
|
2511
|
+
const [thumbnailWidth, setThumbnailWidth] = useState10(() => {
|
|
1660
2512
|
if (typeof window === "undefined") return DEFAULT_THUMBNAIL_WIDTH;
|
|
1661
2513
|
const savedWidth = Number(window.localStorage.getItem(THUMBNAIL_WIDTH_STORAGE_KEY));
|
|
1662
2514
|
return Number.isFinite(savedWidth) && savedWidth >= MIN_THUMBNAIL_WIDTH ? Math.min(savedWidth, MAX_THUMBNAIL_WIDTH) : DEFAULT_THUMBNAIL_WIDTH;
|
|
1663
2515
|
});
|
|
1664
|
-
const
|
|
1665
|
-
const [
|
|
1666
|
-
const [
|
|
1667
|
-
const [
|
|
1668
|
-
const [
|
|
1669
|
-
const [
|
|
1670
|
-
const [
|
|
1671
|
-
const
|
|
1672
|
-
const
|
|
1673
|
-
const
|
|
1674
|
-
const
|
|
1675
|
-
const
|
|
1676
|
-
const
|
|
1677
|
-
const
|
|
1678
|
-
const
|
|
1679
|
-
const
|
|
1680
|
-
const
|
|
1681
|
-
const
|
|
1682
|
-
const dragAnimationTimerRef = useRef6(null);
|
|
2516
|
+
const isAnnotationMode = inspectorMode === "comments";
|
|
2517
|
+
const [selectedAnnotationId, setSelectedAnnotationId] = useState10(null);
|
|
2518
|
+
const [hoveredAnnotationId, setHoveredAnnotationId] = useState10(null);
|
|
2519
|
+
const [isResizingThumbnails, setIsResizingThumbnails] = useState10(false);
|
|
2520
|
+
const [isDragCollapsed, setIsDragCollapsed] = useState10(false);
|
|
2521
|
+
const [isAnimatingDragBoundary, setIsAnimatingDragBoundary] = useState10(false);
|
|
2522
|
+
const [scale, setScale] = useState10(1);
|
|
2523
|
+
const containerRef = useRef8(null);
|
|
2524
|
+
const slideContainerRef = useRef8(null);
|
|
2525
|
+
const previewViewportRef = useRef8(null);
|
|
2526
|
+
const activeThumbnailRef = useRef8(null);
|
|
2527
|
+
const preferredRailOpenRef = useRef8(true);
|
|
2528
|
+
const wasNarrowViewportRef = useRef8(viewportWidth < NARROW_VIEWPORT_WIDTH);
|
|
2529
|
+
const resizeStartRef = useRef8(null);
|
|
2530
|
+
const previousUserSelectRef = useRef8("");
|
|
2531
|
+
const previousCursorRef = useRef8("");
|
|
2532
|
+
const dragCollapsedRef = useRef8(false);
|
|
2533
|
+
const dragAnimationTimerRef = useRef8(null);
|
|
1683
2534
|
const isNarrowViewport = viewportWidth < NARROW_VIEWPORT_WIDTH;
|
|
1684
2535
|
const maxThumbnailWidth = isNarrowViewport ? Math.max(120, Math.floor(viewportWidth * 0.8)) : Math.min(MAX_THUMBNAIL_WIDTH, viewportWidth - 360);
|
|
1685
2536
|
const minThumbnailWidth = Math.min(MIN_THUMBNAIL_WIDTH, maxThumbnailWidth);
|
|
1686
2537
|
const visibleThumbnailWidth = Math.min(thumbnailWidth, maxThumbnailWidth);
|
|
1687
2538
|
const isThumbnailRailVisible = showThumbnailRail && !isDragCollapsed;
|
|
1688
|
-
const setThumbnailRailOpen =
|
|
2539
|
+
const setThumbnailRailOpen = useCallback6((open) => {
|
|
1689
2540
|
preferredRailOpenRef.current = open;
|
|
1690
2541
|
setShowThumbnailRail(open);
|
|
1691
2542
|
}, []);
|
|
1692
|
-
|
|
2543
|
+
useEffect9(() => {
|
|
1693
2544
|
const handleResize = () => setViewportWidth(window.innerWidth);
|
|
1694
2545
|
window.addEventListener("resize", handleResize);
|
|
1695
2546
|
return () => window.removeEventListener("resize", handleResize);
|
|
1696
2547
|
}, []);
|
|
1697
|
-
|
|
2548
|
+
useEffect9(() => {
|
|
1698
2549
|
if (isNarrowViewport !== wasNarrowViewportRef.current) {
|
|
1699
2550
|
setShowThumbnailRail(isNarrowViewport ? false : preferredRailOpenRef.current);
|
|
1700
2551
|
if (isNarrowViewport) {
|
|
1701
|
-
|
|
1702
|
-
setShowAnnotationPanel(false);
|
|
2552
|
+
setInspectorMode((mode) => mode === "comments" ? null : mode);
|
|
1703
2553
|
setSelectedAnnotationId(null);
|
|
1704
2554
|
}
|
|
1705
2555
|
wasNarrowViewportRef.current = isNarrowViewport;
|
|
1706
2556
|
}
|
|
1707
2557
|
}, [isNarrowViewport]);
|
|
1708
|
-
|
|
2558
|
+
useEffect9(() => {
|
|
1709
2559
|
window.localStorage.setItem(THUMBNAIL_WIDTH_STORAGE_KEY, String(thumbnailWidth));
|
|
1710
2560
|
}, [thumbnailWidth]);
|
|
1711
|
-
|
|
2561
|
+
useEffect9(
|
|
1712
2562
|
() => () => {
|
|
1713
2563
|
if (resizeStartRef.current) {
|
|
1714
2564
|
document.body.style.userSelect = previousUserSelectRef.current;
|
|
@@ -1718,7 +2568,7 @@ function SlideDeck({
|
|
|
1718
2568
|
},
|
|
1719
2569
|
[]
|
|
1720
2570
|
);
|
|
1721
|
-
|
|
2571
|
+
useEffect9(() => {
|
|
1722
2572
|
if ((!showThumbnailRail || isPresentationMode || viewMode !== "slide") && resizeStartRef.current) {
|
|
1723
2573
|
resizeStartRef.current = null;
|
|
1724
2574
|
dragCollapsedRef.current = false;
|
|
@@ -1730,7 +2580,7 @@ function SlideDeck({
|
|
|
1730
2580
|
document.body.style.cursor = previousCursorRef.current;
|
|
1731
2581
|
}
|
|
1732
2582
|
}, [showThumbnailRail, isPresentationMode, viewMode]);
|
|
1733
|
-
|
|
2583
|
+
useEffect9(() => {
|
|
1734
2584
|
if (!isResizingThumbnails) return;
|
|
1735
2585
|
const getDraggedWidth = (clientX) => {
|
|
1736
2586
|
const start = resizeStartRef.current;
|
|
@@ -1784,35 +2634,113 @@ function SlideDeck({
|
|
|
1784
2634
|
currentSlide,
|
|
1785
2635
|
animationStep,
|
|
1786
2636
|
totalSteps,
|
|
2637
|
+
isTransitioning,
|
|
1787
2638
|
direction,
|
|
1788
2639
|
showAllAnimations,
|
|
1789
2640
|
advance,
|
|
1790
2641
|
goBack,
|
|
1791
2642
|
goToSlide,
|
|
2643
|
+
goToStep,
|
|
2644
|
+
restartAtSlide,
|
|
1792
2645
|
onTransitionComplete
|
|
1793
2646
|
} = useSlideNavigation({
|
|
1794
2647
|
slides
|
|
1795
2648
|
});
|
|
1796
|
-
const
|
|
2649
|
+
const playback = useLoopPlayback({
|
|
2650
|
+
config: playbackConfig,
|
|
2651
|
+
slides,
|
|
2652
|
+
currentSlide,
|
|
2653
|
+
animationStep,
|
|
2654
|
+
isTransitioning,
|
|
2655
|
+
enabled: isLooping && isPresentationMode,
|
|
2656
|
+
advance,
|
|
2657
|
+
goBack,
|
|
2658
|
+
restartAtSlide
|
|
2659
|
+
});
|
|
2660
|
+
const rememberLoopConfig = (config) => {
|
|
2661
|
+
setLoopConfig(config);
|
|
2662
|
+
setLoopDraft(config);
|
|
2663
|
+
try {
|
|
2664
|
+
localStorage.setItem(
|
|
2665
|
+
storageKey,
|
|
2666
|
+
JSON.stringify({ base: JSON.stringify(defaultLoopConfig), config })
|
|
2667
|
+
);
|
|
2668
|
+
return true;
|
|
2669
|
+
} catch {
|
|
2670
|
+
return false;
|
|
2671
|
+
}
|
|
2672
|
+
};
|
|
2673
|
+
const saveLoopConfig = async (config) => {
|
|
2674
|
+
if (onLoopConfigChange) {
|
|
2675
|
+
await onLoopConfigChange(config);
|
|
2676
|
+
rememberLoopConfig(config);
|
|
2677
|
+
return "Loop settings saved.";
|
|
2678
|
+
}
|
|
2679
|
+
try {
|
|
2680
|
+
const response = await fetch("/__promptslide_loop", {
|
|
2681
|
+
method: "POST",
|
|
2682
|
+
headers: { "Content-Type": "application/json" },
|
|
2683
|
+
body: JSON.stringify(config)
|
|
2684
|
+
});
|
|
2685
|
+
if (response.status === 204) {
|
|
2686
|
+
rememberLoopConfig(config);
|
|
2687
|
+
return "Saved to loop-config.json. Included in future builds and publishing.";
|
|
2688
|
+
}
|
|
2689
|
+
if (response.status !== 404 && response.status !== 405)
|
|
2690
|
+
throw new Error(
|
|
2691
|
+
"Studio could not save loop-config.json. Download the settings to keep a copy."
|
|
2692
|
+
);
|
|
2693
|
+
} catch (error) {
|
|
2694
|
+
if (error instanceof Error && error.message.startsWith("Studio")) throw error;
|
|
2695
|
+
}
|
|
2696
|
+
return rememberLoopConfig(config) ? "Saved in this browser. Download settings and place loop-config.json in the project folder to share or publish them." : "Applied for this session. Browser storage is unavailable; download the settings to keep them.";
|
|
2697
|
+
};
|
|
2698
|
+
const startLoop = async (config = loopDraft) => {
|
|
2699
|
+
if (isEditorOpen) return;
|
|
2700
|
+
const first = includedSlides(config, slides)[0];
|
|
2701
|
+
if (first === void 0) {
|
|
2702
|
+
setLoopError("Include at least one slide before starting the loop.");
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
setLoopError("");
|
|
2706
|
+
try {
|
|
2707
|
+
if (!document.fullscreenElement) await document.documentElement.requestFullscreen();
|
|
2708
|
+
setPlaybackConfig(config);
|
|
2709
|
+
setLoopDraft(config);
|
|
2710
|
+
setViewMode("slide");
|
|
2711
|
+
setInspectorMode(null);
|
|
2712
|
+
setLoopMenuOpen(false);
|
|
2713
|
+
setLoopStartScreen(false);
|
|
2714
|
+
playback.setPaused(false);
|
|
2715
|
+
playback.restart(first);
|
|
2716
|
+
setIsLooping(true);
|
|
2717
|
+
} catch {
|
|
2718
|
+
setLoopError(
|
|
2719
|
+
"Full screen could not start. Allow full screen in your browser, then try again."
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
};
|
|
2723
|
+
const togglePresentationMode = useCallback6(async () => {
|
|
1797
2724
|
if (!document.fullscreenElement) {
|
|
1798
2725
|
await document.documentElement.requestFullscreen();
|
|
1799
2726
|
} else {
|
|
1800
2727
|
await document.exitFullscreen();
|
|
1801
2728
|
}
|
|
1802
2729
|
}, []);
|
|
1803
|
-
|
|
2730
|
+
useEffect9(() => {
|
|
1804
2731
|
const handleFullscreenChange = () => {
|
|
1805
2732
|
setIsPresentationMode(!!document.fullscreenElement);
|
|
2733
|
+
if (!document.fullscreenElement) setIsLooping(false);
|
|
1806
2734
|
};
|
|
1807
2735
|
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
1808
2736
|
return () => document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
1809
2737
|
}, []);
|
|
1810
|
-
|
|
2738
|
+
useEffect9(() => {
|
|
1811
2739
|
if (viewMode === "slide" && !isPresentationMode && showThumbnailRail) {
|
|
1812
2740
|
activeThumbnailRef.current?.scrollIntoView({ block: "nearest" });
|
|
1813
2741
|
}
|
|
1814
2742
|
}, [currentSlide, isPresentationMode, showThumbnailRail, viewMode]);
|
|
1815
|
-
|
|
2743
|
+
useEffect9(() => {
|
|
1816
2744
|
const calculateScale = () => {
|
|
1817
2745
|
if (!isPresentationMode) {
|
|
1818
2746
|
setScale(1);
|
|
@@ -1828,7 +2756,7 @@ function SlideDeck({
|
|
|
1828
2756
|
window.addEventListener("resize", calculateScale);
|
|
1829
2757
|
return () => window.removeEventListener("resize", calculateScale);
|
|
1830
2758
|
}, [isPresentationMode]);
|
|
1831
|
-
|
|
2759
|
+
useLayoutEffect2(() => {
|
|
1832
2760
|
const viewport = previewViewportRef.current;
|
|
1833
2761
|
const canvas = slideContainerRef.current;
|
|
1834
2762
|
if (!viewport || !canvas) return;
|
|
@@ -1844,16 +2772,10 @@ function SlideDeck({
|
|
|
1844
2772
|
observer.observe(viewport);
|
|
1845
2773
|
return () => observer.disconnect();
|
|
1846
2774
|
}, [viewMode, isPresentationMode]);
|
|
1847
|
-
const toggleCommentMode =
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
if (!next) {
|
|
1852
|
-
setSelectedAnnotationId(null);
|
|
1853
|
-
setHoveredAnnotationId(null);
|
|
1854
|
-
}
|
|
1855
|
-
}, [isAnnotationMode]);
|
|
1856
|
-
const handleExportPdf = useCallback5(async () => {
|
|
2775
|
+
const toggleCommentMode = useCallback6(() => {
|
|
2776
|
+
requestInspector(isAnnotationMode && viewMode === "slide" ? null : "comments");
|
|
2777
|
+
}, [isAnnotationMode, viewMode, requestInspector]);
|
|
2778
|
+
const handleExportPdf = useCallback6(async () => {
|
|
1857
2779
|
try {
|
|
1858
2780
|
const response = await fetch("/__promptslide_pdf");
|
|
1859
2781
|
if (response.ok && response.headers.get("content-type")?.includes("application/pdf")) {
|
|
@@ -1879,7 +2801,7 @@ function SlideDeck({
|
|
|
1879
2801
|
window.print();
|
|
1880
2802
|
}, 100);
|
|
1881
2803
|
}, [viewMode]);
|
|
1882
|
-
|
|
2804
|
+
useEffect9(() => {
|
|
1883
2805
|
const handleKeyDown = (e) => {
|
|
1884
2806
|
const target = e.target;
|
|
1885
2807
|
if (target?.closest("input, textarea, [contenteditable='true']")) return;
|
|
@@ -1887,6 +2809,28 @@ function SlideDeck({
|
|
|
1887
2809
|
const key = e.key.toLowerCase();
|
|
1888
2810
|
if (e.repeat && "fvglcdt".includes(key)) return;
|
|
1889
2811
|
if (isEditorOpen && ["f", "v", "g", "l", "c", "d"].includes(key)) return;
|
|
2812
|
+
if (loopStartScreen) return;
|
|
2813
|
+
if (loopMenuOpen) {
|
|
2814
|
+
if (e.key === "Escape") {
|
|
2815
|
+
setLoopMenuOpen(false);
|
|
2816
|
+
loopMenuRef.current?.querySelector("button")?.focus();
|
|
2817
|
+
}
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
if (isLooping) {
|
|
2821
|
+
if (e.key === "Escape") {
|
|
2822
|
+
setIsLooping(false);
|
|
2823
|
+
if (document.fullscreenElement) void document.exitFullscreen().catch(() => {
|
|
2824
|
+
});
|
|
2825
|
+
} else if (e.key === " ") {
|
|
2826
|
+
e.preventDefault();
|
|
2827
|
+
if (!e.repeat) playback.setPaused((value) => !value);
|
|
2828
|
+
} else if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
|
|
2829
|
+
e.preventDefault();
|
|
2830
|
+
playback.move(e.key === "ArrowRight" ? 1 : -1);
|
|
2831
|
+
}
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
1890
2834
|
if (key === "f") {
|
|
1891
2835
|
e.preventDefault();
|
|
1892
2836
|
void togglePresentationMode();
|
|
@@ -1936,6 +2880,10 @@ function SlideDeck({
|
|
|
1936
2880
|
}, [
|
|
1937
2881
|
advance,
|
|
1938
2882
|
goBack,
|
|
2883
|
+
playback,
|
|
2884
|
+
isLooping,
|
|
2885
|
+
loopMenuOpen,
|
|
2886
|
+
loopStartScreen,
|
|
1939
2887
|
viewMode,
|
|
1940
2888
|
togglePresentationMode,
|
|
1941
2889
|
isAnnotationMode,
|
|
@@ -1946,9 +2894,10 @@ function SlideDeck({
|
|
|
1946
2894
|
toggleCommentMode,
|
|
1947
2895
|
handleExportPdf
|
|
1948
2896
|
]);
|
|
1949
|
-
const
|
|
1950
|
-
|
|
1951
|
-
|
|
2897
|
+
const inspectorWidth = viewportWidth <= 850 ? 280 : 320;
|
|
2898
|
+
const inspectorVisible = inspectorMode !== null && !isPresentationMode && viewMode === "slide";
|
|
2899
|
+
return /* @__PURE__ */ jsxs9("div", { className: "min-h-screen w-full bg-neutral-950 text-foreground", children: [
|
|
2900
|
+
/* @__PURE__ */ jsx17("style", { children: `
|
|
1952
2901
|
.slide-thumbnail-scroll-area {
|
|
1953
2902
|
scrollbar-width: thin;
|
|
1954
2903
|
scrollbar-color: #404040 transparent;
|
|
@@ -1994,113 +2943,161 @@ function SlideDeck({
|
|
|
1994
2943
|
}
|
|
1995
2944
|
}
|
|
1996
2945
|
` }),
|
|
1997
|
-
/* @__PURE__ */
|
|
2946
|
+
loopError && /* @__PURE__ */ jsxs9(
|
|
2947
|
+
"div",
|
|
2948
|
+
{
|
|
2949
|
+
role: "alert",
|
|
2950
|
+
className: "fixed bottom-4 left-4 z-[110] max-w-md rounded-lg border border-red-700 bg-neutral-950 p-4 text-sm text-red-300",
|
|
2951
|
+
children: [
|
|
2952
|
+
loopError,
|
|
2953
|
+
/* @__PURE__ */ jsx17("button", { className: "ml-3 underline", onClick: () => setLoopError(""), children: "Dismiss" })
|
|
2954
|
+
]
|
|
2955
|
+
}
|
|
2956
|
+
),
|
|
2957
|
+
loopStartScreen && /* @__PURE__ */ jsxs9("div", { className: "fixed inset-0 z-[90] flex flex-col items-center justify-center gap-5 bg-neutral-950 text-white", children: [
|
|
2958
|
+
/* @__PURE__ */ jsx17("h1", { className: "text-3xl font-semibold", children: "Ready to loop" }),
|
|
2959
|
+
/* @__PURE__ */ jsx17("p", { className: "text-neutral-400", children: "Space pauses or resumes. Escape stops the loop." }),
|
|
2960
|
+
/* @__PURE__ */ jsx17(
|
|
2961
|
+
"button",
|
|
2962
|
+
{
|
|
2963
|
+
className: "rounded-lg border border-orange-500 px-6 py-3 text-orange-400",
|
|
2964
|
+
onClick: () => void startLoop(),
|
|
2965
|
+
children: "Click to start"
|
|
2966
|
+
}
|
|
2967
|
+
),
|
|
2968
|
+
/* @__PURE__ */ jsx17(
|
|
2969
|
+
"button",
|
|
2970
|
+
{
|
|
2971
|
+
className: "text-sm text-neutral-400 underline",
|
|
2972
|
+
onClick: () => {
|
|
2973
|
+
setLoopStartScreen(false);
|
|
2974
|
+
requestInspector("slide");
|
|
2975
|
+
},
|
|
2976
|
+
children: "Configure loop"
|
|
2977
|
+
}
|
|
2978
|
+
)
|
|
2979
|
+
] }),
|
|
2980
|
+
isLooping && playback.paused && /* @__PURE__ */ jsx17(
|
|
2981
|
+
"div",
|
|
2982
|
+
{
|
|
2983
|
+
role: "status",
|
|
2984
|
+
className: "pointer-events-none fixed bottom-4 left-4 z-[90] rounded-md bg-neutral-950 px-4 py-2 text-sm text-white",
|
|
2985
|
+
children: "Loop paused \xB7 Space to resume"
|
|
2986
|
+
}
|
|
2987
|
+
),
|
|
2988
|
+
/* @__PURE__ */ jsxs9(
|
|
1998
2989
|
"div",
|
|
1999
2990
|
{
|
|
2000
2991
|
"data-ps-viewer-toolbar": true,
|
|
2001
|
-
style:
|
|
2992
|
+
style: inspectorVisible ? { right: inspectorWidth + 16, maxWidth: `calc(100vw - ${inspectorWidth + 32}px)` } : void 0,
|
|
2002
2993
|
className: cn(
|
|
2003
|
-
"fixed top-4 z-50 flex gap-1 rounded-lg border border-neutral-800 bg-neutral-950/90 p-1 backdrop-blur-sm transition-[right] duration-200 ease-out print:hidden",
|
|
2004
|
-
(isPresentationMode || isNarrowViewport &&
|
|
2005
|
-
isEditorOpen && "flex-wrap",
|
|
2994
|
+
"fixed top-4 z-50 flex flex-wrap gap-1 rounded-lg border border-neutral-800 bg-neutral-950/90 p-1 backdrop-blur-sm transition-[right] duration-200 ease-out print:hidden",
|
|
2995
|
+
(isPresentationMode || isNarrowViewport && inspectorVisible && !isEditorOpen) && "hidden",
|
|
2006
2996
|
isEditorOpen && viewportWidth <= 850 && "[&_button:disabled]:hidden [&>div]:hidden",
|
|
2007
|
-
!
|
|
2997
|
+
!inspectorVisible && "right-4"
|
|
2008
2998
|
),
|
|
2009
2999
|
children: [
|
|
2010
|
-
/* @__PURE__ */
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
className: cn(
|
|
2015
|
-
"rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
|
|
2016
|
-
viewMode === "slide" && "bg-neutral-800 text-white"
|
|
2017
|
-
),
|
|
2018
|
-
disabled: isEditorOpen,
|
|
2019
|
-
title: "Presentation View (V)",
|
|
2020
|
-
children: /* @__PURE__ */ jsx12(Monitor, { className: "h-4 w-4" })
|
|
2021
|
-
}
|
|
2022
|
-
),
|
|
2023
|
-
/* @__PURE__ */ jsx12(
|
|
2024
|
-
"button",
|
|
2025
|
-
{
|
|
2026
|
-
onClick: () => setViewMode("list"),
|
|
2027
|
-
className: cn(
|
|
2028
|
-
"rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
|
|
2029
|
-
viewMode === "list" && "bg-neutral-800 text-white"
|
|
2030
|
-
),
|
|
2031
|
-
disabled: isEditorOpen,
|
|
2032
|
-
title: "List View (L)",
|
|
2033
|
-
children: /* @__PURE__ */ jsx12(List, { className: "h-4 w-4" })
|
|
2034
|
-
}
|
|
2035
|
-
),
|
|
2036
|
-
/* @__PURE__ */ jsx12(
|
|
2037
|
-
"button",
|
|
2038
|
-
{
|
|
2039
|
-
onClick: () => setViewMode("grid"),
|
|
2040
|
-
className: cn(
|
|
2041
|
-
"rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
|
|
2042
|
-
viewMode === "grid" && "bg-neutral-800 text-white"
|
|
2043
|
-
),
|
|
2044
|
-
disabled: isEditorOpen,
|
|
2045
|
-
title: "Grid View (G)",
|
|
2046
|
-
children: /* @__PURE__ */ jsx12(Grid3X3, { className: "h-4 w-4" })
|
|
2047
|
-
}
|
|
2048
|
-
),
|
|
2049
|
-
/* @__PURE__ */ jsx12("div", { className: "mx-1 w-px bg-neutral-800" }),
|
|
2050
|
-
/* @__PURE__ */ jsxs6(
|
|
2051
|
-
"button",
|
|
3000
|
+
/* @__PURE__ */ jsx17(ViewMenu, { value: viewMode, onChange: setViewMode, disabled: isEditorOpen }),
|
|
3001
|
+
/* @__PURE__ */ jsx17("div", { className: "mx-1 w-px bg-neutral-800" }),
|
|
3002
|
+
/* @__PURE__ */ jsxs9(
|
|
3003
|
+
ToolbarAction,
|
|
2052
3004
|
{
|
|
2053
3005
|
onClick: toggleCommentMode,
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
isAnnotationMode ? "border border-[#FF6B35]/50 bg-[#FF6B35]/15 p-[7px] text-[#FF6B35] hover:border-[#FF6B35] hover:bg-[#FF6B35]/25" : "px-2.5 text-neutral-400 hover:bg-neutral-800 hover:text-white"
|
|
2057
|
-
),
|
|
2058
|
-
disabled: isEditorOpen,
|
|
2059
|
-
"aria-label": "Comment",
|
|
2060
|
-
"aria-pressed": isAnnotationMode,
|
|
3006
|
+
label: "Comment",
|
|
3007
|
+
selected: isAnnotationMode,
|
|
2061
3008
|
title: "Comment (C)",
|
|
2062
3009
|
children: [
|
|
2063
|
-
/* @__PURE__ */
|
|
2064
|
-
|
|
2065
|
-
openCount > 0 && /* @__PURE__ */ jsx12("span", { className: "absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#FF6B35] text-[10px] font-bold text-white", children: openCount })
|
|
3010
|
+
/* @__PURE__ */ jsx17(MessageCircle, { className: "h-[15px] w-[15px] shrink-0" }),
|
|
3011
|
+
openCount > 0 && /* @__PURE__ */ jsx17("span", { className: "absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#FF6B35] text-[10px] font-bold text-white", children: openCount })
|
|
2066
3012
|
]
|
|
2067
3013
|
}
|
|
2068
3014
|
),
|
|
2069
|
-
/* @__PURE__ */
|
|
2070
|
-
/* @__PURE__ */
|
|
2071
|
-
|
|
2072
|
-
"button",
|
|
3015
|
+
/* @__PURE__ */ jsx17("span", { "data-ps-editor-toolbar": true, className: "contents" }),
|
|
3016
|
+
/* @__PURE__ */ jsx17(
|
|
3017
|
+
ToolbarAction,
|
|
2073
3018
|
{
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
children: /* @__PURE__ */ jsx12(Download, { className: "h-4 w-4" })
|
|
3019
|
+
label: "Slide settings",
|
|
3020
|
+
selected: inspectorMode === "slide" && inspectorVisible,
|
|
3021
|
+
onClick: () => requestInspector(inspectorMode === "slide" && inspectorVisible ? null : "slide"),
|
|
3022
|
+
children: /* @__PURE__ */ jsx17(Settings2, { size: 16 })
|
|
2079
3023
|
}
|
|
2080
3024
|
),
|
|
2081
|
-
/* @__PURE__ */
|
|
2082
|
-
|
|
3025
|
+
/* @__PURE__ */ jsx17("div", { className: "mx-1 w-px bg-neutral-800" }),
|
|
3026
|
+
/* @__PURE__ */ jsx17(
|
|
3027
|
+
ToolbarAction,
|
|
2083
3028
|
{
|
|
2084
|
-
onClick:
|
|
2085
|
-
className: "inline-flex h-8 items-center justify-center gap-1.5 rounded-md border border-[#FF6B35]/50 bg-[#FF6B35]/15 px-3 text-sm font-semibold text-[#FF6B35] transition-colors hover:border-[#FF6B35] hover:bg-[#FF6B35]/25",
|
|
3029
|
+
onClick: handleExportPdf,
|
|
2086
3030
|
disabled: isEditorOpen,
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
/* @__PURE__ */ jsx12("span", { children: "Present" })
|
|
2091
|
-
]
|
|
3031
|
+
label: "Download PDF",
|
|
3032
|
+
title: "Download PDF (D)",
|
|
3033
|
+
children: /* @__PURE__ */ jsx17(Download2, { className: "h-4 w-4" })
|
|
2092
3034
|
}
|
|
2093
|
-
)
|
|
3035
|
+
),
|
|
3036
|
+
/* @__PURE__ */ jsxs9("div", { ref: loopMenuRef, className: "relative flex", children: [
|
|
3037
|
+
/* @__PURE__ */ jsxs9(
|
|
3038
|
+
"button",
|
|
3039
|
+
{
|
|
3040
|
+
onClick: togglePresentationMode,
|
|
3041
|
+
className: "inline-flex h-8 items-center justify-center gap-1.5 rounded-l-md border border-[#FF6B35]/50 bg-[#FF6B35]/15 px-3 text-sm font-semibold text-[#FF6B35] transition-colors hover:border-[#FF6B35] hover:bg-[#FF6B35]/25",
|
|
3042
|
+
disabled: isEditorOpen,
|
|
3043
|
+
title: "Present (F)",
|
|
3044
|
+
children: [
|
|
3045
|
+
/* @__PURE__ */ jsx17(Play2, { className: "h-3.5 w-3.5 fill-current" }),
|
|
3046
|
+
/* @__PURE__ */ jsx17("span", { children: "Present" })
|
|
3047
|
+
]
|
|
3048
|
+
}
|
|
3049
|
+
),
|
|
3050
|
+
/* @__PURE__ */ jsx17(
|
|
3051
|
+
"button",
|
|
3052
|
+
{
|
|
3053
|
+
disabled: isEditorOpen,
|
|
3054
|
+
"aria-label": "Presentation options",
|
|
3055
|
+
"aria-expanded": loopMenuOpen,
|
|
3056
|
+
"aria-controls": "presentation-options",
|
|
3057
|
+
className: "rounded-r-md border border-l-0 border-[#FF6B35]/50 bg-[#FF6B35]/15 px-2 text-[#FF6B35] hover:bg-[#FF6B35]/25",
|
|
3058
|
+
onClick: () => setLoopMenuOpen((value) => !value),
|
|
3059
|
+
children: /* @__PURE__ */ jsx17(ChevronDown2, { size: 16 })
|
|
3060
|
+
}
|
|
3061
|
+
),
|
|
3062
|
+
loopMenuOpen && /* @__PURE__ */ jsxs9(
|
|
3063
|
+
"div",
|
|
3064
|
+
{
|
|
3065
|
+
id: "presentation-options",
|
|
3066
|
+
className: "absolute right-0 top-10 w-52 rounded-lg border border-neutral-700 bg-neutral-950 p-1 text-sm text-white",
|
|
3067
|
+
children: [
|
|
3068
|
+
/* @__PURE__ */ jsx17(
|
|
3069
|
+
"button",
|
|
3070
|
+
{
|
|
3071
|
+
className: "w-full rounded px-3 py-2 text-left hover:bg-neutral-800",
|
|
3072
|
+
onClick: () => void startLoop(),
|
|
3073
|
+
children: "Auto-loop"
|
|
3074
|
+
}
|
|
3075
|
+
),
|
|
3076
|
+
/* @__PURE__ */ jsx17(
|
|
3077
|
+
"button",
|
|
3078
|
+
{
|
|
3079
|
+
className: "w-full rounded px-3 py-2 text-left hover:bg-neutral-800",
|
|
3080
|
+
onClick: () => {
|
|
3081
|
+
setLoopMenuOpen(false);
|
|
3082
|
+
requestInspector("slide");
|
|
3083
|
+
},
|
|
3084
|
+
children: "Configure loop\u2026"
|
|
3085
|
+
}
|
|
3086
|
+
)
|
|
3087
|
+
]
|
|
3088
|
+
}
|
|
3089
|
+
)
|
|
3090
|
+
] })
|
|
2094
3091
|
]
|
|
2095
3092
|
}
|
|
2096
3093
|
),
|
|
2097
|
-
viewMode === "slide" && /* @__PURE__ */
|
|
3094
|
+
viewMode === "slide" && /* @__PURE__ */ jsxs9(
|
|
2098
3095
|
"div",
|
|
2099
3096
|
{
|
|
2100
3097
|
className: cn("flex h-screen w-full print:hidden", isPresentationMode ? "bg-black" : ""),
|
|
2101
3098
|
children: [
|
|
2102
|
-
!isPresentationMode && /* @__PURE__ */
|
|
2103
|
-
/* @__PURE__ */
|
|
3099
|
+
!isPresentationMode && /* @__PURE__ */ jsxs9(Fragment4, { children: [
|
|
3100
|
+
/* @__PURE__ */ jsxs9(
|
|
2104
3101
|
"div",
|
|
2105
3102
|
{
|
|
2106
3103
|
className: cn(
|
|
@@ -2110,7 +3107,7 @@ function SlideDeck({
|
|
|
2110
3107
|
),
|
|
2111
3108
|
style: { width: isThumbnailRailVisible ? visibleThumbnailWidth : 0 },
|
|
2112
3109
|
children: [
|
|
2113
|
-
/* @__PURE__ */
|
|
3110
|
+
/* @__PURE__ */ jsx17("div", { className: "h-full w-full overflow-hidden", children: /* @__PURE__ */ jsxs9(
|
|
2114
3111
|
"aside",
|
|
2115
3112
|
{
|
|
2116
3113
|
id: "presentation-thumbnails",
|
|
@@ -2123,13 +3120,13 @@ function SlideDeck({
|
|
|
2123
3120
|
),
|
|
2124
3121
|
style: { width: visibleThumbnailWidth },
|
|
2125
3122
|
children: [
|
|
2126
|
-
/* @__PURE__ */
|
|
2127
|
-
/* @__PURE__ */
|
|
3123
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between border-b border-neutral-800 px-3 py-2", children: [
|
|
3124
|
+
/* @__PURE__ */ jsxs9("span", { className: "text-sm font-semibold", children: [
|
|
2128
3125
|
"Slides (",
|
|
2129
3126
|
slides.length,
|
|
2130
3127
|
")"
|
|
2131
3128
|
] }),
|
|
2132
|
-
/* @__PURE__ */
|
|
3129
|
+
/* @__PURE__ */ jsx17(
|
|
2133
3130
|
"button",
|
|
2134
3131
|
{
|
|
2135
3132
|
type: "button",
|
|
@@ -2137,11 +3134,11 @@ function SlideDeck({
|
|
|
2137
3134
|
className: "rounded p-2 text-neutral-400 hover:bg-neutral-800 hover:text-white",
|
|
2138
3135
|
"aria-label": "Hide slide thumbnails",
|
|
2139
3136
|
title: "Hide thumbnails (T)",
|
|
2140
|
-
children: /* @__PURE__ */
|
|
3137
|
+
children: /* @__PURE__ */ jsx17(PanelLeftClose, { className: "h-4 w-4" })
|
|
2141
3138
|
}
|
|
2142
3139
|
)
|
|
2143
3140
|
] }),
|
|
2144
|
-
/* @__PURE__ */
|
|
3141
|
+
/* @__PURE__ */ jsx17("div", { className: "slide-thumbnail-scroll-area min-h-0 flex-1 space-y-3 overflow-y-auto p-3", children: slides.map((slideConfig, index) => /* @__PURE__ */ jsxs9(
|
|
2145
3142
|
"button",
|
|
2146
3143
|
{
|
|
2147
3144
|
ref: index === currentSlide ? activeThumbnailRef : void 0,
|
|
@@ -2154,18 +3151,21 @@ function SlideDeck({
|
|
|
2154
3151
|
index === currentSlide ? "border-[#FF6B35] bg-[#FF6B35]/15 ring-1 ring-[#FF6B35]" : "border-transparent hover:border-neutral-600 hover:bg-neutral-800"
|
|
2155
3152
|
),
|
|
2156
3153
|
children: [
|
|
2157
|
-
/* @__PURE__ */
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
3154
|
+
/* @__PURE__ */ jsxs9("div", { className: "relative aspect-video w-full overflow-hidden rounded bg-black", children: [
|
|
3155
|
+
loopDraft.slides[loopSlideKey(slideConfig, index)]?.excluded && /* @__PURE__ */ jsx17("span", { className: "absolute right-1 top-1 z-10 rounded border border-neutral-700 bg-neutral-950 px-1.5 py-0.5 text-[9px] text-neutral-400", children: "Skip in loop" }),
|
|
3156
|
+
/* @__PURE__ */ jsx17(
|
|
3157
|
+
GridThumbnail,
|
|
3158
|
+
{
|
|
3159
|
+
slide: slideConfig,
|
|
3160
|
+
index,
|
|
3161
|
+
total: slides.length
|
|
3162
|
+
}
|
|
3163
|
+
)
|
|
3164
|
+
] }),
|
|
3165
|
+
/* @__PURE__ */ jsx17("div", { className: "mt-1.5 flex items-baseline gap-2 px-0.5 text-xs", children: slideConfig.title ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
|
|
3166
|
+
/* @__PURE__ */ jsx17("span", { className: "font-mono text-neutral-400", children: index + 1 }),
|
|
3167
|
+
/* @__PURE__ */ jsx17("span", { className: "truncate", children: slideConfig.title })
|
|
3168
|
+
] }) : /* @__PURE__ */ jsxs9("span", { children: [
|
|
2169
3169
|
"Slide ",
|
|
2170
3170
|
index + 1
|
|
2171
3171
|
] }) })
|
|
@@ -2176,7 +3176,7 @@ function SlideDeck({
|
|
|
2176
3176
|
]
|
|
2177
3177
|
}
|
|
2178
3178
|
) }),
|
|
2179
|
-
isThumbnailRailVisible && /* @__PURE__ */
|
|
3179
|
+
isThumbnailRailVisible && /* @__PURE__ */ jsx17(
|
|
2180
3180
|
"div",
|
|
2181
3181
|
{
|
|
2182
3182
|
role: "separator",
|
|
@@ -2225,16 +3225,17 @@ function SlideDeck({
|
|
|
2225
3225
|
]
|
|
2226
3226
|
}
|
|
2227
3227
|
),
|
|
2228
|
-
/* @__PURE__ */
|
|
3228
|
+
/* @__PURE__ */ jsx17(
|
|
2229
3229
|
"div",
|
|
2230
3230
|
{
|
|
2231
3231
|
"aria-hidden": isThumbnailRailVisible || isResizingThumbnails,
|
|
2232
3232
|
inert: isThumbnailRailVisible || isResizingThumbnails,
|
|
2233
3233
|
className: cn(
|
|
2234
|
-
"fixed
|
|
3234
|
+
"fixed left-4 z-50 rounded-lg border border-neutral-800 bg-neutral-950/90 backdrop-blur-sm transition-opacity duration-150 motion-reduce:transition-none",
|
|
3235
|
+
isNarrowViewport && !inspectorVisible ? "top-16" : "top-4",
|
|
2235
3236
|
isThumbnailRailVisible || isResizingThumbnails ? "pointer-events-none opacity-0" : "opacity-100 delay-200"
|
|
2236
3237
|
),
|
|
2237
|
-
children: /* @__PURE__ */
|
|
3238
|
+
children: /* @__PURE__ */ jsx17(
|
|
2238
3239
|
"button",
|
|
2239
3240
|
{
|
|
2240
3241
|
type: "button",
|
|
@@ -2242,28 +3243,34 @@ function SlideDeck({
|
|
|
2242
3243
|
className: "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
|
|
2243
3244
|
"aria-label": "Show slide thumbnails",
|
|
2244
3245
|
title: "Show thumbnails (T)",
|
|
2245
|
-
children: /* @__PURE__ */
|
|
3246
|
+
children: /* @__PURE__ */ jsx17(PanelLeftOpen, { className: "h-4 w-4" })
|
|
2246
3247
|
}
|
|
2247
3248
|
)
|
|
2248
3249
|
}
|
|
2249
3250
|
)
|
|
2250
3251
|
] }),
|
|
2251
|
-
/* @__PURE__ */
|
|
3252
|
+
/* @__PURE__ */ jsxs9(
|
|
2252
3253
|
"div",
|
|
2253
3254
|
{
|
|
2254
3255
|
ref: containerRef,
|
|
2255
3256
|
role: "presentation",
|
|
3257
|
+
"data-loop-running": isLooping ? "true" : void 0,
|
|
2256
3258
|
tabIndex: isPresentationMode ? 0 : void 0,
|
|
2257
3259
|
className: cn(
|
|
2258
3260
|
"flex min-w-0 flex-1 flex-col items-center justify-center overflow-hidden",
|
|
2259
|
-
isPresentationMode ?
|
|
3261
|
+
isPresentationMode ? `bg-black p-0 ${isLooping ? "cursor-none" : ""}` : "p-4 md:p-8"
|
|
2260
3262
|
),
|
|
2261
|
-
onClick: isPresentationMode ? advance : void 0,
|
|
3263
|
+
onClick: isPresentationMode ? () => isLooping ? playback.move(1) : advance() : void 0,
|
|
2262
3264
|
onKeyDown: isPresentationMode ? (e) => {
|
|
2263
|
-
if (e.key === "Enter"
|
|
3265
|
+
if (e.key === "Enter") {
|
|
3266
|
+
e.preventDefault();
|
|
3267
|
+
e.stopPropagation();
|
|
3268
|
+
if (isLooping) playback.move(1);
|
|
3269
|
+
else advance();
|
|
3270
|
+
}
|
|
2264
3271
|
} : void 0,
|
|
2265
3272
|
children: [
|
|
2266
|
-
/* @__PURE__ */
|
|
3273
|
+
/* @__PURE__ */ jsx17(LayoutGroup, { id: "slide-deck", children: isPresentationMode ? /* @__PURE__ */ jsx17(
|
|
2267
3274
|
"div",
|
|
2268
3275
|
{
|
|
2269
3276
|
className: "pointer-events-none relative overflow-hidden bg-black",
|
|
@@ -2273,9 +3280,10 @@ function SlideDeck({
|
|
|
2273
3280
|
transform: `scale(${scale})`,
|
|
2274
3281
|
transformOrigin: "center center"
|
|
2275
3282
|
},
|
|
2276
|
-
children: /* @__PURE__ */
|
|
3283
|
+
children: /* @__PURE__ */ jsx17(
|
|
2277
3284
|
SlideRenderer,
|
|
2278
3285
|
{
|
|
3286
|
+
playbackKey: isLooping ? playback.epoch : void 0,
|
|
2279
3287
|
slides,
|
|
2280
3288
|
currentSlide,
|
|
2281
3289
|
animationStep,
|
|
@@ -2288,13 +3296,13 @@ function SlideDeck({
|
|
|
2288
3296
|
}
|
|
2289
3297
|
)
|
|
2290
3298
|
}
|
|
2291
|
-
) : /* @__PURE__ */
|
|
3299
|
+
) : /* @__PURE__ */ jsxs9(
|
|
2292
3300
|
"div",
|
|
2293
3301
|
{
|
|
2294
3302
|
ref: previewViewportRef,
|
|
2295
3303
|
className: "relative aspect-video w-full max-w-7xl overflow-hidden rounded-xl border border-neutral-800 bg-black shadow-2xl",
|
|
2296
3304
|
children: [
|
|
2297
|
-
/* @__PURE__ */
|
|
3305
|
+
/* @__PURE__ */ jsxs9(
|
|
2298
3306
|
"div",
|
|
2299
3307
|
{
|
|
2300
3308
|
ref: slideContainerRef,
|
|
@@ -2308,7 +3316,7 @@ function SlideDeck({
|
|
|
2308
3316
|
transformOrigin: "top left"
|
|
2309
3317
|
},
|
|
2310
3318
|
children: [
|
|
2311
|
-
/* @__PURE__ */
|
|
3319
|
+
/* @__PURE__ */ jsx17(
|
|
2312
3320
|
SlideRenderer,
|
|
2313
3321
|
{
|
|
2314
3322
|
slides,
|
|
@@ -2323,7 +3331,7 @@ function SlideDeck({
|
|
|
2323
3331
|
},
|
|
2324
3332
|
editorRevision
|
|
2325
3333
|
),
|
|
2326
|
-
isAnnotationMode && /* @__PURE__ */
|
|
3334
|
+
isAnnotationMode && /* @__PURE__ */ jsx17(
|
|
2327
3335
|
AnnotationOverlay,
|
|
2328
3336
|
{
|
|
2329
3337
|
slides,
|
|
@@ -2332,7 +3340,7 @@ function SlideDeck({
|
|
|
2332
3340
|
selectedId: selectedAnnotationId,
|
|
2333
3341
|
hoveredId: hoveredAnnotationId,
|
|
2334
3342
|
onSelectId: setSelectedAnnotationId,
|
|
2335
|
-
onShowPanel: () =>
|
|
3343
|
+
onShowPanel: () => requestInspector("comments"),
|
|
2336
3344
|
slideAnnotations: getSlideAnnotations(currentSlide),
|
|
2337
3345
|
addAnnotation: effectiveAdd ?? (() => {
|
|
2338
3346
|
})
|
|
@@ -2341,7 +3349,7 @@ function SlideDeck({
|
|
|
2341
3349
|
]
|
|
2342
3350
|
}
|
|
2343
3351
|
),
|
|
2344
|
-
/* @__PURE__ */
|
|
3352
|
+
/* @__PURE__ */ jsx17(
|
|
2345
3353
|
"div",
|
|
2346
3354
|
{
|
|
2347
3355
|
"data-ps-editor-overlay": true,
|
|
@@ -2351,85 +3359,73 @@ function SlideDeck({
|
|
|
2351
3359
|
]
|
|
2352
3360
|
}
|
|
2353
3361
|
) }),
|
|
2354
|
-
!isPresentationMode && /* @__PURE__ */
|
|
2355
|
-
/* @__PURE__ */
|
|
3362
|
+
!isPresentationMode && /* @__PURE__ */ jsxs9("div", { className: "mt-6 flex items-center gap-4", children: [
|
|
3363
|
+
/* @__PURE__ */ jsx17(
|
|
2356
3364
|
"button",
|
|
2357
3365
|
{
|
|
2358
3366
|
onClick: goBack,
|
|
2359
3367
|
className: "rounded-full border border-neutral-800 bg-black/50 p-2 text-neutral-400 backdrop-blur-sm transition-colors hover:bg-neutral-900 hover:text-white",
|
|
2360
3368
|
title: "Previous step or slide (\u2190)",
|
|
2361
|
-
children: /* @__PURE__ */
|
|
3369
|
+
children: /* @__PURE__ */ jsx17(ChevronLeft, { className: "h-5 w-5" })
|
|
2362
3370
|
}
|
|
2363
3371
|
),
|
|
2364
|
-
/* @__PURE__ */
|
|
3372
|
+
/* @__PURE__ */ jsx17("div", { className: "flex min-w-[4rem] flex-col items-center", children: /* @__PURE__ */ jsxs9("span", { className: "font-mono text-sm text-neutral-500", children: [
|
|
2365
3373
|
currentSlide + 1,
|
|
2366
3374
|
" / ",
|
|
2367
3375
|
slides.length
|
|
2368
3376
|
] }) }),
|
|
2369
|
-
/* @__PURE__ */
|
|
3377
|
+
/* @__PURE__ */ jsx17(
|
|
2370
3378
|
"button",
|
|
2371
3379
|
{
|
|
2372
3380
|
onClick: advance,
|
|
2373
3381
|
className: "rounded-full border border-neutral-800 bg-black/50 p-2 text-neutral-400 backdrop-blur-sm transition-colors hover:bg-neutral-900 hover:text-white",
|
|
2374
3382
|
title: "Next step or slide (\u2192 / Space)",
|
|
2375
|
-
children: /* @__PURE__ */
|
|
3383
|
+
children: /* @__PURE__ */ jsx17(ChevronRight, { className: "h-5 w-5" })
|
|
2376
3384
|
}
|
|
2377
3385
|
)
|
|
2378
3386
|
] })
|
|
2379
3387
|
]
|
|
2380
3388
|
}
|
|
2381
3389
|
),
|
|
2382
|
-
/* @__PURE__ */
|
|
2383
|
-
|
|
3390
|
+
/* @__PURE__ */ jsx17(
|
|
3391
|
+
Inspector,
|
|
2384
3392
|
{
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
3393
|
+
mode: inspectorMode,
|
|
3394
|
+
visible: inspectorVisible,
|
|
3395
|
+
editorAvailable,
|
|
3396
|
+
width: inspectorWidth,
|
|
3397
|
+
overlay: isNarrowViewport,
|
|
3398
|
+
onSelect: requestInspector,
|
|
3399
|
+
onClose: () => requestInspector(null),
|
|
3400
|
+
slideSettings: /* @__PURE__ */ jsx17(
|
|
3401
|
+
LoopSettings,
|
|
3402
|
+
{
|
|
3403
|
+
slides,
|
|
3404
|
+
currentSlide,
|
|
3405
|
+
animationStep,
|
|
3406
|
+
config: loopDraft,
|
|
3407
|
+
onChange: setLoopDraft,
|
|
3408
|
+
onStepChange: goToStep,
|
|
3409
|
+
onSave: saveLoopConfig,
|
|
3410
|
+
onStart: (config) => void startLoop(config),
|
|
3411
|
+
onRevert: () => setLoopDraft(loopConfig),
|
|
3412
|
+
dirty: JSON.stringify(loopDraft) !== JSON.stringify(loopConfig)
|
|
3413
|
+
}
|
|
2402
3414
|
),
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
},
|
|
2406
|
-
children: /* @__PURE__ */ jsx12(
|
|
2407
|
-
"div",
|
|
3415
|
+
comments: /* @__PURE__ */ jsx17(
|
|
3416
|
+
AnnotationPanel,
|
|
2408
3417
|
{
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
onHover: setHoveredAnnotationId,
|
|
2421
|
-
onDelete: effectiveDelete ?? (() => {
|
|
2422
|
-
}),
|
|
2423
|
-
onUpdate: effectiveUpdate ?? (() => {
|
|
2424
|
-
}),
|
|
2425
|
-
onClose: () => {
|
|
2426
|
-
setIsAnnotationMode(false);
|
|
2427
|
-
setShowAnnotationPanel(false);
|
|
2428
|
-
setSelectedAnnotationId(null);
|
|
2429
|
-
setHoveredAnnotationId(null);
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
)
|
|
3418
|
+
embedded: true,
|
|
3419
|
+
annotations: getSlideAnnotations(currentSlide),
|
|
3420
|
+
unlinkedAnnotations,
|
|
3421
|
+
selectedId: selectedAnnotationId,
|
|
3422
|
+
onSelect: setSelectedAnnotationId,
|
|
3423
|
+
onHover: setHoveredAnnotationId,
|
|
3424
|
+
onDelete: effectiveDelete ?? (() => {
|
|
3425
|
+
}),
|
|
3426
|
+
onUpdate: effectiveUpdate ?? (() => {
|
|
3427
|
+
}),
|
|
3428
|
+
onClose: () => requestInspector(null)
|
|
2433
3429
|
}
|
|
2434
3430
|
)
|
|
2435
3431
|
}
|
|
@@ -2437,12 +3433,12 @@ function SlideDeck({
|
|
|
2437
3433
|
]
|
|
2438
3434
|
}
|
|
2439
3435
|
),
|
|
2440
|
-
viewMode === "grid" && /* @__PURE__ */
|
|
3436
|
+
viewMode === "grid" && /* @__PURE__ */ jsx17("div", { className: "mx-auto max-w-7xl p-8 pt-16 print:hidden", children: /* @__PURE__ */ jsx17("div", { className: "grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4", children: slides.map((slideConfig, index) => {
|
|
2441
3437
|
const prevSection = index > 0 ? slides[index - 1]?.section : void 0;
|
|
2442
3438
|
const showSectionHeader = slideConfig.section && slideConfig.section !== prevSection;
|
|
2443
|
-
return /* @__PURE__ */
|
|
2444
|
-
showSectionHeader && /* @__PURE__ */
|
|
2445
|
-
/* @__PURE__ */
|
|
3439
|
+
return /* @__PURE__ */ jsxs9("div", { className: "contents", children: [
|
|
3440
|
+
showSectionHeader && /* @__PURE__ */ jsx17("h3", { className: "col-span-full mt-4 mb-0 text-xs font-bold tracking-[0.2em] text-neutral-500 uppercase first:mt-0", children: slideConfig.section }),
|
|
3441
|
+
/* @__PURE__ */ jsxs9(
|
|
2446
3442
|
"button",
|
|
2447
3443
|
{
|
|
2448
3444
|
onClick: () => {
|
|
@@ -2451,15 +3447,15 @@ function SlideDeck({
|
|
|
2451
3447
|
},
|
|
2452
3448
|
className: "group relative aspect-video w-full overflow-hidden rounded-lg border border-neutral-800 bg-black shadow-sm transition-all hover:border-[#FF6B35] hover:shadow-lg hover:shadow-[#FF6B35]/10",
|
|
2453
3449
|
children: [
|
|
2454
|
-
/* @__PURE__ */
|
|
2455
|
-
/* @__PURE__ */
|
|
2456
|
-
/* @__PURE__ */
|
|
3450
|
+
/* @__PURE__ */ jsx17(GridThumbnail, { slide: slideConfig, index, total: slides.length }),
|
|
3451
|
+
/* @__PURE__ */ jsx17("div", { className: "absolute inset-0 bg-black/0 transition-colors group-hover:bg-black/20" }),
|
|
3452
|
+
/* @__PURE__ */ jsx17("div", { className: "absolute bottom-2 left-2 rounded bg-black/70 px-2 py-1 text-xs font-medium text-white", children: slideConfig.title ? `${index + 1}. ${slideConfig.title}` : index + 1 })
|
|
2457
3453
|
]
|
|
2458
3454
|
}
|
|
2459
3455
|
)
|
|
2460
3456
|
] }, slideConfig.id ?? index);
|
|
2461
3457
|
}) }) }),
|
|
2462
|
-
/* @__PURE__ */
|
|
3458
|
+
/* @__PURE__ */ jsx17(
|
|
2463
3459
|
"div",
|
|
2464
3460
|
{
|
|
2465
3461
|
className: cn(
|
|
@@ -2467,7 +3463,7 @@ function SlideDeck({
|
|
|
2467
3463
|
"print:m-0 print:block print:max-w-none print:p-0",
|
|
2468
3464
|
viewMode === "list" ? "block" : "hidden print:block"
|
|
2469
3465
|
),
|
|
2470
|
-
children: /* @__PURE__ */
|
|
3466
|
+
children: /* @__PURE__ */ jsx17("div", { className: "grid grid-cols-1 gap-8 print:block", children: slides.map((slideConfig, index) => /* @__PURE__ */ jsx17(
|
|
2471
3467
|
ListSlide,
|
|
2472
3468
|
{
|
|
2473
3469
|
slide: slideConfig,
|
|
@@ -2491,6 +3487,7 @@ export {
|
|
|
2491
3487
|
EASE_MORPH,
|
|
2492
3488
|
EASE_OUT,
|
|
2493
3489
|
ELEMENT_SLIDE_DISTANCE,
|
|
3490
|
+
LoopConfigProvider,
|
|
2494
3491
|
MORPH_DURATION,
|
|
2495
3492
|
MORPH_TRANSITION,
|
|
2496
3493
|
Morph,
|