@ai-matrx/records-ui 0.16.1 → 0.30.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,379 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
+ ## 0.30.0
4
+
5
+ **The notification editor stops asking for the notifier's own reader.**
6
+
7
+ `NotifyRuleEditor` listed subscriptions with `client.aggSubscriptions()` and read
8
+ its cadences from `client.aggSubscriptionCadences()`. Both are `custom.agg_*` —
9
+ the NOTIFIER's internals — and neither holds a client grant, so a signed-in
10
+ person opened this panel on *"permission denied for function agg_subscriptions"*
11
+ and the cadence picker silently fell back to a word typed into the file. Lane
12
+ TEST-BENCH photographed that refusal on the try-everything page on 2026-09-20.
13
+
14
+ It now asks the person's own doors: `client.subscriptions({ table_id })`, which
15
+ answers what is addressed to them plus — only where they hold admin on this
16
+ Table — anyone's over it, narrowed to Tables they can already open; and
17
+ `client.subscriptionCadences()`, which delegates to the notifier's one array.
18
+
19
+ Two consequences, both visible: the list is now scoped to the Table the editor is
20
+ about (it used to answer the whole organization), and a **muted** subscription
21
+ appears with the sentence *"Switched off — it tells nobody until somebody
22
+ switches it back on."* The notifier's reader excluded muted rows, so switching one
23
+ off used to make it vanish from this list and read as gone rather than as off.
24
+
25
+ **Consumer action:** requires `@ai-matrx/records` 0.17.0 or above.
26
+
27
+ ## 0.29.0
28
+
29
+ **A saved view can now say how the table is SECTIONED, how wide its columns are,
30
+ and how tall its rows are — and the grid windows itself and stops implying it
31
+ counted everything.**
32
+
33
+ The shared data table gained grouping, controlled column widths, virtualization
34
+ and an honest count in design-system 0.23.0. This is the store's half, and it
35
+ rides the document a view already has (`records_ui_view.presentation`): no new
36
+ column, no new table, and an agent sets any of it with the same record write it
37
+ already uses for colors and formats.
38
+
39
+ - **Sections by a Field.** `presentation.grouping` names a Field key, with
40
+ optional per-Field subtotals, a section order and the sections to keep shut.
41
+ Grouped by Status, the table draws one collapsible header per status with a
42
+ count of records on it — and a view that names a Field but no subtotals gets a
43
+ COUNT, never a total nobody asked for.
44
+ - **Remembered column widths.** `presentation.widths` is sparse and keyed by
45
+ Field key, so a view saved before widths existed still reads. Drag a column
46
+ wider and the width is kept.
47
+ - **A row height.** `presentation.rowHeight`, clamped to 24–96px. A view asking
48
+ for anything outside that is dropped rather than obeyed, because a saved look
49
+ must not be able to make a table unreadable.
50
+ - **The grid windows itself.** Virtualization is on for every record table; the
51
+ primitive's own threshold decides when it engages, so short tables are
52
+ untouched and a very long one scrolls.
53
+ - **The count says what it covers.** The search and filters in the grid's
54
+ toolbar run in the browser over the page the read door returned, so the table
55
+ now states that in plain English instead of letting the loaded count pose as
56
+ the whole table — and says the total is unknown when the door cannot give one.
57
+ - **The person's own changes are saved back onto the view.** Picking a column to
58
+ group by, shutting a section and resizing a column travel `Grid`'s new
59
+ `onPresentationChange` into the door every other view change already uses, so
60
+ `TablePage` writes them onto the view record with the version it read. A host
61
+ that binds no door still gets all three for the visit.
62
+
63
+ ### Consumer action
64
+
65
+ - Nothing to change. Every new field is optional and a view that carries none of
66
+ them renders exactly as it did.
67
+ - A host that mounts `Grid` directly and wants these remembered binds
68
+ `onPresentationChange` and saves the look onto its view record;
69
+ `ViewSwitcher` and `TablePage` already do.
70
+ - Needs `@ai-matrx/design-system` 0.23.0 or above.
71
+
72
+ ## 0.28.0
73
+
74
+ **The publish canary installs every peer this package declares.** It named
75
+ `react` and `react-dom` by hand, so the day a THIRD peer arrived (`recharts`,
76
+ with `ChartBlock`) the published bundle imported something no consumer had
77
+ been given and `import * as ui from "@ai-matrx/records-ui"` threw
78
+ `ERR_MODULE_NOT_FOUND` — the canary was right and the whole publish stopped
79
+ (run 35513820597). The peer list is read from the manifest, and the peers are
80
+ installed in a pass of their own so each brings its own peers (`react-is` is
81
+ `recharts`'). 0.27.0 was tagged and never published; its tag was left where it
82
+ is.
83
+
84
+ ## 0.27.0
85
+
86
+ **A first-time admin could not make a table, and the gate could not see it.**
87
+
88
+ Creating one showed a red refusal and made a table anyway; that table opened with
89
+ no grid; the saved-views table behind every table page was itself unusable. One
90
+ cause, in three places: a Field is a record (REC-25), so this package wrote one
91
+ through the ordinary write door — and `custom._field_class_guard` refuses that
92
+ row outright (23514 / FLD-13, "a column of a table is stored as a field, and this
93
+ one says record"). Measured on the published screens (0.16.0) on 2026-09-20: four
94
+ tables in one organization, every one declaring a column NAME with no Field row
95
+ behind it.
96
+
97
+ - **`systemTable.ts` goes through the declare doors.** `declare` and
98
+ `addMissingFields` use `custom.field_declare`; the shape reconciliation uses
99
+ `custom.field_update`, which converts the values a behaviour change orphans and
100
+ writes the migration row — `record_update` wrote the two keys onto the document
101
+ and did none of it. `addMissingFields` no longer patches the Table's own
102
+ `fields` list: that is the door's job, and a client doing it separately is how a
103
+ Table came to declare a column nothing backed.
104
+ - **`records_ui_view` is why the grid was empty.** Its nine columns were refused,
105
+ so `ensureSystemTable` failed on every load, `ViewBar` could never read or seed
106
+ a view, and every table page in the organization read "Waiting for <name>'s
107
+ views" over an empty frame.
108
+ - **`ensureSystemTable`'s in-flight claim was never released on success**, which
109
+ turned a race guard into a permanent cache: after the first declaration, every
110
+ later call returned before the two reconciliation paths ran, so a package table
111
+ could not grow a column in the session that made it.
112
+ - **Creating a table is ONE act from the person's side.** A table that could not
113
+ take all of its columns is taken back, newest first, and a take-back that itself
114
+ fails SAYS SO and says what to do. Never a red sentence beside a table.
115
+ - **`declareTable` hands back the Home it made** (REC-14), so the caller that
116
+ made it can tidy it up.
117
+ - **`fieldDeclarationFor` carries everything the person said** — a list's choices,
118
+ a money column's unit, a date's day-or-day-and-time, a relation's target. Without
119
+ `options` the door refuses a choice list outright.
120
+ - **`TableSettings`' reorder goes through `custom.field_update`**, which is the
121
+ door for a field's own settings and the one that asks `admin` on the TABLE.
122
+
123
+ **A table with no columns is a real state, and it drew nothing at all.** The grid
124
+ now says so — "This table has no columns yet" — with the one control that ends it,
125
+ present for somebody who may change the table's shape and ABSENT with the ladder's
126
+ own reason for everybody else.
127
+
128
+ **The records are never held back by the bookkeeping.** `TablePage` always has a
129
+ view: the saved one when `ViewBar` has it, the unsaved default until then, with a
130
+ line saying the layout is not being kept yet. One table's plumbing can no longer
131
+ decide whether another table's data is visible.
132
+
133
+ **The gate now walks the first thing anybody does.** `demo/first-time-journey.ts`
134
+ is one walk driven twice — recorded real-door answers in `demo/deterministic.test.tsx`,
135
+ the real doors in `demo/live-smoke.test.tsx`: declare a table with no columns,
136
+ declare one with three columns of three different kinds, write a record, read it
137
+ back, see it in the page read, grow a package-owned table, and delete everything.
138
+ Red against the old code on both halves.
139
+ ## 0.26.0 — 2026-09-20
140
+
141
+ **The owner's side of the client portal: who signs in from outside, and exactly
142
+ what they see.**
143
+
144
+ The portal doors could be called from 0.15.0 of `@ai-matrx/records`, and there
145
+ was no screen. `PortalsPanel` is that screen, and it is a RAIL ON THE TABLE'S
146
+ OWN PAGE beside its forms and its notifications — never a Portals area of the
147
+ app, for the same reason a form is not one: a portal is a way IN to Tables, so
148
+ it is reached from a Table.
149
+
150
+ - **The portals over this organization** — title, public address, how many
151
+ Tables, and the two numbers kept apart: signed in, and invited and waiting.
152
+ Adding them together is how an owner comes to believe a client is looking at
153
+ something they cannot reach.
154
+ - **One portal opened** — each exposed Table with the relation Field that makes
155
+ a record theirs (`names_via`), the fields they see, the fields they may
156
+ change, whether they may comment, and the most they are ever conveyed. Each
157
+ person with which of the three states they are in, in words.
158
+ - **Invite** — a picker over the portal's own client Table (a portal principal
159
+ IS one of those rows, so a client cannot be typed) and an email. It passes no
160
+ user id, and it shows the door's own sentence afterwards: they get access the
161
+ moment they follow the link, and until then the row holds nothing.
162
+ - **Remove their access** — the consequence is named BEFORE the click, by name:
163
+ this person can no longer sign in to this portal, and the records that named
164
+ their client stop reaching them. A generic "Are you sure?" fails this repo's
165
+ law. Afterwards the door's own past-tense sentence is shown.
166
+ - **View as this client** — `custom.portal_preview`'s answer, with a line saying
167
+ plainly that it is the same question their own screen asks. For somebody who
168
+ has not signed in, the door's refusal is shown rather than an empty list: an
169
+ empty list reads as "they see nothing" when the truth is they hold nothing yet.
170
+ - **Copy the portal's link** — through `portalPath`, and it shows the address to
171
+ copy by hand when a browser refuses the clipboard.
172
+
173
+ `PortalCardView` is exported as well: it takes a `PortalCard` and two callbacks
174
+ and holds no client at all, which is how `demo/portals-panel.test.tsx` mounts it
175
+ BARE in the blocking gate against a recorded real `custom.portal_card` answer
176
+ (`demo/fixtures/portal-card.json`, re-recordable with
177
+ `demo/record-portal-card.mjs`) with the real `custom.portal_preview` answers,
178
+ including that door's own refusal.
179
+
180
+ **Consumer action:** none required. A host that mounts `TablePage` gets the new
181
+ "Portals" rail with no change; a host that assembles its own page can mount
182
+ `PortalsPanel` anywhere it mounts `FormsPanel`. `RecordsUiHost.publicOrigin` is
183
+ what makes the copied address one a client can actually open.
184
+
185
+ ## 0.21.0
186
+
187
+ **ONE dashboard object, one call, and every number clicks through to the records
188
+ it counts.**
189
+
190
+ `DashboardCanvas` kept its dashboards as rows of a package-owned
191
+ `records_ui_dashboard` Table — a second store the agent could not reach and the
192
+ database never validated, sitting beside the presentation-kernel dashboard that
193
+ `dashboard_propose` writes and `custom.dashboard_run` runs. It now reads
194
+ `custom.dashboards`, runs `custom.dashboard_run` and saves with
195
+ `custom.dashboard_declare`: the same three doors the agent uses. `DASHBOARD_TABLE`,
196
+ `dashboardDocument`, `dashboardFromRecord` and the seeding are gone, replaced by
197
+ one mapper each way — `dashboardFromSummary` / `dashboardDeclareArgs`, and
198
+ `specFromBlock` / `blockFromSpec` for a single block. `ChartKind` IS the store's
199
+ `DashboardBlockKind`; there is no second closed set of shapes.
200
+
201
+ **ONE call for the whole canvas.** Every block used to make its own
202
+ `custom.record_aggregate` request. The door answers all of them in one snapshot,
203
+ under the caller's own principal, each with its own `ms` — so eight blocks cannot
204
+ disagree about what the last hour did, and eight round trips are one. Each block
205
+ shows the milliseconds it took. A block the store REFUSED prints the store's own
206
+ sentence, classified by its SQLSTATE through the same formatter as every other
207
+ refusal, and its siblings still draw.
208
+
209
+ **The platform's chart library.** Bars, columns, lines and donuts are Recharts
210
+ now, not hand-cut `<rect>` and `<circle>`, with the app's own conventions:
211
+ `--color-<key>` variables from a `ChartConfig`-shaped map, semantic tokens only
212
+ (so light and dark both come out right with no second palette), and the app's
213
+ tooltip and legend markup. There is no `ResponsiveContainer` — the box is
214
+ measured with a `ResizeObserver` and falls back to a sane width when there is no
215
+ measurement, so a chart always has real dimensions and a component test proves
216
+ what a person sees. `number` and `table` stay a big number and an HTML table.
217
+
218
+ **Every number clicks through.** A bar, a slice, a table row and the big number
219
+ all open the records they counted, through the new `openRecords` host port, with
220
+ the block's own filter and the clicked group merged. Every group is also a real
221
+ button, so the chart works from a keyboard.
222
+
223
+ **A re-ask box.** One input at the top of the canvas — "break it down by owner" —
224
+ through the new `onReask` host port, which the host drives with
225
+ `dashboard_propose` on the server's `records` tool.
226
+
227
+ **A Dashboards tab** on `TablePage`, beside the views of the same records.
228
+
229
+ ### Consumer action
230
+
231
+ - `recharts` is a PEER dependency now (`>=3.0.0`). A host already on Recharts 3
232
+ provides the one copy; a host that is not must add it.
233
+ - `DashboardCanvas` no longer takes `seed`. Dashboards come from
234
+ `custom.dashboards`; an agent writes one with `dashboard_propose`.
235
+ - `ChartBlock` takes `{ block, subject }` — one `DashboardBlockResult` out of the
236
+ canvas's run — instead of `{ spec }`. It opens no door of its own.
237
+ - `DASHBOARD_TABLE`, `dashboardDocument`, `dashboardFromRecord` and
238
+ `DashboardSpec` are removed.
239
+ - Bind `openRecords` for click-through and `onReask` for the re-ask box; unbound,
240
+ each says in one sentence what to bind rather than drawing a dead control.
241
+ - Needs `@ai-matrx/records` 0.14.0 or above.
242
+ ## 0.20.0 — 2026-09-20
243
+
244
+ **The store's grid inherits the spreadsheet, instead of re-implementing it.**
245
+
246
+ The app's `/data` route had grown colors, rules, formats and a wide-table
247
+ layout, and none of it could reach the record store: that work lives in the app,
248
+ and this package's Grid renders through `@ai-matrx/design-system`'s data table.
249
+ The capabilities moved into that table (design-system 0.22.0); this release is
250
+ the store's half.
251
+
252
+ - **`presentation.ts`** — how a table LOOKS, as one document: `style` (color by a
253
+ column, live rules, manual highlights), `formats` (per-column overrides) and
254
+ `frozen` (the columns that stay while the rest scrolls). It lives on the SAVED
255
+ VIEW, because two people read one table two ways and neither reading is a fact
256
+ about the records — and it lives on a RECORD, not a new column, which is why
257
+ this needed no migration and why an agent sets it through the same
258
+ `record_write` it uses for everything else.
259
+ - **A Field's money is right with nothing configured.** `formatForField` reads
260
+ the Field's OWN declaration — a currency parity type and its unit, a percent, a
261
+ datetime's `config.kind` — so `$1,250.50` and `45%` appear on a table nobody
262
+ has ever styled. A view's `formats` only OVERRIDES that. A relation, a person
263
+ or a choice keeps its own control rather than being handed to a text formatter.
264
+ - **`RecordValue` and `GridCell` take a `format`.** A cell renderer beats
265
+ `MatrxColumnDef.format` — the table hands the cell over and steps back — so a
266
+ column that declared `currency` was showing a raw `1250.5` under its own
267
+ right-aligned header. Both now ask the primitive's formatter for the text, so
268
+ the read view and the table can never disagree about a number.
269
+ - **Color-by never does nothing.** The store keeps a list column's choices as
270
+ records of another Table and this grid has not read their colors, so a value's
271
+ color is derived from the value itself — same word, same color, always — rather
272
+ than leaving a setting a person just changed with nothing to show for it.
273
+ - **`Grid` takes `presentation`**, `ViewSwitcher` passes the open view's, and
274
+ `SavedViewSpec` / the `records_ui_view` Table carry it.
275
+
276
+ ### Fixed — a column could not be declared at all
277
+
278
+ `addFields` wrote each Field as a plain record into the Field kernel Table, and
279
+ the store now refuses that out loud (FLD-13, `23514`): *"a column of a table is
280
+ stored as a field, and this one says record"*. Every table made through
281
+ `declareTable` — the create path AND import — failed at its first column. It
282
+ goes through `custom.field_declare` now, which is also what derives a parity
283
+ type, makes a choice Table for a list column, and stamps the auto Rules a url or
284
+ a percent carries; every column made the old way was thinner than one made
285
+ through the field panel.
286
+
287
+ ### Consumer action
288
+
289
+ None. `presentation` is optional and its absence is today's behavior.
290
+
291
+
292
+ ## 0.19.1
293
+
294
+ **The progress bar and the count beside it said different things.** One question
295
+ at a time, "1 of 4" is a POSITION, and the bar was measuring ANSWERS — so on the
296
+ first question the count said one and the bar sat at zero: an empty sliver in a
297
+ tinted track, which reads as either full or broken. Found by rendering the public
298
+ form, not by reading it. Typeform's bar advances as you move through the form,
299
+ and now so does this one. On a single page there is no position, so it keeps
300
+ measuring what it can: how much is answered.
301
+
302
+
303
+ ## 0.19.0
304
+
305
+ **The choice picker reads a document from any era, and writes the contract.**
306
+ `OptionControl` compared the cell to `option.id` and wrote `option.id`. A choice
307
+ value is now the option's own stable key and the read doors hand back the LABEL,
308
+ so a picker that compares against the id shows nothing selected. It now uses
309
+ `isTheChosen` from `@ai-matrx/records` — which recognises a choice by its key,
310
+ its label or its option id — and writes `optionKey(option)`, for both the single
311
+ and the multi control.
312
+
313
+ `scalarText` is unchanged and did not need to change: it resolves a value
314
+ through the label provider only when the value LOOKS like an id, so a label
315
+ falls through and renders as itself.
316
+
317
+ **Consumer action:** none beyond taking 0.19.0 with `@ai-matrx/records` 0.13.0.
318
+
319
+
320
+ ## 0.18.0
321
+
322
+ **A person can switch off a notification an agent switched on.** "Make me an
323
+ intake form that tells me when a new patient arrives" creates a subscription
324
+ Rule, and until now nothing in any screen could see it, let alone stop it: the
325
+ only reader was `custom.agg_subscriptions`, which is the notifier's and is not
326
+ client-callable. `SubscriptionsPanel` is the owner's side — what this table is
327
+ telling you about, on which channel, how often, and one switch — and it opens in
328
+ the table's own rail beside its Forms, because a subscription watches a Table.
329
+
330
+ A row the store says you may not mute carries no switch at all rather than a
331
+ greyed one, and says whose it is. "Off" is the store's own word: muting is
332
+ honoured inside the single reader every consumer goes through, so a screen can
333
+ never say a subscription is off while it keeps firing.
334
+
335
+ ### Consumer action
336
+
337
+ None — `TablePage` grew the rail. Needs `@ai-matrx/records` 0.12.0 or above for
338
+ `client.subscriptions` and `client.subscriptionMute`.
339
+
340
+
341
+ ## 0.17.0
342
+
343
+ **There is one Form object, and it is the one with the link.** `FormBuilder` kept
344
+ its own form records in a package-owned `records_ui_form` Table — a second form
345
+ with no public link, no accept Rule and no notify Rule, sitting beside the
346
+ `custom.anon_form` one that `form_propose` writes and a stranger actually
347
+ answers. Two objects, one name, nothing reconciling them: a person tweaked one
348
+ form and published the other. The builder now reads `custom.forms` and writes
349
+ `custom.form_declare` — the same two doors the agent uses — so a tweak changes
350
+ the form at the link, and the screen says which link that is and whether it is
351
+ answering.
352
+
353
+ A save is a RE-STATEMENT of the whole form, because that is what the door is, so
354
+ `formDeclareArgs` hands the accept Rule, the notify Rule, the cap and the slug
355
+ straight back. A save that dropped `notify_rule_id` would silently stop the owner
356
+ being told that somebody answered — which is most of what the person asked for.
357
+
358
+ **The public form printed its own title twice**, once as the page's heading and
359
+ again three millimetres below it in the runner's progress row — measured in a
360
+ headless render, not reasoned about. The runner runs the form; the page names
361
+ it. A host that mounts a form has a heading of its own, and a component that
362
+ repeats it is a component deciding something it cannot see. `FormBuilder`'s
363
+ preview pane grew the one line it needed.
364
+
365
+ "New form" is back, and it starts by asking for the Table's first Field: the
366
+ store refuses a form that asks nothing, and it is right to — a form with no
367
+ questions is a link to an empty page.
368
+
369
+ ### Consumer action — BREAKING, and it is three names
370
+
371
+ `FORM_TABLE`, `formDocument` and `formFromRecord` are gone, with the second store
372
+ they served. `SavedForm.version` is gone too (`custom.anon_form` has no record
373
+ version); `SavedForm` now carries `state`, `slug`, `submissionCap`,
374
+ `quarantineRuleId` and `notifyRuleId`. If you mapped forms yourself, the two
375
+ directions are `formFromSummary(summary)` and `formDeclareArgs(form)`.
376
+
3
377
  ## 0.16.1
4
378
 
5
379
  **`FormSubmitOutcome` is exported.** It is the return type of