@wildwinter/app-shell 0.48.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.
package/dist/index.cjs CHANGED
@@ -3120,6 +3120,432 @@ function mountLinkStatus(host, opts) {
3120
3120
  };
3121
3121
  }
3122
3122
 
3123
+ // src/value-control.ts
3124
+ function fallbackText(decl) {
3125
+ const d = decl.default;
3126
+ if (d !== void 0 && d !== null) {
3127
+ if (typeof d === "boolean") return d ? "True" : "False";
3128
+ if (Array.isArray(d)) return d.length === 0 ? "none" : d.join(", ");
3129
+ return d === "" ? "none" : String(d);
3130
+ }
3131
+ switch (decl.type) {
3132
+ case "boolean":
3133
+ return "False";
3134
+ case "number":
3135
+ return "0";
3136
+ case "enum":
3137
+ return decl.values?.[0] ?? "none";
3138
+ case "quality":
3139
+ return decl.stages?.[0] ?? "first stage";
3140
+ case "flags":
3141
+ return "none";
3142
+ default:
3143
+ return "empty";
3144
+ }
3145
+ }
3146
+ var defaultLabel = (decl) => `default (${fallbackText(decl)})`;
3147
+ function stageGateNote(gates) {
3148
+ if (gates <= 0) return "";
3149
+ 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.`;
3150
+ }
3151
+ function rungStrip(opts) {
3152
+ const make = opts.doc === void 0 ? el : (tag, props) => {
3153
+ const node2 = opts.doc.createElement(tag);
3154
+ node2.className = props.className;
3155
+ if (props.text !== void 0) node2.textContent = props.text;
3156
+ if (props.tip !== void 0) {
3157
+ node2.title = props.tip;
3158
+ node2.setAttribute("aria-label", props.tip);
3159
+ }
3160
+ if (props.onClick) node2.addEventListener("click", props.onClick);
3161
+ if (tag === "button") node2.type = "button";
3162
+ return node2;
3163
+ };
3164
+ const strip = make("span", { className: "seg shell-rungs" });
3165
+ const decl = opts.decl ?? { type: "quality", stages: opts.stages };
3166
+ const unset = opts.value === void 0;
3167
+ const fallback = typeof decl.default === "string" && opts.stages.includes(decl.default) ? decl.default : opts.stages[0];
3168
+ const readOnly = opts.onPick === void 0;
3169
+ if (opts.onClear) {
3170
+ strip.append(make("button", {
3171
+ className: `seg-opt shell-rung shell-rung-default${unset ? " on" : ""}`,
3172
+ text: defaultLabel(decl),
3173
+ tip: unset ? "Not set here: the declared default holds" : "Clear the value, back to the default",
3174
+ onClick: () => {
3175
+ if (!unset) opts.onClear();
3176
+ }
3177
+ }));
3178
+ }
3179
+ for (const stage of opts.stages) {
3180
+ const current3 = opts.value === stage;
3181
+ const falls = unset && stage === fallback;
3182
+ const rung = make(readOnly ? "span" : "button", {
3183
+ className: `seg-opt shell-rung${current3 ? " on" : ""}${falls ? " fallback" : ""}`,
3184
+ text: stage,
3185
+ tip: current3 ? "The current stage" : falls ? "The stage it falls back to" : readOnly ? stage : opts.pickTip?.(stage) ?? `Set to "${stage}"`,
3186
+ ...readOnly ? {} : { onClick: () => {
3187
+ if (!current3) opts.onPick(stage);
3188
+ } }
3189
+ });
3190
+ strip.append(rung);
3191
+ }
3192
+ if (!unset && !opts.stages.includes(opts.value)) {
3193
+ strip.append(make("span", {
3194
+ className: "seg-opt shell-rung stray on",
3195
+ text: opts.value,
3196
+ tip: `"${opts.value}" is not one of the stages: pick the one it belongs at now`
3197
+ }));
3198
+ }
3199
+ return strip;
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
+ }
3548
+
3123
3549
  // src/property-list.ts
3124
3550
  var PROPERTY_TYPES = [
3125
3551
  ["number", "Number"],
@@ -3135,35 +3561,37 @@ var option = (value, label2, selected) => {
3135
3561
  o.selected = selected;
3136
3562
  return o;
3137
3563
  };
3138
- var NONE = "(none)";
3564
+ var NONE_CHOSEN = "\0none";
3139
3565
  function defaultControl(p, onChange, opts = {}) {
3140
3566
  const typed = opts.typed === true;
3141
3567
  const write = (raw) => {
3142
- if (raw === "") {
3143
- if (typed) delete p.default;
3144
- else p.default = "";
3145
- } else if (typed && p.type === "boolean") p.default = raw === "true";
3568
+ if (raw === "") delete p.default;
3569
+ else if (raw === NONE_CHOSEN) p.default = "";
3570
+ else if (typed && p.type === "boolean") p.default = raw === "true";
3146
3571
  else if (typed && p.type === "number") p.default = Number(raw);
3147
3572
  else p.default = raw;
3148
3573
  onChange?.();
3149
3574
  };
3150
3575
  const current3 = p.default === void 0 || p.default === null ? "" : String(p.default);
3576
+ const unsetLabel = opts.unsetLabel ?? defaultLabel({ ...p, default: void 0 });
3151
3577
  if (p.type === "boolean") {
3152
3578
  const sel = el("select", "field set-default");
3153
- sel.append(option("", NONE, current3 === ""), option("true", "True", current3 === "true"), option("false", "False", current3 === "false"));
3579
+ sel.append(option("", unsetLabel, current3 === ""), option("true", "True", current3 === "true"), option("false", "False", current3 === "false"));
3154
3580
  sel.addEventListener("change", () => write(sel.value));
3155
3581
  return sel;
3156
3582
  }
3157
3583
  if (p.type === "enum") {
3158
3584
  const sel = el("select", "field set-default");
3159
- sel.append(option("", NONE, current3 === ""));
3585
+ const none = opts.noneChosen === true && p.default === "";
3586
+ sel.append(option("", unsetLabel, current3 === "" && !none));
3587
+ if (opts.noneChosen) sel.append(option(NONE_CHOSEN, "(none)", none));
3160
3588
  for (const v of p.values ?? []) sel.append(option(v, v, current3 === v));
3161
3589
  sel.addEventListener("change", () => write(sel.value));
3162
3590
  return sel;
3163
3591
  }
3164
3592
  if (p.type === "quality") {
3165
3593
  const sel = el("select", "field set-default");
3166
- sel.append(option("", "(first stage)", current3 === ""));
3594
+ sel.append(option("", unsetLabel, current3 === ""));
3167
3595
  for (const v of p.stages ?? []) sel.append(option(v, v, current3 === v));
3168
3596
  sel.addEventListener("change", () => write(sel.value));
3169
3597
  return sel;
@@ -3175,7 +3603,7 @@ function defaultControl(p, onChange, opts = {}) {
3175
3603
  }
3176
3604
  const input = el("input", "field set-default");
3177
3605
  input.type = p.type === "number" ? "number" : "text";
3178
- input.placeholder = "Default";
3606
+ input.placeholder = p.type === "number" ? `Default (${fallbackText({ type: "number" })})` : "Default";
3179
3607
  input.value = current3;
3180
3608
  input.addEventListener("input", () => write(input.value));
3181
3609
  return input;
@@ -3186,24 +3614,32 @@ function mountPropertyList(host, decls, opts = {}) {
3186
3614
  const types = opts.types ?? PROPERTY_TYPES;
3187
3615
  const list = el("div", "set-list");
3188
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";
3189
3620
  const row = (p, i) => {
3190
3621
  const name = el("input", "field set-name");
3191
3622
  name.type = "text";
3192
- name.placeholder = "Property name";
3623
+ name.placeholder = `${Noun} name`;
3193
3624
  name.value = p.name;
3194
3625
  name.spellcheck = false;
3195
- name.dataset["tip"] = PROPERTY_NAME_HINT;
3196
- bindPropertyName(name, (v) => {
3197
- p.name = v;
3626
+ if (freeNames) name.addEventListener("input", () => {
3627
+ p.name = name.value;
3198
3628
  changed();
3199
- }, { hint: PROPERTY_NAME_HINT });
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
+ }
3200
3637
  guard.track(name);
3201
3638
  const type = el("select", "field set-type");
3202
3639
  for (const [v, label2] of types) type.append(option(v, label2, v === p.type));
3203
3640
  type.addEventListener("change", () => {
3204
3641
  p.type = type.value;
3205
- if (opts.typed) delete p.default;
3206
- else p.default = "";
3642
+ delete p.default;
3207
3643
  if (p.type === "enum" || p.type === "flags") p.values ??= [];
3208
3644
  else delete p.values;
3209
3645
  if (p.type === "quality") p.stages ??= [];
@@ -3230,7 +3666,7 @@ function mountPropertyList(host, decls, opts = {}) {
3230
3666
  changed();
3231
3667
  }
3232
3668
  }, i === decls.length - 1);
3233
- const del = iconBtn("close", "Delete property", () => {
3669
+ const del = iconBtn("close", `Delete ${noun}`, () => {
3234
3670
  decls.splice(i, 1);
3235
3671
  render();
3236
3672
  changed();
@@ -3255,7 +3691,7 @@ function mountPropertyList(host, decls, opts = {}) {
3255
3691
  if (opts.purpose !== false) {
3256
3692
  const purpose = el("input", "field set-purpose");
3257
3693
  purpose.type = "text";
3258
- purpose.placeholder = "What this property is for";
3694
+ purpose.placeholder = `What this ${noun} is for`;
3259
3695
  purpose.value = p.purpose ?? "";
3260
3696
  purpose.addEventListener("input", () => {
3261
3697
  if (purpose.value.trim()) p.purpose = purpose.value;
@@ -3285,9 +3721,9 @@ function mountPropertyList(host, decls, opts = {}) {
3285
3721
  host.append(list, add);
3286
3722
  return {
3287
3723
  firstDuplicate: () => guard.firstDuplicate(),
3288
- firstIllegalName: () => firstIllegalPropertyName(host),
3724
+ firstIllegalName: () => freeNames ? null : firstIllegalPropertyName(host),
3289
3725
  // The Save gate takes both faults; the field's own rollover says which.
3290
- firstInvalid: () => guard.firstDuplicate() ?? firstIllegalPropertyName(host),
3726
+ firstInvalid: () => guard.firstDuplicate() ?? (freeNames ? null : firstIllegalPropertyName(host)),
3291
3727
  render
3292
3728
  };
3293
3729
  }
@@ -3390,6 +3826,7 @@ exports.createNavHistory = createNavHistory;
3390
3826
  exports.createSaveController = createSaveController;
3391
3827
  exports.debounce = debounce;
3392
3828
  exports.defaultControl = defaultControl;
3829
+ exports.defaultLabel = defaultLabel;
3393
3830
  exports.defaultProblemCopy = defaultProblemCopy;
3394
3831
  exports.describeProblem = describeProblem;
3395
3832
  exports.dialogFrame = dialogFrame;
@@ -3397,7 +3834,11 @@ exports.dismissToast = dismissToast;
3397
3834
  exports.dupGuard = dupGuard;
3398
3835
  exports.el = el;
3399
3836
  exports.expandableRow = expandableRow;
3837
+ exports.fallbackText = fallbackText;
3400
3838
  exports.feedUpdaterDownloadProgress = feedUpdaterDownloadProgress;
3839
+ exports.fieldRow = fieldRow;
3840
+ exports.fieldRows = fieldRows;
3841
+ exports.fieldValueProblem = fieldValueProblem;
3401
3842
  exports.firstIllegalPropertyName = firstIllegalPropertyName;
3402
3843
  exports.focusNewRow = focusNewRow;
3403
3844
  exports.foldVc = foldVc;
@@ -3462,11 +3903,13 @@ exports.renderStepperBar = renderStepperBar;
3462
3903
  exports.revalidatePropertyRefs = revalidatePropertyRefs;
3463
3904
  exports.revealRow = revealRow;
3464
3905
  exports.revealRowWhenReady = revealRowWhenReady;
3906
+ exports.rungStrip = rungStrip;
3465
3907
  exports.saveIndicator = saveIndicator;
3466
3908
  exports.setKeyPlatform = setKeyPlatform;
3467
3909
  exports.showAbout = showAbout;
3468
3910
  exports.showUpdaterDialog = showUpdaterDialog;
3469
3911
  exports.stageChips = stageChips;
3912
+ exports.stageGateNote = stageGateNote;
3470
3913
  exports.staleBar = staleBar;
3471
3914
  exports.tagChips = tagChips;
3472
3915
  exports.tipAt = tipAt;