@ai-matrx/design-system 0.21.13 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/dist/content-transfer.cjs +1 -0
- package/dist/content-transfer.cjs.map +1 -1
- package/dist/content-transfer.js +1 -0
- package/dist/content-transfer.js.map +1 -1
- package/dist/data-table/copy-helpers.cjs.map +1 -1
- package/dist/data-table/copy-helpers.d.cts +2 -1
- package/dist/data-table/copy-helpers.d.ts +2 -1
- package/dist/data-table/copy-helpers.js.map +1 -1
- package/dist/data-table/data-row-inspector.cjs +1 -0
- package/dist/data-table/data-row-inspector.cjs.map +1 -1
- package/dist/data-table/data-row-inspector.js +1 -0
- package/dist/data-table/data-row-inspector.js.map +1 -1
- package/dist/data-table/data-row-window.cjs +1 -0
- package/dist/data-table/data-row-window.cjs.map +1 -1
- package/dist/data-table/data-row-window.js +1 -0
- package/dist/data-table/data-row-window.js.map +1 -1
- package/dist/data-table/filter-engine.cjs +6 -2
- package/dist/data-table/filter-engine.cjs.map +1 -1
- package/dist/data-table/filter-engine.d.cts +2 -1
- package/dist/data-table/filter-engine.d.ts +2 -1
- package/dist/data-table/filter-engine.js +6 -2
- package/dist/data-table/filter-engine.js.map +1 -1
- package/dist/data-table/host.d.cts +2 -1
- package/dist/data-table/host.d.ts +2 -1
- package/dist/data-table/index.cjs +1547 -425
- package/dist/data-table/index.cjs.map +1 -1
- package/dist/data-table/index.d.cts +91 -3
- package/dist/data-table/index.d.ts +91 -3
- package/dist/data-table/index.js +1549 -423
- package/dist/data-table/index.js.map +1 -1
- package/dist/data-table/infer-filter.cjs.map +1 -1
- package/dist/data-table/infer-filter.d.cts +2 -1
- package/dist/data-table/infer-filter.d.ts +2 -1
- package/dist/data-table/infer-filter.js.map +1 -1
- package/dist/data-table/layered-filters.d.cts +2 -1
- package/dist/data-table/layered-filters.d.ts +2 -1
- package/dist/data-table/query-control.cjs.map +1 -1
- package/dist/data-table/query-control.d.cts +2 -1
- package/dist/data-table/query-control.d.ts +2 -1
- package/dist/data-table/query-control.js.map +1 -1
- package/dist/data-table/types.cjs.map +1 -1
- package/dist/data-table/types.d.cts +2 -1
- package/dist/data-table/types.d.ts +2 -1
- package/dist/data-table/url-state.d.cts +2 -1
- package/dist/data-table/url-state.d.ts +2 -1
- package/dist/data-table/xlsx.cjs.map +1 -1
- package/dist/data-table/xlsx.d.cts +2 -1
- package/dist/data-table/xlsx.d.ts +2 -1
- package/dist/data-table/xlsx.js.map +1 -1
- package/dist/field-formats/index.cjs +741 -0
- package/dist/field-formats/index.cjs.map +1 -0
- package/dist/field-formats/index.d.cts +45 -0
- package/dist/field-formats/index.d.ts +45 -0
- package/dist/field-formats/index.js +723 -0
- package/dist/field-formats/index.js.map +1 -0
- package/dist/types-BdbvlF9-.d.cts +204 -0
- package/dist/types-BdbvlF9-.d.ts +204 -0
- package/dist/{types-D9b_LfTB.d.ts → types-D-flTpK0.d.ts} +241 -1
- package/dist/{types-DaKPLM6T.d.cts → types-Sn249eoy.d.cts} +241 -1
- package/package.json +11 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/field-formats/registry.ts","../../src/field-formats/format.ts"],"sourcesContent":["/**\n * The format registry. ONE table — add a row here and every consumer (data\n * tables, scope context items, anything that adopts `FieldFormatId`) gets it.\n *\n * Every `format()` returns `null` rather than throwing or returning \"\" when the\n * value does not fit — that is what triggers THE FALLBACK LAW in `format.ts`.\n */\n// THE package duration formatter (`@ai-matrx/kit/format`, census H1\n// 2026-09-07). THE UNIT LAW: the unit is in the name, because the fleet's\n// ~35 twins variously took ms, seconds and minutes behind one signature.\nimport {\n formatDurationSeconds,\n formatFileSize,\n formatRelativeTime,\n} from \"@ai-matrx/kit/format\";\n\nimport type {\n FieldChoice,\n FieldFormatDef,\n FieldFormatId,\n FieldFormatOptions,\n FieldBaseType,\n} from \"./types\";\n\n/**\n * A stored `interval` is the one place a NEGATIVE duration is a real quantity\n * rather than a clock bug, so this field format opts into the package's\n * `signed` mode. Everything else about the reading is the package's clock.\n */\nfunction formatSignedDuration(totalSeconds: number): string {\n return formatDurationSeconds(totalSeconds, { signed: true });\n}\n\n// ─── shared helpers ──────────────────────────────────────────────────────────\n\n/** Coerce to a finite number, or null. Accepts numeric strings (\"1,234\", \"$5\"). */\nfunction toNumber(value: unknown): number | null {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n if (typeof value === \"string\") {\n const cleaned = value.replace(/[,\\s$€£¥%]/g, \"\");\n if (cleaned === \"\") return null;\n const n = Number(cleaned);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\nfunction toText(value: unknown): string | null {\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"boolean\")\n return String(value);\n return null;\n}\n\nfunction toDate(value: unknown): Date | null {\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;\n if (typeof value === \"number\") {\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n }\n if (typeof value === \"string\" && value.trim()) {\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n }\n return null;\n}\n\nfunction toList(value: unknown): unknown[] | null {\n if (Array.isArray(value)) return value;\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (trimmed.startsWith(\"[\")) {\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (Array.isArray(parsed)) return parsed;\n } catch {\n /* fall through to comma split */\n }\n }\n if (trimmed === \"\") return [];\n return trimmed.split(\",\").map((s) => s.trim());\n }\n return null;\n}\n\nfunction groupedNumber(n: number, options: FieldFormatOptions): string {\n return new Intl.NumberFormat(undefined, {\n useGrouping: options.useGrouping !== false,\n ...(options.precision != null\n ? {\n minimumFractionDigits: options.precision,\n maximumFractionDigits: options.precision,\n }\n : { maximumFractionDigits: 10 }),\n }).format(n);\n}\n\nfunction affix(text: string, options: FieldFormatOptions): string {\n return `${options.prefix ?? \"\"}${text}${options.suffix ?? \"\"}`;\n}\n\nconst DURATION_TO_SECONDS: Record<\n NonNullable<FieldFormatOptions[\"durationUnit\"]>,\n number\n> = { milliseconds: 0.001, seconds: 1, minutes: 60, hours: 3600 };\n\n/**\n * Find the option a stored value corresponds to.\n *\n * Case-insensitive on purpose: a column adopting a choice format almost always\n * has \"Active\" and \"active\" in it already, and calling one of them a mismatch\n * would be pedantry, not help. Exact match always wins.\n */\nfunction findChoice(\n choices: FieldChoice[] | undefined,\n value: string,\n): FieldChoice | undefined {\n if (!choices || choices.length === 0) return undefined;\n const exact = choices.find((c) => c.value === value);\n if (exact) return exact;\n const lowered = value.toLowerCase();\n return choices.find((c) => c.value.toLowerCase() === lowered);\n}\n\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n/** Explicit scheme, OR a host with a real dot-TLD. No whitespace either way. */\nconst URL_RE =\n /^(?:[a-z][a-z0-9+.-]*:\\/\\/\\S+|(?:[a-z0-9-]+\\.)+[a-z]{2,}(?:[:/?#]\\S*)?)$/i;\nconst HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;\n\n// ─── the registry ────────────────────────────────────────────────────────────\n\nconst DEFS: FieldFormatDef[] = [\n // ── Text ──────────────────────────────────────────────────────────────────\n {\n id: \"text\",\n label: \"Text\",\n description: \"Plain single-line text\",\n group: \"Text\",\n base: \"string\",\n editor: \"text\",\n // Deliberately no optionKeys — plain text is the default for every string\n // column, and giving it prefix/suffix controls puts an options row under\n // every column in the settings dialog for a setting almost nobody wants.\n format: (v, o) => {\n const t = toText(v);\n return t === null ? null : affix(t, o);\n },\n parse: (raw) => (raw === \"\" || raw == null ? null : String(raw)),\n },\n {\n id: \"long_text\",\n label: \"Long text\",\n description: \"Multi-line text, edited in a larger box\",\n group: \"Text\",\n base: \"string\",\n editor: \"textarea\",\n format: (v) => toText(v),\n parse: (raw) => (raw === \"\" || raw == null ? null : String(raw)),\n },\n {\n id: \"markdown\",\n label: \"Markdown\",\n description: \"Rich text written as Markdown\",\n group: \"Text\",\n base: \"string\",\n editor: \"textarea\",\n rich: true,\n format: (v) => toText(v),\n parse: (raw) => (raw === \"\" || raw == null ? null : String(raw)),\n },\n {\n id: \"email\",\n label: \"Email\",\n description: \"Email address — click to compose\",\n group: \"Text\",\n base: \"string\",\n editor: \"email\",\n rich: true,\n format: (v) => {\n const t = toText(v)?.trim();\n if (!t) return null;\n return EMAIL_RE.test(t) ? t : null;\n },\n parse: (raw) => (raw === \"\" || raw == null ? null : String(raw).trim()),\n },\n {\n id: \"url\",\n label: \"Link\",\n description: \"Web address — click to open\",\n group: \"Text\",\n base: \"string\",\n editor: \"url\",\n rich: true,\n format: (v) => {\n const t = toText(v)?.trim();\n if (!t) return null;\n // `new URL()` alone is uselessly permissive here: `https://Beijing` and\n // even `https://Washington, D.C.` parse successfully, so every text value\n // would render as a broken link instead of flagging as a mismatch.\n // Require no whitespace, and either an explicit scheme or a real\n // dot-TLD host. Bare domains stay accepted; https is added on click.\n if (/\\s/.test(t)) return null;\n if (!URL_RE.test(t)) return null;\n try {\n new URL(t.includes(\"://\") ? t : `https://${t}`);\n return t;\n } catch {\n return null;\n }\n },\n parse: (raw) => (raw === \"\" || raw == null ? null : String(raw).trim()),\n },\n {\n id: \"phone\",\n label: \"Phone\",\n description: \"Phone number — click to call\",\n group: \"Text\",\n base: \"string\",\n editor: \"tel\",\n rich: true,\n format: (v) => {\n const t = toText(v)?.trim();\n if (!t) return null;\n // Must contain at least 7 digits to be a plausible phone number.\n return (t.match(/\\d/g) ?? []).length >= 7 ? t : null;\n },\n parse: (raw) => (raw === \"\" || raw == null ? null : String(raw).trim()),\n },\n {\n id: \"color\",\n label: \"Color\",\n description: \"Hex color with a swatch\",\n group: \"Text\",\n base: \"string\",\n editor: \"color\",\n rich: true,\n format: (v) => {\n const t = toText(v)?.trim();\n if (!t) return null;\n return HEX_RE.test(t) ? t.toLowerCase() : null;\n },\n parse: (raw) => (raw === \"\" || raw == null ? null : String(raw).trim()),\n },\n\n // ── Numbers ───────────────────────────────────────────────────────────────\n {\n id: \"number\",\n label: \"Number\",\n description: \"Plain number\",\n group: \"Numbers\",\n base: \"number\",\n alsoAccepts: [\"integer\"],\n editor: \"number\",\n numericAlign: true,\n optionKeys: [\"useGrouping\", \"precision\", \"prefix\", \"suffix\"],\n format: (v, o) => {\n const n = toNumber(v);\n return n === null ? null : affix(groupedNumber(n, o), o);\n },\n parse: (raw) => toNumber(raw),\n },\n {\n id: \"decimal\",\n label: \"Decimal\",\n description: \"Number with fixed decimal places\",\n group: \"Numbers\",\n base: \"number\",\n alsoAccepts: [\"integer\"],\n editor: \"number\",\n numericAlign: true,\n optionKeys: [\"precision\", \"useGrouping\", \"prefix\", \"suffix\"],\n format: (v, o) => {\n const n = toNumber(v);\n if (n === null) return null;\n return affix(groupedNumber(n, { ...o, precision: o.precision ?? 2 }), o);\n },\n parse: (raw) => toNumber(raw),\n },\n {\n id: \"currency\",\n label: \"Currency\",\n description: \"Money — stored as a plain number, shown with a symbol\",\n group: \"Numbers\",\n base: \"number\",\n alsoAccepts: [\"integer\"],\n editor: \"number\",\n numericAlign: true,\n optionKeys: [\"currency\", \"precision\"],\n format: (v, o) => {\n const n = toNumber(v);\n if (n === null) return null;\n const currency = (o.currency || \"USD\").toUpperCase();\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency,\n ...(o.precision != null\n ? {\n minimumFractionDigits: o.precision,\n maximumFractionDigits: o.precision,\n }\n : {}),\n }).format(n);\n } catch {\n // Unknown/invalid ISO code — still show the money, never blank it.\n return `${groupedNumber(n, { precision: o.precision ?? 2 })} ${currency}`;\n }\n },\n parse: (raw) => toNumber(raw),\n },\n {\n id: \"percent\",\n label: \"Percent\",\n description: \"Percentage — stored as a plain number\",\n group: \"Numbers\",\n base: \"number\",\n alsoAccepts: [\"integer\"],\n editor: \"number\",\n numericAlign: true,\n optionKeys: [\"percentScale\", \"precision\"],\n format: (v, o) => {\n const n = toNumber(v);\n if (n === null) return null;\n const shown = o.percentScale === \"fraction\" ? n * 100 : n;\n return `${groupedNumber(shown, o.precision === undefined ? {} : { precision: o.precision })}%`;\n },\n parse: (raw) => toNumber(raw),\n },\n {\n id: \"duration\",\n label: \"Duration\",\n description: \"Length of time, shown as h:mm:ss\",\n group: \"Numbers\",\n base: \"number\",\n alsoAccepts: [\"integer\"],\n editor: \"number\",\n numericAlign: true,\n optionKeys: [\"durationUnit\"],\n format: (v, o) => {\n const n = toNumber(v);\n if (n === null) return null;\n return formatSignedDuration(n * DURATION_TO_SECONDS[o.durationUnit ?? \"seconds\"]);\n },\n parse: (raw) => toNumber(raw),\n },\n {\n id: \"integer\",\n label: \"Whole number\",\n description: \"Integer with no decimal part\",\n group: \"Numbers\",\n base: \"integer\",\n alsoAccepts: [\"number\"],\n editor: \"number\",\n numericAlign: true,\n optionKeys: [\"useGrouping\", \"prefix\", \"suffix\"],\n format: (v, o) => {\n const n = toNumber(v);\n if (n === null) return null;\n return affix(groupedNumber(Math.trunc(n), { ...o, precision: 0 }), o);\n },\n parse: (raw) => {\n const n = toNumber(raw);\n return n === null ? null : Math.trunc(n);\n },\n },\n {\n id: \"rating\",\n label: \"Rating\",\n description: \"Score out of a maximum, shown as stars\",\n group: \"Numbers\",\n base: \"integer\",\n alsoAccepts: [\"number\"],\n editor: \"rating\",\n rich: true,\n optionKeys: [\"ratingMax\"],\n format: (v, o) => {\n const n = toNumber(v);\n if (n === null) return null;\n const max = o.ratingMax ?? 5;\n if (n < 0 || n > max) return null;\n return `${n} / ${max}`;\n },\n parse: (raw) => {\n const n = toNumber(raw);\n return n === null ? null : Math.round(n);\n },\n },\n {\n id: \"file_size\",\n label: \"File size\",\n description: \"Byte count shown as KB / MB / GB\",\n group: \"Numbers\",\n base: \"integer\",\n alsoAccepts: [\"number\"],\n editor: \"number\",\n numericAlign: true,\n format: (v) => {\n const n = toNumber(v);\n return n === null ? null : formatFileSize(n);\n },\n parse: (raw) => {\n const n = toNumber(raw);\n return n === null ? null : Math.trunc(n);\n },\n },\n\n // ── Choice ────────────────────────────────────────────────────────────────\n {\n id: \"boolean\",\n label: \"Yes / No\",\n description: \"True or false\",\n group: \"Choice\",\n base: \"boolean\",\n editor: \"checkbox\",\n format: (v) => {\n if (typeof v === \"boolean\") return v ? \"Yes\" : \"No\";\n if (v === \"true\" || v === 1) return \"Yes\";\n if (v === \"false\" || v === 0) return \"No\";\n return null;\n },\n parse: (raw) => {\n if (typeof raw === \"boolean\") return raw;\n if (raw == null || raw === \"\") return null;\n return String(raw).toLowerCase() === \"true\" || raw === 1;\n },\n },\n\n // ── Dates ─────────────────────────────────────────────────────────────────\n {\n id: \"date\",\n label: \"Date\",\n description: \"Calendar date\",\n group: \"Dates\",\n base: \"date\",\n alsoAccepts: [\"datetime\", \"string\"],\n editor: \"date\",\n optionKeys: [\"dateStyle\"],\n format: (v, o) => {\n const d = toDate(v);\n if (!d) return null;\n return d.toLocaleDateString(undefined, {\n dateStyle: o.dateStyle ?? \"medium\",\n });\n },\n parse: (raw) => (raw === \"\" || raw == null ? null : String(raw)),\n },\n {\n id: \"datetime\",\n label: \"Date & time\",\n description: \"Calendar date with a time of day\",\n group: \"Dates\",\n base: \"datetime\",\n alsoAccepts: [\"date\", \"string\"],\n editor: \"datetime\",\n optionKeys: [\"dateStyle\"],\n format: (v, o) => {\n const d = toDate(v);\n if (!d) return null;\n return d.toLocaleString(undefined, {\n dateStyle: o.dateStyle ?? \"medium\",\n timeStyle: \"short\",\n });\n },\n parse: (raw) => (raw === \"\" || raw == null ? null : String(raw)),\n },\n {\n id: \"relative_time\",\n label: \"Relative time\",\n description: 'Shown as \"3 days ago\"',\n group: \"Dates\",\n base: \"datetime\",\n alsoAccepts: [\"date\", \"string\"],\n editor: \"datetime\",\n format: (v) => {\n const d = toDate(v);\n // THE one relative stamp (@ai-matrx/kit/format). This registry carried\n // its own Intl ladder until 2026-09-12 — the last re-implementation of\n // formatRelativeTime in this repo. It ROUNDED, so a 1h50m-old row read\n // \"2 hours ago\" here and \"1h ago\" on every other surface in the product;\n // the package truncates, which is what the rest of the fleet shows.\n return d ? formatRelativeTime(d, { style: \"intl\" }) : null;\n },\n parse: (raw) => (raw === \"\" || raw == null ? null : String(raw)),\n },\n\n // ── Structured ────────────────────────────────────────────────────────────\n {\n id: \"json\",\n label: \"JSON\",\n description: \"Structured object\",\n group: \"Structured\",\n base: \"json\",\n alsoAccepts: [\"array\"],\n editor: \"json\",\n format: (v) => {\n if (v === null || v === undefined) return null;\n if (typeof v === \"object\") return JSON.stringify(v);\n return toText(v);\n },\n parse: (raw) => {\n if (raw == null || raw === \"\") return null;\n if (typeof raw !== \"string\") return raw;\n try {\n return JSON.parse(raw) as unknown;\n } catch {\n return raw;\n }\n },\n },\n {\n id: \"array\",\n label: \"List\",\n description: \"List of values\",\n group: \"Structured\",\n base: \"array\",\n alsoAccepts: [\"json\"],\n editor: \"json\",\n format: (v) => {\n const list = toList(v);\n return list === null ? null : list.map((i) => String(i)).join(\", \");\n },\n parse: (raw) => {\n if (raw == null || raw === \"\") return null;\n if (Array.isArray(raw)) return raw;\n return toList(raw) ?? raw;\n },\n },\n {\n id: \"tags\",\n label: \"Tags\",\n description: \"List of values shown as chips\",\n group: \"Structured\",\n base: \"array\",\n alsoAccepts: [\"json\", \"string\"],\n editor: \"json\",\n rich: true,\n format: (v) => {\n const list = toList(v);\n return list === null ? null : list.map((i) => String(i)).join(\", \");\n },\n parse: (raw) => {\n if (raw == null || raw === \"\") return null;\n if (Array.isArray(raw)) return raw;\n return toList(raw) ?? raw;\n },\n },\n\n // ─── Choice ────────────────────────────────────────────────────────────────\n //\n // An enum WITHOUT a database enum. The column stays `string` (or `array` for\n // multi), the database enforces nothing, and stripping the format leaves\n // every stored value exactly as it was. Options are a UI layer — the same\n // trick `percent` plays on a number.\n //\n // The fallback law is the whole reason this is safe to switch on over live\n // data: a value not in the option list is NOT an error and is NEVER blanked.\n // `format()` returns null for it, `<FormattedFieldValue>` renders it in amber\n // with the reason, and the user can fix it or adopt it. Declaring the options\n // is therefore how you FIND your stray values, not how you lose them.\n //\n // Options resolve through `choices.ts` — inline `options.choices`, or a\n // structured list via `options.structuredList`. This def only ever sees\n // whatever the caller resolved into `options.choices`, so it stays pure and\n // synchronous like every other format.\n {\n id: \"choice\",\n label: \"Choice\",\n description: \"One value from a list of options\",\n group: \"Choice\",\n base: \"string\",\n alsoAccepts: [\"number\", \"integer\", \"boolean\"],\n editor: \"select\",\n rich: true,\n optionKeys: [\"choices\", \"structuredList\", \"allowOther\"],\n format: (v, options) => {\n const text = toText(v);\n if (text === null) return null;\n const trimmed = text.trim();\n if (trimmed === \"\") return \"\";\n const match = findChoice(options.choices, trimmed);\n // No options resolved yet (a structured list still loading, or a column\n // whose list is unreachable) — show the raw value rather than accusing\n // every cell of being off-list.\n if (!options.choices || options.choices.length === 0) return trimmed;\n return match ? (match.label ?? match.value) : null;\n },\n parse: (raw) => {\n if (raw == null) return null;\n const text = toText(raw);\n if (text === null) return raw;\n const trimmed = text.trim();\n return trimmed === \"\" ? null : trimmed;\n },\n },\n {\n id: \"multi_choice\",\n label: \"Multi-choice\",\n description: \"Any number of values from a list of options\",\n group: \"Choice\",\n base: \"array\",\n alsoAccepts: [\"string\", \"json\"],\n editor: \"multiselect\",\n rich: true,\n optionKeys: [\"choices\", \"structuredList\", \"allowOther\"],\n format: (v, options) => {\n const list = toList(v);\n if (list === null) return null;\n if (list.length === 0) return \"\";\n const labels: string[] = [];\n let anyOffList = false;\n for (const entry of list) {\n const text = toText(entry);\n if (text === null) {\n anyOffList = true;\n continue;\n }\n const match = findChoice(options.choices, text.trim());\n if (match) labels.push(match.label ?? match.value);\n else {\n labels.push(text.trim());\n anyOffList = true;\n }\n }\n // Same leniency as `choice`: with no options resolved, nothing is off-list.\n const haveOptions = !!options.choices && options.choices.length > 0;\n if (haveOptions && anyOffList) return null;\n return labels.join(\", \");\n },\n parse: (raw) => {\n if (raw == null || raw === \"\") return null;\n if (Array.isArray(raw)) return raw;\n return toList(raw) ?? raw;\n },\n },\n\n // ─── Computed ──────────────────────────────────────────────────────────────\n //\n // A formula column stores NOTHING. `metadata.format.options.formula` holds\n // the expression; the stored cell is always null; the value is computed on\n // read from the other columns of the same row\n // (`features/data-tables/formulas.ts`). Grouped under Structured on purpose\n // — the picker's groups are a fixed vocabulary, and a formula is structure\n // over a plain string column, not a sixth family.\n //\n // `format()` here is handed the COMPUTED value, never a stored one, and its\n // only job is to render it: through `resultFormat`'s own `format()` when the\n // column declares one (a computed total shown as Currency, a computed date\n // shown as a Date), and plainly otherwise. It never returns null for an\n // empty value, because a formula legitimately produces BLANK.\n //\n // No `optionKeys`: the expression is authored in its own editor, not in the\n // generic option rail, and listing a key the picker has no control for would\n // open an empty options section under every formula column.\n {\n id: \"formula\",\n label: \"Formula\",\n description: \"Computed from the other columns in the row — never typed in\",\n group: \"Structured\",\n base: \"string\",\n editor: \"computed\",\n format: (v, o) => {\n const target = o.formula?.resultFormat;\n if (target && target !== \"formula\") {\n const delegate = FIELD_FORMATS[target];\n if (delegate) {\n const rendered = delegate.format(v, o);\n if (rendered !== null) return rendered;\n }\n }\n if (v === null || v === undefined) return \"\";\n if (typeof v === \"boolean\") return v ? \"Yes\" : \"No\";\n if (typeof v === \"number\") return groupedNumber(v, o);\n return toText(v);\n },\n // Nothing is ever stored in a formula column. Returning the raw input here\n // would be the one way a computed cell could acquire a value of its own.\n parse: () => null,\n },\n];\n\nexport const FIELD_FORMATS: Readonly<Record<FieldFormatId, FieldFormatDef>> =\n Object.freeze(\n Object.fromEntries(DEFS.map((d) => [d.id, d])) as Record<\n FieldFormatId,\n FieldFormatDef\n >,\n );\n\nexport const FIELD_FORMAT_LIST: readonly FieldFormatDef[] = Object.freeze(DEFS);\n\nexport const FIELD_FORMAT_IDS = DEFS.map((d) => d.id) as FieldFormatId[];\n\nexport function getFieldFormat(\n id: string | null | undefined,\n): FieldFormatDef | null {\n if (!id) return null;\n return FIELD_FORMATS[id as FieldFormatId] ?? null;\n}\n\n/**\n * The format a column gets when it declares none — the identity format for its\n * storage type. Every existing column therefore behaves exactly as before.\n */\nexport function defaultFormatForBase(base: string): FieldFormatId {\n switch (base as FieldBaseType) {\n case \"number\":\n return \"number\";\n case \"integer\":\n return \"integer\";\n case \"boolean\":\n return \"boolean\";\n case \"date\":\n return \"date\";\n case \"datetime\":\n return \"datetime\";\n case \"json\":\n return \"json\";\n case \"array\":\n return \"array\";\n default:\n return \"text\";\n }\n}\n\n/** Formats that can legally sit on a given storage type. */\nexport function formatsForBase(base: string): FieldFormatDef[] {\n return DEFS.filter(\n (d) => d.base === base || (d.alsoAccepts ?? []).includes(base as FieldBaseType),\n );\n}\n\n/** Group the pickable formats for a base type, preserving registry order. */\nexport function groupedFormatsForBase(\n base: string,\n): { group: FieldFormatDef[\"group\"]; formats: FieldFormatDef[] }[] {\n const out: { group: FieldFormatDef[\"group\"]; formats: FieldFormatDef[] }[] =\n [];\n for (const def of formatsForBase(base)) {\n const bucket = out.find((g) => g.group === def.group);\n if (bucket) bucket.formats.push(def);\n else out.push({ group: def.group, formats: [def] });\n }\n return out;\n}\n","/**\n * THE FALLBACK LAW, implemented.\n *\n * `formatFieldValue` never blanks a cell and never throws. When the declared\n * format cannot interpret the stored value it returns `ok: false` with the\n * **base-type** rendering of that same value plus a human `reason`. The UI\n * renders that text in amber (see `<FormattedFieldValue>`), so a mismatch is\n * visible instead of invisible.\n */\nimport {\n defaultFormatForBase,\n getFieldFormat,\n FIELD_FORMATS,\n} from \"./registry\";\nimport type {\n FieldFormatConfig,\n FieldFormatOptions,\n FormatResult,\n} from \"./types\";\n\nfunction isEmpty(value: unknown): boolean {\n return value === null || value === undefined || value === \"\";\n}\n\n/** Last-resort rendering — the raw value, readable, never empty-by-accident. */\nfunction baseText(value: unknown): string {\n if (isEmpty(value)) return \"\";\n if (typeof value === \"object\") {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n }\n return String(value);\n}\n\n/**\n * Read a field's declared format off its `metadata` JSONB. Returns null when\n * the field declares none (or declares one we don't know) — the caller then\n * falls back to the storage type's identity format, so an unknown or removed\n * format id degrades to plain, never to broken.\n */\nexport function readFieldFormatConfig(\n metadata: unknown,\n): FieldFormatConfig | null {\n if (!metadata || typeof metadata !== \"object\") return null;\n const raw = (metadata as Record<string, unknown>).format;\n if (!raw || typeof raw !== \"object\") return null;\n const id = (raw as Record<string, unknown>).id;\n if (typeof id !== \"string\" || !getFieldFormat(id)) return null;\n const options = (raw as Record<string, unknown>).options;\n // `exactOptionalPropertyTypes`: an absent `options` is the key being ABSENT,\n // never the key holding `undefined` — the two are different documents once\n // this config is written back out as JSON.\n return {\n id: id as FieldFormatConfig[\"id\"],\n ...(options && typeof options === \"object\"\n ? { options: options as FieldFormatOptions }\n : {}),\n };\n}\n\n/**\n * Resolve the format a field actually renders with: its declared one, or the\n * identity format for its storage type.\n */\nexport function resolveFieldFormat(\n dataType: string,\n metadata: unknown,\n): FieldFormatConfig {\n const declared = readFieldFormatConfig(metadata);\n if (declared) return declared;\n return { id: defaultFormatForBase(dataType) };\n}\n\nexport function formatFieldValue(\n value: unknown,\n config: FieldFormatConfig | null | undefined,\n /** The column's storage type — used for the fallback rendering. */\n dataType?: string,\n): FormatResult {\n if (isEmpty(value)) return { ok: true, text: \"\", empty: true };\n\n const def = config ? getFieldFormat(config.id) : null;\n const effective =\n def ?? FIELD_FORMATS[defaultFormatForBase(dataType ?? \"string\")];\n\n let formatted: string | null;\n try {\n formatted = effective.format(value, config?.options ?? {});\n } catch {\n formatted = null;\n }\n\n if (formatted !== null) return { ok: true, text: formatted, empty: false };\n\n // Mismatch. Show what IS stored, flagged.\n return {\n ok: false,\n text: baseText(value),\n empty: false,\n reason: `This value is not a valid ${effective.label.toLowerCase()} — showing the stored value instead.`,\n };\n}\n\n/** Turn raw editor input into the value to store under a format. */\nexport function parseFieldInput(\n raw: unknown,\n config: FieldFormatConfig | null | undefined,\n dataType?: string,\n): unknown {\n const def = config ? getFieldFormat(config.id) : null;\n const effective =\n def ?? FIELD_FORMATS[defaultFormatForBase(dataType ?? \"string\")];\n try {\n return effective.parse(raw, config?.options ?? {});\n } catch {\n return raw;\n }\n}\n\n/**\n * Would this stored value survive its declared format? Used to count mismatches\n * so a column can warn (\"4 values don't fit Currency\") instead of silently\n * showing amber cells.\n */\nexport function valueFitsFormat(\n value: unknown,\n config: FieldFormatConfig | null | undefined,\n dataType?: string,\n): boolean {\n return formatFieldValue(value, config, dataType).ok;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/field-formats — THE platform field-format registry.
|
|
3
|
+
*
|
|
4
|
+
* A **format** is a UI-layer semantic type layered on top of a plain storage
|
|
5
|
+
* type. `currency` is not a database type: it is a `number` that the UI knows
|
|
6
|
+
* to display as `$1,234.56`, edit with a currency-aware input, and right-align.
|
|
7
|
+
* Same for `percent`, `email`, `url`, `rating`, `duration`, `tags`, …
|
|
8
|
+
*
|
|
9
|
+
* THE FALLBACK LAW — a format may never blank a cell or throw. If the stored
|
|
10
|
+
* value cannot be interpreted under the declared format, `formatFieldValue`
|
|
11
|
+
* returns `ok: false` together with the **base-type** rendering of the value.
|
|
12
|
+
* The UI then shows that base rendering in amber so the user can SEE that
|
|
13
|
+
* something does not fit, instead of staring at an empty cell. Formats are
|
|
14
|
+
* always additive and always non-breaking: strip the format and you are back
|
|
15
|
+
* to exactly what the database holds.
|
|
16
|
+
*
|
|
17
|
+
* Consumers:
|
|
18
|
+
* - user data tables (`udt_dataset_fields.metadata.format`)
|
|
19
|
+
* - scope context items (`ContextValueType` → `contextValueTypeToFormat`)
|
|
20
|
+
* Add a consumer by mapping its own type vocabulary onto `FieldFormatId`;
|
|
21
|
+
* never fork a second formatter table.
|
|
22
|
+
*/
|
|
23
|
+
/** The storage types a format can sit on. Mirrors the `field_data_type` enum. */
|
|
24
|
+
type FieldBaseType = "string" | "number" | "integer" | "boolean" | "date" | "datetime" | "json" | "array";
|
|
25
|
+
type FieldFormatId = "text" | "long_text" | "markdown" | "email" | "url" | "phone" | "color" | "number" | "decimal" | "currency" | "percent" | "duration" | "integer" | "rating" | "file_size" | "boolean" | "choice" | "multi_choice" | "date" | "datetime" | "relative_time" | "json" | "array" | "tags" | "formula";
|
|
26
|
+
/**
|
|
27
|
+
* A binding from a choice format to a user structured list (a "pick list").
|
|
28
|
+
*
|
|
29
|
+
* THE SAME SHAPE the agent-variable system already uses (aidream's
|
|
30
|
+
* `PicklistBinding`, written by the FE as `customComponent.structured_list`) —
|
|
31
|
+
* a column and an agent variable must speak ONE option vocabulary, or the same
|
|
32
|
+
* list means two things depending on which surface reads it. Never fork this.
|
|
33
|
+
*
|
|
34
|
+
* `groupName` is what makes tiering free: a list's items already carry a
|
|
35
|
+
* `group_name`, so binding to one group of a list yields a narrower set with no
|
|
36
|
+
* second list to maintain, and the ungrouped binding yields sections for free.
|
|
37
|
+
*/
|
|
38
|
+
type StructuredListBinding = {
|
|
39
|
+
/** `workbench.udt_structured_lists.id` the options are hydrated from. */
|
|
40
|
+
listId: string;
|
|
41
|
+
/** Optional FIXED group filter within the list. */
|
|
42
|
+
groupName?: string;
|
|
43
|
+
/**
|
|
44
|
+
* DEPENDENT COLUMNS — the group filter comes from another column's cell
|
|
45
|
+
* instead of being fixed. Holds that column's MACHINE field name.
|
|
46
|
+
*
|
|
47
|
+
* Picking "North America" in Continent narrows Country to the list's
|
|
48
|
+
* "North America" group, because the list's items already carry the group.
|
|
49
|
+
* Nothing is declared on the controlling column: the dependency is stated
|
|
50
|
+
* once, here, by the column that is constrained.
|
|
51
|
+
*
|
|
52
|
+
* Chains are free and need no graph: each column reads its controller's
|
|
53
|
+
* CURRENT cell value at render time, so A → B → C works with no traversal,
|
|
54
|
+
* and a cycle cannot loop (a cell value is data, not a computation).
|
|
55
|
+
*
|
|
56
|
+
* Two behaviours this must always keep, both of them the fallback law:
|
|
57
|
+
* - the controlling cell empty means ALL groups are offered as sections,
|
|
58
|
+
* never an empty dropdown;
|
|
59
|
+
* - changing the controller NEVER rewrites or clears the dependent cell.
|
|
60
|
+
* A value that no longer fits renders in amber, exactly like any other
|
|
61
|
+
* off-list value, and the user decides.
|
|
62
|
+
*
|
|
63
|
+
* Ignored when `groupName` is set — a fixed group wins over a derived one.
|
|
64
|
+
*/
|
|
65
|
+
groupFromField?: string;
|
|
66
|
+
/** Multi-select. Mirrors the agent-variable flag; `multi_choice` implies it. */
|
|
67
|
+
multiple?: boolean;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* One option offered by a choice format.
|
|
71
|
+
*
|
|
72
|
+
* `value` is EXACTLY what the cell stores — the human-readable label, not an
|
|
73
|
+
* id. That is deliberate: it keeps raw data readable, lets an existing column
|
|
74
|
+
* adopt a choice format with zero rewriting, and keeps the fallback law
|
|
75
|
+
* meaningful (a value not in the list is still legible in the cell).
|
|
76
|
+
*/
|
|
77
|
+
type FieldChoice = {
|
|
78
|
+
value: string;
|
|
79
|
+
/** Display text when it should differ from the stored value. */
|
|
80
|
+
label?: string;
|
|
81
|
+
/** Section heading — carried over from a structured list item's group_name. */
|
|
82
|
+
group?: string;
|
|
83
|
+
/** Chip tint. A name from the shared choice palette, never a raw hex. */
|
|
84
|
+
color?: string;
|
|
85
|
+
/** Hover help, carried over from a structured list item's help_text. */
|
|
86
|
+
help?: string;
|
|
87
|
+
};
|
|
88
|
+
/** Per-field options. Every key is optional; every format has sane defaults. */
|
|
89
|
+
type FieldFormatOptions = {
|
|
90
|
+
/** `currency` — ISO 4217 code. Default "USD". */
|
|
91
|
+
currency?: string;
|
|
92
|
+
/** `currency` / `decimal` / `percent` — fixed decimal places. */
|
|
93
|
+
precision?: number;
|
|
94
|
+
/**
|
|
95
|
+
* `percent` — how the number is STORED.
|
|
96
|
+
* - "whole" (default): 45 means 45%
|
|
97
|
+
* - "fraction": 0.45 means 45%
|
|
98
|
+
*/
|
|
99
|
+
percentScale?: "whole" | "fraction";
|
|
100
|
+
/** `duration` — unit the number is stored in. Default "seconds". */
|
|
101
|
+
durationUnit?: "seconds" | "minutes" | "hours" | "milliseconds";
|
|
102
|
+
/** `rating` — top of the scale. Default 5. */
|
|
103
|
+
ratingMax?: number;
|
|
104
|
+
/** `number` / `decimal` / `integer` — thousands separators. Default true. */
|
|
105
|
+
useGrouping?: boolean;
|
|
106
|
+
/** `date` / `datetime` — Intl date style. Default "medium". */
|
|
107
|
+
dateStyle?: "short" | "medium" | "long";
|
|
108
|
+
/** Text shown after the value (e.g. "kg", "req/s"). */
|
|
109
|
+
suffix?: string;
|
|
110
|
+
/** Text shown before the value. */
|
|
111
|
+
prefix?: string;
|
|
112
|
+
/**
|
|
113
|
+
* `choice` / `multi_choice` — the options offered inline. Ignored when
|
|
114
|
+
* `structuredList` is set; that binding is the source of truth for options.
|
|
115
|
+
*/
|
|
116
|
+
choices?: FieldChoice[];
|
|
117
|
+
/**
|
|
118
|
+
* `choice` / `multi_choice` — hydrate options from a shared pick list instead
|
|
119
|
+
* of holding a private copy, so "Status" means one thing across every table.
|
|
120
|
+
*/
|
|
121
|
+
structuredList?: StructuredListBinding;
|
|
122
|
+
/**
|
|
123
|
+
* `choice` / `multi_choice` — may a value outside the option list be entered
|
|
124
|
+
* and kept? Default TRUE, because a format may never take away a working
|
|
125
|
+
* input, and because a column always adopts a format over data that predates
|
|
126
|
+
* it. Off-list values are never rejected or blanked — they render in amber.
|
|
127
|
+
*/
|
|
128
|
+
allowOther?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* `formula` — the computed column's definition.
|
|
131
|
+
*
|
|
132
|
+
* `expression` is the formula text exactly as the user typed it, parsed by
|
|
133
|
+
* `features/data-tables/formulas.ts`. `resultFormat` is how the computed
|
|
134
|
+
* value should be DISPLAYED (a computed total shown as Currency, a computed
|
|
135
|
+
* date shown as a Date); when it is absent the value renders plainly.
|
|
136
|
+
*
|
|
137
|
+
* The expression lives here, in `metadata.format.options`, for the same
|
|
138
|
+
* reason every other format option does: it is a UI layer over an untouched
|
|
139
|
+
* storage type. The cell itself is always `null` — strip the format and the
|
|
140
|
+
* column is an ordinary empty text column, with nothing to migrate.
|
|
141
|
+
*/
|
|
142
|
+
formula?: {
|
|
143
|
+
expression: string;
|
|
144
|
+
resultFormat?: FieldFormatId;
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
/** What a field declares. Persisted as `metadata.format` on the field row. */
|
|
148
|
+
type FieldFormatConfig = {
|
|
149
|
+
id: FieldFormatId;
|
|
150
|
+
options?: FieldFormatOptions;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Result of formatting one value.
|
|
154
|
+
*
|
|
155
|
+
* `ok: false` means the value did not fit the declared format — `text` still
|
|
156
|
+
* carries a readable base-type rendering so the cell is never blank, and
|
|
157
|
+
* `reason` explains what went wrong (surfaced as a tooltip).
|
|
158
|
+
*/
|
|
159
|
+
type FormatResult = {
|
|
160
|
+
ok: boolean;
|
|
161
|
+
/** Always populated. Empty string only for genuinely empty values. */
|
|
162
|
+
text: string;
|
|
163
|
+
/** Set when `ok` is false. */
|
|
164
|
+
reason?: string;
|
|
165
|
+
/** True when the raw value is null/undefined/"" — not a mismatch. */
|
|
166
|
+
empty: boolean;
|
|
167
|
+
};
|
|
168
|
+
/** How a value should be edited. Maps to a concrete input in the UI layer. */
|
|
169
|
+
type FieldEditorKind = "text" | "textarea" | "number" | "checkbox" | "date" | "datetime" | "email" | "url" | "tel" | "color" | "rating" | "json" | "select" | "multiselect"
|
|
170
|
+
/**
|
|
171
|
+
* Not editable at all — the value is COMPUTED, so the grid refuses editing
|
|
172
|
+
* rather than offering an input that would throw the computed value away.
|
|
173
|
+
*/
|
|
174
|
+
| "computed";
|
|
175
|
+
type FieldFormatDef = {
|
|
176
|
+
id: FieldFormatId;
|
|
177
|
+
label: string;
|
|
178
|
+
description: string;
|
|
179
|
+
/** Grouping for the picker UI. */
|
|
180
|
+
group: "Text" | "Numbers" | "Dates" | "Choice" | "Structured";
|
|
181
|
+
/** The storage type this format requires. */
|
|
182
|
+
base: FieldBaseType;
|
|
183
|
+
/** Extra base types this format tolerates (read-only leniency). */
|
|
184
|
+
alsoAccepts?: FieldBaseType[];
|
|
185
|
+
editor: FieldEditorKind;
|
|
186
|
+
/** Right-align in a grid (numeric-ish). */
|
|
187
|
+
numericAlign?: boolean;
|
|
188
|
+
/** Rendered richly by `<FormattedFieldValue>` (link, swatch, chips, …). */
|
|
189
|
+
rich?: boolean;
|
|
190
|
+
/** Which options this format actually reads — drives the options UI. */
|
|
191
|
+
optionKeys?: (keyof FieldFormatOptions)[];
|
|
192
|
+
/**
|
|
193
|
+
* Turn a stored value into display text. Return `null` to signal the value
|
|
194
|
+
* does not fit this format (the caller then falls back to base rendering).
|
|
195
|
+
*/
|
|
196
|
+
format: (value: unknown, options: FieldFormatOptions) => string | null;
|
|
197
|
+
/**
|
|
198
|
+
* Turn raw editor input into the value to store. Must return a value valid
|
|
199
|
+
* for `base`, or `null` to store nothing.
|
|
200
|
+
*/
|
|
201
|
+
parse: (raw: unknown, options: FieldFormatOptions) => unknown;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export type { FieldFormatId as F, StructuredListBinding as S, FieldFormatDef as a, FieldFormatConfig as b, FormatResult as c, FieldBaseType as d, FieldChoice as e, FieldEditorKind as f, FieldFormatOptions as g };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/field-formats — THE platform field-format registry.
|
|
3
|
+
*
|
|
4
|
+
* A **format** is a UI-layer semantic type layered on top of a plain storage
|
|
5
|
+
* type. `currency` is not a database type: it is a `number` that the UI knows
|
|
6
|
+
* to display as `$1,234.56`, edit with a currency-aware input, and right-align.
|
|
7
|
+
* Same for `percent`, `email`, `url`, `rating`, `duration`, `tags`, …
|
|
8
|
+
*
|
|
9
|
+
* THE FALLBACK LAW — a format may never blank a cell or throw. If the stored
|
|
10
|
+
* value cannot be interpreted under the declared format, `formatFieldValue`
|
|
11
|
+
* returns `ok: false` together with the **base-type** rendering of the value.
|
|
12
|
+
* The UI then shows that base rendering in amber so the user can SEE that
|
|
13
|
+
* something does not fit, instead of staring at an empty cell. Formats are
|
|
14
|
+
* always additive and always non-breaking: strip the format and you are back
|
|
15
|
+
* to exactly what the database holds.
|
|
16
|
+
*
|
|
17
|
+
* Consumers:
|
|
18
|
+
* - user data tables (`udt_dataset_fields.metadata.format`)
|
|
19
|
+
* - scope context items (`ContextValueType` → `contextValueTypeToFormat`)
|
|
20
|
+
* Add a consumer by mapping its own type vocabulary onto `FieldFormatId`;
|
|
21
|
+
* never fork a second formatter table.
|
|
22
|
+
*/
|
|
23
|
+
/** The storage types a format can sit on. Mirrors the `field_data_type` enum. */
|
|
24
|
+
type FieldBaseType = "string" | "number" | "integer" | "boolean" | "date" | "datetime" | "json" | "array";
|
|
25
|
+
type FieldFormatId = "text" | "long_text" | "markdown" | "email" | "url" | "phone" | "color" | "number" | "decimal" | "currency" | "percent" | "duration" | "integer" | "rating" | "file_size" | "boolean" | "choice" | "multi_choice" | "date" | "datetime" | "relative_time" | "json" | "array" | "tags" | "formula";
|
|
26
|
+
/**
|
|
27
|
+
* A binding from a choice format to a user structured list (a "pick list").
|
|
28
|
+
*
|
|
29
|
+
* THE SAME SHAPE the agent-variable system already uses (aidream's
|
|
30
|
+
* `PicklistBinding`, written by the FE as `customComponent.structured_list`) —
|
|
31
|
+
* a column and an agent variable must speak ONE option vocabulary, or the same
|
|
32
|
+
* list means two things depending on which surface reads it. Never fork this.
|
|
33
|
+
*
|
|
34
|
+
* `groupName` is what makes tiering free: a list's items already carry a
|
|
35
|
+
* `group_name`, so binding to one group of a list yields a narrower set with no
|
|
36
|
+
* second list to maintain, and the ungrouped binding yields sections for free.
|
|
37
|
+
*/
|
|
38
|
+
type StructuredListBinding = {
|
|
39
|
+
/** `workbench.udt_structured_lists.id` the options are hydrated from. */
|
|
40
|
+
listId: string;
|
|
41
|
+
/** Optional FIXED group filter within the list. */
|
|
42
|
+
groupName?: string;
|
|
43
|
+
/**
|
|
44
|
+
* DEPENDENT COLUMNS — the group filter comes from another column's cell
|
|
45
|
+
* instead of being fixed. Holds that column's MACHINE field name.
|
|
46
|
+
*
|
|
47
|
+
* Picking "North America" in Continent narrows Country to the list's
|
|
48
|
+
* "North America" group, because the list's items already carry the group.
|
|
49
|
+
* Nothing is declared on the controlling column: the dependency is stated
|
|
50
|
+
* once, here, by the column that is constrained.
|
|
51
|
+
*
|
|
52
|
+
* Chains are free and need no graph: each column reads its controller's
|
|
53
|
+
* CURRENT cell value at render time, so A → B → C works with no traversal,
|
|
54
|
+
* and a cycle cannot loop (a cell value is data, not a computation).
|
|
55
|
+
*
|
|
56
|
+
* Two behaviours this must always keep, both of them the fallback law:
|
|
57
|
+
* - the controlling cell empty means ALL groups are offered as sections,
|
|
58
|
+
* never an empty dropdown;
|
|
59
|
+
* - changing the controller NEVER rewrites or clears the dependent cell.
|
|
60
|
+
* A value that no longer fits renders in amber, exactly like any other
|
|
61
|
+
* off-list value, and the user decides.
|
|
62
|
+
*
|
|
63
|
+
* Ignored when `groupName` is set — a fixed group wins over a derived one.
|
|
64
|
+
*/
|
|
65
|
+
groupFromField?: string;
|
|
66
|
+
/** Multi-select. Mirrors the agent-variable flag; `multi_choice` implies it. */
|
|
67
|
+
multiple?: boolean;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* One option offered by a choice format.
|
|
71
|
+
*
|
|
72
|
+
* `value` is EXACTLY what the cell stores — the human-readable label, not an
|
|
73
|
+
* id. That is deliberate: it keeps raw data readable, lets an existing column
|
|
74
|
+
* adopt a choice format with zero rewriting, and keeps the fallback law
|
|
75
|
+
* meaningful (a value not in the list is still legible in the cell).
|
|
76
|
+
*/
|
|
77
|
+
type FieldChoice = {
|
|
78
|
+
value: string;
|
|
79
|
+
/** Display text when it should differ from the stored value. */
|
|
80
|
+
label?: string;
|
|
81
|
+
/** Section heading — carried over from a structured list item's group_name. */
|
|
82
|
+
group?: string;
|
|
83
|
+
/** Chip tint. A name from the shared choice palette, never a raw hex. */
|
|
84
|
+
color?: string;
|
|
85
|
+
/** Hover help, carried over from a structured list item's help_text. */
|
|
86
|
+
help?: string;
|
|
87
|
+
};
|
|
88
|
+
/** Per-field options. Every key is optional; every format has sane defaults. */
|
|
89
|
+
type FieldFormatOptions = {
|
|
90
|
+
/** `currency` — ISO 4217 code. Default "USD". */
|
|
91
|
+
currency?: string;
|
|
92
|
+
/** `currency` / `decimal` / `percent` — fixed decimal places. */
|
|
93
|
+
precision?: number;
|
|
94
|
+
/**
|
|
95
|
+
* `percent` — how the number is STORED.
|
|
96
|
+
* - "whole" (default): 45 means 45%
|
|
97
|
+
* - "fraction": 0.45 means 45%
|
|
98
|
+
*/
|
|
99
|
+
percentScale?: "whole" | "fraction";
|
|
100
|
+
/** `duration` — unit the number is stored in. Default "seconds". */
|
|
101
|
+
durationUnit?: "seconds" | "minutes" | "hours" | "milliseconds";
|
|
102
|
+
/** `rating` — top of the scale. Default 5. */
|
|
103
|
+
ratingMax?: number;
|
|
104
|
+
/** `number` / `decimal` / `integer` — thousands separators. Default true. */
|
|
105
|
+
useGrouping?: boolean;
|
|
106
|
+
/** `date` / `datetime` — Intl date style. Default "medium". */
|
|
107
|
+
dateStyle?: "short" | "medium" | "long";
|
|
108
|
+
/** Text shown after the value (e.g. "kg", "req/s"). */
|
|
109
|
+
suffix?: string;
|
|
110
|
+
/** Text shown before the value. */
|
|
111
|
+
prefix?: string;
|
|
112
|
+
/**
|
|
113
|
+
* `choice` / `multi_choice` — the options offered inline. Ignored when
|
|
114
|
+
* `structuredList` is set; that binding is the source of truth for options.
|
|
115
|
+
*/
|
|
116
|
+
choices?: FieldChoice[];
|
|
117
|
+
/**
|
|
118
|
+
* `choice` / `multi_choice` — hydrate options from a shared pick list instead
|
|
119
|
+
* of holding a private copy, so "Status" means one thing across every table.
|
|
120
|
+
*/
|
|
121
|
+
structuredList?: StructuredListBinding;
|
|
122
|
+
/**
|
|
123
|
+
* `choice` / `multi_choice` — may a value outside the option list be entered
|
|
124
|
+
* and kept? Default TRUE, because a format may never take away a working
|
|
125
|
+
* input, and because a column always adopts a format over data that predates
|
|
126
|
+
* it. Off-list values are never rejected or blanked — they render in amber.
|
|
127
|
+
*/
|
|
128
|
+
allowOther?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* `formula` — the computed column's definition.
|
|
131
|
+
*
|
|
132
|
+
* `expression` is the formula text exactly as the user typed it, parsed by
|
|
133
|
+
* `features/data-tables/formulas.ts`. `resultFormat` is how the computed
|
|
134
|
+
* value should be DISPLAYED (a computed total shown as Currency, a computed
|
|
135
|
+
* date shown as a Date); when it is absent the value renders plainly.
|
|
136
|
+
*
|
|
137
|
+
* The expression lives here, in `metadata.format.options`, for the same
|
|
138
|
+
* reason every other format option does: it is a UI layer over an untouched
|
|
139
|
+
* storage type. The cell itself is always `null` — strip the format and the
|
|
140
|
+
* column is an ordinary empty text column, with nothing to migrate.
|
|
141
|
+
*/
|
|
142
|
+
formula?: {
|
|
143
|
+
expression: string;
|
|
144
|
+
resultFormat?: FieldFormatId;
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
/** What a field declares. Persisted as `metadata.format` on the field row. */
|
|
148
|
+
type FieldFormatConfig = {
|
|
149
|
+
id: FieldFormatId;
|
|
150
|
+
options?: FieldFormatOptions;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Result of formatting one value.
|
|
154
|
+
*
|
|
155
|
+
* `ok: false` means the value did not fit the declared format — `text` still
|
|
156
|
+
* carries a readable base-type rendering so the cell is never blank, and
|
|
157
|
+
* `reason` explains what went wrong (surfaced as a tooltip).
|
|
158
|
+
*/
|
|
159
|
+
type FormatResult = {
|
|
160
|
+
ok: boolean;
|
|
161
|
+
/** Always populated. Empty string only for genuinely empty values. */
|
|
162
|
+
text: string;
|
|
163
|
+
/** Set when `ok` is false. */
|
|
164
|
+
reason?: string;
|
|
165
|
+
/** True when the raw value is null/undefined/"" — not a mismatch. */
|
|
166
|
+
empty: boolean;
|
|
167
|
+
};
|
|
168
|
+
/** How a value should be edited. Maps to a concrete input in the UI layer. */
|
|
169
|
+
type FieldEditorKind = "text" | "textarea" | "number" | "checkbox" | "date" | "datetime" | "email" | "url" | "tel" | "color" | "rating" | "json" | "select" | "multiselect"
|
|
170
|
+
/**
|
|
171
|
+
* Not editable at all — the value is COMPUTED, so the grid refuses editing
|
|
172
|
+
* rather than offering an input that would throw the computed value away.
|
|
173
|
+
*/
|
|
174
|
+
| "computed";
|
|
175
|
+
type FieldFormatDef = {
|
|
176
|
+
id: FieldFormatId;
|
|
177
|
+
label: string;
|
|
178
|
+
description: string;
|
|
179
|
+
/** Grouping for the picker UI. */
|
|
180
|
+
group: "Text" | "Numbers" | "Dates" | "Choice" | "Structured";
|
|
181
|
+
/** The storage type this format requires. */
|
|
182
|
+
base: FieldBaseType;
|
|
183
|
+
/** Extra base types this format tolerates (read-only leniency). */
|
|
184
|
+
alsoAccepts?: FieldBaseType[];
|
|
185
|
+
editor: FieldEditorKind;
|
|
186
|
+
/** Right-align in a grid (numeric-ish). */
|
|
187
|
+
numericAlign?: boolean;
|
|
188
|
+
/** Rendered richly by `<FormattedFieldValue>` (link, swatch, chips, …). */
|
|
189
|
+
rich?: boolean;
|
|
190
|
+
/** Which options this format actually reads — drives the options UI. */
|
|
191
|
+
optionKeys?: (keyof FieldFormatOptions)[];
|
|
192
|
+
/**
|
|
193
|
+
* Turn a stored value into display text. Return `null` to signal the value
|
|
194
|
+
* does not fit this format (the caller then falls back to base rendering).
|
|
195
|
+
*/
|
|
196
|
+
format: (value: unknown, options: FieldFormatOptions) => string | null;
|
|
197
|
+
/**
|
|
198
|
+
* Turn raw editor input into the value to store. Must return a value valid
|
|
199
|
+
* for `base`, or `null` to store nothing.
|
|
200
|
+
*/
|
|
201
|
+
parse: (raw: unknown, options: FieldFormatOptions) => unknown;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export type { FieldFormatId as F, StructuredListBinding as S, FieldFormatDef as a, FieldFormatConfig as b, FormatResult as c, FieldBaseType as d, FieldChoice as e, FieldEditorKind as f, FieldFormatOptions as g };
|