@ai-matrx/records-ui 0.66.0 → 0.68.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,70 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
+ ## 0.68.0
4
+
5
+ **Two things a screen was still doing behind a person's back.** (lane TAILS-6, found while
6
+ proving 0.66.0 on the live seat.)
7
+
8
+ * **The engineer's line was being READ ALOUD.** `sr-only` does not hide a line — it hides it
9
+ from people with EYES and reads it to everybody else. So the one person who could not skim
10
+ past *"@ai-matrx/records never reached the store from checklistTemplates: TypeError: Failed
11
+ to fetch"* was the blind owner of the business, who had it announced in full underneath
12
+ "We could not reach your data". `RefusalNotice` now marks that line `aria-hidden`; it stays
13
+ in the DOM with the SQLSTATE on it, and on the notice's `title`, for whoever debugs it.
14
+ The guard asserts on what a screen reader would SAY, not on what the DOM contains.
15
+ * **The Group by picker contradicted the board beside it.** `offerableFields` answers what is
16
+ a SENSIBLE column to group by — Choice columns — and the select listed only those. A view,
17
+ or a `?group=` link from a dashboard number, can name a column outside that set: the board
18
+ grouped by it happily and the select fell back to its blank "Choose a field". Watched on
19
+ Rincon Plumbing Co's Jobs board with six columns headed RPC-1000, RPC-1001 … over a picker
20
+ saying nothing was chosen. The chosen column is now always in the list, named, and marked
21
+ "an unusual column to use here".
22
+
23
+ **Consumer action:** none.
24
+
25
+ ## 0.67.0
26
+
27
+ **The office can copy a waiting client's own link, from the panel where it invited them.**
28
+ (lane TAILS-5.)
29
+
30
+ * **`PortalsPanel` renders `CopyLink` beside a pending principal**, fed from that person's
31
+ `accept_path` (new in `@ai-matrx/records` 0.48.0). It is the SAME `CopyLink` this panel
32
+ already uses for the portal's own address — the one that shows the address to copy by hand
33
+ when a browser refuses the clipboard — not a second copy control.
34
+ * **A person with no link gets a sentence, not a missing button.** Somebody who is waiting and
35
+ has no `accept_path` was asked some other way (the server's magic-link route binds without an
36
+ invitation) or their link expired, and the panel says exactly that with what to do: invite
37
+ them again and a fresh one is made. Drawing nothing would read as a broken control; drawing a
38
+ dead button would be worse.
39
+ * **`PortalCardView` takes an optional `origin`** so the bare-mountable view can build a whole
40
+ URL, falling back to the browser's own origin. It still holds no client and no network, so
41
+ the blocking gate can keep mounting it against a recorded `custom.portal_card` answer.
42
+
43
+ **And the two buttons that read alike no longer do.** The Jobs toolbar's control that OPENS the
44
+ add-a-column panel read exactly `"Add field"`, and the panel's own SAVE button read exactly
45
+ `"Add field"` too — `FieldEditor` said `"Save field"` only when EDITING. The toolbar control is
46
+ a TOGGLE, so pressing it while the panel is open CLOSES the panel and throws away everything
47
+ typed into it: a person who had typed a column name, chosen a type and picked the table it
48
+ points at could lose all of it by pressing the words they were told to press, silently, with no
49
+ request ever sent. Measured on 2026-09-21 by a headless walk that lost the column twice and
50
+ spent a round blaming the store.
51
+
52
+ * The words now live once, in `fieldTypes.ts` beside `FIELD_TYPE_CHOICES` — the module that
53
+ already owns what a person READS about a column — as `ADD_FIELD_OPEN_LABEL` ("Add field"),
54
+ `ADD_FIELD_SAVE_LABEL` ("Create field") and `EDIT_FIELD_SAVE_LABEL` ("Save field"). Both
55
+ surfaces import them; neither writes the literal any more.
56
+ * **They are NOT in `@ai-matrx/design-system`, deliberately:** "field" and "column" are records
57
+ vocabulary and the design system knows nothing about either, so a labels module there would
58
+ be a domain word in a generic package that the next person adding a button would not find.
59
+ * `the-opener-and-the-committer-never-read-alike.test.tsx` is a forcing function, not a style
60
+ note: it asserts the strings differ, that the committer never says "add", and that neither
61
+ file carries the literal. Shown RED (2 of 3 failing, `expected 'Add field' not to be 'Add
62
+ field'`) with the collision planted, then GREEN.
63
+
64
+ **Consumer action:** none for a host that mounts `PortalsPanel`. A host that mounts
65
+ `PortalCardView` directly and serves the app from a known origin should pass `origin`; without
66
+ it the view reads `window.location.origin`, which is right in a browser and empty in SSR.
67
+
3
68
  ## 0.66.0
