@wildwinter/app-shell 0.37.0 → 0.39.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/LICENSES.md +62 -0
- package/README.md +21 -0
- package/dist/about.css +13 -34
- package/dist/about.css.map +1 -1
- package/dist/anchored.css +5 -5
- package/dist/anchored.css.map +1 -1
- package/dist/app-store.cjs +15 -0
- package/dist/app-store.cjs.map +1 -1
- package/dist/app-store.d.cts +25 -1
- package/dist/app-store.d.ts +25 -1
- package/dist/app-store.js +14 -1
- package/dist/app-store.js.map +1 -1
- package/dist/chunk-2ECJUTSX.js +97 -0
- package/dist/chunk-2ECJUTSX.js.map +1 -0
- package/dist/chunk-2NMVIM54.js +55 -0
- package/dist/chunk-2NMVIM54.js.map +1 -0
- package/dist/chunk-C6UVQYKI.js +70 -0
- package/dist/chunk-C6UVQYKI.js.map +1 -0
- package/dist/chunk-IU7EY6BI.js +26 -0
- package/dist/chunk-IU7EY6BI.js.map +1 -0
- package/dist/chunk-K2AOLQW4.js +82 -0
- package/dist/chunk-K2AOLQW4.js.map +1 -0
- package/dist/chunk-KGAMP5LW.js +87 -0
- package/dist/chunk-KGAMP5LW.js.map +1 -0
- package/dist/chunk-L5HOBEKL.js +534 -0
- package/dist/chunk-L5HOBEKL.js.map +1 -0
- package/dist/chunk-P4G4ERG6.js +60 -0
- package/dist/chunk-P4G4ERG6.js.map +1 -0
- package/dist/chunk-P6WLAEKZ.js +71 -0
- package/dist/chunk-P6WLAEKZ.js.map +1 -0
- package/dist/chunk-S4KWQLYN.js +66 -0
- package/dist/chunk-S4KWQLYN.js.map +1 -0
- package/dist/chunk-XZYZXY7I.js +554 -0
- package/dist/chunk-XZYZXY7I.js.map +1 -0
- package/dist/comments.css +5 -3
- package/dist/comments.css.map +1 -1
- package/dist/confirm.css +1 -54
- package/dist/confirm.css.map +1 -1
- package/dist/context-menu.cjs +250 -0
- package/dist/context-menu.cjs.map +1 -0
- package/dist/context-menu.css +64 -0
- package/dist/context-menu.css.map +1 -0
- package/dist/context-menu.d.cts +25 -0
- package/dist/context-menu.d.ts +25 -0
- package/dist/context-menu.js +5 -0
- package/dist/context-menu.js.map +1 -0
- package/dist/controls.css +155 -0
- package/dist/controls.css.map +1 -0
- package/dist/dialog.cjs +222 -0
- package/dist/dialog.cjs.map +1 -0
- package/dist/dialog.css +55 -0
- package/dist/dialog.css.map +1 -0
- package/dist/dialog.d.cts +28 -0
- package/dist/dialog.d.ts +28 -0
- package/dist/dialog.js +5 -0
- package/dist/dialog.js.map +1 -0
- package/dist/floating.cjs +83 -0
- package/dist/floating.cjs.map +1 -0
- package/dist/floating.d.cts +25 -0
- package/dist/floating.d.ts +25 -0
- package/dist/floating.js +4 -0
- package/dist/floating.js.map +1 -0
- package/dist/identity.css +15 -50
- package/dist/identity.css.map +1 -1
- package/dist/index.cjs +1322 -258
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +349 -131
- package/dist/index.d.ts +349 -131
- package/dist/index.js +259 -808
- package/dist/index.js.map +1 -1
- package/dist/job.css +23 -0
- package/dist/job.css.map +1 -1
- package/dist/link-status.cjs +430 -0
- package/dist/link-status.cjs.map +1 -0
- package/dist/link-status.css +76 -0
- package/dist/link-status.css.map +1 -0
- package/dist/link-status.d.cts +53 -0
- package/dist/link-status.d.ts +53 -0
- package/dist/link-status.js +5 -0
- package/dist/link-status.js.map +1 -0
- package/dist/menu.cjs +62 -1
- package/dist/menu.cjs.map +1 -1
- package/dist/menu.d.cts +88 -1
- package/dist/menu.d.ts +88 -1
- package/dist/menu.js +1 -1
- package/dist/notes-editor.css +3 -46
- package/dist/notes-editor.css.map +1 -1
- package/dist/pane-shell.css +16 -3
- package/dist/pane-shell.css.map +1 -1
- package/dist/property-list-D3VZCJzi.d.cts +162 -0
- package/dist/property-list-D3VZCJzi.d.ts +162 -0
- package/dist/property-list.cjs +784 -0
- package/dist/property-list.cjs.map +1 -0
- package/dist/property-list.d.cts +1 -0
- package/dist/property-list.d.ts +1 -0
- package/dist/property-list.js +4 -0
- package/dist/property-list.js.map +1 -0
- package/dist/settings.css +63 -8
- package/dist/settings.css.map +1 -1
- package/dist/stale.css +3 -0
- package/dist/stale.css.map +1 -1
- package/dist/stepper.css +35 -0
- package/dist/stepper.css.map +1 -1
- package/dist/tokens.css +19 -0
- package/dist/tokens.css.map +1 -1
- package/dist/tool-window.cjs +63 -0
- package/dist/tool-window.cjs.map +1 -1
- package/dist/tool-window.css +6 -3
- package/dist/tool-window.css.map +1 -1
- package/dist/tool-window.d.cts +67 -1
- package/dist/tool-window.d.ts +67 -1
- package/dist/tool-window.js +63 -1
- package/dist/tool-window.js.map +1 -1
- package/dist/updater-view.cjs +283 -0
- package/dist/updater-view.cjs.map +1 -0
- package/dist/updater-view.d.cts +23 -0
- package/dist/updater-view.d.ts +23 -0
- package/dist/updater-view.js +6 -0
- package/dist/updater-view.js.map +1 -0
- package/dist/updater.cjs +12 -16
- package/dist/updater.cjs.map +1 -1
- package/dist/updater.css +44 -0
- package/dist/updater.css.map +1 -0
- package/dist/updater.js +12 -16
- package/dist/updater.js.map +1 -1
- package/dist/util.cjs +78 -0
- package/dist/util.cjs.map +1 -0
- package/dist/util.d.cts +37 -0
- package/dist/util.d.ts +37 -0
- package/dist/util.js +3 -0
- package/dist/util.js.map +1 -0
- package/dist/vc.css +3 -2
- package/dist/vc.css.map +1 -1
- package/dist/welcome.cjs +215 -0
- package/dist/welcome.cjs.map +1 -0
- package/dist/welcome.css +112 -0
- package/dist/welcome.css.map +1 -0
- package/dist/welcome.d.cts +55 -0
- package/dist/welcome.d.ts +55 -0
- package/dist/welcome.js +4 -0
- package/dist/welcome.js.map +1 -0
- package/package.json +58 -2
- package/dist/chunk-QDPFU4WX.js +0 -43
- package/dist/chunk-QDPFU4WX.js.map +0 -1
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/tooltip.ts
|
|
4
|
+
function renderTip(host, text) {
|
|
5
|
+
host.textContent = "";
|
|
6
|
+
const re = /\u0002([^\u0003]*)\u0003/g;
|
|
7
|
+
let last = 0;
|
|
8
|
+
let m;
|
|
9
|
+
while ((m = re.exec(text)) !== null) {
|
|
10
|
+
if (m.index > last) host.append(text.slice(last, m.index));
|
|
11
|
+
const strong = document.createElement("strong");
|
|
12
|
+
strong.textContent = m[1] ?? "";
|
|
13
|
+
host.append(strong);
|
|
14
|
+
last = m.index + m[0].length;
|
|
15
|
+
}
|
|
16
|
+
if (last < text.length) host.append(text.slice(last));
|
|
17
|
+
}
|
|
18
|
+
var inited = false;
|
|
19
|
+
var checked = false;
|
|
20
|
+
var suppressed;
|
|
21
|
+
var SHOW_DELAY = 350;
|
|
22
|
+
var EDGE = 6;
|
|
23
|
+
var GAP = 7;
|
|
24
|
+
var tip = null;
|
|
25
|
+
var timer = 0;
|
|
26
|
+
var active = null;
|
|
27
|
+
function ensureEl() {
|
|
28
|
+
if (tip) return tip;
|
|
29
|
+
const el2 = document.createElement("div");
|
|
30
|
+
el2.className = "tooltip";
|
|
31
|
+
el2.setAttribute("role", "tooltip");
|
|
32
|
+
el2.hidden = true;
|
|
33
|
+
document.body.appendChild(el2);
|
|
34
|
+
tip = el2;
|
|
35
|
+
return el2;
|
|
36
|
+
}
|
|
37
|
+
var rectOf = (anchor) => anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor;
|
|
38
|
+
function place(anchor) {
|
|
39
|
+
const t = ensureEl();
|
|
40
|
+
const r = rectOf(anchor);
|
|
41
|
+
const tr = t.getBoundingClientRect();
|
|
42
|
+
let top = r.top - tr.height - GAP;
|
|
43
|
+
if (top < EDGE) top = r.top + r.height + GAP;
|
|
44
|
+
let left = r.left + r.width / 2 - tr.width / 2;
|
|
45
|
+
left = Math.max(EDGE, Math.min(left, window.innerWidth - tr.width - EDGE));
|
|
46
|
+
t.style.left = `${Math.round(left)}px`;
|
|
47
|
+
t.style.top = `${Math.round(top)}px`;
|
|
48
|
+
}
|
|
49
|
+
function show(anchor) {
|
|
50
|
+
const text = anchor.dataset["tip"];
|
|
51
|
+
if (!text) return;
|
|
52
|
+
paint(anchor, text);
|
|
53
|
+
}
|
|
54
|
+
function paint(anchor, text) {
|
|
55
|
+
if (suppressed?.()) return;
|
|
56
|
+
const t = ensureEl();
|
|
57
|
+
const host = (anchor instanceof HTMLElement ? anchor.closest("dialog[open]") : null) ?? document.body;
|
|
58
|
+
if (t.parentElement !== host) host.appendChild(t);
|
|
59
|
+
active = anchor instanceof HTMLElement ? anchor : null;
|
|
60
|
+
renderTip(t, text);
|
|
61
|
+
t.hidden = false;
|
|
62
|
+
t.classList.remove("show");
|
|
63
|
+
void t.offsetWidth;
|
|
64
|
+
place(anchor);
|
|
65
|
+
t.classList.add("show");
|
|
66
|
+
}
|
|
67
|
+
function hide() {
|
|
68
|
+
if (timer) {
|
|
69
|
+
window.clearTimeout(timer);
|
|
70
|
+
timer = 0;
|
|
71
|
+
}
|
|
72
|
+
active = null;
|
|
73
|
+
if (tip) {
|
|
74
|
+
tip.hidden = true;
|
|
75
|
+
tip.classList.remove("show");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function schedule(anchor) {
|
|
79
|
+
if (anchor === active) return;
|
|
80
|
+
if (timer) window.clearTimeout(timer);
|
|
81
|
+
timer = window.setTimeout(() => show(anchor), SHOW_DELAY);
|
|
82
|
+
}
|
|
83
|
+
function ensureTooltipHost() {
|
|
84
|
+
initTooltips();
|
|
85
|
+
}
|
|
86
|
+
function checkTooltipHost() {
|
|
87
|
+
if (checked) return;
|
|
88
|
+
checked = true;
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
if (inited || typeof document === "undefined") return;
|
|
91
|
+
if (document.querySelector("[data-tip]")) {
|
|
92
|
+
console.warn("app-shell: something set `data-tip` but no tooltip host is mounted, so those rollovers will not draw. Call initTooltips() once per window.");
|
|
93
|
+
}
|
|
94
|
+
}, 0);
|
|
95
|
+
}
|
|
96
|
+
var tipAncestor = (n) => n instanceof Element ? n.closest("[data-tip]") : null;
|
|
97
|
+
function initTooltips(opts = {}) {
|
|
98
|
+
if (opts.suppressed !== void 0) suppressed = opts.suppressed;
|
|
99
|
+
if (inited) return;
|
|
100
|
+
inited = true;
|
|
101
|
+
document.addEventListener("pointerover", (e) => {
|
|
102
|
+
const el2 = tipAncestor(e.target);
|
|
103
|
+
if (el2) schedule(el2);
|
|
104
|
+
});
|
|
105
|
+
document.addEventListener("pointerout", (e) => {
|
|
106
|
+
const el2 = tipAncestor(e.target);
|
|
107
|
+
if (!el2) return;
|
|
108
|
+
const to = e.relatedTarget;
|
|
109
|
+
if (to instanceof Node && el2.contains(to)) return;
|
|
110
|
+
hide();
|
|
111
|
+
});
|
|
112
|
+
document.addEventListener("focusin", (e) => {
|
|
113
|
+
const el2 = tipAncestor(e.target);
|
|
114
|
+
if (el2) schedule(el2);
|
|
115
|
+
});
|
|
116
|
+
document.addEventListener("focusout", () => hide());
|
|
117
|
+
document.addEventListener("pointerdown", () => hide(), true);
|
|
118
|
+
document.addEventListener("scroll", () => {
|
|
119
|
+
if (active) hide();
|
|
120
|
+
}, true);
|
|
121
|
+
document.addEventListener("keydown", (e) => {
|
|
122
|
+
if (e.key === "Escape") hide();
|
|
123
|
+
});
|
|
124
|
+
window.addEventListener("blur", () => hide());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/icons.ts
|
|
128
|
+
var icon = {
|
|
129
|
+
// --- dismissing and revealing ---------------------------------------------
|
|
130
|
+
/** Close a panel, a dialog, a popover. Patterpad's, twelve times over. */
|
|
131
|
+
close: "\u2715",
|
|
132
|
+
/** The overflow menu on a document or a row: "more about this". */
|
|
133
|
+
more: "\u22EF",
|
|
134
|
+
/** Collapsed disclosure, pointing at what would open. */
|
|
135
|
+
collapsed: "\u25B8",
|
|
136
|
+
/** Expanded disclosure. */
|
|
137
|
+
expanded: "\u25BE",
|
|
138
|
+
/** A dropdown's affordance, where it is not a real <select>. */
|
|
139
|
+
dropdown: "\u25BE",
|
|
140
|
+
// --- moving through things ------------------------------------------------
|
|
141
|
+
/** Back / previous, in a breadcrumb or a stepper. */
|
|
142
|
+
back: "\u2039",
|
|
143
|
+
/** Forward / next. */
|
|
144
|
+
forward: "\u203A",
|
|
145
|
+
/** Up one, in a list. */
|
|
146
|
+
up: "\u2191",
|
|
147
|
+
/** Down one. */
|
|
148
|
+
down: "\u2193",
|
|
149
|
+
// --- state ----------------------------------------------------------------
|
|
150
|
+
/** Done, applied, agreed. */
|
|
151
|
+
tick: "\u2713",
|
|
152
|
+
/** Somebody else holds this: the version-control lock. */
|
|
153
|
+
locked: "\u2298",
|
|
154
|
+
/** Read-only on disk with no other holder: the resting state of everything
|
|
155
|
+
* in a lock-based working copy that nobody has checked out yet. */
|
|
156
|
+
readOnly: "\u25CB",
|
|
157
|
+
/** YOU hold this one: checked out / opened by the current user, still yours to
|
|
158
|
+
* edit. Shares its glyph with `note`, which the table permits: an entry is a
|
|
159
|
+
* MEANING, and two meanings may draw the same shape. */
|
|
160
|
+
checkedOut: "\u270E",
|
|
161
|
+
/** Tracked, with local changes not yet committed. */
|
|
162
|
+
modified: "\u25CF",
|
|
163
|
+
/** Not in version control yet: new since the last commit. */
|
|
164
|
+
untracked: "+",
|
|
165
|
+
/** Something needs attention but is not fatal. */
|
|
166
|
+
warning: "\u203C",
|
|
167
|
+
/** A live thing: the running position, an active marker. */
|
|
168
|
+
dot: "\u25CF",
|
|
169
|
+
/** Play, in the sense of "run this". */
|
|
170
|
+
play: "\u25B6",
|
|
171
|
+
/** Start the thing again from the top: a play session, a sweep. Named for the
|
|
172
|
+
* act, not the arrow, which is the table's rule. */
|
|
173
|
+
restart: "\u27F2",
|
|
174
|
+
/** The three ways a collection can be shown. Named by what you get rather than
|
|
175
|
+
* by the glyph, so a second app can pick the same three without copying
|
|
176
|
+
* somebody's taste in symbols. */
|
|
177
|
+
viewCards: "\u25A6",
|
|
178
|
+
viewTable: "\u2630",
|
|
179
|
+
viewNode: "\u25C8",
|
|
180
|
+
// --- handling -------------------------------------------------------------
|
|
181
|
+
/** The drag grip on a reorderable row. */
|
|
182
|
+
grip: "\u283F",
|
|
183
|
+
/** Add one of whatever this list holds. */
|
|
184
|
+
add: "+",
|
|
185
|
+
// --- annotation -----------------------------------------------------------
|
|
186
|
+
/** Documentation notes: the pencil. Both apps had chosen it independently,
|
|
187
|
+
* which is the happiest way for a vocabulary entry to arrive. */
|
|
188
|
+
note: "\u270E",
|
|
189
|
+
/** A comment thread. Speech, not a pencil: documentation states the reason,
|
|
190
|
+
* comments are the conversation, and the two must never look alike. */
|
|
191
|
+
comment: "\u275D"
|
|
192
|
+
};
|
|
193
|
+
var STROKE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.571" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">';
|
|
194
|
+
var FILL = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">';
|
|
195
|
+
var stroked = (inner) => `${STROKE}${inner}</svg>`;
|
|
196
|
+
var filled = (inner) => `${FILL}${inner}</svg>`;
|
|
197
|
+
var DISC = filled('<circle cx="12" cy="12" r="6"/>');
|
|
198
|
+
var DISC_LARGE = filled('<circle cx="12" cy="12" r="9"/>');
|
|
199
|
+
var DRAWN = {
|
|
200
|
+
// --- dismissing and revealing ---------------------------------------------
|
|
201
|
+
/** Close a panel, a dialog, a popover. (x) */
|
|
202
|
+
close: stroked('<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'),
|
|
203
|
+
/** The overflow menu on a document or a row: "more about this". (ellipsis) */
|
|
204
|
+
more: stroked('<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>'),
|
|
205
|
+
/** Collapsed disclosure, pointing at what would open. (chevron-right) */
|
|
206
|
+
collapsed: stroked('<path d="m9 18 6-6-6-6"/>'),
|
|
207
|
+
/** Expanded disclosure. (chevron-down) */
|
|
208
|
+
expanded: stroked('<path d="m6 9 6 6 6-6"/>'),
|
|
209
|
+
/** A dropdown's affordance, where it is not a real <select>. (chevron-down) */
|
|
210
|
+
dropdown: stroked('<path d="m6 9 6 6 6-6"/>'),
|
|
211
|
+
// --- moving through things ------------------------------------------------
|
|
212
|
+
/** Back / previous, in a breadcrumb or a stepper. (chevron-left) */
|
|
213
|
+
back: stroked('<path d="m15 18-6-6 6-6"/>'),
|
|
214
|
+
/** Forward / next. (chevron-right) */
|
|
215
|
+
forward: stroked('<path d="m9 18 6-6-6-6"/>'),
|
|
216
|
+
/** Up one, in a list. (arrow-up) */
|
|
217
|
+
up: stroked('<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>'),
|
|
218
|
+
/** Down one. (arrow-down) */
|
|
219
|
+
down: stroked('<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>'),
|
|
220
|
+
/** History back. (arrow-left) */
|
|
221
|
+
arrowLeft: stroked('<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>'),
|
|
222
|
+
/** History forward; "go there". (arrow-right) */
|
|
223
|
+
arrowRight: stroked('<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>'),
|
|
224
|
+
// --- state ----------------------------------------------------------------
|
|
225
|
+
/** Done, applied, agreed. (check) */
|
|
226
|
+
tick: stroked('<path d="M20 6 9 17l-5-5"/>'),
|
|
227
|
+
/** Somebody else holds this: the version-control lock. (lock) */
|
|
228
|
+
locked: stroked('<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>'),
|
|
229
|
+
/** Read-only on disk with no other holder. (lock-open) An open lock rather
|
|
230
|
+
* than a struck-out eye: this state is not a prohibition. Nobody holds the
|
|
231
|
+
* file, saving checks it out, and it is the resting state of everything the
|
|
232
|
+
* author has not touched yet under a lock-based VCS. eye-off would say "you
|
|
233
|
+
* cannot", which is false here; the open lock says "not held", beside
|
|
234
|
+
* `locked`, which is the shut one, so the two read as a pair. */
|
|
235
|
+
readOnly: stroked('<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/>'),
|
|
236
|
+
/** YOU hold this one: checked out, still yours to edit. (pencil) */
|
|
237
|
+
checkedOut: stroked('<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>'),
|
|
238
|
+
/** Tracked, with local changes not yet committed. (a disc; see DISC) */
|
|
239
|
+
modified: DISC,
|
|
240
|
+
/** Not in version control yet: new since the last commit. (plus) */
|
|
241
|
+
untracked: stroked('<path d="M5 12h14"/><path d="M12 5v14"/>'),
|
|
242
|
+
/** Something needs attention but is not fatal. (triangle-alert) */
|
|
243
|
+
warning: stroked('<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>'),
|
|
244
|
+
/** A live thing: the running position, an active marker. (a disc) */
|
|
245
|
+
dot: DISC,
|
|
246
|
+
/** Play, in the sense of "run this". (play) */
|
|
247
|
+
play: stroked('<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"/>'),
|
|
248
|
+
/** Start the thing again from the top: a play session, a sweep. (rotate-ccw) */
|
|
249
|
+
restart: stroked('<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>'),
|
|
250
|
+
/** The three ways a collection can be shown, named by what you get. */
|
|
251
|
+
/** Cards. (layout-grid) */
|
|
252
|
+
viewCards: stroked('<rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>'),
|
|
253
|
+
/** A table. (rows-3: a ruled box, which is a table. `list` is bullets, and
|
|
254
|
+
* the view is not a list.) */
|
|
255
|
+
viewTable: stroked('<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M21 9H3"/><path d="M21 15H3"/>'),
|
|
256
|
+
/** A node graph. (waypoints: four nodes on edges reads as a graph.
|
|
257
|
+
* `git-fork` reads as version control, which this family also draws, and
|
|
258
|
+
* a picture that means two things is the fault this table exists to
|
|
259
|
+
* prevent.) */
|
|
260
|
+
viewNode: stroked('<path d="m10.586 5.414-5.172 5.172"/><path d="m18.586 13.414-5.172 5.172"/><path d="M6 12h12"/><circle cx="12" cy="20" r="2"/><circle cx="12" cy="4" r="2"/><circle cx="20" cy="12" r="2"/><circle cx="4" cy="12" r="2"/>'),
|
|
261
|
+
/** Record: the disc, in whatever colour the button is. (a disc; see DISC_LARGE) */
|
|
262
|
+
record: DISC_LARGE,
|
|
263
|
+
// --- handling -------------------------------------------------------------
|
|
264
|
+
/** The drag grip on a reorderable row. (grip-vertical) */
|
|
265
|
+
grip: stroked('<circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/>'),
|
|
266
|
+
/** Add one of whatever this list holds. (plus) */
|
|
267
|
+
add: stroked('<path d="M5 12h14"/><path d="M12 5v14"/>'),
|
|
268
|
+
/** Connect: to a server, a live link, a running game. (plug) */
|
|
269
|
+
connect: stroked('<path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/>'),
|
|
270
|
+
/** Search. (search) */
|
|
271
|
+
search: stroked('<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>'),
|
|
272
|
+
/** Keep this window on top. (pin) */
|
|
273
|
+
pin: stroked('<path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/>'),
|
|
274
|
+
/** Settings. (settings) */
|
|
275
|
+
settings: stroked('<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/>'),
|
|
276
|
+
// --- annotation -----------------------------------------------------------
|
|
277
|
+
/** Documentation notes: a document with lines on it. (file-text) The glyph
|
|
278
|
+
* table drew a pencil and shared it with `checkedOut`; the drawn set gives
|
|
279
|
+
* the two meanings two shapes, and notes get the document, which is what
|
|
280
|
+
* the shell's own noteFilled/noteOutline pair had always drawn. */
|
|
281
|
+
note: stroked('<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>'),
|
|
282
|
+
/** A comment thread. Speech, not a document: documentation states the
|
|
283
|
+
* reason, comments are the conversation, and the two must never look
|
|
284
|
+
* alike. (message-square) */
|
|
285
|
+
comment: stroked('<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/>')
|
|
286
|
+
};
|
|
287
|
+
({
|
|
288
|
+
...DRAWN});
|
|
289
|
+
var ICON_SIZE = 14;
|
|
290
|
+
var templates = /* @__PURE__ */ new Map();
|
|
291
|
+
function iconNode(name, size = ICON_SIZE) {
|
|
292
|
+
let tpl = templates.get(name);
|
|
293
|
+
if (!tpl) {
|
|
294
|
+
const holder = document.createElement("template");
|
|
295
|
+
holder.innerHTML = DRAWN[name];
|
|
296
|
+
tpl = holder.content.firstElementChild;
|
|
297
|
+
tpl.setAttribute("data-icon", name);
|
|
298
|
+
templates.set(name, tpl);
|
|
299
|
+
}
|
|
300
|
+
const node = tpl.cloneNode(true);
|
|
301
|
+
node.setAttribute("width", String(size));
|
|
302
|
+
node.setAttribute("height", String(size));
|
|
303
|
+
return node;
|
|
304
|
+
}
|
|
305
|
+
var BY_GLYPH = /* @__PURE__ */ new Map();
|
|
306
|
+
for (const [name, glyph] of Object.entries(icon)) {
|
|
307
|
+
if (!BY_GLYPH.has(glyph)) BY_GLYPH.set(glyph, name);
|
|
308
|
+
}
|
|
309
|
+
function iconNameOfGlyph(glyph) {
|
|
310
|
+
return BY_GLYPH.get(glyph);
|
|
311
|
+
}
|
|
312
|
+
function isIconName(s) {
|
|
313
|
+
return Object.prototype.hasOwnProperty.call(DRAWN, s);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/dom.ts
|
|
317
|
+
function el(tag, classNameOrProps, ...children) {
|
|
318
|
+
const node = document.createElement(tag);
|
|
319
|
+
const props = typeof classNameOrProps === "string" ? { className: classNameOrProps } : classNameOrProps ?? {};
|
|
320
|
+
if (props.className) node.className = props.className;
|
|
321
|
+
if (props.text !== void 0 && props.text !== null) node.textContent = props.text;
|
|
322
|
+
if (props.title !== void 0) node.title = props.title;
|
|
323
|
+
if (props.tip !== void 0) {
|
|
324
|
+
node.dataset["tip"] = props.tip;
|
|
325
|
+
node.setAttribute("aria-label", props.tip);
|
|
326
|
+
ensureTooltipHost();
|
|
327
|
+
}
|
|
328
|
+
if (props.onClick) node.addEventListener("click", props.onClick);
|
|
329
|
+
for (const child of children) {
|
|
330
|
+
if (child === null || child === void 0) continue;
|
|
331
|
+
node.append(child);
|
|
332
|
+
}
|
|
333
|
+
checkTooltipHost();
|
|
334
|
+
return node;
|
|
335
|
+
}
|
|
336
|
+
function iconBtn(name, title, onClick, disabled = false, danger = false) {
|
|
337
|
+
const b = el("button", `shell-icon${danger ? " danger" : ""}`);
|
|
338
|
+
b.type = "button";
|
|
339
|
+
b.dataset["tip"] = title;
|
|
340
|
+
b.setAttribute("aria-label", title);
|
|
341
|
+
const word = isIconName(name) ? name : iconNameOfGlyph(name);
|
|
342
|
+
if (word) b.append(iconNode(word));
|
|
343
|
+
else b.textContent = name;
|
|
344
|
+
b.disabled = disabled;
|
|
345
|
+
b.addEventListener("click", onClick);
|
|
346
|
+
return b;
|
|
347
|
+
}
|
|
348
|
+
function labelled(label, control) {
|
|
349
|
+
const target = control.matches("input, select, textarea") ? control : control.querySelector("input:not([type=button]):not([type=submit]), select, textarea");
|
|
350
|
+
const w = el(target ? "label" : "div", "shell-labelled");
|
|
351
|
+
w.append(el("span", "shell-fieldcap", label), control);
|
|
352
|
+
if (target) {
|
|
353
|
+
if (!target.id) target.id = `shell-f-${Math.random().toString(36).slice(2, 9)}`;
|
|
354
|
+
w.htmlFor = target.id;
|
|
355
|
+
}
|
|
356
|
+
return w;
|
|
357
|
+
}
|
|
358
|
+
function moveItem(arr, i, delta) {
|
|
359
|
+
const j = i + delta;
|
|
360
|
+
if (j < 0 || j >= arr.length) return false;
|
|
361
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
var CHIP_ICON = 10;
|
|
365
|
+
function tagChips(holder, onChange) {
|
|
366
|
+
const wrap = el("div", "shell-tags");
|
|
367
|
+
const input = el("input", "shell-tag-input");
|
|
368
|
+
input.type = "text";
|
|
369
|
+
input.placeholder = "Add value";
|
|
370
|
+
input.spellcheck = false;
|
|
371
|
+
const makeChip = (v) => {
|
|
372
|
+
const chip = el("span", "shell-tag", v);
|
|
373
|
+
const x = el("button", "shell-tag-x", iconNode("close", CHIP_ICON));
|
|
374
|
+
x.type = "button";
|
|
375
|
+
x.dataset["tip"] = `Remove ${v}`;
|
|
376
|
+
x.setAttribute("aria-label", `Remove ${v}`);
|
|
377
|
+
x.addEventListener("click", () => {
|
|
378
|
+
holder.values = (holder.values ?? []).filter((o) => o !== v);
|
|
379
|
+
chip.remove();
|
|
380
|
+
onChange?.();
|
|
381
|
+
});
|
|
382
|
+
chip.append(x);
|
|
383
|
+
return chip;
|
|
384
|
+
};
|
|
385
|
+
const commit = () => {
|
|
386
|
+
const v = input.value.trim();
|
|
387
|
+
if (v && !(holder.values ?? []).includes(v)) {
|
|
388
|
+
(holder.values ??= []).push(v);
|
|
389
|
+
wrap.insertBefore(makeChip(v), input);
|
|
390
|
+
onChange?.();
|
|
391
|
+
}
|
|
392
|
+
input.value = "";
|
|
393
|
+
};
|
|
394
|
+
input.addEventListener("keydown", (e) => {
|
|
395
|
+
if (e.key === "Enter" || e.key === ",") {
|
|
396
|
+
e.preventDefault();
|
|
397
|
+
commit();
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
input.addEventListener("blur", commit);
|
|
401
|
+
for (const v of holder.values ?? []) wrap.append(makeChip(v));
|
|
402
|
+
wrap.append(input);
|
|
403
|
+
return wrap;
|
|
404
|
+
}
|
|
405
|
+
function stageChips(holder, onChange) {
|
|
406
|
+
const wrap = el("div", "shell-tags shell-stages");
|
|
407
|
+
const input = el("input", "shell-tag-input");
|
|
408
|
+
input.type = "text";
|
|
409
|
+
input.placeholder = "Add stage";
|
|
410
|
+
input.spellcheck = false;
|
|
411
|
+
const control = (name, tip2, disabled, onClick) => {
|
|
412
|
+
const b = el("button", "shell-tag-x shell-tag-move", iconNode(name, CHIP_ICON));
|
|
413
|
+
b.type = "button";
|
|
414
|
+
b.dataset["tip"] = tip2;
|
|
415
|
+
b.setAttribute("aria-label", tip2);
|
|
416
|
+
b.disabled = disabled;
|
|
417
|
+
b.addEventListener("click", onClick);
|
|
418
|
+
return b;
|
|
419
|
+
};
|
|
420
|
+
const rebuild = () => {
|
|
421
|
+
for (const c of Array.from(wrap.children)) if (c !== input) c.remove();
|
|
422
|
+
const stages = holder.stages ?? [];
|
|
423
|
+
stages.forEach((v, i) => {
|
|
424
|
+
const chip = el("span", "shell-tag", `${i + 1}. ${v}`);
|
|
425
|
+
chip.dataset["stage"] = v;
|
|
426
|
+
chip.append(
|
|
427
|
+
control(
|
|
428
|
+
"back",
|
|
429
|
+
`Move ${v} earlier`,
|
|
430
|
+
i === 0,
|
|
431
|
+
() => {
|
|
432
|
+
if (moveItem(stages, i, -1)) {
|
|
433
|
+
rebuild();
|
|
434
|
+
onChange?.();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
),
|
|
438
|
+
control(
|
|
439
|
+
"forward",
|
|
440
|
+
`Move ${v} later`,
|
|
441
|
+
i === stages.length - 1,
|
|
442
|
+
() => {
|
|
443
|
+
if (moveItem(stages, i, 1)) {
|
|
444
|
+
rebuild();
|
|
445
|
+
onChange?.();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
);
|
|
450
|
+
const x = el("button", "shell-tag-x", iconNode("close", CHIP_ICON));
|
|
451
|
+
x.type = "button";
|
|
452
|
+
x.dataset["tip"] = `Remove ${v}`;
|
|
453
|
+
x.setAttribute("aria-label", `Remove ${v}`);
|
|
454
|
+
x.addEventListener("click", () => {
|
|
455
|
+
holder.stages = (holder.stages ?? []).filter((o) => o !== v);
|
|
456
|
+
rebuild();
|
|
457
|
+
onChange?.();
|
|
458
|
+
});
|
|
459
|
+
chip.append(x);
|
|
460
|
+
wrap.insertBefore(chip, input);
|
|
461
|
+
});
|
|
462
|
+
};
|
|
463
|
+
const commit = () => {
|
|
464
|
+
const v = input.value.trim();
|
|
465
|
+
if (v && !(holder.stages ?? []).includes(v)) {
|
|
466
|
+
(holder.stages ??= []).push(v);
|
|
467
|
+
rebuild();
|
|
468
|
+
onChange?.();
|
|
469
|
+
}
|
|
470
|
+
input.value = "";
|
|
471
|
+
};
|
|
472
|
+
input.addEventListener("keydown", (e) => {
|
|
473
|
+
if (e.key === "Enter" || e.key === ",") {
|
|
474
|
+
e.preventDefault();
|
|
475
|
+
commit();
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
input.addEventListener("blur", commit);
|
|
479
|
+
wrap.append(input);
|
|
480
|
+
rebuild();
|
|
481
|
+
return wrap;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/settings.ts
|
|
485
|
+
function expandableRow(opts) {
|
|
486
|
+
const row = el("div", "set-row");
|
|
487
|
+
if (opts.name !== void 0) row.dataset.name = opts.name;
|
|
488
|
+
const line = el("div", "set-rowline");
|
|
489
|
+
if (opts.details && opts.details.length) {
|
|
490
|
+
const details = el("div", "set-details");
|
|
491
|
+
details.hidden = true;
|
|
492
|
+
for (const d of opts.details) details.append(d);
|
|
493
|
+
const toggle = el("button", "set-expand");
|
|
494
|
+
toggle.type = "button";
|
|
495
|
+
toggle.dataset.tip = "More";
|
|
496
|
+
toggle.setAttribute("aria-label", "More");
|
|
497
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
498
|
+
toggle.addEventListener("click", () => {
|
|
499
|
+
const open = details.hidden;
|
|
500
|
+
details.hidden = !open;
|
|
501
|
+
toggle.setAttribute("aria-expanded", String(open));
|
|
502
|
+
toggle.classList.toggle("open", open);
|
|
503
|
+
});
|
|
504
|
+
line.append(toggle);
|
|
505
|
+
opts.line.forEach((c) => line.append(c));
|
|
506
|
+
row.append(line, details);
|
|
507
|
+
} else {
|
|
508
|
+
opts.line.forEach((c) => line.append(c));
|
|
509
|
+
row.append(line);
|
|
510
|
+
}
|
|
511
|
+
return row;
|
|
512
|
+
}
|
|
513
|
+
function focusNewRow(listEl) {
|
|
514
|
+
const last = listEl?.lastElementChild;
|
|
515
|
+
if (!(last instanceof HTMLElement)) return;
|
|
516
|
+
last.scrollIntoView({ block: "nearest" });
|
|
517
|
+
const name = last.querySelector("input.set-name") ?? last.querySelector("input");
|
|
518
|
+
name?.focus();
|
|
519
|
+
}
|
|
520
|
+
var DUP_MESSAGE = "Another row already has this name.";
|
|
521
|
+
function dupGuard() {
|
|
522
|
+
const items = [];
|
|
523
|
+
const check = () => {
|
|
524
|
+
const counts = /* @__PURE__ */ new Map();
|
|
525
|
+
for (const it of items) {
|
|
526
|
+
const k = it.key().trim().toLowerCase();
|
|
527
|
+
if (k) counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
528
|
+
}
|
|
529
|
+
let any = false;
|
|
530
|
+
for (const it of items) {
|
|
531
|
+
const k = it.key().trim().toLowerCase();
|
|
532
|
+
const dup = !!k && (counts.get(k) ?? 0) > 1;
|
|
533
|
+
it.input.classList.toggle("invalid", dup);
|
|
534
|
+
if (dup) {
|
|
535
|
+
it.input.dataset["tip"] = DUP_MESSAGE;
|
|
536
|
+
any = true;
|
|
537
|
+
} else if (it.input.dataset["tip"] === DUP_MESSAGE) delete it.input.dataset["tip"];
|
|
538
|
+
}
|
|
539
|
+
return any;
|
|
540
|
+
};
|
|
541
|
+
return {
|
|
542
|
+
reset() {
|
|
543
|
+
items.length = 0;
|
|
544
|
+
},
|
|
545
|
+
track(input, key = () => input.value, also) {
|
|
546
|
+
items.push({ input, key });
|
|
547
|
+
input.addEventListener("input", check);
|
|
548
|
+
for (const a of also ?? []) a.addEventListener("input", check);
|
|
549
|
+
},
|
|
550
|
+
check,
|
|
551
|
+
firstDuplicate() {
|
|
552
|
+
check();
|
|
553
|
+
return items.find((it) => it.input.classList.contains("invalid"))?.input ?? null;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/property-names.ts
|
|
559
|
+
var RESERVED_PROPERTY_NAMES = ["true", "false", "and", "or", "not"];
|
|
560
|
+
var PROPERTY_NAME_HINT = "Lower case letters, digits, and underscores. It can't start with a digit. No hyphens or spaces.";
|
|
561
|
+
function propertyNameify(text) {
|
|
562
|
+
const trimmed = text.trim();
|
|
563
|
+
const deliberateLeading = trimmed.startsWith("_");
|
|
564
|
+
let out = trimmed.toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
565
|
+
if (out === "") return "";
|
|
566
|
+
if (deliberateLeading || /^[0-9]/.test(out)) out = `_${out}`;
|
|
567
|
+
if (RESERVED_PROPERTY_NAMES.includes(out)) out = `${out}_`;
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
function isValidPropertyName(name) {
|
|
571
|
+
return /^[a-z_][a-z0-9_]*$/.test(name) && !RESERVED_PROPERTY_NAMES.includes(name);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/property-name-field.ts
|
|
575
|
+
function propertyNameProblem(name) {
|
|
576
|
+
if (isValidPropertyName(name)) return void 0;
|
|
577
|
+
const suggestion = propertyNameify(name);
|
|
578
|
+
const tail = suggestion ? ` Try "${suggestion}", or press Tab.` : "";
|
|
579
|
+
if (RESERVED_PROPERTY_NAMES.includes(name.toLowerCase())) {
|
|
580
|
+
return `Can't be used. "${name.toLowerCase()}" is a keyword in expressions.${tail}`;
|
|
581
|
+
}
|
|
582
|
+
if (name.includes("-")) return `Can't be used. A hyphen reads as subtraction in an expression.${tail}`;
|
|
583
|
+
if (/^[0-9]/.test(name)) return `Can't be used. A name can't start with a digit.${tail}`;
|
|
584
|
+
if (name !== name.toLowerCase()) {
|
|
585
|
+
return `Can't be used. Expressions fold names, so this would look for "${name.toLowerCase()}".${tail}`;
|
|
586
|
+
}
|
|
587
|
+
return `Can't be used. Only lower case letters, digits, and underscores.${tail}`;
|
|
588
|
+
}
|
|
589
|
+
function bindPropertyName(input, onCommit, opts = {}) {
|
|
590
|
+
const hint = opts.hint;
|
|
591
|
+
const sync = () => {
|
|
592
|
+
const v = input.value.trim();
|
|
593
|
+
const problem = v === "" ? void 0 : propertyNameProblem(v);
|
|
594
|
+
input.classList.toggle("illegal", !!problem);
|
|
595
|
+
if (problem) input.title = problem;
|
|
596
|
+
else if (input.title.startsWith("Can't be used")) {
|
|
597
|
+
if (hint) input.title = hint;
|
|
598
|
+
else input.removeAttribute("title");
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
input.addEventListener("input", () => {
|
|
602
|
+
onCommit(input.value);
|
|
603
|
+
sync();
|
|
604
|
+
});
|
|
605
|
+
input.addEventListener("keydown", (e) => {
|
|
606
|
+
if (e.key !== "Tab" || e.shiftKey) return;
|
|
607
|
+
const typed = input.value.trim();
|
|
608
|
+
if (typed === "" || isValidPropertyName(typed)) return;
|
|
609
|
+
const coerced = propertyNameify(typed);
|
|
610
|
+
if (!coerced || coerced === input.value) return;
|
|
611
|
+
e.preventDefault();
|
|
612
|
+
input.value = coerced;
|
|
613
|
+
onCommit(coerced);
|
|
614
|
+
sync();
|
|
615
|
+
});
|
|
616
|
+
sync();
|
|
617
|
+
}
|
|
618
|
+
function firstIllegalPropertyName(host) {
|
|
619
|
+
return host.querySelector("input.illegal");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/property-list.ts
|
|
623
|
+
var PROPERTY_TYPES = [
|
|
624
|
+
["number", "Number"],
|
|
625
|
+
["boolean", "True / False"],
|
|
626
|
+
["string", "Text"],
|
|
627
|
+
["enum", "List"],
|
|
628
|
+
["flags", "Flags"],
|
|
629
|
+
["quality", "Quality"]
|
|
630
|
+
];
|
|
631
|
+
var option = (value, label, selected) => {
|
|
632
|
+
const o = el("option", void 0, label);
|
|
633
|
+
o.value = value;
|
|
634
|
+
o.selected = selected;
|
|
635
|
+
return o;
|
|
636
|
+
};
|
|
637
|
+
var NONE = "(none)";
|
|
638
|
+
function defaultControl(p, onChange, opts = {}) {
|
|
639
|
+
const typed = opts.typed === true;
|
|
640
|
+
const write = (raw) => {
|
|
641
|
+
if (raw === "") {
|
|
642
|
+
if (typed) delete p.default;
|
|
643
|
+
else p.default = "";
|
|
644
|
+
} else if (typed && p.type === "boolean") p.default = raw === "true";
|
|
645
|
+
else if (typed && p.type === "number") p.default = Number(raw);
|
|
646
|
+
else p.default = raw;
|
|
647
|
+
onChange?.();
|
|
648
|
+
};
|
|
649
|
+
const current = p.default === void 0 || p.default === null ? "" : String(p.default);
|
|
650
|
+
if (p.type === "boolean") {
|
|
651
|
+
const sel = el("select", "field set-default");
|
|
652
|
+
sel.append(option("", NONE, current === ""), option("true", "True", current === "true"), option("false", "False", current === "false"));
|
|
653
|
+
sel.addEventListener("change", () => write(sel.value));
|
|
654
|
+
return sel;
|
|
655
|
+
}
|
|
656
|
+
if (p.type === "enum") {
|
|
657
|
+
const sel = el("select", "field set-default");
|
|
658
|
+
sel.append(option("", NONE, current === ""));
|
|
659
|
+
for (const v of p.values ?? []) sel.append(option(v, v, current === v));
|
|
660
|
+
sel.addEventListener("change", () => write(sel.value));
|
|
661
|
+
return sel;
|
|
662
|
+
}
|
|
663
|
+
if (p.type === "quality") {
|
|
664
|
+
const sel = el("select", "field set-default");
|
|
665
|
+
sel.append(option("", "(first stage)", current === ""));
|
|
666
|
+
for (const v of p.stages ?? []) sel.append(option(v, v, current === v));
|
|
667
|
+
sel.addEventListener("change", () => write(sel.value));
|
|
668
|
+
return sel;
|
|
669
|
+
}
|
|
670
|
+
if (p.type === "flags") {
|
|
671
|
+
const s = el("span", "set-flagnote", "Starts empty");
|
|
672
|
+
s.dataset["tip"] = "A flags property starts with none set.";
|
|
673
|
+
return s;
|
|
674
|
+
}
|
|
675
|
+
const input = el("input", "field set-default");
|
|
676
|
+
input.type = p.type === "number" ? "number" : "text";
|
|
677
|
+
input.placeholder = "Default";
|
|
678
|
+
input.value = current;
|
|
679
|
+
input.addEventListener("input", () => write(input.value));
|
|
680
|
+
return input;
|
|
681
|
+
}
|
|
682
|
+
function mountPropertyList(host, decls, opts = {}) {
|
|
683
|
+
const changed = () => opts.onChange?.();
|
|
684
|
+
const guard = dupGuard();
|
|
685
|
+
const types = opts.types ?? PROPERTY_TYPES;
|
|
686
|
+
const list = el("div", "set-list");
|
|
687
|
+
const keep = (parts) => (parts ?? []).filter((p) => p !== null);
|
|
688
|
+
const row = (p, i) => {
|
|
689
|
+
const name = el("input", "field set-name");
|
|
690
|
+
name.type = "text";
|
|
691
|
+
name.placeholder = "Property name";
|
|
692
|
+
name.value = p.name;
|
|
693
|
+
name.spellcheck = false;
|
|
694
|
+
name.dataset["tip"] = PROPERTY_NAME_HINT;
|
|
695
|
+
bindPropertyName(name, (v) => {
|
|
696
|
+
p.name = v;
|
|
697
|
+
changed();
|
|
698
|
+
}, { hint: PROPERTY_NAME_HINT });
|
|
699
|
+
guard.track(name);
|
|
700
|
+
const type = el("select", "field set-type");
|
|
701
|
+
for (const [v, label] of types) type.append(option(v, label, v === p.type));
|
|
702
|
+
type.addEventListener("change", () => {
|
|
703
|
+
p.type = type.value;
|
|
704
|
+
if (opts.typed) delete p.default;
|
|
705
|
+
else p.default = "";
|
|
706
|
+
if (p.type === "enum" || p.type === "flags") p.values ??= [];
|
|
707
|
+
else delete p.values;
|
|
708
|
+
if (p.type === "quality") p.stages ??= [];
|
|
709
|
+
else delete p.stages;
|
|
710
|
+
render();
|
|
711
|
+
changed();
|
|
712
|
+
});
|
|
713
|
+
let def = defaultControl(p, changed, opts);
|
|
714
|
+
const refreshDefault = () => {
|
|
715
|
+
const fresh = defaultControl(p, changed, opts);
|
|
716
|
+
def.replaceWith(fresh);
|
|
717
|
+
def = fresh;
|
|
718
|
+
changed();
|
|
719
|
+
};
|
|
720
|
+
const up = iconBtn("up", "Move up", () => {
|
|
721
|
+
if (moveItem(decls, i, -1)) {
|
|
722
|
+
render();
|
|
723
|
+
changed();
|
|
724
|
+
}
|
|
725
|
+
}, i === 0);
|
|
726
|
+
const down = iconBtn("down", "Move down", () => {
|
|
727
|
+
if (moveItem(decls, i, 1)) {
|
|
728
|
+
render();
|
|
729
|
+
changed();
|
|
730
|
+
}
|
|
731
|
+
}, i === decls.length - 1);
|
|
732
|
+
const del = iconBtn("close", "Delete property", () => {
|
|
733
|
+
decls.splice(i, 1);
|
|
734
|
+
render();
|
|
735
|
+
changed();
|
|
736
|
+
}, false, true);
|
|
737
|
+
const details = keep(opts.extraDetails?.(p, i));
|
|
738
|
+
if (p.type === "enum" || p.type === "flags") details.push(labelled("Values", tagChips(p, refreshDefault)));
|
|
739
|
+
if (p.type === "quality") details.push(labelled("Stages (in order)", stageChips(p, refreshDefault)));
|
|
740
|
+
if (opts.purpose !== false) {
|
|
741
|
+
const purpose = el("input", "field set-purpose");
|
|
742
|
+
purpose.type = "text";
|
|
743
|
+
purpose.placeholder = "What this property is for";
|
|
744
|
+
purpose.value = p.purpose ?? "";
|
|
745
|
+
purpose.addEventListener("input", () => {
|
|
746
|
+
if (purpose.value.trim()) p.purpose = purpose.value;
|
|
747
|
+
else delete p.purpose;
|
|
748
|
+
changed();
|
|
749
|
+
});
|
|
750
|
+
details.push(labelled("Purpose", purpose));
|
|
751
|
+
}
|
|
752
|
+
return expandableRow({ line: [name, type, def, ...keep(opts.extraLine?.(p, i)), up, down, del], details, name: p.name });
|
|
753
|
+
};
|
|
754
|
+
const render = () => {
|
|
755
|
+
guard.reset();
|
|
756
|
+
list.replaceChildren();
|
|
757
|
+
if (decls.length === 0) list.append(el("p", "empty", opts.emptyText ?? "No properties yet."));
|
|
758
|
+
else decls.forEach((p, i) => list.append(row(p, i)));
|
|
759
|
+
guard.check();
|
|
760
|
+
};
|
|
761
|
+
render();
|
|
762
|
+
const add = el("button", "btn set-add", opts.addLabel ?? "+ Add property");
|
|
763
|
+
add.type = "button";
|
|
764
|
+
add.addEventListener("click", () => {
|
|
765
|
+
decls.push(opts.newDecl ? opts.newDecl() : { name: "", type: types[0]?.[0] ?? "string", ...opts.typed ? {} : { default: "" } });
|
|
766
|
+
render();
|
|
767
|
+
changed();
|
|
768
|
+
focusNewRow(list);
|
|
769
|
+
});
|
|
770
|
+
host.append(list, add);
|
|
771
|
+
return {
|
|
772
|
+
firstDuplicate: () => guard.firstDuplicate(),
|
|
773
|
+
firstIllegalName: () => firstIllegalPropertyName(host),
|
|
774
|
+
// The Save gate takes both faults; the field's own rollover says which.
|
|
775
|
+
firstInvalid: () => guard.firstDuplicate() ?? firstIllegalPropertyName(host),
|
|
776
|
+
render
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
exports.PROPERTY_TYPES = PROPERTY_TYPES;
|
|
781
|
+
exports.defaultControl = defaultControl;
|
|
782
|
+
exports.mountPropertyList = mountPropertyList;
|
|
783
|
+
//# sourceMappingURL=property-list.cjs.map
|
|
784
|
+
//# sourceMappingURL=property-list.cjs.map
|