@ai-matrx/content-ir-react 0.24.5 → 0.25.1

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.25.1
4
+ ### A host can word a failure's Details (G15, 2026-10-07)
5
+
6
+ - `DirectiveFailureWords.details` (optional): when the host's `explainFailure` returns it, the card shows it behind "Details" instead of the raw server text — so a field reads as the form names it ("Title is required", never "name is required"). Absent → the raw text, as before.
7
+
8
+ Consumer action: none required; matrx-frontend `explainDirectiveFailure` returns `details`.
9
+
10
+ ## 0.25.0
11
+ ### An update that changes nothing says so (G11B, 2026-10-07)
12
+
13
+ - `changesNothing(changes)` (new export, `directives/item-changes`): true when an update sets no field, or every field already holds its value. Unknown current values count as a change.
14
+ - `SideEffectDirectiveCard`: once a row reads its record, an update whose every field already holds its value shows "Nothing to change" and a muted Apply — it used to be full-weight while the confirm listed "(no change)".
15
+ - A row's name wraps to two lines on a phone instead of being cut to one.
16
+
17
+ Consumer action: a host confirm for an update can call `changesNothing(itemChanges(item, values))` to say "Nothing to change" (matrx-frontend `DirectiveConsequence` does).
18
+
3
19
  ## 0.24.4
4
20
 
