foss-design 0.9.8 → 0.9.10

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.
@@ -2329,19 +2329,36 @@ var staticSource = {
2329
2329
  return text;
2330
2330
  }
2331
2331
  };
2332
- /** `url` against a unit's base; an empty base leaves it as the doc has it. */
2332
+ /**
2333
+ * `url` against a unit's base, or '' when it may not be loaded. Docs come from builds anyone who
2334
+ * can push has written, so a URL is only kept when it is http(s) and, for a unit build, inside
2335
+ * that unit's base: a `javascript:` frame source would run with the viewer's own origin.
2336
+ */
2333
2337
  function resolveUrl(url, base) {
2334
- if (!base) return url;
2335
2338
  try {
2336
- const root = new URL(base.endsWith("/") ? base : `${base}/`, document.baseURI);
2337
- return new URL(url, root).href;
2339
+ const page = typeof document === "undefined" ? "http://localhost/" : document.baseURI;
2340
+ if (!base) {
2341
+ const own = new URL(url, page);
2342
+ return own.protocol === "http:" || own.protocol === "https:" ? url : "";
2343
+ }
2344
+ const root = new URL(base.endsWith("/") ? base : `${base}/`, page);
2345
+ const resolved = new URL(url, root);
2346
+ return resolved.origin === root.origin && resolved.href.startsWith(root.href) ? resolved.href : "";
2347
+ } catch {
2348
+ return "";
2349
+ }
2350
+ }
2351
+ /** An external page for a `url` item: http or https only. */
2352
+ function externalUrl(url) {
2353
+ try {
2354
+ const parsed = new URL(url);
2355
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : "";
2338
2356
  } catch {
2339
- return url;
2357
+ return "";
2340
2358
  }
2341
2359
  }