4
69
 
5
70
  **A screen stops talking to a developer.** (lane TAILS-6.)
package/dist/index.cjs CHANGED
@@ -868,6 +868,9 @@ function isRelationFieldType(id) {
868
868
  function parityTypesWithNoExplanation() {
869
869
  return import_records2.PARITY_FIELD_TYPES.map((row) => row.parity_type).filter((type) => !BY_ID.has(type));
870
870
  }
871
+ var ADD_FIELD_OPEN_LABEL = "Add field";
872
+ var ADD_FIELD_SAVE_LABEL = "Create field";
873
+ var EDIT_FIELD_SAVE_LABEL = "Save field";
871
874
 
872
875
  // src/parity.ts
873
876
  var kernelId = (name) => import_records3.KERNEL_TABLES.find((t) => t.name === name)?.id ?? "";
@@ -1489,7 +1492,16 @@ function RefusalNotice({
1489
1492
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_design_system5.AlertDescription, { className: "space-y-1 text-xs", children: [
1490
1493
  plain.sentence.replace(/\.$/, "") === plain.title ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: plain.sentence }),
1491
1494
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "opacity-80", children: plain.remedy }),
1492
- plain.forEngineers ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "sr-only", ...error.sqlstate ? { "data-sqlstate": error.sqlstate } : {}, children: plain.forEngineers }) : null,
1495
+ plain.forEngineers ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1496
+ "p",
1497
+ {
1498
+ className: "sr-only",
1499
+ "aria-hidden": "true",
1500
+ "data-for-engineers": "",
1501
+ ...error.sqlstate ? { "data-sqlstate": error.sqlstate } : {},
1502
+ children: plain.forEngineers
1503
+ }
1504
+ ) : null,
1493
1505
  actions
1494
1506
  ] })
1495
1507
  ]
@@ -5021,7 +5033,7 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5021
5033
  size: "sm",
5022
5034
  disabled: mutation.saving || missing !== null,
5023
5035
  onClick: () => void save(),
5024
- children: mutation.saving ? "Saving\u2026" : field ? "Save field" : "Add field"
5036
+ children: mutation.saving ? "Saving\u2026" : field ? EDIT_FIELD_SAVE_LABEL : ADD_FIELD_SAVE_LABEL
5025
5037
  }
5026
5038
  ),
5027
5039
  onCancel ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_design_system14.Button, { size: "sm", variant: "ghost", onClick: onCancel, children: "Cancel" }) : null,
@@ -7426,7 +7438,9 @@ function FieldPicker({
7426
7438
  }) {
7427
7439
  const want = LAYOUT_FIELD_KIND[layout];
7428
7440
  const offerable = want ? offerableFields(all, want) : [];
7429
- if (offerable.length === 0) {
7441
+ const chosenField = chosen ? all.find((field) => field.key === chosen) : void 0;
7442
+ const unusual = chosenField && !offerable.some((field) => field.key === chosenField.key) ? chosenField : void 0;
7443
+ if (offerable.length === 0 && !unusual) {
7430
7444
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { className: "text-xs text-muted-foreground", children: LAYOUT_NO_FIELD[layout] });
7431
7445
  }
7432
7446
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex items-center gap-1.5 text-xs", children: [
@@ -7441,7 +7455,11 @@ function FieldPicker({
7441
7455
  onChange: (event) => onPick(event.target.value === "" ? null : event.target.value),
7442
7456
  children: [
7443
7457
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("option", { value: "", children: "Choose a field" }),
7444
- offerable.map((field) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("option", { value: field.key, children: fieldName(field) }, field.key))
7458
+ offerable.map((field) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("option", { value: field.key, children: fieldName(field) }, field.key)),
7459
+ unusual ? /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("option", { value: unusual.key, children: [
7460
+ fieldName(unusual),
7461
+ " \u2014 an unusual column to use here"
7462
+ ] }, unusual.key) : null
7445
7463
  ]
7446
7464
  }
7447
7465
  ),
@@ -9973,6 +9991,7 @@ function PortalsPanel({ tableId, className }) {
9973
9991
  {
9974
9992
  portalId: portal.portal_id,
9975
9993
  ...tableId ? { tableId } : {},
9994
+ origin,
9976
9995
  onChanged: () => void load()
9977
9996
  }
9978
9997
  ) : null
@@ -9980,7 +9999,7 @@ function PortalsPanel({ tableId, className }) {
9980
9999
  }) })
9981
10000
  ] });
9982
10001
  }
