@wildwinter/app-shell 0.49.0 → 0.51.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.
- package/README.md +37 -0
- package/dist/{chunk-FGAPO4N5.js → chunk-B56FWJ5Y.js} +21 -12
- package/dist/chunk-B56FWJ5Y.js.map +1 -0
- package/dist/chunk-ELEDQRZ7.js +435 -0
- package/dist/chunk-ELEDQRZ7.js.map +1 -0
- package/dist/controls.css +107 -0
- package/dist/controls.css.map +1 -1
- package/dist/history.cjs +194 -0
- package/dist/history.cjs.map +1 -0
- package/dist/history.d.cts +111 -0
- package/dist/history.d.ts +111 -0
- package/dist/history.js +188 -0
- package/dist/history.js.map +1 -0
- package/dist/index.cjs +368 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{property-list-BPGU3rhF.d.cts → property-list-DlpxIkv6.d.cts} +9 -0
- package/dist/{property-list-BPGU3rhF.d.ts → property-list-DlpxIkv6.d.ts} +9 -0
- package/dist/property-list.cjs +18 -9
- package/dist/property-list.cjs.map +1 -1
- package/dist/property-list.d.cts +1 -1
- package/dist/property-list.d.ts +1 -1
- package/dist/property-list.js +2 -2
- package/dist/value-control.cjs +584 -0
- package/dist/value-control.cjs.map +1 -1
- package/dist/value-control.d.cts +61 -1
- package/dist/value-control.d.ts +61 -1
- package/dist/value-control.js +1 -1
- package/dist/value-rows.cjs +1104 -0
- package/dist/value-rows.cjs.map +1 -0
- package/dist/value-rows.d.cts +71 -0
- package/dist/value-rows.d.ts +71 -0
- package/dist/value-rows.js +298 -0
- package/dist/value-rows.js.map +1 -0
- package/dist/window-guard.cjs +74 -0
- package/dist/window-guard.cjs.map +1 -0
- package/dist/window-guard.d.cts +32 -0
- package/dist/window-guard.d.ts +32 -0
- package/dist/window-guard.js +67 -0
- package/dist/window-guard.js.map +1 -0
- package/package.json +26 -4
- package/dist/chunk-EAE7UWQ5.js +0 -84
- package/dist/chunk-EAE7UWQ5.js.map +0 -1
- package/dist/chunk-FGAPO4N5.js.map +0 -1
package/dist/history.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
import { writeTextFilesAsync, deleteFileAsync } from '@wildwinter/simple-vc-lib';
|
|
4
|
+
|
|
5
|
+
// src/history.ts
|
|
6
|
+
function captureBefore(paths) {
|
|
7
|
+
return paths.map((path) => ({ path, content: existsSync(path) ? readFileSync(path, "utf8") : null }));
|
|
8
|
+
}
|
|
9
|
+
async function applyStates(all) {
|
|
10
|
+
const states = all.filter((s) => s.local !== true);
|
|
11
|
+
const writes = states.filter((s) => s.content !== null).map((s) => ({ filePath: s.path, content: s.content }));
|
|
12
|
+
if (writes.length > 0) {
|
|
13
|
+
try {
|
|
14
|
+
const batch = await writeTextFilesAsync(writes, "utf8", { allOrNothing: true });
|
|
15
|
+
if (!batch.success) return { ok: false, error: writeFailure(batch.results), wrote: false };
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return { ok: false, error: `couldn't save: ${e instanceof Error ? e.message : String(e)}`, wrote: false };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const undeleted = [];
|
|
21
|
+
for (const s of states) {
|
|
22
|
+
if (s.content !== null || !existsSync(s.path)) continue;
|
|
23
|
+
try {
|
|
24
|
+
const gone = await deleteFileAsync(s.path);
|
|
25
|
+
if (!gone.success) undeleted.push(`${basename(s.path)} (${gone.message || gone.status})`);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
undeleted.push(`${basename(s.path)} (${e instanceof Error ? e.message : String(e)})`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (undeleted.length > 0) return { ok: false, error: `couldn't delete ${undeleted.join("; ")}`, wrote: writes.length > 0 };
|
|
31
|
+
for (const s of all) {
|
|
32
|
+
if (s.local !== true) continue;
|
|
33
|
+
if (s.content === null) rmSync(s.path, { force: true });
|
|
34
|
+
else writeFileSync(s.path, s.content, "utf8");
|
|
35
|
+
}
|
|
36
|
+
return { ok: true };
|
|
37
|
+
}
|
|
38
|
+
function writeFailure(results) {
|
|
39
|
+
const failed = results.filter((r) => !r.success);
|
|
40
|
+
const refused = failed.filter((r) => r.status === "locked" || r.status === "outOfDate");
|
|
41
|
+
const say = (r) => {
|
|
42
|
+
const name = basename(r.filePath);
|
|
43
|
+
const why = r.message || r.status;
|
|
44
|
+
return why.includes(name) ? why : `${name} (${why})`;
|
|
45
|
+
};
|
|
46
|
+
if (refused.length > 0) return `nothing was saved: ${refused.map(say).join("; ")}`;
|
|
47
|
+
return `couldn't save ${failed.map(say).join("; ") || "the project"}`;
|
|
48
|
+
}
|
|
49
|
+
var UNDO_LIMIT = 100;
|
|
50
|
+
function mergeStates(base, override) {
|
|
51
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const s of base) byPath.set(s.path, s);
|
|
53
|
+
for (const s of override) byPath.set(s.path, s);
|
|
54
|
+
return [...byPath.values()];
|
|
55
|
+
}
|
|
56
|
+
var listNames = (paths) => paths.map((p) => basename(p)).join(", ");
|
|
57
|
+
var History = class {
|
|
58
|
+
undoStack = [];
|
|
59
|
+
redoStack = [];
|
|
60
|
+
opts;
|
|
61
|
+
constructor(opts = {}) {
|
|
62
|
+
this.opts = opts;
|
|
63
|
+
}
|
|
64
|
+
reset() {
|
|
65
|
+
this.undoStack = [];
|
|
66
|
+
this.redoStack = [];
|
|
67
|
+
}
|
|
68
|
+
get canUndo() {
|
|
69
|
+
return this.undoStack.length > 0;
|
|
70
|
+
}
|
|
71
|
+
get canRedo() {
|
|
72
|
+
return this.redoStack.length > 0;
|
|
73
|
+
}
|
|
74
|
+
/** The labels of the next undo and redo, for a menu ("Undo Rename Card"). */
|
|
75
|
+
get undoLabel() {
|
|
76
|
+
return this.undoStack[this.undoStack.length - 1]?.label;
|
|
77
|
+
}
|
|
78
|
+
get redoLabel() {
|
|
79
|
+
return this.redoStack[this.redoStack.length - 1]?.label;
|
|
80
|
+
}
|
|
81
|
+
/** Record a committed change; coalesce with the top entry when the key matches (the same logical edit
|
|
82
|
+
* continuing, as typing into one field). A new distinct edit clears redo. */
|
|
83
|
+
record(label, key, before, after) {
|
|
84
|
+
const top = this.undoStack[this.undoStack.length - 1];
|
|
85
|
+
if (top && top.key === key) {
|
|
86
|
+
top.before = mergeStates(before, top.before);
|
|
87
|
+
top.after = mergeStates(top.after, after);
|
|
88
|
+
} else {
|
|
89
|
+
this.undoStack.push({ label, key, before, after });
|
|
90
|
+
if (this.undoStack.length > UNDO_LIMIT) this.undoStack.shift();
|
|
91
|
+
}
|
|
92
|
+
this.redoStack = [];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fold a write the app made on its own into the step the author took last, rather than recording a step of
|
|
96
|
+
* its own: for a write nobody asked for (a value stored the first time a view is drawn), which a step of its
|
|
97
|
+
* own would have the author's next Undo undo instead of what they meant. With nothing to undo yet, there is
|
|
98
|
+
* nothing to fold into, and the write simply stands.
|
|
99
|
+
*/
|
|
100
|
+
fold(before, after) {
|
|
101
|
+
const top = this.undoStack[this.undoStack.length - 1];
|
|
102
|
+
if (!top) return;
|
|
103
|
+
const known = new Set(top.before.map((s) => s.path));
|
|
104
|
+
top.before = [...top.before, ...before.filter((s) => !known.has(s.path))];
|
|
105
|
+
top.after = mergeStates(top.after, after);
|
|
106
|
+
}
|
|
107
|
+
/** Move the top undo entry to redo and return its before-states, for a caller that writes them itself (and
|
|
108
|
+
* checks drift itself). Most callers want `applyUndo`. */
|
|
109
|
+
undo() {
|
|
110
|
+
const entry = this.undoStack.pop();
|
|
111
|
+
if (!entry) return void 0;
|
|
112
|
+
this.redoStack.push(entry);
|
|
113
|
+
return entry.before;
|
|
114
|
+
}
|
|
115
|
+
/** Move the top redo entry to undo and return its after-states (see `undo`). */
|
|
116
|
+
redo() {
|
|
117
|
+
const entry = this.redoStack.pop();
|
|
118
|
+
if (!entry) return void 0;
|
|
119
|
+
this.undoStack.push(entry);
|
|
120
|
+
return entry.after;
|
|
121
|
+
}
|
|
122
|
+
/** An undo whose states could not be written: the step goes back where it was, so the author can try again
|
|
123
|
+
* once the file can be written. */
|
|
124
|
+
undoFailed() {
|
|
125
|
+
const entry = this.redoStack.pop();
|
|
126
|
+
if (entry) this.undoStack.push(entry);
|
|
127
|
+
}
|
|
128
|
+
/** The same for a redo that could not be written. */
|
|
129
|
+
redoFailed() {
|
|
130
|
+
const entry = this.undoStack.pop();
|
|
131
|
+
if (entry) this.redoStack.push(entry);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Undo the top step: check every file still says what the step left it saying, then write its befores. A
|
|
135
|
+
* file that has changed since refuses the undo, and the step is dropped with every step below it (they were
|
|
136
|
+
* taken on top of what has now been overwritten). A write that fails puts the step back. Undefined when there
|
|
137
|
+
* is nothing to undo.
|
|
138
|
+
*/
|
|
139
|
+
async applyUndo() {
|
|
140
|
+
return this.apply("undo");
|
|
141
|
+
}
|
|
142
|
+
/** Redo the top undone step, by the same rules: drift drops it and every step above it on the redo stack. */
|
|
143
|
+
async applyRedo() {
|
|
144
|
+
return this.apply("redo");
|
|
145
|
+
}
|
|
146
|
+
async apply(way) {
|
|
147
|
+
const from = way === "undo" ? this.undoStack : this.redoStack;
|
|
148
|
+
const entry = from[from.length - 1];
|
|
149
|
+
if (!entry) return void 0;
|
|
150
|
+
const expected = way === "undo" ? entry.after : entry.before;
|
|
151
|
+
const target = way === "undo" ? entry.before : entry.after;
|
|
152
|
+
const restorer = this.opts.restorer;
|
|
153
|
+
const current = new Map((this.opts.read ?? captureBefore)(target.map((s) => s.path)).map((s) => [s.path, s.content]));
|
|
154
|
+
const drifted = expected.filter((s) => {
|
|
155
|
+
if (s.local === true) return false;
|
|
156
|
+
const now = current.get(s.path) ?? null;
|
|
157
|
+
const r = restorer?.(s.path);
|
|
158
|
+
return r ? !r.same(s.content, now) : now !== s.content;
|
|
159
|
+
}).map((s) => s.path);
|
|
160
|
+
if (drifted.length > 0) {
|
|
161
|
+
from.length = 0;
|
|
162
|
+
const name = drifted.length === 1 ? `${basename(drifted[0])} has` : `${listNames(drifted)} have`;
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
label: entry.label,
|
|
166
|
+
drift: drifted,
|
|
167
|
+
error: `couldn't ${way} "${entry.label}": ${name} changed since, so ${way === "undo" ? "undoing" : "redoing"} it would overwrite that. Nothing from that step ${way === "undo" ? "back" : "on"} can be ${way === "undo" ? "undone" : "redone"} now.`
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const states = target.map((s) => {
|
|
171
|
+
const r = restorer?.(s.path);
|
|
172
|
+
return r ? { ...s, content: r.restore(s.content, current.get(s.path) ?? null) } : s;
|
|
173
|
+
});
|
|
174
|
+
from.pop();
|
|
175
|
+
(way === "undo" ? this.redoStack : this.undoStack).push(entry);
|
|
176
|
+
const applied = await (this.opts.write ?? applyStates)(states);
|
|
177
|
+
if (!applied.ok) {
|
|
178
|
+
if (way === "undo") this.undoFailed();
|
|
179
|
+
else this.redoFailed();
|
|
180
|
+
return { ok: false, label: entry.label, error: applied.error };
|
|
181
|
+
}
|
|
182
|
+
return { ok: true, label: entry.label };
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export { History, UNDO_LIMIT, applyStates, captureBefore, writeFailure };
|
|
187
|
+
//# sourceMappingURL=history.js.map
|
|
188
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/history.ts"],"names":[],"mappings":"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","file":"history.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The file-byte history: undo and redo for writes an app makes to files, the\n// files-are-truth way. A step is a set of file-state transitions (a path's\n// content before and after, null meaning absent); undo writes the befores\n// back, redo the afters. No command log: the bytes on disk are the model, so\n// restoring bytes is undo.\n//\n// Storyletter's, lifted into the shell (patterkit design/patterpad-undo.md,\n// ruling 2) so Patterpad's project-level undo is the same code. The two apps'\n// DOCUMENT undo still shares nothing (Patterpad's open scene is ProseMirror's);\n// this is the file half only.\n//\n// Three things beyond Storyletter's first version:\n//\n// - DRIFT IS REFUSED (ruling 5). Before an undo or redo, each file must still\n// say what the step left it saying (its after, for an undo; its before, for a\n// redo). A file changed since, by a pull, a merge or another app, would have\n// that work overwritten, so the step is refused, naming the file, and dropped\n// with every step below it: none of them can now be applied safely.\n// - A PER-FILE RESTORE HOOK, for a file an app restores its own way (Patterpad\n// restores a `.patterx` block by block but stamps its edit trail afresh, which\n// moves on every save and so must not count as drift).\n// - THE WRITER IS THE APP'S. Patterpad writes through its own ordered queue;\n// `applyStates` (all or nothing through the VC layer) is the default.\n//\n// Main-process code: it reads and writes files.\n// ---------------------------------------------------------------------------\n\nimport { existsSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport { deleteFileAsync, writeTextFilesAsync } from \"@wildwinter/simple-vc-lib\";\nimport type { VCWriteOutcome } from \"@wildwinter/simple-vc-lib\";\n\n/** A file's content, or null when the file is absent (created / deleted). */\nexport interface FileState {\n path: string;\n content: string | null;\n /** Bookkeeping that lives beside the project but outside version control: restored with plain writes,\n * never checked out or added, once the versioned files have landed. */\n local?: boolean;\n}\n\n/** Read the current on-disk state of a set of paths. */\nexport function captureBefore(paths: string[]): FileState[] {\n return paths.map((path) => ({ path, content: existsSync(path) ? readFileSync(path, \"utf8\") : null }));\n}\n\n/** What applying a set of states came to. `wrote` says whether the writes landed before something else (a\n * delete) failed, which decides whether there is now a change on disk for the history to know about. */\nexport type Applied = { ok: true } | { ok: false; error: string; wrote: boolean };\n\n/** How a set of states is written: `applyStates`, or the app's own write path. */\nexport type StateWriter = (states: FileState[]) => Promise<Applied>;\n\n/**\n * Apply file states through the VC layer: write content, or delete when null.\n *\n * ALL OR NOTHING, and off the main thread (simple-vc-lib 0.5.0): every file is checked out first, and if any\n * one is refused nothing is written. The deletes run only once the whole batch has landed, so a rename whose\n * new path was refused never loses the old file.\n */\nexport async function applyStates(all: FileState[]): Promise<Applied> {\n const states = all.filter((s) => s.local !== true);\n const writes = states.filter((s) => s.content !== null).map((s) => ({ filePath: s.path, content: s.content! }));\n if (writes.length > 0) {\n try {\n const batch = await writeTextFilesAsync(writes, \"utf8\", { allOrNothing: true });\n if (!batch.success) return { ok: false, error: writeFailure(batch.results), wrote: false };\n } catch (e) {\n return { ok: false, error: `couldn't save: ${e instanceof Error ? e.message : String(e)}`, wrote: false };\n }\n }\n const undeleted: string[] = [];\n for (const s of states) {\n if (s.content !== null || !existsSync(s.path)) continue;\n try {\n const gone = await deleteFileAsync(s.path);\n if (!gone.success) undeleted.push(`${basename(s.path)} (${gone.message || gone.status})`);\n } catch (e) {\n undeleted.push(`${basename(s.path)} (${e instanceof Error ? e.message : String(e)})`);\n }\n }\n if (undeleted.length > 0) return { ok: false, error: `couldn't delete ${undeleted.join(\"; \")}`, wrote: writes.length > 0 };\n // The bookkeeping last, and only once the project's own files have landed, so it never says the project is\n // at a revision its files are not.\n for (const s of all) {\n if (s.local !== true) continue;\n if (s.content === null) rmSync(s.path, { force: true });\n else writeFileSync(s.path, s.content, \"utf8\");\n }\n return { ok: true };\n}\n\n/**\n * What a refused batch says: the file, and why. A refusal names who holds it (\"'docks.storyletdeck' is locked\n * by bob@bob-ws\"); every other file in an all-or-nothing batch reports only that it was not prepared because of\n * that one, which is not worth repeating.\n */\nexport function writeFailure(results: VCWriteOutcome[]): string {\n const failed = results.filter((r) => !r.success);\n const refused = failed.filter((r) => r.status === \"locked\" || r.status === \"outOfDate\");\n const say = (r: VCWriteOutcome): string => {\n const name = basename(r.filePath);\n const why = r.message || r.status;\n return why.includes(name) ? why : `${name} (${why})`;\n };\n if (refused.length > 0) return `nothing was saved: ${refused.map(say).join(\"; \")}`;\n return `couldn't save ${failed.map(say).join(\"; \") || \"the project\"}`;\n}\n\n/** How many steps Undo keeps: ProseMirror's default depth, the same in both apps. A step holds whole files\n * before and after, so an uncapped stack grew for as long as the window stayed open. */\nexport const UNDO_LIMIT = 100;\n\n/**\n * A file an app restores its own way. `same` says whether the file on disk still says what the step expects\n * (the drift check), and `restore` gives the content to write for the target, from what is on disk now. A\n * `.patterx` compares and restores every block but its edit trail, which the undo stamps afresh.\n */\nexport interface FileRestorer {\n same(expected: string | null, current: string | null): boolean;\n restore(target: string | null, current: string | null): string | null;\n}\n\nexport interface HistoryOptions {\n /** How an undo or redo is written. Default `applyStates`. */\n write?: StateWriter;\n /** A restorer for a file the app restores its own way, by path; undefined for every other file. */\n restorer?: (path: string) => FileRestorer | undefined;\n /** How each file's current bytes are read for the drift check. Default from disk (`captureBefore`). */\n read?: (paths: string[]) => FileState[];\n}\n\n/** What an undo or redo came to. `drift` names the files that changed since the step, when that refused it. */\nexport type StepOutcome =\n | { ok: true; label: string }\n | { ok: false; label: string; error: string; drift?: string[] };\n\ninterface Entry {\n label: string;\n key: string;\n before: FileState[];\n after: FileState[];\n}\n\n/** Merge state lists by path, `override` winning. */\nfunction mergeStates(base: FileState[], override: FileState[]): FileState[] {\n const byPath = new Map<string, FileState>();\n for (const s of base) byPath.set(s.path, s);\n for (const s of override) byPath.set(s.path, s);\n return [...byPath.values()];\n}\n\n/** File names, as a refusal lists them. */\nconst listNames = (paths: string[]): string => paths.map((p) => basename(p)).join(\", \");\n\nexport class History {\n private undoStack: Entry[] = [];\n private redoStack: Entry[] = [];\n private readonly opts: HistoryOptions;\n\n constructor(opts: HistoryOptions = {}) {\n this.opts = opts;\n }\n\n reset(): void {\n this.undoStack = [];\n this.redoStack = [];\n }\n\n get canUndo(): boolean { return this.undoStack.length > 0; }\n get canRedo(): boolean { return this.redoStack.length > 0; }\n /** The labels of the next undo and redo, for a menu (\"Undo Rename Card\"). */\n get undoLabel(): string | undefined { return this.undoStack[this.undoStack.length - 1]?.label; }\n get redoLabel(): string | undefined { return this.redoStack[this.redoStack.length - 1]?.label; }\n\n /** Record a committed change; coalesce with the top entry when the key matches (the same logical edit\n * continuing, as typing into one field). A new distinct edit clears redo. */\n record(label: string, key: string, before: FileState[], after: FileState[]): void {\n const top = this.undoStack[this.undoStack.length - 1];\n if (top && top.key === key) {\n top.before = mergeStates(before, top.before); // keep the earliest before per path\n top.after = mergeStates(top.after, after); // keep the latest after per path\n } else {\n this.undoStack.push({ label, key, before, after });\n if (this.undoStack.length > UNDO_LIMIT) this.undoStack.shift();\n }\n this.redoStack = [];\n }\n\n /**\n * Fold a write the app made on its own into the step the author took last, rather than recording a step of\n * its own: for a write nobody asked for (a value stored the first time a view is drawn), which a step of its\n * own would have the author's next Undo undo instead of what they meant. With nothing to undo yet, there is\n * nothing to fold into, and the write simply stands.\n */\n fold(before: FileState[], after: FileState[]): void {\n const top = this.undoStack[this.undoStack.length - 1];\n if (!top) return;\n const known = new Set(top.before.map((s) => s.path));\n top.before = [...top.before, ...before.filter((s) => !known.has(s.path))];\n top.after = mergeStates(top.after, after);\n }\n\n /** Move the top undo entry to redo and return its before-states, for a caller that writes them itself (and\n * checks drift itself). Most callers want `applyUndo`. */\n undo(): FileState[] | undefined {\n const entry = this.undoStack.pop();\n if (!entry) return undefined;\n this.redoStack.push(entry);\n return entry.before;\n }\n\n /** Move the top redo entry to undo and return its after-states (see `undo`). */\n redo(): FileState[] | undefined {\n const entry = this.redoStack.pop();\n if (!entry) return undefined;\n this.undoStack.push(entry);\n return entry.after;\n }\n\n /** An undo whose states could not be written: the step goes back where it was, so the author can try again\n * once the file can be written. */\n undoFailed(): void {\n const entry = this.redoStack.pop();\n if (entry) this.undoStack.push(entry);\n }\n\n /** The same for a redo that could not be written. */\n redoFailed(): void {\n const entry = this.undoStack.pop();\n if (entry) this.redoStack.push(entry);\n }\n\n /**\n * Undo the top step: check every file still says what the step left it saying, then write its befores. A\n * file that has changed since refuses the undo, and the step is dropped with every step below it (they were\n * taken on top of what has now been overwritten). A write that fails puts the step back. Undefined when there\n * is nothing to undo.\n */\n async applyUndo(): Promise<StepOutcome | undefined> {\n return this.apply(\"undo\");\n }\n\n /** Redo the top undone step, by the same rules: drift drops it and every step above it on the redo stack. */\n async applyRedo(): Promise<StepOutcome | undefined> {\n return this.apply(\"redo\");\n }\n\n private async apply(way: \"undo\" | \"redo\"): Promise<StepOutcome | undefined> {\n const from = way === \"undo\" ? this.undoStack : this.redoStack;\n const entry = from[from.length - 1];\n if (!entry) return undefined;\n const expected = way === \"undo\" ? entry.after : entry.before;\n const target = way === \"undo\" ? entry.before : entry.after;\n const restorer = this.opts.restorer;\n const current = new Map((this.opts.read ?? captureBefore)(target.map((s) => s.path)).map((s) => [s.path, s.content]));\n\n // The drift check, over every file the step touches.\n const drifted = expected.filter((s) => {\n if (s.local === true) return false; // bookkeeping outside version control is the app's own\n const now = current.get(s.path) ?? null;\n const r = restorer?.(s.path);\n return r ? !r.same(s.content, now) : now !== s.content;\n }).map((s) => s.path);\n if (drifted.length > 0) {\n // Every step from here down was taken on top of what has changed: none can be applied safely.\n from.length = 0;\n const name = drifted.length === 1 ? `${basename(drifted[0]!)} has` : `${listNames(drifted)} have`;\n return {\n ok: false, label: entry.label, drift: drifted,\n error: `couldn't ${way} \"${entry.label}\": ${name} changed since, so ${way === \"undo\" ? \"undoing\" : \"redoing\"} it would overwrite that. `\n + `Nothing from that step ${way === \"undo\" ? \"back\" : \"on\"} can be ${way === \"undo\" ? \"undone\" : \"redone\"} now.`,\n };\n }\n\n const states = target.map((s) => {\n const r = restorer?.(s.path);\n return r ? { ...s, content: r.restore(s.content, current.get(s.path) ?? null) } : s;\n });\n from.pop();\n (way === \"undo\" ? this.redoStack : this.undoStack).push(entry);\n const applied = await (this.opts.write ?? applyStates)(states);\n if (!applied.ok) {\n if (way === \"undo\") this.undoFailed(); else this.redoFailed();\n return { ok: false, label: entry.label, error: applied.error };\n }\n return { ok: true, label: entry.label };\n }\n}\n"]}
|
package/dist/index.cjs
CHANGED
|
@@ -3198,6 +3198,353 @@ function rungStrip(opts) {
|
|
|
3198
3198
|
}
|
|
3199
3199
|
return strip;
|
|
3200
3200
|
}
|
|
3201
|
+
var fallbackFlags = (decl) => Array.isArray(decl.default) ? decl.default.filter((v) => typeof v === "string") : [];
|
|
3202
|
+
function fieldValueProblem(decl, value) {
|
|
3203
|
+
if (value === void 0 || value === null || value === "") return "";
|
|
3204
|
+
switch (decl.type) {
|
|
3205
|
+
case "number":
|
|
3206
|
+
return typeof value === "number" && Number.isFinite(value) ? "" : "Not a number.";
|
|
3207
|
+
case "boolean":
|
|
3208
|
+
return typeof value === "boolean" ? "" : "Not True or False.";
|
|
3209
|
+
case "enum":
|
|
3210
|
+
return decl.values && !decl.values.includes(String(value)) ? `"${String(value)}" is not one of the values.` : "";
|
|
3211
|
+
case "quality":
|
|
3212
|
+
return decl.stages && !decl.stages.includes(String(value)) ? `"${String(value)}" is not one of the stages.` : "";
|
|
3213
|
+
case "flags": {
|
|
3214
|
+
if (!Array.isArray(value)) return "Not a list of flags.";
|
|
3215
|
+
const strays = decl.values ? value.filter((v) => !decl.values.includes(String(v))) : [];
|
|
3216
|
+
if (strays.length === 0) return "";
|
|
3217
|
+
return strays.length === 1 ? `"${String(strays[0])}" is not one of the values.` : `${strays.map((s) => `"${String(s)}"`).join(", ")} are not among the values.`;
|
|
3218
|
+
}
|
|
3219
|
+
default:
|
|
3220
|
+
return "";
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
var isTextual = (type) => type !== "boolean" && type !== "enum" && type !== "flags" && type !== "quality";
|
|
3224
|
+
var controlShape = (o) => JSON.stringify([
|
|
3225
|
+
o.decl.type,
|
|
3226
|
+
o.decl.values ?? null,
|
|
3227
|
+
o.decl.stages ?? null,
|
|
3228
|
+
o.decl.default ?? null,
|
|
3229
|
+
o.onCommit === void 0,
|
|
3230
|
+
o.disabled === true,
|
|
3231
|
+
o.controlClassName ?? "",
|
|
3232
|
+
o.decl.type === "quality" && (o.decl.stages?.length ?? 0) === 0
|
|
3233
|
+
]);
|
|
3234
|
+
function fieldRow(initial) {
|
|
3235
|
+
let opts = initial;
|
|
3236
|
+
const row = el("div", `shell-fieldrow${opts.className ? ` ${opts.className}` : ""}`);
|
|
3237
|
+
const caption = el("span", `shell-fieldrow-label${opts.labelClassName ? ` ${opts.labelClassName}` : ""}`);
|
|
3238
|
+
caption.id = `shell-fr-${Math.random().toString(36).slice(2, 9)}`;
|
|
3239
|
+
const body = el("div", "shell-fieldrow-body");
|
|
3240
|
+
const purpose = el("p", "shell-fieldrow-purpose");
|
|
3241
|
+
const problem = el("p", "shell-fieldrow-problem");
|
|
3242
|
+
problem.setAttribute("role", "status");
|
|
3243
|
+
row.append(caption, body);
|
|
3244
|
+
let shape = "";
|
|
3245
|
+
let show2 = () => {
|
|
3246
|
+
};
|
|
3247
|
+
let typing = "";
|
|
3248
|
+
const commit = (v) => {
|
|
3249
|
+
opts.onCommit?.(v);
|
|
3250
|
+
};
|
|
3251
|
+
const editable = () => opts.onCommit !== void 0 && opts.disabled !== true;
|
|
3252
|
+
const ctl = (base) => opts.controlClassName ? `${base.replace(/^field /, "")} ${opts.controlClassName}` : base;
|
|
3253
|
+
const labelledBy = (node2) => {
|
|
3254
|
+
node2.setAttribute("aria-labelledby", caption.id);
|
|
3255
|
+
const described = [purpose.id, problem.id].filter(Boolean).join(" ");
|
|
3256
|
+
if (described) node2.setAttribute("aria-describedby", described);
|
|
3257
|
+
};
|
|
3258
|
+
purpose.id = `${caption.id}-p`;
|
|
3259
|
+
problem.id = `${caption.id}-m`;
|
|
3260
|
+
const sayProblem = () => {
|
|
3261
|
+
const words = [typing || fieldValueProblem(opts.decl, opts.value), opts.problem ?? ""].filter((w) => w !== "");
|
|
3262
|
+
problem.textContent = words.join(" ");
|
|
3263
|
+
problem.hidden = words.length === 0;
|
|
3264
|
+
row.classList.toggle("has-problem", words.length > 0);
|
|
3265
|
+
row.classList.toggle("shell-fieldrow-textual", isTextual(opts.decl.type));
|
|
3266
|
+
};
|
|
3267
|
+
const build = () => {
|
|
3268
|
+
const decl = opts.decl;
|
|
3269
|
+
typing = "";
|
|
3270
|
+
if (decl.type === "quality" && (decl.stages?.length ?? 0) > 0) {
|
|
3271
|
+
const holder = el("span", "shell-fieldrow-rungs");
|
|
3272
|
+
show2 = (value) => {
|
|
3273
|
+
holder.replaceChildren(rungStrip({
|
|
3274
|
+
stages: decl.stages,
|
|
3275
|
+
value: typeof value === "string" && value !== "" ? value : void 0,
|
|
3276
|
+
decl,
|
|
3277
|
+
...editable() ? { onPick: (stage) => {
|
|
3278
|
+
opts = { ...opts, value: stage };
|
|
3279
|
+
show2(stage);
|
|
3280
|
+
sayProblem();
|
|
3281
|
+
commit(stage);
|
|
3282
|
+
}, onClear: () => {
|
|
3283
|
+
opts = { ...opts, value: void 0 };
|
|
3284
|
+
show2(void 0);
|
|
3285
|
+
sayProblem();
|
|
3286
|
+
commit(void 0);
|
|
3287
|
+
} } : {}
|
|
3288
|
+
}));
|
|
3289
|
+
const strip = holder.firstElementChild;
|
|
3290
|
+
if (strip) {
|
|
3291
|
+
strip.setAttribute("role", "group");
|
|
3292
|
+
labelledBy(strip);
|
|
3293
|
+
}
|
|
3294
|
+
};
|
|
3295
|
+
return holder;
|
|
3296
|
+
}
|
|
3297
|
+
if (decl.type === "boolean" || decl.type === "enum") {
|
|
3298
|
+
const sel = el("select", ctl("field shell-fieldrow-select"));
|
|
3299
|
+
labelledBy(sel);
|
|
3300
|
+
sel.disabled = !editable();
|
|
3301
|
+
const choices = decl.type === "boolean" ? [["true", "True"], ["false", "False"]] : (decl.values ?? []).map((v) => [v, v]);
|
|
3302
|
+
show2 = (value) => {
|
|
3303
|
+
const current3 = value === void 0 || value === null || value === "" ? "" : String(value);
|
|
3304
|
+
sel.replaceChildren();
|
|
3305
|
+
const add = (v, label2) => {
|
|
3306
|
+
const o = el("option", void 0, label2);
|
|
3307
|
+
o.value = v;
|
|
3308
|
+
sel.append(o);
|
|
3309
|
+
};
|
|
3310
|
+
add("", defaultLabel(decl));
|
|
3311
|
+
for (const [v, label2] of choices) add(v, label2);
|
|
3312
|
+
if (current3 !== "" && !choices.some(([v]) => v === current3)) add(current3, current3);
|
|
3313
|
+
sel.value = current3;
|
|
3314
|
+
};
|
|
3315
|
+
sel.addEventListener("change", () => {
|
|
3316
|
+
const raw = sel.value;
|
|
3317
|
+
const v = raw === "" ? void 0 : decl.type === "boolean" ? raw === "true" : raw;
|
|
3318
|
+
opts = { ...opts, value: v };
|
|
3319
|
+
sayProblem();
|
|
3320
|
+
commit(v);
|
|
3321
|
+
});
|
|
3322
|
+
return sel;
|
|
3323
|
+
}
|
|
3324
|
+
if (decl.type === "flags" && (decl.values?.length ?? 0) > 0) {
|
|
3325
|
+
const holder = el("span", "shell-flags");
|
|
3326
|
+
holder.setAttribute("role", "group");
|
|
3327
|
+
labelledBy(holder);
|
|
3328
|
+
show2 = (value) => {
|
|
3329
|
+
const unset = !Array.isArray(value);
|
|
3330
|
+
const held = unset ? fallbackFlags(decl) : value.map(String);
|
|
3331
|
+
holder.replaceChildren();
|
|
3332
|
+
const chip2 = (text, cls, pressed, tip2, onClick) => {
|
|
3333
|
+
const b = el(onClick && editable() ? "button" : "span", { className: `shell-flag${cls}`, text, tip: tip2 });
|
|
3334
|
+
b.setAttribute("aria-label", `${text}: ${tip2}`);
|
|
3335
|
+
if (b instanceof HTMLButtonElement) {
|
|
3336
|
+
b.type = "button";
|
|
3337
|
+
b.addEventListener("click", onClick);
|
|
3338
|
+
}
|
|
3339
|
+
if (pressed !== void 0) b.setAttribute("aria-pressed", String(pressed));
|
|
3340
|
+
holder.append(b);
|
|
3341
|
+
};
|
|
3342
|
+
const set = (next) => {
|
|
3343
|
+
opts = { ...opts, value: next };
|
|
3344
|
+
show2(next);
|
|
3345
|
+
sayProblem();
|
|
3346
|
+
commit(next);
|
|
3347
|
+
};
|
|
3348
|
+
chip2(
|
|
3349
|
+
defaultLabel(decl),
|
|
3350
|
+
` shell-flag-default${unset ? " on" : ""}`,
|
|
3351
|
+
void 0,
|
|
3352
|
+
unset ? "Not set here: the declared default holds" : "Clear the value, back to the default",
|
|
3353
|
+
unset ? void 0 : () => set(void 0)
|
|
3354
|
+
);
|
|
3355
|
+
for (const v of decl.values) {
|
|
3356
|
+
const on = held.includes(v);
|
|
3357
|
+
chip2(
|
|
3358
|
+
v,
|
|
3359
|
+
`${on ? " on" : ""}${on && unset ? " fallback" : ""}`,
|
|
3360
|
+
on,
|
|
3361
|
+
on ? `Unset ${v}` : `Set ${v}`,
|
|
3362
|
+
() => set(on ? held.filter((x) => x !== v) : decl.values.filter((x) => x === v || held.includes(x)).concat(held.filter((x) => !decl.values.includes(x))))
|
|
3363
|
+
);
|
|
3364
|
+
}
|
|
3365
|
+
for (const v of held.filter((x) => !decl.values.includes(x))) {
|
|
3366
|
+
chip2(v, " stray on", true, `"${v}" is not one of the values: click to drop it`, () => set(held.filter((x) => x !== v)));
|
|
3367
|
+
}
|
|
3368
|
+
};
|
|
3369
|
+
return holder;
|
|
3370
|
+
}
|
|
3371
|
+
if (decl.type === "flags") {
|
|
3372
|
+
const holder = el("span", "shell-fieldrow-chips");
|
|
3373
|
+
show2 = (value) => {
|
|
3374
|
+
const box = { values: Array.isArray(value) ? value.map(String) : [] };
|
|
3375
|
+
const chips = tagChips(box, () => {
|
|
3376
|
+
const next = box.values.length > 0 ? [...box.values] : void 0;
|
|
3377
|
+
opts = { ...opts, value: next };
|
|
3378
|
+
sayProblem();
|
|
3379
|
+
commit(next);
|
|
3380
|
+
});
|
|
3381
|
+
const input = chips.querySelector("input");
|
|
3382
|
+
if (input) {
|
|
3383
|
+
labelledBy(input);
|
|
3384
|
+
input.placeholder = Array.isArray(value) ? "Add value" : defaultLabel(decl);
|
|
3385
|
+
input.disabled = !editable();
|
|
3386
|
+
}
|
|
3387
|
+
chips.querySelectorAll("button").forEach((b) => {
|
|
3388
|
+
b.disabled = !editable();
|
|
3389
|
+
});
|
|
3390
|
+
holder.replaceChildren(chips);
|
|
3391
|
+
};
|
|
3392
|
+
return holder;
|
|
3393
|
+
}
|
|
3394
|
+
const multiline = decl.type === "multiline";
|
|
3395
|
+
const field = multiline ? el("textarea", ctl("field shell-fieldrow-text")) : el("input", ctl("field shell-fieldrow-text"));
|
|
3396
|
+
if (field instanceof HTMLInputElement) {
|
|
3397
|
+
field.type = "text";
|
|
3398
|
+
if (decl.type === "number") field.inputMode = "decimal";
|
|
3399
|
+
} else field.rows = 3;
|
|
3400
|
+
labelledBy(field);
|
|
3401
|
+
field.placeholder = defaultLabel(decl);
|
|
3402
|
+
field.disabled = !editable();
|
|
3403
|
+
const asText = (value) => value === void 0 || value === null ? "" : Array.isArray(value) ? value.join(", ") : String(value);
|
|
3404
|
+
const read = (raw) => {
|
|
3405
|
+
if (raw.trim() === "") return {};
|
|
3406
|
+
if (decl.type !== "number") return { value: raw };
|
|
3407
|
+
const n = Number(raw.trim());
|
|
3408
|
+
return Number.isFinite(n) ? { value: n } : { bad: "Not a number." };
|
|
3409
|
+
};
|
|
3410
|
+
let atFocus = "";
|
|
3411
|
+
show2 = (value) => {
|
|
3412
|
+
if (field.ownerDocument.activeElement === field || keeper.restoring(field) || keeper.holds(row)) return;
|
|
3413
|
+
field.value = asText(value);
|
|
3414
|
+
};
|
|
3415
|
+
const take = () => {
|
|
3416
|
+
const r = read(field.value);
|
|
3417
|
+
typing = r.bad ?? "";
|
|
3418
|
+
if (r.bad === void 0) opts = { ...opts, value: r.value };
|
|
3419
|
+
sayProblem();
|
|
3420
|
+
if (r.bad === void 0) commit(r.value);
|
|
3421
|
+
};
|
|
3422
|
+
field.addEventListener("focus", () => {
|
|
3423
|
+
if (!keeper.restoring(field)) atFocus = field.value;
|
|
3424
|
+
});
|
|
3425
|
+
field.addEventListener("input", take);
|
|
3426
|
+
field.addEventListener("keydown", (e) => {
|
|
3427
|
+
if (e.key !== "Escape" || field.value === atFocus) return;
|
|
3428
|
+
field.value = atFocus;
|
|
3429
|
+
take();
|
|
3430
|
+
});
|
|
3431
|
+
return field;
|
|
3432
|
+
};
|
|
3433
|
+
const draw = (next) => {
|
|
3434
|
+
const decl = next.decl;
|
|
3435
|
+
const label2 = next.label ?? decl.name;
|
|
3436
|
+
if (typeof label2 === "string") caption.textContent = label2;
|
|
3437
|
+
else caption.replaceChildren(label2);
|
|
3438
|
+
caption.className = `shell-fieldrow-label${next.labelClassName ? ` ${next.labelClassName}` : ""}`;
|
|
3439
|
+
row.className = `shell-fieldrow${next.className ? ` ${next.className}` : ""}`;
|
|
3440
|
+
const nextShape = controlShape(next);
|
|
3441
|
+
opts = next;
|
|
3442
|
+
if (nextShape !== shape) {
|
|
3443
|
+
shape = nextShape;
|
|
3444
|
+
body.replaceChildren(build());
|
|
3445
|
+
}
|
|
3446
|
+
show2(next.value);
|
|
3447
|
+
purpose.textContent = decl.purpose ?? "";
|
|
3448
|
+
if (decl.purpose) body.append(purpose);
|
|
3449
|
+
else purpose.remove();
|
|
3450
|
+
body.append(problem);
|
|
3451
|
+
sayProblem();
|
|
3452
|
+
};
|
|
3453
|
+
draw(initial);
|
|
3454
|
+
keeper.watch(row);
|
|
3455
|
+
return { el: row, update: draw };
|
|
3456
|
+
}
|
|
3457
|
+
var keeper = /* @__PURE__ */ (() => {
|
|
3458
|
+
const memos = /* @__PURE__ */ new WeakMap();
|
|
3459
|
+
const restoringNow = /* @__PURE__ */ new WeakSet();
|
|
3460
|
+
const redrawn = /* @__PURE__ */ new WeakMap();
|
|
3461
|
+
const put = (row) => {
|
|
3462
|
+
const memo = memos.get(row);
|
|
3463
|
+
if (!memo) return;
|
|
3464
|
+
const doc = row.ownerDocument;
|
|
3465
|
+
const active2 = doc.activeElement;
|
|
3466
|
+
if (!row.isConnected || !memo.field.isConnected || !row.contains(memo.field)) return;
|
|
3467
|
+
if (active2 && active2 !== doc.body && !row.contains(active2)) {
|
|
3468
|
+
memos.delete(row);
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
3471
|
+
if (active2 === memo.field && memo.field.matches(":focus")) return;
|
|
3472
|
+
restoringNow.add(memo.field);
|
|
3473
|
+
try {
|
|
3474
|
+
memo.field.focus({ preventScroll: true });
|
|
3475
|
+
if (!(memo.field instanceof HTMLSelectElement) && memo.start !== null) memo.field.setSelectionRange(memo.start, memo.end ?? memo.start);
|
|
3476
|
+
} finally {
|
|
3477
|
+
restoringNow.delete(memo.field);
|
|
3478
|
+
}
|
|
3479
|
+
};
|
|
3480
|
+
return {
|
|
3481
|
+
watch(row) {
|
|
3482
|
+
const note = (e) => {
|
|
3483
|
+
const t = e.target;
|
|
3484
|
+
if (!(t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement)) return;
|
|
3485
|
+
const text = !(t instanceof HTMLSelectElement);
|
|
3486
|
+
memos.set(row, { field: t, start: text ? t.selectionStart : null, end: text ? t.selectionEnd : null });
|
|
3487
|
+
};
|
|
3488
|
+
row.addEventListener("focusin", note);
|
|
3489
|
+
row.addEventListener("input", note);
|
|
3490
|
+
row.addEventListener("keyup", note);
|
|
3491
|
+
row.addEventListener("mouseup", note);
|
|
3492
|
+
row.addEventListener("focusout", (e) => {
|
|
3493
|
+
const at = performance.now();
|
|
3494
|
+
const late = e.relatedTarget === null && at - (redrawn.get(row) ?? -Infinity) < 250;
|
|
3495
|
+
if (late && memos.has(row)) {
|
|
3496
|
+
queueMicrotask(() => put(row));
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
queueMicrotask(() => {
|
|
3500
|
+
if ((redrawn.get(row) ?? -Infinity) >= at) {
|
|
3501
|
+
put(row);
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
3504
|
+
if (row.isConnected && !row.contains(row.ownerDocument.activeElement)) memos.delete(row);
|
|
3505
|
+
});
|
|
3506
|
+
});
|
|
3507
|
+
},
|
|
3508
|
+
/** Give the focus back to the row, if the author was in it and has not gone anywhere else since. */
|
|
3509
|
+
restore(row) {
|
|
3510
|
+
if (!memos.has(row)) return;
|
|
3511
|
+
redrawn.set(row, performance.now());
|
|
3512
|
+
queueMicrotask(() => put(row));
|
|
3513
|
+
},
|
|
3514
|
+
restoring: (field) => restoringNow.has(field),
|
|
3515
|
+
/** Forget the row's focus if the author has since gone somewhere else (before the row is redrawn). */
|
|
3516
|
+
settle(row) {
|
|
3517
|
+
const active2 = row.ownerDocument.activeElement;
|
|
3518
|
+
if (active2 && active2 !== row.ownerDocument.body && !row.contains(active2)) memos.delete(row);
|
|
3519
|
+
},
|
|
3520
|
+
/** The row the author is in (before a redraw takes it out). */
|
|
3521
|
+
holds: (row) => memos.has(row)
|
|
3522
|
+
};
|
|
3523
|
+
})();
|
|
3524
|
+
function fieldRows() {
|
|
3525
|
+
const rows = /* @__PURE__ */ new Map();
|
|
3526
|
+
return {
|
|
3527
|
+
row(key, opts) {
|
|
3528
|
+
const had = rows.get(key);
|
|
3529
|
+
if (had) {
|
|
3530
|
+
had.used = true;
|
|
3531
|
+
keeper.settle(had.handle.el);
|
|
3532
|
+
had.handle.update(opts);
|
|
3533
|
+
keeper.restore(had.handle.el);
|
|
3534
|
+
return had.handle.el;
|
|
3535
|
+
}
|
|
3536
|
+
const handle = fieldRow(opts);
|
|
3537
|
+
rows.set(key, { handle, used: true });
|
|
3538
|
+
return handle.el;
|
|
3539
|
+
},
|
|
3540
|
+
sweep() {
|
|
3541
|
+
for (const [key, entry] of rows) {
|
|
3542
|
+
if (!entry.used) rows.delete(key);
|
|
3543
|
+
else entry.used = false;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3201
3548
|
|
|
3202
3549
|
// src/property-list.ts
|
|
3203
3550
|
var PROPERTY_TYPES = [
|
|
@@ -3267,17 +3614,26 @@ function mountPropertyList(host, decls, opts = {}) {
|
|
|
3267
3614
|
const types = opts.types ?? PROPERTY_TYPES;
|
|
3268
3615
|
const list = el("div", "set-list");
|
|
3269
3616
|
const keep = (parts) => (parts ?? []).filter((p) => p !== null);
|
|
3617
|
+
const noun = opts.noun ?? "property";
|
|
3618
|
+
const Noun = noun.charAt(0).toUpperCase() + noun.slice(1);
|
|
3619
|
+
const freeNames = opts.names === "free";
|
|
3270
3620
|
const row = (p, i) => {
|
|
3271
3621
|
const name = el("input", "field set-name");
|
|
3272
3622
|
name.type = "text";
|
|
3273
|
-
name.placeholder =
|
|
3623
|
+
name.placeholder = `${Noun} name`;
|
|
3274
3624
|
name.value = p.name;
|
|
3275
3625
|
name.spellcheck = false;
|
|
3276
|
-
name.
|
|
3277
|
-
|
|
3278
|
-
p.name = v;
|
|
3626
|
+
if (freeNames) name.addEventListener("input", () => {
|
|
3627
|
+
p.name = name.value;
|
|
3279
3628
|
changed();
|
|
3280
|
-
}
|
|
3629
|
+
});
|
|
3630
|
+
else {
|
|
3631
|
+
name.dataset["tip"] = PROPERTY_NAME_HINT;
|
|
3632
|
+
bindPropertyName(name, (v) => {
|
|
3633
|
+
p.name = v;
|
|
3634
|
+
changed();
|
|
3635
|
+
}, { hint: PROPERTY_NAME_HINT });
|
|
3636
|
+
}
|
|
3281
3637
|
guard.track(name);
|
|
3282
3638
|
const type = el("select", "field set-type");
|
|
3283
3639
|
for (const [v, label2] of types) type.append(option(v, label2, v === p.type));
|
|
@@ -3310,7 +3666,7 @@ function mountPropertyList(host, decls, opts = {}) {
|
|
|
3310
3666
|
changed();
|
|
3311
3667
|
}
|
|
3312
3668
|
}, i === decls.length - 1);
|
|
3313
|
-
const del = iconBtn("close",
|
|
3669
|
+
const del = iconBtn("close", `Delete ${noun}`, () => {
|
|
3314
3670
|
decls.splice(i, 1);
|
|
3315
3671
|
render();
|
|
3316
3672
|
changed();
|
|
@@ -3335,7 +3691,7 @@ function mountPropertyList(host, decls, opts = {}) {
|
|
|
3335
3691
|
if (opts.purpose !== false) {
|
|
3336
3692
|
const purpose = el("input", "field set-purpose");
|
|
3337
3693
|
purpose.type = "text";
|
|
3338
|
-
purpose.placeholder =
|
|
3694
|
+
purpose.placeholder = `What this ${noun} is for`;
|
|
3339
3695
|
purpose.value = p.purpose ?? "";
|
|
3340
3696
|
purpose.addEventListener("input", () => {
|
|
3341
3697
|
if (purpose.value.trim()) p.purpose = purpose.value;
|
|
@@ -3365,9 +3721,9 @@ function mountPropertyList(host, decls, opts = {}) {
|
|
|
3365
3721
|
host.append(list, add);
|
|
3366
3722
|
return {
|
|
3367
3723
|
firstDuplicate: () => guard.firstDuplicate(),
|
|
3368
|
-
firstIllegalName: () => firstIllegalPropertyName(host),
|
|
3724
|
+
firstIllegalName: () => freeNames ? null : firstIllegalPropertyName(host),
|
|
3369
3725
|
// The Save gate takes both faults; the field's own rollover says which.
|
|
3370
|
-
firstInvalid: () => guard.firstDuplicate() ?? firstIllegalPropertyName(host),
|
|
3726
|
+
firstInvalid: () => guard.firstDuplicate() ?? (freeNames ? null : firstIllegalPropertyName(host)),
|
|
3371
3727
|
render
|
|
3372
3728
|
};
|
|
3373
3729
|
}
|
|
@@ -3480,6 +3836,9 @@ exports.el = el;
|
|
|
3480
3836
|
exports.expandableRow = expandableRow;
|
|
3481
3837
|
exports.fallbackText = fallbackText;
|
|
3482
3838
|
exports.feedUpdaterDownloadProgress = feedUpdaterDownloadProgress;
|
|
3839
|
+
exports.fieldRow = fieldRow;
|
|
3840
|
+
exports.fieldRows = fieldRows;
|
|
3841
|
+
exports.fieldValueProblem = fieldValueProblem;
|
|
3483
3842
|
exports.firstIllegalPropertyName = firstIllegalPropertyName;
|
|
3484
3843
|
exports.focusNewRow = focusNewRow;
|
|
3485
3844
|
exports.foldVc = foldVc;
|