@wildwinter/app-shell 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 = "Property name";
3623
+ name.placeholder = `${Noun} name`;
3274
3624
  name.value = p.name;
3275
3625
  name.spellcheck = false;
3276
- name.dataset["tip"] = PROPERTY_NAME_HINT;
3277
- bindPropertyName(name, (v) => {
3278
- p.name = v;
3626
+ if (freeNames) name.addEventListener("input", () => {
3627
+ p.name = name.value;
3279
3628
  changed();
3280
- }, { 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
+ }
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", "Delete property", () => {
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 = "What this property is for";
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;