@ai-matrx/records-ui 0.3.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 ADDED
@@ -0,0 +1,91 @@
1
+ # Changelog — @ai-matrx/records-ui
2
+
3
+ ## 0.3.1
4
+
5
+ Ships the canonical `TablesHome`, `TablePage`, `RecordsMount`, and table-creation helpers
6
+ that were added after 0.3.0 was tagged. The real-store demo now shares the package-wide
7
+ verification lock, and the packed-artifact canary verifies `@ai-matrx/records-ui` itself
8
+ instead of accidentally re-verifying the headless `@ai-matrx/records` package.
9
+
10
+ **Consumer action:** replace temporary workspace or source references with
11
+ `@ai-matrx/records-ui@latest`; no API rename is required.
12
+
13
+ ## 0.3.0
14
+
15
+ **The last eight, and the package is complete at thirty-one components.**
16
+
17
+ `RecordChat` (SCR-26) reuses the platform's ONE chat surface instead of shipping
18
+ a second: it builds the context — the record through the read door, each value
19
+ carrying its Field's unit and format, the masked fields named, the `secret` and
20
+ `on_demand` ones held back with the reason — and hands it to a new `chat` port
21
+ that matrx-frontend fills with `AgentConversationColumn`.
22
+
23
+ `EnrichPanel` (SCR-27) drives per-field AI fill off the Fields' own
24
+ `source = agent` and `review_interval_days`, dates each value from its own
25
+ envelope rather than the row, shows VAL-8 provenance on every value (including
26
+ which person an agent acted for), and sends the fill through the server's
27
+ `records` tool via a new `enrich` port — because a browser stamping itself
28
+ `agent` would be a browser claiming to be one.
29
+
30
+ `ChecklistRunner` (SCR-28) instantiates a declarative template as REAL RECORDS
31
+ in one act. `BookingSlots` (SCR-29) holds the slot before it takes the details
32
+ and lets the database decide a double-booking. `CaptureSheet` (SCR-30) is the
33
+ offline capture contract: the client mints the id before the first attempt and
34
+ `custom.anon_capture` deduplicates every replay onto the same record.
35
+
36
+ `PortalShell` (SCR-23), `PublicViewPage` (SCR-24) and `EmbedFrame` (SCR-25) are
37
+ the outsider's three surfaces, over `iam`'s external principal and publish
38
+ binding and over the embed token handshake.
39
+
40
+ New host ports: `chat`, `enrich`, `captureQueue`. Requires
41
+ `@ai-matrx/records` 0.3.0.
42
+
43
+ **A RACE EVERY SEED-TAKING COMPONENT HAD, closed as a class.** `DocTemplate`,
44
+ `ViewBar`, `FormBuilder`, `DashboardCanvas` and `ActionInbox` all read what
45
+ exists, worked out what was missing, and wrote it — from a loader whose identity
46
+ changes when its inputs settle. Against a slower store the second run read a
47
+ list the first was still writing into, and seeded the same thing twice: two
48
+ identical "Agreement" templates, two buttons with one name. A check against the
49
+ server's list is not a lock. `useSeedGuard` claims each seed name once per
50
+ mount, and all five now use it.
51
+
52
+ **Four more defects this wave found and closed.** `FormRunner` told every person that
53
+ a public form's link was "pending" for a door named `custom.anon_record_write`
54
+ that never existed, while the real anonymous lane had shipped — it now names
55
+ `custom.anon_write` and tells a person the true next step (issue an embed). And
56
+ the demo harness read a rehearsal copy that was missing a third of the doors
57
+ this package now calls; it reads the one live store, and stands its own table,
58
+ fields and records up rather than depending on rows somebody typed once. The
59
+ two vocabulary drifts behind `RecordChat`'s context rules — a Field's
60
+ sensitivity words and its context-policy words — were corrected in
61
+ `@ai-matrx/records` 0.3.0; `RecordChat` holds back `confidential` and
62
+ `restricted` values and FLD-12's `exclude` and `on_request` fields, and names
63
+ every one of them rather than letting an agent answer around a gap.
64
+
65
+ ## 0.2.0
66
+
67
+ `DocTemplate`, `DocRender` and `SignBlock` (SCR-20/21/22): a template is saved
68
+ through `custom.doc_template_save`, a render is frozen once and printed through
69
+ `@ai-matrx/print`, and a signature is checked against the exact bytes that were
70
+ signed (`custom.doc_signature_intact`). A token the Table cannot answer is named
71
+ before anything is rendered.
72
+
73
+ Every component now reads through `@ai-matrx/records` 0.2.0's read door, so a
74
+ row is the store's masked document and a masked field is present-and-null with
75
+ its own reason. `useRecordVersion` reads the edited record's version from
76
+ `custom.io_revisions`, and `useSystemTable` adds Fields a package Table grew to
77
+ organizations that already hold a copy.
78
+
79
+ One store defect was found by wiring this and fixed on the main database:
80
+ `custom.read_record` refused records `custom.read_records` had just listed,
81
+ because the one-record door asked a narrower question than its own list sibling.
82
+ It now asks `iam.has_access_for`, the platform's one access question
83
+ (`matrx-frontend/migrations/campaign/w6_ui_the_read_door_asks_the_one_access_question.sql`).
84
+
85
+ ## 0.1.0
86
+
87
+ First release: `Grid`, `RecordForm`, `FieldEditor`, `TableSettings`,
88
+ `RelationPicker`, `Peek`, `ImportWizard`, `ExportMenu` and
89
+ `CustomFieldsSection`, plus the `RecordsUiProvider` rights port and the shared
90
+ refusal surface. Every one of them is proven headlessly against the rehearsal
91
+ branch by `pnpm test`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI Matrix Engine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # @ai-matrx/records-ui
2
+
3
+ THE canonical screens of the AI Matrx record store. Every component here reads
4
+ and writes ONLY through [`@ai-matrx/records`](../records/README.md), renders
5
+ with `@ai-matrx/design-system`'s own primitives and data table, and carries
6
+ `@ai-matrx/alchemy`'s menu where a person reaches for one.
7
+
8
+ There is no bespoke per-feature record UI in this platform. A feature that needs
9
+ a grid, a form, a picker or a peek mounts one of these — and any one usage is a
10
+ few lines.
11
+
12
+ ## Bind once
13
+
14
+ ```tsx
15
+ import { RecordsProvider } from "@ai-matrx/records/react";
16
+ import { RecordsUiProvider } from "@ai-matrx/records-ui";
17
+ import "@ai-matrx/design-system/styles.css";
18
+
19
+ <RecordsProvider config={{ dataSource: supabase, organizationId, actor: { actor: "user", user_id } }}>
20
+ <RecordsUiProvider value={{ rights: (table) => myRights(table), density: "condensed" }}>
21
+ {children}
22
+ </RecordsUiProvider>
23
+ </RecordsProvider>;
24
+ ```
25
+
26
+ `rights` is the port that decides whether a person sees the administering
27
+ controls. Unbound, it answers "no" WITH A REASON, and every such control is
28
+ ABSENT — this package ships no disabled variant of any of them.
29
+
30
+ ## Then, a few lines each
31
+
32
+ ```tsx
33
+ import {
34
+ Grid, RecordForm, FieldEditor, TableSettings, RelationPicker,
35
+ Peek, ImportWizard, ExportMenu, CustomFieldsSection,
36
+ } from "@ai-matrx/records-ui";
37
+
38
+ <Grid tableId={id} onOpenRecord={setPeeking} onAddField={openSettings} /> // records as rows, fields as columns
39
+ <RecordForm tableId={id} /> // create, generated from the Field records
40
+ <RecordForm tableId={id} recordId={recordId} /> // edit, with the conflict shown when someone wins
41
+ <Peek tableId={id} recordId={recordId} onClose={close} /> // any custom table's record, no per-table code
42
+ <TableSettings tableId={id} /> // rename · reorder · retype · delete
43
+ <FieldEditor tableId={id} field={field} /> // one Field's type, rules and sensitivity
44
+ <ImportWizard tableId={id} onProposeField={propose} /> // map columns, preview rows, propose the rest
45
+ <ExportMenu tableId={id} label="Deals" /> // CSV · XLSX · Alchemy
46
+ <CustomFieldsSection tableId={entityTableId} recordId={record.id} /> // THE one line a standard entity page adds
47
+ ```
48
+
49
+ `RelationPicker` is used by `RecordForm` automatically for any `relation` field;
50
+ mount it directly when a screen needs just the picker.
51
+
52
+ ## Views, the queue, history and comments
53
+
54
+ ```tsx
55
+ <ViewBar tableId={id} onActiveView={setView} seed={[{ name: "By stage", subject: id, layout: "kanban", groupField: "stage" }]} />
56
+ <ViewSwitcher view={view} /> // grid · kanban · calendar · gallery, one membership Rule
57
+ <ActionInbox tableId={id} seed={[...]} /> // approvals, assignments and agent proposals, one queue
58
+ <HistoryPanel tableId={id} recordId={recordId} /> // the timeline IS the record; undo is the store's soft delete
59
+ <CommentThread tableId={id} recordId={recordId} /> // a comment is a record
60
+ <FormBuilder tableId={id} seed={[form]} /> <FormRunner form={form} /> // a form is a view on a real Table
61
+ <DashboardCanvas tableId={id} seed={[dashboard]} /> <ChartBlock spec={chart} /> // every block is one `record_aggregate`
62
+ <NotifyRuleEditor tableId={id} /> // a subscription is a Rule record over a saved view
63
+ <DocTemplate tableId={id} seed={[tpl]} /> <DocRender templateId={t} recordId={r} /> // tokens resolved by the store
64
+ <SignBlock tableId={id} recordId={r} render={render} /> // the signature is checked against the exact bytes
65
+ ```
66
+
67
+ ## Talking to a record, and letting an agent keep it filled
68
+
69
+ ```tsx
70
+ <RecordChat tableId={id} recordId={recordId} /> // the HOST's chat surface, this record as its context
71
+ <EnrichPanel tableId={id} recordId={recordId} /> // per-field AI fill, freshness and provenance
72
+ ```
73
+
74
+ `RecordChat` ships **no chat UI**. AI Matrx has exactly one — matrx-frontend's
75
+ `AgentConversationColumn` — and it reads that app's Redux store, so a package
76
+ that imported it would stop being a package and a second one here would drift
77
+ within a month. `RecordChat` builds the part only the record store can build:
78
+ the record read through the read door, each value carrying its Field's unit and
79
+ format (the agent is given `$48,000`, never a bare `48000`), the fields the
80
+ store MASKED named out loud, and the `secret` and `on_demand` ones held back
81
+ with the reason. Then it hands that to the `chat` port:
82
+
83
+ ```tsx
84
+ <RecordsUiProvider value={{
85
+ chat: (ctx) => {
86
+ const { conversationId } = useAgentLauncher(agentId, {
87
+ surfaceKey: ctx.surfaceKey,
88
+ runtime: { context: Object.fromEntries(ctx.entries.map((e) => [e.key, e.value])) },
89
+ });
90
+ return <AgentConversationColumn conversationId={conversationId} surfaceKey={ctx.surfaceKey} />;
91
+ },
92
+ }}>
93
+ ```
94
+
95
+ Resolve the agent through a **Mandate**, never a pinned UUID. Unbound, the panel
96
+ is absent with its reason and draws no composer.
97
+
98
+ `EnrichPanel` reads which Fields an agent keeps filled from the Fields
99
+ themselves (`source = agent`) and how often from `review_interval_days`, and
100
+ dates each value off its own envelope — never off the row's `updated_at`, which
101
+ would call every field stale because one of them changed. The fill itself goes
102
+ through the **server's `records` tool**, because a write stamped actor `agent`
103
+ on behalf of a person can only honestly be made by the server:
104
+
105
+ ```tsx
106
+ <RecordsUiProvider value={{
107
+ enrich: (ask) => fetch("/api/ai/tools/execute", { method: "POST", body: JSON.stringify({
108
+ tool_name: "records", surface: "records-ui:enrich",
109
+ arguments: { action: "record_write", record_id: ask.recordId, values: {/* the agent's answers */} },
110
+ })}).then((r) => r.json()).then((r) => ({ ok: r.ok, message: r.output })),
111
+ }}>
112
+ ```
113
+
114
+ Two live switches sit in front of that call today and the panel names both
115
+ rather than spinning: the knob `custom/code_paths_enabled` resolves false, and
116
+ the `records` row in `tool.binding` is `is_active = false`.
117
+
118
+ ## Work: checklists, bookings and a phone with no signal
119
+
120
+ ```tsx
121
+ <ChecklistRunner tableId={id} seed={[{ name: "Onboarding", subject: id, steps: [{ title: "Confirm the address" }] }]} />
122
+ <BookingSlots tableId={id} form={bookingForm} availability={{ fromHour: 9, toHour: 17, everyMinutes: 60, days: 5 }} />
123
+ <CaptureSheet tableId={id} readingField="amount" noteField="title" />
124
+ ```
125
+
126
+ `ChecklistRunner` makes every step a **real record** — assignable, commentable,
127
+ exportable, historied — and the whole graph is created in ONE act
128
+ (`custom.work_template_instantiate` judges it before writing any of it), so a
129
+ half-made checklist is not a state this store can be in. The states and the
130
+ moves offered come from `custom.work_states()`; a move the store does not list
131
+ as next is not shown greyed out, it is not there.
132
+
133
+ `BookingSlots` holds the slot **first** and takes the details second — taking
134
+ details first is how two people fill in a form and one of them finds out at the
135
+ end. One hold per slot is a UNIQUE INDEX in the database, not a check this
136
+ screen makes, so the loser of a race reads the database's own sentence naming
137
+ the index that refused it. Holds live in a slots Table the component declares
138
+ through `custom.work_slots_declare`; if the store cannot build that index it
139
+ refuses, and this screen shows the refusal rather than a picker that would sell
140
+ the same hour twice.
141
+
142
+ `CaptureSheet` is the offline capture contract, and both halves matter: the
143
+ **client mints the id** the moment a person hits Capture — offline, before the
144
+ first attempt, never re-minted on retry — and **the store dedupes on it**, so a
145
+ reconnecting phone that fires the same capture thirty times writes one record
146
+ and gets the same id back every time. Bind `captureQueue` to keep the queue
147
+ across a reload; unbound, it is in memory for the life of the page and says so.
148
+
149
+ ## Outsiders: a portal, a public page, an embed
150
+
151
+ ```tsx
152
+ <PortalShell tableId={id} form={intakeForm} /> // their list, their record, their form
153
+ <PublicViewPage slug={params.slug} /> // a signed-out visitor's page
154
+ <EmbedFrame tableId={id} formId={formId} embedUrl="https://app.example/embed" /> // issue the token
155
+ const { binding, error } = useEmbedHandshake({ secret, requiredMode: "read" }); // and verify it, inside the iframe
156
+ ```
157
+
158
+ `PortalShell` asks `iam` who the person is rather than deciding for itself:
159
+ "external" means no membership of a **non-personal** organization, because
160
+ signup mints everyone a personal one and the other reading makes nobody
161
+ external. What they reach is `iam.external_principal_reach` — exactly the
162
+ resource ids Visibility handed them, one at a time, never "their
163
+ organization's records".
164
+
165
+ `PublicViewPage` renders a publish binding, and **a share is not a publish**: an
166
+ address nobody made, one that was revoked, and one whose lane closed all resolve
167
+ to the same nothing, because an address that answered differently for a revoked
168
+ binding would be an existence oracle. Every read carries the store's
169
+ "not scanned" note (D-15) — one function, so the page, the API and the admission
170
+ row cannot drift into three different admissions. Reaching the published happy
171
+ path needs a claimed namespace **and a verified publisher**, which is a person
172
+ standing behind what goes on the open internet, not something a screen may forge.
173
+
174
+ `EmbedFrame` mints a token through `custom.anon_token_issue`, shows the secret
175
+ **once** (only its digest is stored), and states the rules before the refusal: a
176
+ token carries one decision — read or write, never both — and its origin list is
177
+ required and matched exactly, so `example.com.evil.test` fails a check a suffix
178
+ match would pass.
179
+
180
+ ## What every one of them does the same way
181
+
182
+ - **The store's own sentence.** A refusal is never re-worded. "Someone else
183
+ changed this record while you were working on it" is what the person reads,
184
+ because it is what the store said, and the conflict comes with it field by
185
+ field.
186
+ - **Absent, never dead.** A control a person may not use is not rendered. There
187
+ is no disabled "+" and no greyed settings panel anywhere in this package.
188
+ - **A Field decides its editor.** The thirteen parity types come from the store
189
+ (`custom.parity_field_types()`), generated into `@ai-matrx/records`. The unit
190
+ and the format are on the Field because they change what the value MEANS.
191
+ - **Nothing is dropped quietly.** An unmapped import column is shown and
192
+ offered as a field proposal; a list that is not live says so; a door that does
193
+ not exist yet is named, with the lane that owes it.
194
+ - **Dense, one-row headers, light and dark, iOS-safe**, through the design
195
+ system's own tokens — this package sets no colours of its own.
196
+
197
+ ## The demo
198
+
199
+ `pnpm demo` runs every component against **the one live store**, headless, on
200
+ this lane's own port (127.0.0.1:3005), and stands its own table, fields and
201
+ records up the first time it runs. Its data seam is the server lane the platform
202
+ uses today, in one transaction per request that opens the product switch, acts
203
+ as the test identity (`test@test.com`) through the store's own doors, and sets
204
+ every switch back before committing — so no other session ever observes one
205
+ open. `pnpm test` renders those same pages in jsdom against that server and
206
+ asserts what a person would see.
207
+
208
+ It reads the live store deliberately: `custom.anon_*`, the embed token, the
209
+ external principal and the publish binding exist only there, and a demo pointed
210
+ at a rehearsal copy would answer `door_absent` for a third of this package and
211
+ call it a finding.
212
+
213
+ ## The thirty-one components
214
+
215
+ | | | |
216
+ |---|---|---|
217
+ | `Grid` | `RecordForm` | `FieldEditor` |
218
+ | `TableSettings` | `RelationPicker` | `Peek` |
219
+ | `ViewSwitcher` | `ViewBar` | `ImportWizard` |
220
+ | `ExportMenu` | `CustomFieldsSection` | `ActionInbox` |
221
+ | `ProposalRow` | `HistoryPanel` | `CommentThread` |
222
+ | `FormBuilder` | `FormRunner` | `ChartBlock` |
223
+ | `DashboardCanvas` | `NotifyRuleEditor` | `DocTemplate` |
224
+ | `DocRender` | `SignBlock` | `RecordChat` |
225
+ | `EnrichPanel` | `ChecklistRunner` | `BookingSlots` |
226
+ | `CaptureSheet` | `PortalShell` | `PublicViewPage` |
227
+ | `EmbedFrame` | | |
228
+
229
+ ## Commands
230
+
231
+ | Command | What it does |
232
+ |---|---|
233
+ | `pnpm demo` | Every component, live on the rehearsal branch, headless |
234
+ | `pnpm test` | The demo pages asserted headlessly |
235
+ | `pnpm build` | tsup → `dist` (esm + cjs + d.ts), then the client boundary stamp |
236
+ | `pnpm check:package` | build · exports agreement · publint · tarball canary |