@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.
@@ -117,17 +117,30 @@ interface PropertyDeclLike {
117
117
  declare const PROPERTY_TYPES: ReadonlyArray<readonly [PropertyListType, string]>;
118
118
  interface DefaultControlOptions {
119
119
  /**
120
- * How the default is stored. `false` (default): the string the field shows,
121
- * "" for none (Storyletter's DTO). `true`: a typed value (boolean, number,
122
- * string), and none DELETES the key (Patterpad's model).
120
+ * How the default is stored. `false` (default): the string the field shows
121
+ * (Storyletter's DTO). `true`: a typed value (boolean, number, string)
122
+ * (Patterpad's model). Either way, no default given DELETES the key: a
123
+ * default is optional in source, and the type's holds (the family's rule,
124
+ * ruled 2026-10-18).
123
125
  */
124
126
  typed?: boolean;
127
+ /**
128
+ * An enum may hold "" (none chosen), as a field's may: the select then
129
+ * offers "(none)" beside "default (x)". Off for a property, whose value is
130
+ * always one of its values.
131
+ */
132
+ noneChosen?: boolean;
133
+ /** What the blank says instead of the type's default, where something else holds when none is given
134
+ * here: a tag's starting value falls back to its group's default, say. */
135
+ unsetLabel?: string;
125
136
  }
126
137
  /**
127
138
  * The type-appropriate control for a declaration's DEFAULT: a true / false
128
139
  * picker for a boolean, the value list for an enum, the stage list for a
129
- * quality (blank = the first rung), a note for flags (a set starts empty), a
130
- * number or text field otherwise. The one label for "no default" is "(none)".
140
+ * quality, a note for flags (a set starts empty), a number or text field
141
+ * otherwise. Leaving it blank gives no default, and the blank says what holds
142
+ * then, "default (x)" (`defaultLabel`): a default is optional, and the type's
143
+ * fills in.
131
144
  */
132
145
  declare function defaultControl(p: PropertyDeclLike, onChange?: () => void, opts?: DefaultControlOptions): HTMLElement;
133
146
  interface PropertyListOptions<T extends PropertyDeclLike> extends DefaultControlOptions {
@@ -149,6 +162,15 @@ interface PropertyListOptions<T extends PropertyDeclLike> extends DefaultControl
149
162
  extraDetails?: (decl: T, index: number) => (HTMLElement | null)[];
150
163
  /** Offer the Purpose field. Default true. */
151
164
  purpose?: boolean;
165
+ /** What a row declares, in the row's own words ("Field name", "What this field is for", "Delete
166
+ * field"). Default "property". */
167
+ noun?: string;
168
+ /**
169
+ * The rule a name follows. `"property"` (default): the property-name rules, bound and gated, since an
170
+ * expression must be able to reach it. `"free"`: any name, only duplicates refused, for a declaration no
171
+ * expression reads (a Patterpad game-data field's name is the host's word).
172
+ */
173
+ names?: "property" | "free";
152
174
  /**
153
175
  * A quality's stage was renamed in place (the declaration's own default
154
176
  * already follows it). Given, the rename is handed here INSTEAD of
@@ -117,17 +117,30 @@ interface PropertyDeclLike {
117
117
  declare const PROPERTY_TYPES: ReadonlyArray<readonly [PropertyListType, string]>;
118
118
  interface DefaultControlOptions {
119
119
  /**
120
- * How the default is stored. `false` (default): the string the field shows,
121
- * "" for none (Storyletter's DTO). `true`: a typed value (boolean, number,
122
- * string), and none DELETES the key (Patterpad's model).
120
+ * How the default is stored. `false` (default): the string the field shows
121
+ * (Storyletter's DTO). `true`: a typed value (boolean, number, string)
122
+ * (Patterpad's model). Either way, no default given DELETES the key: a
123
+ * default is optional in source, and the type's holds (the family's rule,
124
+ * ruled 2026-10-18).
123
125
  */
124
126
  typed?: boolean;
127
+ /**
128
+ * An enum may hold "" (none chosen), as a field's may: the select then
129
+ * offers "(none)" beside "default (x)". Off for a property, whose value is
130
+ * always one of its values.
131
+ */
132
+ noneChosen?: boolean;
133
+ /** What the blank says instead of the type's default, where something else holds when none is given
134
+ * here: a tag's starting value falls back to its group's default, say. */
135
+ unsetLabel?: string;
125
136
  }
126
137
  /**
127
138
  * The type-appropriate control for a declaration's DEFAULT: a true / false
128
139
  * picker for a boolean, the value list for an enum, the stage list for a
129
- * quality (blank = the first rung), a note for flags (a set starts empty), a
130
- * number or text field otherwise. The one label for "no default" is "(none)".
140
+ * quality, a note for flags (a set starts empty), a number or text field
141
+ * otherwise. Leaving it blank gives no default, and the blank says what holds
142
+ * then, "default (x)" (`defaultLabel`): a default is optional, and the type's
143
+ * fills in.
131
144
  */
132
145
  declare function defaultControl(p: PropertyDeclLike, onChange?: () => void, opts?: DefaultControlOptions): HTMLElement;
133
146
  interface PropertyListOptions<T extends PropertyDeclLike> extends DefaultControlOptions {
@@ -149,6 +162,15 @@ interface PropertyListOptions<T extends PropertyDeclLike> extends DefaultControl
149
162
  extraDetails?: (decl: T, index: number) => (HTMLElement | null)[];
150
163
  /** Offer the Purpose field. Default true. */
151
164
  purpose?: boolean;
165
+ /** What a row declares, in the row's own words ("Field name", "What this field is for", "Delete
166
+ * field"). Default "property". */
167
+ noun?: string;
168
+ /**
169
+ * The rule a name follows. `"property"` (default): the property-name rules, bound and gated, since an
170
+ * expression must be able to reach it. `"free"`: any name, only duplicates refused, for a declaration no
171
+ * expression reads (a Patterpad game-data field's name is the host's word).
172
+ */
173
+ names?: "property" | "free";
152
174
  /**
153
175
  * A quality's stage was renamed in place (the declaration's own default
154
176
  * already follows it). Given, the rename is handed here INSTEAD of
@@ -671,6 +671,31 @@ function firstIllegalPropertyName(host) {
671
671
  return host.querySelector("input.illegal");
672
672
  }
673
673
 
674
+ // src/value-control.ts
675
+ function fallbackText(decl) {
676
+ const d = decl.default;
677
+ if (d !== void 0 && d !== null) {
678
+ if (typeof d === "boolean") return d ? "True" : "False";
679
+ if (Array.isArray(d)) return d.length === 0 ? "none" : d.join(", ");
680
+ return d === "" ? "none" : String(d);
681
+ }
682
+ switch (decl.type) {
683
+ case "boolean":
684
+ return "False";
685
+ case "number":
686
+ return "0";
687
+ case "enum":
688
+ return decl.values?.[0] ?? "none";
689
+ case "quality":
690
+ return decl.stages?.[0] ?? "first stage";
691
+ case "flags":
692
+ return "none";
693
+ default:
694
+ return "empty";
695
+ }
696
+ }
697
+ var defaultLabel = (decl) => `default (${fallbackText(decl)})`;
698
+
674
699
  // src/property-list.ts
675
700
  var PROPERTY_TYPES = [
676
701
  ["number", "Number"],
@@ -686,35 +711,37 @@ var option = (value, label, selected) => {
686
711
  o.selected = selected;
687
712
  return o;
688
713
  };
689
- var NONE = "(none)";
714
+ var NONE_CHOSEN = "\0none";
690
715
  function defaultControl(p, onChange, opts = {}) {
691
716
  const typed = opts.typed === true;
692
717
  const write = (raw) => {
693
- if (raw === "") {
694
- if (typed) delete p.default;
695
- else p.default = "";
696
- } else if (typed && p.type === "boolean") p.default = raw === "true";
718
+ if (raw === "") delete p.default;
719
+ else if (raw === NONE_CHOSEN) p.default = "";
720
+ else if (typed && p.type === "boolean") p.default = raw === "true";
697
721
  else if (typed && p.type === "number") p.default = Number(raw);
698
722
  else p.default = raw;
699
723
  onChange?.();
700
724
  };
701
725
  const current = p.default === void 0 || p.default === null ? "" : String(p.default);
726
+ const unsetLabel = opts.unsetLabel ?? defaultLabel({ ...p, default: void 0 });
702
727
  if (p.type === "boolean") {
703
728
  const sel = el("select", "field set-default");
704
- sel.append(option("", NONE, current === ""), option("true", "True", current === "true"), option("false", "False", current === "false"));
729
+ sel.append(option("", unsetLabel, current === ""), option("true", "True", current === "true"), option("false", "False", current === "false"));
705
730
  sel.addEventListener("change", () => write(sel.value));
706
731
  return sel;
707
732
  }
708
733
  if (p.type === "enum") {
709
734
  const sel = el("select", "field set-default");
710
- sel.append(option("", NONE, current === ""));
735
+ const none = opts.noneChosen === true && p.default === "";
736
+ sel.append(option("", unsetLabel, current === "" && !none));
737
+ if (opts.noneChosen) sel.append(option(NONE_CHOSEN, "(none)", none));
711
738
  for (const v of p.values ?? []) sel.append(option(v, v, current === v));
712
739
  sel.addEventListener("change", () => write(sel.value));
713
740
  return sel;
714
741
  }
715
742
  if (p.type === "quality") {
716
743
  const sel = el("select", "field set-default");
717
- sel.append(option("", "(first stage)", current === ""));
744
+ sel.append(option("", unsetLabel, current === ""));
718
745
  for (const v of p.stages ?? []) sel.append(option(v, v, current === v));
719
746
  sel.addEventListener("change", () => write(sel.value));
720
747
  return sel;
@@ -726,7 +753,7 @@ function defaultControl(p, onChange, opts = {}) {
726
753
  }
727
754
  const input = el("input", "field set-default");
728
755
  input.type = p.type === "number" ? "number" : "text";
729
- input.placeholder = "Default";
756
+ input.placeholder = p.type === "number" ? `Default (${fallbackText({ type: "number" })})` : "Default";
730
757
  input.value = current;
731
758
  input.addEventListener("input", () => write(input.value));
732
759
  return input;
@@ -737,24 +764,32 @@ function mountPropertyList(host, decls, opts = {}) {
737
764
  const types = opts.types ?? PROPERTY_TYPES;
738
765
  const list = el("div", "set-list");
739
766
  const keep = (parts) => (parts ?? []).filter((p) => p !== null);
767
+ const noun = opts.noun ?? "property";
768
+ const Noun = noun.charAt(0).toUpperCase() + noun.slice(1);
769
+ const freeNames = opts.names === "free";
740
770
  const row = (p, i) => {
741
771
  const name = el("input", "field set-name");
742
772
  name.type = "text";
743
- name.placeholder = "Property name";
773
+ name.placeholder = `${Noun} name`;
744
774
  name.value = p.name;
745
775
  name.spellcheck = false;
746
- name.dataset["tip"] = PROPERTY_NAME_HINT;
747
- bindPropertyName(name, (v) => {
748
- p.name = v;
776
+ if (freeNames) name.addEventListener("input", () => {
777
+ p.name = name.value;
749
778
  changed();
750
- }, { hint: PROPERTY_NAME_HINT });
779
+ });
780
+ else {
781
+ name.dataset["tip"] = PROPERTY_NAME_HINT;
782
+ bindPropertyName(name, (v) => {
783
+ p.name = v;
784
+ changed();
785
+ }, { hint: PROPERTY_NAME_HINT });
786
+ }
751
787
  guard.track(name);
752
788
  const type = el("select", "field set-type");
753
789
  for (const [v, label] of types) type.append(option(v, label, v === p.type));
754
790
  type.addEventListener("change", () => {
755
791
  p.type = type.value;
756
- if (opts.typed) delete p.default;
757
- else p.default = "";
792
+ delete p.default;
758
793
  if (p.type === "enum" || p.type === "flags") p.values ??= [];
759
794
  else delete p.values;
760
795
  if (p.type === "quality") p.stages ??= [];
@@ -781,7 +816,7 @@ function mountPropertyList(host, decls, opts = {}) {
781
816
  changed();
782
817
  }
783
818
  }, i === decls.length - 1);
784
- const del = iconBtn("close", "Delete property", () => {
819
+ const del = iconBtn("close", `Delete ${noun}`, () => {
785
820
  decls.splice(i, 1);
786
821
  render();
787
822
  changed();
@@ -806,7 +841,7 @@ function mountPropertyList(host, decls, opts = {}) {
806
841
  if (opts.purpose !== false) {
807
842
  const purpose = el("input", "field set-purpose");
808
843
  purpose.type = "text";
809
- purpose.placeholder = "What this property is for";
844
+ purpose.placeholder = `What this ${noun} is for`;
810
845
  purpose.value = p.purpose ?? "";
811
846
  purpose.addEventListener("input", () => {
812
847
  if (purpose.value.trim()) p.purpose = purpose.value;
@@ -836,9 +871,9 @@ function mountPropertyList(host, decls, opts = {}) {
836
871
  host.append(list, add);
837
872
  return {
838
873
  firstDuplicate: () => guard.firstDuplicate(),
839
- firstIllegalName: () => firstIllegalPropertyName(host),
874
+ firstIllegalName: () => freeNames ? null : firstIllegalPropertyName(host),
840
875
  // The Save gate takes both faults; the field's own rollover says which.
841
- firstInvalid: () => guard.firstDuplicate() ?? firstIllegalPropertyName(host),
876
+ firstInvalid: () => guard.firstDuplicate() ?? (freeNames ? null : firstIllegalPropertyName(host)),
842
877
  render
843
878
  };
844
879
  }