@ai-matrx/records-ui 0.93.133 → 0.93.140

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.
@@ -0,0 +1,4559 @@
1
+ "use client";
2
+
3
+ // src/TablePickers.tsx
4
+ import { useEffect as useEffect12, useMemo as useMemo7, useState as useState15 } from "react";
5
+ import { RecordsProvider as RecordsProvider2, useFields, useRecords, useTable } from "@ai-matrx/records/react";
6
+ import { Button as Button9, CreatablePicker } from "@ai-matrx/design-system";
7
+
8
+ // src/editors.tsx
9
+ import { useCallback as useCallback4, useEffect as useEffect10, useRef as useRef9, useState as useState13 } from "react";
10
+ import { allowsOtherValues, choiceValuesOf, coerceTypedAnswer, newerKindOf as newerKindOf4, decideTypedChoice, isTheChosen, optionKey as optionKey2, rankTypedMatches as rankTypedMatches3, readTypedTime as readTypedTime2, typedDateText as typedDateText2, typedTimeText as typedTimeText2 } from "@ai-matrx/records";
11
+ import { useOptionalRecordsClient as useOptionalRecordsClient6 } from "@ai-matrx/records/react";
12
+ import {
13
+ BasicInput as BasicInput6,
14
+ BasicTextarea as BasicTextarea2,
15
+ Badge as Badge3,
16
+ Button as Button8,
17
+ Checkbox,
18
+ Label,
19
+ Popover as Popover4,
20
+ PopoverContent as PopoverContent4,
21
+ PopoverTrigger as PopoverTrigger4,
22
+ Select,
23
+ SelectContent,
24
+ SelectItem,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ cn as cn11
28
+ } from "@ai-matrx/design-system";
29
+
30
+ // src/dates.ts
31
+ import { formatFieldValue } from "@ai-matrx/design-system/field-formats";
32
+ var BARE_CALENDAR_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ]00:00(?::00(?:\.0+)?)?)?$/;
33
+ function systemStampOf(field) {
34
+ if (field.type !== "formula") return null;
35
+ const system = String((field.config ?? {})["system"] ?? "");
36
+ return system === "autonumber" || system === "created_time" || system === "modified_time" ? system : null;
37
+ }
38
+ function carriesTimeOfDay(field, value) {
39
+ if (value !== void 0 && value !== null && value !== "") return hasTimePart(value);
40
+ const stamp = systemStampOf(field);
41
+ if (stamp === "created_time" || stamp === "modified_time") return true;
42
+ return (field.format ?? "").toLowerCase() === "datetime";
43
+ }
44
+ function hasTimePart(value) {
45
+ if (value instanceof Date || typeof value === "number") return true;
46
+ if (typeof value !== "string") return false;
47
+ const trimmed = value.trim();
48
+ if (BARE_CALENDAR_DATE.test(trimmed)) return false;
49
+ return /^\d{4}-\d{2}-\d{2}[T ]\d/.test(trimmed);
50
+ }
51
+ function isDateField(field) {
52
+ const stamp = systemStampOf(field);
53
+ if (stamp === "created_time" || stamp === "modified_time") return true;
54
+ if (field.type !== "range") return false;
55
+ const kind = String((field.config ?? {})["kind"] ?? "");
56
+ return kind === "date" || kind === "datetime";
57
+ }
58
+ function dateFormatFor(field, value) {
59
+ if (!isDateField(field)) return void 0;
60
+ return carriesTimeOfDay(field, value) ? { id: "datetime" } : { id: "date" };
61
+ }
62
+ function dateText(field, value) {
63
+ const format = dateFormatFor(field, value) ?? (isDateLike(value) ? { id: hasTimePart(value) ? "datetime" : "date" } : void 0);
64
+ if (!format) return null;
65
+ const result = formatFieldValue(value, format);
66
+ return result.ok && !result.empty ? result.text : null;
67
+ }
68
+ function isDateLike(value) {
69
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value.trim());
70
+ }
71
+
72
+ // src/newerKindValues.tsx
73
+ import { useEffect, useRef, useState } from "react";
74
+ import { codeSpanText, findInlineCodeSpans } from "@ai-matrx/content-ir/source";
75
+ import {
76
+ ADDRESS_PARTS,
77
+ NOT_A_MEMBER_WORDS,
78
+ STATUS_GROUP_WORDS,
79
+ addressMapUrl,
80
+ addressText,
81
+ durationText,
82
+ isAddressValue,
83
+ isPersonStamp,
84
+ newerKindOf,
85
+ readTypedDuration,
86
+ statusGroupOf
87
+ } from "@ai-matrx/records";
88
+ import { BasicInput, cn } from "@ai-matrx/design-system";
89
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
90
+ var own = (event) => event.stopPropagation();
91
+ function newerKindValue(field, value, opts = {}) {
92
+ switch (newerKindOf(field)) {
93
+ case "duration": {
94
+ const words = durationText(value);
95
+ return words === null ? void 0 : /* @__PURE__ */ jsx("span", { className: "tabular-nums", "data-records-duration": "", children: words });
96
+ }
97
+ case "created_by":
98
+ case "modified_by":
99
+ return isPersonStamp(value) ? /* @__PURE__ */ jsx(PersonStampChip, { name: value.name }) : void 0;
100
+ case "address":
101
+ return isAddressValue(value) || typeof value === "string" ? /* @__PURE__ */ jsx(AddressLink, { value }) : void 0;
102
+ case "barcode":
103
+ return typeof value === "string" ? /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-1 font-mono text-xs", "data-records-barcode": "", title: value, children: [
104
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "\u25AE\u25AF\u25AE" }),
105
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: value })
106
+ ] }) : void 0;
107
+ case "rich_text":
108
+ return typeof value === "string" ? /* @__PURE__ */ jsx(SafeMarkdown, { text: value, clipped: opts.inCell === true }) : void 0;
109
+ case "count":
110
+ return typeof value === "number" ? /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: value.toLocaleString() }) : void 0;
111
+ default:
112
+ return void 0;
113
+ }
114
+ }
115
+ function PersonStampChip({ name }) {
116
+ const words = name && name.trim() !== "" ? name : NOT_A_MEMBER_WORDS;
117
+ const initial = name && name.trim() !== "" ? name.trim()[0].toUpperCase() : "?";
118
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-1.5", "data-records-person-stamp": "", children: [
119
+ /* @__PURE__ */ jsx(
120
+ "span",
121
+ {
122
+ "aria-hidden": "true",
123
+ className: "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-medium text-foreground",
124
+ children: initial
125
+ }
126
+ ),
127
+ /* @__PURE__ */ jsx("span", { className: cn("truncate", name ? "" : "italic text-muted-foreground"), children: words })
128
+ ] });
129
+ }
130
+ function AddressLink({ value }) {
131
+ const words = addressText(value);
132
+ const href = addressMapUrl(value);
133
+ if (!words || !href) return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" });
134
+ return /* @__PURE__ */ jsxs(
135
+ "a",
136
+ {
137
+ href,
138
+ target: "_blank",
139
+ rel: "noopener noreferrer",
140
+ className: "inline-flex min-w-0 items-center gap-1 text-primary hover:underline",
141
+ title: "Open in maps",
142
+ "data-records-address": "",
143
+ onClick: own,
144
+ children: [
145
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2316" }),
146
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: words })
147
+ ]
148
+ }
149
+ );
150
+ }
151
+ var STATUS_GROUP_MARK = { todo: "\u25CB", in_progress: "\u25D0", done: "\u25CF" };
152
+ function statusMarkOf(field, value) {
153
+ if (newerKindOf(field) !== "status" || typeof value !== "string") return null;
154
+ const group = statusGroupOf(field, value);
155
+ return { mark: STATUS_GROUP_MARK[group], group, words: STATUS_GROUP_WORDS[group] };
156
+ }
157
+ var SAFE_HREF = /^(https?:\/\/|mailto:)/i;
158
+ function inline(text, key) {
159
+ const out = [];
160
+ let at = 0;
161
+ let n = 0;
162
+ for (const r of findInlineCodeSpans(text)) {
163
+ if (r.start < at) continue;
164
+ if (r.start > at) out.push(...marks(text.slice(at, r.start), `${key}-m${n++}`));
165
+ out.push(/* @__PURE__ */ jsx("code", { className: "rounded bg-muted px-1 font-mono text-[0.9em]", children: codeSpanText(text.slice(r.start, r.end)) }, `${key}-c${n++}`));
166
+ at = r.end;
167
+ }
168
+ if (at < text.length) out.push(...marks(text.slice(at), `${key}-m${n++}`));
169
+ return out;
170
+ }
171
+ function marks(text, key) {
172
+ const out = [];
173
+ const re = /(\*\*[^*]+\*\*|__[^_]+__|\[[^\]]+\]\([^)\s]+\)|\*[^*\s][^*]*\*|_[^_\s][^_]*_)/g;
174
+ let last = 0;
175
+ let i = 0;
176
+ for (const m of text.matchAll(re)) {
177
+ const at = m.index ?? 0;
178
+ if (at > last) out.push(text.slice(last, at));
179
+ const tok = m[0];
180
+ const k = `${key}-${i++}`;
181
+ if (tok.startsWith("**") || tok.startsWith("__")) out.push(/* @__PURE__ */ jsx("strong", { children: tok.slice(2, -2) }, k));
182
+ else if (tok.startsWith("[")) {
183
+ const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(tok);
184
+ const href = link?.[2] ?? "";
185
+ out.push(
186
+ SAFE_HREF.test(href) ? /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: "noopener noreferrer", className: "text-primary underline-offset-2 hover:underline", onClick: own, children: link?.[1] }, k) : link?.[1] ?? tok
187
+ );
188
+ } else out.push(/* @__PURE__ */ jsx("em", { children: tok.slice(1, -1) }, k));
189
+ last = at + tok.length;
190
+ }
191
+ if (last < text.length) out.push(text.slice(last));
192
+ return out;
193
+ }
194
+ function SafeMarkdown({ text, clipped }) {
195
+ const blocks = [];
196
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
197
+ let list = null;
198
+ let para = [];
199
+ const flushPara = () => {
200
+ if (para.length > 0) {
201
+ blocks.push(/* @__PURE__ */ jsx("p", { children: inline(para.join(" "), `p${blocks.length}`) }, `p${blocks.length}`));
202
+ para = [];
203
+ }
204
+ };
205
+ const flushList = () => {
206
+ if (list) {
207
+ const items = list.items.map((item, i) => /* @__PURE__ */ jsx("li", { children: inline(item, `l${blocks.length}-${i}`) }, i));
208
+ blocks.push(
209
+ list.ordered ? /* @__PURE__ */ jsx("ol", { className: "ml-4 list-decimal", children: items }, `o${blocks.length}`) : /* @__PURE__ */ jsx("ul", { className: "ml-4 list-disc", children: items }, `u${blocks.length}`)
210
+ );
211
+ list = null;
212
+ }
213
+ };
214
+ for (const raw of lines) {
215
+ const line = raw.trimEnd();
216
+ const bullet = /^\s*[-*+]\s+(.*)$/.exec(line);
217
+ const numbered = /^\s*\d+[.)]\s+(.*)$/.exec(line);
218
+ const heading = /^(#{1,3})\s+(.*)$/.exec(line);
219
+ if (bullet || numbered) {
220
+ flushPara();
221
+ const ordered = Boolean(numbered);
222
+ if (list && list.ordered !== ordered) flushList();
223
+ if (!list) list = { ordered, items: [] };
224
+ list.items.push((bullet ?? numbered)[1]);
225
+ } else if (heading) {
226
+ flushPara();
227
+ flushList();
228
+ blocks.push(/* @__PURE__ */ jsx("p", { className: "font-semibold", children: inline(heading[2], `h${blocks.length}`) }, `h${blocks.length}`));
229
+ } else if (line.trim() === "") {
230
+ flushPara();
231
+ flushList();
232
+ } else {
233
+ flushList();
234
+ para.push(line.trim());
235
+ }
236
+ }
237
+ flushPara();
238
+ flushList();
239
+ return /* @__PURE__ */ jsx(
240
+ "span",
241
+ {
242
+ className: cn("block min-w-0 space-y-1 whitespace-normal text-sm", clipped ? "max-h-[4.5em] overflow-hidden" : ""),
243
+ "data-records-rich-text": "",
244
+ children: blocks
245
+ }
246
+ );
247
+ }
248
+ function newerKindControl(props) {
249
+ switch (newerKindOf(props.field)) {
250
+ case "duration":
251
+ return /* @__PURE__ */ jsx(DurationControl, { ...props });
252
+ case "address":
253
+ return /* @__PURE__ */ jsx(AddressControl, { ...props });
254
+ case "barcode":
255
+ return /* @__PURE__ */ jsx(BarcodeControl, { ...props });
256
+ case "created_by":
257
+ case "modified_by":
258
+ case "count":
259
+ return /* @__PURE__ */ jsx("div", { id: props.id, className: "rounded border border-dashed px-2 py-1.5 text-xs text-muted-foreground", children: props.value === null || props.value === void 0 ? "\u2014" : newerKindValue(props.field, props.value) });
260
+ default:
261
+ return void 0;
262
+ }
263
+ }
264
+ function DurationControl({ value, onChange, disabled, id }) {
265
+ const [text, setText] = useState(() => durationText(value) ?? "");
266
+ const [wrong, setWrong] = useState(false);
267
+ useEffect(() => {
268
+ setText(durationText(value) ?? "");
269
+ }, [value]);
270
+ return /* @__PURE__ */ jsxs("span", { className: "flex flex-col gap-1", children: [
271
+ /* @__PURE__ */ jsx(
272
+ BasicInput,
273
+ {
274
+ id,
275
+ value: text,
276
+ disabled,
277
+ inputMode: "numeric",
278
+ placeholder: "45 min",
279
+ "aria-invalid": wrong || void 0,
280
+ className: "tabular-nums",
281
+ onChange: (e) => {
282
+ setText(e.target.value);
283
+ if (e.target.value.trim() === "") {
284
+ setWrong(false);
285
+ onChange(null);
286
+ return;
287
+ }
288
+ const seconds = readTypedDuration(e.target.value);
289
+ setWrong(seconds === null);
290
+ if (seconds !== null) onChange(seconds);
291
+ },
292
+ onBlur: () => {
293
+ if (!wrong) setText(durationText(value) ?? "");
294
+ }
295
+ }
296
+ ),
297
+ wrong ? /* @__PURE__ */ jsx("span", { className: "text-[11px] text-destructive", children: "Write it like 45 min, 1:30 or 1h 30m." }) : null
298
+ ] });
299
+ }
300
+ var ADDRESS_LABELS = {
301
+ street: "Street",
302
+ city: "City",
303
+ region: "State or region",
304
+ postal_code: "Postal code",
305
+ country: "Country"
306
+ };
307
+ var ADDRESS_EXAMPLES = {
308
+ street: "1420 Cedar Ridge Pkwy",
309
+ city: "Boulder",
310
+ region: "CO",
311
+ postal_code: "80301",
312
+ country: "US"
313
+ };
314
+ function AddressControl({ value, onChange, disabled, id }) {
315
+ const held = isAddressValue(value) ? value : typeof value === "string" && value.trim() !== "" ? { street: value } : {};
316
+ const set = (part, words) => {
317
+ const next = { ...held, [part]: words };
318
+ const kept = Object.fromEntries(Object.entries(next).filter(([, v]) => typeof v === "string" && v.trim() !== ""));
319
+ onChange(Object.keys(kept).length === 0 ? null : kept);
320
+ };
321
+ return /* @__PURE__ */ jsx("span", { id, className: "grid grid-cols-2 gap-1.5", "data-records-address-control": "", children: ADDRESS_PARTS.map((part) => /* @__PURE__ */ jsx(
322
+ BasicInput,
323
+ {
324
+ "aria-label": ADDRESS_LABELS[part],
325
+ placeholder: ADDRESS_EXAMPLES[part],
326
+ className: part === "street" ? "col-span-2" : "",
327
+ value: typeof held[part] === "string" ? held[part] : "",
328
+ disabled,
329
+ ...part === "street" ? { autoComplete: "street-address" } : {},
330
+ onChange: (e) => set(part, e.target.value)
331
+ },
332
+ part
333
+ )) });
334
+ }
335
+ function barcodeDetector() {
336
+ const ctor = globalThis.BarcodeDetector;
337
+ return typeof ctor === "function" ? ctor : null;
338
+ }
339
+ function BarcodeControl({ value, onChange, disabled, id }) {
340
+ const file = useRef(null);
341
+ const [said, setSaid] = useState(null);
342
+ const Detect = barcodeDetector();
343
+ return /* @__PURE__ */ jsxs("span", { className: "flex flex-col gap-1", children: [
344
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
345
+ /* @__PURE__ */ jsx(
346
+ BasicInput,
347
+ {
348
+ id,
349
+ className: "font-mono",
350
+ value: typeof value === "string" ? value : "",
351
+ disabled,
352
+ placeholder: "4006381333931",
353
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
354
+ }
355
+ ),
356
+ Detect && !disabled ? /* @__PURE__ */ jsxs(Fragment, { children: [
357
+ /* @__PURE__ */ jsx(
358
+ "button",
359
+ {
360
+ type: "button",
361
+ className: "shrink-0 rounded-md border px-2 py-1 text-xs hover:bg-accent",
362
+ onClick: () => file.current?.click(),
363
+ children: "Scan"
364
+ }
365
+ ),
366
+ /* @__PURE__ */ jsx(
367
+ "input",
368
+ {
369
+ ref: file,
370
+ type: "file",
371
+ accept: "image/*",
372
+ capture: "environment",
373
+ className: "hidden",
374
+ onChange: async (e) => {
375
+ const photo = e.target.files?.[0];
376
+ e.target.value = "";
377
+ if (!photo) return;
378
+ try {
379
+ const found = await new Detect().detect(await createImageBitmap(photo));
380
+ if (found[0]?.rawValue) {
381
+ onChange(found[0].rawValue);
382
+ setSaid(null);
383
+ } else setSaid("No barcode in that photo. Try closer, in good light.");
384
+ } catch {
385
+ setSaid("This phone could not read that photo. Type the code instead.");
386
+ }
387
+ }
388
+ }
389
+ )
390
+ ] }) : null
391
+ ] }),
392
+ said ? /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: said }) : null
393
+ ] });
394
+ }
395
+
396
+ // src/parity.ts
397
+ import {
398
+ KERNEL_TABLES,
399
+ PARITY_FIELD_TYPES,
400
+ fieldKindFor,
401
+ newerKindOf as newerKindOf2
402
+ } from "@ai-matrx/records";
403
+
404
+ // src/names.ts
405
+ import { isUuidShape } from "@ai-matrx/kit/uuid";
406
+ function humanize(token) {
407
+ const words = token.replace(/[_\-.]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
408
+ if (words === "") return token;
409
+ return words.charAt(0).toUpperCase() + words.slice(1);
410
+ }
411
+ function fieldName(field) {
412
+ const label = (field.label ?? "").trim();
413
+ if (label !== "" && label === field.key && /^[a-z0-9]+(?:[_.-][a-z0-9]+)+$/.test(label)) return humanize(label);
414
+ if (label !== "") return label;
415
+ const name = (field.name ?? "").trim();
416
+ if (name !== "") return name;
417
+ return humanize(field.key);
418
+ }
419
+ function recordName(document2, titleKey, fallback = "Untitled") {
420
+ const data = document2 ?? {};
421
+ const keys = [titleKey, "title", "name", "label", "full_name", "subject", "summary"].filter(
422
+ (key) => typeof key === "string" && key !== ""
423
+ );
424
+ for (const key of keys) {
425
+ const value = data[key];
426
+ if (typeof value === "string" && value.trim() !== "" && !looksLikeId(value.trim())) {
427
+ return value.trim();
428
+ }
429
+ if (typeof value === "number") return String(value);
430
+ }
431
+ return fallback;
432
+ }
433
+ function looksLikeId(value) {
434
+ return typeof value === "string" && isUuidShape(value.trim());
435
+ }
436
+ function rowNameIn(table, row, fallback = "Untitled") {
437
+ return recordName(row?.document, table?.title_field ?? null, fallback);
438
+ }
439
+ var WITHHELD_RECORD_LABEL = "A record you have not been given access to";
440
+
441
+ // src/docs.ts
442
+ function isSignatureField(field) {
443
+ return field.type === "text" && (field.format ?? "").toLowerCase() === "signature";
444
+ }
445
+
446
+ // src/parity.ts
447
+ var kernelId = (name) => KERNEL_TABLES.find((t) => t.name === name)?.id ?? "";
448
+ var KERNEL_FILE_TABLE = kernelId("File");
449
+ var KERNEL_PERSON_TABLE = kernelId("Person");
450
+ var PARITY_MADE_OF = Object.fromEntries(
451
+ PARITY_FIELD_TYPES.map((p) => [p.parity_type, p.made_of])
452
+ );
453
+ var KERNEL = { file: KERNEL_FILE_TABLE, person: KERNEL_PERSON_TABLE };
454
+ function editorKindFor(field) {
455
+ return fieldKindFor(field, KERNEL);
456
+ }
457
+
458
+ // src/RelationPicker.tsx
459
+ import { useEffect as useEffect3, useMemo, useState as useState3 } from "react";
460
+ import { rankTypedMatches } from "@ai-matrx/records";
461
+ import { useOptionalRecordsClient } from "@ai-matrx/records/react";
462
+ import { BasicInput as BasicInput3, Button as Button2, cn as cn4 } from "@ai-matrx/design-system";
463
+
464
+ // src/PickOrAdd.tsx
465
+ import { useEffect as useEffect2, useState as useState2 } from "react";
466
+ import { BasicInput as BasicInput2, Button, Popover, PopoverContent, PopoverTrigger, ScrollArea, cn as cn2 } from "@ai-matrx/design-system";
467
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
468
+ function useListKeys(args) {
469
+ const { count, resetOn, onPick, onNothing, onClose } = args;
470
+ const [highlight, setHighlight] = useState2(0);
471
+ useEffect2(() => setHighlight(0), [resetOn]);
472
+ const at = count === 0 ? -1 : Math.min(highlight, count - 1);
473
+ const onKeyDown = (event) => {
474
+ event.stopPropagation();
475
+ if (event.key === "ArrowDown") {
476
+ event.preventDefault();
477
+ if (count > 0) setHighlight((h) => (Math.min(h, count - 1) + 1) % count);
478
+ } else if (event.key === "ArrowUp") {
479
+ event.preventDefault();
480
+ if (count > 0) setHighlight((h) => (Math.min(h, count - 1) - 1 + count) % count);
481
+ } else if (event.key === "Enter") {
482
+ event.preventDefault();
483
+ if (at >= 0) onPick(at);
484
+ else onNothing?.();
485
+ } else if (event.key === "Escape") {
486
+ event.preventDefault();
487
+ onClose();
488
+ }
489
+ };
490
+ return { highlight: at, onKeyDown };
491
+ }
492
+ function PickOrAdd({
493
+ triggerLabel,
494
+ triggerText,
495
+ triggerId,
496
+ open,
497
+ onOpenChange,
498
+ search,
499
+ onSearch,
500
+ items,
501
+ onPick,
502
+ notice,
503
+ empty,
504
+ add,
505
+ foot,
506
+ footNote,
507
+ onEnterWithNoMatch
508
+ }) {
509
+ const hasFoot = Boolean(add) || Boolean(foot) || Boolean(footNote);
510
+ const keys = useListKeys({
511
+ count: items.length,
512
+ resetOn: search,
513
+ onPick: (index) => {
514
+ const item = items[index];
515
+ if (item) onPick(item.id);
516
+ },
517
+ onNothing: onEnterWithNoMatch ?? (add && !add.disabled ? add.onAdd : void 0),
518
+ onClose: () => onOpenChange(false)
519
+ });
520
+ return /* @__PURE__ */ jsxs2(Popover, { open, onOpenChange, children: [
521
+ /* @__PURE__ */ jsx2(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx2(Button, { id: triggerId, type: "button", size: "sm", variant: "outline", "aria-label": triggerLabel, children: triggerText }) }),
522
+ /* @__PURE__ */ jsxs2(
523
+ PopoverContent,
524
+ {
525
+ sizing: "content",
526
+ className: "p-1",
527
+ align: "start",
528
+ "data-matrx-cell-editor": "",
529
+ onKeyDown: (event) => event.stopPropagation(),
530
+ children: [
531
+ /* @__PURE__ */ jsx2(
532
+ BasicInput2,
533
+ {
534
+ value: search,
535
+ onChange: (e) => onSearch(e.target.value),
536
+ onKeyDown: keys.onKeyDown,
537
+ placeholder: "Search",
538
+ "aria-label": `Search \u2014 ${triggerLabel}`,
539
+ autoFocus: true,
540
+ autoComplete: "off",
541
+ className: "mb-1 h-7 text-xs"
542
+ }
543
+ ),
544
+ notice,
545
+ /* @__PURE__ */ jsxs2(ScrollArea, { className: "max-h-56", children: [
546
+ /* @__PURE__ */ jsx2("ul", { role: "listbox", "aria-label": triggerLabel, children: items.map((item, index) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsxs2(
547
+ "button",
548
+ {
549
+ type: "button",
550
+ onClick: () => onPick(item.id),
551
+ title: item.title ?? item.label,
552
+ ...item.attrs ?? {},
553
+ "data-pick-highlighted": index === keys.highlight ? "" : void 0,
554
+ className: cn2(
555
+ "w-full truncate rounded px-2 py-1 text-left text-xs hover:bg-muted",
556
+ item.picked ? "bg-accent text-accent-foreground" : "",
557
+ index === keys.highlight ? "ring-1 ring-ring" : ""
558
+ ),
559
+ children: [
560
+ item.chip ? /* @__PURE__ */ jsx2(
561
+ "span",
562
+ {
563
+ className: cn2("inline-flex max-w-full truncate rounded-md border px-1.5 py-0.5 text-[11px] font-medium", item.chip),
564
+ "data-records-choice-chip": "",
565
+ children: item.label
566
+ }
567
+ ) : item.label,
568
+ item.detail ? /* @__PURE__ */ jsx2("span", { className: "block truncate text-[11px] text-muted-foreground", "data-tell-apart": "", children: item.detail }) : null
569
+ ]
570
+ }
571
+ ) }, item.id)) }),
572
+ items.length === 0 && empty ? empty : null
573
+ ] }),
574
+ hasFoot ? /* @__PURE__ */ jsxs2("div", { className: "mt-1 border-t pt-1", children: [
575
+ foot ?? (add ? /* @__PURE__ */ jsx2(
576
+ Button,
577
+ {
578
+ type: "button",
579
+ size: "sm",
580
+ variant: "ghost",
581
+ title: add.label,
582
+ className: "w-full justify-start text-xs",
583
+ disabled: add.disabled ?? false,
584
+ onClick: add.onAdd,
585
+ children: /* @__PURE__ */ jsx2("span", { className: "block w-full truncate text-left", children: add.label })
586
+ }
587
+ ) : null),
588
+ footNote
589
+ ] }) : null
590
+ ]
591
+ }
592
+ )
593
+ ] });
594
+ }
595
+
596
+ // src/Refusal.tsx
597
+ import { Alert, AlertDescription, AlertTitle, ErrorBox, cn as cn3 } from "@ai-matrx/design-system";
598
+
599
+ // src/plainWords.ts
600
+ var MACHINE_IDENTITY = [
601
+ /\b[A-Z]{2,4}-\d{1,3}\b/,
602
+ // A build lane's tag: capital words joined by hyphens, then a colon ("STORE-TAILS-3:",
603
+ // "MOVE-AND-OUTSIDER:"). A person never reads which lane wrote a sentence.
604
+ /\b[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+:/,
605
+ /\b(?:custom|platform|iam|history|public|auth|campaign_watch)\.[a-z_]+/,
606
+ /\b(?:config|data|document|spec|patch)\.[a-z_]+/,
607
+ /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i,
608
+ /\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/,
609
+ // Five characters of digits and capitals, at least one a digit — so `23514`
610
+ // and `PT409` match and an emphatic `NEVER` does not.
611
+ /\b(?=[0-9A-Z]{5}\b)(?=[0-9A-Z]*\d)[0-9A-Z]{5}\b/
612
+ ];
613
+ function isMachineIdentity(text) {
614
+ return MACHINE_IDENTITY.some((shape) => shape.test(text));
615
+ }
616
+ var TITLE = {
617
+ stale_write: "Someone else wrote first",
618
+ door: "You do not have access to this",
619
+ door_absent: "That is not built yet",
620
+ refused_by_rule: "That value was not accepted",
621
+ invalid_argument: "That was not accepted",
622
+ not_found: "Not there any more",
623
+ missing_reference: "That points at something that is not there",
624
+ already_exists: "That already exists",
625
+ not_supported: "That cannot be done here",
626
+ store_limit: "This has got as big as it can",
627
+ store_not_served: "This organization does not keep its data here",
628
+ unreachable: "We could not reach your data",
629
+ needs_approval: "This is waiting on someone's approval",
630
+ stale_read: "Your view of this is a moment behind",
631
+ timed_out: "That took too long to answer",
632
+ internal: "Something went wrong at our end"
633
+ };
634
+ var REMEDY = {
635
+ stale_write: "Look at what they wrote. Keep theirs, or try again and yours goes on top.",
636
+ door: "Ask somebody who administers this to share it with you at the level you need.",
637
+ door_absent: "There is nothing to do here yet. It is on our list, not yours.",
638
+ refused_by_rule: "Change the value so it passes, then try again.",
639
+ invalid_argument: "Change what you entered, then try again.",
640
+ not_found: "It may have been deleted. Go back to the list and open it again from there.",
641
+ missing_reference: "Pick something that is still there.",
642
+ already_exists: "Use a different name.",
643
+ not_supported: "There is another way to do this \u2014 ask, and we will point you at it.",
644
+ store_limit: "Split it up, or ask us to raise the ceiling for your organization.",
645
+ store_not_served: "An administrator of this organization turns the record store on from the data settings screen.",
646
+ // 🚨 NOTHING WAS WRITTEN AND NOTHING WAS REFUSED (lane TAILS-6). The call
647
+ // never arrived, so "try again" is the honest instruction rather than a
648
+ // consolation — and saying nothing changed is the half a person actually
649
+ // worries about.
650
+ unreachable: "Nothing was changed. Check your connection and open this again.",
651
+ needs_approval: "Nothing was changed yet. The people who can approve it have been asked.",
652
+ stale_read: "Open this again in a moment and it will be up to date.",
653
+ timed_out: "Nothing was changed. Try a smaller page, or try again in a moment.",
654
+ internal: "Nothing you did caused this. Try again, and tell us if it keeps happening."
655
+ };
656
+ var ENGINEER_TEXT = [
657
+ /@ai-matrx\//,
658
+ /\bSQLSTATE\b/i,
659
+ /\bPostgREST\b/i,
660
+ /\bIndexedDB\b/i,
661
+ // `<RecordsUiProvider>` — a component, on a screen.
662
+ /<\/?[A-Z][A-Za-z0-9]*\s*\/?>/,
663
+ // `savedViews`, `openRecords`, `onReask` — an identifier, however it is
664
+ // quoted. English does not write camelCase; code does.
665
+ /\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b/,
666
+ // A backticked token is a thing you type into an editor, not a thing you do.
667
+ /`[A-Za-z_][A-Za-z0-9_.]*`/,
668
+ // "bind X on Y", "pass X", "import X" — an instruction to whoever maintains
669
+ // the app, arriving on the screen of somebody who cannot act on it.
670
+ /\bbind\s+[`'"A-Za-z]/i
671
+ ];
672
+ function isDeveloperHint(hint) {
673
+ const text = hint.trim();
674
+ if (text === "") return false;
675
+ if (text.includes("```")) return true;
676
+ if (/\n\s+at\s+\S+/.test(text)) return true;
677
+ if (text.length > 600) return true;
678
+ return isEngineerText(text);
679
+ }
680
+ function isEngineerText(text) {
681
+ return ENGINEER_TEXT.some((shape) => shape.test(text));
682
+ }
683
+ function withoutMachineClauses(sentence) {
684
+ const dropped = [];
685
+ const footnotes = [];
686
+ const withoutFootnotes = sentence.replace(/\s*\((?:see\s+)?[A-Z]{2,5}-[A-Z0-9-]{1,8}\)/g, (hit) => {
687
+ footnotes.push(hit.trim());
688
+ return "";
689
+ });
690
+ dropped.push(...footnotes);
691
+ const clauses = withoutFootnotes.split(/(?:,\s+so\s+|\s+—\s+|\s+--\s+|\s*\|\s*)/).map((clause) => clause.trim()).filter((clause) => clause !== "");
692
+ const kept = clauses.filter((clause) => {
693
+ if (!isMachineIdentity(clause)) return true;
694
+ dropped.push(clause);
695
+ return false;
696
+ });
697
+ if (kept.length === 0) return { kept: null, dropped };
698
+ if (dropped.length === 0) return { kept: sentence.trim(), dropped };
699
+ let joined = kept.join(". ").replace(/\.\.+/g, ".").trim();
700
+ if (!/[.!?]$/.test(joined)) joined = `${joined}.`;
701
+ return { kept: joined, dropped };
702
+ }
703
+ function peopleSentence(message) {
704
+ const [lead = "", ...tail] = message.split(/\s+—\s+|\s+--\s+/);
705
+ const sentences = lead.trim().split(/(?<=(?<!\b(?:Dr|Mr|Mrs|Ms|St|vs|No|e\.g|i\.e|etc|[A-Z]))[.!?])\s+/);
706
+ const first = sentences[0] ?? "";
707
+ const rest = [...sentences.slice(1), ...tail].map((part) => part.trim()).filter((part) => part !== "");
708
+ const cleaned = plainSentence(first);
709
+ return { kept: cleaned.kept, dropped: [...cleaned.dropped, ...rest] };
710
+ }
711
+ function refusalSentence(error) {
712
+ return refusalForAPerson(error).sentence;
713
+ }
714
+ function plainSentence(message) {
715
+ const dropped = [];
716
+ const kept = [];
717
+ for (const raw of message.split(/(?<=[.!?])\s+/)) {
718
+ const sentence = raw.trim();
719
+ if (sentence === "") continue;
720
+ const cleaned = withoutMachineClauses(sentence);
721
+ dropped.push(...cleaned.dropped);
722
+ if (cleaned.kept) kept.push(cleaned.kept);
723
+ }
724
+ return { kept: kept.length === 0 ? null : kept.join(" "), dropped };
725
+ }
726
+ function exampleOf(error) {
727
+ const detail = error.detail;
728
+ if (!detail || typeof detail !== "object") return null;
729
+ const example = detail.example;
730
+ return typeof example === "string" && example.trim() !== "" ? example.trim() : null;
731
+ }
732
+ function refusalForAPerson(error) {
733
+ const code = error.code ?? "internal";
734
+ const ours = error.ours === true;
735
+ const hint = plainSentence(String(error.hint ?? ""));
736
+ const message = ours ? plainSentence(String(error.message ?? "")) : peopleSentence(String(error.message ?? ""));
737
+ const title = TITLE[code] ?? "The store refused this";
738
+ const hintIsForEngineers = isDeveloperHint(String(error.hint ?? ""));
739
+ const example = exampleOf(error);
740
+ const remedy = (example ? `Enter it like ${example}.` : null) ?? (hintIsForEngineers ? null : hint.kept) ?? REMEDY[code] ?? REMEDY["internal"];
741
+ const messageIsForEngineers = isEngineerText(String(error.message ?? ""));
742
+ const said = (messageIsForEngineers ? "" : message.kept ?? "").trim();
743
+ const forEngineers = [
744
+ error.sqlstate ? `SQLSTATE ${error.sqlstate}.` : null,
745
+ // The client's own note about a class it did not recognise, or a call that
746
+ // never reached the store. It is rendered out of sight, never as a sentence.
747
+ error.diagnostic ? error.diagnostic : null,
748
+ messageIsForEngineers ? `Message: ${error.message}` : null,
749
+ error.hint && (hintIsForEngineers || hint.kept !== error.hint) ? `Hint: ${error.hint}` : null,
750
+ message.dropped.length > 0 || hint.dropped.length > 0 ? `Dropped from the person's view: ${[...message.dropped, ...hint.dropped].join(" | ")}` : null
751
+ ].filter(Boolean).join(" ");
752
+ return {
753
+ title,
754
+ // Nothing invented. When every clause was machine identity the title says
755
+ // what happened and the remedy says what to do; a made-up diagnosis would
756
+ // be worse than the identifier it replaced.
757
+ sentence: said === "" ? `${title}.` : said,
758
+ remedy,
759
+ storeSaid: said !== "",
760
+ forEngineers
761
+ };
762
+ }
763
+ function refusalLineForAPerson(error) {
764
+ const plain = refusalForAPerson(error);
765
+ const sentence = plain.sentence.trim();
766
+ const ended = sentence === "" || /[.!?…:]$/.test(sentence) ? sentence : `${sentence}.`;
767
+ return `${ended} ${plain.remedy}`.trim();
768
+ }
769
+
770
+ // src/Refusal.tsx
771
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
772
+ function RefusalNotice({
773
+ error,
774
+ className,
775
+ actions,
776
+ onKeepEditing,
777
+ onDiscard
778
+ }) {
779
+ const plain = refusalForAPerson(error);
780
+ const doors = onKeepEditing || onDiscard ? /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-1 pt-0.5", "data-refusal-doors": "", children: [
781
+ onKeepEditing ? /* @__PURE__ */ jsx3(
782
+ "button",
783
+ {
784
+ type: "button",
785
+ "data-refusal-keep-editing": "",
786
+ className: "rounded border px-2 py-0.5 text-xs hover:bg-muted",
787
+ onClick: onKeepEditing,
788
+ children: "Keep editing"
789
+ }
790
+ ) : null,
791
+ onDiscard ? /* @__PURE__ */ jsx3(
792
+ "button",
793
+ {
794
+ type: "button",
795
+ "data-refusal-discard": "",
796
+ className: "rounded border px-2 py-0.5 text-xs hover:bg-muted",
797
+ onClick: onDiscard,
798
+ children: "Discard"
799
+ }
800
+ ) : null
801
+ ] }) : null;
802
+ return /* @__PURE__ */ jsxs3(
803
+ Alert,
804
+ {
805
+ variant: "destructive",
806
+ role: "alert",
807
+ "data-refusal-notice": "",
808
+ className: cn3("text-xs", className),
809
+ errorFacts: { message: plain.sentence, title: plain.title, error, origin: "records-ui/RefusalNotice" },
810
+ ...plain.forEngineers ? { "data-for-engineers": plain.forEngineers } : {},
811
+ ...error.sqlstate ? { "data-sqlstate": error.sqlstate } : {},
812
+ children: [
813
+ /* @__PURE__ */ jsx3(AlertTitle, { className: "text-xs font-medium", children: plain.storeSaid ? plain.sentence : plain.title }),
814
+ /* @__PURE__ */ jsxs3(AlertDescription, { className: "space-y-1 text-xs", children: [
815
+ plain.storeSaid || plain.sentence.replace(/\.$/, "") === plain.title ? null : /* @__PURE__ */ jsx3("p", { children: plain.sentence }),
816
+ /* @__PURE__ */ jsx3("p", { className: "opacity-80", children: plain.remedy }),
817
+ actions,
818
+ doors
819
+ ] })
820
+ ]
821
+ }
822
+ );
823
+ }
824
+ function RefusalLine({ error, className }) {
825
+ const plain = refusalForAPerson(error);
826
+ return /* @__PURE__ */ jsx3(
827
+ ErrorBox,
828
+ {
829
+ "data-refusal-line": "",
830
+ className: cn3("text-xs text-destructive", className),
831
+ message: refusalLineForAPerson(error),
832
+ error,
833
+ origin: "records-ui/RefusalLine",
834
+ ...plain.forEngineers ? { "data-for-engineers": plain.forEngineers } : {}
835
+ }
836
+ );
837
+ }
838
+
839
+ // src/refusals.ts
840
+ function refusal(code, sentence, remedy, diagnostic) {
841
+ return {
842
+ code,
843
+ message: sentence,
844
+ ours: true,
845
+ ...remedy ? { hint: remedy } : {},
846
+ ...diagnostic ? { diagnostic } : {}
847
+ };
848
+ }
849
+ function refusalFromThrown(thrown, sentence, remedy, code = "internal") {
850
+ const said = thrown instanceof Error ? thrown.message : String(thrown ?? "");
851
+ return refusal(code, sentence, remedy, said ? `Thrown: ${said}` : null);
852
+ }
853
+
854
+ // src/tellApart.ts
855
+ function shown(value) {
856
+ if (value === null || value === void 0) return null;
857
+ if (typeof value === "string") return value.trim() === "" ? null : value.trim();
858
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
859
+ return null;
860
+ }
861
+ function tellApart(rows, nameOf) {
862
+ const byName = /* @__PURE__ */ new Map();
863
+ for (const row of rows) {
864
+ const name = nameOf(row.id).trim().toLowerCase();
865
+ byName.set(name, [...byName.get(name) ?? [], row]);
866
+ }
867
+ const out = /* @__PURE__ */ new Map();
868
+ for (const [name, group] of byName) {
869
+ if (group.length < 2) continue;
870
+ const keys = [
871
+ ...new Set(
872
+ group.flatMap(
873
+ (row) => Object.keys(row.document ?? {}).filter((k) => !k.startsWith("_") && shown(row.document?.[k]) !== null)
874
+ )
875
+ )
876
+ ].filter((k) => group.some((row) => (shown(row.document?.[k]) ?? "").toLowerCase() !== name));
877
+ const valuesOf = (k) => group.map((row) => shown(row.document?.[k]) ?? "");
878
+ const differs = keys.filter((k) => new Set(valuesOf(k)).size > 1);
879
+ const whole = differs.find((k) => new Set(valuesOf(k)).size === group.length);
880
+ const chosen = whole ? [whole] : differs.slice(0, 2);
881
+ if (chosen.length === 0) continue;
882
+ for (const row of group) {
883
+ const words = chosen.map((k) => shown(row.document?.[k])).filter((w) => w !== null);
884
+ if (words.length > 0) out.set(row.id, words.join(" \xB7 "));
885
+ }
886
+ }
887
+ return out;
888
+ }
889
+
890
+ // src/RelationPicker.tsx
891
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
892
+ var NO_STORE_ON_THIS_SCREEN_REASON = "This question points at a record in somebody's own data, and a page answered without an account is never allowed to search that. Leave it blank \u2014 whoever receives your answer will fill it in.";
893
+ function RelationPicker({ field, value, onChange, disabled, id, className, allowCreate = true }) {
894
+ const client = useOptionalRecordsClient();
895
+ const [rows, setRows] = useState3(null);
896
+ const [words, setWords] = useState3({});
897
+ const [refusal2, setRefusal] = useState3(null);
898
+ const [search, setSearch] = useState3("");
899
+ const [open, setOpen] = useState3(false);
900
+ const [target, setTarget] = useState3(null);
901
+ const [targetRefusal, setTargetRefusal] = useState3(null);
902
+ const [creating, setCreating] = useState3(false);
903
+ const [newTitle, setNewTitle] = useState3("");
904
+ const [busy, setBusy] = useState3(false);
905
+ const [createRefusal, setCreateRefusal] = useState3(null);
906
+ const picked = useMemo(
907
+ () => Array.isArray(value) ? value.map(String) : value === null || value === void 0 ? [] : [String(value)],
908
+ [value]
909
+ );
910
+ useEffect3(() => {
911
+ const target2 = field.relation_target;
912
+ if (!target2 || !client) return;
913
+ let cancelled = false;
914
+ void client.list({ table_id: target2, limit: 200 }).then((result) => {
915
+ if (cancelled) return;
916
+ if (result.ok) setRows(result.data.rows);
917
+ else setRefusal(result.error);
918
+ });
919
+ return () => {
920
+ cancelled = true;
921
+ };
922
+ }, [client, field.relation_target]);
923
+ useEffect3(() => {
924
+ if (!rows || !client) return;
925
+ const wanted = [.../* @__PURE__ */ new Set([...rows.map((row) => row.id), ...picked])].filter(
926
+ (id2) => words[id2] === void 0
927
+ );
928
+ if (wanted.length === 0) return;
929
+ let cancelled = false;
930
+ void client.relationWordsMany({ field_id: field.id, record_ids: wanted }).then((result) => {
931
+ if (cancelled) return;
932
+ const learned = {};
933
+ for (const id2 of wanted) {
934
+ const said = result.ok ? result.data[id2] : void 0;
935
+ learned[id2] = typeof said === "string" && said !== "" ? said : WITHHELD_RECORD_LABEL;
936
+ }
937
+ setWords((current) => ({ ...current, ...learned }));
938
+ });
939
+ return () => {
940
+ cancelled = true;
941
+ };
942
+ }, [client, field.id, rows, picked, words]);
943
+ useEffect3(() => {
944
+ if (!open || !allowCreate || target || targetRefusal || !client) return;
945
+ const id2 = field.relation_target;
946
+ if (!id2) return;
947
+ let cancelled = false;
948
+ void client.tableList().then((result) => {
949
+ if (cancelled) return;
950
+ if (!result.ok) {
951
+ setTargetRefusal(result.error);
952
+ return;
953
+ }
954
+ const found = result.data.find((t) => t.id === id2) ?? null;
955
+ if (found) setTarget(found);
956
+ else
957
+ setTargetRefusal(
958
+ refusal(
959
+ "door",
960
+ "That Table is not one you can open, so a new record cannot be added to it from here."
961
+ )
962
+ );
963
+ });
964
+ return () => {
965
+ cancelled = true;
966
+ };
967
+ }, [client, open, allowCreate, target, targetRefusal, field.relation_target]);
968
+ if (!client) {
969
+ return (
970
+ // The Field's own name is already printed above this control by
971
+ // `FieldLabel`, so the sentence does not say it a second time.
972
+ /* @__PURE__ */ jsx4("p", { id, className: cn4("text-xs text-muted-foreground", className), children: NO_STORE_ON_THIS_SCREEN_REASON })
973
+ );
974
+ }
975
+ if (!field.relation_target) {
976
+ return /* @__PURE__ */ jsxs4("p", { className: "text-xs text-muted-foreground", children: [
977
+ fieldName(field),
978
+ " points at records, but it does not say which Table yet, so there is nothing to pick from. A table admin sets its target in the field editor."
979
+ ] });
980
+ }
981
+ const titleOf = (recordId) => words[recordId] ?? "Loading\u2026";
982
+ const full = field.multi && field.relation_max !== null && picked.length >= field.relation_max;
983
+ const toggle = (recordId) => {
984
+ if (!field.multi) {
985
+ onChange(picked[0] === recordId ? null : recordId);
986
+ setOpen(false);
987
+ return;
988
+ }
989
+ const next = new Set(picked);
990
+ if (next.has(recordId)) next.delete(recordId);
991
+ else if (!full) next.add(recordId);
992
+ onChange(next.size === 0 ? null : [...next]);
993
+ };
994
+ const targetWord = (target?.label_singular ?? "").trim() || (target?.name ?? "").trim() || "record";
995
+ const create = async (rawTitle) => {
996
+ const title = rawTitle.trim();
997
+ const tableId = field.relation_target;
998
+ if (!tableId || title === "" || busy) return;
999
+ if (!target) {
1000
+ setCreateRefusal(refusal("unreachable", "Still reading that Table.", "Try again in a moment."));
1001
+ return;
1002
+ }
1003
+ const titleField = target.title_field;
1004
+ if (!titleField) {
1005
+ setCreateRefusal(
1006
+ refusal(
1007
+ "not_supported",
1008
+ `${target.name} has no column that names a record yet, so a new one would have nothing to show here.`,
1009
+ "A table admin picks its title column in that table's settings."
1010
+ )
1011
+ );
1012
+ return;
1013
+ }
1014
+ setBusy(true);
1015
+ setCreateRefusal(null);
1016
+ const written = await client.recordWrite({ table_id: tableId, data: { [titleField]: title } });
1017
+ if (!written.ok) {
1018
+ setBusy(false);
1019
+ setCreateRefusal(written.error);
1020
+ return;
1021
+ }
1022
+ const createdId = String(written.data);
1023
+ const refreshed = await client.list({ table_id: tableId, limit: 200 });
1024
+ if (refreshed.ok) setRows(refreshed.data.rows);
1025
+ setBusy(false);
1026
+ setCreating(false);
1027
+ setNewTitle("");
1028
+ setSearch("");
1029
+ toggle(createdId);
1030
+ };
1031
+ const apart = tellApart(rows ?? [], titleOf);
1032
+ const visible = rankTypedMatches(rows ?? [], search, (row) => [titleOf(row.id), apart.get(row.id) ?? ""]);
1033
+ return (
1034
+ // Never wider than where it is drawn (a grid cell beside a sticky column: B3-07) — chips truncate, the button wraps.
1035
+ /* @__PURE__ */ jsxs4("div", { className: cn4("flex min-w-0 max-w-full flex-wrap items-center gap-1", className), children: [
1036
+ picked.map((recordId) => /* @__PURE__ */ jsx4(
1037
+ RecordChip,
1038
+ {
1039
+ title: titleOf(recordId),
1040
+ ...disabled ? {} : { onRemove: () => toggle(recordId) }
1041
+ },
1042
+ recordId
1043
+ )),
1044
+ disabled ? null : (
1045
+ // THE ONE "PICK OR ADD" BUILDER (PickOrAdd.tsx) — the choice editor draws the same one.
1046
+ /* @__PURE__ */ jsx4(
1047
+ PickOrAdd,
1048
+ {
1049
+ triggerId: id,
1050
+ triggerLabel: `Pick for ${fieldName(field)}`,
1051
+ triggerText: picked.length === 0 ? "Pick" : "Change",
1052
+ open,
1053
+ onOpenChange: setOpen,
1054
+ search,
1055
+ onSearch: setSearch,
1056
+ items: visible.map((row) => ({
1057
+ id: row.id,
1058
+ label: titleOf(row.id),
1059
+ detail: apart.get(row.id) ?? null,
1060
+ picked: picked.includes(row.id),
1061
+ title: apart.has(row.id) ? `${titleOf(row.id)} \u2014 ${apart.get(row.id)}` : titleOf(row.id),
1062
+ attrs: { "data-relation-candidate": row.id }
1063
+ })),
1064
+ onPick: toggle,
1065
+ notice: /* @__PURE__ */ jsxs4(Fragment2, { children: [
1066
+ refusal2 ? /* @__PURE__ */ jsx4(RefusalLine, { error: refusal2, className: "p-2" }) : null,
1067
+ !rows && !refusal2 ? /* @__PURE__ */ jsx4("p", { className: "p-2 text-xs text-muted-foreground", children: "Reading\u2026" }) : null,
1068
+ full ? /* @__PURE__ */ jsxs4("p", { className: "p-2 text-xs text-muted-foreground", children: [
1069
+ fieldName(field),
1070
+ " can point at ",
1071
+ field.relation_max,
1072
+ " at most. Remove one to add another."
1073
+ ] }) : null
1074
+ ] }),
1075
+ empty: rows ? /* @__PURE__ */ jsx4("p", { className: "p-2 text-xs text-muted-foreground", children: "Nothing in that table matches." }) : null,
1076
+ add: allowCreate && !full && !targetRefusal && target && !creating ? {
1077
+ label: search.trim() !== "" ? `Create \u201C${search.trim()}\u201D` : `New ${targetWord}`,
1078
+ disabled: busy,
1079
+ // TYPED SOMETHING THAT IS NOT THERE? That IS the name. The champions
1080
+ // (Airtable, Notion) make the search text the new record's title in one
1081
+ // press; an empty box asks for one.
1082
+ onAdd: () => {
1083
+ if (search.trim() !== "") void create(search);
1084
+ else {
1085
+ setNewTitle("");
1086
+ setCreating(true);
1087
+ }
1088
+ }
1089
+ } : null,
1090
+ foot: !allowCreate || full ? void 0 : targetRefusal ? /* @__PURE__ */ jsx4(RefusalLine, { error: targetRefusal, className: "p-2" }) : !target ? /* @__PURE__ */ jsx4("p", { className: "p-2 text-xs text-muted-foreground", children: "Reading that table\u2026" }) : creating ? /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1 p-1", children: [
1091
+ /* @__PURE__ */ jsx4(
1092
+ BasicInput3,
1093
+ {
1094
+ autoFocus: true,
1095
+ value: newTitle,
1096
+ onChange: (e) => setNewTitle(e.target.value),
1097
+ onKeyDown: (e) => {
1098
+ if (e.key === "Enter") {
1099
+ e.preventDefault();
1100
+ void create(newTitle);
1101
+ }
1102
+ },
1103
+ placeholder: `New ${targetWord}`,
1104
+ className: "h-7 flex-1 text-xs",
1105
+ "aria-label": `Name the new ${targetWord}`
1106
+ }
1107
+ ),
1108
+ /* @__PURE__ */ jsx4(
1109
+ Button2,
1110
+ {
1111
+ type: "button",
1112
+ size: "sm",
1113
+ variant: "default",
1114
+ disabled: busy || newTitle.trim() === "",
1115
+ onClick: () => void create(newTitle),
1116
+ children: busy ? "Creating\u2026" : "Create"
1117
+ }
1118
+ )
1119
+ ] }) : void 0,
1120
+ footNote: allowCreate && !full && createRefusal ? (
1121
+ // The typed title is still in the search box: the notice owns the
1122
+ // two doors out (lane REFUSAL-SWEEP).
1123
+ /* @__PURE__ */ jsx4(
1124
+ RefusalNotice,
1125
+ {
1126
+ error: createRefusal,
1127
+ className: "mt-1 max-w-72",
1128
+ onKeepEditing: () => setCreateRefusal(null),
1129
+ onDiscard: () => {
1130
+ setCreateRefusal(null);
1131
+ setSearch("");
1132
+ }
1133
+ }
1134
+ )
1135
+ ) : null
1136
+ }
1137
+ )
1138
+ )
1139
+ ] })
1140
+ );
1141
+ }
1142
+
1143
+ // src/gridAttachments.tsx
1144
+ import { useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
1145
+ import { useOptionalRecordsClient as useOptionalRecordsClient2 } from "@ai-matrx/records/react";
1146
+ import { Badge, Button as Button5 } from "@ai-matrx/design-system";
1147
+
1148
+ // src/host.tsx
1149
+ import { createContext, useContext, useMemo as useMemo2 } from "react";
1150
+ import { useMyLevel, useMyLevels } from "@ai-matrx/records/react";
1151
+
1152
+ // src/rights.ts
1153
+ import { atLeast } from "@ai-matrx/records";
1154
+ var LEVEL_WORD = {
1155
+ viewer: "Viewer",
1156
+ commenter: "Commenter",
1157
+ editor: "Editor",
1158
+ admin: "Admin"
1159
+ };
1160
+ var NEEDS = {
1161
+ read: "viewer",
1162
+ // custom.io_comment_write
1163
+ comment: "commenter",
1164
+ // custom.record_update / custom.record_write
1165
+ write: "editor",
1166
+ // custom.record_delete
1167
+ remove: "editor",
1168
+ // custom.share_grant — passing your access on is an administrator's right
1169
+ share: "admin",
1170
+ // custom.field_declare / field_update / field_retire / table settings
1171
+ structure: "admin"
1172
+ };
1173
+ var NOT_YET = "Still asking the store what you may do here. Nothing is offered until it has answered.";
1174
+ var NOTHING_AT_ALL = "You have not been given this. Ask somebody who administers it to share it with you.";
1175
+ function sentenceFor(what, level) {
1176
+ if (level === null) return NOTHING_AT_ALL;
1177
+ const need = LEVEL_WORD[NEEDS[what]];
1178
+ const have = LEVEL_WORD[level];
1179
+ const doing = {
1180
+ read: "open this",
1181
+ comment: "leave a comment",
1182
+ write: "change anything here",
1183
+ remove: "delete this",
1184
+ share: "pass this on to somebody else",
1185
+ structure: "change the columns of this table"
1186
+ };
1187
+ return `You are ${have} here, and it takes ${need} to ${doing[what]}. Ask somebody who is Admin on it to move you up.`;
1188
+ }
1189
+ function whatYouMayDo(level, known = true) {
1190
+ const may = (what) => known && atLeast(level, NEEDS[what]);
1191
+ return {
1192
+ level: known ? level : null,
1193
+ known,
1194
+ read: may("read"),
1195
+ comment: may("comment"),
1196
+ write: may("write"),
1197
+ remove: may("remove"),
1198
+ share: may("share"),
1199
+ structure: may("structure"),
1200
+ why: (what) => known ? sentenceFor(what, level) : NOT_YET
1201
+ };
1202
+ }
1203
+ var NOT_ANSWERED_YET = whatYouMayDo(null, false);
1204
+
1205
+ // src/host.tsx
1206
+ import { jsx as jsx5 } from "react/jsx-runtime";
1207
+ function tableRights(may, overallReason) {
1208
+ return {
1209
+ ...may,
1210
+ admin: may.structure,
1211
+ reason: overallReason ?? (may.structure ? "" : may.why("structure"))
1212
+ };
1213
+ }
1214
+ var NO_RIGHTS = tableRights(NOT_ANSWERED_YET);
1215
+ var NO_MEMBERS_REASON = "This app cannot offer the people in your organization on this screen yet, so there is nobody to pick. Type the person's name into a text field for now, and tell us \u2014 it is on our list, not yours.";
1216
+ var NO_UPLOAD_REASON = "This screen cannot take a file yet, so there is no chooser here rather than one that loses what you pick. Put a link to the file in a text field for now. It is on our list, not yours.";
1217
+ var HostContext = createContext(null);
1218
+ function useRecordsUi() {
1219
+ return useContext(HostContext) ?? {};
1220
+ }
1221
+
1222
+ // src/labels.tsx
1223
+ import { createContext as createContext2, useCallback, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo3, useRef as useRef3, useState as useState5 } from "react";
1224
+ import { useRecordsClient } from "@ai-matrx/records/react";
1225
+ import { newerKindExportWords, optionKey } from "@ai-matrx/records";
1226
+ import { isUuidShape as isUuidShape2 } from "@ai-matrx/kit/uuid";
1227
+
1228
+ // src/signature.tsx
1229
+ import { useEffect as useEffect4, useRef as useRef2, useState as useState4 } from "react";
1230
+ import { Button as Button3, ErrorBox as ErrorBox2, cn as cn5 } from "@ai-matrx/design-system";
1231
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1232
+ var DRAWN = /^data:image\/(png|jpeg);base64,[A-Za-z0-9+/=]{64,}$/;
1233
+ function drawnSignatureOf(value) {
1234
+ return typeof value === "string" && DRAWN.test(value) ? value : null;
1235
+ }
1236
+ var SIGNED_WORD = "Signed";
1237
+ function signatureWords(value) {
1238
+ if (value === null || value === void 0 || value === "") return null;
1239
+ return drawnSignatureOf(value) ? SIGNED_WORD : String(value);
1240
+ }
1241
+ function SignatureValue({ value, inCell }) {
1242
+ const drawn = drawnSignatureOf(value);
1243
+ if (drawn) {
1244
+ return /* @__PURE__ */ jsx6(
1245
+ "img",
1246
+ {
1247
+ src: drawn,
1248
+ alt: SIGNED_WORD,
1249
+ "data-records-signature": "",
1250
+ className: cn5("max-w-[12rem] object-contain object-left dark:invert", inCell ? "h-7" : "h-12")
1251
+ }
1252
+ );
1253
+ }
1254
+ return /* @__PURE__ */ jsx6("span", { className: "font-serif italic", children: String(value) });
1255
+ }
1256
+ var INK = "#111827";
1257
+ var SIGNATURE_BUDGET_BYTES = 6e4;
1258
+ var SIGNATURE_TOO_DETAILED = "Too detailed to save \u2014 clear and sign again.";
1259
+ function SignaturePad({
1260
+ value,
1261
+ onChange,
1262
+ disabled,
1263
+ id,
1264
+ className
1265
+ }) {
1266
+ const kept = drawnSignatureOf(value);
1267
+ const named = !kept && typeof value === "string" && value !== "" ? value : null;
1268
+ const [showPad, setShowPad] = useState4(() => !kept && !named);
1269
+ const canvasRef = useRef2(null);
1270
+ const drawing = useRef2(false);
1271
+ const [dirty, setDirty] = useState4(false);
1272
+ const [tooBig, setTooBig] = useState4(false);
1273
+ useEffect4(() => {
1274
+ if (!showPad) return;
1275
+ const canvas = canvasRef.current;
1276
+ if (!canvas) return;
1277
+ const ratio = window.devicePixelRatio || 1;
1278
+ const rect = canvas.getBoundingClientRect();
1279
+ canvas.width = Math.round(rect.width * ratio);
1280
+ canvas.height = Math.round(rect.height * ratio);
1281
+ const context = canvas.getContext("2d");
1282
+ if (!context) return;
1283
+ context.scale(ratio, ratio);
1284
+ context.lineWidth = 2.2;
1285
+ context.lineCap = "round";
1286
+ context.lineJoin = "round";
1287
+ context.strokeStyle = INK;
1288
+ }, [showPad]);
1289
+ const point = (event) => {
1290
+ const rect = event.currentTarget.getBoundingClientRect();
1291
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
1292
+ };
1293
+ const start = (event) => {
1294
+ if (disabled) return;
1295
+ event.currentTarget.setPointerCapture?.(event.pointerId);
1296
+ const context = canvasRef.current?.getContext("2d");
1297
+ if (!context) return;
1298
+ const { x, y } = point(event);
1299
+ context.beginPath();
1300
+ context.moveTo(x, y);
1301
+ drawing.current = true;
1302
+ };
1303
+ const move = (event) => {
1304
+ if (!drawing.current) return;
1305
+ const context = canvasRef.current?.getContext("2d");
1306
+ if (!context) return;
1307
+ const { x, y } = point(event);
1308
+ context.lineTo(x, y);
1309
+ context.stroke();
1310
+ if (!dirty) setDirty(true);
1311
+ };
1312
+ const end = () => {
1313
+ if (!drawing.current) return;
1314
+ drawing.current = false;
1315
+ const canvas = canvasRef.current;
1316
+ if (!canvas) return;
1317
+ const fitted = fitDrawing(canvas);
1318
+ setTooBig(fitted === null);
1319
+ onChange(fitted);
1320
+ setDirty(true);
1321
+ };
1322
+ const clear = () => {
1323
+ const canvas = canvasRef.current;
1324
+ const context = canvas?.getContext("2d");
1325
+ if (canvas && context) {
1326
+ context.save();
1327
+ context.setTransform(1, 0, 0, 1, 0, 0);
1328
+ context.clearRect(0, 0, canvas.width, canvas.height);
1329
+ context.restore();
1330
+ }
1331
+ setDirty(false);
1332
+ setTooBig(false);
1333
+ setShowPad(true);
1334
+ onChange(null);
1335
+ };
1336
+ if (!showPad) {
1337
+ return /* @__PURE__ */ jsxs5("div", { id, className: cn5("flex items-center justify-between gap-2 rounded-md border px-3 py-2", className), children: [
1338
+ /* @__PURE__ */ jsx6(SignatureValue, { value }),
1339
+ !disabled ? /* @__PURE__ */ jsx6(Button3, { type: "button", variant: "ghost", size: "sm", onClick: clear, children: "Sign again" }) : null
1340
+ ] });
1341
+ }
1342
+ return /* @__PURE__ */ jsxs5("div", { className: cn5("flex w-full flex-col gap-1", className), children: [
1343
+ /* @__PURE__ */ jsxs5("div", { className: "relative", children: [
1344
+ /* @__PURE__ */ jsx6(
1345
+ "canvas",
1346
+ {
1347
+ id,
1348
+ ref: canvasRef,
1349
+ role: "img",
1350
+ "aria-label": "Signature pad",
1351
+ "data-records-signature-pad": "",
1352
+ onPointerDown: start,
1353
+ onPointerMove: move,
1354
+ onPointerUp: end,
1355
+ onPointerLeave: end,
1356
+ onPointerCancel: end,
1357
+ className: cn5(
1358
+ "block h-36 w-full touch-none rounded-md border border-border bg-white",
1359
+ disabled ? "cursor-not-allowed opacity-60" : "cursor-crosshair"
1360
+ )
1361
+ }
1362
+ ),
1363
+ !dirty ? /* @__PURE__ */ jsx6("span", { className: "pointer-events-none absolute inset-x-0 bottom-3 text-center text-xs text-neutral-400", children: "Sign here" }) : null
1364
+ ] }),
1365
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-end gap-2", children: [
1366
+ tooBig ? /* @__PURE__ */ jsx6(
1367
+ ErrorBox2,
1368
+ {
1369
+ as: "span",
1370
+ className: "mr-auto text-xs text-destructive",
1371
+ message: SIGNATURE_TOO_DETAILED,
1372
+ operation: "Save a signature",
1373
+ origin: "records-ui/signature"
1374
+ }
1375
+ ) : null,
1376
+ /* @__PURE__ */ jsx6(Button3, { type: "button", variant: "ghost", size: "sm", onClick: clear, disabled: disabled || !dirty, children: "Clear" })
1377
+ ] })
1378
+ ] });
1379
+ }
1380
+ var LADDER = [
1381
+ ["image/png", 600, 1],
1382
+ ["image/png", 400, 1],
1383
+ ["image/jpeg", 600, 0.7],
1384
+ ["image/jpeg", 400, 0.6],
1385
+ ["image/jpeg", 300, 0.5]
1386
+ ];
1387
+ function fitDrawing(canvas) {
1388
+ for (const [type, maxWidth, quality] of LADDER) {
1389
+ const encoded = encodeAt(canvas, type, maxWidth, quality);
1390
+ if (encoded && encoded.length <= SIGNATURE_BUDGET_BYTES) return encoded;
1391
+ }
1392
+ return null;
1393
+ }
1394
+ function encodeAt(canvas, type, maxWidth, quality) {
1395
+ const width = Math.min(maxWidth, canvas.width || maxWidth);
1396
+ const scale = canvas.width > 0 ? width / canvas.width : 1;
1397
+ const out = document.createElement("canvas");
1398
+ out.width = width;
1399
+ out.height = Math.max(1, Math.round((canvas.height || 1) * scale));
1400
+ const context = out.getContext("2d");
1401
+ if (!context) return null;
1402
+ if (type === "image/jpeg") {
1403
+ context.fillStyle = "#ffffff";
1404
+ context.fillRect(0, 0, out.width, out.height);
1405
+ }
1406
+ context.drawImage(canvas, 0, 0, out.width, out.height);
1407
+ return out.toDataURL(type, quality);
1408
+ }
1409
+
1410
+ // src/labels.tsx
1411
+ import { jsx as jsx7 } from "react/jsx-runtime";
1412
+ var LabelContext = createContext2({
1413
+ // With no provider the resolver is ABSENT, not silently empty: a caller gets
1414
+ // null and says "Loading…" forever, which is visible. The mount always
1415
+ // provides one, so this is only ever a bare unit test.
1416
+ lookup: () => null,
1417
+ want: () => {
1418
+ },
1419
+ waiting: () => null
1420
+ });
1421
+ var SAY_WHAT_YOU_ARE_WAITING_FOR_AFTER_MS = 1e3;
1422
+ function waitingLineFor(field, target) {
1423
+ return field.type === "list" ? "Reading this column\u2019s list of choices\u2026" : `Reading the ${target ?? "records"} this column points at\u2026`;
1424
+ }
1425
+ function idsOf(value) {
1426
+ if (Array.isArray(value)) return value.map(String).filter(isId);
1427
+ if (typeof value === "string" && isId(value)) return [value];
1428
+ return [];
1429
+ }
1430
+ function choiceKeyName(fieldId, key) {
1431
+ return `${fieldId}\0key\0${key}`;
1432
+ }
1433
+ function isId(value) {
1434
+ return typeof value === "string" && isUuidShape2(value);
1435
+ }
1436
+ function pointsAtRecords(field) {
1437
+ return field.type === "list" || field.type === "relation";
1438
+ }
1439
+ function RecordLabelProvider({ children }) {
1440
+ const client = useRecordsClient();
1441
+ const [names, setNames] = useState5({});
1442
+ const [archivedRefs, setArchivedRefs] = useState5({});
1443
+ const [settled, setSettled] = useState5({});
1444
+ const [waitingLines, setWaitingLines] = useState5({});
1445
+ const asked2 = useRef3(/* @__PURE__ */ new Set());
1446
+ const chased = useRef3(/* @__PURE__ */ new Set());
1447
+ const timers = useRef3(/* @__PURE__ */ new Map());
1448
+ const startWaitClock = useCallback((field, target) => {
1449
+ if (timers.current.has(field.id)) return;
1450
+ timers.current.set(
1451
+ field.id,
1452
+ setTimeout(() => {
1453
+ timers.current.delete(field.id);
1454
+ setWaitingLines(
1455
+ (current) => current[field.id] ? current : { ...current, [field.id]: waitingLineFor(field, target) }
1456
+ );
1457
+ }, SAY_WHAT_YOU_ARE_WAITING_FOR_AFTER_MS)
1458
+ );
1459
+ }, []);
1460
+ const stopWaitClock = useCallback((fieldId) => {
1461
+ const timer = timers.current.get(fieldId);
1462
+ if (timer) {
1463
+ clearTimeout(timer);
1464
+ timers.current.delete(fieldId);
1465
+ }
1466
+ setWaitingLines((current) => {
1467
+ if (!current[fieldId]) return current;
1468
+ const next = { ...current };
1469
+ delete next[fieldId];
1470
+ return next;
1471
+ });
1472
+ }, []);
1473
+ useEffect5(
1474
+ () => () => {
1475
+ for (const timer of timers.current.values()) clearTimeout(timer);
1476
+ timers.current.clear();
1477
+ },
1478
+ []
1479
+ );
1480
+ const want = useCallback(
1481
+ (field) => {
1482
+ if (!pointsAtRecords(field)) return;
1483
+ if (asked2.current.has(field.id)) return;
1484
+ asked2.current.add(field.id);
1485
+ startWaitClock(field, null);
1486
+ void (async () => {
1487
+ if (field.type === "list") {
1488
+ const result = await client.fieldOptions({ field_id: field.id });
1489
+ if (result.ok) {
1490
+ setNames((current) => {
1491
+ const next = { ...current };
1492
+ for (const option of result.data ?? []) {
1493
+ const words = recordName(option.data, null, "Unnamed choice");
1494
+ next[option.id] = words;
1495
+ const key = optionKey(option);
1496
+ if (key) next[choiceKeyName(field.id, key)] = words;
1497
+ }
1498
+ return next;
1499
+ });
1500
+ }
1501
+ setSettled((current) => ({ ...current, [field.id]: true }));
1502
+ stopWaitClock(field.id);
1503
+ return;
1504
+ }
1505
+ if (!field.relation_target) {
1506
+ setSettled((current) => ({ ...current, [field.id]: true }));
1507
+ stopWaitClock(field.id);
1508
+ return;
1509
+ }
1510
+ setSettled((current) => ({ ...current, [field.id]: true }));
1511
+ })();
1512
+ },
1513
+ [client, startWaitClock, stopWaitClock]
1514
+ );
1515
+ const queued = useRef3(/* @__PURE__ */ new Map());
1516
+ const flushing = useRef3(null);
1517
+ const flush = useCallback(() => {
1518
+ flushing.current = null;
1519
+ const batch = [...queued.current.values()];
1520
+ queued.current.clear();
1521
+ if (batch.length === 0) return;
1522
+ void (async () => {
1523
+ const learned = {};
1524
+ const archivedNow = {};
1525
+ await Promise.all(
1526
+ batch.map(async ({ field, ids }) => {
1527
+ const wanted = [...ids];
1528
+ const result = await client.relationWordsMany({
1529
+ field_id: field.id,
1530
+ record_ids: wanted
1531
+ });
1532
+ for (const id of wanted) {
1533
+ const words = result.ok ? result.data[id] : void 0;
1534
+ learned[id] = typeof words === "string" && words !== "" ? words : WITHHELD_RECORD_LABEL;
1535
+ }
1536
+ if (result.ok) for (const id of result.archived ?? []) archivedNow[`${field.id}:${id}`] = true;
1537
+ })
1538
+ );
1539
+ setNames((current) => ({ ...current, ...learned }));
1540
+ if (Object.keys(archivedNow).length > 0) setArchivedRefs((current) => ({ ...current, ...archivedNow }));
1541
+ for (const { field } of batch) stopWaitClock(field.id);
1542
+ })();
1543
+ }, [client, stopWaitClock]);
1544
+ const chase = useCallback(
1545
+ (field, id) => {
1546
+ const key = `${field.id}:${id}`;
1547
+ if (chased.current.has(key)) return;
1548
+ chased.current.add(key);
1549
+ const pending = queued.current.get(field.id);
1550
+ if (pending) pending.ids.add(id);
1551
+ else queued.current.set(field.id, { field, ids: /* @__PURE__ */ new Set([id]) });
1552
+ startWaitClock(field, null);
1553
+ if (flushing.current === null) flushing.current = setTimeout(flush, 0);
1554
+ },
1555
+ [flush, startWaitClock]
1556
+ );
1557
+ useEffect5(
1558
+ () => () => {
1559
+ if (flushing.current !== null) clearTimeout(flushing.current);
1560
+ },
1561
+ []
1562
+ );
1563
+ const rereadFor = useRef3(/* @__PURE__ */ new Set());
1564
+ const lookupWords = useCallback(
1565
+ (field, id) => {
1566
+ if (field.type === "list" && !isId(id)) {
1567
+ const named = names[choiceKeyName(field.id, id)];
1568
+ if (named !== void 0) return named;
1569
+ const once = choiceKeyName(field.id, id);
1570
+ if (settled[field.id] && !rereadFor.current.has(once)) {
1571
+ rereadFor.current.add(once);
1572
+ asked2.current.delete(field.id);
1573
+ want(field);
1574
+ }
1575
+ return null;
1576
+ }
1577
+ const known = names[id];
1578
+ if (known !== void 0) return known;
1579
+ if (settled[field.id]) chase(field, id);
1580
+ return null;
1581
+ },
1582
+ [names, settled, chase, want]
1583
+ );
1584
+ const lookup = useMemo3(
1585
+ () => {
1586
+ const answer = ((field, id) => lookupWords(field, id));
1587
+ answer.archived = (field, id) => archivedRefs[`${field.id}:${id}`] === true;
1588
+ return answer;
1589
+ },
1590
+ [lookupWords, archivedRefs]
1591
+ );
1592
+ const waiting = useCallback((field) => waitingLines[field.id] ?? null, [waitingLines]);
1593
+ const value = useMemo3(() => ({ lookup, want, waiting }), [lookup, want, waiting]);
1594
+ return /* @__PURE__ */ jsx7(LabelContext.Provider, { value, children });
1595
+ }
1596
+ function useRecordLabels(fields) {
1597
+ const { lookup, want } = useContext2(LabelContext);
1598
+ useEffect5(() => {
1599
+ for (const field of fields ?? []) want(field);
1600
+ }, [fields, want]);
1601
+ return lookup;
1602
+ }
1603
+
1604
+ // src/FilePicker.tsx
1605
+ import { useCallback as useCallback2, useId, useRef as useRef4, useState as useState6 } from "react";
1606
+ import { Button as Button4, cn as cn6 } from "@ai-matrx/design-system";
1607
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1608
+ function FilePicker({
1609
+ label,
1610
+ accept,
1611
+ inputLabel,
1612
+ hint,
1613
+ disabled = false,
1614
+ onFile,
1615
+ onClear,
1616
+ chosenName,
1617
+ className
1618
+ }) {
1619
+ const inputId = useId();
1620
+ const input = useRef4(null);
1621
+ const [over, setOver] = useState6(false);
1622
+ const take = useCallback2(
1623
+ (file) => {
1624
+ if (!file || disabled) return;
1625
+ onFile(file);
1626
+ },
1627
+ [disabled, onFile]
1628
+ );
1629
+ const onDrop = useCallback2(
1630
+ (e) => {
1631
+ e.preventDefault();
1632
+ setOver(false);
1633
+ take(e.dataTransfer?.files?.[0]);
1634
+ },
1635
+ [take]
1636
+ );
1637
+ return /* @__PURE__ */ jsxs6(
1638
+ "div",
1639
+ {
1640
+ "data-matrx-file-picker": true,
1641
+ className: cn6("flex min-w-0 flex-col gap-1", className),
1642
+ onDragOver: (e) => {
1643
+ if (disabled) return;
1644
+ e.preventDefault();
1645
+ setOver(true);
1646
+ },
1647
+ onDragLeave: () => setOver(false),
1648
+ onDrop,
1649
+ children: [
1650
+ /* @__PURE__ */ jsxs6(
1651
+ "div",
1652
+ {
1653
+ className: cn6(
1654
+ "flex min-w-0 flex-wrap items-center gap-2 rounded-md border border-dashed px-2 py-1.5",
1655
+ over ? "border-primary bg-primary/5" : "border-border",
1656
+ disabled && "opacity-60"
1657
+ ),
1658
+ children: [
1659
+ /* @__PURE__ */ jsx8(
1660
+ "input",
1661
+ {
1662
+ ref: input,
1663
+ id: inputId,
1664
+ type: "file",
1665
+ "aria-label": inputLabel ?? label,
1666
+ tabIndex: -1,
1667
+ ...accept ? { accept } : {},
1668
+ disabled,
1669
+ className: "sr-only",
1670
+ onChange: (e) => {
1671
+ take(e.target.files?.[0]);
1672
+ e.target.value = "";
1673
+ }
1674
+ }
1675
+ ),
1676
+ /* @__PURE__ */ jsx8(
1677
+ Button4,
1678
+ {
1679
+ type: "button",
1680
+ size: "sm",
1681
+ variant: "outline",
1682
+ disabled,
1683
+ onClick: () => input.current?.click(),
1684
+ children: chosenName ? "Choose a different file" : label
1685
+ }
1686
+ ),
1687
+ chosenName ? /* @__PURE__ */ jsx8("span", { className: "min-w-0 flex-1 truncate text-xs", title: chosenName, children: chosenName }) : /* @__PURE__ */ jsx8("span", { className: "text-xs text-muted-foreground", children: "or drop it here" }),
1688
+ chosenName && onClear ? /* @__PURE__ */ jsx8(Button4, { type: "button", size: "sm", variant: "ghost", disabled, onClick: onClear, children: "Cancel" }) : null
1689
+ ]
1690
+ }
1691
+ ),
1692
+ hint && !chosenName ? /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: hint }) : null
1693
+ ]
1694
+ }
1695
+ );
1696
+ }
1697
+
1698
+ // src/gridAttachments.tsx
1699
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1700
+ function attachmentIdsOf(value) {
1701
+ return idsOf(value);
1702
+ }
1703
+ function withAttached(field, value, added) {
1704
+ const held = attachmentIdsOf(value);
1705
+ if (!field.multi) return added.length > 0 ? added[added.length - 1] : held[0] ?? null;
1706
+ const next = [...held];
1707
+ for (const id of added) if (!next.includes(id)) next.push(id);
1708
+ return next.length > 0 ? next : null;
1709
+ }
1710
+ function withoutAttached(field, value, removed) {
1711
+ const next = attachmentIdsOf(value).filter((id) => id !== removed);
1712
+ if (!field.multi) return next[0] ?? null;
1713
+ return next.length > 0 ? next : null;
1714
+ }
1715
+ var handedHostFileIds = /* @__PURE__ */ new Set();
1716
+ function rememberHostFileIds(ids) {
1717
+ for (const id of ids) handedHostFileIds.add(id);
1718
+ }
1719
+ var FILE_CHIP_UNREAD = "File";
1720
+ var FILE_CHIP_NO_PAGE = "This screen has no way to open files yet.";
1721
+ function AttachmentChip({ id, onRemove }) {
1722
+ const client = useOptionalRecordsClient2();
1723
+ const host = useRecordsUi();
1724
+ const [read, setRead] = useState7(null);
1725
+ useEffect6(() => {
1726
+ let cancelled = false;
1727
+ if (!client) {
1728
+ setRead({ name: null, fileId: null, why: null });
1729
+ return;
1730
+ }
1731
+ if (handedHostFileIds.has(id)) {
1732
+ setRead({ name: null, fileId: id, why: null });
1733
+ return;
1734
+ }
1735
+ void client.recordRead({ record_id: id }).then((answered) => {
1736
+ if (cancelled) return;
1737
+ if (!answered.ok) {
1738
+ setRead({ name: null, fileId: id, why: refusalSentence(answered.error) });
1739
+ return;
1740
+ }
1741
+ const doc = answered.data.document ?? {};
1742
+ const fileId = typeof doc["file_id"] === "string" ? doc["file_id"] : null;
1743
+ const name = typeof doc["name"] === "string" && doc["name"] !== "" ? doc["name"] : null;
1744
+ setRead({ name, fileId, why: null });
1745
+ });
1746
+ return () => {
1747
+ cancelled = true;
1748
+ };
1749
+ }, [client, id]);
1750
+ const words = read?.name ?? (read ? FILE_CHIP_UNREAD : "\u2026");
1751
+ const href = read?.fileId ? host.hrefForFile?.({ fileId: read.fileId }) : void 0;
1752
+ const title = href ? `Open ${words}` : read?.why ?? FILE_CHIP_NO_PAGE;
1753
+ return /* @__PURE__ */ jsxs7(Badge, { variant: "secondary", className: "max-w-[14rem] gap-1 text-[11px]", "data-records-file-chip": id, title, children: [
1754
+ href ? /* @__PURE__ */ jsx9(
1755
+ "a",
1756
+ {
1757
+ href,
1758
+ target: "_blank",
1759
+ rel: "noreferrer",
1760
+ className: "truncate underline-offset-2 hover:underline",
1761
+ "data-records-file-link": read?.fileId ?? "",
1762
+ onClick: (event) => event.stopPropagation(),
1763
+ onMouseDown: (event) => event.stopPropagation(),
1764
+ children: words
1765
+ }
1766
+ ) : /* @__PURE__ */ jsx9("span", { className: "truncate", children: words }),
1767
+ onRemove ? /* @__PURE__ */ jsx9(
1768
+ "button",
1769
+ {
1770
+ type: "button",
1771
+ "aria-label": `Remove ${words}`,
1772
+ className: "shrink-0 rounded px-0.5 text-muted-foreground hover:text-foreground",
1773
+ onClick: (event) => {
1774
+ event.stopPropagation();
1775
+ onRemove();
1776
+ },
1777
+ children: "\xD7"
1778
+ }
1779
+ ) : null
1780
+ ] });
1781
+ }
1782
+ function AttachmentChips({ value, onRemove }) {
1783
+ const ids = attachmentIdsOf(value);
1784
+ if (ids.length === 0) return null;
1785
+ return /* @__PURE__ */ jsx9("span", { className: "flex flex-wrap gap-1", "data-records-file-chips": "", children: ids.map((id) => /* @__PURE__ */ jsx9(AttachmentChip, { id, ...onRemove ? { onRemove: () => onRemove(id) } : {} }, id)) });
1786
+ }
1787
+ function hostTakesFiles(host) {
1788
+ return Boolean(host.pickFiles || host.upload);
1789
+ }
1790
+ function AttachmentControl({
1791
+ field,
1792
+ value,
1793
+ onChange,
1794
+ disabled,
1795
+ id,
1796
+ autoOpen,
1797
+ onClose
1798
+ }) {
1799
+ const host = useRecordsUi();
1800
+ const [busy, setBusy] = useState7(false);
1801
+ const [why, setWhy] = useState7(null);
1802
+ const latest = useRef5(value);
1803
+ latest.current = value;
1804
+ const multiple = Boolean(field.multi);
1805
+ const pick = async () => {
1806
+ if (!host.pickFiles || disabled) return;
1807
+ setBusy(true);
1808
+ setWhy(null);
1809
+ try {
1810
+ const picked = await host.pickFiles({ multiple });
1811
+ if (picked && picked.length > 0) {
1812
+ rememberHostFileIds(picked);
1813
+ onChange(withAttached(field, latest.current, picked));
1814
+ }
1815
+ } catch (cause) {
1816
+ setWhy(cause instanceof Error ? cause.message : String(cause));
1817
+ setBusy(false);
1818
+ return;
1819
+ }
1820
+ setBusy(false);
1821
+ onClose?.();
1822
+ };
1823
+ const opened = useRef5(false);
1824
+ useEffect6(() => {
1825
+ if (!autoOpen || opened.current || !host.pickFiles) return;
1826
+ opened.current = true;
1827
+ void pick();
1828
+ }, [autoOpen]);
1829
+ if (!hostTakesFiles(host)) {
1830
+ return /* @__PURE__ */ jsx9("p", { id, className: "rounded border border-dashed px-2 py-1 text-xs text-muted-foreground", "data-records-no-file-door": "", children: NO_UPLOAD_REASON });
1831
+ }
1832
+ return /* @__PURE__ */ jsxs7("span", { id, className: "flex flex-col gap-1", "data-records-attachment-editor": "", children: [
1833
+ /* @__PURE__ */ jsx9(
1834
+ AttachmentChips,
1835
+ {
1836
+ value,
1837
+ ...disabled ? {} : { onRemove: (removed) => onChange(withoutAttached(field, latest.current, removed)) }
1838
+ }
1839
+ ),
1840
+ /* @__PURE__ */ jsxs7("span", { className: "flex flex-wrap items-center gap-1", children: [
1841
+ host.pickFiles ? /* @__PURE__ */ jsx9(Button5, { size: "sm", variant: "outline", disabled: disabled || busy, onClick: () => void pick(), "data-records-attach-files": "", children: busy ? "Choosing\u2026" : attachmentIdsOf(value).length > 0 ? "Add files\u2026" : "Attach files\u2026" }) : null,
1842
+ host.upload ? /* @__PURE__ */ jsx9(
1843
+ FilePicker,
1844
+ {
1845
+ label: "Upload a file",
1846
+ inputLabel: `Upload a file to ${fieldName(field)}`,
1847
+ disabled: disabled || busy,
1848
+ onFile: (file) => {
1849
+ setBusy(true);
1850
+ setWhy(null);
1851
+ void host.upload(file).then((result) => {
1852
+ setBusy(false);
1853
+ if (result.ok) {
1854
+ rememberHostFileIds([result.fileId]);
1855
+ onChange(withAttached(field, latest.current, [result.fileId]));
1856
+ } else setWhy(result.reason);
1857
+ });
1858
+ }
1859
+ }
1860
+ ) : null
1861
+ ] }),
1862
+ why ? /* @__PURE__ */ jsx9(RefusalLine, { error: refusal("invalid_argument", why, "Pick another file, or try this one again.") }) : null
1863
+ ] });
1864
+ }
1865
+
1866
+ // src/EntityReferenceControl.tsx
1867
+ import { useMemo as useMemo4, useState as useState8 } from "react";
1868
+ import { entityRefsOf, entityRefsToWrite } from "@ai-matrx/records";
1869
+ import { useEntityReferenceKinds, useEntityReferenceSearch, useEntityReferenceWords, useOptionalRecordsClient as useOptionalRecordsClient3 } from "@ai-matrx/records/react";
1870
+ import { Badge as Badge2, Button as Button6, cn as cn7 } from "@ai-matrx/design-system";
1871
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1872
+ var WITHHELD_ENTITY_LABEL = "Something you have not been given access to";
1873
+ var ENTITY_REFERENCE_EMPTY = "Nothing is pointed at yet.";
1874
+ function entityReferenceText(value) {
1875
+ const refs = entityRefsOf(value);
1876
+ if (refs.length === 0) return "\u2014";
1877
+ return refs.map((r) => r.label ?? WITHHELD_ENTITY_LABEL).join(", ");
1878
+ }
1879
+ function allowedKindsOf(field) {
1880
+ const config = field.config ?? {};
1881
+ const kinds = Array.isArray(config.allowed_types) ? config.allowed_types : [];
1882
+ return kinds.map((k) => String(k).trim().toLowerCase()).filter(Boolean);
1883
+ }
1884
+ function EntityReferenceControl(props) {
1885
+ const client = useOptionalRecordsClient3();
1886
+ return client ? /* @__PURE__ */ jsx10(WithWords, { ...props }) : /* @__PURE__ */ jsx10(Chips, { ...props, words: null, picker: null });
1887
+ }
1888
+ function WithWords(props) {
1889
+ const refs = useMemo4(() => entityRefsOf(props.value), [props.value]);
1890
+ const [known, setKnown] = useState8([]);
1891
+ const withKnown = refs.map((r) => r.label === null ? known.find((k) => k.token === r.token && k.id === r.id) ?? r : r);
1892
+ const unlabelled = useMemo4(
1893
+ () => withKnown.filter((r) => r.label === null).map((r) => ({ token: r.token, id: r.id })),
1894
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1895
+ [JSON.stringify(withKnown)]
1896
+ );
1897
+ const words = useEntityReferenceWords(unlabelled);
1898
+ const picker = props.disabled ? null : /* @__PURE__ */ jsx10(
1899
+ EntityReferencePicker,
1900
+ {
1901
+ field: props.field,
1902
+ refs: withKnown,
1903
+ onPick: (picked) => {
1904
+ setKnown((k) => [...k.filter((x) => !(x.token === picked.token && x.id === picked.id)), picked]);
1905
+ const multi = Boolean(props.field.multi);
1906
+ const next = multi ? [...withKnown.filter((x) => !(x.token === picked.token && x.id === picked.id)), picked] : [picked];
1907
+ props.onChange(entityRefsToWrite(next, multi));
1908
+ }
1909
+ }
1910
+ );
1911
+ return /* @__PURE__ */ jsx10(Chips, { ...props, value: withKnown, words, picker });
1912
+ }
1913
+ function EntityReferencePicker({
1914
+ field,
1915
+ refs,
1916
+ onPick
1917
+ }) {
1918
+ const allowed = allowedKindsOf(field);
1919
+ const kinds = useEntityReferenceKinds();
1920
+ const labelOfKind = (token) => kinds.data?.find((k) => k.token === token)?.label ?? token.replace(/_/g, " ");
1921
+ const [open, setOpen] = useState8(false);
1922
+ const [search, setSearch] = useState8("");
1923
+ const [only, setOnly] = useState8(null);
1924
+ const tokens = only ? [only] : allowed;
1925
+ const found = useEntityReferenceSearch(tokens, search, open);
1926
+ const multi = Boolean(field.multi);
1927
+ const items = (found.data ?? []).map((r) => ({
1928
+ id: `${r.token}:${r.id}`,
1929
+ label: r.label ?? WITHHELD_ENTITY_LABEL,
1930
+ detail: allowed.length > 1 ? labelOfKind(r.token) : null,
1931
+ picked: refs.some((x) => x.token === r.token && x.id === r.id),
1932
+ attrs: { "data-entity-reference-candidate": `${r.token}:${r.id}` }
1933
+ }));
1934
+ const kindChoice = allowed.length > 1 ? allowed.length <= 4 ? /* @__PURE__ */ jsx10("div", { className: "mb-1 flex flex-wrap gap-1", role: "group", "aria-label": `Which kind \u2014 ${field.label}`, children: [null, ...allowed].map((token) => /* @__PURE__ */ jsx10(
1935
+ Button6,
1936
+ {
1937
+ type: "button",
1938
+ size: "sm",
1939
+ variant: only === token ? "secondary" : "ghost",
1940
+ className: "h-6 px-2 text-[11px]",
1941
+ "aria-pressed": only === token,
1942
+ onClick: () => setOnly(token),
1943
+ children: token ? labelOfKind(token) : "Any"
1944
+ },
1945
+ token ?? "any"
1946
+ )) }) : /* @__PURE__ */ jsxs8(
1947
+ "select",
1948
+ {
1949
+ className: "mb-1 h-7 w-full rounded border border-border bg-background px-1 text-xs",
1950
+ "aria-label": `Which kind \u2014 ${field.label}`,
1951
+ value: only ?? "",
1952
+ onChange: (e) => setOnly(e.target.value || null),
1953
+ children: [
1954
+ /* @__PURE__ */ jsx10("option", { value: "", children: "Any kind" }),
1955
+ allowed.map((token) => /* @__PURE__ */ jsx10("option", { value: token, children: labelOfKind(token) }, token))
1956
+ ]
1957
+ }
1958
+ ) : null;
1959
+ const notice = /* @__PURE__ */ jsxs8(Fragment3, { children: [
1960
+ kindChoice,
1961
+ found.error ? /* @__PURE__ */ jsx10(RefusalLine, { error: found.error, className: "px-2 py-1 text-[11px]" }) : null,
1962
+ found.loading && !found.data ? /* @__PURE__ */ jsx10("p", { className: "px-2 py-1 text-[11px] text-muted-foreground", children: "Searching\u2026" }) : null
1963
+ ] });
1964
+ const kindWords = allowed.length === 1 ? labelOfKind(allowed[0]).toLowerCase() : "thing";
1965
+ return /* @__PURE__ */ jsx10(
1966
+ PickOrAdd,
1967
+ {
1968
+ triggerLabel: `Pick for ${field.label}`,
1969
+ triggerText: refs.length > 0 && !multi ? "Change" : "Pick",
1970
+ open,
1971
+ onOpenChange: (next) => {
1972
+ setOpen(next);
1973
+ if (!next) setSearch("");
1974
+ },
1975
+ search,
1976
+ onSearch: setSearch,
1977
+ items,
1978
+ notice,
1979
+ empty: found.loading ? null : /* @__PURE__ */ jsx10("p", { className: "px-2 py-1 text-[11px] text-muted-foreground", children: search.trim() ? `No ${kindWords} you can open is called \u201C${search.trim()}\u201D.` : `There is no ${kindWords} you can open yet.` }),
1980
+ onPick: (key) => {
1981
+ const r = (found.data ?? []).find((x) => `${x.token}:${x.id}` === key);
1982
+ if (!r) return;
1983
+ onPick(r);
1984
+ if (!multi) setOpen(false);
1985
+ }
1986
+ }
1987
+ );
1988
+ }
1989
+ function Chips({
1990
+ field,
1991
+ value,
1992
+ onChange,
1993
+ disabled,
1994
+ id,
1995
+ className,
1996
+ words,
1997
+ picker
1998
+ }) {
1999
+ const refs = useMemo4(() => entityRefsOf(value), [value]);
2000
+ const labelOf = (token, refId, own3) => {
2001
+ if (own3) return own3;
2002
+ const read = words?.data?.find((w) => w.token === token && w.id === refId)?.label;
2003
+ if (read) return read;
2004
+ if (words === null) return "A linked item (its name is read when you are signed in)";
2005
+ if (words.loading) return "Reading\u2026";
2006
+ return WITHHELD_ENTITY_LABEL;
2007
+ };
2008
+ const multi = Boolean(field.multi);
2009
+ return /* @__PURE__ */ jsxs8("div", { id, className: cn7("flex flex-wrap items-center gap-1", className), "aria-label": `What ${field.label} points at`, children: [
2010
+ refs.length === 0 && !picker ? /* @__PURE__ */ jsx10("span", { className: "text-[11px] text-muted-foreground", children: ENTITY_REFERENCE_EMPTY }) : null,
2011
+ refs.map((r) => {
2012
+ const label = labelOf(r.token, r.id, r.label);
2013
+ return /* @__PURE__ */ jsxs8(Badge2, { variant: "secondary", className: "gap-1 text-[11px]", "data-entity-reference-chip": `${r.token}:${r.id}`, children: [
2014
+ label,
2015
+ disabled ? null : /* @__PURE__ */ jsx10(
2016
+ "button",
2017
+ {
2018
+ type: "button",
2019
+ className: "ml-0.5 rounded-sm px-0.5 opacity-70 hover:opacity-100",
2020
+ "aria-label": `Take ${label} off`,
2021
+ onClick: () => {
2022
+ const rest = refs.filter((x) => !(x.token === r.token && x.id === r.id));
2023
+ onChange(entityRefsToWrite(rest, multi));
2024
+ },
2025
+ children: "\xD7"
2026
+ }
2027
+ )
2028
+ ] }, `${r.token}:${r.id}`);
2029
+ }),
2030
+ picker
2031
+ ] });
2032
+ }
2033
+
2034
+ // src/PersonPicker.tsx
2035
+ import { useCallback as useCallback3, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef6, useState as useState9 } from "react";
2036
+ import { rankTypedMatches as rankTypedMatches2 } from "@ai-matrx/records";
2037
+ import { useOptionalRecordsClient as useOptionalRecordsClient4 } from "@ai-matrx/records/react";
2038
+ import {
2039
+ BasicInput as BasicInput4,
2040
+ Button as Button7,
2041
+ Popover as Popover2,
2042
+ PopoverContent as PopoverContent2,
2043
+ PopoverTrigger as PopoverTrigger2,
2044
+ ScrollArea as ScrollArea2,
2045
+ cn as cn8
2046
+ } from "@ai-matrx/design-system";
2047
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2048
+ function personFromRow(row) {
2049
+ const data = row.document ?? {};
2050
+ const userId = data["user_id"];
2051
+ return {
2052
+ id: row.id,
2053
+ userId: typeof userId === "string" && userId !== "" ? userId : null,
2054
+ name: String(data["full_name"] ?? data["name"] ?? data["email"] ?? "").trim()
2055
+ };
2056
+ }
2057
+ function memberName(member) {
2058
+ const display = (member.name ?? "").trim();
2059
+ if (display !== "") return display;
2060
+ const email = (member.email ?? "").trim();
2061
+ if (email !== "") return email;
2062
+ return "A member with no name set";
2063
+ }
2064
+ async function personRecordForMember(client, personTableId, people, member) {
2065
+ const held = people.find((p) => p.userId === member.userId);
2066
+ if (held) return { ok: true, data: held.id };
2067
+ const written = await client.recordWrite({
2068
+ table_id: personTableId,
2069
+ data: {
2070
+ user_id: member.userId,
2071
+ full_name: memberName(member),
2072
+ ...member.email ? { email: member.email } : {}
2073
+ }
2074
+ });
2075
+ return written;
2076
+ }
2077
+ function PersonPicker({ field, value, onChange, disabled, id, className, autoOpen, onClose, initialSearch }) {
2078
+ const client = useOptionalRecordsClient4();
2079
+ const host = useRecordsUi();
2080
+ const [members, setMembers] = useState9(null);
2081
+ const [people, setPeople] = useState9([]);
2082
+ const [error, setError] = useState9(null);
2083
+ const [search, setSearch] = useState9(initialSearch ?? "");
2084
+ const [open, setOpenState] = useState9(autoOpen === true);
2085
+ const onCloseRef = useRef6(onClose);
2086
+ onCloseRef.current = onClose;
2087
+ const setOpen = useCallback3((next) => {
2088
+ setOpenState(next);
2089
+ if (!next) onCloseRef.current?.();
2090
+ }, []);
2091
+ const [busy, setBusy] = useState9(false);
2092
+ const membersPort = host.members;
2093
+ const personTable = field.relation_target;
2094
+ const picked = useMemo5(
2095
+ () => Array.isArray(value) ? value.map(String) : value === null || value === void 0 ? [] : [String(value)],
2096
+ [value]
2097
+ );
2098
+ const load = useCallback3(async () => {
2099
+ if (!membersPort || !client) return;
2100
+ const [roster, records] = await Promise.all([
2101
+ membersPort(),
2102
+ personTable ? client.list({ table_id: personTable, limit: 500 }) : Promise.resolve(null)
2103
+ ]);
2104
+ setMembers(roster);
2105
+ if (records && !records.ok) {
2106
+ setError(records.error);
2107
+ return;
2108
+ }
2109
+ setPeople(records ? records.data.rows.map(personFromRow).filter((p) => p.userId !== null) : []);
2110
+ }, [membersPort, client, personTable]);
2111
+ useEffect7(() => {
2112
+ if (!open) return;
2113
+ void load();
2114
+ }, [open, load]);
2115
+ useEffect7(() => {
2116
+ if (picked.length === 0 || !personTable || people.length > 0 || !client) return;
2117
+ let cancelled = false;
2118
+ void client.list({ table_id: personTable, limit: 500 }).then((result) => {
2119
+ if (cancelled || !result.ok) return;
2120
+ setPeople(result.data.rows.map(personFromRow).filter((p) => p.userId !== null));
2121
+ });
2122
+ return () => {
2123
+ cancelled = true;
2124
+ };
2125
+ }, [client, personTable, picked.length, people.length]);
2126
+ const listed = rankTypedMatches2(members ?? [], search, memberName);
2127
+ const keys = useListKeys({
2128
+ count: listed.length,
2129
+ resetOn: search,
2130
+ onPick: (index) => {
2131
+ const member = listed[index];
2132
+ if (member && !busy) void pickRef.current?.(member);
2133
+ },
2134
+ onClose: () => setOpen(false)
2135
+ });
2136
+ const pickRef = useRef6(null);
2137
+ if (!membersPort || !client) {
2138
+ return /* @__PURE__ */ jsx11("p", { className: cn8("text-xs text-muted-foreground", className), children: NO_MEMBERS_REASON });
2139
+ }
2140
+ if (!personTable) {
2141
+ return /* @__PURE__ */ jsxs9("p", { className: cn8("text-xs text-muted-foreground", className), children: [
2142
+ fieldName(field),
2143
+ " is a person field, but it does not say which Table the people are records of yet, so there is nothing to pick from. A table admin sets its target in the field editor."
2144
+ ] });
2145
+ }
2146
+ const nameOfRecord = (recordId) => {
2147
+ const person = people.find((p) => p.id === recordId);
2148
+ if (!person) return "Loading\u2026";
2149
+ if (person.name !== "") return person.name;
2150
+ const member = (members ?? []).find((m) => m.userId === person.userId);
2151
+ return member ? memberName(member) : "A member with no name set";
2152
+ };
2153
+ const pick = async (member) => {
2154
+ setBusy(true);
2155
+ setError(null);
2156
+ const resolved = await personRecordForMember(client, personTable, people, member);
2157
+ setBusy(false);
2158
+ if (!resolved.ok) {
2159
+ setError(resolved.error);
2160
+ return;
2161
+ }
2162
+ if (!people.some((p) => p.id === resolved.data)) {
2163
+ setPeople((held) => [...held, { id: resolved.data, userId: member.userId, name: memberName(member) }]);
2164
+ }
2165
+ if (!field.multi) {
2166
+ onChange(picked[0] === resolved.data ? null : resolved.data);
2167
+ setOpen(false);
2168
+ return;
2169
+ }
2170
+ const next = new Set(picked);
2171
+ if (next.has(resolved.data)) next.delete(resolved.data);
2172
+ else next.add(resolved.data);
2173
+ onChange(next.size === 0 ? null : [...next]);
2174
+ };
2175
+ pickRef.current = pick;
2176
+ const drop = (recordId) => {
2177
+ const next = picked.filter((id2) => id2 !== recordId);
2178
+ onChange(next.length === 0 ? null : field.multi ? next : next[0] ?? null);
2179
+ };
2180
+ const visible = listed;
2181
+ const pickedUserIds = new Set(
2182
+ picked.map((recordId) => people.find((p) => p.id === recordId)?.userId).filter(Boolean)
2183
+ );
2184
+ return /* @__PURE__ */ jsxs9("div", { className: cn8("flex flex-wrap items-center gap-1", className), children: [
2185
+ picked.map((recordId) => /* @__PURE__ */ jsx11(
2186
+ RecordChip,
2187
+ {
2188
+ title: nameOfRecord(recordId),
2189
+ ...disabled ? {} : { onRemove: () => drop(recordId) }
2190
+ },
2191
+ recordId
2192
+ )),
2193
+ disabled ? null : /* @__PURE__ */ jsxs9(Popover2, { open, onOpenChange: setOpen, children: [
2194
+ /* @__PURE__ */ jsx11(PopoverTrigger2, { asChild: true, children: /* @__PURE__ */ jsx11(
2195
+ Button7,
2196
+ {
2197
+ id,
2198
+ type: "button",
2199
+ size: "sm",
2200
+ variant: "outline",
2201
+ "aria-label": `Pick a person for ${fieldName(field)}`,
2202
+ children: picked.length === 0 ? "Pick a person" : "Change"
2203
+ }
2204
+ ) }),
2205
+ /* @__PURE__ */ jsxs9(
2206
+ PopoverContent2,
2207
+ {
2208
+ sizing: "content",
2209
+ className: "p-1",
2210
+ align: "start",
2211
+ "data-matrx-cell-editor": "",
2212
+ onKeyDown: (event) => event.stopPropagation(),
2213
+ children: [
2214
+ /* @__PURE__ */ jsx11(
2215
+ BasicInput4,
2216
+ {
2217
+ value: search,
2218
+ onChange: (e) => setSearch(e.target.value),
2219
+ onKeyDown: keys.onKeyDown,
2220
+ autoFocus: true,
2221
+ autoComplete: "off",
2222
+ "aria-label": `Search people for ${fieldName(field)}`,
2223
+ placeholder: "Search people",
2224
+ className: "mb-1 h-7 text-xs"
2225
+ }
2226
+ ),
2227
+ error ? /* @__PURE__ */ jsx11(RefusalLine, { error, className: "p-2" }) : null,
2228
+ members === null ? /* @__PURE__ */ jsx11("p", { className: "p-2 text-xs text-muted-foreground", children: "Reading the members\u2026" }) : null,
2229
+ /* @__PURE__ */ jsxs9(ScrollArea2, { className: "max-h-56", children: [
2230
+ /* @__PURE__ */ jsx11("ul", { role: "listbox", "aria-label": `People for ${fieldName(field)}`, children: visible.map((member, index) => /* @__PURE__ */ jsx11("li", { children: /* @__PURE__ */ jsxs9(
2231
+ "button",
2232
+ {
2233
+ type: "button",
2234
+ disabled: busy,
2235
+ "data-pick-highlighted": index === keys.highlight ? "" : void 0,
2236
+ onClick: () => void pick(member),
2237
+ className: cn8(
2238
+ "w-full truncate rounded px-2 py-1 text-left text-xs hover:bg-muted",
2239
+ pickedUserIds.has(member.userId) ? "bg-accent text-accent-foreground" : "",
2240
+ index === keys.highlight ? "ring-1 ring-ring" : ""
2241
+ ),
2242
+ children: [
2243
+ /* @__PURE__ */ jsx11("span", { children: memberName(member) }),
2244
+ member.email && member.email !== memberName(member) ? /* @__PURE__ */ jsxs9("span", { className: "ml-1.5 opacity-60", children: [
2245
+ "\xB7 ",
2246
+ member.email
2247
+ ] }) : null
2248
+ ]
2249
+ }
2250
+ ) }, member.userId)) }),
2251
+ members !== null && visible.length === 0 ? /* @__PURE__ */ jsx11("p", { className: "p-2 text-xs text-muted-foreground", children: members.length === 0 ? "This organization has no other members yet, so there is nobody to assign." : "Nobody in this organization matches." }) : null
2252
+ ] })
2253
+ ]
2254
+ }
2255
+ )
2256
+ ] })
2257
+ ] });
2258
+ }
2259
+
2260
+ // src/presentation.ts
2261
+ import {
2262
+ STYLE_COLORS,
2263
+ parseTableStyle,
2264
+ tableStyleIsEmpty
2265
+ } from "@ai-matrx/design-system/data-table";
2266
+ import { getFieldFormat } from "@ai-matrx/design-system/field-formats";
2267
+ import { newerKindOf as newerKindOf3, ratingMaxOf } from "@ai-matrx/records";
2268
+ var DRAWN_KINDS = /* @__PURE__ */ new Set(["select", "multi_select", "relation", "member", "person", "attachment", "file", "entity_reference", "checkbox", "boolean", "signature", "json"]);
2269
+ function shownAsOf(field) {
2270
+ if (DRAWN_KINDS.has(String(editorKindFor(field)))) return void 0;
2271
+ const raw = field.display_format;
2272
+ if (!raw || typeof raw !== "object") return void 0;
2273
+ const id = raw.id;
2274
+ if (typeof id !== "string" || id === "formula" || !getFieldFormat(id)) return void 0;
2275
+ const options = raw.options;
2276
+ return options && typeof options === "object" && Object.keys(options).length > 0 ? { id, options } : { id };
2277
+ }
2278
+ function formatForField(field) {
2279
+ const shownAs = shownAsOf(field);
2280
+ if (newerKindOf3(field) === "rating" && (!shownAs || shownAs.id === "rating")) {
2281
+ return { id: "rating", options: { ...shownAs?.options ?? {}, ratingMax: ratingMaxOf(field) } };
2282
+ }
2283
+ if (shownAs) return shownAs;
2284
+ const stamp = dateFormatFor(field);
2285
+ if (stamp && editorKindFor(field) !== "datetime") return stamp;
2286
+ switch (editorKindFor(field)) {
2287
+ case "currency":
2288
+ return {
2289
+ id: "currency",
2290
+ options: { currency: currencyCodeFor(field.unit) }
2291
+ };
2292
+ case "percent":
2293
+ return { id: "percent" };
2294
+ case "datetime":
2295
+ return String(field.config?.["kind"] ?? "") === "date" ? { id: "date" } : { id: "datetime" };
2296
+ case "email":
2297
+ return { id: "email" };
2298
+ case "url":
2299
+ return { id: "url" };
2300
+ case "phone":
2301
+ return { id: "phone" };
2302
+ case "number":
2303
+ return { id: "number" };
2304
+ default:
2305
+ return void 0;
2306
+ }
2307
+ }
2308
+ var SYMBOL_TO_CODE = {
2309
+ $: "USD",
2310
+ "\xA3": "GBP",
2311
+ "\u20AC": "EUR",
2312
+ "\xA5": "JPY",
2313
+ "\u20B9": "INR",
2314
+ "\u20BD": "RUB",
2315
+ "\u20A9": "KRW",
2316
+ "R$": "BRL",
2317
+ "C$": "CAD",
2318
+ "A$": "AUD"
2319
+ };
2320
+ function currencyCodeFor(unit) {
2321
+ const raw = (unit ?? "").trim();
2322
+ if (raw === "") return "USD";
2323
+ return SYMBOL_TO_CODE[raw] ?? raw.toUpperCase();
2324
+ }
2325
+
2326
+ // src/formatControls.tsx
2327
+ import { useEffect as useEffect8, useRef as useRef8, useState as useState10 } from "react";
2328
+ import { BasicInput as BasicInput5, BasicTextarea } from "@ai-matrx/design-system";
2329
+ import { getFieldFormat as getFieldFormat3, parseFieldInput } from "@ai-matrx/design-system/field-formats";
2330
+
2331
+ // src/formattedValue.tsx
2332
+ import { useRef as useRef7 } from "react";
2333
+ import { FormattedCell } from "@ai-matrx/design-system/data-table";
2334
+ import { formatFieldValue as formatFieldValue2, getFieldFormat as getFieldFormat2 } from "@ai-matrx/design-system/field-formats";
2335
+ import { cn as cn9 } from "@ai-matrx/design-system";
2336
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2337
+ var LINK = "inline-flex min-w-0 items-center gap-1 text-primary hover:underline";
2338
+ var own2 = (event) => event.stopPropagation();
2339
+ function numberOf(value) {
2340
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
2341
+ if (typeof value === "string" && value.trim() !== "") {
2342
+ const n = Number(value.replace(/[,\s%]/g, ""));
2343
+ return Number.isFinite(n) ? n : null;
2344
+ }
2345
+ return null;
2346
+ }
2347
+ function RatingStars({
2348
+ value,
2349
+ max = 5,
2350
+ onRate,
2351
+ label,
2352
+ selectFirst
2353
+ }) {
2354
+ const armed = useRef7(true);
2355
+ const score = numberOf(value) ?? 0;
2356
+ const shown2 = Math.round(score);
2357
+ const said = `${shown2} out of ${max}`;
2358
+ return /* @__PURE__ */ jsx12(
2359
+ "span",
2360
+ {
2361
+ "data-records-rating": "",
2362
+ role: onRate ? "radiogroup" : "img",
2363
+ "aria-label": label ? `${label}: ${said}` : said,
2364
+ title: said,
2365
+ className: "inline-flex items-center gap-0.5",
2366
+ ...onRate ? { "data-matrx-cell-control": "" } : {},
2367
+ children: Array.from({ length: max }, (_, i) => {
2368
+ const n = i + 1;
2369
+ const lit = n <= shown2;
2370
+ const star = /* @__PURE__ */ jsx12("span", { "aria-hidden": true, className: cn9("text-sm leading-none", lit ? "text-amber-400" : "text-muted-foreground/40"), children: "\u2605" });
2371
+ return onRate ? /* @__PURE__ */ jsx12(
2372
+ "button",
2373
+ {
2374
+ type: "button",
2375
+ role: "radio",
2376
+ "aria-checked": n === shown2,
2377
+ "aria-label": `${n} out of ${max}`,
2378
+ "data-records-rating-star": n,
2379
+ className: "rounded px-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
2380
+ onMouseDown: (event) => {
2381
+ if (!selectFirst) {
2382
+ own2(event);
2383
+ return;
2384
+ }
2385
+ armed.current = event.currentTarget.closest("td")?.hasAttribute("data-matrx-cell-active") === true;
2386
+ },
2387
+ onDoubleClick: own2,
2388
+ onClick: (event) => {
2389
+ event.stopPropagation();
2390
+ if (selectFirst && event.detail !== 0 && !armed.current) return;
2391
+ onRate(n === shown2 ? null : n);
2392
+ },
2393
+ children: star
2394
+ },
2395
+ n
2396
+ ) : /* @__PURE__ */ jsx12("span", { children: star }, n);
2397
+ })
2398
+ }
2399
+ );
2400
+ }
2401
+ function rich(id, text, raw, format) {
2402
+ switch (id) {
2403
+ case "email":
2404
+ return /* @__PURE__ */ jsx12("a", { href: `mailto:${text}`, className: LINK, onClick: own2, children: /* @__PURE__ */ jsx12("span", { className: "truncate", children: text }) });
2405
+ case "url":
2406
+ return /* @__PURE__ */ jsx12("a", { href: text.includes("://") ? text : `https://${text}`, target: "_blank", rel: "noopener noreferrer", className: LINK, onClick: own2, children: /* @__PURE__ */ jsx12("span", { className: "truncate", children: text }) });
2407
+ case "phone":
2408
+ return /* @__PURE__ */ jsx12("a", { href: `tel:${text.replace(/[^\d+]/g, "")}`, className: LINK, onClick: own2, title: text, children: /* @__PURE__ */ jsx12("span", { className: "truncate tabular-nums", children: formatPhone(text) }) });
2409
+ case "address":
2410
+ return /* @__PURE__ */ jsx12(
2411
+ "a",
2412
+ {
2413
+ href: `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(text)}`,
2414
+ target: "_blank",
2415
+ rel: "noopener noreferrer",
2416
+ className: LINK,
2417
+ title: "Open in maps",
2418
+ onClick: own2,
2419
+ children: /* @__PURE__ */ jsx12("span", { className: "truncate", children: text })
2420
+ }
2421
+ );
2422
+ case "progress": {
2423
+ const n = numberOf(raw);
2424
+ if (n === null) return void 0;
2425
+ const pct = format.options?.percentScale === "fraction" ? n * 100 : n;
2426
+ const clamped = Math.max(0, Math.min(100, pct));
2427
+ return /* @__PURE__ */ jsxs10("span", { className: "inline-flex w-full min-w-0 items-center gap-2", title: text, children: [
2428
+ /* @__PURE__ */ jsx12(
2429
+ "span",
2430
+ {
2431
+ role: "progressbar",
2432
+ "aria-valuemin": 0,
2433
+ "aria-valuemax": 100,
2434
+ "aria-valuenow": Math.round(clamped),
2435
+ className: "h-1.5 min-w-8 flex-1 overflow-hidden rounded-full bg-muted",
2436
+ children: /* @__PURE__ */ jsx12("span", { className: cn9("block h-full rounded-full", pct >= 100 ? "bg-emerald-500" : "bg-primary"), style: { width: `${clamped}%` } })
2437
+ }
2438
+ ),
2439
+ /* @__PURE__ */ jsx12("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: text })
2440
+ ] });
2441
+ }
2442
+ case "color":
2443
+ return /* @__PURE__ */ jsxs10("span", { className: "inline-flex items-center gap-1.5", children: [
2444
+ /* @__PURE__ */ jsx12("span", { className: "h-4 w-4 shrink-0 rounded border border-border", style: { backgroundColor: text } }),
2445
+ /* @__PURE__ */ jsx12("code", { className: "font-mono text-xs", children: text })
2446
+ ] });
2447
+ case "tags": {
2448
+ const items = Array.isArray(raw) ? raw.map(String) : text.split(",").map((s) => s.trim()).filter(Boolean);
2449
+ if (items.length === 0) return void 0;
2450
+ return /* @__PURE__ */ jsx12("span", { className: "flex flex-wrap items-center gap-1", children: items.map((item, i) => /* @__PURE__ */ jsx12("span", { className: "inline-flex items-center rounded-md border border-border bg-muted px-1.5 py-0.5 text-xs", children: item }, `${item}-${i}`)) });
2451
+ }
2452
+ case "rating":
2453
+ return numberOf(raw) === null ? void 0 : /* @__PURE__ */ jsx12(RatingStars, { value: raw, max: format.options?.ratingMax ?? 5 });
2454
+ default:
2455
+ return void 0;
2456
+ }
2457
+ }
2458
+ function FormattedValue({
2459
+ value,
2460
+ format,
2461
+ onRate,
2462
+ selectFirst
2463
+ }) {
2464
+ const def = getFieldFormat2(format.id);
2465
+ const score = numberOf(value);
2466
+ const max = format.options?.ratingMax ?? 5;
2467
+ if (def?.id === "rating" && onRate && (value === null || value === void 0 || value === "" || score !== null && score >= 0 && score <= max)) {
2468
+ return /* @__PURE__ */ jsx12(RatingStars, { value, max: format.options?.ratingMax ?? 5, onRate, selectFirst });
2469
+ }
2470
+ const result = formatFieldValue2(value, format);
2471
+ if (!result.ok || result.empty || !def?.rich) return /* @__PURE__ */ jsx12(FormattedCell, { result });
2472
+ return rich(def.id, result.text, value, format) ?? /* @__PURE__ */ jsx12(FormattedCell, { result });
2473
+ }
2474
+ function formatPhone(raw) {
2475
+ const text = raw.trim();
2476
+ const digits = text.replace(/\D/g, "");
2477
+ if (/[a-z]/i.test(text.replace(/ext\.?|x/gi, ""))) return text;
2478
+ if (digits.length === 10 && !text.startsWith("+")) return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
2479
+ if (digits.length === 11 && digits.startsWith("1") && (text.startsWith("+1") || text.startsWith("1") || !text.startsWith("+"))) {
2480
+ return `+1 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7)}`;
2481
+ }
2482
+ return text;
2483
+ }
2484
+ function linkChipText(raw) {
2485
+ try {
2486
+ const url = new URL(raw.includes("://") ? raw : `https://${raw}`);
2487
+ const host = url.hostname.replace(/^www\./, "");
2488
+ const last = url.pathname.split("/").filter(Boolean).pop();
2489
+ return last ? `${host} \xB7 ${decodeURIComponent(last)}` : host;
2490
+ } catch {
2491
+ return raw;
2492
+ }
2493
+ }
2494
+
2495
+ // src/formatControls.tsx
2496
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2497
+ var TYPED_NUMBERS = /* @__PURE__ */ new Set(["currency", "percent", "progress", "decimal", "duration", "file_size", "integer"]);
2498
+ function scaled(format) {
2499
+ return (format.id === "percent" || format.id === "progress") && format.options?.percentScale === "fraction";
2500
+ }
2501
+ function typedText(format, value) {
2502
+ if (value === null || value === void 0) return "";
2503
+ if (typeof value === "number" && scaled(format)) return String(Math.round(value * 100 * 1e6) / 1e6);
2504
+ return String(value);
2505
+ }
2506
+ function keptValue(format, raw) {
2507
+ if (raw.trim() === "") return null;
2508
+ const parsed = parseFieldInput(raw, format);
2509
+ if (typeof parsed === "number" && scaled(format)) return Math.round(parsed / 100 * 1e9) / 1e9;
2510
+ return parsed;
2511
+ }
2512
+ function NumberInFormat({ format, value, onChange, disabled, id }) {
2513
+ const [text, setText] = useState10(() => typedText(format, value));
2514
+ const lastSent = useRef8(void 0);
2515
+ useEffect8(() => {
2516
+ if (value === lastSent.current) return;
2517
+ setText(typedText(format, value));
2518
+ }, [value, format]);
2519
+ return /* @__PURE__ */ jsx13(
2520
+ BasicInput5,
2521
+ {
2522
+ id,
2523
+ inputMode: "decimal",
2524
+ className: "tabular-nums",
2525
+ value: text,
2526
+ disabled,
2527
+ placeholder: format.id === "currency" ? "$0.00" : format.id === "percent" || format.id === "progress" ? "45%" : void 0,
2528
+ onChange: (e) => {
2529
+ setText(e.target.value);
2530
+ const next = keptValue(format, e.target.value);
2531
+ lastSent.current = next;
2532
+ onChange(next);
2533
+ }
2534
+ }
2535
+ );
2536
+ }
2537
+ function formatControl(args) {
2538
+ const def = getFieldFormat3(args.format.id);
2539
+ if (!def) return null;
2540
+ const { format, value, onChange, disabled, id, label, ownDetails } = args;
2541
+ const text = value === null || value === void 0 ? "" : String(value);
2542
+ const commit = (raw) => onChange(raw === "" ? null : parseFieldInput(raw, format));
2543
+ if (def.id === "rating") {
2544
+ return disabled ? /* @__PURE__ */ jsx13(RatingStars, { value, max: format.options?.ratingMax ?? 5, label }) : /* @__PURE__ */ jsx13("span", { id, className: "inline-flex", children: /* @__PURE__ */ jsx13(RatingStars, { value, max: format.options?.ratingMax ?? 5, label, onRate: (n) => onChange(n) }) });
2545
+ }
2546
+ if (TYPED_NUMBERS.has(def.id)) return /* @__PURE__ */ jsx13(NumberInFormat, { ...args });
2547
+ switch (def.editor) {
2548
+ // A TIME OF DAY is not this file's: `FieldControl` draws the typed box and its helper
2549
+ // (`TypedWordsControl`), never the browser's time control (grids review 3, 2026-09-30).
2550
+ case "email":
2551
+ case "url":
2552
+ case "tel":
2553
+ return /* @__PURE__ */ jsx13(
2554
+ BasicInput5,
2555
+ {
2556
+ id,
2557
+ type: def.editor,
2558
+ ...ownDetails && def.editor !== "url" ? { autoComplete: def.editor === "email" ? "email" : "tel" } : {},
2559
+ value: text,
2560
+ disabled,
2561
+ onChange: (e) => commit(e.target.value)
2562
+ }
2563
+ );
2564
+ case "color":
2565
+ return /* @__PURE__ */ jsxs11("span", { className: "flex items-center gap-2", children: [
2566
+ /* @__PURE__ */ jsx13(
2567
+ BasicInput5,
2568
+ {
2569
+ id: `${id}-swatch`,
2570
+ type: "color",
2571
+ "aria-label": `${label} \u2014 pick a colour`,
2572
+ className: "h-8 w-12 p-1",
2573
+ value: /^#[0-9a-fA-F]{6}$/.test(text) ? text : "#000000",
2574
+ disabled,
2575
+ onChange: (e) => commit(e.target.value)
2576
+ }
2577
+ ),
2578
+ /* @__PURE__ */ jsx13(BasicInput5, { id, value: text, placeholder: "#3b82f6", disabled, onChange: (e) => commit(e.target.value) })
2579
+ ] });
2580
+ case "textarea":
2581
+ return /* @__PURE__ */ jsx13(BasicTextarea, { id, rows: 4, value: text, disabled, onChange: (e) => commit(e.target.value) });
2582
+ default:
2583
+ return null;
2584
+ }
2585
+ }
2586
+
2587
+ // src/typedDateHelper.tsx
2588
+ import { useState as useState11 } from "react";
2589
+ import { Popover as Popover3, PopoverContent as PopoverContent3, PopoverTrigger as PopoverTrigger3, cn as cn10 } from "@ai-matrx/design-system";
2590
+ import { readTypedDate, readTypedTime, typedDateText, typedTimeText } from "@ai-matrx/records";
2591
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2592
+ var WEEK = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
2593
+ var MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
2594
+ function pad(n) {
2595
+ return String(n).padStart(2, "0");
2596
+ }
2597
+ function readBox(text, kind) {
2598
+ if (kind === "time") {
2599
+ const t = readTypedTime(text);
2600
+ if (!t.ok) return { day: null, time: null };
2601
+ const [h, m] = t.value.split(":").map(Number);
2602
+ return { day: null, time: { h, m } };
2603
+ }
2604
+ const read = readTypedDate(text, { withTime: kind === "datetime" });
2605
+ if (!read.ok) return { day: null, time: null };
2606
+ if (kind === "datetime" && read.instant) {
2607
+ const at = new Date(read.instant);
2608
+ return { day: read.day, time: { h: at.getHours(), m: at.getMinutes() } };
2609
+ }
2610
+ return { day: read.day, time: null };
2611
+ }
2612
+ function wordsFor(kind, day, time) {
2613
+ if (kind === "time") return time ? typedTimeText(time.h, time.m) : "";
2614
+ if (!day) return "";
2615
+ const dayText = typedDateText(day);
2616
+ return kind === "datetime" && time ? `${dayText} ${typedTimeText(time.h, time.m)}` : dayText;
2617
+ }
2618
+ var keepFocus = { onMouseDown: (e) => e.preventDefault(), tabIndex: -1 };
2619
+ function CalendarGlyph() {
2620
+ return /* @__PURE__ */ jsxs12("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", width: "14", height: "14", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2621
+ /* @__PURE__ */ jsx14("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
2622
+ /* @__PURE__ */ jsx14("path", { d: "M16 2v4M8 2v4M3 10h18" })
2623
+ ] });
2624
+ }
2625
+ function ClockGlyph() {
2626
+ return /* @__PURE__ */ jsxs12("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", width: "14", height: "14", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2627
+ /* @__PURE__ */ jsx14("circle", { cx: "12", cy: "12", r: "10" }),
2628
+ /* @__PURE__ */ jsx14("path", { d: "M12 6v6l4 2" })
2629
+ ] });
2630
+ }
2631
+ function TypedDateHelper({ kind, text, onText, label }) {
2632
+ const [open, setOpen] = useState11(false);
2633
+ const held = readBox(text, kind);
2634
+ const today = /* @__PURE__ */ new Date();
2635
+ const start = held.day ? new Date(Number(held.day.slice(0, 4)), Number(held.day.slice(5, 7)) - 1, 1) : new Date(today.getFullYear(), today.getMonth(), 1);
2636
+ const [month, setMonth] = useState11(null);
2637
+ const shown2 = month ?? { y: start.getFullYear(), m: start.getMonth() };
2638
+ const first = new Date(shown2.y, shown2.m, 1).getDay();
2639
+ const days = new Date(shown2.y, shown2.m + 1, 0).getDate();
2640
+ const cells = [...Array.from({ length: first }, () => null), ...Array.from({ length: days }, (_, i) => i + 1)];
2641
+ const todayKey = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
2642
+ const times = Array.from({ length: 48 }, (_, i) => ({ h: Math.floor(i / 2), m: i % 2 * 30 }));
2643
+ const pickDay = (d) => {
2644
+ const day = `${shown2.y}-${pad(shown2.m + 1)}-${pad(d)}`;
2645
+ onText(wordsFor(kind, day, held.time));
2646
+ if (kind === "date") setOpen(false);
2647
+ };
2648
+ const pickTime = (t) => {
2649
+ const day = kind === "datetime" ? held.day ?? todayKey : null;
2650
+ onText(wordsFor(kind, day, t));
2651
+ setOpen(false);
2652
+ };
2653
+ const turn = (by) => {
2654
+ const at = new Date(shown2.y, shown2.m + by, 1);
2655
+ setMonth({ y: at.getFullYear(), m: at.getMonth() });
2656
+ };
2657
+ return /* @__PURE__ */ jsxs12(Popover3, { open, onOpenChange: setOpen, children: [
2658
+ /* @__PURE__ */ jsx14(PopoverTrigger3, { asChild: true, children: /* @__PURE__ */ jsx14(
2659
+ "button",
2660
+ {
2661
+ type: "button",
2662
+ ...keepFocus,
2663
+ "data-records-date-helper": "",
2664
+ "aria-label": kind === "time" ? `Pick a time for ${label}` : `Pick a date for ${label}`,
2665
+ title: kind === "time" ? "Pick a time" : "Pick a date",
2666
+ className: "inline-flex h-7 w-7 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground",
2667
+ children: kind === "time" ? /* @__PURE__ */ jsx14(ClockGlyph, {}) : /* @__PURE__ */ jsx14(CalendarGlyph, {})
2668
+ }
2669
+ ) }),
2670
+ /* @__PURE__ */ jsx14(
2671
+ PopoverContent3,
2672
+ {
2673
+ sizing: "content",
2674
+ align: "start",
2675
+ className: "p-2",
2676
+ "data-matrx-cell-editor": "",
2677
+ "data-records-date-helper-panel": "",
2678
+ onOpenAutoFocus: (e) => e.preventDefault(),
2679
+ onCloseAutoFocus: (e) => e.preventDefault(),
2680
+ children: /* @__PURE__ */ jsxs12("div", { className: "flex gap-2", children: [
2681
+ kind === "time" ? null : /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-1", role: "grid", "aria-label": `${MONTHS[shown2.m]} ${shown2.y}`, children: [
2682
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between text-xs font-medium", children: [
2683
+ /* @__PURE__ */ jsx14("button", { type: "button", ...keepFocus, "aria-label": "Previous month", className: "rounded px-1.5 hover:bg-muted", onClick: () => turn(-1), children: "\u2039" }),
2684
+ /* @__PURE__ */ jsxs12("span", { children: [
2685
+ MONTHS[shown2.m],
2686
+ " ",
2687
+ shown2.y
2688
+ ] }),
2689
+ /* @__PURE__ */ jsx14("button", { type: "button", ...keepFocus, "aria-label": "Next month", className: "rounded px-1.5 hover:bg-muted", onClick: () => turn(1), children: "\u203A" })
2690
+ ] }),
2691
+ /* @__PURE__ */ jsxs12("div", { className: "grid grid-cols-7 gap-0.5 text-center text-[11px]", children: [
2692
+ WEEK.map((w) => /* @__PURE__ */ jsx14("span", { className: "text-muted-foreground", children: w }, w)),
2693
+ cells.map((d, i) => {
2694
+ if (d === null) return /* @__PURE__ */ jsx14("span", {}, `blank-${i}`);
2695
+ const key = `${shown2.y}-${pad(shown2.m + 1)}-${pad(d)}`;
2696
+ return /* @__PURE__ */ jsx14(
2697
+ "button",
2698
+ {
2699
+ type: "button",
2700
+ ...keepFocus,
2701
+ "data-records-date-helper-day": key,
2702
+ className: cn10(
2703
+ "h-6 w-6 rounded tabular-nums hover:bg-muted",
2704
+ key === held.day ? "bg-primary text-primary-foreground hover:bg-primary" : "",
2705
+ key === todayKey && key !== held.day ? "ring-1 ring-ring" : ""
2706
+ ),
2707
+ onClick: () => pickDay(d),
2708
+ children: d
2709
+ },
2710
+ key
2711
+ );
2712
+ })
2713
+ ] })
2714
+ ] }),
2715
+ kind === "date" ? null : /* @__PURE__ */ jsx14("div", { className: "flex max-h-48 flex-col overflow-y-auto pr-1 text-[11px]", "aria-label": "Times", children: times.map((t) => /* @__PURE__ */ jsx14(
2716
+ "button",
2717
+ {
2718
+ type: "button",
2719
+ ...keepFocus,
2720
+ "data-records-date-helper-time": `${pad(t.h)}:${pad(t.m)}`,
2721
+ className: cn10(
2722
+ "whitespace-nowrap rounded px-2 py-0.5 text-left tabular-nums hover:bg-muted",
2723
+ held.time && held.time.h === t.h && held.time.m === t.m ? "bg-primary text-primary-foreground" : ""
2724
+ ),
2725
+ onClick: () => pickTime(t),
2726
+ children: typedTimeText(t.h, t.m)
2727
+ },
2728
+ `${t.h}:${t.m}`
2729
+ )) })
2730
+ ] })
2731
+ }
2732
+ )
2733
+ ] });
2734
+ }
2735
+
2736
+ // src/choiceNudge.ts
2737
+ import { useEffect as useEffect9, useState as useState12 } from "react";
2738
+ import { useOptionalRecordsClient as useOptionalRecordsClient5 } from "@ai-matrx/records/react";
2739
+ var asked = /* @__PURE__ */ new WeakMap();
2740
+ function choiceNudgeOf(client) {
2741
+ let answer = asked.get(client);
2742
+ if (!answer) {
2743
+ answer = Promise.resolve().then(() => client.choiceNudge()).then(
2744
+ (result) => result.ok ? result.data : "ask",
2745
+ () => "ask"
2746
+ );
2747
+ asked.set(client, answer);
2748
+ }
2749
+ return answer;
2750
+ }
2751
+ function useChoiceNudge(enabled = true) {
2752
+ const client = useOptionalRecordsClient5();
2753
+ const [nudge, setNudge] = useState12(null);
2754
+ useEffect9(() => {
2755
+ if (!enabled || !client) return;
2756
+ let live = true;
2757
+ void choiceNudgeOf(client).then((answer) => {
2758
+ if (live) setNudge(answer);
2759
+ });
2760
+ return () => {
2761
+ live = false;
2762
+ };
2763
+ }, [client, enabled]);
2764
+ if (!client) return "ask";
2765
+ return nudge;
2766
+ }
2767
+
2768
+ // src/choiceLooks.ts
2769
+ import {
2770
+ CHOICE_CHIP_CLASS,
2771
+ NEUTRAL_CHIP_CLASS,
2772
+ STYLE_COLORS as STYLE_COLORS2
2773
+ } from "@ai-matrx/design-system/data-table";
2774
+ function displayFormatOf(field) {
2775
+ const raw = field.display_format;
2776
+ if (!raw || typeof raw !== "object") return null;
2777
+ const id = raw.id;
2778
+ if (typeof id !== "string" || id === "") return null;
2779
+ const options = raw.options;
2780
+ return { id, options: options && typeof options === "object" ? options : {} };
2781
+ }
2782
+ function isStyleColor(v) {
2783
+ return typeof v === "string" && STYLE_COLORS2.includes(v);
2784
+ }
2785
+ function choiceLooksOf(field) {
2786
+ const display = displayFormatOf(field);
2787
+ const choices = display?.options["choices"];
2788
+ if (!Array.isArray(choices)) return [];
2789
+ const out = [];
2790
+ for (const c of choices) {
2791
+ if (!c || typeof c !== "object") continue;
2792
+ const value = c.value;
2793
+ if (typeof value !== "string" || value.trim() === "") continue;
2794
+ const color = c.color;
2795
+ const label = c.label;
2796
+ out.push({
2797
+ value: value.trim(),
2798
+ ...typeof label === "string" && label.trim() !== "" ? { label: label.trim() } : {},
2799
+ ...isStyleColor(color) ? { color } : {}
2800
+ });
2801
+ }
2802
+ return out;
2803
+ }
2804
+ function choiceWord(value) {
2805
+ return String(value ?? "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
2806
+ }
2807
+ function storedChoiceColor(field, value) {
2808
+ if (value === null || value === void 0 || value === "") return void 0;
2809
+ const word = choiceWord(value);
2810
+ if (word === "") return void 0;
2811
+ for (const look of choiceLooksOf(field)) {
2812
+ if (!look.color) continue;
2813
+ if (choiceWord(look.value) === word || look.label && choiceWord(look.label) === word) return look.color;
2814
+ }
2815
+ return void 0;
2816
+ }
2817
+ function choiceChipLookOf(field, ...words) {
2818
+ for (const word of words) {
2819
+ const color = storedChoiceColor(field, word);
2820
+ if (color) return { className: CHOICE_CHIP_CLASS[color], color };
2821
+ }
2822
+ return { className: NEUTRAL_CHIP_CLASS, color: null };
2823
+ }
2824
+
2825
+ // src/editors.tsx
2826
+ import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
2827
+ var CHOICE_EDITOR_ADDS_WHAT_IS_TYPED = true;
2828
+ function isChoiceAddition(value) {
2829
+ return typeof value === "object" && value !== null && value.addingChoices === true;
2830
+ }
2831
+ function choiceAddition(value, add) {
2832
+ return { addingChoices: true, value, add: [...add] };
2833
+ }
2834
+ function withoutAddition(value) {
2835
+ return isChoiceAddition(value) ? { value: value.value, add: value.add } : { value, add: [] };
2836
+ }
2837
+ function quoted(words) {
2838
+ const q = words.map((w) => `\u201C${w}\u201D`);
2839
+ return q.length <= 1 ? q[0] ?? "" : `${q.slice(0, -1).join(", ")} and ${q[q.length - 1]}`;
2840
+ }
2841
+ function choiceAskSentence(field, words) {
2842
+ return `Add ${quoted(words)} to the choices for ${fieldName(field)}?`;
2843
+ }
2844
+ function notAChoiceSentence(field, words) {
2845
+ return `${quoted(words)} ${words.length === 1 ? "is" : "are"} not one of the choices for ${fieldName(field)}.`;
2846
+ }
2847
+ function ChoiceAskButtons({
2848
+ field,
2849
+ words,
2850
+ keepOffered,
2851
+ onAdd,
2852
+ onKeep,
2853
+ onCancel,
2854
+ busy
2855
+ }) {
2856
+ return /* @__PURE__ */ jsxs13("span", { "data-records-choice-ask": "", role: "group", "aria-label": choiceAskSentence(field, words), className: "flex flex-col gap-1", children: [
2857
+ /* @__PURE__ */ jsx15("span", { className: "text-[11px] leading-snug", children: choiceAskSentence(field, words) }),
2858
+ /* @__PURE__ */ jsxs13("span", { className: "flex flex-wrap items-center gap-1", children: [
2859
+ /* @__PURE__ */ jsx15(Button8, { type: "button", size: "sm", className: "h-6 px-2 text-xs", disabled: busy ?? false, onClick: onAdd, children: "Add" }),
2860
+ keepOffered ? /* @__PURE__ */ jsx15(Button8, { type: "button", size: "sm", variant: "outline", className: "h-6 px-2 text-xs", disabled: busy ?? false, onClick: onKeep, children: "Keep as typed" }) : null,
2861
+ /* @__PURE__ */ jsx15(Button8, { type: "button", size: "sm", variant: "ghost", className: "h-6 px-2 text-xs", disabled: busy ?? false, onClick: onCancel, children: "Cancel" })
2862
+ ] })
2863
+ ] });
2864
+ }
2865
+ var FORMAT_BLIND_KINDS = /* @__PURE__ */ new Set(["formula", "lookup", "rollup", "json", "checkbox", "select", "multi_select", "member", "entity_reference", "relation", "attachment", "datetime"]);
2866
+ function isTypedDraft(value) {
2867
+ return typeof value === "object" && value !== null && value.typedDraft === true;
2868
+ }
2869
+ function judgeTyped(field, text, choices, nudge) {
2870
+ const trimmed = text.trim();
2871
+ const kind = String(editorKindFor(field));
2872
+ const format = formatForField(field);
2873
+ if (trimmed === "") return { value: null };
2874
+ if (format && format.id === "time") {
2875
+ const read2 = readTypedTime2(trimmed);
2876
+ return read2.ok ? { value: read2.value } : { refusal: `${fieldName(field)} holds a time of day, and ${read2.why}` };
2877
+ }
2878
+ if (kind === "select" || kind === "multi_select") {
2879
+ if (!choices || nudge === null) return null;
2880
+ const several = field.multi || kind === "multi_select";
2881
+ const words = several ? trimmed.split(/\s*[,;\n]\s*/).filter(Boolean) : [trimmed];
2882
+ const allowOther = allowsOtherValues(field);
2883
+ const parts = [];
2884
+ const add = [];
2885
+ const ask = [];
2886
+ const notChoices = [];
2887
+ let keepOffered = allowOther;
2888
+ for (const word of words) {
2889
+ const decided = decideTypedChoice({ words: word, options: choices, allowOther, nudge });
2890
+ if (decided.decision === "empty") continue;
2891
+ if (decided.decision === "chosen") parts.push(decided.key);
2892
+ else {
2893
+ parts.push(decided.words);
2894
+ if (decided.decision === "add") add.push(decided.words);
2895
+ else if (decided.decision === "ask") {
2896
+ ask.push(decided.words);
2897
+ keepOffered = keepOffered && decided.keepOffered;
2898
+ } else if (decided.decision === "not_a_choice") notChoices.push(decided.words);
2899
+ }
2900
+ }
2901
+ const valueOf = (list) => several ? list : list[0] ?? null;
2902
+ if (notChoices.length > 0) return { refusal: notAChoiceSentence(field, notChoices), cancelOnly: true };
2903
+ if (ask.length > 0) {
2904
+ return {
2905
+ ask: { words: ask, keepOffered, onAdd: choiceAddition(valueOf(parts), [...add, ...ask]), asTyped: valueOf(parts) }
2906
+ };
2907
+ }
2908
+ if (add.length > 0) return { value: choiceAddition(valueOf(parts), add) };
2909
+ return { value: valueOf(parts) };
2910
+ }
2911
+ if (format && format.id === "rating") {
2912
+ const n = Number(trimmed);
2913
+ const max = format.options?.ratingMax ?? 5;
2914
+ return Number.isInteger(n) && n >= 0 && n <= max ? { value: n === 0 ? null : n } : { refusal: `${fieldName(field)} is a rating from 1 to ${max}, and \u201C${text}\u201D is not one.` };
2915
+ }
2916
+ const showsAsPercent = Boolean(format && (format.id === "percent" || format.id === "progress"));
2917
+ const read = coerceTypedAnswer(field, text, { label: fieldName(field), showsAsPercent });
2918
+ if ("refusal" in read) return { refusal: read.refusal };
2919
+ if (typeof read.value === "number" && format && (format.id === "percent" || format.id === "progress") && format.options?.percentScale === "fraction") {
2920
+ return { value: Math.round(read.value / 100 * 1e9) / 1e9 };
2921
+ }
2922
+ return { value: read.value };
2923
+ }
2924
+ function dateHelperKind(field, value) {
2925
+ const format = formatForField(field);
2926
+ if (format && format.id === "time") return "time";
2927
+ if (String(editorKindFor(field)) !== "datetime") return null;
2928
+ return String(field.config?.["kind"] ?? "") === "datetime" || carriesTimeOfDay(field, value) ? "datetime" : "date";
2929
+ }
2930
+ function openingText(field, value) {
2931
+ if (value === null || value === void 0 || value === "") return "";
2932
+ const helper = dateHelperKind(field, value);
2933
+ if (helper === "time") {
2934
+ const read = readTypedTime2(String(value));
2935
+ if (!read.ok) return String(value);
2936
+ const [h, m] = read.value.split(":").map(Number);
2937
+ return typedTimeText2(h, m);
2938
+ }
2939
+ if (helper) return typedDateText2(value, { withTime: helper === "datetime" });
2940
+ const format = formatForField(field);
2941
+ return format ? typedText(format, value) : String(value);
2942
+ }
2943
+ function TypedTextControl({ field, value, onChange, disabled, id, onCommit, onCancel }) {
2944
+ const client = useOptionalRecordsClient6();
2945
+ const kind = String(editorKindFor(field));
2946
+ const needsChoices = kind === "select" || kind === "multi_select";
2947
+ const nudge = useChoiceNudge(needsChoices);
2948
+ const [choices, setChoices] = useState13(null);
2949
+ useEffect10(() => {
2950
+ if (!needsChoices || !client) return;
2951
+ let cancelled = false;
2952
+ void client.fieldOptions({ field_id: field.id }).then((result) => {
2953
+ if (!cancelled) setChoices(result.ok ? inTheStoresOrder(result.data ?? []) : []);
2954
+ });
2955
+ return () => {
2956
+ cancelled = true;
2957
+ };
2958
+ }, [client, field.id, needsChoices]);
2959
+ useEffect10(() => {
2960
+ if (choices && nudge && value.judged === null) onChange({ ...value, judged: judgeTyped(field, value.text, choices, nudge) });
2961
+ }, [choices, nudge, value, field, onChange]);
2962
+ const judged = value.judged;
2963
+ const format = formatForField(field);
2964
+ const helper = dateHelperKind(field);
2965
+ const retype = (text) => onChange({
2966
+ typedDraft: true,
2967
+ text,
2968
+ judged: judgeTyped(field, text, needsChoices ? choices : void 0, needsChoices ? nudge : void 0),
2969
+ ...value.openedAs !== void 0 ? { openedAs: value.openedAs } : {}
2970
+ });
2971
+ const box = /* @__PURE__ */ jsx15(
2972
+ BasicInput6,
2973
+ {
2974
+ id,
2975
+ value: value.text,
2976
+ disabled,
2977
+ autoComplete: "off",
2978
+ "aria-label": fieldName(field),
2979
+ "aria-invalid": judged !== null && "refusal" in judged ? true : void 0,
2980
+ onChange: (e) => retype(e.target.value)
2981
+ }
2982
+ );
2983
+ return /* @__PURE__ */ jsxs13("span", { className: "flex flex-col gap-0.5", children: [
2984
+ helper && !disabled ? /* @__PURE__ */ jsxs13("span", { className: "flex min-w-0 items-center gap-0.5", children: [
2985
+ box,
2986
+ /* @__PURE__ */ jsx15(TypedDateHelper, { kind: helper, text: value.text, onText: retype, label: fieldName(field) })
2987
+ ] }) : box,
2988
+ judged === null ? /* @__PURE__ */ jsxs13("span", { className: "text-[10px] text-muted-foreground", children: [
2989
+ "Reading ",
2990
+ fieldName(field),
2991
+ "'s choices\u2026"
2992
+ ] }) : "ask" in judged ? /* @__PURE__ */ jsx15(
2993
+ ChoiceAskButtons,
2994
+ {
2995
+ field,
2996
+ words: judged.ask.words,
2997
+ keepOffered: judged.ask.keepOffered,
2998
+ onAdd: () => {
2999
+ onChange({ ...value, judged: { value: judged.ask.onAdd } });
3000
+ onCommit?.();
3001
+ },
3002
+ onKeep: () => {
3003
+ onChange({ ...value, judged: { value: judged.ask.asTyped } });
3004
+ onCommit?.();
3005
+ },
3006
+ onCancel: () => onCancel?.()
3007
+ }
3008
+ ) : "refusal" in judged ? /* @__PURE__ */ jsxs13("span", { className: "flex flex-col gap-1", children: [
3009
+ /* @__PURE__ */ jsx15("span", { "data-records-typed-refusal": "", role: "status", className: "text-[11px] leading-snug text-amber-700 dark:text-amber-400", children: judged.refusal }),
3010
+ judged.cancelOnly && onCancel ? /* @__PURE__ */ jsx15("span", { "data-records-choice-ask": "", children: /* @__PURE__ */ jsx15(Button8, { type: "button", size: "sm", variant: "ghost", className: "h-6 px-2 text-xs", onClick: () => onCancel(), children: "Cancel" }) }) : null
3011
+ ] }) : judged.value === null || judged.value === void 0 ? null : /* @__PURE__ */ jsxs13("span", { "data-records-typed-reads-as": "", className: "text-[10px] text-muted-foreground", children: [
3012
+ isChoiceAddition(judged.value) ? `Adds ${quoted(judged.value.add)} to the choices for ${fieldName(field)}. ` : null,
3013
+ "Reads as",
3014
+ " ",
3015
+ needsChoices ? String(
3016
+ (Array.isArray(withoutAddition(judged.value).value) ? withoutAddition(judged.value).value : [withoutAddition(judged.value).value]).map((k) => {
3017
+ const option = choices?.find((o) => optionKey2(o) === k);
3018
+ return option ? recordName(option.data, null, String(k)) : String(k);
3019
+ }).join(", ")
3020
+ ) : (() => {
3021
+ const shown2 = format ?? (kind === "datetime" ? { id: carriesTimeOfDay(field, judged.value) ? "datetime" : "date" } : void 0);
3022
+ return shown2 ? /* @__PURE__ */ jsx15(FormattedValue, { value: judged.value, format: shown2 }) : String(judged.value);
3023
+ })()
3024
+ ] })
3025
+ ] });
3026
+ }
3027
+ function isPersonsName(field) {
3028
+ const words = `${field.key} ${field.label ?? ""}`.toLowerCase();
3029
+ return /(^|[\s_])(full[\s_]?)?name(\s|$)/.test(words) && !/(therapist|doctor|physician|company|business|pet|room)/.test(words);
3030
+ }
3031
+ function FieldControl({ field, value, onChange, disabled, id, autoOpen, onClose, onCommit, onCancel, addsChoices, initialSearch, asForm }) {
3032
+ const host = useRecordsUi();
3033
+ const kind = editorKindFor(field);
3034
+ const inputId = id ?? `field-${field.key}`;
3035
+ const ownDetails = useOptionalRecordsClient6() === null;
3036
+ if (isTypedDraft(value)) return /* @__PURE__ */ jsx15(TypedTextControl, { field, value, onChange, disabled, id: inputId, onCommit, onCancel });
3037
+ if (isSignatureField(field)) return /* @__PURE__ */ jsx15(SignaturePad, { id: inputId, value, onChange, disabled });
3038
+ const newerControl = newerKindControl({ field, value, onChange, disabled, id: inputId });
3039
+ if (newerControl !== void 0) return newerControl;
3040
+ const format = formatForField(field);
3041
+ if (format && format.id === "time") return /* @__PURE__ */ jsx15(TypedWordsControl, { field, value, onChange, disabled, id: inputId });
3042
+ if (format && !FORMAT_BLIND_KINDS.has(String(kind))) {
3043
+ const own3 = formatControl({ format, value, onChange, disabled, id: inputId, label: fieldName(field), ownDetails });
3044
+ if (own3) return own3;
3045
+ }
3046
+ switch (kind) {
3047
+ case "formula":
3048
+ case "lookup":
3049
+ case "rollup":
3050
+ return /* @__PURE__ */ jsxs13("div", { id: inputId, className: "rounded border border-dashed px-2 py-1.5 text-xs text-muted-foreground", children: [
3051
+ value === null || value === void 0 ? "\u2014" : String(value),
3052
+ /* @__PURE__ */ jsxs13("span", { className: "ml-2 italic", children: [
3053
+ fieldName(field),
3054
+ " is worked out by the system, so it cannot be typed in"
3055
+ ] })
3056
+ ] });
3057
+ case "json":
3058
+ return /* @__PURE__ */ jsx15(JsonControl, { field, value, onChange, disabled, id: inputId });
3059
+ case "long_text":
3060
+ return /* @__PURE__ */ jsx15(
3061
+ BasicTextarea2,
3062
+ {
3063
+ id: inputId,
3064
+ rows: 3,
3065
+ value: value === null || value === void 0 ? "" : String(value),
3066
+ disabled,
3067
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
3068
+ }
3069
+ );
3070
+ case "number":
3071
+ case "currency":
3072
+ case "percent":
3073
+ return /* @__PURE__ */ jsx15(
3074
+ BasicInput6,
3075
+ {
3076
+ id: inputId,
3077
+ type: "number",
3078
+ inputMode: "decimal",
3079
+ className: "tabular-nums",
3080
+ value: value === null || value === void 0 ? "" : String(value),
3081
+ disabled,
3082
+ onChange: (e) => onChange(e.target.value === "" ? null : Number(e.target.value))
3083
+ }
3084
+ );
3085
+ case "datetime":
3086
+ return /* @__PURE__ */ jsx15(TypedWordsControl, { field, value, onChange, disabled, id: inputId });
3087
+ case "email":
3088
+ case "phone":
3089
+ case "url":
3090
+ return /* @__PURE__ */ jsx15(
3091
+ BasicInput6,
3092
+ {
3093
+ id: inputId,
3094
+ type: kind === "email" ? "email" : kind === "phone" ? "tel" : "url",
3095
+ ...ownDetails && kind !== "url" ? { autoComplete: kind === "email" ? "email" : "tel" } : {},
3096
+ value: value === null || value === void 0 ? "" : String(value),
3097
+ disabled,
3098
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
3099
+ }
3100
+ );
3101
+ case "checkbox":
3102
+ return /* @__PURE__ */ jsx15(CheckboxControl, { field, value, onChange, disabled, id: inputId });
3103
+ case "select":
3104
+ case "multi_select":
3105
+ return /* @__PURE__ */ jsx15(OptionControl, { field, value, onChange, disabled, id: inputId, autoOpen, onClose, addsChoices, initialSearch, asForm });
3106
+ case "member":
3107
+ return /* @__PURE__ */ jsx15(PersonPicker, { field, value, onChange, disabled, id: inputId, autoOpen, onClose, initialSearch });
3108
+ case "entity_reference":
3109
+ return /* @__PURE__ */ jsx15(EntityReferenceControl, { field, value, onChange, disabled, id: inputId });
3110
+ case "attachment":
3111
+ if (hostTakesFiles(host)) {
3112
+ return /* @__PURE__ */ jsx15(AttachmentControl, { field, value, onChange, disabled, id: inputId, autoOpen, onClose });
3113
+ }
3114
+ return /* @__PURE__ */ jsx15(RelationPicker, { field, value, onChange, disabled, id: inputId });
3115
+ case "relation":
3116
+ return /* @__PURE__ */ jsx15(RelationPicker, { field, value, onChange, disabled, id: inputId });
3117
+ default:
3118
+ if (typeof value === "boolean" || field.config?.["kind"] === "boolean") {
3119
+ return /* @__PURE__ */ jsx15(
3120
+ Checkbox,
3121
+ {
3122
+ id: inputId,
3123
+ checked: value === true,
3124
+ disabled,
3125
+ onCheckedChange: (next) => onChange(next === true)
3126
+ }
3127
+ );
3128
+ }
3129
+ return /* @__PURE__ */ jsx15(
3130
+ BasicInput6,
3131
+ {
3132
+ id: inputId,
3133
+ ...ownDetails && isPersonsName(field) ? { autoComplete: "name" } : {},
3134
+ value: value === null || value === void 0 ? "" : String(value),
3135
+ disabled,
3136
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
3137
+ }
3138
+ );
3139
+ }
3140
+ }
3141
+ function TypedWordsControl({ field, value, onChange, disabled, id }) {
3142
+ const [text, setText] = useState13(() => openingText(field, value));
3143
+ const [refusal2, setRefusal] = useState13(null);
3144
+ const sent = useRef9(value);
3145
+ useEffect10(() => {
3146
+ if (value === sent.current) return;
3147
+ sent.current = value;
3148
+ setText(openingText(field, value));
3149
+ setRefusal(null);
3150
+ }, [value, field]);
3151
+ const helper = dateHelperKind(field, value);
3152
+ const retype = (next) => {
3153
+ setText(next);
3154
+ const judged = judgeTyped(field, next);
3155
+ if (judged && "value" in judged) {
3156
+ setRefusal(null);
3157
+ sent.current = judged.value;
3158
+ onChange(judged.value);
3159
+ } else if (judged && "refusal" in judged) {
3160
+ setRefusal(judged.refusal);
3161
+ }
3162
+ };
3163
+ return /* @__PURE__ */ jsxs13("span", { className: "flex flex-col gap-0.5", children: [
3164
+ /* @__PURE__ */ jsxs13("span", { className: "flex min-w-0 items-center gap-0.5", children: [
3165
+ /* @__PURE__ */ jsx15(
3166
+ BasicInput6,
3167
+ {
3168
+ id,
3169
+ value: text,
3170
+ disabled,
3171
+ autoComplete: "off",
3172
+ "aria-invalid": refusal2 ? true : void 0,
3173
+ placeholder: helper === "time" ? "2:30 PM" : helper === "datetime" ? `${typedDateText2("2026-10-03")} 2:30 PM` : helper ? typedDateText2("2026-10-03") : void 0,
3174
+ onChange: (e) => retype(e.target.value),
3175
+ onBlur: () => {
3176
+ if (!refusal2) return;
3177
+ setText(openingText(field, sent.current));
3178
+ setRefusal(null);
3179
+ }
3180
+ }
3181
+ ),
3182
+ helper && !disabled ? /* @__PURE__ */ jsx15(TypedDateHelper, { kind: helper, text, onText: retype, label: fieldName(field) }) : null
3183
+ ] }),
3184
+ refusal2 ? /* @__PURE__ */ jsx15("span", { "data-records-typed-refusal": "", role: "status", className: "text-[11px] leading-snug text-amber-700 dark:text-amber-400", children: refusal2 }) : null
3185
+ ] });
3186
+ }
3187
+ function CheckboxControl({ field, value, onChange, disabled, id }) {
3188
+ const unanswered = value === null || value === void 0;
3189
+ return /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2", children: [
3190
+ /* @__PURE__ */ jsx15(
3191
+ Checkbox,
3192
+ {
3193
+ id,
3194
+ checked: value === true,
3195
+ disabled,
3196
+ "aria-label": fieldName(field),
3197
+ onCheckedChange: (next) => onChange(next === true)
3198
+ }
3199
+ ),
3200
+ unanswered ? /* @__PURE__ */ jsx15("span", { className: "text-xs italic text-muted-foreground", children: "not answered yet" }) : /* @__PURE__ */ jsxs13(Fragment4, { children: [
3201
+ /* @__PURE__ */ jsx15("span", { className: "text-xs text-muted-foreground", children: value === true ? "Yes" : "No" }),
3202
+ field.required ? null : /* @__PURE__ */ jsx15(
3203
+ Button8,
3204
+ {
3205
+ size: "sm",
3206
+ variant: "ghost",
3207
+ className: "h-5 px-1 text-[10px]",
3208
+ disabled,
3209
+ onClick: () => onChange(null),
3210
+ children: "Clear"
3211
+ }
3212
+ )
3213
+ ] })
3214
+ ] });
3215
+ }
3216
+ function JsonControl({ field, value, onChange, disabled, id }) {
3217
+ const initial = value === null || value === void 0 ? "" : typeof value === "string" ? value : JSON.stringify(value, null, 2);
3218
+ const [text, setText] = useState13(initial);
3219
+ const [broken, setBroken] = useState13(null);
3220
+ return /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-1", children: [
3221
+ /* @__PURE__ */ jsx15(
3222
+ BasicTextarea2,
3223
+ {
3224
+ id,
3225
+ rows: 4,
3226
+ className: "font-mono text-[11px]",
3227
+ value: text,
3228
+ disabled,
3229
+ onChange: (e) => {
3230
+ const next = e.target.value;
3231
+ setText(next);
3232
+ if (next.trim() === "") {
3233
+ setBroken(null);
3234
+ onChange(null);
3235
+ return;
3236
+ }
3237
+ try {
3238
+ onChange(JSON.parse(next));
3239
+ setBroken(null);
3240
+ } catch (thrown) {
3241
+ setBroken(
3242
+ refusalFromThrown(
3243
+ thrown,
3244
+ `${fieldName(field)} is not readable as structured data yet.`,
3245
+ "Fix the text so it reads as structured data, or discard it to put back what was there.",
3246
+ "invalid_argument"
3247
+ )
3248
+ );
3249
+ }
3250
+ }
3251
+ }
3252
+ ),
3253
+ broken ? (
3254
+ // The typed text is still in the box: the notice owns the two doors out.
3255
+ /* @__PURE__ */ jsx15(
3256
+ RefusalNotice,
3257
+ {
3258
+ error: broken,
3259
+ onKeepEditing: () => id ? document.getElementById(id)?.focus() : void 0,
3260
+ onDiscard: () => {
3261
+ setText(initial);
3262
+ setBroken(null);
3263
+ }
3264
+ }
3265
+ )
3266
+ ) : null
3267
+ ] });
3268
+ }
3269
+ function inTheStoresOrder(options) {
3270
+ const position = (o) => {
3271
+ const raw = o.metadata?.["option_position"];
3272
+ const n = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : Number.NaN;
3273
+ return Number.isFinite(n) ? n : Number.POSITIVE_INFINITY;
3274
+ };
3275
+ const made = (o) => String(o.created_at ?? "");
3276
+ return [...options].sort((a, b) => position(a) - position(b) || made(a).localeCompare(made(b)));
3277
+ }
3278
+ function OptionControl({ field, value, onChange, disabled, id, autoOpen, onClose, addsChoices, initialSearch, asForm }) {
3279
+ const client = useOptionalRecordsClient6();
3280
+ const [options, setOptions] = useState13(null);
3281
+ const [refusal2, setRefusal] = useState13(null);
3282
+ const [reread, setReread] = useState13(0);
3283
+ useEffect10(() => {
3284
+ if (!client) return;
3285
+ let cancelled = false;
3286
+ void client.fieldOptions({ field_id: field.id }).then((result) => {
3287
+ if (cancelled) return;
3288
+ if (result.ok) setOptions(inTheStoresOrder(result.data ?? []));
3289
+ else setRefusal(result.error);
3290
+ });
3291
+ return () => {
3292
+ cancelled = true;
3293
+ };
3294
+ }, [client, field.id, reread]);
3295
+ const handed = publicChoicesOf(field);
3296
+ if (!client && handed.length > 0) {
3297
+ return /* @__PURE__ */ jsx15(ChoiceAnswer, { field, choices: handed, value, onChange, disabled, id });
3298
+ }
3299
+ if (!client) {
3300
+ return /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-1", children: [
3301
+ /* @__PURE__ */ jsx15(
3302
+ BasicInput6,
3303
+ {
3304
+ id,
3305
+ value: value === null || value === void 0 ? "" : String(value),
3306
+ disabled,
3307
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
3308
+ }
3309
+ ),
3310
+ /* @__PURE__ */ jsx15("p", { className: "text-xs text-muted-foreground", children: "This is normally picked from a list. A page answered without an account cannot read that list, so type your answer and whoever receives it will match it up." })
3311
+ ] });
3312
+ }
3313
+ if (refusal2)
3314
+ return /* @__PURE__ */ jsx15("div", { id, children: /* @__PURE__ */ jsx15(RefusalLine, { error: refusal2 }) });
3315
+ if (!options) return /* @__PURE__ */ jsx15("p", { id, className: "text-xs text-muted-foreground", children: "Reading this field's choices\u2026" });
3316
+ if (asForm && options.length > 0) {
3317
+ const titled = (option) => recordName(option.data, null, "Unnamed choice");
3318
+ return /* @__PURE__ */ jsx15(
3319
+ ChoiceAnswer,
3320
+ {
3321
+ field,
3322
+ choices: options.map((option) => ({ key: optionKey2(option), label: titled(option) })),
3323
+ value,
3324
+ onChange,
3325
+ disabled,
3326
+ id,
3327
+ isChosen: (choice, v) => {
3328
+ const option = options.find((o) => optionKey2(o) === choice.key);
3329
+ return option ? isTheChosen(option, v) : false;
3330
+ }
3331
+ }
3332
+ );
3333
+ }
3334
+ if (CHOICE_EDITOR_ADDS_WHAT_IS_TYPED) {
3335
+ return /* @__PURE__ */ jsx15(
3336
+ OpenChoicePicker,
3337
+ {
3338
+ field,
3339
+ options,
3340
+ value,
3341
+ onChange,
3342
+ disabled,
3343
+ id,
3344
+ autoOpen,
3345
+ onClose,
3346
+ initialSearch,
3347
+ addsChoices,
3348
+ allowOther: allowsOtherValues(field),
3349
+ onChoicesChanged: () => setReread((n) => n + 1)
3350
+ }
3351
+ );
3352
+ }
3353
+ if (options.length === 0) {
3354
+ return /* @__PURE__ */ jsx15("p", { id, className: "text-xs text-muted-foreground", children: field.options_table_id ? `${fieldName(field)} takes its choices from a Table that has no records yet \u2014 add the choices there and they appear here.` : `${fieldName(field)} does not name the Table its choices come from yet, so there is nothing to choose. A table admin sets it in the field editor.` });
3355
+ }
3356
+ const titleOf = (option) => recordName(option.data, null, "Unnamed choice");
3357
+ if (field.multi) {
3358
+ const chosen = options.filter((option) => choiceValuesOf(value).some((v) => isTheChosen(option, v)));
3359
+ const picked = new Set(chosen.map((option) => optionKey2(option)));
3360
+ const toggles = /* @__PURE__ */ jsx15("div", { id: autoOpen ? void 0 : id, className: "flex flex-wrap gap-1", children: options.map((option) => /* @__PURE__ */ jsx15(
3361
+ Button8,
3362
+ {
3363
+ type: "button",
3364
+ size: "sm",
3365
+ variant: picked.has(optionKey2(option)) ? "default" : "outline",
3366
+ disabled,
3367
+ onClick: () => {
3368
+ const next = new Set(picked);
3369
+ if (next.has(optionKey2(option))) next.delete(optionKey2(option));
3370
+ else next.add(optionKey2(option));
3371
+ onChange(next.size === 0 ? null : [...next]);
3372
+ },
3373
+ children: titleOf(option)
3374
+ },
3375
+ option.id
3376
+ )) });
3377
+ if (!autoOpen) return toggles;
3378
+ return /* @__PURE__ */ jsxs13(
3379
+ Popover4,
3380
+ {
3381
+ defaultOpen: true,
3382
+ onOpenChange: (open) => {
3383
+ if (!open) onClose?.();
3384
+ },
3385
+ children: [
3386
+ /* @__PURE__ */ jsx15(PopoverTrigger4, { asChild: true, children: /* @__PURE__ */ jsx15(Button8, { id, type: "button", size: "sm", variant: "outline", className: "h-6 max-w-full truncate", children: chosen.length === 0 ? "Choose" : chosen.map(titleOf).join(", ") }) }),
3387
+ /* @__PURE__ */ jsx15(PopoverContent4, { sizing: "content", className: "max-w-[20rem] p-2", align: "start", children: toggles })
3388
+ ]
3389
+ }
3390
+ );
3391
+ }
3392
+ const current = options.find((option) => isTheChosen(option, value));
3393
+ return /* @__PURE__ */ jsxs13(
3394
+ Select,
3395
+ {
3396
+ value: current ? optionKey2(current) : "",
3397
+ disabled: disabled ?? false,
3398
+ onValueChange: (next) => onChange(next === "" ? null : next),
3399
+ ...autoOpen ? {
3400
+ // IN A GRID CELL the list opens at once and a pick is the commit (Airtable).
3401
+ defaultOpen: true,
3402
+ onOpenChange: (open) => {
3403
+ if (!open) onClose?.();
3404
+ }
3405
+ } : {},
3406
+ children: [
3407
+ /* @__PURE__ */ jsx15(SelectTrigger, { id, size: "sm", children: /* @__PURE__ */ jsx15(SelectValue, { placeholder: "Choose" }) }),
3408
+ /* @__PURE__ */ jsx15(SelectContent, { children: options.map((option) => /* @__PURE__ */ jsx15(SelectItem, { value: optionKey2(option), children: titleOf(option) }, option.id)) })
3409
+ ]
3410
+ }
3411
+ );
3412
+ }
3413
+ function OpenChoicePicker({
3414
+ field,
3415
+ options,
3416
+ value,
3417
+ onChange,
3418
+ disabled,
3419
+ id,
3420
+ addsChoices,
3421
+ autoOpen,
3422
+ onClose,
3423
+ initialSearch,
3424
+ allowOther = true,
3425
+ onChoicesChanged
3426
+ }) {
3427
+ const client = useOptionalRecordsClient6();
3428
+ const nudge = useChoiceNudge();
3429
+ const [open, setOpenState] = useState13(autoOpen === true);
3430
+ const onCloseRef = useRef9(onClose);
3431
+ onCloseRef.current = onClose;
3432
+ const setOpen = useCallback4((next) => {
3433
+ setOpenState(next);
3434
+ if (!next) onCloseRef.current?.();
3435
+ }, []);
3436
+ const [search, setSearch] = useState13(initialSearch ?? "");
3437
+ const [adding, setAdding] = useState13(false);
3438
+ const [addRefused, setAddRefused] = useState13(null);
3439
+ const titleOf = (option) => recordName(option.data, null, "Unnamed choice");
3440
+ const held = withoutAddition(value);
3441
+ const current = choiceValuesOf(held.value);
3442
+ const pickedWords = current.map((v) => {
3443
+ const option = options.find((o) => isTheChosen(o, v));
3444
+ return option ? titleOf(option) : String(v);
3445
+ });
3446
+ const chipOf = (option, word) => option ? choiceChipLookOf(field, optionKey2(option), titleOf(option)) : choiceChipLookOf(field, word);
3447
+ const q = search.trim();
3448
+ const visible = rankTypedMatches3(options, q, titleOf);
3449
+ const decided = decideTypedChoice({ words: q, options, allowOther, nudge: nudge ?? "ask" });
3450
+ const choose = (word, addingIt = false) => {
3451
+ const pending = held.add.filter((w) => w.toLowerCase() !== word.toLowerCase());
3452
+ let next;
3453
+ let add2 = [...pending];
3454
+ if (field.multi) {
3455
+ const existing = current.map((v) => {
3456
+ const option = options.find((o) => isTheChosen(o, v));
3457
+ return option ? optionKey2(option) : String(v);
3458
+ });
3459
+ const has = existing.some((v) => v.toLowerCase() === word.toLowerCase());
3460
+ const list = has ? existing.filter((v) => v.toLowerCase() !== word.toLowerCase()) : [...existing, word];
3461
+ if (addingIt && !has) add2.push(word);
3462
+ add2 = add2.filter((w) => list.some((v) => v.toLowerCase() === w.toLowerCase()));
3463
+ next = list.length === 0 ? null : list;
3464
+ } else {
3465
+ add2 = addingIt ? [word] : [];
3466
+ next = word;
3467
+ }
3468
+ onChange(add2.length > 0 ? choiceAddition(next, add2) : next);
3469
+ setSearch("");
3470
+ if (!field.multi) setOpen(false);
3471
+ };
3472
+ const addTyped = () => {
3473
+ setAddRefused(null);
3474
+ if (addsChoices || !client) {
3475
+ choose(q, Boolean(addsChoices));
3476
+ return;
3477
+ }
3478
+ setAdding(true);
3479
+ void client.fieldUpdate({ field_id: field.id, patch: { options_add: [q] } }).then((result) => {
3480
+ setAdding(false);
3481
+ if (!result.ok) {
3482
+ setAddRefused(result.error);
3483
+ return;
3484
+ }
3485
+ onChoicesChanged();
3486
+ choose(q);
3487
+ });
3488
+ };
3489
+ const offList = q !== "" && decided.decision !== "chosen" && decided.decision !== "empty";
3490
+ const foot = offList && decided.decision === "ask" ? /* @__PURE__ */ jsx15(
3491
+ ChoiceAskButtons,
3492
+ {
3493
+ field,
3494
+ words: [q],
3495
+ keepOffered: decided.keepOffered,
3496
+ busy: adding,
3497
+ onAdd: addTyped,
3498
+ onKeep: () => choose(q),
3499
+ onCancel: () => {
3500
+ setSearch("");
3501
+ setAddRefused(null);
3502
+ if (autoOpen) setOpen(false);
3503
+ }
3504
+ }
3505
+ ) : null;
3506
+ const add = offList && decided.decision === "add" ? { label: `Add \u201C${q}\u201D to the choices`, onAdd: addTyped, disabled: adding } : offList && decided.decision === "keep_as_typed" ? { label: `Keep \u201C${q}\u201D as typed`, onAdd: () => choose(q) } : null;
3507
+ return /* @__PURE__ */ jsxs13("div", { id, className: "flex flex-wrap items-center gap-1", children: [
3508
+ pickedWords.map((word, i) => {
3509
+ const chip = chipOf(options.find((o) => isTheChosen(o, current[i])), word);
3510
+ return /* @__PURE__ */ jsx15(Badge3, { variant: "outline", className: cn11("text-[11px]", chip.className), "data-records-choice-chip": "", children: word }, word);
3511
+ }),
3512
+ disabled ? null : (
3513
+ // THE ONE "PICK OR ADD" BUILDER (PickOrAdd.tsx) — the relation picker draws the same one.
3514
+ /* @__PURE__ */ jsx15(
3515
+ PickOrAdd,
3516
+ {
3517
+ triggerLabel: `Pick for ${fieldName(field)}`,
3518
+ triggerText: pickedWords.length === 0 ? "Choose" : "Change",
3519
+ open,
3520
+ onOpenChange: setOpen,
3521
+ search,
3522
+ onSearch: (next) => {
3523
+ setSearch(next);
3524
+ setAddRefused(null);
3525
+ },
3526
+ items: visible.map((option) => ({
3527
+ id: option.id,
3528
+ label: titleOf(option),
3529
+ chip: chipOf(option, titleOf(option)).className,
3530
+ picked: current.some((v) => isTheChosen(option, v))
3531
+ })),
3532
+ onPick: (optionId) => {
3533
+ const option = options.find((o) => o.id === optionId);
3534
+ if (option) choose(optionKey2(option));
3535
+ },
3536
+ empty: q === "" ? /* @__PURE__ */ jsx15("p", { className: "p-2 text-xs text-muted-foreground", children: "No choices yet \u2014 type one to add it." }) : null,
3537
+ add,
3538
+ foot,
3539
+ ...offList && decided.decision === "ask" && !adding ? { onEnterWithNoMatch: addTyped } : {},
3540
+ footNote: addRefused ? /* @__PURE__ */ jsx15(RefusalLine, { error: addRefused }) : offList && decided.decision === "not_a_choice" ? (
3541
+ // never_add on a column that takes only its choices: said, never a silent nothing.
3542
+ /* @__PURE__ */ jsxs13(Fragment4, { children: [
3543
+ /* @__PURE__ */ jsx15("p", { "data-records-typed-refusal": "", className: "p-2 text-xs text-muted-foreground", children: notAChoiceSentence(field, [q]) }),
3544
+ autoOpen ? /* @__PURE__ */ jsx15("span", { "data-records-choice-ask": "", className: "px-2 pb-1", children: /* @__PURE__ */ jsx15(Button8, { type: "button", size: "sm", variant: "ghost", className: "h-6 px-2 text-xs", onClick: () => setOpen(false), children: "Cancel" }) }) : null
3545
+ ] })
3546
+ ) : null
3547
+ }
3548
+ )
3549
+ )
3550
+ ] });
3551
+ }
3552
+ function RecordChip({
3553
+ title,
3554
+ onRemove,
3555
+ className
3556
+ }) {
3557
+ return (
3558
+ // A chip carries a RECORD'S OWN TITLE, which nobody here chose the length of —
3559
+ // "Marisol Okonkwo — 418 Calle Puerto Vallarta, Camarillo" is an ordinary customer
3560
+ // name. So it caps at its container and ends in an ellipsis with the whole name in
3561
+ // its tooltip, rather than being cut mid-word by whatever box it landed in
3562
+ // (lane FIX-14, 2026-09-22).
3563
+ /* @__PURE__ */ jsxs13(Badge3, { variant: "secondary", className: cn11("max-w-full gap-1 text-[11px]", className), title, children: [
3564
+ /* @__PURE__ */ jsx15("span", { className: "min-w-0 truncate", children: title }),
3565
+ onRemove ? /* @__PURE__ */ jsx15("button", { type: "button", onClick: onRemove, "aria-label": `Remove ${title}`, className: "shrink-0 opacity-60 hover:opacity-100", children: "\xD7" }) : null
3566
+ ] })
3567
+ );
3568
+ }
3569
+ function publicChoicesOf(field) {
3570
+ const raw = field.public_choices;
3571
+ if (!Array.isArray(raw)) return [];
3572
+ return raw.flatMap((c) => {
3573
+ const o = c;
3574
+ return typeof o.key === "string" && typeof o.label === "string" ? [{ key: o.key, label: o.label }] : [];
3575
+ });
3576
+ }
3577
+ var FORM_PILL_LIMIT = 4;
3578
+ function ChoiceAnswer({
3579
+ field,
3580
+ choices,
3581
+ value,
3582
+ onChange,
3583
+ disabled,
3584
+ id,
3585
+ isChosen
3586
+ }) {
3587
+ const [search, setSearch] = useState13("");
3588
+ const held = field.multi ? choiceValuesOf(value) : value === null || value === void 0 || value === "" ? [] : [value];
3589
+ const on = (choice) => held.some((v) => isChosen ? isChosen(choice, v) : String(v) === choice.key || String(v) === choice.label);
3590
+ const press = (choice) => {
3591
+ if (!field.multi) {
3592
+ onChange(on(choice) ? null : choice.key);
3593
+ return;
3594
+ }
3595
+ const keys = choices.filter((c) => c.key === choice.key ? !on(c) : on(c)).map((c) => c.key);
3596
+ onChange(keys.length === 0 ? null : keys);
3597
+ };
3598
+ const pill = (choice) => {
3599
+ const chosen = on(choice);
3600
+ const look = choiceChipLookOf(field, choice.key, choice.label);
3601
+ return /* @__PURE__ */ jsxs13(
3602
+ "button",
3603
+ {
3604
+ type: "button",
3605
+ role: field.multi ? "checkbox" : "radio",
3606
+ "aria-checked": chosen,
3607
+ disabled,
3608
+ onClick: () => press(choice),
3609
+ "data-records-choice-chip": "",
3610
+ "data-records-choice-answer": choice.key,
3611
+ className: cn11(
3612
+ "inline-flex min-h-11 max-w-full items-center gap-1 rounded-md border px-3 text-sm font-medium sm:min-h-8 sm:px-2.5 sm:text-xs",
3613
+ look.className,
3614
+ chosen ? "ring-2 ring-ring ring-offset-1" : "opacity-80 hover:opacity-100"
3615
+ ),
3616
+ children: [
3617
+ chosen ? /* @__PURE__ */ jsx15("span", { "aria-hidden": "true", children: "\u2713" }) : null,
3618
+ /* @__PURE__ */ jsx15("span", { className: "truncate", children: choice.label })
3619
+ ]
3620
+ },
3621
+ choice.key
3622
+ );
3623
+ };
3624
+ const group = { role: field.multi ? "group" : "radiogroup", "aria-label": fieldName(field) };
3625
+ if (choices.length <= FORM_PILL_LIMIT) {
3626
+ return /* @__PURE__ */ jsx15("div", { id, className: "flex flex-wrap gap-1.5", ...group, children: choices.map(pill) });
3627
+ }
3628
+ const q = search.trim().toLowerCase();
3629
+ const shown2 = q === "" ? choices : choices.filter((c) => c.label.toLowerCase().includes(q));
3630
+ return /* @__PURE__ */ jsxs13("div", { id, className: "flex flex-col gap-1.5", children: [
3631
+ /* @__PURE__ */ jsx15(
3632
+ BasicInput6,
3633
+ {
3634
+ value: search,
3635
+ onChange: (e) => setSearch(e.target.value),
3636
+ placeholder: "Search",
3637
+ "aria-label": `Search \u2014 ${fieldName(field)}`,
3638
+ autoComplete: "off",
3639
+ className: "h-11 text-base sm:h-8 sm:text-xs"
3640
+ }
3641
+ ),
3642
+ /* @__PURE__ */ jsxs13("div", { className: "flex max-h-56 flex-wrap content-start gap-1.5 overflow-y-auto", ...group, children: [
3643
+ shown2.map(pill),
3644
+ shown2.length === 0 ? /* @__PURE__ */ jsx15("p", { className: "text-xs text-muted-foreground", children: "No match" }) : null
3645
+ ] })
3646
+ ] });
3647
+ }
3648
+
3649
+ // src/RecordsMount.tsx
3650
+ import { useRef as useRef10 } from "react";
3651
+ import { RecordsProvider } from "@ai-matrx/records/react";
3652
+ import { jsx as jsx16 } from "react/jsx-runtime";
3653
+ function personActor(userId) {
3654
+ return userId ? { actor: "user", user_id: userId } : { actor: "user" };
3655
+ }
3656
+
3657
+ // src/tablePicking.tsx
3658
+ import { useEffect as useEffect11, useMemo as useMemo6, useState as useState14 } from "react";
3659
+ import { withTablePlacement } from "@ai-matrx/records";
3660
+ import { useRecordsClient as useRecordsClient2, useTables } from "@ai-matrx/records/react";
3661
+ import { Checkbox as Checkbox2, cn as cn12 } from "@ai-matrx/design-system";
3662
+ import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
3663
+ var APP_TABLE_PREFIX = "records_ui_";
3664
+ function isValueSet(table) {
3665
+ if (table.kept_for === "choices") return true;
3666
+ return table.kept_for == null && table.kind === "list" && table.kept_by_the_app === true;
3667
+ }
3668
+ function isKeptTable(table) {
3669
+ return table.kept_by_the_app === true || isValueSet(table) || Boolean(table.work_kind) || (table.slug ?? "").startsWith(APP_TABLE_PREFIX);
3670
+ }
3671
+ var CHOICES_SUFFIX = / choices$/i;
3672
+ function tablePickerEntries(tables, options = {}) {
3673
+ const byId = new Map(tables.map((t) => [t.id, t]));
3674
+ const lists = tables.filter(isValueSet);
3675
+ const listLabel = (t) => {
3676
+ const column = t.name.replace(CHOICES_SUFFIX, "");
3677
+ if (!t.used_in_table_id || t.used_in_table_id === t.id) {
3678
+ const made = madeOn(t.created_at);
3679
+ return made ? `${column} \u2014 no column \xB7 ${made}` : `${column} \u2014 no column`;
3680
+ }
3681
+ const owner = byId.get(t.used_in_table_id);
3682
+ if (!owner) return t.name;
3683
+ return `${column} \u2014 ${owner.name}`;
3684
+ };
3685
+ const plain = tables.filter((t) => !isKeptTable(t) || t.id === options.keep && !isValueSet(t)).map((t) => ({ table: t, id: t.id, label: t.name, isList: false })).sort((a, b) => a.label.localeCompare(b.label));
3686
+ const shownLists = lists.filter((t) => options.showLists || t.id === options.keep).map((t) => ({ table: t, id: t.id, label: listLabel(t), isList: true })).sort((a, b) => a.label.localeCompare(b.label));
3687
+ const entries = [...plain, ...shownLists];
3688
+ const seen = /* @__PURE__ */ new Map();
3689
+ for (const entry of entries) {
3690
+ const key = entry.label.toLowerCase();
3691
+ const n = (seen.get(key) ?? 0) + 1;
3692
+ seen.set(key, n);
3693
+ if (n > 1) entry.label = `${entry.label} (${n})`;
3694
+ }
3695
+ return { entries, listCount: lists.length };
3696
+ }
3697
+ function madeOn(stamp) {
3698
+ if (!stamp) return null;
3699
+ const at = new Date(stamp);
3700
+ if (Number.isNaN(at.getTime())) return null;
3701
+ return new Intl.DateTimeFormat(void 0, { month: "short", day: "numeric", year: "numeric" }).format(at);
3702
+ }
3703
+
3704
+ // src/TablePickers.tsx
3705
+ import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
3706
+ function useTablesAnywhere(dataSource) {
3707
+ const [state, setState] = useState15({ loading: true, error: null, rows: [] });
3708
+ useEffect12(() => {
3709
+ let live = true;
3710
+ void Promise.resolve(dataSource.rpc("data_home_tables", {}, { schema: "custom" })).then(
3711
+ ({ data, error }) => {
3712
+ if (!live) return;
3713
+ if (error) {
3714
+ setState({ loading: false, error: refusalSentence({ message: String(error["message"] ?? "") }) || "Tables could not be read.", rows: [] });
3715
+ return;
3716
+ }
3717
+ const rows = (Array.isArray(data) ? data : []).slice();
3718
+ rows.sort((a, b) => a.table_name.localeCompare(b.table_name));
3719
+ setState({ loading: false, error: null, rows });
3720
+ },
3721
+ (thrown) => {
3722
+ if (live) setState({ loading: false, error: String(thrown?.message ?? thrown), rows: [] });
3723
+ }
3724
+ );
3725
+ return () => {
3726
+ live = false;
3727
+ };
3728
+ }, [dataSource]);
3729
+ return state;
3730
+ }
3731
+ function TablePicker({ dataSource, value, onChange, disabled, tables }) {
3732
+ const own3 = useTablesAnywhere(dataSource);
3733
+ const list = tables ?? own3;
3734
+ const options = tablePickerEntries(
3735
+ list.rows.map((t) => ({ ...t, id: t.table_id, name: t.table_name })),
3736
+ { keep: value }
3737
+ ).entries.map(({ table: t }) => ({
3738
+ value: t.table_id,
3739
+ // The organization hint already tells two same-named tables apart across organizations.
3740
+ label: t.table_name,
3741
+ hint: t.organization_name,
3742
+ keywords: t.organization_name
3743
+ }));
3744
+ const missing = Boolean(value) && !list.loading && !list.error && !list.rows.some((t) => t.table_id === value);
3745
+ return /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-1", children: [
3746
+ /* @__PURE__ */ jsx18(
3747
+ CreatablePicker,
3748
+ {
3749
+ value: value || null,
3750
+ options,
3751
+ onSelect: onChange,
3752
+ placeholder: missing ? "Table not available" : "Choose a table",
3753
+ searchPlaceholder: "Search tables",
3754
+ noun: "table",
3755
+ loading: list.loading,
3756
+ ...disabled ? { disabled: true } : {},
3757
+ ariaLabel: "Table"
3758
+ }
3759
+ ),
3760
+ list.error && /* @__PURE__ */ jsx18(RefusalLine, { error: refusal("door", list.error, "Try again in a moment.") }),
3761
+ missing && /* @__PURE__ */ jsx18("p", { className: "text-[11px] text-destructive", children: "This table is archived or not shared with you." })
3762
+ ] });
3763
+ }
3764
+ function TableScope({ dataSource, userId, tableId, children, fallback = null, tables }) {
3765
+ const own3 = useTablesAnywhere(dataSource);
3766
+ const list = tables ?? own3;
3767
+ const organizationId = list.rows.find((t) => t.table_id === tableId)?.organization_id ?? null;
3768
+ if (!tableId || !organizationId) return /* @__PURE__ */ jsx18(Fragment5, { children: fallback });
3769
+ return /* @__PURE__ */ jsx18(RecordsProvider2, { config: { dataSource, actor: personActor(userId), organizationId }, children });
3770
+ }
3771
+ function ColumnValuesEditor({ tableId, value, onChange, disabled, addLabel }) {
3772
+ const fields = useFields(tableId);
3773
+ const all = useMemo7(() => fields.data ?? [], [fields.data]);
3774
+ const byKey = useMemo7(() => new Map(all.map((f) => [f.key, f])), [all]);
3775
+ const keys = Object.keys(value);
3776
+ const [adding, setAdding] = useState15(false);
3777
+ const optionsFor = (current) => all.filter((f) => f.key === current || !(f.key in value)).map((f) => ({ value: f.key, label: fieldName(f) }));
3778
+ const rename = (from, to) => {
3779
+ if (from === to) return;
3780
+ const next = {};
3781
+ for (const k of keys) next[k === from ? to : k] = k === from ? null : value[k];
3782
+ onChange(next);
3783
+ };
3784
+ const remove = (key) => {
3785
+ const { [key]: _gone, ...rest } = value;
3786
+ void _gone;
3787
+ onChange(rest);
3788
+ };
3789
+ return /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-2", children: [
3790
+ fields.error && /* @__PURE__ */ jsx18(RefusalLine, { error: fields.error }),
3791
+ keys.map((key) => {
3792
+ const field = byKey.get(key);
3793
+ return /* @__PURE__ */ jsx18(
3794
+ ColumnValueRow,
3795
+ {
3796
+ columnKey: key,
3797
+ field,
3798
+ loading: fields.loading,
3799
+ options: optionsFor(key),
3800
+ value: value[key],
3801
+ disabled,
3802
+ onColumn: (to) => rename(key, to),
3803
+ onValue: (v) => onChange({ ...value, [key]: v }),
3804
+ onRemove: () => remove(key)
3805
+ },
3806
+ key
3807
+ );
3808
+ }),
3809
+ adding && /* @__PURE__ */ jsx18(
3810
+ ColumnValueRow,
3811
+ {
3812
+ columnKey: null,
3813
+ field: void 0,
3814
+ loading: fields.loading,
3815
+ options: optionsFor(null),
3816
+ value: null,
3817
+ disabled,
3818
+ onColumn: (to) => {
3819
+ setAdding(false);
3820
+ onChange({ ...value, [to]: null });
3821
+ },
3822
+ onValue: () => void 0,
3823
+ onRemove: () => setAdding(false)
3824
+ }
3825
+ ),
3826
+ !disabled && !adding && keys.length < all.length && /* @__PURE__ */ jsx18(Button9, { type: "button", variant: "outline", size: "sm", className: "self-start", onClick: () => setAdding(true), children: addLabel })
3827
+ ] });
3828
+ }
3829
+ function ColumnValueRow({
3830
+ columnKey,
3831
+ field,
3832
+ loading,
3833
+ options,
3834
+ value,
3835
+ disabled,
3836
+ onColumn,
3837
+ onValue,
3838
+ onRemove
3839
+ }) {
3840
+ const orphan = columnKey !== null && !loading && field === void 0;
3841
+ const shown2 = orphan ? [{ value: columnKey, label: columnKey, hint: "Not a column" }, ...options] : options;
3842
+ return /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-1 rounded-md border border-border/60 p-2", children: [
3843
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2", children: [
3844
+ /* @__PURE__ */ jsx18("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx18(
3845
+ CreatablePicker,
3846
+ {
3847
+ value: columnKey,
3848
+ options: shown2,
3849
+ onSelect: onColumn,
3850
+ placeholder: "Choose a column",
3851
+ searchPlaceholder: "Search columns",
3852
+ noun: "column",
3853
+ loading,
3854
+ ...disabled ? { disabled: true } : {},
3855
+ ariaLabel: "Column"
3856
+ }
3857
+ ) }),
3858
+ !disabled && /* @__PURE__ */ jsx18(Button9, { type: "button", variant: "ghost", size: "sm", "aria-label": "Remove", onClick: onRemove, children: "\u2715" })
3859
+ ] }),
3860
+ field ? /* @__PURE__ */ jsx18(FieldControl, { field, value, onChange: onValue, disabled }) : orphan ? /* @__PURE__ */ jsx18("p", { className: "text-[11px] text-muted-foreground", children: value === null || value === void 0 ? "\u2014" : String(value) }) : null
3861
+ ] });
3862
+ }
3863
+ function RecordPicker({ tableId, value, onChange, disabled }) {
3864
+ const table = useTable(tableId);
3865
+ const records = useRecords(tableId, { pageSize: 200 });
3866
+ const rows = records.data?.rows ?? [];
3867
+ const options = rows.map((row) => ({
3868
+ value: row.id,
3869
+ label: rowNameIn(table.data ?? null, row)
3870
+ }));
3871
+ const missing = Boolean(value) && !records.loading && !records.error && !rows.some((r) => r.id === value);
3872
+ return /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-1", children: [
3873
+ /* @__PURE__ */ jsx18(
3874
+ CreatablePicker,
3875
+ {
3876
+ value: value || null,
3877
+ options,
3878
+ onSelect: onChange,
3879
+ placeholder: missing ? "Record not in this table" : "Choose a record",
3880
+ searchPlaceholder: "Search records",
3881
+ noun: "record",
3882
+ loading: records.loading,
3883
+ ...disabled ? { disabled: true } : {},
3884
+ ariaLabel: "Record"
3885
+ }
3886
+ ),
3887
+ records.error && /* @__PURE__ */ jsx18(RefusalLine, { error: records.error })
3888
+ ] });
3889
+ }
3890
+
3891
+ // src/DataBindingPicker.tsx
3892
+ import { useEffect as useEffect13, useMemo as useMemo8 } from "react";
3893
+ import { isEntityReferenceConfig } from "@ai-matrx/records";
3894
+ import { useFields as useFields2, useTable as useTable2 } from "@ai-matrx/records/react";
3895
+ import { BasicInput as BasicInput7, Button as Button10, CreatablePicker as CreatablePicker2, cn as cn13 } from "@ai-matrx/design-system";
3896
+ import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
3897
+ var DATA_BINDING_SHAPES = [
3898
+ { value: "collection", label: "The whole table" },
3899
+ { value: "reference", label: "One record" },
3900
+ { value: "value", label: "One field of a record" }
3901
+ ];
3902
+ var DATA_BINDING_MISSING = [
3903
+ { value: "absent", label: "Say it's missing" },
3904
+ { value: "block", label: "Stop the run" }
3905
+ ];
3906
+ function emptyDataBinding() {
3907
+ return {
3908
+ kind: "merge_field",
3909
+ source: "record",
3910
+ semantic_type: "collection",
3911
+ table_id: "",
3912
+ missing: "absent",
3913
+ override_policy: "shown_locked"
3914
+ };
3915
+ }
3916
+ function isDataBinding(value) {
3917
+ return Boolean(value) && typeof value === "object" && value.kind === "merge_field";
3918
+ }
3919
+ function isCompleteDataBinding(binding) {
3920
+ if (!binding.table_id) return false;
3921
+ if (binding.semantic_type === "collection") return true;
3922
+ if (!binding.record_id) return false;
3923
+ return binding.semantic_type === "value" ? Boolean(binding.field_key) : true;
3924
+ }
3925
+ function bindingPlaceholdersFor(field) {
3926
+ const label = fieldName(field);
3927
+ if (isEntityReferenceConfig(field.config)) {
3928
+ return [
3929
+ { token: `{${field.key}.name}`, label: `${label} \xB7 name` },
3930
+ { token: `{${field.key}.id}`, label: `${label} \xB7 id` }
3931
+ ];
3932
+ }
3933
+ return [{ token: `{${field.key}}`, label }];
3934
+ }
3935
+ var NAME_LIKE = /(^|_)(name|title|label)$/;
3936
+ var DESCRIPTIVE = /(desc|description|summary|notes?|details?|about|why|purpose|role|category|type|group|status)/;
3937
+ function defaultBindingTemplate(table, fields) {
3938
+ const first = fields[0];
3939
+ if (!first) return "- {name}";
3940
+ const declared = table?.title_field;
3941
+ const title = (declared ? fields.find((f) => f.key === declared || f.id === declared) : void 0) ?? fields.find((f) => NAME_LIKE.test(f.key)) ?? first;
3942
+ const token = (f) => bindingPlaceholdersFor(f)[0]?.token ?? `{${f.key}}`;
3943
+ const rest = fields.filter((f) => f.id !== title.id);
3944
+ const [a, b] = [...rest.filter((f) => DESCRIPTIVE.test(f.key)), ...rest.filter((f) => !DESCRIPTIVE.test(f.key))];
3945
+ if (!a) return `- ${token(title)}`;
3946
+ return b ? `- ${token(title)}: ${token(a)} (${token(b)})` : `- ${token(title)}: ${token(a)}`;
3947
+ }
3948
+ function DataBindingPicker({ dataSource, userId, binding, onChange, disabled, defaultRowLimit = null }) {
3949
+ const tables = useTablesAnywhere(dataSource);
3950
+ const tableId = binding.table_id || null;
3951
+ const selectTable = (id) => {
3952
+ if (id === binding.table_id) return;
3953
+ onChange({
3954
+ kind: "merge_field",
3955
+ source: "record",
3956
+ semantic_type: binding.semantic_type,
3957
+ table_id: id,
3958
+ missing: binding.missing,
3959
+ override_policy: "shown_locked",
3960
+ ...binding.semantic_type === "collection" && defaultRowLimit ? { limit: defaultRowLimit } : {}
3961
+ });
3962
+ };
3963
+ return /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-2", children: [
3964
+ /* @__PURE__ */ jsx19(TablePicker, { dataSource, value: tableId, onChange: selectTable, disabled: Boolean(disabled), tables }),
3965
+ /* @__PURE__ */ jsx19(TableScope, { dataSource, userId, tableId, tables, children: tableId && /* @__PURE__ */ jsx19(
3966
+ BindingDetails,
3967
+ {
3968
+ tableId,
3969
+ binding,
3970
+ onChange,
3971
+ disabled,
3972
+ defaultRowLimit
3973
+ }
3974
+ ) })
3975
+ ] });
3976
+ }
3977
+ function BindingDetails({
3978
+ tableId,
3979
+ binding,
3980
+ onChange,
3981
+ disabled,
3982
+ defaultRowLimit
3983
+ }) {
3984
+ const table = useTable2(tableId);
3985
+ const fields = useFields2(tableId);
3986
+ const fieldList = useMemo8(() => fields.data ?? [], [fields.data]);
3987
+ const shape = binding.semantic_type;
3988
+ useEffect13(() => {
3989
+ if (disabled || shape === "value" || binding.transform || fields.loading || fieldList.length === 0) return;
3990
+ onChange({
3991
+ ...binding,
3992
+ transform: {
3993
+ name: "list",
3994
+ template: defaultBindingTemplate(table.data ?? null, fieldList),
3995
+ join: "\n",
3996
+ ...shape === "collection" && binding.limit !== void 0 ? { max: binding.limit } : {}
3997
+ }
3998
+ });
3999
+ }, [disabled, shape, binding, fields.loading, fieldList, table.data, onChange]);
4000
+ const selectShape = (next) => {
4001
+ if (next === shape) return;
4002
+ const base = {
4003
+ kind: "merge_field",
4004
+ source: "record",
4005
+ semantic_type: next,
4006
+ table_id: binding.table_id,
4007
+ missing: binding.missing,
4008
+ override_policy: "shown_locked"
4009
+ };
4010
+ if (next !== "collection" && binding.record_id) base.record_id = binding.record_id;
4011
+ if (next === "value" && binding.field_key) base.field_key = binding.field_key;
4012
+ if (next === "collection") {
4013
+ const limit = binding.limit ?? defaultRowLimit;
4014
+ if (limit) base.limit = limit;
4015
+ }
4016
+ if (next !== "value" && binding.transform) {
4017
+ base.transform = {
4018
+ name: "list",
4019
+ template: binding.transform.template,
4020
+ ...binding.transform.join !== void 0 ? { join: binding.transform.join } : {},
4021
+ ...next === "collection" && base.limit !== void 0 ? { max: base.limit } : {}
4022
+ };
4023
+ }
4024
+ onChange(base);
4025
+ };
4026
+ const fieldOptions = fieldList.map((f) => ({ value: f.key, label: fieldName(f) }));
4027
+ const template = binding.transform?.template ?? "";
4028
+ const setTemplate = (next) => onChange({ ...binding, transform: { join: "\n", ...binding.transform, name: "list", template: next } });
4029
+ return /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-2", children: [
4030
+ /* @__PURE__ */ jsx19("div", { className: "flex flex-wrap gap-1", role: "radiogroup", "aria-label": "What to read", children: DATA_BINDING_SHAPES.map((s) => /* @__PURE__ */ jsx19(
4031
+ Button10,
4032
+ {
4033
+ type: "button",
4034
+ size: "sm",
4035
+ variant: s.value === shape ? "default" : "outline",
4036
+ role: "radio",
4037
+ "aria-checked": s.value === shape,
4038
+ disabled,
4039
+ onClick: () => selectShape(s.value),
4040
+ children: s.label
4041
+ },
4042
+ s.value
4043
+ )) }),
4044
+ shape !== "collection" && /* @__PURE__ */ jsx19(
4045
+ RecordPicker,
4046
+ {
4047
+ tableId,
4048
+ value: binding.record_id ?? null,
4049
+ onChange: (id) => onChange({ ...binding, record_id: id }),
4050
+ disabled: Boolean(disabled)
4051
+ }
4052
+ ),
4053
+ shape === "value" && /* @__PURE__ */ jsx19(
4054
+ CreatablePicker2,
4055
+ {
4056
+ value: binding.field_key ?? null,
4057
+ options: fieldOptions,
4058
+ onSelect: (key) => onChange({ ...binding, field_key: key }),
4059
+ placeholder: "Choose a column",
4060
+ searchPlaceholder: "Search columns",
4061
+ noun: "column",
4062
+ loading: fields.loading,
4063
+ ...disabled ? { disabled: true } : {},
4064
+ ariaLabel: "Column"
4065
+ }
4066
+ ),
4067
+ shape !== "value" && /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-1", children: [
4068
+ /* @__PURE__ */ jsx19(
4069
+ "textarea",
4070
+ {
4071
+ className: cn13(
4072
+ "min-h-[3.5rem] w-full rounded-md border border-border bg-background px-2 py-1.5 font-mono text-xs"
4073
+ ),
4074
+ "aria-label": "Each row reads as",
4075
+ value: template,
4076
+ disabled,
4077
+ onChange: (e) => setTemplate(e.target.value)
4078
+ }
4079
+ ),
4080
+ !disabled && /* @__PURE__ */ jsx19("div", { className: "flex flex-wrap gap-1", children: fieldList.flatMap(bindingPlaceholdersFor).map((p) => /* @__PURE__ */ jsx19(
4081
+ Button10,
4082
+ {
4083
+ type: "button",
4084
+ size: "sm",
4085
+ variant: "ghost",
4086
+ className: "h-6 px-1.5 text-[11px]",
4087
+ onClick: () => setTemplate(template + p.token),
4088
+ children: p.label
4089
+ },
4090
+ p.token
4091
+ )) })
4092
+ ] }),
4093
+ shape === "collection" && /* @__PURE__ */ jsxs16("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
4094
+ "Up to",
4095
+ /* @__PURE__ */ jsx19(
4096
+ BasicInput7,
4097
+ {
4098
+ type: "number",
4099
+ min: 1,
4100
+ className: "h-7 w-20 text-xs",
4101
+ value: binding.limit ?? "",
4102
+ disabled,
4103
+ onChange: (e) => {
4104
+ const n = Number.parseInt(e.target.value, 10);
4105
+ if (!Number.isFinite(n) || n < 1) return;
4106
+ onChange({ ...binding, limit: n, ...binding.transform ? { transform: { ...binding.transform, max: n } } : {} });
4107
+ }
4108
+ }
4109
+ ),
4110
+ "rows"
4111
+ ] }),
4112
+ /* @__PURE__ */ jsxs16("div", { className: "flex flex-wrap items-center gap-1", role: "radiogroup", "aria-label": "If nothing is found", children: [
4113
+ /* @__PURE__ */ jsx19("span", { className: "text-xs text-muted-foreground", children: "If nothing is found" }),
4114
+ DATA_BINDING_MISSING.map((m) => /* @__PURE__ */ jsx19(
4115
+ Button10,
4116
+ {
4117
+ type: "button",
4118
+ size: "sm",
4119
+ variant: m.value === binding.missing ? "default" : "outline",
4120
+ role: "radio",
4121
+ "aria-checked": m.value === binding.missing,
4122
+ disabled,
4123
+ onClick: () => onChange({ ...binding, missing: m.value }),
4124
+ children: m.label
4125
+ },
4126
+ m.value
4127
+ ))
4128
+ ] })
4129
+ ] });
4130
+ }
4131
+
4132
+ // src/PickedSummaries.tsx
4133
+ import { useFields as useFields3, useRecord, useTable as useTable3 } from "@ai-matrx/records/react";
4134
+
4135
+ // src/values.tsx
4136
+ import { formatColumnValue } from "@ai-matrx/design-system/data-table";
4137
+ import { formatFieldValue as formatFieldValue3 } from "@ai-matrx/design-system/field-formats";
4138
+ import { Badge as Badge5, cn as cn15 } from "@ai-matrx/design-system";
4139
+
4140
+ // src/enumLabels.ts
4141
+ function exhaustiveValuesOf() {
4142
+ return function(values) {
4143
+ return values;
4144
+ };
4145
+ }
4146
+ var WORK_DUE_STATES = exhaustiveValuesOf()([
4147
+ "overdue",
4148
+ "due_today",
4149
+ "scheduled",
4150
+ "undated",
4151
+ "finished"
4152
+ ]);
4153
+ var WORK_INBOX_KINDS = exhaustiveValuesOf()(["assignment", "approval", "proposal"]);
4154
+ var WORK_APPROVAL_STATES = exhaustiveValuesOf()(["pending", "approved", "declined", "withdrawn"]);
4155
+ var CHECKLIST_REQUIREMENT_KINDS = exhaustiveValuesOf()([
4156
+ "none",
4157
+ "note",
4158
+ "answer",
4159
+ "record_field",
4160
+ "form",
4161
+ "document"
4162
+ ]);
4163
+ var SUBSCRIPTION_CADENCES = exhaustiveValuesOf()(["instant", "hourly", "daily", "weekly"]);
4164
+ var ABSENCE_WORD_LABEL = {
4165
+ "never asked": "never asked",
4166
+ none: "none",
4167
+ refused: "refused",
4168
+ conflicting: "conflicting"
4169
+ };
4170
+
4171
+ // src/values.tsx
4172
+ import { wholeValueOf } from "@ai-matrx/records/core";
4173
+
4174
+ // src/WholeValue.tsx
4175
+ import { useEffect as useEffect14, useState as useState16 } from "react";
4176
+ import { wholeValueSize } from "@ai-matrx/records/core";
4177
+ import { BasicTextarea as BasicTextarea3 } from "@ai-matrx/design-system";
4178
+ import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
4179
+ var WHOLE_VALUE_UNBOUND = "The whole text is kept as a file, and this screen has no way to open files yet.";
4180
+ function wholeValueLine(pointer) {
4181
+ return `Open the whole text (${wholeValueSize(pointer)})`;
4182
+ }
4183
+ function WholeValueCell({ words, pointer }) {
4184
+ const host = useRecordsUi();
4185
+ const href = host.hrefForFile?.({ fileId: pointer.file_id });
4186
+ const line = wholeValueLine(pointer);
4187
+ return /* @__PURE__ */ jsxs17("span", { className: "flex min-w-0 items-baseline gap-2", "data-whole-value-file": pointer.file_id, children: [
4188
+ /* @__PURE__ */ jsxs17("span", { className: "truncate", title: `The first ${pointer.shown_chars} of ${pointer.chars} characters`, children: [
4189
+ words,
4190
+ "\u2026"
4191
+ ] }),
4192
+ href ? host.Link ? host.Link({ href, children: line, className: "shrink-0 text-xs text-primary underline-offset-2 hover:underline" }) : /* @__PURE__ */ jsx20("a", { className: "shrink-0 text-xs text-primary underline-offset-2 hover:underline", href, target: "_blank", rel: "noreferrer", children: line }) : /* @__PURE__ */ jsx20("span", { className: "shrink-0 text-xs italic text-muted-foreground", title: WHOLE_VALUE_UNBOUND, children: `${wholeValueSize(pointer)} in a file` })
4193
+ ] });
4194
+ }
4195
+
4196
+ // src/ReferenceChips.tsx
4197
+ import { entityRefsOf as entityRefsOf2 } from "@ai-matrx/records";
4198
+ import { Badge as Badge4, cn as cn14 } from "@ai-matrx/design-system";
4199
+
4200
+ // src/kindValues.ts
4201
+ import { tryDecodeDirective } from "@ai-matrx/content-ir/directives";
4202
+ var KIND_KEY = "__kind";
4203
+ var WHOLE_FENCE = /^\s*```(?:matrx|json)?[ \t]*\r?\n?([\s\S]*?)\r?\n?[ \t]*```\s*$/;
4204
+ function asObject(value) {
4205
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
4206
+ }
4207
+ function parseJsonObject(text) {
4208
+ const trimmed = text.trim();
4209
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
4210
+ try {
4211
+ return asObject(JSON.parse(trimmed));
4212
+ } catch {
4213
+ return null;
4214
+ }
4215
+ }
4216
+ function fromObject(obj) {
4217
+ const directive = tryDecodeDirective(obj);
4218
+ if (directive) return { value: obj, kind: directive.slug, directive: true, items: [...directive.items] };
4219
+ const named = obj[KIND_KEY];
4220
+ if (typeof named === "string" && named.trim() !== "") {
4221
+ const items = Array.isArray(obj.items) ? obj.items.filter((i) => asObject(i) !== null) : [];
4222
+ return { value: obj, kind: named.trim(), directive: false, items };
4223
+ }
4224
+ return null;
4225
+ }
4226
+ function kindValueOf(value) {
4227
+ const obj = asObject(value);
4228
+ if (obj) return fromObject(obj);
4229
+ if (typeof value !== "string" || value.length > 2e5) return null;
4230
+ const fenced = WHOLE_FENCE.exec(value);
4231
+ const parsed = parseJsonObject(fenced ? fenced[1] : value);
4232
+ return parsed ? fromObject(parsed) : null;
4233
+ }
4234
+ function kindItemWords(item) {
4235
+ for (const key of ["label", "title", "name", "display_name"]) {
4236
+ const v = item[key];
4237
+ if (typeof v === "string" && v.trim() !== "") return v.trim();
4238
+ }
4239
+ return null;
4240
+ }
4241
+ function kindValueWords(kv) {
4242
+ const named = kv.items.map(kindItemWords).filter((w) => w !== null);
4243
+ if (named.length > 0) return named.join(", ");
4244
+ return kindLabel(kv.kind);
4245
+ }
4246
+ function kindLabel(kind) {
4247
+ const words = kind.replace(/^directive_v\d+_/, "").replace(/_v\d+$/, "").replace(/_/g, " ").trim();
4248
+ return words ? words.charAt(0).toUpperCase() + words.slice(1) : "Structured value";
4249
+ }
4250
+
4251
+ // src/ReferenceChips.tsx
4252
+ import { jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
4253
+ function referencesOf(field, value, kind, labels) {
4254
+ if (kind === "entity_reference") {
4255
+ return entityRefsOf2(value).map((r) => ({ token: r.token, id: r.id, label: r.label }));
4256
+ }
4257
+ const ids = (Array.isArray(value) ? value : [value]).filter(isId).map(String);
4258
+ const tableId = typeof field.relation_target === "string" ? field.relation_target : void 0;
4259
+ return ids.map((id) => ({
4260
+ token: "record",
4261
+ id,
4262
+ label: labels?.(field, id) ?? null,
4263
+ tableId,
4264
+ ...labels?.archived?.(field, id) ? { archived: true } : {}
4265
+ }));
4266
+ }
4267
+ var ARCHIVED_REFERENCE_WORD = "Archived";
4268
+ var ARCHIVED_REFERENCE_TIP = "In Trash; restore it to use it again";
4269
+ function ReferenceChip({ reference, waiting }) {
4270
+ const host = useRecordsUi();
4271
+ const words = reference.label ?? (reference.token === "record" ? waiting ?? "Loading\u2026" : WITHHELD_ENTITY_LABEL);
4272
+ const drawn = host.renderReference ? host.renderReference({ ...reference, label: reference.label ?? words }) : null;
4273
+ return /* @__PURE__ */ jsxs18(
4274
+ Badge4,
4275
+ {
4276
+ variant: "secondary",
4277
+ className: cn14(
4278
+ "max-w-full min-w-0 gap-1 px-1.5 py-0 text-[11px] font-normal",
4279
+ reference.archived && "opacity-70"
4280
+ ),
4281
+ "data-records-reference-chip": reference.token,
4282
+ "data-records-reference-archived": reference.archived ? "" : void 0,
4283
+ title: reference.archived ? ARCHIVED_REFERENCE_TIP : words,
4284
+ children: [
4285
+ drawn ?? /* @__PURE__ */ jsx21("span", { className: "truncate", children: words }),
4286
+ reference.archived ? /* @__PURE__ */ jsx21("span", { className: "shrink-0 text-muted-foreground", children: ARCHIVED_REFERENCE_WORD }) : null
4287
+ ]
4288
+ }
4289
+ );
4290
+ }
4291
+ function ReferenceChips({ refs, waiting }) {
4292
+ if (refs.length === 0) return /* @__PURE__ */ jsx21("span", { className: "text-muted-foreground", children: "\u2014" });
4293
+ return /* @__PURE__ */ jsx21("span", { className: "flex min-w-0 flex-wrap gap-1", "data-records-reference-chips": "", children: refs.map((r) => /* @__PURE__ */ jsx21(ReferenceChip, { reference: r, waiting }, `${r.token}:${r.id}`)) });
4294
+ }
4295
+ function KindValueCell({
4296
+ kv,
4297
+ where,
4298
+ title
4299
+ }) {
4300
+ const host = useRecordsUi();
4301
+ if (host.renderKind) {
4302
+ return /* @__PURE__ */ jsx21("span", { className: "flex min-w-0 max-w-full items-center", "data-records-kind-value": kv.kind, children: host.renderKind({ value: kv.value, kind: kv.kind, where, title }) });
4303
+ }
4304
+ const named = kv.items.map(kindItemWords).filter((w) => w !== null);
4305
+ return /* @__PURE__ */ jsx21("span", { className: cn14("flex min-w-0 flex-wrap gap-1"), "data-records-kind-value": kv.kind, title: JSON.stringify(kv.value), children: named.length > 0 ? named.map((w, i) => /* @__PURE__ */ jsx21(Badge4, { variant: "secondary", className: "px-1.5 py-0 text-[11px] font-normal", children: w }, `${w}-${i}`)) : /* @__PURE__ */ jsx21(Badge4, { variant: "outline", className: "px-1.5 py-0 text-[11px] font-normal", children: kindLabel(kv.kind) }) });
4306
+ }
4307
+
4308
+ // src/values.tsx
4309
+ import { newerKindWords } from "@ai-matrx/records";
4310
+ import { Fragment as Fragment6, jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
4311
+ function statusMark(field, item) {
4312
+ const mark = statusMarkOf(field, item);
4313
+ return mark ? /* @__PURE__ */ jsx22("span", { "aria-label": mark.words, title: mark.words, className: "mr-1", "data-records-status-group": mark.group, children: mark.mark }) : null;
4314
+ }
4315
+ function envelopeFor(document2, key) {
4316
+ const block = document2?._values;
4317
+ return block?.[key];
4318
+ }
4319
+ function unresolvedFor(document2, key) {
4320
+ const sources = document2?._sources;
4321
+ if (!sources || typeof sources !== "object") return void 0;
4322
+ for (const source of Object.values(sources)) {
4323
+ const held = source?.unresolved?.[key];
4324
+ if (typeof held === "string" && held) return held;
4325
+ if (Array.isArray(held) && held.length) return held.map(String).join(", ");
4326
+ }
4327
+ return void 0;
4328
+ }
4329
+ function hasFormula(field) {
4330
+ const config = field.config ?? {};
4331
+ return Boolean(config.expr) || Boolean(String(config.formula_text ?? "").trim());
4332
+ }
4333
+ function computedEmptyOf(field, document2) {
4334
+ const kind = editorKindFor(field);
4335
+ const config = field.config ?? {};
4336
+ if (kind === "lookup" || kind === "rollup") {
4337
+ const via = typeof config["via"] === "string" ? config["via"] : null;
4338
+ const held = document2 && via ? document2[via] : void 0;
4339
+ const known = Boolean(document2 && via);
4340
+ const linked = !(held === null || held === void 0 || held === "" || Array.isArray(held) && held.length === 0);
4341
+ const count = kind === "rollup" && config["agg"] === "count";
4342
+ const which = kind === "lookup" ? "lookup" : count ? "count" : "rollup";
4343
+ if (!known) return { kind: which };
4344
+ if (kind === "lookup") return { kind: which, why: linked ? "Empty on the linked record" : "Pick a linked record" };
4345
+ if (!linked) return { kind: which, why: "No linked records yet" };
4346
+ return count ? { kind: which, words: "0" } : { kind: which, why: "Nothing to add up" };
4347
+ }
4348
+ if (kind !== "formula" && field.type !== "formula") return null;
4349
+ if (typeof config["system"] === "string") return { kind: "stamp" };
4350
+ if (!hasFormula(field)) return { kind: "formula", sentence: "Worked out once its formula is in place" };
4351
+ return { kind: "formula" };
4352
+ }
4353
+ var ABSENCE_WORDS = ABSENCE_WORD_LABEL;
4354
+ function renderValue(field, value, document2, labels, stillWaitingFor) {
4355
+ const envelope = envelopeFor(document2, field.key);
4356
+ if (envelope?.absent) {
4357
+ return /* @__PURE__ */ jsx22("span", { className: "text-xs italic text-muted-foreground", title: `VAL-2: ${envelope.absent}`, children: ABSENCE_WORDS[String(envelope.absent)] ?? String(envelope.absent) });
4358
+ }
4359
+ if (value === null || value === void 0 || value === "") {
4360
+ const gone = unresolvedFor(document2, field.key);
4361
+ if (gone) {
4362
+ return /* @__PURE__ */ jsx22("span", { className: "text-xs italic text-muted-foreground", title: `It pointed at ${gone}`, children: "The record this pointed at no longer exists" });
4363
+ }
4364
+ const computed = computedEmptyOf(field, document2);
4365
+ if (computed?.sentence) return /* @__PURE__ */ jsx22("span", { className: "text-xs italic text-muted-foreground", children: computed.sentence });
4366
+ if (computed?.words) return /* @__PURE__ */ jsx22("span", { "data-records-empty-why": computed.kind, children: computed.words });
4367
+ if (computed?.why) {
4368
+ return /* @__PURE__ */ jsx22("span", { className: "text-muted-foreground", title: computed.why, "data-records-empty-why": computed.kind, children: "\u2014" });
4369
+ }
4370
+ return /* @__PURE__ */ jsx22("span", { className: "text-muted-foreground", children: "\u2014" });
4371
+ }
4372
+ if (isSignatureField(field)) return /* @__PURE__ */ jsx22(SignatureValue, { value });
4373
+ const newer = newerKindValue(field, value);
4374
+ if (newer !== void 0) return newer;
4375
+ const whole = typeof value === "string" ? wholeValueOf(document2, field.key) : null;
4376
+ if (whole) return /* @__PURE__ */ jsx22(WholeValueCell, { words: value, pointer: whole });
4377
+ const kind = editorKindFor(field);
4378
+ if (kind === "relation" || kind === "entity_reference") {
4379
+ return /* @__PURE__ */ jsx22(ReferenceChips, { refs: referencesOf(field, value, kind, labels), waiting: stillWaitingFor });
4380
+ }
4381
+ const kv = kind === "attachment" || kind === "member" ? null : kindValueOf(value);
4382
+ if (kv) return /* @__PURE__ */ jsx22(KindValueCell, { kv, where: "cell", title: field.label });
4383
+ const chosen = kind === "select" || kind === "multi_select";
4384
+ const chipOf = (item) => {
4385
+ const look = choiceChipLookOf(field, item, scalarText(field, item, labels));
4386
+ return { className: look.className, tinted: look.color !== null };
4387
+ };
4388
+ if (chosen) {
4389
+ const items = Array.isArray(value) ? value : [value];
4390
+ return /* @__PURE__ */ jsx22("span", { className: "flex flex-wrap gap-1", "data-records-choice-chips": "", children: items.map((item, i) => {
4391
+ const chip = chipOf(item);
4392
+ return /* @__PURE__ */ jsxs19(
4393
+ Badge5,
4394
+ {
4395
+ variant: "outline",
4396
+ className: cn15("text-[11px]", chip.className),
4397
+ "data-records-choice-chip": "",
4398
+ ...chip.tinted ? { "data-records-choice-tint": "" } : {},
4399
+ children: [
4400
+ statusMark(field, item),
4401
+ scalarText(field, item, labels, Array.isArray(value) ? void 0 : stillWaitingFor)
4402
+ ]
4403
+ },
4404
+ `${String(item)}-${i}`
4405
+ );
4406
+ }) });
4407
+ }
4408
+ if (kind === "attachment") return /* @__PURE__ */ jsx22(AttachmentChips, { value });
4409
+ if (Array.isArray(value)) {
4410
+ return /* @__PURE__ */ jsx22("span", { className: "flex flex-wrap gap-1", children: value.map((item, i) => /* @__PURE__ */ jsx22(Badge5, { variant: "secondary", className: "text-[11px]", children: scalarText(field, item, labels) }, `${String(item)}-${i}`)) });
4411
+ }
4412
+ if (kind === "url" && typeof value === "string") {
4413
+ const href = value.includes("://") ? value : `https://${value}`;
4414
+ return /* @__PURE__ */ jsxs19(
4415
+ "a",
4416
+ {
4417
+ className: "inline-flex min-w-0 max-w-full items-center gap-1 rounded-md border border-border bg-muted/60 px-1.5 py-0.5 text-[11px] text-foreground hover:bg-accent",
4418
+ href,
4419
+ target: "_blank",
4420
+ rel: "noreferrer",
4421
+ title: value,
4422
+ "data-records-link-chip": "",
4423
+ onClick: (event) => event.stopPropagation(),
4424
+ children: [
4425
+ /* @__PURE__ */ jsx22("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "\u2197" }),
4426
+ /* @__PURE__ */ jsx22("span", { className: "truncate", children: linkChipText(value) })
4427
+ ]
4428
+ }
4429
+ );
4430
+ }
4431
+ if (kind === "phone" && typeof value === "string" && value.trim() !== "") {
4432
+ return /* @__PURE__ */ jsx22(
4433
+ "a",
4434
+ {
4435
+ className: "truncate tabular-nums text-primary underline-offset-2 hover:underline",
4436
+ href: `tel:${value.replace(/[^\d+]/g, "")}`,
4437
+ title: value,
4438
+ "data-records-phone": "",
4439
+ onClick: (event) => event.stopPropagation(),
4440
+ children: formatPhone(value)
4441
+ }
4442
+ );
4443
+ }
4444
+ if (kind === "email" && typeof value === "string") {
4445
+ return /* @__PURE__ */ jsx22("a", { className: "truncate text-primary underline-offset-2 hover:underline", href: `mailto:${value}`, children: value });
4446
+ }
4447
+ if (typeof value === "boolean") {
4448
+ return /* @__PURE__ */ jsx22("span", { className: kind === "checkbox" ? "text-sm" : "", title: value ? "Yes" : "No", children: value ? "\u2713" : "\u2014" });
4449
+ }
4450
+ return /* @__PURE__ */ jsx22("span", { className: cn15("truncate", kind === "number" || kind === "currency" || kind === "percent" ? "tabular-nums" : ""), children: scalarText(field, value, labels, stillWaitingFor) });
4451
+ }
4452
+ function scalarText(field, value, labels, stillWaitingFor) {
4453
+ if (value === null || value === void 0) return "\u2014";
4454
+ if (isSignatureField(field)) return signatureWords(value) ?? "\u2014";
4455
+ const newerWords = newerKindWords(field, value);
4456
+ if (newerWords !== null) return newerWords;
4457
+ const kind = editorKindFor(field);
4458
+ if (kind === "entity_reference") return entityReferenceText(value);
4459
+ if ((kind === "relation" || field.type === "list") && Array.isArray(value)) {
4460
+ return value.map((item) => scalarText(field, item, labels, stillWaitingFor)).join(", ");
4461
+ }
4462
+ const kv = kindValueOf(value);
4463
+ if (kv) return kindValueWords(kv);
4464
+ if (isId(value) && (field.type === "list" || field.type === "relation")) {
4465
+ return labels?.(field, String(value)) ?? stillWaitingFor ?? "Loading\u2026";
4466
+ }
4467
+ if (field.type === "list" && typeof value === "string" && labels) {
4468
+ const words = labels(field, value);
4469
+ if (words) return words;
4470
+ }
4471
+ const shownAs = shownAsOf(field);
4472
+ if (shownAs) {
4473
+ const said = formatFieldValue3(value, shownAs);
4474
+ if (said.ok && !said.empty) return said.text;
4475
+ }
4476
+ if (typeof value === "number") {
4477
+ if (kind === "percent") return `${value}%`;
4478
+ if (kind === "currency") return field.unit ? `${field.unit}${value.toLocaleString()}` : value.toLocaleString();
4479
+ return field.unit ? `${value.toLocaleString()} ${field.unit}` : value.toLocaleString();
4480
+ }
4481
+ if (kind === "datetime" || systemStampOf(field) === "created_time" || systemStampOf(field) === "modified_time") {
4482
+ const said = dateText(field, value);
4483
+ if (said !== null) return said;
4484
+ }
4485
+ if (Array.isArray(value)) return value.length === 0 ? "" : `${value.length} of them`;
4486
+ if (typeof value === "object") return "\u2026";
4487
+ return String(value);
4488
+ }
4489
+
4490
+ // src/PickedSummaries.tsx
4491
+ import { Fragment as Fragment7, jsx as jsx23, jsxs as jsxs20 } from "react/jsx-runtime";
4492
+ function TableNameText({ dataSource, tableId }) {
4493
+ const tables = useTablesAnywhere(dataSource);
4494
+ if (!tableId) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2014" });
4495
+ if (tables.loading) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2026" });
4496
+ return /* @__PURE__ */ jsx23(Fragment7, { children: tables.rows.find((t) => t.table_id === tableId)?.table_name ?? "\u2014" });
4497
+ }
4498
+ function ColumnValuesText({
4499
+ dataSource,
4500
+ userId,
4501
+ tableId,
4502
+ value
4503
+ }) {
4504
+ if (!tableId) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2014" });
4505
+ return /* @__PURE__ */ jsx23(TableScope, { dataSource, userId, tableId, fallback: /* @__PURE__ */ jsx23(Fragment7, { children: "\u2026" }), children: /* @__PURE__ */ jsx23(RecordLabelProvider, { children: /* @__PURE__ */ jsx23(ColumnValuesWords, { tableId, value }) }) });
4506
+ }
4507
+ function ColumnValuesWords({ tableId, value }) {
4508
+ const fields = useFields3(tableId);
4509
+ const labels = useRecordLabels(fields.data ?? void 0);
4510
+ if (fields.loading) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2026" });
4511
+ const parts = [];
4512
+ for (const [key, stored] of Object.entries(value)) {
4513
+ const field = (fields.data ?? []).find((f) => f.key === key);
4514
+ if (parts.length) parts.push(/* @__PURE__ */ jsx23("span", { children: ", " }, `${key}-sep`));
4515
+ parts.push(
4516
+ /* @__PURE__ */ jsxs20("span", { children: [
4517
+ field ? fieldName(field) : "A removed column",
4518
+ " is ",
4519
+ field ? renderValue(field, stored, void 0, labels) : "\u2014"
4520
+ ] }, key)
4521
+ );
4522
+ }
4523
+ return parts.length ? /* @__PURE__ */ jsx23(Fragment7, { children: parts }) : /* @__PURE__ */ jsx23(Fragment7, { children: "\u2014" });
4524
+ }
4525
+ function RecordNameText({
4526
+ dataSource,
4527
+ userId,
4528
+ tableId,
4529
+ recordId
4530
+ }) {
4531
+ if (!tableId || !recordId) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2014" });
4532
+ return /* @__PURE__ */ jsx23(TableScope, { dataSource, userId, tableId, fallback: /* @__PURE__ */ jsx23(Fragment7, { children: "\u2026" }), children: /* @__PURE__ */ jsx23(RecordNameWords, { tableId, recordId }) });
4533
+ }
4534
+ function RecordNameWords({ tableId, recordId }) {
4535
+ const table = useTable3(tableId);
4536
+ const record = useRecord(recordId);
4537
+ if (record.loading || table.loading) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2026" });
4538
+ if (!record.data) return /* @__PURE__ */ jsx23(Fragment7, { children: "A record you cannot open" });
4539
+ return /* @__PURE__ */ jsx23(Fragment7, { children: recordName(record.data.document, table.data?.title_field ?? null) });
4540
+ }
4541
+ export {
4542
+ ColumnValuesEditor,
4543
+ ColumnValuesText,
4544
+ DATA_BINDING_MISSING,
4545
+ DATA_BINDING_SHAPES,
4546
+ DataBindingPicker,
4547
+ RecordNameText,
4548
+ RecordPicker,
4549
+ TableNameText,
4550
+ TablePicker,
4551
+ TableScope,
4552
+ bindingPlaceholdersFor,
4553
+ defaultBindingTemplate,
4554
+ emptyDataBinding,
4555
+ isCompleteDataBinding,
4556
+ isDataBinding,
4557
+ useTablesAnywhere
4558
+ };
4559
+ //# sourceMappingURL=pickers.js.map