foss-design 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -6717,24 +6721,57 @@ function AppTopBar() {
6717
6721
  const project = useProject().data;
6718
6722
  const { slots } = useViewer();
6719
6723
  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
- ]
6724
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
6725
+ /* @__PURE__ */ jsxs(Bar, { children: [
6726
+ slots.barStart,
6727
+ /* @__PURE__ */ jsxs("div", {
6728
+ className: "flex min-w-0 items-center gap-6",
6729
+ children: [slots.brand ?? /* @__PURE__ */ jsx(Wordmark, {
6730
+ name: project?.name,
6731
+ version: project?.version
6732
+ }), /* @__PURE__ */ jsx(Tabs, { location })]
6733
+ }),
6734
+ /* @__PURE__ */ jsxs("div", {
6735
+ className: "ml-auto flex items-center gap-2",
6736
+ children: [
6737
+ /* @__PURE__ */ jsx(SearchButton, {}),
6738
+ /* @__PURE__ */ jsx(ThemeMenu, {}),
6739
+ slots.barEnd
6740
+ ]
6741
+ })
6742
+ ] }),
6743
+ /* @__PURE__ */ jsx(PhoneTabs, { location }),
6744
+ slots.belowBar
6745
+ ] });
6746
+ }
6747
+ /** The section tabs on a phone, where the bar has no room for them: a row under it. */
6748
+ function PhoneTabs({ location }) {
6749
+ return /* @__PURE__ */ jsx("nav", {
6750
+ className: "sticky top-[52px] z-30 hidden h-10 shrink-0 items-stretch border-b border-rule bg-surface px-2 max-md:flex",
6751
+ children: TABS.map((tab) => {
6752
+ const on = tab.match(location);
6753
+ return /* @__PURE__ */ jsxs(Link, {
6754
+ href: tab.to,
6755
+ "aria-current": on ? "page" : void 0,
6756
+ className: cn("relative flex grow items-center justify-center text-[13.5px] no-underline", on ? "font-medium text-ink" : "text-ink2"),
6757
+ children: [tab.label, on ? /* @__PURE__ */ jsx("span", {
6758
+ "aria-hidden": true,
6759
+ className: "absolute inset-x-6 bottom-0 h-[2px] rounded-full bg-action"
6760
+ }) : null]
6761
+ }, tab.to);
6736
6762
  })
6737
- ] }), slots.belowBar] });
6763
+ });
6764
+ }
6765
+ //#endregion
6766
+ //#region viewer/src/lib/phone.ts
6767
+ /** Phones: under Tailwind's `md`. The canvas gives way to one screen at a time there. */
6768
+ var media = window.matchMedia("(max-width: 767px)");
6769
+ var subscribe = (onChange) => {
6770
+ media.addEventListener("change", onChange);
6771
+ return () => media.removeEventListener("change", onChange);
6772
+ };
6773
+ function useIsPhone() {
6774
+ return useSyncExternalStore(subscribe, () => media.matches);
6738
6775
  }
6739
6776
  //#endregion
6740
6777
  //#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
