foss-design 0.3.6 → 0.4.1

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.
@@ -2585,14 +2585,14 @@ function readPref() {
2585
2585
  return "system";
2586
2586
  }
2587
2587
  var pref = new Store(readPref());
2588
- var media = window.matchMedia("(prefers-color-scheme: dark)");
2588
+ var media$1 = window.matchMedia("(prefers-color-scheme: dark)");
2589
2589
  function resolve(value) {
2590
- return value === "system" ? media.matches ? "dark" : "light" : value;
2590
+ return value === "system" ? media$1.matches ? "dark" : "light" : value;
2591
2591
  }
2592
2592
  function apply() {
2593
2593
  document.documentElement.dataset.theme = resolve(pref.get());
2594
2594
  }
2595
- media.addEventListener("change", apply);
2595
+ media$1.addEventListener("change", apply);
2596
2596
  pref.subscribe(apply);
2597
2597
  apply();
2598
2598
  function setThemePref(value) {
@@ -2608,9 +2608,9 @@ function useThemePref() {
2608
2608
  function useTheme() {
2609
2609
  const value = useStore(pref);
2610
2610
  const systemDark = useSyncExternalStore((listener) => {
2611
- media.addEventListener("change", listener);
2612
- return () => media.removeEventListener("change", listener);
2613
- }, () => media.matches);
2611
+ media$1.addEventListener("change", listener);
2612
+ return () => media$1.removeEventListener("change", listener);
2613
+ }, () => media$1.matches);
2614
2614
  return value === "system" ? systemDark ? "dark" : "light" : value;
2615
2615
  }
2616
2616
  //#endregion
@@ -3094,7 +3094,8 @@ function SearchButton({ width = 240, label = "Search and jump" }) {
3094
3094
  type: "button",
3095
3095
  onClick: openPalette,
3096
3096
  style: { width },
3097
- className: "flex h-ctrl min-w-0 shrink cursor-pointer items-center gap-2 rounded-[6px] border border-rule bg-soft pr-1.5 pl-3 text-ctrl text-muted transition-colors hover:border-rule-strong max-lg:w-auto!",
3097
+ "aria-label": label,
3098
+ className: "flex h-ctrl min-w-0 shrink cursor-pointer items-center gap-2 rounded-[6px] border border-rule bg-soft pr-1.5 pl-3 text-ctrl text-muted transition-colors hover:border-rule-strong max-lg:w-auto! max-md:w-ctrl! max-md:justify-center max-md:border-0! max-md:bg-transparent! max-md:p-0!",
3098
3099
  children: [
3099
3100
  /* @__PURE__ */ jsx(Icon, {
3100
3101
  name: "search",
@@ -3104,7 +3105,10 @@ function SearchButton({ width = 240, label = "Search and jump" }) {
3104
3105
  className: "min-w-0 grow truncate text-left max-lg:hidden",
3105
3106
  children: label
3106
3107
  }),
3107
- /* @__PURE__ */ jsx(Kbd, { children: MOD_KEY === "⌘" ? "⌘K" : "Ctrl K" })
3108
+ /* @__PURE__ */ jsx("span", {
3109
+ className: "flex max-md:hidden",
3110
+ children: /* @__PURE__ */ jsx(Kbd, { children: MOD_KEY === "⌘" ? "⌘K" : "Ctrl K" })
3111
+ })
3108
3112
  ]
3109
3113
  });
3110
3114
  }
@@ -5637,12 +5641,12 @@ function useIsHydrated() {
5637
5641
  }
5638
5642
  __name$6(useIsHydrated, "useIsHydrated");
5639
5643
  var useReactSyncExternalStore = React[" useSyncExternalStore ".trim().toString()];
5640
- function subscribe() {
5644
+ function subscribe$1() {
5641
5645
  return () => {};
5642
5646
  }
5643
- __name$6(subscribe, "subscribe");
5647
+ __name$6(subscribe$1, "subscribe");
5644
5648
  function useIsHydratedModern() {
5645
- return useReactSyncExternalStore(subscribe, () => true, () => false);
5649
+ return useReactSyncExternalStore(subscribe$1, () => true, () => false);
5646
5650
  }
5647
5651
  __name$6(useIsHydratedModern, "useIsHydratedModern");
5648
5652
  var useIsHydrated2 = typeof useReactSyncExternalStore === "function" ? useIsHydratedModern : useIsHydrated;
@@ -6501,7 +6505,10 @@ function Menu({ trigger, children, width = 220, align = "start", side = "bottom"
6501
6505
  sideOffset,
6502
6506
  collisionPadding: 12,
6503
6507
  className: menuSurface,
6504
- style: { width },
6508
+ style: {
6509
+ width,
6510
+ maxWidth: "calc(100vw - 24px)"
6511
+ },
6505
6512
  children
6506
6513
  }) })]
6507
6514
  });
@@ -6717,24 +6724,60 @@ function AppTopBar() {
6717
6724
  const project = useProject().data;
6718
6725
  const { slots } = useViewer();
6719
6726
  const [location] = useLocation();
6720
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Bar, { children: [
6721
- slots.barStart,
6722
- /* @__PURE__ */ jsxs("div", {
6723
- className: "flex min-w-0 items-center gap-6",
6724
- children: [slots.brand ?? /* @__PURE__ */ jsx(Wordmark, {
6725
- name: project?.name,
6726
- version: project?.version
6727
- }), /* @__PURE__ */ jsx(Tabs, { location })]
6728
- }),
6729
- /* @__PURE__ */ jsxs("div", {
6730
- className: "ml-auto flex items-center gap-2",
6731
- children: [
6732
- /* @__PURE__ */ jsx(SearchButton, {}),
6733
- /* @__PURE__ */ jsx(ThemeMenu, {}),
6734
- slots.barEnd
6735
- ]
6727
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
6728
+ /* @__PURE__ */ jsxs(Bar, { children: [
6729
+ slots.barStart,
6730
+ /* @__PURE__ */ jsxs("div", {
6731
+ className: "flex min-w-0 items-center gap-6",
6732
+ children: [slots.brand ?? /* @__PURE__ */ jsx(Wordmark, {
6733
+ name: project?.name,
6734
+ version: project?.version
6735
+ }), /* @__PURE__ */ jsx(Tabs, { location })]
6736
+ }),
6737
+ /* @__PURE__ */ jsxs("div", {
6738
+ className: "ml-auto flex items-center gap-2",
6739
+ children: [
6740
+ /* @__PURE__ */ jsx(SearchButton, {}),
6741
+ /* @__PURE__ */ jsx("span", {
6742
+ className: cn("flex", slots.barEnd ? "max-md:hidden" : void 0),
6743
+ children: /* @__PURE__ */ jsx(ThemeMenu, {})
6744
+ }),
6745
+ slots.barEnd
6746
+ ]
6747
+ })
6748
+ ] }),
6749
+ /* @__PURE__ */ jsx(PhoneTabs, { location }),
6750
+ slots.belowBar
6751
+ ] });
6752
+ }
6753
+ /** The section tabs on a phone, where the bar has no room for them: a row under it. */
6754
+ function PhoneTabs({ location }) {
6755
+ return /* @__PURE__ */ jsx("nav", {
6756
+ className: "sticky top-[52px] z-30 hidden h-10 shrink-0 items-stretch border-b border-rule bg-surface px-2 max-md:flex",
6757
+ children: TABS.map((tab) => {
6758
+ const on = tab.match(location);
6759
+ return /* @__PURE__ */ jsxs(Link, {
6760
+ href: tab.to,
6761
+ "aria-current": on ? "page" : void 0,
6762
+ className: cn("relative flex grow items-center justify-center text-[13.5px] no-underline", on ? "font-medium text-ink" : "text-ink2"),
6763
+ children: [tab.label, on ? /* @__PURE__ */ jsx("span", {
6764
+ "aria-hidden": true,
6765
+ className: "absolute inset-x-6 bottom-0 h-[2px] rounded-full bg-action"
6766
+ }) : null]
6767
+ }, tab.to);
6736
6768
  })
6737
- ] }), slots.belowBar] });
6769
+ });
6770
+ }
6771
+ //#endregion
6772
+ //#region viewer/src/lib/phone.ts
6773
+ /** Phones: under Tailwind's `md`. The canvas gives way to one screen at a time there. */
6774
+ var media = window.matchMedia("(max-width: 767px)");
6775
+ var subscribe = (onChange) => {
6776
+ media.addEventListener("change", onChange);
6777
+ return () => media.removeEventListener("change", onChange);
6778
+ };
6779
+ function useIsPhone() {
6780
+ return useSyncExternalStore(subscribe, () => media.matches);
6738
6781
  }