2342
- /** A canvas with its frame, snapshot and image URLs resolved against its unit's base. */
2360
+ /** A canvas with its frame, snapshot and image URLs resolved against its unit's base, unsafe ones dropped. */
2343
2361
  function resolveCanvas(doc, base) {
2344
- if (!base) return doc;
2345
2362
  const at = (url) => resolveUrl(url, base);
2346
2363
  return {
2347
2364
  ...doc,
@@ -2351,10 +2368,12 @@ function resolveCanvas(doc, base) {
2351
2368
  ...section,
2352
2369
  items: section.items.map((item) => {
2353
2370
  if (item.kind === "screen") {
2354
- const each = (set) => set ? Object.fromEntries(Object.entries(set).map(([theme, url]) => [theme, url && at(url)])) : void 0;
2371
+ const each = (set) => set ? Object.fromEntries(Object.entries(set).map(([theme, url]) => [theme, url && at(url)]).filter(([, url]) => url)) : void 0;
2372
+ const url = at(item.url);
2355
2373
  return {
2356
2374
  ...item,
2357
- url: at(item.url),
2375
+ url,
2376
+ missing: item.missing || !url,
2358
2377
  snapshots: each(item.snapshots),
2359
2378
  thumbs: each(item.thumbs)
2360
2379
  };
@@ -2363,6 +2382,10 @@ function resolveCanvas(doc, base) {
2363
2382
  ...item,
2364
2383
  url: at(item.url)
2365
2384
  };
2385
+ if (item.kind === "url") return {
2386
+ ...item,
2387
+ url: externalUrl(item.url)
2388
+ };
2366
2389
  return item;
2367
2390
  })
2368
2391
  }))
@@ -2371,7 +2394,6 @@ function resolveCanvas(doc, base) {
2371
2394
  }
2372
2395
  /** A design system with its specimen and asset URLs resolved against the system unit's base. */
2373
2396
  function resolveSystem(doc, base) {
2374
- if (!base) return doc;
2375
2397
  const at = (url) => resolveUrl(url, base);
2376
2398
  return {
2377
2399
  ...doc,
@@ -7794,1926 +7816,52 @@ function linkPath(href, base = "/") {
7794
7816
  return url.pathname + url.search + url.hash;
7795
7817
  }
7796
7818
  //#endregion
7797
- //#region viewer/src/lib/frames.ts
7798
- /** Routes runtime messages to whoever listens to the frame that sent them. */
7799
- var handlers = /* @__PURE__ */ new Map();
7800
- window.addEventListener("message", (event) => {
7801
- const data = event.data;
7802
- if (data?.source !== "design-runtime" || !event.source) return;
7803
- for (const handler of handlers.get(event.source) ?? []) handler(data);
7804
- });
7805
- function listenToFrame(frame, handler) {
7806
- let win = null;
7807
- const detach = () => {
7808
- if (!win) return;
7809
- const set = handlers.get(win);
7810
- set?.delete(handler);
7811
- if (!set?.size) handlers.delete(win);
7812
- win = null;
7813
- };
7814
- const attach = () => {
7815
- detach();
7816
- win = frame.contentWindow;
7817
- if (!win) return;
7818
- const set = handlers.get(win) ?? /* @__PURE__ */ new Set();
7819
- set.add(handler);
7820
- handlers.set(win, set);
7821
- };
7822
- attach();
7823
- frame.addEventListener("load", attach);
7824
- return () => {
7825
- frame.removeEventListener("load", attach);
7826
- detach();
7827
- };
7828
- }
7829
- function sendToFrame(frame, message) {
7830
- frame?.contentWindow?.postMessage({
7831
- source: "design-viewer",
7832
- ...message
7833
- }, "*");
7834
- }
7835
- /** Tell a frame it sits on the canvas, so it hands the wheel to the viewer. */
7836
- function claimWheel(frame) {
7837
- sendToFrame(frame, { type: "canvas" });
7838
- }
7819
+ //#region ../../node_modules/.pnpm/marked@18.0.14/node_modules/marked/lib/marked.esm.js
7839
7820
  /**
7840
- * A wheel the frame had no use for (it reached its edge, or holds nothing that scrolls): scroll
7841
- * whatever scrolls around the frame, as the browser would if the frame's edges passed it on.
7821
+ * marked v18.0.14 - a markdown parser
7822
+ * Copyright (c) 2018-2026, MarkedJS. (MIT License)
7823
+ * Copyright (c) 2011-2018, Christopher Jeffrey. (MIT License)
7824
+ * https://github.com/markedjs/marked
7842
7825
  */
7843
- function scrollAround(frame, delta) {
7844
- for (let node = frame.parentElement; node; node = node.parentElement) {
7845
- const style = getComputedStyle(node);
7846
- const scrolls = (overflow) => /(auto|scroll|overlay)/.test(overflow);
7847
- const y = scrolls(style.overflowY) && node.scrollHeight > node.clientHeight;
7848
- const x = scrolls(style.overflowX) && node.scrollWidth > node.clientWidth;
7849
- if (y || x) {
7850
- node.scrollBy({
7851
- left: x ? delta.deltaX : 0,
7852
- top: y ? delta.deltaY : 0,
7853
- behavior: "instant"
7854
- });
7855
- return;
7856
- }
7857
- }
7858
- window.scrollBy({
7859
- left: delta.deltaX,
7860
- top: delta.deltaY,
7861
- behavior: "instant"
7862
- });
7863
- }
7864
- function sendTheme(frame, theme) {
7865
- sendToFrame(frame, {
7866
- type: "theme",
7867
- theme
7868
- });
7869
- }
7870
- /** Load the frame again; works whatever origin it is on. */
7871
- function reloadFrame(frame) {
7872
- if (!frame) return;
7873
- frame.src = frame.src;
7874
- }
7875
7826
  /**
7876
- * Where the dev server's frames load from. Browsers give each site its own
7877
- * process, so frames served from the viewer's twin host (localhost ↔ 127.0.0.1,
7878
- * both reach the same server) run their scripts and animations off the viewer's
7879
- * thread: a canvas full of live screens still pans smoothly. Any other host,
7880
- * and static builds, keep frames on the viewer's origin.
7827
+ * DO NOT EDIT THIS FILE
7828
+ * The code in this file is generated from files in ./src/
7881
7829
  */
7882
- var FRAME_ORIGIN = (() => {
7883
- if (STATIC_SITE) return "";
7884
- const { protocol, hostname, port } = window.location;
7885
- const twin = hostname === "localhost" ? "127.0.0.1" : hostname === "127.0.0.1" ? "localhost" : null;
7886
- return twin ? `${protocol}//${twin}${port ? `:${port}` : ""}` : "";
7887
- })();
7888
- /** A frame URL with the theme it should open in. */
7889
- function frameSrc(url, theme, extra) {
7890
- const [path = "", query = ""] = url.split("?");
7891
- const params = new URLSearchParams(query);
7892
- params.set("theme", theme);
7893
- for (const [key, value] of Object.entries(extra ?? {})) params.set(key, value);
7894
- return `${path.startsWith("/") ? FRAME_ORIGIN : ""}${path}?${params.toString()}`;
7895
- }
7896
- /** Absolute URL for opening a frame in a new tab. */
7897
- function absoluteUrl(url) {
7898
- return new URL(url, document.baseURI).toString();
7830
+ function I() {
7831
+ return {
7832
+ async: !1,
7833
+ breaks: !1,
7834
+ extensions: null,
7835
+ gfm: !0,
7836
+ hooks: null,
7837
+ pedantic: !1,
7838
+ renderer: null,
7839
+ silent: !1,
7840
+ tokenizer: null,
7841
+ walkTokens: null
7842
+ };
7899
7843
  }
7900
- /**
7901
- * A link, form or history push the runtime kept from leaving its frame. The path goes to the
7902
- * screen whose `route` matches it best (a relative `href` resolves against the route of the
7903
- * screen it is in); without routes, a path whose last part names a screen still opens it. A
7904
- * link to another site opens in a new tab. A push nobody's route matches belongs to the
7905
- * screen's own router; a link or form that leads nowhere stays put with a note.
7906
- */
7907
- function followLink(message, canvas, go, from) {
7908
- if (message.form) {
7909
- toast("Forms are not sent from a screen", void 0, "info");
7910
- return;
7911
- }
7912
- if (message.path === null) {
7913
- const tab = window.open(message.href, "_blank");
7914
- if (tab) tab.opener = null;
7915
- else toast("Link to another site", message.href, "info");
7916
- return;
7917
- }
7918
- const frames = canvas.pages.flatMap((page) => page.sections.flatMap((section) => section.items.filter((item) => item.kind === "screen" || item.kind === "url")));
7919
- const here = frames.find((item) => item.id === from);
7920
- const path = message.raw === void 0 ? message.path : linkPath(message.raw, here?.routes?.[0]);
7921
- if (path === null) return;
7922
- const routed = matchRoute(frames.map((item) => ({
7923
- routes: (item.routes ?? []).map(compileRoute),
7924
- value: item.id
7925
- })), path);
7926
- if (routed) {
7927
- if (routed !== from) go(routed);
7928
- return;
7929
- }
7930
- const bare = path.split(/[?#]/)[0].replace(/\/+$/, "");
7931
- let name = bare.slice(bare.lastIndexOf("/") + 1).replace(/\.html?$/, "");
7932
- try {
7933
- name = decodeURIComponent(name);
7934
- } catch {}
7935
- if (name && name !== from && frames.some((item) => item.id === name)) go(name);
7936
- else if (!message.pushed) toast("No screen for this link", path, "info");
7844
+ var y = I();
7845
+ function W(l) {
7846
+ y = l;
7937
7847
  }
7938
- //#endregion
7939
- //#region viewer/src/components/inspector.tsx
7940
- /**
7941
- * Inspect frames: while `enabled`, every frame in `frames` highlights on hover
7942
- * and picks on click. The inspecting runs inside each frame (see the runtime's
7943
- * inspect.ts); this side only switches it and shows what the frames report.
7944
- * The pick outlives `enabled` (so a quick ⌘-hold leaves the panel open) until
7945
- * it is cleared.
7946
- */
7947
- function useInspection(frames, enabled, options) {
7948
- const project = useProject().data;
7949
- const tokens = useSystem(!!project?.system).data?.tokens;
7950
- const [selected, setSelected] = useState(null);
7951
- const state = useRef({
7952
- enabled,
7953
- tokens
7954
- });
7955
- state.current = {
7956
- enabled,
7957
- tokens
7848
+ var A = { exec: () => null };
7849
+ function C(l) {
7850
+ let e = [];
7851
+ return (t) => {
7852
+ let n = Math.max(0, Math.min(3, t - 1)), s = e[n];
7853
+ return s || (s = l(n), e[n] = s), s;
7958
7854
  };
7959
- const onEscape = useRef(options.onEscape);
7960
- onEscape.current = options.onEscape;
7961
- const framesRef = useRef(frames);
7962
- framesRef.current = frames;
7963
- const told = useRef(/* @__PURE__ */ new Map());
7964
- const tell = (frame, force = false) => {
7965
- const { enabled: on, tokens: list } = state.current;
7966
- const last = told.current.get(frame);
7967
- if (!force && last?.on === on && last.tokens === list) return;
7968
- told.current.set(frame, {
7969
- on,
7970
- tokens: list
7971
- });
7972
- sendToFrame(frame, {
7973
- type: "inspect",
7974
- on,
7975
- tokens: on ? list ?? [] : void 0
7976
- });
7855
+ }
7856
+ function h(l, e = "") {
7857
+ let t = typeof l == "string" ? l : l.source, n = {
7858
+ replace: (s, r) => {
7859
+ let o = typeof r == "string" ? r : r.source;
7860
+ return o = o.replace(x.caret, "$1"), t = t.replace(s, o), n;
7861
+ },
7862
+ getRegex: () => new RegExp(t, e)
7977
7863
  };
7978
- useEffect(() => {
7979
- for (const frame of frames) tell(frame);
7980
- for (const frame of told.current.keys()) if (!frames.includes(frame)) told.current.delete(frame);
7981
- }, [
7982
- frames,
7983
- enabled,
7984
- tokens
7985
- ]);
7986
- useEffect(() => {
7987
- const offs = frames.map((frame) => listenToFrame(frame, (message) => {
7988
- if (message.type === "ready") {
7989
- if (state.current.enabled) tell(frame, true);
7990
- else told.current.set(frame, { on: false });
7991
- } else if (message.type === "inspect") {
7992
- if (message.info) {
7993
- for (const other of framesRef.current) if (other !== frame) sendToFrame(other, {
7994
- type: "inspect-select",
7995
- ref: null
7996
- });
7997
- setSelected({
7998
- info: message.info,
7999
- frame
8000
- });
8001
- } else setSelected((current) => current?.frame === frame ? null : current);
8002
- } else if (message.type === "inspect-escape") onEscape.current();
8003
- }));
8004
- return () => {
8005
- for (const off of offs) off();
8006
- };
8007
- }, [frames]);
8008
- const frame = selected && frames.includes(selected.frame) ? selected.frame : null;
8009
- return {
8010
- info: frame ? selected.info : null,
8011
- frame,
8012
- /** Pick another element in the same frame by its ref, or clear the pick. */
8013
- select(ref) {
8014
- if (ref === null) {
8015
- for (const each of framesRef.current) sendToFrame(each, {
8016
- type: "inspect-select",
8017
- ref: null
8018
- });
8019
- setSelected(null);
8020
- } else if (selected) sendToFrame(selected.frame, {
8021
- type: "inspect-select",
8022
- ref
8023
- });
8024
- }
8025
- };
8026
- }
8027
- /**
8028
- * True while ⌘ or Ctrl is held on its own, in the viewer or inside any of
8029
- * `frames`. A modifier used for a shortcut (⌘K, ⌘C) does not count.
8030
- */
8031
- function useModifierHold(frames) {
8032
- const [held, setHeld] = useState(false);
8033
- useEffect(() => {
8034
- const isModifier = (key) => key === "Meta" || key === "Control";
8035
- let down = false;
8036
- let combo = false;
8037
- let timer;
8038
- const release = () => {
8039
- down = false;
8040
- clearTimeout(timer);
8041
- setHeld(false);
8042
- };
8043
- const press = (key) => {
8044
- if (isModifier(key)) {
8045
- if (down) return;
8046
- down = true;
8047
- combo = false;
8048
- timer = setTimeout(() => {
8049
- if (down && !combo) setHeld(true);
8050
- }, 150);
8051
- } else if (down) {
8052
- combo = true;
8053
- clearTimeout(timer);
8054
- setHeld(false);
8055
- }
8056
- };
8057
- const onDown = (event) => press(event.key);
8058
- const onUp = (event) => {
8059
- if (isModifier(event.key)) release();
8060
- };
8061
- window.addEventListener("keydown", onDown, true);
8062
- window.addEventListener("keyup", onUp, true);
8063
- window.addEventListener("blur", release);
8064
- const offs = frames.map((frame) => listenToFrame(frame, (message) => {
8065
- if (message.type === "key") {
8066
- if (message.down) press(message.name);
8067
- else if (isModifier(message.name)) release();
8068
- } else if (message.type === "blur") release();
8069
- }));
8070
- return () => {
8071
- window.removeEventListener("keydown", onDown, true);
8072
- window.removeEventListener("keyup", onUp, true);
8073
- window.removeEventListener("blur", release);
8074
- for (const off of offs) off();
8075
- clearTimeout(timer);
8076
- };
8077
- }, [frames]);
8078
- return held;
8079
- }
8080
- function Section({ title, children, right }) {
8081
- return /* @__PURE__ */ jsxs("section", {
8082
- className: "border-t border-rule pb-2 first:border-t-0",
8083
- children: [/* @__PURE__ */ jsxs("div", {
8084
- className: "flex h-9 items-center justify-between px-4 text-[12px] font-medium text-muted",
8085
- children: [title, right]
8086
- }), children]
8087
- });
8088
- }
8089
- function Line({ label, children }) {
8090
- return /* @__PURE__ */ jsxs("div", {
8091
- className: "grid min-h-7 grid-cols-[92px_minmax(0,1fr)] items-center gap-x-3 px-4 py-0.5 text-[12.5px]",
8092
- children: [/* @__PURE__ */ jsx("span", {
8093
- className: "text-muted",
8094
- children: label
8095
- }), /* @__PURE__ */ jsx("div", {
8096
- className: "flex min-w-0 items-center gap-1.5 text-ink",
8097
- children
8098
- })]
8099
- });
8100
- }
8101
- function TokenChip({ name }) {
8102
- if (!name) return null;
8103
- return /* @__PURE__ */ jsx("button", {
8104
- type: "button",
8105
- onClick: () => void copyText(`var(${name})`, "Token copied"),
8106
- title: "Copy var()",
8107
- className: "shrink-0 cursor-pointer truncate rounded-[4px] border-0 bg-action-soft px-1.5 py-px font-mono text-[11px] text-action",
8108
- children: name
8109
- });
8110
- }
8111
- function Value$1({ children, title }) {
8112
- return /* @__PURE__ */ jsx("span", {
8113
- className: "min-w-0 truncate font-mono text-[12px]",
8114
- title,
8115
- children
8116
- });
8117
- }
8118
- function ColorLine({ label, color }) {
8119
- return /* @__PURE__ */ jsxs(Line, {
8120
- label,
8121
- children: [
8122
- /* @__PURE__ */ jsx("span", {
8123
- className: "size-3.5 shrink-0 rounded-[3px] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.12)]",
8124
- style: { background: color.css }
8125
- }),
8126
- /* @__PURE__ */ jsx(Value$1, {
8127
- title: color.css,
8128
- children: color.value
8129
- }),
8130
- /* @__PURE__ */ jsx(TokenChip, { name: color.token })
8131
- ]
8132
- });
8133
- }
8134
- var dash = (value) => value ? String(value) : "–";
8135
- function Layer({ label, values, className, children }) {
8136
- const [top, right, bottom, left] = values;
8137
- return /* @__PURE__ */ jsxs("div", {
8138
- className: cn("relative grid grid-cols-[28px_minmax(0,1fr)_28px] grid-rows-[20px_auto_20px] rounded-[4px]", className),
8139
- children: [
8140
- /* @__PURE__ */ jsx("span", {
8141
- className: "absolute top-1 left-1.5 text-[9.5px] tracking-wide text-muted uppercase",
8142
- children: label
8143
- }),
8144
- /* @__PURE__ */ jsx("span", {
8145
- className: "col-start-2 self-center text-center",
8146
- children: dash(top)
8147
- }),
8148
- /* @__PURE__ */ jsx("span", {
8149
- className: "row-start-2 self-center text-center",
8150
- children: dash(left)
8151
- }),
8152
- /* @__PURE__ */ jsx("div", {
8153
- className: "row-start-2 col-start-2",
8154
- children
8155
- }),
8156
- /* @__PURE__ */ jsx("span", {
8157
- className: "row-start-2 col-start-3 self-center text-center",
8158
- children: dash(right)
8159
- }),
8160
- /* @__PURE__ */ jsx("span", {
8161
- className: "row-start-3 col-start-2 self-center text-center",
8162
- children: dash(bottom)
8163
- })
8164
- ]
8165
- });
8166
- }
8167
- function BoxModel({ info }) {
8168
- const content = {
8169
- w: Math.max(0, info.rect.w - info.border[1] - info.border[3] - info.padding[1] - info.padding[3]),
8170
- h: Math.max(0, info.rect.h - info.border[0] - info.border[2] - info.padding[0] - info.padding[2])
8171
- };
8172
- return /* @__PURE__ */ jsx("div", {
8173
- className: "px-4 pt-1 pb-2 font-mono text-[11px] text-ink2",
8174
- children: /* @__PURE__ */ jsx(Layer, {
8175
- label: "margin",
8176
- values: info.margin,
8177
- className: "border border-dashed border-rule-strong bg-[#f6b26b]/15",
8178
- children: /* @__PURE__ */ jsx(Layer, {
8179
- label: "border",
8180
- values: info.border,
8181
- className: "border border-rule-strong bg-[#f9e27d]/15",
8182
- children: /* @__PURE__ */ jsx(Layer, {
8183
- label: "padding",
8184
- values: info.padding,
8185
- className: "border border-rule-strong bg-[#93c47d]/20",
8186
- children: /* @__PURE__ */ jsxs("div", {
8187
- className: "flex h-8 items-center justify-center rounded-[3px] border border-rule-strong bg-[#6fa8dc]/25 text-ink",
8188
- children: [
8189
- Math.round(content.w * 100) / 100,
8190
- " × ",
8191
- Math.round(content.h * 100) / 100
8192
- ]
8193
- })
8194
- })
8195
- })
8196
- })
8197
- });
8198
- }
8199
- function Crumbs({ info, onSelect }) {
8200
- return /* @__PURE__ */ jsxs("div", {
8201
- className: "flex flex-wrap items-center gap-1 px-4 pb-2",
8202
- children: [info.path.slice(-4).map((crumb, index) => /* @__PURE__ */ jsxs("span", {
8203
- className: "flex items-center gap-1",
8204
- children: [/* @__PURE__ */ jsx("button", {
8205
- type: "button",
8206
- onClick: () => onSelect(crumb.ref),
8207
- className: "cursor-pointer rounded-[4px] border-0 bg-soft2 px-1.5 py-0.5 font-mono text-[11px] text-ink2 hover:text-ink",
8208
- children: crumb.label
8209
- }), /* @__PURE__ */ jsx(Icon, {
8210
- name: "chevron-right",
8211
- size: 11,
8212
- className: "text-muted"
8213
- })]
8214
- }, index)), /* @__PURE__ */ jsx("span", {
8215
- className: "rounded-[4px] bg-action-soft px-1.5 py-0.5 font-mono text-[11px] text-action",
8216
- children: info.label
8217
- })]
8218
- });
8219
- }
8220
- /** The right sidebar: what the selected element is and how it is styled. */
8221
- function InspectorPanel({ info, onSelect, onClose, active }) {
8222
- return /* @__PURE__ */ jsxs("aside", {
8223
- "data-ui": true,
8224
- className: "flex w-[320px] shrink-0 flex-col overflow-hidden border-l border-rule bg-surface",
8225
- children: [
8226
- /* @__PURE__ */ jsxs("div", {
8227
- className: "flex h-[46px] shrink-0 items-center justify-between border-b border-rule pr-2 pl-4",
8228
- children: [/* @__PURE__ */ jsxs("span", {
8229
- className: "flex items-center gap-2 text-[14px] font-semibold",
8230
- children: [/* @__PURE__ */ jsx(Icon, {
8231
- name: "pointer",
8232
- size: 15,
8233
- className: "text-action"
8234
- }), " Inspect"]
8235
- }), /* @__PURE__ */ jsx(IconButton, {
8236
- icon: "x",
8237
- label: "Close inspector (I)",
8238
- onClick: onClose
8239
- })]
8240
- }),
8241
- !info ? /* @__PURE__ */ jsxs("div", {
8242
- className: "flex flex-col items-center gap-2 px-6 py-10 text-center",
8243
- children: [
8244
- /* @__PURE__ */ jsx(Icon, {
8245
- name: "pointer",
8246
- size: 22,
8247
- className: "text-muted"
8248
- }),
8249
- /* @__PURE__ */ jsx("span", {
8250
- className: "text-[13.5px] text-ink2",
8251
- children: active ? "Click an element in any screen." : `Hold ${MOD_KEY} to highlight, click to inspect.`
8252
- }),
8253
- /* @__PURE__ */ jsx("span", {
8254
- className: "text-[12.5px] leading-[1.5] text-muted",
8255
- children: "Hover shows margin, padding and content. Clicks go to the inspector, not the screen."
8256
- }),
8257
- /* @__PURE__ */ jsxs("span", {
8258
- className: "mt-2 flex items-center gap-1.5 text-[12px] text-muted",
8259
- children: [
8260
- /* @__PURE__ */ jsx(Kbd, { children: "I" }),
8261
- " toggle · hold ",
8262
- /* @__PURE__ */ jsx(Kbd, { children: MOD_KEY }),
8263
- " · ",
8264
- /* @__PURE__ */ jsx(Kbd, { children: "esc" }),
8265
- " clear"
8266
- ]
8267
- })
8268
- ]
8269
- }) : /* @__PURE__ */ jsxs("div", {
8270
- className: "min-h-0 grow overflow-y-auto",
8271
- children: [
8272
- /* @__PURE__ */ jsxs(Section, {
8273
- title: "Element",
8274
- children: [/* @__PURE__ */ jsxs("div", {
8275
- className: "flex flex-col gap-1 px-4 pb-2",
8276
- children: [
8277
- /* @__PURE__ */ jsx("div", {
8278
- className: "flex min-w-0 items-baseline gap-1.5",
8279
- children: info.component?.owners[0] ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
8280
- className: "truncate font-mono text-[14px] font-medium text-ink",
8281
- children: [
8282
- "<",
8283
- info.component.owners[0],
8284
- ">"
8285
- ]
8286
- }), /* @__PURE__ */ jsx("span", {
8287
- className: "truncate font-mono text-[12px] text-muted",
8288
- children: info.tag
8289
- })] }) : /* @__PURE__ */ jsx("span", {
8290
- className: "truncate font-mono text-[14px] font-medium text-ink",
8291
- children: info.label
8292
- })
8293
- }),
8294
- info.component && info.component.owners.length > 1 ? /* @__PURE__ */ jsxs("span", {
8295
- className: "truncate text-[12px] text-muted",
8296
- children: ["in ", info.component.owners.slice(1).join(" ← ")]
8297
- }) : null,
8298
- info.component?.file ? /* @__PURE__ */ jsx("button", {
8299
- type: "button",
8300
- onClick: () => void copyText(info.component.file, "Path copied"),
8301
- className: "w-fit max-w-full cursor-pointer truncate border-0 bg-transparent p-0 text-left font-mono text-[11.5px] text-muted hover:text-ink2",
8302
- title: "Copy path",
8303
- children: info.component.file
8304
- }) : null,
8305
- info.text ? /* @__PURE__ */ jsxs("span", {
8306
- className: "line-clamp-2 text-[12.5px] text-ink2",
8307
- children: [
8308
- "“",
8309
- info.text,
8310
- "”"
8311
- ]
8312
- }) : null
8313
- ]
8314
- }), /* @__PURE__ */ jsx(Crumbs, {
8315
- info,
8316
- onSelect
8317
- })]
8318
- }),
8319
- /* @__PURE__ */ jsxs(Section, {
8320
- title: "Box",
8321
- children: [
8322
- /* @__PURE__ */ jsx(BoxModel, { info }),
8323
- /* @__PURE__ */ jsx(Line, {
8324
- label: "Size",
8325
- children: /* @__PURE__ */ jsxs(Value$1, { children: [
8326
- info.rect.w,
8327
- " × ",
8328
- info.rect.h
8329
- ] })
8330
- }),
8331
- /* @__PURE__ */ jsx(Line, {
8332
- label: "Position",
8333
- children: /* @__PURE__ */ jsxs(Value$1, { children: [
8334
- "x ",
8335
- info.rect.x,
8336
- " · y ",
8337
- info.rect.y
8338
- ] })
8339
- })
8340
- ]
8341
- }),
8342
- info.layout.length ? /* @__PURE__ */ jsx(Section, {
8343
- title: "Layout",
8344
- children: info.layout.map(([label, value]) => /* @__PURE__ */ jsx(Line, {
8345
- label,
8346
- children: /* @__PURE__ */ jsx(Value$1, {
8347
- title: value,
8348
- children: value
8349
- })
8350
- }, label))
8351
- }) : null,
8352
- info.typography ? /* @__PURE__ */ jsxs(Section, {
8353
- title: "Typography",
8354
- children: [
8355
- /* @__PURE__ */ jsxs(Line, {
8356
- label: "Font",
8357
- children: [/* @__PURE__ */ jsx(Value$1, {
8358
- title: info.typography.family,
8359
- children: info.typography.family.split(",")[0]?.replace(/["']/g, "")
8360
- }), /* @__PURE__ */ jsx(TokenChip, { name: info.typography.familyToken })]
8361
- }),
8362
- /* @__PURE__ */ jsxs(Line, {
8363
- label: "Size",
8364
- children: [/* @__PURE__ */ jsxs(Value$1, { children: [
8365
- info.typography.size,
8366
- " / ",
8367
- info.typography.lineHeight
8368
- ] }), /* @__PURE__ */ jsx(TokenChip, { name: info.typography.sizeToken })]
8369
- }),
8370
- /* @__PURE__ */ jsx(Line, {
8371
- label: "Weight",
8372
- children: /* @__PURE__ */ jsx(Value$1, { children: info.typography.weight })
8373
- }),
8374
- info.typography.letterSpacing !== "normal" ? /* @__PURE__ */ jsx(Line, {
8375
- label: "Tracking",
8376
- children: /* @__PURE__ */ jsx(Value$1, { children: info.typography.letterSpacing })
8377
- }) : null,
8378
- /* @__PURE__ */ jsx(ColorLine, {
8379
- label: "Color",
8380
- color: info.typography.color
8381
- }),
8382
- info.typography.align !== "start" ? /* @__PURE__ */ jsx(Line, {
8383
- label: "Align",
8384
- children: /* @__PURE__ */ jsx(Value$1, { children: info.typography.align })
8385
- }) : null,
8386
- info.typography.transform ? /* @__PURE__ */ jsx(Line, {
8387
- label: "Transform",
8388
- children: /* @__PURE__ */ jsx(Value$1, { children: info.typography.transform })
8389
- }) : null
8390
- ]
8391
- }) : null,
8392
- Object.values(info.appearance).some(Boolean) ? /* @__PURE__ */ jsxs(Section, {
8393
- title: "Appearance",
8394
- children: [
8395
- info.appearance.background ? /* @__PURE__ */ jsx(ColorLine, {
8396
- label: "Background",
8397
- color: info.appearance.background
8398
- }) : null,
8399
- info.appearance.backgroundImage ? /* @__PURE__ */ jsx(Line, {
8400
- label: "Image",
8401
- children: /* @__PURE__ */ jsx(Value$1, {
8402
- title: info.appearance.backgroundImage,
8403
- children: info.appearance.backgroundImage
8404
- })
8405
- }) : null,
8406
- info.appearance.border ? /* @__PURE__ */ jsxs(Line, {
8407
- label: "Border",
8408
- children: [
8409
- /* @__PURE__ */ jsx("span", {
8410
- className: "size-3.5 shrink-0 rounded-[3px] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.12)]",
8411
- style: { background: info.appearance.border.color.css }
8412
- }),
8413
- /* @__PURE__ */ jsxs(Value$1, { children: [
8414
- info.appearance.border.width,
8415
- " ",
8416
- info.appearance.border.style,
8417
- " ",
8418
- info.appearance.border.color.value
8419
- ] }),
8420
- /* @__PURE__ */ jsx(TokenChip, { name: info.appearance.border.color.token })
8421
- ]
8422
- }) : null,
8423
- info.appearance.radius ? /* @__PURE__ */ jsxs(Line, {
8424
- label: "Radius",
8425
- children: [/* @__PURE__ */ jsx(Value$1, { children: info.appearance.radius.value }), /* @__PURE__ */ jsx(TokenChip, { name: info.appearance.radius.token })]
8426
- }) : null,
8427
- info.appearance.shadow ? /* @__PURE__ */ jsxs(Line, {
8428
- label: "Shadow",
8429
- children: [/* @__PURE__ */ jsx(Value$1, {
8430
- title: info.appearance.shadow.value,
8431
- children: info.appearance.shadow.value
8432
- }), /* @__PURE__ */ jsx(TokenChip, { name: info.appearance.shadow.token })]
8433
- }) : null,
8434
- info.appearance.opacity ? /* @__PURE__ */ jsx(Line, {
8435
- label: "Opacity",
8436
- children: /* @__PURE__ */ jsx(Value$1, { children: info.appearance.opacity })
8437
- }) : null
8438
- ]
8439
- }) : null,
8440
- info.classes.length ? /* @__PURE__ */ jsx(Section, {
8441
- title: "Classes",
8442
- right: /* @__PURE__ */ jsx("button", {
8443
- type: "button",
8444
- onClick: () => void copyText(info.classes.join(" "), "Classes copied"),
8445
- className: "cursor-pointer border-0 bg-transparent text-[12px] text-muted hover:text-ink2",
8446
- children: "Copy"
8447
- }),
8448
- children: /* @__PURE__ */ jsx("div", {
8449
- className: "flex flex-wrap gap-1 px-4 pb-1",
8450
- children: info.classes.map((name) => /* @__PURE__ */ jsx("span", {
8451
- className: "rounded-[4px] bg-soft2 px-1.5 py-0.5 font-mono text-[11px] text-ink2",
8452
- children: name
8453
- }, name))
8454
- })
8455
- }) : null,
8456
- info.attributes.length ? /* @__PURE__ */ jsx(Section, {
8457
- title: "Attributes",
8458
- children: info.attributes.map(([name, value]) => /* @__PURE__ */ jsx(Line, {
8459
- label: name,
8460
- children: /* @__PURE__ */ jsx(Value$1, {
8461
- title: value,
8462
- children: value
8463
- })
8464
- }, name))
8465
- }) : null,
8466
- info.children.length ? /* @__PURE__ */ jsx(Section, {
8467
- title: `Children · ${info.children.length}`,
8468
- children: /* @__PURE__ */ jsx("div", {
8469
- className: "flex flex-col px-2 pb-1",
8470
- children: info.children.map((child, index) => /* @__PURE__ */ jsxs("button", {
8471
- type: "button",
8472
- onClick: () => onSelect(child.ref),
8473
- className: "flex h-7 cursor-pointer items-center gap-2 rounded-[6px] border-0 bg-transparent px-2 text-left font-mono text-[12px] text-ink2 hover:bg-soft2",
8474
- children: [/* @__PURE__ */ jsx(Icon, {
8475
- name: "chevron-right",
8476
- size: 12,
8477
- className: "text-muted"
8478
- }), /* @__PURE__ */ jsx("span", {
8479
- className: "truncate",
8480
- children: child.label
8481
- })]
8482
- }, index))
8483
- })
8484
- }) : null
8485
- ]
8486
- }),
8487
- info ? /* @__PURE__ */ jsx("div", {
8488
- className: "flex shrink-0 items-center gap-2 border-t border-rule bg-soft px-4 py-2.5",
8489
- children: /* @__PURE__ */ jsx(Button, {
8490
- icon: "copy",
8491
- onClick: () => void copyText(info.css, "CSS copied"),
8492
- className: "grow",
8493
- children: "Copy CSS"
8494
- })
8495
- }) : null
8496
- ]
8497
- });
8498
- }
8499
- //#endregion
8500
- //#region viewer/src/ui/badge.tsx
8501
- var BADGE = {
8502
- neutral: "bg-soft2 text-ink2",
8503
- action: "bg-action-soft text-action",
8504
- ok: "bg-ok-soft text-ok-text",
8505
- danger: "bg-danger-soft text-danger-text"
8506
- };
8507
- function Badge({ tone = "neutral", children, className }) {
8508
- return /* @__PURE__ */ jsx("span", {
8509
- className: cn("inline-flex h-[22px] shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] px-2 text-[12px] font-medium", BADGE[tone], className),
8510
- children
8511
- });
8512
- }
8513
- function Count({ value, tone = "neutral" }) {
8514
- return /* @__PURE__ */ jsx("span", {
8515
- className: cn("inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-[4px] px-[5px] text-[11.5px] font-semibold tabular", tone === "action" ? "bg-action-soft text-action" : tone === "danger" ? "bg-danger-soft text-danger-text" : "bg-soft2 text-ink2"),
8516
- children: value
8517
- });
8518
- }
8519
- //#endregion
8520
- //#region viewer/src/ui/panel.tsx
8521
- function Panel({ children, className, style }) {
8522
- return /* @__PURE__ */ jsx("section", {
8523
- className: cn("min-w-0 shrink-0 overflow-hidden rounded-[10px] border border-rule bg-surface", className),
8524
- style,
8525
- children
8526
- });
8527
- }
8528
- function PanelHead({ title, count, sub, right, as: Heading = "h2" }) {
8529
- return /* @__PURE__ */ jsxs("div", {
8530
- className: "flex min-h-[46px] items-center justify-between gap-3 px-4",
8531
- children: [/* @__PURE__ */ jsxs(Heading, {
8532
- className: "m-0 flex min-w-0 items-center gap-2 whitespace-nowrap text-[14px] font-semibold",
8533
- children: [
8534
- title,
8535
- count !== void 0 ? /* @__PURE__ */ jsx(Count, { value: count }) : null,
8536
- sub ? /* @__PURE__ */ jsx("span", {
8537
- className: "truncate text-[12.5px] font-normal text-muted",
8538
- children: sub
8539
- }) : null
8540
- ]
8541
- }), right ? /* @__PURE__ */ jsx("div", {
8542
- className: "flex items-center gap-2",
8543
- children: right
8544
- }) : null]
8545
- });
8546
- }
8547
- function PanelTabs({ items, value, onChange, right }) {
8548
- const indicator = useId();
8549
- return /* @__PURE__ */ jsxs("div", {
8550
- className: "flex h-[46px] items-center justify-between gap-3 px-4",
8551
- children: [/* @__PURE__ */ jsx("div", {
8552
- role: "tablist",
8553
- className: "flex gap-5",
8554
- children: items.map((item) => {
8555
- const on = item.value === value;
8556
- return /* @__PURE__ */ jsxs("button", {
8557
- type: "button",
8558
- role: "tab",
8559
- "aria-selected": on,
8560
- onClick: () => onChange(item.value),
8561
- className: cn("relative flex h-[46px] cursor-pointer items-center gap-1.5 border-0 bg-transparent px-0.5 text-[14px] transition-colors", on ? "font-semibold text-ink" : "font-medium text-ink2 hover:text-ink"),
8562
- children: [
8563
- item.label,
8564
- item.count !== void 0 ? /* @__PURE__ */ jsx(Count, { value: item.count }) : null,
8565
- on ? /* @__PURE__ */ jsx(motion.span, {
8566
- layoutId: indicator,
8567
- className: "absolute inset-x-0 bottom-0 h-[2px] bg-ink",
8568
- transition: {
8569
- type: "spring",
8570
- stiffness: 520,
8571
- damping: 42
8572
- }
8573
- }) : null
8574
- ]
8575
- }, item.value);
8576
- })
8577
- }), right ? /* @__PURE__ */ jsx("div", {
8578
- className: "flex items-center gap-2",
8579
- children: right
8580
- }) : null]
8581
- });
8582
- }
8583
- function Cell({ align, children }) {
8584
- return /* @__PURE__ */ jsx("div", {
8585
- className: cn("flex min-w-0 items-center gap-2", align === "r" && "justify-end text-right", align === "c" && "justify-center"),
8586
- children
8587
- });
8588
- }
8589
- function cells(children, align) {
8590
- return Children.toArray(children).map((child, index) => /* @__PURE__ */ jsx(Cell, {
8591
- align: align?.[index] ?? "l",
8592
- children: child
8593
- }, index));
8594
- }
8595
- function THead({ cols, align, labels }) {
8596
- return /* @__PURE__ */ jsx("div", {
8597
- className: "grid h-8 items-center gap-x-4 border-t border-rule bg-soft px-4 text-[12.5px] text-muted",
8598
- style: { gridTemplateColumns: cols },
8599
- children: labels.map((label, index) => /* @__PURE__ */ jsx(Cell, {
8600
- align: align?.[index] ?? "l",
8601
- children: /* @__PURE__ */ jsx("span", {
8602
- className: "whitespace-nowrap",
8603
- children: label
8604
- })
8605
- }, index))
8606
- });
8607
- }
8608
- var rowClass = (first, interactive, className) => cn("grid items-center gap-x-4 border-rule", !first && "border-t", interactive && "cursor-pointer transition-colors hover:bg-soft", className);
8609
- function Row({ cols, align, minH = 44, pad = "10px 16px", children, first, className, onClick, title }) {
8610
- return /* @__PURE__ */ jsx("div", {
8611
- className: rowClass(first, !!onClick, className),
8612
- style: {
8613
- gridTemplateColumns: cols,
8614
- minHeight: minH,
8615
- padding: pad
8616
- },
8617
- onClick,
8618
- title,
8619
- children: cells(children, align)
8620
- });
8621
- }
8622
- /** A table row that navigates somewhere in the app. */
8623
- function RowLink({ to, cols, align, minH = 44, pad = "10px 16px", first, className, children }) {
8624
- return /* @__PURE__ */ jsx(Link, {
8625
- href: to,
8626
- className: cn(rowClass(first, true, className), "text-inherit no-underline"),
8627
- style: {
8628
- gridTemplateColumns: cols,
8629
- minHeight: minH,
8630
- padding: pad
8631
- },
8632
- children: cells(children, align)
8633
- });
8634
- }
8635
- function KV({ label, children, labelWidth = 180, minH = 40, end = false }) {
8636
- return /* @__PURE__ */ jsxs("div", {
8637
- className: "grid content-center items-baseline gap-x-4 border-t border-rule px-4 py-2",
8638
- style: {
8639
- gridTemplateColumns: `min(${labelWidth}px, 42%) minmax(0, 1fr)`,
8640
- minHeight: minH
8641
- },
8642
- children: [/* @__PURE__ */ jsx("span", {
8643
- className: "text-[13px] text-muted",
8644
- children: label
8645
- }), /* @__PURE__ */ jsx("div", {
8646
- className: cn("flex min-w-0 flex-wrap items-center gap-2", end && "justify-end"),
8647
- children
8648
- })]
8649
- });
8650
- }
8651
- function PanelBody({ children, pad = "16px", gap = 16, top = true, className }) {
8652
- return /* @__PURE__ */ jsx("div", {
8653
- className: cn("flex flex-col", top && "border-t border-rule", className),
8654
- style: {
8655
- padding: pad,
8656
- gap
8657
- },
8658
- children
8659
- });
8660
- }
8661
- function PanelFoot({ left, right }) {
8662
- return /* @__PURE__ */ jsxs("div", {
8663
- className: "flex items-center justify-between gap-3 border-t border-rule bg-soft px-4 py-3",
8664
- children: [/* @__PURE__ */ jsx("div", {
8665
- className: "flex min-w-0 items-center gap-2 text-[12.5px] text-muted",
8666
- children: left
8667
- }), /* @__PURE__ */ jsx("div", {
8668
- className: "flex items-center gap-2",
8669
- children: right
8670
- })]
8671
- });
8672
- }
8673
- /** Bottom summary strip. */
8674
- function Strip({ children, className }) {
8675
- return /* @__PURE__ */ jsx("div", {
8676
- className: cn("flex min-h-[38px] shrink-0 items-center gap-2 border-t border-rule bg-soft px-4 py-2 text-[12.5px] text-muted", className),
8677
- children
8678
- });
8679
- }
8680
- /** Group header row inside tables. */
8681
- function GroupRow({ children, sub }) {
8682
- return /* @__PURE__ */ jsxs("div", {
8683
- className: "flex h-9 items-center gap-2.5 border-t border-rule bg-soft px-4 text-[13px] font-medium",
8684
- children: [children, sub ? /* @__PURE__ */ jsx("span", {
8685
- className: "text-[12.5px] font-normal text-muted",
8686
- children: sub
8687
- }) : null]
8688
- });
8689
- }
8690
- //#endregion
8691
- //#region viewer/src/components/issues.tsx
8692
- function IssueList({ issues, className }) {
8693
- return /* @__PURE__ */ jsx("div", {
8694
- className: cn("flex flex-col", className),
8695
- children: issues.map((issue, index) => /* @__PURE__ */ jsxs("div", {
8696
- className: "flex items-start gap-2.5 border-t border-rule px-4 py-2.5 text-[13px] first:border-t-0",
8697
- children: [/* @__PURE__ */ jsx(Icon, {
8698
- name: issue.severity === "error" ? "alert" : "info",
8699
- size: 15,
8700
- className: cn("mt-0.5", issue.severity === "error" ? "text-danger-text" : "text-action")
8701
- }), /* @__PURE__ */ jsxs("div", {
8702
- className: "flex min-w-0 flex-col gap-0.5",
8703
- children: [/* @__PURE__ */ jsx("span", {
8704
- className: "text-ink",
8705
- children: issue.message
8706
- }), /* @__PURE__ */ jsxs("span", {
8707
- className: "font-mono text-[12px] break-all text-muted",
8708
- children: [issue.file, issue.at ? ` · ${issue.at}` : ""]
8709
- })]
8710
- })]
8711
- }, index))
8712
- });
8713
- }
8714
- /** Problems found in the project's files, as a panel. */
8715
- function IssuesPanel({ issues, title = "Problems" }) {
8716
- if (!issues.length) return null;
8717
- const errors = issues.filter((issue) => issue.severity === "error").length;
8718
- return /* @__PURE__ */ jsxs(Panel, {
8719
- className: errors ? "border-danger/40" : void 0,
8720
- children: [/* @__PURE__ */ jsx(PanelHead, {
8721
- title,
8722
- count: issues.length,
8723
- sub: errors ? `${errors} blocking` : "warnings"
8724
- }), /* @__PURE__ */ jsx("div", {
8725
- className: "border-t border-rule",
8726
- children: /* @__PURE__ */ jsx(IssueList, { issues })
8727
- })]
8728
- });
8729
- }
8730
- //#endregion
8731
- //#region viewer/src/components/slide-panel.tsx
8732
- var EASE = [
8733
- .2,
8734
- .7,
8735
- .2,
8736
- 1
8737
- ];
8738
- /**
8739
- * A side panel that slides open and shut. The wrapper animates its width while
8740
- * the content keeps its own, so nothing inside reflows mid-animation.
8741
- */
8742
- function SlidePanel({ open, side, width, children }) {
8743
- return /* @__PURE__ */ jsx(AnimatePresence, {
8744
- initial: false,
8745
- children: open ? /* @__PURE__ */ jsx(motion.div, {
8746
- initial: {
8747
- width: 0,
8748
- opacity: .6
8749
- },
8750
- animate: {
8751
- width,
8752
- opacity: 1
8753
- },
8754
- exit: {
8755
- width: 0,
8756
- opacity: .6
8757
- },
8758
- transition: {
8759
- duration: .22,
8760
- ease: EASE
8761
- },
8762
- className: cn("flex min-h-0 shrink-0 overflow-hidden", side === "right" ? "justify-start" : "justify-end"),
8763
- children: /* @__PURE__ */ jsx("div", {
8764
- className: "flex min-h-0 shrink-0",
8765
- style: { width },
8766
- children
8767
- })
8768
- }, "panel") : null
8769
- });
8770
- }
8771
- //#endregion
8772
- //#region viewer/src/ui/page.tsx
8773
- /** Page body under the top bar. */
8774
- function Content({ children, className, gap = 20, wide = false }) {
8775
- return /* @__PURE__ */ jsx("main", {
8776
- className: cn("mx-auto flex w-full min-h-0 grow flex-col px-8 pt-6 pb-7 animate-[q-fade-in_160ms_ease-out] max-md:px-4 max-md:pt-4", wide ? "max-w-[1440px]" : "max-w-[1200px]", className),
8777
- style: { gap },
8778
- children
8779
- });
8780
- }
8781
- function PageHead({ title, sub, right, badge, crumbs }) {
8782
- return /* @__PURE__ */ jsxs("div", {
8783
- className: "flex shrink-0 items-end justify-between gap-6 max-md:flex-col max-md:items-stretch max-md:gap-3",
8784
- children: [/* @__PURE__ */ jsxs("div", {
8785
- className: "flex min-w-0 flex-col gap-1",
8786
- children: [
8787
- crumbs ? /* @__PURE__ */ jsx("div", {
8788
- className: "flex items-center gap-1.5 text-[13px] text-muted",
8789
- children: crumbs
8790
- }) : null,
8791
- /* @__PURE__ */ jsxs("div", {
8792
- className: "flex items-center gap-3",
8793
- children: [/* @__PURE__ */ jsx("h1", {
8794
- className: "m-0 text-[22px] font-semibold tracking-[-0.01em]",
8795
- children: title
8796
- }), badge]
8797
- }),
8798
- sub ? /* @__PURE__ */ jsx("div", {
8799
- className: "text-[13px] [overflow-wrap:anywhere] text-muted",
8800
- children: sub
8801
- }) : null
8802
- ]
8803
- }), right ? /* @__PURE__ */ jsx("div", {
8804
- className: "flex flex-wrap items-center gap-2",
8805
- children: right
8806
- }) : null]
8807
- });
8808
- }
8809
- function SubNav({ items, label }) {
8810
- const [location] = useLocation();
8811
- return /* @__PURE__ */ jsx("nav", {
8812
- "aria-label": label,
8813
- className: "flex flex-col gap-0.5",
8814
- children: items.map((item) => {
8815
- const active = location === item.to;
8816
- return /* @__PURE__ */ jsxs(Link, {
8817
- href: item.to,
8818
- "aria-current": active ? "page" : void 0,
8819
- className: cn("relative flex h-8 items-center gap-2 whitespace-nowrap rounded-[6px] px-3 text-[13.5px] no-underline transition-colors focus-visible:outline-offset-[-2px]", active ? "font-medium text-ink" : "text-ink2 hover:bg-soft2"),
8820
- children: [
8821
- active ? /* @__PURE__ */ jsx(motion.span, {
8822
- layoutId: "sub-nav-active",
8823
- "aria-hidden": true,
8824
- className: "absolute inset-0 z-0 rounded-[6px] bg-soft2",
8825
- transition: {
8826
- type: "spring",
8827
- stiffness: 520,
8828
- damping: 42
8829
- }
8830
- }) : null,
8831
- /* @__PURE__ */ jsx("span", {
8832
- className: "relative z-[1] min-w-0 truncate",
8833
- children: item.label
8834
- }),
8835
- item.hint ? /* @__PURE__ */ jsx("span", {
8836
- className: "relative z-[1] ml-auto text-[12px] font-normal text-muted",
8837
- children: item.hint
8838
- }) : null
8839
- ]
8840
- }, item.to);
8841
- })
8842
- });
8843
- }
8844
- function NavLabel({ children }) {
8845
- return /* @__PURE__ */ jsx("div", {
8846
- className: "px-3 pt-4 pb-1 text-[12px] text-muted",
8847
- children
8848
- });
8849
- }
8850
- /** Standalone message card: not found, empty, failed to load. */
8851
- function NoticeCard({ title, text, action }) {
8852
- return /* @__PURE__ */ jsxs(Panel, {
8853
- className: "flex w-[480px] max-w-full flex-col items-center gap-2 px-6 py-8 text-center",
8854
- children: [
8855
- /* @__PURE__ */ jsx("h1", {
8856
- className: "m-0 text-[16px] font-semibold",
8857
- children: title
8858
- }),
8859
- text ? /* @__PURE__ */ jsx("div", {
8860
- className: "m-0 max-w-[400px] text-[14px] text-ink2",
8861
- children: text
8862
- }) : null,
8863
- action ? /* @__PURE__ */ jsx("div", {
8864
- className: "mt-3 flex flex-wrap justify-center gap-2",
8865
- children: action
8866
- }) : null
8867
- ]
8868
- });
8869
- }
8870
- /** NoticeCard centred in the free space. */
8871
- function Notice(props) {
8872
- return /* @__PURE__ */ jsx("div", {
8873
- className: "flex min-h-[50vh] grow items-center justify-center p-6 max-md:px-4",
8874
- children: /* @__PURE__ */ jsx(NoticeCard, { ...props })
8875
- });
8876
- }
8877
- //#endregion
8878
- //#region viewer/src/pages/not-found.tsx
8879
- function NotFound({ title = "Nothing here", text }) {
8880
- const scoped = !!useViewer().scope;
8881
- return /* @__PURE__ */ jsx(Notice, {
8882
- title,
8883
- text: text ?? "This address does not match a canvas or a page of the design system.",
8884
- action: scoped ? void 0 : /* @__PURE__ */ jsx(ButtonLink, {
8885
- to: "/",
8886
- children: "All canvases"
8887
- })
8888
- });
8889
- }
8890
- //#endregion
8891
- //#region viewer/src/pages/canvas/camera.ts
8892
- /** The farthest out the camera goes; past it a page is specks, and every one of them still costs. */
8893
- var MIN_ZOOM = .1;
8894
- var clampZoom = (z, min = MIN_ZOOM) => Math.min(8, Math.max(min, z));
8895
- /** The canvas camera. Lives outside React so panning never re-renders the tree. */
8896
- var CameraStore = class {
8897
- camera = {
8898
- x: 0,
8899
- y: 0,
8900
- z: 1
8901
- };
8902
- /** How far out the camera may go; the viewport sets it from the size of the page. */
8903
- minZoom = MIN_ZOOM;
8904
- listeners = /* @__PURE__ */ new Set();
8905
- animation = 0;
8906
- subscribe(listener) {
8907
- this.listeners.add(listener);
8908
- return () => {
8909
- this.listeners.delete(listener);
8910
- };
8911
- }
8912
- set(next) {
8913
- this.camera = {
8914
- x: next.x,
8915
- y: next.y,
8916
- z: clampZoom(next.z, this.minZoom)
8917
- };
8918
- for (const listener of this.listeners) listener(this.camera);
8919
- }
8920
- /** Stop any animation: the user took over. */
8921
- interrupt() {
8922
- cancelAnimationFrame(this.animation);
8923
- this.animation = 0;
8924
- }
8925
- panBy(dx, dy) {
8926
- this.interrupt();
8927
- const { x, y, z } = this.camera;
8928
- this.set({
8929
- x: x + dx,
8930
- y: y + dy,
8931
- z
8932
- });
8933
- }
8934
- /** Zoom by `factor` keeping the screen point (sx, sy) in place. */
8935
- zoomAt(sx, sy, factor) {
8936
- this.interrupt();
8937
- const { x, y, z } = this.camera;
8938
- const next = clampZoom(z * factor, this.minZoom);
8939
- const wx = (sx - x) / z;
8940
- const wy = (sy - y) / z;
8941
- this.set({
8942
- x: sx - wx * next,
8943
- y: sy - wy * next,
8944
- z: next
8945
- });
8946
- }
8947
- animateTo(target, duration = 320) {
8948
- this.interrupt();
8949
- const from = { ...this.camera };
8950
- const to = {
8951
- ...target,
8952
- z: clampZoom(target.z, this.minZoom)
8953
- };
8954
- if (duration <= 0 || matchMedia("(prefers-reduced-motion: reduce)").matches) {
8955
- this.set(to);
8956
- return;
8957
- }
8958
- const start = performance.now();
8959
- const step = (now) => {
8960
- const t = Math.min(1, (now - start) / duration);
8961
- const e = 1 - (1 - t) ** 3;
8962
- const z = from.z * (to.z / from.z) ** e;
8963
- const k = from.z === to.z ? e : (z - from.z) / (to.z - from.z);
8964
- this.set({
8965
- x: from.x + (to.x - from.x) * k,
8966
- y: from.y + (to.y - from.y) * k,
8967
- z
8968
- });
8969
- if (t < 1) this.animation = requestAnimationFrame(step);
8970
- else this.animation = 0;
8971
- };
8972
- this.animation = requestAnimationFrame(step);
8973
- }
8974
- toWorld(sx, sy) {
8975
- const { x, y, z } = this.camera;
8976
- return {
8977
- x: (sx - x) / z,
8978
- y: (sy - y) / z
8979
- };
8980
- }
8981
- };
8982
- /**
8983
- * The camera that shows `rect` inside a viewport of `w`×`h` with `pad` screen pixels around it.
8984
- * When `rect` does not fit even at `minZoom`, it lines up with the top (and the left, if it is too wide).
8985
- */
8986
- function fitRect(rect, w, h, pad = 72, maxZoom = 1, minZoom = MIN_ZOOM) {
8987
- const z = clampZoom(Math.min((w - pad * 2) / Math.max(rect.w, 1), (h - pad * 2) / Math.max(rect.h, 1), maxZoom), minZoom);
8988
- const place = (start, size, room) => size * z <= room - pad * 2 ? room / 2 - (start + size / 2) * z : pad - start * z;
8989
- return {
8990
- z,
8991
- x: place(rect.x, rect.w, w),
8992
- y: place(rect.y, rect.h, h)
8993
- };
8994
- }
8995
- function intersects(a, b) {
8996
- return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
8997
- }
8998
- //#endregion
8999
- //#region viewer/src/ui/choice.tsx
9000
- function Switch({ checked, onChange, label, disabled }) {
9001
- return /* @__PURE__ */ jsx("button", {
9002
- type: "button",
9003
- role: "switch",
9004
- "aria-checked": checked,
9005
- "aria-label": label,
9006
- disabled,
9007
- onClick: () => onChange?.(!checked),
9008
- className: cn("relative h-[18px] w-[32px] shrink-0 cursor-pointer rounded-[9px] border-0 p-0 transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-50", checked ? "bg-ink" : "bg-rule-strong"),
9009
- children: /* @__PURE__ */ jsx("span", {
9010
- className: "absolute top-[2px] left-[2px] size-[14px] rounded-full bg-surface shadow-[0_1px_2px_rgba(0,0,0,0.25)] transition-transform duration-200 ease-[cubic-bezier(0.2,0.7,0.2,1)]",
9011
- style: { transform: checked ? "translateX(14px)" : "none" }
9012
- })
9013
- });
9014
- }
9015
- function Segmented({ items, value, onChange, label, stretch, size = "ctrl" }) {
9016
- const indicator = useId();
9017
- return /* @__PURE__ */ jsx("div", {
9018
- role: "group",
9019
- "aria-label": label,
9020
- className: cn("shrink-0 items-center gap-0.5 rounded-[6px] bg-track p-[3px]", size === "ctrl" ? "h-ctrl" : "h-7", stretch ? "flex" : "inline-flex"),
9021
- children: items.map((item) => {
9022
- const on = item.value === value;
9023
- return /* @__PURE__ */ jsxs("button", {
9024
- type: "button",
9025
- "aria-pressed": on,
9026
- title: item.title,
9027
- onClick: () => onChange(item.value),
9028
- className: cn("relative flex h-full cursor-pointer items-center justify-center whitespace-nowrap rounded-[4px] border-0 bg-transparent font-medium transition-colors", size === "ctrl" ? "px-2.5 text-ctrl" : "px-2 text-[12.5px]", stretch && "flex-1", on ? "text-ink" : "text-muted hover:text-ink2"),
9029
- children: [on ? /* @__PURE__ */ jsx(motion.span, {
9030
- layoutId: indicator,
9031
- className: "absolute inset-0 z-0 rounded-[4px] bg-surface shadow-[0_1px_2px_rgba(0,0,0,0.10)]",
9032
- transition: {
9033
- type: "spring",
9034
- stiffness: 520,
9035
- damping: 42
9036
- }
9037
- }) : null, /* @__PURE__ */ jsx("span", {
9038
- className: "relative z-[1] flex items-center gap-1.5",
9039
- children: item.label
9040
- })]
9041
- }, item.value);
9042
- })
9043
- });
9044
- }
9045
- //#endregion
9046
- //#region viewer/src/pages/canvas/full-page.tsx
9047
- /** The full-window page of one screen, relative to the viewer's base. */
9048
- var fullHref = (canvas, item, theme) => `/c/${encodeURIComponent(canvas)}/full/${encodeURIComponent(item)}?theme=${theme}`;
9049
- /**
9050
- * "Open in a new tab" for a screen. A sandboxed viewer serves screens to its frames only, so the
9051
- * tab gets the full-window page of the viewer; otherwise the screen's own URL.
9052
- */
9053
- function useOpenUrl(canvasId, item, theme) {
9054
- const { frameSandbox } = useViewer();
9055
- const { base } = useRouter();
9056
- if (!item) return null;
9057
- if (item.kind === "url") return item.url;
9058
- if (!frameSandbox) return absoluteUrl(frameSrc(item.url, item.theme ?? theme));
9059
- return absoluteUrl(`${base}${fullHref(canvasId, item.id, item.theme ?? theme)}`);
9060
- }
9061
- /** `/c/:canvas/full/:item`: one screen filling the window, a way out on hover and Esc. */
9062
- function FullPage({ canvasId, itemId }) {
9063
- const { data: canvas, error, isLoading } = useCanvas(canvasId);
9064
- const [, navigate] = useLocation();
9065
- const { frameSandbox } = useViewer();
9066
- const appTheme = useTheme();
9067
- const frame = useRef(null);
9068
- const param = new URLSearchParams(window.location.search).get("theme");
9069
- const items = canvas ? canvas.pages.flatMap((page) => page.sections.flatMap((section) => section.items.filter((item) => item.kind === "screen" || item.kind === "url"))) : [];
9070
- const item = items.find((entry) => entry.id === itemId);
9071
- const theme = (item?.kind === "screen" ? item.theme : void 0) ?? (param === "light" || param === "dark" ? param : canvas?.theme ?? appTheme);
9072
- const url = item ? item.kind === "screen" ? frameSrc(item.url, theme) : item.url : "";
9073
- const back = `/c/${encodeURIComponent(canvasId)}`;
9074
- useEffect(() => {
9075
- if (item) document.title = `${item.title} · ${canvas?.title ?? ""}`;
9076
- }, [item, canvas?.title]);
9077
- useEffect(() => {
9078
- const onKey = (event) => {
9079
- if (event.key === "Escape") navigate(back);
9080
- };
9081
- window.addEventListener("keydown", onKey);
9082
- return () => window.removeEventListener("keydown", onKey);
9083
- }, [navigate, back]);
9084
- useEffect(() => {
9085
- const el = frame.current;
9086
- if (!el || item?.kind !== "screen") return;
9087
- return listenToFrame(el, (message) => {
9088
- if (message.type === "keydown" && message.code === "Escape") navigate(back);
9089
- else if (message.type === "go") {
9090
- const id = message.target.includes("/") ? message.target.split("/")[1] : message.target;
9091
- if (items.some((entry) => entry.id === id)) navigate(fullHref(canvasId, id, theme), { replace: true });
9092
- } else if (message.type === "link" && canvas) followLink(message, canvas, (id) => navigate(fullHref(canvasId, id, theme), { replace: true }), itemId);
9093
- });
9094
- }, [url, item?.kind]);
9095
- if (error) return /* @__PURE__ */ jsx(NotFound, {
9096
- title: "Canvas not found",
9097
- text: error.message
9098
- });
9099
- if (isLoading || !canvas) return /* @__PURE__ */ jsx("div", { className: "h-dvh bg-surface" });
9100
- if (!item) return /* @__PURE__ */ jsx(NotFound, {
9101
- title: "Screen not found",
9102
- text: `No screen "${itemId}" on this canvas.`
9103
- });
9104
- return /* @__PURE__ */ jsxs("div", {
9105
- className: "group fixed inset-0 bg-surface",
9106
- children: [/* @__PURE__ */ jsx("iframe", {
9107
- ref: frame,
9108
- src: url,
9109
- title: item.title,
9110
- allow: "clipboard-read; clipboard-write; fullscreen",
9111
- sandbox: frameSandbox,
9112
- className: "block h-full w-full border-0",
9113
- style: { colorScheme: theme },
9114
- onLoad: () => item.kind === "screen" && sendTheme(frame.current, theme)
9115
- }, `${item.id}:${item.kind === "screen" ? item.rev : item.url}`), /* @__PURE__ */ jsx("div", {
9116
- className: "pointer-events-none absolute inset-x-0 top-0 flex justify-end p-3 opacity-0 transition-opacity duration-200 group-hover:opacity-100 focus-within:opacity-100",
9117
- children: /* @__PURE__ */ jsxs(Link, {
9118
- href: back,
9119
- className: "pointer-events-auto flex h-8 items-center gap-2 rounded-[8px] border border-rule bg-surface px-3 text-[12.5px] text-ink2 no-underline shadow-pop hover:text-ink",
9120
- children: [
9121
- /* @__PURE__ */ jsx(Icon, {
9122
- name: "x",
9123
- size: 14
9124
- }),
9125
- "Back to ",
9126
- canvas.title,
9127
- /* @__PURE__ */ jsx(Kbd, { children: "esc" })
9128
- ]
9129
- })
9130
- })]
9131
- });
9132
- }
9133
- //#endregion
9134
- //#region viewer/src/pages/canvas/sidebar.tsx
9135
- function itemIcon(item) {
9136
- if (item.kind === "note") return "note";
9137
- if (item.kind === "image") return "image";
9138
- if (item.kind === "url") return "globe";
9139
- return item.frame.width < 600 ? "smartphone" : item.frame.width < 1100 ? "tablet" : "monitor";
9140
- }
9141
- function pageHref(canvas, page) {
9142
- return `/c/${encodeURIComponent(canvas.id)}/p/${encodeURIComponent(page.id)}`;
9143
- }
9144
- /** Pages of the canvas and the items on the current one. */
9145
- function Sidebar({ canvas, page, store, onPick }) {
9146
- const selected = useStore(store, (state) => state.selected);
9147
- const errors = useStore(store, (state) => state.frames);
9148
- return /* @__PURE__ */ jsxs("aside", {
9149
- "data-ui": true,
9150
- className: "flex w-[264px] shrink-0 flex-col overflow-hidden border-r border-rule bg-surface",
9151
- children: [/* @__PURE__ */ jsxs("div", {
9152
- className: "flex flex-col gap-1 border-b border-rule px-4 py-3.5",
9153
- children: [/* @__PURE__ */ jsx("div", {
9154
- className: "truncate text-[14px] font-semibold",
9155
- children: canvas.title
9156
- }), canvas.description ? /* @__PURE__ */ jsx("div", {
9157
- className: "line-clamp-3 text-[12.5px] leading-[1.45] text-muted",
9158
- children: canvas.description
9159
- }) : null]
9160
- }), /* @__PURE__ */ jsxs("div", {
9161
- className: "min-h-0 grow overflow-y-auto pb-4",
9162
- children: [
9163
- canvas.pages.length > 1 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
9164
- className: "px-4 pt-3 pb-1 text-[12px] text-muted",
9165
- children: "Pages"
9166
- }), /* @__PURE__ */ jsx("nav", {
9167
- className: "flex flex-col gap-0.5 px-2",
9168
- children: canvas.pages.map((p) => {
9169
- const count = p.sections.reduce((sum, s) => sum + s.items.filter((i) => i.kind === "screen" || i.kind === "url").length, 0);
9170
- return /* @__PURE__ */ jsxs(Link, {
9171
- href: pageHref(canvas, p),
9172
- className: cn("flex h-8 items-center gap-2 rounded-[6px] px-2.5 text-[13.5px] no-underline transition-colors focus-visible:outline-offset-[-2px]", p.id === page.id ? "bg-soft2 font-medium text-ink" : "text-ink2 hover:bg-soft2"),
9173
- children: [
9174
- /* @__PURE__ */ jsx(Icon, {
9175
- name: "file",
9176
- size: 15,
9177
- className: "text-muted"
9178
- }),
9179
- /* @__PURE__ */ jsx("span", {
9180
- className: "min-w-0 grow truncate",
9181
- children: p.title
9182
- }),
9183
- /* @__PURE__ */ jsx("span", {
9184
- className: "text-[12px] text-muted tabular",
9185
- children: count
9186
- })
9187
- ]
9188
- }, p.id);
9189
- })
9190
- })] }) : null,
9191
- /* @__PURE__ */ jsx("div", {
9192
- className: "px-4 pt-3 pb-1 text-[12px] text-muted",
9193
- children: canvas.pages.length > 1 ? page.title : "Layers"
9194
- }),
9195
- /* @__PURE__ */ jsx("div", {
9196
- className: "flex flex-col px-2",
9197
- children: page.sections.map((section) => /* @__PURE__ */ jsxs("div", {
9198
- className: "flex flex-col",
9199
- children: [section.title ? /* @__PURE__ */ jsxs("div", {
9200
- className: "flex h-7 items-center gap-2 px-2.5 pt-1 text-[12px] font-medium text-ink2",
9201
- children: [/* @__PURE__ */ jsx("span", {
9202
- className: "truncate",
9203
- children: section.title
9204
- }), /* @__PURE__ */ jsx(Count, { value: section.items.length })]
9205
- }) : null, section.items.map((item) => {
9206
- const hasErrors = (errors[item.id]?.errors.length ?? 0) > 0 || item.kind === "screen" && item.missing;
9207
- return /* @__PURE__ */ jsxs("button", {
9208
- type: "button",
9209
- onClick: () => onPick(item.id),
9210
- className: cn("flex h-8 w-full cursor-pointer items-center gap-2 rounded-[6px] border-0 px-2.5 text-left text-[13px] transition-colors focus-visible:outline-offset-[-2px]", section.title && "pl-4", selected === item.id ? "bg-action-soft text-ink" : "bg-transparent text-ink2 hover:bg-soft2"),
9211
- children: [
9212
- /* @__PURE__ */ jsx(Icon, {
9213
- name: itemIcon(item),
9214
- size: 15,
9215
- className: "text-muted"
9216
- }),
9217
- /* @__PURE__ */ jsx("span", {
9218
- className: "min-w-0 grow truncate",
9219
- children: item.title || (item.kind === "note" ? "Note" : item.id)
9220
- }),
9221
- hasErrors ? /* @__PURE__ */ jsx("span", { className: "size-[7px] shrink-0 rounded-full bg-danger" }) : null
9222
- ]
9223
- }, item.id);
9224
- })]
9225
- }, section.id))
9226
- })
9227
- ]
9228
- })]
9229
- });
9230
- }
9231
- //#endregion
9232
- //#region viewer/src/pages/canvas/canvas-bar.tsx
9233
- /** The canvas page's top bar: the canvas and its pages, screen theme, inspect and play. */
9234
- function CanvasBar({ canvas, page, theme, onTheme, sidebar, onSidebar, onPlay, canPlay, inspect, onInspect }) {
9235
- const [, navigate] = useLocation();
9236
- const { slots, scope } = useViewer();
9237
- const titleRef = useRef(null);
9238
- const endRef = useRef(null);
9239
- const compact = useFitBar(titleRef, endRef, [
9240
- canvas.title,
9241
- page?.title,
9242
- canvas.pages.length
9243
- ]);
9244
- return /* @__PURE__ */ jsxs(BarCompactProvider, {
9245
- value: compact,
9246
- children: [/* @__PURE__ */ jsxs(Bar, { children: [
9247
- slots.barStart,
9248
- slots.brand ? slots.brand : scope ? /* @__PURE__ */ jsx(Mark, {}) : /* @__PURE__ */ jsx(Tooltip, {
9249
- content: "All canvases",
9250
- children: /* @__PURE__ */ jsx(Link, {
9251
- href: "/",
9252
- className: "flex items-center",
9253
- "aria-label": "All canvases",
9254
- children: /* @__PURE__ */ jsx(Mark, {})
9255
- })
9256
- }),
9257
- /* @__PURE__ */ jsx(IconButton, {
9258
- icon: "panel-left",
9259
- label: sidebar ? "Hide sidebar (⌘\\)" : "Show sidebar (⌘\\)",
9260
- onClick: () => onSidebar(!sidebar)
9261
- }),
9262
- /* @__PURE__ */ jsxs("div", {
9263
- ref: titleRef,
9264
- className: "flex min-w-0 items-center gap-1.5 text-[14px]",
9265
- children: [/* @__PURE__ */ jsx("span", {
9266
- "data-fit": true,
9267
- className: "truncate font-semibold",
9268
- children: canvas.title
9269
- }), page && canvas.pages.length > 1 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, {
9270
- name: "chevron-right",
9271
- size: 14,
9272
- className: "shrink-0 text-muted"
9273
- }), /* @__PURE__ */ jsxs(Menu, {
9274
- width: 240,
9275
- trigger: /* @__PURE__ */ jsxs("button", {
9276
- type: "button",
9277
- className: "flex h-ctrl cursor-pointer items-center gap-1 rounded-[6px] border-0 bg-transparent px-2 text-[14px] text-ink2 hover:bg-soft2",
9278
- children: [/* @__PURE__ */ jsx("span", {
9279
- "data-fit": true,
9280
- className: "truncate",
9281
- children: page.title
9282
- }), /* @__PURE__ */ jsx(Icon, {
9283
- name: "chevron-down",
9284
- size: 14,
9285
- className: "text-muted"
9286
- })]
9287
- }),
9288
- children: [/* @__PURE__ */ jsx(MenuLabel, { children: "Pages" }), canvas.pages.map((p) => /* @__PURE__ */ jsxs(MenuItem, {
9289
- active: p.id === page.id,
9290
- onSelect: () => navigate(pageHref(canvas, p)),
9291
- children: [/* @__PURE__ */ jsx(Icon, {
9292
- name: "file",
9293
- size: 15,
9294
- className: "text-muted"
9295
- }), p.title]
9296
- }, p.id))]
9297
- })] }) : null]
9298
- }),
9299
- /* @__PURE__ */ jsxs("div", {
9300
- ref: endRef,
9301
- className: "ml-auto flex shrink-0 items-center gap-2",
9302
- children: [
9303
- /* @__PURE__ */ jsx(Segmented, {
9304
- label: "Screen theme",
9305
- value: theme,
9306
- onChange: onTheme,
9307
- items: [{
9308
- value: "light",
9309
- label: /* @__PURE__ */ jsx(Icon, {
9310
- name: "sun",
9311
- size: 15
9312
- }),
9313
- title: "Light screens"
9314
- }, {
9315
- value: "dark",
9316
- label: /* @__PURE__ */ jsx(Icon, {
9317
- name: "moon",
9318
- size: 15
9319
- }),
9320
- title: "Dark screens"
9321
- }]
9322
- }),
9323
- /* @__PURE__ */ jsx(InspectToggle, {
9324
- on: inspect,
9325
- onChange: onInspect,
9326
- compact
9327
- }),
9328
- /* @__PURE__ */ jsx(Tooltip, {
9329
- content: /* @__PURE__ */ jsxs("span", {
9330
- className: "flex items-center gap-1.5",
9331
- children: ["Play ", /* @__PURE__ */ jsx(Kbd, { children: "P" })]
9332
- }),
9333
- children: /* @__PURE__ */ jsx(Button, {
9334
- kind: "secondary",
9335
- icon: "play",
9336
- onClick: onPlay,
9337
- disabled: !canPlay,
9338
- "aria-label": "Play",
9339
- className: compact ? "w-ctrl px-0" : void 0,
9340
- children: compact ? null : "Play"
9341
- })
9342
- }),
9343
- /* @__PURE__ */ jsx(SearchButton, {
9344
- width: 180,
9345
- label: "Search",
9346
- compact
9347
- }),
9348
- /* @__PURE__ */ jsx(ThemeMenu, {}),
9349
- slots.barEnd
9350
- ]
9351
- })
9352
- ] }), slots.belowBar ? /* @__PURE__ */ jsx("div", {
9353
- className: "shrink-0",
9354
- children: slots.belowBar
9355
- }) : null]
9356
- });
9357
- }
9358
- /** The selected item's name and actions, floating over the bottom of the canvas. */
9359
- function SelectionBar({ canvas, store, layoutItems, theme, onPlay, inspect }) {
9360
- const selected = useStore(store, (state) => state.selected);
9361
- const active = useStore(store, (state) => state.active);
9362
- const status = useStore(store, (state) => selected ? state.frames[selected] : void 0);
9363
- const item = layoutItems.find((i) => i.id === selected);
9364
- const openUrl = useOpenUrl(canvas.id, item?.kind === "screen" || item?.kind === "url" ? item : void 0, theme);
9365
- if (!item) return null;
9366
- const frame = item.kind === "screen" || item.kind === "url";
9367
- const reload = () => reloadFrame(document.querySelector(`[data-item="${CSS.escape(item.id)}"] iframe`));
9368
- const errors = status?.errors ?? [];
9369
- return /* @__PURE__ */ jsx("div", {
9370
- "data-ui": true,
9371
- className: "pointer-events-none absolute inset-x-0 bottom-5 z-20 flex justify-center px-4",
9372
- children: /* @__PURE__ */ jsxs("div", {
9373
- className: "pointer-events-auto flex max-w-full flex-col overflow-hidden rounded-[10px] border border-rule bg-surface shadow-pop animate-[q-pop_120ms_ease-out]",
9374
- children: [errors.length ? /* @__PURE__ */ jsxs("div", {
9375
- className: "flex max-h-40 items-start gap-2 overflow-y-auto border-b border-rule bg-danger-soft px-3.5 py-2 font-mono text-[12px] whitespace-pre-wrap text-danger-text",
9376
- children: [/* @__PURE__ */ jsx(Icon, {
9377
- name: "alert",
9378
- size: 14,
9379
- className: "mt-px"
9380
- }), /* @__PURE__ */ jsx("span", {
9381
- className: "min-w-0",
9382
- children: errors.join("\n")
9383
- })]
9384
- }) : null, /* @__PURE__ */ jsxs("div", {
9385
- className: "flex h-[44px] items-center gap-1 pr-1.5 pl-3.5",
9386
- children: [
9387
- /* @__PURE__ */ jsxs("div", {
9388
- className: "flex min-w-0 items-center gap-2 pr-2",
9389
- children: [/* @__PURE__ */ jsx("span", {
9390
- className: "truncate text-[13.5px] font-medium",
9391
- children: item.title || item.id
9392
- }), frame && active === item.id ? /* @__PURE__ */ jsxs("span", {
9393
- className: "flex shrink-0 items-center gap-1.5 text-[12px] text-muted",
9394
- children: [
9395
- /* @__PURE__ */ jsx(Icon, {
9396
- name: "pointer",
9397
- size: 13
9398
- }),
9399
- " ",
9400
- inspect ? "Inspecting" : "Interacting",
9401
- " ",
9402
- /* @__PURE__ */ jsx(Kbd, { children: "esc" })
9403
- ]
9404
- }) : frame ? /* @__PURE__ */ jsx("span", {
9405
- className: "shrink-0 text-[12px] text-muted",
9406
- children: inspect ? "Click an element to inspect it" : "Click the screen to interact"
9407
- }) : null]
9408
- }),
9409
- frame ? /* @__PURE__ */ jsx(Tooltip, {
9410
- content: /* @__PURE__ */ jsxs("span", {
9411
- className: "flex items-center gap-1.5",
9412
- children: ["Play ", /* @__PURE__ */ jsx(Kbd, { children: "↵" })]
9413
- }),
9414
- children: /* @__PURE__ */ jsx(IconButton, {
9415
- icon: "play",
9416
- label: "Play",
9417
- onClick: () => onPlay(item.id)
9418
- })
9419
- }) : null,
9420
- openUrl ? /* @__PURE__ */ jsx(Tooltip, {
9421
- content: "Open in a new tab",
9422
- children: /* @__PURE__ */ jsx("a", {
9423
- href: openUrl,
9424
- target: "_blank",
9425
- rel: "noreferrer",
9426
- "aria-label": "Open in a new tab",
9427
- className: "inline-flex h-ctrl w-ctrl items-center justify-center rounded-[6px] text-muted hover:bg-soft2 hover:text-ink2",
9428
- children: /* @__PURE__ */ jsx(Icon, {
9429
- name: "external",
9430
- size: 16
9431
- })
9432
- })
9433
- }) : null,
9434
- frame ? /* @__PURE__ */ jsx(Tooltip, {
9435
- content: "Reload",
9436
- children: /* @__PURE__ */ jsx(IconButton, {
9437
- icon: "refresh",
9438
- label: "Reload",
9439
- onClick: reload
9440
- })
9441
- }) : null,
9442
- item.kind === "screen" ? /* @__PURE__ */ jsx(Tooltip, {
9443
- content: "Copy source path",
9444
- children: /* @__PURE__ */ jsx(IconButton, {
9445
- icon: "copy",
9446
- label: "Copy source path",
9447
- onClick: () => void copyText(item.file, "Path copied")
9448
- })
9449
- }) : null,
9450
- /* @__PURE__ */ jsx(Tooltip, {
9451
- content: /* @__PURE__ */ jsxs("span", {
9452
- className: "flex items-center gap-1.5",
9453
- children: ["Zoom to ", /* @__PURE__ */ jsx(Kbd, { children: "⇧2" })]
9454
- }),
9455
- children: /* @__PURE__ */ jsx(IconButton, {
9456
- icon: "fit",
9457
- label: "Zoom to",
9458
- onClick: () => window.dispatchEvent(new CustomEvent("design:focus", { detail: {
9459
- canvas: canvas.id,
9460
- item: item.id
9461
- } }))
9462
- })
9463
- })
9464
- ]
9465
- })]
9466
- })
9467
- });
9468
- }
9469
- /** The inspect mode switch: a button that stays pressed while on. */
9470
- function InspectToggle({ on, onChange, compact = false }) {
9471
- return /* @__PURE__ */ jsx(Tooltip, {
9472
- content: /* @__PURE__ */ jsxs("span", {
9473
- className: "flex items-center gap-1.5",
9474
- children: ["Inspect elements ", /* @__PURE__ */ jsx(Kbd, { children: "I" })]
9475
- }),
9476
- children: /* @__PURE__ */ jsxs("button", {
9477
- type: "button",
9478
- "aria-pressed": on,
9479
- "aria-label": "Inspect elements",
9480
- onClick: () => onChange(!on),
9481
- className: cn("inline-flex h-ctrl shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-[6px] border text-ctrl font-medium", compact ? "w-ctrl px-0" : "px-ctrl", on ? "border-action-line bg-action-soft text-action" : "border-rule-strong bg-surface text-ink hover:bg-soft"),
9482
- children: [/* @__PURE__ */ jsx(Icon, {
9483
- name: "pointer",
9484
- size: 15
9485
- }), compact ? null : "Inspect"]
9486
- })
9487
- });
9488
- }
9489
- //#endregion
9490
- //#region viewer/src/pages/canvas/layout.ts
9491
- var SECTION_GAP = 280;
9492
- var ITEM_GAP = 96;
9493
- var ROW_GAP = 150;
9494
- var HEADER_GAP = 110;
9495
- var HEADER_MIN_WIDTH = 720;
9496
- var isFrame = (item) => item.kind === "screen" || item.kind === "url";
9497
- function itemSize(item, sizes) {
9498
- const measured = sizes[item.id];
9499
- if (item.kind === "screen" || item.kind === "url") {
9500
- const w = item.frame.width;
9501
- if (item.frame.height !== "auto") return {
9502
- w,
9503
- h: item.frame.height
9504
- };
9505
- const fallback = item.kind === "screen" ? item.measuredHeight : void 0;
9506
- return {
9507
- w,
9508
- h: measured?.h ?? fallback ?? 900
9509
- };
9510
- }
9511
- if (item.kind === "note") return {
9512
- w: item.width,
9513
- h: measured?.h ?? Math.max(80, Math.ceil(item.text.length / 38) * 26 + 40)
9514
- };
9515
- const w = item.width ?? measured?.w ?? 480;
9516
- return {
9517
- w,
9518
- h: measured?.w ? measured.h / measured.w * w : measured?.h ?? w * .62
9519
- };
9520
- }
9521
- function sectionHeaderHeight(section, sizes) {
9522
- if (!section.title && !section.description) return 0;
9523
- return sizes[`section:${section.id}`]?.h ?? (section.title ? 56 : 0) + (section.description ? 60 : 0);
9524
- }
9525
- /** Sections stack top to bottom; items run left to right, wrapping after `columns`. */
9526
- function layoutPage(page, sizes) {
9527
- const items = [];
9528
- const sections = [];
9529
- if (page.layout === "free") {
9530
- let cursor = 0;
9531
- for (const section of page.sections) for (const item of section.items) {
9532
- const { w, h } = itemSize(item, sizes);
9533
- const x = item.x ?? cursor;
9534
- const y = item.y ?? 0;
9535
- cursor = Math.max(cursor, x + w + ITEM_GAP);
9536
- items.push({
9537
- item,
9538
- section,
9539
- x,
9540
- y,
9541
- w,
9542
- h
9543
- });
9544
- }
9545
- } else {
9546
- let y = 0;
9547
- for (const section of page.sections) {
9548
- const header = sectionHeaderHeight(section, sizes);
9549
- const top = y;
9550
- let rowY = y + (header ? header + HEADER_GAP : 0);
9551
- let x = 0;
9552
- let rowHeight = 0;
9553
- let width = 0;
9554
- let column = 0;
9555
- for (const item of section.items) {
9556
- if (section.columns && column === section.columns) {
9557
- rowY += rowHeight + ROW_GAP;
9558
- x = 0;
9559
- rowHeight = 0;
9560
- column = 0;
9561
- }
9562
- const { w, h } = itemSize(item, sizes);
9563
- items.push({
9564
- item,
9565
- section,
9566
- x,
9567
- y: rowY,
9568
- w,
9569
- h
9570
- });
9571
- x += w + ITEM_GAP;
9572
- width = Math.max(width, x - ITEM_GAP);
9573
- rowHeight = Math.max(rowHeight, h);
9574
- column++;
9575
- }
9576
- const bottom = rowY + rowHeight;
9577
- sections.push({
9578
- section,
9579
- x: 0,
9580
- y: top,
9581
- w: Math.max(width, header ? HEADER_MIN_WIDTH : 0),
9582
- h: bottom - top,
9583
- header
9584
- });
9585
- y = bottom + SECTION_GAP;
9586
- }
9587
- }
9588
- let minX = 0;
9589
- let minY = 0;
9590
- let maxX = 0;
9591
- let maxY = 0;
9592
- const all = [...items, ...sections];
9593
- if (all.length) {
9594
- minX = Math.min(...all.map((r) => r.x));
9595
- minY = Math.min(...all.map((r) => r.y));
9596
- maxX = Math.max(...all.map((r) => r.x + r.w));
9597
- maxY = Math.max(...all.map((r) => r.y + r.h));
9598
- }
9599
- return {
9600
- items,
9601
- sections,
9602
- bounds: {
9603
- x: minX,
9604
- y: minY,
9605
- w: maxX - minX,
9606
- h: maxY - minY
9607
- }
9608
- };
9609
- }
9610
- /** Frames in reading order, for arrow-key stepping and play mode. */
9611
- function frameOrder(layout) {
9612
- return layout.items.filter((placed) => isFrame(placed.item));
9613
- }
9614
- //#endregion
9615
- //#region viewer/src/pages/canvas/view-state.ts
9616
- function createViewState() {
9617
- return new Store({
9618
- selected: null,
9619
- active: null,
9620
- hovered: null,
9621
- sizes: {},
9622
- frames: {},
9623
- live: [],
9624
- panning: false,
9625
- zoomTiny: false,
9626
- frameEls: {},
9627
- inspecting: false
9628
- });
9629
- }
9630
- function setSize(store, key, size) {
9631
- store.set((state) => {
9632
- const prev = state.sizes[key];
9633
- if (prev && Math.abs(prev.h - size.h) < 1 && prev.w === size.w) return state;
9634
- return {
9635
- ...state,
9636
- sizes: {
9637
- ...state.sizes,
9638
- [key]: size
9639
- }
9640
- };
9641
- });
9642
- }
9643
- function setFrame(store, id, patch) {
9644
- store.set((state) => {
9645
- const prev = state.frames[id] ?? {
9646
- ready: false,
9647
- errors: []
9648
- };
9649
- return {
9650
- ...state,
9651
- frames: {
9652
- ...state.frames,
9653
- [id]: {
9654
- ...prev,
9655
- ...patch
9656
- }
9657
- }
9658
- };
9659
- });
9660
- }
9661
- function setFrameEl(store, id, el) {
9662
- store.set((state) => state.frameEls[id] === el ? state : {
9663
- ...state,
9664
- frameEls: {
9665
- ...state.frameEls,
9666
- [id]: el
9667
- }
9668
- });
9669
- }
9670
- //#endregion
9671
- //#region ../../node_modules/.pnpm/marked@18.0.14/node_modules/marked/lib/marked.esm.js
9672
- /**
9673
- * marked v18.0.14 - a markdown parser
9674
- * Copyright (c) 2018-2026, MarkedJS. (MIT License)
9675
- * Copyright (c) 2011-2018, Christopher Jeffrey. (MIT License)
9676
- * https://github.com/markedjs/marked
9677
- */
9678
- /**
9679
- * DO NOT EDIT THIS FILE
9680
- * The code in this file is generated from files in ./src/
9681
- */
9682
- function I() {
9683
- return {
9684
- async: !1,
9685
- breaks: !1,
9686
- extensions: null,
9687
- gfm: !0,
9688
- hooks: null,
9689
- pedantic: !1,
9690
- renderer: null,
9691
- silent: !1,
9692
- tokenizer: null,
9693
- walkTokens: null
9694
- };
9695
- }
9696
- var y = I();
9697
- function W(l) {
9698
- y = l;
9699
- }
9700
- var A = { exec: () => null };
9701
- function C(l) {
9702
- let e = [];
9703
- return (t) => {
9704
- let n = Math.max(0, Math.min(3, t - 1)), s = e[n];
9705
- return s || (s = l(n), e[n] = s), s;
9706
- };
9707
- }
9708
- function h(l, e = "") {
9709
- let t = typeof l == "string" ? l : l.source, n = {
9710
- replace: (s, r) => {
9711
- let o = typeof r == "string" ? r : r.source;
9712
- return o = o.replace(x.caret, "$1"), t = t.replace(s, o), n;
9713
- },
9714
- getRegex: () => new RegExp(t, e)
9715
- };
9716
- return n;
7864
+ return n;
9717
7865
  }
