@ai-matrx/records-ui 0.109.6 → 0.110.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/parity.ts","../src/gridLook.ts","../src/gridPage.ts","../src/presentation.ts","../src/viewKeys.ts","../src/views.ts","../src/viewLook.ts","../src/firstPage.ts"],"names":["KERNEL_TABLES","PARITY_FIELD_TYPES","fieldKindFor","parseTableStyle","tableStyleIsEmpty","raw","recordingDataSource","asRecordsDataSource","createRecordsClient"],"mappings":"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","file":"first-page.cjs","sourcesContent":["// src/parity.ts — @ai-matrx/records-ui\n//\n// FLD-11, the parity floor. A Field stores a BEHAVIOUR (one of five) plus\n// modifiers, unit and format; a person picked one of THIRTEEN parity types in\n// the UI. This resolves the second from the first, using the same facts the\n// store's own `custom.parity_field_types()` describes — which is generated into\n// `@ai-matrx/records` and imported here, never retyped.\n//\n// Nothing in this file decides policy. It decides which editor to draw.\n\nimport { displayFormatOf } from \"./choiceLooks\";\nimport {\n KERNEL_TABLES,\n PARITY_FIELD_TYPES,\n fieldKindFor,\n newerKindOf,\n type Field,\n type FieldKind,\n type KernelTargets,\n type ParityFieldType,\n} from \"@ai-matrx/records\";\nimport { humanizeIdentifier } from \"@ai-matrx/kit/text-case\";\nimport { isSignatureField } from \"./docs\";\nimport { fieldTypeChoice, isSystemFieldKind, type PickableFieldType } from \"./fieldTypes\";\n\n/** The kernel ids come from the store's own kernel rows, never from a literal here. */\nconst kernelId = (name: string): string =>\n KERNEL_TABLES.find((t) => t.name === name)?.id ?? \"\";\nconst KERNEL_FILE_TABLE = kernelId(\"File\");\nconst KERNEL_PERSON_TABLE = kernelId(\"Person\");\n\nexport const PARITY_LABEL: Record<ParityFieldType, string> = {\n attachment: \"Attachment\",\n checkbox: \"Yes / no\",\n currency: \"Currency\",\n datetime: \"Date\",\n email: \"Email\",\n formula: \"Formula\",\n lookup: \"Lookup\",\n member: \"Person\",\n multi_select: \"Multi-select\",\n percent: \"Percent\",\n phone: \"Phone\",\n rollup: \"Rollup\",\n select: \"Select\",\n url: \"Link\",\n};\n\n/** The \"made of\" sentence the store itself publishes, for the field editor's help text. */\nexport const PARITY_MADE_OF: Record<string, string> = Object.fromEntries(\n PARITY_FIELD_TYPES.map((p) => [p.parity_type, p.made_of]),\n);\n\n/**\n * Plain text and plain number are behaviours with no parity type of their own.\n *\n * 🚨 THIS IS NOW AN ALIAS, AND THAT IS THE POINT (TAILS-3, 2026-09-21). The\n * classifier moved into `@ai-matrx/records` as `FieldKind` so that the editor a\n * person types into and the COERCION applied to what they typed are decided by\n * one body. They were decided by two, and the public booking page paid for it:\n * \"Vehicle Year takes a number, and it was given a string\" on every table with\n * a number field.\n */\nexport type EditorKind = FieldKind;\n\n/** The kernel ids come from the store's own kernel rows, never from a literal here. */\nconst KERNEL: KernelTargets = { file: KERNEL_FILE_TABLE, person: KERNEL_PERSON_TABLE };\n\n/**\n * Which editor one Field wants. Reads the Field's own declaration only — the\n * behaviour, the modifiers, the format, the relation target and the config —\n * because those are what the store stores, and a screen that guessed from a\n * value would draw a different editor for an empty record.\n *\n * It is `fieldKindFor` with this package's kernel ids bound. Nothing here\n * decides anything the core does not.\n */\nexport function editorKindFor(field: Field): EditorKind {\n return fieldKindFor(field, KERNEL);\n}\n\n/**\n * WHICH KIND A PERSON PICKED, read back off the Field the store keeps — the ONE reader every\n * \"what does this column hold\" answer goes through (the Settings column list, the column panel).\n *\n * `editorKindFor` answers which EDITOR to draw, and for two families that is not the kind a\n * person picked: a Signature is a text Field whose format is `signature` (it draws a line of\n * text), and the store's stamps — Record number, Created time, Last changed time — are formula\n * Fields whose `config.system` names the stamp. Read through the editor classifier alone, the\n * Settings list called a Signature \"Text\" and a Record number \"Worked out from other columns\"\n * (lane 10 W1-A, guard `every-pickable-kind-reads-back-as-itself.test.tsx`).\n *\n * `undefined` when the Field is none of the kinds the menu offers.\n */\nexport function pickableTypeOf(field: Field): PickableFieldType | undefined {\n if (isSignatureField(field)) return \"signature\";\n // LANE 10 P4: the nine newer kinds, each a base type wearing a format (`newerKindOf`).\n const newer = newerKindOf(field);\n if (newer) return newer;\n // A RATING OR A TIME made as a plain column wearing its look (`LOOK_KINDS`): named by that kind.\n const look = displayFormatOf(field)?.id;\n if (look === \"rating\" && field.type === \"range\") return \"rating\";\n if (look === \"time\" && field.type === \"text\") return \"time\";\n const system = (field.config as Record<string, unknown> | undefined)?.[\"system\"];\n if (field.type === \"formula\" && typeof system === \"string\" && isSystemFieldKind(system)) return system;\n const kind = editorKindFor(field);\n return fieldTypeChoice(kind) ? (kind as PickableFieldType) : undefined;\n}\n\n/**\n * The type word a person sees in a header or a settings row — and it is the\n * SAME word the field panel offers, read from `FIELD_TYPE_CHOICES`, so a column\n * added as \"Money\" is never described anywhere else as \"Currency\".\n */\nexport function fieldTypeLabel(field: Field): string {\n const kind = pickableTypeOf(field) ?? editorKindFor(field);\n const known = fieldTypeChoice(kind)?.label;\n if (known) return field.multi && kind !== \"multi_select\" ? `${known} (many)` : known;\n const plain: Record<string, string> = {\n // `boolean` is the BEHAVIOUR word; `checkbox` is the type a person picks. Both are\n // written the way a person writes them, never as a raw token.\n boolean: \"Yes / no\",\n date: \"Date\",\n json: \"Structured data\",\n relation: \"Link to a record\",\n };\n // Never a raw token on a screen: an editor kind this table has not named is\n // still written the way a person writes it.\n return plain[kind] ?? humanizeIdentifier(kind);\n}\n","// src/gridLook.ts — @ai-matrx/records-ui · lane DEFAULT-GRID-PARITY\n//\n// WHAT THE STORE ALREADY SAYS ABOUT HOW A TABLE OPENS, READ IN ONE PLACE.\n//\n// The owner, on his real table's new page (2026-09-23): \"none of my advanced features are\n// copied over.\" Each of the things below was IN THE STORE and the default grid never read\n// it (`common-docs/systems/data/custom-data/HANDOFF.md`, rows D1 D3 D4 D5 D6\n// S2 F1 R3). They are pure functions on purpose — the grid is a large component and every\n// one of these answers is a question a test must be able to ask without mounting it:\n//\n// · the table's DEFAULT SORT (`data.default_sort`, `[{field, direction}]`), which the store\n// keeps and nothing read;\n// · the LAYOUT the grid opens with: the platform, then the organization's knob\n// (`custom/grid_layout`, read through `custom.grid_layout`), then THIS VIEW's own choice —\n// three layers, in that order, and the view always wins;\n// · the SUMMARY measures a column may offer, from the store's own list\n// (`custom.agg_operations`), filtered to what the column can mean;\n// · a REFERENCE a person pastes into a prompt — the `table` / `table_row` reference nouns.\n\nimport type { Field, GridLayout, GridLayoutMode, GridRowHeight, ReadRow, Table } from \"@ai-matrx/records\";\nimport type { MatrxDataTableCopyConfig } from \"@ai-matrx/design-system/data-table/types\";\n\nimport { fieldName } from \"./names\";\nimport { shownValue, type LabelLookup } from \"./labels\";\nimport { editorKindFor } from \"./parity\";\n\n// ── the default sort ─────────────────────────────────────────────────────────────────────\n\nexport interface DefaultSort {\n /** The Field KEY — the column id the grid builds every column with. */\n id: string;\n direction: \"asc\" | \"desc\";\n}\n\n/**\n * THE TABLE'S OWN SORT, as the grid's primitive takes it — or `null`.\n *\n * The store writes `default_sort` as `[{field, direction}]` (`custom.table_declare`, the\n * mover, and this package's own `declareTable` all do), with `field` a Field KEY. The type\n * the read carries says `string | null`, and older rows hold a bare key or `\"key desc\"`, so\n * all three shapes are read. A sort that names a column this table no longer has is\n * DROPPED, never guessed at: sorting by the nearest column would be the screen inventing an\n * order nobody chose. Only the first sort key is used — the grid's primitive sorts by one.\n */\nexport function defaultSortOf(\n table: Pick<Table, \"default_sort\"> | null | undefined,\n fields: readonly Field[],\n): DefaultSort | null {\n const raw = (table?.default_sort ?? null) as unknown;\n const first = Array.isArray(raw) ? raw[0] : raw;\n let name: string | null = null;\n let direction: \"asc\" | \"desc\" = \"asc\";\n if (typeof first === \"string\") {\n const trimmed = first.trim();\n if (trimmed === \"\") return null;\n if (trimmed.startsWith(\"-\")) {\n name = trimmed.slice(1).trim();\n direction = \"desc\";\n } else {\n const [head, tail] = trimmed.split(/\\s+/);\n name = head ?? null;\n if (tail && tail.toLowerCase() === \"desc\") direction = \"desc\";\n }\n } else if (first && typeof first === \"object\") {\n const entry = first as Record<string, unknown>;\n const named = entry[\"field\"] ?? entry[\"key\"] ?? entry[\"id\"];\n name = typeof named === \"string\" ? named : null;\n if (String(entry[\"direction\"] ?? \"\").toLowerCase() === \"desc\") direction = \"desc\";\n }\n if (!name) return null;\n // A Field named by its KEY (what the store writes) or, in an older row, by its id.\n const field = fields.find((f) => f.key === name) ?? fields.find((f) => f.id === name);\n return field ? { id: field.key, direction } : null;\n}\n\n// ── the layout ───────────────────────────────────────────────────────────────────────────\n\n/** A view's own layout choices — the per-view override of the organization's knob. */\nexport interface ViewLook {\n /** px, the grid's existing per-view row height. */\n rowHeight?: number | undefined;\n /** The view's own frozen columns, by key (the grid's existing per-view freeze). */\n frozen?: string[] | undefined;\n wrap?: boolean | undefined;\n fit?: GridLayoutMode | undefined;\n /**\n * \"Freeze the first column\", said by THIS VIEW — `false` is an explicit NO over an\n * organization whose knob freezes it, which an empty `frozen` list cannot say (an empty\n * list is how a view that never chose reads).\n */\n freezeFirst?: boolean | undefined;\n}\n\n/**\n * The store's three row heights, in the pixels the grid's primitive takes. \"normal\" is the\n * primitive's own density default, which is why it maps to nothing rather than a number\n * that would override the density a host chose.\n */\nexport const ROW_HEIGHT_PX: Record<GridRowHeight, number | undefined> = {\n compact: 28,\n normal: undefined,\n tall: 64,\n};\n\n/** The px a view remembers, read back as the store's word — for the menu's check mark. */\nexport function rowHeightWord(px: number | undefined): GridRowHeight {\n if (px === undefined) return \"normal\";\n if (px <= 32) return \"compact\";\n if (px >= 52) return \"tall\";\n return \"normal\";\n}\n\nexport interface ResolvedLook {\n rowHeight: number | undefined;\n rowHeightWord: GridRowHeight;\n wrap: boolean;\n /** What the table does with its width: fit it, or keep natural widths and scroll. */\n fit: \"fit\" | \"scroll\";\n /** The choice as stored — `auto` resolves to fit or scroll by the column count. */\n fitChoice: GridLayoutMode;\n frozen: string[];\n /** Where each of the four came from, so the menu can say \"your organization's\". */\n from: { rowHeight: Source; wrap: Source; fit: Source; frozen: Source };\n}\n\nexport type Source = \"view\" | \"organization\" | \"platform\";\n\nfunction storeSource(layout: GridLayout | null, key: string): Source {\n const said = layout?.source?.[key];\n return said === \"organization\" ? \"organization\" : \"platform\";\n}\n\n/**\n * THE COLUMN ORDER THE LOOK READS \"FIRST\" FROM: the Fields' own order, with the column that names\n * each record (`title_field`) leading — \"freeze the first column\" freezes the record's name, as\n * Airtable's primary field is always the one that stays (merged-grid review 2, D1: the menu said\n * \"Freeze Title\" and pinned whichever column sorted first). Pure.\n */\nexport function lookColumnKeys(orderedKeys: readonly string[], titleField: string | null | undefined): string[] {\n return titleField && orderedKeys.includes(titleField)\n ? [titleField, ...orderedKeys.filter((k) => k !== titleField)]\n : [...orderedKeys];\n}\n\n/**\n * THE LOOK THE GRID OPENS WITH: the view's own choice over the organization's knob over the\n * platform. `columnKeys` is the grid's column order, because \"freeze the first column\" is a\n * statement about whichever column is first, and \"automatic\" fit is a statement about how\n * many columns there are (`fit_max_columns`).\n *\n * `layout` null means the store's answer has not arrived or was refused; the grid then opens\n * exactly as it always did — the knob is an override, never the only thing that makes a\n * table readable.\n */\nexport function resolveLook(\n layout: GridLayout | null,\n view: ViewLook,\n columnKeys: readonly string[],\n): ResolvedLook {\n const stored = layout?.layout;\n const rowHeight =\n view.rowHeight !== undefined ? view.rowHeight : stored ? ROW_HEIGHT_PX[stored.row_height] : undefined;\n const wrap = view.wrap ?? stored?.wrap ?? true;\n const fitChoice: GridLayoutMode = view.fit ?? stored?.mode ?? \"auto\";\n const max = stored?.fit_max_columns ?? 8;\n const fit: \"fit\" | \"scroll\" =\n fitChoice === \"fit\" ? \"fit\" : fitChoice === \"scroll\" ? \"scroll\" : columnKeys.length <= max ? \"fit\" : \"scroll\";\n const freezeFirst = view.freezeFirst ?? stored?.freeze_first_column ?? false;\n const frozen =\n view.frozen && view.frozen.length > 0\n ? view.frozen\n : freezeFirst && columnKeys[0]\n ? [columnKeys[0]]\n : [];\n return {\n rowHeight,\n rowHeightWord: view.rowHeight !== undefined ? rowHeightWord(view.rowHeight) : (stored?.row_height ?? \"normal\"),\n wrap,\n fit,\n fitChoice,\n frozen,\n from: {\n rowHeight: view.rowHeight !== undefined ? \"view\" : storeSource(layout, \"row_height\"),\n wrap: view.wrap !== undefined ? \"view\" : storeSource(layout, \"wrap\"),\n fit: view.fit !== undefined ? \"view\" : storeSource(layout, \"mode\"),\n frozen:\n (view.frozen && view.frozen.length > 0) || view.freezeFirst !== undefined\n ? \"view\"\n : storeSource(layout, \"freeze_first_column\"),\n },\n };\n}\n\n// ── the summary bar ──────────────────────────────────────────────────────────────────────\n\n/** What each of the store's measures is called on screen. */\nexport const MEASURE_LABEL: Record<string, string> = {\n count: \"Count\",\n sum: \"Sum\",\n avg: \"Average\",\n min: \"Smallest\",\n max: \"Largest\",\n median: \"Median\",\n filled: \"Filled\",\n empty: \"Empty\",\n unique: \"Unique\",\n percent_empty: \"Percent empty\",\n percent_filled: \"Percent filled\",\n range: \"Range\",\n};\n\n/**\n * NOTION'S CALCULATE MENU (Spaces N6) — the embedded grid's footer offers Notion's list, in Notion's\n * order and words, each answered by one of the store's measures. `footer` is what the footer cell\n * says before the value (\"Sum 1,240\"). Count all counts records, so every column offers it.\n */\nexport const NOTION_CALCULATIONS: ReadonlyArray<{ op: string; menu: string; footer: string }> = [\n { op: \"count\", menu: \"Count all\", footer: \"Count\" },\n { op: \"unique\", menu: \"Count unique values\", footer: \"Unique\" },\n { op: \"empty\", menu: \"Count empty\", footer: \"Empty\" },\n { op: \"filled\", menu: \"Count not empty\", footer: \"Not empty\" },\n { op: \"percent_empty\", menu: \"Percent empty\", footer: \"Empty\" },\n { op: \"percent_filled\", menu: \"Percent not empty\", footer: \"Not empty\" },\n { op: \"sum\", menu: \"Sum\", footer: \"Sum\" },\n { op: \"avg\", menu: \"Average\", footer: \"Average\" },\n { op: \"median\", menu: \"Median\", footer: \"Median\" },\n { op: \"min\", menu: \"Min\", footer: \"Min\" },\n { op: \"max\", menu: \"Max\", footer: \"Max\" },\n { op: \"range\", menu: \"Range\", footer: \"Range\" },\n];\n\n/** Notion's calculations this column can mean, among those the store offers. */\nexport function calculationsFor(field: Field, storeOps: readonly string[]): Array<{ op: string; menu: string; footer: string }> {\n const offered = new Set(measuresFor(field, storeOps, true));\n return NOTION_CALCULATIONS.filter((c) => offered.has(c.op));\n}\n\nconst NUMERIC_ONLY = new Set([\"sum\", \"avg\", \"median\"]);\nconst ARITHMETIC = new Set([\n \"add\", \"sub\", \"mul\", \"div\", \"mod\", \"power\", \"sum\", \"average\", \"round\", \"abs\", \"min\", \"max\", \"ceiling\", \"floor\",\n]);\nconst ORDERED = new Set([\"min\", \"max\", \"range\"]);\n\n/** Is this a column whose values are numbers the store can add up? */\nexport function isNumericField(field: Field): boolean {\n const kind = editorKindFor(field);\n if (kind === \"number\" || kind === \"currency\" || kind === \"percent\" || kind === \"rollup\") return true;\n // A worked-out column is a number when its expression is arithmetic — `{Visit fee} -\n // {Deposit taken}` is stored as `{\"op\":\"fx.sub\",…}`. A joined label is not.\n if (kind !== \"formula\") return false;\n const expr = ((field.config ?? {}) as Record<string, unknown>)[\"expr\"] as { op?: unknown } | undefined;\n const op = typeof expr?.op === \"string\" ? expr.op.replace(/^fx\\./, \"\") : \"\";\n return ARITHMETIC.has(op);\n}\n\nfunction isOrderedField(field: Field): boolean {\n return isNumericField(field) || editorKindFor(field) === \"datetime\";\n}\n\n/**\n * THE MEASURES THIS COLUMN MAY OFFER — the store's own list (`custom.agg_operations`),\n * narrowed to what the column can mean. A sum of a text column is not a number the store\n * would refuse; it is a number that means nothing, which is worse, so it is never offered.\n * `count` counts records and belongs to no column, so it is offered once, on the first.\n */\nexport function measuresFor(field: Field, storeOps: readonly string[], firstColumn: boolean): string[] {\n return storeOps.filter((op) => {\n if (op === \"count\") return firstColumn;\n if (NUMERIC_ONLY.has(op)) return isNumericField(field);\n if (ORDERED.has(op)) return isOrderedField(field);\n return true;\n });\n}\n\n// ── a reference for a prompt ─────────────────────────────────────────────────────────────\n\n/**\n * THE GRID'S COPY, WITH ITS REFERENCES (TABLE-PARITY R3).\n *\n * The data table's Copy already offers the rows as text and as agent JSON; what it could not\n * offer is a REFERENCE — the pointer an agent expands itself (`table` and `table_row`, the\n * reference nouns the server resolves, and which follow a moved table into the record store).\n * The primitive takes references only on an explicit copy config, so this is that config: the\n * same text and JSON the primitive's own default writes (every column that reads a value, in\n * the view's order, by its own name), plus the two references. A row reference is NAMED by\n * the table's row label so it reads as a name. The host's content-transfer port decides\n * whether it can build them; where none is installed, the reference choice is simply absent.\n */\nexport function gridCopyConfig(args: {\n tableId: string;\n tableName: string;\n fields: readonly Field[];\n rowLabel: (row: ReadRow) => string;\n /**\n * WHAT A REFERENCE IS CALLED — the grid's own resolver (`useRecordLabels`). Copy and Copy for\n * AI carry the words the cell shows (\"Ojeda residence\"), never the id the store keeps.\n */\n labels?: LabelLookup;\n}): MatrxDataTableCopyConfig<ReadRow> {\n const { tableId, tableName, fields, rowLabel, labels } = args;\n const shown = (row: ReadRow) => {\n const document = (row.document ?? {}) as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const field of fields) {\n if (document[field.key] !== undefined) out[fieldName(field)] = shownValue(field, document[field.key], labels);\n }\n return out;\n };\n const text = (value: unknown): string =>\n value === null || value === undefined ? \"\" : typeof value === \"object\" ? JSON.stringify(value) : String(value);\n return {\n label: tableName,\n listLabel: tableName,\n location: tableName,\n rowKind: \"table-row\",\n listKind: \"table-view\",\n listDescription: `${tableName} — the records on this page, filtered and sorted as shown.`,\n humanRow: (row) =>\n Object.entries(shown(row))\n .map(([name, value]) => `${name}: ${text(value)}`)\n .join(\"\\n\"),\n agentRow: (row) => shown(row),\n rowReferences: (row) => [\n {\n id: `table_row:${tableId}:${row.id}`,\n label: rowLabel(row),\n noun: \"table_row\",\n items: [{ table_id: tableId, row_id: row.id, table_name: tableName, row_label: rowLabel(row) }],\n },\n ],\n references: () => [\n {\n id: `table:${tableId}`,\n label: tableName,\n noun: \"table\",\n items: [{ table_id: tableId, table_name: tableName }],\n },\n ],\n };\n}\n","// src/gridPage.ts — @ai-matrx/records-ui · merge tranche 6k (server search, sort and paging)\n//\n// THE MERGED GRID ASKS THE STORE FOR ITS PAGE (`custom.read_records_page` through `useRecordPage`).\n//\n// The classic grid read one page and then searched, filtered and sorted THAT page in the browser —\n// the readiness audit searched a 1,000-row table for a record on page six and found nothing, and a\n// sort ordered fifty rows while claiming to order the table. Here the grid's own query state (the\n// data table's search box, its column filters, its header sort, its page) is translated into the\n// store's one question and the store answers it: sorted over the whole table, searched over every\n// column this reader may see, filtered in its own WHERE clause, counted.\n//\n// These are the pure halves: the sort in the store's shape, and the column filters in the store's\n// one filter shape. A filter the store cannot ask is NEVER applied in the browser to the page it\n// happens to hold (that is the lie this tranche removes) — it is refused by name, and the grid\n// says so.\n\nimport type { Field, RecordFilter, RecordPageSort, RecordsError, RuleExpression, StoreFilter } from \"@ai-matrx/records\";\nimport { isRuleFilter } from \"@ai-matrx/records\";\nimport { dateFilterBounds, dateFilterDayBounds, dayInstantWindow } from \"@ai-matrx/design-system/data-table\";\nimport { getPersonTimeZone } from \"./personTime\";\nimport type { ColumnFilterValue, ColumnFiltersState, SortState } from \"@ai-matrx/design-system/data-table/types\";\nimport { editorKindFor } from \"./parity\";\nimport { fieldName } from \"./names\";\n\n/**\n * 🚨 WHY THE STORE CANNOT SORT BY THIS COLUMN, or `null` when it can (merged-grid review\n * 2026-09-26). A column worked out on every read (`type: \"formula\"`, `compute_on: \"read\"` — the\n * default, and what a lookup or a rollup is) keeps no value in the record, so\n * `custom.read_records_page` refuses to order the whole table by it. The header offered \"Sort\n * ascending\" anyway; the refused page then emptied the grid. The screen asks this ONE question\n * before offering a sort, and says the store's own reason instead.\n */\nexport function sortRefusal(f: Field): string | null {\n if (!computedOnRead(f)) return null;\n return `${fieldName(f)} is worked out each time it is read, so the store keeps no value to sort the whole table by. Sort by a column it is worked out from.`;\n}\n\n/** The same fact for a filter: the store's WHERE clause reads kept values, and this column keeps none. */\nexport function filterRefusal(f: Field): string | null {\n if (!computedOnRead(f)) return null;\n return `${fieldName(f)} is worked out each time it is read, so the store keeps no value to filter the whole table by. Filter by a column it is worked out from.`;\n}\n\nfunction computedOnRead(f: Field): boolean {\n const kind = String(editorKindFor(f));\n return (\n (f.type === \"formula\" || kind === \"formula\" || kind === \"lookup\" || kind === \"rollup\") &&\n String((f as { compute_on?: unknown }).compute_on ?? \"read\") === \"read\"\n );\n}\n\n/**\n * Does this column hold NUMBERS the store keeps — a number, money or percent column, or the record\n * number (`config.system: autonumber`, worked out once on save and kept)?\n */\nexport function holdsNumbers(field: Field): boolean {\n const kind = String(editorKindFor(field));\n if (kind === \"number\" || kind === \"currency\" || kind === \"percent\") return true;\n return String(((field.config ?? {}) as Record<string, unknown>)[\"system\"] ?? \"\") === \"autonumber\";\n}\n\n/** How the store should compare a column's values when it sorts by it. */\nexport function sortAsFor(field: Field): NonNullable<RecordPageSort[\"as\"]> {\n const kind = String(editorKindFor(field));\n // The record number sorts 1, 2 … 10, never \"10\" before \"9\".\n if (holdsNumbers(field)) return \"number\";\n if (kind === \"datetime\") return \"date\";\n return \"text\";\n}\n\n/** The grid's header sort, in the store's page-order shape. A sort on a column that is gone is no sort. */\nexport function pageSortFor(fields: readonly Field[], sort: SortState | null | undefined): RecordPageSort[] {\n if (!sort) return [];\n const field = fields.find((f) => f.key === sort.id);\n if (!field) return [];\n return [{ field: field.key, direction: sort.direction === \"desc\" ? \"desc\" : \"asc\", as: sortAsFor(field) }];\n}\n\n/** A text a person typed, as a case-insensitive \"contains\" pattern (the store's `matches` is a regex). */\nexport function containsPattern(text: string): string {\n return `(?i)${text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}`;\n}\n\nexport interface StoreQuestion {\n /** The whole question: the view's own filter AND every column filter the store can ask. */\n filter: StoreFilter | undefined;\n /** Column filters the store cannot ask, each as a sentence naming the column. Never applied locally. */\n refused: string[];\n}\n\ntype Clause = { flat: RecordFilter } | { rule: RuleExpression };\n\n/**\n * THE DATA TABLE'S \"(empty)\" TICK, as it writes it into a select filter's `values`\n * (`SELECT_EMPTY_VALUE` in the design system's filter engine). Asked of the store as \"has not been\n * answered\". `a-text-column-filters-by-its-values-counted-by-the-store.test.tsx` ticks the real\n * checklist, so this word drifting from the table's is a red test, not a silent no-op.\n */\nexport const BLANK_VALUE = \"__empty__\";\n\nconst field = (f: Field): RuleExpression => ({ field: f.id });\nconst constant = (value: unknown): RuleExpression => ({ const: value });\n/** A column's moment is in the half-open window `{from, to}` — the store's `within` node. */\nconst withinWindow = (f: Field, window: { from?: string; to?: string }): RuleExpression =>\n ({ op: \"within\", args: [field(f), constant(window)] }) as unknown as RuleExpression;\nconst op = (name: string, ...args: RuleExpression[]): RuleExpression =>\n ({ op: name, args }) as unknown as RuleExpression;\n\n/** One column filter, as a store clause — or the sentence saying why the store cannot ask it. */\nexport function clauseFor(f: Field, value: ColumnFilterValue, now: Date = new Date()): Clause | string | null {\n const name = fieldName(f);\n const refused = filterRefusal(f);\n if (refused) return refused;\n const not = (rule: RuleExpression): RuleExpression => op(\"not\", rule);\n switch (value.kind) {\n case \"text\": {\n const mode = value.mode ?? \"contains\";\n if (mode === \"empty\" || mode === \"null\") return value.negated ? { rule: op(\"present\", field(f)) } : { rule: not(op(\"present\", field(f))) };\n if (mode === \"not_empty\" || mode === \"not_null\") return value.negated ? { rule: not(op(\"present\", field(f))) } : { rule: op(\"present\", field(f)) };\n const text = value.value.trim();\n if (text === \"\") return null;\n const kind = String(editorKindFor(f));\n if (kind === \"number\" || kind === \"currency\" || kind === \"percent\" || kind === \"checkbox\") {\n return `${name} holds numbers, so it is filtered by a range, not by words.`;\n }\n const matches = op(\"matches\", field(f), constant(containsPattern(text)));\n return { rule: value.negated ? not(matches) : matches };\n }\n case \"select\": {\n // \"Is any of\" the ticked values; \"(empty)\" ticked (`BLANK_VALUE`) is \"has not been answered\";\n // Exclude is everything else — a blank row is not one of the ticked values, so Exclude keeps it\n // unless \"(empty)\" is ticked too (the Sheet's \"Exclude these instead\", slice C).\n const ticked = value.values ?? (value.value === \"\" ? [] : [value.value]);\n const blank = ticked.includes(BLANK_VALUE);\n const words = ticked.filter((w) => w !== BLANK_VALUE);\n if (!blank && words.length === 0) return value.negated ? null : { rule: constant(false) };\n const present = op(\"present\", field(f));\n const kind = String(editorKindFor(f));\n if (kind === \"datetime\" || kind === \"date\") {\n // TICKED DAYS: each is a half-open window, asked as the store's one `within` leaf. One day\n // alone stays a flat window (what a dashboard number asks); several days are an ANY of\n // them, a day beside \"(empty)\" adds the unanswered, and Exclude is NOT — the same shapes\n // every other value list makes, kept by a saved view in the one filter grammar.\n if (words.length === 0) return { rule: value.negated ? present : not(present) };\n const holdsDays = String(((f.config ?? {}) as Record<string, unknown>)[\"kind\"] ?? \"\") === \"date\";\n if (words.length === 1 && !blank && !value.negated) return { flat: { [f.key]: dayWindow(words[0]!, holdsDays) } };\n const leaves = words.map((day) => withinWindow(f, dayWindow(day, holdsDays)));\n const anyDay = leaves.length === 1 ? leaves[0]! : op(\"or\", ...leaves);\n if (!value.negated) return { rule: blank ? op(\"or\", not(present), anyDay) : anyDay };\n return { rule: blank ? op(\"and\", present, not(anyDay)) : not(anyDay) };\n }\n // The list's words are text; a number or yes/no column is asked in its own kind.\n const asStored = (w: string): unknown =>\n holdsNumbers(f) || kind === \"rating\"\n ? Number.isFinite(Number(w)) ? Number(w) : w\n : kind === \"checkbox\" && (w === \"true\" || w === \"false\") ? w === \"true\" : w;\n const stored = words.map(asStored);\n const anyWord =\n stored.length === 0 ? null : stored.length === 1 ? op(\"eq\", field(f), constant(stored[0])) : op(\"or\", ...stored.map((w) => op(\"eq\", field(f), constant(w))));\n if (!value.negated) {\n if (!blank && stored.length === 1) return { flat: { [f.key]: stored[0] as string | number | boolean } };\n if (!blank) return { rule: anyWord! };\n return { rule: anyWord ? op(\"or\", not(present), anyWord) : not(present) };\n }\n if (blank) return { rule: anyWord ? op(\"and\", present, not(anyWord)) : present };\n return { rule: op(\"or\", not(present), not(anyWord!)) };\n }\n case \"boolean\":\n return value.negated ? { rule: op(\"ne\", field(f), constant(value.value)) } : { flat: { [f.key]: value.value } };\n case \"number\": {\n // The number filter's operators (lane C): =, ≠, <, >, empty, not empty; \"between\" below.\n // The header's Exclude (slice C) turns every one of them round, never only \"between\".\n const turned = (rule: RuleExpression): Clause => ({ rule: value.negated ? not(rule) : rule });\n if (value.op === \"empty\") return turned(not(op(\"present\", field(f))));\n if (value.op === \"not_empty\") return turned(op(\"present\", field(f)));\n if (value.op === \"eq\" || value.op === \"ne\") {\n if (typeof value.min !== \"number\") return null;\n return turned(op(value.op, field(f), constant(value.min)));\n }\n if (value.op === \"lt\") return typeof value.max === \"number\" ? turned(op(\"lt\", field(f), constant(value.max))) : null;\n if (value.op === \"gt\") return typeof value.min === \"number\" ? turned(op(\"gt\", field(f), constant(value.min))) : null;\n const parts: RuleExpression[] = [];\n if (typeof value.min === \"number\") parts.push(op(\"gte\", field(f), constant(value.min)));\n if (typeof value.max === \"number\") parts.push(op(\"lte\", field(f), constant(value.max)));\n if (parts.length === 0) return null;\n const range = parts.length === 1 ? parts[0]! : op(\"and\", ...parts);\n return { rule: value.negated ? not(range) : range };\n }\n case \"date\": {\n // A relative preset (\"Today\", \"Last hour\") is worked out at the moment the question is asked,\n // in the viewer's own clock, and sent as exact bounds (grids review 3, lane H item 3). A saved\n // view's \"Today\" is today when it is opened — never the day somebody first picked it. A\n // column that holds a DAY is asked in days, never compared with an instant.\n // The window is read in the PERSON'S zone (setPersonTimeZone), so \"Today\" here is what the calendar calls today.\n const zone = getPersonTimeZone() ?? undefined;\n const moments = dateFilterBounds(value.preset ? { ...value, anchor: now.toISOString() } : value, now, zone);\n const holdsDays = String(((f.config ?? {}) as Record<string, unknown>)[\"kind\"] ?? \"\") === \"date\";\n const bounds = holdsDays ? dateFilterDayBounds(moments, zone) : moments;\n if (bounds.since === undefined && bounds.until === undefined) return null;\n return {\n flat: {\n [f.key]: {\n ...(bounds.since === undefined ? {} : { from: bounds.since }),\n ...(bounds.until === undefined ? {} : { to: bounds.until }),\n },\n },\n };\n }\n default:\n return `${name} cannot be filtered that way by the store.`;\n }\n}\n\n/**\n * ONE DAY AS A HALF-OPEN WINDOW. A column that holds DAYS is asked in days (\"2026-09-01\" up to\n * \"2026-09-02\"); a column of moments is asked from the viewer's own midnight to the next.\n */\nexport function dayWindow(day: string, holdsDays: boolean): { from: string; to: string } {\n if (!holdsDays) return dayInstantWindow(day, getPersonTimeZone() ?? undefined);\n const [y, m, d] = day.split(\"-\").map(Number);\n const ymd = (t: Date) => `${t.getUTCFullYear()}-${String(t.getUTCMonth() + 1).padStart(2, \"0\")}-${String(t.getUTCDate()).padStart(2, \"0\")}`;\n return { from: ymd(new Date(Date.UTC(y!, (m ?? 1) - 1, d ?? 1))), to: ymd(new Date(Date.UTC(y!, (m ?? 1) - 1, (d ?? 1) + 1))) };\n}\n\n/**\n * THE VIEW'S QUESTION AND THE COLUMN FILTERS, AS ONE STORE QUESTION.\n *\n * Flat clauses (an equality, `null`, a date window) stay flat when nothing else needs a Rule, so a\n * plain \"Status is Scheduled\" is the same question a dashboard number asks. When any clause needs a\n * Rule (words that contain, a range, several choices, NOT), the flat equalities become `eq` nodes of\n * one ALL group. A date window becomes a `within` leaf of that group.\n */\nexport function storeQuestion(\n fields: readonly Field[],\n base: StoreFilter | undefined,\n columnFilters: ColumnFiltersState,\n now: Date = new Date(),\n): StoreQuestion {\n const refused: string[] = [];\n const flat: RecordFilter = {};\n const rules: RuleExpression[] = [];\n if (base && isRuleFilter(base)) rules.push(base);\n else if (base) Object.assign(flat, base);\n for (const [key, value] of Object.entries(columnFilters)) {\n if (!value) continue;\n const f = fields.find((candidate) => candidate.key === key);\n if (!f) continue;\n const clause = clauseFor(f, value, now);\n if (clause === null) continue;\n if (typeof clause === \"string\") {\n refused.push(clause);\n continue;\n }\n if (\"flat\" in clause) Object.assign(flat, clause.flat);\n else rules.push(clause.rule);\n }\n const flatKeys = Object.keys(flat);\n if (rules.length === 0) return { filter: flatKeys.length > 0 ? flat : undefined, refused };\n for (const key of flatKeys) {\n const f = fields.find((candidate) => candidate.key === key);\n const value = flat[key];\n if (!f) continue;\n if (value !== null && typeof value === \"object\") {\n rules.push(withinWindow(f, value as { from?: string; to?: string }));\n continue;\n }\n rules.push(value === null ? op(\"not\", op(\"present\", field(f))) : op(\"eq\", field(f), constant(value)));\n }\n return { filter: rules.length === 1 ? rules[0]! : op(\"and\", ...rules), refused };\n}\n\n/**\n * 🚨 THE STORE'S SENTENCE, WITH ITS COLUMNS CALLED BY THEIR NAMES (merged-grid review 2026-09-26).\n * A refused page read names the column by its KEY (\"The column \\\"visit_total\\\" is worked out each\n * time it is read…\"), and the refusal formatter — rightly — drops a sentence carrying a machine\n * key, so the grid printed only \"That was not accepted\" and \"Nothing was read.\": a red fragment.\n * The key is the grid's to translate: every Field key the sentence names becomes that column's\n * name, and the store's sentence is then said whole.\n */\nexport function inColumnWords(error: RecordsError, fields: readonly Field[]): RecordsError {\n const say = (text: string | null | undefined): string | null | undefined => {\n if (!text) return text;\n let out = text;\n for (const f of [...fields].sort((a, b) => b.key.length - a.key.length)) {\n const key = f.key.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n out = out.replace(new RegExp(`([\"“'])${key}([\"”'])`, \"g\"), `$1${fieldName(f)}$2`);\n }\n return out;\n };\n return { ...error, message: say(error.message) ?? error.message, ...(error.hint ? { hint: say(error.hint) ?? error.hint } : {}) };\n}\n","/**\n * HOW A TABLE LOOKS — colors, formats and the frozen column.\n *\n * THE SPLIT, AND WHY IT IS THIS ONE. A Field declares WHAT it holds: a\n * currency, a percent, a date, a person. A VIEW declares HOW that reads and\n * what it is worth looking at: two decimal places or none, tint the row when\n * Status is Blocked, keep Name in place while the rest scrolls. Airtable draws\n * the line in exactly this place, and it has to be here: two people read one\n * table two different ways, and neither reading is a fact about the records.\n *\n * So nothing in this module is a new column on a Field and nothing here needed\n * a migration. A presentation lives on the saved-view record (`views.ts`), which\n * is an ordinary record of an ordinary Table — which means an agent sets it\n * through the same `records` tool it uses for everything else, and it travels\n * with the table the way every other record does.\n *\n * A FIELD STILL GETS ITS MONEY RIGHT WITH NO CONFIGURATION. `formatForField`\n * reads the Field's OWN declaration — a `currency` parity type with unit `$`, a\n * `percent`, a `datetime` with `config.kind` — and returns the format that\n * follows from it. The view's `formats` map only OVERRIDES that. A table nobody\n * has ever styled still shows `$1,250.50` and `45%`.\n */\n\n// The style parser from its own client-free subpath (lane SSR-ROWS): `@ai-matrx/records-ui/first-page`\n// runs in a host's SERVER component, and every export of the \"use client\" `data-table` entry is a\n// client reference there — calling `parseTableStyle` through it threw on the server.\nimport { parseTableStyle, tableStyleIsEmpty } from \"@ai-matrx/design-system/data-table/table-style\";\nimport {\n COLOR_ROTATION,\n type ChoiceColorLookup,\n type GroupAggregateKind,\n type GroupAggregateSpec,\n type GroupOrder,\n type StyleColor,\n type TableStyle,\n} from \"@ai-matrx/design-system/data-table\";\nimport { getFieldFormat, type FieldFormatConfig } from \"@ai-matrx/design-system/field-formats\";\nimport type { ColorRule } from \"@ai-matrx/design-system/data-table\";\nimport { newerKindOf, ratingMaxOf, type Field, type TableDecorations } from \"@ai-matrx/records\";\n\nimport { editorKindFor } from \"./parity\";\nimport { dateFormatFor } from \"./dates\";\n\n/**\n * SECTIONS BY A FIELD — the view's own grouping.\n *\n * `field` is a Field KEY, exactly like `frozen` and the keys of `formats`,\n * because a view is written against the Table's declaration and never against\n * whatever id a screen happened to build its columns with.\n *\n * `aggregates` is keyed by Field key too, and `collapsed` holds the data\n * table's own group keys (`s:Blocked`, `n:3`, `empty`), which is what the\n * primitive emits and the only thing that survives a value rendering the same\n * text as another.\n *\n * NOTHING HERE IS EVER CALLED `valueOf`, `toString` or `hasOwnProperty`: every\n * object in JavaScript already has all three, so an optional field by one of\n * those names is NEVER absent and a `config.valueOf ? … : …` check silently\n * reads `Object.prototype`. It cost the primitive a whole grouping once.\n */\nexport type GridGrouping = {\n /** The Field key whose value makes the sections. */\n field: string;\n /** Per-Field-key subtotal on every section header. */\n aggregates?: Record<string, GroupAggregateSpec>;\n /** Section order. The primitive's default is `value-asc`; blanks sort last. */\n order?: GroupOrder;\n /** The sections this view keeps shut, by the primitive's own group key. */\n collapsed?: string[];\n /** The grouped column drawn as a column too. Absent = it steps aside (the headers name its value). */\n showColumn?: boolean;\n /**\n * SUB-GROUPS (lane S1-PRIME VIEW-KEYS; ui-bench LISTS \"sub-grouping\"): up to two more levels\n * inside each section, by Field key. At most three levels in all — the store refuses a fourth\n * (`custom.view_declare`). A layout that draws one level ignores the rest; it never drops them,\n * because this list is parsed and written back whole with the rest of the look.\n */\n then?: GridSubGroup[];\n /**\n * DRILL (lane DRILL-TABLE-PRIMITIVE): the Dimension pivoted across the top — a Field key, or\n * `<key>:<grain>` for a date cut into periods.\n */\n across?: string;\n /** DRILL: the period `field` is cut into when it is a date (`month`); the store keeps `field` a Field key. */\n grain?: string;\n /** The Measures shown (\"what to display\"), by the store's own keys (`count`, `sum_quantity`). */\n show?: string[];\n /** The drill trail, in order: each crumb a Dimension reference and the value it narrowed to. */\n trail?: GridDrillStep[];\n};\n\n/** One crumb of a saved drill trail. `value: null` is \"no value\". */\nexport type GridDrillStep = { dim: string; value: string | null };\n\n/** One sub-group level: a Field key and its section order. */\nexport type GridSubGroup = { field: string; order?: GroupOrder; grain?: string };\n\n/** Everything a view says about how its table LOOKS. */\nexport type GridPresentation = {\n /** Color-by-a-column, rules and manual highlights. */\n style?: TableStyle;\n /** Per-column format overrides, by the Field's key. */\n formats?: Record<string, FieldFormatConfig>;\n /**\n * The columns that stay in place while the table scrolls sideways, by Field\n * key. Only the LEADING run of the view's column order can actually freeze —\n * the data table decides that, and declines rather than pinning a column at a\n * guessed offset.\n */\n frozen?: string[];\n /** Collapsible sections by one Field, with subtotals on each header. */\n grouping?: GridGrouping;\n /**\n * The width a person dragged a column to, by Field key. SPARSE on purpose —\n * a view saved before widths existed carries none and still parses, and a\n * column nobody resized is left to the table's own measurement.\n */\n widths?: Record<string, number>;\n /** The body row height this view remembers, in px. */\n rowHeight?: number;\n /**\n * Field keys hidden from this view — the SAME hidden-column list the grid's\n * own column picker holds, now given a place on the view so it survives a\n * reload and a layout switch reads the identical set gallery and grid agree\n * on, rather than a second \"fields shown\" list nobody kept in sync.\n */\n hiddenFields?: string[];\n /**\n * THE FIELDS A CARD LEAVES OFF (board, list, gallery, calendar, timeline), by Field key — the\n * card's own choice, kept apart from `hiddenFields` so a board's \"Card fields\" never hides the\n * grid's columns when the view switches layout (v7 TABLE-EXPERIENCE, walk 2026-10-05). An empty\n * list is a choice (every field shown); absent means the card never chose.\n */\n cardHidden?: string[];\n /**\n * THE ORDER A PERSON DRAGGED THE COLUMNS INTO, by Field key (merge tranche 6a). Sparse like\n * `widths`: a key it does not name keeps its Field's own place after the named ones, and a key\n * whose Field is gone is skipped — a column order can never hide or invent a column.\n */\n columnOrder?: string[];\n /**\n * The gallery's card size, remembered the same way the grid remembers its\n * row height — a per-view look, not a global default.\n */\n gallerySize?: \"small\" | \"medium\" | \"large\";\n /**\n * THIS VIEW'S OWN LAYOUT CHOICES, over the organization's `custom/grid_layout` knob (lane\n * DEFAULT-GRID-PARITY, TABLE-PARITY D1 D5 D6). Absent means \"whatever the organization\n * says\"; present is the view's word. `freezeFirst: false` is an explicit NO over an\n * organization that freezes the first column — an empty `frozen` cannot say that.\n */\n wrap?: boolean;\n /** NOTION'S \"WRAP COLUMN\" (N5): the columns that wrap their text when the view as a whole does not. */\n wrapColumns?: string[];\n fit?: \"auto\" | \"fit\" | \"scroll\";\n freezeFirst?: boolean;\n /**\n * THE SUMMARY BAR (TABLE-PARITY F1): Field key → the store measure shown under that column\n * (`count`, `sum`, `avg`, `min`, `max`, `median`, `filled`, `empty`, `unique`), worked out\n * by `custom.record_aggregate` over the whole table.\n */\n summaries?: Record<string, string>;\n /**\n * WHERE THE FOOTER SITS (lane TABLE-PAGE-CHROME, owner 2026-09-25) — the row count and the\n * pages: `sticky`, pinned to the bottom of the screen while the rows scroll (the older grid's\n * way, and the default when a view says nothing), or `inline`, right after the last row with\n * the page scrolling. Read by the Grid and a host's Sheet.\n */\n footer?: FooterPlacement;\n};\n\n/** Where a grid's footer sits. */\nexport type FooterPlacement = \"sticky\" | \"inline\";\n\n/** What a view that says nothing gets — the older grid's pinned footer. */\nexport const DEFAULT_FOOTER: FooterPlacement = \"sticky\";\n\n/** Where THIS view's footer sits, said or defaulted. */\nexport function footerOf(p: GridPresentation | null | undefined): FooterPlacement {\n return p?.footer ?? DEFAULT_FOOTER;\n}\n\nexport const EMPTY_PRESENTATION: GridPresentation = {};\n\n/** A frozen column needs a known width, or the table declines to freeze at all. */\nexport const FROZEN_COLUMN_WIDTH = 200;\n\n/**\n * WHAT A ROW HEIGHT IS ALLOWED TO BE. Below the floor the text is clipped and\n * no editor fits; above the ceiling one record fills the screen. A view asking\n * for anything outside it is DROPPED rather than obeyed, because a saved look\n * must not be able to make a table unreadable — and the Fields' own heights\n * still draw, which is the tolerant half.\n */\nexport const MIN_ROW_HEIGHT = 24;\nexport const MAX_ROW_HEIGHT = 96;\n\n/**\n * The aggregate kinds and orders the primitive actually implements. Listed here\n * because they are the tolerant parser's allow-list and a view is hostile input:\n * `satisfies` ties them to the primitive's own union, so a kind it renames stops\n * this file compiling instead of quietly parsing into a subtotal nobody draws.\n */\nconst AGGREGATE_KINDS = [\n \"count\",\n \"sum\",\n \"average\",\n \"min\",\n \"max\",\n \"empty\",\n \"filled\",\n \"unique\",\n \"checked\",\n \"unchecked\",\n \"percentFilled\",\n] as const satisfies readonly GroupAggregateKind[];\n\nconst GROUP_ORDERS = [\n \"value-asc\",\n \"value-desc\",\n \"count-desc\",\n \"first-seen\",\n] as const satisfies readonly GroupOrder[];\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\n/**\n * Read a presentation out of whatever the view record holds — an object, or the\n * JSON string a text field stores.\n *\n * Tolerant in exactly the way `parseTableStyle` is: an unreadable half is\n * dropped and the rest still applies, because a table must keep drawing when\n * one piece of its styling goes out of date.\n */\nexport function parseGridPresentation(raw: unknown): GridPresentation {\n let value = raw;\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (trimmed === \"\") return {};\n try {\n value = JSON.parse(trimmed);\n } catch {\n return {};\n }\n }\n if (!isRecord(value)) return {};\n\n const out: GridPresentation = {};\n\n const style = parseTableStyle(value[\"style\"]);\n if (!tableStyleIsEmpty(style)) out.style = style;\n\n if (isRecord(value[\"formats\"])) {\n const formats: Record<string, FieldFormatConfig> = {};\n for (const [key, cfg] of Object.entries(value[\"formats\"])) {\n if (!isRecord(cfg)) continue;\n const id = cfg[\"id\"];\n if (typeof id !== \"string\") continue;\n const options = cfg[\"options\"];\n formats[key] = isRecord(options)\n ? {\n id: id as FieldFormatConfig[\"id\"],\n options: options as NonNullable<FieldFormatConfig[\"options\"]>,\n }\n : { id: id as FieldFormatConfig[\"id\"] };\n }\n if (Object.keys(formats).length > 0) out.formats = formats;\n }\n\n if (Array.isArray(value[\"frozen\"])) {\n const frozen = value[\"frozen\"].filter((k): k is string => typeof k === \"string\");\n if (frozen.length > 0) out.frozen = frozen;\n }\n\n const grouping = parseGrouping(value[\"grouping\"]);\n if (grouping) out.grouping = grouping;\n\n if (isRecord(value[\"widths\"])) {\n const widths: Record<string, number> = {};\n for (const [key, raw] of Object.entries(value[\"widths\"])) {\n // A width that is not a usable number is one column's loss, never the\n // view's: the rest of the widths still apply and that column is measured.\n if (typeof raw !== \"number\" || !Number.isFinite(raw) || raw <= 0) continue;\n widths[key] = Math.round(raw);\n }\n if (Object.keys(widths).length > 0) out.widths = widths;\n }\n\n const rowHeight = value[\"rowHeight\"];\n if (\n typeof rowHeight === \"number\" &&\n Number.isFinite(rowHeight) &&\n rowHeight >= MIN_ROW_HEIGHT &&\n rowHeight <= MAX_ROW_HEIGHT\n ) {\n out.rowHeight = Math.round(rowHeight);\n }\n\n if (Array.isArray(value[\"hiddenFields\"])) {\n const hiddenFields = value[\"hiddenFields\"].filter((k): k is string => typeof k === \"string\");\n if (hiddenFields.length > 0) out.hiddenFields = hiddenFields;\n }\n if (Array.isArray(value[\"cardHidden\"])) {\n out.cardHidden = value[\"cardHidden\"].filter((k): k is string => typeof k === \"string\");\n }\n\n if (Array.isArray(value[\"columnOrder\"])) {\n const columnOrder = value[\"columnOrder\"].filter((k): k is string => typeof k === \"string\");\n if (columnOrder.length > 0) out.columnOrder = columnOrder;\n }\n\n const gallerySize = value[\"gallerySize\"];\n if (gallerySize === \"small\" || gallerySize === \"medium\" || gallerySize === \"large\") {\n out.gallerySize = gallerySize;\n }\n\n if (typeof value[\"wrap\"] === \"boolean\") out.wrap = value[\"wrap\"];\n if (Array.isArray(value[\"wrapColumns\"])) {\n const wrapColumns = value[\"wrapColumns\"].filter((k): k is string => typeof k === \"string\");\n if (wrapColumns.length > 0) out.wrapColumns = wrapColumns;\n }\n const fit = value[\"fit\"];\n if (fit === \"auto\" || fit === \"fit\" || fit === \"scroll\") out.fit = fit;\n if (typeof value[\"freezeFirst\"] === \"boolean\") out.freezeFirst = value[\"freezeFirst\"];\n const footer = value[\"footer\"];\n if (footer === \"sticky\" || footer === \"inline\") out.footer = footer;\n if (isRecord(value[\"summaries\"])) {\n const summaries: Record<string, string> = {};\n for (const [key, op] of Object.entries(value[\"summaries\"])) {\n // A measure name is judged by the store when it is asked; a non-string is nobody's.\n if (typeof op === \"string\" && op.trim() !== \"\") summaries[key] = op;\n }\n if (Object.keys(summaries).length > 0) out.summaries = summaries;\n }\n\n return out;\n}\n\n/**\n * A grouping, or nothing. A grouping with no Field to group BY is not a partial\n * grouping — it is no grouping at all, so it is dropped whole; an unreadable\n * subtotal or a section order this primitive does not have costs only itself.\n */\nfunction parseGrouping(raw: unknown): GridGrouping | undefined {\n if (!isRecord(raw)) return undefined;\n const field = raw[\"field\"];\n if (typeof field !== \"string\" || field.trim() === \"\") return undefined;\n\n const out: GridGrouping = { field };\n\n if (isRecord(raw[\"aggregates\"])) {\n const aggregates: Record<string, GroupAggregateSpec> = {};\n for (const [key, spec] of Object.entries(raw[\"aggregates\"])) {\n if (!isRecord(spec)) continue;\n const kind = spec[\"kind\"];\n if (typeof kind !== \"string\") continue;\n if (!(AGGREGATE_KINDS as readonly string[]).includes(kind)) continue;\n const label = spec[\"label\"];\n aggregates[key] =\n typeof label === \"string\"\n ? { kind: kind as GroupAggregateKind, label }\n : { kind: kind as GroupAggregateKind };\n }\n if (Object.keys(aggregates).length > 0) out.aggregates = aggregates;\n }\n\n const order = raw[\"order\"];\n if (typeof order === \"string\" && (GROUP_ORDERS as readonly string[]).includes(order)) {\n out.order = order as GroupOrder;\n }\n\n if (Array.isArray(raw[\"collapsed\"])) {\n const collapsed = raw[\"collapsed\"].filter((k): k is string => typeof k === \"string\");\n if (collapsed.length > 0) out.collapsed = collapsed;\n }\n\n if (raw[\"showColumn\"] === true) out.showColumn = true;\n\n if (Array.isArray(raw[\"then\"])) {\n const then: GridSubGroup[] = [];\n for (const level of raw[\"then\"]) {\n if (!isRecord(level)) continue;\n const sub = level[\"field\"];\n if (typeof sub !== \"string\" || sub.trim() === \"\") continue;\n const subOrder = level[\"order\"];\n const subGrain = level[\"grain\"];\n then.push({\n field: sub,\n ...(typeof subOrder === \"string\" && (GROUP_ORDERS as readonly string[]).includes(subOrder)\n ? { order: subOrder as GroupOrder }\n : {}),\n ...(typeof subGrain === \"string\" && subGrain !== \"\" ? { grain: subGrain } : {}),\n });\n }\n if (then.length > 0) out.then = then.slice(0, 2);\n }\n\n const grain = raw[\"grain\"];\n if (typeof grain === \"string\" && grain !== \"\") out.grain = grain;\n const across = raw[\"across\"];\n if (typeof across === \"string\" && across.trim() !== \"\") out.across = across;\n if (Array.isArray(raw[\"show\"])) {\n const show = raw[\"show\"].filter((k): k is string => typeof k === \"string\" && k !== \"\");\n if (show.length > 0) out.show = show;\n }\n if (Array.isArray(raw[\"trail\"])) {\n const trail: GridDrillStep[] = [];\n for (const step of raw[\"trail\"]) {\n if (!isRecord(step) || typeof step[\"dim\"] !== \"string\" || step[\"dim\"] === \"\") continue;\n const value = step[\"value\"];\n if (value !== null && typeof value !== \"string\") continue;\n trail.push({ dim: step[\"dim\"], value });\n }\n if (trail.length > 0) out.trail = trail;\n }\n\n return out;\n}\n\n/** True when nothing here would change how anything looks. */\nexport function presentationIsEmpty(p: GridPresentation): boolean {\n return (\n (p.style === undefined || tableStyleIsEmpty(p.style)) &&\n Object.keys(p.formats ?? {}).length === 0 &&\n (p.frozen?.length ?? 0) === 0 &&\n p.grouping === undefined &&\n Object.keys(p.widths ?? {}).length === 0 &&\n p.rowHeight === undefined &&\n (p.hiddenFields?.length ?? 0) === 0 &&\n p.cardHidden === undefined &&\n (p.columnOrder?.length ?? 0) === 0 &&\n p.gallerySize === undefined &&\n p.wrap === undefined &&\n p.fit === undefined &&\n p.freezeFirst === undefined &&\n p.footer === undefined &&\n Object.keys(p.summaries ?? {}).length === 0\n );\n}\n\n/** The string a view record stores. Empty string for \"this view styles nothing\". */\nexport function presentationDocument(p: GridPresentation): string {\n return presentationIsEmpty(p) ? \"\" : JSON.stringify(p);\n}\n\n/** Kinds whose value is a control the record renderer draws, never a formatter's string. */\nconst DRAWN_KINDS = new Set([\"select\", \"multi_select\", \"relation\", \"member\", \"person\", \"attachment\", \"file\", \"entity_reference\", \"checkbox\", \"boolean\", \"signature\", \"json\"]);\n\n/**\n * The display format a column's settings chose for it, when it is one the platform registry\n * knows and the column is not one the record renderer draws itself. `undefined` otherwise.\n */\nexport function shownAsOf(field: Field): FieldFormatConfig | undefined {\n if (DRAWN_KINDS.has(String(editorKindFor(field)))) return undefined;\n const raw = (field as unknown as { display_format?: unknown }).display_format;\n if (!raw || typeof raw !== \"object\") return undefined;\n const id = (raw as { id?: unknown }).id;\n // `formula` is the column's KIND, never a look: a worked-out column shows as its result format.\n if (typeof id !== \"string\" || id === \"formula\" || !getFieldFormat(id)) return undefined;\n const options = (raw as { options?: unknown }).options;\n return options && typeof options === \"object\" && Object.keys(options).length > 0\n ? ({ id, options } as FieldFormatConfig)\n : ({ id } as FieldFormatConfig);\n}\n\n/**\n * The format a Field's OWN declaration implies, before any view says anything.\n *\n * Returns `undefined` for a Field whose value the record renderer already draws\n * better than a formatter could — a relation chip, a person, an attachment, a\n * choice — because handing those to a text formatter would REPLACE a working\n * control with a string.\n */\nexport function formatForField(field: Field): FieldFormatConfig | undefined {\n // \"SHOWS AS\" — THE FORMAT THE COLUMN'S OWN SETTINGS CHOSE (merge tranche 6a/6e). The column\n // keeps it on the Field (`display_format`, written by `custom.field_update`), so the grid, the\n // record panel and the export read one answer. A column the record renderer draws better than\n // a formatter (a choice, a link, a person, a file) keeps its own drawing.\n const shownAs = shownAsOf(field);\n // A RATING'S TOP STAR IS ITS MAX RULE (lane 10 P4), so changing the rule changes the stars.\n if (newerKindOf(field) === \"rating\" && (!shownAs || shownAs.id === \"rating\")) {\n return { id: \"rating\", options: { ...(shownAs?.options ?? {}), ratingMax: ratingMaxOf(field) } };\n }\n if (shownAs) return shownAs;\n // THE STORE'S OWN STAMPS (Created time, Last changed time) are instants and print like\n // every other date-and-time cell — `dates.ts` answers, the same helper every card reads.\n const stamp = dateFormatFor(field);\n if (stamp && editorKindFor(field) !== \"datetime\") return stamp;\n switch (editorKindFor(field)) {\n case \"currency\":\n return {\n id: \"currency\",\n options: { currency: currencyCodeFor(field.unit) },\n };\n case \"percent\":\n return { id: \"percent\" };\n case \"datetime\":\n // The COLUMN's declaration: a `kind: \"date\"` column is a date column, anything else can\n // hold a moment. Each VALUE is still printed in its own shape (`RecordValue` →\n // `dates.dateFormatFor(field, value)`), so a bare date in a datetime column reads as a date.\n return String(field.config?.[\"kind\"] ?? \"\") === \"date\" ? { id: \"date\" } : { id: \"datetime\" };\n case \"email\":\n return { id: \"email\" };\n case \"url\":\n return { id: \"url\" };\n case \"phone\":\n return { id: \"phone\" };\n case \"number\":\n return { id: \"number\" };\n default:\n return undefined;\n }\n}\n\n/**\n * THE FORMAT ONE CELL OF A COLUMN IS DRAWN IN — the grid's cell and the gallery's card alike\n * (guard G4 \"one format\", lane VIEWS-AND-FIELDS W1-B): the view's own format for the column when\n * it set one, else the Field's own (`formatForField`). One function, so no layout formats money\n * (or a date, or a percent) its own way.\n */\nexport function cellFormatFor(field: Field, viewFormat?: FieldFormatConfig | undefined): FieldFormatConfig | undefined {\n return viewFormat ?? formatForField(field);\n}\n\n/**\n * The store lets a person type the SYMBOL they think in (`$`, `€`), and\n * `Intl.NumberFormat` wants the ISO code. The symbols people actually enter are\n * mapped; anything else is passed straight through, because a real ISO code is\n * the other thing that lands in this field, and an unknown code still renders\n * (`1,234.00 XYZ`) rather than blanking — THE FALLBACK LAW.\n */\nconst SYMBOL_TO_CODE: Record<string, string> = {\n $: \"USD\",\n \"£\": \"GBP\",\n \"€\": \"EUR\",\n \"¥\": \"JPY\",\n \"₹\": \"INR\",\n \"₽\": \"RUB\",\n \"₩\": \"KRW\",\n \"R$\": \"BRL\",\n \"C$\": \"CAD\",\n \"A$\": \"AUD\",\n};\n\nexport function currencyCodeFor(unit: string | null | undefined): string {\n const raw = (unit ?? \"\").trim();\n if (raw === \"\") return \"USD\";\n return SYMBOL_TO_CODE[raw] ?? raw.toUpperCase();\n}\n\n/**\n * The color a value paints with when a view colors BY a column.\n *\n * NOTHING FAILS SILENTLY, so this never returns \"no color\" for a value that\n * exists. The store keeps a list column's choices as records of another Table,\n * and this grid has not read that Table's option colors, so the color is\n * derived from the value's own text: the same word always gets the same color,\n * different words get different ones, and `true` is green the way every\n * checklist reads. When those option colors do land, this is the ONE function\n * that changes and every screen follows.\n */\nexport const colorFromTheValue: ChoiceColorLookup = (_fieldKey, value) => {\n if (value === true) return \"green\";\n if (value === false || value === null || value === undefined) return undefined;\n const text = String(value).trim().toLowerCase();\n if (text === \"\") return undefined;\n let hash = 0;\n for (let i = 0; i < text.length; i += 1) {\n hash = (hash * 31 + text.charCodeAt(i)) >>> 0;\n }\n return COLOR_ROTATION[hash % COLOR_ROTATION.length] as StyleColor;\n};\n\n/**\n * G1's TABLE-LEVEL COLORS (`custom.table_decorations` / `client.tableDecorations`),\n * TRANSLATED INTO A `TableStyle` — the same document a saved view's own `presentation.style`\n * is, so one resolver (`resolveTableStyle`, below) can treat \"the table's colors\" and \"this\n * view's colors\" as two layers of the SAME thing instead of two unrelated systems.\n *\n * THE TRANSLATION IS NECESSARY AND IT IS THE ONLY ONE. G1 addresses a Field by its ID\n * (REC-17 — a column renamed keeps its color), because it is the STORE's own door; a\n * `TableStyle` addresses a Field by its KEY, because it reads straight off `row.document`,\n * which is keyed by Field key. A rule or a color-by whose Field has been deleted (its id no\n * longer resolves to a key) is dropped rather than crashing the whole style — a defect in\n * one rule costs that rule, never the read.\n */\nexport function styleFromDecorations(\n decorations: TableDecorations | null | undefined,\n fields: readonly Field[],\n): TableStyle {\n if (!decorations) return { version: 1 };\n const keyOf = (id: string) => fields.find((f) => f.id === id)?.key;\n const style: TableStyle = { version: 1 };\n\n if (decorations.color_by?.field) {\n const field = keyOf(decorations.color_by.field);\n if (field) style.colorBy = { field, target: decorations.color_by.target };\n }\n\n if (Array.isArray(decorations.rules) && decorations.rules.length > 0) {\n const rules: ColorRule[] = [];\n decorations.rules.forEach((rule, index) => {\n const field = keyOf(rule.field);\n if (!field) return;\n rules.push({\n id: rule.id ?? `decoration:${index}`,\n field,\n op: rule.op,\n color: rule.color,\n target: rule.target,\n ...(rule.value === undefined || rule.value === null ? {} : { value: String(rule.value) }),\n });\n });\n if (rules.length > 0) style.rules = rules;\n }\n\n if (decorations.rows && Object.keys(decorations.rows).length > 0) {\n style.rows = { ...decorations.rows };\n }\n\n const columns: Record<string, StyleColor> = {};\n for (const [id, color] of Object.entries(decorations.columns ?? {})) {\n const key = keyOf(id);\n if (key) columns[key] = color;\n }\n if (Object.keys(columns).length > 0) style.columns = columns;\n\n const cells: Record<string, Record<string, StyleColor>> = {};\n for (const [rowId, byField] of Object.entries(decorations.cells ?? {})) {\n for (const [id, color] of Object.entries(byField ?? {})) {\n const key = keyOf(id);\n if (!key) continue;\n (cells[rowId] ??= {})[key] = color;\n }\n }\n if (Object.keys(cells).length > 0) style.cells = cells;\n\n return style;\n}\n\n/**\n * THE ONE PRESENTATION RESOLVER every colored surface in this package reads —\n * `Grid`, and (via `Card`) the kanban board, the calendar and the gallery.\n *\n * THE CLASS THIS CLOSES (lane UI-QUICK-1, TABLE-PARITY review, 2026-09-24). Threading\n * `view.presentation.style` into `Card` closed the instance the CALENDAR-GALLERY bench\n * found — a VIEW that had its own color-by looked right everywhere — but it left the\n * class open: a table whose colors are the ORGANIZATION's decorations (G1's\n * `custom.table_decorations` — \"color rows by Account Type\", \"MAXED and NEAR rules\" on\n * Arman's own Coding Accounts table) and whose views never set a `presentation.style` of\n * their own drew NOTHING, on the grid exactly as much as on every other layout, because\n * nothing in this package had ever read G1's door at all.\n *\n * TWO LAYERS, ONE DOCUMENT. The table's decorations are the base coat — what every view\n * of this table shows until a view says otherwise. A view's OWN `presentation.style` is\n * an override, piece by piece: a view that only ever set manual row highlights still\n * inherits the table's color-by and rules for everything it never touched, and a view\n * that fully redecorated (its own `colorBy` AND its own `rules`) is not haunted by a\n * table rule it meant to replace. Per-piece, never all-or-nothing, because a view is a\n * LOOK on top of the table, not a wholesale substitute for it.\n */\nexport function resolveTableStyle(\n decorations: TableDecorations | null | undefined,\n fields: readonly Field[],\n viewStyle: TableStyle | undefined,\n): TableStyle {\n const base = styleFromDecorations(decorations, fields);\n if (!viewStyle) return base;\n const colorBy = viewStyle.colorBy ?? base.colorBy;\n const rules = viewStyle.rules ?? base.rules;\n const rows = viewStyle.rows ?? base.rows;\n const columns = viewStyle.columns ?? base.columns;\n const cells = viewStyle.cells ?? base.cells;\n return {\n version: 1,\n ...(colorBy !== undefined ? { colorBy } : {}),\n ...(rules !== undefined ? { rules } : {}),\n ...(rows !== undefined ? { rows } : {}),\n ...(columns !== undefined ? { columns } : {}),\n ...(cells !== undefined ? { cells } : {}),\n };\n}\n","\"use client\";\n\n// src/viewKeys.ts — @ai-matrx/records-ui · lane S1-PRIME VIEW-KEYS (UI-CHAMPIONS-PLAN rev 2)\n//\n// EVERY PER-VIEW SETTING IS A DECLARED KEY OF THE ONE SAVED VIEW.\n//\n// A saved view is one row of `platform.saved_view` (lane S0). What it may hold is decided in ONE\n// place: `custom.view_keys()`, the registry `custom.view_declare` judges every save against. This\n// file is that registry's client half and the ONE accessor every layout reads a view through:\n//\n// - `VIEW_KEYS` mirrors `custom.view_keys()` row for row (path, shape, the layouts that read it,\n// who may write it). The live walk compares the two through REST, as test@test.com, so a key\n// added on one side and not the other fails a walk, never a person.\n// - `viewSettings(definition)` is THE reader: a stored definition in, every declared key out,\n// typed. `viewFromRow` (views.ts) goes through it, so the Grid, the board, the calendar,\n// and the gallery all see a view the same way.\n// - `viewSettingsFor(view, layout)` hands a layout only the keys it reads; a layout that does\n// not use a key never sees it (the calendar is never told about swimlanes).\n// - `viewSettingsPatch(patch)` is THE writer's shape: only the keys a press changed, a cleared\n// key as null (the door removes it). `saveViewPatch` sends it through `custom.view_declare`.\n//\n// Fields are named by KEY (a Field's key is immutable; every reader draws rows by key). The door\n// accepts an id and stores the key, so an agent may send either.\n\nimport type { RecordFilter, RuleExpression } from \"@ai-matrx/records\";\nimport { parseGridPresentation, presentationDocument, presentationIsEmpty, type GridPresentation } from \"./presentation\";\n\n/** Every surface a saved view is drawn by. */\nexport type ViewKeyLayout = \"grid\" | \"kanban\" | \"calendar\" | \"gallery\" | \"timeline\" | \"list\";\n\n/**\n * 🚩 THE ONE SWITCH FOR THE TIMELINE'S STORE KEYS (lane VIEWS-AND-FIELDS, sublane TL).\n *\n * `vk_a_calendar_keeps_where_a_record_ends_and_a_timeline_is_a_view.sql` (sublane VK) teaches\n * `custom.view_keys()` the layout `timeline`, the keys `start_field` and `end_field` (calendar +\n * timeline), and `group_field` for the timeline. Until that migration is on production this stays\n * `false`, and:\n * · `timeline` is not a layout the store keeps (`STORE_KEPT_LAYOUTS`), so it is drawn for the visit;\n * · `start_field` / `end_field` are held for the visit, never sent (`viewSettingsPatch`), and the\n * Start also rides `date_field`, which the store already keeps;\n * · the timeline's toolbar says \"Kept for this visit only\".\n * Flipped to `true`, VIEW_KEYS below mirrors the migrated registry row for row (the live walk\n * compares them) and every one of those is kept on the view.\n */\nexport const STORE_KEEPS_TIMELINE = true;\n\n/** Who may write a key: anyone who may save the view, an input the door rewrites, or the server only. */\nexport type ViewKeyWriter = \"caller\" | \"input\" | \"server\";\n\nexport interface ViewKeyDeclaration {\n /** Where it lives in `platform.saved_view.definition` (dotted under `presentation`). */\n path: string;\n /** What the store's guard checks (`custom._view_key_value`). */\n shape: string;\n /** The layouts that read it. */\n layouts: readonly ViewKeyLayout[];\n writer: ViewKeyWriter;\n}\n\nconst ALL: readonly ViewKeyLayout[] = [\"grid\", \"kanban\", \"calendar\", \"gallery\", ...(STORE_KEEPS_TIMELINE ? ([\"timeline\"] as const) : []), \"list\"];\n/** The layouts whose cards draw the fields a view leaves shown (`presentation.hiddenFields`). */\nconst HIDES: readonly ViewKeyLayout[] = [\"grid\", \"gallery\", \"kanban\", \"calendar\", ...(STORE_KEEPS_TIMELINE ? ([\"timeline\"] as const) : []), \"list\"];\nconst GRIDS: readonly ViewKeyLayout[] = [\"grid\"];\n\n/**\n * THE REGISTRY, as `custom.view_keys()` declares it (uichamp_s1_a_view_keeps_every_setting_it_is_given.sql).\n * Order and content are compared with the store by the live walk.\n */\nexport const VIEW_KEYS: readonly ViewKeyDeclaration[] = [\n { path: \"layout\", shape: \"kind\", layouts: ALL, writer: \"caller\" },\n { path: \"filters\", shape: \"filter\", layouts: ALL, writer: \"caller\" },\n { path: \"where\", shape: \"rule\", layouts: ALL, writer: \"caller\" },\n { path: \"group_field\", shape: \"field\", layouts: [\"kanban\", ...(STORE_KEEPS_TIMELINE ? ([\"timeline\"] as const) : [])], writer: \"caller\" },\n { path: \"swimlane_field\", shape: \"field:lane\", layouts: [\"kanban\"], writer: \"caller\" },\n { path: \"collapsed_columns\", shape: \"values\", layouts: [\"kanban\"], writer: \"caller\" },\n { path: \"measure\", shape: \"field:number\", layouts: [\"kanban\"], writer: \"caller\" },\n { path: \"date_field\", shape: \"field:date\", layouts: [\"calendar\"], writer: \"caller\" },\n ...(STORE_KEEPS_TIMELINE\n ? ([\n { path: \"start_field\", shape: \"field:date\", layouts: [\"timeline\"], writer: \"caller\" },\n { path: \"end_field\", shape: \"field:date\", layouts: [\"calendar\", \"timeline\"], writer: \"caller\" },\n ] as const)\n : []),\n { path: \"image_field\", shape: \"field\", layouts: [\"gallery\"], writer: \"caller\" },\n { path: \"sorts\", shape: \"sorts\", layouts: ALL, writer: \"caller\" },\n { path: \"rule_id\", shape: \"uuid\", layouts: ALL, writer: \"caller\" },\n { path: \"is_default\", shape: \"boolean\", layouts: ALL, writer: \"caller\" },\n { path: \"presentation\", shape: \"presentation\", layouts: ALL, writer: \"caller\" },\n { path: \"presentation.style\", shape: \"style\", layouts: ALL, writer: \"caller\" },\n { path: \"presentation.formats\", shape: \"object\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.frozen\", shape: \"fields\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.grouping\", shape: \"grouping\", layouts: [\"grid\", \"gallery\", \"list\"], writer: \"caller\" },\n { path: \"presentation.widths\", shape: \"widths\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.rowHeight\", shape: \"row_height\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.hiddenFields\", shape: \"fields\", layouts: HIDES, writer: \"caller\" },\n { path: \"presentation.cardHidden\", shape: \"fields\", layouts: [\"kanban\", \"calendar\", ...(STORE_KEEPS_TIMELINE ? ([\"timeline\"] as const) : []), \"list\", \"gallery\"], writer: \"caller\" },\n { path: \"presentation.columnOrder\", shape: \"fields\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.gallerySize\", shape: \"gallery_size\", layouts: [\"gallery\"], writer: \"caller\" },\n { path: \"presentation.wrap\", shape: \"boolean\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.wrapColumns\", shape: \"fields\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.fit\", shape: \"fit\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.freezeFirst\", shape: \"boolean\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.footer\", shape: \"footer\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.summaries\", shape: \"summaries\", layouts: GRIDS, writer: \"caller\" },\n { path: \"grid\", shape: \"grid\", layouts: GRIDS, writer: \"caller\" },\n { path: \"hidden_fields\", shape: \"input\", layouts: HIDES, writer: \"input\" },\n { path: \"table_id\", shape: \"server\", layouts: ALL, writer: \"server\" },\n { path: \"order\", shape: \"server\", layouts: GRIDS, writer: \"server\" },\n { path: \"moved_from\", shape: \"server\", layouts: ALL, writer: \"server\" },\n];\n\n/**\n * THE LAYOUTS THE STORE KEEPS ON A VIEW. `custom.view_keys()` judges a view's `layout` against its\n * own list (grid, kanban, calendar, gallery) and refuses any other word by name. A layout this\n * package draws before the store keeps it — the timeline, 2026-10-02 — is never sent as a view's or a\n * look's layout (`viewSettingsPatch`, `viewDocument`): it is drawn for this visit and says so. When the\n * store's registry admits it, add it here and nothing else changes.\n */\nexport const STORE_KEPT_LAYOUTS: readonly string[] = [\"grid\", \"kanban\", \"calendar\", \"gallery\", ...(STORE_KEEPS_TIMELINE ? [\"timeline\"] : []), \"list\"];\n\n/** Whether the store keeps this layout on a saved view. */\nexport function storeKeepsLayout(layout: string): boolean {\n return STORE_KEPT_LAYOUTS.includes(layout);\n}\n\n/** The keys one layout reads, by path. */\nexport function viewKeysFor(layout: ViewKeyLayout): string[] {\n return VIEW_KEYS.filter((k) => k.layouts.includes(layout)).map((k) => k.path);\n}\n\n/** One sort clause: a Field key and a direction. */\nexport interface ViewSortKey {\n field: string;\n direction: \"asc\" | \"desc\";\n}\n\n/**\n * EVERY DECLARED KEY OF A VIEW, TYPED. Absent keys are `null` / empty, never guessed.\n * `presentation` is the parsed look (colour field, pinned fields, row height, grouping with its\n * sub-groups and shut sections, hidden fields, gallery size, summaries).\n */\nexport interface ViewSettings {\n layout: \"grid\" | \"kanban\" | \"calendar\" | \"gallery\" | \"timeline\" | \"list\";\n filters: RecordFilter | null;\n where: RuleExpression | null;\n groupField: string | null;\n swimlaneField: string | null;\n collapsedColumns: string[];\n measure: string | null;\n dateField: string | null;\n /** Timeline: where a record's bar starts (`start_field`). */\n startField: string | null;\n /** Calendar and timeline: where a record ends (`end_field`). */\n endField: string | null;\n imageField: string | null;\n sorts: ViewSortKey[];\n ruleId: string | null;\n isDefault: boolean;\n presentation: GridPresentation;\n movedFrom: Record<string, unknown> | null;\n}\n\n// The layouts a stored view reads back as; one the store does not keep yet reads as a grid. A\n// saved timeline read back as \"grid\" was the reload that lost it (FDT, 2026-10-03).\nconst KINDS: readonly string[] = STORE_KEPT_LAYOUTS;\n\n/** THE READER. A stored `definition` (and the row's flat `filters`) in, every declared key out. */\nexport function viewSettings(definition: Record<string, unknown> | null | undefined, rowFilters?: RecordFilter | null): ViewSettings {\n const data = definition ?? {};\n const text = (key: string): string | null => {\n const v = data[key];\n return typeof v === \"string\" && v !== \"\" ? v : null;\n };\n const layout = String(data[\"layout\"] ?? \"grid\");\n const flat = rowFilters ?? (isObject(data[\"filters\"]) ? (data[\"filters\"] as RecordFilter) : null);\n const moved = data[\"moved_from\"];\n return {\n layout: (KINDS as readonly string[]).includes(layout) ? (layout as ViewSettings[\"layout\"]) : \"grid\",\n filters: flat && Object.keys(flat).length > 0 ? flat : null,\n where: isRule(data[\"where\"]) ? (data[\"where\"] as RuleExpression) : null,\n groupField: text(\"group_field\"),\n swimlaneField: text(\"swimlane_field\"),\n collapsedColumns: Array.isArray(data[\"collapsed_columns\"])\n ? (data[\"collapsed_columns\"] as unknown[]).filter((v) => [\"string\", \"number\", \"boolean\"].includes(typeof v)).map(String)\n : [],\n measure: text(\"measure\"),\n dateField: text(\"date_field\"),\n startField: text(\"start_field\"),\n endField: text(\"end_field\"),\n imageField: text(\"image_field\"),\n sorts: parseSorts(data[\"sorts\"]),\n ruleId: text(\"rule_id\"),\n isDefault: data[\"is_default\"] === true || data[\"is_default\"] === \"true\",\n presentation: parseGridPresentation(data[\"presentation\"]),\n movedFrom: isObject(moved) ? (moved as Record<string, unknown>) : null,\n };\n}\n\n/** The settings as the spec names them — what `viewSettingsPatch` takes. */\nexport type ViewSettingsPatch = Partial<Omit<ViewSettings, \"movedFrom\" | \"layout\">> & {\n /** Any layout the package draws; one the store does not keep is not sent. */\n layout?: string;\n};\n\n/**\n * THE KEYS ONE LAYOUT READS, and nothing else. A layout that does not use a key never sees it:\n * a calendar is not told the board's swimlanes, a board is not told the grid's pinned fields.\n */\nexport function viewSettingsFor(settings: ViewSettings, layout: ViewKeyLayout): Partial<ViewSettings> {\n const reads = new Set(viewKeysFor(layout));\n const out: Partial<ViewSettings> = { layout: settings.layout };\n const take = <K extends keyof ViewSettings>(key: K, path: string) => {\n if (reads.has(path)) out[key] = settings[key];\n };\n take(\"filters\", \"filters\");\n take(\"where\", \"where\");\n take(\"groupField\", \"group_field\");\n take(\"swimlaneField\", \"swimlane_field\");\n take(\"collapsedColumns\", \"collapsed_columns\");\n take(\"measure\", \"measure\");\n take(\"dateField\", \"date_field\");\n take(\"startField\", \"start_field\");\n take(\"endField\", \"end_field\");\n take(\"imageField\", \"image_field\");\n take(\"sorts\", \"sorts\");\n take(\"ruleId\", \"rule_id\");\n take(\"isDefault\", \"is_default\");\n const p = settings.presentation;\n const look: GridPresentation = {};\n for (const [key, value] of Object.entries(p)) {\n if (reads.has(`presentation.${key}`)) (look as Record<string, unknown>)[key] = value;\n }\n out.presentation = look;\n return out;\n}\n\n/**\n * THE WRITER'S SHAPE. Only the keys a press changed; a cleared Field is `null` (the door removes\n * it); lists are sent whole. `presentation` is sent whole because the door judges it as one key.\n */\nexport function viewSettingsPatch(patch: ViewSettingsPatch): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n const field = (key: keyof ViewSettingsPatch, path: string) => {\n if (patch[key] !== undefined) out[path] = (patch[key] as string | null) || null;\n };\n // A layout the store does not keep yet is held for this visit, never sent (`STORE_KEPT_LAYOUTS`).\n if (patch.layout !== undefined && storeKeepsLayout(patch.layout)) out[\"layout\"] = patch.layout;\n field(\"groupField\", \"group_field\");\n field(\"swimlaneField\", \"swimlane_field\");\n field(\"measure\", \"measure\");\n field(\"dateField\", \"date_field\");\n // Held for the visit until the store keeps them (`STORE_KEEPS_TIMELINE`).\n if (STORE_KEEPS_TIMELINE) {\n field(\"startField\", \"start_field\");\n field(\"endField\", \"end_field\");\n }\n field(\"imageField\", \"image_field\");\n field(\"ruleId\", \"rule_id\");\n if (patch.collapsedColumns !== undefined) out[\"collapsed_columns\"] = patch.collapsedColumns ?? [];\n if (patch.sorts !== undefined) out[\"sorts\"] = patch.sorts ?? [];\n if (patch.where !== undefined) out[\"where\"] = patch.where ?? null;\n if (patch.isDefault !== undefined) out[\"is_default\"] = patch.isDefault === true;\n if (patch.presentation !== undefined) {\n out[\"presentation\"] =\n !patch.presentation || presentationIsEmpty(patch.presentation)\n ? null\n : (JSON.parse(presentationDocument(patch.presentation)) as Record<string, unknown>);\n }\n return out;\n}\n\nfunction isObject(raw: unknown): boolean {\n return Boolean(raw) && typeof raw === \"object\" && !Array.isArray(raw);\n}\n\nfunction isRule(raw: unknown): boolean {\n if (!isObject(raw)) return false;\n const node = raw as { op?: unknown; args?: unknown };\n return typeof node.op === \"string\" && Array.isArray(node.args);\n}\n\nfunction parseSorts(raw: unknown): ViewSortKey[] {\n let list: unknown = raw;\n if (typeof raw === \"string\") {\n try {\n list = raw.trim() === \"\" ? [] : JSON.parse(raw);\n } catch {\n return [];\n }\n }\n if (!Array.isArray(list)) return [];\n return list\n .filter((s): s is { field: string; direction?: string } => isObject(s) && typeof (s as { field?: unknown }).field === \"string\")\n .map((s) => ({ field: s.field, direction: s.direction === \"desc\" ? \"desc\" : \"asc\" }));\n}\n","\"use client\";\n\n// src/views.ts — @ai-matrx/records-ui · SCR-6, SCR-7, SCR-N-6\n//\n// A SAVED VIEW IS ONE ROW OF THE ONE SAVED-VIEW STORE, `platform.saved_view`\n// (chair ruling 2026-09-23, lane S0 ONE-SAVED-VIEW), read through `custom.views`\n// and written through `custom.view_declare` — the same door the digests, the\n// notify rules, the agent's boards, the grid's choices (G1/G7), the hand-set\n// order (G13) and the mover use. It owns its layout and REFERENCES its\n// membership rule.\n//\n// 🚨 THERE WERE TWO STORES (UI-CHAMPIONS-PLAN-ATTACK hole 1). Until S0 this bar\n// kept each view as a record of a package-owned `records_ui_view` Table in every\n// organization, beside `platform.saved_view`: a view the mover carried in never\n// reached the bar, a view saved in the bar was invisible to every subscription,\n// and a grid choice G1 kept had no screen that read it. The repair migration\n// `oneview_every_view_bar_view_moves_into_the_one_store.sql` moved every one of\n// those records into `platform.saved_view` UNDER THE SAME ID and archived the\n// Tables; nothing in this package reads or writes them any more.\n//\n// The split matters. A view does not carry a filter predicate of its own that a\n// screen re-implements in JavaScript: it names a Rule, and the STORE answers\n// who is in it (`custom.rule_members`). So the same view means the same thing\n// to a grid, to an export, to an agent and to the server. What the view owns\n// itself is presentation — which of the four layouts, which Field the kanban\n// columns come from, which Field the calendar reads, which Field the gallery\n// shows — because that is not data, it is how this person wants to look at it.\n//\n// The whole view is also a plain declarative object (`SavedViewSpec`), which is\n// what makes \"an agent writes the view in 45 seconds\" true: one `view_declare`\n// of one definition, and the person only tweaks.\n\nimport { presentationDocument, presentationIsEmpty, type GridPresentation } from \"./presentation\";\nimport { STORE_KEEPS_TIMELINE, storeKeepsLayout, viewSettings, viewSettingsPatch } from \"./viewKeys\";\nexport { STORE_KEEPS_TIMELINE, STORE_KEPT_LAYOUTS, storeKeepsLayout } from \"./viewKeys\";\nimport type { RecordFilter, RecordsResult, RuleExpression, SavedViewRow, Uuid } from \"@ai-matrx/records\";\nimport type { RecordsClient } from \"@ai-matrx/records/core\";\n\n/** The ways one saved view can be looked at (SCR-6; the timeline, lane VIEWS-AND-FIELDS TL). */\nexport const VIEW_LAYOUTS = [\"grid\", \"kanban\", \"calendar\", \"gallery\", \"timeline\", \"list\"] as const;\nexport type ViewLayout = (typeof VIEW_LAYOUTS)[number];\n\nexport const LAYOUT_LABEL: Record<ViewLayout, string> = {\n grid: \"Grid\",\n kanban: \"Kanban\",\n calendar: \"Calendar\",\n gallery: \"Gallery\",\n timeline: \"Timeline\",\n list: \"List\",\n};\n\n\n/**\n * ─────────────────────────────────────────────────────────────────────────────\n * WHAT EACH LAYOUT NEEDS FROM THE TABLE, AND HOW THE SCREEN SUPPLIES IT\n * (lane DATA-V2-VIEWS-1, Arman 2026-09-28).\n *\n * THE DEFECT. A plain table opened as a Kanban said \"A kanban board needs a Field to make its\n * columns from … Add column, then Select — and it appears here.\" It told the person what to do\n * instead of doing it, and on an AI platform it offered no AI. The ruling: ONE primitive, used by\n * every view (`ViewNeeds`) — the empty state says in one plain sentence what is missing and what\n * pressing will do, offers \"Make it work\" (the column is declared through the store's\n * `custom.field_declare`, its key minted by the store, its name and choices editable before the\n * press) and \"Ask AI\" (the agent does it through the same door, held for the person's approval by\n * the organization's \"Agent changes to this organization's data\" setting).\n *\n * THE CENSUS — every layout this package draws, and what it reads:\n * · grid nothing.\n * · kanban a CHOICE column (its options become the board's columns). Blocking.\n * · calendar a DATE column (it places each record on a day). Blocking.\n * · gallery a FILE column for the card's picture. NOT blocking: the cards draw without one,\n * so the offer sits above them instead of in place of them.\n * · timeline a DATE column each bar starts on (the calendar's `dateField`). Blocking.\n * `PAGE_VIEWS`' two extra values (dashboards, archived) read\n * nothing a table can lack. This package ships no map layout; the day it does,\n * `Record<ViewLayout, …>` below refuses to compile until the new layout names what it needs.\n */\nexport type ViewNeedKind = \"choice\" | \"date\" | \"file\";\n\nexport interface ViewNeed {\n /** What the layout reads, as a kind a column declares (`offerableFields`). */\n kind: ViewNeedKind;\n /** The view key the made column is written onto, so the layout opens drawn by it. */\n viewKey: \"groupField\" | \"dateField\" | \"imageField\";\n /** True when the layout cannot draw at all without it (kanban, calendar). */\n blocking: boolean;\n /** The column the press makes, before the person edits it: its name, the store's word, its choices. */\n starter: { label: string; type: \"select\" | \"datetime\" | \"attachment\"; options?: readonly string[] };\n /** The table HAS columns this layout can use and none is chosen: the sentence over the picker. */\n pick: string;\n /** One plain sentence: what the table is missing (all a person who may not change it is told). */\n missing: string;\n /** One plain sentence: what pressing Make it work will do, with the column's name. */\n does: (label: string) => string;\n /** The sentence an agent is asked with — the same act, said in words. */\n ask: (label: string, options: readonly string[]) => string;\n}\n\n/** A list of words as a person says it: \"To do, In progress and Done\". */\nexport function wordsList(words: readonly string[]): string {\n const clean = words.map((w) => w.trim()).filter((w) => w !== \"\");\n if (clean.length <= 1) return clean.join(\"\");\n return `${clean.slice(0, -1).join(\", \")} and ${clean[clean.length - 1]}`;\n}\n\nexport const VIEW_NEEDS: Record<ViewLayout, ViewNeed | null> = {\n grid: null,\n // A list reads nothing a table can lack: rows, titled, grouped only when the person chooses.\n list: null,\n kanban: {\n kind: \"choice\",\n viewKey: \"groupField\",\n blocking: true,\n starter: { label: \"Status\", type: \"select\", options: [\"To do\", \"In progress\", \"Done\"] },\n pick: \"Pick the choice column the board's columns come from.\",\n missing: \"This table has no choice column to make the board's columns from.\",\n does: (label) => `Make it work adds a ${label} column with the choices below and groups the board by it.`,\n ask: (label, options) =>\n `Add a choice column called ${label} to this table${options.length > 0 ? ` with the choices ${wordsList(options)}` : \"\"}, then group the Kanban board by it.`,\n },\n calendar: {\n kind: \"date\",\n viewKey: \"dateField\",\n blocking: true,\n starter: { label: \"Date\", type: \"datetime\" },\n pick: \"Pick the date column the calendar places records by.\",\n missing: \"This table has no date column to place records on.\",\n does: (label) => `Make it work adds a ${label} column and lays the calendar out by it.`,\n ask: (label) => `Add a date column called ${label} to this table, then lay the Calendar out by it.`,\n },\n gallery: {\n kind: \"file\",\n viewKey: \"imageField\",\n blocking: false,\n starter: { label: \"Photo\", type: \"attachment\" },\n pick: \"Pick the file column each card shows as its picture.\",\n missing: \"The cards have no picture because this table has no file column.\",\n does: (label) => `Make it work adds a ${label} column and shows it on each card.`,\n ask: (label) => `Add a file column called ${label} to this table, then show it as the picture on each Gallery card.`,\n },\n timeline: {\n kind: \"date\",\n viewKey: \"dateField\",\n blocking: true,\n starter: { label: \"Start\", type: \"datetime\" },\n pick: \"Pick the date column each bar starts on.\",\n missing: \"This table has no date column to place records on.\",\n does: (label) => `Make it work adds a ${label} column and lays the timeline out by it.`,\n ask: (label) => `Add a date column called ${label} to this table, then lay the Timeline out by it.`,\n },\n};\n\n/**\n * A NAME NO COLUMN OF THIS TABLE ALREADY HAS: the starter's own, else \"Status 2\", \"Status 3\"…\n * A table that already holds a text column called Status must not be refused the board.\n */\nexport function freeColumnName(wanted: string, taken: readonly string[]): string {\n const held = new Set(taken.map((t) => t.trim().toLowerCase()));\n const base = wanted.trim() === \"\" ? \"Column\" : wanted.trim();\n if (!held.has(base.toLowerCase())) return base;\n for (let n = 2; n < 100; n += 1) {\n const next = `${base} ${n}`;\n if (!held.has(next.toLowerCase())) return next;\n }\n return base;\n}\n\n/** One sort clause. The Field's key and a direction; nothing else is a sort. */\nexport interface ViewSort {\n field: string;\n direction: \"asc\" | \"desc\";\n}\n\n/** How a saved view is ordered: by hand, by its sort, or it never said (the same as sorted). */\nexport type ViewOrder = \"manual\" | \"sorted\" | null;\n\n/** The order word a stored definition carries. Only the two words the store writes are read. */\nexport function viewOrderOf(definition: Record<string, unknown> | null | undefined): ViewOrder {\n const raw = definition?.[\"order\"];\n return raw === \"manual\" ? \"manual\" : raw === \"sorted\" ? \"sorted\" : null;\n}\n\n/**\n * THE DECLARATIVE SPEC. An agent writes this whole object; a person tweaks it.\n * Every builder component in this package takes one of these as a prop.\n */\nexport interface SavedViewSpec {\n name: string;\n /** The Table this view looks at. */\n subject: Uuid;\n layout: ViewLayout;\n /** Kanban: the Field whose value becomes a column. */\n groupField?: string | null;\n /**\n * Kanban: a number Field to TOTAL under each column heading. It lives on the VIEW for\n * the same reason the colours do — two people read one board two ways and neither\n * reading is a fact about the records.\n */\n measure?: string | null;\n /**\n * Kanban: the Field whose values cut the board into SWIMLANES across the columns — a\n * dispatcher's board by Status, one lane per Technician (S1-PRIME VIEW-KEYS). Never the\n * column Field itself; the store refuses that.\n */\n swimlaneField?: string | null;\n /** Kanban: the columns this view keeps shut to a strip, by the column's stored value. */\n collapsedColumns?: string[];\n /** Calendar: the Field that places a record in the month. */\n dateField?: string | null;\n /** Timeline: the Field a record's bar starts on (`start_field`). */\n startField?: string | null;\n /** Calendar and timeline: the Field a record ends on (`end_field`). */\n endField?: string | null;\n /** Gallery: the Field shown large on the card. */\n imageField?: string | null;\n sorts?: ViewSort[];\n /**\n * HOW THE VIEW IS ORDERED (ORDER-FIX): `\"manual\"` — the positions a person set by hand ARE the\n * view's sort, and it carries no `sorts`; `\"sorted\"` or null — by `sorts`. READ ONLY: the store\n * writes \"manual\" only when the rows are placed (`custom.view_record_order_set`) and turns it\n * off itself when a sort is saved, so no patch ever sends it (`viewSettingsPatch` does not).\n */\n order?: ViewOrder;\n /**\n * The membership Rule. The STORE decides who is in this view; this is a\n * reference to its Rule record, never a predicate this package evaluates.\n */\n ruleId?: Uuid | null;\n /**\n * THE FLAT FILTER a digest or a notify rule wrote the view with — the one shape\n * `custom.record_filter_sql` compiles, by Field KEY. A view saved in the bar has\n * none; a view saved by `DigestScheduler` (\"where Status is Open\") does, and the\n * grid and the board show exactly the records it names.\n */\n filters?: RecordFilter;\n /**\n * S2-PRIME — THE VIEW'S OWN QUESTION, as the condition builder writes it: the store's Rule\n * shape `{op, args}`, ALL / ANY / NOT groups nested to any depth, Fields by id. Kept in the\n * definition as `where` and asked of the store unchanged — the grid's list door, the board's\n * headings and the dashboard's numbers compile it into the one WHERE clause\n * (`custom.record_filter_sql`). Never stored under `filters`: that key is the flat map the\n * digests and the notifier read, and they would refuse it by name.\n */\n where?: RuleExpression | null;\n isDefault?: boolean;\n /**\n * HOW THIS VIEW LOOKS — its colors, its per-column formats and its frozen\n * column (`presentation.ts`).\n *\n * It lives on the VIEW and not on the Fields because two people read one\n * table two ways and neither reading is a fact about the records; it lives on\n * a RECORD, not a new column, because that is what makes it something an\n * agent writes with the same `records` tool as everything else.\n */\n presentation?: GridPresentation;\n}\n\n/**\n * A saved view as it comes back out of the store, with its row's own id.\n *\n * It carries no version: `custom.view_declare` MERGES one key at a time, so two\n * tabs pressing two different controls on one view both stand, and one control\n * pressed in both keeps the latest — how Airtable and Notion keep a view change.\n */\nexport interface SavedView extends SavedViewSpec {\n id: Uuid;\n /** Where the view came from when it was moved in (the mover, the S0 repair), else null. */\n movedFrom: Record<string, unknown> | null;\n}\n\n/** THE TABLE'S DEFAULT VIEW: the first view that says it is. */\nexport function defaultViewOf<V extends Pick<SavedView, \"isDefault\">>(views: readonly V[]): V | undefined {\n return views.find((v) => v.isDefault);\n}\n\n/**\n * WHICH FIELD EACH LAYOUT CAN BE DRAWN FROM, decided from the FIELD'S OWN\n * DECLARATION and never from the values it happens to hold.\n *\n * A kanban makes columns from a choice: the Field whose value is one of a\n * closed set (FLD-5 / FLD-6 — a pick-list is a Table, and a select stores the\n * id of one of its records). Grouping by a free-text Field produces one column\n * per record, which is not a board. A calendar places a record in time, so it\n * needs a Field that holds a date.\n *\n * These two answers are here, beside the layouts, because the picker on the\n * screen and any agent writing a view spec must agree about what is offerable.\n */\nexport const LAYOUT_FIELD_KIND: Record<ViewLayout, ViewNeedKind | null> = {\n grid: null,\n kanban: \"choice\",\n calendar: \"date\",\n gallery: \"file\",\n timeline: \"date\",\n list: null,\n};\n\n/**\n * ─────────────────────────────────────────────────────────────────────────────\n * WHICH VIEW A LINK NAMES — the address bar as a first-class control.\n *\n * 🚨 THE DEFECT THIS CLOSES (lane FRONT-DOOR, 2026-09-21; VERIFIER-8 HIGH-2).\n * `/data/<table>?view=kanban` returned 200 and rendered THE GRID. The\n * switcher was there, the four buttons worked, and the parameter was read by\n * nobody — so a person who bookmarked a board, or sent one to a colleague, got\n * a grid, and nothing on the screen said the view had been dropped. A link that\n * does not land is the inbox-link defect (`activeRecordId`) and the dashboard\n * defect (`activeDashboardId`) for the third time, so this is the same shape of\n * fix: the URL names it, the page opens on it, and the URL FOLLOWS the person\n * when they pick another.\n *\n * The dashboards are a fifth value and not a special case: to the person, \"the\n * dashboard\" is one of the ways to look at this table, and a link to it has to\n * work like a link to the board.\n */\n/**\n * 🚨 `archived` IS THE SIXTH VALUE, AND IT IS NOT A SPECIAL CASE EITHER (lane\n * FIX-10A-ARCHIVE, 2026-09-22; VERIFIER-10 F4).\n *\n * Archiving a record used to be a ONE-WAY DOOR: the grid's row action soft-\n * deletes correctly — `deleted_at` set, version bumped, nothing removed — and\n * then no screen anywhere in the platform could bring it back. The store's\n * `custom.record_restore` was there the whole time and did exactly the right\n * thing; nothing reached it, because every read door answers live rows only.\n *\n * So the archive is a WAY OF LOOKING AT THIS TABLE, like the board and the\n * dashboards, reached by the same row of buttons and named by the same\n * `?view=` parameter — never a Trash area of the app somebody has to know\n * exists, and never a per-table screen. Every Table inherits it the moment it\n * is in this list.\n */\nexport const PAGE_VIEWS = [...VIEW_LAYOUTS, \"dashboards\", \"archived\"] as const;\nexport type PageView = (typeof PAGE_VIEWS)[number];\n\nexport const PAGE_VIEW_LABEL: Record<PageView, string> = {\n ...LAYOUT_LABEL,\n dashboards: \"Dashboards\",\n archived: \"Archived\",\n};\n\n/**\n * THE WORDS A LINK MAY USE. The canonical token first; the rest are what a\n * person or an agent actually types, because a link that dies on \"board\" when\n * the button says \"Kanban\" is the same dead link with better spelling.\n */\nconst PAGE_VIEW_SPELLINGS: Record<string, PageView> = {\n grid: \"grid\",\n table: \"grid\",\n rows: \"grid\",\n list: \"list\",\n kanban: \"kanban\",\n board: \"kanban\",\n pipeline: \"kanban\",\n calendar: \"calendar\",\n month: \"calendar\",\n schedule: \"calendar\",\n gallery: \"gallery\",\n cards: \"gallery\",\n timeline: \"timeline\",\n gantt: \"timeline\",\n dashboards: \"dashboards\",\n dashboard: \"dashboards\",\n charts: \"dashboards\",\n archived: \"archived\",\n archive: \"archived\",\n // The grid's row action says \"Delete\" and the store soft-deletes, so the word\n // a person types is as likely to be the one the button used as the one this\n // view is called. Both land in the same place; neither dies.\n deleted: \"archived\",\n trash: \"archived\",\n removed: \"archived\",\n};\n\n/**\n * What a `?view=` value means, or `null` when it means nothing here.\n *\n * `null` is NOT \"show the grid\": the caller is expected to say that the link\n * named something this table has no view for, because silently substituting a\n * different view is exactly the defect above wearing a parser.\n */\nexport function pageViewFromParam(raw: string | null | undefined): PageView | null {\n if (typeof raw !== \"string\") return null;\n const token = raw.trim().toLowerCase();\n if (token === \"\") return null;\n return PAGE_VIEW_SPELLINGS[token] ?? null;\n}\n\n/** The sentence a link that named a view nobody has says. Never silent. */\nexport function unknownViewLine(raw: string): string {\n return (\n `The link asked for a view called “${raw}”, which this table has no view for, so you are looking at ` +\n `${PAGE_VIEW_LABEL.grid}. The views this table has are ` +\n `${PAGE_VIEWS.map((v) => PAGE_VIEW_LABEL[v]).join(\", \")} — pick one above and the address follows it.`\n );\n}\n\n/**\n * THE SPEC AS THE ONE DEFINITION `platform.saved_view` HOLDS — the keys\n * `custom.view_declare` documents, by Field KEY. `subject` is not in it: the door\n * takes the Table as its own argument and writes `table_id` itself.\n */\nexport function viewDocument(spec: SavedViewSpec): Record<string, unknown> {\n const out: Record<string, unknown> = {\n // A layout the store does not keep yet is not sent: the view is saved as a grid (`STORE_KEPT_LAYOUTS`).\n layout: storeKeepsLayout(spec.layout) ? spec.layout : \"grid\",\n sorts: spec.sorts ?? [],\n is_default: spec.isDefault === true,\n };\n if (spec.groupField) out[\"group_field\"] = spec.groupField;\n if (spec.swimlaneField) out[\"swimlane_field\"] = spec.swimlaneField;\n if (spec.collapsedColumns && spec.collapsedColumns.length > 0) out[\"collapsed_columns\"] = spec.collapsedColumns;\n if (spec.measure) out[\"measure\"] = spec.measure;\n if (spec.dateField) out[\"date_field\"] = spec.dateField;\n if (STORE_KEEPS_TIMELINE && spec.startField) out[\"start_field\"] = spec.startField;\n if (STORE_KEEPS_TIMELINE && spec.endField) out[\"end_field\"] = spec.endField;\n if (spec.imageField) out[\"image_field\"] = spec.imageField;\n if (spec.ruleId) out[\"rule_id\"] = spec.ruleId;\n if (spec.where) out[\"where\"] = spec.where;\n if (spec.presentation && !presentationIsEmpty(spec.presentation)) {\n out[\"presentation\"] = JSON.parse(presentationDocument(spec.presentation)) as Record<string, unknown>;\n }\n return out;\n}\n\n/**\n * One row of `custom.views` back into the spec a screen and an agent both read — THROUGH THE ONE\n * ACCESSOR (`viewSettings`, viewKeys.ts), so every layout sees every declared key the same way.\n */\nexport function viewFromRow(row: SavedViewRow): SavedView {\n const data = (row.definition ?? {}) as Record<string, unknown>;\n const k = viewSettings(data, row.filters && Object.keys(row.filters).length > 0 ? row.filters : null);\n const tableFromDefinition = typeof data[\"table_id\"] === \"string\" && data[\"table_id\"] !== \"\" ? (data[\"table_id\"] as string) : null;\n return {\n id: row.view_id,\n name: row.name || \"Untitled view\",\n subject: (row.table_id ?? tableFromDefinition ?? \"\") as Uuid,\n layout: k.layout,\n groupField: k.groupField,\n swimlaneField: k.swimlaneField,\n collapsedColumns: k.collapsedColumns,\n measure: k.measure,\n dateField: k.dateField,\n startField: k.startField,\n endField: k.endField,\n imageField: k.imageField,\n sorts: k.sorts,\n order: viewOrderOf(data),\n ruleId: k.ruleId,\n ...(k.filters ? { filters: k.filters } : {}),\n where: k.where,\n // The older table's own default view, carried in by the mover, IS this table's default —\n // the mover wrote the row's column, which `custom.views` does not return. Read as anything\n // else, the page would seed a second \"All records\" beside it and open on that.\n isDefault: k.isDefault || k.movedFrom?.[\"kind\"] === \"default_view\",\n presentation: k.presentation,\n movedFrom: k.movedFrom,\n };\n}\n\n/**\n * SAVE ONE NEW VIEW of a Table — the bar's \"New view\" and an agent's seed spec.\n * One `view_declare` with no `view_id`; the answer is the new view's id.\n */\nexport async function declareView(\n client: Pick<RecordsClient, \"viewDeclare\">,\n spec: SavedViewSpec,\n): Promise<RecordsResult<Uuid>> {\n return client.viewDeclare({\n table_id: spec.subject,\n spec: {\n name: spec.name,\n ...(spec.filters ? { filters: spec.filters } : {}),\n definition: viewDocument(spec),\n },\n });\n}\n\n/**\n * WRITE A CHANGE ONTO A SAVED VIEW, AND KEEP IT.\n *\n * `custom.view_declare` with the view's id MERGES: only the keys in `patch` are\n * written, a key cleared to null is removed, and everything the patch did not\n * name — the grid's own choices, a hand-set order, where the view was moved from,\n * a colour set in another tab — stays exactly as it was (S0, 2026-09-23: the door\n * used to rebuild the whole definition from what one press sent).\n *\n * There is no version to be stale against (VERIFIER-15 M4 is closed by the shape,\n * not by a retry): a saved view is a person's own arrangement of a table, and\n * their latest press on a control is the one that stands. A refusal — the view\n * was removed, or belongs to a table this person cannot open — is handed back for\n * the page to say through the one notice.\n */\nexport async function saveViewPatch(\n client: Pick<RecordsClient, \"viewDeclare\">,\n tableId: Uuid,\n viewId: Uuid,\n patch: Partial<SavedViewSpec>,\n): Promise<RecordsResult<Uuid>> {\n const definition = viewPatchDocument(patch);\n const renamed = patch.name !== undefined && patch.name.trim() !== \"\";\n const refiltered = patch.filters !== undefined;\n return client.viewDeclare({\n table_id: tableId,\n spec: {\n view_id: viewId,\n ...(renamed ? { name: patch.name!.trim() } : {}),\n ...(refiltered ? { filters: patch.filters ?? null } : {}),\n definition,\n },\n });\n}\n\n/**\n * A PARTIAL change to a saved view, as the definition keys the door merges. Only\n * the keys the caller actually changed are sent; a Field cleared (a board with no\n * \"Group by\") is sent as `null`, which the door removes.\n */\nexport function viewPatchDocument(patch: Partial<SavedViewSpec>): Record<string, unknown> {\n const { name: _name, subject: _subject, filters: _filters, ...keys } = patch;\n return viewSettingsPatch(keys);\n}\n\n/**\n * A LOOK AT A TABLE NEVER CHANGES HOW IT OPENS (lane VIEW-SWITCH-NOT-DESIGNATION, VERIFIER-21).\n *\n * On production the owner pressed Kanban, then Calendar, and the office member's table opened in\n * the Calendar: every layout press was saved onto the DEFAULT view, which is how the table opens\n * for everyone. The table's default view's `layout` and `isDefault` are its DESIGNATION, changed\n * only by an editor's deliberate \"Make this the default\" (`designateView`); the store refuses them\n * through `view_declare` by name. So a press on the default view carries every other key it\n * changed — a board's grouping, a calendar's date, a colour — and never those two.\n */\nexport function viewPatchFor(\n view: Pick<SavedView, \"isDefault\">,\n patch: Partial<SavedViewSpec>,\n): Partial<SavedViewSpec> {\n if (!view.isDefault) return patch;\n const { layout: _layout, isDefault: _isDefault, ...kept } = patch;\n return kept;\n}\n\n/** The one words for the explicit designation, with the layout it would make the default. */\nexport function makeDefaultLabel(layoutLabel: string): string {\n return `Make ${layoutLabel} the default`;\n}\n\n/**\n * MAKE THIS THE DEFAULT — the ONE designation door (`custom.view_designate`, editor on the table).\n * `layout` is the layout this table should open in for everyone: one of the layouts the store keeps. The view\n * becomes the table's only default.\n */\nexport async function designateView(\n client: Pick<RecordsClient, \"viewDesignate\">,\n tableId: Uuid,\n viewId: Uuid,\n layout: string | null,\n): Promise<RecordsResult<Uuid>> {\n return client.viewDesignate({ table_id: tableId, view_id: viewId, layout });\n}\n","\"use client\";\n\n// src/viewLook.ts — @ai-matrx/records-ui · lane VIEW-STATE-PERSONAL (VERIFIER-23 item 1, 2026-09-25)\n//\n// A LOOK AT A VIEW IS YOURS UNTIL YOU SAVE IT.\n//\n// On production the owner of a maintenance-requests table grouped its board by Status, and the\n// tenant liaison's board came up grouped by Status the next time she opened it: every pick was\n// written onto the shared view. Airtable and Notion keep the two apart — how you are looking at a\n// shared view (its grouping, its date field, its sort, its filter, hidden columns, widths, the\n// layout) is YOUR look, it persists for you, and the view changes only when you press \"Save to\n// view\" (Notion's \"Save for everyone\"), save it as a new view, or \"Reset to view\".\n//\n// The store keeps one look per (person, view) through `custom.view_look_set` / `view_look_read`\n// (viewer on the table, never written onto the view row). This file is the look's shape on this\n// side: the keys a look may hold, how it reads back, how it lays over the view, and what \"Save to\n// view\" sends.\n\nimport type { RecordsResult, Uuid } from \"@ai-matrx/records\";\nimport type { RecordsClient } from \"@ai-matrx/records/core\";\n\nimport { parseGridPresentation } from \"./presentation\";\nimport { viewSettings, viewSettingsPatch } from \"./viewKeys\";\nimport { VIEW_LAYOUTS, viewPatchFor, type SavedView, type SavedViewSpec, type ViewLayout } from \"./views\";\n\n/**\n * WHAT A LOOK MAY HOLD — how a person looks, never what the view is: not its name, not whether it\n * is the default (`view_designate`), not its Rule, not the flat filters the digests read, not the\n * order the store writes. The store refuses anything else by name.\n */\nexport type ViewLook = Partial<\n Pick<\n SavedViewSpec,\n | \"layout\"\n | \"groupField\"\n | \"swimlaneField\"\n | \"collapsedColumns\"\n | \"measure\"\n | \"dateField\"\n | \"startField\"\n | \"endField\"\n | \"imageField\"\n | \"sorts\"\n | \"where\"\n | \"presentation\"\n >\n>;\n\nconst LOOK_KEYS: ReadonlyArray<keyof ViewLook> = [\n \"layout\",\n \"groupField\",\n \"swimlaneField\",\n \"collapsedColumns\",\n \"measure\",\n \"dateField\",\n \"startField\",\n \"endField\",\n \"imageField\",\n \"sorts\",\n \"where\",\n \"presentation\",\n];\n\n/** Only the look's own keys of a press — a name, a default, a Rule or an order is never a look. */\nexport function lookPart(patch: Partial<SavedViewSpec>): ViewLook {\n const out: ViewLook = {};\n for (const key of LOOK_KEYS) {\n if (patch[key] !== undefined) (out as Record<string, unknown>)[key] = patch[key];\n }\n return out;\n}\n\n/** The look as the definition keys the store keeps (`view_look_set`); a cleared key is null. */\nexport function lookDocument(look: ViewLook): Record<string, unknown> {\n return viewSettingsPatch(look);\n}\n\n/** A kept look back into the spec's words. Only the keys it holds; a null key is \"none, for me\". */\nexport function lookFromDocument(doc: Record<string, unknown> | null | undefined): ViewLook {\n const data = doc ?? {};\n const read = viewSettings(data);\n const out: ViewLook = {};\n const has = (key: string) => Object.prototype.hasOwnProperty.call(data, key);\n if (has(\"layout\") && (VIEW_LAYOUTS as readonly string[]).includes(String(data[\"layout\"]))) {\n out.layout = data[\"layout\"] as ViewLayout;\n }\n if (has(\"group_field\")) out.groupField = read.groupField;\n if (has(\"swimlane_field\")) out.swimlaneField = read.swimlaneField;\n if (has(\"collapsed_columns\")) out.collapsedColumns = read.collapsedColumns;\n if (has(\"measure\")) out.measure = read.measure;\n if (has(\"date_field\")) out.dateField = read.dateField;\n if (has(\"start_field\")) out.startField = read.startField;\n if (has(\"end_field\")) out.endField = read.endField;\n if (has(\"image_field\")) out.imageField = read.imageField;\n if (has(\"sorts\")) out.sorts = read.sorts;\n if (has(\"where\")) out.where = read.where;\n if (has(\"presentation\")) out.presentation = parseGridPresentation(data[\"presentation\"]);\n return out;\n}\n\n/** THE VIEW AS THIS PERSON SEES IT: the saved view, with her look laid over it. */\nexport function applyLook(view: SavedView, look: ViewLook | null | undefined): SavedView {\n if (!look) return view;\n return { ...view, ...look } as SavedView;\n}\n\n/** Whether the look shows anything the view does not — which is when \"Save to view\" means something. */\nexport function lookDiffers(view: SavedView, look: ViewLook | null | undefined): boolean {\n if (!look) return false;\n const shown = applyLook(view, look);\n return LOOK_KEYS.some(\n (key) => look[key] !== undefined && JSON.stringify(shown[key] ?? null) !== JSON.stringify(view[key] ?? null),\n );\n}\n\n/**\n * WHAT \"SAVE TO VIEW\" SENDS, and what stays the person's own afterwards. The default view's layout\n * is the table's designation (`viewPatchFor`): it is never saved here — \"Make … the default\" is —\n * so a look's layout on the default view stays her look.\n */\nexport function saveToViewPlan(view: SavedView, look: ViewLook): { patch: Partial<SavedViewSpec>; left: ViewLook | null } {\n const patch = viewPatchFor(view, look);\n const left: ViewLook = {};\n if (look.layout !== undefined && patch.layout === undefined && look.layout !== view.layout) left.layout = look.layout;\n return { patch, left: Object.keys(left).length > 0 ? left : null };\n}\n\n/** The caller's own looks at every view of a table, by view id. */\nexport async function readLooks(\n client: Pick<RecordsClient, \"viewLookRead\">,\n tableId: Uuid,\n): Promise<RecordsResult<Record<string, ViewLook>>> {\n const answered = await client.viewLookRead({ table_id: tableId });\n if (!answered.ok) return answered;\n const out: Record<string, ViewLook> = {};\n for (const [viewId, row] of Object.entries(answered.data ?? {})) {\n out[viewId] = lookFromDocument(row?.look ?? null);\n }\n return { ok: true, data: out };\n}\n\n/** Keep the whole look (null: \"Reset to view\"). The shared view is never written. */\nexport async function keepLook(\n client: Pick<RecordsClient, \"viewLookSet\">,\n tableId: Uuid,\n viewId: Uuid,\n look: ViewLook | null,\n): Promise<RecordsResult<Record<string, unknown> | null>> {\n const doc = look ? lookDocument(look) : null;\n return client.viewLookSet({ table_id: tableId, view_id: viewId, look: doc && Object.keys(doc).length > 0 ? doc : null });\n}\n\n/** The words of the look bar. One sentence, then the doors. */\nexport const LOOK_WORDS = {\n /** The reason, for a tooltip or a screen reader — never a line on the page (interface-text). */\n yours: \"You changed how this view looks. Only you see it.\",\n /** The visible label for the same fact. */\n yoursLabel: \"Only you see this\",\n saveToView: \"Save to view\",\n saveAsNew: \"Save as new view\",\n reset: \"Reset to view\",\n /** The one button your look's doors fold into when the toolbar row has no room for three. */\n folded: \"Your look\",\n newName: \"Name for the new view\",\n} as const;\n","// src/firstPage.ts — @ai-matrx/records-ui/first-page · lane PAGE-BUNDLE-2\n//\n// THE GRID'S FIRST PAGE, ASKED BEFORE THE GRID EXISTS.\n//\n// A host that renders on a server (Next.js) can ask the table page's first rows while the browser\n// is still loading the app — but only if it asks with EXACTLY the arguments the grid will send, or\n// the records client cannot serve the grid from it (a part is used only on an exact door + args\n// match). This builds that request from the same pieces the screen uses: the view the bar opens\n// (`ViewBar`: the default view, else the first), the person's own look laid over it (`applyLook`),\n// the opening sort (`Grid`: the view's sort, else the table's `default_sort`, through\n// `pageSortFor`) and the table page's page size (100).\n//\n// IT ONLY PREDICTS THE PLAIN OPENING. A view grouped into sections, drilled, filtered, hand-ordered,\n// built on a Rule, or not a grid answers null — the grid asks for itself, exactly as before. A wrong\n// guess costs one wasted read and never a wrong row: the grid uses a seeded page only on an exact\n// match. No React here (server-safe entry).\n\nimport type { Field, RecordPageSort, Table, Uuid } from \"@ai-matrx/records\";\nimport type { RecordsClient, RecordsSeed } from \"@ai-matrx/records/core\";\nimport { asRecordsDataSource, createRecordsClient, recordingDataSource } from \"@ai-matrx/records/core\";\nimport type { RecordsConfig } from \"@ai-matrx/records\";\nimport { defaultSortOf } from \"./gridLook\";\nimport { pageSortFor } from \"./gridPage\";\nimport { applyLook, readLooks } from \"./viewLook\";\nimport { defaultViewOf, viewFromRow, type SavedView } from \"./views\";\n\n/** The table page's own page size (`TablePage`: 100 unless embedded). */\nexport const TABLE_PAGE_SIZE = 100;\n\n/**\n * HOW MANY ROWS AN EMBEDDED TABLE READS AT A TIME (CHAIR-EMBEDDED-FIDELITY 16): its first page, and\n * each \"Show more\" press brings about this many more. `embeddedLook.tsx` re-exports it.\n */\nexport const EMBEDDED_PAGE_SIZE = 50;\n\nexport interface FirstGridPageArgs {\n p_table_id: Uuid;\n p_filter: Record<string, never>;\n p_search: null;\n p_sort: RecordPageSort[];\n p_view_id: null;\n p_limit: number;\n p_offset: 0;\n}\n\n/** The request for the plain opening, from what the screen itself reads — or null. Pure. */\nexport function firstGridPageArgs(input: {\n tableId: Uuid;\n table: Pick<Table, \"default_sort\"> | null;\n fields: readonly Field[];\n /** The view as THIS person sees it (the saved view with her look over it), or null for none. */\n shown: SavedView | null;\n pageSize?: number;\n}): FirstGridPageArgs | null {\n const { shown, fields } = input;\n if (!shown) return null;\n if ((shown.layout ?? \"grid\") !== \"grid\") return null;\n if (shown.ruleId) return null;\n if (shown.order === \"manual\") return null;\n if (shown.filters && Object.keys(shown.filters).length > 0) return null;\n if (shown.presentation?.grouping) return null;\n const first = (shown.sorts ?? [])[0];\n const viewSort = first && fields.some((f) => f.key === first.field) ? { id: first.field, direction: first.direction } : null;\n const opening = viewSort ?? defaultSortOf(input.table, fields);\n return {\n p_table_id: input.tableId,\n p_filter: {},\n p_search: null,\n p_sort: pageSortFor(fields, opening),\n p_view_id: null,\n p_limit: input.pageSize ?? TABLE_PAGE_SIZE,\n p_offset: 0,\n };\n}\n\n/**\n * Read what the screen reads (through `client`, e.g. one served from `custom.table_page_bundle`)\n * and build the first page's request. Null when anything it needs is not answered.\n */\nexport async function firstGridPageRequest(\n client: Pick<RecordsClient, \"tableRead\" | \"fields\" | \"views\" | \"viewLookRead\">,\n tableId: Uuid,\n options: { pageSize?: number } = {},\n): Promise<FirstGridPageArgs | null> {\n const [table, fields, views, looks] = await Promise.all([\n client.tableRead({ table_id: tableId }),\n client.fields({ table_id: tableId }),\n client.views({ table_id: tableId }),\n readLooks(client, tableId),\n ]);\n if (!table.ok || !fields.ok || !views.ok || !looks.ok) return null;\n const mine = views.data.map(viewFromRow).filter((v) => v.subject === tableId);\n const view = defaultViewOf(mine) ?? mine[0] ?? null;\n const shown = view ? applyLook(view, looks.data[view.id]) : null;\n return firstGridPageArgs({ tableId, table: table.data, fields: fields.data, shown, ...(options.pageSize ? { pageSize: options.pageSize } : {}) });\n}\n\n/**\n * A TABLE PAGE'S SEED, ASKED BY THE SERVER AS THE PERSON (lane SSR-ROWS).\n *\n * Asks what the table page opens on — the table's bundle (`custom.table_page_bundle`, through the\n * records client exactly as the browser would), the grid's first page when `firstGridPageRequest`\n * can predict it, and on a record page the record's bundle — through a recording data source, and\n * answers every door answer it got as a `RecordsSeed`. The host hands that to\n * `<RecordsSeedProvider>` (`@ai-matrx/records/react`) above the table page: the page is drawn with\n * its rows in the server's HTML and hydrates onto them without asking again.\n *\n * `dataSource` is the person's own (a server supabase client carrying her session): every door\n * decides exactly as it does from the browser. Never throws; a door that refuses is simply not in\n * the seed, and the page asks it for itself.\n */\nexport async function askTablePageSeed(input: {\n dataSource: unknown;\n organizationId: Uuid;\n tableId: Uuid;\n actor: RecordsConfig[\"actor\"];\n /** A record page: its record's bundle (`custom.record_page_bundle`) is asked beside the table's. */\n recordId?: Uuid | null;\n /** Ask the grid's first page (default true). Off when the address opens something else. */\n rows?: boolean;\n /** The table is drawn EMBEDDED (`TablePage presentation=\"embedded\"`, a Space's inline table): its page size. */\n embedded?: boolean;\n pageSize?: number;\n}): Promise<RecordsSeed> {\n const recording = recordingDataSource(asRecordsDataSource(input.dataSource));\n const client = createRecordsClient({\n dataSource: recording.source,\n organizationId: input.organizationId,\n actor: input.actor,\n onError: () => {},\n });\n const record = input.recordId\n ? Promise.resolve(\n recording.source.rpc(\"record_page_bundle\", { p_organization_id: input.organizationId, p_record_id: input.recordId }, { schema: \"custom\" }),\n ).catch(() => null)\n : null;\n try {\n const pageSize = input.pageSize ?? (input.embedded ? EMBEDDED_PAGE_SIZE : TABLE_PAGE_SIZE);\n const args = await firstGridPageRequest(client, input.tableId, { pageSize });\n if (args && input.rows !== false) {\n await client.listPage({\n table_id: args.p_table_id,\n filter: args.p_filter,\n search: args.p_search,\n sort: args.p_sort,\n view_id: args.p_view_id,\n limit: args.p_limit,\n offset: args.p_offset,\n });\n }\n } catch {\n // a seed is a head start, never a requirement: whatever was answered is kept\n }\n if (record) await record;\n return recording.seed();\n}\n"]}
1
+ {"version":3,"sources":["../src/parity.ts","../src/gridLook.ts","../src/gridPage.ts","../src/presentation.ts","../src/viewKeys.ts","../src/views.ts","../src/viewLook.ts","../src/firstPage.ts"],"names":["KERNEL_TABLES","PARITY_FIELD_TYPES","fieldKindFor","parseTableStyle","tableStyleIsEmpty","raw","recordingDataSource","asRecordsDataSource","createRecordsClient"],"mappings":"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","file":"first-page.cjs","sourcesContent":["// src/parity.ts — @ai-matrx/records-ui\n//\n// FLD-11, the parity floor. A Field stores a BEHAVIOUR (one of five) plus\n// modifiers, unit and format; a person picked one of THIRTEEN parity types in\n// the UI. This resolves the second from the first, using the same facts the\n// store's own `custom.parity_field_types()` describes — which is generated into\n// `@ai-matrx/records` and imported here, never retyped.\n//\n// Nothing in this file decides policy. It decides which editor to draw.\n\nimport { displayFormatOf } from \"./choiceLooks\";\nimport {\n KERNEL_TABLES,\n PARITY_FIELD_TYPES,\n fieldKindFor,\n newerKindOf,\n type Field,\n type FieldKind,\n type KernelTargets,\n type ParityFieldType,\n} from \"@ai-matrx/records\";\nimport { humanizeIdentifier } from \"@ai-matrx/kit/text-case\";\nimport { isSignatureField } from \"./docs\";\nimport { fieldTypeChoice, isSystemFieldKind, type PickableFieldType } from \"./fieldTypes\";\n\n/** The kernel ids come from the store's own kernel rows, never from a literal here. */\nconst kernelId = (name: string): string =>\n KERNEL_TABLES.find((t) => t.name === name)?.id ?? \"\";\nconst KERNEL_FILE_TABLE = kernelId(\"File\");\nconst KERNEL_PERSON_TABLE = kernelId(\"Person\");\n\nexport const PARITY_LABEL: Record<ParityFieldType, string> = {\n attachment: \"Attachment\",\n checkbox: \"Yes / no\",\n currency: \"Currency\",\n datetime: \"Date\",\n email: \"Email\",\n formula: \"Formula\",\n lookup: \"Lookup\",\n member: \"Person\",\n multi_select: \"Multi-select\",\n percent: \"Percent\",\n phone: \"Phone\",\n rollup: \"Rollup\",\n select: \"Select\",\n url: \"Link\",\n};\n\n/** The \"made of\" sentence the store itself publishes, for the field editor's help text. */\nexport const PARITY_MADE_OF: Record<string, string> = Object.fromEntries(\n PARITY_FIELD_TYPES.map((p) => [p.parity_type, p.made_of]),\n);\n\n/**\n * Plain text and plain number are behaviours with no parity type of their own.\n *\n * 🚨 THIS IS NOW AN ALIAS, AND THAT IS THE POINT (TAILS-3, 2026-09-21). The\n * classifier moved into `@ai-matrx/records` as `FieldKind` so that the editor a\n * person types into and the COERCION applied to what they typed are decided by\n * one body. They were decided by two, and the public booking page paid for it:\n * \"Vehicle Year takes a number, and it was given a string\" on every table with\n * a number field.\n */\nexport type EditorKind = FieldKind;\n\n/** The kernel ids come from the store's own kernel rows, never from a literal here. */\nconst KERNEL: KernelTargets = { file: KERNEL_FILE_TABLE, person: KERNEL_PERSON_TABLE };\n\n/**\n * Which editor one Field wants. Reads the Field's own declaration only — the\n * behaviour, the modifiers, the format, the relation target and the config —\n * because those are what the store stores, and a screen that guessed from a\n * value would draw a different editor for an empty record.\n *\n * It is `fieldKindFor` with this package's kernel ids bound. Nothing here\n * decides anything the core does not.\n */\nexport function editorKindFor(field: Field): EditorKind {\n return fieldKindFor(field, KERNEL);\n}\n\n/**\n * WHICH KIND A PERSON PICKED, read back off the Field the store keeps — the ONE reader every\n * \"what does this column hold\" answer goes through (the Settings column list, the column panel).\n *\n * `editorKindFor` answers which EDITOR to draw, and for two families that is not the kind a\n * person picked: a Signature is a text Field whose format is `signature` (it draws a line of\n * text), and the store's stamps — Record number, Created time, Last changed time — are formula\n * Fields whose `config.system` names the stamp. Read through the editor classifier alone, the\n * Settings list called a Signature \"Text\" and a Record number \"Worked out from other columns\"\n * (lane 10 W1-A, guard `every-pickable-kind-reads-back-as-itself.test.tsx`).\n *\n * `undefined` when the Field is none of the kinds the menu offers.\n */\nexport function pickableTypeOf(field: Field): PickableFieldType | undefined {\n if (isSignatureField(field)) return \"signature\";\n // LANE 10 P4: the nine newer kinds, each a base type wearing a format (`newerKindOf`).\n const newer = newerKindOf(field);\n if (newer) return newer;\n // A RATING OR A TIME made as a plain column wearing its look (`LOOK_KINDS`): named by that kind.\n const look = displayFormatOf(field)?.id;\n if (look === \"rating\" && field.type === \"range\") return \"rating\";\n if (look === \"time\" && field.type === \"text\") return \"time\";\n const system = (field.config as Record<string, unknown> | undefined)?.[\"system\"];\n if (field.type === \"formula\" && typeof system === \"string\" && isSystemFieldKind(system)) return system;\n const kind = editorKindFor(field);\n return fieldTypeChoice(kind) ? (kind as PickableFieldType) : undefined;\n}\n\n/**\n * The type word a person sees in a header or a settings row — and it is the\n * SAME word the field panel offers, read from `FIELD_TYPE_CHOICES`, so a column\n * added as \"Money\" is never described anywhere else as \"Currency\".\n */\nexport function fieldTypeLabel(field: Field): string {\n const kind = pickableTypeOf(field) ?? editorKindFor(field);\n const known = fieldTypeChoice(kind)?.label;\n if (known) return field.multi && kind !== \"multi_select\" ? `${known} (many)` : known;\n const plain: Record<string, string> = {\n // `boolean` is the BEHAVIOUR word; `checkbox` is the type a person picks. Both are\n // written the way a person writes them, never as a raw token.\n boolean: \"Yes / no\",\n date: \"Date\",\n json: \"Structured data\",\n relation: \"Link to a record\",\n };\n // Never a raw token on a screen: an editor kind this table has not named is\n // still written the way a person writes it.\n return plain[kind] ?? humanizeIdentifier(kind);\n}\n","// src/gridLook.ts — @ai-matrx/records-ui · lane DEFAULT-GRID-PARITY\n//\n// WHAT THE STORE ALREADY SAYS ABOUT HOW A TABLE OPENS, READ IN ONE PLACE.\n//\n// The owner, on his real table's new page (2026-09-23): \"none of my advanced features are\n// copied over.\" Each of the things below was IN THE STORE and the default grid never read\n// it (`common-docs/systems/data/custom-data/HANDOFF.md`, rows D1 D3 D4 D5 D6\n// S2 F1 R3). They are pure functions on purpose — the grid is a large component and every\n// one of these answers is a question a test must be able to ask without mounting it:\n//\n// · the table's DEFAULT SORT (`data.default_sort`, `[{field, direction}]`), which the store\n// keeps and nothing read;\n// · the LAYOUT the grid opens with: the platform, then the organization's knob\n// (`custom/grid_layout`, read through `custom.grid_layout`), then THIS VIEW's own choice —\n// three layers, in that order, and the view always wins;\n// · the SUMMARY measures a column may offer, from the store's own list\n// (`custom.agg_operations`), filtered to what the column can mean;\n// · a REFERENCE a person pastes into a prompt — the `table` / `table_row` reference nouns.\n\nimport type { Field, GridLayout, GridLayoutMode, GridRowHeight, ReadRow, Table } from \"@ai-matrx/records\";\nimport type { MatrxDataTableCopyConfig } from \"@ai-matrx/design-system/data-table/types\";\n\nimport { fieldName } from \"./names\";\nimport { shownValue, type LabelLookup } from \"./labels\";\nimport { editorKindFor } from \"./parity\";\n\n// ── the default sort ─────────────────────────────────────────────────────────────────────\n\nexport interface DefaultSort {\n /** The Field KEY — the column id the grid builds every column with. */\n id: string;\n direction: \"asc\" | \"desc\";\n}\n\n/**\n * THE TABLE'S OWN SORT, as the grid's primitive takes it — or `null`.\n *\n * The store writes `default_sort` as `[{field, direction}]` (`custom.table_declare`, the\n * mover, and this package's own `declareTable` all do), with `field` a Field KEY. The type\n * the read carries says `string | null`, and older rows hold a bare key or `\"key desc\"`, so\n * all three shapes are read. A sort that names a column this table no longer has is\n * DROPPED, never guessed at: sorting by the nearest column would be the screen inventing an\n * order nobody chose. Only the first sort key is used — the grid's primitive sorts by one.\n */\nexport function defaultSortOf(\n table: Pick<Table, \"default_sort\"> | null | undefined,\n fields: readonly Field[],\n): DefaultSort | null {\n const raw = (table?.default_sort ?? null) as unknown;\n const first = Array.isArray(raw) ? raw[0] : raw;\n let name: string | null = null;\n let direction: \"asc\" | \"desc\" = \"asc\";\n if (typeof first === \"string\") {\n const trimmed = first.trim();\n if (trimmed === \"\") return null;\n if (trimmed.startsWith(\"-\")) {\n name = trimmed.slice(1).trim();\n direction = \"desc\";\n } else {\n const [head, tail] = trimmed.split(/\\s+/);\n name = head ?? null;\n if (tail && tail.toLowerCase() === \"desc\") direction = \"desc\";\n }\n } else if (first && typeof first === \"object\") {\n const entry = first as Record<string, unknown>;\n const named = entry[\"field\"] ?? entry[\"key\"] ?? entry[\"id\"];\n name = typeof named === \"string\" ? named : null;\n if (String(entry[\"direction\"] ?? \"\").toLowerCase() === \"desc\") direction = \"desc\";\n }\n if (!name) return null;\n // A Field named by its KEY (what the store writes) or, in an older row, by its id.\n const field = fields.find((f) => f.key === name) ?? fields.find((f) => f.id === name);\n return field ? { id: field.key, direction } : null;\n}\n\n// ── the layout ───────────────────────────────────────────────────────────────────────────\n\n/** A view's own layout choices — the per-view override of the organization's knob. */\nexport interface ViewLook {\n /** px, the grid's existing per-view row height. */\n rowHeight?: number | undefined;\n /** The view's own frozen columns, by key (the grid's existing per-view freeze). */\n frozen?: string[] | undefined;\n wrap?: boolean | undefined;\n fit?: GridLayoutMode | undefined;\n /**\n * \"Freeze the first column\", said by THIS VIEW — `false` is an explicit NO over an\n * organization whose knob freezes it, which an empty `frozen` list cannot say (an empty\n * list is how a view that never chose reads).\n */\n freezeFirst?: boolean | undefined;\n}\n\n/**\n * The store's three row heights, in the pixels the grid's primitive takes. \"normal\" is the\n * primitive's own density default, which is why it maps to nothing rather than a number\n * that would override the density a host chose.\n */\nexport const ROW_HEIGHT_PX: Record<GridRowHeight, number | undefined> = {\n compact: 28,\n normal: undefined,\n tall: 64,\n};\n\n/** The px a view remembers, read back as the store's word — for the menu's check mark. */\nexport function rowHeightWord(px: number | undefined): GridRowHeight {\n if (px === undefined) return \"normal\";\n if (px <= 32) return \"compact\";\n if (px >= 52) return \"tall\";\n return \"normal\";\n}\n\nexport interface ResolvedLook {\n rowHeight: number | undefined;\n rowHeightWord: GridRowHeight;\n wrap: boolean;\n /** What the table does with its width: fit it, or keep natural widths and scroll. */\n fit: \"fit\" | \"scroll\";\n /** The choice as stored — `auto` resolves to fit or scroll by the column count. */\n fitChoice: GridLayoutMode;\n frozen: string[];\n /** Where each of the four came from, so the menu can say \"your organization's\". */\n from: { rowHeight: Source; wrap: Source; fit: Source; frozen: Source };\n}\n\nexport type Source = \"view\" | \"organization\" | \"platform\";\n\nfunction storeSource(layout: GridLayout | null, key: string): Source {\n const said = layout?.source?.[key];\n return said === \"organization\" ? \"organization\" : \"platform\";\n}\n\n/**\n * THE COLUMN ORDER THE LOOK READS \"FIRST\" FROM: the Fields' own order, with the column that names\n * each record (`title_field`) leading — \"freeze the first column\" freezes the record's name, as\n * Airtable's primary field is always the one that stays (merged-grid review 2, D1: the menu said\n * \"Freeze Title\" and pinned whichever column sorted first). Pure.\n */\nexport function lookColumnKeys(orderedKeys: readonly string[], titleField: string | null | undefined): string[] {\n return titleField && orderedKeys.includes(titleField)\n ? [titleField, ...orderedKeys.filter((k) => k !== titleField)]\n : [...orderedKeys];\n}\n\n/**\n * THE LOOK THE GRID OPENS WITH: the view's own choice over the organization's knob over the\n * platform. `columnKeys` is the grid's column order, because \"freeze the first column\" is a\n * statement about whichever column is first, and \"automatic\" fit is a statement about how\n * many columns there are (`fit_max_columns`).\n *\n * `layout` null means the store's answer has not arrived or was refused; the grid then opens\n * exactly as it always did — the knob is an override, never the only thing that makes a\n * table readable.\n */\nexport function resolveLook(\n layout: GridLayout | null,\n view: ViewLook,\n columnKeys: readonly string[],\n): ResolvedLook {\n const stored = layout?.layout;\n const rowHeight =\n view.rowHeight !== undefined ? view.rowHeight : stored ? ROW_HEIGHT_PX[stored.row_height] : undefined;\n const wrap = view.wrap ?? stored?.wrap ?? true;\n const fitChoice: GridLayoutMode = view.fit ?? stored?.mode ?? \"auto\";\n const max = stored?.fit_max_columns ?? 8;\n const fit: \"fit\" | \"scroll\" =\n fitChoice === \"fit\" ? \"fit\" : fitChoice === \"scroll\" ? \"scroll\" : columnKeys.length <= max ? \"fit\" : \"scroll\";\n const freezeFirst = view.freezeFirst ?? stored?.freeze_first_column ?? false;\n const frozen =\n view.frozen && view.frozen.length > 0\n ? view.frozen\n : freezeFirst && columnKeys[0]\n ? [columnKeys[0]]\n : [];\n return {\n rowHeight,\n rowHeightWord: view.rowHeight !== undefined ? rowHeightWord(view.rowHeight) : (stored?.row_height ?? \"normal\"),\n wrap,\n fit,\n fitChoice,\n frozen,\n from: {\n rowHeight: view.rowHeight !== undefined ? \"view\" : storeSource(layout, \"row_height\"),\n wrap: view.wrap !== undefined ? \"view\" : storeSource(layout, \"wrap\"),\n fit: view.fit !== undefined ? \"view\" : storeSource(layout, \"mode\"),\n frozen:\n (view.frozen && view.frozen.length > 0) || view.freezeFirst !== undefined\n ? \"view\"\n : storeSource(layout, \"freeze_first_column\"),\n },\n };\n}\n\n// ── the summary bar ──────────────────────────────────────────────────────────────────────\n\n/** What each of the store's measures is called on screen. */\nexport const MEASURE_LABEL: Record<string, string> = {\n count: \"Count\",\n sum: \"Sum\",\n avg: \"Average\",\n min: \"Smallest\",\n max: \"Largest\",\n median: \"Median\",\n filled: \"Filled\",\n empty: \"Empty\",\n unique: \"Unique\",\n percent_empty: \"Percent empty\",\n percent_filled: \"Percent filled\",\n range: \"Range\",\n};\n\n/**\n * NOTION'S CALCULATE MENU (Spaces N6) — the embedded grid's footer offers Notion's list, in Notion's\n * order and words, each answered by one of the store's measures. `footer` is what the footer cell\n * says before the value (\"Sum 1,240\"). Count all counts records, so every column offers it.\n */\nexport const NOTION_CALCULATIONS: ReadonlyArray<{ op: string; menu: string; footer: string }> = [\n { op: \"count\", menu: \"Count all\", footer: \"Count\" },\n { op: \"unique\", menu: \"Count unique values\", footer: \"Unique\" },\n { op: \"empty\", menu: \"Count empty\", footer: \"Empty\" },\n { op: \"filled\", menu: \"Count not empty\", footer: \"Not empty\" },\n { op: \"percent_empty\", menu: \"Percent empty\", footer: \"Empty\" },\n { op: \"percent_filled\", menu: \"Percent not empty\", footer: \"Not empty\" },\n { op: \"sum\", menu: \"Sum\", footer: \"Sum\" },\n { op: \"avg\", menu: \"Average\", footer: \"Average\" },\n { op: \"median\", menu: \"Median\", footer: \"Median\" },\n { op: \"min\", menu: \"Min\", footer: \"Min\" },\n { op: \"max\", menu: \"Max\", footer: \"Max\" },\n { op: \"range\", menu: \"Range\", footer: \"Range\" },\n];\n\n/** Notion's calculations this column can mean, among those the store offers. */\nexport function calculationsFor(field: Field, storeOps: readonly string[]): Array<{ op: string; menu: string; footer: string }> {\n const offered = new Set(measuresFor(field, storeOps, true));\n return NOTION_CALCULATIONS.filter((c) => offered.has(c.op));\n}\n\nconst NUMERIC_ONLY = new Set([\"sum\", \"avg\", \"median\"]);\nconst ARITHMETIC = new Set([\n \"add\", \"sub\", \"mul\", \"div\", \"mod\", \"power\", \"sum\", \"average\", \"round\", \"abs\", \"min\", \"max\", \"ceiling\", \"floor\",\n]);\nconst ORDERED = new Set([\"min\", \"max\", \"range\"]);\n\n/** Is this a column whose values are numbers the store can add up? */\nexport function isNumericField(field: Field): boolean {\n const kind = editorKindFor(field);\n if (kind === \"number\" || kind === \"currency\" || kind === \"percent\" || kind === \"rollup\") return true;\n // A worked-out column is a number when its expression is arithmetic — `{Visit fee} -\n // {Deposit taken}` is stored as `{\"op\":\"fx.sub\",…}`. A joined label is not.\n if (kind !== \"formula\") return false;\n const expr = ((field.config ?? {}) as Record<string, unknown>)[\"expr\"] as { op?: unknown } | undefined;\n const op = typeof expr?.op === \"string\" ? expr.op.replace(/^fx\\./, \"\") : \"\";\n return ARITHMETIC.has(op);\n}\n\nfunction isOrderedField(field: Field): boolean {\n return isNumericField(field) || editorKindFor(field) === \"datetime\";\n}\n\n/**\n * THE MEASURES THIS COLUMN MAY OFFER — the store's own list (`custom.agg_operations`),\n * narrowed to what the column can mean. A sum of a text column is not a number the store\n * would refuse; it is a number that means nothing, which is worse, so it is never offered.\n * `count` counts records and belongs to no column, so it is offered once, on the first.\n */\nexport function measuresFor(field: Field, storeOps: readonly string[], firstColumn: boolean): string[] {\n return storeOps.filter((op) => {\n if (op === \"count\") return firstColumn;\n if (NUMERIC_ONLY.has(op)) return isNumericField(field);\n if (ORDERED.has(op)) return isOrderedField(field);\n return true;\n });\n}\n\n// ── a reference for a prompt ─────────────────────────────────────────────────────────────\n\n/**\n * THE GRID'S COPY, WITH ITS REFERENCES (TABLE-PARITY R3).\n *\n * The data table's Copy already offers the rows as text and as agent JSON; what it could not\n * offer is a REFERENCE — the pointer an agent expands itself (`table` and `table_row`, the\n * reference nouns the server resolves, and which follow a moved table into the record store).\n * The primitive takes references only on an explicit copy config, so this is that config: the\n * same text and JSON the primitive's own default writes (every column that reads a value, in\n * the view's order, by its own name), plus the two references. A row reference is NAMED by\n * the table's row label so it reads as a name. The host's content-transfer port decides\n * whether it can build them; where none is installed, the reference choice is simply absent.\n */\nexport function gridCopyConfig(args: {\n tableId: string;\n tableName: string;\n fields: readonly Field[];\n rowLabel: (row: ReadRow) => string;\n /**\n * WHAT A REFERENCE IS CALLED — the grid's own resolver (`useRecordLabels`). Copy and Copy for\n * AI carry the words the cell shows (\"Ojeda residence\"), never the id the store keeps.\n */\n labels?: LabelLookup;\n}): MatrxDataTableCopyConfig<ReadRow> {\n const { tableId, tableName, fields, rowLabel, labels } = args;\n const shown = (row: ReadRow) => {\n const document = (row.document ?? {}) as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const field of fields) {\n if (document[field.key] !== undefined) out[fieldName(field)] = shownValue(field, document[field.key], labels);\n }\n return out;\n };\n const text = (value: unknown): string =>\n value === null || value === undefined ? \"\" : typeof value === \"object\" ? JSON.stringify(value) : String(value);\n return {\n label: tableName,\n listLabel: tableName,\n location: tableName,\n rowKind: \"table-row\",\n listKind: \"table-view\",\n listDescription: `${tableName} — the records on this page, filtered and sorted as shown.`,\n humanRow: (row) =>\n Object.entries(shown(row))\n .map(([name, value]) => `${name}: ${text(value)}`)\n .join(\"\\n\"),\n agentRow: (row) => shown(row),\n rowReferences: (row) => [\n {\n id: `table_row:${tableId}:${row.id}`,\n label: rowLabel(row),\n noun: \"table_row\",\n items: [{ table_id: tableId, row_id: row.id, table_name: tableName, row_label: rowLabel(row) }],\n },\n ],\n references: () => [\n {\n id: `table:${tableId}`,\n label: tableName,\n noun: \"table\",\n items: [{ table_id: tableId, table_name: tableName }],\n },\n ],\n };\n}\n","// src/gridPage.ts — @ai-matrx/records-ui · merge tranche 6k (server search, sort and paging)\n//\n// THE MERGED GRID ASKS THE STORE FOR ITS PAGE (`custom.read_records_page` through `useRecordPage`).\n//\n// The classic grid read one page and then searched, filtered and sorted THAT page in the browser —\n// the readiness audit searched a 1,000-row table for a record on page six and found nothing, and a\n// sort ordered fifty rows while claiming to order the table. Here the grid's own query state (the\n// data table's search box, its column filters, its header sort, its page) is translated into the\n// store's one question and the store answers it: sorted over the whole table, searched over every\n// column this reader may see, filtered in its own WHERE clause, counted.\n//\n// These are the pure halves: the sort in the store's shape, and the column filters in the store's\n// one filter shape. A filter the store cannot ask is NEVER applied in the browser to the page it\n// happens to hold (that is the lie this tranche removes) — it is refused by name, and the grid\n// says so.\n\nimport type { Field, RecordFilter, RecordPageSort, RecordsError, RuleExpression, StoreFilter } from \"@ai-matrx/records\";\nimport { isRuleFilter } from \"@ai-matrx/records\";\nimport { dateFilterBounds, dateFilterDayBounds, dayInstantWindow } from \"@ai-matrx/design-system/data-table\";\nimport { getPersonTimeZone } from \"./personTime\";\nimport type { ColumnFilterValue, ColumnFiltersState, SortState } from \"@ai-matrx/design-system/data-table/types\";\nimport { editorKindFor } from \"./parity\";\nimport { fieldName } from \"./names\";\n\n/**\n * 🚨 WHY THE STORE CANNOT SORT BY THIS COLUMN, or `null` when it can (merged-grid review\n * 2026-09-26). A column worked out on every read (`type: \"formula\"`, `compute_on: \"read\"` — the\n * default, and what a lookup or a rollup is) keeps no value in the record, so\n * `custom.read_records_page` refuses to order the whole table by it. The header offered \"Sort\n * ascending\" anyway; the refused page then emptied the grid. The screen asks this ONE question\n * before offering a sort, and says the store's own reason instead.\n */\nexport function sortRefusal(f: Field): string | null {\n if (!computedOnRead(f)) return null;\n return `${fieldName(f)} is worked out each time it is read, so the store keeps no value to sort the whole table by. Sort by a column it is worked out from.`;\n}\n\n/** The same fact for a filter: the store's WHERE clause reads kept values, and this column keeps none. */\nexport function filterRefusal(f: Field): string | null {\n if (!computedOnRead(f)) return null;\n return `${fieldName(f)} is worked out each time it is read, so the store keeps no value to filter the whole table by. Filter by a column it is worked out from.`;\n}\n\nfunction computedOnRead(f: Field): boolean {\n const kind = String(editorKindFor(f));\n return (\n (f.type === \"formula\" || kind === \"formula\" || kind === \"lookup\" || kind === \"rollup\") &&\n String((f as { compute_on?: unknown }).compute_on ?? \"read\") === \"read\"\n );\n}\n\n/**\n * Does this column hold NUMBERS the store keeps — a number, money or percent column, or the record\n * number (`config.system: autonumber`, worked out once on save and kept)?\n */\nexport function holdsNumbers(field: Field): boolean {\n const kind = String(editorKindFor(field));\n if (kind === \"number\" || kind === \"currency\" || kind === \"percent\") return true;\n return String(((field.config ?? {}) as Record<string, unknown>)[\"system\"] ?? \"\") === \"autonumber\";\n}\n\n/** How the store should compare a column's values when it sorts by it. */\nexport function sortAsFor(field: Field): NonNullable<RecordPageSort[\"as\"]> {\n const kind = String(editorKindFor(field));\n // The record number sorts 1, 2 … 10, never \"10\" before \"9\".\n if (holdsNumbers(field)) return \"number\";\n if (kind === \"datetime\") return \"date\";\n return \"text\";\n}\n\n/** The grid's header sort, in the store's page-order shape. A sort on a column that is gone is no sort. */\nexport function pageSortFor(fields: readonly Field[], sort: SortState | null | undefined): RecordPageSort[] {\n if (!sort) return [];\n const field = fields.find((f) => f.key === sort.id);\n if (!field) return [];\n return [{ field: field.key, direction: sort.direction === \"desc\" ? \"desc\" : \"asc\", as: sortAsFor(field) }];\n}\n\n/** A text a person typed, as a case-insensitive \"contains\" pattern (the store's `matches` is a regex). */\nexport function containsPattern(text: string): string {\n return `(?i)${text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}`;\n}\n\nexport interface StoreQuestion {\n /** The whole question: the view's own filter AND every column filter the store can ask. */\n filter: StoreFilter | undefined;\n /** Column filters the store cannot ask, each as a sentence naming the column. Never applied locally. */\n refused: string[];\n}\n\ntype Clause = { flat: RecordFilter } | { rule: RuleExpression };\n\n/**\n * THE DATA TABLE'S \"(empty)\" TICK, as it writes it into a select filter's `values`\n * (`SELECT_EMPTY_VALUE` in the design system's filter engine). Asked of the store as \"has not been\n * answered\". `a-text-column-filters-by-its-values-counted-by-the-store.test.tsx` ticks the real\n * checklist, so this word drifting from the table's is a red test, not a silent no-op.\n */\nexport const BLANK_VALUE = \"__empty__\";\n\nconst field = (f: Field): RuleExpression => ({ field: f.id });\nconst constant = (value: unknown): RuleExpression => ({ const: value });\n/** A column's moment is in the half-open window `{from, to}` — the store's `within` node. */\nconst withinWindow = (f: Field, window: { from?: string; to?: string }): RuleExpression =>\n ({ op: \"within\", args: [field(f), constant(window)] }) as unknown as RuleExpression;\nconst op = (name: string, ...args: RuleExpression[]): RuleExpression =>\n ({ op: name, args }) as unknown as RuleExpression;\n\n/** One column filter, as a store clause — or the sentence saying why the store cannot ask it. */\nexport function clauseFor(f: Field, value: ColumnFilterValue, now: Date = new Date()): Clause | string | null {\n const name = fieldName(f);\n const refused = filterRefusal(f);\n if (refused) return refused;\n const not = (rule: RuleExpression): RuleExpression => op(\"not\", rule);\n switch (value.kind) {\n case \"text\": {\n const mode = value.mode ?? \"contains\";\n if (mode === \"empty\" || mode === \"null\") return value.negated ? { rule: op(\"present\", field(f)) } : { rule: not(op(\"present\", field(f))) };\n if (mode === \"not_empty\" || mode === \"not_null\") return value.negated ? { rule: not(op(\"present\", field(f))) } : { rule: op(\"present\", field(f)) };\n const text = value.value.trim();\n if (text === \"\") return null;\n const kind = String(editorKindFor(f));\n if (kind === \"number\" || kind === \"currency\" || kind === \"percent\" || kind === \"checkbox\") {\n return `${name} holds numbers, so it is filtered by a range, not by words.`;\n }\n const matches = op(\"matches\", field(f), constant(containsPattern(text)));\n return { rule: value.negated ? not(matches) : matches };\n }\n case \"select\": {\n // \"Is any of\" the ticked values; \"(empty)\" ticked (`BLANK_VALUE`) is \"has not been answered\";\n // Exclude is everything else — a blank row is not one of the ticked values, so Exclude keeps it\n // unless \"(empty)\" is ticked too (the Sheet's \"Exclude these instead\", slice C).\n const ticked = value.values ?? (value.value === \"\" ? [] : [value.value]);\n const blank = ticked.includes(BLANK_VALUE);\n const words = ticked.filter((w) => w !== BLANK_VALUE);\n if (!blank && words.length === 0) return value.negated ? null : { rule: constant(false) };\n const present = op(\"present\", field(f));\n const kind = String(editorKindFor(f));\n if (kind === \"datetime\" || kind === \"date\") {\n // TICKED DAYS: each is a half-open window, asked as the store's one `within` leaf. One day\n // alone stays a flat window (what a dashboard number asks); several days are an ANY of\n // them, a day beside \"(empty)\" adds the unanswered, and Exclude is NOT — the same shapes\n // every other value list makes, kept by a saved view in the one filter grammar.\n if (words.length === 0) return { rule: value.negated ? present : not(present) };\n const holdsDays = String(((f.config ?? {}) as Record<string, unknown>)[\"kind\"] ?? \"\") === \"date\";\n if (words.length === 1 && !blank && !value.negated) return { flat: { [f.key]: dayWindow(words[0]!, holdsDays) } };\n const leaves = words.map((day) => withinWindow(f, dayWindow(day, holdsDays)));\n const anyDay = leaves.length === 1 ? leaves[0]! : op(\"or\", ...leaves);\n if (!value.negated) return { rule: blank ? op(\"or\", not(present), anyDay) : anyDay };\n return { rule: blank ? op(\"and\", present, not(anyDay)) : not(anyDay) };\n }\n // The list's words are text; a number or yes/no column is asked in its own kind.\n const asStored = (w: string): unknown =>\n holdsNumbers(f) || kind === \"rating\"\n ? Number.isFinite(Number(w)) ? Number(w) : w\n : kind === \"checkbox\" && (w === \"true\" || w === \"false\") ? w === \"true\" : w;\n const stored = words.map(asStored);\n const anyWord =\n stored.length === 0 ? null : stored.length === 1 ? op(\"eq\", field(f), constant(stored[0])) : op(\"or\", ...stored.map((w) => op(\"eq\", field(f), constant(w))));\n if (!value.negated) {\n if (!blank && stored.length === 1) return { flat: { [f.key]: stored[0] as string | number | boolean } };\n if (!blank) return { rule: anyWord! };\n return { rule: anyWord ? op(\"or\", not(present), anyWord) : not(present) };\n }\n if (blank) return { rule: anyWord ? op(\"and\", present, not(anyWord)) : present };\n return { rule: op(\"or\", not(present), not(anyWord!)) };\n }\n case \"boolean\":\n return value.negated ? { rule: op(\"ne\", field(f), constant(value.value)) } : { flat: { [f.key]: value.value } };\n case \"number\": {\n // The number filter's operators (lane C): =, ≠, <, >, empty, not empty; \"between\" below.\n // The header's Exclude (slice C) turns every one of them round, never only \"between\".\n const turned = (rule: RuleExpression): Clause => ({ rule: value.negated ? not(rule) : rule });\n if (value.op === \"empty\") return turned(not(op(\"present\", field(f))));\n if (value.op === \"not_empty\") return turned(op(\"present\", field(f)));\n if (value.op === \"eq\" || value.op === \"ne\") {\n if (typeof value.min !== \"number\") return null;\n return turned(op(value.op, field(f), constant(value.min)));\n }\n if (value.op === \"lt\") return typeof value.max === \"number\" ? turned(op(\"lt\", field(f), constant(value.max))) : null;\n if (value.op === \"gt\") return typeof value.min === \"number\" ? turned(op(\"gt\", field(f), constant(value.min))) : null;\n const parts: RuleExpression[] = [];\n if (typeof value.min === \"number\") parts.push(op(\"gte\", field(f), constant(value.min)));\n if (typeof value.max === \"number\") parts.push(op(\"lte\", field(f), constant(value.max)));\n if (parts.length === 0) return null;\n const range = parts.length === 1 ? parts[0]! : op(\"and\", ...parts);\n return { rule: value.negated ? not(range) : range };\n }\n case \"date\": {\n // A relative preset (\"Today\", \"Last hour\") is worked out at the moment the question is asked,\n // in the viewer's own clock, and sent as exact bounds (grids review 3, lane H item 3). A saved\n // view's \"Today\" is today when it is opened — never the day somebody first picked it. A\n // column that holds a DAY is asked in days, never compared with an instant.\n // The window is read in the PERSON'S zone (setPersonTimeZone), so \"Today\" here is what the calendar calls today.\n const zone = getPersonTimeZone() ?? undefined;\n const moments = dateFilterBounds(value.preset ? { ...value, anchor: now.toISOString() } : value, now, zone);\n const holdsDays = String(((f.config ?? {}) as Record<string, unknown>)[\"kind\"] ?? \"\") === \"date\";\n const bounds = holdsDays ? dateFilterDayBounds(moments, zone) : moments;\n if (bounds.since === undefined && bounds.until === undefined) return null;\n return {\n flat: {\n [f.key]: {\n ...(bounds.since === undefined ? {} : { from: bounds.since }),\n ...(bounds.until === undefined ? {} : { to: bounds.until }),\n },\n },\n };\n }\n default:\n return `${name} cannot be filtered that way by the store.`;\n }\n}\n\n/**\n * ONE DAY AS A HALF-OPEN WINDOW. A column that holds DAYS is asked in days (\"2026-09-01\" up to\n * \"2026-09-02\"); a column of moments is asked from the viewer's own midnight to the next.\n */\nexport function dayWindow(day: string, holdsDays: boolean): { from: string; to: string } {\n if (!holdsDays) return dayInstantWindow(day, getPersonTimeZone() ?? undefined);\n const [y, m, d] = day.split(\"-\").map(Number);\n const ymd = (t: Date) => `${t.getUTCFullYear()}-${String(t.getUTCMonth() + 1).padStart(2, \"0\")}-${String(t.getUTCDate()).padStart(2, \"0\")}`;\n return { from: ymd(new Date(Date.UTC(y!, (m ?? 1) - 1, d ?? 1))), to: ymd(new Date(Date.UTC(y!, (m ?? 1) - 1, (d ?? 1) + 1))) };\n}\n\n/**\n * THE VIEW'S QUESTION AND THE COLUMN FILTERS, AS ONE STORE QUESTION.\n *\n * Flat clauses (an equality, `null`, a date window) stay flat when nothing else needs a Rule, so a\n * plain \"Status is Scheduled\" is the same question a dashboard number asks. When any clause needs a\n * Rule (words that contain, a range, several choices, NOT), the flat equalities become `eq` nodes of\n * one ALL group. A date window becomes a `within` leaf of that group.\n */\nexport function storeQuestion(\n fields: readonly Field[],\n base: StoreFilter | undefined,\n columnFilters: ColumnFiltersState,\n now: Date = new Date(),\n): StoreQuestion {\n const refused: string[] = [];\n const flat: RecordFilter = {};\n const rules: RuleExpression[] = [];\n if (base && isRuleFilter(base)) rules.push(base);\n else if (base) Object.assign(flat, base);\n for (const [key, value] of Object.entries(columnFilters)) {\n if (!value) continue;\n const f = fields.find((candidate) => candidate.key === key);\n if (!f) continue;\n const clause = clauseFor(f, value, now);\n if (clause === null) continue;\n if (typeof clause === \"string\") {\n refused.push(clause);\n continue;\n }\n if (\"flat\" in clause) Object.assign(flat, clause.flat);\n else rules.push(clause.rule);\n }\n const flatKeys = Object.keys(flat);\n if (rules.length === 0) return { filter: flatKeys.length > 0 ? flat : undefined, refused };\n for (const key of flatKeys) {\n const f = fields.find((candidate) => candidate.key === key);\n const value = flat[key];\n if (!f) continue;\n if (value !== null && typeof value === \"object\") {\n rules.push(withinWindow(f, value as { from?: string; to?: string }));\n continue;\n }\n rules.push(value === null ? op(\"not\", op(\"present\", field(f))) : op(\"eq\", field(f), constant(value)));\n }\n return { filter: rules.length === 1 ? rules[0]! : op(\"and\", ...rules), refused };\n}\n\n/**\n * 🚨 THE STORE'S SENTENCE, WITH ITS COLUMNS CALLED BY THEIR NAMES (merged-grid review 2026-09-26).\n * A refused page read names the column by its KEY (\"The column \\\"visit_total\\\" is worked out each\n * time it is read…\"), and the refusal formatter — rightly — drops a sentence carrying a machine\n * key, so the grid printed only \"That was not accepted\" and \"Nothing was read.\": a red fragment.\n * The key is the grid's to translate: every Field key the sentence names becomes that column's\n * name, and the store's sentence is then said whole.\n */\nexport function inColumnWords(error: RecordsError, fields: readonly Field[]): RecordsError {\n const say = (text: string | null | undefined): string | null | undefined => {\n if (!text) return text;\n let out = text;\n for (const f of [...fields].sort((a, b) => b.key.length - a.key.length)) {\n const key = f.key.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n out = out.replace(new RegExp(`([\"“'])${key}([\"”'])`, \"g\"), `$1${fieldName(f)}$2`);\n }\n return out;\n };\n return { ...error, message: say(error.message) ?? error.message, ...(error.hint ? { hint: say(error.hint) ?? error.hint } : {}) };\n}\n","/**\n * HOW A TABLE LOOKS — colors, formats and the frozen column.\n *\n * THE SPLIT, AND WHY IT IS THIS ONE. A Field declares WHAT it holds: a\n * currency, a percent, a date, a person. A VIEW declares HOW that reads and\n * what it is worth looking at: two decimal places or none, tint the row when\n * Status is Blocked, keep Name in place while the rest scrolls. Airtable draws\n * the line in exactly this place, and it has to be here: two people read one\n * table two different ways, and neither reading is a fact about the records.\n *\n * So nothing in this module is a new column on a Field and nothing here needed\n * a migration. A presentation lives on the saved-view record (`views.ts`), which\n * is an ordinary record of an ordinary Table — which means an agent sets it\n * through the same `records` tool it uses for everything else, and it travels\n * with the table the way every other record does.\n *\n * A FIELD STILL GETS ITS MONEY RIGHT WITH NO CONFIGURATION. `formatForField`\n * reads the Field's OWN declaration — a `currency` parity type with unit `$`, a\n * `percent`, a `datetime` with `config.kind` — and returns the format that\n * follows from it. The view's `formats` map only OVERRIDES that. A table nobody\n * has ever styled still shows `$1,250.50` and `45%`.\n */\n\n// The style parser from its own client-free subpath (lane SSR-ROWS): `@ai-matrx/records-ui/first-page`\n// runs in a host's SERVER component, and every export of the \"use client\" `data-table` entry is a\n// client reference there — calling `parseTableStyle` through it threw on the server.\nimport { parseTableStyle, tableStyleIsEmpty } from \"@ai-matrx/design-system/data-table/table-style\";\nimport {\n COLOR_ROTATION,\n type ChoiceColorLookup,\n type GroupAggregateKind,\n type GroupAggregateSpec,\n type GroupOrder,\n type StyleColor,\n type TableStyle,\n} from \"@ai-matrx/design-system/data-table\";\nimport { getFieldFormat, type FieldFormatConfig } from \"@ai-matrx/design-system/field-formats\";\nimport type { ColorRule } from \"@ai-matrx/design-system/data-table\";\nimport { newerKindOf, ratingMaxOf, type Field, type TableDecorations } from \"@ai-matrx/records\";\n\nimport { editorKindFor } from \"./parity\";\nimport { dateFormatFor } from \"./dates\";\n\n/**\n * SECTIONS BY A FIELD — the view's own grouping.\n *\n * `field` is a Field KEY, exactly like `frozen` and the keys of `formats`,\n * because a view is written against the Table's declaration and never against\n * whatever id a screen happened to build its columns with.\n *\n * `aggregates` is keyed by Field key too, and `collapsed` holds the data\n * table's own group keys (`s:Blocked`, `n:3`, `empty`), which is what the\n * primitive emits and the only thing that survives a value rendering the same\n * text as another.\n *\n * NOTHING HERE IS EVER CALLED `valueOf`, `toString` or `hasOwnProperty`: every\n * object in JavaScript already has all three, so an optional field by one of\n * those names is NEVER absent and a `config.valueOf ? … : …` check silently\n * reads `Object.prototype`. It cost the primitive a whole grouping once.\n */\nexport type GridGrouping = {\n /** The Field key whose value makes the sections. */\n field: string;\n /** Per-Field-key subtotal on every section header. */\n aggregates?: Record<string, GroupAggregateSpec>;\n /** Section order. The primitive's default is `value-asc`; blanks sort last. */\n order?: GroupOrder;\n /** The sections this view keeps shut, by the primitive's own group key. */\n collapsed?: string[];\n /** The grouped column drawn as a column too. Absent = it steps aside (the headers name its value). */\n showColumn?: boolean;\n /**\n * SUB-GROUPS (lane S1-PRIME VIEW-KEYS; ui-bench LISTS \"sub-grouping\"): up to two more levels\n * inside each section, by Field key. At most three levels in all — the store refuses a fourth\n * (`custom.view_declare`). A layout that draws one level ignores the rest; it never drops them,\n * because this list is parsed and written back whole with the rest of the look.\n */\n then?: GridSubGroup[];\n /**\n * DRILL (lane DRILL-TABLE-PRIMITIVE): the Dimension pivoted across the top — a Field key, or\n * `<key>:<grain>` for a date cut into periods.\n */\n across?: string;\n /** DRILL: the period `field` is cut into when it is a date (`month`); the store keeps `field` a Field key. */\n grain?: string;\n /** The Measures shown (\"what to display\"), by the store's own keys (`count`, `sum_quantity`). */\n show?: string[];\n /** The drill trail, in order: each crumb a Dimension reference and the value it narrowed to. */\n trail?: GridDrillStep[];\n};\n\n/** One crumb of a saved drill trail. `value: null` is \"no value\". */\nexport type GridDrillStep = { dim: string; value: string | null };\n\n/** One sub-group level: a Field key and its section order. */\nexport type GridSubGroup = { field: string; order?: GroupOrder; grain?: string };\n\n/** Everything a view says about how its table LOOKS. */\nexport type GridPresentation = {\n /** Color-by-a-column, rules and manual highlights. */\n style?: TableStyle;\n /** Per-column format overrides, by the Field's key. */\n formats?: Record<string, FieldFormatConfig>;\n /**\n * The columns that stay in place while the table scrolls sideways, by Field\n * key. Only the LEADING run of the view's column order can actually freeze —\n * the data table decides that, and declines rather than pinning a column at a\n * guessed offset.\n */\n frozen?: string[];\n /** Collapsible sections by one Field, with subtotals on each header. */\n grouping?: GridGrouping;\n /**\n * The width a person dragged a column to, by Field key. SPARSE on purpose —\n * a view saved before widths existed carries none and still parses, and a\n * column nobody resized is left to the table's own measurement.\n */\n widths?: Record<string, number>;\n /** The body row height this view remembers, in px. */\n rowHeight?: number;\n /**\n * Field keys hidden from this view — the SAME hidden-column list the grid's\n * own column picker holds, now given a place on the view so it survives a\n * reload and a layout switch reads the identical set gallery and grid agree\n * on, rather than a second \"fields shown\" list nobody kept in sync.\n */\n hiddenFields?: string[];\n /**\n * THE FIELDS A CARD LEAVES OFF (board, list, gallery, calendar, timeline), by Field key — the\n * card's own choice, kept apart from `hiddenFields` so a board's \"Card fields\" never hides the\n * grid's columns when the view switches layout (v7 TABLE-EXPERIENCE, walk 2026-10-05). An empty\n * list is a choice (every field shown); absent means the card never chose.\n */\n cardHidden?: string[];\n /**\n * THE ORDER A PERSON DRAGGED THE COLUMNS INTO, by Field key (merge tranche 6a). Sparse like\n * `widths`: a key it does not name keeps its Field's own place after the named ones, and a key\n * whose Field is gone is skipped — a column order can never hide or invent a column.\n */\n columnOrder?: string[];\n /**\n * The gallery's card size, remembered the same way the grid remembers its\n * row height — a per-view look, not a global default.\n */\n gallerySize?: \"small\" | \"medium\" | \"large\";\n /**\n * THIS VIEW'S OWN LAYOUT CHOICES, over the organization's `custom/grid_layout` knob (lane\n * DEFAULT-GRID-PARITY, TABLE-PARITY D1 D5 D6). Absent means \"whatever the organization\n * says\"; present is the view's word. `freezeFirst: false` is an explicit NO over an\n * organization that freezes the first column — an empty `frozen` cannot say that.\n */\n wrap?: boolean;\n /** NOTION'S \"WRAP COLUMN\" (N5): the columns that wrap their text when the view as a whole does not. */\n wrapColumns?: string[];\n fit?: \"auto\" | \"fit\" | \"scroll\";\n freezeFirst?: boolean;\n /**\n * THE SUMMARY BAR (TABLE-PARITY F1): Field key → the store measure shown under that column\n * (`count`, `sum`, `avg`, `min`, `max`, `median`, `filled`, `empty`, `unique`), worked out\n * by `custom.record_aggregate` over the whole table.\n */\n summaries?: Record<string, string>;\n /**\n * WHERE THE FOOTER SITS (lane TABLE-PAGE-CHROME, owner 2026-09-25) — the row count and the\n * pages: `sticky`, pinned to the bottom of the screen while the rows scroll (the older grid's\n * way, and the default when a view says nothing), or `inline`, right after the last row with\n * the page scrolling. Read by the Grid and a host's Sheet.\n */\n footer?: FooterPlacement;\n};\n\n/** Where a grid's footer sits. */\nexport type FooterPlacement = \"sticky\" | \"inline\";\n\n/** What a view that says nothing gets — the older grid's pinned footer. */\nexport const DEFAULT_FOOTER: FooterPlacement = \"sticky\";\n\n/** Where THIS view's footer sits, said or defaulted. */\nexport function footerOf(p: GridPresentation | null | undefined): FooterPlacement {\n return p?.footer ?? DEFAULT_FOOTER;\n}\n\nexport const EMPTY_PRESENTATION: GridPresentation = {};\n\n/** A frozen column needs a known width, or the table declines to freeze at all. */\nexport const FROZEN_COLUMN_WIDTH = 200;\n\n/**\n * WHAT A ROW HEIGHT IS ALLOWED TO BE. Below the floor the text is clipped and\n * no editor fits; above the ceiling one record fills the screen. A view asking\n * for anything outside it is DROPPED rather than obeyed, because a saved look\n * must not be able to make a table unreadable — and the Fields' own heights\n * still draw, which is the tolerant half.\n */\nexport const MIN_ROW_HEIGHT = 24;\nexport const MAX_ROW_HEIGHT = 96;\n\n/**\n * The aggregate kinds and orders the primitive actually implements. Listed here\n * because they are the tolerant parser's allow-list and a view is hostile input:\n * `satisfies` ties them to the primitive's own union, so a kind it renames stops\n * this file compiling instead of quietly parsing into a subtotal nobody draws.\n */\nconst AGGREGATE_KINDS = [\n \"count\",\n \"sum\",\n \"average\",\n \"min\",\n \"max\",\n \"empty\",\n \"filled\",\n \"unique\",\n \"checked\",\n \"unchecked\",\n \"percentFilled\",\n] as const satisfies readonly GroupAggregateKind[];\n\nconst GROUP_ORDERS = [\n \"value-asc\",\n \"value-desc\",\n \"count-desc\",\n \"first-seen\",\n] as const satisfies readonly GroupOrder[];\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\n/**\n * Read a presentation out of whatever the view record holds — an object, or the\n * JSON string a text field stores.\n *\n * Tolerant in exactly the way `parseTableStyle` is: an unreadable half is\n * dropped and the rest still applies, because a table must keep drawing when\n * one piece of its styling goes out of date.\n */\nexport function parseGridPresentation(raw: unknown): GridPresentation {\n let value = raw;\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (trimmed === \"\") return {};\n try {\n value = JSON.parse(trimmed);\n } catch {\n return {};\n }\n }\n if (!isRecord(value)) return {};\n\n const out: GridPresentation = {};\n\n const style = parseTableStyle(value[\"style\"]);\n if (!tableStyleIsEmpty(style)) out.style = style;\n\n if (isRecord(value[\"formats\"])) {\n const formats: Record<string, FieldFormatConfig> = {};\n for (const [key, cfg] of Object.entries(value[\"formats\"])) {\n if (!isRecord(cfg)) continue;\n const id = cfg[\"id\"];\n if (typeof id !== \"string\") continue;\n const options = cfg[\"options\"];\n formats[key] = isRecord(options)\n ? {\n id: id as FieldFormatConfig[\"id\"],\n options: options as NonNullable<FieldFormatConfig[\"options\"]>,\n }\n : { id: id as FieldFormatConfig[\"id\"] };\n }\n if (Object.keys(formats).length > 0) out.formats = formats;\n }\n\n if (Array.isArray(value[\"frozen\"])) {\n const frozen = value[\"frozen\"].filter((k): k is string => typeof k === \"string\");\n if (frozen.length > 0) out.frozen = frozen;\n }\n\n const grouping = parseGrouping(value[\"grouping\"]);\n if (grouping) out.grouping = grouping;\n\n if (isRecord(value[\"widths\"])) {\n const widths: Record<string, number> = {};\n for (const [key, raw] of Object.entries(value[\"widths\"])) {\n // A width that is not a usable number is one column's loss, never the\n // view's: the rest of the widths still apply and that column is measured.\n if (typeof raw !== \"number\" || !Number.isFinite(raw) || raw <= 0) continue;\n widths[key] = Math.round(raw);\n }\n if (Object.keys(widths).length > 0) out.widths = widths;\n }\n\n const rowHeight = value[\"rowHeight\"];\n if (\n typeof rowHeight === \"number\" &&\n Number.isFinite(rowHeight) &&\n rowHeight >= MIN_ROW_HEIGHT &&\n rowHeight <= MAX_ROW_HEIGHT\n ) {\n out.rowHeight = Math.round(rowHeight);\n }\n\n if (Array.isArray(value[\"hiddenFields\"])) {\n const hiddenFields = value[\"hiddenFields\"].filter((k): k is string => typeof k === \"string\");\n if (hiddenFields.length > 0) out.hiddenFields = hiddenFields;\n }\n if (Array.isArray(value[\"cardHidden\"])) {\n out.cardHidden = value[\"cardHidden\"].filter((k): k is string => typeof k === \"string\");\n }\n\n if (Array.isArray(value[\"columnOrder\"])) {\n const columnOrder = value[\"columnOrder\"].filter((k): k is string => typeof k === \"string\");\n if (columnOrder.length > 0) out.columnOrder = columnOrder;\n }\n\n const gallerySize = value[\"gallerySize\"];\n if (gallerySize === \"small\" || gallerySize === \"medium\" || gallerySize === \"large\") {\n out.gallerySize = gallerySize;\n }\n\n if (typeof value[\"wrap\"] === \"boolean\") out.wrap = value[\"wrap\"];\n if (Array.isArray(value[\"wrapColumns\"])) {\n const wrapColumns = value[\"wrapColumns\"].filter((k): k is string => typeof k === \"string\");\n if (wrapColumns.length > 0) out.wrapColumns = wrapColumns;\n }\n const fit = value[\"fit\"];\n if (fit === \"auto\" || fit === \"fit\" || fit === \"scroll\") out.fit = fit;\n if (typeof value[\"freezeFirst\"] === \"boolean\") out.freezeFirst = value[\"freezeFirst\"];\n const footer = value[\"footer\"];\n if (footer === \"sticky\" || footer === \"inline\") out.footer = footer;\n if (isRecord(value[\"summaries\"])) {\n const summaries: Record<string, string> = {};\n for (const [key, op] of Object.entries(value[\"summaries\"])) {\n // A measure name is judged by the store when it is asked; a non-string is nobody's.\n if (typeof op === \"string\" && op.trim() !== \"\") summaries[key] = op;\n }\n if (Object.keys(summaries).length > 0) out.summaries = summaries;\n }\n\n return out;\n}\n\n/**\n * A grouping, or nothing. A grouping with no Field to group BY is not a partial\n * grouping — it is no grouping at all, so it is dropped whole; an unreadable\n * subtotal or a section order this primitive does not have costs only itself.\n */\nfunction parseGrouping(raw: unknown): GridGrouping | undefined {\n if (!isRecord(raw)) return undefined;\n const field = raw[\"field\"];\n if (typeof field !== \"string\" || field.trim() === \"\") return undefined;\n\n const out: GridGrouping = { field };\n\n if (isRecord(raw[\"aggregates\"])) {\n const aggregates: Record<string, GroupAggregateSpec> = {};\n for (const [key, spec] of Object.entries(raw[\"aggregates\"])) {\n if (!isRecord(spec)) continue;\n const kind = spec[\"kind\"];\n if (typeof kind !== \"string\") continue;\n if (!(AGGREGATE_KINDS as readonly string[]).includes(kind)) continue;\n const label = spec[\"label\"];\n aggregates[key] =\n typeof label === \"string\"\n ? { kind: kind as GroupAggregateKind, label }\n : { kind: kind as GroupAggregateKind };\n }\n if (Object.keys(aggregates).length > 0) out.aggregates = aggregates;\n }\n\n const order = raw[\"order\"];\n if (typeof order === \"string\" && (GROUP_ORDERS as readonly string[]).includes(order)) {\n out.order = order as GroupOrder;\n }\n\n if (Array.isArray(raw[\"collapsed\"])) {\n const collapsed = raw[\"collapsed\"].filter((k): k is string => typeof k === \"string\");\n if (collapsed.length > 0) out.collapsed = collapsed;\n }\n\n if (raw[\"showColumn\"] === true) out.showColumn = true;\n\n if (Array.isArray(raw[\"then\"])) {\n const then: GridSubGroup[] = [];\n for (const level of raw[\"then\"]) {\n if (!isRecord(level)) continue;\n const sub = level[\"field\"];\n if (typeof sub !== \"string\" || sub.trim() === \"\") continue;\n const subOrder = level[\"order\"];\n const subGrain = level[\"grain\"];\n then.push({\n field: sub,\n ...(typeof subOrder === \"string\" && (GROUP_ORDERS as readonly string[]).includes(subOrder)\n ? { order: subOrder as GroupOrder }\n : {}),\n ...(typeof subGrain === \"string\" && subGrain !== \"\" ? { grain: subGrain } : {}),\n });\n }\n if (then.length > 0) out.then = then.slice(0, 2);\n }\n\n const grain = raw[\"grain\"];\n if (typeof grain === \"string\" && grain !== \"\") out.grain = grain;\n const across = raw[\"across\"];\n if (typeof across === \"string\" && across.trim() !== \"\") out.across = across;\n if (Array.isArray(raw[\"show\"])) {\n const show = raw[\"show\"].filter((k): k is string => typeof k === \"string\" && k !== \"\");\n if (show.length > 0) out.show = show;\n }\n if (Array.isArray(raw[\"trail\"])) {\n const trail: GridDrillStep[] = [];\n for (const step of raw[\"trail\"]) {\n if (!isRecord(step) || typeof step[\"dim\"] !== \"string\" || step[\"dim\"] === \"\") continue;\n const value = step[\"value\"];\n if (value !== null && typeof value !== \"string\") continue;\n trail.push({ dim: step[\"dim\"], value });\n }\n if (trail.length > 0) out.trail = trail;\n }\n\n return out;\n}\n\n/** True when nothing here would change how anything looks. */\nexport function presentationIsEmpty(p: GridPresentation): boolean {\n return (\n (p.style === undefined || tableStyleIsEmpty(p.style)) &&\n Object.keys(p.formats ?? {}).length === 0 &&\n (p.frozen?.length ?? 0) === 0 &&\n p.grouping === undefined &&\n Object.keys(p.widths ?? {}).length === 0 &&\n p.rowHeight === undefined &&\n (p.hiddenFields?.length ?? 0) === 0 &&\n p.cardHidden === undefined &&\n (p.columnOrder?.length ?? 0) === 0 &&\n p.gallerySize === undefined &&\n p.wrap === undefined &&\n p.fit === undefined &&\n p.freezeFirst === undefined &&\n p.footer === undefined &&\n Object.keys(p.summaries ?? {}).length === 0\n );\n}\n\n/** The string a view record stores. Empty string for \"this view styles nothing\". */\nexport function presentationDocument(p: GridPresentation): string {\n return presentationIsEmpty(p) ? \"\" : JSON.stringify(p);\n}\n\n/** Kinds whose value is a control the record renderer draws, never a formatter's string. */\nconst DRAWN_KINDS = new Set([\"select\", \"multi_select\", \"relation\", \"member\", \"person\", \"attachment\", \"file\", \"entity_reference\", \"checkbox\", \"boolean\", \"signature\", \"json\"]);\n\n/**\n * The display format a column's settings chose for it, when it is one the platform registry\n * knows and the column is not one the record renderer draws itself. `undefined` otherwise.\n */\nexport function shownAsOf(field: Field): FieldFormatConfig | undefined {\n if (DRAWN_KINDS.has(String(editorKindFor(field)))) return undefined;\n const raw = (field as unknown as { display_format?: unknown }).display_format;\n if (!raw || typeof raw !== \"object\") return undefined;\n const id = (raw as { id?: unknown }).id;\n // `formula` is the column's KIND, never a look: a worked-out column shows as its result format.\n if (typeof id !== \"string\" || id === \"formula\" || !getFieldFormat(id)) return undefined;\n const options = (raw as { options?: unknown }).options;\n return options && typeof options === \"object\" && Object.keys(options).length > 0\n ? ({ id, options } as FieldFormatConfig)\n : ({ id } as FieldFormatConfig);\n}\n\n/**\n * The format a Field's OWN declaration implies, before any view says anything.\n *\n * Returns `undefined` for a Field whose value the record renderer already draws\n * better than a formatter could — a relation chip, a person, an attachment, a\n * choice — because handing those to a text formatter would REPLACE a working\n * control with a string.\n */\nexport function formatForField(field: Field): FieldFormatConfig | undefined {\n // \"SHOWS AS\" — THE FORMAT THE COLUMN'S OWN SETTINGS CHOSE (merge tranche 6a/6e). The column\n // keeps it on the Field (`display_format`, written by `custom.field_update`), so the grid, the\n // record panel and the export read one answer. A column the record renderer draws better than\n // a formatter (a choice, a link, a person, a file) keeps its own drawing.\n const shownAs = shownAsOf(field);\n // A RATING'S TOP STAR IS ITS MAX RULE (lane 10 P4), so changing the rule changes the stars.\n if (newerKindOf(field) === \"rating\" && (!shownAs || shownAs.id === \"rating\")) {\n return { id: \"rating\", options: { ...(shownAs?.options ?? {}), ratingMax: ratingMaxOf(field) } };\n }\n if (shownAs) return shownAs;\n // THE STORE'S OWN STAMPS (Created time, Last changed time) are instants and print like\n // every other date-and-time cell — `dates.ts` answers, the same helper every card reads.\n const stamp = dateFormatFor(field);\n if (stamp && editorKindFor(field) !== \"datetime\") return stamp;\n switch (editorKindFor(field)) {\n case \"currency\":\n return {\n id: \"currency\",\n options: { currency: currencyCodeFor(field.unit) },\n };\n case \"percent\":\n return { id: \"percent\" };\n case \"datetime\":\n // The COLUMN's declaration: a `kind: \"date\"` column is a date column, anything else can\n // hold a moment. Each VALUE is still printed in its own shape (`RecordValue` →\n // `dates.dateFormatFor(field, value)`), so a bare date in a datetime column reads as a date.\n return String(field.config?.[\"kind\"] ?? \"\") === \"date\" ? { id: \"date\" } : { id: \"datetime\" };\n case \"email\":\n return { id: \"email\" };\n case \"url\":\n return { id: \"url\" };\n case \"phone\":\n return { id: \"phone\" };\n case \"number\":\n return { id: \"number\" };\n default:\n return undefined;\n }\n}\n\n/**\n * THE FORMAT ONE CELL OF A COLUMN IS DRAWN IN — the grid's cell and the gallery's card alike\n * (guard G4 \"one format\", lane VIEWS-AND-FIELDS W1-B): the view's own format for the column when\n * it set one, else the Field's own (`formatForField`). One function, so no layout formats money\n * (or a date, or a percent) its own way.\n */\nexport function cellFormatFor(field: Field, viewFormat?: FieldFormatConfig | undefined): FieldFormatConfig | undefined {\n return viewFormat ?? formatForField(field);\n}\n\n/**\n * The store lets a person type the SYMBOL they think in (`$`, `€`), and\n * `Intl.NumberFormat` wants the ISO code. The symbols people actually enter are\n * mapped; anything else is passed straight through, because a real ISO code is\n * the other thing that lands in this field, and an unknown code still renders\n * (`1,234.00 XYZ`) rather than blanking — THE FALLBACK LAW.\n */\nconst SYMBOL_TO_CODE: Record<string, string> = {\n $: \"USD\",\n \"£\": \"GBP\",\n \"€\": \"EUR\",\n \"¥\": \"JPY\",\n \"₹\": \"INR\",\n \"₽\": \"RUB\",\n \"₩\": \"KRW\",\n \"R$\": \"BRL\",\n \"C$\": \"CAD\",\n \"A$\": \"AUD\",\n};\n\nexport function currencyCodeFor(unit: string | null | undefined): string {\n const raw = (unit ?? \"\").trim();\n if (raw === \"\") return \"USD\";\n return SYMBOL_TO_CODE[raw] ?? raw.toUpperCase();\n}\n\n/**\n * The color a value paints with when a view colors BY a column.\n *\n * NOTHING FAILS SILENTLY, so this never returns \"no color\" for a value that\n * exists. The store keeps a list column's choices as records of another Table,\n * and this grid has not read that Table's option colors, so the color is\n * derived from the value's own text: the same word always gets the same color,\n * different words get different ones, and `true` is green the way every\n * checklist reads. When those option colors do land, this is the ONE function\n * that changes and every screen follows.\n */\nexport const colorFromTheValue: ChoiceColorLookup = (_fieldKey, value) => {\n if (value === true) return \"green\";\n if (value === false || value === null || value === undefined) return undefined;\n const text = String(value).trim().toLowerCase();\n if (text === \"\") return undefined;\n let hash = 0;\n for (let i = 0; i < text.length; i += 1) {\n hash = (hash * 31 + text.charCodeAt(i)) >>> 0;\n }\n return COLOR_ROTATION[hash % COLOR_ROTATION.length] as StyleColor;\n};\n\n/**\n * G1's TABLE-LEVEL COLORS (`custom.table_decorations` / `client.tableDecorations`),\n * TRANSLATED INTO A `TableStyle` — the same document a saved view's own `presentation.style`\n * is, so one resolver (`resolveTableStyle`, below) can treat \"the table's colors\" and \"this\n * view's colors\" as two layers of the SAME thing instead of two unrelated systems.\n *\n * THE TRANSLATION IS NECESSARY AND IT IS THE ONLY ONE. G1 addresses a Field by its ID\n * (REC-17 — a column renamed keeps its color), because it is the STORE's own door; a\n * `TableStyle` addresses a Field by its KEY, because it reads straight off `row.document`,\n * which is keyed by Field key. A rule or a color-by whose Field has been deleted (its id no\n * longer resolves to a key) is dropped rather than crashing the whole style — a defect in\n * one rule costs that rule, never the read.\n */\nexport function styleFromDecorations(\n decorations: TableDecorations | null | undefined,\n fields: readonly Field[],\n): TableStyle {\n if (!decorations) return { version: 1 };\n const keyOf = (id: string) => fields.find((f) => f.id === id)?.key;\n const style: TableStyle = { version: 1 };\n\n if (decorations.color_by?.field) {\n const field = keyOf(decorations.color_by.field);\n if (field) style.colorBy = { field, target: decorations.color_by.target };\n }\n\n if (Array.isArray(decorations.rules) && decorations.rules.length > 0) {\n const rules: ColorRule[] = [];\n decorations.rules.forEach((rule, index) => {\n const field = keyOf(rule.field);\n if (!field) return;\n rules.push({\n id: rule.id ?? `decoration:${index}`,\n field,\n op: rule.op,\n color: rule.color,\n target: rule.target,\n ...(rule.value === undefined || rule.value === null ? {} : { value: String(rule.value) }),\n });\n });\n if (rules.length > 0) style.rules = rules;\n }\n\n if (decorations.rows && Object.keys(decorations.rows).length > 0) {\n style.rows = { ...decorations.rows };\n }\n\n const columns: Record<string, StyleColor> = {};\n for (const [id, color] of Object.entries(decorations.columns ?? {})) {\n const key = keyOf(id);\n if (key) columns[key] = color;\n }\n if (Object.keys(columns).length > 0) style.columns = columns;\n\n const cells: Record<string, Record<string, StyleColor>> = {};\n for (const [rowId, byField] of Object.entries(decorations.cells ?? {})) {\n for (const [id, color] of Object.entries(byField ?? {})) {\n const key = keyOf(id);\n if (!key) continue;\n (cells[rowId] ??= {})[key] = color;\n }\n }\n if (Object.keys(cells).length > 0) style.cells = cells;\n\n return style;\n}\n\n/**\n * THE ONE PRESENTATION RESOLVER every colored surface in this package reads —\n * `Grid`, and (via `Card`) the kanban board, the calendar and the gallery.\n *\n * THE CLASS THIS CLOSES (lane UI-QUICK-1, TABLE-PARITY review, 2026-09-24). Threading\n * `view.presentation.style` into `Card` closed the instance the CALENDAR-GALLERY bench\n * found — a VIEW that had its own color-by looked right everywhere — but it left the\n * class open: a table whose colors are the ORGANIZATION's decorations (G1's\n * `custom.table_decorations` — \"color rows by Account Type\", \"MAXED and NEAR rules\" on\n * Arman's own Coding Accounts table) and whose views never set a `presentation.style` of\n * their own drew NOTHING, on the grid exactly as much as on every other layout, because\n * nothing in this package had ever read G1's door at all.\n *\n * TWO LAYERS, ONE DOCUMENT. The table's decorations are the base coat — what every view\n * of this table shows until a view says otherwise. A view's OWN `presentation.style` is\n * an override, piece by piece: a view that only ever set manual row highlights still\n * inherits the table's color-by and rules for everything it never touched, and a view\n * that fully redecorated (its own `colorBy` AND its own `rules`) is not haunted by a\n * table rule it meant to replace. Per-piece, never all-or-nothing, because a view is a\n * LOOK on top of the table, not a wholesale substitute for it.\n */\nexport function resolveTableStyle(\n decorations: TableDecorations | null | undefined,\n fields: readonly Field[],\n viewStyle: TableStyle | undefined,\n): TableStyle {\n const base = styleFromDecorations(decorations, fields);\n if (!viewStyle) return base;\n const colorBy = viewStyle.colorBy ?? base.colorBy;\n const rules = viewStyle.rules ?? base.rules;\n const rows = viewStyle.rows ?? base.rows;\n const columns = viewStyle.columns ?? base.columns;\n const cells = viewStyle.cells ?? base.cells;\n return {\n version: 1,\n ...(colorBy !== undefined ? { colorBy } : {}),\n ...(rules !== undefined ? { rules } : {}),\n ...(rows !== undefined ? { rows } : {}),\n ...(columns !== undefined ? { columns } : {}),\n ...(cells !== undefined ? { cells } : {}),\n };\n}\n","\"use client\";\n\n// src/viewKeys.ts — @ai-matrx/records-ui · lane S1-PRIME VIEW-KEYS (UI-CHAMPIONS-PLAN rev 2)\n//\n// EVERY PER-VIEW SETTING IS A DECLARED KEY OF THE ONE SAVED VIEW.\n//\n// A saved view is one row of `platform.saved_view` (lane S0). What it may hold is decided in ONE\n// place: `custom.view_keys()`, the registry `custom.view_declare` judges every save against. This\n// file is that registry's client half and the ONE accessor every layout reads a view through:\n//\n// - `VIEW_KEYS` mirrors `custom.view_keys()` row for row (path, shape, the layouts that read it,\n// who may write it). The live walk compares the two through REST, as test@test.com, so a key\n// added on one side and not the other fails a walk, never a person.\n// - `viewSettings(definition)` is THE reader: a stored definition in, every declared key out,\n// typed. `viewFromRow` (views.ts) goes through it, so the Grid, the board, the calendar,\n// and the gallery all see a view the same way.\n// - `viewSettingsFor(view, layout)` hands a layout only the keys it reads; a layout that does\n// not use a key never sees it (the calendar is never told about swimlanes).\n// - `viewSettingsPatch(patch)` is THE writer's shape: only the keys a press changed, a cleared\n// key as null (the door removes it). `saveViewPatch` sends it through `custom.view_declare`.\n//\n// Fields are named by KEY (a Field's key is immutable; every reader draws rows by key). The door\n// accepts an id and stores the key, so an agent may send either.\n\nimport type { RecordFilter, RuleExpression } from \"@ai-matrx/records\";\nimport { parseGridPresentation, presentationDocument, presentationIsEmpty, type GridPresentation } from \"./presentation\";\n\n/** Every surface a saved view is drawn by. */\nexport type ViewKeyLayout = \"grid\" | \"kanban\" | \"calendar\" | \"gallery\" | \"timeline\" | \"list\";\n\n/**\n * 🚩 THE ONE SWITCH FOR THE TIMELINE'S STORE KEYS (lane VIEWS-AND-FIELDS, sublane TL).\n *\n * `vk_a_calendar_keeps_where_a_record_ends_and_a_timeline_is_a_view.sql` (sublane VK) teaches\n * `custom.view_keys()` the layout `timeline`, the keys `start_field` and `end_field` (calendar +\n * timeline), and `group_field` for the timeline. Until that migration is on production this stays\n * `false`, and:\n * · `timeline` is not a layout the store keeps (`STORE_KEPT_LAYOUTS`), so it is drawn for the visit;\n * · `start_field` / `end_field` are held for the visit, never sent (`viewSettingsPatch`), and the\n * Start also rides `date_field`, which the store already keeps;\n * · the timeline's toolbar says \"Kept for this visit only\".\n * Flipped to `true`, VIEW_KEYS below mirrors the migrated registry row for row (the live walk\n * compares them) and every one of those is kept on the view.\n */\nexport const STORE_KEEPS_TIMELINE = true;\n\n/** Who may write a key: anyone who may save the view, an input the door rewrites, or the server only. */\nexport type ViewKeyWriter = \"caller\" | \"input\" | \"server\";\n\nexport interface ViewKeyDeclaration {\n /** Where it lives in `platform.saved_view.definition` (dotted under `presentation`). */\n path: string;\n /** What the store's guard checks (`custom._view_key_value`). */\n shape: string;\n /** The layouts that read it. */\n layouts: readonly ViewKeyLayout[];\n writer: ViewKeyWriter;\n}\n\nconst ALL: readonly ViewKeyLayout[] = [\"grid\", \"kanban\", \"calendar\", \"gallery\", ...(STORE_KEEPS_TIMELINE ? ([\"timeline\"] as const) : []), \"list\"];\n/** The layouts whose cards draw the fields a view leaves shown (`presentation.hiddenFields`). */\nconst HIDES: readonly ViewKeyLayout[] = [\"grid\", \"gallery\", \"kanban\", \"calendar\", ...(STORE_KEEPS_TIMELINE ? ([\"timeline\"] as const) : []), \"list\"];\nconst GRIDS: readonly ViewKeyLayout[] = [\"grid\"];\n\n/**\n * THE REGISTRY, as `custom.view_keys()` declares it (uichamp_s1_a_view_keeps_every_setting_it_is_given.sql).\n * Order and content are compared with the store by the live walk.\n */\nexport const VIEW_KEYS: readonly ViewKeyDeclaration[] = [\n { path: \"layout\", shape: \"kind\", layouts: ALL, writer: \"caller\" },\n { path: \"filters\", shape: \"filter\", layouts: ALL, writer: \"caller\" },\n { path: \"where\", shape: \"rule\", layouts: ALL, writer: \"caller\" },\n { path: \"group_field\", shape: \"field\", layouts: [\"kanban\", ...(STORE_KEEPS_TIMELINE ? ([\"timeline\"] as const) : [])], writer: \"caller\" },\n { path: \"swimlane_field\", shape: \"field:lane\", layouts: [\"kanban\"], writer: \"caller\" },\n { path: \"collapsed_columns\", shape: \"values\", layouts: [\"kanban\"], writer: \"caller\" },\n { path: \"measure\", shape: \"field:number\", layouts: [\"kanban\"], writer: \"caller\" },\n { path: \"date_field\", shape: \"field:date\", layouts: [\"calendar\"], writer: \"caller\" },\n ...(STORE_KEEPS_TIMELINE\n ? ([\n { path: \"start_field\", shape: \"field:date\", layouts: [\"timeline\"], writer: \"caller\" },\n { path: \"end_field\", shape: \"field:date\", layouts: [\"calendar\", \"timeline\"], writer: \"caller\" },\n ] as const)\n : []),\n { path: \"image_field\", shape: \"field\", layouts: [\"gallery\"], writer: \"caller\" },\n { path: \"sorts\", shape: \"sorts\", layouts: ALL, writer: \"caller\" },\n { path: \"rule_id\", shape: \"uuid\", layouts: ALL, writer: \"caller\" },\n { path: \"is_default\", shape: \"boolean\", layouts: ALL, writer: \"caller\" },\n { path: \"presentation\", shape: \"presentation\", layouts: ALL, writer: \"caller\" },\n { path: \"presentation.style\", shape: \"style\", layouts: ALL, writer: \"caller\" },\n { path: \"presentation.formats\", shape: \"object\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.frozen\", shape: \"fields\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.grouping\", shape: \"grouping\", layouts: [\"grid\", \"gallery\", \"list\"], writer: \"caller\" },\n { path: \"presentation.widths\", shape: \"widths\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.rowHeight\", shape: \"row_height\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.hiddenFields\", shape: \"fields\", layouts: HIDES, writer: \"caller\" },\n { path: \"presentation.cardHidden\", shape: \"fields\", layouts: [\"kanban\", \"calendar\", ...(STORE_KEEPS_TIMELINE ? ([\"timeline\"] as const) : []), \"list\", \"gallery\"], writer: \"caller\" },\n { path: \"presentation.columnOrder\", shape: \"fields\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.gallerySize\", shape: \"gallery_size\", layouts: [\"gallery\"], writer: \"caller\" },\n { path: \"presentation.wrap\", shape: \"boolean\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.wrapColumns\", shape: \"fields\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.fit\", shape: \"fit\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.freezeFirst\", shape: \"boolean\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.footer\", shape: \"footer\", layouts: GRIDS, writer: \"caller\" },\n { path: \"presentation.summaries\", shape: \"summaries\", layouts: GRIDS, writer: \"caller\" },\n { path: \"grid\", shape: \"grid\", layouts: GRIDS, writer: \"caller\" },\n { path: \"hidden_fields\", shape: \"input\", layouts: HIDES, writer: \"input\" },\n { path: \"table_id\", shape: \"server\", layouts: ALL, writer: \"server\" },\n { path: \"order\", shape: \"server\", layouts: GRIDS, writer: \"server\" },\n { path: \"moved_from\", shape: \"server\", layouts: ALL, writer: \"server\" },\n];\n\n/**\n * THE LAYOUTS THE STORE KEEPS ON A VIEW. `custom.view_keys()` judges a view's `layout` against its\n * own list (grid, kanban, calendar, gallery) and refuses any other word by name. A layout this\n * package draws before the store keeps it — the timeline, 2026-10-02 — is never sent as a view's or a\n * look's layout (`viewSettingsPatch`, `viewDocument`): it is drawn for this visit and says so. When the\n * store's registry admits it, add it here and nothing else changes.\n */\nexport const STORE_KEPT_LAYOUTS: readonly string[] = [\"grid\", \"kanban\", \"calendar\", \"gallery\", ...(STORE_KEEPS_TIMELINE ? [\"timeline\"] : []), \"list\"];\n\n/** Whether the store keeps this layout on a saved view. */\nexport function storeKeepsLayout(layout: string): boolean {\n return STORE_KEPT_LAYOUTS.includes(layout);\n}\n\n/** The keys one layout reads, by path. */\nexport function viewKeysFor(layout: ViewKeyLayout): string[] {\n return VIEW_KEYS.filter((k) => k.layouts.includes(layout)).map((k) => k.path);\n}\n\n/** One sort clause: a Field key and a direction. */\nexport interface ViewSortKey {\n field: string;\n direction: \"asc\" | \"desc\";\n}\n\n/**\n * EVERY DECLARED KEY OF A VIEW, TYPED. Absent keys are `null` / empty, never guessed.\n * `presentation` is the parsed look (colour field, pinned fields, row height, grouping with its\n * sub-groups and shut sections, hidden fields, gallery size, summaries).\n */\nexport interface ViewSettings {\n layout: \"grid\" | \"kanban\" | \"calendar\" | \"gallery\" | \"timeline\" | \"list\";\n filters: RecordFilter | null;\n where: RuleExpression | null;\n groupField: string | null;\n swimlaneField: string | null;\n collapsedColumns: string[];\n measure: string | null;\n dateField: string | null;\n /** Timeline: where a record's bar starts (`start_field`). */\n startField: string | null;\n /** Calendar and timeline: where a record ends (`end_field`). */\n endField: string | null;\n imageField: string | null;\n sorts: ViewSortKey[];\n ruleId: string | null;\n isDefault: boolean;\n presentation: GridPresentation;\n movedFrom: Record<string, unknown> | null;\n}\n\n// The layouts a stored view reads back as; one the store does not keep yet reads as a grid. A\n// saved timeline read back as \"grid\" was the reload that lost it (FDT, 2026-10-03).\nconst KINDS: readonly string[] = STORE_KEPT_LAYOUTS;\n\n/** THE READER. A stored `definition` (and the row's flat `filters`) in, every declared key out. */\nexport function viewSettings(definition: Record<string, unknown> | null | undefined, rowFilters?: RecordFilter | null): ViewSettings {\n const data = definition ?? {};\n const text = (key: string): string | null => {\n const v = data[key];\n return typeof v === \"string\" && v !== \"\" ? v : null;\n };\n const layout = String(data[\"layout\"] ?? \"grid\");\n const flat = rowFilters ?? (isObject(data[\"filters\"]) ? (data[\"filters\"] as RecordFilter) : null);\n const moved = data[\"moved_from\"];\n return {\n layout: (KINDS as readonly string[]).includes(layout) ? (layout as ViewSettings[\"layout\"]) : \"grid\",\n filters: flat && Object.keys(flat).length > 0 ? flat : null,\n where: isRule(data[\"where\"]) ? (data[\"where\"] as RuleExpression) : null,\n groupField: text(\"group_field\"),\n swimlaneField: text(\"swimlane_field\"),\n collapsedColumns: Array.isArray(data[\"collapsed_columns\"])\n ? (data[\"collapsed_columns\"] as unknown[]).filter((v) => [\"string\", \"number\", \"boolean\"].includes(typeof v)).map(String)\n : [],\n measure: text(\"measure\"),\n dateField: text(\"date_field\"),\n startField: text(\"start_field\"),\n endField: text(\"end_field\"),\n imageField: text(\"image_field\"),\n sorts: parseSorts(data[\"sorts\"]),\n ruleId: text(\"rule_id\"),\n isDefault: data[\"is_default\"] === true || data[\"is_default\"] === \"true\",\n presentation: parseGridPresentation(data[\"presentation\"]),\n movedFrom: isObject(moved) ? (moved as Record<string, unknown>) : null,\n };\n}\n\n/** The settings as the spec names them — what `viewSettingsPatch` takes. */\nexport type ViewSettingsPatch = Partial<Omit<ViewSettings, \"movedFrom\" | \"layout\">> & {\n /** Any layout the package draws; one the store does not keep is not sent. */\n layout?: string;\n};\n\n/**\n * THE KEYS ONE LAYOUT READS, and nothing else. A layout that does not use a key never sees it:\n * a calendar is not told the board's swimlanes, a board is not told the grid's pinned fields.\n */\nexport function viewSettingsFor(settings: ViewSettings, layout: ViewKeyLayout): Partial<ViewSettings> {\n const reads = new Set(viewKeysFor(layout));\n const out: Partial<ViewSettings> = { layout: settings.layout };\n const take = <K extends keyof ViewSettings>(key: K, path: string) => {\n if (reads.has(path)) out[key] = settings[key];\n };\n take(\"filters\", \"filters\");\n take(\"where\", \"where\");\n take(\"groupField\", \"group_field\");\n take(\"swimlaneField\", \"swimlane_field\");\n take(\"collapsedColumns\", \"collapsed_columns\");\n take(\"measure\", \"measure\");\n take(\"dateField\", \"date_field\");\n take(\"startField\", \"start_field\");\n take(\"endField\", \"end_field\");\n take(\"imageField\", \"image_field\");\n take(\"sorts\", \"sorts\");\n take(\"ruleId\", \"rule_id\");\n take(\"isDefault\", \"is_default\");\n const p = settings.presentation;\n const look: GridPresentation = {};\n for (const [key, value] of Object.entries(p)) {\n if (reads.has(`presentation.${key}`)) (look as Record<string, unknown>)[key] = value;\n }\n out.presentation = look;\n return out;\n}\n\n/**\n * THE WRITER'S SHAPE. Only the keys a press changed; a cleared Field is `null` (the door removes\n * it); lists are sent whole. `presentation` is sent whole because the door judges it as one key.\n */\nexport function viewSettingsPatch(patch: ViewSettingsPatch): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n const field = (key: keyof ViewSettingsPatch, path: string) => {\n if (patch[key] !== undefined) out[path] = (patch[key] as string | null) || null;\n };\n // A layout the store does not keep yet is held for this visit, never sent (`STORE_KEPT_LAYOUTS`).\n if (patch.layout !== undefined && storeKeepsLayout(patch.layout)) out[\"layout\"] = patch.layout;\n field(\"groupField\", \"group_field\");\n field(\"swimlaneField\", \"swimlane_field\");\n field(\"measure\", \"measure\");\n field(\"dateField\", \"date_field\");\n // Held for the visit until the store keeps them (`STORE_KEEPS_TIMELINE`).\n if (STORE_KEEPS_TIMELINE) {\n field(\"startField\", \"start_field\");\n field(\"endField\", \"end_field\");\n }\n field(\"imageField\", \"image_field\");\n field(\"ruleId\", \"rule_id\");\n if (patch.collapsedColumns !== undefined) out[\"collapsed_columns\"] = patch.collapsedColumns ?? [];\n if (patch.sorts !== undefined) out[\"sorts\"] = patch.sorts ?? [];\n if (patch.where !== undefined) out[\"where\"] = patch.where ?? null;\n if (patch.isDefault !== undefined) out[\"is_default\"] = patch.isDefault === true;\n if (patch.presentation !== undefined) {\n out[\"presentation\"] =\n !patch.presentation || presentationIsEmpty(patch.presentation)\n ? null\n : (JSON.parse(presentationDocument(patch.presentation)) as Record<string, unknown>);\n }\n return out;\n}\n\nfunction isObject(raw: unknown): boolean {\n return Boolean(raw) && typeof raw === \"object\" && !Array.isArray(raw);\n}\n\nfunction isRule(raw: unknown): boolean {\n if (!isObject(raw)) return false;\n const node = raw as { op?: unknown; args?: unknown };\n return typeof node.op === \"string\" && Array.isArray(node.args);\n}\n\nfunction parseSorts(raw: unknown): ViewSortKey[] {\n let list: unknown = raw;\n if (typeof raw === \"string\") {\n try {\n list = raw.trim() === \"\" ? [] : JSON.parse(raw);\n } catch {\n return [];\n }\n }\n if (!Array.isArray(list)) return [];\n return list\n .filter((s): s is { field: string; direction?: string } => isObject(s) && typeof (s as { field?: unknown }).field === \"string\")\n .map((s) => ({ field: s.field, direction: s.direction === \"desc\" ? \"desc\" : \"asc\" }));\n}\n","\"use client\";\n\n// src/views.ts — @ai-matrx/records-ui · SCR-6, SCR-7, SCR-N-6\n//\n// A SAVED VIEW IS ONE ROW OF THE ONE SAVED-VIEW STORE, `platform.saved_view`\n// (chair ruling 2026-09-23, lane S0 ONE-SAVED-VIEW), read through `custom.views`\n// and written through `custom.view_declare` — the same door the digests, the\n// notify rules, the agent's boards, the grid's choices (G1/G7), the hand-set\n// order (G13) and the mover use. It owns its layout and REFERENCES its\n// membership rule.\n//\n// 🚨 THERE WERE TWO STORES (UI-CHAMPIONS-PLAN-ATTACK hole 1). Until S0 this bar\n// kept each view as a record of a package-owned `records_ui_view` Table in every\n// organization, beside `platform.saved_view`: a view the mover carried in never\n// reached the bar, a view saved in the bar was invisible to every subscription,\n// and a grid choice G1 kept had no screen that read it. The repair migration\n// `oneview_every_view_bar_view_moves_into_the_one_store.sql` moved every one of\n// those records into `platform.saved_view` UNDER THE SAME ID and archived the\n// Tables; nothing in this package reads or writes them any more.\n//\n// The split matters. A view does not carry a filter predicate of its own that a\n// screen re-implements in JavaScript: it names a Rule, and the STORE answers\n// who is in it (`custom.rule_members`). So the same view means the same thing\n// to a grid, to an export, to an agent and to the server. What the view owns\n// itself is presentation — which of the four layouts, which Field the kanban\n// columns come from, which Field the calendar reads, which Field the gallery\n// shows — because that is not data, it is how this person wants to look at it.\n//\n// The whole view is also a plain declarative object (`SavedViewSpec`), which is\n// what makes \"an agent writes the view in 45 seconds\" true: one `view_declare`\n// of one definition, and the person only tweaks.\n\nimport { presentationDocument, presentationIsEmpty, type GridPresentation } from \"./presentation\";\nimport { STORE_KEEPS_TIMELINE, storeKeepsLayout, viewSettings, viewSettingsPatch } from \"./viewKeys\";\nexport { STORE_KEEPS_TIMELINE, STORE_KEPT_LAYOUTS, storeKeepsLayout } from \"./viewKeys\";\nimport type { RecordFilter, RecordsResult, RuleExpression, SavedViewRow, Uuid } from \"@ai-matrx/records\";\nimport type { RecordsClient } from \"@ai-matrx/records/core\";\n\n/** The ways one saved view can be looked at (SCR-6; the timeline, lane VIEWS-AND-FIELDS TL). */\nexport const VIEW_LAYOUTS = [\"grid\", \"kanban\", \"calendar\", \"gallery\", \"timeline\", \"list\"] as const;\nexport type ViewLayout = (typeof VIEW_LAYOUTS)[number];\n\nexport const LAYOUT_LABEL: Record<ViewLayout, string> = {\n grid: \"Grid\",\n kanban: \"Kanban\",\n calendar: \"Calendar\",\n gallery: \"Gallery\",\n timeline: \"Timeline\",\n list: \"List\",\n};\n\n\n/**\n * ─────────────────────────────────────────────────────────────────────────────\n * WHAT EACH LAYOUT NEEDS FROM THE TABLE, AND HOW THE SCREEN SUPPLIES IT\n * (lane DATA-V2-VIEWS-1, Arman 2026-09-28).\n *\n * THE DEFECT. A plain table opened as a Kanban said \"A kanban board needs a Field to make its\n * columns from … Add column, then Select — and it appears here.\" It told the person what to do\n * instead of doing it, and on an AI platform it offered no AI. The ruling: ONE primitive, used by\n * every view (`ViewNeeds`) — the empty state says in one plain sentence what is missing and what\n * pressing will do, offers \"Make it work\" (the column is declared through the store's\n * `custom.field_declare`, its key minted by the store, its name and choices editable before the\n * press) and \"Ask AI\" (the agent does it through the same door, held for the person's approval by\n * the organization's \"Agent changes to this organization's data\" setting).\n *\n * THE CENSUS — every layout this package draws, and what it reads:\n * · grid nothing.\n * · kanban a CHOICE column (its options become the board's columns). Blocking.\n * · calendar a DATE column (it places each record on a day). Blocking.\n * · gallery a FILE column for the card's picture. NOT blocking: the cards draw without one,\n * so the offer sits above them instead of in place of them.\n * · timeline a DATE column each bar starts on (the calendar's `dateField`). Blocking.\n * `PAGE_VIEWS`' two extra values (dashboards, archived) read\n * nothing a table can lack. This package ships no map layout; the day it does,\n * `Record<ViewLayout, …>` below refuses to compile until the new layout names what it needs.\n */\nexport type ViewNeedKind = \"choice\" | \"date\" | \"file\";\n\nexport interface ViewNeed {\n /** What the layout reads, as a kind a column declares (`offerableFields`). */\n kind: ViewNeedKind;\n /** The view key the made column is written onto, so the layout opens drawn by it. */\n viewKey: \"groupField\" | \"dateField\" | \"imageField\";\n /** True when the layout cannot draw at all without it (kanban, calendar). */\n blocking: boolean;\n /** The column the press makes, before the person edits it: its name, the store's word, its choices. */\n starter: { label: string; type: \"select\" | \"datetime\" | \"attachment\"; options?: readonly string[] };\n /** The table HAS columns this layout can use and none is chosen: the sentence over the picker. */\n pick: string;\n /** One plain sentence: what the table is missing (all a person who may not change it is told). */\n missing: string;\n /** One plain sentence: what pressing Make it work will do, with the column's name. */\n does: (label: string) => string;\n /** The sentence an agent is asked with — the same act, said in words. */\n ask: (label: string, options: readonly string[]) => string;\n}\n\n/** A list of words as a person says it: \"To do, In progress and Done\". */\nexport function wordsList(words: readonly string[]): string {\n const clean = words.map((w) => w.trim()).filter((w) => w !== \"\");\n if (clean.length <= 1) return clean.join(\"\");\n return `${clean.slice(0, -1).join(\", \")} and ${clean[clean.length - 1]}`;\n}\n\nexport const VIEW_NEEDS: Record<ViewLayout, ViewNeed | null> = {\n grid: null,\n // A list reads nothing a table can lack: rows, titled, grouped only when the person chooses.\n list: null,\n kanban: {\n kind: \"choice\",\n viewKey: \"groupField\",\n blocking: true,\n starter: { label: \"Status\", type: \"select\", options: [\"To do\", \"In progress\", \"Done\"] },\n pick: \"Pick the choice column the board's columns come from.\",\n missing: \"This table has no choice column to make the board's columns from.\",\n does: (label) => `Make it work adds a ${label} column with the choices below and groups the board by it.`,\n ask: (label, options) =>\n `Add a choice column called ${label} to this table${options.length > 0 ? ` with the choices ${wordsList(options)}` : \"\"}, then group the Kanban board by it.`,\n },\n calendar: {\n kind: \"date\",\n viewKey: \"dateField\",\n blocking: true,\n starter: { label: \"Date\", type: \"datetime\" },\n pick: \"Pick the date column the calendar places records by.\",\n missing: \"This table has no date column to place records on.\",\n does: (label) => `Make it work adds a ${label} column and lays the calendar out by it.`,\n ask: (label) => `Add a date column called ${label} to this table, then lay the Calendar out by it.`,\n },\n gallery: {\n kind: \"file\",\n viewKey: \"imageField\",\n blocking: false,\n starter: { label: \"Photo\", type: \"attachment\" },\n pick: \"Pick the file column each card shows as its picture.\",\n missing: \"The cards have no picture because this table has no file column.\",\n does: (label) => `Make it work adds a ${label} column and shows it on each card.`,\n ask: (label) => `Add a file column called ${label} to this table, then show it as the picture on each Gallery card.`,\n },\n timeline: {\n kind: \"date\",\n viewKey: \"dateField\",\n blocking: true,\n starter: { label: \"Start\", type: \"datetime\" },\n pick: \"Pick the date column each bar starts on.\",\n missing: \"This table has no date column to place records on.\",\n does: (label) => `Make it work adds a ${label} column and lays the timeline out by it.`,\n ask: (label) => `Add a date column called ${label} to this table, then lay the Timeline out by it.`,\n },\n};\n\n/**\n * A NAME NO COLUMN OF THIS TABLE ALREADY HAS: the starter's own, else \"Status 2\", \"Status 3\"…\n * A table that already holds a text column called Status must not be refused the board.\n */\nexport function freeColumnName(wanted: string, taken: readonly string[]): string {\n const held = new Set(taken.map((t) => t.trim().toLowerCase()));\n const base = wanted.trim() === \"\" ? \"Column\" : wanted.trim();\n if (!held.has(base.toLowerCase())) return base;\n for (let n = 2; n < 100; n += 1) {\n const next = `${base} ${n}`;\n if (!held.has(next.toLowerCase())) return next;\n }\n return base;\n}\n\n/** One sort clause. The Field's key and a direction; nothing else is a sort. */\nexport interface ViewSort {\n field: string;\n direction: \"asc\" | \"desc\";\n}\n\n/** How a saved view is ordered: by hand, by its sort, or it never said (the same as sorted). */\nexport type ViewOrder = \"manual\" | \"sorted\" | null;\n\n/** The order word a stored definition carries. Only the two words the store writes are read. */\nexport function viewOrderOf(definition: Record<string, unknown> | null | undefined): ViewOrder {\n const raw = definition?.[\"order\"];\n return raw === \"manual\" ? \"manual\" : raw === \"sorted\" ? \"sorted\" : null;\n}\n\n/**\n * THE DECLARATIVE SPEC. An agent writes this whole object; a person tweaks it.\n * Every builder component in this package takes one of these as a prop.\n */\nexport interface SavedViewSpec {\n name: string;\n /** The Table this view looks at. */\n subject: Uuid;\n layout: ViewLayout;\n /** Kanban: the Field whose value becomes a column. */\n groupField?: string | null;\n /**\n * Kanban: a number Field to TOTAL under each column heading. It lives on the VIEW for\n * the same reason the colours do — two people read one board two ways and neither\n * reading is a fact about the records.\n */\n measure?: string | null;\n /**\n * Kanban: the Field whose values cut the board into SWIMLANES across the columns — a\n * dispatcher's board by Status, one lane per Technician (S1-PRIME VIEW-KEYS). Never the\n * column Field itself; the store refuses that.\n */\n swimlaneField?: string | null;\n /** Kanban: the columns this view keeps shut to a strip, by the column's stored value. */\n collapsedColumns?: string[];\n /** Calendar: the Field that places a record in the month. */\n dateField?: string | null;\n /** Timeline: the Field a record's bar starts on (`start_field`). */\n startField?: string | null;\n /** Calendar and timeline: the Field a record ends on (`end_field`). */\n endField?: string | null;\n /** Gallery: the Field shown large on the card. */\n imageField?: string | null;\n sorts?: ViewSort[];\n /**\n * HOW THE VIEW IS ORDERED (ORDER-FIX): `\"manual\"` — the positions a person set by hand ARE the\n * view's sort, and it carries no `sorts`; `\"sorted\"` or null — by `sorts`. READ ONLY: the store\n * writes \"manual\" only when the rows are placed (`custom.view_record_order_set`) and turns it\n * off itself when a sort is saved, so no patch ever sends it (`viewSettingsPatch` does not).\n */\n order?: ViewOrder;\n /**\n * The membership Rule. The STORE decides who is in this view; this is a\n * reference to its Rule record, never a predicate this package evaluates.\n */\n ruleId?: Uuid | null;\n /**\n * THE FLAT FILTER a digest or a notify rule wrote the view with — the one shape\n * `custom.record_filter_sql` compiles, by Field KEY. A view saved in the bar has\n * none; a view saved by `DigestScheduler` (\"where Status is Open\") does, and the\n * grid and the board show exactly the records it names.\n */\n filters?: RecordFilter;\n /**\n * S2-PRIME — THE VIEW'S OWN QUESTION, as the condition builder writes it: the store's Rule\n * shape `{op, args}`, ALL / ANY / NOT groups nested to any depth, Fields by id. Kept in the\n * definition as `where` and asked of the store unchanged — the grid's list door, the board's\n * headings and the dashboard's numbers compile it into the one WHERE clause\n * (`custom.record_filter_sql`). Never stored under `filters`: that key is the flat map the\n * digests and the notifier read, and they would refuse it by name.\n */\n where?: RuleExpression | null;\n isDefault?: boolean;\n /**\n * HOW THIS VIEW LOOKS — its colors, its per-column formats and its frozen\n * column (`presentation.ts`).\n *\n * It lives on the VIEW and not on the Fields because two people read one\n * table two ways and neither reading is a fact about the records; it lives on\n * a RECORD, not a new column, because that is what makes it something an\n * agent writes with the same `records` tool as everything else.\n */\n presentation?: GridPresentation;\n}\n\n/**\n * A saved view as it comes back out of the store, with its row's own id.\n *\n * It carries no version: `custom.view_declare` MERGES one key at a time, so two\n * tabs pressing two different controls on one view both stand, and one control\n * pressed in both keeps the latest — how Airtable and Notion keep a view change.\n */\nexport interface SavedView extends SavedViewSpec {\n id: Uuid;\n /** Where the view came from when it was moved in (the mover, the S0 repair), else null. */\n movedFrom: Record<string, unknown> | null;\n}\n\n/** THE TABLE'S DEFAULT VIEW: the first view that says it is. */\nexport function defaultViewOf<V extends Pick<SavedView, \"isDefault\">>(views: readonly V[]): V | undefined {\n return views.find((v) => v.isDefault);\n}\n\n/**\n * WHICH FIELD EACH LAYOUT CAN BE DRAWN FROM, decided from the FIELD'S OWN\n * DECLARATION and never from the values it happens to hold.\n *\n * A kanban makes columns from a choice: the Field whose value is one of a\n * closed set (FLD-5 / FLD-6 — a pick-list is a Table, and a select stores the\n * id of one of its records). Grouping by a free-text Field produces one column\n * per record, which is not a board. A calendar places a record in time, so it\n * needs a Field that holds a date.\n *\n * These two answers are here, beside the layouts, because the picker on the\n * screen and any agent writing a view spec must agree about what is offerable.\n */\nexport const LAYOUT_FIELD_KIND: Record<ViewLayout, ViewNeedKind | null> = {\n grid: null,\n kanban: \"choice\",\n calendar: \"date\",\n gallery: \"file\",\n timeline: \"date\",\n list: null,\n};\n\n/**\n * ─────────────────────────────────────────────────────────────────────────────\n * WHICH VIEW A LINK NAMES — the address bar as a first-class control.\n *\n * 🚨 THE DEFECT THIS CLOSES (lane FRONT-DOOR, 2026-09-21; VERIFIER-8 HIGH-2).\n * `/data/<table>?view=kanban` returned 200 and rendered THE GRID. The\n * switcher was there, the four buttons worked, and the parameter was read by\n * nobody — so a person who bookmarked a board, or sent one to a colleague, got\n * a grid, and nothing on the screen said the view had been dropped. A link that\n * does not land is the inbox-link defect (`activeRecordId`) and the dashboard\n * defect (`activeDashboardId`) for the third time, so this is the same shape of\n * fix: the URL names it, the page opens on it, and the URL FOLLOWS the person\n * when they pick another.\n *\n * The dashboards are a fifth value and not a special case: to the person, \"the\n * dashboard\" is one of the ways to look at this table, and a link to it has to\n * work like a link to the board.\n */\n/**\n * 🚨 `archived` IS THE SIXTH VALUE, AND IT IS NOT A SPECIAL CASE EITHER (lane\n * FIX-10A-ARCHIVE, 2026-09-22; VERIFIER-10 F4).\n *\n * Archiving a record used to be a ONE-WAY DOOR: the grid's row action soft-\n * deletes correctly — `deleted_at` set, version bumped, nothing removed — and\n * then no screen anywhere in the platform could bring it back. The store's\n * `custom.record_restore` was there the whole time and did exactly the right\n * thing; nothing reached it, because every read door answers live rows only.\n *\n * So the archive is a WAY OF LOOKING AT THIS TABLE, like the board and the\n * dashboards, reached by the same row of buttons and named by the same\n * `?view=` parameter — never a Trash area of the app somebody has to know\n * exists, and never a per-table screen. Every Table inherits it the moment it\n * is in this list.\n */\nexport const PAGE_VIEWS = [...VIEW_LAYOUTS, \"dashboards\", \"archived\"] as const;\nexport type PageView = (typeof PAGE_VIEWS)[number];\n\nexport const PAGE_VIEW_LABEL: Record<PageView, string> = {\n ...LAYOUT_LABEL,\n dashboards: \"Dashboards\",\n archived: \"Archived\",\n};\n\n/**\n * THE WORDS A LINK MAY USE. The canonical token first; the rest are what a\n * person or an agent actually types, because a link that dies on \"board\" when\n * the button says \"Kanban\" is the same dead link with better spelling.\n */\nconst PAGE_VIEW_SPELLINGS: Record<string, PageView> = {\n grid: \"grid\",\n table: \"grid\",\n rows: \"grid\",\n list: \"list\",\n kanban: \"kanban\",\n board: \"kanban\",\n pipeline: \"kanban\",\n calendar: \"calendar\",\n month: \"calendar\",\n schedule: \"calendar\",\n gallery: \"gallery\",\n cards: \"gallery\",\n timeline: \"timeline\",\n gantt: \"timeline\",\n dashboards: \"dashboards\",\n dashboard: \"dashboards\",\n charts: \"dashboards\",\n archived: \"archived\",\n archive: \"archived\",\n // The grid's row action says \"Delete\" and the store soft-deletes, so the word\n // a person types is as likely to be the one the button used as the one this\n // view is called. Both land in the same place; neither dies.\n deleted: \"archived\",\n trash: \"archived\",\n removed: \"archived\",\n};\n\n/**\n * What a `?view=` value means, or `null` when it means nothing here.\n *\n * `null` is NOT \"show the grid\": the caller is expected to say that the link\n * named something this table has no view for, because silently substituting a\n * different view is exactly the defect above wearing a parser.\n */\nexport function pageViewFromParam(raw: string | null | undefined): PageView | null {\n if (typeof raw !== \"string\") return null;\n const token = raw.trim().toLowerCase();\n if (token === \"\") return null;\n return PAGE_VIEW_SPELLINGS[token] ?? null;\n}\n\n/** The sentence a link that named a view nobody has says. Never silent. */\nexport function unknownViewLine(raw: string): string {\n return (\n `The link asked for a view called “${raw}”, which this table has no view for, so you are looking at ` +\n `${PAGE_VIEW_LABEL.grid}. The views this table has are ` +\n `${PAGE_VIEWS.map((v) => PAGE_VIEW_LABEL[v]).join(\", \")} — pick one above and the address follows it.`\n );\n}\n\n/**\n * THE SPEC AS THE ONE DEFINITION `platform.saved_view` HOLDS — the keys\n * `custom.view_declare` documents, by Field KEY. `subject` is not in it: the door\n * takes the Table as its own argument and writes `table_id` itself.\n */\nexport function viewDocument(spec: SavedViewSpec): Record<string, unknown> {\n const out: Record<string, unknown> = {\n // A layout the store does not keep yet is not sent: the view is saved as a grid (`STORE_KEPT_LAYOUTS`).\n layout: storeKeepsLayout(spec.layout) ? spec.layout : \"grid\",\n sorts: spec.sorts ?? [],\n is_default: spec.isDefault === true,\n };\n if (spec.groupField) out[\"group_field\"] = spec.groupField;\n if (spec.swimlaneField) out[\"swimlane_field\"] = spec.swimlaneField;\n if (spec.collapsedColumns && spec.collapsedColumns.length > 0) out[\"collapsed_columns\"] = spec.collapsedColumns;\n if (spec.measure) out[\"measure\"] = spec.measure;\n if (spec.dateField) out[\"date_field\"] = spec.dateField;\n if (STORE_KEEPS_TIMELINE && spec.startField) out[\"start_field\"] = spec.startField;\n if (STORE_KEEPS_TIMELINE && spec.endField) out[\"end_field\"] = spec.endField;\n if (spec.imageField) out[\"image_field\"] = spec.imageField;\n if (spec.ruleId) out[\"rule_id\"] = spec.ruleId;\n if (spec.where) out[\"where\"] = spec.where;\n if (spec.presentation && !presentationIsEmpty(spec.presentation)) {\n out[\"presentation\"] = JSON.parse(presentationDocument(spec.presentation)) as Record<string, unknown>;\n }\n return out;\n}\n\n/**\n * One row of `custom.views` back into the spec a screen and an agent both read — THROUGH THE ONE\n * ACCESSOR (`viewSettings`, viewKeys.ts), so every layout sees every declared key the same way.\n */\nexport function viewFromRow(row: SavedViewRow): SavedView {\n const data = (row.definition ?? {}) as Record<string, unknown>;\n const k = viewSettings(data, row.filters && Object.keys(row.filters).length > 0 ? row.filters : null);\n const tableFromDefinition = typeof data[\"table_id\"] === \"string\" && data[\"table_id\"] !== \"\" ? (data[\"table_id\"] as string) : null;\n return {\n id: row.view_id,\n name: row.name || \"Untitled view\",\n subject: (row.table_id ?? tableFromDefinition ?? \"\") as Uuid,\n layout: k.layout,\n groupField: k.groupField,\n swimlaneField: k.swimlaneField,\n collapsedColumns: k.collapsedColumns,\n measure: k.measure,\n dateField: k.dateField,\n startField: k.startField,\n endField: k.endField,\n imageField: k.imageField,\n sorts: k.sorts,\n order: viewOrderOf(data),\n ruleId: k.ruleId,\n ...(k.filters ? { filters: k.filters } : {}),\n where: k.where,\n // The older table's own default view, carried in by the mover, IS this table's default —\n // the mover wrote the row's column, which `custom.views` does not return. Read as anything\n // else, the page would seed a second \"All records\" beside it and open on that.\n isDefault: k.isDefault || k.movedFrom?.[\"kind\"] === \"default_view\",\n presentation: k.presentation,\n movedFrom: k.movedFrom,\n };\n}\n\n/**\n * SAVE ONE NEW VIEW of a Table — the bar's \"New view\" and an agent's seed spec.\n * One `view_declare` with no `view_id`; the answer is the new view's id.\n */\nexport async function declareView(\n client: Pick<RecordsClient, \"viewDeclare\">,\n spec: SavedViewSpec,\n): Promise<RecordsResult<Uuid>> {\n return client.viewDeclare({\n table_id: spec.subject,\n spec: {\n name: spec.name,\n ...(spec.filters ? { filters: spec.filters } : {}),\n definition: viewDocument(spec),\n },\n });\n}\n\n/**\n * WRITE A CHANGE ONTO A SAVED VIEW, AND KEEP IT.\n *\n * `custom.view_declare` with the view's id MERGES: only the keys in `patch` are\n * written, a key cleared to null is removed, and everything the patch did not\n * name — the grid's own choices, a hand-set order, where the view was moved from,\n * a colour set in another tab — stays exactly as it was (S0, 2026-09-23: the door\n * used to rebuild the whole definition from what one press sent).\n *\n * There is no version to be stale against (VERIFIER-15 M4 is closed by the shape,\n * not by a retry): a saved view is a person's own arrangement of a table, and\n * their latest press on a control is the one that stands. A refusal — the view\n * was removed, or belongs to a table this person cannot open — is handed back for\n * the page to say through the one notice.\n */\nexport async function saveViewPatch(\n client: Pick<RecordsClient, \"viewDeclare\">,\n tableId: Uuid,\n viewId: Uuid,\n patch: Partial<SavedViewSpec>,\n): Promise<RecordsResult<Uuid>> {\n const definition = viewPatchDocument(patch);\n const renamed = patch.name !== undefined && patch.name.trim() !== \"\";\n const refiltered = patch.filters !== undefined;\n return client.viewDeclare({\n table_id: tableId,\n spec: {\n view_id: viewId,\n ...(renamed ? { name: patch.name!.trim() } : {}),\n ...(refiltered ? { filters: patch.filters ?? null } : {}),\n definition,\n },\n });\n}\n\n/**\n * A PARTIAL change to a saved view, as the definition keys the door merges. Only\n * the keys the caller actually changed are sent; a Field cleared (a board with no\n * \"Group by\") is sent as `null`, which the door removes.\n */\nexport function viewPatchDocument(patch: Partial<SavedViewSpec>): Record<string, unknown> {\n const { name: _name, subject: _subject, filters: _filters, ...keys } = patch;\n return viewSettingsPatch(keys);\n}\n\n/**\n * A LOOK AT A TABLE NEVER CHANGES HOW IT OPENS (lane VIEW-SWITCH-NOT-DESIGNATION, VERIFIER-21).\n *\n * On production the owner pressed Kanban, then Calendar, and the office member's table opened in\n * the Calendar: every layout press was saved onto the DEFAULT view, which is how the table opens\n * for everyone. The table's default view's `layout` and `isDefault` are its DESIGNATION, changed\n * only by an editor's deliberate \"Make this the default\" (`designateView`); the store refuses them\n * through `view_declare` by name. So a press on the default view carries every other key it\n * changed — a board's grouping, a calendar's date, a colour — and never those two.\n */\nexport function viewPatchFor(\n view: Pick<SavedView, \"isDefault\">,\n patch: Partial<SavedViewSpec>,\n): Partial<SavedViewSpec> {\n if (!view.isDefault) return patch;\n const { layout: _layout, isDefault: _isDefault, ...kept } = patch;\n return kept;\n}\n\n/** The one words for the explicit designation, with the layout it would make the default. */\nexport function makeDefaultLabel(layoutLabel: string): string {\n return `Make ${layoutLabel} the default`;\n}\n\n/**\n * MAKE THIS THE DEFAULT — the ONE designation door (`custom.view_designate`, editor on the table).\n * `layout` is the layout this table should open in for everyone: one of the layouts the store keeps. The view\n * becomes the table's only default.\n */\nexport async function designateView(\n client: Pick<RecordsClient, \"viewDesignate\">,\n tableId: Uuid,\n viewId: Uuid,\n layout: string | null,\n): Promise<RecordsResult<Uuid>> {\n return client.viewDesignate({ table_id: tableId, view_id: viewId, layout });\n}\n","\"use client\";\n\n// src/viewLook.ts — @ai-matrx/records-ui · lane VIEW-STATE-PERSONAL (VERIFIER-23 item 1, 2026-09-25)\n//\n// A LOOK AT A VIEW IS YOURS UNTIL YOU SAVE IT.\n//\n// On production the owner of a maintenance-requests table grouped its board by Status, and the\n// tenant liaison's board came up grouped by Status the next time she opened it: every pick was\n// written onto the shared view. Airtable and Notion keep the two apart — how you are looking at a\n// shared view (its grouping, its date field, its sort, its filter, hidden columns, widths, the\n// layout) is YOUR look, it persists for you, and the view changes only when you press \"Save to\n// view\" (Notion's \"Save for everyone\"), save it as a new view, or \"Reset to view\".\n//\n// The store keeps one look per (person, view) through `custom.view_look_set` / `view_look_read`\n// (viewer on the table, never written onto the view row). This file is the look's shape on this\n// side: the keys a look may hold, how it reads back, how it lays over the view, and what \"Save to\n// view\" sends.\n\nimport type { RecordsResult, Uuid } from \"@ai-matrx/records\";\nimport type { RecordsClient } from \"@ai-matrx/records/core\";\n\nimport { parseGridPresentation } from \"./presentation\";\nimport { viewSettings, viewSettingsPatch } from \"./viewKeys\";\nimport { VIEW_LAYOUTS, viewPatchFor, type SavedView, type SavedViewSpec, type ViewLayout } from \"./views\";\n\n/**\n * WHAT A LOOK MAY HOLD — how a person looks, never what the view is: not its name, not whether it\n * is the default (`view_designate`), not its Rule, not the flat filters the digests read, not the\n * order the store writes. The store refuses anything else by name.\n */\nexport type ViewLook = Partial<\n Pick<\n SavedViewSpec,\n | \"layout\"\n | \"groupField\"\n | \"swimlaneField\"\n | \"collapsedColumns\"\n | \"measure\"\n | \"dateField\"\n | \"startField\"\n | \"endField\"\n | \"imageField\"\n | \"sorts\"\n | \"where\"\n | \"presentation\"\n >\n>;\n\nconst LOOK_KEYS: ReadonlyArray<keyof ViewLook> = [\n \"layout\",\n \"groupField\",\n \"swimlaneField\",\n \"collapsedColumns\",\n \"measure\",\n \"dateField\",\n \"startField\",\n \"endField\",\n \"imageField\",\n \"sorts\",\n \"where\",\n \"presentation\",\n];\n\n/** Only the look's own keys of a press — a name, a default, a Rule or an order is never a look. */\nexport function lookPart(patch: Partial<SavedViewSpec>): ViewLook {\n const out: ViewLook = {};\n for (const key of LOOK_KEYS) {\n if (patch[key] !== undefined) (out as Record<string, unknown>)[key] = patch[key];\n }\n return out;\n}\n\n/** The look as the definition keys the store keeps (`view_look_set`); a cleared key is null. */\nexport function lookDocument(look: ViewLook): Record<string, unknown> {\n return viewSettingsPatch(look);\n}\n\n/** A kept look back into the spec's words. Only the keys it holds; a null key is \"none, for me\". */\nexport function lookFromDocument(doc: Record<string, unknown> | null | undefined): ViewLook {\n const data = doc ?? {};\n const read = viewSettings(data);\n const out: ViewLook = {};\n const has = (key: string) => Object.prototype.hasOwnProperty.call(data, key);\n if (has(\"layout\") && (VIEW_LAYOUTS as readonly string[]).includes(String(data[\"layout\"]))) {\n out.layout = data[\"layout\"] as ViewLayout;\n }\n if (has(\"group_field\")) out.groupField = read.groupField;\n if (has(\"swimlane_field\")) out.swimlaneField = read.swimlaneField;\n if (has(\"collapsed_columns\")) out.collapsedColumns = read.collapsedColumns;\n if (has(\"measure\")) out.measure = read.measure;\n if (has(\"date_field\")) out.dateField = read.dateField;\n if (has(\"start_field\")) out.startField = read.startField;\n if (has(\"end_field\")) out.endField = read.endField;\n if (has(\"image_field\")) out.imageField = read.imageField;\n if (has(\"sorts\")) out.sorts = read.sorts;\n if (has(\"where\")) out.where = read.where;\n if (has(\"presentation\")) out.presentation = parseGridPresentation(data[\"presentation\"]);\n return out;\n}\n\n/** THE VIEW AS THIS PERSON SEES IT: the saved view, with her look laid over it. */\nexport function applyLook(view: SavedView, look: ViewLook | null | undefined): SavedView {\n if (!look) return view;\n return { ...view, ...look } as SavedView;\n}\n\n/** Whether the look shows anything the view does not — which is when \"Save to view\" means something. */\nexport function lookDiffers(view: SavedView, look: ViewLook | null | undefined): boolean {\n if (!look) return false;\n const shown = applyLook(view, look);\n return LOOK_KEYS.some(\n (key) => look[key] !== undefined && JSON.stringify(shown[key] ?? null) !== JSON.stringify(view[key] ?? null),\n );\n}\n\n/**\n * WHAT \"SAVE TO VIEW\" SENDS, and what stays the person's own afterwards. The default view's layout\n * is the table's designation (`viewPatchFor`): it is never saved here — \"Make … the default\" is —\n * so a look's layout on the default view stays her look.\n */\nexport function saveToViewPlan(view: SavedView, look: ViewLook): { patch: Partial<SavedViewSpec>; left: ViewLook | null } {\n const patch = viewPatchFor(view, look);\n const left: ViewLook = {};\n if (look.layout !== undefined && patch.layout === undefined && look.layout !== view.layout) left.layout = look.layout;\n return { patch, left: Object.keys(left).length > 0 ? left : null };\n}\n\n/** The caller's own looks at every view of a table, by view id. */\nexport async function readLooks(\n client: Pick<RecordsClient, \"viewLookRead\">,\n tableId: Uuid,\n): Promise<RecordsResult<Record<string, ViewLook>>> {\n const answered = await client.viewLookRead({ table_id: tableId });\n if (!answered.ok) return answered;\n const out: Record<string, ViewLook> = {};\n for (const [viewId, row] of Object.entries(answered.data ?? {})) {\n out[viewId] = lookFromDocument(row?.look ?? null);\n }\n return { ok: true, data: out };\n}\n\n/** Keep the whole look (null: \"Reset to view\"). The shared view is never written. */\nexport async function keepLook(\n client: Pick<RecordsClient, \"viewLookSet\">,\n tableId: Uuid,\n viewId: Uuid,\n look: ViewLook | null,\n): Promise<RecordsResult<Record<string, unknown> | null>> {\n const doc = look ? lookDocument(look) : null;\n return client.viewLookSet({ table_id: tableId, view_id: viewId, look: doc && Object.keys(doc).length > 0 ? doc : null });\n}\n\n/** The words of the look bar. One sentence, then the doors. */\nexport const LOOK_WORDS = {\n /** The reason, for a tooltip or a screen reader — never a line on the page (interface-text). */\n yours: \"You changed how this view looks. Only you see it.\",\n /** The visible label for the same fact. */\n yoursLabel: \"Only you see this\",\n saveToView: \"Save to view\",\n saveAsNew: \"Save as new view\",\n reset: \"Reset to view\",\n /** The one button your look's doors fold into when the toolbar row has no room for three. */\n folded: \"Your look\",\n newName: \"Name for the new view\",\n} as const;\n","// src/firstPage.ts — @ai-matrx/records-ui/first-page · lane PAGE-BUNDLE-2\n//\n// THE GRID'S FIRST PAGE, ASKED BEFORE THE GRID EXISTS.\n//\n// A host that renders on a server (Next.js) can ask the table page's first rows while the browser\n// is still loading the app — but only if it asks with EXACTLY the arguments the grid will send, or\n// the records client cannot serve the grid from it (a part is used only on an exact door + args\n// match). This builds that request from the same pieces the screen uses: the view the bar opens\n// (`ViewBar`: the default view, else the first), the person's own look laid over it (`applyLook`),\n// the opening sort (`Grid`: the view's sort, else the table's `default_sort`, through\n// `pageSortFor`) and the table page's page size (100).\n//\n// IT ONLY PREDICTS THE PLAIN OPENING. A view grouped into sections, drilled, filtered, hand-ordered,\n// built on a Rule, or not a grid answers null — the grid asks for itself, exactly as before. A wrong\n// guess costs one wasted read and never a wrong row: the grid uses a seeded page only on an exact\n// match. No React here (server-safe entry).\n\nimport type { Field, RecordPageSort, Table, Uuid } from \"@ai-matrx/records\";\nimport type { RecordsClient, RecordsSeed } from \"@ai-matrx/records/core\";\nimport { asRecordsDataSource, createRecordsClient, recordingDataSource } from \"@ai-matrx/records/core\";\nimport type { RecordsConfig } from \"@ai-matrx/records\";\nimport { defaultSortOf } from \"./gridLook\";\nimport { pageSortFor } from \"./gridPage\";\nimport { applyLook, readLooks } from \"./viewLook\";\nimport { defaultViewOf, viewFromRow, type SavedView } from \"./views\";\n\n/** The table page's own page size (`TablePage`: 100 unless embedded). */\nexport const TABLE_PAGE_SIZE = 100;\n\n/**\n * HOW MANY ROWS AN EMBEDDED TABLE READS AT A TIME (CHAIR-EMBEDDED-FIDELITY 16): its first page, and\n * each \"Show more\" press brings about this many more. `embeddedLook.tsx` re-exports it.\n */\nexport const EMBEDDED_PAGE_SIZE = 50;\n\nexport interface FirstGridPageArgs {\n p_table_id: Uuid;\n p_filter: Record<string, never>;\n p_search: null;\n p_sort: RecordPageSort[];\n p_view_id: null;\n p_limit: number;\n p_offset: 0;\n}\n\n/** The request for the plain opening, from what the screen itself reads — or null. Pure. */\nexport function firstGridPageArgs(input: {\n tableId: Uuid;\n table: Pick<Table, \"default_sort\"> | null;\n fields: readonly Field[];\n /** The view as THIS person sees it (the saved view with her look over it), or null for none. */\n shown: SavedView | null;\n pageSize?: number;\n}): FirstGridPageArgs | null {\n const { shown, fields } = input;\n if (!shown) return null;\n if ((shown.layout ?? \"grid\") !== \"grid\") return null;\n if (shown.ruleId) return null;\n if (shown.order === \"manual\") return null;\n if (shown.filters && Object.keys(shown.filters).length > 0) return null;\n if (shown.presentation?.grouping) return null;\n const first = (shown.sorts ?? [])[0];\n const viewSort = first && fields.some((f) => f.key === first.field) ? { id: first.field, direction: first.direction } : null;\n const opening = viewSort ?? defaultSortOf(input.table, fields);\n return {\n p_table_id: input.tableId,\n p_filter: {},\n p_search: null,\n p_sort: pageSortFor(fields, opening),\n p_view_id: null,\n p_limit: input.pageSize ?? TABLE_PAGE_SIZE,\n p_offset: 0,\n };\n}\n\n/**\n * Read what the screen reads (through `client`, e.g. one served from `custom.table_page_bundle`)\n * and build the first page's request. Null when anything it needs is not answered.\n */\nexport async function firstGridPageRequest(\n client: Pick<RecordsClient, \"tableRead\" | \"fields\" | \"views\" | \"viewLookRead\">,\n tableId: Uuid,\n options: { pageSize?: number } = {},\n): Promise<FirstGridPageArgs | null> {\n const [table, fields, views, looks] = await Promise.all([\n client.tableRead({ table_id: tableId }),\n client.fields({ table_id: tableId }),\n client.views({ table_id: tableId }),\n readLooks(client, tableId),\n ]);\n if (!table.ok || !fields.ok || !views.ok || !looks.ok) return null;\n const mine = views.data.map(viewFromRow).filter((v) => v.subject === tableId);\n const view = defaultViewOf(mine) ?? mine[0] ?? null;\n const shown = view ? applyLook(view, looks.data[view.id]) : null;\n return firstGridPageArgs({ tableId, table: table.data, fields: fields.data, shown, ...(options.pageSize ? { pageSize: options.pageSize } : {}) });\n}\n\n/**\n * A TABLE PAGE'S SEED, ASKED BY THE SERVER AS THE PERSON (lane SSR-ROWS).\n *\n * Asks what the table page opens on — the table's bundle (`custom.table_page_bundle`, through the\n * records client exactly as the browser would), the grid's first page when `firstGridPageRequest`\n * can predict it, and on a record page the record's bundle — through a recording data source, and\n * answers every door answer it got as a `RecordsSeed`. The host hands that to\n * `<RecordsSeedProvider>` (`@ai-matrx/records/react`) above the table page: the page is drawn with\n * its rows in the server's HTML and hydrates onto them without asking again.\n *\n * `dataSource` is the person's own (a server supabase client carrying her session): every door\n * decides exactly as it does from the browser. Never throws; a door that refuses is simply not in\n * the seed, and the page asks it for itself.\n */\n/**\n * THE PERSON'S SERVER-ROWS KNOBS, FROM THE TABLE'S BUNDLE (lane SSR-ROWS-2): `custom.table_page_bundle`\n * carries a `server_rows` part — `data.server_rows` and `data.server_rows_budget_ms` resolved for the\n * person asking in that organization. Null when the seed holds no such answer (an older store, a\n * refused part): the host treats that as off.\n */\nexport function serverRowsOf(seed: RecordsSeed | null | undefined): { on: boolean; budgetMs: number | null } | null {\n const found = seed?.answers.find((a) => a.door === \"server_rows\");\n const data = found?.data as { on?: unknown; budget_ms?: unknown } | undefined;\n if (!data || typeof data !== \"object\") return null;\n return { on: data.on === true, budgetMs: typeof data.budget_ms === \"number\" && data.budget_ms > 0 ? data.budget_ms : null };\n}\n\nexport async function askTablePageSeed(input: {\n dataSource: unknown;\n organizationId: Uuid;\n tableId: Uuid;\n actor: RecordsConfig[\"actor\"];\n /** A record page: its record's bundle (`custom.record_page_bundle`) is asked beside the table's. */\n recordId?: Uuid | null;\n /**\n * Ask the grid's first page (default true). Off when the address opens something else. A function\n * decides once the table's shape (its bundle) has been answered — it is handed everything asked so\n * far, so a host can read the person's `server_rows` knob from the bundle (`serverRowsOf`) and ask\n * the rows only when it is on, at no extra read.\n */\n rows?: boolean | ((asked: RecordsSeed) => boolean);\n /** The table is drawn EMBEDDED (`TablePage presentation=\"embedded\"`, a Space's inline table): its page size. */\n embedded?: boolean;\n pageSize?: number;\n}): Promise<RecordsSeed> {\n const recording = recordingDataSource(asRecordsDataSource(input.dataSource));\n const client = createRecordsClient({\n dataSource: recording.source,\n organizationId: input.organizationId,\n actor: input.actor,\n onError: () => {},\n });\n const record = input.recordId\n ? Promise.resolve(\n recording.source.rpc(\"record_page_bundle\", { p_organization_id: input.organizationId, p_record_id: input.recordId }, { schema: \"custom\" }),\n ).catch(() => null)\n : null;\n try {\n const pageSize = input.pageSize ?? (input.embedded ? EMBEDDED_PAGE_SIZE : TABLE_PAGE_SIZE);\n const args = await firstGridPageRequest(client, input.tableId, { pageSize });\n const wantRows = typeof input.rows === \"function\" ? input.rows(recording.seed()) : input.rows !== false;\n if (args && wantRows) {\n await client.listPage({\n table_id: args.p_table_id,\n filter: args.p_filter,\n search: args.p_search,\n sort: args.p_sort,\n view_id: args.p_view_id,\n limit: args.p_limit,\n offset: args.p_offset,\n });\n }\n } catch {\n // a seed is a head start, never a requirement: whatever was answered is kept\n }\n if (record) await record;\n return recording.seed();\n}\n"]}