@wildwinter/app-shell 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ // src/window-guard.ts
4
+ var FAMILY_CSP = {
5
+ "default-src": ["'self'"],
6
+ "style-src": ["'self'", "'unsafe-inline'"],
7
+ "img-src": ["'self'", "data:"],
8
+ "object-src": ["'none'"],
9
+ "base-uri": ["'none'"]
10
+ };
11
+ function familyCsp(extra = {}) {
12
+ const merged = /* @__PURE__ */ new Map();
13
+ for (const [name, sources] of Object.entries(FAMILY_CSP)) merged.set(name, [...sources]);
14
+ for (const [name, sources] of Object.entries(extra)) {
15
+ const into = merged.get(name) ?? [...FAMILY_CSP["default-src"] ?? []];
16
+ for (const s of sources) if (!into.includes(s)) into.push(s);
17
+ merged.set(name, into);
18
+ }
19
+ return [...merged].map(([name, sources]) => `${name} ${sources.join(" ")}`).join("; ");
20
+ }
21
+ function cspOfPage(html) {
22
+ for (const tag of html.match(/<meta\b[^>]*>/gi) ?? []) {
23
+ if (!/http-equiv\s*=\s*["']Content-Security-Policy["']/i.test(tag)) continue;
24
+ const content = /\bcontent\s*=\s*"([^"]*)"/i.exec(tag) ?? /\bcontent\s*=\s*'([^']*)'/i.exec(tag);
25
+ return content ? content[1].trim() : null;
26
+ }
27
+ return null;
28
+ }
29
+ var EXTERNAL = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
30
+ var parse = (url) => {
31
+ try {
32
+ return new URL(url);
33
+ } catch {
34
+ return null;
35
+ }
36
+ };
37
+ function isSamePage(from, to) {
38
+ const a = parse(from), b = parse(to);
39
+ if (!a || !b || a.protocol !== b.protocol) return false;
40
+ if (a.protocol === "http:" || a.protocol === "https:") return a.origin === b.origin;
41
+ return a.host === b.host && a.pathname === b.pathname;
42
+ }
43
+ function guardWebContents(contents, opts = {}) {
44
+ const away = (what, url) => {
45
+ opts.onBlocked?.(what, url);
46
+ const target = parse(url);
47
+ if (target && EXTERNAL.has(target.protocol)) opts.openExternal?.(url);
48
+ };
49
+ contents.on("will-navigate", (event, url) => {
50
+ if (isSamePage(contents.getURL(), url)) return;
51
+ event.preventDefault();
52
+ away("navigate", url);
53
+ });
54
+ contents.setWindowOpenHandler(({ url }) => {
55
+ away("window", url);
56
+ return { action: "deny" };
57
+ });
58
+ contents.on("will-attach-webview", (event) => {
59
+ event.preventDefault();
60
+ opts.onBlocked?.("webview", "");
61
+ });
62
+ }
63
+ function guardAllWindows(app, opts = {}) {
64
+ app.on("web-contents-created", (_event, contents) => guardWebContents(contents, opts));
65
+ }
66
+
67
+ exports.FAMILY_CSP = FAMILY_CSP;
68
+ exports.cspOfPage = cspOfPage;
69
+ exports.familyCsp = familyCsp;
70
+ exports.guardAllWindows = guardAllWindows;
71
+ exports.guardWebContents = guardWebContents;
72
+ exports.isSamePage = isSamePage;
73
+ //# sourceMappingURL=window-guard.cjs.map
74
+ //# sourceMappingURL=window-guard.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/window-guard.ts"],"names":[],"mappings":"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","file":"window-guard.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// Window hardening for both apps (patterkit design/family-parity-2026-10.md\n// item 12): one content security policy, and guards on navigation and\n// window.open for every window the app makes.\n//\n// The policy is a `<meta http-equiv=\"Content-Security-Policy\">` on each page,\n// since the apps load their pages from files in a release build and a header\n// cannot reach those. `familyCsp` is the one spelling of it: a page's meta\n// carries `familyCsp(...)` for that page, and each app's tests check every page\n// against it, so a new page cannot ship without one. Styles allow\n// 'unsafe-inline' because both editors set inline styles (ProseMirror does, in\n// Patterpad); scripts never do.\n//\n// The guards: a window never navigates away from the page it loaded, never\n// opens another window, and never attaches a webview. A link to the web asks\n// the app to open it in the browser instead (`openExternal`), and nothing else\n// leaves the app. `guardAllWindows` applies them to every web contents the app\n// creates, tool windows and any later window included, which is why it is the\n// one call each app makes, at startup.\n//\n// No Electron import: the module takes the app and the web contents it is\n// handed, so it runs under test without Electron.\n// ---------------------------------------------------------------------------\n\nimport type { App, WebContents } from \"electron\";\n\n/** A policy as directives: each name with its sources. */\nexport type CspDirectives = Record<string, readonly string[]>;\n\n/** The family's base policy: everything from the app itself, inline styles, and images also from data URLs. */\nexport const FAMILY_CSP: CspDirectives = {\n \"default-src\": [\"'self'\"],\n \"style-src\": [\"'self'\", \"'unsafe-inline'\"],\n \"img-src\": [\"'self'\", \"data:\"],\n \"object-src\": [\"'none'\"],\n \"base-uri\": [\"'none'\"],\n};\n\n/**\n * The family's policy with a page's additions, as the meta's `content`. An addition to a directive the base\n * has extends it (`{ \"img-src\": [\"blob:\"] }` keeps `'self' data:`); a new directive is added. Directives come\n * out in the base's order, then the page's.\n */\nexport function familyCsp(extra: CspDirectives = {}): string {\n const merged = new Map<string, string[]>();\n for (const [name, sources] of Object.entries(FAMILY_CSP)) merged.set(name, [...sources]);\n for (const [name, sources] of Object.entries(extra)) {\n // A directive the base leaves out falls back to default-src, so an addition starts from that.\n const into = merged.get(name) ?? [...(FAMILY_CSP[\"default-src\"] ?? [])];\n for (const s of sources) if (!into.includes(s)) into.push(s);\n merged.set(name, into);\n }\n return [...merged].map(([name, sources]) => `${name} ${sources.join(\" \")}`).join(\"; \");\n}\n\n/** The policy a page's HTML carries in its meta, or null when it has none. */\nexport function cspOfPage(html: string): string | null {\n for (const tag of html.match(/<meta\\b[^>]*>/gi) ?? []) {\n if (!/http-equiv\\s*=\\s*[\"']Content-Security-Policy[\"']/i.test(tag)) continue;\n const content = /\\bcontent\\s*=\\s*\"([^\"]*)\"/i.exec(tag) ?? /\\bcontent\\s*=\\s*'([^']*)'/i.exec(tag);\n return content ? content[1]!.trim() : null;\n }\n return null;\n}\n\nexport interface WindowGuardOptions {\n /** Open a web address in the browser (Electron's `shell.openExternal`). Given, a link to an http, https or\n * mailto address that the page tried to navigate to or open goes here; absent, it goes nowhere. */\n openExternal?: (url: string) => void;\n /** Told of everything refused, for a dev console. */\n onBlocked?: (what: \"navigate\" | \"window\" | \"webview\", url: string) => void;\n}\n\nconst EXTERNAL = new Set([\"http:\", \"https:\", \"mailto:\"]);\n\nconst parse = (url: string): URL | null => { try { return new URL(url); } catch { return null; } };\n\n/**\n * Whether `to` is the page `from` already is: the same document, a hash or query apart. A dev server's page\n * may move within its own origin (a reload, its own client); a file page may not move to another file.\n */\nexport function isSamePage(from: string, to: string): boolean {\n const a = parse(from), b = parse(to);\n if (!a || !b || a.protocol !== b.protocol) return false;\n if (a.protocol === \"http:\" || a.protocol === \"https:\") return a.origin === b.origin;\n return a.host === b.host && a.pathname === b.pathname;\n}\n\n/** The guards on one web contents. */\nexport function guardWebContents(contents: WebContents, opts: WindowGuardOptions = {}): void {\n const away = (what: \"navigate\" | \"window\", url: string): void => {\n opts.onBlocked?.(what, url);\n const target = parse(url);\n if (target && EXTERNAL.has(target.protocol)) opts.openExternal?.(url);\n };\n contents.on(\"will-navigate\", (event, url) => {\n if (isSamePage(contents.getURL(), url)) return;\n event.preventDefault();\n away(\"navigate\", url);\n });\n contents.setWindowOpenHandler(({ url }) => {\n away(\"window\", url);\n return { action: \"deny\" };\n });\n contents.on(\"will-attach-webview\", (event) => {\n event.preventDefault();\n opts.onBlocked?.(\"webview\", \"\");\n });\n}\n\n/** The guards on every web contents the app creates from now on: the one call each app makes, at startup. */\nexport function guardAllWindows(app: App, opts: WindowGuardOptions = {}): void {\n app.on(\"web-contents-created\", (_event, contents) => guardWebContents(contents, opts));\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import { App, WebContents } from 'electron';
2
+
3
+ /** A policy as directives: each name with its sources. */
4
+ type CspDirectives = Record<string, readonly string[]>;
5
+ /** The family's base policy: everything from the app itself, inline styles, and images also from data URLs. */
6
+ declare const FAMILY_CSP: CspDirectives;
7
+ /**
8
+ * The family's policy with a page's additions, as the meta's `content`. An addition to a directive the base
9
+ * has extends it (`{ "img-src": ["blob:"] }` keeps `'self' data:`); a new directive is added. Directives come
10
+ * out in the base's order, then the page's.
11
+ */
12
+ declare function familyCsp(extra?: CspDirectives): string;
13
+ /** The policy a page's HTML carries in its meta, or null when it has none. */
14
+ declare function cspOfPage(html: string): string | null;
15
+ interface WindowGuardOptions {
16
+ /** Open a web address in the browser (Electron's `shell.openExternal`). Given, a link to an http, https or
17
+ * mailto address that the page tried to navigate to or open goes here; absent, it goes nowhere. */
18
+ openExternal?: (url: string) => void;
19
+ /** Told of everything refused, for a dev console. */
20
+ onBlocked?: (what: "navigate" | "window" | "webview", url: string) => void;
21
+ }
22
+ /**
23
+ * Whether `to` is the page `from` already is: the same document, a hash or query apart. A dev server's page
24
+ * may move within its own origin (a reload, its own client); a file page may not move to another file.
25
+ */
26
+ declare function isSamePage(from: string, to: string): boolean;
27
+ /** The guards on one web contents. */
28
+ declare function guardWebContents(contents: WebContents, opts?: WindowGuardOptions): void;
29
+ /** The guards on every web contents the app creates from now on: the one call each app makes, at startup. */
30
+ declare function guardAllWindows(app: App, opts?: WindowGuardOptions): void;
31
+
32
+ export { type CspDirectives, FAMILY_CSP, type WindowGuardOptions, cspOfPage, familyCsp, guardAllWindows, guardWebContents, isSamePage };
@@ -0,0 +1,32 @@
1
+ import { App, WebContents } from 'electron';
2
+
3
+ /** A policy as directives: each name with its sources. */
4
+ type CspDirectives = Record<string, readonly string[]>;
5
+ /** The family's base policy: everything from the app itself, inline styles, and images also from data URLs. */
6
+ declare const FAMILY_CSP: CspDirectives;
7
+ /**
8
+ * The family's policy with a page's additions, as the meta's `content`. An addition to a directive the base
9
+ * has extends it (`{ "img-src": ["blob:"] }` keeps `'self' data:`); a new directive is added. Directives come
10
+ * out in the base's order, then the page's.
11
+ */
12
+ declare function familyCsp(extra?: CspDirectives): string;
13
+ /** The policy a page's HTML carries in its meta, or null when it has none. */
14
+ declare function cspOfPage(html: string): string | null;
15
+ interface WindowGuardOptions {
16
+ /** Open a web address in the browser (Electron's `shell.openExternal`). Given, a link to an http, https or
17
+ * mailto address that the page tried to navigate to or open goes here; absent, it goes nowhere. */
18
+ openExternal?: (url: string) => void;
19
+ /** Told of everything refused, for a dev console. */
20
+ onBlocked?: (what: "navigate" | "window" | "webview", url: string) => void;
21
+ }
22
+ /**
23
+ * Whether `to` is the page `from` already is: the same document, a hash or query apart. A dev server's page
24
+ * may move within its own origin (a reload, its own client); a file page may not move to another file.
25
+ */
26
+ declare function isSamePage(from: string, to: string): boolean;
27
+ /** The guards on one web contents. */
28
+ declare function guardWebContents(contents: WebContents, opts?: WindowGuardOptions): void;
29
+ /** The guards on every web contents the app creates from now on: the one call each app makes, at startup. */
30
+ declare function guardAllWindows(app: App, opts?: WindowGuardOptions): void;
31
+
32
+ export { type CspDirectives, FAMILY_CSP, type WindowGuardOptions, cspOfPage, familyCsp, guardAllWindows, guardWebContents, isSamePage };
@@ -0,0 +1,67 @@
1
+ // src/window-guard.ts
2
+ var FAMILY_CSP = {
3
+ "default-src": ["'self'"],
4
+ "style-src": ["'self'", "'unsafe-inline'"],
5
+ "img-src": ["'self'", "data:"],
6
+ "object-src": ["'none'"],
7
+ "base-uri": ["'none'"]
8
+ };
9
+ function familyCsp(extra = {}) {
10
+ const merged = /* @__PURE__ */ new Map();
11
+ for (const [name, sources] of Object.entries(FAMILY_CSP)) merged.set(name, [...sources]);
12
+ for (const [name, sources] of Object.entries(extra)) {
13
+ const into = merged.get(name) ?? [...FAMILY_CSP["default-src"] ?? []];
14
+ for (const s of sources) if (!into.includes(s)) into.push(s);
15
+ merged.set(name, into);
16
+ }
17
+ return [...merged].map(([name, sources]) => `${name} ${sources.join(" ")}`).join("; ");
18
+ }
19
+ function cspOfPage(html) {
20
+ for (const tag of html.match(/<meta\b[^>]*>/gi) ?? []) {
21
+ if (!/http-equiv\s*=\s*["']Content-Security-Policy["']/i.test(tag)) continue;
22
+ const content = /\bcontent\s*=\s*"([^"]*)"/i.exec(tag) ?? /\bcontent\s*=\s*'([^']*)'/i.exec(tag);
23
+ return content ? content[1].trim() : null;
24
+ }
25
+ return null;
26
+ }
27
+ var EXTERNAL = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
28
+ var parse = (url) => {
29
+ try {
30
+ return new URL(url);
31
+ } catch {
32
+ return null;
33
+ }
34
+ };
35
+ function isSamePage(from, to) {
36
+ const a = parse(from), b = parse(to);
37
+ if (!a || !b || a.protocol !== b.protocol) return false;
38
+ if (a.protocol === "http:" || a.protocol === "https:") return a.origin === b.origin;
39
+ return a.host === b.host && a.pathname === b.pathname;
40
+ }
41
+ function guardWebContents(contents, opts = {}) {
42
+ const away = (what, url) => {
43
+ opts.onBlocked?.(what, url);
44
+ const target = parse(url);
45
+ if (target && EXTERNAL.has(target.protocol)) opts.openExternal?.(url);
46
+ };
47
+ contents.on("will-navigate", (event, url) => {
48
+ if (isSamePage(contents.getURL(), url)) return;
49
+ event.preventDefault();
50
+ away("navigate", url);
51
+ });
52
+ contents.setWindowOpenHandler(({ url }) => {
53
+ away("window", url);
54
+ return { action: "deny" };
55
+ });
56
+ contents.on("will-attach-webview", (event) => {
57
+ event.preventDefault();
58
+ opts.onBlocked?.("webview", "");
59
+ });
60
+ }
61
+ function guardAllWindows(app, opts = {}) {
62
+ app.on("web-contents-created", (_event, contents) => guardWebContents(contents, opts));
63
+ }
64
+
65
+ export { FAMILY_CSP, cspOfPage, familyCsp, guardAllWindows, guardWebContents, isSamePage };
66
+ //# sourceMappingURL=window-guard.js.map
67
+ //# sourceMappingURL=window-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/window-guard.ts"],"names":[],"mappings":"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","file":"window-guard.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Window hardening for both apps (patterkit design/family-parity-2026-10.md\n// item 12): one content security policy, and guards on navigation and\n// window.open for every window the app makes.\n//\n// The policy is a `<meta http-equiv=\"Content-Security-Policy\">` on each page,\n// since the apps load their pages from files in a release build and a header\n// cannot reach those. `familyCsp` is the one spelling of it: a page's meta\n// carries `familyCsp(...)` for that page, and each app's tests check every page\n// against it, so a new page cannot ship without one. Styles allow\n// 'unsafe-inline' because both editors set inline styles (ProseMirror does, in\n// Patterpad); scripts never do.\n//\n// The guards: a window never navigates away from the page it loaded, never\n// opens another window, and never attaches a webview. A link to the web asks\n// the app to open it in the browser instead (`openExternal`), and nothing else\n// leaves the app. `guardAllWindows` applies them to every web contents the app\n// creates, tool windows and any later window included, which is why it is the\n// one call each app makes, at startup.\n//\n// No Electron import: the module takes the app and the web contents it is\n// handed, so it runs under test without Electron.\n// ---------------------------------------------------------------------------\n\nimport type { App, WebContents } from \"electron\";\n\n/** A policy as directives: each name with its sources. */\nexport type CspDirectives = Record<string, readonly string[]>;\n\n/** The family's base policy: everything from the app itself, inline styles, and images also from data URLs. */\nexport const FAMILY_CSP: CspDirectives = {\n \"default-src\": [\"'self'\"],\n \"style-src\": [\"'self'\", \"'unsafe-inline'\"],\n \"img-src\": [\"'self'\", \"data:\"],\n \"object-src\": [\"'none'\"],\n \"base-uri\": [\"'none'\"],\n};\n\n/**\n * The family's policy with a page's additions, as the meta's `content`. An addition to a directive the base\n * has extends it (`{ \"img-src\": [\"blob:\"] }` keeps `'self' data:`); a new directive is added. Directives come\n * out in the base's order, then the page's.\n */\nexport function familyCsp(extra: CspDirectives = {}): string {\n const merged = new Map<string, string[]>();\n for (const [name, sources] of Object.entries(FAMILY_CSP)) merged.set(name, [...sources]);\n for (const [name, sources] of Object.entries(extra)) {\n // A directive the base leaves out falls back to default-src, so an addition starts from that.\n const into = merged.get(name) ?? [...(FAMILY_CSP[\"default-src\"] ?? [])];\n for (const s of sources) if (!into.includes(s)) into.push(s);\n merged.set(name, into);\n }\n return [...merged].map(([name, sources]) => `${name} ${sources.join(\" \")}`).join(\"; \");\n}\n\n/** The policy a page's HTML carries in its meta, or null when it has none. */\nexport function cspOfPage(html: string): string | null {\n for (const tag of html.match(/<meta\\b[^>]*>/gi) ?? []) {\n if (!/http-equiv\\s*=\\s*[\"']Content-Security-Policy[\"']/i.test(tag)) continue;\n const content = /\\bcontent\\s*=\\s*\"([^\"]*)\"/i.exec(tag) ?? /\\bcontent\\s*=\\s*'([^']*)'/i.exec(tag);\n return content ? content[1]!.trim() : null;\n }\n return null;\n}\n\nexport interface WindowGuardOptions {\n /** Open a web address in the browser (Electron's `shell.openExternal`). Given, a link to an http, https or\n * mailto address that the page tried to navigate to or open goes here; absent, it goes nowhere. */\n openExternal?: (url: string) => void;\n /** Told of everything refused, for a dev console. */\n onBlocked?: (what: \"navigate\" | \"window\" | \"webview\", url: string) => void;\n}\n\nconst EXTERNAL = new Set([\"http:\", \"https:\", \"mailto:\"]);\n\nconst parse = (url: string): URL | null => { try { return new URL(url); } catch { return null; } };\n\n/**\n * Whether `to` is the page `from` already is: the same document, a hash or query apart. A dev server's page\n * may move within its own origin (a reload, its own client); a file page may not move to another file.\n */\nexport function isSamePage(from: string, to: string): boolean {\n const a = parse(from), b = parse(to);\n if (!a || !b || a.protocol !== b.protocol) return false;\n if (a.protocol === \"http:\" || a.protocol === \"https:\") return a.origin === b.origin;\n return a.host === b.host && a.pathname === b.pathname;\n}\n\n/** The guards on one web contents. */\nexport function guardWebContents(contents: WebContents, opts: WindowGuardOptions = {}): void {\n const away = (what: \"navigate\" | \"window\", url: string): void => {\n opts.onBlocked?.(what, url);\n const target = parse(url);\n if (target && EXTERNAL.has(target.protocol)) opts.openExternal?.(url);\n };\n contents.on(\"will-navigate\", (event, url) => {\n if (isSamePage(contents.getURL(), url)) return;\n event.preventDefault();\n away(\"navigate\", url);\n });\n contents.setWindowOpenHandler(({ url }) => {\n away(\"window\", url);\n return { action: \"deny\" };\n });\n contents.on(\"will-attach-webview\", (event) => {\n event.preventDefault();\n opts.onBlocked?.(\"webview\", \"\");\n });\n}\n\n/** The guards on every web contents the app creates from now on: the one call each app makes, at startup. */\nexport function guardAllWindows(app: App, opts: WindowGuardOptions = {}): void {\n app.on(\"web-contents-created\", (_event, contents) => guardWebContents(contents, opts));\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildwinter/app-shell",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "The shared shell for the editor family (Patterpad, Storylet Studio): the app-agnostic design-token grammar, the per-entity colour hash, and the vanilla-TS DOM primitive. Two palettes, one grammar - each app supplies its own palette/fonts over these tokens.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,6 +35,11 @@
35
35
  "import": "./dist/value-control.js",