9718
7866
  var _e = ((l = "") => {
9719
7867
  try {
@@ -11076,333 +9224,2209 @@ var T = class l {
11076
9224
  continue;
11077
9225
  }
11078
9226
  }
11079
- let o = r;
11080
- switch (o.type) {
11081
- case "escape":
11082
- n += t.text(o);
11083
- break;
11084
- case "html":
11085
- n += t.html(o);
11086
- break;
11087
- case "link":
11088
- n += t.link(o);
11089
- break;
11090
- case "image":
11091
- n += t.image(o);
11092
- break;
11093
- case "checkbox":
11094
- n += t.checkbox(o);
11095
- break;
11096
- case "strong":
11097
- n += t.strong(o);
11098
- break;
11099
- case "em":
11100
- n += t.em(o);
11101
- break;
11102
- case "codespan":
11103
- n += t.codespan(o);
11104
- break;
11105
- case "br":
11106
- n += t.br(o);
11107
- break;
11108
- case "del":
11109
- n += t.del(o);
11110
- break;
11111
- case "text":
11112
- n += t.text(o);
11113
- break;
11114
- default: {
11115
- let i = "Token with \"" + o.type + "\" type was not found.";
11116
- if (this.options.silent) return console.error(i), "";
11117
- throw new Error(i);
9227
+ let o = r;
9228
+ switch (o.type) {
9229
+ case "escape":
9230
+ n += t.text(o);
9231
+ break;
9232
+ case "html":
9233
+ n += t.html(o);
9234
+ break;
9235
+ case "link":
9236
+ n += t.link(o);
9237
+ break;
9238
+ case "image":
9239
+ n += t.image(o);
9240
+ break;
9241
+ case "checkbox":
9242
+ n += t.checkbox(o);
9243
+ break;
9244
+ case "strong":
9245
+ n += t.strong(o);
9246
+ break;
9247
+ case "em":
9248
+ n += t.em(o);
9249
+ break;
9250
+ case "codespan":
9251
+ n += t.codespan(o);
9252
+ break;
9253
+ case "br":
9254
+ n += t.br(o);
9255
+ break;
9256
+ case "del":
9257
+ n += t.del(o);
9258
+ break;
9259
+ case "text":
9260
+ n += t.text(o);
9261
+ break;
9262
+ default: {
9263
+ let i = "Token with \"" + o.type + "\" type was not found.";
9264
+ if (this.options.silent) return console.error(i), "";
9265
+ throw new Error(i);
9266
+ }
9267
+ }
9268
+ }
9269
+ return n;
9270
+ }
9271
+ };
9272
+ var _ = class {
9273
+ options;
9274
+ block;
9275
+ constructor(e) {
9276
+ this.options = e || y;
9277
+ }
9278
+ static passThroughHooks = /* @__PURE__ */ new Set([
9279
+ "preprocess",
9280
+ "postprocess",
9281
+ "processAllTokens",
9282
+ "emStrongMask"
9283
+ ]);
9284
+ static passThroughHooksRespectAsync = /* @__PURE__ */ new Set([
9285
+ "preprocess",
9286
+ "postprocess",
9287
+ "processAllTokens"
9288
+ ]);
9289
+ preprocess(e) {
9290
+ return e;
9291
+ }
9292
+ postprocess(e) {
9293
+ return e;
9294
+ }
9295
+ processAllTokens(e) {
9296
+ return e;
9297
+ }
9298
+ emStrongMask(e) {
9299
+ return e;
9300
+ }
9301
+ provideLexer(e = this.block) {
9302
+ return e ? R.lex : R.lexInline;
9303
+ }
9304
+ provideParser(e = this.block) {
9305
+ return e ? T.parse : T.parseInline;
9306
+ }
9307
+ };
9308
+ var F = class {
9309
+ defaults = I();
9310
+ options = this.setOptions;
9311
+ parse = this.parseMarkdown(!0);
9312
+ parseInline = this.parseMarkdown(!1);
9313
+ Parser = T;
9314
+ Renderer = S;
9315
+ TextRenderer = z;
9316
+ Lexer = R;
9317
+ Tokenizer = P;
9318
+ Hooks = _;
9319
+ constructor(...e) {
9320
+ this.use(...e);
9321
+ }
9322
+ walkTokens(e, t) {
9323
+ let n = [];
9324
+ for (let s of e) switch (n = n.concat(t.call(this, s)), s.type) {
9325
+ case "table": {
9326
+ let r = s;
9327
+ for (let o of r.header) n = n.concat(this.walkTokens(o.tokens, t));
9328
+ for (let o of r.rows) for (let i of o) n = n.concat(this.walkTokens(i.tokens, t));
9329
+ break;
9330
+ }
9331
+ case "list": {
9332
+ let r = s;
9333
+ n = n.concat(this.walkTokens(r.items, t));
9334
+ break;
9335
+ }
9336
+ default: {
9337
+ let r = s;
9338
+ this.defaults.extensions?.childTokens?.[r.type] ? this.defaults.extensions.childTokens[r.type].forEach((o) => {
9339
+ let i = r[o].flat(1 / 0);
9340
+ n = n.concat(this.walkTokens(i, t));
9341
+ }) : r.tokens && (n = n.concat(this.walkTokens(r.tokens, t)));
9342
+ }
9343
+ }
9344
+ return n;
9345
+ }
9346
+ use(...e) {
9347
+ let t = this.defaults.extensions || {
9348
+ renderers: {},
9349
+ childTokens: {}
9350
+ };
9351
+ return e.forEach((n) => {
9352
+ let s = { ...n };
9353
+ if (s.async = this.defaults.async || s.async || !1, n.extensions && (n.extensions.forEach((r) => {
9354
+ if (!r.name) throw new Error("extension name required");
9355
+ if ("renderer" in r) {
9356
+ let o = t.renderers[r.name];
9357
+ o ? t.renderers[r.name] = function(...i) {
9358
+ let u = r.renderer.apply(this, i);
9359
+ return u === !1 && (u = o.apply(this, i)), u;
9360
+ } : t.renderers[r.name] = r.renderer;
9361
+ }
9362
+ if ("tokenizer" in r) {
9363
+ if (!r.level || r.level !== "block" && r.level !== "inline") throw new Error("extension level must be 'block' or 'inline'");
9364
+ let o = t[r.level];
9365
+ o ? o.unshift(r.tokenizer) : t[r.level] = [r.tokenizer], r.start && (r.level === "block" ? t.startBlock ? t.startBlock.push(r.start) : t.startBlock = [r.start] : r.level === "inline" && (t.startInline ? t.startInline.push(r.start) : t.startInline = [r.start]));
9366
+ }
9367
+ "childTokens" in r && r.childTokens && (t.childTokens[r.name] = r.childTokens);
9368
+ }), s.extensions = t), n.renderer) {
9369
+ let r = this.defaults.renderer || new S(this.defaults);
9370
+ for (let o in n.renderer) {
9371
+ if (!(o in r)) throw new Error(`renderer '${o}' does not exist`);
9372
+ if (["options", "parser"].includes(o)) continue;
9373
+ let i = o, u = n.renderer[i], a = r[i];
9374
+ r[i] = (...p) => {
9375
+ let c = u.apply(r, p);
9376
+ return c === !1 && (c = a.apply(r, p)), c || "";
9377
+ };
9378
+ }
9379
+ s.renderer = r;
9380
+ }
9381
+ if (n.tokenizer) {
9382
+ let r = this.defaults.tokenizer || new P(this.defaults);
9383
+ for (let o in n.tokenizer) {
9384
+ if (!(o in r)) throw new Error(`tokenizer '${o}' does not exist`);
9385
+ if ([
9386
+ "options",
9387
+ "rules",
9388
+ "lexer"
9389
+ ].includes(o)) continue;
9390
+ let i = o, u = n.tokenizer[i], a = r[i];
9391
+ r[i] = (...p) => {
9392
+ let c = u.apply(r, p);
9393
+ return c === !1 && (c = a.apply(r, p)), c;
9394
+ };
9395
+ }
9396
+ s.tokenizer = r;
9397
+ }
9398
+ if (n.hooks) {
9399
+ let r = this.defaults.hooks || new _();
9400
+ for (let o in n.hooks) {
9401
+ if (!(o in r)) throw new Error(`hook '${o}' does not exist`);
9402
+ if (["options", "block"].includes(o)) continue;
9403
+ let i = o, u = n.hooks[i], a = r[i];
9404
+ _.passThroughHooks.has(o) ? r[i] = (p) => {
9405
+ if (this.defaults.async && _.passThroughHooksRespectAsync.has(o)) return (async () => {
9406
+ let d = await u.call(r, p);
9407
+ return a.call(r, d);
9408
+ })();
9409
+ let c = u.call(r, p);
9410
+ return a.call(r, c);
9411
+ } : r[i] = (...p) => {
9412
+ if (this.defaults.async) return (async () => {
9413
+ let d = await u.apply(r, p);
9414
+ return d === !1 && (d = await a.apply(r, p)), d;
9415
+ })();
9416
+ let c = u.apply(r, p);
9417
+ return c === !1 && (c = a.apply(r, p)), c;
9418
+ };
11118
9419
  }
9420
+ s.hooks = r;
11119
9421
  }
11120
- }
11121
- return n;
9422
+ if (n.walkTokens) {
9423
+ let r = this.defaults.walkTokens, o = n.walkTokens;
9424
+ s.walkTokens = function(i) {
9425
+ let u = [];
9426
+ return u.push(o.call(this, i)), r && (u = u.concat(r.call(this, i))), u;
9427
+ };
9428
+ }
9429
+ this.defaults = {
9430
+ ...this.defaults,
9431
+ ...s
9432
+ };
9433
+ }), this;
11122
9434
  }
11123
- };
11124
- var _ = class {
11125
- options;
11126
- block;
11127
- constructor(e) {
11128
- this.options = e || y;
9435
+ setOptions(e) {
9436
+ return this.defaults = {
9437
+ ...this.defaults,
9438
+ ...e
9439
+ }, this;
11129
9440
  }
11130
- static passThroughHooks = /* @__PURE__ */ new Set([
11131
- "preprocess",
11132
- "postprocess",
11133
- "processAllTokens",
11134
- "emStrongMask"
11135
- ]);
11136
- static passThroughHooksRespectAsync = /* @__PURE__ */ new Set([
11137
- "preprocess",
11138
- "postprocess",
11139
- "processAllTokens"
11140
- ]);
11141
- preprocess(e) {
11142
- return e;
9441
+ lexer(e, t) {
9442
+ return R.lex(e, t ?? this.defaults);
11143
9443
  }
11144
- postprocess(e) {
11145
- return e;
9444
+ parser(e, t) {
9445
+ return T.parse(e, t ?? this.defaults);
11146
9446
  }
11147
- processAllTokens(e) {
11148
- return e;
9447
+ parseMarkdown(e) {
9448
+ return (n, s) => {
9449
+ let r = { ...s }, o = {
9450
+ ...this.defaults,
9451
+ ...r
9452
+ }, i = this.onError(!!o.silent, !!o.async);
9453
+ if (this.defaults.async === !0 && r.async === !1) return i(/* @__PURE__ */ new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));
9454
+ if (typeof n > "u" || n === null) return i(/* @__PURE__ */ new Error("marked(): input parameter is undefined or null"));
9455
+ if (typeof n != "string") return i(/* @__PURE__ */ new Error("marked(): input parameter is of type " + Object.prototype.toString.call(n) + ", string expected"));
9456
+ if (o.hooks && (o.hooks.options = o, o.hooks.block = e), o.async) return (async () => {
9457
+ let u = o.hooks ? await o.hooks.preprocess(n) : n, p = await (o.hooks ? await o.hooks.provideLexer(e) : e ? R.lex : R.lexInline)(u, o), c = o.hooks ? await o.hooks.processAllTokens(p) : p;
9458
+ o.walkTokens && await Promise.all(this.walkTokens(c, o.walkTokens));
9459
+ let m = await (o.hooks ? await o.hooks.provideParser(e) : e ? T.parse : T.parseInline)(c, o);
9460
+ return o.hooks ? await o.hooks.postprocess(m) : m;
9461
+ })().catch(i);
9462
+ try {
9463
+ o.hooks && (n = o.hooks.preprocess(n));
9464
+ let a = (o.hooks ? o.hooks.provideLexer(e) : e ? R.lex : R.lexInline)(n, o);
9465
+ o.hooks && (a = o.hooks.processAllTokens(a)), o.walkTokens && this.walkTokens(a, o.walkTokens);
9466
+ let c = (o.hooks ? o.hooks.provideParser(e) : e ? T.parse : T.parseInline)(a, o);
9467
+ return o.hooks && (c = o.hooks.postprocess(c)), c;
9468
+ } catch (u) {
9469
+ return i(u);
9470
+ }
9471
+ };
9472
+ }
9473
+ onError(e, t) {
9474
+ return (n) => {
9475
+ if (n.message += `
9476
+ Please report this to https://github.com/markedjs/marked.`, e) {
9477
+ let s = "<p>An error occurred:</p><pre>" + O(n.message + "", !0) + "</pre>";
9478
+ return t ? Promise.resolve(s) : s;
9479
+ }
9480
+ if (t) return Promise.reject(n);
9481
+ throw n;
9482
+ };
9483
+ }
9484
+ };
9485
+ var E = new F();
9486
+ function k(l, e) {
9487
+ return E.parse(l, e);
9488
+ }
9489
+ k.options = k.setOptions = function(l) {
9490
+ return E.setOptions(l), k.defaults = E.defaults, W(k.defaults), k;
9491
+ };
9492
+ k.getDefaults = I;
9493
+ k.defaults = y;
9494
+ function Pt(...l) {
9495
+ return E.use(...l), k.defaults = E.defaults, W(k.defaults), k;
9496
+ }
9497
+ k.use = Pt;
9498
+ k.walkTokens = function(l, e) {
9499
+ return E.walkTokens(l, e);
9500
+ };
9501
+ k.parseInline = E.parseInline;
9502
+ k.Parser = T;
9503
+ k.parser = T.parse;
9504
+ k.Renderer = S;
9505
+ k.TextRenderer = z;
9506
+ k.Lexer = R;
9507
+ k.lexer = R.lex;
9508
+ k.Tokenizer = P;
9509
+ k.Hooks = _;
9510
+ k.parse = k;
9511
+ k.options;
9512
+ k.setOptions;
9513
+ k.walkTokens;
9514
+ k.parseInline;
9515
+ T.parse;
9516
+ R.lex;
9517
+ //#endregion
9518
+ //#region viewer/src/components/markdown.tsx
9519
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set([
9520
+ "http:",
9521
+ "https:",
9522
+ "mailto:"
9523
+ ]);
9524
+ /** http, https, mailto or relative. Browsers drop whitespace and control characters inside a scheme, so do we. */
9525
+ function isSafeHref(href) {
9526
+ const bare = href.replace(/[\u0000- \u007f]/g, "");
9527
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(bare)?.[1];
9528
+ return !scheme || SAFE_SCHEMES.has(`${scheme.toLowerCase()}:`);
9529
+ }
9530
+ function escapeHtml(text) {
9531
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
9532
+ }
9533
+ var markdown = new F({
9534
+ gfm: true,
9535
+ breaks: false
9536
+ }, { renderer: {
9537
+ html: ({ text }) => escapeHtml(text),
9538
+ link(token) {
9539
+ return isSafeHref(token.href) ? false : this.parser.parseInline(token.tokens);
9540
+ },
9541
+ image(token) {
9542
+ return isSafeHref(token.href) ? false : escapeHtml(token.text);
9543
+ }
9544
+ } });
9545
+ function renderMarkdown(text) {
9546
+ return markdown.parse(text, { async: false }).replace(/<a href=/g, "<a target=\"_blank\" rel=\"noreferrer\" href=");
9547
+ }
9548
+ /** Markdown from the project's own files (guidelines, notes, descriptions). */
9549
+ function Markdown({ text, className }) {
9550
+ const html = useMemo(() => renderMarkdown(text), [text]);
9551
+ return /* @__PURE__ */ jsx("div", {
9552
+ className: cn("prose", className),
9553
+ dangerouslySetInnerHTML: { __html: html }
9554
+ });
9555
+ }
9556
+ //#endregion
9557
+ //#region viewer/src/lib/frames.ts
9558
+ /** Routes runtime messages to whoever listens to the frame that sent them. */
9559
+ var handlers = /* @__PURE__ */ new Map();
9560
+ window.addEventListener("message", (event) => {
9561
+ const data = event.data;
9562
+ if (data?.source !== "design-runtime" || !event.source) return;
9563
+ for (const handler of handlers.get(event.source) ?? []) handler(data);
9564
+ });
9565
+ function listenToFrame(frame, handler) {
9566
+ let win = null;
9567
+ const detach = () => {
9568
+ if (!win) return;
9569
+ const set = handlers.get(win);
9570
+ set?.delete(handler);
9571
+ if (!set?.size) handlers.delete(win);
9572
+ win = null;
9573
+ };
9574
+ const attach = () => {
9575
+ detach();
9576
+ win = frame.contentWindow;
9577
+ if (!win) return;
9578
+ const set = handlers.get(win) ?? /* @__PURE__ */ new Set();
9579
+ set.add(handler);
9580
+ handlers.set(win, set);
9581
+ };
9582
+ attach();
9583
+ frame.addEventListener("load", attach);
9584
+ return () => {
9585
+ frame.removeEventListener("load", attach);
9586
+ detach();
9587
+ };
9588
+ }
9589
+ function sendToFrame(frame, message) {
9590
+ frame?.contentWindow?.postMessage({
9591
+ source: "design-viewer",
9592
+ ...message
9593
+ }, "*");
9594
+ }
9595
+ /** Tell a frame it sits on the canvas, so it hands the wheel to the viewer. */
9596
+ function claimWheel(frame) {
9597
+ sendToFrame(frame, { type: "canvas" });
9598
+ }
9599
+ /**
9600
+ * A wheel the frame had no use for (it reached its edge, or holds nothing that scrolls): scroll
9601
+ * whatever scrolls around the frame, as the browser would if the frame's edges passed it on.
9602
+ */
9603
+ function scrollAround(frame, delta) {
9604
+ for (let node = frame.parentElement; node; node = node.parentElement) {
9605
+ const style = getComputedStyle(node);
9606
+ const scrolls = (overflow) => /(auto|scroll|overlay)/.test(overflow);
9607
+ const y = scrolls(style.overflowY) && node.scrollHeight > node.clientHeight;
9608
+ const x = scrolls(style.overflowX) && node.scrollWidth > node.clientWidth;
9609
+ if (y || x) {
9610
+ node.scrollBy({
9611
+ left: x ? delta.deltaX : 0,
9612
+ top: y ? delta.deltaY : 0,
9613
+ behavior: "instant"
9614
+ });
9615
+ return;
9616
+ }
11149
9617
  }
11150
- emStrongMask(e) {
11151
- return e;
9618
+ window.scrollBy({
9619
+ left: delta.deltaX,
9620
+ top: delta.deltaY,
9621
+ behavior: "instant"
9622
+ });
9623
+ }
9624
+ function sendTheme(frame, theme) {
9625
+ sendToFrame(frame, {
9626
+ type: "theme",
9627
+ theme
9628
+ });
9629
+ }
9630
+ /** Load the frame again; works whatever origin it is on. */
9631
+ function reloadFrame(frame) {
9632
+ if (!frame) return;
9633
+ frame.src = frame.src;
9634
+ }
9635
+ /**
9636
+ * Where the dev server's frames load from. Browsers give each site its own
9637
+ * process, so frames served from the viewer's twin host (localhost ↔ 127.0.0.1,
9638
+ * both reach the same server) run their scripts and animations off the viewer's
9639
+ * thread: a canvas full of live screens still pans smoothly. Any other host,
9640
+ * and static builds, keep frames on the viewer's origin.
9641
+ */
9642
+ var FRAME_ORIGIN = (() => {
9643
+ if (STATIC_SITE) return "";
9644
+ const { protocol, hostname, port } = window.location;
9645
+ const twin = hostname === "localhost" ? "127.0.0.1" : hostname === "127.0.0.1" ? "localhost" : null;
9646
+ return twin ? `${protocol}//${twin}${port ? `:${port}` : ""}` : "";
9647
+ })();
9648
+ /** A frame URL with the theme it should open in. */
9649
+ function frameSrc(url, theme, extra) {
9650
+ const [path = "", query = ""] = url.split("?");
9651
+ const params = new URLSearchParams(query);
9652
+ params.set("theme", theme);
9653
+ for (const [key, value] of Object.entries(extra ?? {})) params.set(key, value);
9654
+ return `${path.startsWith("/") ? FRAME_ORIGIN : ""}${path}?${params.toString()}`;
9655
+ }
9656
+ /** Absolute URL for opening a frame in a new tab. */
9657
+ function absoluteUrl(url) {
9658
+ return new URL(url, document.baseURI).toString();
9659
+ }
9660
+ /**
9661
+ * A link, form or history push the runtime kept from leaving its frame. The path goes to the
9662
+ * screen whose `route` matches it best (a relative `href` resolves against the route of the
9663
+ * screen it is in); without routes, a path whose last part names a screen still opens it. A
9664
+ * link to another site opens in a new tab. A push nobody's route matches belongs to the
9665
+ * screen's own router; a link or form that leads nowhere stays put with a note.
9666
+ */
9667
+ function followLink(message, canvas, go, from) {
9668
+ if (message.form) {
9669
+ toast("Forms are not sent from a screen", void 0, "info");
9670
+ return;
11152
9671
  }
11153
- provideLexer(e = this.block) {
11154
- return e ? R.lex : R.lexInline;
9672
+ if (message.path === null) {
9673
+ if (!isSafeHref(message.href) || !/^(https?|mailto):/i.test(message.href.trim())) {
9674
+ toast("Link not opened", message.href.slice(0, 200), "info");
9675
+ return;
9676
+ }
9677
+ if (!window.open(message.href, "_blank", "noopener,noreferrer")) toast("Link to another site", message.href, "info");
9678
+ return;
11155
9679
  }
11156
- provideParser(e = this.block) {
11157
- return e ? T.parse : T.parseInline;
9680
+ const frames = canvas.pages.flatMap((page) => page.sections.flatMap((section) => section.items.filter((item) => item.kind === "screen" || item.kind === "url")));
9681
+ const here = frames.find((item) => item.id === from);
9682
+ const path = message.raw === void 0 ? message.path : linkPath(message.raw, here?.routes?.[0]);
9683
+ if (path === null) return;
9684
+ const routed = matchRoute(frames.map((item) => ({
9685
+ routes: (item.routes ?? []).map(compileRoute),
9686
+ value: item.id
9687
+ })), path);
9688
+ if (routed) {
9689
+ if (routed !== from) go(routed);
9690
+ return;
11158
9691
  }
9692
+ const bare = path.split(/[?#]/)[0].replace(/\/+$/, "");
9693
+ let name = bare.slice(bare.lastIndexOf("/") + 1).replace(/\.html?$/, "");
9694
+ try {
9695
+ name = decodeURIComponent(name);
9696
+ } catch {}
9697
+ if (name && name !== from && frames.some((item) => item.id === name)) go(name);
9698
+ else if (!message.pushed) toast("No screen for this link", path, "info");
9699
+ }
9700
+ //#endregion
9701
+ //#region viewer/src/components/inspector.tsx
9702
+ /**
9703
+ * Inspect frames: while `enabled`, every frame in `frames` highlights on hover
9704
+ * and picks on click. The inspecting runs inside each frame (see the runtime's
9705
+ * inspect.ts); this side only switches it and shows what the frames report.
9706
+ * The pick outlives `enabled` (so a quick ⌘-hold leaves the panel open) until
9707
+ * it is cleared.
9708
+ */
9709
+ function useInspection(frames, enabled, options) {
9710
+ const project = useProject().data;
9711
+ const tokens = useSystem(!!project?.system).data?.tokens;
9712
+ const [selected, setSelected] = useState(null);
9713
+ const state = useRef({
9714
+ enabled,
9715
+ tokens
9716
+ });
9717
+ state.current = {
9718
+ enabled,
9719
+ tokens
9720
+ };
9721
+ const onEscape = useRef(options.onEscape);
9722
+ onEscape.current = options.onEscape;
9723
+ const framesRef = useRef(frames);
9724
+ framesRef.current = frames;
9725
+ const told = useRef(/* @__PURE__ */ new Map());
9726
+ const tell = (frame, force = false) => {
9727
+ const { enabled: on, tokens: list } = state.current;
9728
+ const last = told.current.get(frame);
9729
+ if (!force && last?.on === on && last.tokens === list) return;
9730
+ told.current.set(frame, {
9731
+ on,
9732
+ tokens: list
9733
+ });
9734
+ sendToFrame(frame, {
9735
+ type: "inspect",
9736
+ on,
9737
+ tokens: on ? list ?? [] : void 0
9738
+ });
9739
+ };
9740
+ useEffect(() => {
9741
+ for (const frame of frames) tell(frame);
9742
+ for (const frame of told.current.keys()) if (!frames.includes(frame)) told.current.delete(frame);
9743
+ }, [
9744
+ frames,
9745
+ enabled,
9746
+ tokens
9747
+ ]);
9748
+ useEffect(() => {
9749
+ const offs = frames.map((frame) => listenToFrame(frame, (message) => {
9750
+ if (message.type === "ready") {
9751
+ if (state.current.enabled) tell(frame, true);
9752
+ else told.current.set(frame, { on: false });
9753
+ } else if (message.type === "inspect") {
9754
+ if (message.info) {
9755
+ for (const other of framesRef.current) if (other !== frame) sendToFrame(other, {
9756
+ type: "inspect-select",
9757
+ ref: null
9758
+ });
9759
+ setSelected({
9760
+ info: message.info,
9761
+ frame
9762
+ });
9763
+ } else setSelected((current) => current?.frame === frame ? null : current);
9764
+ } else if (message.type === "inspect-escape") onEscape.current();
9765
+ }));
9766
+ return () => {
9767
+ for (const off of offs) off();
9768
+ };
9769
+ }, [frames]);
9770
+ const frame = selected && frames.includes(selected.frame) ? selected.frame : null;
9771
+ return {
9772
+ info: frame ? selected.info : null,
9773
+ frame,
9774
+ /** Pick another element in the same frame by its ref, or clear the pick. */
9775
+ select(ref) {
9776
+ if (ref === null) {
9777
+ for (const each of framesRef.current) sendToFrame(each, {
9778
+ type: "inspect-select",
9779
+ ref: null
9780
+ });
9781
+ setSelected(null);
9782
+ } else if (selected) sendToFrame(selected.frame, {
9783
+ type: "inspect-select",
9784
+ ref
9785
+ });
9786
+ }
9787
+ };
9788
+ }
9789
+ /**
9790
+ * True while ⌘ or Ctrl is held on its own, in the viewer or inside any of
9791
+ * `frames`. A modifier used for a shortcut (⌘K, ⌘C) does not count.
9792
+ */
9793
+ function useModifierHold(frames) {
9794
+ const [held, setHeld] = useState(false);
9795
+ useEffect(() => {
9796
+ const isModifier = (key) => key === "Meta" || key === "Control";
9797
+ let down = false;
9798
+ let combo = false;
9799
+ let timer;
9800
+ const release = () => {
9801
+ down = false;
9802
+ clearTimeout(timer);
9803
+ setHeld(false);
9804
+ };
9805
+ const press = (key) => {
9806
+ if (isModifier(key)) {
9807
+ if (down) return;
9808
+ down = true;
9809
+ combo = false;
9810
+ timer = setTimeout(() => {
9811
+ if (down && !combo) setHeld(true);
9812
+ }, 150);
9813
+ } else if (down) {
9814
+ combo = true;
9815
+ clearTimeout(timer);
9816
+ setHeld(false);
9817
+ }
9818
+ };
9819
+ const onDown = (event) => press(event.key);
9820
+ const onUp = (event) => {
9821
+ if (isModifier(event.key)) release();
9822
+ };
9823
+ window.addEventListener("keydown", onDown, true);
9824
+ window.addEventListener("keyup", onUp, true);
9825
+ window.addEventListener("blur", release);
9826
+ const offs = frames.map((frame) => listenToFrame(frame, (message) => {
9827
+ if (message.type === "key") {
9828
+ if (message.down) press(message.name);
9829
+ else if (isModifier(message.name)) release();
9830
+ } else if (message.type === "blur") release();
9831
+ }));
9832
+ return () => {
9833
+ window.removeEventListener("keydown", onDown, true);
9834
+ window.removeEventListener("keyup", onUp, true);
9835
+ window.removeEventListener("blur", release);
9836
+ for (const off of offs) off();
9837
+ clearTimeout(timer);
9838
+ };
9839
+ }, [frames]);
9840
+ return held;
9841
+ }
9842
+ function Section({ title, children, right }) {
9843
+ return /* @__PURE__ */ jsxs("section", {
9844
+ className: "border-t border-rule pb-2 first:border-t-0",
9845
+ children: [/* @__PURE__ */ jsxs("div", {
9846
+ className: "flex h-9 items-center justify-between px-4 text-[12px] font-medium text-muted",
9847
+ children: [title, right]
9848
+ }), children]
9849
+ });
9850
+ }
9851
+ function Line({ label, children }) {
9852
+ return /* @__PURE__ */ jsxs("div", {
9853
+ className: "grid min-h-7 grid-cols-[92px_minmax(0,1fr)] items-center gap-x-3 px-4 py-0.5 text-[12.5px]",
9854
+ children: [/* @__PURE__ */ jsx("span", {
9855
+ className: "text-muted",
9856
+ children: label
9857
+ }), /* @__PURE__ */ jsx("div", {
9858
+ className: "flex min-w-0 items-center gap-1.5 text-ink",
9859
+ children
9860
+ })]
9861
+ });
9862
+ }
9863
+ function TokenChip({ name }) {
9864
+ if (!name) return null;
9865
+ return /* @__PURE__ */ jsx("button", {
9866
+ type: "button",
9867
+ onClick: () => void copyText(`var(${name})`, "Token copied"),
9868
+ title: "Copy var()",
9869
+ className: "shrink-0 cursor-pointer truncate rounded-[4px] border-0 bg-action-soft px-1.5 py-px font-mono text-[11px] text-action",
9870
+ children: name
9871
+ });
9872
+ }
9873
+ function Value$1({ children, title }) {
9874
+ return /* @__PURE__ */ jsx("span", {
9875
+ className: "min-w-0 truncate font-mono text-[12px]",
9876
+ title,
9877
+ children
9878
+ });
9879
+ }
9880
+ function ColorLine({ label, color }) {
9881
+ return /* @__PURE__ */ jsxs(Line, {
9882
+ label,
9883
+ children: [
9884
+ /* @__PURE__ */ jsx("span", {
9885
+ className: "size-3.5 shrink-0 rounded-[3px] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.12)]",
9886
+ style: { background: color.css }
9887
+ }),
9888
+ /* @__PURE__ */ jsx(Value$1, {
9889
+ title: color.css,
9890
+ children: color.value
9891
+ }),
9892
+ /* @__PURE__ */ jsx(TokenChip, { name: color.token })
9893
+ ]
9894
+ });
9895
+ }
9896
+ var dash = (value) => value ? String(value) : "–";
9897
+ function Layer({ label, values, className, children }) {
9898
+ const [top, right, bottom, left] = values;
9899
+ return /* @__PURE__ */ jsxs("div", {
9900
+ className: cn("relative grid grid-cols-[28px_minmax(0,1fr)_28px] grid-rows-[20px_auto_20px] rounded-[4px]", className),
9901
+ children: [
9902
+ /* @__PURE__ */ jsx("span", {
9903
+ className: "absolute top-1 left-1.5 text-[9.5px] tracking-wide text-muted uppercase",
9904
+ children: label
9905
+ }),
9906
+ /* @__PURE__ */ jsx("span", {
9907
+ className: "col-start-2 self-center text-center",
9908
+ children: dash(top)
9909
+ }),
9910
+ /* @__PURE__ */ jsx("span", {
9911
+ className: "row-start-2 self-center text-center",
9912
+ children: dash(left)
9913
+ }),
9914
+ /* @__PURE__ */ jsx("div", {
9915
+ className: "row-start-2 col-start-2",
9916
+ children
9917
+ }),
9918
+ /* @__PURE__ */ jsx("span", {
9919
+ className: "row-start-2 col-start-3 self-center text-center",
9920
+ children: dash(right)
9921
+ }),
9922
+ /* @__PURE__ */ jsx("span", {
9923
+ className: "row-start-3 col-start-2 self-center text-center",
9924
+ children: dash(bottom)
9925
+ })
9926
+ ]
9927
+ });
9928
+ }
9929
+ function BoxModel({ info }) {
9930
+ const content = {
9931
+ w: Math.max(0, info.rect.w - info.border[1] - info.border[3] - info.padding[1] - info.padding[3]),
9932
+ h: Math.max(0, info.rect.h - info.border[0] - info.border[2] - info.padding[0] - info.padding[2])
9933
+ };
9934
+ return /* @__PURE__ */ jsx("div", {
9935
+ className: "px-4 pt-1 pb-2 font-mono text-[11px] text-ink2",
9936
+ children: /* @__PURE__ */ jsx(Layer, {
9937
+ label: "margin",
9938
+ values: info.margin,
9939
+ className: "border border-dashed border-rule-strong bg-[#f6b26b]/15",
9940
+ children: /* @__PURE__ */ jsx(Layer, {
9941
+ label: "border",
9942
+ values: info.border,
9943
+ className: "border border-rule-strong bg-[#f9e27d]/15",
9944
+ children: /* @__PURE__ */ jsx(Layer, {
9945
+ label: "padding",
9946
+ values: info.padding,
9947
+ className: "border border-rule-strong bg-[#93c47d]/20",
9948
+ children: /* @__PURE__ */ jsxs("div", {
9949
+ className: "flex h-8 items-center justify-center rounded-[3px] border border-rule-strong bg-[#6fa8dc]/25 text-ink",
9950
+ children: [
9951
+ Math.round(content.w * 100) / 100,
9952
+ " × ",
9953
+ Math.round(content.h * 100) / 100
9954
+ ]
9955
+ })
9956
+ })
9957
+ })
9958
+ })
9959
+ });
9960
+ }
9961
+ function Crumbs({ info, onSelect }) {
9962
+ return /* @__PURE__ */ jsxs("div", {
9963
+ className: "flex flex-wrap items-center gap-1 px-4 pb-2",
9964
+ children: [info.path.slice(-4).map((crumb, index) => /* @__PURE__ */ jsxs("span", {
9965
+ className: "flex items-center gap-1",
9966
+ children: [/* @__PURE__ */ jsx("button", {
9967
+ type: "button",
9968
+ onClick: () => onSelect(crumb.ref),
9969
+ className: "cursor-pointer rounded-[4px] border-0 bg-soft2 px-1.5 py-0.5 font-mono text-[11px] text-ink2 hover:text-ink",
9970
+ children: crumb.label
9971
+ }), /* @__PURE__ */ jsx(Icon, {
9972
+ name: "chevron-right",
9973
+ size: 11,
9974
+ className: "text-muted"
9975
+ })]
9976
+ }, index)), /* @__PURE__ */ jsx("span", {
9977
+ className: "rounded-[4px] bg-action-soft px-1.5 py-0.5 font-mono text-[11px] text-action",
9978
+ children: info.label
9979
+ })]
9980
+ });
9981
+ }
9982
+ /** The right sidebar: what the selected element is and how it is styled. */
9983
+ function InspectorPanel({ info, onSelect, onClose, active }) {
9984
+ return /* @__PURE__ */ jsxs("aside", {
9985
+ "data-ui": true,
9986
+ className: "flex w-[320px] shrink-0 flex-col overflow-hidden border-l border-rule bg-surface",
9987
+ children: [
9988
+ /* @__PURE__ */ jsxs("div", {
9989
+ className: "flex h-[46px] shrink-0 items-center justify-between border-b border-rule pr-2 pl-4",
9990
+ children: [/* @__PURE__ */ jsxs("span", {
9991
+ className: "flex items-center gap-2 text-[14px] font-semibold",
9992
+ children: [/* @__PURE__ */ jsx(Icon, {
9993
+ name: "pointer",
9994
+ size: 15,
9995
+ className: "text-action"
9996
+ }), " Inspect"]
9997
+ }), /* @__PURE__ */ jsx(IconButton, {
9998
+ icon: "x",
9999
+ label: "Close inspector (I)",
10000
+ onClick: onClose
10001
+ })]
10002
+ }),
10003
+ !info ? /* @__PURE__ */ jsxs("div", {
10004
+ className: "flex flex-col items-center gap-2 px-6 py-10 text-center",
10005
+ children: [
10006
+ /* @__PURE__ */ jsx(Icon, {
10007
+ name: "pointer",
10008
+ size: 22,
10009
+ className: "text-muted"
10010
+ }),
10011
+ /* @__PURE__ */ jsx("span", {
10012
+ className: "text-[13.5px] text-ink2",
10013
+ children: active ? "Click an element in any screen." : `Hold ${MOD_KEY} to highlight, click to inspect.`
10014
+ }),
10015
+ /* @__PURE__ */ jsx("span", {
10016
+ className: "text-[12.5px] leading-[1.5] text-muted",
10017
+ children: "Hover shows margin, padding and content. Clicks go to the inspector, not the screen."
10018
+ }),
10019
+ /* @__PURE__ */ jsxs("span", {
10020
+ className: "mt-2 flex items-center gap-1.5 text-[12px] text-muted",
10021
+ children: [
10022
+ /* @__PURE__ */ jsx(Kbd, { children: "I" }),
10023
+ " toggle · hold ",
10024
+ /* @__PURE__ */ jsx(Kbd, { children: MOD_KEY }),
10025
+ " · ",
10026
+ /* @__PURE__ */ jsx(Kbd, { children: "esc" }),
10027
+ " clear"
10028
+ ]
10029
+ })
10030
+ ]
10031
+ }) : /* @__PURE__ */ jsxs("div", {
10032
+ className: "min-h-0 grow overflow-y-auto",
10033
+ children: [
10034
+ /* @__PURE__ */ jsxs(Section, {
10035
+ title: "Element",
10036
+ children: [/* @__PURE__ */ jsxs("div", {
10037
+ className: "flex flex-col gap-1 px-4 pb-2",
10038
+ children: [
10039
+ /* @__PURE__ */ jsx("div", {
10040
+ className: "flex min-w-0 items-baseline gap-1.5",
10041
+ children: info.component?.owners[0] ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
10042
+ className: "truncate font-mono text-[14px] font-medium text-ink",
10043
+ children: [
10044
+ "<",
10045
+ info.component.owners[0],
10046
+ ">"
10047
+ ]
10048
+ }), /* @__PURE__ */ jsx("span", {
10049
+ className: "truncate font-mono text-[12px] text-muted",
10050
+ children: info.tag
10051
+ })] }) : /* @__PURE__ */ jsx("span", {
10052
+ className: "truncate font-mono text-[14px] font-medium text-ink",
10053
+ children: info.label
10054
+ })
10055
+ }),
10056
+ info.component && info.component.owners.length > 1 ? /* @__PURE__ */ jsxs("span", {
10057
+ className: "truncate text-[12px] text-muted",
10058
+ children: ["in ", info.component.owners.slice(1).join(" ← ")]
10059
+ }) : null,
10060
+ info.component?.file ? /* @__PURE__ */ jsx("button", {
10061
+ type: "button",
10062
+ onClick: () => void copyText(info.component.file, "Path copied"),
10063
+ className: "w-fit max-w-full cursor-pointer truncate border-0 bg-transparent p-0 text-left font-mono text-[11.5px] text-muted hover:text-ink2",
10064
+ title: "Copy path",
10065
+ children: info.component.file
10066
+ }) : null,
10067
+ info.text ? /* @__PURE__ */ jsxs("span", {
10068
+ className: "line-clamp-2 text-[12.5px] text-ink2",
10069
+ children: [
10070
+ "“",
10071
+ info.text,
10072
+ "”"
10073
+ ]
10074
+ }) : null
10075
+ ]
10076
+ }), /* @__PURE__ */ jsx(Crumbs, {
10077
+ info,
10078
+ onSelect
10079
+ })]
10080
+ }),
10081
+ /* @__PURE__ */ jsxs(Section, {
10082
+ title: "Box",
10083
+ children: [
10084
+ /* @__PURE__ */ jsx(BoxModel, { info }),
10085
+ /* @__PURE__ */ jsx(Line, {
10086
+ label: "Size",
10087
+ children: /* @__PURE__ */ jsxs(Value$1, { children: [
10088
+ info.rect.w,
10089
+ " × ",
10090
+ info.rect.h
10091
+ ] })
10092
+ }),
10093
+ /* @__PURE__ */ jsx(Line, {
10094
+ label: "Position",
10095
+ children: /* @__PURE__ */ jsxs(Value$1, { children: [
10096
+ "x ",
10097
+ info.rect.x,
10098
+ " · y ",
10099
+ info.rect.y
10100
+ ] })
10101
+ })
10102
+ ]
10103
+ }),
10104
+ info.layout.length ? /* @__PURE__ */ jsx(Section, {
10105
+ title: "Layout",
10106
+ children: info.layout.map(([label, value]) => /* @__PURE__ */ jsx(Line, {
10107
+ label,
10108
+ children: /* @__PURE__ */ jsx(Value$1, {
10109
+ title: value,
10110
+ children: value
10111
+ })
10112
+ }, label))
10113
+ }) : null,
10114
+ info.typography ? /* @__PURE__ */ jsxs(Section, {
10115
+ title: "Typography",
10116
+ children: [
10117
+ /* @__PURE__ */ jsxs(Line, {
10118
+ label: "Font",
10119
+ children: [/* @__PURE__ */ jsx(Value$1, {
10120
+ title: info.typography.family,
10121
+ children: info.typography.family.split(",")[0]?.replace(/["']/g, "")
10122
+ }), /* @__PURE__ */ jsx(TokenChip, { name: info.typography.familyToken })]
10123
+ }),
10124
+ /* @__PURE__ */ jsxs(Line, {
10125
+ label: "Size",
10126
+ children: [/* @__PURE__ */ jsxs(Value$1, { children: [
10127
+ info.typography.size,
10128
+ " / ",
10129
+ info.typography.lineHeight
10130
+ ] }), /* @__PURE__ */ jsx(TokenChip, { name: info.typography.sizeToken })]
10131
+ }),
10132
+ /* @__PURE__ */ jsx(Line, {
10133
+ label: "Weight",
10134
+ children: /* @__PURE__ */ jsx(Value$1, { children: info.typography.weight })
10135
+ }),
10136
+ info.typography.letterSpacing !== "normal" ? /* @__PURE__ */ jsx(Line, {
10137
+ label: "Tracking",
10138
+ children: /* @__PURE__ */ jsx(Value$1, { children: info.typography.letterSpacing })
10139
+ }) : null,
10140
+ /* @__PURE__ */ jsx(ColorLine, {
10141
+ label: "Color",
10142
+ color: info.typography.color
10143
+ }),
10144
+ info.typography.align !== "start" ? /* @__PURE__ */ jsx(Line, {
10145
+ label: "Align",
10146
+ children: /* @__PURE__ */ jsx(Value$1, { children: info.typography.align })
10147
+ }) : null,
10148
+ info.typography.transform ? /* @__PURE__ */ jsx(Line, {
10149
+ label: "Transform",
10150
+ children: /* @__PURE__ */ jsx(Value$1, { children: info.typography.transform })
10151
+ }) : null
10152
+ ]
10153
+ }) : null,
10154
+ Object.values(info.appearance).some(Boolean) ? /* @__PURE__ */ jsxs(Section, {
10155
+ title: "Appearance",
10156
+ children: [
10157
+ info.appearance.background ? /* @__PURE__ */ jsx(ColorLine, {
10158
+ label: "Background",
10159
+ color: info.appearance.background
10160
+ }) : null,
10161
+ info.appearance.backgroundImage ? /* @__PURE__ */ jsx(Line, {
10162
+ label: "Image",
10163
+ children: /* @__PURE__ */ jsx(Value$1, {
10164
+ title: info.appearance.backgroundImage,
10165
+ children: info.appearance.backgroundImage
10166
+ })
10167
+ }) : null,
10168
+ info.appearance.border ? /* @__PURE__ */ jsxs(Line, {
10169
+ label: "Border",
10170
+ children: [
10171
+ /* @__PURE__ */ jsx("span", {
10172
+ className: "size-3.5 shrink-0 rounded-[3px] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.12)]",
10173
+ style: { background: info.appearance.border.color.css }
10174
+ }),
10175
+ /* @__PURE__ */ jsxs(Value$1, { children: [
10176
+ info.appearance.border.width,
10177
+ " ",
10178
+ info.appearance.border.style,
10179
+ " ",
10180
+ info.appearance.border.color.value
10181
+ ] }),
10182
+ /* @__PURE__ */ jsx(TokenChip, { name: info.appearance.border.color.token })
10183
+ ]
10184
+ }) : null,
10185
+ info.appearance.radius ? /* @__PURE__ */ jsxs(Line, {
10186
+ label: "Radius",
10187
+ children: [/* @__PURE__ */ jsx(Value$1, { children: info.appearance.radius.value }), /* @__PURE__ */ jsx(TokenChip, { name: info.appearance.radius.token })]
10188
+ }) : null,
10189
+ info.appearance.shadow ? /* @__PURE__ */ jsxs(Line, {
10190
+ label: "Shadow",
10191
+ children: [/* @__PURE__ */ jsx(Value$1, {
10192
+ title: info.appearance.shadow.value,
10193
+ children: info.appearance.shadow.value
10194
+ }), /* @__PURE__ */ jsx(TokenChip, { name: info.appearance.shadow.token })]
10195
+ }) : null,
10196
+ info.appearance.opacity ? /* @__PURE__ */ jsx(Line, {
10197
+ label: "Opacity",
10198
+ children: /* @__PURE__ */ jsx(Value$1, { children: info.appearance.opacity })
10199
+ }) : null
10200
+ ]
10201
+ }) : null,
10202
+ info.classes.length ? /* @__PURE__ */ jsx(Section, {
10203
+ title: "Classes",
10204
+ right: /* @__PURE__ */ jsx("button", {
10205
+ type: "button",
10206
+ onClick: () => void copyText(info.classes.join(" "), "Classes copied"),
10207
+ className: "cursor-pointer border-0 bg-transparent text-[12px] text-muted hover:text-ink2",
10208
+ children: "Copy"
10209
+ }),
10210
+ children: /* @__PURE__ */ jsx("div", {
10211
+ className: "flex flex-wrap gap-1 px-4 pb-1",
10212
+ children: info.classes.map((name) => /* @__PURE__ */ jsx("span", {
10213
+ className: "rounded-[4px] bg-soft2 px-1.5 py-0.5 font-mono text-[11px] text-ink2",
10214
+ children: name
10215
+ }, name))
10216
+ })
10217
+ }) : null,
10218
+ info.attributes.length ? /* @__PURE__ */ jsx(Section, {
10219
+ title: "Attributes",
10220
+ children: info.attributes.map(([name, value]) => /* @__PURE__ */ jsx(Line, {
10221
+ label: name,
10222
+ children: /* @__PURE__ */ jsx(Value$1, {
10223
+ title: value,
10224
+ children: value
10225
+ })
10226
+ }, name))
10227
+ }) : null,
10228
+ info.children.length ? /* @__PURE__ */ jsx(Section, {
10229
+ title: `Children · ${info.children.length}`,
10230
+ children: /* @__PURE__ */ jsx("div", {
10231
+ className: "flex flex-col px-2 pb-1",
10232
+ children: info.children.map((child, index) => /* @__PURE__ */ jsxs("button", {
10233
+ type: "button",
10234
+ onClick: () => onSelect(child.ref),
10235
+ className: "flex h-7 cursor-pointer items-center gap-2 rounded-[6px] border-0 bg-transparent px-2 text-left font-mono text-[12px] text-ink2 hover:bg-soft2",
10236
+ children: [/* @__PURE__ */ jsx(Icon, {
10237
+ name: "chevron-right",
10238
+ size: 12,
10239
+ className: "text-muted"
10240
+ }), /* @__PURE__ */ jsx("span", {
10241
+ className: "truncate",
10242
+ children: child.label
10243
+ })]
10244
+ }, index))
10245
+ })
10246
+ }) : null
10247
+ ]
10248
+ }),
10249
+ info ? /* @__PURE__ */ jsx("div", {
10250
+ className: "flex shrink-0 items-center gap-2 border-t border-rule bg-soft px-4 py-2.5",
10251
+ children: /* @__PURE__ */ jsx(Button, {
10252
+ icon: "copy",
10253
+ onClick: () => void copyText(info.css, "CSS copied"),
10254
+ className: "grow",
10255
+ children: "Copy CSS"
10256
+ })
10257
+ }) : null
10258
+ ]
10259
+ });
10260
+ }
10261
+ //#endregion
10262
+ //#region viewer/src/ui/badge.tsx
10263
+ var BADGE = {
10264
+ neutral: "bg-soft2 text-ink2",
10265
+ action: "bg-action-soft text-action",
10266
+ ok: "bg-ok-soft text-ok-text",
10267
+ danger: "bg-danger-soft text-danger-text"
11159
10268
  };
11160
- var F = class {
11161
- defaults = I();
11162
- options = this.setOptions;
11163
- parse = this.parseMarkdown(!0);
11164
- parseInline = this.parseMarkdown(!1);
11165
- Parser = T;
11166
- Renderer = S;
11167
- TextRenderer = z;
11168
- Lexer = R;
11169
- Tokenizer = P;
11170
- Hooks = _;
11171
- constructor(...e) {
11172
- this.use(...e);
11173
- }
11174
- walkTokens(e, t) {
11175
- let n = [];
11176
- for (let s of e) switch (n = n.concat(t.call(this, s)), s.type) {
11177
- case "table": {
11178
- let r = s;
11179
- for (let o of r.header) n = n.concat(this.walkTokens(o.tokens, t));
11180
- for (let o of r.rows) for (let i of o) n = n.concat(this.walkTokens(i.tokens, t));
11181
- break;
11182
- }
11183
- case "list": {
11184
- let r = s;
11185
- n = n.concat(this.walkTokens(r.items, t));
11186
- break;
11187
- }
11188
- default: {
11189
- let r = s;
11190
- this.defaults.extensions?.childTokens?.[r.type] ? this.defaults.extensions.childTokens[r.type].forEach((o) => {
11191
- let i = r[o].flat(1 / 0);
11192
- n = n.concat(this.walkTokens(i, t));
11193
- }) : r.tokens && (n = n.concat(this.walkTokens(r.tokens, t)));
11194
- }
11195
- }
11196
- return n;
10269
+ function Badge({ tone = "neutral", children, className }) {
10270
+ return /* @__PURE__ */ jsx("span", {
10271
+ className: cn("inline-flex h-[22px] shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] px-2 text-[12px] font-medium", BADGE[tone], className),
10272
+ children
10273
+ });
10274
+ }
10275
+ function Count({ value, tone = "neutral" }) {
10276
+ return /* @__PURE__ */ jsx("span", {
10277
+ className: cn("inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-[4px] px-[5px] text-[11.5px] font-semibold tabular", tone === "action" ? "bg-action-soft text-action" : tone === "danger" ? "bg-danger-soft text-danger-text" : "bg-soft2 text-ink2"),
10278
+ children: value
10279
+ });
10280
+ }
10281
+ //#endregion
10282
+ //#region viewer/src/ui/panel.tsx
10283
+ function Panel({ children, className, style }) {
10284
+ return /* @__PURE__ */ jsx("section", {
10285
+ className: cn("min-w-0 shrink-0 overflow-hidden rounded-[10px] border border-rule bg-surface", className),
10286
+ style,
10287
+ children
10288
+ });
10289
+ }
10290
+ function PanelHead({ title, count, sub, right, as: Heading = "h2" }) {
10291
+ return /* @__PURE__ */ jsxs("div", {
10292
+ className: "flex min-h-[46px] items-center justify-between gap-3 px-4",
10293
+ children: [/* @__PURE__ */ jsxs(Heading, {
10294
+ className: "m-0 flex min-w-0 items-center gap-2 whitespace-nowrap text-[14px] font-semibold",
10295
+ children: [
10296
+ title,
10297
+ count !== void 0 ? /* @__PURE__ */ jsx(Count, { value: count }) : null,
10298
+ sub ? /* @__PURE__ */ jsx("span", {
10299
+ className: "truncate text-[12.5px] font-normal text-muted",
10300
+ children: sub
10301
+ }) : null
10302
+ ]
10303
+ }), right ? /* @__PURE__ */ jsx("div", {
10304
+ className: "flex items-center gap-2",
10305
+ children: right
10306
+ }) : null]
10307
+ });
10308
+ }
10309
+ function PanelTabs({ items, value, onChange, right }) {
10310
+ const indicator = useId();
10311
+ return /* @__PURE__ */ jsxs("div", {
10312
+ className: "flex h-[46px] items-center justify-between gap-3 px-4",
10313
+ children: [/* @__PURE__ */ jsx("div", {
10314
+ role: "tablist",
10315
+ className: "flex gap-5",
10316
+ children: items.map((item) => {
10317
+ const on = item.value === value;
10318
+ return /* @__PURE__ */ jsxs("button", {
10319
+ type: "button",
10320
+ role: "tab",
10321
+ "aria-selected": on,
10322
+ onClick: () => onChange(item.value),
10323
+ className: cn("relative flex h-[46px] cursor-pointer items-center gap-1.5 border-0 bg-transparent px-0.5 text-[14px] transition-colors", on ? "font-semibold text-ink" : "font-medium text-ink2 hover:text-ink"),
10324
+ children: [
10325
+ item.label,
10326
+ item.count !== void 0 ? /* @__PURE__ */ jsx(Count, { value: item.count }) : null,
10327
+ on ? /* @__PURE__ */ jsx(motion.span, {
10328
+ layoutId: indicator,
10329
+ className: "absolute inset-x-0 bottom-0 h-[2px] bg-ink",
10330
+ transition: {
10331
+ type: "spring",
10332
+ stiffness: 520,
10333
+ damping: 42
10334
+ }
10335
+ }) : null
10336
+ ]
10337
+ }, item.value);
10338
+ })
10339
+ }), right ? /* @__PURE__ */ jsx("div", {
10340
+ className: "flex items-center gap-2",
10341
+ children: right
10342
+ }) : null]
10343
+ });
10344
+ }
10345
+ function Cell({ align, children }) {
10346
+ return /* @__PURE__ */ jsx("div", {
10347
+ className: cn("flex min-w-0 items-center gap-2", align === "r" && "justify-end text-right", align === "c" && "justify-center"),
10348
+ children
10349
+ });
10350
+ }
10351
+ function cells(children, align) {
10352
+ return Children.toArray(children).map((child, index) => /* @__PURE__ */ jsx(Cell, {
10353
+ align: align?.[index] ?? "l",
10354
+ children: child
10355
+ }, index));
10356
+ }
10357
+ function THead({ cols, align, labels }) {
10358
+ return /* @__PURE__ */ jsx("div", {
10359
+ className: "grid h-8 items-center gap-x-4 border-t border-rule bg-soft px-4 text-[12.5px] text-muted",
10360
+ style: { gridTemplateColumns: cols },
10361
+ children: labels.map((label, index) => /* @__PURE__ */ jsx(Cell, {
10362
+ align: align?.[index] ?? "l",
10363
+ children: /* @__PURE__ */ jsx("span", {
10364
+ className: "whitespace-nowrap",
10365
+ children: label
10366
+ })
10367
+ }, index))
10368
+ });
10369
+ }
10370
+ var rowClass = (first, interactive, className) => cn("grid items-center gap-x-4 border-rule", !first && "border-t", interactive && "cursor-pointer transition-colors hover:bg-soft", className);
10371
+ function Row({ cols, align, minH = 44, pad = "10px 16px", children, first, className, onClick, title }) {
10372
+ return /* @__PURE__ */ jsx("div", {
10373
+ className: rowClass(first, !!onClick, className),
10374
+ style: {
10375
+ gridTemplateColumns: cols,
10376
+ minHeight: minH,
10377
+ padding: pad
10378
+ },
10379
+ onClick,
10380
+ title,
10381
+ children: cells(children, align)
10382
+ });
10383
+ }
10384
+ /** A table row that navigates somewhere in the app. */
10385
+ function RowLink({ to, cols, align, minH = 44, pad = "10px 16px", first, className, children }) {
10386
+ return /* @__PURE__ */ jsx(Link, {
10387
+ href: to,
10388
+ className: cn(rowClass(first, true, className), "text-inherit no-underline"),
10389
+ style: {
10390
+ gridTemplateColumns: cols,
10391
+ minHeight: minH,
10392
+ padding: pad
10393
+ },
10394
+ children: cells(children, align)
10395
+ });
10396
+ }
10397
+ function KV({ label, children, labelWidth = 180, minH = 40, end = false }) {
10398
+ return /* @__PURE__ */ jsxs("div", {
10399
+ className: "grid content-center items-baseline gap-x-4 border-t border-rule px-4 py-2",
10400
+ style: {
10401
+ gridTemplateColumns: `min(${labelWidth}px, 42%) minmax(0, 1fr)`,
10402
+ minHeight: minH
10403
+ },
10404
+ children: [/* @__PURE__ */ jsx("span", {
10405
+ className: "text-[13px] text-muted",
10406
+ children: label
10407
+ }), /* @__PURE__ */ jsx("div", {
10408
+ className: cn("flex min-w-0 flex-wrap items-center gap-2", end && "justify-end"),
10409
+ children
10410
+ })]
10411
+ });
10412
+ }
10413
+ function PanelBody({ children, pad = "16px", gap = 16, top = true, className }) {
10414
+ return /* @__PURE__ */ jsx("div", {
10415
+ className: cn("flex flex-col", top && "border-t border-rule", className),
10416
+ style: {
10417
+ padding: pad,
10418
+ gap
10419
+ },
10420
+ children
10421
+ });
10422
+ }
10423
+ function PanelFoot({ left, right }) {
10424
+ return /* @__PURE__ */ jsxs("div", {
10425
+ className: "flex items-center justify-between gap-3 border-t border-rule bg-soft px-4 py-3",
10426
+ children: [/* @__PURE__ */ jsx("div", {
10427
+ className: "flex min-w-0 items-center gap-2 text-[12.5px] text-muted",
10428
+ children: left
10429
+ }), /* @__PURE__ */ jsx("div", {
10430
+ className: "flex items-center gap-2",
10431
+ children: right
10432
+ })]
10433
+ });
10434
+ }
10435
+ /** Bottom summary strip. */
10436
+ function Strip({ children, className }) {
10437
+ return /* @__PURE__ */ jsx("div", {
10438
+ className: cn("flex min-h-[38px] shrink-0 items-center gap-2 border-t border-rule bg-soft px-4 py-2 text-[12.5px] text-muted", className),
10439
+ children
10440
+ });
10441
+ }
10442
+ /** Group header row inside tables. */
10443
+ function GroupRow({ children, sub }) {
10444
+ return /* @__PURE__ */ jsxs("div", {
10445
+ className: "flex h-9 items-center gap-2.5 border-t border-rule bg-soft px-4 text-[13px] font-medium",
10446
+ children: [children, sub ? /* @__PURE__ */ jsx("span", {
10447
+ className: "text-[12.5px] font-normal text-muted",
10448
+ children: sub
10449
+ }) : null]
10450
+ });
10451
+ }
10452
+ //#endregion
10453
+ //#region viewer/src/components/issues.tsx
10454
+ function IssueList({ issues, className }) {
10455
+ return /* @__PURE__ */ jsx("div", {
10456
+ className: cn("flex flex-col", className),
10457
+ children: issues.map((issue, index) => /* @__PURE__ */ jsxs("div", {
10458
+ className: "flex items-start gap-2.5 border-t border-rule px-4 py-2.5 text-[13px] first:border-t-0",
10459
+ children: [/* @__PURE__ */ jsx(Icon, {
10460
+ name: issue.severity === "error" ? "alert" : "info",
10461
+ size: 15,
10462
+ className: cn("mt-0.5", issue.severity === "error" ? "text-danger-text" : "text-action")
10463
+ }), /* @__PURE__ */ jsxs("div", {
10464
+ className: "flex min-w-0 flex-col gap-0.5",
10465
+ children: [/* @__PURE__ */ jsx("span", {
10466
+ className: "text-ink",
10467
+ children: issue.message
10468
+ }), /* @__PURE__ */ jsxs("span", {
10469
+ className: "font-mono text-[12px] break-all text-muted",
10470
+ children: [issue.file, issue.at ? ` · ${issue.at}` : ""]
10471
+ })]
10472
+ })]
10473
+ }, index))
10474
+ });
10475
+ }
10476
+ /** Problems found in the project's files, as a panel. */
10477
+ function IssuesPanel({ issues, title = "Problems" }) {
10478
+ if (!issues.length) return null;
10479
+ const errors = issues.filter((issue) => issue.severity === "error").length;
10480
+ return /* @__PURE__ */ jsxs(Panel, {
10481
+ className: errors ? "border-danger/40" : void 0,
10482
+ children: [/* @__PURE__ */ jsx(PanelHead, {
10483
+ title,
10484
+ count: issues.length,
10485
+ sub: errors ? `${errors} blocking` : "warnings"
10486
+ }), /* @__PURE__ */ jsx("div", {
10487
+ className: "border-t border-rule",
10488
+ children: /* @__PURE__ */ jsx(IssueList, { issues })
10489
+ })]
10490
+ });
10491
+ }
10492
+ //#endregion
10493
+ //#region viewer/src/components/slide-panel.tsx
10494
+ var EASE = [
10495
+ .2,
10496
+ .7,
10497
+ .2,
10498
+ 1
10499
+ ];
10500
+ /**
10501
+ * A side panel that slides open and shut. The wrapper animates its width while
10502
+ * the content keeps its own, so nothing inside reflows mid-animation.
10503
+ */
10504
+ function SlidePanel({ open, side, width, children }) {
10505
+ return /* @__PURE__ */ jsx(AnimatePresence, {
10506
+ initial: false,
10507
+ children: open ? /* @__PURE__ */ jsx(motion.div, {
10508
+ initial: {
10509
+ width: 0,
10510
+ opacity: .6
10511
+ },
10512
+ animate: {
10513
+ width,
10514
+ opacity: 1
10515
+ },
10516
+ exit: {
10517
+ width: 0,
10518
+ opacity: .6
10519
+ },
10520
+ transition: {
10521
+ duration: .22,
10522
+ ease: EASE
10523
+ },
10524
+ className: cn("flex min-h-0 shrink-0 overflow-hidden", side === "right" ? "justify-start" : "justify-end"),
10525
+ children: /* @__PURE__ */ jsx("div", {
10526
+ className: "flex min-h-0 shrink-0",
10527
+ style: { width },
10528
+ children
10529
+ })
10530
+ }, "panel") : null
10531
+ });
10532
+ }
10533
+ //#endregion
10534
+ //#region viewer/src/ui/page.tsx
10535
+ /** Page body under the top bar. */
10536
+ function Content({ children, className, gap = 20, wide = false }) {
10537
+ return /* @__PURE__ */ jsx("main", {
10538
+ className: cn("mx-auto flex w-full min-h-0 grow flex-col px-8 pt-6 pb-7 animate-[q-fade-in_160ms_ease-out] max-md:px-4 max-md:pt-4", wide ? "max-w-[1440px]" : "max-w-[1200px]", className),
10539
+ style: { gap },
10540
+ children
10541
+ });
10542
+ }
10543
+ function PageHead({ title, sub, right, badge, crumbs }) {
10544
+ return /* @__PURE__ */ jsxs("div", {
10545
+ className: "flex shrink-0 items-end justify-between gap-6 max-md:flex-col max-md:items-stretch max-md:gap-3",
10546
+ children: [/* @__PURE__ */ jsxs("div", {
10547
+ className: "flex min-w-0 flex-col gap-1",
10548
+ children: [
10549
+ crumbs ? /* @__PURE__ */ jsx("div", {
10550
+ className: "flex items-center gap-1.5 text-[13px] text-muted",
10551
+ children: crumbs
10552
+ }) : null,
10553
+ /* @__PURE__ */ jsxs("div", {
10554
+ className: "flex items-center gap-3",
10555
+ children: [/* @__PURE__ */ jsx("h1", {
10556
+ className: "m-0 text-[22px] font-semibold tracking-[-0.01em]",
10557
+ children: title
10558
+ }), badge]
10559
+ }),
10560
+ sub ? /* @__PURE__ */ jsx("div", {
10561
+ className: "text-[13px] [overflow-wrap:anywhere] text-muted",
10562
+ children: sub
10563
+ }) : null
10564
+ ]
10565
+ }), right ? /* @__PURE__ */ jsx("div", {
10566
+ className: "flex flex-wrap items-center gap-2",
10567
+ children: right
10568
+ }) : null]
10569
+ });
10570
+ }
10571
+ function SubNav({ items, label }) {
10572
+ const [location] = useLocation();
10573
+ return /* @__PURE__ */ jsx("nav", {
10574
+ "aria-label": label,
10575
+ className: "flex flex-col gap-0.5",
10576
+ children: items.map((item) => {
10577
+ const active = location === item.to;
10578
+ return /* @__PURE__ */ jsxs(Link, {
10579
+ href: item.to,
10580
+ "aria-current": active ? "page" : void 0,
10581
+ className: cn("relative flex h-8 items-center gap-2 whitespace-nowrap rounded-[6px] px-3 text-[13.5px] no-underline transition-colors focus-visible:outline-offset-[-2px]", active ? "font-medium text-ink" : "text-ink2 hover:bg-soft2"),
10582
+ children: [
10583
+ active ? /* @__PURE__ */ jsx(motion.span, {
10584
+ layoutId: "sub-nav-active",
10585
+ "aria-hidden": true,
10586
+ className: "absolute inset-0 z-0 rounded-[6px] bg-soft2",
10587
+ transition: {
10588
+ type: "spring",
10589
+ stiffness: 520,
10590
+ damping: 42
10591
+ }
10592
+ }) : null,
10593
+ /* @__PURE__ */ jsx("span", {
10594
+ className: "relative z-[1] min-w-0 truncate",
10595
+ children: item.label
10596
+ }),
10597
+ item.hint ? /* @__PURE__ */ jsx("span", {
10598
+ className: "relative z-[1] ml-auto text-[12px] font-normal text-muted",
10599
+ children: item.hint
10600
+ }) : null
10601
+ ]
10602
+ }, item.to);
10603
+ })
10604
+ });
10605
+ }
10606
+ function NavLabel({ children }) {
10607
+ return /* @__PURE__ */ jsx("div", {
10608
+ className: "px-3 pt-4 pb-1 text-[12px] text-muted",
10609
+ children
10610
+ });
10611
+ }
10612
+ /** Standalone message card: not found, empty, failed to load. */
10613
+ function NoticeCard({ title, text, action }) {
10614
+ return /* @__PURE__ */ jsxs(Panel, {
10615
+ className: "flex w-[480px] max-w-full flex-col items-center gap-2 px-6 py-8 text-center",
10616
+ children: [
10617
+ /* @__PURE__ */ jsx("h1", {
10618
+ className: "m-0 text-[16px] font-semibold",
10619
+ children: title
10620
+ }),
10621
+ text ? /* @__PURE__ */ jsx("div", {
10622
+ className: "m-0 max-w-[400px] text-[14px] text-ink2",
10623
+ children: text
10624
+ }) : null,
10625
+ action ? /* @__PURE__ */ jsx("div", {
10626
+ className: "mt-3 flex flex-wrap justify-center gap-2",
10627
+ children: action
10628
+ }) : null
10629
+ ]
10630
+ });
10631
+ }
10632
+ /** NoticeCard centred in the free space. */
10633
+ function Notice(props) {
10634
+ return /* @__PURE__ */ jsx("div", {
10635
+ className: "flex min-h-[50vh] grow items-center justify-center p-6 max-md:px-4",
10636
+ children: /* @__PURE__ */ jsx(NoticeCard, { ...props })
10637
+ });
10638
+ }
10639
+ //#endregion
10640
+ //#region viewer/src/pages/not-found.tsx
10641
+ function NotFound({ title = "Nothing here", text }) {
10642
+ const scoped = !!useViewer().scope;
10643
+ return /* @__PURE__ */ jsx(Notice, {
10644
+ title,
10645
+ text: text ?? "This address does not match a canvas or a page of the design system.",
10646
+ action: scoped ? void 0 : /* @__PURE__ */ jsx(ButtonLink, {
10647
+ to: "/",
10648
+ children: "All canvases"
10649
+ })
10650
+ });
10651
+ }
10652
+ //#endregion
10653
+ //#region viewer/src/pages/canvas/camera.ts
10654
+ /** The farthest out the camera goes; past it a page is specks, and every one of them still costs. */
10655
+ var MIN_ZOOM = .1;
10656
+ var clampZoom = (z, min = MIN_ZOOM) => Math.min(8, Math.max(min, z));
10657
+ /** The canvas camera. Lives outside React so panning never re-renders the tree. */
10658
+ var CameraStore = class {
10659
+ camera = {
10660
+ x: 0,
10661
+ y: 0,
10662
+ z: 1
10663
+ };
10664
+ /** How far out the camera may go; the viewport sets it from the size of the page. */
10665
+ minZoom = MIN_ZOOM;
10666
+ listeners = /* @__PURE__ */ new Set();
10667
+ animation = 0;
10668
+ subscribe(listener) {
10669
+ this.listeners.add(listener);
10670
+ return () => {
10671
+ this.listeners.delete(listener);
10672
+ };
11197
10673
  }
11198
- use(...e) {
11199
- let t = this.defaults.extensions || {
11200
- renderers: {},
11201
- childTokens: {}
10674
+ set(next) {
10675
+ this.camera = {
10676
+ x: next.x,
10677
+ y: next.y,
10678
+ z: clampZoom(next.z, this.minZoom)
11202
10679
  };
11203
- return e.forEach((n) => {
11204
- let s = { ...n };
11205
- if (s.async = this.defaults.async || s.async || !1, n.extensions && (n.extensions.forEach((r) => {
11206
- if (!r.name) throw new Error("extension name required");
11207
- if ("renderer" in r) {
11208
- let o = t.renderers[r.name];
11209
- o ? t.renderers[r.name] = function(...i) {
11210
- let u = r.renderer.apply(this, i);
11211
- return u === !1 && (u = o.apply(this, i)), u;
11212
- } : t.renderers[r.name] = r.renderer;
11213
- }
11214
- if ("tokenizer" in r) {
11215
- if (!r.level || r.level !== "block" && r.level !== "inline") throw new Error("extension level must be 'block' or 'inline'");
11216
- let o = t[r.level];
11217
- o ? o.unshift(r.tokenizer) : t[r.level] = [r.tokenizer], r.start && (r.level === "block" ? t.startBlock ? t.startBlock.push(r.start) : t.startBlock = [r.start] : r.level === "inline" && (t.startInline ? t.startInline.push(r.start) : t.startInline = [r.start]));
11218
- }
11219
- "childTokens" in r && r.childTokens && (t.childTokens[r.name] = r.childTokens);
11220
- }), s.extensions = t), n.renderer) {
11221
- let r = this.defaults.renderer || new S(this.defaults);
11222
- for (let o in n.renderer) {
11223
- if (!(o in r)) throw new Error(`renderer '${o}' does not exist`);
11224
- if (["options", "parser"].includes(o)) continue;
11225
- let i = o, u = n.renderer[i], a = r[i];
11226
- r[i] = (...p) => {
11227
- let c = u.apply(r, p);
11228
- return c === !1 && (c = a.apply(r, p)), c || "";
11229
- };
11230
- }
11231
- s.renderer = r;
11232
- }
11233
- if (n.tokenizer) {
11234
- let r = this.defaults.tokenizer || new P(this.defaults);
11235
- for (let o in n.tokenizer) {
11236
- if (!(o in r)) throw new Error(`tokenizer '${o}' does not exist`);
11237
- if ([
11238
- "options",
11239
- "rules",
11240
- "lexer"
11241
- ].includes(o)) continue;
11242
- let i = o, u = n.tokenizer[i], a = r[i];
11243
- r[i] = (...p) => {
11244
- let c = u.apply(r, p);
11245
- return c === !1 && (c = a.apply(r, p)), c;
11246
- };
11247
- }
11248
- s.tokenizer = r;
11249
- }
11250
- if (n.hooks) {
11251
- let r = this.defaults.hooks || new _();
11252
- for (let o in n.hooks) {
11253
- if (!(o in r)) throw new Error(`hook '${o}' does not exist`);
11254
- if (["options", "block"].includes(o)) continue;
11255
- let i = o, u = n.hooks[i], a = r[i];
11256
- _.passThroughHooks.has(o) ? r[i] = (p) => {
11257
- if (this.defaults.async && _.passThroughHooksRespectAsync.has(o)) return (async () => {
11258
- let d = await u.call(r, p);
11259
- return a.call(r, d);
11260
- })();
11261
- let c = u.call(r, p);
11262
- return a.call(r, c);
11263
- } : r[i] = (...p) => {
11264
- if (this.defaults.async) return (async () => {
11265
- let d = await u.apply(r, p);
11266
- return d === !1 && (d = await a.apply(r, p)), d;
11267
- })();
11268
- let c = u.apply(r, p);
11269
- return c === !1 && (c = a.apply(r, p)), c;
11270
- };
11271
- }
11272
- s.hooks = r;
11273
- }
11274
- if (n.walkTokens) {
11275
- let r = this.defaults.walkTokens, o = n.walkTokens;
11276
- s.walkTokens = function(i) {
11277
- let u = [];
11278
- return u.push(o.call(this, i)), r && (u = u.concat(r.call(this, i))), u;
11279
- };
11280
- }
11281
- this.defaults = {
11282
- ...this.defaults,
11283
- ...s
11284
- };
11285
- }), this;
10680
+ for (const listener of this.listeners) listener(this.camera);
11286
10681
  }
11287
- setOptions(e) {
11288
- return this.defaults = {
11289
- ...this.defaults,
11290
- ...e
11291
- }, this;
10682
+ /** Stop any animation: the user took over. */
10683
+ interrupt() {
10684
+ cancelAnimationFrame(this.animation);
10685
+ this.animation = 0;
11292
10686
  }
11293
- lexer(e, t) {
11294
- return R.lex(e, t ?? this.defaults);
10687
+ panBy(dx, dy) {
10688
+ this.interrupt();
10689
+ const { x, y, z } = this.camera;
10690
+ this.set({
10691
+ x: x + dx,
10692
+ y: y + dy,
10693
+ z
10694
+ });
11295
10695
  }
11296
- parser(e, t) {
11297
- return T.parse(e, t ?? this.defaults);
10696
+ /** Zoom by `factor` keeping the screen point (sx, sy) in place. */
10697
+ zoomAt(sx, sy, factor) {
10698
+ this.interrupt();
10699
+ const { x, y, z } = this.camera;
10700
+ const next = clampZoom(z * factor, this.minZoom);
10701
+ const wx = (sx - x) / z;
10702
+ const wy = (sy - y) / z;
10703
+ this.set({
10704
+ x: sx - wx * next,
10705
+ y: sy - wy * next,
10706
+ z: next
10707
+ });
11298
10708
  }
11299
- parseMarkdown(e) {
11300
- return (n, s) => {
11301
- let r = { ...s }, o = {
11302
- ...this.defaults,
11303
- ...r
11304
- }, i = this.onError(!!o.silent, !!o.async);
11305
- if (this.defaults.async === !0 && r.async === !1) return i(/* @__PURE__ */ new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));
11306
- if (typeof n > "u" || n === null) return i(/* @__PURE__ */ new Error("marked(): input parameter is undefined or null"));
11307
- if (typeof n != "string") return i(/* @__PURE__ */ new Error("marked(): input parameter is of type " + Object.prototype.toString.call(n) + ", string expected"));
11308
- if (o.hooks && (o.hooks.options = o, o.hooks.block = e), o.async) return (async () => {
11309
- let u = o.hooks ? await o.hooks.preprocess(n) : n, p = await (o.hooks ? await o.hooks.provideLexer(e) : e ? R.lex : R.lexInline)(u, o), c = o.hooks ? await o.hooks.processAllTokens(p) : p;
11310
- o.walkTokens && await Promise.all(this.walkTokens(c, o.walkTokens));
11311
- let m = await (o.hooks ? await o.hooks.provideParser(e) : e ? T.parse : T.parseInline)(c, o);
11312
- return o.hooks ? await o.hooks.postprocess(m) : m;
11313
- })().catch(i);
11314
- try {
11315
- o.hooks && (n = o.hooks.preprocess(n));
11316
- let a = (o.hooks ? o.hooks.provideLexer(e) : e ? R.lex : R.lexInline)(n, o);
11317
- o.hooks && (a = o.hooks.processAllTokens(a)), o.walkTokens && this.walkTokens(a, o.walkTokens);
11318
- let c = (o.hooks ? o.hooks.provideParser(e) : e ? T.parse : T.parseInline)(a, o);
11319
- return o.hooks && (c = o.hooks.postprocess(c)), c;
11320
- } catch (u) {
11321
- return i(u);
11322
- }
10709
+ animateTo(target, duration = 320) {
10710
+ this.interrupt();
10711
+ const from = { ...this.camera };
10712
+ const to = {
10713
+ ...target,
10714
+ z: clampZoom(target.z, this.minZoom)
10715
+ };
10716
+ if (duration <= 0 || matchMedia("(prefers-reduced-motion: reduce)").matches) {
10717
+ this.set(to);
10718
+ return;
10719
+ }
10720
+ const start = performance.now();
10721
+ const step = (now) => {
10722
+ const t = Math.min(1, (now - start) / duration);
10723
+ const e = 1 - (1 - t) ** 3;
10724
+ const z = from.z * (to.z / from.z) ** e;
10725
+ const k = from.z === to.z ? e : (z - from.z) / (to.z - from.z);
10726
+ this.set({
10727
+ x: from.x + (to.x - from.x) * k,
10728
+ y: from.y + (to.y - from.y) * k,
10729
+ z
10730
+ });
10731
+ if (t < 1) this.animation = requestAnimationFrame(step);
10732
+ else this.animation = 0;
10733
+ };
10734
+ this.animation = requestAnimationFrame(step);
10735
+ }
10736
+ toWorld(sx, sy) {
10737
+ const { x, y, z } = this.camera;
10738
+ return {
10739
+ x: (sx - x) / z,
10740
+ y: (sy - y) / z
10741
+ };
10742
+ }
10743
+ };
10744
+ /**
10745
+ * The camera that shows `rect` inside a viewport of `w`×`h` with `pad` screen pixels around it.
10746
+ * When `rect` does not fit even at `minZoom`, it lines up with the top (and the left, if it is too wide).
10747
+ */
10748
+ function fitRect(rect, w, h, pad = 72, maxZoom = 1, minZoom = MIN_ZOOM) {
10749
+ const z = clampZoom(Math.min((w - pad * 2) / Math.max(rect.w, 1), (h - pad * 2) / Math.max(rect.h, 1), maxZoom), minZoom);
10750
+ const place = (start, size, room) => size * z <= room - pad * 2 ? room / 2 - (start + size / 2) * z : pad - start * z;
10751
+ return {
10752
+ z,
10753
+ x: place(rect.x, rect.w, w),
10754
+ y: place(rect.y, rect.h, h)
10755
+ };
10756
+ }
10757
+ function intersects(a, b) {
10758
+ return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
10759
+ }
10760
+ //#endregion
10761
+ //#region viewer/src/ui/choice.tsx
10762
+ function Switch({ checked, onChange, label, disabled }) {
10763
+ return /* @__PURE__ */ jsx("button", {
10764
+ type: "button",
10765
+ role: "switch",
10766
+ "aria-checked": checked,
10767
+ "aria-label": label,
10768
+ disabled,
10769
+ onClick: () => onChange?.(!checked),
10770
+ className: cn("relative h-[18px] w-[32px] shrink-0 cursor-pointer rounded-[9px] border-0 p-0 transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-50", checked ? "bg-ink" : "bg-rule-strong"),
10771
+ children: /* @__PURE__ */ jsx("span", {
10772
+ className: "absolute top-[2px] left-[2px] size-[14px] rounded-full bg-surface shadow-[0_1px_2px_rgba(0,0,0,0.25)] transition-transform duration-200 ease-[cubic-bezier(0.2,0.7,0.2,1)]",
10773
+ style: { transform: checked ? "translateX(14px)" : "none" }
10774
+ })
10775
+ });
10776
+ }
10777
+ function Segmented({ items, value, onChange, label, stretch, size = "ctrl" }) {
10778
+ const indicator = useId();
10779
+ return /* @__PURE__ */ jsx("div", {
10780
+ role: "group",
10781
+ "aria-label": label,
10782
+ className: cn("shrink-0 items-center gap-0.5 rounded-[6px] bg-track p-[3px]", size === "ctrl" ? "h-ctrl" : "h-7", stretch ? "flex" : "inline-flex"),
10783
+ children: items.map((item) => {
10784
+ const on = item.value === value;
10785
+ return /* @__PURE__ */ jsxs("button", {
10786
+ type: "button",
10787
+ "aria-pressed": on,
10788
+ title: item.title,
10789
+ onClick: () => onChange(item.value),
10790
+ className: cn("relative flex h-full cursor-pointer items-center justify-center whitespace-nowrap rounded-[4px] border-0 bg-transparent font-medium transition-colors", size === "ctrl" ? "px-2.5 text-ctrl" : "px-2 text-[12.5px]", stretch && "flex-1", on ? "text-ink" : "text-muted hover:text-ink2"),
10791
+ children: [on ? /* @__PURE__ */ jsx(motion.span, {
10792
+ layoutId: indicator,
10793
+ className: "absolute inset-0 z-0 rounded-[4px] bg-surface shadow-[0_1px_2px_rgba(0,0,0,0.10)]",
10794
+ transition: {
10795
+ type: "spring",
10796
+ stiffness: 520,
10797
+ damping: 42
10798
+ }
10799
+ }) : null, /* @__PURE__ */ jsx("span", {
10800
+ className: "relative z-[1] flex items-center gap-1.5",
10801
+ children: item.label
10802
+ })]
10803
+ }, item.value);
10804
+ })
10805
+ });
10806
+ }
10807
+ //#endregion
10808
+ //#region viewer/src/pages/canvas/full-page.tsx
10809
+ /** The full-window page of one screen, relative to the viewer's base. */
10810
+ var fullHref = (canvas, item, theme) => `/c/${encodeURIComponent(canvas)}/full/${encodeURIComponent(item)}?theme=${theme}`;
10811
+ /**
10812
+ * "Open in a new tab" for a screen. A sandboxed viewer serves screens to its frames only, so the
10813
+ * tab gets the full-window page of the viewer; otherwise the screen's own URL.
10814
+ */
10815
+ function useOpenUrl(canvasId, item, theme) {
10816
+ const { frameSandbox } = useViewer();
10817
+ const { base } = useRouter();
10818
+ if (!item) return null;
10819
+ if (item.kind === "url") return item.url;
10820
+ if (!frameSandbox) return absoluteUrl(frameSrc(item.url, item.theme ?? theme));
10821
+ return absoluteUrl(`${base}${fullHref(canvasId, item.id, item.theme ?? theme)}`);
10822
+ }
10823
+ /** `/c/:canvas/full/:item`: one screen filling the window, a way out on hover and Esc. */
10824
+ function FullPage({ canvasId, itemId }) {
10825
+ const { data: canvas, error, isLoading } = useCanvas(canvasId);
10826
+ const [, navigate] = useLocation();
10827
+ const { frameSandbox } = useViewer();
10828
+ const appTheme = useTheme();
10829
+ const frame = useRef(null);
10830
+ const param = new URLSearchParams(window.location.search).get("theme");
10831
+ const items = canvas ? canvas.pages.flatMap((page) => page.sections.flatMap((section) => section.items.filter((item) => item.kind === "screen" || item.kind === "url"))) : [];
10832
+ const item = items.find((entry) => entry.id === itemId);
10833
+ const theme = (item?.kind === "screen" ? item.theme : void 0) ?? (param === "light" || param === "dark" ? param : canvas?.theme ?? appTheme);
10834
+ const url = item ? item.kind === "screen" ? frameSrc(item.url, theme) : item.url : "";
10835
+ const back = `/c/${encodeURIComponent(canvasId)}`;
10836
+ useEffect(() => {
10837
+ if (item) document.title = `${item.title} · ${canvas?.title ?? ""}`;
10838
+ }, [item, canvas?.title]);
10839
+ useEffect(() => {
10840
+ const onKey = (event) => {
10841
+ if (event.key === "Escape") navigate(back);
10842
+ };
10843
+ window.addEventListener("keydown", onKey);
10844
+ return () => window.removeEventListener("keydown", onKey);
10845
+ }, [navigate, back]);
10846
+ useEffect(() => {
10847
+ const el = frame.current;
10848
+ if (!el || item?.kind !== "screen") return;
10849
+ return listenToFrame(el, (message) => {
10850
+ if (message.type === "keydown" && message.code === "Escape") navigate(back);
10851
+ else if (message.type === "go") {
10852
+ const id = message.target.includes("/") ? message.target.split("/")[1] : message.target;
10853
+ if (items.some((entry) => entry.id === id)) navigate(fullHref(canvasId, id, theme), { replace: true });
10854
+ } else if (message.type === "link" && canvas) followLink(message, canvas, (id) => navigate(fullHref(canvasId, id, theme), { replace: true }), itemId);
10855
+ });
10856
+ }, [url, item?.kind]);
10857
+ if (error) return /* @__PURE__ */ jsx(NotFound, {
10858
+ title: "Canvas not found",
10859
+ text: error.message
10860
+ });
10861
+ if (isLoading || !canvas) return /* @__PURE__ */ jsx("div", { className: "h-dvh bg-surface" });
10862
+ if (!item) return /* @__PURE__ */ jsx(NotFound, {
10863
+ title: "Screen not found",
10864
+ text: `No screen "${itemId}" on this canvas.`
10865
+ });
10866
+ return /* @__PURE__ */ jsxs("div", {
10867
+ className: "group fixed inset-0 bg-surface",
10868
+ children: [/* @__PURE__ */ jsx("iframe", {
10869
+ ref: frame,
10870
+ src: url,
10871
+ title: item.title,
10872
+ allow: "clipboard-read; clipboard-write; fullscreen",
10873
+ sandbox: frameSandbox,
10874
+ className: "block h-full w-full border-0",
10875
+ style: { colorScheme: theme },
10876
+ onLoad: () => item.kind === "screen" && sendTheme(frame.current, theme)
10877
+ }, `${item.id}:${item.kind === "screen" ? item.rev : item.url}`), /* @__PURE__ */ jsx("div", {
10878
+ className: "pointer-events-none absolute inset-x-0 top-0 flex justify-end p-3 opacity-0 transition-opacity duration-200 group-hover:opacity-100 focus-within:opacity-100",
10879
+ children: /* @__PURE__ */ jsxs(Link, {
10880
+ href: back,
10881
+ className: "pointer-events-auto flex h-8 items-center gap-2 rounded-[8px] border border-rule bg-surface px-3 text-[12.5px] text-ink2 no-underline shadow-pop hover:text-ink",
10882
+ children: [
10883
+ /* @__PURE__ */ jsx(Icon, {
10884
+ name: "x",
10885
+ size: 14
10886
+ }),
10887
+ "Back to ",
10888
+ canvas.title,
10889
+ /* @__PURE__ */ jsx(Kbd, { children: "esc" })
10890
+ ]
10891
+ })
10892
+ })]
10893
+ });
10894
+ }
10895
+ //#endregion
10896
+ //#region viewer/src/pages/canvas/sidebar.tsx
10897
+ function itemIcon(item) {
10898
+ if (item.kind === "note") return "note";
10899
+ if (item.kind === "image") return "image";
10900
+ if (item.kind === "url") return "globe";
10901
+ return item.frame.width < 600 ? "smartphone" : item.frame.width < 1100 ? "tablet" : "monitor";
10902
+ }
10903
+ function pageHref(canvas, page) {
10904
+ return `/c/${encodeURIComponent(canvas.id)}/p/${encodeURIComponent(page.id)}`;
10905
+ }
10906
+ /** Pages of the canvas and the items on the current one. */
10907
+ function Sidebar({ canvas, page, store, onPick }) {
10908
+ const selected = useStore(store, (state) => state.selected);
10909
+ const errors = useStore(store, (state) => state.frames);
10910
+ return /* @__PURE__ */ jsxs("aside", {
10911
+ "data-ui": true,
10912
+ className: "flex w-[264px] shrink-0 flex-col overflow-hidden border-r border-rule bg-surface",
10913
+ children: [/* @__PURE__ */ jsxs("div", {
10914
+ className: "flex flex-col gap-1 border-b border-rule px-4 py-3.5",
10915
+ children: [/* @__PURE__ */ jsx("div", {
10916
+ className: "truncate text-[14px] font-semibold",
10917
+ children: canvas.title
10918
+ }), canvas.description ? /* @__PURE__ */ jsx("div", {
10919
+ className: "line-clamp-3 text-[12.5px] leading-[1.45] text-muted",
10920
+ children: canvas.description
10921
+ }) : null]
10922
+ }), /* @__PURE__ */ jsxs("div", {
10923
+ className: "min-h-0 grow overflow-y-auto pb-4",
10924
+ children: [
10925
+ canvas.pages.length > 1 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
10926
+ className: "px-4 pt-3 pb-1 text-[12px] text-muted",
10927
+ children: "Pages"
10928
+ }), /* @__PURE__ */ jsx("nav", {
10929
+ className: "flex flex-col gap-0.5 px-2",
10930
+ children: canvas.pages.map((p) => {
10931
+ const count = p.sections.reduce((sum, s) => sum + s.items.filter((i) => i.kind === "screen" || i.kind === "url").length, 0);
10932
+ return /* @__PURE__ */ jsxs(Link, {
10933
+ href: pageHref(canvas, p),
10934
+ className: cn("flex h-8 items-center gap-2 rounded-[6px] px-2.5 text-[13.5px] no-underline transition-colors focus-visible:outline-offset-[-2px]", p.id === page.id ? "bg-soft2 font-medium text-ink" : "text-ink2 hover:bg-soft2"),
10935
+ children: [
10936
+ /* @__PURE__ */ jsx(Icon, {
10937
+ name: "file",
10938
+ size: 15,
10939
+ className: "text-muted"
10940
+ }),
10941
+ /* @__PURE__ */ jsx("span", {
10942
+ className: "min-w-0 grow truncate",
10943
+ children: p.title
10944
+ }),
10945
+ /* @__PURE__ */ jsx("span", {
10946
+ className: "text-[12px] text-muted tabular",
10947
+ children: count
10948
+ })
10949
+ ]
10950
+ }, p.id);
10951
+ })
10952
+ })] }) : null,
10953
+ /* @__PURE__ */ jsx("div", {
10954
+ className: "px-4 pt-3 pb-1 text-[12px] text-muted",
10955
+ children: canvas.pages.length > 1 ? page.title : "Layers"
10956
+ }),
10957
+ /* @__PURE__ */ jsx("div", {
10958
+ className: "flex flex-col px-2",
10959
+ children: page.sections.map((section) => /* @__PURE__ */ jsxs("div", {
10960
+ className: "flex flex-col",
10961
+ children: [section.title ? /* @__PURE__ */ jsxs("div", {
10962
+ className: "flex h-7 items-center gap-2 px-2.5 pt-1 text-[12px] font-medium text-ink2",
10963
+ children: [/* @__PURE__ */ jsx("span", {
10964
+ className: "truncate",
10965
+ children: section.title
10966
+ }), /* @__PURE__ */ jsx(Count, { value: section.items.length })]
10967
+ }) : null, section.items.map((item) => {
10968
+ const hasErrors = (errors[item.id]?.errors.length ?? 0) > 0 || item.kind === "screen" && item.missing;
10969
+ return /* @__PURE__ */ jsxs("button", {
10970
+ type: "button",
10971
+ onClick: () => onPick(item.id),
10972
+ className: cn("flex h-8 w-full cursor-pointer items-center gap-2 rounded-[6px] border-0 px-2.5 text-left text-[13px] transition-colors focus-visible:outline-offset-[-2px]", section.title && "pl-4", selected === item.id ? "bg-action-soft text-ink" : "bg-transparent text-ink2 hover:bg-soft2"),
10973
+ children: [
10974
+ /* @__PURE__ */ jsx(Icon, {
10975
+ name: itemIcon(item),
10976
+ size: 15,
10977
+ className: "text-muted"
10978
+ }),
10979
+ /* @__PURE__ */ jsx("span", {
10980
+ className: "min-w-0 grow truncate",
10981
+ children: item.title || (item.kind === "note" ? "Note" : item.id)
10982
+ }),
10983
+ hasErrors ? /* @__PURE__ */ jsx("span", { className: "size-[7px] shrink-0 rounded-full bg-danger" }) : null
10984
+ ]
10985
+ }, item.id);
10986
+ })]
10987
+ }, section.id))
10988
+ })
10989
+ ]
10990
+ })]
10991
+ });
10992
+ }
10993
+ //#endregion
10994
+ //#region viewer/src/pages/canvas/canvas-bar.tsx
10995
+ /** The canvas page's top bar: the canvas and its pages, screen theme, inspect and play. */
10996
+ function CanvasBar({ canvas, page, theme, onTheme, sidebar, onSidebar, onPlay, canPlay, inspect, onInspect }) {
10997
+ const [, navigate] = useLocation();
10998
+ const { slots, scope } = useViewer();
10999
+ const titleRef = useRef(null);
11000
+ const endRef = useRef(null);
11001
+ const compact = useFitBar(titleRef, endRef, [
11002
+ canvas.title,
11003
+ page?.title,
11004
+ canvas.pages.length
11005
+ ]);
11006
+ return /* @__PURE__ */ jsxs(BarCompactProvider, {
11007
+ value: compact,
11008
+ children: [/* @__PURE__ */ jsxs(Bar, { children: [
11009
+ slots.barStart,
11010
+ slots.brand ? slots.brand : scope ? /* @__PURE__ */ jsx(Mark, {}) : /* @__PURE__ */ jsx(Tooltip, {
11011
+ content: "All canvases",
11012
+ children: /* @__PURE__ */ jsx(Link, {
11013
+ href: "/",
11014
+ className: "flex items-center",
11015
+ "aria-label": "All canvases",
11016
+ children: /* @__PURE__ */ jsx(Mark, {})
11017
+ })
11018
+ }),
11019
+ /* @__PURE__ */ jsx(IconButton, {
11020
+ icon: "panel-left",
11021
+ label: sidebar ? "Hide sidebar (⌘\\)" : "Show sidebar (⌘\\)",
11022
+ onClick: () => onSidebar(!sidebar)
11023
+ }),
11024
+ /* @__PURE__ */ jsxs("div", {
11025
+ ref: titleRef,
11026
+ className: "flex min-w-0 items-center gap-1.5 text-[14px]",
11027
+ children: [/* @__PURE__ */ jsx("span", {
11028
+ "data-fit": true,
11029
+ className: "truncate font-semibold",
11030
+ children: canvas.title
11031
+ }), page && canvas.pages.length > 1 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, {
11032
+ name: "chevron-right",
11033
+ size: 14,
11034
+ className: "shrink-0 text-muted"
11035
+ }), /* @__PURE__ */ jsxs(Menu, {
11036
+ width: 240,
11037
+ trigger: /* @__PURE__ */ jsxs("button", {
11038
+ type: "button",
11039
+ className: "flex h-ctrl cursor-pointer items-center gap-1 rounded-[6px] border-0 bg-transparent px-2 text-[14px] text-ink2 hover:bg-soft2",
11040
+ children: [/* @__PURE__ */ jsx("span", {
11041
+ "data-fit": true,
11042
+ className: "truncate",
11043
+ children: page.title
11044
+ }), /* @__PURE__ */ jsx(Icon, {
11045
+ name: "chevron-down",
11046
+ size: 14,
11047
+ className: "text-muted"
11048
+ })]
11049
+ }),
11050
+ children: [/* @__PURE__ */ jsx(MenuLabel, { children: "Pages" }), canvas.pages.map((p) => /* @__PURE__ */ jsxs(MenuItem, {
11051
+ active: p.id === page.id,
11052
+ onSelect: () => navigate(pageHref(canvas, p)),
11053
+ children: [/* @__PURE__ */ jsx(Icon, {
11054
+ name: "file",
11055
+ size: 15,
11056
+ className: "text-muted"
11057
+ }), p.title]
11058
+ }, p.id))]
11059
+ })] }) : null]
11060
+ }),
11061
+ /* @__PURE__ */ jsxs("div", {
11062
+ ref: endRef,
11063
+ className: "ml-auto flex shrink-0 items-center gap-2",
11064
+ children: [
11065
+ /* @__PURE__ */ jsx(Segmented, {
11066
+ label: "Screen theme",
11067
+ value: theme,
11068
+ onChange: onTheme,
11069
+ items: [{
11070
+ value: "light",
11071
+ label: /* @__PURE__ */ jsx(Icon, {
11072
+ name: "sun",
11073
+ size: 15
11074
+ }),
11075
+ title: "Light screens"
11076
+ }, {
11077
+ value: "dark",
11078
+ label: /* @__PURE__ */ jsx(Icon, {
11079
+ name: "moon",
11080
+ size: 15
11081
+ }),
11082
+ title: "Dark screens"
11083
+ }]
11084
+ }),
11085
+ /* @__PURE__ */ jsx(InspectToggle, {
11086
+ on: inspect,
11087
+ onChange: onInspect,
11088
+ compact
11089
+ }),
11090
+ /* @__PURE__ */ jsx(Tooltip, {
11091
+ content: /* @__PURE__ */ jsxs("span", {
11092
+ className: "flex items-center gap-1.5",
11093
+ children: ["Play ", /* @__PURE__ */ jsx(Kbd, { children: "P" })]
11094
+ }),
11095
+ children: /* @__PURE__ */ jsx(Button, {
11096
+ kind: "secondary",
11097
+ icon: "play",
11098
+ onClick: onPlay,
11099
+ disabled: !canPlay,
11100
+ "aria-label": "Play",
11101
+ className: compact ? "w-ctrl px-0" : void 0,
11102
+ children: compact ? null : "Play"
11103
+ })
11104
+ }),
11105
+ /* @__PURE__ */ jsx(SearchButton, {
11106
+ width: 180,
11107
+ label: "Search",
11108
+ compact
11109
+ }),
11110
+ /* @__PURE__ */ jsx(ThemeMenu, {}),
11111
+ slots.barEnd
11112
+ ]
11113
+ })
11114
+ ] }), slots.belowBar ? /* @__PURE__ */ jsx("div", {
11115
+ className: "shrink-0",
11116
+ children: slots.belowBar
11117
+ }) : null]
11118
+ });
11119
+ }
11120
+ /** The selected item's name and actions, floating over the bottom of the canvas. */
11121
+ function SelectionBar({ canvas, store, layoutItems, theme, onPlay, inspect }) {
11122
+ const selected = useStore(store, (state) => state.selected);
11123
+ const active = useStore(store, (state) => state.active);
11124
+ const status = useStore(store, (state) => selected ? state.frames[selected] : void 0);
11125
+ const item = layoutItems.find((i) => i.id === selected);
11126
+ const openUrl = useOpenUrl(canvas.id, item?.kind === "screen" || item?.kind === "url" ? item : void 0, theme);
11127
+ if (!item) return null;
11128
+ const frame = item.kind === "screen" || item.kind === "url";
11129
+ const reload = () => reloadFrame(document.querySelector(`[data-item="${CSS.escape(item.id)}"] iframe`));
11130
+ const errors = status?.errors ?? [];
11131
+ return /* @__PURE__ */ jsx("div", {
11132
+ "data-ui": true,
11133
+ className: "pointer-events-none absolute inset-x-0 bottom-5 z-20 flex justify-center px-4",
11134
+ children: /* @__PURE__ */ jsxs("div", {
11135
+ className: "pointer-events-auto flex max-w-full flex-col overflow-hidden rounded-[10px] border border-rule bg-surface shadow-pop animate-[q-pop_120ms_ease-out]",
11136
+ children: [errors.length ? /* @__PURE__ */ jsxs("div", {
11137
+ className: "flex max-h-40 items-start gap-2 overflow-y-auto border-b border-rule bg-danger-soft px-3.5 py-2 font-mono text-[12px] whitespace-pre-wrap text-danger-text",
11138
+ children: [/* @__PURE__ */ jsx(Icon, {
11139
+ name: "alert",
11140
+ size: 14,
11141
+ className: "mt-px"
11142
+ }), /* @__PURE__ */ jsx("span", {
11143
+ className: "min-w-0",
11144
+ children: errors.join("\n")
11145
+ })]
11146
+ }) : null, /* @__PURE__ */ jsxs("div", {
11147
+ className: "flex h-[44px] items-center gap-1 pr-1.5 pl-3.5",
11148
+ children: [
11149
+ /* @__PURE__ */ jsxs("div", {
11150
+ className: "flex min-w-0 items-center gap-2 pr-2",
11151
+ children: [/* @__PURE__ */ jsx("span", {
11152
+ className: "truncate text-[13.5px] font-medium",
11153
+ children: item.title || item.id
11154
+ }), frame && active === item.id ? /* @__PURE__ */ jsxs("span", {
11155
+ className: "flex shrink-0 items-center gap-1.5 text-[12px] text-muted",
11156
+ children: [
11157
+ /* @__PURE__ */ jsx(Icon, {
11158
+ name: "pointer",
11159
+ size: 13
11160
+ }),
11161
+ " ",
11162
+ inspect ? "Inspecting" : "Interacting",
11163
+ " ",
11164
+ /* @__PURE__ */ jsx(Kbd, { children: "esc" })
11165
+ ]
11166
+ }) : frame ? /* @__PURE__ */ jsx("span", {
11167
+ className: "shrink-0 text-[12px] text-muted",
11168
+ children: inspect ? "Click an element to inspect it" : "Click the screen to interact"
11169
+ }) : null]
11170
+ }),
11171
+ frame ? /* @__PURE__ */ jsx(Tooltip, {
11172
+ content: /* @__PURE__ */ jsxs("span", {
11173
+ className: "flex items-center gap-1.5",
11174
+ children: ["Play ", /* @__PURE__ */ jsx(Kbd, { children: "↵" })]
11175
+ }),
11176
+ children: /* @__PURE__ */ jsx(IconButton, {
11177
+ icon: "play",
11178
+ label: "Play",
11179
+ onClick: () => onPlay(item.id)
11180
+ })
11181
+ }) : null,
11182
+ openUrl ? /* @__PURE__ */ jsx(Tooltip, {
11183
+ content: "Open in a new tab",
11184
+ children: /* @__PURE__ */ jsx("a", {
11185
+ href: openUrl,
11186
+ target: "_blank",
11187
+ rel: "noreferrer",
11188
+ "aria-label": "Open in a new tab",
11189
+ className: "inline-flex h-ctrl w-ctrl items-center justify-center rounded-[6px] text-muted hover:bg-soft2 hover:text-ink2",
11190
+ children: /* @__PURE__ */ jsx(Icon, {
11191
+ name: "external",
11192
+ size: 16
11193
+ })
11194
+ })
11195
+ }) : null,
11196
+ frame ? /* @__PURE__ */ jsx(Tooltip, {
11197
+ content: "Reload",
11198
+ children: /* @__PURE__ */ jsx(IconButton, {
11199
+ icon: "refresh",
11200
+ label: "Reload",
11201
+ onClick: reload
11202
+ })
11203
+ }) : null,
11204
+ item.kind === "screen" ? /* @__PURE__ */ jsx(Tooltip, {
11205
+ content: "Copy source path",
11206
+ children: /* @__PURE__ */ jsx(IconButton, {
11207
+ icon: "copy",
11208
+ label: "Copy source path",
11209
+ onClick: () => void copyText(item.file, "Path copied")
11210
+ })
11211
+ }) : null,
11212
+ /* @__PURE__ */ jsx(Tooltip, {
11213
+ content: /* @__PURE__ */ jsxs("span", {
11214
+ className: "flex items-center gap-1.5",
11215
+ children: ["Zoom to ", /* @__PURE__ */ jsx(Kbd, { children: "⇧2" })]
11216
+ }),
11217
+ children: /* @__PURE__ */ jsx(IconButton, {
11218
+ icon: "fit",
11219
+ label: "Zoom to",
11220
+ onClick: () => window.dispatchEvent(new CustomEvent("design:focus", { detail: {
11221
+ canvas: canvas.id,
11222
+ item: item.id
11223
+ } }))
11224
+ })
11225
+ })
11226
+ ]
11227
+ })]
11228
+ })
11229
+ });
11230
+ }
11231
+ /** The inspect mode switch: a button that stays pressed while on. */
11232
+ function InspectToggle({ on, onChange, compact = false }) {
11233
+ return /* @__PURE__ */ jsx(Tooltip, {
11234
+ content: /* @__PURE__ */ jsxs("span", {
11235
+ className: "flex items-center gap-1.5",
11236
+ children: ["Inspect elements ", /* @__PURE__ */ jsx(Kbd, { children: "I" })]
11237
+ }),
11238
+ children: /* @__PURE__ */ jsxs("button", {
11239
+ type: "button",
11240
+ "aria-pressed": on,
11241
+ "aria-label": "Inspect elements",
11242
+ onClick: () => onChange(!on),
11243
+ className: cn("inline-flex h-ctrl shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-[6px] border text-ctrl font-medium", compact ? "w-ctrl px-0" : "px-ctrl", on ? "border-action-line bg-action-soft text-action" : "border-rule-strong bg-surface text-ink hover:bg-soft"),
11244
+ children: [/* @__PURE__ */ jsx(Icon, {
11245
+ name: "pointer",
11246
+ size: 15
11247
+ }), compact ? null : "Inspect"]
11248
+ })
11249
+ });
11250
+ }
11251
+ //#endregion
11252
+ //#region viewer/src/pages/canvas/layout.ts
11253
+ var SECTION_GAP = 280;
11254
+ var ITEM_GAP = 96;
11255
+ var ROW_GAP = 150;
11256
+ var HEADER_GAP = 110;
11257
+ var HEADER_MIN_WIDTH = 720;
11258
+ var isFrame = (item) => item.kind === "screen" || item.kind === "url";
11259
+ function itemSize(item, sizes) {
11260
+ const measured = sizes[item.id];
11261
+ if (item.kind === "screen" || item.kind === "url") {
11262
+ const w = item.frame.width;
11263
+ if (item.frame.height !== "auto") return {
11264
+ w,
11265
+ h: item.frame.height
11266
+ };
11267
+ const fallback = item.kind === "screen" ? item.measuredHeight : void 0;
11268
+ return {
11269
+ w,
11270
+ h: measured?.h ?? fallback ?? 900
11323
11271
  };
11324
11272
  }
11325
- onError(e, t) {
11326
- return (n) => {
11327
- if (n.message += `
11328
- Please report this to https://github.com/markedjs/marked.`, e) {
11329
- let s = "<p>An error occurred:</p><pre>" + O(n.message + "", !0) + "</pre>";
11330
- return t ? Promise.resolve(s) : s;
11273
+ if (item.kind === "note") return {
11274
+ w: item.width,
11275
+ h: measured?.h ?? Math.max(80, Math.ceil(item.text.length / 38) * 26 + 40)
11276
+ };
11277
+ const w = item.width ?? measured?.w ?? 480;
11278
+ return {
11279
+ w,
11280
+ h: measured?.w ? measured.h / measured.w * w : measured?.h ?? w * .62
11281
+ };
11282
+ }
11283
+ function sectionHeaderHeight(section, sizes) {
11284
+ if (!section.title && !section.description) return 0;
11285
+ return sizes[`section:${section.id}`]?.h ?? (section.title ? 56 : 0) + (section.description ? 60 : 0);
11286
+ }
11287
+ /** Sections stack top to bottom; items run left to right, wrapping after `columns`. */
11288
+ function layoutPage(page, sizes) {
11289
+ const items = [];
11290
+ const sections = [];
11291
+ if (page.layout === "free") {
11292
+ let cursor = 0;
11293
+ for (const section of page.sections) for (const item of section.items) {
11294
+ const { w, h } = itemSize(item, sizes);
11295
+ const x = item.x ?? cursor;
11296
+ const y = item.y ?? 0;
11297
+ cursor = Math.max(cursor, x + w + ITEM_GAP);
11298
+ items.push({
11299
+ item,
11300
+ section,
11301
+ x,
11302
+ y,
11303
+ w,
11304
+ h
11305
+ });
11306
+ }
11307
+ } else {
11308
+ let y = 0;
11309
+ for (const section of page.sections) {
11310
+ const header = sectionHeaderHeight(section, sizes);
11311
+ const top = y;
11312
+ let rowY = y + (header ? header + HEADER_GAP : 0);
11313
+ let x = 0;
11314
+ let rowHeight = 0;
11315
+ let width = 0;
11316
+ let column = 0;
11317
+ for (const item of section.items) {
11318
+ if (section.columns && column === section.columns) {
11319
+ rowY += rowHeight + ROW_GAP;
11320
+ x = 0;
11321
+ rowHeight = 0;
11322
+ column = 0;
11323
+ }
11324
+ const { w, h } = itemSize(item, sizes);
11325
+ items.push({
11326
+ item,
11327
+ section,
11328
+ x,
11329
+ y: rowY,
11330
+ w,
11331
+ h
11332
+ });
11333
+ x += w + ITEM_GAP;
11334
+ width = Math.max(width, x - ITEM_GAP);
11335
+ rowHeight = Math.max(rowHeight, h);
11336
+ column++;
11331
11337
  }
11332
- if (t) return Promise.reject(n);
11333
- throw n;
11334
- };
11338
+ const bottom = rowY + rowHeight;
11339
+ sections.push({
11340
+ section,
11341
+ x: 0,
11342
+ y: top,
11343
+ w: Math.max(width, header ? HEADER_MIN_WIDTH : 0),
11344
+ h: bottom - top,
11345
+ header
11346
+ });
11347
+ y = bottom + SECTION_GAP;
11348
+ }
11335
11349
  }
11336
- };
11337
- var E = new F();
11338
- function k(l, e) {
11339
- return E.parse(l, e);
11350
+ let minX = 0;
11351
+ let minY = 0;
11352
+ let maxX = 0;
11353
+ let maxY = 0;
11354
+ const all = [...items, ...sections];
11355
+ if (all.length) {
11356
+ minX = Math.min(...all.map((r) => r.x));
11357
+ minY = Math.min(...all.map((r) => r.y));
11358
+ maxX = Math.max(...all.map((r) => r.x + r.w));
11359
+ maxY = Math.max(...all.map((r) => r.y + r.h));
11360
+ }
11361
+ return {
11362
+ items,
11363
+ sections,
11364
+ bounds: {
11365
+ x: minX,
11366
+ y: minY,
11367
+ w: maxX - minX,
11368
+ h: maxY - minY
11369
+ }
11370
+ };
11340
11371
  }
11341
- k.options = k.setOptions = function(l) {
11342
- return E.setOptions(l), k.defaults = E.defaults, W(k.defaults), k;
11343
- };
11344
- k.getDefaults = I;
11345
- k.defaults = y;
11346
- function Pt(...l) {
11347
- return E.use(...l), k.defaults = E.defaults, W(k.defaults), k;
11372
+ /** Frames in reading order, for arrow-key stepping and play mode. */
11373
+ function frameOrder(layout) {
11374
+ return layout.items.filter((placed) => isFrame(placed.item));
11348
11375
  }
11349
- k.use = Pt;
11350
- k.walkTokens = function(l, e) {
11351
- return E.walkTokens(l, e);
11352
- };
11353
- k.parseInline = E.parseInline;
11354
- k.Parser = T;
11355
- k.parser = T.parse;
11356
- k.Renderer = S;
11357
- k.TextRenderer = z;
11358
- k.Lexer = R;
11359
- k.lexer = R.lex;
11360
- k.Tokenizer = P;
11361
- k.Hooks = _;
11362
- k.parse = k;
11363
- k.options;
11364
- k.setOptions;
11365
- k.walkTokens;
11366
- k.parseInline;
11367
- T.parse;
11368
- R.lex;
11369
11376
  //#endregion
11370
- //#region viewer/src/components/markdown.tsx
11371
- var SAFE_SCHEMES = /* @__PURE__ */ new Set([
11372
- "http:",
11373
- "https:",
11374
- "mailto:"
11375
- ]);
11376
- /** http, https, mailto or relative. Browsers drop whitespace and control characters inside a scheme, so do we. */
11377
- function isSafeHref(href) {
11378
- const bare = href.replace(/[\u0000- \u007f]/g, "");
11379
- const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(bare)?.[1];
11380
- return !scheme || SAFE_SCHEMES.has(`${scheme.toLowerCase()}:`);
11377
+ //#region viewer/src/pages/canvas/view-state.ts
11378
+ function createViewState() {
11379
+ return new Store({
11380
+ selected: null,
11381
+ active: null,
11382
+ hovered: null,
11383
+ sizes: {},
11384
+ frames: {},
11385
+ live: [],
11386
+ panning: false,
11387
+ zoomTiny: false,
11388
+ frameEls: {},
11389
+ inspecting: false
11390
+ });
11381
11391
  }
11382
- function escapeHtml(text) {
11383
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
11392
+ function setSize(store, key, size) {
11393
+ store.set((state) => {
11394
+ const prev = state.sizes[key];
11395
+ if (prev && Math.abs(prev.h - size.h) < 1 && prev.w === size.w) return state;
11396
+ return {
11397
+ ...state,
11398
+ sizes: {
11399
+ ...state.sizes,
11400
+ [key]: size
11401
+ }
11402
+ };
11403
+ });
11384
11404
  }
11385
- var markdown = new F({
11386
- gfm: true,
11387
- breaks: false
11388
- }, { renderer: {
11389
- html: ({ text }) => escapeHtml(text),
11390
- link(token) {
11391
- return isSafeHref(token.href) ? false : this.parser.parseInline(token.tokens);
11392
- },
11393
- image(token) {
11394
- return isSafeHref(token.href) ? false : escapeHtml(token.text);
11395
- }
11396
- } });
11397
- function renderMarkdown(text) {
11398
- return markdown.parse(text, { async: false }).replace(/<a href=/g, "<a target=\"_blank\" rel=\"noreferrer\" href=");
11405
+ function setFrame(store, id, patch) {
11406
+ store.set((state) => {
11407
+ const prev = state.frames[id] ?? {
11408
+ ready: false,
11409
+ errors: []
11410
+ };
11411
+ return {
11412
+ ...state,
11413
+ frames: {
11414
+ ...state.frames,
11415
+ [id]: {
11416
+ ...prev,
11417
+ ...patch
11418
+ }
11419
+ }
11420
+ };
11421
+ });
11399
11422
  }
11400
- /** Markdown from the project's own files (guidelines, notes, descriptions). */
11401
- function Markdown({ text, className }) {
11402
- const html = useMemo(() => renderMarkdown(text), [text]);
11403
- return /* @__PURE__ */ jsx("div", {
11404
- className: cn("prose", className),
11405
- dangerouslySetInnerHTML: { __html: html }
11423
+ function setFrameEl(store, id, el) {
11424
+ store.set((state) => state.frameEls[id] === el ? state : {
11425
+ ...state,
11426
+ frameEls: {
11427
+ ...state.frameEls,
11428
+ [id]: el
11429
+ }
11406
11430
  });
11407
11431
  }
11408
11432
  //#endregion
@@ -11444,7 +11468,7 @@ var FrameItem = memo(function FrameItem({ placed, mode, thumb, theme, store, eve
11444
11468
  const src = item.kind === "screen" ? frameSrc(item.url, openTheme, capture ? { capture: "1" } : void 0) : item.url;
11445
11469
  const ready = !!status?.ready;
11446
11470
  const off = mode === "off";
11447
- const mounted = !off && !(item.kind === "screen" && item.missing);
11471
+ const mounted = !off && !(item.kind === "screen" && item.missing) && !!item.url;
11448
11472
  useEffect(() => {
11449
11473
  if (off) setFrame(store, item.id, { ready: false });
11450
11474
  }, [