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