@ai-matrx/content-ir-react 0.19.0 → 0.20.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/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.20.0 — 2026-10-02
4
+
5
+ The G8A review of the action cards (nightly clone). Guard:
6
+ `__tests__/directive-card-g8a.test.tsx` (4 red on 0.19.2).
7
+
8
+ - **One value, one word.** New host seam `DirectiveHost.valueLabel({ noun, field, value })`
9
+ — the app's word for a pick-list value, the same word its forms and screens show. The
10
+ change list words old AND new through it ("Status Inbox → Completed", never
11
+ "incomplete → completed"); card facts too. New: `DirectiveValueRef`,
12
+ `DirectiveValueWord`, `ItemChangesOptions.valueWord`, `directiveValueWord(host, noun)`.
13
+ - **An empty value reads "None"** — before and after, card and confirm (was "empty" /
14
+ "cleared"). `EMPTY_VALUE_WORD`.
15
+ - **A value someone must judge is never cut.** The change list wraps every value (no CSS
16
+ truncation) and, below a 20rem container, stacks label / old / new one per line
17
+ (container query — a narrow panel stacks too). `CHANGE_VALUE_MAX` 60 → 280: only prose
18
+ longer than that is shortened.
19
+ - **One field-label rule.** `directiveFieldLabel(key, titleColumn)` — the title column reads
20
+ "Title", every other field through THE humanizer — for change rows AND card facts. A
21
+ create card no longer repeats its title column as a fact ("workbook name"). Facts read
22
+ "Label value".
23
+
24
+ ### Consumer action
25
+
26
+ - Pass `valueLabel` on your `DirectiveHost` and the same `valueWord` to any `itemChanges` you
27
+ call yourself (a host confirm), so card and confirm say the same words.
28
+
29
+ ## 0.19.2
30
+ ### Directive field labels use THE humanizer
31
+
32
+ - `fieldLabel` (directive item changes) goes through `humanizeIdentifier`: `due_date` → "Due Date".
33
+ - Consumer action: none.
34
+
35
+ ## 0.19.1 — 2026-10-02
36
+
37
+ - **One record-revision store per page.** Every entry of this package (`.`,
38
+ `./directives`, …) is bundled self-contained, so 0.19.0's module-level counter
39
+ was a separate counter per entry: an apply announced through one entry was
40
+ never seen by a reader imported through the other. The store now lives in a
41
+ `Symbol.for("@ai-matrx/content-ir-react/record-revisions")` slot on
42
+ `globalThis`. Guard: `directive-record-values-g7.test.tsx` ("the store is ONE
43
+ per page…", red on 0.19.0).
44
+
45
+ No consumer action.
46
+
3
47
  ## 0.19.0 — 2026-10-02
4
48
 
5
49
  - **A record's old value is the value it holds NOW.** `useRecordValues` read a
@@ -174,6 +174,15 @@ interface DirectiveHost {
174
174
  openItem?: (options: DirectiveOpenItemOptions) => void;
175
175
  /** Host copy controls. */
176
176
  renderCopy?: (props: DirectiveCopyProps) => ReactNode;
177
+ /**
178
+ * The app's word for one stored value of one pick-list field — the SAME word
179
+ * the record's own screens and the write forms show ("incomplete" on a task
180
+ * reads "Inbox"). One value, one display word everywhere (G8A review,
181
+ * 2026-10-02: the card said "Status incomplete → completed"). Return null for
182
+ * a field that is not a pick-list: its value is shown as written. Absent →
183
+ * every value is shown as stored.
184
+ */
185
+ valueLabel?: (ref: DirectiveValueRef) => string | null;
177
186
  /** The mirrored noun catalog (label / family / title column). */
178
187
  nouns?: DirectiveNounCatalog;
179
188
  /** slug → the kind ONE item is (THE DIRECTIVE⇄KIND SEAM), or null. */
@@ -189,6 +198,12 @@ interface DirectiveHost {
189
198
  */
190
199
  explainFailure?: (failure: DirectiveFailure) => DirectiveFailureWords | null;
191
200
  }
201
+ /** One stored value of one field of one record type. */
202
+ interface DirectiveValueRef {
203
+ noun: string;
204
+ field: string;
205
+ value: string;
206
+ }
192
207
  /** One failed apply, as the card knows it. */
193
208
  interface DirectiveFailure {
194
209
  /** The server's / host's own text — technical; shown only behind Details. */
@@ -233,4 +248,4 @@ declare function DirectiveHostProvider({ host, children }: DirectiveHostProvider
233
248
  */
234
249
  declare function useDirectiveHost(): DirectiveHost;
235
250
 
236
- export { type DirectiveFailure as D, type DirectiveFailureWords as a, type DirectiveRecordRef as b, type DirectiveApplyResult as c, type DirectiveApplyState as d, type DirectiveAskRequest as e, type DirectiveCopyProps as f, type DirectiveHost as g, DirectiveHostProvider as h, type DirectiveHostProviderProps as i, type DirectiveOpenItemOptions as j, type DirectiveRecordProps as k, type DirectiveScope as l, DirectiveScopeProvider as m, type DirectiveShell as n, useDirectiveScope as o, useDirectiveHost as u };
251
+ export { type DirectiveHost as D, type DirectiveFailure as a, type DirectiveFailureWords as b, type DirectiveRecordRef as c, type DirectiveApplyResult as d, type DirectiveApplyState as e, type DirectiveAskRequest as f, type DirectiveCopyProps as g, DirectiveHostProvider as h, type DirectiveHostProviderProps as i, type DirectiveOpenItemOptions as j, type DirectiveRecordProps as k, type DirectiveScope as l, DirectiveScopeProvider as m, type DirectiveShell as n, type DirectiveValueRef as o, useDirectiveScope as p, useDirectiveHost as u };
@@ -174,6 +174,15 @@ interface DirectiveHost {
174
174
  openItem?: (options: DirectiveOpenItemOptions) => void;
175
175
  /** Host copy controls. */
176
176
  renderCopy?: (props: DirectiveCopyProps) => ReactNode;
177
+ /**
178
+ * The app's word for one stored value of one pick-list field — the SAME word
179
+ * the record's own screens and the write forms show ("incomplete" on a task
180
+ * reads "Inbox"). One value, one display word everywhere (G8A review,
181
+ * 2026-10-02: the card said "Status incomplete → completed"). Return null for
182
+ * a field that is not a pick-list: its value is shown as written. Absent →
183
+ * every value is shown as stored.
184
+ */
185
+ valueLabel?: (ref: DirectiveValueRef) => string | null;
177
186
  /** The mirrored noun catalog (label / family / title column). */
178
187
  nouns?: DirectiveNounCatalog;
179
188
  /** slug → the kind ONE item is (THE DIRECTIVE⇄KIND SEAM), or null. */
@@ -189,6 +198,12 @@ interface DirectiveHost {
189
198
  */
190
199
  explainFailure?: (failure: DirectiveFailure) => DirectiveFailureWords | null;
191
200
  }
201
+ /** One stored value of one field of one record type. */
202
+ interface DirectiveValueRef {
203
+ noun: string;
204
+ field: string;
205
+ value: string;
206
+ }
192
207
  /** One failed apply, as the card knows it. */
193
208
  interface DirectiveFailure {
194
209
  /** The server's / host's own text — technical; shown only behind Details. */
@@ -233,4 +248,4 @@ declare function DirectiveHostProvider({ host, children }: DirectiveHostProvider
233
248
  */
234
249
  declare function useDirectiveHost(): DirectiveHost;
235
250
 
236
- export { type DirectiveFailure as D, type DirectiveFailureWords as a, type DirectiveRecordRef as b, type DirectiveApplyResult as c, type DirectiveApplyState as d, type DirectiveAskRequest as e, type DirectiveCopyProps as f, type DirectiveHost as g, DirectiveHostProvider as h, type DirectiveHostProviderProps as i, type DirectiveOpenItemOptions as j, type DirectiveRecordProps as k, type DirectiveScope as l, DirectiveScopeProvider as m, type DirectiveShell as n, useDirectiveScope as o, useDirectiveHost as u };
251
+ export { type DirectiveHost as D, type DirectiveFailure as a, type DirectiveFailureWords as b, type DirectiveRecordRef as c, type DirectiveApplyResult as d, type DirectiveApplyState as e, type DirectiveAskRequest as f, type DirectiveCopyProps as g, DirectiveHostProvider as h, type DirectiveHostProviderProps as i, type DirectiveOpenItemOptions as j, type DirectiveRecordProps as k, type DirectiveScope as l, DirectiveScopeProvider as m, type DirectiveShell as n, type DirectiveValueRef as o, useDirectiveScope as p, useDirectiveHost as u };
@@ -29,10 +29,13 @@ __export(directives_exports, {
29
29
  DirectiveProvisionalCard: () => DirectiveProvisionalCard,
30
30
  DirectiveRender: () => DirectiveRender,
31
31
  DirectiveScopeProvider: () => DirectiveScopeProvider,
32
+ EMPTY_VALUE_WORD: () => EMPTY_VALUE_WORD,
32
33
  SideEffectDirectiveCard: () => SideEffectDirectiveCard,
33
34
  TARGETING_CLASSES: () => TARGETING_CLASSES,
34
35
  defaultFailureWords: () => defaultFailureWords,
35
36
  describeChangeValue: () => describeChangeValue,
37
+ directiveFieldLabel: () => directiveFieldLabel,
38
+ directiveValueWord: () => directiveValueWord,
36
39
  failureWords: () => failureWords,
37
40
  fieldLabel: () => fieldLabel,
38
41
  getDirectiveRenderer: () => getDirectiveRenderer,
@@ -99,9 +102,10 @@ var import_content_ir2 = require("@ai-matrx/content-ir");
99
102
 
100
103
  // directives/item-changes.ts
101
104
  var import_uuid = require("@ai-matrx/kit/uuid");
105
+ var import_text_case = require("@ai-matrx/kit/text-case");
102
106
  var TARGETING_CLASSES = /* @__PURE__ */ new Set(["update", "delete"]);
103
107
  var NOT_A_CHANGE = /* @__PURE__ */ new Set(["__kind", "id"]);
104
- var CHANGE_VALUE_MAX = 60;
108
+ var CHANGE_VALUE_MAX = 280;
105
109
  function itemRecordId(item) {
106
110
  const id = item.id;
107
111
  if (typeof id === "string" && id.trim()) return id.trim();
@@ -109,21 +113,27 @@ function itemRecordId(item) {
109
113
  return null;
110
114
  }
111
115
  function fieldLabel(key) {
112
- const spaced = key.replace(/_/g, " ").trim();
113
- return spaced.charAt(0).toUpperCase() + spaced.slice(1);
116
+ return (0, import_text_case.humanizeIdentifier)(key) || key;
114
117
  }
118
+ function directiveFieldLabel(key, titleColumn) {
119
+ return titleColumn && key === titleColumn ? "Title" : fieldLabel(key);
120
+ }
121
+ var EMPTY_VALUE_WORD = "None";
115
122
  function recordFallbackName(nounLabel3, id) {
116
123
  const short = (0, import_uuid.isUuidShape)(id) ? id.slice(0, 8) : id.length > 12 ? `${id.slice(0, 12)}\u2026` : id;
117
124
  return `${nounLabel3} ${short}`;
118
125
  }
119
- function describeChangeValue(value) {
120
- return describeValue(value);
126
+ function describeChangeValue(value, word) {
127
+ return describeValue(value, word);
121
128
  }
122
- function describeValue(value) {
123
- if (value === null || value === void 0) return { value: "cleared", full: "cleared", clears: true };
129
+ var EMPTY = { value: EMPTY_VALUE_WORD, full: EMPTY_VALUE_WORD, clears: true };
130
+ function describeValue(value, word) {
131
+ if (value === null || value === void 0) return EMPTY;
124
132
  if (typeof value === "string") {
125
133
  const trimmed = value.trim();
126
- if (!trimmed) return { value: "cleared", full: "cleared", clears: true };
134
+ if (!trimmed) return EMPTY;
135
+ const named = word?.(trimmed) ?? null;
136
+ if (named) return { value: named, full: named, clears: false };
127
137
  const oneLine = trimmed.replace(/\s+/g, " ");
128
138
  const shown = oneLine.length > CHANGE_VALUE_MAX ? `${oneLine.slice(0, CHANGE_VALUE_MAX - 1)}\u2026` : oneLine;
129
139
  return { value: shown, full: trimmed, clears: false };
@@ -133,11 +143,11 @@ function describeValue(value) {
133
143
  }
134
144
  if (Array.isArray(value)) {
135
145
  const n2 = value.length;
136
- const text2 = n2 === 0 ? "cleared" : `${n2} ${n2 === 1 ? "entry" : "entries"}`;
146
+ const text2 = n2 === 0 ? EMPTY_VALUE_WORD : `${n2} ${n2 === 1 ? "entry" : "entries"}`;
137
147
  return { value: text2, full: safeJson(value), clears: n2 === 0 };
138
148
  }
139
149
  const n = Object.keys(value).length;
140
- const text = n === 0 ? "cleared" : `${n} ${n === 1 ? "field" : "fields"}`;
150
+ const text = n === 0 ? EMPTY_VALUE_WORD : `${n} ${n === 1 ? "field" : "fields"}`;
141
151
  return { value: text, full: safeJson(value), clears: n === 0 };
142
152
  }
143
153
  function safeJson(value) {
@@ -162,10 +172,12 @@ function itemChanges(item, current, options = {}) {
162
172
  const titleColumn = options.titleColumn ?? null;
163
173
  for (const [key, raw] of Object.entries(item)) {
164
174
  if (NOT_A_CHANGE.has(key)) continue;
165
- const label = key === titleColumn ? "Title" : fieldLabel(key);
166
- const change = { key, label, ...describeValue(raw) };
175
+ const label = directiveFieldLabel(key, titleColumn);
176
+ const valueWord = options.valueWord;
177
+ const word = key === titleColumn || !valueWord ? void 0 : (v) => valueWord(key, v);
178
+ const change = { key, label, ...describeValue(raw, word) };
167
179
  if (current && Object.prototype.hasOwnProperty.call(current, key)) {
168
- change.before = describeValue(current[key]);
180
+ change.before = describeValue(current[key], word);
169
181
  change.same = sameValue(current[key], raw);
170
182
  }
171
183
  out.push(change);
@@ -175,8 +187,17 @@ function itemChanges(item, current, options = {}) {
175
187
 
176
188
  // directives/record-values.ts
177
189
  var import_react2 = require("react");
178
- var revisions = /* @__PURE__ */ new Map();
179
- var listeners = /* @__PURE__ */ new Set();
190
+ var STORE_SLOT = /* @__PURE__ */ Symbol.for("@ai-matrx/content-ir-react/record-revisions");
191
+ function revisionStore() {
192
+ const slots = globalThis;
193
+ let store = slots[STORE_SLOT];
194
+ if (!store) {
195
+ store = { revisions: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() };
196
+ slots[STORE_SLOT] = store;
197
+ }
198
+ return store;
199
+ }
200
+ var { revisions, listeners } = revisionStore();
180
201
  function recordRevision(id) {
181
202
  return revisions.get(id) ?? 0;
182
203
  }
@@ -734,6 +755,14 @@ function FailureNotice({
734
755
  var import_react4 = require("react");
735
756
  var import_content_ir3 = require("@ai-matrx/content-ir");
736
757
  var import_jsx_runtime4 = require("react/jsx-runtime");
758
+ function directiveValueWord(host, noun) {
759
+ const valueLabel = host.valueLabel;
760
+ return valueLabel ? (field, value) => valueLabel({ noun, field, value }) : null;
761
+ }
762
+ function omitKey(item, key) {
763
+ const { [key]: _omitted, ...rest } = item;
764
+ return rest;
765
+ }
737
766
  var ROWS_BEFORE_FOLD = 3;
738
767
  function ItemRow({
739
768
  directive,
@@ -750,16 +779,21 @@ function ItemRow({
750
779
  const titleColumn = (0, import_content_ir3.nounTitleColumn)(directive.noun, host.nouns);
751
780
  const title = (0, import_content_ir3.itemTitle)(item, titleColumn, index, total);
752
781
  const subtitle = (0, import_content_ir3.itemSubtitle)(item);
753
- const facts = (0, import_content_ir3.itemFacts)(item);
782
+ const valueWord = directiveValueWord(host, directive.noun);
783
+ const facts = (0, import_content_ir3.itemFacts)(titleColumn ? omitKey(item, titleColumn) : item).map((fact) => ({
784
+ ...fact,
785
+ label: directiveFieldLabel(fact.key, titleColumn),
786
+ value: valueWord?.(fact.key, fact.value) ?? fact.value
787
+ }));
754
788
  const kind = host.itemKind?.(directive.slug) ?? null;
755
789
  const display = (0, import_content_ir3.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
756
790
  const openItem = host.openItem;
757
791
  const body = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
758
792
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "min-w-0 shrink-0 truncate font-medium text-foreground", children: title }),
759
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "hidden min-w-0 flex-1 items-baseline gap-2 overflow-hidden sm:flex", children: facts.map((fact) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "truncate text-[11px] text-muted-foreground", children: [
760
- fact.value,
793
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "hidden min-w-0 flex-1 items-baseline gap-2 overflow-hidden sm:flex", children: facts.map((fact) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "truncate text-[11px] text-muted-foreground", "data-item-fact": fact.key, children: [
794
+ fact.label,
761
795
  " ",
762
- fact.label
796
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-foreground/80", children: fact.value })
763
797
  ] }, fact.key)) }),
764
798
  subtitle && facts.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "hidden truncate text-[11px] text-muted-foreground sm:inline", children: subtitle }) : null,
765
799
  openItem ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MaximizeIcon, { className: "ml-auto h-3 w-3 shrink-0 text-muted-foreground" }) : null
@@ -794,35 +828,36 @@ function DirectiveChangeList({
794
828
  if (changes.length === 0) {
795
829
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: `text-[11px] text-muted-foreground${className ? ` ${className}` : ""}`, children: "Sets no fields \u2014 nothing would change." });
796
830
  }
797
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: `space-y-0.5${className ? ` ${className}` : ""}`, "data-directive-changes": "", children: changes.map((change) => {
831
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: `@container space-y-1${className ? ` ${className}` : ""}`, "data-directive-changes": "", children: changes.map((change) => {
798
832
  const before = change.before && !change.same ? change.before : null;
799
- const was = before ? before.clears ? "empty" : before.value : null;
800
833
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
801
834
  "li",
802
835
  {
803
- className: "flex min-w-0 items-baseline gap-1.5 text-[11px]",
804
- title: before ? `${change.label}: ${before.clears ? "empty" : before.full} \u2192 ${change.full}` : `${change.label} \u2192 ${change.full}`,
836
+ className: "flex min-w-0 flex-col text-[11px] @xs:flex-row @xs:flex-wrap @xs:items-baseline @xs:gap-x-1.5",
837
+ title: before ? `${change.label}: ${before.full} \u2192 ${change.full}` : `${change.label} \u2192 ${change.full}`,
805
838
  "data-change-key": change.key,
806
839
  children: [
807
840
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "shrink-0 text-muted-foreground", children: change.label }),
808
- was !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
841
+ before ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
809
842
  "span",
810
843
  {
811
- className: "min-w-0 max-w-[40%] shrink truncate text-muted-foreground line-through",
844
+ className: "min-w-0 text-muted-foreground line-through [overflow-wrap:anywhere]",
812
845
  "data-change-before": "",
813
- children: was
846
+ children: before.value
814
847
  }
815
848
  ) : null,
816
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground", children: "\u2192" }),
817
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
818
- "span",
819
- {
820
- className: `min-w-0 truncate ${change.clears ? "italic text-muted-foreground" : "text-foreground"}`,
821
- "data-change-after": "",
822
- children: change.value
823
- }
824
- ),
825
- change.same ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "shrink-0 text-muted-foreground", children: "(no change)" }) : null
849
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex min-w-0 items-baseline gap-1.5", children: [
850
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground", children: "\u2192" }),
851
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
852
+ "span",
853
+ {
854
+ className: `min-w-0 [overflow-wrap:anywhere] ${change.clears ? "italic text-muted-foreground" : "text-foreground"}`,
855
+ "data-change-after": "",
856
+ children: change.value
857
+ }
858
+ ),
859
+ change.same ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "shrink-0 text-muted-foreground", children: "(no change)" }) : null
860
+ ] })
826
861
  ]
827
862
  },
828
863
  change.key
@@ -845,7 +880,10 @@ function TargetRow({
845
880
  readsLive ? { noun: directive.noun, id: recordId } : null
846
881
  );
847
882
  const compareTo = phase.phase === "applied" ? phase.before?.[index] ?? null : readsLive ? live.values : null;
848
- const changes = isUpdate ? itemChanges(item, compareTo, { titleColumn: (0, import_content_ir3.nounTitleColumn)(directive.noun, host.nouns) }) : [];
883
+ const changes = isUpdate ? itemChanges(item, compareTo, {
884
+ titleColumn: (0, import_content_ir3.nounTitleColumn)(directive.noun, host.nouns),
885
+ valueWord: directiveValueWord(host, directive.noun)
886
+ }) : [];
849
887
  const name = host.renderRecord ? host.renderRecord({ noun: directive.noun, id: recordId, fallback, context: "row" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate font-medium text-foreground", children: fallback });
850
888
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
851
889
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [