@ai-matrx/records-ui

中文文档
0.116.126发布于 2026/10/30MITTypeScriptESM + CJS近 30 天下载 10.7万

The thirty-one canonical screens of the AI Matrx record store: grid, generated record form, field editor, table settings, relation picker, peek, views, import/export, custom fields, action inbox, history, comments, forms, charts, documents, record chat, A

原始使用文档
文档目录10 节
  1. Bind once
  2. Then, a few lines each
  3. Views, the queue, history and comments
  4. Talking to a record, and letting an agent keep it filled
  5. Work: checklists, bookings and a phone with no signal
  6. Outsiders: a portal, a public page, an embed
  7. What every one of them does the same way
  8. The demo
  9. The thirty-one components
  10. Commands

@ai-matrx/records-ui

The platform's primary record UI components. Every component here reads and writes ONLY through @ai-matrx/records, renders with @ai-matrx/design-system's own primitives and data table, and carries @ai-matrx/alchemy's menu where a person reaches for one.

There is no bespoke per-feature record UI in this platform. A feature that needs a grid, a form, a picker or a peek mounts one of these — and any one usage is a few lines.

Bind once

import { RecordsProvider } from "@ai-matrx/records/react";
import { RecordsUiProvider } from "@ai-matrx/records-ui";
import "@ai-matrx/design-system/styles.css";

<RecordsProvider config={{ dataSource: supabase, organizationId, actor: { actor: "user", user_id } }}>
  <RecordsUiProvider value={{ rights: (table) => myRights(table), density: "condensed" }}>
    {children}
  </RecordsUiProvider>
</RecordsProvider>;

rights is the port that decides whether a person sees the administering controls. Unbound, it answers "no" WITH A REASON, and every such control is ABSENT — this package ships no disabled variant of any of them.

Then, a few lines each

import {
  Grid, RecordForm, FieldEditor, TableSettings, RelationPicker,
  Peek, ImportWizard, ExportMenu, CustomFieldsSection,
} from "@ai-matrx/records-ui";

<Grid tableId={id} onOpenRecord={setPeeking} onAddField={openSettings} />           // records as rows, fields as columns
<RecordForm tableId={id} />                                                          // create, generated from the Field records
<RecordForm tableId={id} recordId={recordId} />                                      // edit, with the conflict shown when someone wins
<Peek tableId={id} recordId={recordId} onClose={close} />                            // any custom table's record, no per-table code
<TableSettings tableId={id} />                                                       // rename · reorder · retype · delete
<FieldEditor tableId={id} field={field} />                                           // one Field's type, rules and sensitivity
<ImportWizard tableId={id} onProposeField={propose} />                               // map columns, preview rows, propose the rest
<ExportMenu tableId={id} label="Deals" />                                            // CSV · XLSX · Alchemy
<CustomFieldsSection tableId={entityTableId} recordId={record.id} />                 // THE one line a standard entity page adds

RelationPicker is used by RecordForm automatically for any relation field; mount it directly when a screen needs just the picker.

Views, the queue, history and comments

<ViewBar tableId={id} onActiveView={setView} seed={[{ name: "By stage", subject: id, layout: "kanban", groupField: "stage" }]} />
<ViewSwitcher view={view} />                                                         // grid · kanban · calendar · gallery, one membership Rule
<ActionInbox tableId={id} seed={[...]} />                                            // approvals, assignments and agent proposals, one queue
<HistoryPanel tableId={id} recordId={recordId} />                                    // the timeline IS the record; undo is the store's soft delete
<CommentThread tableId={id} recordId={recordId} />                                   // a comment is a record
<FormBuilder tableId={id} seed={[form]} />  <FormRunner form={form} />               // a form is a view on a real Table
<DashboardCanvas tableId={id} seed={[dashboard]} />  <ChartBlock spec={chart} />     // every block is one `record_aggregate`
<NotifyRuleEditor tableId={id} />                                                    // a subscription is a Rule record over a saved view
<DocTemplate tableId={id} seed={[tpl]} />  <DocRender templateId={t} recordId={r} /> // tokens resolved by the store
<SignBlock tableId={id} recordId={r} render={render} />                              // the signature is checked against the exact bytes

Talking to a record, and letting an agent keep it filled

<RecordChat tableId={id} recordId={recordId} />                                      // the HOST's chat surface, this record as its context
<EnrichPanel tableId={id} recordId={recordId} />                                     // per-field AI fill, freshness and provenance

