promptslide 0.3.18 → 0.3.19

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