promptslide 0.3.17 → 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/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,
@@ -616,6 +639,7 @@ function SlideRenderer({
616
639
  return /* @__PURE__ */ jsx6(AnimatePresence, { initial: false, children: /* @__PURE__ */ jsx6(
617
640
  motion3.div,
618
641
  {
642
+ "data-slide-render-index": currentSlide,
619
643
  variants: slideVariants,
620
644
  initial: "enter",
621
645
  animate: "center",
@@ -633,20 +657,14 @@ function SlideRenderer({
633
657
  currentStep: animationStep,
634
658
  totalSteps,
635
659
  showAllAnimations,
636
- children: /* @__PURE__ */ jsx6(
637
- SlideErrorBoundary,
660
+ children: /* @__PURE__ */ jsx6(SlideErrorBoundary, { slideIndex: currentSlide, slideTitle: slides[currentSlide]?.title, children: /* @__PURE__ */ jsx6(
661
+ CurrentSlideComponent,
638
662
  {
639
- slideIndex: currentSlide,
640
- slideTitle: slides[currentSlide]?.title,
641
- children: /* @__PURE__ */ jsx6(
642
- CurrentSlideComponent,
643
- {
644
- slideNumber: currentSlide + 1,
645
- totalSlides: slides.length
646
- }
647
- )
648
- }
649
- )
663
+ slideNumber: currentSlide + 1,
664
+ totalSlides: slides.length
665
+ },
666
+ playbackKey
667
+ ) })
650
668
  }
651
669
  )
652
670
  },
@@ -753,18 +771,17 @@ function SlideEmbed({ slides, transition, directionalTransition }) {
753
771
  // src/core/slide-deck.tsx
754
772
  import { LayoutGroup } from "framer-motion";
755
773
  import {
774
+ ChevronDown as ChevronDown2,
756
775
  ChevronLeft,
757
776
  ChevronRight,
758
- Download,
759
- Grid3X3,
760
- List,
777
+ Download as Download2,
761
778
  MessageCircle,
762
- Monitor,
763
779
  PanelLeftClose,
764
780
  PanelLeftOpen,
765
- Play
781
+ Play as Play2,
782
+ Settings2
766
783
  } from "lucide-react";
767
- import { useCallback as useCallback5, useEffect as useEffect7, useLayoutEffect, useMemo as useMemo4, useRef as useRef6, useState as useState7 } from "react";
784
+ import { useCallback as useCallback6, useEffect as useEffect9, useLayoutEffect as useLayoutEffect2, useMemo as useMemo5, useRef as useRef8, useState as useState10 } from "react";
768
785
 
769
786
  // src/core/annotations/annotation-overlay.tsx
770
787
  import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
@@ -1105,6 +1122,7 @@ import { Check, Pencil, PanelRightClose, RotateCcw, Trash2 } from "lucide-react"
1105
1122
  import { useEffect as useEffect5, useRef as useRef4, useState as useState5 } from "react";
1106
1123
  import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
1107
1124
  function AnnotationPanel({
1125
+ embedded = false,
1108
1126
  annotations,
1109
1127
  unlinkedAnnotations = [],
1110
1128
  selectedId,
@@ -1118,82 +1136,88 @@ function AnnotationPanel({
1118
1136
  const resolved = annotations.filter((a) => a.status === "resolved");
1119
1137
  const total = annotations.length + unlinkedAnnotations.length;
1120
1138
  const numberFor = (annotation) => annotations.findIndex((a) => a.id === annotation.id) + 1;
1121
- return /* @__PURE__ */ jsxs5("div", { className: "flex h-full w-80 max-w-[90vw] flex-shrink-0 flex-col border-l border-neutral-800 bg-neutral-950 text-white", children: [
1122
- /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between border-b border-neutral-800 px-3 py-2", children: [
1123
- /* @__PURE__ */ jsx11(
1124
- "button",
1125
- {
1126
- onClick: onClose,
1127
- className: "rounded p-2 text-neutral-400 hover:bg-neutral-800 hover:text-white",
1128
- "aria-label": "Hide comments",
1129
- title: "Hide comments (C)",
1130
- children: /* @__PURE__ */ jsx11(PanelRightClose, { className: "h-4 w-4" })
1131
- }
1132
- ),
1133
- /* @__PURE__ */ jsxs5("span", { className: "text-right text-sm font-semibold", children: [
1134
- "Comments (",
1135
- total,
1136
- ")"
1137
- ] })
1138
- ] }),
1139
- /* @__PURE__ */ jsxs5("div", { className: "min-h-0 flex-1 overflow-y-auto p-3", children: [
1140
- annotations.length === 0 && unlinkedAnnotations.length === 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-4 px-3 py-6", children: /* @__PURE__ */ jsx11(WorkflowHint, {}) }),
1141
- open.length > 0 && /* @__PURE__ */ jsxs5("div", { children: [
1142
- /* @__PURE__ */ jsx11("div", { className: "mb-1.5 px-2 pt-1 text-[11px] font-medium tracking-wider text-neutral-500 uppercase", children: "Open" }),
1143
- open.map((a) => /* @__PURE__ */ jsx11(
1144
- AnnotationItem,
1145
- {
1146
- annotation: a,
1147
- number: numberFor(a),
1148
- isSelected: a.id === selectedId,
1149
- onSelect: () => onSelect(a.id),
1150
- onHover: (hovered) => onHover(hovered ? a.id : null),
1151
- onDelete: () => onDelete(a.id),
1152
- onUpdate: (patch) => onUpdate(a.id, patch)
1153
- },
1154
- a.id
1155
- ))
1156
- ] }),
1157
- resolved.length > 0 && /* @__PURE__ */ jsxs5("div", { className: open.length > 0 ? "mt-3" : "", children: [
1158
- /* @__PURE__ */ jsx11("div", { className: "mb-1.5 px-2 pt-1 text-[11px] font-medium tracking-wider text-neutral-500 uppercase", children: "Resolved" }),
1159
- resolved.map((a) => /* @__PURE__ */ jsx11(
1160
- AnnotationItem,
1161
- {
1162
- annotation: a,
1163
- number: numberFor(a),
1164
- isSelected: a.id === selectedId,
1165
- onSelect: () => onSelect(a.id),
1166
- onHover: (hovered) => onHover(hovered ? a.id : null),
1167
- onDelete: () => onDelete(a.id),
1168
- onUpdate: (patch) => onUpdate(a.id, patch)
1169
- },
1170
- a.id
1171
- ))
1172
- ] }),
1173
- unlinkedAnnotations.length > 0 && /* @__PURE__ */ jsxs5("div", { className: "mt-3", children: [
1174
- /* @__PURE__ */ jsx11("div", { className: "mb-1.5 px-2 pt-1 text-[11px] font-medium tracking-wider text-neutral-500 uppercase", children: "Removed slides" }),
1175
- unlinkedAnnotations.map((a, i) => /* @__PURE__ */ jsxs5("div", { children: [
1176
- /* @__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: [
1177
1145
  /* @__PURE__ */ jsx11(
1178
- AnnotationItem,
1146
+ "button",
1179
1147
  {
1180
- annotation: a,
1181
- number: i + 1,
1182
- isSelected: a.id === selectedId,
1183
- onSelect: () => onSelect(a.id),
1184
- onHover: (hovered) => onHover(hovered ? a.id : null),
1185
- onDelete: () => onDelete(a.id),
1186
- 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" })
1187
1153
  }
1188
- )
1189
- ] }, a.id))
1190
- ] })
1191
- ] }),
1192
- annotations.length + unlinkedAnnotations.length > 0 && /* @__PURE__ */ jsxs5(Fragment2, { children: [
1193
- /* @__PURE__ */ jsx11("div", { className: "border-t border-neutral-800" }),
1194
- /* @__PURE__ */ jsx11("div", { className: "px-3 py-3", children: /* @__PURE__ */ jsx11(WorkflowHint, {}) })
1195
- ] })
1196
- ] });
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
+ );
1197
1221
  }
1198
1222
  function WorkflowHint() {
1199
1223
  return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
@@ -1480,6 +1504,10 @@ function useAnnotations(slidesOrAdapter, adapter) {
1480
1504
  };
1481
1505
  }
1482
1506
 
1507
+ // src/core/inspector.tsx
1508
+ import { X as X2 } from "lucide-react";
1509
+ import { useId } from "react";
1510
+
1483
1511
  // src/core/utils.ts
1484
1512
  import { clsx } from "clsx";
1485
1513
  import { twMerge } from "tailwind-merge";
@@ -1487,22 +1515,752 @@ function cn(...inputs) {
1487
1515
  return twMerge(clsx(inputs));
1488
1516
  }
1489
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
+
1490
2235
  // src/core/slide-deck.tsx
1491
- import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
2236
+ import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
1492
2237
  var NARROW_VIEWPORT_WIDTH = 768;
1493
2238
  var DEFAULT_THUMBNAIL_WIDTH = 240;
1494
2239
  var MIN_THUMBNAIL_WIDTH = 176;
1495
2240
  var MAX_THUMBNAIL_WIDTH = 480;
1496
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
+ }
1497
2255
  function GridThumbnail({
1498
2256
  slide,
1499
2257
  index,
1500
2258
  total
1501
2259
  }) {
1502
- const viewportRef = useRef6(null);
1503
- const canvasRef = useRef6(null);
2260
+ const viewportRef = useRef8(null);
2261
+ const canvasRef = useRef8(null);
1504
2262
  const SlideComponent = slide.component;
1505
- useLayoutEffect(() => {
2263
+ useLayoutEffect2(() => {
1506
2264
  const viewport = viewportRef.current;
1507
2265
  const canvas = canvasRef.current;
1508
2266
  if (!viewport || !canvas) return;
@@ -1514,21 +2272,21 @@ function GridThumbnail({
1514
2272
  observer.observe(viewport);
1515
2273
  return () => observer.disconnect();
1516
2274
  }, []);
1517
- return /* @__PURE__ */ jsx12("div", { ref: viewportRef, className: "absolute inset-0 overflow-hidden", children: /* @__PURE__ */ jsx12(
2275
+ return /* @__PURE__ */ jsx17("div", { ref: viewportRef, className: "absolute inset-0 overflow-hidden", children: /* @__PURE__ */ jsx17(
1518
2276
  "div",
1519
2277
  {
1520
2278
  ref: canvasRef,
1521
2279
  className: "absolute top-0 left-0 origin-top-left",
1522
2280
  style: { width: SLIDE_DIMENSIONS.width, height: SLIDE_DIMENSIONS.height },
1523
- children: /* @__PURE__ */ jsx12(AnimationProvider, { currentStep: slide.steps, totalSteps: slide.steps, showAllAnimations: true, children: /* @__PURE__ */ jsx12(SlideErrorBoundary, { slideIndex: index, slideTitle: slide.title, children: /* @__PURE__ */ jsx12(SlideComponent, { slideNumber: index + 1, totalSlides: total }) }) })
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 }) }) })
1524
2282
  }
1525
2283
  ) });
1526
2284
  }
1527
2285
  function ListSlide({ slide, index, total }) {
1528
- const viewportRef = useRef6(null);
1529
- const canvasRef = useRef6(null);
2286
+ const viewportRef = useRef8(null);
2287
+ const canvasRef = useRef8(null);
1530
2288
  const SlideComponent = slide.component;
1531
- useLayoutEffect(() => {
2289
+ useLayoutEffect2(() => {
1532
2290
  const viewport = viewportRef.current;
1533
2291
  const canvas = canvasRef.current;
1534
2292
  if (!viewport || !canvas) return;
@@ -1540,29 +2298,29 @@ function ListSlide({ slide, index, total }) {
1540
2298
  observer.observe(viewport);
1541
2299
  return () => observer.disconnect();
1542
2300
  }, []);
1543
- return /* @__PURE__ */ jsx12(
2301
+ return /* @__PURE__ */ jsx17(
1544
2302
  "div",
1545
2303
  {
1546
2304
  ref: viewportRef,
1547
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",
1548
- children: /* @__PURE__ */ jsx12(
2306
+ children: /* @__PURE__ */ jsx17(
1549
2307
  "div",
1550
2308
  {
1551
2309
  ref: canvasRef,
1552
2310
  className: "list-slide-canvas absolute top-0 left-0 origin-top-left",
1553
2311
  style: { width: SLIDE_DIMENSIONS.width, height: SLIDE_DIMENSIONS.height },
1554
- children: /* @__PURE__ */ jsx12(AnimationProvider, { currentStep: slide.steps, totalSteps: slide.steps, showAllAnimations: true, children: /* @__PURE__ */ jsx12(SlideErrorBoundary, { slideIndex: index, slideTitle: slide.title, children: /* @__PURE__ */ jsx12(SlideComponent, { slideNumber: index + 1, totalSlides: total }) }) })
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 }) }) })
1555
2313
  }
1556
2314
  )
1557
2315
  }
1558
2316
  );
1559
2317
  }
1560
2318
  function SlideExportView({ slides, slideIndex }) {
1561
- const [ready, setReady] = useState7(false);
2319
+ const [ready, setReady] = useState10(false);
1562
2320
  const clampedIndex = Math.max(0, Math.min(slideIndex, slides.length - 1));
1563
2321
  const slideConfig = slides[clampedIndex];
1564
2322
  const SlideComponent = slideConfig.component;
1565
- useEffect7(() => {
2323
+ useEffect9(() => {
1566
2324
  let cancelled = false;
1567
2325
  const markReady = async () => {
1568
2326
  await document.fonts.ready;
@@ -1579,7 +2337,7 @@ function SlideExportView({ slides, slideIndex }) {
1579
2337
  cancelled = true;
1580
2338
  };
1581
2339
  }, []);
1582
- return /* @__PURE__ */ jsx12(
2340
+ return /* @__PURE__ */ jsx17(
1583
2341
  "div",
1584
2342
  {
1585
2343
  "data-export-ready": ready ? "true" : void 0,
@@ -1590,13 +2348,13 @@ function SlideExportView({ slides, slideIndex }) {
1590
2348
  position: "relative",
1591
2349
  background: "black"
1592
2350
  },
1593
- children: /* @__PURE__ */ jsx12(
2351
+ children: /* @__PURE__ */ jsx17(
1594
2352
  AnimationProvider,
1595
2353
  {
1596
2354
  currentStep: slideConfig.steps,
1597
2355
  totalSteps: slideConfig.steps,
1598
2356
  showAllAnimations: true,
1599
- children: /* @__PURE__ */ jsx12(SlideErrorBoundary, { slideIndex: clampedIndex, slideTitle: slideConfig.title, children: /* @__PURE__ */ jsx12(SlideComponent, { slideNumber: clampedIndex + 1, totalSlides: slides.length }) })
2357
+ children: /* @__PURE__ */ jsx17(SlideErrorBoundary, { slideIndex: clampedIndex, slideTitle: slideConfig.title, children: /* @__PURE__ */ jsx17(SlideComponent, { slideNumber: clampedIndex + 1, totalSlides: slides.length }) })
1600
2358
  }
1601
2359
  )
1602
2360
  }
@@ -1606,19 +2364,21 @@ function SlideDeck({
1606
2364
  slides,
1607
2365
  transition,
1608
2366
  directionalTransition,
2367
+ loopConfig: suppliedLoopConfig,
2368
+ onLoopConfigChange,
1609
2369
  annotations,
1610
2370
  onAnnotationAdd,
1611
2371
  onAnnotationDelete,
1612
2372
  onAnnotationUpdate
1613
2373
  }) {
1614
- const [exportParams] = useState7(() => {
2374
+ const [exportParams] = useState10(() => {
1615
2375
  if (typeof window === "undefined") return null;
1616
2376
  const params = new URLSearchParams(window.location.search);
1617
2377
  if (params.get("export") !== "true") return null;
1618
2378
  return { slideIndex: parseInt(params.get("slide") || "0", 10) };
1619
2379
  });
1620
2380
  if (exportParams) {
1621
- return /* @__PURE__ */ jsx12(SlideExportView, { slides, slideIndex: exportParams.slideIndex });
2381
+ return /* @__PURE__ */ jsx17(SlideExportView, { slides, slideIndex: exportParams.slideIndex });
1622
2382
  }
1623
2383
  const internal = useAnnotations(slides);
1624
2384
  const isExternallyManaged = annotations !== void 0;
@@ -1626,81 +2386,179 @@ function SlideDeck({
1626
2386
  const effectiveAdd = isExternallyManaged ? onAnnotationAdd : internal.addAnnotation;
1627
2387
  const effectiveDelete = isExternallyManaged ? onAnnotationDelete : internal.deleteAnnotation;
1628
2388
  const effectiveUpdate = isExternallyManaged ? onAnnotationUpdate : internal.updateAnnotation;
1629
- const openCount = useMemo4(
2389
+ const openCount = useMemo5(
1630
2390
  () => effectiveAnnotations.filter((a) => a.status === "open").length,
1631
2391
  [effectiveAnnotations]
1632
2392
  );
1633
- const getSlideAnnotations = useCallback5(
2393
+ const getSlideAnnotations = useCallback6(
1634
2394
  (slideIndex) => effectiveAnnotations.filter(
1635
2395
  (a) => a.slideId ? a.slideId === slides[slideIndex]?.id : a.slideIndex === slideIndex
1636
2396
  ),
1637
2397
  [effectiveAnnotations, slides]
1638
2398
  );
1639
- const unlinkedAnnotations = useMemo4(
2399
+ const unlinkedAnnotations = useMemo5(
1640
2400
  () => effectiveAnnotations.filter((a) => a.slideId && !slides.some((slide) => slide.id === a.slideId)),
1641
2401
  [effectiveAnnotations, slides]
1642
2402
  );
1643
- const [viewMode, setViewMode] = useState7("slide");
1644
- const [isPresentationMode, setIsPresentationMode] = useState7(false);
1645
- const [viewportWidth, setViewportWidth] = useState7(
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(() => {
2441
+ const onEditorState = (event) => {
2442
+ const open = Boolean(event.detail?.open);
2443
+ const changed = editorOpenRef.current !== open;
2444
+ editorOpenRef.current = open;
2445
+ setIsEditorOpen(open);
2446
+ if (open) {
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);
2453
+ }
2454
+ };
2455
+ const onEditorReady = (event) => setEditorAvailable(Boolean(event.detail?.available));
2456
+ const onCloseCancelled = () => {
2457
+ pendingInspector.current = void 0;
2458
+ };
2459
+ const refreshEditor = () => setEditorRevision((value) => value + 1);
2460
+ window.addEventListener("promptslide:editor-state", onEditorState);
2461
+ window.addEventListener("promptslide:editor-ready", onEditorReady);
2462
+ window.addEventListener("promptslide:editor-close-cancelled", onCloseCancelled);
2463
+ window.addEventListener("promptslide:editor-refresh", refreshEditor);
2464
+ return () => {
2465
+ window.removeEventListener("promptslide:editor-state", onEditorState);
2466
+ window.removeEventListener("promptslide:editor-ready", onEditorReady);
2467
+ window.removeEventListener("promptslide:editor-close-cancelled", onCloseCancelled);
2468
+ window.removeEventListener("promptslide:editor-refresh", refreshEditor);
2469
+ };
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(
1646
2506
  () => typeof window === "undefined" ? 1280 : window.innerWidth
1647
2507
  );
1648
- const [showThumbnailRail, setShowThumbnailRail] = useState7(
2508
+ const [showThumbnailRail, setShowThumbnailRail] = useState10(
1649
2509
  () => typeof window === "undefined" ? true : window.innerWidth >= NARROW_VIEWPORT_WIDTH
1650
2510
  );
1651
- const [thumbnailWidth, setThumbnailWidth] = useState7(() => {
2511
+ const [thumbnailWidth, setThumbnailWidth] = useState10(() => {
1652
2512
  if (typeof window === "undefined") return DEFAULT_THUMBNAIL_WIDTH;
1653
2513
  const savedWidth = Number(window.localStorage.getItem(THUMBNAIL_WIDTH_STORAGE_KEY));
1654
2514
  return Number.isFinite(savedWidth) && savedWidth >= MIN_THUMBNAIL_WIDTH ? Math.min(savedWidth, MAX_THUMBNAIL_WIDTH) : DEFAULT_THUMBNAIL_WIDTH;
1655
2515
  });
1656
- const [isAnnotationMode, setIsAnnotationMode] = useState7(false);
1657
- const [showAnnotationPanel, setShowAnnotationPanel] = useState7(false);
1658
- const [selectedAnnotationId, setSelectedAnnotationId] = useState7(null);
1659
- const [hoveredAnnotationId, setHoveredAnnotationId] = useState7(null);
1660
- const [isResizingThumbnails, setIsResizingThumbnails] = useState7(false);
1661
- const [isDragCollapsed, setIsDragCollapsed] = useState7(false);
1662
- const [isAnimatingDragBoundary, setIsAnimatingDragBoundary] = useState7(false);
1663
- const [scale, setScale] = useState7(1);
1664
- const containerRef = useRef6(null);
1665
- const slideContainerRef = useRef6(null);
1666
- const previewViewportRef = useRef6(null);
1667
- const activeThumbnailRef = useRef6(null);
1668
- const preferredRailOpenRef = useRef6(true);
1669
- const wasNarrowViewportRef = useRef6(viewportWidth < NARROW_VIEWPORT_WIDTH);
1670
- const resizeStartRef = useRef6(null);
1671
- const previousUserSelectRef = useRef6("");
1672
- const previousCursorRef = useRef6("");
1673
- const dragCollapsedRef = useRef6(false);
1674
- 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);
1675
2534
  const isNarrowViewport = viewportWidth < NARROW_VIEWPORT_WIDTH;
1676
2535
  const maxThumbnailWidth = isNarrowViewport ? Math.max(120, Math.floor(viewportWidth * 0.8)) : Math.min(MAX_THUMBNAIL_WIDTH, viewportWidth - 360);
1677
2536
  const minThumbnailWidth = Math.min(MIN_THUMBNAIL_WIDTH, maxThumbnailWidth);
1678
2537
  const visibleThumbnailWidth = Math.min(thumbnailWidth, maxThumbnailWidth);
1679
2538
  const isThumbnailRailVisible = showThumbnailRail && !isDragCollapsed;
1680
- const setThumbnailRailOpen = useCallback5((open) => {
2539
+ const setThumbnailRailOpen = useCallback6((open) => {
1681
2540
  preferredRailOpenRef.current = open;
1682
2541
  setShowThumbnailRail(open);
1683
2542
  }, []);
1684
- useEffect7(() => {
2543
+ useEffect9(() => {
1685
2544
  const handleResize = () => setViewportWidth(window.innerWidth);
1686
2545
  window.addEventListener("resize", handleResize);
1687
2546
  return () => window.removeEventListener("resize", handleResize);
1688
2547
  }, []);
1689
- useEffect7(() => {
2548
+ useEffect9(() => {
1690
2549
  if (isNarrowViewport !== wasNarrowViewportRef.current) {
1691
2550
  setShowThumbnailRail(isNarrowViewport ? false : preferredRailOpenRef.current);
1692
2551
  if (isNarrowViewport) {
1693
- setIsAnnotationMode(false);
1694
- setShowAnnotationPanel(false);
2552
+ setInspectorMode((mode) => mode === "comments" ? null : mode);
1695
2553
  setSelectedAnnotationId(null);
1696
2554
  }
1697
2555
  wasNarrowViewportRef.current = isNarrowViewport;
1698
2556
  }
1699
2557
  }, [isNarrowViewport]);
1700
- useEffect7(() => {
2558
+ useEffect9(() => {
1701
2559
  window.localStorage.setItem(THUMBNAIL_WIDTH_STORAGE_KEY, String(thumbnailWidth));
1702
2560
  }, [thumbnailWidth]);
1703
- useEffect7(
2561
+ useEffect9(
1704
2562
  () => () => {
1705
2563
  if (resizeStartRef.current) {
1706
2564
  document.body.style.userSelect = previousUserSelectRef.current;
@@ -1710,7 +2568,7 @@ function SlideDeck({
1710
2568
  },
1711
2569
  []
1712
2570
  );
1713
- useEffect7(() => {
2571
+ useEffect9(() => {
1714
2572
  if ((!showThumbnailRail || isPresentationMode || viewMode !== "slide") && resizeStartRef.current) {
1715
2573
  resizeStartRef.current = null;
1716
2574
  dragCollapsedRef.current = false;
@@ -1722,7 +2580,7 @@ function SlideDeck({
1722
2580
  document.body.style.cursor = previousCursorRef.current;
1723
2581
  }
1724
2582
  }, [showThumbnailRail, isPresentationMode, viewMode]);
1725
- useEffect7(() => {
2583
+ useEffect9(() => {
1726
2584
  if (!isResizingThumbnails) return;
1727
2585
  const getDraggedWidth = (clientX) => {
1728
2586
  const start = resizeStartRef.current;
@@ -1776,35 +2634,113 @@ function SlideDeck({
1776
2634
  currentSlide,
1777
2635
  animationStep,
1778
2636
  totalSteps,
2637
+ isTransitioning,
1779
2638
  direction,
1780
2639
  showAllAnimations,
1781
2640
  advance,
1782
2641
  goBack,
1783
2642
  goToSlide,
2643
+ goToStep,
2644
+ restartAtSlide,
1784
2645
  onTransitionComplete
1785
2646
  } = useSlideNavigation({
1786
2647
  slides
1787
2648
  });
1788
- const togglePresentationMode = useCallback5(async () => {
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 () => {
1789
2724
  if (!document.fullscreenElement) {
1790
2725
  await document.documentElement.requestFullscreen();
1791
2726
  } else {
1792
2727
  await document.exitFullscreen();
1793
2728
  }
1794
2729
  }, []);
1795
- useEffect7(() => {
2730
+ useEffect9(() => {
1796
2731
  const handleFullscreenChange = () => {
1797
2732
  setIsPresentationMode(!!document.fullscreenElement);
2733
+ if (!document.fullscreenElement) setIsLooping(false);
1798
2734
  };
1799
2735
  document.addEventListener("fullscreenchange", handleFullscreenChange);
1800
2736
  return () => document.removeEventListener("fullscreenchange", handleFullscreenChange);
1801
2737
  }, []);
1802
- useEffect7(() => {
2738
+ useEffect9(() => {
1803
2739
  if (viewMode === "slide" && !isPresentationMode && showThumbnailRail) {
1804
2740
  activeThumbnailRef.current?.scrollIntoView({ block: "nearest" });
1805
2741
  }
1806
2742
  }, [currentSlide, isPresentationMode, showThumbnailRail, viewMode]);
1807
- useEffect7(() => {
2743
+ useEffect9(() => {
1808
2744
  const calculateScale = () => {
1809
2745
  if (!isPresentationMode) {
1810
2746
  setScale(1);
@@ -1820,7 +2756,7 @@ function SlideDeck({
1820
2756
  window.addEventListener("resize", calculateScale);
1821
2757
  return () => window.removeEventListener("resize", calculateScale);
1822
2758
  }, [isPresentationMode]);
1823
- useLayoutEffect(() => {
2759
+ useLayoutEffect2(() => {
1824
2760
  const viewport = previewViewportRef.current;
1825
2761
  const canvas = slideContainerRef.current;
1826
2762
  if (!viewport || !canvas) return;
@@ -1836,16 +2772,10 @@ function SlideDeck({
1836
2772
  observer.observe(viewport);
1837
2773
  return () => observer.disconnect();
1838
2774
  }, [viewMode, isPresentationMode]);
1839
- const toggleCommentMode = useCallback5(() => {
1840
- const next = !isAnnotationMode;
1841
- setIsAnnotationMode(next);
1842
- setShowAnnotationPanel(next);
1843
- if (!next) {
1844
- setSelectedAnnotationId(null);
1845
- setHoveredAnnotationId(null);
1846
- }
1847
- }, [isAnnotationMode]);
1848
- const handleExportPdf = useCallback5(async () => {
2775
+ const toggleCommentMode = useCallback6(() => {
2776
+ requestInspector(isAnnotationMode && viewMode === "slide" ? null : "comments");
2777
+ }, [isAnnotationMode, viewMode, requestInspector]);
2778
+ const handleExportPdf = useCallback6(async () => {
1849
2779
  try {
1850
2780
  const response = await fetch("/__promptslide_pdf");
1851
2781
  if (response.ok && response.headers.get("content-type")?.includes("application/pdf")) {
@@ -1871,13 +2801,36 @@ function SlideDeck({
1871
2801
  window.print();
1872
2802
  }, 100);
1873
2803
  }, [viewMode]);
1874
- useEffect7(() => {
2804
+ useEffect9(() => {
1875
2805
  const handleKeyDown = (e) => {
1876
2806
  const target = e.target;
1877
2807
  if (target?.closest("input, textarea, [contenteditable='true']")) return;
1878
2808
  if (e.altKey || e.ctrlKey || e.metaKey) return;
1879
2809
  const key = e.key.toLowerCase();
1880
2810
  if (e.repeat && "fvglcdt".includes(key)) return;
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
+ }
1881
2834
  if (key === "f") {
1882
2835
  e.preventDefault();
1883
2836
  void togglePresentationMode();
@@ -1927,17 +2880,46 @@ function SlideDeck({
1927
2880
  }, [
1928
2881
  advance,
1929
2882
  goBack,
2883
+ playback,
2884
+ isLooping,
2885
+ loopMenuOpen,
2886
+ loopStartScreen,
1930
2887
  viewMode,
1931
2888
  togglePresentationMode,
1932
2889
  isAnnotationMode,
2890
+ isEditorOpen,
1933
2891
  isPresentationMode,
1934
2892
  setThumbnailRailOpen,
1935
2893
  showThumbnailRail,
1936
2894
  toggleCommentMode,
1937
2895
  handleExportPdf
1938
2896
  ]);
1939
- return /* @__PURE__ */ jsxs6("div", { className: "min-h-screen w-full bg-neutral-950 text-foreground", children: [
1940
- /* @__PURE__ */ jsx12("style", { children: `
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: `
2901
+ .slide-thumbnail-scroll-area {
2902
+ scrollbar-width: thin;
2903
+ scrollbar-color: #404040 transparent;
2904
+ }
2905
+ .slide-thumbnail-scroll-area::-webkit-scrollbar {
2906
+ width: 10px;
2907
+ }
2908
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-track {
2909
+ background: transparent;
2910
+ }
2911
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-thumb {
2912
+ background-color: #404040;
2913
+ border: 3px solid transparent;
2914
+ border-radius: 9999px;
2915
+ background-clip: padding-box;
2916
+ }
2917
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-thumb:hover {
2918
+ background-color: #525252;
2919
+ }
2920
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-corner {
2921
+ background: transparent;
2922
+ }
1941
2923
  @media print {
1942
2924
  .list-slide-canvas {
1943
2925
  transform: scale(1.5) !important;
@@ -1961,99 +2943,161 @@ function SlideDeck({
1961
2943
  }
1962
2944
  }
1963
2945
  ` }),
1964
- /* @__PURE__ */ jsxs6(
2946
+ loopError && /* @__PURE__ */ jsxs9(
1965
2947
  "div",
1966
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(
2989
+ "div",
2990
+ {
2991
+ "data-ps-viewer-toolbar": true,
2992
+ style: inspectorVisible ? { right: inspectorWidth + 16, maxWidth: `calc(100vw - ${inspectorWidth + 32}px)` } : void 0,
1967
2993
  className: cn(
1968
- "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",
1969
- (isPresentationMode || isNarrowViewport && isAnnotationMode && showAnnotationPanel) && "hidden",
1970
- isAnnotationMode && showAnnotationPanel ? "right-[21rem]" : "right-4"
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",
2996
+ isEditorOpen && viewportWidth <= 850 && "[&_button:disabled]:hidden [&>div]:hidden",
2997
+ !inspectorVisible && "right-4"
1971
2998
  ),
1972
2999
  children: [
1973
- /* @__PURE__ */ jsx12(
1974
- "button",
1975
- {
1976
- onClick: () => setViewMode("slide"),
1977
- className: cn(
1978
- "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
1979
- viewMode === "slide" && "bg-neutral-800 text-white"
1980
- ),
1981
- title: "Presentation View (V)",
1982
- children: /* @__PURE__ */ jsx12(Monitor, { className: "h-4 w-4" })
1983
- }
1984
- ),
1985
- /* @__PURE__ */ jsx12(
1986
- "button",
1987
- {
1988
- onClick: () => setViewMode("list"),
1989
- className: cn(
1990
- "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
1991
- viewMode === "list" && "bg-neutral-800 text-white"
1992
- ),
1993
- title: "List View (L)",
1994
- children: /* @__PURE__ */ jsx12(List, { className: "h-4 w-4" })
1995
- }
1996
- ),
1997
- /* @__PURE__ */ jsx12(
1998
- "button",
1999
- {
2000
- onClick: () => setViewMode("grid"),
2001
- className: cn(
2002
- "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
2003
- viewMode === "grid" && "bg-neutral-800 text-white"
2004
- ),
2005
- title: "Grid View (G)",
2006
- children: /* @__PURE__ */ jsx12(Grid3X3, { className: "h-4 w-4" })
2007
- }
2008
- ),
2009
- /* @__PURE__ */ jsx12("div", { className: "mx-1 w-px bg-neutral-800" }),
2010
- /* @__PURE__ */ jsxs6(
2011
- "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,
2012
3004
  {
2013
3005
  onClick: toggleCommentMode,
2014
- className: cn(
2015
- "relative inline-flex h-8 items-center justify-center rounded-md transition-colors",
2016
- isAnnotationMode ? "border border-[#FF6B35]/50 bg-[#FF6B35]/15 p-[7px] text-[#FF6B35] hover:border-[#FF6B35] hover:bg-[#FF6B35]/25" : "p-2 text-neutral-400 hover:bg-neutral-800 hover:text-white"
2017
- ),
3006
+ label: "Comment",
3007
+ selected: isAnnotationMode,
2018
3008
  title: "Comment (C)",
2019
3009
  children: [
2020
- /* @__PURE__ */ jsx12(MessageCircle, { className: "h-4 w-4" }),
2021
- 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 })
2022
3012
  ]
2023
3013
  }
2024
3014
  ),
2025
- /* @__PURE__ */ jsx12("div", { className: "mx-1 w-px bg-neutral-800" }),
2026
- /* @__PURE__ */ jsx12(
2027
- "button",
3015
+ /* @__PURE__ */ jsx17("span", { "data-ps-editor-toolbar": true, className: "contents" }),
3016
+ /* @__PURE__ */ jsx17(
3017
+ ToolbarAction,
2028
3018
  {
2029
- onClick: handleExportPdf,
2030
- className: "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
2031
- title: "Download PDF (D)",
2032
- 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 })
2033
3023
  }
2034
3024
  ),
2035
- /* @__PURE__ */ jsxs6(
2036
- "button",
3025
+ /* @__PURE__ */ jsx17("div", { className: "mx-1 w-px bg-neutral-800" }),
3026
+ /* @__PURE__ */ jsx17(
3027
+ ToolbarAction,
2037
3028
  {
2038
- onClick: togglePresentationMode,
2039
- 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",
2040
- title: "Present (F)",
2041
- children: [
2042
- /* @__PURE__ */ jsx12(Play, { className: "h-3.5 w-3.5 fill-current" }),
2043
- /* @__PURE__ */ jsx12("span", { children: "Present" })
2044
- ]
3029
+ onClick: handleExportPdf,
3030
+ disabled: isEditorOpen,
3031
+ label: "Download PDF",
3032
+ title: "Download PDF (D)",
3033
+ children: /* @__PURE__ */ jsx17(Download2, { className: "h-4 w-4" })
2045
3034
  }
2046
- )
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
+ ] })
2047
3091
  ]
2048
3092
  }
2049
3093
  ),
2050
- viewMode === "slide" && /* @__PURE__ */ jsxs6(
3094
+ viewMode === "slide" && /* @__PURE__ */ jsxs9(
2051
3095
  "div",
2052
3096
  {
2053
3097
  className: cn("flex h-screen w-full print:hidden", isPresentationMode ? "bg-black" : ""),
2054
3098
  children: [
2055
- !isPresentationMode && /* @__PURE__ */ jsxs6(Fragment3, { children: [
2056
- /* @__PURE__ */ jsxs6(
3099
+ !isPresentationMode && /* @__PURE__ */ jsxs9(Fragment4, { children: [
3100
+ /* @__PURE__ */ jsxs9(
2057
3101
  "div",
2058
3102
  {
2059
3103
  className: cn(
@@ -2063,7 +3107,7 @@ function SlideDeck({
2063
3107
  ),
2064
3108
  style: { width: isThumbnailRailVisible ? visibleThumbnailWidth : 0 },
2065
3109
  children: [
2066
- /* @__PURE__ */ jsx12("div", { className: "h-full w-full overflow-hidden", children: /* @__PURE__ */ jsxs6(
3110
+ /* @__PURE__ */ jsx17("div", { className: "h-full w-full overflow-hidden", children: /* @__PURE__ */ jsxs9(
2067
3111
  "aside",
2068
3112
  {
2069
3113
  id: "presentation-thumbnails",
@@ -2076,13 +3120,13 @@ function SlideDeck({
2076
3120
  ),
2077
3121
  style: { width: visibleThumbnailWidth },
2078
3122
  children: [
2079
- /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between border-b border-neutral-800 px-3 py-2", children: [
2080
- /* @__PURE__ */ jsxs6("span", { className: "text-sm font-semibold", children: [
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: [
2081
3125
  "Slides (",
2082
3126
  slides.length,
2083
3127
  ")"
2084
3128
  ] }),
2085
- /* @__PURE__ */ jsx12(
3129
+ /* @__PURE__ */ jsx17(
2086
3130
  "button",
2087
3131
  {
2088
3132
  type: "button",
@@ -2090,11 +3134,11 @@ function SlideDeck({
2090
3134
  className: "rounded p-2 text-neutral-400 hover:bg-neutral-800 hover:text-white",
2091
3135
  "aria-label": "Hide slide thumbnails",
2092
3136
  title: "Hide thumbnails (T)",
2093
- children: /* @__PURE__ */ jsx12(PanelLeftClose, { className: "h-4 w-4" })
3137
+ children: /* @__PURE__ */ jsx17(PanelLeftClose, { className: "h-4 w-4" })
2094
3138
  }
2095
3139
  )
2096
3140
  ] }),
2097
- /* @__PURE__ */ jsx12("div", { className: "min-h-0 flex-1 space-y-3 overflow-y-auto p-3", children: slides.map((slideConfig, index) => /* @__PURE__ */ jsxs6(
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(
2098
3142
  "button",
2099
3143
  {
2100
3144
  ref: index === currentSlide ? activeThumbnailRef : void 0,
@@ -2107,18 +3151,21 @@ function SlideDeck({
2107
3151
  index === currentSlide ? "border-[#FF6B35] bg-[#FF6B35]/15 ring-1 ring-[#FF6B35]" : "border-transparent hover:border-neutral-600 hover:bg-neutral-800"
2108
3152
  ),
2109
3153
  children: [
2110
- /* @__PURE__ */ jsx12("div", { className: "relative aspect-video w-full overflow-hidden rounded bg-black", children: /* @__PURE__ */ jsx12(
2111
- GridThumbnail,
2112
- {
2113
- slide: slideConfig,
2114
- index,
2115
- total: slides.length
2116
- }
2117
- ) }),
2118
- /* @__PURE__ */ jsx12("div", { className: "mt-1.5 flex items-baseline gap-2 px-0.5 text-xs", children: slideConfig.title ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
2119
- /* @__PURE__ */ jsx12("span", { className: "font-mono text-neutral-400", children: index + 1 }),
2120
- /* @__PURE__ */ jsx12("span", { className: "truncate", children: slideConfig.title })
2121
- ] }) : /* @__PURE__ */ jsxs6("span", { children: [
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: [
2122
3169
  "Slide ",
2123
3170
  index + 1
2124
3171
  ] }) })
@@ -2129,7 +3176,7 @@ function SlideDeck({
2129
3176
  ]
2130
3177
  }
2131
3178
  ) }),
2132
- isThumbnailRailVisible && /* @__PURE__ */ jsx12(
3179
+ isThumbnailRailVisible && /* @__PURE__ */ jsx17(
2133
3180
  "div",
2134
3181
  {
2135
3182
  role: "separator",
@@ -2178,16 +3225,17 @@ function SlideDeck({
2178
3225
  ]
2179
3226
  }
2180
3227
  ),
2181
- /* @__PURE__ */ jsx12(
3228
+ /* @__PURE__ */ jsx17(
2182
3229
  "div",
2183
3230
  {
2184
3231
  "aria-hidden": isThumbnailRailVisible || isResizingThumbnails,
2185
3232
  inert: isThumbnailRailVisible || isResizingThumbnails,
2186
3233
  className: cn(
2187
- "fixed top-4 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",
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",
2188
3236
  isThumbnailRailVisible || isResizingThumbnails ? "pointer-events-none opacity-0" : "opacity-100 delay-200"
2189
3237
  ),
2190
- children: /* @__PURE__ */ jsx12(
3238
+ children: /* @__PURE__ */ jsx17(
2191
3239
  "button",
2192
3240
  {
2193
3241
  type: "button",
@@ -2195,28 +3243,34 @@ function SlideDeck({
2195
3243
  className: "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
2196
3244
  "aria-label": "Show slide thumbnails",
2197
3245
  title: "Show thumbnails (T)",
2198
- children: /* @__PURE__ */ jsx12(PanelLeftOpen, { className: "h-4 w-4" })
3246
+ children: /* @__PURE__ */ jsx17(PanelLeftOpen, { className: "h-4 w-4" })
2199
3247
  }
2200
3248
  )
2201
3249
  }
2202
3250
  )
2203
3251
  ] }),
2204
- /* @__PURE__ */ jsxs6(
3252
+ /* @__PURE__ */ jsxs9(
2205
3253
  "div",
2206
3254
  {
2207
3255
  ref: containerRef,
2208
3256
  role: "presentation",
3257
+ "data-loop-running": isLooping ? "true" : void 0,
2209
3258
  tabIndex: isPresentationMode ? 0 : void 0,
2210
3259
  className: cn(
2211
3260
  "flex min-w-0 flex-1 flex-col items-center justify-center overflow-hidden",
2212
- isPresentationMode ? "bg-black p-0" : "p-4 md:p-8"
3261
+ isPresentationMode ? `bg-black p-0 ${isLooping ? "cursor-none" : ""}` : "p-4 md:p-8"
2213
3262
  ),
2214
- onClick: isPresentationMode ? advance : void 0,
3263
+ onClick: isPresentationMode ? () => isLooping ? playback.move(1) : advance() : void 0,
2215
3264
  onKeyDown: isPresentationMode ? (e) => {
2216
- if (e.key === "Enter" || e.key === " ") advance();
3265
+ if (e.key === "Enter") {
3266
+ e.preventDefault();
3267
+ e.stopPropagation();
3268
+ if (isLooping) playback.move(1);
3269
+ else advance();
3270
+ }
2217
3271
  } : void 0,
2218
3272
  children: [
2219
- /* @__PURE__ */ jsx12(LayoutGroup, { id: "slide-deck", children: isPresentationMode ? /* @__PURE__ */ jsx12(
3273
+ /* @__PURE__ */ jsx17(LayoutGroup, { id: "slide-deck", children: isPresentationMode ? /* @__PURE__ */ jsx17(
2220
3274
  "div",
2221
3275
  {
2222
3276
  className: "pointer-events-none relative overflow-hidden bg-black",
@@ -2226,9 +3280,10 @@ function SlideDeck({
2226
3280
  transform: `scale(${scale})`,
2227
3281
  transformOrigin: "center center"
2228
3282
  },
2229
- children: /* @__PURE__ */ jsx12(
3283
+ children: /* @__PURE__ */ jsx17(
2230
3284
  SlideRenderer,
2231
3285
  {
3286
+ playbackKey: isLooping ? playback.epoch : void 0,
2232
3287
  slides,
2233
3288
  currentSlide,
2234
3289
  animationStep,
@@ -2241,124 +3296,136 @@ function SlideDeck({
2241
3296
  }
2242
3297
  )
2243
3298
  }
2244
- ) : /* @__PURE__ */ jsx12(
3299
+ ) : /* @__PURE__ */ jsxs9(
2245
3300
  "div",
2246
3301
  {
2247
3302
  ref: previewViewportRef,
2248
3303
  className: "relative aspect-video w-full max-w-7xl overflow-hidden rounded-xl border border-neutral-800 bg-black shadow-2xl",
2249
- children: /* @__PURE__ */ jsxs6(
2250
- "div",
2251
- {
2252
- ref: slideContainerRef,
2253
- className: "absolute top-0 left-0 overflow-hidden",
2254
- style: {
2255
- width: SLIDE_DIMENSIONS.width,
2256
- height: SLIDE_DIMENSIONS.height,
2257
- transformOrigin: "top left"
2258
- },
2259
- children: [
2260
- /* @__PURE__ */ jsx12(
2261
- SlideRenderer,
2262
- {
2263
- slides,
2264
- currentSlide,
2265
- animationStep,
2266
- totalSteps,
2267
- direction,
2268
- showAllAnimations,
2269
- transition,
2270
- directionalTransition,
2271
- onTransitionComplete
2272
- }
2273
- ),
2274
- isAnnotationMode && /* @__PURE__ */ jsx12(
2275
- AnnotationOverlay,
2276
- {
2277
- slides,
2278
- currentSlide,
2279
- slideContainerRef,
2280
- selectedId: selectedAnnotationId,
2281
- hoveredId: hoveredAnnotationId,
2282
- onSelectId: setSelectedAnnotationId,
2283
- onShowPanel: () => setShowAnnotationPanel(true),
2284
- slideAnnotations: getSlideAnnotations(currentSlide),
2285
- addAnnotation: effectiveAdd ?? (() => {
2286
- })
2287
- }
2288
- )
2289
- ]
2290
- }
2291
- )
3304
+ children: [
3305
+ /* @__PURE__ */ jsxs9(
3306
+ "div",
3307
+ {
3308
+ ref: slideContainerRef,
3309
+ "data-ps-editor-canvas": true,
3310
+ tabIndex: -1,
3311
+ "data-slide-index": currentSlide,
3312
+ className: "absolute top-0 left-0 overflow-hidden outline-none",
3313
+ style: {
3314
+ width: SLIDE_DIMENSIONS.width,
3315
+ height: SLIDE_DIMENSIONS.height,
3316
+ transformOrigin: "top left"
3317
+ },
3318
+ children: [
3319
+ /* @__PURE__ */ jsx17(
3320
+ SlideRenderer,
3321
+ {
3322
+ slides,
3323
+ currentSlide,
3324
+ animationStep,
3325
+ totalSteps,
3326
+ direction,
3327
+ showAllAnimations,
3328
+ transition,
3329
+ directionalTransition,
3330
+ onTransitionComplete
3331
+ },
3332
+ editorRevision
3333
+ ),
3334
+ isAnnotationMode && /* @__PURE__ */ jsx17(
3335
+ AnnotationOverlay,
3336
+ {
3337
+ slides,
3338
+ currentSlide,
3339
+ slideContainerRef,
3340
+ selectedId: selectedAnnotationId,
3341
+ hoveredId: hoveredAnnotationId,
3342
+ onSelectId: setSelectedAnnotationId,
3343
+ onShowPanel: () => requestInspector("comments"),
3344
+ slideAnnotations: getSlideAnnotations(currentSlide),
3345
+ addAnnotation: effectiveAdd ?? (() => {
3346
+ })
3347
+ }
3348
+ )
3349
+ ]
3350
+ }
3351
+ ),
3352
+ /* @__PURE__ */ jsx17(
3353
+ "div",
3354
+ {
3355
+ "data-ps-editor-overlay": true,
3356
+ className: "pointer-events-none absolute inset-0 z-20"
3357
+ }
3358
+ )
3359
+ ]
2292
3360
  }
2293
3361
  ) }),
2294
- !isPresentationMode && /* @__PURE__ */ jsxs6("div", { className: "mt-6 flex items-center gap-4", children: [
2295
- /* @__PURE__ */ jsx12(
3362
+ !isPresentationMode && /* @__PURE__ */ jsxs9("div", { className: "mt-6 flex items-center gap-4", children: [
3363
+ /* @__PURE__ */ jsx17(
2296
3364
  "button",
2297
3365
  {
2298
3366
  onClick: goBack,
2299
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",
2300
3368
  title: "Previous step or slide (\u2190)",
2301
- children: /* @__PURE__ */ jsx12(ChevronLeft, { className: "h-5 w-5" })
3369
+ children: /* @__PURE__ */ jsx17(ChevronLeft, { className: "h-5 w-5" })
2302
3370
  }
2303
3371
  ),
2304
- /* @__PURE__ */ jsx12("div", { className: "flex min-w-[4rem] flex-col items-center", children: /* @__PURE__ */ jsxs6("span", { className: "font-mono text-sm text-neutral-500", children: [
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: [
2305
3373
  currentSlide + 1,
2306
3374
  " / ",
2307
3375
  slides.length
2308
3376
  ] }) }),
2309
- /* @__PURE__ */ jsx12(
3377
+ /* @__PURE__ */ jsx17(
2310
3378
  "button",
2311
3379
  {
2312
3380
  onClick: advance,
2313
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",
2314
3382
  title: "Next step or slide (\u2192 / Space)",
2315
- children: /* @__PURE__ */ jsx12(ChevronRight, { className: "h-5 w-5" })
3383
+ children: /* @__PURE__ */ jsx17(ChevronRight, { className: "h-5 w-5" })
2316
3384
  }
2317
3385
  )
2318
3386
  ] })
2319
3387
  ]
2320
3388
  }
2321
3389
  ),
2322
- !isPresentationMode && /* @__PURE__ */ jsx12(
2323
- "div",
3390
+ /* @__PURE__ */ jsx17(
3391
+ Inspector,
2324
3392
  {
2325
- "aria-hidden": !isAnnotationMode || !showAnnotationPanel,
2326
- inert: !isAnnotationMode || !showAnnotationPanel,
2327
- className: cn(
2328
- "h-full shrink-0 overflow-hidden transition-[width,transform] duration-200 ease-out motion-reduce:transition-none",
2329
- isNarrowViewport && "fixed inset-y-0 right-0 z-50 shadow-xl",
2330
- isNarrowViewport && (!isAnnotationMode || !showAnnotationPanel) && "translate-x-full"
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
+ }
2331
3414
  ),
2332
- style: {
2333
- width: isAnnotationMode && showAnnotationPanel ? isNarrowViewport ? "min(20rem, 90vw)" : "20rem" : 0
2334
- },
2335
- children: /* @__PURE__ */ jsx12(
2336
- "div",
3415
+ comments: /* @__PURE__ */ jsx17(
3416
+ AnnotationPanel,
2337
3417
  {
2338
- className: cn(
2339
- "h-full transition-opacity motion-reduce:transition-none",
2340
- isAnnotationMode && showAnnotationPanel ? "opacity-100 delay-100 duration-100" : "opacity-0 duration-75"
2341
- ),
2342
- children: /* @__PURE__ */ jsx12(
2343
- AnnotationPanel,
2344
- {
2345
- annotations: getSlideAnnotations(currentSlide),
2346
- unlinkedAnnotations,
2347
- selectedId: selectedAnnotationId,
2348
- onSelect: setSelectedAnnotationId,
2349
- onHover: setHoveredAnnotationId,
2350
- onDelete: effectiveDelete ?? (() => {
2351
- }),
2352
- onUpdate: effectiveUpdate ?? (() => {
2353
- }),
2354
- onClose: () => {
2355
- setIsAnnotationMode(false);
2356
- setShowAnnotationPanel(false);
2357
- setSelectedAnnotationId(null);
2358
- setHoveredAnnotationId(null);
2359
- }
2360
- }
2361
- )
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)
2362
3429
  }
2363
3430
  )
2364
3431
  }
@@ -2366,12 +3433,12 @@ function SlideDeck({
2366
3433
  ]
2367
3434
  }
2368
3435
  ),
2369
- viewMode === "grid" && /* @__PURE__ */ jsx12("div", { className: "mx-auto max-w-7xl p-8 pt-16 print:hidden", children: /* @__PURE__ */ jsx12("div", { className: "grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4", children: slides.map((slideConfig, index) => {
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) => {
2370
3437
  const prevSection = index > 0 ? slides[index - 1]?.section : void 0;
2371
3438
  const showSectionHeader = slideConfig.section && slideConfig.section !== prevSection;
2372
- return /* @__PURE__ */ jsxs6("div", { className: "contents", children: [
2373
- showSectionHeader && /* @__PURE__ */ jsx12("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 }),
2374
- /* @__PURE__ */ jsxs6(
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(
2375
3442
  "button",
2376
3443
  {
2377
3444
  onClick: () => {
@@ -2380,15 +3447,15 @@ function SlideDeck({
2380
3447
  },
2381
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",
2382
3449
  children: [
2383
- /* @__PURE__ */ jsx12(GridThumbnail, { slide: slideConfig, index, total: slides.length }),
2384
- /* @__PURE__ */ jsx12("div", { className: "absolute inset-0 bg-black/0 transition-colors group-hover:bg-black/20" }),
2385
- /* @__PURE__ */ jsx12("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 })
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 })
2386
3453
  ]
2387
3454
  }
2388
3455
  )
2389
3456
  ] }, slideConfig.id ?? index);
2390
3457
  }) }) }),
2391
- /* @__PURE__ */ jsx12(
3458
+ /* @__PURE__ */ jsx17(
2392
3459
  "div",
2393
3460
  {
2394
3461
  className: cn(
@@ -2396,7 +3463,7 @@ function SlideDeck({
2396
3463
  "print:m-0 print:block print:max-w-none print:p-0",
2397
3464
  viewMode === "list" ? "block" : "hidden print:block"
2398
3465
  ),
2399
- children: /* @__PURE__ */ jsx12("div", { className: "grid grid-cols-1 gap-8 print:block", children: slides.map((slideConfig, index) => /* @__PURE__ */ jsx12(
3466
+ children: /* @__PURE__ */ jsx17("div", { className: "grid grid-cols-1 gap-8 print:block", children: slides.map((slideConfig, index) => /* @__PURE__ */ jsx17(
2400
3467
  ListSlide,
2401
3468
  {
2402
3469
  slide: slideConfig,
@@ -2420,6 +3487,7 @@ export {
2420
3487
  EASE_MORPH,
2421
3488
  EASE_OUT,
2422
3489
  ELEMENT_SLIDE_DISTANCE,
3490
+ LoopConfigProvider,
2423
3491
  MORPH_DURATION,
2424
3492
  MORPH_TRANSITION,
2425
3493
  Morph,