RecordChat ships no chat UI. AI Matrx has exactly one — matrx-frontend's AgentConversationColumn — and it reads that app's Redux store, so a package that imported it would stop being a package and a second one here would drift within a month. RecordChat builds the part only the record store can build: the record read through the read door, each value carrying its Field's unit and format (the agent is given $48,000, never a bare 48000), the fields the store MASKED named out loud, and the secret and on_demand ones held back with the reason. Then it hands that to the chat port:

<RecordsUiProvider value={{
  chat: (ctx) => {
    const { conversationId } = useAgentLauncher(agentId, {
      surfaceKey: ctx.surfaceKey,
      runtime: { context: Object.fromEntries(ctx.entries.map((e) => [e.key, e.value])) },
    });
    return <AgentConversationColumn conversationId={conversationId} surfaceKey={ctx.surfaceKey} />;
  },
}}>

Resolve the agent through a Mandate, never a pinned UUID. Unbound, the panel is absent with its reason and draws no composer.

EnrichPanel reads which Fields an agent keeps filled from the Fields themselves (source = agent) and how often from review_interval_days, and dates each value off its own envelope — never off the row's updated_at, which would call every field stale because one of them changed. The fill itself goes through the server's records tool, because a write stamped actor agent on behalf of a person can only honestly be made by the server:

<RecordsUiProvider value={{
  enrich: (ask) => fetch("/api/ai/tools/execute", { method: "POST", body: JSON.stringify({
    tool_name: "records", surface: "records-ui:enrich",
    arguments: { action: "record_write", record_id: ask.recordId, values: {/* the agent's answers */} },
  })}).then((r) => r.json()).then((r) => ({ ok: r.ok, message: r.output })),
}}>

Two live switches sit in front of that call today and the panel names both rather than spinning: the knob custom/code_paths_enabled resolves false, and the records row in tool.binding is is_active = false.

Work: checklists, bookings and a phone with no signal

<ChecklistRunner tableId={id} seed={[{ name: "Onboarding", subject: id, steps: [{ title: "Confirm the address" }] }]} />
<BookingSlots tableId={id} form={bookingForm} availability={{ fromHour: 9, toHour: 17, everyMinutes: 60, days: 5 }} />
<CaptureSheet tableId={id} readingField="amount" noteField="title" />

ChecklistRunner makes every step a real record — assignable, commentable, exportable, historied — and the whole graph is created in ONE act (custom.work_template_instantiate judges it before writing any of it), so a half-made checklist is not a state this store can be in. The states and the moves offered come from custom.work_states(); a move the store does not list as next is not shown grayed out, it is not there.

BookingSlots holds the slot first and takes the details second — taking details first is how two people fill in a form and one of them finds out at the end. One hold per slot is a UNIQUE INDEX in the database, not a check this screen makes, so the loser of a race reads the database's own sentence naming the index that refused it. Holds live in a slots Table the component declares through custom.work_slots_declare; if the store cannot build that index it refuses, and this screen shows the refusal rather than a picker that would sell the same hour twice.

CaptureSheet is the offline capture contract, and both halves matter: the client mints the id the moment a person hits Capture — offline, before the first attempt, never re-minted on retry — and the store dedupes on it, so a reconnecting phone that fires the same capture thirty times writes one record and gets the same id back every time. Bind captureQueue to keep the queue across a reload; unbound, it is in memory for the life of the page and says so.

Outsiders: a portal, a public page, an embed

<PortalShell tableId={id} form={intakeForm} />                                       // their list, their record, their form
<PublicViewPage slug={params.slug} />                                                // a signed-out visitor's page
<EmbedFrame tableId={id} formId={formId} embedUrl="https://app.example/embed" />     // issue the token
const { binding, error } = useEmbedHandshake({ secret, requiredMode: "read" });       // and verify it, inside the iframe

PortalShell asks iam who the person is rather than deciding for itself: "external" means no membership of a non-personal organization, because signup mints everyone a personal one and the other reading makes nobody external. What they reach is iam.external_principal_reach — exactly the resource ids Visibility handed them, one at a time, never "their organization's records".

PublicViewPage renders a publish binding, and a share is not a publish: an address nobody made, one that was revoked, and one whose lane closed all resolve to the same nothing, because an address that answered differently for a revoked binding would be an existence oracle. Every read carries the store's "not scanned" note (D-15) — one function, so the page, the API and the admission row cannot drift into three different admissions. Reaching the published happy path needs a claimed namespace and a verified publisher, which is a person standing behind what goes on the open internet, not something a screen may forge.