5
21
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -228,6 +228,12 @@ interface DirectiveFailure {
228
228
  interface DirectiveFailureWords {
229
229
  what: string;
230
230
  next: string;
231
+ /**
232
+ * The text behind "Details", when the host words it (e.g. the server's field
233
+ * names read as the form labels them — "Title is required", never
234
+ * "name is required"). Absent → the raw text.
235
+ */
236
+ details?: string;
231
237
  }
232
238
  /** Where a directive renders — today, the conversation it sits in (if any). */
233
239
  interface DirectiveScope {
@@ -228,6 +228,12 @@ interface DirectiveFailure {
228
228
  interface DirectiveFailureWords {
229
229
  what: string;
230
230
  next: string;
231
+ /**
232
+ * The text behind "Details", when the host words it (e.g. the server's field
233
+ * names read as the form labels them — "Title is required", never
234
+ * "name is required"). Absent → the raw text.
235
+ */
236
+ details?: string;
231
237
  }
232
238
  /** Where a directive renders — today, the conversation it sits in (if any). */
233
239
  interface DirectiveScope {
@@ -33,6 +33,7 @@ __export(directives_exports, {
33
33
  NO_TITLE_WORD: () => NO_TITLE_WORD,
34
34
  SideEffectDirectiveCard: () => SideEffectDirectiveCard,
35
35
  TARGETING_CLASSES: () => TARGETING_CLASSES,
36
+ changesNothing: () => changesNothing,
36
37
  defaultFailureWords: () => defaultFailureWords,
37
38
  describeChangeValue: () => describeChangeValue,
38
39
  directiveFieldLabel: () => directiveFieldLabel,
@@ -190,6 +191,9 @@ function itemChanges(item, current, options = {}) {
190
191
  }
191
192
  return out;
192
193
  }
194
+ function changesNothing(changes) {
195
+ return changes.every((change) => change.same === true);
196
+ }
193
197
 
194
198
  // directives/record-values.ts
195
199
  var import_react2 = require("react");
@@ -698,7 +702,7 @@ function ApplyDirectiveButton({
698
702
  onFocus: warm,
699
703
  disabled: view.status === "applying" || view.status === "asking" || items.length === 0,
700
704
  "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
701
- title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
705
+ title: muted ? "Applying it changes nothing." : void 0,
702
706
  className: `inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded-md border px-2 py-1 text-xs font-medium disabled:opacity-50 ${muted ? "border-border/60 bg-background text-muted-foreground hover:bg-accent" : destructive ? "border-destructive/50 bg-background text-destructive hover:bg-destructive/10" : "border-border bg-background hover:bg-accent"}`,
703
707
  children: [
704
708
  view.status === "applying" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PlayIcon, { className: "h-3.5 w-3.5" }),
@@ -738,7 +742,7 @@ function FailureNotice({
738
742
  raw
739
743
  }) {
740
744
  const [open, setOpen] = (0, import_react3.useState)(false);
741
- const detail = raw?.trim() ?? "";
745
+ const detail = raw?.trim() ? words.details?.trim() || raw.trim() : "";
742
746
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "flex min-w-0 flex-col gap-0.5", "data-apply-failure": true, children: [
743
747
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "min-w-0", children: [
744
748
  words.what,
@@ -785,12 +789,23 @@ function ItemRow({
785
789
  item,
786
790
  index,
787
791
  total,
788
- phase
792
+ phase,
793
+ onNothing
789
794
  }) {
790
795
  const host = useDirectiveHost();
791
796
  const recordId = TARGETING_CLASSES.has(directive.directiveClass) ? itemRecordId(item) : null;
792
797
  if (recordId) {
793
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TargetRow, { directive, item, recordId, index, phase });
798
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
799
+ TargetRow,
800
+ {
801
+ directive,
802
+ item,
803
+ recordId,
804
+ index,
805
+ phase,
806
+ onNothing
807
+ }
808
+ );
794
809
  }
795
810
  const titleColumn = (0, import_content_ir4.nounTitleColumn)(directive.noun, host.nouns);
796
811
  const ownName = itemOwnName(item, titleColumn);
@@ -809,7 +824,8 @@ function ItemRow({
809
824
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
810
825
  "span",
811
826
  {
812
- className: `min-w-0 shrink-0 truncate font-medium ${ownName ? "text-foreground" : "italic text-muted-foreground"}`,
827
+ className: `min-w-0 line-clamp-2 font-medium [overflow-wrap:anywhere] sm:shrink-0 sm:truncate ${ownName ? "text-foreground" : "italic text-muted-foreground"}`,
828
+ "data-item-name": "",
813
829
  "data-item-untitled": ownName ? void 0 : "",
814
830
  children: title
815
831
  }
@@ -898,7 +914,8 @@ function TargetRow({
898
914
  item,
899
915
  recordId,
900
916
  index,
901
- phase
917
+ phase,
918
+ onNothing
902
919
  }) {
903
920
  const host = useDirectiveHost();
904
921
  const fallback = recordFallbackName((0, import_content_ir4.nounLabel)(directive.noun, host.nouns));
@@ -913,7 +930,11 @@ function TargetRow({
913
930
  titleColumn: (0, import_content_ir4.nounTitleColumn)(directive.noun, host.nouns),
914
931
  valueWord: directiveValueWord(host, directive.noun)
915
932
  }) : [];
916
- 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 });
933
+ const nothing = readsLive && live.values !== null && changesNothing(changes);
934
+ (0, import_react4.useEffect)(() => {
935
+ onNothing?.(index, nothing);
936
+ }, [onNothing, index, nothing]);
937
+ const name = host.renderRecord ? host.renderRecord({ noun: directive.noun, id: recordId, fallback, context: "row" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "line-clamp-2 min-w-0 font-medium text-foreground [overflow-wrap:anywhere]", children: fallback });
917
938
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
918
939
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
919
940
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "flex min-w-0 flex-1 items-center gap-1 px-1", children: name }),
@@ -937,7 +958,9 @@ function SideEffectDirectiveCard({ directive }) {
937
958
  const [phase, setPhase] = (0, import_react4.useState)(
938
959
  () => readsApplyLedger(host, directive) ? { phase: "pending" } : { phase: "open" }
939
960
  );
940
- const nothingToChange = directive.directiveClass === "update" && total > 0 && items.every((item) => itemChanges(item).length === 0);
961
+ const [nothingAt, setNothingAt] = (0, import_react4.useState)({});
962
+ const noteNothing = (index, nothing) => setNothingAt((prev) => prev[index] === nothing ? prev : { ...prev, [index]: nothing });
963
+ const nothingToChange = directive.directiveClass === "update" && total > 0 && items.every((item, index) => itemChanges(item).length === 0 || nothingAt[index] === true);
941
964
  const shown = expanded ? items : items.slice(0, ROWS_BEFORE_FOLD);
942
965
  const hidden = total - shown.length;
943
966
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "my-3 rounded-md border border-border bg-card px-2.5 py-1.5", "data-directive": directive.slug, children: [
@@ -966,7 +989,18 @@ function SideEffectDirectiveCard({ directive }) {
966
989
  total === 0 ? (
967
990
  // Stated, never silent: an empty batch is a real (and suspicious) state.
968
991
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "mt-1 text-xs text-muted-foreground", children: "No items \u2014 nothing would be written." })
969
- ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ItemRow, { directive, item, index: i, total, phase }, i)) }),
992
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
993
+ ItemRow,
994
+ {
995
+ directive,
996
+ item,
997
+ index: i,
998
+ total,
999
+ phase,
1000
+ onNothing: noteNothing
1001
+ },
1002
+ i
1003
+ )) }),
970
1004
  hidden > 0 || expanded ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
971
1005
  "button",
972
1006
  {