@ai-matrx/records-ui 0.115.28 → 0.115.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/index.cjs +7594 -7234
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +7068 -6688
- package/dist/index.js.map +1 -1
- package/dist/object-actions.cjs.map +1 -1
- package/dist/object-actions.d.cts +1 -1
- package/dist/object-actions.d.ts +1 -1
- package/dist/object-actions.js.map +1 -1
- package/dist/{objectActions-CV8VEz58.d.cts → objectActions-ac0_y8UY.d.cts} +1 -1
- package/dist/{objectActions-CV8VEz58.d.ts → objectActions-ac0_y8UY.d.ts} +1 -1
- package/dist/pickers.cjs.map +1 -1
- package/dist/pickers.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/objectActions.ts","../src/fieldTypes.ts","../src/rights.ts"],"sourcesContent":["// src/objectActions.ts — @ai-matrx/records-ui\n//\n// THE ONE ACTION LIST FOR A RECORD-STORE TABLE (lane TABLE-ACTIONS, v6 lane 1).\n//\n// Every menu a person meets on a table — the header ⋯, a row ⋯ on the Data\n// home, right-click, a card, the phone sheet, later ⌘K — is drawn from the\n// array `tableActions()` returns. Before this there were three hand-made lists\n// (`TABLE_MENU` in TablePage, `dataHomeRowActions` in matrx-frontend, the\n// Sheet's own v3 sections) and they drifted: no Archive, no Copy link, no\n// Duplicate, Rename and Share only as hand JSX beside the list.\n//\n// THE SHAPE IS COPIED, NOT INVENTED. It is the in-house registry pattern of\n// `features/agents/browse/agentActionRegistry.tsx` and `conversationActionRegistry.tsx`\n// in matrx-frontend: a context of HOST HANDLERS in, one ordered list out, every\n// renderer reading the same list. The one difference is deliberate: those two\n// build matrx-frontend's `ItemMenuConfig` directly, which this package cannot\n// import, so the list here is renderer-neutral (`ObjectAction[]`) and each\n// renderer has a pure adapter (`toDropdownEntries` / `toStripAndRows` here;\n// `toItemMenuConfig` and `toExtraSections` in matrx-frontend\n// `features/unified-data/actions/`).\n//\n// TWO INVARIANTS, each with a guard (`one-table-action-list-*.test.tsx`,\n// `rights-never-change-the-table-menus-shape.test.tsx`):\n// G1 — every renderer draws the same ids — none hidden, none added, disabled ones\n// drawn disabled with their reason — for every seat and with handlers missing.\n// G2 — rights change only `disabledReason`, never which ids appear, their order,\n// their verb or their rank (Airtable hides items from members; we beat it).\n// A missing host handler is the same: the action stays, disabled, with its reason.\n//\n// THE ORDER (handoff §4, the same everywhere):\n// Open · Open in new tab · Copy link | Rename · Duplicate · Move to… · Favorite |\n// Share… · Export… · Import… | Add column · Settings · History · Make … the default |\n// Built on it ▸ (Forms, Bookings, Checklists, Notifications, Portals, Inbox,\n// Dashboards, Archived records) | Archive table (last, destructive).\n// \"Add column\" and \"Make … the default\" are the header ⋯'s own entries before the\n// registry (TABLE_MENU and the designate item); they sit with Settings.\n//\n// VERB AND RANK — for the icon-strip menu. An action with a `verb` is one of the\n// platform's standard verbs (a strip icon); every other action has a `rank`, its\n// place among the feature rows. Neither changes the list's order: `toStripAndRows`\n// is a renderer that splits the same list, and G1 holds it to the same ids.\n//\n// No JSX here: this file is the `./object-actions` subpath, light enough for a\n// list row's menu to import without pulling every screen of the package.\n\nimport type { Table } from \"@ai-matrx/records\";\nimport { formatShortcut } from \"@ai-matrx/design-system/tap-target\";\nimport { ADD_FIELD_OPEN_LABEL } from \"./fieldTypes\";\nimport { LEVEL_WORD, levelNeededFor, type Capability, type WhatYouMayDo } from \"./rights\";\n\n/**\n * A Lucide icon NAME, kebab-case — the same currency `Table.icon` already uses.\n * Closed on purpose: each renderer maps every member to its own glyph, and a\n * new member fails that renderer's typecheck until it is mapped.\n */\nexport type ObjectActionIcon =\n | \"folder-open\"\n | \"external-link\"\n | \"link\"\n | \"pencil\"\n | \"copy\"\n | \"folder-input\"\n | \"star\"\n | \"star-off\"\n | \"share-2\"\n | \"download\"\n | \"upload\"\n | \"columns-3\"\n | \"settings\"\n | \"history\"\n | \"blocks\"\n | \"file-text\"\n | \"calendar-clock\"\n | \"list-checks\"\n | \"bell\"\n | \"door-open\"\n | \"inbox\"\n | \"layout-dashboard\"\n | \"archive-restore\"\n | \"archive\"\n | \"link-2\"\n | \"layout-grid\"\n | \"git-merge\"\n | \"lock\"\n | \"lock-open\";\n\n/** The groups, in their one order. Separators fall out of group boundaries. */\nexport type ObjectActionGroup = \"open\" | \"organize\" | \"share\" | \"manage\" | \"built-on\" | \"danger\";\n\n/**\n * THE STANDARD VERBS every object's menu shares — what the icon strip draws. One\n * per meaning; an object maps its action onto the verb, never a second \"share\".\n */\nexport type StandardVerbId =\n | \"open\"\n | \"open-new-tab\"\n | \"copy-link\"\n | \"rename\"\n | \"duplicate\"\n | \"move\"\n | \"favorite\"\n | \"share\"\n | \"export\"\n | \"import\"\n | \"settings\"\n | \"history\"\n | \"archive\";\n\nexport const STANDARD_VERBS: readonly StandardVerbId[] = [\n \"open\",\n \"open-new-tab\",\n \"copy-link\",\n \"rename\",\n \"duplicate\",\n \"move\",\n \"favorite\",\n \"share\",\n \"export\",\n \"import\",\n \"settings\",\n \"history\",\n \"archive\",\n];\n\nexport interface ObjectActionGroupDef {\n id: ObjectActionGroup;\n /** Present only on a group that renders as a submenu. */\n label?: string;\n icon?: ObjectActionIcon;\n /** Rendered as ONE submenu entry holding the group's actions. */\n submenu: boolean;\n}\n\nexport const OBJECT_ACTION_GROUPS: readonly ObjectActionGroupDef[] = [\n { id: \"open\", submenu: false },\n { id: \"organize\", submenu: false },\n { id: \"share\", submenu: false },\n { id: \"manage\", submenu: false },\n { id: \"built-on\", label: \"Built on it\", icon: \"blocks\", submenu: true },\n { id: \"danger\", submenu: false },\n];\n\nconst GROUP_IDS = new Set<string>(OBJECT_ACTION_GROUPS.map((g) => g.id));\n\n/** One thing a person can do to an object, for any renderer. */\nexport interface ObjectAction {\n /** Stable within the object's list. React key, test handle, future ⌘K id. */\n id: string;\n label: string;\n icon: ObjectActionIcon;\n group: ObjectActionGroup;\n /** The standard verb this action IS (a strip icon). Absent = a feature row. */\n verb?: StandardVerbId;\n /** A feature row's place among the rows, 1 first. Ignored when `verb` is set. */\n rank?: number;\n /** Display-only hint (\"⌘D\"). */\n shortcut?: string;\n destructive?: boolean;\n /** Present = shown disabled with this line (≤ 60 chars). Absent = enabled. */\n disabledReason?: string;\n /** Called only when enabled. The host owns the side effect and its feedback. */\n run: () => void | Promise<void>;\n}\n\n/** What the list needs to know about the table. A full `Table` satisfies it. */\nexport type TableActionSubject = Pick<Table, \"id\" | \"name\"> & Partial<Pick<Table, \"is_kernel\">>;\n\n/** The places built on a table, in their one order. */\nexport type BuiltOnDestination =\n | \"forms\"\n | \"bookings\"\n | \"checklists\"\n | \"notifications\"\n | \"portals\"\n | \"inbox\"\n | \"dashboards\"\n | \"archived\";\n\ntype Handler = (table: TableActionSubject) => void | Promise<void>;\n\n/**\n * THE SIDE EFFECTS THIS PACKAGE CANNOT OWN. Every handler is optional: one the\n * host leaves out keeps its action in the list, disabled, saying so.\n */\nexport interface TableActionHost {\n /** Origin for Copy link and Open in new tab. Default `https://www.aimatrx.com`. */\n origin?: string;\n open?: Handler;\n /** Receives the table's link (`<origin>/data/<id>`). */\n openInNewTab?: (url: string, table: TableActionSubject) => void;\n /** Receives the table's link. Runs inside the click (clipboard needs the gesture). */\n copyText?: (text: string, table: TableActionSubject) => void | Promise<void>;\n rename?: Handler;\n duplicate?: Handler;\n move?: Handler;\n /** Current favorite state; flips the Favorite label. Unknown = \"Add to favorites\". */\n isFavorite?: boolean;\n toggleFavorite?: Handler;\n share?: Handler;\n export?: Handler;\n import?: Handler;\n addColumn?: Handler;\n settings?: Handler;\n history?: Handler;\n /** Make the layout on screen the table's default. Leave out when it already is. */\n makeDefault?: Handler;\n /** The layout on screen, for \"Make Kanban the default\". */\n layoutLabel?: string;\n /** The \"Built on it\" destinations, one handler for all eight. */\n openBuiltOn?: (destination: BuiltOnDestination, table: TableActionSubject) => void;\n archive?: Handler;\n /**\n * Why an action whose handler is left out is unavailable here, by action id\n * (≤ 60 chars) — e.g. `make-default: \"Already the default\"`. Unnamed ids say\n * \"Not available here\". Never used when rights already refuse.\n */\n unavailableReasons?: Readonly<Record<string, string>>;\n /**\n * ANOTHER LANE'S ENTRIES (\"Link a record…\", a hub tile's verbs, a feature's own\n * row such as \"When a row changes, run an agent…\"). Return the same ids whatever\n * `rights` say — express rights as `disabledReason`. Each entry lands at the END\n * of its named group, in the order returned. Refused, loudly: the `danger` group\n * (Archive is always last), a group that does not exist, and an id already in\n * the list.\n */\n extend?: (table: TableActionSubject, rights: WhatYouMayDo) => readonly ObjectAction[];\n}\n\nexport interface TableActionsInput {\n table: TableActionSubject;\n rights: WhatYouMayDo;\n host: TableActionHost;\n}\n\nexport const DEFAULT_ACTION_ORIGIN = \"https://www.aimatrx.com\";\n\n/** The one link to a table: `<origin>/data/<tableId>`. */\nexport function tableLink(tableId: string, origin: string = DEFAULT_ACTION_ORIGIN): string {\n return `${origin.replace(/\\/+$/, \"\")}/data/${encodeURIComponent(tableId)}`;\n}\n\n/** The designate entry's words, matching `makeDefaultLabel` in views.ts. */\nfunction makeDefaultWords(layoutLabel: string | undefined): string {\n return layoutLabel ? `Make ${layoutLabel} the default` : \"Make this the default\";\n}\n\n// Reasons — one line, ≤ 60 characters, said on the action itself.\nexport const ACTION_NOT_HERE_REASON = \"Not available here\";\nexport const ACTION_CHECKING_ACCESS_REASON = \"Checking your access…\";\nexport const ACTION_NO_ACCESS_REASON = \"You don't have access\";\nexport const ACTION_PLATFORM_TABLE_REASON = \"Platform tables can't be changed here\";\n\n/** Why `rights` refuse `what`, in one short line — or null when they allow it. */\nexport function rightsReason(\n rights: WhatYouMayDo,\n what: Capability,\n table?: TableActionSubject,\n): string | null {\n if (!rights.known) return ACTION_CHECKING_ACCESS_REASON;\n if (what === \"structure\" && table?.is_kernel) return ACTION_PLATFORM_TABLE_REASON;\n if (rights[what]) return null;\n if (rights.level === null) return ACTION_NO_ACCESS_REASON;\n return `Needs ${LEVEL_WORD[levelNeededFor(what)]} access`;\n}\n\n// Feature rows' ranks: what the strip menu shows as its first rows. Lower is first.\nconst BUILT_ON: ReadonlyArray<{ destination: BuiltOnDestination; label: string; icon: ObjectActionIcon; rank: number }> = [\n { destination: \"forms\", label: \"Forms\", icon: \"file-text\", rank: 2 },\n { destination: \"bookings\", label: \"Bookings\", icon: \"calendar-clock\", rank: 6 },\n { destination: \"checklists\", label: \"Checklists\", icon: \"list-checks\", rank: 7 },\n { destination: \"notifications\", label: \"Notifications\", icon: \"bell\", rank: 8 },\n { destination: \"portals\", label: \"Portals\", icon: \"door-open\", rank: 9 },\n { destination: \"inbox\", label: \"Inbox\", icon: \"inbox\", rank: 10 },\n { destination: \"dashboards\", label: \"Dashboards\", icon: \"layout-dashboard\", rank: 3 },\n { destination: \"archived\", label: \"Archived records\", icon: \"archive-restore\", rank: 5 },\n];\n\nconst NOOP = () => {};\n\n/**\n * THE TABLE'S KEYBOARD CHORDS (lane TABLE-ACTIONS wave 4, item 17): ⌥⇧ + one key, the same on\n * every surface that runs the list. Chosen because nothing else owns them: ⌘D is the browser's\n * bookmark, ⌘S save, ⌘P print, ⌘K the command bar, ⌘⇧K the menu's palette, ⌘Z undo — and the\n * app binds no ⌥⇧ chord. A chord is never taken while the person types in a field\n * (`objectActionForKey`). `code` is the KeyboardEvent.code, so ⌥ on a Mac (which changes\n * `key` to a symbol) still matches.\n */\nexport const TABLE_ACTION_CHORDS: Readonly<Record<string, { code: string; chord: string }>> = {\n \"copy-link\": { code: \"KeyL\", chord: \"alt+shift+l\" },\n rename: { code: \"KeyR\", chord: \"alt+shift+r\" },\n duplicate: { code: \"KeyD\", chord: \"alt+shift+d\" },\n move: { code: \"KeyM\", chord: \"alt+shift+m\" },\n favorite: { code: \"KeyF\", chord: \"alt+shift+f\" },\n share: { code: \"KeyS\", chord: \"alt+shift+s\" },\n export: { code: \"KeyE\", chord: \"alt+shift+e\" },\n import: { code: \"KeyI\", chord: \"alt+shift+i\" },\n settings: { code: \"Comma\", chord: \"alt+shift+,\" },\n history: { code: \"KeyH\", chord: \"alt+shift+h\" },\n archive: { code: \"Backspace\", chord: \"alt+shift+backspace\" },\n};\n\n/**\n * The chords with their LABEL in this platform's words — \"⌥⇧S\" on a Mac, \"Alt+Shift+S\" on Windows\n * and Linux — through the design system's one shortcut formatter (`formatShortcut`, the tap\n * button's tooltip), so a menu entry and the ⌘K bar read alike everywhere.\n */\nexport const TABLE_ACTION_KEYS: Readonly<Record<string, { code: string; label: string }>> = Object.fromEntries(\n Object.entries(TABLE_ACTION_CHORDS).map(([id, { code, chord }]) => [id, { code, label: formatShortcut(chord) }]),\n);\n\n/** The fields of a keyboard event a chord reads (a DOM KeyboardEvent satisfies it). */\nexport interface ChordEvent {\n code: string;\n altKey: boolean;\n shiftKey: boolean;\n metaKey: boolean;\n ctrlKey: boolean;\n target?: EventTarget | null;\n}\n\nfunction typingIn(target: EventTarget | null | undefined): boolean {\n const el = target as { tagName?: string; isContentEditable?: boolean } | null | undefined;\n if (!el || typeof el.tagName !== \"string\") return false;\n return el.isContentEditable === true || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);\n}\n\n/**\n * The action a key press asks for, or null: ⌥⇧ + its key, no ⌘/Ctrl, never inside a field. A\n * disabled action is still RETURNED (the caller says its reason instead of doing nothing).\n */\nexport function objectActionForKey(actions: readonly ObjectAction[], event: ChordEvent): ObjectAction | null {\n if (!event.altKey || !event.shiftKey || event.metaKey || event.ctrlKey) return null;\n if (typingIn(event.target)) return null;\n const id = Object.keys(TABLE_ACTION_KEYS).find((k) => TABLE_ACTION_KEYS[k]!.code === event.code);\n return id ? (actions.find((a) => a.id === id) ?? null) : null;\n}\n\n/**\n * One action, from its needs: the capability it asks for and the host handler\n * that performs it. Rights speak first; a missing handler second.\n */\nfunction action(\n base: Omit<ObjectAction, \"run\" | \"disabledReason\">,\n needs: Capability,\n input: TableActionsInput,\n perform: (() => void | Promise<void>) | undefined,\n): ObjectAction {\n const chord = TABLE_ACTION_KEYS[base.id];\n if (chord && base.shortcut === undefined) base = { ...base, shortcut: chord.label };\n const refused = rightsReason(input.rights, needs, input.table);\n if (refused !== null) return { ...base, disabledReason: refused, run: NOOP };\n if (perform === undefined) {\n return { ...base, disabledReason: input.host.unavailableReasons?.[base.id] ?? ACTION_NOT_HERE_REASON, run: NOOP };\n }\n return { ...base, run: perform };\n}\n\nfunction bind(handler: Handler | undefined, table: TableActionSubject): (() => void | Promise<void>) | undefined {\n return handler ? () => handler(table) : undefined;\n}\n\n/**\n * THE TABLE'S ACTIONS, in the one order. Same ids, same order, same verbs and\n * ranks, for every seat and every host — only `disabledReason` varies.\n */\nexport function tableActions(input: TableActionsInput): ObjectAction[] {\n const { table, host } = input;\n const url = tableLink(table.id, host.origin);\n const favorite = host.isFavorite === true;\n const openInNewTab = host.openInNewTab;\n const copyText = host.copyText;\n const openBuiltOn = host.openBuiltOn;\n\n const built: ObjectAction[] = [\n action({ id: \"open\", label: \"Open\", icon: \"folder-open\", group: \"open\", verb: \"open\" }, \"read\", input, bind(host.open, table)),\n action(\n { id: \"open-new-tab\", label: \"Open in new tab\", icon: \"external-link\", group: \"open\", verb: \"open-new-tab\" },\n \"read\",\n input,\n openInNewTab ? () => openInNewTab(url, table) : undefined,\n ),\n action(\n { id: \"copy-link\", label: \"Copy link\", icon: \"link\", group: \"open\", verb: \"copy-link\" },\n \"read\",\n input,\n copyText ? () => copyText(url, table) : undefined,\n ),\n action({ id: \"rename\", label: \"Rename…\", icon: \"pencil\", group: \"organize\", verb: \"rename\" }, \"structure\", input, bind(host.rename, table)),\n action({ id: \"duplicate\", label: \"Duplicate\", icon: \"copy\", group: \"organize\", verb: \"duplicate\" }, \"read\", input, bind(host.duplicate, table)),\n action({ id: \"move\", label: \"Move to…\", icon: \"folder-input\", group: \"organize\", verb: \"move\" }, \"structure\", input, bind(host.move, table)),\n action(\n {\n id: \"favorite\",\n label: favorite ? \"Remove from favorites\" : \"Add to favorites\",\n icon: favorite ? \"star-off\" : \"star\",\n group: \"organize\",\n verb: \"favorite\",\n },\n \"read\",\n input,\n bind(host.toggleFavorite, table),\n ),\n action({ id: \"share\", label: \"Share…\", icon: \"share-2\", group: \"share\", verb: \"share\" }, \"share\", input, bind(host.share, table)),\n action({ id: \"export\", label: \"Export…\", icon: \"download\", group: \"share\", verb: \"export\" }, \"read\", input, bind(host.export, table)),\n action({ id: \"import\", label: \"Import…\", icon: \"upload\", group: \"share\", verb: \"import\" }, \"structure\", input, bind(host.import, table)),\n action({ id: \"add-column\", label: ADD_FIELD_OPEN_LABEL, icon: \"columns-3\", group: \"manage\", rank: 1 }, \"structure\", input, bind(host.addColumn, table)),\n action({ id: \"settings\", label: \"Settings\", icon: \"settings\", group: \"manage\", verb: \"settings\" }, \"structure\", input, bind(host.settings, table)),\n action({ id: \"history\", label: \"History\", icon: \"history\", group: \"manage\", verb: \"history\" }, \"read\", input, bind(host.history, table)),\n // custom.view_designate asks editor on the table — `canDesignate` in TablePage.\n action(\n { id: \"make-default\", label: makeDefaultWords(host.layoutLabel), icon: \"layout-grid\", group: \"manage\", rank: 4 },\n \"organise\",\n input,\n bind(host.makeDefault, table),\n ),\n ...BUILT_ON.map(({ destination, label, icon, rank }) =>\n action(\n { id: `built-on.${destination}`, label, icon, group: \"built-on\", rank },\n \"read\",\n input,\n openBuiltOn ? () => openBuiltOn(destination, table) : undefined,\n ),\n ),\n action(\n { id: \"archive\", label: \"Archive table\", icon: \"archive\", group: \"danger\", destructive: true, verb: \"archive\" },\n \"structure\",\n input,\n bind(host.archive, table),\n ),\n ];\n\n return withExtensions(built, host.extend ? host.extend(table, input.rights) : []);\n}\n\n/**\n * Another lane's entries, placed deterministically: each at the end of its\n * group, in the order given. Loud on every mistake that would break the\n * invariants — a `danger` entry (Archive must stay last), a group that does not\n * exist (it would be dropped by every renderer) and a duplicate id.\n */\nexport function withExtensions(base: readonly ObjectAction[], extra: readonly ObjectAction[]): ObjectAction[] {\n if (extra.length === 0) return [...base];\n const seen = new Set(base.map((a) => a.id));\n for (const entry of extra) {\n if (entry.group === \"danger\") {\n throw new Error(`Object action \"${entry.id}\": the danger group is closed to extensions.`);\n }\n if (!GROUP_IDS.has(entry.group)) {\n throw new Error(`Object action \"${entry.id}\": \"${String(entry.group)}\" is not a group.`);\n }\n if (seen.has(entry.id)) {\n throw new Error(`Object action \"${entry.id}\" is already in the list.`);\n }\n seen.add(entry.id);\n }\n const out: ObjectAction[] = [];\n for (const group of OBJECT_ACTION_GROUPS) {\n out.push(...base.filter((a) => a.group === group.id), ...extra.filter((a) => a.group === group.id));\n }\n return out;\n}\n\n/** A run of actions that renders together: a plain section, or one submenu. */\nexport interface ObjectActionSection {\n group: ObjectActionGroupDef;\n actions: ObjectAction[];\n}\n\n/**\n * THE ONE GROUPING every adapter uses: the list cut at group boundaries, in\n * group order, empty groups dropped, the order inside a group kept.\n */\nexport function groupObjectActions(actions: readonly ObjectAction[]): ObjectActionSection[] {\n const sections: ObjectActionSection[] = [];\n for (const group of OBJECT_ACTION_GROUPS) {\n const inGroup = actions.filter((a) => a.group === group.id);\n if (inGroup.length > 0) sections.push({ group, actions: inGroup });\n }\n return sections;\n}\n\n/** The header ⋯ dropdown's entries — records-ui's own renderer reads these. */\nexport type ObjectActionDropdownEntry =\n | { kind: \"separator\"; id: string }\n | { kind: \"item\"; action: ObjectAction }\n | { kind: \"submenu\"; id: string; label: string; icon: ObjectActionIcon | undefined; actions: ObjectAction[] };\n\nexport function toDropdownEntries(actions: readonly ObjectAction[]): ObjectActionDropdownEntry[] {\n const entries: ObjectActionDropdownEntry[] = [];\n groupObjectActions(actions).forEach(({ group, actions: inGroup }, index) => {\n if (index > 0) entries.push({ kind: \"separator\", id: `separator:${group.id}` });\n if (group.submenu) {\n entries.push({ kind: \"submenu\", id: group.id, label: group.label ?? group.id, icon: group.icon, actions: inGroup });\n } else {\n for (const a of inGroup) entries.push({ kind: \"item\", action: a });\n }\n });\n return entries;\n}\n\n/** The actions a dropdown draws, in order — what G1 compares. */\nexport function dropdownActions(entries: readonly ObjectActionDropdownEntry[]): ObjectAction[] {\n return entries.flatMap((e) => (e.kind === \"item\" ? [e.action] : e.kind === \"submenu\" ? e.actions : []));\n}\n\n/** The action ids a dropdown draws, in order. */\nexport function dropdownActionIds(entries: readonly ObjectActionDropdownEntry[]): string[] {\n return dropdownActions(entries).map((a) => a.id);\n}\n\n/** The icon-strip menu: standard verbs as icons, then the first feature rows, then the rest. */\nexport interface StripAndRows {\n /** Every action with a `verb`, in the list's order. */\n strip: ObjectAction[];\n /** The first `rowCount` feature actions by rank (ties keep list order). */\n rows: ObjectAction[];\n /** The remaining feature actions, behind \"More … options\", same ordering. */\n more: ObjectAction[];\n}\n\nexport const STRIP_FEATURE_ROWS = 4;\n\n/**\n * THE STRIP RENDERER'S SPLIT of the same list. Nothing is added or dropped: every\n * id lands in exactly one of strip / rows / more (G1). Unranked feature actions\n * (an extension that gave none) follow every ranked one.\n */\nexport function toStripAndRows(actions: readonly ObjectAction[], rowCount: number = STRIP_FEATURE_ROWS): StripAndRows {\n const strip = actions.filter((a) => a.verb !== undefined);\n const features = actions\n .map((a, index) => ({ a, index }))\n .filter(({ a }) => a.verb === undefined)\n .sort((x, y) => (x.a.rank ?? Infinity) - (y.a.rank ?? Infinity) || x.index - y.index)\n .map(({ a }) => a);\n return { strip, rows: features.slice(0, rowCount), more: features.slice(rowCount) };\n}\n","// src/fieldTypes.ts — @ai-matrx/records-ui\n//\n// EVERY KIND OF COLUMN THE STORE TAKES, IN THE WORDS A PERSON USES.\n//\n// The store publishes fourteen parity types and, for each, a sentence about what\n// it is MADE OF — \"REC-31: a File record reached through a relation, target the\n// kernel `File` Table\", \"FLD-9: a declared computation, on read or on write,\n// evaluated by custom.rule_eval\", \"config.via names the relation field,\n// config.pick the Value on the far side\". Those sentences are true and they are\n// for us. The 19 September verdict found them printed at the person choosing a\n// column type, alongside contract row ids, and called it what it is: machine\n// identity where a person is looking.\n//\n// So this file holds the OTHER sentence — what the type is FOR — and the field\n// panel shows that one. The store's own `made_of` stays where it belongs, in\n// `PARITY_MADE_OF`, for an engineer reading the package.\n//\n// The LIST itself is still the store's: `PARITY_FIELD_TYPES` is generated from\n// `custom.parity_field_types()`, and `assertEveryParityTypeIsExplained` (run by\n// the suite) fails the moment the store ships a fourteenth and nobody wrote a\n// sentence for it. A type with no explanation would otherwise appear with a\n// blank line under it, which is the silent version of the same defect. It caught the\n// fourteenth — `checkbox`, 2026-09-21 — exactly as it was written to.\n\nimport { DECLARABLE_FIELD_KINDS, NEWER_FIELD_KINDS, PARITY_FIELD_TYPES, type DeclarableFieldKind, type NewerFieldKind, type ParityFieldType } from \"@ai-matrx/records\";\n\n/** The three behaviours that carry no parity type of their own. */\nexport type PlainFieldType = \"text\" | \"long_text\" | \"number\";\n\n/**\n * THE ONE A PERSON COMING FROM AIRTABLE REACHES FOR FIRST, AND IT WAS NOT HERE.\n *\n * The list above offered `member` (a Person) and `attachment` (a File) — the two\n * relations whose target is a Table the PLATFORM keeps — and nothing at all for\n * \"this job belongs to that customer\". The store has taken it since 2026-09-20\n * (`custom.field_declare` with `type: \"relation\"` and a `relation_target`), the\n * Field type has named it since the same day, and `editorKindFor` has answered\n * `\"relation\"` for one since long before that. Only the menu did not offer it,\n * so the guide's section 3 told a person to pick a kind that did not exist.\n *\n * It is NOT a parity type of its own. `custom.parity_field_types()` ships\n * fourteen (the fourteenth is `checkbox`) and a relation is a BEHAVIOUR — the same behaviour `member` and\n * `attachment` are made of — so it sits beside the three plain ones as its own\n * word, and `parityTypesWithNoExplanation()` is unaffected by it in both\n * directions: it does not appear there, and it cannot hide a real gap there.\n */\nexport type RelationFieldType = \"relation\";\n\n/**\n * What a person picks in the panel — AND IT IS THE STORE'S OWN LIST, not a union\n * assembled here.\n *\n * 🚨 THE CLASS FIX (FIX-10B-F6, VERIFIER-10). This used to read\n * `ParityFieldType | PlainFieldType | RelationFieldType` — the parity floor plus two words\n * this file knew about — and that union was the whole defect. `custom.doc_sign` accepts\n * exactly one field, a text column whose format is `signature`, and it is granted to\n * `authenticated`; the Documents screen told a person in so many words to go and declare\n * one; and no list any screen could read named it, so the control did not exist and the\n * e-sign half of Documents was unreachable from the product. A kind the store takes and no\n * screen offers is invisible, and nothing failed — which is the only reason it survived.\n *\n * `custom.field_kinds()` is now the ONE registry and `DECLARABLE_FIELD_KINDS` is generated\n * from it, so the next kind the store learns arrives here as a TYPE ERROR in\n * `FIELD_TYPE_CHOICES`'s coverage and as a red `kindsWithNoChoice()` below — never as a\n * silence.\n */\nexport type PickableFieldType = DeclarableFieldKind | SystemFieldKind | \"entity_reference\" | NewerFieldKind | LookFieldKind;\n\n/**\n * A KIND THAT IS A PLAIN COLUMN WEARING A LOOK (grids review 3, lane H item 5). The store keeps a\n * column's \"Shows as\" on the Field (`display_format`, `custom.display_format_ids()` has `rating` and\n * `time`), so a Rating is a whole number shown as stars and a Time is a time of day kept as its\n * 24-hour text — declared through the ordinary door (`{ plain, display_format }`) the same way the\n * \"Shows as\" picker makes one. Offered whenever the store does not yet publish the kind as its own\n * word (`custom.field_kinds()`): a Rating the store publishes goes as `{ type: \"rating\", max }`.\n */\nexport type LookFieldKind = \"time\";\n\nexport interface LookKind {\n plain: \"number\" | \"text\";\n look: (args: { ratingMax: number }) => { id: string; options?: Record<string, unknown> };\n}\n\nexport const LOOK_KINDS: Partial<Record<PickableFieldType, LookKind>> = {\n rating: { plain: \"number\", look: ({ ratingMax }) => ({ id: \"rating\", options: { ratingMax } }) },\n time: { plain: \"text\", look: () => ({ id: \"time\" }) },\n};\n\n/** Is this kind declared as a plain column wearing a look (because the store does not publish it)? */\nexport function declaredAsLook(kind: string): boolean {\n return kind in LOOK_KINDS && !storePublishesKind(kind);\n}\n\n/**\n * LANE 10 P4: THE NINE NEWER KINDS (rating, duration, created by, last changed by, status, address,\n * barcode, formatted text, count). Each is listed in `FIELD_TYPE_CATALOGUE` below, so every reader\n * (Settings, the column panel, every layout) names a column of one by its own word; the MENU offers\n * one only once `custom.field_kinds()` publishes it (after the chair's apply and\n * `pnpm records:generate`), so a person is never offered a kind the live store would refuse.\n */\nexport function storePublishesKind(kind: string): boolean {\n return DECLARABLE_FIELD_KINDS.some((row) => (row.kind as string) === kind);\n}\n\n/**\n * LANE SC-R: a column that points at a PLATFORM thing (a note, a web site, an agent). Its menu entry\n * is drawn only once `custom.field_kinds()` publishes `entity_reference` (after the chair's apply\n * and `pnpm records:generate`), so the menu never offers a kind the live store would refuse.\n */\nexport const STORE_PUBLISHES_ENTITY_REFERENCE = DECLARABLE_FIELD_KINDS.some(\n (row) => (row.kind as string) === \"entity_reference\",\n);\n\n/**\n * THE STORE'S OWN STAMPS AS COLUMNS (GRID-PRIMITIVES G5; TABLE-PARITY H2 H3, lane\n * DEFAULT-GRID-PARITY). `custom.field_declare` takes these three WORDS and makes each a formula\n * Field whose expression is the system node (`fx.autonumber`, `fx.created_time`,\n * `fx.modified_time`, `config.system` naming which), so nobody types into them and every reader\n * that serves a worked-out value serves them. `custom.field_kinds()` does not publish them,\n * because each is a formula, not a behaviour of its own. So they are named here, sent as their\n * word through the `{ type }` arm, and a new autonumber column numbers the records already in\n * the table through `custom.autonumber_backfill`.\n */\nexport type SystemFieldKind = \"autonumber\" | \"created_time\" | \"modified_time\";\n\nexport const SYSTEM_FIELD_KINDS: readonly SystemFieldKind[] = [\"autonumber\", \"created_time\", \"modified_time\"];\n\nexport function isSystemFieldKind(id: string): id is SystemFieldKind {\n return (SYSTEM_FIELD_KINDS as readonly string[]).includes(id);\n}\n\nexport interface FieldTypeChoice {\n id: PickableFieldType;\n /** The word on the menu. */\n label: string;\n /** One line, under the word, saying what it is FOR. Never what it is made of. */\n explanation: string;\n /** The heading it sits under, so seventeen choices read as four short lists. */\n group: \"Words\" | \"Numbers and dates\" | \"Choices and people\" | \"Worked out\";\n}\n\n/**\n * EVERY KIND THIS PACKAGE CAN NAME AND DRAW, in menu order. The menu (`FIELD_TYPE_CHOICES`) is this\n * list less the kinds the live store does not take yet; every reader names a column from this one.\n */\nexport const FIELD_TYPE_CATALOGUE: FieldTypeChoice[] = [\n { id: \"text\", group: \"Words\", label: \"Text\", explanation: \"A short line of writing — a name, a reference, a title.\" },\n { id: \"long_text\", group: \"Words\", label: \"Long text\", explanation: \"Several lines — a description, notes, an address.\" },\n { id: \"rich_text\", group: \"Words\", label: \"Formatted text\", explanation: \"Notes with bold, lists and links.\" },\n { id: \"email\", group: \"Words\", label: \"Email address\", explanation: \"An email address, checked as you type and ready to write to.\" },\n { id: \"phone\", group: \"Words\", label: \"Phone number\", explanation: \"A phone number, checked as you type.\" },\n { id: \"url\", group: \"Words\", label: \"Link\", explanation: \"A web address that opens in a new tab when you click it.\" },\n { id: \"address\", group: \"Words\", label: \"Address\", explanation: \"A street address in parts, opened in maps.\" },\n { id: \"barcode\", group: \"Words\", label: \"Barcode\", explanation: \"A code a scanner reads, like a wristband or a box.\" },\n { id: \"button\", group: \"Words\", label: \"Button\", explanation: \"A button on each record that opens a link or runs an action.\" },\n\n { id: \"number\", group: \"Numbers and dates\", label: \"Number\", explanation: \"A plain number you can total up and sort by.\" },\n { id: \"currency\", group: \"Numbers and dates\", label: \"Money\", explanation: \"An amount of money, shown with its currency wherever it appears.\" },\n { id: \"percent\", group: \"Numbers and dates\", label: \"Percentage\", explanation: \"A number out of a hundred, kept between 0 and 100.\" },\n { id: \"rating\", group: \"Numbers and dates\", label: \"Rating\", explanation: \"A score in stars, set with one click.\" },\n { id: \"duration\", group: \"Numbers and dates\", label: \"Duration\", explanation: \"A length of time, typed as 45 min or 1:30.\" },\n { id: \"time\", group: \"Numbers and dates\", label: \"Time\", explanation: \"A time of day with no date — a shift start, opening hours.\" },\n { id: \"datetime\", group: \"Numbers and dates\", label: \"Date\", explanation: \"A day, or a day and a time. Dates are what the calendar view reads.\" },\n\n { id: \"select\", group: \"Choices and people\", label: \"Choice\", explanation: \"One answer from a list you write here. Choices are what the board view groups by.\" },\n { id: \"multi_select\", group: \"Choices and people\", label: \"Several choices\", explanation: \"Any number of answers from a list you write here.\" },\n { id: \"status\", group: \"Choices and people\", label: \"Status\", explanation: \"Choices grouped into To do, In progress and Done.\" },\n { id: \"checkbox\", group: \"Choices and people\", label: \"Yes / no\", explanation: \"A box you tick. Left alone it stays unanswered, which is not the same as a no.\" },\n { id: \"member\", group: \"Choices and people\", label: \"Person\", explanation: \"Somebody in this organization, picked from the people who are in it.\" },\n { id: \"attachment\", group: \"Choices and people\", label: \"File\", explanation: \"A file or a picture kept with the record.\" },\n { id: \"signature\", group: \"Choices and people\", label: \"Signature\", explanation: \"Somebody's sign-off on a document made from this record. Nobody types in it — it fills in when they sign.\" },\n { id: \"relation\", group: \"Choices and people\", label: \"Points at another record\", explanation: \"A link to records in another of your tables — the customer a job is for, the lines on an invoice.\" },\n {\n id: \"entity_reference\",\n group: \"Choices and people\",\n label: \"Points at something else on the platform\",\n explanation: \"A note, a web site, an agent, a workbook — anything you keep here that is not one of your tables.\",\n },\n\n { id: \"lookup\", group: \"Worked out\", label: \"Borrowed value\", explanation: \"A value read from a record this one points at, kept in step with it.\" },\n { id: \"rollup\", group: \"Worked out\", label: \"Total of linked records\", explanation: \"A count or a total worked out from the records this one points at.\" },\n { id: \"count\", group: \"Worked out\", label: \"Count of linked records\", explanation: \"How many records this one points at.\" },\n { id: \"formula\", group: \"Worked out\", label: \"Worked out from other columns\", explanation: \"A value this record works out for itself from its own other columns.\" },\n { id: \"autonumber\", group: \"Worked out\", label: \"Record number\", explanation: \"A number the table gives each record, one after another, never reused. Records already here are numbered in the order they were made.\" },\n { id: \"created_time\", group: \"Worked out\", label: \"Created time\", explanation: \"When each record was made. Nobody types it.\" },\n { id: \"modified_time\", group: \"Worked out\", label: \"Last changed time\", explanation: \"When each record last changed. Nobody types it.\" },\n { id: \"created_by\", group: \"Worked out\", label: \"Created by\", explanation: \"Who made each record. Nobody types it.\" },\n { id: \"modified_by\", group: \"Worked out\", label: \"Last changed by\", explanation: \"Who last changed each record. Nobody types it.\" },\n];\n\n/** The kinds the menu offers only once the live store publishes them. */\nconst GATED_KINDS = new Set<string>([\"entity_reference\", ...NEWER_FIELD_KINDS]);\n\nexport const FIELD_TYPE_CHOICES: FieldTypeChoice[] = FIELD_TYPE_CATALOGUE.filter(\n (c) => !GATED_KINDS.has(c.id) || storePublishesKind(c.id) || c.id in LOOK_KINDS,\n);\n\nexport const FIELD_TYPE_GROUPS: FieldTypeChoice[\"group\"][] = [\n \"Words\",\n \"Numbers and dates\",\n \"Choices and people\",\n \"Worked out\",\n];\n\nconst BY_ID = new Map<string, FieldTypeChoice>(FIELD_TYPE_CATALOGUE.map((c) => [c.id, c]));\n\nexport function fieldTypeChoice(id: string): FieldTypeChoice | undefined {\n return BY_ID.get(id);\n}\n\n/** The three that are a plain behaviour rather than one of the fourteen. */\nexport function isPlainFieldType(id: string): id is PlainFieldType {\n return id === \"text\" || id === \"long_text\" || id === \"number\";\n}\n\n/**\n * The one that is a relation a person aims themselves. It is neither plain nor\n * one of the thirteen, so every caller that branched on `isPlainFieldType`\n * alone has to answer this question too — which is why it is a named function\n * and not an inline string comparison at four call sites.\n */\nexport function isRelationFieldType(id: string): id is RelationFieldType {\n return id === \"relation\";\n}\n\n/**\n * One of FLD-11's fourteen, which is what `parity_type` on a declaration means.\n *\n * Read from the store's own generated floor, never from a list here — because a kind that\n * is NOT one of the fourteen (plain text, a person-aimed relation, a signature) has to be\n * declared by its WORD (`type`) and a panel that sent `parity_type` for it would be refused\n * \"There is no field type called …\". That is the shape of the F6 defect: the panel's `save`\n * had exactly two branches, relation and plain, and everything else fell into `parity_type`.\n */\nexport function isParityFieldType(id: string): id is ParityFieldType {\n return PARITY_FIELD_TYPES.some((row) => row.parity_type === id);\n}\n\n/**\n * THE GUARD THIS FILE NEEDS, because the list it explains is generated.\n *\n * Returns the parity types the store ships that nothing here explains. The\n * suite asserts it is empty; a fourteenth type would otherwise reach the menu\n * with a blank line under it and nobody would notice until a person did.\n *\n * `relation` is deliberately NOT one of the thirteen and therefore never\n * appears in this answer: `PARITY_FIELD_TYPES` is the store's own generated\n * list and a behaviour that carries no parity type is not in it.\n */\nexport function parityTypesWithNoExplanation(): string[] {\n return PARITY_FIELD_TYPES.map((row) => row.parity_type).filter((type) => !BY_ID.has(type));\n}\n\n/**\n * 🚨 THE GUARD THAT CLOSES THE CLASS (FIX-10B-F6).\n *\n * `parityTypesWithNoExplanation()` above watches the parity FLOOR, and the floor was never\n * the whole list: the store also declares plain text, long text, number, a person-aimed\n * relation and a signature, and for months nothing anywhere compared the panel's menu with\n * THAT. So `signature` — the one field `custom.doc_sign` accepts, on a door granted to\n * every signed-in person, named to the person by the Documents screen itself — was absent\n * from the menu and every check stayed green.\n *\n * This returns the kinds `custom.field_kinds()` publishes that this panel does not offer.\n * The suite asserts it is empty, so a kind the store learns tomorrow either reaches the\n * person or turns a test red the same day. It is the class, not the instance: it would have\n * caught `signature`, and it will catch the twentieth kind nobody has thought of yet.\n */\nexport function kindsWithNoChoice(): string[] {\n return DECLARABLE_FIELD_KINDS.map((row) => row.kind).filter((kind) => !BY_ID.has(kind));\n}\n\n// ───────────────────────────────────────────────────────────────────────────\n// THE WORDS ON THE TWO BUTTONS, IN ONE PLACE — because they used to be the SAME\n// WORDS in two places, and that cost somebody their column.\n//\n// 🚨 THE DEFECT (TAILS-5, 2026-09-21, measured while driving the panel headless\n// on Rincon Plumbing's Summerland branch). The Jobs toolbar's control that OPENS\n// the field panel read exactly \"Add field\", and the panel's own SAVE button read\n// exactly \"Add field\" too — `FieldEditor` says \"Save field\" only when EDITING an\n// existing column, so on the create path the two were character-identical. The\n// toolbar control is a TOGGLE: pressing it while the panel is open closes the\n// panel and throws away everything typed into it. So a person who typed a column\n// name, chose a type, picked the target table and its display columns, then\n// looked up at the words they were told to press, could lose all of it — with no\n// sentence, no request sent, and nothing to say it had happened. A walk written\n// by an agent lost the column twice that way and spent a round diagnosing the\n// store before reading the two labels side by side.\n//\n// WHY THEY LIVE HERE AND NOT IN `@ai-matrx/design-system`. The words are RECORDS\n// vocabulary — \"field\", \"column\" — and the design system knows nothing about\n// either; a labels module there would be a domain word in a generic package, and\n// the next person adding a button would not find it. This file is already the\n// one place that owns what a person READS about a column (`FIELD_TYPE_CHOICES`\n// is right above), and both surfaces that draw these buttons are in this\n// package. One module, one import each, and `fieldTypes.test.ts` asserts the two\n// can never be the same string again.\n//\n// THE RULE: the control that OPENS a panel and the control that COMMITS it never\n// read the same words. The opener is an invitation; the committer says what it\n// will make.\n\n/** The toolbar control that OPENS the add-a-column panel. An invitation. */\nexport const ADD_FIELD_OPEN_LABEL = \"Add column\";\n\n/** The panel's own primary, on the CREATE path. It says what pressing it makes. */\nexport const ADD_FIELD_SAVE_LABEL = \"Create column\";\n\n/** The panel's own primary when an existing column is being changed. */\nexport const EDIT_FIELD_SAVE_LABEL = \"Save column\";\n","// src/rights.ts — @ai-matrx/records-ui\n//\n// THE ONE PLACE A LEVEL BECOMES A SCREEN.\n//\n// WHAT A PERSON HIT (independent verdict, sixth pass, 19-20 September). A\n// colleague who had been shared a table at viewer opened it, double-clicked a\n// cell, got a live editor, typed a value and pressed Enter — and only THEN was\n// told she may not write. \"The permission is right; the screen told her she\n// could.\" Measured against Airtable, Notion and Linear the verdict called it\n// out by name: \"None of these three products lets you start an action it\n// already knows it will reject.\"\n//\n// WHY THE SCREEN COULD NOT KNOW. The rights question was a PORT with two honest\n// answers and no third: `NO_RIGHTS` (every administering control absent, which\n// is useless for a host whose authority IS the store's) and\n// `storeDecidesRights` — `{admin: true, comment: true, write: true}` for every\n// non-kernel table, for everybody. That second one is the defect, written down:\n// it is the screen ASSUMING the answer and letting the door say no afterwards.\n//\n// WHAT REPLACES IT. The store already answers this, and the client now carries\n// it: `custom.read_records` returns the reader's level on the TABLE on every\n// row (so `Table.your_level` costs nothing), and `custom.my_levels` — the door\n// this lane added — answers the caller's level on any subject they name, one\n// call for a whole page. This file is the only place in the package that turns\n// one of those four words into what a screen may DRAW.\n//\n// THE LADDER IS THE STORE'S, NOT OURS: viewer < commenter < editor < admin, and\n// which rung each door requires is read from the doors themselves —\n// `custom.record_update` asserts `editor` on the record, `custom.field_declare`\n// asserts `admin` on the TABLE (\"the SHAPE of this table, which is an admin's\n// right and not an editor's\"), `custom.io_comment_write` admits a commenter.\n// Nothing here invents a rule; it mirrors the four assertions those doors make\n// and is wrong the moment they change, which is what the suite is for.\n//\n// ABSENT OR HONEST, NEVER DEAD. Every `false` here carries the sentence that\n// says why and what to do about it, so a control can be left out AND explained.\n// A screen that hid a control with nothing to say is the silent half of the\n// same defect.\n\nimport { atLeast, type PermissionLevel, type Table } from \"@ai-matrx/records\";\n\n/** The six things a person does to a record or a table, as the screens offer them. */\nexport type Capability = \"read\" | \"comment\" | \"write\" | \"organise\" | \"remove\" | \"share\" | \"structure\";\n\nexport interface WhatYouMayDo {\n /** The word the store answered, or null when it answered nothing. */\n level: PermissionLevel | null;\n /**\n * Whether the store has ANSWERED yet. `false` is a third state and not a\n * refusal: a screen that drew \"you may not\" while the answer was in flight\n * would blink every control off on every reload, which is its own lie.\n */\n known: boolean;\n read: boolean;\n comment: boolean;\n write: boolean;\n /** Duplicate, merge, restore, make a layout the default: the doors that ask `editor`, above content. */\n organise: boolean;\n remove: boolean;\n share: boolean;\n structure: boolean;\n /** Why this person may not do that, in one sentence naming the way in. */\n why: (what: Capability) => string;\n}\n\n/** What each rung of the ladder is called where a person reads it. */\nexport const LEVEL_WORD: Record<PermissionLevel, string> = {\n viewer: \"Viewer\",\n commenter: \"Commenter\",\n edit_content: \"Content editor\",\n editor: \"Editor\",\n admin: \"Admin\",\n};\n\n/**\n * WHICH RUNG EACH DOOR ASKS FOR. Read off the doors themselves, in one place,\n * so a screen never re-decides it and the suite has one thing to check.\n */\nconst NEEDS: Record<Capability, PermissionLevel> = {\n read: \"viewer\",\n // custom.io_comment_write\n comment: \"commenter\",\n // custom.record_update / custom.record_write — `edit_content` changes rows and page content;\n // columns, views and settings (structure) stay with admin below, and sharing with admin.\n write: \"edit_content\",\n // custom.view_designate / view_declare / migrate_merge / record_restore — all ask `editor`: more than\n // changing content (`edit_content`), less than changing columns (`admin`).\n organise: \"editor\",\n // custom.record_delete\n remove: \"editor\",\n // custom.share_grant — passing your access on is an administrator's right\n share: \"admin\",\n // custom.field_declare / field_update / field_retire / table settings\n structure: \"admin\",\n};\n\n/** The rung a capability asks for — so a short \"Needs Admin access\" never re-decides the ladder. */\nexport function levelNeededFor(what: Capability): PermissionLevel {\n return NEEDS[what];\n}\n\nconst NOT_YET =\n \"Still asking the store what you may do here. Nothing is offered until it has answered.\";\n\nconst NOTHING_AT_ALL =\n \"You have not been given this. Ask somebody who administers it to share it with you.\";\n\n/** The sentence for one refusal: what the level is, what it would have to be. */\nfunction sentenceFor(what: Capability, level: PermissionLevel | null): string {\n if (level === null) return NOTHING_AT_ALL;\n const need = LEVEL_WORD[NEEDS[what]];\n const have = LEVEL_WORD[level];\n const doing: Record<Capability, string> = {\n read: \"open this\",\n comment: \"leave a comment\",\n write: \"change anything here\",\n organise: \"organise this\",\n remove: \"delete this\",\n share: \"pass this on to somebody else\",\n structure: \"change the columns of this table\",\n };\n return `You are ${have} here, and it takes ${need} to ${doing[what]}. Ask somebody who is Admin on it to move you up.`;\n}\n\n/**\n * WHAT ONE LEVEL LETS A PERSON DO. `level` null and `known` true means the\n * store said \"nothing\" — a real answer. `known` false means it has not said.\n */\nexport function whatYouMayDo(level: PermissionLevel | null, known = true): WhatYouMayDo {\n const may = (what: Capability) => known && atLeast(level, NEEDS[what]);\n return {\n level: known ? level : null,\n known,\n read: may(\"read\"),\n comment: may(\"comment\"),\n write: may(\"write\"),\n organise: may(\"organise\"),\n remove: may(\"remove\"),\n share: may(\"share\"),\n structure: may(\"structure\"),\n why: (what) => (known ? sentenceFor(what, level) : NOT_YET),\n };\n}\n\n/** Nothing offered, because nothing is known yet. */\nexport const NOT_ANSWERED_YET: WhatYouMayDo = whatYouMayDo(null, false);\n\n/**\n * REC-27. The nine Tables the platform ships are not administered from a\n * person's screen whatever level they hold on them — so the structure controls\n * are absent for a kernel Table and say why, and everything else follows the\n * level as usual.\n */\nexport const KERNEL_REASON =\n \"This is one of the tables the platform ships, so its columns are not changed from a screen. \" +\n \"Make your own table to change columns.\";\n\nexport function whatYouMayDoWithTable(\n table: Table | null | undefined,\n level: PermissionLevel | null,\n known: boolean,\n): WhatYouMayDo {\n const base = whatYouMayDo(level, known);\n // A LOCKED DATABASE (lane NOTION-SMALL-2): the columns and the table's settings stay as they are until\n // somebody unlocks it. An editing convenience, never a permission — the lock changes what is OFFERED, the\n // store's ladder is untouched and anyone who may edit the table may unlock it.\n if (!table?.is_kernel) return table && tableIsLocked(table) ? lockedStructure(base) : base;\n return {\n ...base,\n structure: false,\n why: (what) => (what === \"structure\" ? KERNEL_REASON : base.why(what)),\n };\n}\n\n/** Is this table locked against column and settings changes? Kept on the table's own metadata. */\nexport function tableIsLocked(table: Pick<Table, \"metadata\"> | null | undefined): boolean {\n return table?.metadata?.[\"locked\"] === true;\n}\n\nexport const DATABASE_LOCKED_REASON = \"This database is locked, so its columns and settings are read-only. Unlock it to change them.\";\n\nfunction lockedStructure(base: WhatYouMayDo): WhatYouMayDo {\n return { ...base, structure: false, why: (what) => (what === \"structure\" ? DATABASE_LOCKED_REASON : base.why(what)) };\n}\n"],"mappings":"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","names":["import_records"]}
|
|
1
|
+
{"version":3,"sources":["../src/objectActions.ts","../src/fieldTypes.ts","../src/rights.ts"],"sourcesContent":["// src/objectActions.ts — @ai-matrx/records-ui\n//\n// THE ONE ACTION LIST FOR A RECORD-STORE TABLE (lane TABLE-ACTIONS, v6 lane 1).\n//\n// Every menu a person meets on a table — the header ⋯, a row ⋯ on the Data\n// home, right-click, a card, the phone sheet, later ⌘K — is drawn from the\n// array `tableActions()` returns. Before this there were three hand-made lists\n// (`TABLE_MENU` in TablePage, `dataHomeRowActions` in matrx-frontend, the\n// Sheet's own v3 sections) and they drifted: no Archive, no Copy link, no\n// Duplicate, Rename and Share only as hand JSX beside the list.\n//\n// THE SHAPE IS COPIED, NOT INVENTED. It is the in-house registry pattern of\n// `features/agents/browse/agentActionRegistry.tsx` and `conversationActionRegistry.tsx`\n// in matrx-frontend: a context of HOST HANDLERS in, one ordered list out, every\n// renderer reading the same list. The one difference is deliberate: those two\n// build matrx-frontend's `ItemMenuConfig` directly, which this package cannot\n// import, so the list here is renderer-neutral (`ObjectAction[]`) and each\n// renderer has a pure adapter (`toDropdownEntries` / `toStripAndRows` here;\n// `toItemMenuConfig` and `toExtraSections` in matrx-frontend\n// `features/unified-data/actions/`).\n//\n// TWO INVARIANTS, each with a guard (`one-table-action-list-*.test.tsx`,\n// `rights-never-change-the-table-menus-shape.test.tsx`):\n// G1 — every renderer draws the same ids — none hidden, none added, disabled ones\n// drawn disabled with their reason — for every seat and with handlers missing.\n// G2 — rights change only `disabledReason`, never which ids appear, their order,\n// their verb or their rank (Airtable hides items from members; we beat it).\n// A missing host handler is the same: the action stays, disabled, with its reason.\n//\n// THE ORDER (handoff §4, the same everywhere):\n// Open · Open in new tab · Copy link | Rename · Duplicate · Move to… · Favorite |\n// Share… · Export… · Import… | Add column · Settings · History · Make … the default |\n// Built on it ▸ (Forms, Bookings, Checklists, Notifications, Portals, Inbox,\n// Dashboards, Archived records) | Archive table (last, destructive).\n// \"Add column\" and \"Make … the default\" are the header ⋯'s own entries before the\n// registry (TABLE_MENU and the designate item); they sit with Settings.\n//\n// VERB AND RANK — for the icon-strip menu. An action with a `verb` is one of the\n// platform's standard verbs (a strip icon); every other action has a `rank`, its\n// place among the feature rows. Neither changes the list's order: `toStripAndRows`\n// is a renderer that splits the same list, and G1 holds it to the same ids.\n//\n// No JSX here: this file is the `./object-actions` subpath, light enough for a\n// list row's menu to import without pulling every screen of the package.\n\nimport type { Table } from \"@ai-matrx/records\";\nimport { formatShortcut } from \"@ai-matrx/design-system/tap-target\";\nimport { ADD_FIELD_OPEN_LABEL } from \"./fieldTypes\";\nimport { LEVEL_WORD, levelNeededFor, type Capability, type WhatYouMayDo } from \"./rights\";\n\n/**\n * A Lucide icon NAME, kebab-case — the same currency `Table.icon` already uses.\n * Closed on purpose: each renderer maps every member to its own glyph, and a\n * new member fails that renderer's typecheck until it is mapped.\n */\nexport type ObjectActionIcon =\n | \"folder-open\"\n | \"external-link\"\n | \"link\"\n | \"pencil\"\n | \"copy\"\n | \"folder-input\"\n | \"star\"\n | \"star-off\"\n | \"share-2\"\n | \"download\"\n | \"upload\"\n | \"columns-3\"\n | \"settings\"\n | \"history\"\n | \"blocks\"\n | \"file-text\"\n | \"calendar-clock\"\n | \"list-checks\"\n | \"bell\"\n | \"door-open\"\n | \"inbox\"\n | \"layout-dashboard\"\n | \"archive-restore\"\n | \"archive\"\n | \"link-2\"\n | \"layout-grid\"\n | \"git-merge\"\n | \"split\"\n | \"arrow-up-from-line\"\n | \"lock\"\n | \"lock-open\";\n\n/** The groups, in their one order. Separators fall out of group boundaries. */\nexport type ObjectActionGroup = \"open\" | \"organize\" | \"share\" | \"manage\" | \"built-on\" | \"danger\";\n\n/**\n * THE STANDARD VERBS every object's menu shares — what the icon strip draws. One\n * per meaning; an object maps its action onto the verb, never a second \"share\".\n */\nexport type StandardVerbId =\n | \"open\"\n | \"open-new-tab\"\n | \"copy-link\"\n | \"rename\"\n | \"duplicate\"\n | \"move\"\n | \"favorite\"\n | \"share\"\n | \"export\"\n | \"import\"\n | \"settings\"\n | \"history\"\n | \"archive\";\n\nexport const STANDARD_VERBS: readonly StandardVerbId[] = [\n \"open\",\n \"open-new-tab\",\n \"copy-link\",\n \"rename\",\n \"duplicate\",\n \"move\",\n \"favorite\",\n \"share\",\n \"export\",\n \"import\",\n \"settings\",\n \"history\",\n \"archive\",\n];\n\nexport interface ObjectActionGroupDef {\n id: ObjectActionGroup;\n /** Present only on a group that renders as a submenu. */\n label?: string;\n icon?: ObjectActionIcon;\n /** Rendered as ONE submenu entry holding the group's actions. */\n submenu: boolean;\n}\n\nexport const OBJECT_ACTION_GROUPS: readonly ObjectActionGroupDef[] = [\n { id: \"open\", submenu: false },\n { id: \"organize\", submenu: false },\n { id: \"share\", submenu: false },\n { id: \"manage\", submenu: false },\n { id: \"built-on\", label: \"Built on it\", icon: \"blocks\", submenu: true },\n { id: \"danger\", submenu: false },\n];\n\nconst GROUP_IDS = new Set<string>(OBJECT_ACTION_GROUPS.map((g) => g.id));\n\n/** One thing a person can do to an object, for any renderer. */\nexport interface ObjectAction {\n /** Stable within the object's list. React key, test handle, future ⌘K id. */\n id: string;\n label: string;\n icon: ObjectActionIcon;\n group: ObjectActionGroup;\n /** The standard verb this action IS (a strip icon). Absent = a feature row. */\n verb?: StandardVerbId;\n /** A feature row's place among the rows, 1 first. Ignored when `verb` is set. */\n rank?: number;\n /** Display-only hint (\"⌘D\"). */\n shortcut?: string;\n destructive?: boolean;\n /** Present = shown disabled with this line (≤ 60 chars). Absent = enabled. */\n disabledReason?: string;\n /** Called only when enabled. The host owns the side effect and its feedback. */\n run: () => void | Promise<void>;\n}\n\n/** What the list needs to know about the table. A full `Table` satisfies it. */\nexport type TableActionSubject = Pick<Table, \"id\" | \"name\"> & Partial<Pick<Table, \"is_kernel\">>;\n\n/** The places built on a table, in their one order. */\nexport type BuiltOnDestination =\n | \"forms\"\n | \"bookings\"\n | \"checklists\"\n | \"notifications\"\n | \"portals\"\n | \"inbox\"\n | \"dashboards\"\n | \"archived\";\n\ntype Handler = (table: TableActionSubject) => void | Promise<void>;\n\n/**\n * THE SIDE EFFECTS THIS PACKAGE CANNOT OWN. Every handler is optional: one the\n * host leaves out keeps its action in the list, disabled, saying so.\n */\nexport interface TableActionHost {\n /** Origin for Copy link and Open in new tab. Default `https://www.aimatrx.com`. */\n origin?: string;\n open?: Handler;\n /** Receives the table's link (`<origin>/data/<id>`). */\n openInNewTab?: (url: string, table: TableActionSubject) => void;\n /** Receives the table's link. Runs inside the click (clipboard needs the gesture). */\n copyText?: (text: string, table: TableActionSubject) => void | Promise<void>;\n rename?: Handler;\n duplicate?: Handler;\n move?: Handler;\n /** Current favorite state; flips the Favorite label. Unknown = \"Add to favorites\". */\n isFavorite?: boolean;\n toggleFavorite?: Handler;\n share?: Handler;\n export?: Handler;\n import?: Handler;\n addColumn?: Handler;\n settings?: Handler;\n history?: Handler;\n /** Make the layout on screen the table's default. Leave out when it already is. */\n makeDefault?: Handler;\n /** The layout on screen, for \"Make Kanban the default\". */\n layoutLabel?: string;\n /** The \"Built on it\" destinations, one handler for all eight. */\n openBuiltOn?: (destination: BuiltOnDestination, table: TableActionSubject) => void;\n archive?: Handler;\n /**\n * Why an action whose handler is left out is unavailable here, by action id\n * (≤ 60 chars) — e.g. `make-default: \"Already the default\"`. Unnamed ids say\n * \"Not available here\". Never used when rights already refuse.\n */\n unavailableReasons?: Readonly<Record<string, string>>;\n /**\n * ANOTHER LANE'S ENTRIES (\"Link a record…\", a hub tile's verbs, a feature's own\n * row such as \"When a row changes, run an agent…\"). Return the same ids whatever\n * `rights` say — express rights as `disabledReason`. Each entry lands at the END\n * of its named group, in the order returned. Refused, loudly: the `danger` group\n * (Archive is always last), a group that does not exist, and an id already in\n * the list.\n */\n extend?: (table: TableActionSubject, rights: WhatYouMayDo) => readonly ObjectAction[];\n}\n\nexport interface TableActionsInput {\n table: TableActionSubject;\n rights: WhatYouMayDo;\n host: TableActionHost;\n}\n\nexport const DEFAULT_ACTION_ORIGIN = \"https://www.aimatrx.com\";\n\n/** The one link to a table: `<origin>/data/<tableId>`. */\nexport function tableLink(tableId: string, origin: string = DEFAULT_ACTION_ORIGIN): string {\n return `${origin.replace(/\\/+$/, \"\")}/data/${encodeURIComponent(tableId)}`;\n}\n\n/** The designate entry's words, matching `makeDefaultLabel` in views.ts. */\nfunction makeDefaultWords(layoutLabel: string | undefined): string {\n return layoutLabel ? `Make ${layoutLabel} the default` : \"Make this the default\";\n}\n\n// Reasons — one line, ≤ 60 characters, said on the action itself.\nexport const ACTION_NOT_HERE_REASON = \"Not available here\";\nexport const ACTION_CHECKING_ACCESS_REASON = \"Checking your access…\";\nexport const ACTION_NO_ACCESS_REASON = \"You don't have access\";\nexport const ACTION_PLATFORM_TABLE_REASON = \"Platform tables can't be changed here\";\n\n/** Why `rights` refuse `what`, in one short line — or null when they allow it. */\nexport function rightsReason(\n rights: WhatYouMayDo,\n what: Capability,\n table?: TableActionSubject,\n): string | null {\n if (!rights.known) return ACTION_CHECKING_ACCESS_REASON;\n if (what === \"structure\" && table?.is_kernel) return ACTION_PLATFORM_TABLE_REASON;\n if (rights[what]) return null;\n if (rights.level === null) return ACTION_NO_ACCESS_REASON;\n return `Needs ${LEVEL_WORD[levelNeededFor(what)]} access`;\n}\n\n// Feature rows' ranks: what the strip menu shows as its first rows. Lower is first.\nconst BUILT_ON: ReadonlyArray<{ destination: BuiltOnDestination; label: string; icon: ObjectActionIcon; rank: number }> = [\n { destination: \"forms\", label: \"Forms\", icon: \"file-text\", rank: 2 },\n { destination: \"bookings\", label: \"Bookings\", icon: \"calendar-clock\", rank: 6 },\n { destination: \"checklists\", label: \"Checklists\", icon: \"list-checks\", rank: 7 },\n { destination: \"notifications\", label: \"Notifications\", icon: \"bell\", rank: 8 },\n { destination: \"portals\", label: \"Portals\", icon: \"door-open\", rank: 9 },\n { destination: \"inbox\", label: \"Inbox\", icon: \"inbox\", rank: 10 },\n { destination: \"dashboards\", label: \"Dashboards\", icon: \"layout-dashboard\", rank: 3 },\n { destination: \"archived\", label: \"Archived records\", icon: \"archive-restore\", rank: 5 },\n];\n\nconst NOOP = () => {};\n\n/**\n * THE TABLE'S KEYBOARD CHORDS (lane TABLE-ACTIONS wave 4, item 17): ⌥⇧ + one key, the same on\n * every surface that runs the list. Chosen because nothing else owns them: ⌘D is the browser's\n * bookmark, ⌘S save, ⌘P print, ⌘K the command bar, ⌘⇧K the menu's palette, ⌘Z undo — and the\n * app binds no ⌥⇧ chord. A chord is never taken while the person types in a field\n * (`objectActionForKey`). `code` is the KeyboardEvent.code, so ⌥ on a Mac (which changes\n * `key` to a symbol) still matches.\n */\nexport const TABLE_ACTION_CHORDS: Readonly<Record<string, { code: string; chord: string }>> = {\n \"copy-link\": { code: \"KeyL\", chord: \"alt+shift+l\" },\n rename: { code: \"KeyR\", chord: \"alt+shift+r\" },\n duplicate: { code: \"KeyD\", chord: \"alt+shift+d\" },\n move: { code: \"KeyM\", chord: \"alt+shift+m\" },\n favorite: { code: \"KeyF\", chord: \"alt+shift+f\" },\n share: { code: \"KeyS\", chord: \"alt+shift+s\" },\n export: { code: \"KeyE\", chord: \"alt+shift+e\" },\n import: { code: \"KeyI\", chord: \"alt+shift+i\" },\n settings: { code: \"Comma\", chord: \"alt+shift+,\" },\n history: { code: \"KeyH\", chord: \"alt+shift+h\" },\n archive: { code: \"Backspace\", chord: \"alt+shift+backspace\" },\n};\n\n/**\n * The chords with their LABEL in this platform's words — \"⌥⇧S\" on a Mac, \"Alt+Shift+S\" on Windows\n * and Linux — through the design system's one shortcut formatter (`formatShortcut`, the tap\n * button's tooltip), so a menu entry and the ⌘K bar read alike everywhere.\n */\nexport const TABLE_ACTION_KEYS: Readonly<Record<string, { code: string; label: string }>> = Object.fromEntries(\n Object.entries(TABLE_ACTION_CHORDS).map(([id, { code, chord }]) => [id, { code, label: formatShortcut(chord) }]),\n);\n\n/** The fields of a keyboard event a chord reads (a DOM KeyboardEvent satisfies it). */\nexport interface ChordEvent {\n code: string;\n altKey: boolean;\n shiftKey: boolean;\n metaKey: boolean;\n ctrlKey: boolean;\n target?: EventTarget | null;\n}\n\nfunction typingIn(target: EventTarget | null | undefined): boolean {\n const el = target as { tagName?: string; isContentEditable?: boolean } | null | undefined;\n if (!el || typeof el.tagName !== \"string\") return false;\n return el.isContentEditable === true || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);\n}\n\n/**\n * The action a key press asks for, or null: ⌥⇧ + its key, no ⌘/Ctrl, never inside a field. A\n * disabled action is still RETURNED (the caller says its reason instead of doing nothing).\n */\nexport function objectActionForKey(actions: readonly ObjectAction[], event: ChordEvent): ObjectAction | null {\n if (!event.altKey || !event.shiftKey || event.metaKey || event.ctrlKey) return null;\n if (typingIn(event.target)) return null;\n const id = Object.keys(TABLE_ACTION_KEYS).find((k) => TABLE_ACTION_KEYS[k]!.code === event.code);\n return id ? (actions.find((a) => a.id === id) ?? null) : null;\n}\n\n/**\n * One action, from its needs: the capability it asks for and the host handler\n * that performs it. Rights speak first; a missing handler second.\n */\nfunction action(\n base: Omit<ObjectAction, \"run\" | \"disabledReason\">,\n needs: Capability,\n input: TableActionsInput,\n perform: (() => void | Promise<void>) | undefined,\n): ObjectAction {\n const chord = TABLE_ACTION_KEYS[base.id];\n if (chord && base.shortcut === undefined) base = { ...base, shortcut: chord.label };\n const refused = rightsReason(input.rights, needs, input.table);\n if (refused !== null) return { ...base, disabledReason: refused, run: NOOP };\n if (perform === undefined) {\n return { ...base, disabledReason: input.host.unavailableReasons?.[base.id] ?? ACTION_NOT_HERE_REASON, run: NOOP };\n }\n return { ...base, run: perform };\n}\n\nfunction bind(handler: Handler | undefined, table: TableActionSubject): (() => void | Promise<void>) | undefined {\n return handler ? () => handler(table) : undefined;\n}\n\n/**\n * THE TABLE'S ACTIONS, in the one order. Same ids, same order, same verbs and\n * ranks, for every seat and every host — only `disabledReason` varies.\n */\nexport function tableActions(input: TableActionsInput): ObjectAction[] {\n const { table, host } = input;\n const url = tableLink(table.id, host.origin);\n const favorite = host.isFavorite === true;\n const openInNewTab = host.openInNewTab;\n const copyText = host.copyText;\n const openBuiltOn = host.openBuiltOn;\n\n const built: ObjectAction[] = [\n action({ id: \"open\", label: \"Open\", icon: \"folder-open\", group: \"open\", verb: \"open\" }, \"read\", input, bind(host.open, table)),\n action(\n { id: \"open-new-tab\", label: \"Open in new tab\", icon: \"external-link\", group: \"open\", verb: \"open-new-tab\" },\n \"read\",\n input,\n openInNewTab ? () => openInNewTab(url, table) : undefined,\n ),\n action(\n { id: \"copy-link\", label: \"Copy link\", icon: \"link\", group: \"open\", verb: \"copy-link\" },\n \"read\",\n input,\n copyText ? () => copyText(url, table) : undefined,\n ),\n action({ id: \"rename\", label: \"Rename…\", icon: \"pencil\", group: \"organize\", verb: \"rename\" }, \"structure\", input, bind(host.rename, table)),\n action({ id: \"duplicate\", label: \"Duplicate\", icon: \"copy\", group: \"organize\", verb: \"duplicate\" }, \"read\", input, bind(host.duplicate, table)),\n action({ id: \"move\", label: \"Move to…\", icon: \"folder-input\", group: \"organize\", verb: \"move\" }, \"structure\", input, bind(host.move, table)),\n action(\n {\n id: \"favorite\",\n label: favorite ? \"Remove from favorites\" : \"Add to favorites\",\n icon: favorite ? \"star-off\" : \"star\",\n group: \"organize\",\n verb: \"favorite\",\n },\n \"read\",\n input,\n bind(host.toggleFavorite, table),\n ),\n action({ id: \"share\", label: \"Share…\", icon: \"share-2\", group: \"share\", verb: \"share\" }, \"share\", input, bind(host.share, table)),\n action({ id: \"export\", label: \"Export…\", icon: \"download\", group: \"share\", verb: \"export\" }, \"read\", input, bind(host.export, table)),\n action({ id: \"import\", label: \"Import…\", icon: \"upload\", group: \"share\", verb: \"import\" }, \"structure\", input, bind(host.import, table)),\n action({ id: \"add-column\", label: ADD_FIELD_OPEN_LABEL, icon: \"columns-3\", group: \"manage\", rank: 1 }, \"structure\", input, bind(host.addColumn, table)),\n action({ id: \"settings\", label: \"Settings\", icon: \"settings\", group: \"manage\", verb: \"settings\" }, \"structure\", input, bind(host.settings, table)),\n action({ id: \"history\", label: \"History\", icon: \"history\", group: \"manage\", verb: \"history\" }, \"read\", input, bind(host.history, table)),\n // custom.view_designate asks editor on the table — `canDesignate` in TablePage.\n action(\n { id: \"make-default\", label: makeDefaultWords(host.layoutLabel), icon: \"layout-grid\", group: \"manage\", rank: 4 },\n \"organise\",\n input,\n bind(host.makeDefault, table),\n ),\n ...BUILT_ON.map(({ destination, label, icon, rank }) =>\n action(\n { id: `built-on.${destination}`, label, icon, group: \"built-on\", rank },\n \"read\",\n input,\n openBuiltOn ? () => openBuiltOn(destination, table) : undefined,\n ),\n ),\n action(\n { id: \"archive\", label: \"Archive table\", icon: \"archive\", group: \"danger\", destructive: true, verb: \"archive\" },\n \"structure\",\n input,\n bind(host.archive, table),\n ),\n ];\n\n return withExtensions(built, host.extend ? host.extend(table, input.rights) : []);\n}\n\n/**\n * Another lane's entries, placed deterministically: each at the end of its\n * group, in the order given. Loud on every mistake that would break the\n * invariants — a `danger` entry (Archive must stay last), a group that does not\n * exist (it would be dropped by every renderer) and a duplicate id.\n */\nexport function withExtensions(base: readonly ObjectAction[], extra: readonly ObjectAction[]): ObjectAction[] {\n if (extra.length === 0) return [...base];\n const seen = new Set(base.map((a) => a.id));\n for (const entry of extra) {\n if (entry.group === \"danger\") {\n throw new Error(`Object action \"${entry.id}\": the danger group is closed to extensions.`);\n }\n if (!GROUP_IDS.has(entry.group)) {\n throw new Error(`Object action \"${entry.id}\": \"${String(entry.group)}\" is not a group.`);\n }\n if (seen.has(entry.id)) {\n throw new Error(`Object action \"${entry.id}\" is already in the list.`);\n }\n seen.add(entry.id);\n }\n const out: ObjectAction[] = [];\n for (const group of OBJECT_ACTION_GROUPS) {\n out.push(...base.filter((a) => a.group === group.id), ...extra.filter((a) => a.group === group.id));\n }\n return out;\n}\n\n/** A run of actions that renders together: a plain section, or one submenu. */\nexport interface ObjectActionSection {\n group: ObjectActionGroupDef;\n actions: ObjectAction[];\n}\n\n/**\n * THE ONE GROUPING every adapter uses: the list cut at group boundaries, in\n * group order, empty groups dropped, the order inside a group kept.\n */\nexport function groupObjectActions(actions: readonly ObjectAction[]): ObjectActionSection[] {\n const sections: ObjectActionSection[] = [];\n for (const group of OBJECT_ACTION_GROUPS) {\n const inGroup = actions.filter((a) => a.group === group.id);\n if (inGroup.length > 0) sections.push({ group, actions: inGroup });\n }\n return sections;\n}\n\n/** The header ⋯ dropdown's entries — records-ui's own renderer reads these. */\nexport type ObjectActionDropdownEntry =\n | { kind: \"separator\"; id: string }\n | { kind: \"item\"; action: ObjectAction }\n | { kind: \"submenu\"; id: string; label: string; icon: ObjectActionIcon | undefined; actions: ObjectAction[] };\n\nexport function toDropdownEntries(actions: readonly ObjectAction[]): ObjectActionDropdownEntry[] {\n const entries: ObjectActionDropdownEntry[] = [];\n groupObjectActions(actions).forEach(({ group, actions: inGroup }, index) => {\n if (index > 0) entries.push({ kind: \"separator\", id: `separator:${group.id}` });\n if (group.submenu) {\n entries.push({ kind: \"submenu\", id: group.id, label: group.label ?? group.id, icon: group.icon, actions: inGroup });\n } else {\n for (const a of inGroup) entries.push({ kind: \"item\", action: a });\n }\n });\n return entries;\n}\n\n/** The actions a dropdown draws, in order — what G1 compares. */\nexport function dropdownActions(entries: readonly ObjectActionDropdownEntry[]): ObjectAction[] {\n return entries.flatMap((e) => (e.kind === \"item\" ? [e.action] : e.kind === \"submenu\" ? e.actions : []));\n}\n\n/** The action ids a dropdown draws, in order. */\nexport function dropdownActionIds(entries: readonly ObjectActionDropdownEntry[]): string[] {\n return dropdownActions(entries).map((a) => a.id);\n}\n\n/** The icon-strip menu: standard verbs as icons, then the first feature rows, then the rest. */\nexport interface StripAndRows {\n /** Every action with a `verb`, in the list's order. */\n strip: ObjectAction[];\n /** The first `rowCount` feature actions by rank (ties keep list order). */\n rows: ObjectAction[];\n /** The remaining feature actions, behind \"More … options\", same ordering. */\n more: ObjectAction[];\n}\n\nexport const STRIP_FEATURE_ROWS = 4;\n\n/**\n * THE STRIP RENDERER'S SPLIT of the same list. Nothing is added or dropped: every\n * id lands in exactly one of strip / rows / more (G1). Unranked feature actions\n * (an extension that gave none) follow every ranked one.\n */\nexport function toStripAndRows(actions: readonly ObjectAction[], rowCount: number = STRIP_FEATURE_ROWS): StripAndRows {\n const strip = actions.filter((a) => a.verb !== undefined);\n const features = actions\n .map((a, index) => ({ a, index }))\n .filter(({ a }) => a.verb === undefined)\n .sort((x, y) => (x.a.rank ?? Infinity) - (y.a.rank ?? Infinity) || x.index - y.index)\n .map(({ a }) => a);\n return { strip, rows: features.slice(0, rowCount), more: features.slice(rowCount) };\n}\n","// src/fieldTypes.ts — @ai-matrx/records-ui\n//\n// EVERY KIND OF COLUMN THE STORE TAKES, IN THE WORDS A PERSON USES.\n//\n// The store publishes fourteen parity types and, for each, a sentence about what\n// it is MADE OF — \"REC-31: a File record reached through a relation, target the\n// kernel `File` Table\", \"FLD-9: a declared computation, on read or on write,\n// evaluated by custom.rule_eval\", \"config.via names the relation field,\n// config.pick the Value on the far side\". Those sentences are true and they are\n// for us. The 19 September verdict found them printed at the person choosing a\n// column type, alongside contract row ids, and called it what it is: machine\n// identity where a person is looking.\n//\n// So this file holds the OTHER sentence — what the type is FOR — and the field\n// panel shows that one. The store's own `made_of` stays where it belongs, in\n// `PARITY_MADE_OF`, for an engineer reading the package.\n//\n// The LIST itself is still the store's: `PARITY_FIELD_TYPES` is generated from\n// `custom.parity_field_types()`, and `assertEveryParityTypeIsExplained` (run by\n// the suite) fails the moment the store ships a fourteenth and nobody wrote a\n// sentence for it. A type with no explanation would otherwise appear with a\n// blank line under it, which is the silent version of the same defect. It caught the\n// fourteenth — `checkbox`, 2026-09-21 — exactly as it was written to.\n\nimport { DECLARABLE_FIELD_KINDS, NEWER_FIELD_KINDS, PARITY_FIELD_TYPES, type DeclarableFieldKind, type NewerFieldKind, type ParityFieldType } from \"@ai-matrx/records\";\n\n/** The three behaviours that carry no parity type of their own. */\nexport type PlainFieldType = \"text\" | \"long_text\" | \"number\";\n\n/**\n * THE ONE A PERSON COMING FROM AIRTABLE REACHES FOR FIRST, AND IT WAS NOT HERE.\n *\n * The list above offered `member` (a Person) and `attachment` (a File) — the two\n * relations whose target is a Table the PLATFORM keeps — and nothing at all for\n * \"this job belongs to that customer\". The store has taken it since 2026-09-20\n * (`custom.field_declare` with `type: \"relation\"` and a `relation_target`), the\n * Field type has named it since the same day, and `editorKindFor` has answered\n * `\"relation\"` for one since long before that. Only the menu did not offer it,\n * so the guide's section 3 told a person to pick a kind that did not exist.\n *\n * It is NOT a parity type of its own. `custom.parity_field_types()` ships\n * fourteen (the fourteenth is `checkbox`) and a relation is a BEHAVIOUR — the same behaviour `member` and\n * `attachment` are made of — so it sits beside the three plain ones as its own\n * word, and `parityTypesWithNoExplanation()` is unaffected by it in both\n * directions: it does not appear there, and it cannot hide a real gap there.\n */\nexport type RelationFieldType = \"relation\";\n\n/**\n * What a person picks in the panel — AND IT IS THE STORE'S OWN LIST, not a union\n * assembled here.\n *\n * 🚨 THE CLASS FIX (FIX-10B-F6, VERIFIER-10). This used to read\n * `ParityFieldType | PlainFieldType | RelationFieldType` — the parity floor plus two words\n * this file knew about — and that union was the whole defect. `custom.doc_sign` accepts\n * exactly one field, a text column whose format is `signature`, and it is granted to\n * `authenticated`; the Documents screen told a person in so many words to go and declare\n * one; and no list any screen could read named it, so the control did not exist and the\n * e-sign half of Documents was unreachable from the product. A kind the store takes and no\n * screen offers is invisible, and nothing failed — which is the only reason it survived.\n *\n * `custom.field_kinds()` is now the ONE registry and `DECLARABLE_FIELD_KINDS` is generated\n * from it, so the next kind the store learns arrives here as a TYPE ERROR in\n * `FIELD_TYPE_CHOICES`'s coverage and as a red `kindsWithNoChoice()` below — never as a\n * silence.\n */\nexport type PickableFieldType = DeclarableFieldKind | SystemFieldKind | \"entity_reference\" | NewerFieldKind | LookFieldKind;\n\n/**\n * A KIND THAT IS A PLAIN COLUMN WEARING A LOOK (grids review 3, lane H item 5). The store keeps a\n * column's \"Shows as\" on the Field (`display_format`, `custom.display_format_ids()` has `rating` and\n * `time`), so a Rating is a whole number shown as stars and a Time is a time of day kept as its\n * 24-hour text — declared through the ordinary door (`{ plain, display_format }`) the same way the\n * \"Shows as\" picker makes one. Offered whenever the store does not yet publish the kind as its own\n * word (`custom.field_kinds()`): a Rating the store publishes goes as `{ type: \"rating\", max }`.\n */\nexport type LookFieldKind = \"time\";\n\nexport interface LookKind {\n plain: \"number\" | \"text\";\n look: (args: { ratingMax: number }) => { id: string; options?: Record<string, unknown> };\n}\n\nexport const LOOK_KINDS: Partial<Record<PickableFieldType, LookKind>> = {\n rating: { plain: \"number\", look: ({ ratingMax }) => ({ id: \"rating\", options: { ratingMax } }) },\n time: { plain: \"text\", look: () => ({ id: \"time\" }) },\n};\n\n/** Is this kind declared as a plain column wearing a look (because the store does not publish it)? */\nexport function declaredAsLook(kind: string): boolean {\n return kind in LOOK_KINDS && !storePublishesKind(kind);\n}\n\n/**\n * LANE 10 P4: THE NINE NEWER KINDS (rating, duration, created by, last changed by, status, address,\n * barcode, formatted text, count). Each is listed in `FIELD_TYPE_CATALOGUE` below, so every reader\n * (Settings, the column panel, every layout) names a column of one by its own word; the MENU offers\n * one only once `custom.field_kinds()` publishes it (after the chair's apply and\n * `pnpm records:generate`), so a person is never offered a kind the live store would refuse.\n */\nexport function storePublishesKind(kind: string): boolean {\n return DECLARABLE_FIELD_KINDS.some((row) => (row.kind as string) === kind);\n}\n\n/**\n * LANE SC-R: a column that points at a PLATFORM thing (a note, a web site, an agent). Its menu entry\n * is drawn only once `custom.field_kinds()` publishes `entity_reference` (after the chair's apply\n * and `pnpm records:generate`), so the menu never offers a kind the live store would refuse.\n */\nexport const STORE_PUBLISHES_ENTITY_REFERENCE = DECLARABLE_FIELD_KINDS.some(\n (row) => (row.kind as string) === \"entity_reference\",\n);\n\n/**\n * THE STORE'S OWN STAMPS AS COLUMNS (GRID-PRIMITIVES G5; TABLE-PARITY H2 H3, lane\n * DEFAULT-GRID-PARITY). `custom.field_declare` takes these three WORDS and makes each a formula\n * Field whose expression is the system node (`fx.autonumber`, `fx.created_time`,\n * `fx.modified_time`, `config.system` naming which), so nobody types into them and every reader\n * that serves a worked-out value serves them. `custom.field_kinds()` does not publish them,\n * because each is a formula, not a behaviour of its own. So they are named here, sent as their\n * word through the `{ type }` arm, and a new autonumber column numbers the records already in\n * the table through `custom.autonumber_backfill`.\n */\nexport type SystemFieldKind = \"autonumber\" | \"created_time\" | \"modified_time\";\n\nexport const SYSTEM_FIELD_KINDS: readonly SystemFieldKind[] = [\"autonumber\", \"created_time\", \"modified_time\"];\n\nexport function isSystemFieldKind(id: string): id is SystemFieldKind {\n return (SYSTEM_FIELD_KINDS as readonly string[]).includes(id);\n}\n\nexport interface FieldTypeChoice {\n id: PickableFieldType;\n /** The word on the menu. */\n label: string;\n /** One line, under the word, saying what it is FOR. Never what it is made of. */\n explanation: string;\n /** The heading it sits under, so seventeen choices read as four short lists. */\n group: \"Words\" | \"Numbers and dates\" | \"Choices and people\" | \"Worked out\";\n}\n\n/**\n * EVERY KIND THIS PACKAGE CAN NAME AND DRAW, in menu order. The menu (`FIELD_TYPE_CHOICES`) is this\n * list less the kinds the live store does not take yet; every reader names a column from this one.\n */\nexport const FIELD_TYPE_CATALOGUE: FieldTypeChoice[] = [\n { id: \"text\", group: \"Words\", label: \"Text\", explanation: \"A short line of writing — a name, a reference, a title.\" },\n { id: \"long_text\", group: \"Words\", label: \"Long text\", explanation: \"Several lines — a description, notes, an address.\" },\n { id: \"rich_text\", group: \"Words\", label: \"Formatted text\", explanation: \"Notes with bold, lists and links.\" },\n { id: \"email\", group: \"Words\", label: \"Email address\", explanation: \"An email address, checked as you type and ready to write to.\" },\n { id: \"phone\", group: \"Words\", label: \"Phone number\", explanation: \"A phone number, checked as you type.\" },\n { id: \"url\", group: \"Words\", label: \"Link\", explanation: \"A web address that opens in a new tab when you click it.\" },\n { id: \"address\", group: \"Words\", label: \"Address\", explanation: \"A street address in parts, opened in maps.\" },\n { id: \"barcode\", group: \"Words\", label: \"Barcode\", explanation: \"A code a scanner reads, like a wristband or a box.\" },\n { id: \"button\", group: \"Words\", label: \"Button\", explanation: \"A button on each record that opens a link or runs an action.\" },\n\n { id: \"number\", group: \"Numbers and dates\", label: \"Number\", explanation: \"A plain number you can total up and sort by.\" },\n { id: \"currency\", group: \"Numbers and dates\", label: \"Money\", explanation: \"An amount of money, shown with its currency wherever it appears.\" },\n { id: \"percent\", group: \"Numbers and dates\", label: \"Percentage\", explanation: \"A number out of a hundred, kept between 0 and 100.\" },\n { id: \"rating\", group: \"Numbers and dates\", label: \"Rating\", explanation: \"A score in stars, set with one click.\" },\n { id: \"duration\", group: \"Numbers and dates\", label: \"Duration\", explanation: \"A length of time, typed as 45 min or 1:30.\" },\n { id: \"time\", group: \"Numbers and dates\", label: \"Time\", explanation: \"A time of day with no date — a shift start, opening hours.\" },\n { id: \"datetime\", group: \"Numbers and dates\", label: \"Date\", explanation: \"A day, or a day and a time. Dates are what the calendar view reads.\" },\n\n { id: \"select\", group: \"Choices and people\", label: \"Choice\", explanation: \"One answer from a list you write here. Choices are what the board view groups by.\" },\n { id: \"multi_select\", group: \"Choices and people\", label: \"Several choices\", explanation: \"Any number of answers from a list you write here.\" },\n { id: \"status\", group: \"Choices and people\", label: \"Status\", explanation: \"Choices grouped into To do, In progress and Done.\" },\n { id: \"checkbox\", group: \"Choices and people\", label: \"Yes / no\", explanation: \"A box you tick. Left alone it stays unanswered, which is not the same as a no.\" },\n { id: \"member\", group: \"Choices and people\", label: \"Person\", explanation: \"Somebody in this organization, picked from the people who are in it.\" },\n { id: \"attachment\", group: \"Choices and people\", label: \"File\", explanation: \"A file or a picture kept with the record.\" },\n { id: \"signature\", group: \"Choices and people\", label: \"Signature\", explanation: \"Somebody's sign-off on a document made from this record. Nobody types in it — it fills in when they sign.\" },\n { id: \"relation\", group: \"Choices and people\", label: \"Points at another record\", explanation: \"A link to records in another of your tables — the customer a job is for, the lines on an invoice.\" },\n {\n id: \"entity_reference\",\n group: \"Choices and people\",\n label: \"Points at something else on the platform\",\n explanation: \"A note, a web site, an agent, a workbook — anything you keep here that is not one of your tables.\",\n },\n\n { id: \"lookup\", group: \"Worked out\", label: \"Borrowed value\", explanation: \"A value read from a record this one points at, kept in step with it.\" },\n { id: \"rollup\", group: \"Worked out\", label: \"Total of linked records\", explanation: \"A count or a total worked out from the records this one points at.\" },\n { id: \"count\", group: \"Worked out\", label: \"Count of linked records\", explanation: \"How many records this one points at.\" },\n { id: \"formula\", group: \"Worked out\", label: \"Worked out from other columns\", explanation: \"A value this record works out for itself from its own other columns.\" },\n { id: \"autonumber\", group: \"Worked out\", label: \"Record number\", explanation: \"A number the table gives each record, one after another, never reused. Records already here are numbered in the order they were made.\" },\n { id: \"created_time\", group: \"Worked out\", label: \"Created time\", explanation: \"When each record was made. Nobody types it.\" },\n { id: \"modified_time\", group: \"Worked out\", label: \"Last changed time\", explanation: \"When each record last changed. Nobody types it.\" },\n { id: \"created_by\", group: \"Worked out\", label: \"Created by\", explanation: \"Who made each record. Nobody types it.\" },\n { id: \"modified_by\", group: \"Worked out\", label: \"Last changed by\", explanation: \"Who last changed each record. Nobody types it.\" },\n];\n\n/** The kinds the menu offers only once the live store publishes them. */\nconst GATED_KINDS = new Set<string>([\"entity_reference\", ...NEWER_FIELD_KINDS]);\n\nexport const FIELD_TYPE_CHOICES: FieldTypeChoice[] = FIELD_TYPE_CATALOGUE.filter(\n (c) => !GATED_KINDS.has(c.id) || storePublishesKind(c.id) || c.id in LOOK_KINDS,\n);\n\nexport const FIELD_TYPE_GROUPS: FieldTypeChoice[\"group\"][] = [\n \"Words\",\n \"Numbers and dates\",\n \"Choices and people\",\n \"Worked out\",\n];\n\nconst BY_ID = new Map<string, FieldTypeChoice>(FIELD_TYPE_CATALOGUE.map((c) => [c.id, c]));\n\nexport function fieldTypeChoice(id: string): FieldTypeChoice | undefined {\n return BY_ID.get(id);\n}\n\n/** The three that are a plain behaviour rather than one of the fourteen. */\nexport function isPlainFieldType(id: string): id is PlainFieldType {\n return id === \"text\" || id === \"long_text\" || id === \"number\";\n}\n\n/**\n * The one that is a relation a person aims themselves. It is neither plain nor\n * one of the thirteen, so every caller that branched on `isPlainFieldType`\n * alone has to answer this question too — which is why it is a named function\n * and not an inline string comparison at four call sites.\n */\nexport function isRelationFieldType(id: string): id is RelationFieldType {\n return id === \"relation\";\n}\n\n/**\n * One of FLD-11's fourteen, which is what `parity_type` on a declaration means.\n *\n * Read from the store's own generated floor, never from a list here — because a kind that\n * is NOT one of the fourteen (plain text, a person-aimed relation, a signature) has to be\n * declared by its WORD (`type`) and a panel that sent `parity_type` for it would be refused\n * \"There is no field type called …\". That is the shape of the F6 defect: the panel's `save`\n * had exactly two branches, relation and plain, and everything else fell into `parity_type`.\n */\nexport function isParityFieldType(id: string): id is ParityFieldType {\n return PARITY_FIELD_TYPES.some((row) => row.parity_type === id);\n}\n\n/**\n * THE GUARD THIS FILE NEEDS, because the list it explains is generated.\n *\n * Returns the parity types the store ships that nothing here explains. The\n * suite asserts it is empty; a fourteenth type would otherwise reach the menu\n * with a blank line under it and nobody would notice until a person did.\n *\n * `relation` is deliberately NOT one of the thirteen and therefore never\n * appears in this answer: `PARITY_FIELD_TYPES` is the store's own generated\n * list and a behaviour that carries no parity type is not in it.\n */\nexport function parityTypesWithNoExplanation(): string[] {\n return PARITY_FIELD_TYPES.map((row) => row.parity_type).filter((type) => !BY_ID.has(type));\n}\n\n/**\n * 🚨 THE GUARD THAT CLOSES THE CLASS (FIX-10B-F6).\n *\n * `parityTypesWithNoExplanation()` above watches the parity FLOOR, and the floor was never\n * the whole list: the store also declares plain text, long text, number, a person-aimed\n * relation and a signature, and for months nothing anywhere compared the panel's menu with\n * THAT. So `signature` — the one field `custom.doc_sign` accepts, on a door granted to\n * every signed-in person, named to the person by the Documents screen itself — was absent\n * from the menu and every check stayed green.\n *\n * This returns the kinds `custom.field_kinds()` publishes that this panel does not offer.\n * The suite asserts it is empty, so a kind the store learns tomorrow either reaches the\n * person or turns a test red the same day. It is the class, not the instance: it would have\n * caught `signature`, and it will catch the twentieth kind nobody has thought of yet.\n */\nexport function kindsWithNoChoice(): string[] {\n return DECLARABLE_FIELD_KINDS.map((row) => row.kind).filter((kind) => !BY_ID.has(kind));\n}\n\n// ───────────────────────────────────────────────────────────────────────────\n// THE WORDS ON THE TWO BUTTONS, IN ONE PLACE — because they used to be the SAME\n// WORDS in two places, and that cost somebody their column.\n//\n// 🚨 THE DEFECT (TAILS-5, 2026-09-21, measured while driving the panel headless\n// on Rincon Plumbing's Summerland branch). The Jobs toolbar's control that OPENS\n// the field panel read exactly \"Add field\", and the panel's own SAVE button read\n// exactly \"Add field\" too — `FieldEditor` says \"Save field\" only when EDITING an\n// existing column, so on the create path the two were character-identical. The\n// toolbar control is a TOGGLE: pressing it while the panel is open closes the\n// panel and throws away everything typed into it. So a person who typed a column\n// name, chose a type, picked the target table and its display columns, then\n// looked up at the words they were told to press, could lose all of it — with no\n// sentence, no request sent, and nothing to say it had happened. A walk written\n// by an agent lost the column twice that way and spent a round diagnosing the\n// store before reading the two labels side by side.\n//\n// WHY THEY LIVE HERE AND NOT IN `@ai-matrx/design-system`. The words are RECORDS\n// vocabulary — \"field\", \"column\" — and the design system knows nothing about\n// either; a labels module there would be a domain word in a generic package, and\n// the next person adding a button would not find it. This file is already the\n// one place that owns what a person READS about a column (`FIELD_TYPE_CHOICES`\n// is right above), and both surfaces that draw these buttons are in this\n// package. One module, one import each, and `fieldTypes.test.ts` asserts the two\n// can never be the same string again.\n//\n// THE RULE: the control that OPENS a panel and the control that COMMITS it never\n// read the same words. The opener is an invitation; the committer says what it\n// will make.\n\n/** The toolbar control that OPENS the add-a-column panel. An invitation. */\nexport const ADD_FIELD_OPEN_LABEL = \"Add column\";\n\n/** The panel's own primary, on the CREATE path. It says what pressing it makes. */\nexport const ADD_FIELD_SAVE_LABEL = \"Create column\";\n\n/** The panel's own primary when an existing column is being changed. */\nexport const EDIT_FIELD_SAVE_LABEL = \"Save column\";\n","// src/rights.ts — @ai-matrx/records-ui\n//\n// THE ONE PLACE A LEVEL BECOMES A SCREEN.\n//\n// WHAT A PERSON HIT (independent verdict, sixth pass, 19-20 September). A\n// colleague who had been shared a table at viewer opened it, double-clicked a\n// cell, got a live editor, typed a value and pressed Enter — and only THEN was\n// told she may not write. \"The permission is right; the screen told her she\n// could.\" Measured against Airtable, Notion and Linear the verdict called it\n// out by name: \"None of these three products lets you start an action it\n// already knows it will reject.\"\n//\n// WHY THE SCREEN COULD NOT KNOW. The rights question was a PORT with two honest\n// answers and no third: `NO_RIGHTS` (every administering control absent, which\n// is useless for a host whose authority IS the store's) and\n// `storeDecidesRights` — `{admin: true, comment: true, write: true}` for every\n// non-kernel table, for everybody. That second one is the defect, written down:\n// it is the screen ASSUMING the answer and letting the door say no afterwards.\n//\n// WHAT REPLACES IT. The store already answers this, and the client now carries\n// it: `custom.read_records` returns the reader's level on the TABLE on every\n// row (so `Table.your_level` costs nothing), and `custom.my_levels` — the door\n// this lane added — answers the caller's level on any subject they name, one\n// call for a whole page. This file is the only place in the package that turns\n// one of those four words into what a screen may DRAW.\n//\n// THE LADDER IS THE STORE'S, NOT OURS: viewer < commenter < editor < admin, and\n// which rung each door requires is read from the doors themselves —\n// `custom.record_update` asserts `editor` on the record, `custom.field_declare`\n// asserts `admin` on the TABLE (\"the SHAPE of this table, which is an admin's\n// right and not an editor's\"), `custom.io_comment_write` admits a commenter.\n// Nothing here invents a rule; it mirrors the four assertions those doors make\n// and is wrong the moment they change, which is what the suite is for.\n//\n// ABSENT OR HONEST, NEVER DEAD. Every `false` here carries the sentence that\n// says why and what to do about it, so a control can be left out AND explained.\n// A screen that hid a control with nothing to say is the silent half of the\n// same defect.\n\nimport { atLeast, type PermissionLevel, type Table } from \"@ai-matrx/records\";\n\n/** The six things a person does to a record or a table, as the screens offer them. */\nexport type Capability = \"read\" | \"comment\" | \"write\" | \"organise\" | \"remove\" | \"share\" | \"structure\";\n\nexport interface WhatYouMayDo {\n /** The word the store answered, or null when it answered nothing. */\n level: PermissionLevel | null;\n /**\n * Whether the store has ANSWERED yet. `false` is a third state and not a\n * refusal: a screen that drew \"you may not\" while the answer was in flight\n * would blink every control off on every reload, which is its own lie.\n */\n known: boolean;\n read: boolean;\n comment: boolean;\n write: boolean;\n /** Duplicate, merge, restore, make a layout the default: the doors that ask `editor`, above content. */\n organise: boolean;\n remove: boolean;\n share: boolean;\n structure: boolean;\n /** Why this person may not do that, in one sentence naming the way in. */\n why: (what: Capability) => string;\n}\n\n/** What each rung of the ladder is called where a person reads it. */\nexport const LEVEL_WORD: Record<PermissionLevel, string> = {\n viewer: \"Viewer\",\n commenter: \"Commenter\",\n edit_content: \"Content editor\",\n editor: \"Editor\",\n admin: \"Admin\",\n};\n\n/**\n * WHICH RUNG EACH DOOR ASKS FOR. Read off the doors themselves, in one place,\n * so a screen never re-decides it and the suite has one thing to check.\n */\nconst NEEDS: Record<Capability, PermissionLevel> = {\n read: \"viewer\",\n // custom.io_comment_write\n comment: \"commenter\",\n // custom.record_update / custom.record_write — `edit_content` changes rows and page content;\n // columns, views and settings (structure) stay with admin below, and sharing with admin.\n write: \"edit_content\",\n // custom.view_designate / view_declare / migrate_merge / record_restore — all ask `editor`: more than\n // changing content (`edit_content`), less than changing columns (`admin`).\n organise: \"editor\",\n // custom.record_delete\n remove: \"editor\",\n // custom.share_grant — passing your access on is an administrator's right\n share: \"admin\",\n // custom.field_declare / field_update / field_retire / table settings\n structure: \"admin\",\n};\n\n/** The rung a capability asks for — so a short \"Needs Admin access\" never re-decides the ladder. */\nexport function levelNeededFor(what: Capability): PermissionLevel {\n return NEEDS[what];\n}\n\nconst NOT_YET =\n \"Still asking the store what you may do here. Nothing is offered until it has answered.\";\n\nconst NOTHING_AT_ALL =\n \"You have not been given this. Ask somebody who administers it to share it with you.\";\n\n/** The sentence for one refusal: what the level is, what it would have to be. */\nfunction sentenceFor(what: Capability, level: PermissionLevel | null): string {\n if (level === null) return NOTHING_AT_ALL;\n const need = LEVEL_WORD[NEEDS[what]];\n const have = LEVEL_WORD[level];\n const doing: Record<Capability, string> = {\n read: \"open this\",\n comment: \"leave a comment\",\n write: \"change anything here\",\n organise: \"organise this\",\n remove: \"delete this\",\n share: \"pass this on to somebody else\",\n structure: \"change the columns of this table\",\n };\n return `You are ${have} here, and it takes ${need} to ${doing[what]}. Ask somebody who is Admin on it to move you up.`;\n}\n\n/**\n * WHAT ONE LEVEL LETS A PERSON DO. `level` null and `known` true means the\n * store said \"nothing\" — a real answer. `known` false means it has not said.\n */\nexport function whatYouMayDo(level: PermissionLevel | null, known = true): WhatYouMayDo {\n const may = (what: Capability) => known && atLeast(level, NEEDS[what]);\n return {\n level: known ? level : null,\n known,\n read: may(\"read\"),\n comment: may(\"comment\"),\n write: may(\"write\"),\n organise: may(\"organise\"),\n remove: may(\"remove\"),\n share: may(\"share\"),\n structure: may(\"structure\"),\n why: (what) => (known ? sentenceFor(what, level) : NOT_YET),\n };\n}\n\n/** Nothing offered, because nothing is known yet. */\nexport const NOT_ANSWERED_YET: WhatYouMayDo = whatYouMayDo(null, false);\n\n/**\n * REC-27. The nine Tables the platform ships are not administered from a\n * person's screen whatever level they hold on them — so the structure controls\n * are absent for a kernel Table and say why, and everything else follows the\n * level as usual.\n */\nexport const KERNEL_REASON =\n \"This is one of the tables the platform ships, so its columns are not changed from a screen. \" +\n \"Make your own table to change columns.\";\n\nexport function whatYouMayDoWithTable(\n table: Table | null | undefined,\n level: PermissionLevel | null,\n known: boolean,\n): WhatYouMayDo {\n const base = whatYouMayDo(level, known);\n // A LOCKED DATABASE (lane NOTION-SMALL-2): the columns and the table's settings stay as they are until\n // somebody unlocks it. An editing convenience, never a permission — the lock changes what is OFFERED, the\n // store's ladder is untouched and anyone who may edit the table may unlock it.\n if (!table?.is_kernel) return table && tableIsLocked(table) ? lockedStructure(base) : base;\n return {\n ...base,\n structure: false,\n why: (what) => (what === \"structure\" ? KERNEL_REASON : base.why(what)),\n };\n}\n\n/** Is this table locked against column and settings changes? Kept on the table's own metadata. */\nexport function tableIsLocked(table: Pick<Table, \"metadata\"> | null | undefined): boolean {\n return table?.metadata?.[\"locked\"] === true;\n}\n\nexport const DATABASE_LOCKED_REASON = \"This database is locked, so its columns and settings are read-only. Unlock it to change them.\";\n\nfunction lockedStructure(base: WhatYouMayDo): WhatYouMayDo {\n return { ...base, structure: false, why: (what) => (what === \"structure\" ? DATABASE_LOCKED_REASON : base.why(what)) };\n}\n"],"mappings":"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","names":["import_records"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import '@ai-matrx/records';
|
|
2
|
-
export { A as ACTION_CHECKING_ACCESS_REASON, b as ACTION_NOT_HERE_REASON, c as ACTION_NO_ACCESS_REASON, d as ACTION_PLATFORM_TABLE_REASON, B as BuiltOnDestination, E as ChordEvent, D as DEFAULT_ACTION_ORIGIN, e as OBJECT_ACTION_GROUPS, O as ObjectAction, f as ObjectActionDropdownEntry, g as ObjectActionGroup, h as ObjectActionGroupDef, a as ObjectActionIcon, i as ObjectActionSection, S as STANDARD_VERBS, j as STRIP_FEATURE_ROWS, k as StandardVerbId, l as StripAndRows, F as TABLE_ACTION_CHORDS, G as TABLE_ACTION_KEYS, T as TableActionHost, m as TableActionSubject, n as TableActionsInput, o as dropdownActionIds, p as dropdownActions, q as groupObjectActions, H as objectActionForKey, s as rightsReason, t as tableActions, u as tableLink, v as toDropdownEntries, w as toStripAndRows, z as withExtensions } from './objectActions-
|
|
2
|
+
export { A as ACTION_CHECKING_ACCESS_REASON, b as ACTION_NOT_HERE_REASON, c as ACTION_NO_ACCESS_REASON, d as ACTION_PLATFORM_TABLE_REASON, B as BuiltOnDestination, E as ChordEvent, D as DEFAULT_ACTION_ORIGIN, e as OBJECT_ACTION_GROUPS, O as ObjectAction, f as ObjectActionDropdownEntry, g as ObjectActionGroup, h as ObjectActionGroupDef, a as ObjectActionIcon, i as ObjectActionSection, S as STANDARD_VERBS, j as STRIP_FEATURE_ROWS, k as StandardVerbId, l as StripAndRows, F as TABLE_ACTION_CHORDS, G as TABLE_ACTION_KEYS, T as TableActionHost, m as TableActionSubject, n as TableActionsInput, o as dropdownActionIds, p as dropdownActions, q as groupObjectActions, H as objectActionForKey, s as rightsReason, t as tableActions, u as tableLink, v as toDropdownEntries, w as toStripAndRows, z as withExtensions } from './objectActions-ac0_y8UY.cjs';
|
package/dist/object-actions.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import '@ai-matrx/records';
|
|
2
|
-
export { A as ACTION_CHECKING_ACCESS_REASON, b as ACTION_NOT_HERE_REASON, c as ACTION_NO_ACCESS_REASON, d as ACTION_PLATFORM_TABLE_REASON, B as BuiltOnDestination, E as ChordEvent, D as DEFAULT_ACTION_ORIGIN, e as OBJECT_ACTION_GROUPS, O as ObjectAction, f as ObjectActionDropdownEntry, g as ObjectActionGroup, h as ObjectActionGroupDef, a as ObjectActionIcon, i as ObjectActionSection, S as STANDARD_VERBS, j as STRIP_FEATURE_ROWS, k as StandardVerbId, l as StripAndRows, F as TABLE_ACTION_CHORDS, G as TABLE_ACTION_KEYS, T as TableActionHost, m as TableActionSubject, n as TableActionsInput, o as dropdownActionIds, p as dropdownActions, q as groupObjectActions, H as objectActionForKey, s as rightsReason, t as tableActions, u as tableLink, v as toDropdownEntries, w as toStripAndRows, z as withExtensions } from './objectActions-
|
|
2
|
+
export { A as ACTION_CHECKING_ACCESS_REASON, b as ACTION_NOT_HERE_REASON, c as ACTION_NO_ACCESS_REASON, d as ACTION_PLATFORM_TABLE_REASON, B as BuiltOnDestination, E as ChordEvent, D as DEFAULT_ACTION_ORIGIN, e as OBJECT_ACTION_GROUPS, O as ObjectAction, f as ObjectActionDropdownEntry, g as ObjectActionGroup, h as ObjectActionGroupDef, a as ObjectActionIcon, i as ObjectActionSection, S as STANDARD_VERBS, j as STRIP_FEATURE_ROWS, k as StandardVerbId, l as StripAndRows, F as TABLE_ACTION_CHORDS, G as TABLE_ACTION_KEYS, T as TableActionHost, m as TableActionSubject, n as TableActionsInput, o as dropdownActionIds, p as dropdownActions, q as groupObjectActions, H as objectActionForKey, s as rightsReason, t as tableActions, u as tableLink, v as toDropdownEntries, w as toStripAndRows, z as withExtensions } from './objectActions-ac0_y8UY.js';
|