@ai-matrx/design-system 0.21.14 → 0.25.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 +188 -0
- package/dist/data-table/cell-undo.cjs +98 -0
- package/dist/data-table/cell-undo.cjs.map +1 -0
- package/dist/data-table/cell-undo.d.cts +85 -0
- package/dist/data-table/cell-undo.d.ts +85 -0
- package/dist/data-table/cell-undo.js +78 -0
- package/dist/data-table/cell-undo.js.map +1 -0
- package/dist/data-table/copy-helpers.d.cts +6 -1
- package/dist/data-table/copy-helpers.d.ts +6 -1
- package/dist/data-table/coverage.cjs +116 -0
- package/dist/data-table/coverage.cjs.map +1 -0
- package/dist/data-table/coverage.d.cts +65 -0
- package/dist/data-table/coverage.d.ts +65 -0
- package/dist/data-table/coverage.js +96 -0
- package/dist/data-table/coverage.js.map +1 -0
- package/dist/data-table/fill.cjs +299 -0
- package/dist/data-table/fill.cjs.map +1 -0
- package/dist/data-table/fill.d.cts +129 -0
- package/dist/data-table/fill.d.ts +129 -0
- package/dist/data-table/fill.js +277 -0
- package/dist/data-table/fill.js.map +1 -0
- package/dist/data-table/filter-engine.d.cts +6 -1
- package/dist/data-table/filter-engine.d.ts +6 -1
- package/dist/data-table/grid-clipboard.cjs +165 -0
- package/dist/data-table/grid-clipboard.cjs.map +1 -0
- package/dist/data-table/grid-clipboard.d.cts +85 -0
- package/dist/data-table/grid-clipboard.d.ts +85 -0
- package/dist/data-table/grid-clipboard.js +145 -0
- package/dist/data-table/grid-clipboard.js.map +1 -0
- package/dist/data-table/grid-selection.cjs +258 -0
- package/dist/data-table/grid-selection.cjs.map +1 -0
- package/dist/data-table/grid-selection.d.cts +166 -0
- package/dist/data-table/grid-selection.d.ts +166 -0
- package/dist/data-table/grid-selection.js +238 -0
- package/dist/data-table/grid-selection.js.map +1 -0
- package/dist/data-table/grouping.cjs +302 -0
- package/dist/data-table/grouping.cjs.map +1 -0
- package/dist/data-table/grouping.d.cts +119 -0
- package/dist/data-table/grouping.d.ts +119 -0
- package/dist/data-table/grouping.js +282 -0
- package/dist/data-table/grouping.js.map +1 -0
- package/dist/data-table/host.d.cts +6 -1
- package/dist/data-table/host.d.ts +6 -1
- package/dist/data-table/index.cjs +4120 -806
- package/dist/data-table/index.cjs.map +1 -1
- package/dist/data-table/index.d.cts +99 -3
- package/dist/data-table/index.d.ts +99 -3
- package/dist/data-table/index.js +3971 -653
- package/dist/data-table/index.js.map +1 -1
- package/dist/data-table/infer-filter.d.cts +6 -1
- package/dist/data-table/infer-filter.d.ts +6 -1
- package/dist/data-table/layered-filters.d.cts +6 -1
- package/dist/data-table/layered-filters.d.ts +6 -1
- package/dist/data-table/query-control.d.cts +6 -1
- package/dist/data-table/query-control.d.ts +6 -1
- package/dist/data-table/spreadsheet.cjs +1014 -0
- package/dist/data-table/spreadsheet.cjs.map +1 -0
- package/dist/data-table/spreadsheet.d.cts +118 -0
- package/dist/data-table/spreadsheet.d.ts +118 -0
- package/dist/data-table/spreadsheet.js +994 -0
- package/dist/data-table/spreadsheet.js.map +1 -0
- package/dist/data-table/types.cjs.map +1 -1
- package/dist/data-table/types.d.cts +6 -1
- package/dist/data-table/types.d.ts +6 -1
- package/dist/data-table/url-state.d.cts +6 -1
- package/dist/data-table/url-state.d.ts +6 -1
- package/dist/data-table/validation.cjs +229 -0
- package/dist/data-table/validation.cjs.map +1 -0
- package/dist/data-table/validation.d.cts +125 -0
- package/dist/data-table/validation.d.ts +125 -0
- package/dist/data-table/validation.js +209 -0
- package/dist/data-table/validation.js.map +1 -0
- package/dist/data-table/virtual-window.cjs +203 -0
- package/dist/data-table/virtual-window.cjs.map +1 -0
- package/dist/data-table/virtual-window.d.cts +84 -0
- package/dist/data-table/virtual-window.d.ts +84 -0
- package/dist/data-table/virtual-window.js +183 -0
- package/dist/data-table/virtual-window.js.map +1 -0
- package/dist/data-table/xlsx.d.cts +6 -1
- package/dist/data-table/xlsx.d.ts +6 -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/formulas/index.cjs +1041 -0
- package/dist/formulas/index.cjs.map +1 -0
- package/dist/formulas/index.d.cts +389 -0
- package/dist/formulas/index.d.ts +389 -0
- package/dist/formulas/index.js +1019 -0
- package/dist/formulas/index.js.map +1 -0
- package/dist/types-CJWU1Gae.d.cts +204 -0
- package/dist/types-CJWU1Gae.d.ts +204 -0
- package/dist/{types-BgqIKbhq.d.cts → types-CmQrOz-T.d.cts} +382 -1
- package/dist/{types-emLxze_m.d.ts → types-nlLCMiqd.d.ts} +382 -1
- package/package.json +119 -2
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/field-formats/registry.ts
|
|
4
|
+
import {
|
|
5
|
+
formatDurationSeconds,
|
|
6
|
+
formatFileSize,
|
|
7
|
+
formatRelativeTime
|
|
8
|
+
} from "@ai-matrx/kit/format";
|
|
9
|
+
function formatSignedDuration(totalSeconds) {
|
|
10
|
+
return formatDurationSeconds(totalSeconds, { signed: true });
|
|
11
|
+
}
|
|
12
|
+
function toNumber(value) {
|
|
13
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
14
|
+
if (typeof value === "string") {
|
|
15
|
+
const cleaned = value.replace(/[,\s$€£¥%]/g, "");
|
|
16
|
+
if (cleaned === "") return null;
|
|
17
|
+
const n = Number(cleaned);
|
|
18
|
+
return Number.isFinite(n) ? n : null;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function toText(value) {
|
|
23
|
+
if (typeof value === "string") return value;
|
|
24
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
25
|
+
return String(value);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function toDate(value) {
|
|
29
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
30
|
+
if (typeof value === "number") {
|
|
31
|
+
const d = new Date(value);
|
|
32
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "string" && value.trim()) {
|
|
35
|
+
const d = new Date(value);
|
|
36
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function toList(value) {
|
|
41
|
+
if (Array.isArray(value)) return value;
|
|
42
|
+
if (typeof value === "string") {
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
if (trimmed.startsWith("[")) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(trimmed);
|
|
47
|
+
if (Array.isArray(parsed)) return parsed;
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (trimmed === "") return [];
|
|
52
|
+
return trimmed.split(",").map((s) => s.trim());
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function groupedNumber(n, options) {
|
|
57
|
+
return new Intl.NumberFormat(void 0, {
|
|
58
|
+
useGrouping: options.useGrouping !== false,
|
|
59
|
+
...options.precision != null ? {
|
|
60
|
+
minimumFractionDigits: options.precision,
|
|
61
|
+
maximumFractionDigits: options.precision
|
|
62
|
+
} : { maximumFractionDigits: 10 }
|
|
63
|
+
}).format(n);
|
|
64
|
+
}
|
|
65
|
+
function affix(text, options) {
|
|
66
|
+
return `${options.prefix ?? ""}${text}${options.suffix ?? ""}`;
|
|
67
|
+
}
|
|
68
|
+
var DURATION_TO_SECONDS = { milliseconds: 1e-3, seconds: 1, minutes: 60, hours: 3600 };
|
|
69
|
+
function findChoice(choices, value) {
|
|
70
|
+
if (!choices || choices.length === 0) return void 0;
|
|
71
|
+
const exact = choices.find((c) => c.value === value);
|
|
72
|
+
if (exact) return exact;
|
|
73
|
+
const lowered = value.toLowerCase();
|
|
74
|
+
return choices.find((c) => c.value.toLowerCase() === lowered);
|
|
75
|
+
}
|
|
76
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
77
|
+
var URL_RE = /^(?:[a-z][a-z0-9+.-]*:\/\/\S+|(?:[a-z0-9-]+\.)+[a-z]{2,}(?:[:/?#]\S*)?)$/i;
|
|
78
|
+
var HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
79
|
+
var DEFS = [
|
|
80
|
+
// ── Text ──────────────────────────────────────────────────────────────────
|
|
81
|
+
{
|
|
82
|
+
id: "text",
|
|
83
|
+
label: "Text",
|
|
84
|
+
description: "Plain single-line text",
|
|
85
|
+
group: "Text",
|
|
86
|
+
base: "string",
|
|
87
|
+
editor: "text",
|
|
88
|
+
// Deliberately no optionKeys — plain text is the default for every string
|
|
89
|
+
// column, and giving it prefix/suffix controls puts an options row under
|
|
90
|
+
// every column in the settings dialog for a setting almost nobody wants.
|
|
91
|
+
format: (v, o) => {
|
|
92
|
+
const t = toText(v);
|
|
93
|
+
return t === null ? null : affix(t, o);
|
|
94
|
+
},
|
|
95
|
+
parse: (raw) => raw === "" || raw == null ? null : String(raw)
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "long_text",
|
|
99
|
+
label: "Long text",
|
|
100
|
+
description: "Multi-line text, edited in a larger box",
|
|
101
|
+
group: "Text",
|
|
102
|
+
base: "string",
|
|
103
|
+
editor: "textarea",
|
|
104
|
+
format: (v) => toText(v),
|
|
105
|
+
parse: (raw) => raw === "" || raw == null ? null : String(raw)
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "markdown",
|
|
109
|
+
label: "Markdown",
|
|
110
|
+
description: "Rich text written as Markdown",
|
|
111
|
+
group: "Text",
|
|
112
|
+
base: "string",
|
|
113
|
+
editor: "textarea",
|
|
114
|
+
rich: true,
|
|
115
|
+
format: (v) => toText(v),
|
|
116
|
+
parse: (raw) => raw === "" || raw == null ? null : String(raw)
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "email",
|
|
120
|
+
label: "Email",
|
|
121
|
+
description: "Email address \u2014 click to compose",
|
|
122
|
+
group: "Text",
|
|
123
|
+
base: "string",
|
|
124
|
+
editor: "email",
|
|
125
|
+
rich: true,
|
|
126
|
+
format: (v) => {
|
|
127
|
+
const t = toText(v)?.trim();
|
|
128
|
+
if (!t) return null;
|
|
129
|
+
return EMAIL_RE.test(t) ? t : null;
|
|
130
|
+
},
|
|
131
|
+
parse: (raw) => raw === "" || raw == null ? null : String(raw).trim()
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "url",
|
|
135
|
+
label: "Link",
|
|
136
|
+
description: "Web address \u2014 click to open",
|
|
137
|
+
group: "Text",
|
|
138
|
+
base: "string",
|
|
139
|
+
editor: "url",
|
|
140
|
+
rich: true,
|
|
141
|
+
format: (v) => {
|
|
142
|
+
const t = toText(v)?.trim();
|
|
143
|
+
if (!t) return null;
|
|
144
|
+
if (/\s/.test(t)) return null;
|
|
145
|
+
if (!URL_RE.test(t)) return null;
|
|
146
|
+
try {
|
|
147
|
+
new URL(t.includes("://") ? t : `https://${t}`);
|
|
148
|
+
return t;
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
parse: (raw) => raw === "" || raw == null ? null : String(raw).trim()
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "phone",
|
|
157
|
+
label: "Phone",
|
|
158
|
+
description: "Phone number \u2014 click to call",
|
|
159
|
+
group: "Text",
|
|
160
|
+
base: "string",
|
|
161
|
+
editor: "tel",
|
|
162
|
+
rich: true,
|
|
163
|
+
format: (v) => {
|
|
164
|
+
const t = toText(v)?.trim();
|
|
165
|
+
if (!t) return null;
|
|
166
|
+
return (t.match(/\d/g) ?? []).length >= 7 ? t : null;
|
|
167
|
+
},
|
|
168
|
+
parse: (raw) => raw === "" || raw == null ? null : String(raw).trim()
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "color",
|
|
172
|
+
label: "Color",
|
|
173
|
+
description: "Hex color with a swatch",
|
|
174
|
+
group: "Text",
|
|
175
|
+
base: "string",
|
|
176
|
+
editor: "color",
|
|
177
|
+
rich: true,
|
|
178
|
+
format: (v) => {
|
|
179
|
+
const t = toText(v)?.trim();
|
|
180
|
+
if (!t) return null;
|
|
181
|
+
return HEX_RE.test(t) ? t.toLowerCase() : null;
|
|
182
|
+
},
|
|
183
|
+
parse: (raw) => raw === "" || raw == null ? null : String(raw).trim()
|
|
184
|
+
},
|
|
185
|
+
// ── Numbers ───────────────────────────────────────────────────────────────
|
|
186
|
+
{
|
|
187
|
+
id: "number",
|
|
188
|
+
label: "Number",
|
|
189
|
+
description: "Plain number",
|
|
190
|
+
group: "Numbers",
|
|
191
|
+
base: "number",
|
|
192
|
+
alsoAccepts: ["integer"],
|
|
193
|
+
editor: "number",
|
|
194
|
+
numericAlign: true,
|
|
195
|
+
optionKeys: ["useGrouping", "precision", "prefix", "suffix"],
|
|
196
|
+
format: (v, o) => {
|
|
197
|
+
const n = toNumber(v);
|
|
198
|
+
return n === null ? null : affix(groupedNumber(n, o), o);
|
|
199
|
+
},
|
|
200
|
+
parse: (raw) => toNumber(raw)
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "decimal",
|
|
204
|
+
label: "Decimal",
|
|
205
|
+
description: "Number with fixed decimal places",
|
|
206
|
+
group: "Numbers",
|
|
207
|
+
base: "number",
|
|
208
|
+
alsoAccepts: ["integer"],
|
|
209
|
+
editor: "number",
|
|
210
|
+
numericAlign: true,
|
|
211
|
+
optionKeys: ["precision", "useGrouping", "prefix", "suffix"],
|
|
212
|
+
format: (v, o) => {
|
|
213
|
+
const n = toNumber(v);
|
|
214
|
+
if (n === null) return null;
|
|
215
|
+
return affix(groupedNumber(n, { ...o, precision: o.precision ?? 2 }), o);
|
|
216
|
+
},
|
|
217
|
+
parse: (raw) => toNumber(raw)
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: "currency",
|
|
221
|
+
label: "Currency",
|
|
222
|
+
description: "Money \u2014 stored as a plain number, shown with a symbol",
|
|
223
|
+
group: "Numbers",
|
|
224
|
+
base: "number",
|
|
225
|
+
alsoAccepts: ["integer"],
|
|
226
|
+
editor: "number",
|
|
227
|
+
numericAlign: true,
|
|
228
|
+
optionKeys: ["currency", "precision"],
|
|
229
|
+
format: (v, o) => {
|
|
230
|
+
const n = toNumber(v);
|
|
231
|
+
if (n === null) return null;
|
|
232
|
+
const currency = (o.currency || "USD").toUpperCase();
|
|
233
|
+
try {
|
|
234
|
+
return new Intl.NumberFormat(void 0, {
|
|
235
|
+
style: "currency",
|
|
236
|
+
currency,
|
|
237
|
+
...o.precision != null ? {
|
|
238
|
+
minimumFractionDigits: o.precision,
|
|
239
|
+
maximumFractionDigits: o.precision
|
|
240
|
+
} : {}
|
|
241
|
+
}).format(n);
|
|
242
|
+
} catch {
|
|
243
|
+
return `${groupedNumber(n, { precision: o.precision ?? 2 })} ${currency}`;
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
parse: (raw) => toNumber(raw)
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: "percent",
|
|
250
|
+
label: "Percent",
|
|
251
|
+
description: "Percentage \u2014 stored as a plain number",
|
|
252
|
+
group: "Numbers",
|
|
253
|
+
base: "number",
|
|
254
|
+
alsoAccepts: ["integer"],
|
|
255
|
+
editor: "number",
|
|
256
|
+
numericAlign: true,
|
|
257
|
+
optionKeys: ["percentScale", "precision"],
|
|
258
|
+
format: (v, o) => {
|
|
259
|
+
const n = toNumber(v);
|
|
260
|
+
if (n === null) return null;
|
|
261
|
+
const shown = o.percentScale === "fraction" ? n * 100 : n;
|
|
262
|
+
return `${groupedNumber(shown, o.precision === void 0 ? {} : { precision: o.precision })}%`;
|
|
263
|
+
},
|
|
264
|
+
parse: (raw) => toNumber(raw)
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: "duration",
|
|
268
|
+
label: "Duration",
|
|
269
|
+
description: "Length of time, shown as h:mm:ss",
|
|
270
|
+
group: "Numbers",
|
|
271
|
+
base: "number",
|
|
272
|
+
alsoAccepts: ["integer"],
|
|
273
|
+
editor: "number",
|
|
274
|
+
numericAlign: true,
|
|
275
|
+
optionKeys: ["durationUnit"],
|
|
276
|
+
format: (v, o) => {
|
|
277
|
+
const n = toNumber(v);
|
|
278
|
+
if (n === null) return null;
|
|
279
|
+
return formatSignedDuration(n * DURATION_TO_SECONDS[o.durationUnit ?? "seconds"]);
|
|
280
|
+
},
|
|
281
|
+
parse: (raw) => toNumber(raw)
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: "integer",
|
|
285
|
+
label: "Whole number",
|
|
286
|
+
description: "Integer with no decimal part",
|
|
287
|
+
group: "Numbers",
|
|
288
|
+
base: "integer",
|
|
289
|
+
alsoAccepts: ["number"],
|
|
290
|
+
editor: "number",
|
|
291
|
+
numericAlign: true,
|
|
292
|
+
optionKeys: ["useGrouping", "prefix", "suffix"],
|
|
293
|
+
format: (v, o) => {
|
|
294
|
+
const n = toNumber(v);
|
|
295
|
+
if (n === null) return null;
|
|
296
|
+
return affix(groupedNumber(Math.trunc(n), { ...o, precision: 0 }), o);
|
|
297
|
+
},
|
|
298
|
+
parse: (raw) => {
|
|
299
|
+
const n = toNumber(raw);
|
|
300
|
+
return n === null ? null : Math.trunc(n);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: "rating",
|
|
305
|
+
label: "Rating",
|
|
306
|
+
description: "Score out of a maximum, shown as stars",
|
|
307
|
+
group: "Numbers",
|
|
308
|
+
base: "integer",
|
|
309
|
+
alsoAccepts: ["number"],
|
|
310
|
+
editor: "rating",
|
|
311
|
+
rich: true,
|
|
312
|
+
optionKeys: ["ratingMax"],
|
|
313
|
+
format: (v, o) => {
|
|
314
|
+
const n = toNumber(v);
|
|
315
|
+
if (n === null) return null;
|
|
316
|
+
const max = o.ratingMax ?? 5;
|
|
317
|
+
if (n < 0 || n > max) return null;
|
|
318
|
+
return `${n} / ${max}`;
|
|
319
|
+
},
|
|
320
|
+
parse: (raw) => {
|
|
321
|
+
const n = toNumber(raw);
|
|
322
|
+
return n === null ? null : Math.round(n);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: "file_size",
|
|
327
|
+
label: "File size",
|
|
328
|
+
description: "Byte count shown as KB / MB / GB",
|
|
329
|
+
group: "Numbers",
|
|
330
|
+
base: "integer",
|
|
331
|
+
alsoAccepts: ["number"],
|
|
332
|
+
editor: "number",
|
|
333
|
+
numericAlign: true,
|
|
334
|
+
format: (v) => {
|
|
335
|
+
const n = toNumber(v);
|
|
336
|
+
return n === null ? null : formatFileSize(n);
|
|
337
|
+
},
|
|
338
|
+
parse: (raw) => {
|
|
339
|
+
const n = toNumber(raw);
|
|
340
|
+
return n === null ? null : Math.trunc(n);
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
// ── Choice ────────────────────────────────────────────────────────────────
|
|
344
|
+
{
|
|
345
|
+
id: "boolean",
|
|
346
|
+
label: "Yes / No",
|
|
347
|
+
description: "True or false",
|
|
348
|
+
group: "Choice",
|
|
349
|
+
base: "boolean",
|
|
350
|
+
editor: "checkbox",
|
|
351
|
+
format: (v) => {
|
|
352
|
+
if (typeof v === "boolean") return v ? "Yes" : "No";
|
|
353
|
+
if (v === "true" || v === 1) return "Yes";
|
|
354
|
+
if (v === "false" || v === 0) return "No";
|
|
355
|
+
return null;
|
|
356
|
+
},
|
|
357
|
+
parse: (raw) => {
|
|
358
|
+
if (typeof raw === "boolean") return raw;
|
|
359
|
+
if (raw == null || raw === "") return null;
|
|
360
|
+
return String(raw).toLowerCase() === "true" || raw === 1;
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
// ── Dates ─────────────────────────────────────────────────────────────────
|
|
364
|
+
{
|
|
365
|
+
id: "date",
|
|
366
|
+
label: "Date",
|
|
367
|
+
description: "Calendar date",
|
|
368
|
+
group: "Dates",
|
|
369
|
+
base: "date",
|
|
370
|
+
alsoAccepts: ["datetime", "string"],
|
|
371
|
+
editor: "date",
|
|
372
|
+
optionKeys: ["dateStyle"],
|
|
373
|
+
format: (v, o) => {
|
|
374
|
+
const d = toDate(v);
|
|
375
|
+
if (!d) return null;
|
|
376
|
+
return d.toLocaleDateString(void 0, {
|
|
377
|
+
dateStyle: o.dateStyle ?? "medium"
|
|
378
|
+
});
|
|
379
|
+
},
|
|
380
|
+
parse: (raw) => raw === "" || raw == null ? null : String(raw)
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: "datetime",
|
|
384
|
+
label: "Date & time",
|
|
385
|
+
description: "Calendar date with a time of day",
|
|
386
|
+
group: "Dates",
|
|
387
|
+
base: "datetime",
|
|
388
|
+
alsoAccepts: ["date", "string"],
|
|
389
|
+
editor: "datetime",
|
|
390
|
+
optionKeys: ["dateStyle"],
|
|
391
|
+
format: (v, o) => {
|
|
392
|
+
const d = toDate(v);
|
|
393
|
+
if (!d) return null;
|
|
394
|
+
return d.toLocaleString(void 0, {
|
|
395
|
+
dateStyle: o.dateStyle ?? "medium",
|
|
396
|
+
timeStyle: "short"
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
parse: (raw) => raw === "" || raw == null ? null : String(raw)
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "relative_time",
|
|
403
|
+
label: "Relative time",
|
|
404
|
+
description: 'Shown as "3 days ago"',
|
|
405
|
+
group: "Dates",
|
|
406
|
+
base: "datetime",
|
|
407
|
+
alsoAccepts: ["date", "string"],
|
|
408
|
+
editor: "datetime",
|
|
409
|
+
format: (v) => {
|
|
410
|
+
const d = toDate(v);
|
|
411
|
+
return d ? formatRelativeTime(d, { style: "intl" }) : null;
|
|
412
|
+
},
|
|
413
|
+
parse: (raw) => raw === "" || raw == null ? null : String(raw)
|
|
414
|
+
},
|
|
415
|
+
// ── Structured ────────────────────────────────────────────────────────────
|
|
416
|
+
{
|
|
417
|
+
id: "json",
|
|
418
|
+
label: "JSON",
|
|
419
|
+
description: "Structured object",
|
|
420
|
+
group: "Structured",
|
|
421
|
+
base: "json",
|
|
422
|
+
alsoAccepts: ["array"],
|
|
423
|
+
editor: "json",
|
|
424
|
+
format: (v) => {
|
|
425
|
+
if (v === null || v === void 0) return null;
|
|
426
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
427
|
+
return toText(v);
|
|
428
|
+
},
|
|
429
|
+
parse: (raw) => {
|
|
430
|
+
if (raw == null || raw === "") return null;
|
|
431
|
+
if (typeof raw !== "string") return raw;
|
|
432
|
+
try {
|
|
433
|
+
return JSON.parse(raw);
|
|
434
|
+
} catch {
|
|
435
|
+
return raw;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: "array",
|
|
441
|
+
label: "List",
|
|
442
|
+
description: "List of values",
|
|
443
|
+
group: "Structured",
|
|
444
|
+
base: "array",
|
|
445
|
+
alsoAccepts: ["json"],
|
|
446
|
+
editor: "json",
|
|
447
|
+
format: (v) => {
|
|
448
|
+
const list = toList(v);
|
|
449
|
+
return list === null ? null : list.map((i) => String(i)).join(", ");
|
|
450
|
+
},
|
|
451
|
+
parse: (raw) => {
|
|
452
|
+
if (raw == null || raw === "") return null;
|
|
453
|
+
if (Array.isArray(raw)) return raw;
|
|
454
|
+
return toList(raw) ?? raw;
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
id: "tags",
|
|
459
|
+
label: "Tags",
|
|
460
|
+
description: "List of values shown as chips",
|
|
461
|
+
group: "Structured",
|
|
462
|
+
base: "array",
|
|
463
|
+
alsoAccepts: ["json", "string"],
|
|
464
|
+
editor: "json",
|
|
465
|
+
rich: true,
|
|
466
|
+
format: (v) => {
|
|
467
|
+
const list = toList(v);
|
|
468
|
+
return list === null ? null : list.map((i) => String(i)).join(", ");
|
|
469
|
+
},
|
|
470
|
+
parse: (raw) => {
|
|
471
|
+
if (raw == null || raw === "") return null;
|
|
472
|
+
if (Array.isArray(raw)) return raw;
|
|
473
|
+
return toList(raw) ?? raw;
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
// ─── Choice ────────────────────────────────────────────────────────────────
|
|
477
|
+
//
|
|
478
|
+
// An enum WITHOUT a database enum. The column stays `string` (or `array` for
|
|
479
|
+
// multi), the database enforces nothing, and stripping the format leaves
|
|
480
|
+
// every stored value exactly as it was. Options are a UI layer — the same
|
|
481
|
+
// trick `percent` plays on a number.
|
|
482
|
+
//
|
|
483
|
+
// The fallback law is the whole reason this is safe to switch on over live
|
|
484
|
+
// data: a value not in the option list is NOT an error and is NEVER blanked.
|
|
485
|
+
// `format()` returns null for it, `<FormattedFieldValue>` renders it in amber
|
|
486
|
+
// with the reason, and the user can fix it or adopt it. Declaring the options
|
|
487
|
+
// is therefore how you FIND your stray values, not how you lose them.
|
|
488
|
+
//
|
|
489
|
+
// Options resolve through `choices.ts` — inline `options.choices`, or a
|
|
490
|
+
// structured list via `options.structuredList`. This def only ever sees
|
|
491
|
+
// whatever the caller resolved into `options.choices`, so it stays pure and
|
|
492
|
+
// synchronous like every other format.
|
|
493
|
+
{
|
|
494
|
+
id: "choice",
|
|
495
|
+
label: "Choice",
|
|
496
|
+
description: "One value from a list of options",
|
|
497
|
+
group: "Choice",
|
|
498
|
+
base: "string",
|
|
499
|
+
alsoAccepts: ["number", "integer", "boolean"],
|
|
500
|
+
editor: "select",
|
|
501
|
+
rich: true,
|
|
502
|
+
optionKeys: ["choices", "structuredList", "allowOther"],
|
|
503
|
+
format: (v, options) => {
|
|
504
|
+
const text = toText(v);
|
|
505
|
+
if (text === null) return null;
|
|
506
|
+
const trimmed = text.trim();
|
|
507
|
+
if (trimmed === "") return "";
|
|
508
|
+
const match = findChoice(options.choices, trimmed);
|
|
509
|
+
if (!options.choices || options.choices.length === 0) return trimmed;
|
|
510
|
+
return match ? match.label ?? match.value : null;
|
|
511
|
+
},
|
|
512
|
+
parse: (raw) => {
|
|
513
|
+
if (raw == null) return null;
|
|
514
|
+
const text = toText(raw);
|
|
515
|
+
if (text === null) return raw;
|
|
516
|
+
const trimmed = text.trim();
|
|
517
|
+
return trimmed === "" ? null : trimmed;
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
id: "multi_choice",
|
|
522
|
+
label: "Multi-choice",
|
|
523
|
+
description: "Any number of values from a list of options",
|
|
524
|
+
group: "Choice",
|
|
525
|
+
base: "array",
|
|
526
|
+
alsoAccepts: ["string", "json"],
|
|
527
|
+
editor: "multiselect",
|
|
528
|
+
rich: true,
|
|
529
|
+
optionKeys: ["choices", "structuredList", "allowOther"],
|
|
530
|
+
format: (v, options) => {
|
|
531
|
+
const list = toList(v);
|
|
532
|
+
if (list === null) return null;
|
|
533
|
+
if (list.length === 0) return "";
|
|
534
|
+
const labels = [];
|
|
535
|
+
let anyOffList = false;
|
|
536
|
+
for (const entry of list) {
|
|
537
|
+
const text = toText(entry);
|
|
538
|
+
if (text === null) {
|
|
539
|
+
anyOffList = true;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const match = findChoice(options.choices, text.trim());
|
|
543
|
+
if (match) labels.push(match.label ?? match.value);
|
|
544
|
+
else {
|
|
545
|
+
labels.push(text.trim());
|
|
546
|
+
anyOffList = true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const haveOptions = !!options.choices && options.choices.length > 0;
|
|
550
|
+
if (haveOptions && anyOffList) return null;
|
|
551
|
+
return labels.join(", ");
|
|
552
|
+
},
|
|
553
|
+
parse: (raw) => {
|
|
554
|
+
if (raw == null || raw === "") return null;
|
|
555
|
+
if (Array.isArray(raw)) return raw;
|
|
556
|
+
return toList(raw) ?? raw;
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
// ─── Computed ──────────────────────────────────────────────────────────────
|
|
560
|
+
//
|
|
561
|
+
// A formula column stores NOTHING. `metadata.format.options.formula` holds
|
|
562
|
+
// the expression; the stored cell is always null; the value is computed on
|
|
563
|
+
// read from the other columns of the same row
|
|
564
|
+
// (`features/data-tables/formulas.ts`). Grouped under Structured on purpose
|
|
565
|
+
// — the picker's groups are a fixed vocabulary, and a formula is structure
|
|
566
|
+
// over a plain string column, not a sixth family.
|
|
567
|
+
//
|
|
568
|
+
// `format()` here is handed the COMPUTED value, never a stored one, and its
|
|
569
|
+
// only job is to render it: through `resultFormat`'s own `format()` when the
|
|
570
|
+
// column declares one (a computed total shown as Currency, a computed date
|
|
571
|
+
// shown as a Date), and plainly otherwise. It never returns null for an
|
|
572
|
+
// empty value, because a formula legitimately produces BLANK.
|
|
573
|
+
//
|
|
574
|
+
// No `optionKeys`: the expression is authored in its own editor, not in the
|
|
575
|
+
// generic option rail, and listing a key the picker has no control for would
|
|
576
|
+
// open an empty options section under every formula column.
|
|
577
|
+
{
|
|
578
|
+
id: "formula",
|
|
579
|
+
label: "Formula",
|
|
580
|
+
description: "Computed from the other columns in the row \u2014 never typed in",
|
|
581
|
+
group: "Structured",
|
|
582
|
+
base: "string",
|
|
583
|
+
editor: "computed",
|
|
584
|
+
format: (v, o) => {
|
|
585
|
+
const target = o.formula?.resultFormat;
|
|
586
|
+
if (target && target !== "formula") {
|
|
587
|
+
const delegate = FIELD_FORMATS[target];
|
|
588
|
+
if (delegate) {
|
|
589
|
+
const rendered = delegate.format(v, o);
|
|
590
|
+
if (rendered !== null) return rendered;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (v === null || v === void 0) return "";
|
|
594
|
+
if (typeof v === "boolean") return v ? "Yes" : "No";
|
|
595
|
+
if (typeof v === "number") return groupedNumber(v, o);
|
|
596
|
+
return toText(v);
|
|
597
|
+
},
|
|
598
|
+
// Nothing is ever stored in a formula column. Returning the raw input here
|
|
599
|
+
// would be the one way a computed cell could acquire a value of its own.
|
|
600
|
+
parse: () => null
|
|
601
|
+
}
|
|
602
|
+
];
|
|
603
|
+
var FIELD_FORMATS = Object.freeze(
|
|
604
|
+
Object.fromEntries(DEFS.map((d) => [d.id, d]))
|
|
605
|
+
);
|
|
606
|
+
var FIELD_FORMAT_LIST = Object.freeze(DEFS);
|
|
607
|
+
var FIELD_FORMAT_IDS = DEFS.map((d) => d.id);
|
|
608
|
+
function getFieldFormat(id) {
|
|
609
|
+
if (!id) return null;
|
|
610
|
+
return FIELD_FORMATS[id] ?? null;
|
|
611
|
+
}
|
|
612
|
+
function defaultFormatForBase(base) {
|
|
613
|
+
switch (base) {
|
|
614
|
+
case "number":
|
|
615
|
+
return "number";
|
|
616
|
+
case "integer":
|
|
617
|
+
return "integer";
|
|
618
|
+
case "boolean":
|
|
619
|
+
return "boolean";
|
|
620
|
+
case "date":
|
|
621
|
+
return "date";
|
|
622
|
+
case "datetime":
|
|
623
|
+
return "datetime";
|
|
624
|
+
case "json":
|
|
625
|
+
return "json";
|
|
626
|
+
case "array":
|
|
627
|
+
return "array";
|
|
628
|
+
default:
|
|
629
|
+
return "text";
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function formatsForBase(base) {
|
|
633
|
+
return DEFS.filter(
|
|
634
|
+
(d) => d.base === base || (d.alsoAccepts ?? []).includes(base)
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
function groupedFormatsForBase(base) {
|
|
638
|
+
const out = [];
|
|
639
|
+
for (const def of formatsForBase(base)) {
|
|
640
|
+
const bucket = out.find((g) => g.group === def.group);
|
|
641
|
+
if (bucket) bucket.formats.push(def);
|
|
642
|
+
else out.push({ group: def.group, formats: [def] });
|
|
643
|
+
}
|
|
644
|
+
return out;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/field-formats/format.ts
|
|
648
|
+
function isEmpty(value) {
|
|
649
|
+
return value === null || value === void 0 || value === "";
|
|
650
|
+
}
|
|
651
|
+
function baseText(value) {
|
|
652
|
+
if (isEmpty(value)) return "";
|
|
653
|
+
if (typeof value === "object") {
|
|
654
|
+
try {
|
|
655
|
+
return JSON.stringify(value);
|
|
656
|
+
} catch {
|
|
657
|
+
return String(value);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return String(value);
|
|
661
|
+
}
|
|
662
|
+
function readFieldFormatConfig(metadata) {
|
|
663
|
+
if (!metadata || typeof metadata !== "object") return null;
|
|
664
|
+
const raw = metadata.format;
|
|
665
|
+
if (!raw || typeof raw !== "object") return null;
|
|
666
|
+
const id = raw.id;
|
|
667
|
+
if (typeof id !== "string" || !getFieldFormat(id)) return null;
|
|
668
|
+
const options = raw.options;
|
|
669
|
+
return {
|
|
670
|
+
id,
|
|
671
|
+
...options && typeof options === "object" ? { options } : {}
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function resolveFieldFormat(dataType, metadata) {
|
|
675
|
+
const declared = readFieldFormatConfig(metadata);
|
|
676
|
+
if (declared) return declared;
|
|
677
|
+
return { id: defaultFormatForBase(dataType) };
|
|
678
|
+
}
|
|
679
|
+
function formatFieldValue(value, config, dataType) {
|
|
680
|
+
if (isEmpty(value)) return { ok: true, text: "", empty: true };
|
|
681
|
+
const def = config ? getFieldFormat(config.id) : null;
|
|
682
|
+
const effective = def ?? FIELD_FORMATS[defaultFormatForBase(dataType ?? "string")];
|
|
683
|
+
let formatted;
|
|
684
|
+
try {
|
|
685
|
+
formatted = effective.format(value, config?.options ?? {});
|
|
686
|
+
} catch {
|
|
687
|
+
formatted = null;
|
|
688
|
+
}
|
|
689
|
+
if (formatted !== null) return { ok: true, text: formatted, empty: false };
|
|
690
|
+
return {
|
|
691
|
+
ok: false,
|
|
692
|
+
text: baseText(value),
|
|
693
|
+
empty: false,
|
|
694
|
+
reason: `This value is not a valid ${effective.label.toLowerCase()} \u2014 showing the stored value instead.`
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function parseFieldInput(raw, config, dataType) {
|
|
698
|
+
const def = config ? getFieldFormat(config.id) : null;
|
|
699
|
+
const effective = def ?? FIELD_FORMATS[defaultFormatForBase(dataType ?? "string")];
|
|
700
|
+
try {
|
|
701
|
+
return effective.parse(raw, config?.options ?? {});
|
|
702
|
+
} catch {
|
|
703
|
+
return raw;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function valueFitsFormat(value, config, dataType) {
|
|
707
|
+
return formatFieldValue(value, config, dataType).ok;
|
|
708
|
+
}
|
|
709
|
+
export {
|
|
710
|
+
FIELD_FORMATS,
|
|
711
|
+
FIELD_FORMAT_IDS,
|
|
712
|
+
FIELD_FORMAT_LIST,
|
|
713
|
+
defaultFormatForBase,
|
|
714
|
+
formatFieldValue,
|
|
715
|
+
formatsForBase,
|
|
716
|
+
getFieldFormat,
|
|
717
|
+
groupedFormatsForBase,
|
|
718
|
+
parseFieldInput,
|
|
719
|
+
readFieldFormatConfig,
|
|
720
|
+
resolveFieldFormat,
|
|
721
|
+
valueFitsFormat
|
|
722
|
+
};
|
|
723
|
+
//# sourceMappingURL=index.js.map
|