@agent-native/core 0.124.0 → 0.124.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +25 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/assets/branding/favicon-base64.ts +5 -0
- package/corpus/core/src/cli/templates-meta.ts +1 -0
- package/corpus/core/src/server/email.ts +2 -5
- package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +6 -3
- package/corpus/templates/analytics/server/lib/report-chart-svg.ts +18 -5
- package/corpus/templates/crm/actions/_crm-list-utils.ts +178 -0
- package/corpus/templates/crm/actions/add-crm-record-to-list.ts +23 -2
- package/corpus/templates/crm/actions/create-crm-list.ts +229 -43
- package/corpus/templates/crm/actions/create-crm-record.ts +13 -5
- package/corpus/templates/crm/actions/update-crm-list-entry.ts +1 -1
- package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +22 -2
- package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +143 -32
- package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +190 -75
- package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +267 -115
- package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +265 -131
- package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +79 -100
- package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +12 -8
- package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +27 -31
- package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +16 -13
- package/corpus/templates/crm/app/components/crm/record/attribute-row.tsx +108 -0
- package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +201 -9
- package/corpus/templates/crm/app/components/crm/record/panel-resize.ts +61 -0
- package/corpus/templates/crm/app/components/crm/record/record-data.ts +5 -4
- package/corpus/templates/crm/app/components/crm/shared/README-tokens.md +103 -0
- package/corpus/templates/crm/app/components/crm/shared/RecordReferencePicker.tsx +117 -0
- package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +84 -0
- package/corpus/templates/crm/app/components/crm/shared/ui-tokens.ts +89 -0
- package/corpus/templates/crm/app/global.css +170 -0
- package/corpus/templates/crm/app/i18n/en-US.ts +1 -0
- package/corpus/templates/crm/changelog/2026-07-26-a-half-typed-number-or-date-no-longer-saves-as-empty-the-fie.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-a-new-pipeline-board-now-opens-with-stage-columns-a-summabl.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-record-reference-fields-like-account-can-now-be-linked-from-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-retyping-a-record-field-now-replaces-the-old-value-instead-o.md +6 -0
- package/dist/assets/branding/favicon-base64.d.ts +2 -0
- package/dist/assets/branding/favicon-base64.d.ts.map +1 -0
- package/dist/assets/branding/favicon-base64.js +5 -0
- package/dist/assets/branding/favicon-base64.js.map +1 -0
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +1 -0
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +2 -2
- package/dist/server/email.js.map +1 -1
- package/package.json +1 -1
- package/src/assets/branding/favicon-base64.ts +5 -0
- package/src/cli/templates-meta.ts +1 -0
- package/src/server/email.ts +2 -5
|
@@ -1,13 +1,28 @@
|
|
|
1
|
-
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
2
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
3
2
|
import {
|
|
3
|
+
IconAbc,
|
|
4
4
|
IconAlertTriangle,
|
|
5
|
+
IconCalendar,
|
|
5
6
|
IconCheck,
|
|
7
|
+
IconClock,
|
|
6
8
|
IconClockExclamation,
|
|
7
9
|
IconCopy,
|
|
10
|
+
IconCurrencyDollar,
|
|
8
11
|
IconExternalLink,
|
|
12
|
+
IconId,
|
|
13
|
+
IconLink,
|
|
14
|
+
IconMail,
|
|
15
|
+
IconMapPin,
|
|
16
|
+
IconMessage,
|
|
17
|
+
IconNumbers,
|
|
18
|
+
IconPhone,
|
|
19
|
+
IconProgressCheck,
|
|
20
|
+
IconSelector,
|
|
21
|
+
IconSquareCheck,
|
|
9
22
|
IconStar,
|
|
10
23
|
IconStarFilled,
|
|
24
|
+
IconUser,
|
|
25
|
+
IconWorld,
|
|
11
26
|
} from "@tabler/icons-react";
|
|
12
27
|
import { useEffect, useRef, useState } from "react";
|
|
13
28
|
import { toast } from "sonner";
|
|
@@ -25,16 +40,20 @@ import {
|
|
|
25
40
|
} from "@/components/ui/popover";
|
|
26
41
|
import { cn } from "@/lib/utils";
|
|
27
42
|
|
|
43
|
+
import type { CrmAttributeType } from "../../../../shared/crm-attributes";
|
|
28
44
|
import {
|
|
29
45
|
activeOptions,
|
|
30
46
|
attributeInputValue,
|
|
47
|
+
editorInputType,
|
|
31
48
|
RATING_MAX,
|
|
49
|
+
referenceMembers,
|
|
50
|
+
referenceSearchKind,
|
|
32
51
|
valueTokens,
|
|
52
|
+
type CrmValueToken,
|
|
33
53
|
} from "../shared/attribute-value";
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} from "../shared/AttributeValueParts";
|
|
54
|
+
import { AttributeRating } from "../shared/AttributeValueParts";
|
|
55
|
+
import { RecordReferencePicker } from "../shared/RecordReferencePicker";
|
|
56
|
+
import { overlayProps } from "../shared/ui-tokens";
|
|
38
57
|
import {
|
|
39
58
|
cellSpecFor,
|
|
40
59
|
formatCell,
|
|
@@ -49,6 +68,86 @@ type Translate = ReturnType<typeof useT>;
|
|
|
49
68
|
|
|
50
69
|
const EMPTY = "—";
|
|
51
70
|
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Header affordances
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/** The leading glyph on a column header: the attribute's type, not its name. */
|
|
76
|
+
const TYPE_ICONS: Record<
|
|
77
|
+
CrmAttributeType,
|
|
78
|
+
React.ComponentType<{ className?: string }>
|
|
79
|
+
> = {
|
|
80
|
+
text: IconAbc,
|
|
81
|
+
number: IconNumbers,
|
|
82
|
+
checkbox: IconSquareCheck,
|
|
83
|
+
currency: IconCurrencyDollar,
|
|
84
|
+
date: IconCalendar,
|
|
85
|
+
timestamp: IconClock,
|
|
86
|
+
rating: IconStar,
|
|
87
|
+
status: IconProgressCheck,
|
|
88
|
+
select: IconSelector,
|
|
89
|
+
"record-reference": IconLink,
|
|
90
|
+
"actor-reference": IconUser,
|
|
91
|
+
location: IconMapPin,
|
|
92
|
+
domain: IconWorld,
|
|
93
|
+
"email-address": IconMail,
|
|
94
|
+
"phone-number": IconPhone,
|
|
95
|
+
interaction: IconMessage,
|
|
96
|
+
"personal-name": IconId,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export function AttributeTypeIcon({
|
|
100
|
+
type,
|
|
101
|
+
className,
|
|
102
|
+
}: {
|
|
103
|
+
type: CrmAttributeType;
|
|
104
|
+
className?: string;
|
|
105
|
+
}) {
|
|
106
|
+
const Icon = TYPE_ICONS[type] ?? IconAbc;
|
|
107
|
+
return <Icon className={className} />;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Avatars
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
export type CrmAvatarShape = "person" | "company";
|
|
115
|
+
|
|
116
|
+
function initials(name: string): string {
|
|
117
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
118
|
+
if (!words.length) return "";
|
|
119
|
+
const first = words[0]?.[0] ?? "";
|
|
120
|
+
const second = words.length > 1 ? (words[words.length - 1]?.[0] ?? "") : "";
|
|
121
|
+
return `${first}${second}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A record's avatar. Shape carries the object type — a round avatar is a
|
|
126
|
+
* person, a squircle is an organisation — so the two never have to be labelled.
|
|
127
|
+
*/
|
|
128
|
+
export function RecordAvatar({
|
|
129
|
+
name,
|
|
130
|
+
shape,
|
|
131
|
+
className,
|
|
132
|
+
}: {
|
|
133
|
+
name: string;
|
|
134
|
+
shape: CrmAvatarShape;
|
|
135
|
+
className?: string;
|
|
136
|
+
}) {
|
|
137
|
+
return (
|
|
138
|
+
<span
|
|
139
|
+
aria-hidden
|
|
140
|
+
className={cn(
|
|
141
|
+
"inline-flex size-5 shrink-0 items-center justify-center bg-muted text-[10px] font-medium uppercase leading-none text-content-secondary ring-1 ring-inset ring-hairline",
|
|
142
|
+
shape === "person" ? "rounded-full" : "rounded-avatar-company",
|
|
143
|
+
className,
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
{initials(name)}
|
|
147
|
+
</span>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
52
151
|
// ---------------------------------------------------------------------------
|
|
53
152
|
// Provenance
|
|
54
153
|
// ---------------------------------------------------------------------------
|
|
@@ -166,39 +265,122 @@ function copyToClipboard(text: string, t: Translate) {
|
|
|
166
265
|
.catch(() => toast.error(t("grid.copyFailed")));
|
|
167
266
|
}
|
|
168
267
|
|
|
268
|
+
/**
|
|
269
|
+
* A record reference. Hover tint rides the shared overlay rather than a
|
|
270
|
+
* background swap so it composites over a tinted row exactly like every other
|
|
271
|
+
* hover in the app.
|
|
272
|
+
*/
|
|
169
273
|
function Chip({
|
|
170
274
|
children,
|
|
275
|
+
avatar,
|
|
171
276
|
onCopy,
|
|
172
277
|
href,
|
|
173
278
|
}: {
|
|
174
279
|
children: React.ReactNode;
|
|
280
|
+
avatar?: React.ReactNode;
|
|
175
281
|
onCopy?: () => void;
|
|
176
282
|
href?: string;
|
|
177
283
|
}) {
|
|
178
284
|
return (
|
|
179
|
-
<span
|
|
180
|
-
|
|
285
|
+
<span
|
|
286
|
+
{...overlayProps({
|
|
287
|
+
className:
|
|
288
|
+
"inline-flex max-w-full items-center gap-1.5 self-start rounded-chip py-0.5 pl-1 pr-1.5 text-sm before:duration-[var(--motion-breezy)]",
|
|
289
|
+
})}
|
|
290
|
+
>
|
|
291
|
+
{avatar}
|
|
292
|
+
<span className="min-w-0 truncate">{children}</span>
|
|
181
293
|
{href ? (
|
|
182
294
|
<a
|
|
183
295
|
href={href}
|
|
184
296
|
target="_blank"
|
|
185
297
|
rel="noreferrer"
|
|
186
|
-
className="shrink-0 text-
|
|
298
|
+
className="shrink-0 text-content-ghost hover:text-foreground"
|
|
187
299
|
onClick={(event) => event.stopPropagation()}
|
|
188
300
|
>
|
|
189
|
-
<IconExternalLink className="size-3" />
|
|
301
|
+
<IconExternalLink className="size-3.5" />
|
|
190
302
|
</a>
|
|
191
303
|
) : null}
|
|
192
304
|
{onCopy ? (
|
|
193
305
|
<button
|
|
194
306
|
type="button"
|
|
195
|
-
className="shrink-0 cursor-pointer text-
|
|
307
|
+
className="shrink-0 cursor-pointer text-content-ghost hover:text-foreground"
|
|
196
308
|
onClick={(event) => {
|
|
197
309
|
event.stopPropagation();
|
|
198
310
|
onCopy();
|
|
199
311
|
}}
|
|
200
312
|
>
|
|
201
|
-
<IconCopy className="size-3" />
|
|
313
|
+
<IconCopy className="size-3.5" />
|
|
314
|
+
</button>
|
|
315
|
+
) : null}
|
|
316
|
+
</span>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* An option renders as a *tint* of its own colour, never a saturated fill: a
|
|
322
|
+
* grid of filled chips reads as decoration, and the colour stops carrying
|
|
323
|
+
* information. `color-mix` rather than an appended hex alpha — an option colour
|
|
324
|
+
* may be any CSS colour, and `#0a0` + "22" is not a colour at all.
|
|
325
|
+
*/
|
|
326
|
+
function OptionPill({ token }: { token: CrmValueToken }) {
|
|
327
|
+
return (
|
|
328
|
+
<span
|
|
329
|
+
className="inline-flex max-w-full items-center rounded-badge bg-foreground/[0.06] px-1.5 py-0.5 text-xs font-medium text-content-secondary"
|
|
330
|
+
style={
|
|
331
|
+
token.color
|
|
332
|
+
? {
|
|
333
|
+
backgroundColor: `color-mix(in srgb, ${token.color} 14%, transparent)`,
|
|
334
|
+
color: token.color,
|
|
335
|
+
}
|
|
336
|
+
: undefined
|
|
337
|
+
}
|
|
338
|
+
>
|
|
339
|
+
<span className="min-w-0 truncate">{token.label}</span>
|
|
340
|
+
</span>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** A value that is a destination: underlined, with a decoration soft enough
|
|
345
|
+
* that a column of them does not read as a stack of rules. */
|
|
346
|
+
function LinkValue({
|
|
347
|
+
text,
|
|
348
|
+
href,
|
|
349
|
+
onCopy,
|
|
350
|
+
}: {
|
|
351
|
+
text: string;
|
|
352
|
+
href?: string;
|
|
353
|
+
onCopy?: () => void;
|
|
354
|
+
}) {
|
|
355
|
+
const underline =
|
|
356
|
+
"min-w-0 truncate underline decoration-content-ghost underline-offset-[0.14em] hover:decoration-content-secondary";
|
|
357
|
+
return (
|
|
358
|
+
<span className="inline-flex min-w-0 max-w-full items-center gap-1">
|
|
359
|
+
{href ? (
|
|
360
|
+
<a
|
|
361
|
+
href={href}
|
|
362
|
+
// A `mailto:` opened in a new tab leaves an empty one behind.
|
|
363
|
+
{...(href.startsWith("http")
|
|
364
|
+
? { target: "_blank", rel: "noreferrer" }
|
|
365
|
+
: {})}
|
|
366
|
+
className={underline}
|
|
367
|
+
onClick={(event) => event.stopPropagation()}
|
|
368
|
+
>
|
|
369
|
+
{text}
|
|
370
|
+
</a>
|
|
371
|
+
) : (
|
|
372
|
+
<span className={underline}>{text}</span>
|
|
373
|
+
)}
|
|
374
|
+
{onCopy ? (
|
|
375
|
+
<button
|
|
376
|
+
type="button"
|
|
377
|
+
className="shrink-0 cursor-pointer text-content-ghost opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/row:opacity-100"
|
|
378
|
+
onClick={(event) => {
|
|
379
|
+
event.stopPropagation();
|
|
380
|
+
onCopy();
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
<IconCopy className="size-3.5" />
|
|
202
384
|
</button>
|
|
203
385
|
) : null}
|
|
204
386
|
</span>
|
|
@@ -218,10 +400,10 @@ function StatusPill({
|
|
|
218
400
|
const token = valueTokens(attribute, value)[0];
|
|
219
401
|
// A status whose option was deleted still shows its stored value: "—" would
|
|
220
402
|
// claim the record has no stage at all.
|
|
221
|
-
if (!token) return <span className="text-
|
|
403
|
+
if (!token) return <span className="text-content-ghost">{EMPTY}</span>;
|
|
222
404
|
return (
|
|
223
405
|
<span className="inline-flex items-center gap-1.5">
|
|
224
|
-
<
|
|
406
|
+
<OptionPill token={token} />
|
|
225
407
|
{overrun !== null ? (
|
|
226
408
|
<IconClockExclamation
|
|
227
409
|
className="size-3.5 shrink-0 text-amber-600 dark:text-amber-500"
|
|
@@ -274,12 +456,12 @@ export function CellDisplay({
|
|
|
274
456
|
if (type === "select") {
|
|
275
457
|
const tokens = valueTokens(attribute, value);
|
|
276
458
|
if (!tokens.length) {
|
|
277
|
-
return <span className="text-
|
|
459
|
+
return <span className="text-content-ghost">{EMPTY}</span>;
|
|
278
460
|
}
|
|
279
461
|
return (
|
|
280
|
-
<span className="flex
|
|
462
|
+
<span className="flex items-center gap-1 overflow-hidden">
|
|
281
463
|
{tokens.map((token, index) => (
|
|
282
|
-
<
|
|
464
|
+
<OptionPill key={`${token.label}:${index}`} token={token} />
|
|
283
465
|
))}
|
|
284
466
|
</span>
|
|
285
467
|
);
|
|
@@ -287,22 +469,25 @@ export function CellDisplay({
|
|
|
287
469
|
if (type === "email-address" || type === "domain") {
|
|
288
470
|
const values = Array.isArray(value) ? value : value === null ? [] : [value];
|
|
289
471
|
if (!values.length) {
|
|
290
|
-
return <span className="text-
|
|
472
|
+
return <span className="text-content-ghost">{EMPTY}</span>;
|
|
291
473
|
}
|
|
292
474
|
return (
|
|
293
|
-
<span className="flex
|
|
475
|
+
<span className="flex items-center gap-2 overflow-hidden">
|
|
294
476
|
{values.map((entry) => {
|
|
295
477
|
const text = String(entry);
|
|
296
478
|
return (
|
|
297
|
-
<
|
|
479
|
+
<LinkValue
|
|
298
480
|
key={text}
|
|
481
|
+
text={text}
|
|
299
482
|
onCopy={() => copyToClipboard(text, t)}
|
|
300
|
-
{
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
483
|
+
href={
|
|
484
|
+
type === "domain"
|
|
485
|
+
? text.startsWith("http")
|
|
486
|
+
? text
|
|
487
|
+
: `https://${text}`
|
|
488
|
+
: `mailto:${text}`
|
|
489
|
+
}
|
|
490
|
+
/>
|
|
306
491
|
);
|
|
307
492
|
})}
|
|
308
493
|
</span>
|
|
@@ -310,13 +495,27 @@ export function CellDisplay({
|
|
|
310
495
|
}
|
|
311
496
|
if (type === "record-reference" || type === "actor-reference") {
|
|
312
497
|
const text = formatCell(attribute, value);
|
|
313
|
-
if (!text) return <span className="text-
|
|
314
|
-
return
|
|
498
|
+
if (!text) return <span className="text-content-ghost">{EMPTY}</span>;
|
|
499
|
+
return (
|
|
500
|
+
<Chip
|
|
501
|
+
avatar={
|
|
502
|
+
<RecordAvatar
|
|
503
|
+
name={text}
|
|
504
|
+
// A reference carries no object type, so only an actor — always a
|
|
505
|
+
// person — can claim the round shape.
|
|
506
|
+
shape={type === "actor-reference" ? "person" : "company"}
|
|
507
|
+
className="size-4 text-[9px]"
|
|
508
|
+
/>
|
|
509
|
+
}
|
|
510
|
+
>
|
|
511
|
+
{text}
|
|
512
|
+
</Chip>
|
|
513
|
+
);
|
|
315
514
|
}
|
|
316
515
|
|
|
317
516
|
const text = formatCell(attribute, value);
|
|
318
|
-
if (!text) return <span className="text-
|
|
319
|
-
return <span className="block truncate">{text}</span>;
|
|
517
|
+
if (!text) return <span className="text-content-ghost">{EMPTY}</span>;
|
|
518
|
+
return <span className="block min-w-0 truncate">{text}</span>;
|
|
320
519
|
}
|
|
321
520
|
|
|
322
521
|
// ---------------------------------------------------------------------------
|
|
@@ -324,7 +523,7 @@ export function CellDisplay({
|
|
|
324
523
|
// ---------------------------------------------------------------------------
|
|
325
524
|
|
|
326
525
|
const INPUT_CLASS =
|
|
327
|
-
"h-full w-full border-0 bg-transparent px-
|
|
526
|
+
"h-full w-full border-0 bg-transparent px-3 text-sm outline-none ring-0 placeholder:text-content-ghost";
|
|
328
527
|
|
|
329
528
|
function useAutoFocus<T extends HTMLElement>() {
|
|
330
529
|
const ref = useRef<T>(null);
|
|
@@ -335,76 +534,6 @@ function useAutoFocus<T extends HTMLElement>() {
|
|
|
335
534
|
return ref;
|
|
336
535
|
}
|
|
337
536
|
|
|
338
|
-
function ReferencePicker({
|
|
339
|
-
attribute,
|
|
340
|
-
onPick,
|
|
341
|
-
onCancel,
|
|
342
|
-
}: {
|
|
343
|
-
attribute: CrmGridAttribute;
|
|
344
|
-
onPick: (value: string) => void;
|
|
345
|
-
onCancel: () => void;
|
|
346
|
-
}) {
|
|
347
|
-
const t = useT();
|
|
348
|
-
const [search, setSearch] = useState("");
|
|
349
|
-
const inputRef = useAutoFocus<HTMLInputElement>();
|
|
350
|
-
const results = useActionQuery<{
|
|
351
|
-
records?: Array<{ id: string; displayName: string; subtitle?: string }>;
|
|
352
|
-
}>(
|
|
353
|
-
"list-crm-records" as never,
|
|
354
|
-
{ query: search.trim() || undefined, limit: 8 } as never,
|
|
355
|
-
{ enabled: search.trim().length > 1 } as never,
|
|
356
|
-
);
|
|
357
|
-
const records = results.data?.records ?? [];
|
|
358
|
-
return (
|
|
359
|
-
<div className="w-64">
|
|
360
|
-
<input
|
|
361
|
-
ref={inputRef}
|
|
362
|
-
value={search}
|
|
363
|
-
onChange={(event) => setSearch(event.target.value)}
|
|
364
|
-
onKeyDown={(event) => {
|
|
365
|
-
if (event.key === "Escape") onCancel();
|
|
366
|
-
}}
|
|
367
|
-
placeholder={t("grid.searchRecords")}
|
|
368
|
-
className="w-full border-b border-border/70 bg-transparent px-3 py-2 text-sm outline-none"
|
|
369
|
-
/>
|
|
370
|
-
<div className="max-h-56 overflow-y-auto py-1">
|
|
371
|
-
{search.trim().length <= 1 ? (
|
|
372
|
-
<p className="px-3 py-2 text-xs text-muted-foreground">
|
|
373
|
-
{t("grid.searchToLink")}
|
|
374
|
-
</p>
|
|
375
|
-
) : results.isError ? (
|
|
376
|
-
<p className="px-3 py-2 text-xs text-destructive">
|
|
377
|
-
{t("grid.searchFailed")}
|
|
378
|
-
</p>
|
|
379
|
-
) : records.length === 0 ? (
|
|
380
|
-
<p className="px-3 py-2 text-xs text-muted-foreground">
|
|
381
|
-
{results.isLoading ? t("grid.searching") : t("grid.noMatches")}
|
|
382
|
-
</p>
|
|
383
|
-
) : (
|
|
384
|
-
records.map((record) => (
|
|
385
|
-
<button
|
|
386
|
-
key={record.id}
|
|
387
|
-
type="button"
|
|
388
|
-
className="flex w-full cursor-pointer flex-col items-start px-3 py-1.5 text-left text-sm hover:bg-muted"
|
|
389
|
-
onClick={() => onPick(record.displayName)}
|
|
390
|
-
>
|
|
391
|
-
<span className="truncate font-medium">{record.displayName}</span>
|
|
392
|
-
{record.subtitle ? (
|
|
393
|
-
<span className="truncate text-xs text-muted-foreground">
|
|
394
|
-
{record.subtitle}
|
|
395
|
-
</span>
|
|
396
|
-
) : null}
|
|
397
|
-
</button>
|
|
398
|
-
))
|
|
399
|
-
)}
|
|
400
|
-
</div>
|
|
401
|
-
<p className="border-t border-border/60 px-3 py-1.5 text-[11px] text-muted-foreground">
|
|
402
|
-
{t("grid.referenceHint", { attribute: attribute.label })}
|
|
403
|
-
</p>
|
|
404
|
-
</div>
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
537
|
function OptionPicker({
|
|
409
538
|
attribute,
|
|
410
539
|
value,
|
|
@@ -446,10 +575,13 @@ function OptionPicker({
|
|
|
446
575
|
onClick={() => toggle(option.value)}
|
|
447
576
|
>
|
|
448
577
|
<span
|
|
449
|
-
className=
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
578
|
+
className={cn(
|
|
579
|
+
"size-3 shrink-0 rounded-full",
|
|
580
|
+
// An uncolored option is a ring, not a grey dot: a filled grey
|
|
581
|
+
// reads as "colored grey" next to real option colours.
|
|
582
|
+
option.color ? "" : "ring-1 ring-inset ring-hairline",
|
|
583
|
+
)}
|
|
584
|
+
style={option.color ? { backgroundColor: option.color } : undefined}
|
|
453
585
|
/>
|
|
454
586
|
<span className="flex-1 truncate">{option.title}</span>
|
|
455
587
|
{selected.has(option.value) ? (
|
|
@@ -495,7 +627,7 @@ export function CellEditor({
|
|
|
495
627
|
|
|
496
628
|
if (spec.editor === "checkbox") {
|
|
497
629
|
return (
|
|
498
|
-
<span className="flex h-full items-
|
|
630
|
+
<span className="flex h-full items-start justify-center pt-2">
|
|
499
631
|
<Checkbox
|
|
500
632
|
autoFocus
|
|
501
633
|
checked={value === true}
|
|
@@ -509,23 +641,25 @@ export function CellEditor({
|
|
|
509
641
|
if (spec.editor === "rating") {
|
|
510
642
|
const current = typeof value === "number" ? value : 0;
|
|
511
643
|
return (
|
|
512
|
-
<span className="flex h-full items-
|
|
513
|
-
|
|
514
|
-
(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
644
|
+
<span className="flex h-full items-start px-3 pt-2">
|
|
645
|
+
<span className="flex items-center gap-0.5 rounded-lg px-1.5 py-0.5 ring-1 ring-inset ring-transparent transition-[box-shadow] duration-[var(--motion-comfortable)] hover:ring-hairline">
|
|
646
|
+
{Array.from({ length: RATING_MAX }, (_, index) => index + 1).map(
|
|
647
|
+
(step) => (
|
|
648
|
+
<button
|
|
649
|
+
key={step}
|
|
650
|
+
type="button"
|
|
651
|
+
className="cursor-pointer"
|
|
652
|
+
onClick={() => onCommit(step === current ? null : step, false)}
|
|
653
|
+
>
|
|
654
|
+
{step <= current ? (
|
|
655
|
+
<IconStarFilled className="size-3.5 text-amber-500" />
|
|
656
|
+
) : (
|
|
657
|
+
<IconStar className="size-3.5 text-content-ghost hover:text-amber-500" />
|
|
658
|
+
)}
|
|
659
|
+
</button>
|
|
660
|
+
),
|
|
661
|
+
)}
|
|
662
|
+
</span>
|
|
529
663
|
</span>
|
|
530
664
|
);
|
|
531
665
|
}
|
|
@@ -539,7 +673,7 @@ export function CellEditor({
|
|
|
539
673
|
}}
|
|
540
674
|
>
|
|
541
675
|
<PopoverTrigger asChild>
|
|
542
|
-
<span className="flex h-full items-
|
|
676
|
+
<span className="flex h-full items-start px-3 pt-2 text-sm">
|
|
543
677
|
<CellDisplay attribute={attribute} value={value} />
|
|
544
678
|
</span>
|
|
545
679
|
</PopoverTrigger>
|
|
@@ -563,13 +697,15 @@ export function CellEditor({
|
|
|
563
697
|
}}
|
|
564
698
|
>
|
|
565
699
|
<PopoverTrigger asChild>
|
|
566
|
-
<span className="flex h-full items-
|
|
700
|
+
<span className="flex h-full items-start px-3 pt-2 text-sm">
|
|
567
701
|
<CellDisplay attribute={attribute} value={value} />
|
|
568
702
|
</span>
|
|
569
703
|
</PopoverTrigger>
|
|
570
704
|
<PopoverContent align="start" className="p-0">
|
|
571
|
-
<
|
|
572
|
-
|
|
705
|
+
<RecordReferencePicker
|
|
706
|
+
label={attribute.label}
|
|
707
|
+
kind={referenceSearchKind(attribute)}
|
|
708
|
+
selected={referenceMembers(value)}
|
|
573
709
|
onPick={(next) => onCommit(next, true)}
|
|
574
710
|
onCancel={onCancel}
|
|
575
711
|
/>
|
|
@@ -578,16 +714,14 @@ export function CellEditor({
|
|
|
578
714
|
);
|
|
579
715
|
}
|
|
580
716
|
|
|
581
|
-
|
|
582
|
-
// 34px cell steal the scroll wheel while the sheet is being scrolled.
|
|
583
|
-
const inputType = spec.inputType === "number" ? "text" : spec.inputType;
|
|
717
|
+
const input = editorInputType(attribute);
|
|
584
718
|
|
|
585
719
|
return (
|
|
586
720
|
<input
|
|
587
721
|
ref={inputRef}
|
|
588
|
-
type={
|
|
722
|
+
type={input.type}
|
|
589
723
|
defaultValue={text}
|
|
590
|
-
|
|
724
|
+
{...(input.inputMode ? { inputMode: input.inputMode } : {})}
|
|
591
725
|
onChange={(event) => setText(event.target.value)}
|
|
592
726
|
onKeyDown={(event) => {
|
|
593
727
|
const intent = resolveGridKey(event, { editing: true });
|