EmbedFrame mints a token through custom.anon_token_issue, shows the secret once (only its digest is stored), and states the rules before the refusal: a token carries one decision — read or write, never both — and its origin list is required and matched exactly, so example.com.evil.test fails a check a suffix match would pass.

What every one of them does the same way

  • The store's own sentence. A refusal is never re-worded. "Someone else changed this record while you were working on it" is what the person reads, because it is what the store said, and the conflict comes with it field by field.
  • Absent, never dead. A control a person may not use is not rendered. There is no disabled "+" and no grayed settings panel anywhere in this package.
  • A Field decides its editor. The thirteen parity types come from the store (custom.parity_field_types()), generated into @ai-matrx/records. The unit and the format are on the Field because they change what the value MEANS.
  • Nothing is dropped quietly. An unmapped import column is shown and offered as a field proposal; a list that is not live says so; a door that does not exist yet is named, with the lane that owes it.
  • Dense, one-row headers, light and dark, iOS-safe, through the design system's own tokens — this package sets no colors of its own.

The demo

pnpm demo runs every component against the one live store, headless, on this lane's own port (127.0.0.1:3005), and stands its own table, fields and records up the first time it runs. Its data seam is the server lane the platform uses today, in one transaction per request that opens the product switch, acts as the test identity ([email protected]) through the store's own doors, and sets every switch back before committing — so no other session ever observes one open. pnpm test renders those same pages in jsdom against that server and asserts what a person would see.

It reads the live store deliberately: custom.anon_*, the embed token, the external principal and the publish binding exist only there, and a demo pointed at a rehearsal copy would answer door_absent for a third of this package and call it a finding.

The thirty-one components

GridRecordFormFieldEditor
TableSettingsRelationPickerPeek
ViewSwitcherViewBarImportWizard
ExportMenuCustomFieldsSectionActionInbox
ProposalRowHistoryPanelCommentThread
FormBuilderFormRunnerChartBlock
DashboardCanvasNotifyRuleEditorDocTemplate
DocRenderSignBlockRecordChat
EnrichPanelChecklistRunnerBookingSlots
CaptureSheetPortalShellPublicViewPage
EmbedFrame

Commands

CommandWhat it does
pnpm demoEvery component, live on the rehearsal branch, headless
pnpm testThe demo pages asserted headlessly
pnpm buildtsup → dist (esm + cjs + d.ts), then the client boundary stamp
pnpm check:packagebuild · exports agreement · publint · tarball canary

版本与发布记录

@ai-matrx/records-ui 共发布 507 个版本,首次发布于 2026/09/19;过去 12 个月发布了 507 个版本。

版本发布日期距今
0.116.126latest2026/10/306 天前
0.116.1252026/10/297 天前
0.116.1242026/10/297 天前
0.116.1232026/10/298 天前
0.116.1222026/10/288 天前
0.116.1212026/10/288 天前

其他发布通道:backfill → 0.113.4。

关于 @ai-matrx/records-ui 的常见问题

@ai-matrx/records-ui 是什么?

The thirty-one canonical screens of the AI Matrx record store: grid, generated record form, field editor, table settings, relation picker, peek, views, import/export, custom fields, action inbox, history, comments, forms, charts, documents, record chat, A

如何安装 @ai-matrx/records-ui?

在项目目录运行 npm install @ai-matrx/records-ui,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18.0.0)、recharts(>=3.0.0)、react-dom(>=18.0.0)、@dnd-kit/core(>=6.3.1)、@dnd-kit/sortable(>=10.0.0)、@dnd-kit/utilities(>=3.2.2)、@tanstack/react-query(>=5.100.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@ai-matrx/records-ui 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ai-matrx/records-ui 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@ai-matrx/records-ui 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ai-matrx/records-ui 可以商用吗?

@ai-matrx/records-ui 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ai-matrx/records-ui 的下载量有多少?

根据 npm 官方下载统计,@ai-matrx/records-ui 在 2026/10/05 – 2026/11/03 这 30 天内被下载 107,469 次,日均约 3,582 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ai-matrx/records-ui 还在维护吗?

最新版本 0.116.126 发布于 2026/10/30(6 天前);至今共发布 507 个版本,其中过去 12 个月发布了 507 个;首次发布于 2026/09/19。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。