6739
6782
  //#endregion
6740
6783
  //#region ../../node_modules/.pnpm/@radix-ui+react-popover@1.1.23_@types+react-dom@19.3.0_@types+react@19.3.0__@types+reac_bdb8fe50570f35efe0254f5915b263fd/node_modules/@radix-ui/react-popover/dist/index.mjs
@@ -7836,7 +7879,7 @@ function KV({ label, children, labelWidth = 180, minH = 40, end = false }) {
7836
7879
  return /* @__PURE__ */ jsxs("div", {
7837
7880
  className: "grid content-center items-baseline gap-x-4 border-t border-rule px-4 py-2",
7838
7881
  style: {
7839
- gridTemplateColumns: `${labelWidth}px minmax(0, 1fr)`,
7882
+ gridTemplateColumns: `min(${labelWidth}px, 42%) minmax(0, 1fr)`,
7840
7883
  minHeight: minH
7841
7884
  },
7842
7885
  children: [/* @__PURE__ */ jsx("span", {
@@ -7996,7 +8039,7 @@ function PageHead({ title, sub, right, badge, crumbs }) {
7996
8039
  }), badge]
7997
8040
  }),
7998
8041
  sub ? /* @__PURE__ */ jsx("div", {
7999
- className: "text-[13px] text-muted",
8042
+ className: "text-[13px] [overflow-wrap:anywhere] text-muted",
8000
8043
  children: sub
8001
8044
  }) : null
8002
8045
  ]
@@ -11949,7 +11992,7 @@ function isTyping(target) {
11949
11992
  //#endregion
11950
11993
  //#region viewer/src/pages/canvas/canvas-page.tsx
11951
11994
  var query = new URLSearchParams(window.location.search);
11952
- var CAPTURE = query.has("capture");
11995
+ var CAPTURE$1 = query.has("capture");
11953
11996
  var cameraKey = (canvas, page) => `foss-design.camera.${canvas}.${page}`;
11954
11997
  function loadCamera(key) {
11955
11998
  try {
@@ -11985,7 +12028,7 @@ function CanvasView({ canvas, page }) {
11985
12028
  const camera = useMemo(() => new CameraStore(), []);
11986
12029
  const apiRef = useRef(null);
11987
12030
  const [theme, setTheme] = useScreenTheme(canvas);
11988
- const [sidebar, setSidebar] = useState(() => !CAPTURE && window.innerWidth > 900);
12031
+ const [sidebar, setSidebar] = useState(() => !CAPTURE$1 && window.innerWidth > 900);
11989
12032
  const [inspectOn, setInspect] = useState(false);
11990
12033
  const sizes = useStore(store, (state) => state.sizes);
11991
12034
  const layout = useMemo(() => page ? layoutPage(page, sizes) : {
@@ -12029,7 +12072,7 @@ function CanvasView({ canvas, page }) {
12029
12072
  if (placed.current || !layout.bounds.w) return;
12030
12073
  placed.current = true;
12031
12074
  const focus = takeFocus(canvas.id);
12032
- const saved = CAPTURE ? null : loadCamera(key);
12075
+ const saved = CAPTURE$1 ? null : loadCamera(key);
12033
12076
  if (focus) {
12034
12077
  store.set((state) => ({
12035
12078
  ...state,
@@ -12037,7 +12080,7 @@ function CanvasView({ canvas, page }) {
12037
12080
  }));
12038
12081
  requestAnimationFrame(() => apiRef.current?.fitItem(focus, false));
12039
12082
  } else if (saved && saved.z >= camera.minZoom) camera.set(saved);
12040
- else requestAnimationFrame(() => CAPTURE ? apiRef.current?.fitAll(false) : apiRef.current?.openView());
12083
+ else requestAnimationFrame(() => CAPTURE$1 ? apiRef.current?.fitAll(false) : apiRef.current?.openView());
12041
12084
  }, [
12042
12085
  layout,
12043
12086
  camera,
@@ -12046,7 +12089,7 @@ function CanvasView({ canvas, page }) {
12046
12089
  store
12047
12090
  ]);
12048
12091
  useEffect(() => {
12049
- if (CAPTURE) return;
12092
+ if (CAPTURE$1) return;
12050
12093
  let timer;
12051
12094
  const off = camera.subscribe((c) => {
12052
12095
  clearTimeout(timer);
@@ -12139,7 +12182,7 @@ function CanvasView({ canvas, page }) {
12139
12182
  navigate
12140
12183
  ]);
12141
12184
  useEffect(() => {
12142
- if (!CAPTURE) return;
12185
+ if (!CAPTURE$1) return;
12143
12186
  const started = Date.now();
12144
12187
  const timer = setInterval(() => {
12145
12188
  const frames = store.get().frames;
@@ -12222,7 +12265,7 @@ function CanvasView({ canvas, page }) {
12222
12265
  inspectOn,
12223
12266
  inspection
12224
12267
  ]);
12225
- if (CAPTURE) return /* @__PURE__ */ jsx("div", {
12268
+ if (CAPTURE$1) return /* @__PURE__ */ jsx("div", {
12226
12269
  className: "flex h-dvh",
12227
12270
  children: /* @__PURE__ */ jsx(Viewport, {
12228
12271
  layout,
@@ -12438,6 +12481,79 @@ function useScreenFrames(frameEls, layout) {
12438
12481
  }, [frameEls, layout]);
12439
12482
  }
12440
12483
  //#endregion
12484
+ //#region viewer/src/ui/sheet.tsx
12485
+ /**
12486
+ * A panel sliding up from the bottom edge, for phones: lists to pick from and actions. Like
12487
+ * Dialog, focus stays inside, and Escape, a tap on the dimmed page or the close button close it.
12488
+ */
12489
+ function Sheet({ open, onOpenChange, title, children }) {
12490
+ return /* @__PURE__ */ jsx(Dialog$1, {
12491
+ open,
12492
+ onOpenChange,
12493
+ children: /* @__PURE__ */ jsx(AnimatePresence, { children: open ? /* @__PURE__ */ jsxs(DialogPortal, {
12494
+ forceMount: true,
12495
+ children: [/* @__PURE__ */ jsx(DialogOverlay, {
12496
+ asChild: true,
12497
+ forceMount: true,
12498
+ children: /* @__PURE__ */ jsx(motion.div, {
12499
+ initial: { opacity: 0 },
12500
+ animate: { opacity: 1 },
12501
+ exit: { opacity: 0 },
12502
+ transition: { duration: .16 },
12503
+ className: "fixed inset-0 z-50 bg-overlay"
12504
+ })
12505
+ }), /* @__PURE__ */ jsx(DialogContent, {
12506
+ asChild: true,
12507
+ forceMount: true,
12508
+ "aria-describedby": void 0,
12509
+ children: /* @__PURE__ */ jsxs(motion.div, {
12510
+ initial: { y: "100%" },
12511
+ animate: { y: 0 },
12512
+ exit: {
12513
+ y: "100%",
12514
+ transition: {
12515
+ duration: .18,
12516
+ ease: "easeIn"
12517
+ }
12518
+ },
12519
+ transition: {
12520
+ type: "spring",
12521
+ stiffness: 420,
12522
+ damping: 40
12523
+ },
12524
+ className: "fixed inset-x-0 bottom-0 z-50 flex max-h-[82dvh] flex-col rounded-t-[14px] border-t border-rule bg-surface pb-[env(safe-area-inset-bottom)] shadow-pop outline-none",
12525
+ children: [
12526
+ /* @__PURE__ */ jsx("div", {
12527
+ className: "flex justify-center pt-2",
12528
+ children: /* @__PURE__ */ jsx("span", {
12529
+ "aria-hidden": true,
12530
+ className: "h-1 w-9 rounded-full bg-rule-strong"
12531
+ })
12532
+ }),
12533
+ /* @__PURE__ */ jsxs("div", {
12534
+ className: "flex min-h-[48px] items-center justify-between gap-3 pr-2 pl-4",
12535
+ children: [/* @__PURE__ */ jsx(DialogTitle, {
12536
+ className: "m-0 truncate text-[15px] font-semibold",
12537
+ children: title
12538
+ }), /* @__PURE__ */ jsx(DialogClose, {
12539
+ asChild: true,
12540
+ children: /* @__PURE__ */ jsx(IconButton, {
12541
+ icon: "x",
12542
+ label: "Close"
12543
+ })
12544
+ })]
12545
+ }),
12546
+ /* @__PURE__ */ jsx("div", {
12547
+ className: "min-h-0 overflow-y-auto overscroll-contain border-t border-rule",
12548
+ children
12549
+ })
12550
+ ]
12551
+ })
12552
+ })]
12553
+ }) : null })
12554
+ });
12555
+ }
12556
+ //#endregion
12441
12557
  //#region viewer/src/pages/canvas/play-page.tsx
12442
12558
  function framesOf(canvas, pageId) {
12443
12559
  return (pageId ? canvas.pages.filter((p) => p.id === pageId) : canvas.pages).flatMap((page) => page.sections.flatMap((section) => section.items.filter((item) => item.kind === "screen" || item.kind === "url").map((item) => ({
@@ -12743,6 +12859,441 @@ function Player({ canvas, itemId }) {
12743
12859
  });
12744
12860
  }
12745
12861
  //#endregion
12862
+ //#region viewer/src/pages/canvas/phone-view.tsx
12863
+ /** Room around the screen on the stage, per side. */
12864
+ var GUTTER = 12;
12865
+ /**
12866
+ * A canvas on a phone: no board to pan, one screen at a time as in play mode. The screen fills
12867
+ * the width and scrolls; the bottom bar steps through every screen of the canvas and opens the
12868
+ * list of them. Serves `/c/:canvas`, its pages and `/c/:canvas/play/:item` under 768px.
12869
+ */
12870
+ function PhoneCanvas({ canvasId, pageId, itemId }) {
12871
+ const { data: canvas, error, isLoading } = useCanvas(canvasId);
12872
+ if (error) return /* @__PURE__ */ jsx(NotFound, {
12873
+ title: "Canvas not found",
12874
+ text: error.message
12875
+ });
12876
+ if (isLoading || !canvas) return /* @__PURE__ */ jsx("div", { className: "canvas-dots h-dvh" });
12877
+ return /* @__PURE__ */ jsx(PhoneViewer, {
12878
+ canvas,
12879
+ pageId,
12880
+ itemId
12881
+ });
12882
+ }
12883
+ function PhoneViewer({ canvas, pageId, itemId }) {
12884
+ const [, navigate] = useLocation();
12885
+ const { frameSandbox, slots, scope } = useViewer();
12886
+ const appTheme = useTheme();
12887
+ const all = useMemo(() => framesOf(canvas, null), [canvas]);
12888
+ const index = Math.max(0, itemId ? all.findIndex((entry) => entry.item.id === itemId) : pageId ? all.findIndex((entry) => entry.pageId === pageId) : 0);
12889
+ const entry = all[index];
12890
+ const item = entry?.item;
12891
+ const [theme, setTheme] = useState(canvas.theme ?? appTheme);
12892
+ const [listOpen, setListOpen] = useState(false);
12893
+ const [errors, setErrors] = useState([]);
12894
+ const [stageWidth, setStageWidth] = useState(0);
12895
+ const [autoHeight, setAutoHeight] = useState(null);
12896
+ const [direction, setDirection] = useState(0);
12897
+ const [actual, setActual] = useState(false);
12898
+ const stage = useRef(null);
12899
+ const frame = useRef(null);
12900
+ const [frameEl, setFrameEl] = useState(null);
12901
+ const setFrame = useCallback((el) => {
12902
+ if (!el) return;
12903
+ frame.current = el;
12904
+ setFrameEl(el);
12905
+ }, []);
12906
+ const show = useCallback((target, step = 0) => {
12907
+ setDirection(step);
12908
+ navigate(playHref(canvas.id, target.item.id, target.pageId), { replace: true });
12909
+ }, [canvas.id, navigate]);
12910
+ const move = (step) => {
12911
+ if (all.length < 2) return;
12912
+ show(all[(index + step + all.length) % all.length], step);
12913
+ };
12914
+ const goTo = (id) => {
12915
+ const target = all.find((e) => e.item.id === id);
12916
+ if (target) show(target, all.indexOf(target) > index ? 1 : -1);
12917
+ };
12918
+ useLayoutEffect(() => {
12919
+ const el = stage.current;
12920
+ if (!el) return;
12921
+ const observer = new ResizeObserver(() => setStageWidth(el.clientWidth));
12922
+ observer.observe(el);
12923
+ return () => observer.disconnect();
12924
+ }, []);
12925
+ const frameTheme = item?.kind === "screen" ? item.theme ?? theme : theme;
12926
+ const [openTheme] = useState(frameTheme);
12927
+ const src = item ? item.kind === "screen" ? frameSrc(item.url, openTheme) : item.url : "";
12928
+ const openUrl = useOpenUrl(canvas.id, item, frameTheme);
12929
+ useEffect(() => {
12930
+ setErrors([]);
12931
+ setAutoHeight(item?.kind === "screen" ? item.measuredHeight ?? null : null);
12932
+ stage.current?.scrollTo({ top: 0 });
12933
+ const el = frame.current;
12934
+ if (!el || item?.kind !== "screen") return;
12935
+ return listenToFrame(el, (message) => {
12936
+ if (message.type === "size") setAutoHeight(message.height);
12937
+ else if (message.type === "error") setErrors((list) => list.includes(message.message) ? list : [...list, message.message]);
12938
+ else if (message.type === "updated") setErrors([]);
12939
+ else if (message.type === "go") goTo(message.target.includes("/") ? message.target.split("/")[1] : message.target);
12940
+ else if (message.type === "link") followLink(message, canvas, goTo);
12941
+ });
12942
+ }, [frameEl, src]);
12943
+ useEffect(() => {
12944
+ if (item?.kind === "screen") sendTheme(frame.current, frameTheme);
12945
+ }, [frameTheme, item]);
12946
+ useEffect(() => {
12947
+ const onKey = (event) => {
12948
+ if (document.querySelector("[role=\"dialog\"]")) return;
12949
+ if (event.key === "ArrowRight") move(1);
12950
+ else if (event.key === "ArrowLeft") move(-1);
12951
+ };
12952
+ window.addEventListener("keydown", onKey);
12953
+ return () => window.removeEventListener("keydown", onKey);
12954
+ });
12955
+ const swipe = useRef(null);
12956
+ const onTouchStart = (event) => {
12957
+ const touch = event.touches[0];
12958
+ swipe.current = touch ? {
12959
+ x: touch.clientX,
12960
+ y: touch.clientY
12961
+ } : null;
12962
+ };
12963
+ const onTouchEnd = (event) => {
12964
+ const start = swipe.current;
12965
+ const touch = event.changedTouches[0];
12966
+ swipe.current = null;
12967
+ if (!start || !touch) return;
12968
+ const dx = touch.clientX - start.x;
12969
+ const dy = touch.clientY - start.y;
12970
+ if (Math.abs(dx) > 56 && Math.abs(dx) > Math.abs(dy) * 1.5) move(dx < 0 ? 1 : -1);
12971
+ };
12972
+ if (!item || !entry) return /* @__PURE__ */ jsxs("div", {
12973
+ className: "flex h-dvh flex-col",
12974
+ children: [/* @__PURE__ */ jsx(PhoneBar, {
12975
+ canvas,
12976
+ title: canvas.title,
12977
+ sub: null
12978
+ }), /* @__PURE__ */ jsx(NotFound, {
12979
+ title: "No screens here",
12980
+ text: "This canvas has no screens to show yet."
12981
+ })]
12982
+ });
12983
+ const width = item.frame.width;
12984
+ const fitScale = stageWidth ? Math.min(1, (stageWidth - 24) / width) : 0;
12985
+ const wide = fitScale > 0 && fitScale < .8;
12986
+ const scale = wide && actual ? 1 : fitScale;
12987
+ const height = item.frame.height === "auto" ? Math.max(autoHeight ?? 0, 480) : item.frame.height;
12988
+ const canTheme = item.kind === "screen" && !item.theme;
12989
+ return /* @__PURE__ */ jsxs("div", {
12990
+ className: "flex h-dvh flex-col",
12991
+ children: [
12992
+ /* @__PURE__ */ jsx(PhoneBar, {
12993
+ canvas,
12994
+ title: item.title || item.id,
12995
+ sub: [canvas.title, entry.where].filter(Boolean).join(" · "),
12996
+ back: scope ? void 0 : "/",
12997
+ end: /* @__PURE__ */ jsxs(Fragment, { children: [
12998
+ wide ? /* @__PURE__ */ jsx(IconButton, {
12999
+ icon: actual ? "zoom-out" : "zoom-in",
13000
+ label: actual ? "Fit to width" : "Actual size",
13001
+ onClick: () => setActual((value) => !value)
13002
+ }) : null,
13003
+ /* @__PURE__ */ jsxs(Menu, {
13004
+ align: "end",
13005
+ width: 220,
13006
+ trigger: /* @__PURE__ */ jsx(IconButton, {
13007
+ icon: "more",
13008
+ label: "Screen options"
13009
+ }),
13010
+ children: [
13011
+ canTheme ? /* @__PURE__ */ jsxs(Fragment, { children: [
13012
+ /* @__PURE__ */ jsx(MenuLabel, { children: "Screen theme" }),
13013
+ /* @__PURE__ */ jsx(ThemeItem, {
13014
+ value: "light",
13015
+ current: theme,
13016
+ onPick: setTheme
13017
+ }),
13018
+ /* @__PURE__ */ jsx(ThemeItem, {
13019
+ value: "dark",
13020
+ current: theme,
13021
+ onPick: setTheme
13022
+ }),
13023
+ /* @__PURE__ */ jsx(MenuSeparator, {})
13024
+ ] }) : null,
13025
+ /* @__PURE__ */ jsxs(MenuItem, {
13026
+ onSelect: () => reloadFrame(frame.current),
13027
+ children: [/* @__PURE__ */ jsx(Icon, {
13028
+ name: "refresh",
13029
+ size: 15,
13030
+ className: "text-ink2"
13031
+ }), "Reload screen"]
13032
+ }),
13033
+ openUrl ? /* @__PURE__ */ jsxs(MenuItem, {
13034
+ onSelect: () => window.open(openUrl, "_blank", "noopener"),
13035
+ children: [/* @__PURE__ */ jsx(Icon, {
13036
+ name: "external",
13037
+ size: 15,
13038
+ className: "text-ink2"
13039
+ }), "Open in a new tab"]
13040
+ }) : null,
13041
+ /* @__PURE__ */ jsxs(MenuItem, {
13042
+ onSelect: () => setThemePref(appTheme === "dark" ? "light" : "dark"),
13043
+ children: [/* @__PURE__ */ jsx(Icon, {
13044
+ name: appTheme === "dark" ? "sun" : "moon",
13045
+ size: 15,
13046
+ className: "text-ink2"
13047
+ }), appTheme === "dark" ? "Light appearance" : "Dark appearance"]
13048
+ })
13049
+ ]
13050
+ }),
13051
+ slots.barEnd
13052
+ ] })
13053
+ }),
13054
+ slots.belowBar,
13055
+ /* @__PURE__ */ jsxs("div", {
13056
+ ref: stage,
13057
+ onTouchStart,
13058
+ onTouchEnd,
13059
+ className: cn("canvas-dots relative min-h-0 grow overflow-y-auto overscroll-contain", wide && actual ? "overflow-x-auto" : "overflow-x-hidden"),
13060
+ children: [errors.length ? /* @__PURE__ */ jsxs("div", {
13061
+ className: "sticky top-0 z-10 flex items-start gap-2 border-b border-rule bg-danger-soft px-4 py-2 font-mono text-[12px] whitespace-pre-wrap text-danger-text",
13062
+ children: [/* @__PURE__ */ jsx(Icon, {
13063
+ name: "alert",
13064
+ size: 14,
13065
+ className: "mt-px shrink-0"
13066
+ }), /* @__PURE__ */ jsx("span", {
13067
+ className: "min-w-0 break-words",
13068
+ children: errors.join("\n")
13069
+ })]
13070
+ }) : null, /* @__PURE__ */ jsx(AnimatePresence, {
13071
+ initial: false,
13072
+ mode: "popLayout",
13073
+ custom: direction,
13074
+ children: /* @__PURE__ */ jsx(motion.div, {
13075
+ custom: direction,
13076
+ initial: {
13077
+ opacity: 0,
13078
+ x: direction * 40
13079
+ },
13080
+ animate: {
13081
+ opacity: 1,
13082
+ x: 0
13083
+ },
13084
+ exit: {
13085
+ opacity: 0,
13086
+ x: direction * -40,
13087
+ transition: { duration: .12 }
13088
+ },
13089
+ transition: {
13090
+ type: "spring",
13091
+ stiffness: 420,
13092
+ damping: 38
13093
+ },
13094
+ className: cn("flex", wide && actual ? "w-max" : "justify-center"),
13095
+ style: { padding: GUTTER },
13096
+ children: scale ? /* @__PURE__ */ jsx("div", {
13097
+ className: "shrink-0 overflow-hidden rounded-[6px] bg-surface",
13098
+ style: {
13099
+ width: width * scale,
13100
+ height: height * scale,
13101
+ boxShadow: "var(--shadow-frame), 0 0 0 1px var(--rule)"
13102
+ },
13103
+ children: /* @__PURE__ */ jsx("iframe", {
13104
+ ref: setFrame,
13105
+ src,
13106
+ title: item.title,
13107
+ allow: "clipboard-read; clipboard-write; fullscreen",
13108
+ sandbox: frameSandbox,
13109
+ className: "block border-0",
13110
+ style: {
13111
+ width,
13112
+ height,
13113
+ transform: `scale(${scale})`,
13114
+ transformOrigin: "0 0",
13115
+ colorScheme: frameTheme
13116
+ },
13117
+ onLoad: () => item.kind === "screen" && sendTheme(frame.current, frameTheme)
13118
+ }, `${item.id}:${item.kind === "screen" ? item.rev : item.url}`)
13119
+ }) : null
13120
+ }, item.id)
13121
+ })]
13122
+ }),
13123
+ /* @__PURE__ */ jsxs("nav", {
13124
+ "aria-label": "Screens",
13125
+ className: "flex shrink-0 items-center gap-1 border-t border-rule bg-surface px-2 pt-1.5 pb-[max(6px,env(safe-area-inset-bottom))]",
13126
+ children: [
13127
+ /* @__PURE__ */ jsx(IconButton, {
13128
+ icon: "chevron-left",
13129
+ label: "Previous screen",
13130
+ onClick: () => move(-1),
13131
+ disabled: all.length < 2
13132
+ }),
13133
+ /* @__PURE__ */ jsxs("button", {
13134
+ type: "button",
13135
+ onClick: () => setListOpen(true),
13136
+ className: "flex h-10 min-w-0 grow cursor-pointer items-center justify-center gap-2 rounded-[8px] border-0 bg-transparent px-3 text-[13.5px] text-ink active:bg-soft2",
13137
+ children: [
13138
+ /* @__PURE__ */ jsx(Icon, {
13139
+ name: "list",
13140
+ size: 15,
13141
+ className: "shrink-0 text-muted"
13142
+ }),
13143
+ /* @__PURE__ */ jsxs("span", {
13144
+ className: "tabular shrink-0 text-muted",
13145
+ children: [
13146
+ index + 1,
13147
+ " / ",
13148
+ all.length
13149
+ ]
13150
+ }),
13151
+ /* @__PURE__ */ jsx("span", {
13152
+ className: "min-w-0 truncate font-medium",
13153
+ children: item.title || item.id
13154
+ })
13155
+ ]
13156
+ }),
13157
+ /* @__PURE__ */ jsx(IconButton, {
13158
+ icon: "chevron-right",
13159
+ label: "Next screen",
13160
+ onClick: () => move(1),
13161
+ disabled: all.length < 2
13162
+ })
13163
+ ]
13164
+ }),
13165
+ /* @__PURE__ */ jsx(Sheet, {
13166
+ open: listOpen,
13167
+ onOpenChange: setListOpen,
13168
+ title: canvas.title,
13169
+ children: /* @__PURE__ */ jsx(ScreenList, {
13170
+ canvas,
13171
+ current: item.id,
13172
+ onPick: (id) => {
13173
+ setListOpen(false);
13174
+ goTo(id);
13175
+ }
13176
+ })
13177
+ })
13178
+ ]
13179
+ });
13180
+ }
13181
+ function ThemeItem({ value, current, onPick }) {
13182
+ return /* @__PURE__ */ jsxs(MenuItem, {
13183
+ active: current === value,
13184
+ onSelect: () => onPick(value),
13185
+ children: [
13186
+ /* @__PURE__ */ jsx(Icon, {
13187
+ name: value === "dark" ? "moon" : "sun",
13188
+ size: 15,
13189
+ className: "text-ink2"
13190
+ }),
13191
+ value === "dark" ? "Dark" : "Light",
13192
+ current === value ? /* @__PURE__ */ jsx(Icon, {
13193
+ name: "check",
13194
+ size: 15,
13195
+ className: "ml-auto text-ink2"
13196
+ }) : null
13197
+ ]
13198
+ });
13199
+ }
13200
+ /** The phone's top bar: a way back, the screen and where it sits, and the screen's actions. */
13201
+ function PhoneBar({ canvas, title, sub, back, end }) {
13202
+ const { slots } = useViewer();
13203
+ return /* @__PURE__ */ jsxs("header", {
13204
+ className: "flex h-[52px] shrink-0 items-center gap-2 border-b border-rule bg-surface pr-2 pl-2 pt-[env(safe-area-inset-top)]",
13205
+ children: [
13206
+ slots.barStart,
13207
+ back ? /* @__PURE__ */ jsx(Link, {
13208
+ href: back,
13209
+ "aria-label": `All canvases, leaving ${canvas.title}`,
13210
+ className: "inline-flex size-10 shrink-0 items-center justify-center rounded-[8px] text-ink2 active:bg-soft2",
13211
+ children: /* @__PURE__ */ jsx(Icon, {
13212
+ name: "arrow-left",
13213
+ size: 18
13214
+ })
13215
+ }) : /* @__PURE__ */ jsx("span", {
13216
+ className: "flex size-10 shrink-0 items-center justify-center",
13217
+ children: /* @__PURE__ */ jsx(Mark, {})
13218
+ }),
13219
+ /* @__PURE__ */ jsxs("div", {
13220
+ className: "flex min-w-0 grow flex-col leading-tight",
13221
+ children: [/* @__PURE__ */ jsx("span", {
13222
+ className: "truncate text-[14.5px] font-semibold",
13223
+ children: title
13224
+ }), sub ? /* @__PURE__ */ jsx("span", {
13225
+ className: "truncate text-[12px] text-muted",
13226
+ children: sub
13227
+ }) : null]
13228
+ }),
13229
+ /* @__PURE__ */ jsx("div", {
13230
+ className: "flex shrink-0 items-center gap-1",
13231
+ children: end
13232
+ })
13233
+ ]
13234
+ });
13235
+ }
13236
+ var KIND_ICON = (entry) => entry.item.kind === "url" ? "globe" : entry.item.frame.device === "phone" ? "smartphone" : entry.item.frame.device === "tablet" ? "tablet" : "monitor";
13237
+ /** Every screen of the canvas by page and section, the current one marked. */
13238
+ function ScreenList({ canvas, current, onPick }) {
13239
+ const activeRef = useRef(null);
13240
+ useEffect(() => {
13241
+ activeRef.current?.scrollIntoView({ block: "center" });
13242
+ }, []);
13243
+ const multi = canvas.pages.length > 1;
13244
+ return /* @__PURE__ */ jsx("div", {
13245
+ className: "flex flex-col pb-2",
13246
+ children: canvas.pages.map((page) => {
13247
+ const entries = framesOf(canvas, page.id);
13248
+ if (!entries.length) return null;
13249
+ return /* @__PURE__ */ jsxs("section", {
13250
+ className: "flex flex-col",
13251
+ children: [multi ? /* @__PURE__ */ jsx("h3", {
13252
+ className: "sticky top-0 z-[1] m-0 border-b border-rule bg-soft px-4 py-2 text-[12px] font-semibold text-ink2",
13253
+ children: page.title
13254
+ }) : null, page.sections.map((section) => {
13255
+ const items = entries.filter((entry) => section.items.includes(entry.item));
13256
+ if (!items.length) return null;
13257
+ return /* @__PURE__ */ jsxs("div", {
13258
+ className: "flex flex-col",
13259
+ children: [section.title ? /* @__PURE__ */ jsx("div", {
13260
+ className: "px-4 pt-3 pb-1 text-[12px] text-muted",
13261
+ children: section.title
13262
+ }) : null, items.map((entry) => {
13263
+ const on = entry.item.id === current;
13264
+ return /* @__PURE__ */ jsxs("button", {
13265
+ ref: on ? activeRef : void 0,
13266
+ type: "button",
13267
+ "aria-current": on ? "true" : void 0,
13268
+ onClick: () => onPick(entry.item.id),
13269
+ className: cn("flex min-h-[48px] cursor-pointer items-center gap-3 border-0 bg-transparent px-4 text-left text-[14px] active:bg-soft2", on ? "font-medium text-ink" : "text-ink2"),
13270
+ children: [
13271
+ /* @__PURE__ */ jsx("span", {
13272
+ className: cn("flex size-8 shrink-0 items-center justify-center rounded-[8px]", on ? "bg-action-soft text-action" : "bg-soft2 text-muted"),
13273
+ children: /* @__PURE__ */ jsx(Icon, {
13274
+ name: KIND_ICON(entry),
13275
+ size: 15
13276
+ })
13277
+ }),
13278
+ /* @__PURE__ */ jsx("span", {
13279
+ className: "min-w-0 grow truncate",
13280
+ children: entry.item.title || entry.item.id
13281
+ }),
13282
+ on ? /* @__PURE__ */ jsx(Icon, {
13283
+ name: "check",
13284
+ size: 16,
13285
+ className: "shrink-0 text-action"
13286
+ }) : null
13287
+ ]
13288
+ }, entry.item.id);
13289
+ })]
13290
+ }, section.id);
13291
+ })]
13292
+ }, page.id);
13293
+ })
13294
+ });
13295
+ }
13296
+ //#endregion
12746
13297
  //#region viewer/src/lib/format.ts
12747
13298
  var units = [
12748
13299
  ["year", 31536e3],
@@ -12843,6 +13394,7 @@ function Foot({ children }) {
12843
13394
  }
12844
13395
  function CanvasList({ project }) {
12845
13396
  const { canvasRowActions: rowActions, canvasesFoot } = useViewer().slots;
13397
+ const phone = useIsPhone();
12846
13398
  if (!project.canvases.length) return /* @__PURE__ */ jsxs(Panel, { children: [
12847
13399
  /* @__PURE__ */ jsx(PanelHead, {
12848
13400
  title: "Canvases",
@@ -12873,7 +13425,31 @@ function CanvasList({ project }) {
12873
13425
  const actions = rowActions?.(canvas.id);
12874
13426
  return /* @__PURE__ */ jsx(WithActions, {
12875
13427
  actions,
12876
- children: /* @__PURE__ */ jsxs(RowLink, {
13428
+ children: phone ? /* @__PURE__ */ jsxs(RowLink, {
13429
+ to: `/c/${encodeURIComponent(canvas.id)}`,
13430
+ cols: "86px minmax(0,1fr)",
13431
+ align: "ll",
13432
+ pad: "10px 16px",
13433
+ minH: 74,
13434
+ first: !!actions,
13435
+ children: [/* @__PURE__ */ jsx(Cover, { canvas }), /* @__PURE__ */ jsx(Two, {
13436
+ top: canvas.title,
13437
+ bottom: /* @__PURE__ */ jsxs("span", {
13438
+ className: "flex min-w-0 items-center gap-2",
13439
+ children: [canvas.issues ? /* @__PURE__ */ jsx(Badge, {
13440
+ tone: "danger",
13441
+ children: plural(canvas.issues, "problem")
13442
+ }) : null, /* @__PURE__ */ jsxs("span", {
13443
+ className: "truncate",
13444
+ children: [
13445
+ plural(canvas.screens, "screen"),
13446
+ " · ",
13447
+ relativeTime(canvas.updatedAt)
13448
+ ]
13449
+ })]
13450
+ })
13451
+ })]
13452
+ }) : /* @__PURE__ */ jsxs(RowLink, {
12877
13453
  to: `/c/${encodeURIComponent(canvas.id)}`,
12878
13454
  cols: "86px minmax(0,1fr) auto auto",
12879
13455
  align: "llrr",
@@ -12991,6 +13567,7 @@ function HomeSkeleton() {
12991
13567
  function HomePage() {
12992
13568
  const { data: project, error, isLoading } = useProject();
12993
13569
  const { slots } = useViewer();
13570
+ const phone = useIsPhone();
12994
13571
  if (error) return /* @__PURE__ */ jsx(Notice, {
12995
13572
  title: "Could not load the project",
12996
13573
  text: error.message
@@ -12999,13 +13576,19 @@ function HomePage() {
12999
13576
  return /* @__PURE__ */ jsxs(Content, { children: [
13000
13577
  /* @__PURE__ */ jsx(PageHead, {
13001
13578
  title: project.name,
13002
- right: slots.homeActions,
13579
+ right: phone ? void 0 : slots.homeActions,
13580
+ badge: phone && slots.homeActions ? /* @__PURE__ */ jsx("span", {
13581
+ className: "ml-auto flex",
13582
+ children: slots.homeActions
13583
+ }) : void 0,
13003
13584
  sub: slots.homeSub ? slots.homeSub : project.static ? `Snapshot built with foss-design ${project.version}` : /* @__PURE__ */ jsxs(Fragment, { children: [
13004
13585
  /* @__PURE__ */ jsxs(Mono, {
13005
13586
  size: 12.5,
13587
+ wrap: true,
13006
13588
  children: [project.root, "/.design"]
13007
13589
  }),
13008
- " · foss-design ",
13590
+ " ",
13591
+ "· foss-design ",
13009
13592
  project.version
13010
13593
  ] })
13011
13594
  }),
@@ -16666,6 +17249,7 @@ function MotionPage({ system }) {
16666
17249
  //#region viewer/src/pages/system/nav.tsx
16667
17250
  /** Grouped links of the system section; a wrapping row above the content on narrow screens. */
16668
17251
  function SystemNav({ system }) {
17252
+ const phone = useIsPhone();
16669
17253
  const kinds = new Set(system.tokens.map((token) => token.kind));
16670
17254
  const foundations = [
16671
17255
  {
@@ -16707,6 +17291,33 @@ function SystemNav({ system }) {
16707
17291
  groups.set(component.group, list);
16708
17292
  }
16709
17293
  const guidelines = system.guidelines.filter((guide) => guide.slug !== "overview");
17294
+ if (phone) {
17295
+ const sections = [
17296
+ {
17297
+ label: "",
17298
+ items: [{
17299
+ to: "/system",
17300
+ label: "Overview"
17301
+ }]
17302
+ },
17303
+ {
17304
+ label: "Foundations",
17305
+ items: foundations
17306
+ },
17307
+ {
17308
+ label: "Guidelines",
17309
+ items: guidelines.map((guide) => ({
17310
+ to: `/system/guidelines/${encodeURIComponent(guide.slug)}`,
17311
+ label: guide.title
17312
+ }))
17313
+ },
17314
+ ...[...groups].map(([label, items]) => ({
17315
+ label,
17316
+ items
17317
+ }))
17318
+ ];
17319
+ return /* @__PURE__ */ jsx(PhoneSystemNav, { sections: sections.filter((section) => section.items.length) });
17320
+ }
16710
17321
  return /* @__PURE__ */ jsxs("div", {
16711
17322
  className: "flex flex-col max-lg:gap-1 max-lg:[&_nav]:flex-row max-lg:[&_nav]:flex-wrap",
16712
17323
  children: [
@@ -16742,6 +17353,70 @@ function SystemNav({ system }) {
16742
17353
  function count(system, kind) {
16743
17354
  return system.tokens.filter((token) => token.kind === kind).length || void 0;
16744
17355
  }
17356
+ /** On a phone: the current section on a full-width button that lists them all in a sheet. */
17357
+ function PhoneSystemNav({ sections }) {
17358
+ const [location] = useLocation();
17359
+ const [open, setOpen] = useState(false);
17360
+ const current = sections.flatMap((section) => section.items).find((item) => item.to === location);
17361
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
17362
+ type: "button",
17363
+ onClick: () => setOpen(true),
17364
+ className: "flex h-11 w-full cursor-pointer items-center gap-2 rounded-[8px] border border-rule bg-surface px-3.5 text-left text-[14px] text-ink",
17365
+ children: [
17366
+ /* @__PURE__ */ jsx("span", {
17367
+ className: "text-[12.5px] text-muted",
17368
+ children: "Section"
17369
+ }),
17370
+ /* @__PURE__ */ jsx("span", {
17371
+ className: "min-w-0 grow truncate font-medium",
17372
+ children: current?.label ?? "Overview"
17373
+ }),
17374
+ /* @__PURE__ */ jsx(Icon, {
17375
+ name: "chevron-down",
17376
+ size: 16,
17377
+ className: "shrink-0 text-muted"
17378
+ })
17379
+ ]
17380
+ }), /* @__PURE__ */ jsx(Sheet, {
17381
+ open,
17382
+ onOpenChange: setOpen,
17383
+ title: "Design system",
17384
+ children: /* @__PURE__ */ jsx("nav", {
17385
+ "aria-label": "Design system",
17386
+ className: "flex flex-col pb-2",
17387
+ children: sections.map((section) => /* @__PURE__ */ jsxs("div", {
17388
+ className: "flex flex-col",
17389
+ children: [section.label ? /* @__PURE__ */ jsx("div", {
17390
+ className: "px-4 pt-3 pb-1 text-[12px] text-muted",
17391
+ children: section.label
17392
+ }) : null, section.items.map((item) => {
17393
+ const on = item.to === location;
17394
+ return /* @__PURE__ */ jsxs(Link, {
17395
+ href: item.to,
17396
+ onClick: () => setOpen(false),
17397
+ "aria-current": on ? "page" : void 0,
17398
+ className: cn("flex min-h-[46px] items-center gap-2 px-4 text-[14px] no-underline active:bg-soft2", on ? "font-medium text-ink" : "text-ink2"),
17399
+ children: [
17400
+ /* @__PURE__ */ jsx("span", {
17401
+ className: "min-w-0 grow truncate",
17402
+ children: item.label
17403
+ }),
17404
+ item.hint ? /* @__PURE__ */ jsx("span", {
17405
+ className: "text-[12.5px] text-muted",
17406
+ children: item.hint
17407
+ }) : null,
17408
+ on ? /* @__PURE__ */ jsx(Icon, {
17409
+ name: "check",
17410
+ size: 16,
17411
+ className: "shrink-0 text-action"
17412
+ }) : null
17413
+ ]
17414
+ }, item.to);
17415
+ })]
17416
+ }, section.label || "overview"))
17417
+ })
17418
+ })] });
17419
+ }
16745
17420
  //#endregion
16746
17421
  //#region viewer/src/pages/system/other-tokens.tsx
16747
17422
  function OtherTokensPage({ system }) {
@@ -17437,7 +18112,30 @@ function Title() {
17437
18112
  }, [name]);
17438
18113
  return null;
17439
18114
  }
18115
+ /** Screenshots (`?capture`) always get the canvas, whatever the window. */
18116
+ var CAPTURE = new URLSearchParams(window.location.search).has("capture");
18117
+ /** The canvas routes' pages; on a phone the canvas is one screen at a time. */
18118
+ function useCanvasPages() {
18119
+ const phone = useIsPhone() && !CAPTURE;
18120
+ return {
18121
+ play: (canvas, item) => phone ? /* @__PURE__ */ jsx(PhoneCanvas, {
18122
+ canvasId: canvas,
18123
+ itemId: item
18124
+ }) : /* @__PURE__ */ jsx(PlayPage, {
18125
+ canvasId: canvas,
18126
+ itemId: item
18127
+ }),
18128
+ canvas: (canvas, page) => phone ? /* @__PURE__ */ jsx(PhoneCanvas, {
18129
+ canvasId: canvas,
18130
+ pageId: page
18131
+ }) : /* @__PURE__ */ jsx(CanvasPage, {
18132
+ canvasId: canvas,
18133
+ pageId: page
18134
+ })
18135
+ };
18136
+ }
17440
18137
  function Routes() {
18138
+ const pages = useCanvasPages();
17441
18139
  return /* @__PURE__ */ jsxs(Switch$1, { children: [
17442
18140
  /* @__PURE__ */ jsx(Route, {
17443
18141
  path: "/c/:canvas/full/:item",
@@ -17448,21 +18146,15 @@ function Routes() {
17448
18146
  }),
17449
18147
  /* @__PURE__ */ jsx(Route, {
17450
18148
  path: "/c/:canvas/play/:item",
17451
- children: (params) => /* @__PURE__ */ jsx(PlayPage, {
17452
- canvasId: params.canvas,
17453
- itemId: params.item
17454
- })
18149
+ children: (params) => pages.play(params.canvas, params.item)
17455
18150
  }),
17456
18151
  /* @__PURE__ */ jsx(Route, {
17457
18152
  path: "/c/:canvas/p/:page",
17458
- children: (params) => /* @__PURE__ */ jsx(CanvasPage, {
17459
- canvasId: params.canvas,
17460
- pageId: params.page
17461
- })
18153
+ children: (params) => pages.canvas(params.canvas, params.page)
17462
18154
  }),
17463
18155
  /* @__PURE__ */ jsx(Route, {
17464
18156
  path: "/c/:canvas",
17465
- children: (params) => /* @__PURE__ */ jsx(CanvasPage, { canvasId: params.canvas })
18157
+ children: (params) => pages.canvas(params.canvas)
17466
18158
  }),
17467
18159
  /* @__PURE__ */ jsxs(Route, {
17468
18160
  path: "/system/*?",
@@ -17482,6 +18174,7 @@ function ScopedRoutes({ canvas }) {
17482
18174
  to: home,
17483
18175
  replace: true
17484
18176
  });
18177
+ const pages = useCanvasPages();
17485
18178
  return /* @__PURE__ */ jsxs(Switch$1, { children: [
17486
18179
  /* @__PURE__ */ jsx(Route, {
17487
18180
  path: "/c/:canvas/full/:item",
@@ -17492,21 +18185,15 @@ function ScopedRoutes({ canvas }) {
17492
18185
  }),
17493
18186
  /* @__PURE__ */ jsx(Route, {
17494
18187
  path: "/c/:canvas/play/:item",
17495
- children: (params) => guard(params.canvas, /* @__PURE__ */ jsx(PlayPage, {
17496
- canvasId: canvas,
17497
- itemId: params.item
17498
- }))
18188
+ children: (params) => guard(params.canvas, pages.play(canvas, params.item))
17499
18189
  }),
17500
18190
  /* @__PURE__ */ jsx(Route, {
17501
18191
  path: "/c/:canvas/p/:page",
17502
- children: (params) => guard(params.canvas, /* @__PURE__ */ jsx(CanvasPage, {
17503
- canvasId: canvas,
17504
- pageId: params.page
17505
- }))
18192
+ children: (params) => guard(params.canvas, pages.canvas(canvas, params.page))
17506
18193
  }),
17507
18194
  /* @__PURE__ */ jsx(Route, {
17508
18195
  path: "/c/:canvas",
17509
- children: (params) => guard(params.canvas, /* @__PURE__ */ jsx(CanvasPage, { canvasId: canvas }))
18196
+ children: (params) => guard(params.canvas, pages.canvas(canvas))
17510
18197
  }),
17511
18198
  /* @__PURE__ */ jsx(Route, { children: /* @__PURE__ */ jsx(Redirect, {
17512
18199
  to: home,
@@ -17808,4 +18495,4 @@ function TextArea({ label, hint, error, className, id, disabled, rows = 4, ...re
17808
18495
  });
17809
18496
  }
17810
18497
  //#endregion
17811
- export { ApiError, Avatar, Badge, Bar, Button, ButtonAnchor, ButtonLink, Content, Count, DesignViewer, Dialog, GroupRow, Icon, IconButton, KV, Kbd, Mark, Menu, MenuItem, MenuLabel, MenuSeparator, Meter, Mono, Muted, NavLabel, Notice, NoticeCard, PageHead, Panel, PanelBody, PanelFoot, PanelHead, PanelTabs, Row, RowLink, Segmented, Skeleton, SkeletonRows, SlidePanel, Strip, SubNav, Switch, THead, TextArea, TextField, ThemeMenu, Toaster, Tooltip, TooltipProvider, Two, Wordmark, buttonClass, cn, copyText, floatingSurface, iconButtonClass, initials, localSource, menuItemClass, menuSurface, resolveUrl, setThemePref, staticSource, toast, useTheme, useThemePref };
18498
+ export { ApiError, Avatar, Badge, Bar, Button, ButtonAnchor, ButtonLink, Content, Count, DesignViewer, Dialog, GroupRow, Icon, IconButton, KV, Kbd, Mark, Menu, MenuItem, MenuLabel, MenuSeparator, Meter, Mono, Muted, NavLabel, Notice, NoticeCard, PageHead, Panel, PanelBody, PanelFoot, PanelHead, PanelTabs, Row, RowLink, Segmented, Sheet, Skeleton, SkeletonRows, SlidePanel, Strip, SubNav, Switch, THead, TextArea, TextField, ThemeMenu, Toaster, Tooltip, TooltipProvider, Two, Wordmark, buttonClass, cn, copyText, floatingSurface, iconButtonClass, initials, localSource, menuItemClass, menuSurface, resolveUrl, setThemePref, staticSource, toast, useIsPhone, useTheme, useThemePref };