@wildwinter/app-shell 0.46.4 → 0.46.5

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.
@@ -1,4 +1,4 @@
1
- import { copyWithFeedback } from './chunk-P6WLAEKZ.js';
1
+ import { copyWithFeedback } from './chunk-QVACRWG7.js';
2
2
  import { el, iconNode } from './chunk-UG2P777F.js';
3
3
 
4
4
  // src/link-status.ts
@@ -10,16 +10,16 @@ function linkStatusClass(s) {
10
10
  function linkStatusTip(s) {
11
11
  switch (s.state) {
12
12
  case "off":
13
- return "Live link is off. Click to start listening.";
13
+ return "Live Link is off. Click to start listening.";
14
14
  case "error":
15
- return `Live link failed (${s.message}). Click to retry.`;
15
+ return `Live Link failed (${s.message}). Click to retry.`;
16
16
  case "listening":
17
- return "Live link is listening for a game. Click to stop.";
17
+ return "Live Link is listening for a game. Click to stop.";
18
18
  case "connected": {
19
19
  const who = s.project !== void 0 ? ` to ${s.project}` : "";
20
20
  const build = s.build === "stale" ? " on a different build, so save or rebuild to re-sync" : s.build === "match" ? " and in sync" : "";
21
21
  const note = s.note ? ` ${s.note}` : "";
22
- return `Live link is connected${who}${build}.${note} Click to stop.`;
22
+ return `Live Link is connected${who}${build}.${note} Click to stop.`;
23
23
  }
24
24
  }
25
25
  }
@@ -59,7 +59,7 @@ function mountLinkStatus(host, opts) {
59
59
  const addr = linkAddress(status);
60
60
  if (addr !== void 0) {
61
61
  url.textContent = addr;
62
- url.dataset["tip"] = "Click to copy the live link address";
62
+ url.dataset["tip"] = "Click to copy the Live Link address";
63
63
  url.hidden = false;
64
64
  } else url.hidden = true;
65
65
  publishReserve();
@@ -96,5 +96,5 @@ function mountLinkStatus(host, opts) {
96
96
  }
97
97
 
98
98
  export { linkAddress, linkStatusClass, linkStatusTip, mountLinkStatus };
99
- //# sourceMappingURL=chunk-G2JT3HZH.js.map
100
- //# sourceMappingURL=chunk-G2JT3HZH.js.map
99
+ //# sourceMappingURL=chunk-B2TC6Q5J.js.map
100
+ //# sourceMappingURL=chunk-B2TC6Q5J.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/link-status.ts"],"names":[],"mappings":"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","file":"chunk-G2JT3HZH.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The live-link status chip: a compact control in the bottom-right corner for\n// the loopback server a running game connects to. All the state collapses to\n// one coloured \"connect\" icon (grey off, amber listening, green connected and\n// in sync, red connected on a different build); click it to toggle the link\n// (off -> listening -> stop), and while the link is up the copiable ws://\n// address sits beside it. Patterpad's debug-panel.ts and Storyletter's\n// live-link.ts were this file twice, character for character down to the\n// 1000ms `.copied` revert. Styles ship as link-status.css.\n//\n// What the game sends is the app's business; this is the control and the\n// status only.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { iconNode } from \"./icons.js\";\nimport { copyWithFeedback } from \"./util.js\";\n\nexport type LinkStatus =\n | { state: \"off\" }\n | { state: \"error\"; message: string }\n | { state: \"listening\"; port: number; address?: string }\n | {\n state: \"connected\"; port: number; address?: string;\n /** What connected, if it said. */\n project?: string;\n /** Whether the game's build matches the editor's. */\n build?: \"match\" | \"stale\" | \"unknown\";\n /** One extra sentence for the tip (Storyletter names the live boxes). */\n note?: string;\n };\n\n/** The chip's colour class for a state: the four. */\nexport type LinkStatusClass = \"off\" | \"listening\" | \"live\" | \"stale\";\n\nexport interface LinkStatusOptions {\n /** A word beside the chip (\"Live link\"), so a bottom-corner plug is not a\n * mystery. Omitted draws none. */\n label?: string;\n /** Start when off or failed, stop otherwise. A returned status is applied;\n * a status pushed later through `apply` works as well. */\n onToggle: (current: LinkStatus) => LinkStatus | void | Promise<LinkStatus | void>;\n /** Runs after the address is copied. The copy itself is the chip's. */\n onCopy?: (address: string) => void;\n /** Controls before the address (Patterpad's flow picker). */\n lead?: HTMLElement[];\n}\n\nexport interface LinkStatusChip {\n el: HTMLElement;\n apply(status: LinkStatus): void;\n status(): LinkStatus;\n /** Show / hide the control (shown when a project is open). */\n setVisible(on: boolean): void;\n /** Toggle the link (a menu item routes here). */\n toggle(): void;\n}\n\nexport function linkStatusClass(s: LinkStatus): LinkStatusClass {\n if (s.state === \"off\" || s.state === \"error\") return \"off\";\n if (s.state === \"listening\") return \"listening\";\n return s.build === \"stale\" ? \"stale\" : \"live\";\n}\n\n/** The tooltip, spelling the state out. One wording for the family. */\nexport function linkStatusTip(s: LinkStatus): string {\n switch (s.state) {\n case \"off\": return \"Live link is off. Click to start listening.\";\n case \"error\": return `Live link failed (${s.message}). Click to retry.`;\n case \"listening\": return \"Live link is listening for a game. Click to stop.\";\n case \"connected\": {\n const who = s.project !== undefined ? ` to ${s.project}` : \"\";\n const build = s.build === \"stale\" ? \" on a different build, so save or rebuild to re-sync\"\n : s.build === \"match\" ? \" and in sync\" : \"\";\n const note = s.note ? ` ${s.note}` : \"\";\n return `Live link is connected${who}${build}.${note} Click to stop.`;\n }\n }\n}\n\n/** The address a game connects to. */\nexport function linkAddress(s: LinkStatus): string | undefined {\n if (s.state !== \"listening\" && s.state !== \"connected\") return undefined;\n return s.address ?? `ws://127.0.0.1:${s.port}`;\n}\n\n/** Where the chip sits, which link-status.css says too: its right inset, and the\n * breathing room anything beside it keeps. Kept beside the reserve that uses\n * them, since the reserve is a promise about the CSS. */\nconst CORNER_INSET = 14.5;\nconst CORNER_GAP = 12;\n\n/** Mount the chip into `host` (document.body in both apps: it is fixed to the\n * window's corner). Hidden until `setVisible(true)`. */\nexport function mountLinkStatus(host: HTMLElement, opts: LinkStatusOptions): LinkStatusChip {\n const wrap = el(\"div\", \"linkstatus\"); wrap.hidden = true;\n const url = el(\"button\", \"linkstatus-url\"); url.type = \"button\"; url.hidden = true;\n const toggle = el(\"button\", \"linkstatus-toggle off\"); toggle.type = \"button\"; toggle.append(iconNode(\"connect\"));\n if (opts.label !== undefined) wrap.append(el(\"span\", \"linkstatus-word\", opts.label));\n wrap.append(...(opts.lead ?? []), url, toggle);\n host.append(wrap);\n\n let status: LinkStatus = { state: \"off\" };\n\n // The corner the chip floats over belongs to whatever the host put there: a\n // problems bar's error link and its buttons, a canvas strip's last control.\n // Both apps had it drawn under the chip (2026-09-17). So the chip publishes\n // how much of the corner it takes, as --linkstatus-reserve on the document,\n // and the things that can sit there leave that much room (the stepper bar\n // does, in stepper.css; a host's own strips opt in). 0px while hidden.\n // Re-measured on every render, because the address comes and goes, and on a\n // resize where the platform offers one, because a web font landing moves it.\n const root = host.ownerDocument.documentElement;\n const publishReserve = (): void => {\n const width = wrap.hidden ? 0 : wrap.getBoundingClientRect().width;\n root.style.setProperty(\"--linkstatus-reserve\",\n width > 0 ? `${Math.ceil(width + CORNER_INSET + CORNER_GAP)}px` : \"0px\");\n };\n if (typeof ResizeObserver !== \"undefined\") new ResizeObserver(publishReserve).observe(wrap);\n\n const render = (): void => {\n toggle.className = `linkstatus-toggle ${linkStatusClass(status)}`;\n const tip = linkStatusTip(status);\n toggle.dataset[\"tip\"] = tip;\n toggle.setAttribute(\"aria-label\", tip);\n // The copiable address means something once the server is up.\n const addr = linkAddress(status);\n if (addr !== undefined) {\n url.textContent = addr;\n url.dataset[\"tip\"] = \"Click to copy the live link address\";\n url.hidden = false;\n } else url.hidden = true;\n publishReserve();\n };\n const apply = (s: LinkStatus): void => { status = s; render(); };\n render();\n\n toggle.addEventListener(\"click\", () => {\n const out = opts.onToggle(status);\n void Promise.resolve(out).then((next) => { if (next) apply(next); });\n });\n url.addEventListener(\"click\", () => {\n const addr = url.textContent ?? \"\";\n if (!addr) return;\n void copyWithFeedback(url, addr);\n opts.onCopy?.(addr);\n });\n\n return {\n el: wrap, apply,\n status: () => status,\n setVisible(on: boolean): void { wrap.hidden = !on; publishReserve(); },\n toggle(): void { toggle.click(); },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/link-status.ts"],"names":[],"mappings":"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","file":"chunk-B2TC6Q5J.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The live-link status chip: a compact control in the bottom-right corner for\n// the loopback server a running game connects to. All the state collapses to\n// one coloured \"connect\" icon (grey off, amber listening, green connected and\n// in sync, red connected on a different build); click it to toggle the link\n// (off -> listening -> stop), and while the link is up the copiable ws://\n// address sits beside it. Patterpad's debug-panel.ts and Storyletter's\n// live-link.ts were this file twice, character for character down to the\n// 1000ms `.copied` revert. Styles ship as link-status.css.\n//\n// What the game sends is the app's business; this is the control and the\n// status only.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { iconNode } from \"./icons.js\";\nimport { copyWithFeedback } from \"./util.js\";\n\nexport type LinkStatus =\n | { state: \"off\" }\n | { state: \"error\"; message: string }\n | { state: \"listening\"; port: number; address?: string }\n | {\n state: \"connected\"; port: number; address?: string;\n /** What connected, if it said. */\n project?: string;\n /** Whether the game's build matches the editor's. */\n build?: \"match\" | \"stale\" | \"unknown\";\n /** One extra sentence for the tip (Storyletter names the live boxes). */\n note?: string;\n };\n\n/** The chip's colour class for a state: the four. */\nexport type LinkStatusClass = \"off\" | \"listening\" | \"live\" | \"stale\";\n\nexport interface LinkStatusOptions {\n /** A word beside the chip (\"Live Link\"), so a bottom-corner plug is not a\n * mystery. Omitted draws none. */\n label?: string;\n /** Start when off or failed, stop otherwise. A returned status is applied;\n * a status pushed later through `apply` works as well. */\n onToggle: (current: LinkStatus) => LinkStatus | void | Promise<LinkStatus | void>;\n /** Runs after the address is copied. The copy itself is the chip's. */\n onCopy?: (address: string) => void;\n /** Controls before the address (Patterpad's flow picker). */\n lead?: HTMLElement[];\n}\n\nexport interface LinkStatusChip {\n el: HTMLElement;\n apply(status: LinkStatus): void;\n status(): LinkStatus;\n /** Show / hide the control (shown when a project is open). */\n setVisible(on: boolean): void;\n /** Toggle the link (a menu item routes here). */\n toggle(): void;\n}\n\nexport function linkStatusClass(s: LinkStatus): LinkStatusClass {\n if (s.state === \"off\" || s.state === \"error\") return \"off\";\n if (s.state === \"listening\") return \"listening\";\n return s.build === \"stale\" ? \"stale\" : \"live\";\n}\n\n/** The tooltip, spelling the state out. One wording for the family. */\nexport function linkStatusTip(s: LinkStatus): string {\n switch (s.state) {\n case \"off\": return \"Live Link is off. Click to start listening.\";\n case \"error\": return `Live Link failed (${s.message}). Click to retry.`;\n case \"listening\": return \"Live Link is listening for a game. Click to stop.\";\n case \"connected\": {\n const who = s.project !== undefined ? ` to ${s.project}` : \"\";\n const build = s.build === \"stale\" ? \" on a different build, so save or rebuild to re-sync\"\n : s.build === \"match\" ? \" and in sync\" : \"\";\n const note = s.note ? ` ${s.note}` : \"\";\n return `Live Link is connected${who}${build}.${note} Click to stop.`;\n }\n }\n}\n\n/** The address a game connects to. */\nexport function linkAddress(s: LinkStatus): string | undefined {\n if (s.state !== \"listening\" && s.state !== \"connected\") return undefined;\n return s.address ?? `ws://127.0.0.1:${s.port}`;\n}\n\n/** Where the chip sits, which link-status.css says too: its right inset, and the\n * breathing room anything beside it keeps. Kept beside the reserve that uses\n * them, since the reserve is a promise about the CSS. */\nconst CORNER_INSET = 14.5;\nconst CORNER_GAP = 12;\n\n/** Mount the chip into `host` (document.body in both apps: it is fixed to the\n * window's corner). Hidden until `setVisible(true)`. */\nexport function mountLinkStatus(host: HTMLElement, opts: LinkStatusOptions): LinkStatusChip {\n const wrap = el(\"div\", \"linkstatus\"); wrap.hidden = true;\n const url = el(\"button\", \"linkstatus-url\"); url.type = \"button\"; url.hidden = true;\n const toggle = el(\"button\", \"linkstatus-toggle off\"); toggle.type = \"button\"; toggle.append(iconNode(\"connect\"));\n if (opts.label !== undefined) wrap.append(el(\"span\", \"linkstatus-word\", opts.label));\n wrap.append(...(opts.lead ?? []), url, toggle);\n host.append(wrap);\n\n let status: LinkStatus = { state: \"off\" };\n\n // The corner the chip floats over belongs to whatever the host put there: a\n // problems bar's error link and its buttons, a canvas strip's last control.\n // Both apps had it drawn under the chip (2026-09-17). So the chip publishes\n // how much of the corner it takes, as --linkstatus-reserve on the document,\n // and the things that can sit there leave that much room (the stepper bar\n // does, in stepper.css; a host's own strips opt in). 0px while hidden.\n // Re-measured on every render, because the address comes and goes, and on a\n // resize where the platform offers one, because a web font landing moves it.\n const root = host.ownerDocument.documentElement;\n const publishReserve = (): void => {\n const width = wrap.hidden ? 0 : wrap.getBoundingClientRect().width;\n root.style.setProperty(\"--linkstatus-reserve\",\n width > 0 ? `${Math.ceil(width + CORNER_INSET + CORNER_GAP)}px` : \"0px\");\n };\n if (typeof ResizeObserver !== \"undefined\") new ResizeObserver(publishReserve).observe(wrap);\n\n const render = (): void => {\n toggle.className = `linkstatus-toggle ${linkStatusClass(status)}`;\n const tip = linkStatusTip(status);\n toggle.dataset[\"tip\"] = tip;\n toggle.setAttribute(\"aria-label\", tip);\n // The copiable address means something once the server is up.\n const addr = linkAddress(status);\n if (addr !== undefined) {\n url.textContent = addr;\n url.dataset[\"tip\"] = \"Click to copy the Live Link address\";\n url.hidden = false;\n } else url.hidden = true;\n publishReserve();\n };\n const apply = (s: LinkStatus): void => { status = s; render(); };\n render();\n\n toggle.addEventListener(\"click\", () => {\n const out = opts.onToggle(status);\n void Promise.resolve(out).then((next) => { if (next) apply(next); });\n });\n url.addEventListener(\"click\", () => {\n const addr = url.textContent ?? \"\";\n if (!addr) return;\n void copyWithFeedback(url, addr);\n opts.onCopy?.(addr);\n });\n\n return {\n el: wrap, apply,\n status: () => status,\n setVisible(on: boolean): void { wrap.hidden = !on; publishReserve(); },\n toggle(): void { toggle.click(); },\n };\n}\n"]}
@@ -65,7 +65,17 @@ function relativeTime(date, now = Date.now()) {
65
65
  if (days < 7) return `${days} days ago`;
66
66
  return new Date(then).toLocaleDateString(void 0, { day: "numeric", month: "short", year: "numeric" });
67
67
  }
68
+ function isLocalOrigin(origin) {
69
+ if (origin === void 0 || origin === "") return true;
70
+ let host;
71
+ try {
72
+ host = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(origin) ? origin : `http://${origin}`).hostname;
73
+ } catch {
74
+ return false;
75
+ }
76
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
77
+ }
68
78
 
69
- export { copyWithFeedback, debounce, formatCount, isEditableTarget, plural, relativeTime };
70
- //# sourceMappingURL=chunk-P6WLAEKZ.js.map
71
- //# sourceMappingURL=chunk-P6WLAEKZ.js.map
79
+ export { copyWithFeedback, debounce, formatCount, isEditableTarget, isLocalOrigin, plural, relativeTime };
80
+ //# sourceMappingURL=chunk-QVACRWG7.js.map
81
+ //# sourceMappingURL=chunk-QVACRWG7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util.ts"],"names":[],"mappings":"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","file":"chunk-QVACRWG7.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The small idioms both apps wrote by hand and never gave a name: the plural\n// suffix (about forty sites), the debounce (eight copies), \"is the focus in a\n// field\" (four copies that disagreed about <select>), the count with grouping\n// separators, clipboard copy with the `.copied` revert (three timings), and\n// relative time (two wordings), and which connection to a live link is this\n// machine's own (isLocalOrigin, two copies). One answer each, here.\n//\n// DOM-free except where the idiom is about the DOM (isEditableTarget,\n// copyWithFeedback), so main-process code can import the rest.\n// ---------------------------------------------------------------------------\n\n/** \"1 link\" / \"3 links\". The plural form defaults to `noun + \"s\"`; pass the\n * irregular one (\"entry\", \"entries\") when that is wrong. */\nexport function plural(n: number, noun: string, pluralForm = `${noun}s`): string {\n return `${n} ${n === 1 ? noun : pluralForm}`;\n}\n\nexport interface Debounced<A extends unknown[]> {\n (...args: A): void;\n /** Drop the pending call. */\n cancel(): void;\n /** Run the pending call now, if there is one. */\n flush(): void;\n}\n\n/** Trailing-edge debounce: the call runs `ms` after the LAST call. */\nexport function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number): Debounced<A> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let pending: A | undefined;\n const run = (): void => {\n timer = undefined;\n if (pending === undefined) return;\n const args = pending;\n pending = undefined;\n fn(...args);\n };\n const debounced = ((...args: A): void => {\n pending = args;\n if (timer !== undefined) clearTimeout(timer);\n timer = setTimeout(run, ms);\n }) as Debounced<A>;\n debounced.cancel = () => { if (timer !== undefined) clearTimeout(timer); timer = undefined; pending = undefined; };\n debounced.flush = () => { if (timer !== undefined) clearTimeout(timer); run(); };\n return debounced;\n}\n\n/**\n * Is the keyboard's focus in a field? The one agreed answer: an input, a\n * textarea, a select, or anything contenteditable (the attribute is read as\n * well as `isContentEditable`, which jsdom does not implement). A window-level\n * shortcut checks this before acting, so a bare letter typed into a field\n * stays a letter.\n */\nexport function isEditableTarget(t: EventTarget | null | undefined): boolean {\n if (!(t instanceof Element)) return false;\n if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement) return true;\n if (t instanceof HTMLElement && t.isContentEditable === true) return true;\n const attr = t.getAttribute(\"contenteditable\");\n return attr !== null && attr !== \"false\";\n}\n\n/** A count for reading: grouping separators in the user's locale. */\nexport function formatCount(n: number): string {\n return n.toLocaleString();\n}\n\nconst copyTimers = new WeakMap<Element, ReturnType<typeof setTimeout>>();\n\n/**\n * Copy `text` to the clipboard and light `el` with `.copied` for `ms` (the\n * family's 1000). A second click restarts the timer rather than stacking a\n * second revert under the first. Resolves when the clipboard write has.\n */\nexport function copyWithFeedback(el: Element, text: string, ms = 1000): Promise<void> {\n const write = typeof navigator !== \"undefined\" && navigator.clipboard\n ? navigator.clipboard.writeText(text).catch(() => {})\n : Promise.resolve();\n el.classList.add(\"copied\");\n const prior = copyTimers.get(el);\n if (prior !== undefined) clearTimeout(prior);\n copyTimers.set(el, setTimeout(() => { el.classList.remove(\"copied\"); copyTimers.delete(el); }, ms));\n return write;\n}\n\n/**\n * How long ago, in one wording: \"just now\", \"4 minutes ago\", \"an hour ago\",\n * \"3 hours ago\", \"yesterday\", \"5 days ago\", and past a week the date itself.\n * Coarse on purpose: the question a reader has is whether the thing is from\n * this sitting, not how many seconds ago it was. A future date is \"just now\".\n */\nexport function relativeTime(date: Date | number | string, now: number = Date.now()): string {\n const then = date instanceof Date ? date.getTime() : new Date(date).getTime();\n if (!Number.isFinite(then)) return \"\";\n const mins = Math.floor((now - then) / 60000);\n if (mins < 1) return \"just now\";\n if (mins < 60) return `${plural(mins, \"minute\")} ago`;\n const hours = Math.floor(mins / 60);\n if (hours === 1) return \"an hour ago\";\n if (hours < 24) return `${hours} hours ago`;\n const days = Math.floor(hours / 24);\n if (days === 1) return \"yesterday\";\n if (days < 7) return `${days} days ago`;\n return new Date(then).toLocaleDateString(undefined, { day: \"numeric\", month: \"short\", year: \"numeric\" });\n}\n\n/**\n * Whether a WebSocket upgrade's Origin is this machine, so a game may take a\n * live link's slot (Patterpad's Live Link and Storyletter's, which listen on\n * loopback). A web page in any browser can open a socket to localhost, and the\n * browser stamps the upgrade with the page's Origin, so the Origin is the check:\n *\n * - no Origin: Unity's ClientWebSocket, Godot and Node send none;\n * - a loopback host, literally `localhost`, `127.0.0.1` or `[::1]`, with or\n * without a scheme and port: Unreal's libwebsockets sends the bare address\n * (`127.0.0.1`), and a browser game on a local dev server sends\n * `http://localhost:5173`;\n * - anything else is refused: another site, `null` (a file:// page sends it,\n * but so does a sandboxed frame on any site), and a name that only resolves\n * to loopback (`127.0.0.1.nip.io`), since the Origin names the domain, not\n * the address.\n */\nexport function isLocalOrigin(origin: string | undefined): boolean {\n if (origin === undefined || origin === \"\") return true;\n let host: string;\n try {\n // A browser sends scheme://host[:port]; Unreal sends a bare host, which URL would misread as a scheme.\n host = new URL(/^[a-z][a-z0-9+.-]*:\\/\\//i.test(origin) ? origin : `http://${origin}`).hostname;\n } catch { return false; }\n return host === \"localhost\" || host === \"127.0.0.1\" || host === \"[::1]\";\n}\n"]}
package/dist/index.cjs CHANGED
@@ -1289,6 +1289,16 @@ function relativeTime(date, now = Date.now()) {
1289
1289
  if (days < 7) return `${days} days ago`;
1290
1290
  return new Date(then).toLocaleDateString(void 0, { day: "numeric", month: "short", year: "numeric" });
1291
1291
  }
1292
+ function isLocalOrigin(origin) {
1293
+ if (origin === void 0 || origin === "") return true;
1294
+ let host;
1295
+ try {
1296
+ host = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(origin) ? origin : `http://${origin}`).hostname;
1297
+ } catch {
1298
+ return false;
1299
+ }
1300
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
1301
+ }
1292
1302
 