9983
- function CopyLink({ url }) {
10002
+ function CopyLink({ url, what }) {
9984
10003
  const [copied, setCopied] = (0, import_react68.useState)(false);
9985
10004
  const [shown, setShown] = (0, import_react68.useState)(false);
9986
10005
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
@@ -10001,7 +10020,7 @@ function CopyLink({ url }) {
10001
10020
  }
10002
10021
  })();
10003
10022
  },
10004
- children: copied ? "Copied" : "Copy link"
10023
+ children: copied ? "Copied" : what ? `Copy ${what}` : "Copy link"
10005
10024
  }
10006
10025
  ),
10007
10026
  shown ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("p", { className: "mt-1.5 w-full break-all rounded border border-dashed border-border px-2 py-1 text-xs", children: [
@@ -10013,6 +10032,7 @@ function CopyLink({ url }) {
10013
10032
  function PortalDetail({
10014
10033
  portalId,
10015
10034
  tableId,
10035
+ origin,
10016
10036
  onChanged
10017
10037
  }) {
10018
10038
  const client = (0, import_react69.useRecordsClient)();
@@ -10062,6 +10082,7 @@ function PortalDetail({
10062
10082
  PortalCardView,
10063
10083
  {
10064
10084
  card,
10085
+ origin,
10065
10086
  ...tableId ? { tableId } : {},
10066
10087
  onRevoke: revoke,
10067
10088
  onPreview: preview,
@@ -10084,6 +10105,7 @@ function PortalCardView({
10084
10105
  onRevoke,
10085
10106
  onPreview,
10086
10107
  invite,
10108
+ origin,
10087
10109
  className
10088
10110
  }) {
10089
10111
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: (0, import_design_system33.cn)("mt-2.5 flex flex-col gap-2.5 border-t border-border pt-2.5", className), children: [
@@ -10114,7 +10136,7 @@ function PortalCardView({
10114
10136
  ] }, exposed.table_id))
10115
10137
  ] }),
10116
10138
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_design_system33.Separator, {}),
10117
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(People, { card, onRevoke, onPreview }),
10139
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(People, { card, onRevoke, onPreview, origin }),
10118
10140
  invite ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
10119
10141
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_design_system33.Separator, {}),
10120
10142
  invite
@@ -10124,8 +10146,10 @@ function PortalCardView({
10124
10146
  function People({
10125
10147
  card,
10126
10148
  onRevoke,
10127
- onPreview
10149
+ onPreview,
10150
+ origin
10128
10151
  }) {
10152
+ const here = origin ?? (typeof window === "undefined" ? "" : window.location.origin);
10129
10153
  const [askingToRevoke, setAskingToRevoke] = (0, import_react68.useState)(null);
10130
10154
  const [busy, setBusy] = (0, import_react68.useState)(false);
10131
10155
  const [said, setSaid] = (0, import_react68.useState)(null);
@@ -10167,6 +10191,7 @@ function People({
10167
10191
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "mt-0.5 break-all text-muted-foreground", children: person.email }),
10168
10192
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "mt-0.5 text-muted-foreground", children: state.why }),
10169
10193
  person.is_active ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "mt-1.5 flex flex-wrap gap-1.5", children: [
10194
+ person.accept_path ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(CopyLink, { url: `${here}${person.accept_path}`, what: "their link" }) : null,
10170
10195
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
10171
10196
  import_design_system33.Button,
10172
10197
  {
@@ -10178,6 +10203,11 @@ function People({
10178
10203
  ),
10179
10204
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_design_system33.Button, { size: "sm", variant: "ghost", onClick: () => setAskingToRevoke(person), children: "Remove their access" })
10180
10205
  ] }) : null,
10206
+ person.is_active && !person.signed_in && !person.accept_path ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("p", { className: "mt-1.5 text-muted-foreground", children: [
10207
+ "There is no link to copy for ",
10208
+ person.client || person.email,
10209
+ ": they were asked some other way, or the link they were sent has expired. Invite them again and a fresh one is made."
10210
+ ] }) : null,
10181
10211
  asking ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "mt-1.5 flex flex-col gap-1.5 rounded border border-destructive/40 px-2 py-1.5", children: [
10182
10212
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "text-destructive", children: revokeConsequence(person, card.title) }),
10183
10213
  /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-wrap gap-1.5", children: [
@@ -14999,7 +15029,7 @@ function TablePage({
14999
15029
  variant: surfaceChosen(surface, { rail: "field" }) ? "secondary" : "outline",
15000
15030
  "aria-pressed": surfaceChosen(surface, { rail: "field" }),
15001
15031
  onClick: () => press({ rail: "field" }),
15002
- children: "Add field"
15032
+ children: ADD_FIELD_OPEN_LABEL
15003
15033
  }
15004
15034
  ),
15005
15035
  /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(