36
36
  "require": "./dist/value-control.cjs"
37
37
  },
38
+ "./window-guard": {
39
+ "types": "./dist/window-guard.d.ts",
40
+ "import": "./dist/window-guard.js",
41
+ "require": "./dist/window-guard.cjs"
42
+ },
38
43
  "./dialog": {
39
44
  "types": "./dist/dialog.d.ts",
40
45
  "import": "./dist/dialog.js",
@@ -1,84 +0,0 @@
1
- import { el } from './chunk-W4WC4FO5.js';
2
-
3
- // src/value-control.ts
4
- function fallbackText(decl) {
5
- const d = decl.default;
6
- if (d !== void 0 && d !== null) {
7
- if (typeof d === "boolean") return d ? "True" : "False";
8
- if (Array.isArray(d)) return d.length === 0 ? "none" : d.join(", ");
9
- return d === "" ? "none" : String(d);
10
- }
11
- switch (decl.type) {
12
- case "boolean":
13
- return "False";
14
- case "number":
15
- return "0";
16
- case "enum":
17
- return decl.values?.[0] ?? "none";
18
- case "quality":
19
- return decl.stages?.[0] ?? "first stage";
20
- case "flags":
21
- return "none";
22
- default:
23
- return "empty";
24
- }
25
- }
26
- var defaultLabel = (decl) => `default (${fallbackText(decl)})`;
27
- function stageGateNote(gates) {
28
- if (gates <= 0) return "";
29
- return gates === 1 ? "1 gate compares against this ladder, and it now reads the new order." : `${gates} gates compare against this ladder, and each now reads the new order.`;
30
- }
31
- function rungStrip(opts) {
32
- const make = opts.doc === void 0 ? el : (tag, props) => {
33
- const node = opts.doc.createElement(tag);
34
- node.className = props.className;
35
- if (props.text !== void 0) node.textContent = props.text;
36
- if (props.tip !== void 0) {
37
- node.title = props.tip;
38
- node.setAttribute("aria-label", props.tip);
39
- }
40
- if (props.onClick) node.addEventListener("click", props.onClick);
41
- if (tag === "button") node.type = "button";
42
- return node;
43
- };
44
- const strip = make("span", { className: "seg shell-rungs" });
45
- const decl = opts.decl ?? { type: "quality", stages: opts.stages };
46
- const unset = opts.value === void 0;
47
- const fallback = typeof decl.default === "string" && opts.stages.includes(decl.default) ? decl.default : opts.stages[0];
48
- const readOnly = opts.onPick === void 0;
49
- if (opts.onClear) {
50
- strip.append(make("button", {
51
- className: `seg-opt shell-rung shell-rung-default${unset ? " on" : ""}`,
52
- text: defaultLabel(decl),
53
- tip: unset ? "Not set here: the declared default holds" : "Clear the value, back to the default",
54
- onClick: () => {
55
- if (!unset) opts.onClear();
56
- }
57
- }));
58
- }
59
- for (const stage of opts.stages) {
60
- const current = opts.value === stage;
61
- const falls = unset && stage === fallback;
62
- const rung = make(readOnly ? "span" : "button", {
63
- className: `seg-opt shell-rung${current ? " on" : ""}${falls ? " fallback" : ""}`,
64
- text: stage,
65
- tip: current ? "The current stage" : falls ? "The stage it falls back to" : readOnly ? stage : opts.pickTip?.(stage) ?? `Set to "${stage}"`,
66
- ...readOnly ? {} : { onClick: () => {
67
- if (!current) opts.onPick(stage);
68
- } }
69
- });
70
- strip.append(rung);
71
- }
72
- if (!unset && !opts.stages.includes(opts.value)) {
73
- strip.append(make("span", {
74
- className: "seg-opt shell-rung stray on",
75
- text: opts.value,
76
- tip: `"${opts.value}" is not one of the stages: pick the one it belongs at now`
77
- }));
78
- }
79
- return strip;
80
- }
81
-
82
- export { defaultLabel, fallbackText, rungStrip, stageGateNote };
83
- //# sourceMappingURL=chunk-EAE7UWQ5.js.map
84
- //# sourceMappingURL=chunk-EAE7UWQ5.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/value-control.ts"],"names":[],"mappings":"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","file":"chunk-EAE7UWQ5.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Typed value controls: how both apps show and set a value of a declared type.\n//\n// The first is the quality's. A quality is a step on a ladder, so its value is\n// shown as the LADDER, with the current rung marked, rather than a dropdown that\n// shows one name and hides where it stands (patterkit design/proposals/\n// patter-qualities.md section 4). Storyletter's Board drew it this way first;\n// it lives here so Storyletter's Fields tab, Patterpad's play panel and Game\n// Data rows, and the play inspectors all draw the same strip.\n//\n// An UNSET value (a field or game-data value the node leaves out) names what it\n// falls back to, \"default (x)\", by the family's one rule: the declaration's own\n// default, else the type's (a quality's first stage). A value that is no longer\n// a stage (a deleted one) is shown as such, so it can be put back on the ladder.\n//\n// No Electron, no app state: this module is DOM only, and is its own entry point\n// (`@wildwinter/app-shell/value-control`) so a play inspector outside the apps\n// can take it.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\n\n/** A declaration as these controls read it: a property's, a field's or a game-data field's. */\nexport interface ValueDeclLike {\n type: string;\n default?: unknown;\n /** enum / flags */\n values?: readonly string[];\n /** quality: the ladder, in order */\n stages?: readonly string[];\n}\n\n/**\n * What an unset value of `decl` falls back to, as the words a control shows: its own default where it has\n * one, else its type's (False, 0, the first value, no flags, the first stage, empty text). The kernel's\n * `defaultFor`, in words; a host reads the same value from the bundle.\n */\nexport function fallbackText(decl: ValueDeclLike): string {\n const d = decl.default;\n if (d !== undefined && d !== null) {\n if (typeof d === \"boolean\") return d ? \"True\" : \"False\";\n if (Array.isArray(d)) return d.length === 0 ? \"none\" : d.join(\", \");\n return d === \"\" ? \"none\" : String(d);\n }\n switch (decl.type) {\n case \"boolean\": return \"False\";\n case \"number\": return \"0\";\n case \"enum\": return decl.values?.[0] ?? \"none\";\n case \"quality\": return decl.stages?.[0] ?? \"first stage\";\n case \"flags\": return \"none\";\n default: return \"empty\";\n }\n}\n\n/** The label an unset value shows: \"default (x)\", naming what it falls back to. */\nexport const defaultLabel = (decl: ValueDeclLike): string => `default (${fallbackText(decl)})`;\n\n/**\n * The quiet line under a ladder that has just been reordered: how many gates order against it. A reorder\n * changes no value, and is the one edit that changes what a gate MEANS without saying so (patter-qualities.md\n * section 2), so the editor says so. Empty when no gate orders the quality.\n */\nexport function stageGateNote(gates: number): string {\n if (gates <= 0) return \"\";\n return gates === 1\n ? \"1 gate compares against this ladder, and it now reads the new order.\"\n : `${gates} gates compare against this ladder, and each now reads the new order.`;\n}\n\nexport interface RungStripOptions {\n /** The ladder, in order. */\n stages: readonly string[];\n /** The value held, or undefined when unset: a field or game-data value the node leaves out. */\n value: string | undefined;\n /** The declaration, for what an unset value falls back to (its default, else the first stage). */\n decl?: ValueDeclLike;\n /** Pick a stage. Absent: the strip is read-only. */\n onPick?: (stage: string) => void;\n /** Clear back to unset. With it, the strip leads with \"default (x)\", which clears the value. */\n onClear?: () => void;\n /** The rollover on a rung that is not the current one (default: `Set to \"x\"`). */\n pickTip?: (stage: string) => string;\n /**\n * The document to build in, for a host outside the apps (a play inspector in a page or a frame of its\n * own). Given, rollovers are plain `title`s and the family's themed tooltips are not mounted; the host\n * styles `.seg`, `.seg-opt` and `.shell-rung` itself. Absent: the apps' document and themed rollovers.\n */\n doc?: Document;\n}\n\n/**\n * A quality's value as its ladder: one rung per stage, in order, the current one marked (`on`). Unset, the\n * rung it falls back to is marked as the fallback (`fallback`), and with `onClear` a leading \"default (x)\"\n * option is the current choice. A value that is not a stage (a deleted one) is shown after the ladder,\n * marked `stray`, until a rung is picked.\n */\nexport function rungStrip(opts: RungStripOptions): HTMLElement {\n const make = opts.doc === undefined ? el : (tag: \"span\" | \"button\", props: { className: string; text?: string; tip?: string; onClick?: () => void }) => {\n const node = opts.doc!.createElement(tag);\n node.className = props.className;\n if (props.text !== undefined) node.textContent = props.text;\n if (props.tip !== undefined) { node.title = props.tip; node.setAttribute(\"aria-label\", props.tip); }\n if (props.onClick) node.addEventListener(\"click\", props.onClick);\n if (tag === \"button\") (node as HTMLButtonElement).type = \"button\";\n return node;\n };\n const strip = make(\"span\", { className: \"seg shell-rungs\" });\n const decl: ValueDeclLike = opts.decl ?? { type: \"quality\", stages: opts.stages };\n const unset = opts.value === undefined;\n const fallback = typeof decl.default === \"string\" && opts.stages.includes(decl.default) ? decl.default : opts.stages[0];\n const readOnly = opts.onPick === undefined;\n if (opts.onClear) {\n strip.append(make(\"button\", {\n className: `seg-opt shell-rung shell-rung-default${unset ? \" on\" : \"\"}`, text: defaultLabel(decl),\n tip: unset ? \"Not set here: the declared default holds\" : \"Clear the value, back to the default\",\n onClick: () => { if (!unset) opts.onClear!(); },\n }));\n }\n for (const stage of opts.stages) {\n const current = opts.value === stage;\n const falls = unset && stage === fallback;\n const rung = make(readOnly ? \"span\" : \"button\", {\n className: `seg-opt shell-rung${current ? \" on\" : \"\"}${falls ? \" fallback\" : \"\"}`, text: stage,\n tip: current ? \"The current stage\" : falls ? \"The stage it falls back to\" : readOnly ? stage : (opts.pickTip?.(stage) ?? `Set to \"${stage}\"`),\n ...(readOnly ? {} : { onClick: () => { if (!current) opts.onPick!(stage); } }),\n });\n strip.append(rung);\n }\n if (!unset && !opts.stages.includes(opts.value!)) {\n strip.append(make(\"span\", {\n className: \"seg-opt shell-rung stray on\", text: opts.value!,\n tip: `\"${opts.value}\" is not one of the stages: pick the one it belongs at now`,\n }));\n }\n return strip;\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/settings.ts","../src/property-names.ts","../src/property-name-field.ts","../src/property-list.ts"],"names":[],"mappings":"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","file":"chunk-FGAPO4N5.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The shared settings surface: a native <dialog> with a left-rail of grouped\n// tabs and one panel per section (mountSettingsDialog), plus the list-editing\n// core those sections lean on (expandableRow, focusNewRow, dupGuard). The app\n// supplies the sections; each holds its own state and reports the first invalid\n// control so the Save gate can block + jump. Framework here, content in the app.\n//\n// Lifted from Patterpad's Project Settings (settings-list.ts + the inlined\n// dialog logic in its renderer), with neutral class names. Styling ships as\n// settings.css.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\n\n// --- list-editing core --------------------------------------------------------\n\n/** A compact list row: a single `.set-rowline` (inline controls) plus, when\n * `details` are given, a disclosure that reveals a `.set-details` panel below,\n * so a list scans like a table with secondary fields tucked away. `name` is\n * what the row declares (a property's name, a field's), stamped on it as\n * `data-name` so `revealRow` can land on it. */\nexport function expandableRow(opts: { line: HTMLElement[]; details?: HTMLElement[]; name?: string }): HTMLElement {\n const row = el(\"div\", \"set-row\");\n if (opts.name !== undefined) row.dataset.name = opts.name;\n const line = el(\"div\", \"set-rowline\");\n if (opts.details && opts.details.length) {\n const details = el(\"div\", \"set-details\"); details.hidden = true;\n for (const d of opts.details) details.append(d);\n // The triangle is drawn by CSS off `.open` (a text glyph stays hairline-thin).\n const toggle = el(\"button\", \"set-expand\"); toggle.type = \"button\";\n // `data-tip`, not `title`: this package ships a themed tooltip and nothing\n // else in it raises an OS rollover. A lone `title` here would be the one\n // place a host's chrome broke character, and on the platform's own delay.\n toggle.dataset.tip = \"More\"; toggle.setAttribute(\"aria-label\", \"More\");\n toggle.setAttribute(\"aria-expanded\", \"false\");\n toggle.addEventListener(\"click\", () => {\n const open = details.hidden;\n details.hidden = !open;\n toggle.setAttribute(\"aria-expanded\", String(open));\n toggle.classList.toggle(\"open\", open);\n });\n line.append(toggle);\n opts.line.forEach((c) => line.append(c));\n row.append(line, details);\n } else {\n opts.line.forEach((c) => line.append(c));\n row.append(line);\n }\n return row;\n}\n\n/**\n * Land on one row of a list by the `name` it was built with: open its details\n * (an enum's values sit behind the disclosure, and they are usually what the\n * jump was for), bring it to the middle of its scroller, and light it for a\n * moment (`.set-row.landed` in settings.css). \"Go to definition\" in both apps\n * used to open the page a property is declared on and stop there, with the\n * declaration below the fold and nothing marking it (Storyletter, 2026-09-14).\n *\n * Returns false when no row carries the name, so a host whose page fills in\n * asynchronously can ask again on a later frame; asking again is the host's,\n * since only the host knows what opens on its own time. The row is matched by\n * reading the stamp rather than by an attribute selector, so a name needs no\n * escaping.\n */\nexport function revealRow(within: ParentNode, name: string): boolean {\n const row = [...within.querySelectorAll<HTMLElement>(\".set-row\")].find((r) => r.dataset.name === name);\n if (!row) return false;\n const toggle = row.querySelector<HTMLButtonElement>(\".set-rowline > .set-expand\");\n if (toggle && toggle.getAttribute(\"aria-expanded\") !== \"true\") toggle.click();\n const still = typeof matchMedia === \"function\" && matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n row.scrollIntoView({ block: \"center\", behavior: still ? \"auto\" : \"smooth\" });\n // Restarted, so landing on the same row twice lights it twice.\n row.classList.remove(\"landed\");\n void row.offsetWidth;\n row.classList.add(\"landed\");\n row.addEventListener(\"animationend\", () => row.classList.remove(\"landed\"), { once: true });\n return true;\n}\n\n/**\n * `revealRow`, retried over the next frames until the row exists or the tries\n * run out: for a page that fills in on its own time after a navigation. Both\n * apps hand-rolled this as `landOn` with the same twelve tries\n * (ui-review-2026-09, finding 29). Resolves true when it landed, false when it\n * gave up, and giving up is quiet: a name no page shows as a row is not a fault.\n */\nexport function revealRowWhenReady(within: ParentNode, name: string, tries = 12): Promise<boolean> {\n return new Promise((resolve) => {\n const attempt = (left: number): void => {\n if (revealRow(within, name)) { resolve(true); return; }\n if (left <= 0) { resolve(false); return; }\n requestAnimationFrame(() => attempt(left - 1));\n };\n attempt(tries);\n });\n}\n\n/** After a \"+ Add\" re-renders a list, bring the new (last) row into view and\n * focus its name input (`.set-name`, else the first input). */\nexport function focusNewRow(listEl: HTMLElement | null | undefined): void {\n const last = listEl?.lastElementChild;\n if (!(last instanceof HTMLElement)) return;\n last.scrollIntoView({ block: \"nearest\" });\n const name = last.querySelector<HTMLInputElement>(\"input.set-name\") ?? last.querySelector<HTMLInputElement>(\"input\");\n name?.focus();\n}\n\nexport interface DupGuard {\n /** Forget the previous render's inputs (call at the top of render). */\n reset(): void;\n /** Register a name input; `key` returns the comparison string (default the\n * input's value). Re-checks live on input. `also` = extra inputs that change the key. */\n track(input: HTMLInputElement, key?: () => string, also?: HTMLInputElement[]): void;\n /** Re-paint duplicates; true if any two non-blank keys collide. */\n check(): boolean;\n /** The first input currently flagged (after a fresh check), or null. */\n firstDuplicate(): HTMLInputElement | null;\n}\n\nconst DUP_MESSAGE = \"Another row already has this name.\";\n\n/** Case-insensitive duplicate-name detection: paints clashing inputs `.invalid`\n * and lets a Save gate block + jump via `firstDuplicate()`. */\nexport function dupGuard(): DupGuard {\n const items: Array<{ input: HTMLInputElement; key: () => string }> = [];\n const check = (): boolean => {\n const counts = new Map<string, number>();\n for (const it of items) { const k = it.key().trim().toLowerCase(); if (k) counts.set(k, (counts.get(k) ?? 0) + 1); }\n let any = false;\n for (const it of items) {\n const k = it.key().trim().toLowerCase();\n const dup = !!k && (counts.get(k) ?? 0) > 1;\n it.input.classList.toggle(\"invalid\", dup);\n // The reason rides `data-tip` (the themed rollover, and what the Save\n // gate's message channel reads), never `title`.\n if (dup) { it.input.dataset[\"tip\"] = DUP_MESSAGE; any = true; }\n else if (it.input.dataset[\"tip\"] === DUP_MESSAGE) delete it.input.dataset[\"tip\"];\n }\n return any;\n };\n return {\n reset() { items.length = 0; },\n track(input, key = () => input.value, also) {\n items.push({ input, key });\n input.addEventListener(\"input\", check);\n for (const a of also ?? []) a.addEventListener(\"input\", check);\n },\n check,\n firstDuplicate() { check(); return items.find((it) => it.input.classList.contains(\"invalid\"))?.input ?? null; },\n };\n}\n\n// --- the settings dialog ------------------------------------------------------\n\nexport interface SettingsSectionHandle {\n /** The first invalid control to jump to, blocking Save (e.g. a duplicate\n * name). Return `{ el, message }` to say WHICH fault in the error line;\n * a bare element says it through its own `data-tip` (the dup guard's, a\n * property-name field's), and the line falls back to a generic sentence\n * when it has neither. */\n firstInvalid?(): HTMLElement | { el: HTMLElement; message?: string } | null;\n}\n\nexport interface SettingsSection {\n id: string;\n label: string;\n /** Optional left-rail group heading above this tab. */\n group?: string;\n /** Build the section body into `host`; called fresh on each open so it reads\n * the current data. Return a handle (for the Save gate). */\n mount(host: HTMLElement): SettingsSectionHandle;\n /** Why this tab cannot be used right now, or null when it can. A reason\n * renders the tab dimmed and inert with the reason as its rollover\n * (Patterpad's Audio tab while the project is not voiced). Read on open\n * and on `refreshTabs()`, so a control inside the dialog can flip it. */\n disabled?: () => string | null;\n}\n\nexport interface SettingsDialogOptions {\n title: string;\n sections: SettingsSection[];\n /** Fired when Save is clicked and every section validates. The app reads its\n * own section state and persists. May be async; the dialog stays open until\n * it resolves, then closes.\n *\n * Return `false` (or a Promise of it) when the save was REFUSED, a version\n * control lock say, and the dialog stays open with the author's edits in it\n * for another try; saying why is the app's (a toast, usually). A throw, or a\n * rejected Promise, is treated the same way rather than surfacing as an\n * unhandled rejection. Anything else closes the dialog, as it always has. */\n onSave: () => void | boolean | Promise<void | boolean>;\n}\n\nexport interface SettingsDialog {\n /** Open (optionally on a given tab id). Sections re-mount from current data. */\n open(tabId?: string): void;\n /** Re-read every section's `disabled()`; a tab that has just become\n * disabled while showing hands over to the first usable one. */\n refreshTabs(): void;\n destroy(): void;\n}\n\n/** Build a settings dialog. The frame owns the modal, the tab rail, save-on-Save\n * (with a validation gate), and Cancel; the sections own their content + state. */\nexport function mountSettingsDialog(opts: SettingsDialogOptions): SettingsDialog {\n const dialog = el(\"dialog\", \"settings-dialog\");\n const form = el(\"form\", \"settings-form\"); form.method = \"dialog\";\n const tabs = el(\"nav\", \"settings-tabs\");\n const panels = el(\"div\", \"settings-panels\");\n const error = el(\"p\", \"settings-error\"); error.hidden = true;\n const cancel = el(\"button\", \"settings-cancel\", \"Cancel\"); cancel.type = \"button\"; cancel.value = \"cancel\";\n const save = el(\"button\", \"settings-save primary\", \"Save\"); save.value = \"save\";\n\n const panelFor = new Map<string, HTMLElement>();\n const tabFor = new Map<string, HTMLButtonElement>();\n const handles = new Map<string, SettingsSectionHandle>();\n let active = opts.sections[0]?.id ?? \"\";\n\n let lastGroup: string | undefined;\n for (const s of opts.sections) {\n if (s.group && s.group !== lastGroup) { tabs.append(el(\"div\", \"settings-group\", s.group)); lastGroup = s.group; }\n const tab = el(\"button\", \"settings-tab\", s.label); tab.type = \"button\"; tab.dataset[\"tab\"] = s.id;\n // A disabled tab is inert by class, not by the `disabled` attribute: a\n // disabled button swallows the pointer, and the reason has to be readable\n // on hover.\n tab.addEventListener(\"click\", () => { if (!tab.classList.contains(\"is-disabled\")) showTab(s.id); });\n tabs.append(tab); tabFor.set(s.id, tab);\n const panel = el(\"section\", \"settings-panel\"); panel.dataset[\"panel\"] = s.id; panel.hidden = true;\n panels.append(panel); panelFor.set(s.id, panel);\n }\n\n function showTab(id: string): void {\n active = id;\n for (const [sid, t] of tabFor) t.classList.toggle(\"active\", sid === id);\n for (const [sid, p] of panelFor) p.hidden = sid !== id;\n }\n\n const usable = (s: SettingsSection): boolean => !tabFor.get(s.id)?.classList.contains(\"is-disabled\");\n function refreshTabs(): void {\n for (const s of opts.sections) {\n const tab = tabFor.get(s.id)!;\n const reason = s.disabled?.() ?? null;\n tab.classList.toggle(\"is-disabled\", reason !== null);\n if (reason !== null) { tab.dataset[\"tip\"] = reason; tab.setAttribute(\"aria-disabled\", \"true\"); }\n else { delete tab.dataset[\"tip\"]; tab.removeAttribute(\"aria-disabled\"); }\n }\n if (!opts.sections.some((s) => s.id === active && usable(s))) {\n const first = opts.sections.find(usable);\n if (first) showTab(first.id);\n }\n }\n\n form.append(\n el(\"h2\", \"settings-title\", opts.title),\n el(\"div\", \"settings-body\", tabs, panels),\n el(\"div\", \"settings-actions\", error, cancel, save),\n );\n dialog.append(form);\n document.body.append(dialog);\n\n // Enter should not submit while editing a field; only the Save button does.\n form.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" && (e.target as HTMLElement)?.tagName === \"INPUT\") e.preventDefault();\n });\n\n cancel.addEventListener(\"click\", () => dialog.close(\"cancel\"));\n save.addEventListener(\"click\", (e) => {\n e.preventDefault();\n error.hidden = true;\n for (const s of opts.sections) {\n const found = handles.get(s.id)?.firstInvalid?.();\n if (!found) continue;\n const bad = found instanceof HTMLElement ? found : found.el;\n const message = (found instanceof HTMLElement ? undefined : found.message) ?? bad.dataset[\"tip\"];\n showTab(s.id);\n bad.focus();\n // The field's own rollover says which fault it is, so the line repeats it\n // rather than inventing a summary; the generic sentence is the fallback.\n error.textContent = message || \"Fix the highlighted fields first.\";\n error.hidden = false;\n return;\n }\n // The dialog closes only on a save that went through. It used to close\n // whatever happened, so a save refused under a lock took the author's edits\n // away with it. Save is off while one is in flight, so a second click\n // cannot start a second save.\n save.disabled = true;\n void (async () => {\n let saved: unknown;\n try { saved = await opts.onSave(); } catch (err) { saved = false; console.error(\"Settings save failed:\", err); }\n save.disabled = false;\n if (saved !== false) dialog.close(\"save\");\n })();\n });\n\n return {\n open(tabId) {\n handles.clear();\n for (const s of opts.sections) {\n const panel = panelFor.get(s.id)!;\n panel.replaceChildren();\n handles.set(s.id, s.mount(panel));\n }\n error.hidden = true;\n showTab(tabId ?? active ?? opts.sections[0]?.id ?? \"\");\n refreshTabs(); // a disabled tab asked for by id hands over to the first usable one\n dialog.showModal();\n },\n refreshTabs,\n destroy() { dialog.remove(); },\n };\n}\n","// ---------------------------------------------------------------------------\n// Property names: what an author may call a value the story reads and writes.\n//\n// This belongs to the shell for the same reason gameIds do (see ids.ts): it\n// follows from the SHAPE these apps share - a project of data files, authored in\n// an editor, compiled for something else to run - rather than from any one\n// subject. Nothing here needs the words card, beat, hand or scene.\n//\n// UNLIKE gameIds, the rule is not a house style. It is dictated by the\n// expression language both families embed (`@wildwinter/expr`), and every clause\n// below is a failure observed by compiling one:\n//\n// parser.ts tokenises an identifier as /[a-zA-Z_][a-zA-Z0-9_]*/ and then\n// FOLDS it: `name: propTok.value.toLowerCase()`.\n//\n// @patter.is_night -> [\"sv\",\"patter\",\"is_night\"] reachable\n// @patter.isNight -> [\"sv\",\"patter\",\"isnight\"] folded: a declaration\n// spelled `isNight` is\n// one nothing can reach\n// @patter.is-night -> [\"bin\",\"-\",[\"sv\",\"patter\",\"is\"], NOT an error: it is\n// [\"s\",\"night\"]] SUBTRACTION. A hyphen\n// silently compiles to\n// a different meaning\n// @patter.is night -> parse error\n// @patter.9lives -> parse error (a name cannot start with a digit)\n// @patter.not -> parse error (`true false and or not` are keywords)\n//\n// The hyphen line is why this is worth a shared rule rather than a per-app\n// check: every other violation is loud, and that one quietly means something\n// else. It is also why a property name is NOT a gameId with different spelling -\n// a gameId is built out of hyphens, and here a hyphen is an operator.\n//\n// DEPENDENCY-FREE on purpose, ids.ts's reasoning verbatim: the apps' compilers,\n// CLIs and embedded runtimes need these rules far from any UI, so they keep\n// their own copies and assert parity with these in a test.\n// ---------------------------------------------------------------------------\n\n/** The five words `@wildwinter/expr` lexes as keywords, so a property may not be\n * called any of them: the reference does not resolve, it fails to parse. Kept\n * here as a copy for the same reason the rules are (no dependency), and each\n * app probes the real parser to prove the list is still true. */\nexport const RESERVED_PROPERTY_NAMES: readonly string[] = [\"true\", \"false\", \"and\", \"or\", \"not\"];\n\n/** What an editor should tell an author, in one line. */\nexport const PROPERTY_NAME_HINT =\n \"Lower case letters, digits, and underscores. It can't start with a digit. No hyphens or spaces.\";\n\n/**\n * Coerce a label into a legal property name: lower case, apostrophes dropped,\n * runs of anything else to a single underscore, no trailing underscore, an\n * underscore in front if it would otherwise start with a digit, and one behind\n * if it would otherwise be a keyword.\n *\n * A LEADING underscore is kept where the author typed one, because `_private` is\n * a legal name and a meaningful choice; only the ends that would be illegal are\n * touched.\n *\n * May return \"\", when there was nothing usable in the input. Callers treat that\n * as \"no name yet\" rather than as a name.\n */\nexport function propertyNameify(text: string): string {\n const trimmed = text.trim();\n // An underscore the author actually typed is kept; one that is only the ghost of\n // leading punctuation is not. That is the difference between `_private` and `!gold`.\n const deliberateLeading = trimmed.startsWith(\"_\");\n let out = trimmed.toLowerCase().replace(/['’]/g, \"\")\n .replace(/[^a-z0-9_]+/g, \"_\").replace(/_+/g, \"_\").replace(/^_+|_+$/g, \"\");\n if (out === \"\") return \"\";\n if (deliberateLeading || /^[0-9]/.test(out)) out = `_${out}`;\n if (RESERVED_PROPERTY_NAMES.includes(out)) out = `${out}_`;\n return out;\n}\n\n/**\n * Is this a name the expression language can actually reach? Lower case letters,\n * digits and underscores, not starting with a digit, and not a keyword.\n *\n * \"\" is NOT legal: an unnamed declaration is an unfinished row, which is a\n * different thing from an illegal one, and callers distinguish them.\n */\nexport function isValidPropertyName(name: string): boolean {\n return /^[a-z_][a-z0-9_]*$/.test(name) && !RESERVED_PROPERTY_NAMES.includes(name);\n}\n\n/**\n * The one thing that is only a case difference, e.g. `isNight` -> `isnight`.\n *\n * Worth its own question because it is the only violation that can be repaired\n * without guessing at intent: the language already folds every REFERENCE, so\n * folding the declaration to match changes nothing observable, where inventing\n * an underscore for a space is a decision about what the author meant. Loaders\n * fold this class silently and report the rest.\n */\nexport function isCaseOnlyPropertyName(name: string): boolean {\n return !isValidPropertyName(name) && isValidPropertyName(name.toLowerCase());\n}\n","// ---------------------------------------------------------------------------\n// The manners for a field that names a declared property.\n//\n// The RULE is property-names.ts, which is dependency-free because each app's\n// domain layer keeps its own copy of it. This is the other half: how a field\n// BEHAVES while somebody types a name into it, which only UI touches, so the\n// shell owns it outright.\n//\n// The manners are the gameId editor's, adopted rather than re-argued (see\n// id-editor.ts): an illegal name is marked and blocks Save, and Tab coerces it,\n// in the field, where it can be seen and undone. Nothing is rewritten as it is\n// typed. Patterpad's first pass at this folded every keystroke to lower case;\n// that is the manner the id editor had already rejected, on the grounds that an\n// app which silently renames a thing has made a decision that was not its to\n// make, and a property name is exactly that kind of thing: the author is naming\n// something their own conditions will call.\n//\n// It arrived here as two copies, one per app, written in one session and already\n// carrying the same forty lines of fault wording. Lifting them before they could\n// drift is the whole reason this file exists.\n// ---------------------------------------------------------------------------\n\nimport { isValidPropertyName, propertyNameify, RESERVED_PROPERTY_NAMES } from \"./property-names.js\";\n\n/** Re-check callbacks for bound REFERENCE fields, so a host can say \"the declarations\n * changed\" without holding a handle per row. */\nconst refSyncs = new WeakMap<HTMLInputElement, () => void>();\n\n/**\n * One line saying what is wrong with this name, or undefined when nothing is.\n *\n * Each branch names what actually HAPPENS rather than reciting the rule, because\n * the person reading it is usually somebody wondering why a condition does\n * nothing. Kept to one line: it lands in a rollover on the field, and hosts\n * repeat it in the Save error rather than inventing a summary of their own.\n */\nexport function propertyNameProblem(name: string): string | undefined {\n if (isValidPropertyName(name)) return undefined;\n const suggestion = propertyNameify(name);\n const tail = suggestion ? ` Try \"${suggestion}\", or press Tab.` : \"\";\n if (RESERVED_PROPERTY_NAMES.includes(name.toLowerCase())) {\n return `Can't be used. \"${name.toLowerCase()}\" is a keyword in expressions.${tail}`;\n }\n // The one that is not an error anywhere else: `@scope.a-b` compiles to a-minus-b.\n if (name.includes(\"-\")) return `Can't be used. A hyphen reads as subtraction in an expression.${tail}`;\n if (/^[0-9]/.test(name)) return `Can't be used. A name can't start with a digit.${tail}`;\n if (name !== name.toLowerCase()) {\n return `Can't be used. Expressions fold names, so this would look for \"${name.toLowerCase()}\".${tail}`;\n }\n return `Can't be used. Only lower case letters, digits, and underscores.${tail}`;\n}\n\n/**\n * Wire a name field: mark it live, explain the fault in its rollover, and coerce\n * on Tab. `onCommit` receives what the author typed, VERBATIM, on every input.\n *\n * `.illegal` rather than dupGuard's `.invalid`, because both can be true of one\n * field and `dupGuard.check()` owns that class: it would clear this marking every\n * time it ran, including at the moment of a blocked Save.\n *\n * An empty field is not marked. An unfinished row is a different thing from an\n * illegal one, and hosts prune blank names on save.\n */\nexport function bindPropertyName(\n input: HTMLInputElement,\n onCommit: (value: string) => void,\n opts: { hint?: string } = {},\n): void {\n const hint = opts.hint;\n const sync = (): void => {\n const v = input.value.trim();\n const problem = v === \"\" ? undefined : propertyNameProblem(v);\n input.classList.toggle(\"illegal\", !!problem);\n if (problem) input.title = problem;\n else if (input.title.startsWith(\"Can't be used\")) {\n if (hint) input.title = hint;\n else input.removeAttribute(\"title\");\n }\n };\n input.addEventListener(\"input\", () => { onCommit(input.value); sync(); });\n input.addEventListener(\"keydown\", (e) => {\n // The opt-in coercion, the id editor's gesture. Shift+Tab is left alone: it is\n // how somebody leaves a field they have not finished with.\n if (e.key !== \"Tab\" || e.shiftKey) return;\n const typed = input.value.trim();\n if (typed === \"\" || isValidPropertyName(typed)) return;\n const coerced = propertyNameify(typed);\n if (!coerced || coerced === input.value) return;\n e.preventDefault();\n input.value = coerced;\n onCommit(coerced);\n sync();\n });\n sync();\n}\n\n/**\n * The first field in `host` holding a name no expression could reach, for a Save\n * gate. Hosts hand this back beside `dupGuard.firstDuplicate()`: two faults, two\n * sets of words, one gate.\n */\nexport function firstIllegalPropertyName(host: ParentNode): HTMLInputElement | null {\n return host.querySelector<HTMLInputElement>(\"input.illegal\");\n}\n\n// ---------------------------------------------------------------------------\n// Reference fields, which are a different job from declaration fields.\n//\n// A declaration NAMES a property; a reference POINTS at one. The expression\n// parser folds every reference, so `@world.isNight` reaches `isnight` perfectly\n// well and complaining about the capital would be inventing a rule the language\n// does not have. Everything else still bites, and harder: no declaration may\n// contain a hyphen, so `is-night` in a reference can never match anything that\n// exists.\n//\n// And a reference has a fault a declaration cannot have: naming something that\n// was never declared. That is not a grammar question, so the host supplies the\n// list. The cost of getting it wrong is quiet - a coverage driver pointed at a\n// property nobody declared feeds a value nobody reads, the branches it was meant\n// to exercise stay unexercised, and the report shows them as never hit, which\n// reads as \"this content is unreachable\" rather than \"that name is a typo\".\n// ---------------------------------------------------------------------------\n\nexport interface PropertyRefOptions {\n /** The names that exist, read fresh on every check so a declaration added in the\n * same dialog clears the complaint. Compared case-insensitively, since the\n * parser folds. */\n known: () => string[];\n /** The scope token this field addresses, for the wording (\"no @world property...\"). */\n scope?: string;\n /** Shown on the field when there is nothing to complain about. */\n hint?: string;\n}\n\n/** The closest known name, when one is close enough to be worth naming. Deliberately\n * narrow: a wrong suggestion is worse than none, so it wants a shared prefix or a\n * single-character slip, not the least-bad match in the list. */\nfunction nearest(name: string, known: string[]): string | undefined {\n const n = name.toLowerCase();\n const byPrefix = known.filter((k) => k.toLowerCase().startsWith(n) || n.startsWith(k.toLowerCase()));\n if (byPrefix.length === 1) return byPrefix[0];\n let best: string | undefined;\n let bestScore = 3; // strictly better than \"three edits away\"\n for (const k of known) {\n const a = k.toLowerCase();\n if (Math.abs(a.length - n.length) > 2) continue;\n // Levenshtein, small strings, no need for a matrix beyond two rows.\n let prev = Array.from({ length: n.length + 1 }, (_, i) => i);\n for (let i = 1; i <= a.length; i++) {\n const row = [i];\n for (let j = 1; j <= n.length; j++) {\n row[j] = Math.min(prev[j]! + 1, row[j - 1]! + 1, prev[j - 1]! + (a[i - 1] === n[j - 1] ? 0 : 1));\n }\n prev = row;\n }\n const d = prev[n.length]!;\n if (d < bestScore) { bestScore = d; best = k; }\n }\n return best;\n}\n\n/** Why this reference cannot be used, or undefined when it can. */\nexport function propertyRefProblem(name: string, opts: PropertyRefOptions): string | undefined {\n const scope = opts.scope ? `@${opts.scope}` : \"that scope\";\n // Case is NOT a fault here: the parser folds references.\n const folded = name.toLowerCase();\n if (!isValidPropertyName(folded)) {\n const suggestion = propertyNameify(name);\n const tail = suggestion ? ` Try \"${suggestion}\", or press Tab.` : \"\";\n if (RESERVED_PROPERTY_NAMES.includes(folded)) return `Can't be used. \"${folded}\" is a keyword in expressions.${tail}`;\n if (name.includes(\"-\")) return `Can't be used. A hyphen reads as subtraction in an expression.${tail}`;\n if (/^[0-9]/.test(name)) return `Can't be used. A name can't start with a digit.${tail}`;\n return `Can't be used. Only lower case letters, digits, and underscores.${tail}`;\n }\n const known = opts.known();\n if (known.some((k) => k.toLowerCase() === folded)) return undefined;\n const near = nearest(folded, known);\n if (near) return `No ${scope} property is called \"${name}\". Did you mean \"${near}\"?`;\n if (known.length === 0) return `No ${scope} properties are declared yet. Declare one above first.`;\n const shown = known.slice(0, 3).map((k) => `\"${k}\"`).join(\", \");\n const more = known.length > 3 ? `, and ${known.length - 3} more` : \"\";\n return `No ${scope} property is called \"${name}\". Pick one of ${shown}${more}.`;\n}\n\n/**\n * Wire a field that POINTS at a declared property: same manners as a declaration\n * field (mark, do not rewrite, Tab coerces), plus the declared names offered as a\n * datalist, because the rule is declare-then-reference and a picker makes that\n * painless rather than pedantic.\n *\n * Marks `.illegal` for an undeclared name too, which gates Save wherever the host\n * already asks `firstIllegalPropertyName`.\n */\nexport function bindPropertyRef(\n input: HTMLInputElement,\n onCommit: (value: string) => void,\n opts: PropertyRefOptions,\n): void {\n const list = document.createElement(\"datalist\");\n list.id = `shell-propref-${Math.random().toString(36).slice(2, 9)}`;\n input.setAttribute(\"list\", list.id);\n\n // Attached lazily, not once at bind. A datalist has to be IN the document to do anything, and it is\n // attached as a sibling of the input - so binding a field that has not been appended yet (which is\n // the ordinary order for a builder that makes its nodes and then mounts them) silently produced no\n // autocomplete at all: no error, nothing to see. This re-tries on every sync, so it heals as soon as\n // the field reaches the tree.\n const attach = (): void => {\n if (!list.isConnected && input.parentNode) input.after(list);\n };\n\n const sync = (): void => {\n attach();\n const known = opts.known();\n list.replaceChildren(...known.map((k) => { const o = document.createElement(\"option\"); o.value = k; return o; }));\n const v = input.value.trim();\n const problem = v === \"\" ? undefined : propertyRefProblem(v, opts);\n input.classList.toggle(\"illegal\", !!problem);\n if (problem) input.title = problem;\n else if (input.title.startsWith(\"Can't be used\") || input.title.startsWith(\"No \")) {\n if (opts.hint) input.title = opts.hint;\n else input.removeAttribute(\"title\");\n }\n };\n refSyncs.set(input, sync);\n\n input.addEventListener(\"input\", () => { onCommit(input.value); sync(); });\n input.addEventListener(\"keydown\", (e) => {\n if (e.key !== \"Tab\" || e.shiftKey) return;\n const typed = input.value.trim();\n // Only a GRAMMAR fault is coercible. An undeclared name is not something Tab can\n // fix, and a legal reference in the wrong case is not a fault at all.\n if (typed === \"\" || isValidPropertyName(typed.toLowerCase())) return;\n const coerced = propertyNameify(typed);\n if (!coerced || coerced === input.value) return;\n e.preventDefault();\n input.value = coerced;\n onCommit(coerced);\n sync();\n });\n sync();\n}\n\n/**\n * Re-check every bound reference field inside `host`. Hosts call this when the\n * DECLARATIONS change, since a reference's validity depends on a list it does not\n * own: rename a property and the driver pointing at it is wrong from that moment,\n * with nothing typed into it.\n */\nexport function revalidatePropertyRefs(host: ParentNode): void {\n for (const input of host.querySelectorAll(\"input\")) refSyncs.get(input)?.();\n}\n","// ---------------------------------------------------------------------------\n// THE property declaration list: name, type, default, values or stages,\n// purpose, in a row per declaration, with reorder, remove, add-then-focus and\n// the duplicate / illegal-name gates. Patterpad's settings-properties.ts and\n// Storyletter's prop-list.ts were built from the same shell primitives in the\n// same order (dupGuard, bindPropertyName, a type select over the same six\n// types, defaultControl, three iconBtns on moveItem, labelled tagChips and\n// stageChips, expandableRow, focusNewRow); this is the one list, and each\n// app's extras (Shared, Temporary, Read-only, Durable) arrive through\n// `extraLine` / `extraDetails`.\n//\n// `defaultControl` was four copies (three inside Patterpad), and the two apps'\n// option labels had already parted (\"(unset)\" vs \"(none)\"). One label set now.\n//\n// Storage of the default differs by app: Patterpad's model holds a typed\n// value (true, 3), Storyletter's DTO holds the string the field showed. The\n// list mutates `decls` in place either way; `typed` picks the encoding.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { iconBtn, labelled, moveItem, stageChips, tagChips } from \"./dom.js\";\nimport { dupGuard, expandableRow, focusNewRow } from \"./settings.js\";\nimport type { SettingsSectionHandle } from \"./settings.js\";\nimport { bindPropertyName, firstIllegalPropertyName } from \"./property-name-field.js\";\nimport { PROPERTY_NAME_HINT } from \"./property-names.js\";\nimport { defaultLabel, fallbackText } from \"./value-control.js\";\n\n/** The six types both apps declare. */\nexport type PropertyListType = \"number\" | \"boolean\" | \"string\" | \"enum\" | \"flags\" | \"quality\";\n\n/** The shape a row edits. Apps' DTOs carry more (shared, writable, durable);\n * those ride through untouched. */\nexport interface PropertyDeclLike {\n name: string;\n type: string;\n default?: unknown;\n /** enum / flags */\n values?: string[];\n /** quality: the ladder, in order */\n stages?: string[];\n purpose?: string;\n}\n\n/** The type list, in the family's order, with the words the select shows. */\nexport const PROPERTY_TYPES: ReadonlyArray<readonly [PropertyListType, string]> = [\n [\"number\", \"Number\"], [\"boolean\", \"True / False\"], [\"string\", \"Text\"],\n [\"enum\", \"List\"], [\"flags\", \"Flags\"], [\"quality\", \"Quality\"],\n];\n\nexport interface DefaultControlOptions {\n /**\n * How the default is stored. `false` (default): the string the field shows\n * (Storyletter's DTO). `true`: a typed value (boolean, number, string)\n * (Patterpad's model). Either way, no default given DELETES the key: a\n * default is optional in source, and the type's holds (the family's rule,\n * ruled 2026-10-18).\n */\n typed?: boolean;\n /**\n * An enum may hold \"\" (none chosen), as a field's may: the select then\n * offers \"(none)\" beside \"default (x)\". Off for a property, whose value is\n * always one of its values.\n */\n noneChosen?: boolean;\n /** What the blank says instead of the type's default, where something else holds when none is given\n * here: a tag's starting value falls back to its group's default, say. */\n unsetLabel?: string;\n}\n\nconst option = (value: string, label: string, selected: boolean): HTMLOptionElement => {\n const o = el(\"option\", undefined, label);\n o.value = value; o.selected = selected;\n return o;\n};\n\n/** The select value that stands for an enum's \"\" (none chosen), apart from no default at all. */\nconst NONE_CHOSEN = \"\\u0000none\";\n\n/**\n * The type-appropriate control for a declaration's DEFAULT: a true / false\n * picker for a boolean, the value list for an enum, the stage list for a\n * quality, a note for flags (a set starts empty), a number or text field\n * otherwise. Leaving it blank gives no default, and the blank says what holds\n * then, \"default (x)\" (`defaultLabel`): a default is optional, and the type's\n * fills in.\n */\nexport function defaultControl(p: PropertyDeclLike, onChange?: () => void, opts: DefaultControlOptions = {}): HTMLElement {\n const typed = opts.typed === true;\n const write = (raw: string): void => {\n if (raw === \"\") delete p.default;\n else if (raw === NONE_CHOSEN) p.default = \"\";\n else if (typed && p.type === \"boolean\") p.default = raw === \"true\";\n else if (typed && p.type === \"number\") p.default = Number(raw);\n else p.default = raw;\n onChange?.();\n };\n const current = p.default === undefined || p.default === null ? \"\" : String(p.default);\n // What a blank default falls back to, said on the blank itself: the type's, not this one's.\n const unsetLabel = opts.unsetLabel ?? defaultLabel({ ...p, default: undefined });\n if (p.type === \"boolean\") {\n const sel = el(\"select\", \"field set-default\");\n sel.append(option(\"\", unsetLabel, current === \"\"), option(\"true\", \"True\", current === \"true\"), option(\"false\", \"False\", current === \"false\"));\n sel.addEventListener(\"change\", () => write(sel.value));\n return sel;\n }\n if (p.type === \"enum\") {\n const sel = el(\"select\", \"field set-default\");\n const none = opts.noneChosen === true && p.default === \"\";\n sel.append(option(\"\", unsetLabel, current === \"\" && !none));\n if (opts.noneChosen) sel.append(option(NONE_CHOSEN, \"(none)\", none));\n for (const v of p.values ?? []) sel.append(option(v, v, current === v));\n sel.addEventListener(\"change\", () => write(sel.value));\n return sel;\n }\n if (p.type === \"quality\") {\n // The default is a STAGE; blank means the first rung, and says which.\n const sel = el(\"select\", \"field set-default\");\n sel.append(option(\"\", unsetLabel, current === \"\"));\n for (const v of p.stages ?? []) sel.append(option(v, v, current === v));\n sel.addEventListener(\"change\", () => write(sel.value));\n return sel;\n }\n if (p.type === \"flags\") {\n // Flags hold a SET of values, so there is no single default: it starts empty.\n const s = el(\"span\", \"set-flagnote\", \"Starts empty\");\n s.dataset[\"tip\"] = \"A flags property starts with none set.\";\n return s;\n }\n const input = el(\"input\", \"field set-default\");\n input.type = p.type === \"number\" ? \"number\" : \"text\";\n input.placeholder = p.type === \"number\" ? `Default (${fallbackText({ type: \"number\" })})` : \"Default\";\n input.value = current;\n input.addEventListener(\"input\", () => write(input.value));\n return input;\n}\n\nexport interface PropertyListOptions<T extends PropertyDeclLike> extends DefaultControlOptions {\n /** Called after every mutation; a centre editor feeds its autosave here. */\n onChange?: () => void;\n /** The add button's label. Default \"+ Add property\". */\n addLabel?: string;\n /** The empty sentence. Default \"No properties yet.\" */\n emptyText?: string;\n /** The type select's entries; default `PROPERTY_TYPES`. */\n types?: ReadonlyArray<readonly [string, string]>;\n /** A fresh declaration for the add button. Default: a blank of the first type. */\n newDecl?: () => T;\n /** Controls on the row's line, between the default and the movers\n * (Storyletter's \"uses\" affordance). */\n extraLine?: (decl: T, index: number) => (HTMLElement | null)[];\n /** Fields behind the disclosure, before Values / Stages / Purpose\n * (Shared, Temporary, Read-only, Durable). */\n extraDetails?: (decl: T, index: number) => (HTMLElement | null)[];\n /** Offer the Purpose field. Default true. */\n purpose?: boolean;\n /**\n * A quality's stage was renamed in place (the declaration's own default\n * already follows it). Given, the rename is handed here INSTEAD of\n * `onChange`, so the caller can carry every value that names the stage in\n * the same edit as the declaration; absent, it is an ordinary change.\n */\n onStageRename?: (decl: T, from: string, to: string) => void;\n /** A quality's stages were reordered (after `onChange`). `note` is a quiet\n * line under the stages for the caller to fill, such as how many gates\n * compare against the ladder. */\n onStageReorder?: (decl: T, note: HTMLElement) => void;\n}\n\nexport interface PropertyListHandle extends SettingsSectionHandle {\n /** Both faults, for the Save gate: a duplicate first, then an illegal name. */\n firstInvalid(): HTMLInputElement | null;\n /** The first name that clashes with another, or null. */\n firstDuplicate(): HTMLInputElement | null;\n /** The first name no expression could reach, or null. */\n firstIllegalName(): HTMLInputElement | null;\n /** Redraw from `decls` (after an outside change). */\n render(): void;\n}\n\n/** Mount the list into `host`. Mutates `decls` in place; the caller reads\n * them back on save (pruning blank names is the caller's, since only it\n * knows its DTO). */\nexport function mountPropertyList<T extends PropertyDeclLike>(host: HTMLElement, decls: T[], opts: PropertyListOptions<T> = {}): PropertyListHandle {\n const changed = (): void => opts.onChange?.();\n const guard = dupGuard();\n const types = opts.types ?? PROPERTY_TYPES;\n const list = el(\"div\", \"set-list\");\n const keep = (parts: (HTMLElement | null)[] | undefined): HTMLElement[] =>\n (parts ?? []).filter((p): p is HTMLElement => p !== null);\n\n const row = (p: T, i: number): HTMLElement => {\n const name = el(\"input\", \"field set-name\");\n name.type = \"text\"; name.placeholder = \"Property name\"; name.value = p.name; name.spellcheck = false;\n name.dataset[\"tip\"] = PROPERTY_NAME_HINT;\n bindPropertyName(name, (v) => { p.name = v; changed(); }, { hint: PROPERTY_NAME_HINT });\n guard.track(name);\n\n const type = el(\"select\", \"field set-type\");\n for (const [v, label] of types) type.append(option(v, label, v === p.type));\n type.addEventListener(\"change\", () => {\n p.type = type.value;\n delete p.default; // a default rarely survives a type change: the new type's holds\n if (p.type === \"enum\" || p.type === \"flags\") p.values ??= []; else delete p.values;\n if (p.type === \"quality\") p.stages ??= []; else delete p.stages;\n render(); changed();\n });\n\n // The default control is rebuilt in place when the values change, so a\n // new value becomes selectable as the default.\n let def = defaultControl(p, changed, opts);\n const refreshDefault = (): void => { const fresh = defaultControl(p, changed, opts); def.replaceWith(fresh); def = fresh; changed(); };\n\n const up = iconBtn(\"up\", \"Move up\", () => { if (moveItem(decls, i, -1)) { render(); changed(); } }, i === 0);\n const down = iconBtn(\"down\", \"Move down\", () => { if (moveItem(decls, i, 1)) { render(); changed(); } }, i === decls.length - 1);\n const del = iconBtn(\"close\", \"Delete property\", () => { decls.splice(i, 1); render(); changed(); }, false, true);\n\n const details: HTMLElement[] = keep(opts.extraDetails?.(p, i));\n if (p.type === \"enum\" || p.type === \"flags\") details.push(labelled(\"Values\", tagChips(p, refreshDefault)));\n // A quality's ladder, IN ORDER: the chips carry movers because position is the meaning.\n if (p.type === \"quality\") {\n const note = el(\"p\", \"shell-stage-note\");\n const stages = stageChips(p, refreshDefault, {\n onRename: (from, to) => {\n if (p.default === from) p.default = to;\n const fresh = defaultControl(p, changed, opts); def.replaceWith(fresh); def = fresh;\n if (opts.onStageRename) opts.onStageRename(p, from, to); else changed();\n },\n onReorder: () => opts.onStageReorder?.(p, note),\n });\n details.push(labelled(\"Stages (in order)\", stages), note);\n }\n if (opts.purpose !== false) {\n const purpose = el(\"input\", \"field set-purpose\");\n purpose.type = \"text\"; purpose.placeholder = \"What this property is for\"; purpose.value = p.purpose ?? \"\";\n purpose.addEventListener(\"input\", () => {\n if (purpose.value.trim()) p.purpose = purpose.value; else delete p.purpose;\n changed();\n });\n details.push(labelled(\"Purpose\", purpose));\n }\n // Stamped with the declared name so \"Go to definition\" lands on the row (revealRow).\n return expandableRow({ line: [name, type, def, ...keep(opts.extraLine?.(p, i)), up, down, del], details, name: p.name });\n };\n\n const render = (): void => {\n guard.reset();\n list.replaceChildren();\n if (decls.length === 0) list.append(el(\"p\", \"empty\", opts.emptyText ?? \"No properties yet.\"));\n else decls.forEach((p, i) => list.append(row(p, i)));\n guard.check();\n };\n render();\n\n const add = el(\"button\", \"btn set-add\", opts.addLabel ?? \"+ Add property\");\n add.type = \"button\";\n add.addEventListener(\"click\", () => {\n decls.push(opts.newDecl ? opts.newDecl() : ({ name: \"\", type: types[0]?.[0] ?? \"string\", ...(opts.typed ? {} : { default: \"\" }) } as unknown as T));\n render(); changed(); focusNewRow(list);\n });\n host.append(list, add);\n\n return {\n firstDuplicate: () => guard.firstDuplicate(),\n firstIllegalName: () => firstIllegalPropertyName(host),\n // The Save gate takes both faults; the field's own rollover says which.\n firstInvalid: () => guard.firstDuplicate() ?? firstIllegalPropertyName(host),\n render,\n };\n}\n"]}