1293
1303
  // src/tool-window-web.ts
1294
1304
  function pinButton(opts) {
@@ -2532,7 +2542,7 @@ function staleBar(opts) {
2532
2542
  { className: "stale-bar" },
2533
2543
  el("span", {
2534
2544
  className: "stale-bar-msg",
2535
- text: `${opts.subject} changed in the editor. ${opts.advice ?? "Restart to play the new version."}`
2545
+ text: `${opts.message ?? `${opts.subject} changed in the editor.`} ${opts.advice ?? "Restart to play the new version."}`
2536
2546
  }),
2537
2547
  // The icon is a node beside the word, not a character inside it; the
2538
2548
  // button's flex gap sets the space (stale.css).
@@ -2934,16 +2944,16 @@ function linkStatusClass(s) {
2934
2944
  function linkStatusTip(s) {
2935
2945
  switch (s.state) {
2936
2946
  case "off":
2937
- return "Live link is off. Click to start listening.";
2947
+ return "Live Link is off. Click to start listening.";
2938
2948
  case "error":
2939
- return `Live link failed (${s.message}). Click to retry.`;
2949
+ return `Live Link failed (${s.message}). Click to retry.`;
2940
2950
  case "listening":
2941
- return "Live link is listening for a game. Click to stop.";
2951
+ return "Live Link is listening for a game. Click to stop.";
2942
2952
  case "connected": {
2943
2953
  const who = s.project !== void 0 ? ` to ${s.project}` : "";
2944
2954
  const build = s.build === "stale" ? " on a different build, so save or rebuild to re-sync" : s.build === "match" ? " and in sync" : "";
2945
2955
  const note = s.note ? ` ${s.note}` : "";
2946
- return `Live link is connected${who}${build}.${note} Click to stop.`;
2956
+ return `Live Link is connected${who}${build}.${note} Click to stop.`;
2947
2957
  }
2948
2958
  }
2949
2959
  }
@@ -2983,7 +2993,7 @@ function mountLinkStatus(host, opts) {
2983
2993
  const addr = linkAddress(status);
2984
2994
  if (addr !== void 0) {
2985
2995
  url.textContent = addr;
2986
- url.dataset["tip"] = "Click to copy the live link address";
2996
+ url.dataset["tip"] = "Click to copy the Live Link address";
2987
2997
  url.hidden = false;
2988
2998
  } else url.hidden = true;
2989
2999
  publishReserve();
@@ -3299,6 +3309,7 @@ exports.isCaseOnlyPropertyName = isCaseOnlyPropertyName;
3299
3309
  exports.isEditableTarget = isEditableTarget;
3300
3310
  exports.isIconName = isIconName;
3301
3311
  exports.isKeyCombo = isKeyCombo;
3312
+ exports.isLocalOrigin = isLocalOrigin;
3302
3313
  exports.isValidGameId = isValidGameId;
3303
3314
  exports.isValidPropertyName = isValidPropertyName;
3304
3315
  exports.keyHint = keyHint;