@@ -6999,6 +7036,10 @@ function Toaster() {
6999
7036
  name: "alert",
7000
7037
  size: 15,
7001
7038
  className: "text-danger-text"
7039
+ }) : item.tone === "info" ? /* @__PURE__ */ jsx(Icon, {
7040
+ name: "info",
7041
+ size: 15,
7042
+ className: "text-muted"
7002
7043
  }) : /* @__PURE__ */ jsx(Icon, {
7003
7044
  name: "check",
7004
7045
  size: 15,
@@ -7106,6 +7147,31 @@ function frameSrc(url, theme, extra) {
7106
7147
  function absoluteUrl(url) {
7107
7148
  return new URL(url, document.baseURI).toString();
7108
7149
  }
7150
+ /**
7151
+ * A link or form the runtime kept from leaving its frame. A path whose last part names a screen
7152
+ * or URL item on the canvas (`/settings`, `settings.html`) goes there; a link to another site
7153
+ * opens in a new tab; anything else says where it pointed.
7154
+ */
7155
+ function followLink(message, canvas, go) {
7156
+ if (message.form) {
7157
+ toast("Forms are not sent from a screen", void 0, "info");
7158
+ return;
7159
+ }
7160
+ if (message.path === null) {
7161
+ const tab = window.open(message.href, "_blank");
7162
+ if (tab) tab.opener = null;
7163
+ else toast("Link to another site", message.href, "info");
7164
+ return;
7165
+ }
7166
+ const path = message.path.split(/[?#]/)[0].replace(/\/+$/, "");
7167
+ let name = path.slice(path.lastIndexOf("/") + 1).replace(/\.html?$/, "");
7168
+ try {
7169
+ name = decodeURIComponent(name);
7170
+ } catch {}
7171
+ const ids = new Set(canvas.pages.flatMap((page) => page.sections.flatMap((section) => section.items.filter((item) => item.kind === "screen" || item.kind === "url").map((item) => item.id))));
7172
+ if (name && ids.has(name)) go(name);
7173
+ else toast("Not a screen on this canvas", message.path, "info");
7174
+ }
7109
7175
  //#endregion
7110
7176
  //#region viewer/src/components/inspector.tsx
7111
7177
  /**
@@ -7807,7 +7873,7 @@ function KV({ label, children, labelWidth = 180, minH = 40, end = false }) {
7807
7873
  return /* @__PURE__ */ jsxs("div", {
7808
7874
  className: "grid content-center items-baseline gap-x-4 border-t border-rule px-4 py-2",
7809
7875
  style: {
7810
- gridTemplateColumns: `${labelWidth}px minmax(0, 1fr)`,
7876
+ gridTemplateColumns: `min(${labelWidth}px, 42%) minmax(0, 1fr)`,
7811
7877
  minHeight: minH
7812
7878
  },
7813
7879
  children: [/* @__PURE__ */ jsx("span", {
@@ -7967,7 +8033,7 @@ function PageHead({ title, sub, right, badge, crumbs }) {
7967
8033
  }), badge]
7968
8034
  }),
7969
8035
  sub ? /* @__PURE__ */ jsx("div", {
7970
- className: "text-[13px] text-muted",
8036
+ className: "text-[13px] [overflow-wrap:anywhere] text-muted",
7971
8037
  children: sub
7972
8038
  }) : null
7973
8039
  ]
@@ -8779,7 +8845,7 @@ function FullPage({ canvasId, itemId }) {
8779
8845
  else if (message.type === "go") {
8780
8846
  const id = message.target.includes("/") ? message.target.split("/")[1] : message.target;
8781
8847
  if (items.some((entry) => entry.id === id)) navigate(fullHref(canvasId, id, theme), { replace: true });
8782
- }
8848
+ } else if (message.type === "link" && canvas) followLink(message, canvas, (id) => navigate(fullHref(canvasId, id, theme), { replace: true }));
8783
8849
  });
8784
8850
  }, [url, item?.kind]);
8785
8851
  if (error) return /* @__PURE__ */ jsx(NotFound, {
@@ -11137,6 +11203,9 @@ var FrameItem = memo(function FrameItem({ placed, mode, thumb, theme, store, eve
11137
11203
  case "go":
11138
11204
  events.onGo(item.id, message.target);
11139
11205
  break;
11206
+ case "link":
11207
+ events.onLink(item.id, message);
11208
+ break;
11140
11209
  case "wheel":
11141
11210
  events.onWheel(frame, message);
11142
11211
  break;
@@ -11917,7 +11986,7 @@ function isTyping(target) {
11917
11986
  //#endregion
11918
11987
  //#region viewer/src/pages/canvas/canvas-page.tsx
11919
11988
  var query = new URLSearchParams(window.location.search);
11920
- var CAPTURE = query.has("capture");
11989
+ var CAPTURE$1 = query.has("capture");
11921
11990
  var cameraKey = (canvas, page) => `foss-design.camera.${canvas}.${page}`;
11922
11991
  function loadCamera(key) {
11923
11992
  try {
@@ -11953,7 +12022,7 @@ function CanvasView({ canvas, page }) {
11953
12022
  const camera = useMemo(() => new CameraStore(), []);
11954
12023
  const apiRef = useRef(null);
11955
12024
  const [theme, setTheme] = useScreenTheme(canvas);
11956
- const [sidebar, setSidebar] = useState(() => !CAPTURE && window.innerWidth > 900);
12025
+ const [sidebar, setSidebar] = useState(() => !CAPTURE$1 && window.innerWidth > 900);
11957
12026
  const [inspectOn, setInspect] = useState(false);
11958
12027
  const sizes = useStore(store, (state) => state.sizes);
11959
12028
  const layout = useMemo(() => page ? layoutPage(page, sizes) : {
@@ -11997,7 +12066,7 @@ function CanvasView({ canvas, page }) {
11997
12066
  if (placed.current || !layout.bounds.w) return;
11998
12067
  placed.current = true;
11999
12068
  const focus = takeFocus(canvas.id);
12000
- const saved = CAPTURE ? null : loadCamera(key);
12069
+ const saved = CAPTURE$1 ? null : loadCamera(key);
12001
12070
  if (focus) {
12002
12071
  store.set((state) => ({
12003
12072
  ...state,
@@ -12005,7 +12074,7 @@ function CanvasView({ canvas, page }) {
12005
12074
  }));
12006
12075
  requestAnimationFrame(() => apiRef.current?.fitItem(focus, false));
12007
12076
  } else if (saved && saved.z >= camera.minZoom) camera.set(saved);
12008
- else requestAnimationFrame(() => CAPTURE ? apiRef.current?.fitAll(false) : apiRef.current?.openView());
12077
+ else requestAnimationFrame(() => CAPTURE$1 ? apiRef.current?.fitAll(false) : apiRef.current?.openView());
12009
12078
  }, [
12010
12079
  layout,
12011
12080
  camera,
@@ -12014,7 +12083,7 @@ function CanvasView({ canvas, page }) {
12014
12083
  store
12015
12084
  ]);
12016
12085
  useEffect(() => {
12017
- if (CAPTURE) return;
12086
+ if (CAPTURE$1) return;
12018
12087
  let timer;
12019
12088
  const off = camera.subscribe((c) => {
12020
12089
  clearTimeout(timer);
@@ -12078,6 +12147,9 @@ function CanvasView({ canvas, page }) {
12078
12147
  }
12079
12148
  console.warn(`[design] go("${target}"): no such screen`);
12080
12149
  },
12150
+ onLink(from, message) {
12151
+ followLink(message, canvas, (id) => this.onGo(from, id));
12152
+ },
12081
12153
  onWheel(frame, message) {
12082
12154
  const root = frame.closest(".canvas-root");
12083
12155
  if (!root) return;
@@ -12104,7 +12176,7 @@ function CanvasView({ canvas, page }) {
12104
12176
  navigate
12105
12177
  ]);
12106
12178
  useEffect(() => {
12107
- if (!CAPTURE) return;
12179
+ if (!CAPTURE$1) return;
12108
12180
  const started = Date.now();
12109
12181
  const timer = setInterval(() => {
12110
12182
  const frames = store.get().frames;
@@ -12187,7 +12259,7 @@ function CanvasView({ canvas, page }) {
12187
12259
  inspectOn,
12188
12260
  inspection
12189
12261
  ]);
12190
- if (CAPTURE) return /* @__PURE__ */ jsx("div", {
12262
+ if (CAPTURE$1) return /* @__PURE__ */ jsx("div", {
12191
12263
  className: "flex h-dvh",
12192
12264
  children: /* @__PURE__ */ jsx(Viewport, {
12193
12265
  layout,
@@ -12403,6 +12475,79 @@ function useScreenFrames(frameEls, layout) {
12403
12475
  }, [frameEls, layout]);
12404
12476
  }
12405
12477
  //#endregion
12478
+ //#region viewer/src/ui/sheet.tsx
12479
+ /**
12480
+ * A panel sliding up from the bottom edge, for phones: lists to pick from and actions. Like
12481
+ * Dialog, focus stays inside, and Escape, a tap on the dimmed page or the close button close it.
12482
+ */
12483
+ function Sheet({ open, onOpenChange, title, children }) {
12484
+ return /* @__PURE__ */ jsx(Dialog$1, {
12485
+ open,
12486
+ onOpenChange,
12487
+ children: /* @__PURE__ */ jsx(AnimatePresence, { children: open ? /* @__PURE__ */ jsxs(DialogPortal, {
12488
+ forceMount: true,
12489
+ children: [/* @__PURE__ */ jsx(DialogOverlay, {
12490
+ asChild: true,
12491
+ forceMount: true,
12492
+ children: /* @__PURE__ */ jsx(motion.div, {
12493
+ initial: { opacity: 0 },
12494
+ animate: { opacity: 1 },
12495
+ exit: { opacity: 0 },
12496
+ transition: { duration: .16 },
12497
+ className: "fixed inset-0 z-50 bg-overlay"
12498
+ })
12499
+ }), /* @__PURE__ */ jsx(DialogContent, {
12500
+ asChild: true,
12501
+ forceMount: true,
12502
+ "aria-describedby": void 0,
12503
+ children: /* @__PURE__ */ jsxs(motion.div, {
12504
+ initial: { y: "100%" },
12505
+ animate: { y: 0 },
12506
+ exit: {
12507
+ y: "100%",
12508
+ transition: {
12509
+ duration: .18,
12510
+ ease: "easeIn"
12511
+ }
12512
+ },
12513
+ transition: {
12514
+ type: "spring",
12515
+ stiffness: 420,
12516
+ damping: 40
12517
+ },
12518
+ 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",
12519
+ children: [
12520
+ /* @__PURE__ */ jsx("div", {
12521
+ className: "flex justify-center pt-2",
12522
+ children: /* @__PURE__ */ jsx("span", {
12523
+ "aria-hidden": true,
12524
+ className: "h-1 w-9 rounded-full bg-rule-strong"
12525
+ })
12526
+ }),
12527
+ /* @__PURE__ */ jsxs("div", {
12528
+ className: "flex min-h-[48px] items-center justify-between gap-3 pr-2 pl-4",
12529
+ children: [/* @__PURE__ */ jsx(DialogTitle, {
12530
+ className: "m-0 truncate text-[15px] font-semibold",
12531
+ children: title
12532
+ }), /* @__PURE__ */ jsx(DialogClose, {
12533
+ asChild: true,
12534
+ children: /* @__PURE__ */ jsx(IconButton, {
12535
+ icon: "x",
12536
+ label: "Close"
12537
+ })
12538
+ })]
12539
+ }),
12540
+ /* @__PURE__ */ jsx("div", {
12541
+ className: "min-h-0 overflow-y-auto overscroll-contain border-t border-rule",
12542
+ children
12543
+ })
12544
+ ]
12545
+ })
12546
+ })]
12547
+ }) : null })
12548
+ });
12549
+ }
12550
+ //#endregion
12406
12551
  //#region viewer/src/pages/canvas/play-page.tsx
12407
12552
  function framesOf(canvas, pageId) {
12408
12553
  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) => ({
@@ -12509,7 +12654,10 @@ function Player({ canvas, itemId }) {
12509
12654
  const [pagePart, idPart] = message.target.includes("/") ? message.target.split("/") : [null, message.target];
12510
12655
  const target = all.find((e) => e.item.id === idPart && (!pagePart || e.pageId === pagePart));
12511
12656
  if (target) navigate(playHref(canvas.id, target.item.id, target.pageId));
12512
- }
12657
+ } else if (message.type === "link") followLink(message, canvas, (id) => {
12658
+ const target = all.find((e) => e.item.id === id);
12659
+ if (target) navigate(playHref(canvas.id, target.item.id, target.pageId));
12660
+ });
12513
12661
  });
12514
12662
  }, [
12515
12663
  item,
@@ -12705,6 +12853,441 @@ function Player({ canvas, itemId }) {
12705
12853
  });
12706
12854
  }
12707
12855
  //#endregion
12856
+ //#region viewer/src/pages/canvas/phone-view.tsx
12857
+ /** Room around the screen on the stage, per side. */
12858
+ var GUTTER = 12;
12859
+ /**
12860
+ * A canvas on a phone: no board to pan, one screen at a time as in play mode. The screen fills
12861
+ * the width and scrolls; the bottom bar steps through every screen of the canvas and opens the
12862
+ * list of them. Serves `/c/:canvas`, its pages and `/c/:canvas/play/:item` under 768px.
12863
+ */
12864
+ function PhoneCanvas({ canvasId, pageId, itemId }) {
12865
+ const { data: canvas, error, isLoading } = useCanvas(canvasId);
12866
+ if (error) return /* @__PURE__ */ jsx(NotFound, {
12867
+ title: "Canvas not found",
12868
+ text: error.message
12869
+ });
12870
+ if (isLoading || !canvas) return /* @__PURE__ */ jsx("div", { className: "canvas-dots h-dvh" });
12871
+ return /* @__PURE__ */ jsx(PhoneViewer, {
12872
+ canvas,
12873
+ pageId,
12874
+ itemId
12875
+ });
12876
+ }
12877
+ function PhoneViewer({ canvas, pageId, itemId }) {
12878
+ const [, navigate] = useLocation();
12879
+ const { frameSandbox, slots, scope } = useViewer();
12880
+ const appTheme = useTheme();
12881
+ const all = useMemo(() => framesOf(canvas, null), [canvas]);
12882
+ const index = Math.max(0, itemId ? all.findIndex((entry) => entry.item.id === itemId) : pageId ? all.findIndex((entry) => entry.pageId === pageId) : 0);
12883
+ const entry = all[index];
12884
+ const item = entry?.item;
12885
+ const [theme, setTheme] = useState(canvas.theme ?? appTheme);
12886
+ const [listOpen, setListOpen] = useState(false);
12887
+ const [errors, setErrors] = useState([]);
12888
+ const [stageWidth, setStageWidth] = useState(0);
12889
+ const [autoHeight, setAutoHeight] = useState(null);
12890
+ const [direction, setDirection] = useState(0);
12891
+ const [actual, setActual] = useState(false);
12892
+ const stage = useRef(null);
12893
+ const frame = useRef(null);
12894
+ const [frameEl, setFrameEl] = useState(null);
12895
+ const setFrame = useCallback((el) => {
12896
+ if (!el) return;
12897
+ frame.current = el;
12898
+ setFrameEl(el);
12899
+ }, []);
12900
+ const show = useCallback((target, step = 0) => {
12901
+ setDirection(step);
12902
+ navigate(playHref(canvas.id, target.item.id, target.pageId), { replace: true });
12903
+ }, [canvas.id, navigate]);
12904
+ const move = (step) => {
12905
+ if (all.length < 2) return;
12906
+ show(all[(index + step + all.length) % all.length], step);
12907
+ };
12908
+ const goTo = (id) => {
12909
+ const target = all.find((e) => e.item.id === id);
12910
+ if (target) show(target, all.indexOf(target) > index ? 1 : -1);
12911
+ };
12912
+ useLayoutEffect(() => {
12913
+ const el = stage.current;
12914
+ if (!el) return;
12915
+ const observer = new ResizeObserver(() => setStageWidth(el.clientWidth));
12916
+ observer.observe(el);
12917
+ return () => observer.disconnect();
12918
+ }, []);
12919
+ const frameTheme = item?.kind === "screen" ? item.theme ?? theme : theme;
12920
+ const [openTheme] = useState(frameTheme);
12921
+ const src = item ? item.kind === "screen" ? frameSrc(item.url, openTheme) : item.url : "";
12922
+ const openUrl = useOpenUrl(canvas.id, item, frameTheme);
12923
+ useEffect(() => {
12924
+ setErrors([]);
12925
+ setAutoHeight(item?.kind === "screen" ? item.measuredHeight ?? null : null);
12926
+ stage.current?.scrollTo({ top: 0 });
12927
+ const el = frame.current;
12928
+ if (!el || item?.kind !== "screen") return;
12929
+ return listenToFrame(el, (message) => {
12930
+ if (message.type === "size") setAutoHeight(message.height);
12931
+ else if (message.type === "error") setErrors((list) => list.includes(message.message) ? list : [...list, message.message]);
12932
+ else if (message.type === "updated") setErrors([]);
12933
+ else if (message.type === "go") goTo(message.target.includes("/") ? message.target.split("/")[1] : message.target);
12934
+ else if (message.type === "link") followLink(message, canvas, goTo);
12935
+ });
12936
+ }, [frameEl, src]);
12937
+ useEffect(() => {
12938
+ if (item?.kind === "screen") sendTheme(frame.current, frameTheme);
12939
+ }, [frameTheme, item]);
12940
+ useEffect(() => {
12941
+ const onKey = (event) => {
12942
+ if (document.querySelector("[role=\"dialog\"]")) return;
12943
+ if (event.key === "ArrowRight") move(1);
12944
+ else if (event.key === "ArrowLeft") move(-1);
12945
+ };
12946
+ window.addEventListener("keydown", onKey);
12947
+ return () => window.removeEventListener("keydown", onKey);
12948
+ });
12949
+ const swipe = useRef(null);
12950
+ const onTouchStart = (event) => {
12951
+ const touch = event.touches[0];
12952
+ swipe.current = touch ? {
12953
+ x: touch.clientX,
12954
+ y: touch.clientY
12955
+ } : null;
12956
+ };
12957
+ const onTouchEnd = (event) => {
12958
+ const start = swipe.current;
12959
+ const touch = event.changedTouches[0];
12960
+ swipe.current = null;
12961
+ if (!start || !touch) return;
12962
+ const dx = touch.clientX - start.x;
12963
+ const dy = touch.clientY - start.y;
12964
+ if (Math.abs(dx) > 56 && Math.abs(dx) > Math.abs(dy) * 1.5) move(dx < 0 ? 1 : -1);
12965
+ };
12966
+ if (!item || !entry) return /* @__PURE__ */ jsxs("div", {
12967
+ className: "flex h-dvh flex-col",
12968
+ children: [/* @__PURE__ */ jsx(PhoneBar, {
12969
+ canvas,
12970
+ title: canvas.title,
12971
+ sub: null
12972
+ }), /* @__PURE__ */ jsx(NotFound, {
12973
+ title: "No screens here",
12974
+ text: "This canvas has no screens to show yet."
12975
+ })]
12976
+ });
12977
+ const width = item.frame.width;
12978
+ const fitScale = stageWidth ? Math.min(1, (stageWidth - 24) / width) : 0;
12979
+ const wide = fitScale > 0 && fitScale < .8;
12980
+ const scale = wide && actual ? 1 : fitScale;
12981
+ const height = item.frame.height === "auto" ? Math.max(autoHeight ?? 0, 480) : item.frame.height;
12982
+ const canTheme = item.kind === "screen" && !item.theme;
12983
+ return /* @__PURE__ */ jsxs("div", {
12984
+ className: "flex h-dvh flex-col",
12985
+ children: [
12986
+ /* @__PURE__ */ jsx(PhoneBar, {
12987
+ canvas,
12988
+ title: item.title || item.id,
12989
+ sub: [canvas.title, entry.where].filter(Boolean).join(" · "),
12990
+ back: scope ? void 0 : "/",
12991
+ end: /* @__PURE__ */ jsxs(Fragment, { children: [
12992
+ wide ? /* @__PURE__ */ jsx(IconButton, {
12993
+ icon: actual ? "zoom-out" : "zoom-in",
12994
+ label: actual ? "Fit to width" : "Actual size",
12995
+ onClick: () => setActual((value) => !value)
12996
+ }) : null,
12997
+ /* @__PURE__ */ jsxs(Menu, {
12998
+ align: "end",
12999
+ width: 220,
13000
+ trigger: /* @__PURE__ */ jsx(IconButton, {
13001
+ icon: "more",
13002
+ label: "Screen options"
13003
+ }),
13004
+ children: [
13005
+ canTheme ? /* @__PURE__ */ jsxs(Fragment, { children: [
13006
+ /* @__PURE__ */ jsx(MenuLabel, { children: "Screen theme" }),
13007
+ /* @__PURE__ */ jsx(ThemeItem, {
13008
+ value: "light",
13009
+ current: theme,
13010
+ onPick: setTheme
13011
+ }),
13012
+ /* @__PURE__ */ jsx(ThemeItem, {
13013
+ value: "dark",
13014
+ current: theme,
13015
+ onPick: setTheme
13016
+ }),
13017
+ /* @__PURE__ */ jsx(MenuSeparator, {})
13018
+ ] }) : null,
13019
+ /* @__PURE__ */ jsxs(MenuItem, {
13020
+ onSelect: () => reloadFrame(frame.current),
13021
+ children: [/* @__PURE__ */ jsx(Icon, {
13022
+ name: "refresh",
13023
+ size: 15,
13024
+ className: "text-ink2"
13025
+ }), "Reload screen"]
13026
+ }),
13027
+ openUrl ? /* @__PURE__ */ jsxs(MenuItem, {
13028
+ onSelect: () => window.open(openUrl, "_blank", "noopener"),
13029
+ children: [/* @__PURE__ */ jsx(Icon, {
13030
+ name: "external",
13031
+ size: 15,
13032
+ className: "text-ink2"
13033
+ }), "Open in a new tab"]
13034
+ }) : null,
13035
+ /* @__PURE__ */ jsxs(MenuItem, {
13036
+ onSelect: () => setThemePref(appTheme === "dark" ? "light" : "dark"),
13037
+ children: [/* @__PURE__ */ jsx(Icon, {
13038
+ name: appTheme === "dark" ? "sun" : "moon",
13039
+ size: 15,
13040
+ className: "text-ink2"
13041
+ }), appTheme === "dark" ? "Light appearance" : "Dark appearance"]
13042
+ })
13043
+ ]
13044
+ }),
13045
+ slots.barEnd
13046
+ ] })
13047
+ }),
13048
+ slots.belowBar,
13049
+ /* @__PURE__ */ jsxs("div", {
13050
+ ref: stage,
13051
+ onTouchStart,
13052
+ onTouchEnd,
13053
+ className: cn("canvas-dots relative min-h-0 grow overflow-y-auto overscroll-contain", wide && actual ? "overflow-x-auto" : "overflow-x-hidden"),
13054
+ children: [errors.length ? /* @__PURE__ */ jsxs("div", {
13055
+ 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",
13056
+ children: [/* @__PURE__ */ jsx(Icon, {
13057
+ name: "alert",
13058
+ size: 14,
13059
+ className: "mt-px shrink-0"
13060
+ }), /* @__PURE__ */ jsx("span", {
13061
+ className: "min-w-0 break-words",
13062
+ children: errors.join("\n")
13063
+ })]
13064
+ }) : null, /* @__PURE__ */ jsx(AnimatePresence, {
13065
+ initial: false,
13066
+ mode: "popLayout",
13067
+ custom: direction,
13068
+ children: /* @__PURE__ */ jsx(motion.div, {
13069
+ custom: direction,
13070
+ initial: {
13071
+ opacity: 0,
13072
+ x: direction * 40
13073
+ },
13074
+ animate: {
13075
+ opacity: 1,
13076
+ x: 0
13077
+ },
13078
+ exit: {
13079
+ opacity: 0,
13080
+ x: direction * -40,
13081
+ transition: { duration: .12 }
13082
+ },
13083
+ transition: {
13084
+ type: "spring",
13085
+ stiffness: 420,
13086
+ damping: 38
13087
+ },
13088
+ className: cn("flex", wide && actual ? "w-max" : "justify-center"),
13089
+ style: { padding: GUTTER },
13090
+ children: scale ? /* @__PURE__ */ jsx("div", {
13091
+ className: "shrink-0 overflow-hidden rounded-[6px] bg-surface",
13092
+ style: {
13093
+ width: width * scale,
13094
+ height: height * scale,
13095
+ boxShadow: "var(--shadow-frame), 0 0 0 1px var(--rule)"
13096
+ },
13097
+ children: /* @__PURE__ */ jsx("iframe", {
13098
+ ref: setFrame,
13099
+ src,
13100
+ title: item.title,
13101
+ allow: "clipboard-read; clipboard-write; fullscreen",
13102
+ sandbox: frameSandbox,
13103
+ className: "block border-0",
13104
+ style: {
13105
+ width,
13106
+ height,
13107
+ transform: `scale(${scale})`,
13108
+ transformOrigin: "0 0",
13109
+ colorScheme: frameTheme
13110
+ },
13111
+ onLoad: () => item.kind === "screen" && sendTheme(frame.current, frameTheme)
13112
+ }, `${item.id}:${item.kind === "screen" ? item.rev : item.url}`)
13113
+ }) : null
13114
+ }, item.id)
13115
+ })]
13116
+ }),
13117
+ /* @__PURE__ */ jsxs("nav", {
13118
+ "aria-label": "Screens",
13119
+ 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))]",
13120
+ children: [
13121
+ /* @__PURE__ */ jsx(IconButton, {
13122
+ icon: "chevron-left",
13123
+ label: "Previous screen",
13124
+ onClick: () => move(-1),
13125
+ disabled: all.length < 2
13126
+ }),
13127
+ /* @__PURE__ */ jsxs("button", {
13128
+ type: "button",
13129
+ onClick: () => setListOpen(true),
13130
+ 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",
13131
+ children: [
13132
+ /* @__PURE__ */ jsx(Icon, {
13133
+ name: "list",
13134
+ size: 15,
13135
+ className: "shrink-0 text-muted"
13136
+ }),
13137
+ /* @__PURE__ */ jsxs("span", {
13138
+ className: "tabular shrink-0 text-muted",
13139
+ children: [
13140
+ index + 1,
13141
+ " / ",
13142
+ all.length
13143
+ ]
13144
+ }),
13145
+ /* @__PURE__ */ jsx("span", {
13146
+ className: "min-w-0 truncate font-medium",
13147
+ children: item.title || item.id
13148
+ })
13149
+ ]
13150
+ }),
13151
+ /* @__PURE__ */ jsx(IconButton, {
13152
+ icon: "chevron-right",
13153
+ label: "Next screen",
13154
+ onClick: () => move(1),
13155
+ disabled: all.length < 2
13156
+ })
13157
+ ]
13158
+ }),
13159
+ /* @__PURE__ */ jsx(Sheet, {
13160
+ open: listOpen,
13161
+ onOpenChange: setListOpen,
13162
+ title: canvas.title,
13163
+ children: /* @__PURE__ */ jsx(ScreenList, {
13164
+ canvas,
13165
+ current: item.id,
13166
+ onPick: (id) => {
13167
+ setListOpen(false);
13168
+ goTo(id);
13169
+ }
13170
+ })
13171
+ })
13172
+ ]
13173
+ });
13174
+ }
13175
+ function ThemeItem({ value, current, onPick }) {
13176
+ return /* @__PURE__ */ jsxs(MenuItem, {
13177
+ active: current === value,
13178
+ onSelect: () => onPick(value),
13179
+ children: [
13180
+ /* @__PURE__ */ jsx(Icon, {
13181
+ name: value === "dark" ? "moon" : "sun",
13182
+ size: 15,
13183
+ className: "text-ink2"
13184
+ }),
13185
+ value === "dark" ? "Dark" : "Light",
13186
+ current === value ? /* @__PURE__ */ jsx(Icon, {
13187
+ name: "check",
13188
+ size: 15,
13189
+ className: "ml-auto text-ink2"
13190
+ }) : null
13191
+ ]
13192
+ });
13193
+ }
13194
+ /** The phone's top bar: a way back, the screen and where it sits, and the screen's actions. */
13195
+ function PhoneBar({ canvas, title, sub, back, end }) {
13196
+ const { slots } = useViewer();
13197
+ return /* @__PURE__ */ jsxs("header", {
13198
+ 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)]",
13199
+ children: [
13200
+ slots.barStart,
13201
+ back ? /* @__PURE__ */ jsx(Link, {
13202
+ href: back,
13203
+ "aria-label": `All canvases, leaving ${canvas.title}`,
13204
+ className: "inline-flex size-10 shrink-0 items-center justify-center rounded-[8px] text-ink2 active:bg-soft2",
13205
+ children: /* @__PURE__ */ jsx(Icon, {
13206
+ name: "arrow-left",
13207
+ size: 18
13208
+ })
13209
+ }) : /* @__PURE__ */ jsx("span", {
13210
+ className: "flex size-10 shrink-0 items-center justify-center",
13211
+ children: /* @__PURE__ */ jsx(Mark, {})
13212
+ }),
13213
+ /* @__PURE__ */ jsxs("div", {
13214
+ className: "flex min-w-0 grow flex-col leading-tight",
13215
+ children: [/* @__PURE__ */ jsx("span", {
13216
+ className: "truncate text-[14.5px] font-semibold",
13217
+ children: title
13218
+ }), sub ? /* @__PURE__ */ jsx("span", {
13219
+ className: "truncate text-[12px] text-muted",
13220
+ children: sub
13221
+ }) : null]
13222
+ }),
13223
+ /* @__PURE__ */ jsx("div", {
13224
+ className: "flex shrink-0 items-center gap-1",
13225
+ children: end
13226
+ })
13227
+ ]
13228
+ });
13229
+ }
13230
+ var KIND_ICON = (entry) => entry.item.kind === "url" ? "globe" : entry.item.frame.device === "phone" ? "smartphone" : entry.item.frame.device === "tablet" ? "tablet" : "monitor";
13231
+ /** Every screen of the canvas by page and section, the current one marked. */
13232
+ function ScreenList({ canvas, current, onPick }) {
13233
+ const activeRef = useRef(null);
13234
+ useEffect(() => {
13235
+ activeRef.current?.scrollIntoView({ block: "center" });
13236
+ }, []);
13237
+ const multi = canvas.pages.length > 1;
13238
+ return /* @__PURE__ */ jsx("div", {
13239
+ className: "flex flex-col pb-2",
13240
+ children: canvas.pages.map((page) => {
13241
+ const entries = framesOf(canvas, page.id);
13242
+ if (!entries.length) return null;
13243
+ return /* @__PURE__ */ jsxs("section", {
13244
+ className: "flex flex-col",
13245
+ children: [multi ? /* @__PURE__ */ jsx("h3", {
13246
+ className: "sticky top-0 z-[1] m-0 border-b border-rule bg-soft px-4 py-2 text-[12px] font-semibold text-ink2",
13247
+ children: page.title
13248
+ }) : null, page.sections.map((section) => {
13249
+ const items = entries.filter((entry) => section.items.includes(entry.item));
13250
+ if (!items.length) return null;
13251
+ return /* @__PURE__ */ jsxs("div", {
13252
+ className: "flex flex-col",
13253
+ children: [section.title ? /* @__PURE__ */ jsx("div", {
13254
+ className: "px-4 pt-3 pb-1 text-[12px] text-muted",
13255
+ children: section.title
13256
+ }) : null, items.map((entry) => {
13257
+ const on = entry.item.id === current;
13258
+ return /* @__PURE__ */ jsxs("button", {
13259
+ ref: on ? activeRef : void 0,
13260
+ type: "button",
13261
+ "aria-current": on ? "true" : void 0,
13262
+ onClick: () => onPick(entry.item.id),
13263
+ 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"),
13264
+ children: [
13265
+ /* @__PURE__ */ jsx("span", {
13266
+ className: cn("flex size-8 shrink-0 items-center justify-center rounded-[8px]", on ? "bg-action-soft text-action" : "bg-soft2 text-muted"),
13267
+ children: /* @__PURE__ */ jsx(Icon, {
13268
+ name: KIND_ICON(entry),
13269
+ size: 15
13270
+ })
13271
+ }),
13272
+ /* @__PURE__ */ jsx("span", {
13273
+ className: "min-w-0 grow truncate",
13274
+ children: entry.item.title || entry.item.id
13275
+ }),
13276
+ on ? /* @__PURE__ */ jsx(Icon, {
13277
+ name: "check",
13278
+ size: 16,
13279
+ className: "shrink-0 text-action"
13280
+ }) : null
13281
+ ]
13282
+ }, entry.item.id);
13283
+ })]
13284
+ }, section.id);
13285
+ })]
13286
+ }, page.id);
13287
+ })
13288
+ });
13289
+ }
13290
+ //#endregion
12708
13291
  //#region viewer/src/lib/format.ts
12709
13292
  var units = [
12710
13293
  ["year", 31536e3],
@@ -12805,6 +13388,7 @@ function Foot({ children }) {
12805
13388
  }
12806
13389
  function CanvasList({ project }) {
12807
13390
  const { canvasRowActions: rowActions, canvasesFoot } = useViewer().slots;
13391
+ const phone = useIsPhone();
12808
13392
  if (!project.canvases.length) return /* @__PURE__ */ jsxs(Panel, { children: [
12809
13393
  /* @__PURE__ */ jsx(PanelHead, {
12810
13394
  title: "Canvases",
@@ -12835,7 +13419,31 @@ function CanvasList({ project }) {
12835
13419
  const actions = rowActions?.(canvas.id);
12836
13420
  return /* @__PURE__ */ jsx(WithActions, {
12837
13421
  actions,
12838
- children: /* @__PURE__ */ jsxs(RowLink, {
13422
+ children: phone ? /* @__PURE__ */ jsxs(RowLink, {
13423
+ to: `/c/${encodeURIComponent(canvas.id)}`,
13424
+ cols: "86px minmax(0,1fr)",
13425
+ align: "ll",
13426
+ pad: "10px 16px",
13427
+ minH: 74,
13428
+ first: !!actions,
13429
+ children: [/* @__PURE__ */ jsx(Cover, { canvas }), /* @__PURE__ */ jsx(Two, {
13430
+ top: canvas.title,
13431
+ bottom: /* @__PURE__ */ jsxs("span", {
13432
+ className: "flex min-w-0 items-center gap-2",
13433
+ children: [canvas.issues ? /* @__PURE__ */ jsx(Badge, {
13434
+ tone: "danger",
13435
+ children: plural(canvas.issues, "problem")
13436
+ }) : null, /* @__PURE__ */ jsxs("span", {
13437
+ className: "truncate",
13438
+ children: [
13439
+ plural(canvas.screens, "screen"),
13440
+ " · ",
13441
+ relativeTime(canvas.updatedAt)
13442
+ ]
13443
+ })]
13444
+ })
13445
+ })]
13446
+ }) : /* @__PURE__ */ jsxs(RowLink, {
12839
13447
  to: `/c/${encodeURIComponent(canvas.id)}`,
12840
13448
  cols: "86px minmax(0,1fr) auto auto",
12841
13449
  align: "llrr",
@@ -12965,9 +13573,11 @@ function HomePage() {
12965
13573
  sub: slots.homeSub ? slots.homeSub : project.static ? `Snapshot built with foss-design ${project.version}` : /* @__PURE__ */ jsxs(Fragment, { children: [
12966
13574
  /* @__PURE__ */ jsxs(Mono, {
12967
13575
  size: 12.5,
13576
+ wrap: true,
12968
13577
  children: [project.root, "/.design"]
12969
13578
  }),
12970
- " · foss-design ",
13579
+ " ",
13580
+ "· foss-design ",
12971
13581
  project.version
12972
13582
  ] })
12973
13583
  }),
@@ -16628,6 +17238,7 @@ function MotionPage({ system }) {
16628
17238
  //#region viewer/src/pages/system/nav.tsx
16629
17239
  /** Grouped links of the system section; a wrapping row above the content on narrow screens. */
16630
17240
  function SystemNav({ system }) {
17241
+ const phone = useIsPhone();
16631
17242
  const kinds = new Set(system.tokens.map((token) => token.kind));
16632
17243
  const foundations = [
16633
17244
  {
@@ -16669,6 +17280,33 @@ function SystemNav({ system }) {
16669
17280
  groups.set(component.group, list);
16670
17281
  }
16671
17282
  const guidelines = system.guidelines.filter((guide) => guide.slug !== "overview");
17283
+ if (phone) {
17284
+ const sections = [
17285
+ {
17286
+ label: "",
17287
+ items: [{
17288
+ to: "/system",
17289
+ label: "Overview"
17290
+ }]
17291
+ },
17292
+ {
17293
+ label: "Foundations",
17294
+ items: foundations
17295
+ },
17296
+ {
17297
+ label: "Guidelines",
17298
+ items: guidelines.map((guide) => ({
17299
+ to: `/system/guidelines/${encodeURIComponent(guide.slug)}`,
17300
+ label: guide.title
17301
+ }))
17302
+ },
17303
+ ...[...groups].map(([label, items]) => ({
17304
+ label,
17305
+ items
17306
+ }))
17307
+ ];
17308
+ return /* @__PURE__ */ jsx(PhoneSystemNav, { sections: sections.filter((section) => section.items.length) });
17309
+ }
16672
17310
  return /* @__PURE__ */ jsxs("div", {
16673
17311
  className: "flex flex-col max-lg:gap-1 max-lg:[&_nav]:flex-row max-lg:[&_nav]:flex-wrap",
16674
17312
  children: [
@@ -16704,6 +17342,70 @@ function SystemNav({ system }) {
16704
17342
  function count(system, kind) {
16705
17343
  return system.tokens.filter((token) => token.kind === kind).length || void 0;
16706
17344
  }
17345
+ /** On a phone: the current section on a full-width button that lists them all in a sheet. */
17346
+ function PhoneSystemNav({ sections }) {
17347
+ const [location] = useLocation();
17348
+ const [open, setOpen] = useState(false);
17349
+ const current = sections.flatMap((section) => section.items).find((item) => item.to === location);
17350
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
17351
+ type: "button",
17352
+ onClick: () => setOpen(true),
17353
+ 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",
17354
+ children: [
17355
+ /* @__PURE__ */ jsx("span", {
17356
+ className: "text-[12.5px] text-muted",
17357
+ children: "Section"
17358
+ }),
17359
+ /* @__PURE__ */ jsx("span", {
17360
+ className: "min-w-0 grow truncate font-medium",
17361
+ children: current?.label ?? "Overview"
17362
+ }),
17363
+ /* @__PURE__ */ jsx(Icon, {
17364
+ name: "chevron-down",
17365
+ size: 16,
17366
+ className: "shrink-0 text-muted"
17367
+ })
17368
+ ]
17369
+ }), /* @__PURE__ */ jsx(Sheet, {
17370
+ open,
17371
+ onOpenChange: setOpen,
17372
+ title: "Design system",
17373
+ children: /* @__PURE__ */ jsx("nav", {
17374
+ "aria-label": "Design system",
17375
+ className: "flex flex-col pb-2",
17376
+ children: sections.map((section) => /* @__PURE__ */ jsxs("div", {
17377
+ className: "flex flex-col",
17378
+ children: [section.label ? /* @__PURE__ */ jsx("div", {
17379
+ className: "px-4 pt-3 pb-1 text-[12px] text-muted",
17380
+ children: section.label
17381
+ }) : null, section.items.map((item) => {
17382
+ const on = item.to === location;
17383
+ return /* @__PURE__ */ jsxs(Link, {
17384
+ href: item.to,
17385
+ onClick: () => setOpen(false),
17386
+ "aria-current": on ? "page" : void 0,
17387
+ 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"),
17388
+ children: [
17389
+ /* @__PURE__ */ jsx("span", {
17390
+ className: "min-w-0 grow truncate",
17391
+ children: item.label
17392
+ }),
17393
+ item.hint ? /* @__PURE__ */ jsx("span", {
17394
+ className: "text-[12.5px] text-muted",
17395
+ children: item.hint
17396
+ }) : null,
17397
+ on ? /* @__PURE__ */ jsx(Icon, {
17398
+ name: "check",
17399
+ size: 16,
17400
+ className: "shrink-0 text-action"
17401
+ }) : null
17402
+ ]
17403
+ }, item.to);
17404
+ })]
17405
+ }, section.label || "overview"))
17406
+ })
17407
+ })] });
17408
+ }
16707
17409
  //#endregion
16708
17410
  //#region viewer/src/pages/system/other-tokens.tsx
16709
17411
  function OtherTokensPage({ system }) {
@@ -17399,7 +18101,30 @@ function Title() {
17399
18101
  }, [name]);
17400
18102
  return null;
17401
18103
  }
18104
+ /** Screenshots (`?capture`) always get the canvas, whatever the window. */
18105
+ var CAPTURE = new URLSearchParams(window.location.search).has("capture");
18106
+ /** The canvas routes' pages; on a phone the canvas is one screen at a time. */
18107
+ function useCanvasPages() {
18108
+ const phone = useIsPhone() && !CAPTURE;
18109
+ return {
18110
+ play: (canvas, item) => phone ? /* @__PURE__ */ jsx(PhoneCanvas, {
18111
+ canvasId: canvas,
18112
+ itemId: item
18113
+ }) : /* @__PURE__ */ jsx(PlayPage, {
18114
+ canvasId: canvas,
18115
+ itemId: item
18116
+ }),
18117
+ canvas: (canvas, page) => phone ? /* @__PURE__ */ jsx(PhoneCanvas, {
18118
+ canvasId: canvas,
18119
+ pageId: page
18120
+ }) : /* @__PURE__ */ jsx(CanvasPage, {
18121
+ canvasId: canvas,
18122
+ pageId: page
18123
+ })
18124
+ };
18125
+ }
17402
18126
  function Routes() {
18127
+ const pages = useCanvasPages();
17403
18128
  return /* @__PURE__ */ jsxs(Switch$1, { children: [
17404
18129
  /* @__PURE__ */ jsx(Route, {
17405
18130
  path: "/c/:canvas/full/:item",
@@ -17410,21 +18135,15 @@ function Routes() {
17410
18135
  }),
17411
18136
  /* @__PURE__ */ jsx(Route, {
17412
18137
  path: "/c/:canvas/play/:item",
17413
- children: (params) => /* @__PURE__ */ jsx(PlayPage, {
17414
- canvasId: params.canvas,
17415
- itemId: params.item
17416
- })
18138
+ children: (params) => pages.play(params.canvas, params.item)
17417
18139
  }),
17418
18140
  /* @__PURE__ */ jsx(Route, {
17419
18141
  path: "/c/:canvas/p/:page",
17420
- children: (params) => /* @__PURE__ */ jsx(CanvasPage, {
17421
- canvasId: params.canvas,
17422
- pageId: params.page
17423
- })
18142
+ children: (params) => pages.canvas(params.canvas, params.page)
17424
18143
  }),
17425
18144
  /* @__PURE__ */ jsx(Route, {
17426
18145
  path: "/c/:canvas",
17427
- children: (params) => /* @__PURE__ */ jsx(CanvasPage, { canvasId: params.canvas })
18146
+ children: (params) => pages.canvas(params.canvas)
17428
18147
  }),
17429
18148
  /* @__PURE__ */ jsxs(Route, {
17430
18149
  path: "/system/*?",
@@ -17444,6 +18163,7 @@ function ScopedRoutes({ canvas }) {
17444
18163
  to: home,
17445
18164
  replace: true
17446
18165
  });
18166
+ const pages = useCanvasPages();
17447
18167
  return /* @__PURE__ */ jsxs(Switch$1, { children: [
17448
18168
  /* @__PURE__ */ jsx(Route, {
17449
18169
  path: "/c/:canvas/full/:item",
@@ -17454,21 +18174,15 @@ function ScopedRoutes({ canvas }) {
17454
18174
  }),
17455
18175
  /* @__PURE__ */ jsx(Route, {
17456
18176
  path: "/c/:canvas/play/:item",
17457
- children: (params) => guard(params.canvas, /* @__PURE__ */ jsx(PlayPage, {
17458
- canvasId: canvas,
17459
- itemId: params.item
17460
- }))
18177
+ children: (params) => guard(params.canvas, pages.play(canvas, params.item))
17461
18178
  }),
17462
18179
  /* @__PURE__ */ jsx(Route, {
17463
18180
  path: "/c/:canvas/p/:page",
17464
- children: (params) => guard(params.canvas, /* @__PURE__ */ jsx(CanvasPage, {
17465
- canvasId: canvas,
17466
- pageId: params.page
17467
- }))
18181
+ children: (params) => guard(params.canvas, pages.canvas(canvas, params.page))
17468
18182
  }),
17469
18183
  /* @__PURE__ */ jsx(Route, {
17470
18184
  path: "/c/:canvas",
17471
- children: (params) => guard(params.canvas, /* @__PURE__ */ jsx(CanvasPage, { canvasId: canvas }))
18185
+ children: (params) => guard(params.canvas, pages.canvas(canvas))
17472
18186
  }),
17473
18187
  /* @__PURE__ */ jsx(Route, { children: /* @__PURE__ */ jsx(Redirect, {
17474
18188
  to: home,
@@ -17770,4 +18484,4 @@ function TextArea({ label, hint, error, className, id, disabled, rows = 4, ...re
17770
18484
  });
17771
18485
  }
17772
18486
  //#endregion
17773
- 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 };
18487
+ 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 };