@tradalab/lyra 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/blocks/index.js +2 -1
- package/dist/blocks/index.js.map +1 -1
- package/dist/chunk-5M44N7SV.js +237 -0
- package/dist/chunk-5M44N7SV.js.map +1 -0
- package/dist/chunk-7GY2KYNW.js +250 -0
- package/dist/chunk-7GY2KYNW.js.map +1 -0
- package/dist/{chunk-RSAN65FI.js → chunk-APYQOJAJ.js} +4 -20
- package/dist/chunk-APYQOJAJ.js.map +1 -0
- package/dist/{chunk-2PHVSGAE.js → chunk-F5HR244O.js} +18 -254
- package/dist/chunk-F5HR244O.js.map +1 -0
- package/dist/chunk-PI6SANLG.js +22 -0
- package/dist/chunk-PI6SANLG.js.map +1 -0
- package/dist/chunk-PL37KFRJ.js +3 -0
- package/dist/chunk-PL37KFRJ.js.map +1 -0
- package/dist/chunk-QGUKTRBR.js +25 -0
- package/dist/chunk-QGUKTRBR.js.map +1 -0
- package/dist/{chunk-SQA4ILAK.js → chunk-VEWKPOMS.js} +4 -18
- package/dist/chunk-VEWKPOMS.js.map +1 -0
- package/dist/data-table/index.js +2 -1
- package/dist/data-table/index.js.map +1 -1
- package/dist/people/index.d.ts +170 -0
- package/dist/people/index.js +319 -0
- package/dist/people/index.js.map +1 -0
- package/dist/shell/index.js +4 -2
- package/dist/shell/index.js.map +1 -1
- package/dist/ui/index.js +13 -233
- package/dist/ui/index.js.map +1 -1
- package/dist/utils/index.js +1 -0
- package/package.json +5 -1
- package/styles/globals.css +1 -1
- package/dist/chunk-2PHVSGAE.js.map +0 -1
- package/dist/chunk-RSAN65FI.js.map +0 -1
- package/dist/chunk-SQA4ILAK.js.map +0 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as React$1 from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The shape the pickers render.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately NOT contact's `Person` type. lyra must not know that a service called
|
|
7
|
+
* contact exists: if it imported that type, every field contact adds would force a
|
|
8
|
+
* lyra release and re-lock every app that consumes it. Each app maps its own DTO onto
|
|
9
|
+
* this in about five lines, and only that app changes when its source changes.
|
|
10
|
+
*
|
|
11
|
+
* It covers people AND groups/orgs because the pickers are used to grant access as
|
|
12
|
+
* often as to address a message, and "share with the Legal team" is the same gesture
|
|
13
|
+
* as "share with Ada".
|
|
14
|
+
*/
|
|
15
|
+
interface Principal<T = unknown> {
|
|
16
|
+
/**
|
|
17
|
+
* The caller's own object, handed straight back on select.
|
|
18
|
+
*
|
|
19
|
+
* A picker's job is to answer "which one", but the app almost always needs more
|
|
20
|
+
* than what it renders — contact needs the contactId to open the right editor,
|
|
21
|
+
* drive needs the grantee kind. Without this the app has to rebuild a lookup from
|
|
22
|
+
* the key it just handed out, which is a map that exists only because the type
|
|
23
|
+
* threw information away.
|
|
24
|
+
*/
|
|
25
|
+
data?: T;
|
|
26
|
+
/** Stable id in the caller's world: a uid, a group id, an org id. */
|
|
27
|
+
id?: string;
|
|
28
|
+
email?: string;
|
|
29
|
+
displayName?: string;
|
|
30
|
+
avatarUrl?: string;
|
|
31
|
+
/** One line under the name: a title, an org, "in your contacts". */
|
|
32
|
+
subtitle?: string;
|
|
33
|
+
kind?: "person" | "group" | "org";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* How the app finds candidates. Injected, never implemented here.
|
|
37
|
+
*
|
|
38
|
+
* An EMPTY query is a legitimate call and should return the caller's recent or
|
|
39
|
+
* frequent people — that is what makes a picker feel like it knows you rather than
|
|
40
|
+
* like a search box you have to feed first.
|
|
41
|
+
*
|
|
42
|
+
* The signal is passed so a fast typist does not race their own requests; the picker
|
|
43
|
+
* aborts the previous lookup on every keystroke.
|
|
44
|
+
*/
|
|
45
|
+
type PrincipalSearch<T = unknown> = (query: string, signal?: AbortSignal) => Promise<Principal<T>[]>;
|
|
46
|
+
/** Labels are injected: lyra ships no copy and no i18n runtime. */
|
|
47
|
+
interface PeopleLabels {
|
|
48
|
+
placeholder?: string;
|
|
49
|
+
/** Shown while the list is empty and nothing has been typed. */
|
|
50
|
+
hint?: string;
|
|
51
|
+
noResults?: string;
|
|
52
|
+
/** Offer to use a typed address that matched nothing, e.g. "Use {email}". */
|
|
53
|
+
useTyped?: string;
|
|
54
|
+
/** Marks an entry the caller excluded, e.g. "already added". */
|
|
55
|
+
excluded?: string;
|
|
56
|
+
remove?: string;
|
|
57
|
+
}
|
|
58
|
+
/** A plausible email. Not RFC-complete on purpose — this gates a UI affordance,
|
|
59
|
+
* and the server validates for real. Being strict here only refuses addresses that
|
|
60
|
+
* actually work. */
|
|
61
|
+
declare function looksLikeEmail(s: string): boolean;
|
|
62
|
+
/** The key a principal is deduped and excluded by. */
|
|
63
|
+
declare function principalKey(p: Principal<unknown>): string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Initials for the avatar fallback. Falls back through name → email → "?" so a
|
|
67
|
+
* principal that is nothing but a typed address still renders as something.
|
|
68
|
+
*/
|
|
69
|
+
declare function initials(p: Principal): string;
|
|
70
|
+
declare function PersonAvatar({ principal, className, }: {
|
|
71
|
+
principal: Principal;
|
|
72
|
+
className?: string;
|
|
73
|
+
}): React$1.JSX.Element;
|
|
74
|
+
/**
|
|
75
|
+
* One line: avatar, name, and whatever the app chose to say underneath.
|
|
76
|
+
*
|
|
77
|
+
* Shared by the picker, the token field and any table that lists people — a person
|
|
78
|
+
* rendered three different ways in three apps is how a suite stops feeling like one
|
|
79
|
+
* product.
|
|
80
|
+
*/
|
|
81
|
+
declare function PersonRow({ principal, className, trailing, }: {
|
|
82
|
+
principal: Principal;
|
|
83
|
+
className?: string;
|
|
84
|
+
/** Slot for whatever the caller attaches per person: a role select, a remove button. */
|
|
85
|
+
trailing?: React.ReactNode;
|
|
86
|
+
}): React$1.JSX.Element;
|
|
87
|
+
|
|
88
|
+
interface PeoplePickerProps<T = unknown> {
|
|
89
|
+
/** Injected by the app — lyra never knows where people come from. */
|
|
90
|
+
search: PrincipalSearch<T>;
|
|
91
|
+
onSelect: (p: Principal<T>) => void;
|
|
92
|
+
/** Already-chosen keys (id or email): shown dimmed, not hidden. */
|
|
93
|
+
exclude?: string[];
|
|
94
|
+
/**
|
|
95
|
+
* Offer a typed address that matched nothing. Essential wherever the other party
|
|
96
|
+
* is a stranger by definition (a contract counterparty), and wrong wherever they
|
|
97
|
+
* must already exist (adding somebody to an org).
|
|
98
|
+
*/
|
|
99
|
+
allowFreeEntry?: boolean;
|
|
100
|
+
labels?: PeopleLabels;
|
|
101
|
+
disabled?: boolean;
|
|
102
|
+
className?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Pick one person (or group, or org), from a popover.
|
|
106
|
+
*
|
|
107
|
+
* Filtering is SERVER-side — `shouldFilter={false}` — because ranking is the point:
|
|
108
|
+
* the backend knows who this user actually works with, and cmdk's local fuzzy match
|
|
109
|
+
* would reorder that into alphabetical noise.
|
|
110
|
+
*/
|
|
111
|
+
declare function PeoplePicker<T = unknown>({ search, onSelect, exclude, allowFreeEntry, labels, disabled, className, }: PeoplePickerProps<T>): React$1.JSX.Element;
|
|
112
|
+
|
|
113
|
+
interface PeopleFieldProps<T = unknown> {
|
|
114
|
+
search: PrincipalSearch<T>;
|
|
115
|
+
/** Chosen so far — rendered as tokens. */
|
|
116
|
+
value: Principal<T>[];
|
|
117
|
+
onAdd: (p: Principal<T>) => void;
|
|
118
|
+
onRemove: (p: Principal<T>) => void;
|
|
119
|
+
allowFreeEntry?: boolean;
|
|
120
|
+
labels?: PeopleLabels;
|
|
121
|
+
disabled?: boolean;
|
|
122
|
+
className?: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* A To:-style token field: type, pick, repeat.
|
|
126
|
+
*
|
|
127
|
+
* The popover picker and this are NOT the same component with a flag. A "To" line is
|
|
128
|
+
* used dozens of times a minute and lives or dies on keyboard flow — type, Enter,
|
|
129
|
+
* type, Enter, Backspace to undo — whereas a share dialog is opened once and clicked
|
|
130
|
+
* through. Forcing one into the other's shape produces something that is bad at both.
|
|
131
|
+
*
|
|
132
|
+
* Paste is handled: a list copied out of a spreadsheet or another mail client arrives
|
|
133
|
+
* as "a@x.com, b@y.com; c@z.com" and becomes three tokens, because the alternative is
|
|
134
|
+
* the user hand-splitting it.
|
|
135
|
+
*/
|
|
136
|
+
declare function PeopleField<T = unknown>({ search, value, onAdd, onRemove, allowFreeEntry, labels, disabled, className, }: PeopleFieldProps<T>): React$1.JSX.Element;
|
|
137
|
+
|
|
138
|
+
interface UsePrincipalSearchOptions<T = unknown> {
|
|
139
|
+
search: PrincipalSearch<T>;
|
|
140
|
+
/** Keys (id or email) to mark as already chosen. */
|
|
141
|
+
exclude?: string[];
|
|
142
|
+
/** Milliseconds to wait after the last keystroke. */
|
|
143
|
+
debounceMs?: number;
|
|
144
|
+
limit?: number;
|
|
145
|
+
}
|
|
146
|
+
interface PrincipalSearchState<T = unknown> {
|
|
147
|
+
query: string;
|
|
148
|
+
setQuery: (q: string) => void;
|
|
149
|
+
results: Principal<T>[];
|
|
150
|
+
loading: boolean;
|
|
151
|
+
error: string | null;
|
|
152
|
+
/** Excluded keys, lowercased — the UI dims these rather than hiding them. */
|
|
153
|
+
excluded: Set<string>;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The behaviour both pickers share: debounce, abort the previous request, keep the
|
|
157
|
+
* last results visible while the next ones load.
|
|
158
|
+
*
|
|
159
|
+
* Two decisions worth keeping:
|
|
160
|
+
*
|
|
161
|
+
* - **Results are NOT cleared while loading.** Emptying the list on every keystroke
|
|
162
|
+
* makes a picker flicker between "here they are" and "nothing found", and users
|
|
163
|
+
* read that flash as a failed search.
|
|
164
|
+
* - **Excluded entries stay in the list, dimmed.** Removing them makes the search
|
|
165
|
+
* look broken to the one person who most expects a hit — "I know Ada is in here,
|
|
166
|
+
* why can't I find her?" — when the truth is she is already added.
|
|
167
|
+
*/
|
|
168
|
+
declare function usePrincipalSearch<T = unknown>({ search, exclude, debounceMs, limit, }: UsePrincipalSearchOptions<T>): PrincipalSearchState<T>;
|
|
169
|
+
|
|
170
|
+
export { PeopleField, type PeopleFieldProps, type PeopleLabels, PeoplePicker, type PeoplePickerProps, PersonAvatar, PersonRow, type Principal, type PrincipalSearch, type PrincipalSearchState, type UsePrincipalSearchOptions, initials, looksLikeEmail, principalKey, usePrincipalSearch };
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import '../chunk-PL37KFRJ.js';
|
|
3
|
+
import { Popover, PopoverTrigger, PopoverContent, Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from '../chunk-5M44N7SV.js';
|
|
4
|
+
import '../chunk-APYQOJAJ.js';
|
|
5
|
+
import { Avatar, AvatarImage, AvatarFallback } from '../chunk-7GY2KYNW.js';
|
|
6
|
+
import { Input } from '../chunk-PI6SANLG.js';
|
|
7
|
+
import { Button } from '../chunk-5Q4UTRYU.js';
|
|
8
|
+
import { cn } from '../chunk-XH55BHUU.js';
|
|
9
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
10
|
+
import * as React from 'react';
|
|
11
|
+
import { ChevronsUpDown, X } from 'lucide-react';
|
|
12
|
+
|
|
13
|
+
// src/people/types.ts
|
|
14
|
+
function looksLikeEmail(s) {
|
|
15
|
+
const v = s.trim();
|
|
16
|
+
return v.length >= 3 && !/\s/.test(v) && /^[^@]+@[^@.]+\.[^@]+$/.test(v);
|
|
17
|
+
}
|
|
18
|
+
function principalKey(p) {
|
|
19
|
+
return (p.id || p.email || p.displayName || "").toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
function initials(p) {
|
|
22
|
+
const src = (p.displayName || "").trim() || p.email || "?";
|
|
23
|
+
const parts = src.split(/\s+/).filter(Boolean);
|
|
24
|
+
if (parts.length >= 2 && parts[0] && parts[1]) {
|
|
25
|
+
return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
26
|
+
}
|
|
27
|
+
return src.slice(0, 2).toUpperCase();
|
|
28
|
+
}
|
|
29
|
+
function PersonAvatar({
|
|
30
|
+
principal,
|
|
31
|
+
className
|
|
32
|
+
}) {
|
|
33
|
+
return /* @__PURE__ */ jsxs(Avatar, { className, children: [
|
|
34
|
+
principal.avatarUrl ? /* @__PURE__ */ jsx(AvatarImage, { src: principal.avatarUrl, alt: principal.displayName ?? "" }) : null,
|
|
35
|
+
/* @__PURE__ */ jsx(AvatarFallback, { className: "text-xs", children: initials(principal) })
|
|
36
|
+
] });
|
|
37
|
+
}
|
|
38
|
+
function PersonRow({
|
|
39
|
+
principal,
|
|
40
|
+
className,
|
|
41
|
+
trailing
|
|
42
|
+
}) {
|
|
43
|
+
const name = principal.displayName || principal.email || "\u2014";
|
|
44
|
+
const sub = principal.subtitle && principal.subtitle !== name ? principal.subtitle : void 0;
|
|
45
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex min-w-0 items-center gap-3", className), children: [
|
|
46
|
+
/* @__PURE__ */ jsx(PersonAvatar, { principal, className: "size-8 shrink-0" }),
|
|
47
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
48
|
+
/* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium", children: name }),
|
|
49
|
+
sub ? /* @__PURE__ */ jsx("div", { className: "text-muted-foreground truncate text-xs", children: sub }) : null
|
|
50
|
+
] }),
|
|
51
|
+
trailing
|
|
52
|
+
] });
|
|
53
|
+
}
|
|
54
|
+
function usePrincipalSearch({
|
|
55
|
+
search,
|
|
56
|
+
exclude,
|
|
57
|
+
debounceMs = 200,
|
|
58
|
+
limit
|
|
59
|
+
}) {
|
|
60
|
+
const [query, setQuery] = React.useState("");
|
|
61
|
+
const [results, setResults] = React.useState([]);
|
|
62
|
+
const [loading, setLoading] = React.useState(false);
|
|
63
|
+
const [error, setError] = React.useState(null);
|
|
64
|
+
const excluded = React.useMemo(
|
|
65
|
+
() => new Set((exclude ?? []).filter(Boolean).map((k) => k.toLowerCase())),
|
|
66
|
+
[exclude]
|
|
67
|
+
);
|
|
68
|
+
const searchRef = React.useRef(search);
|
|
69
|
+
searchRef.current = search;
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
let cancelled = false;
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
setLoading(true);
|
|
75
|
+
setError(null);
|
|
76
|
+
searchRef.current(query, controller.signal).then((found) => {
|
|
77
|
+
if (cancelled) return;
|
|
78
|
+
const seen = /* @__PURE__ */ new Set();
|
|
79
|
+
const deduped = found.filter((p) => {
|
|
80
|
+
const k = principalKey(p);
|
|
81
|
+
if (!k || seen.has(k)) return false;
|
|
82
|
+
seen.add(k);
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
setResults(limit ? deduped.slice(0, limit) : deduped);
|
|
86
|
+
}).catch((e) => {
|
|
87
|
+
if (cancelled || e instanceof DOMException && e.name === "AbortError") return;
|
|
88
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
89
|
+
}).finally(() => {
|
|
90
|
+
if (!cancelled) setLoading(false);
|
|
91
|
+
});
|
|
92
|
+
}, query ? debounceMs : 0);
|
|
93
|
+
return () => {
|
|
94
|
+
cancelled = true;
|
|
95
|
+
controller.abort();
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
};
|
|
98
|
+
}, [query, debounceMs, limit]);
|
|
99
|
+
return { query, setQuery, results, loading, error, excluded };
|
|
100
|
+
}
|
|
101
|
+
function PeoplePicker({
|
|
102
|
+
search,
|
|
103
|
+
onSelect,
|
|
104
|
+
exclude,
|
|
105
|
+
allowFreeEntry,
|
|
106
|
+
labels,
|
|
107
|
+
disabled,
|
|
108
|
+
className
|
|
109
|
+
}) {
|
|
110
|
+
const [open, setOpen] = React.useState(false);
|
|
111
|
+
const { query, setQuery, results, loading, excluded } = usePrincipalSearch({
|
|
112
|
+
search,
|
|
113
|
+
exclude
|
|
114
|
+
});
|
|
115
|
+
const typedEmail = allowFreeEntry && looksLikeEmail(query) ? query.trim() : "";
|
|
116
|
+
const alreadyListed = results.some((p) => p.email?.toLowerCase() === typedEmail.toLowerCase());
|
|
117
|
+
const offerTyped = typedEmail !== "" && !alreadyListed;
|
|
118
|
+
function choose(p) {
|
|
119
|
+
onSelect(p);
|
|
120
|
+
setOpen(false);
|
|
121
|
+
setQuery("");
|
|
122
|
+
}
|
|
123
|
+
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
124
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
125
|
+
Button,
|
|
126
|
+
{
|
|
127
|
+
variant: "outline",
|
|
128
|
+
role: "combobox",
|
|
129
|
+
"aria-expanded": open,
|
|
130
|
+
disabled,
|
|
131
|
+
className: cn("text-muted-foreground w-full justify-between font-normal", className),
|
|
132
|
+
children: [
|
|
133
|
+
labels?.placeholder ?? "Search people",
|
|
134
|
+
/* @__PURE__ */ jsx(ChevronsUpDown, { className: "size-4 opacity-50" })
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
) }),
|
|
138
|
+
/* @__PURE__ */ jsx(PopoverContent, { className: "w-[22rem] p-0", align: "start", children: /* @__PURE__ */ jsxs(Command, { shouldFilter: false, children: [
|
|
139
|
+
/* @__PURE__ */ jsx(
|
|
140
|
+
CommandInput,
|
|
141
|
+
{
|
|
142
|
+
value: query,
|
|
143
|
+
onValueChange: setQuery,
|
|
144
|
+
placeholder: labels?.placeholder ?? "Search people"
|
|
145
|
+
}
|
|
146
|
+
),
|
|
147
|
+
/* @__PURE__ */ jsxs(CommandList, { children: [
|
|
148
|
+
results.length === 0 && !offerTyped && !loading ? /* @__PURE__ */ jsx(CommandEmpty, { children: query ? labels?.noResults ?? "No matches" : labels?.hint ?? "Start typing" }) : null,
|
|
149
|
+
offerTyped ? /* @__PURE__ */ jsx(CommandGroup, { children: /* @__PURE__ */ jsxs(
|
|
150
|
+
CommandItem,
|
|
151
|
+
{
|
|
152
|
+
value: "__typed__",
|
|
153
|
+
onSelect: () => choose({ email: typedEmail, kind: "person" }),
|
|
154
|
+
children: [
|
|
155
|
+
/* @__PURE__ */ jsx(
|
|
156
|
+
PersonRow,
|
|
157
|
+
{
|
|
158
|
+
principal: { email: typedEmail, displayName: typedEmail }
|
|
159
|
+
}
|
|
160
|
+
),
|
|
161
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground ml-auto shrink-0 text-xs", children: labels?.useTyped ?? "Use this address" })
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
) }) : null,
|
|
165
|
+
results.length > 0 ? /* @__PURE__ */ jsx(CommandGroup, { children: results.map((p, i) => {
|
|
166
|
+
const off = excluded.has(principalKey(p));
|
|
167
|
+
return /* @__PURE__ */ jsxs(
|
|
168
|
+
CommandItem,
|
|
169
|
+
{
|
|
170
|
+
value: String(i),
|
|
171
|
+
disabled: off,
|
|
172
|
+
onSelect: () => !off && choose(p),
|
|
173
|
+
className: cn(off && "opacity-50"),
|
|
174
|
+
children: [
|
|
175
|
+
/* @__PURE__ */ jsx(PersonRow, { principal: p }),
|
|
176
|
+
off ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground ml-auto shrink-0 text-xs", children: labels?.excluded ?? "already added" }) : null
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
principalKey(p) || i
|
|
180
|
+
);
|
|
181
|
+
}) }) : null
|
|
182
|
+
] })
|
|
183
|
+
] }) })
|
|
184
|
+
] });
|
|
185
|
+
}
|
|
186
|
+
function PeopleField({
|
|
187
|
+
search,
|
|
188
|
+
value,
|
|
189
|
+
onAdd,
|
|
190
|
+
onRemove,
|
|
191
|
+
allowFreeEntry,
|
|
192
|
+
labels,
|
|
193
|
+
disabled,
|
|
194
|
+
className
|
|
195
|
+
}) {
|
|
196
|
+
const [focused, setFocused] = React.useState(false);
|
|
197
|
+
const exclude = React.useMemo(() => value.map(principalKey), [value]);
|
|
198
|
+
const { query, setQuery, results, excluded } = usePrincipalSearch({ search, exclude, limit: 6 });
|
|
199
|
+
const chosen = React.useMemo(() => new Set(exclude), [exclude]);
|
|
200
|
+
const typedEmail = allowFreeEntry && looksLikeEmail(query) ? query.trim() : "";
|
|
201
|
+
function add(p) {
|
|
202
|
+
const k = principalKey(p);
|
|
203
|
+
if (!k || chosen.has(k)) return;
|
|
204
|
+
onAdd(p);
|
|
205
|
+
setQuery("");
|
|
206
|
+
}
|
|
207
|
+
function onKeyDown(e) {
|
|
208
|
+
if ((e.key === "Enter" || e.key === "Tab" || e.key === ",") && query.trim()) {
|
|
209
|
+
const pick = results.find((p) => !excluded.has(principalKey(p)));
|
|
210
|
+
if (pick) {
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
add(pick);
|
|
213
|
+
} else if (typedEmail) {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
add({ email: typedEmail, kind: "person" });
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (e.key === "Backspace" && query === "" && value.length > 0) {
|
|
220
|
+
onRemove(value[value.length - 1]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function onPaste(e) {
|
|
224
|
+
const text = e.clipboardData.getData("text");
|
|
225
|
+
const parts = text.split(/[,;\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
226
|
+
if (parts.length < 2) return;
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
for (const part of parts) {
|
|
229
|
+
if (looksLikeEmail(part)) add({ email: part, kind: "person" });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", className), children: [
|
|
233
|
+
/* @__PURE__ */ jsxs(
|
|
234
|
+
"div",
|
|
235
|
+
{
|
|
236
|
+
className: cn(
|
|
237
|
+
"border-input focus-within:ring-ring flex min-h-10 flex-wrap items-center gap-1.5 rounded-md border px-2 py-1.5 focus-within:ring-1",
|
|
238
|
+
disabled && "opacity-50"
|
|
239
|
+
),
|
|
240
|
+
children: [
|
|
241
|
+
value.map((p) => /* @__PURE__ */ jsxs(
|
|
242
|
+
"span",
|
|
243
|
+
{
|
|
244
|
+
className: "bg-muted flex items-center gap-1.5 rounded-full py-0.5 pl-0.5 pr-2 text-xs",
|
|
245
|
+
children: [
|
|
246
|
+
/* @__PURE__ */ jsx(PersonAvatar, { principal: p, className: "size-5" }),
|
|
247
|
+
/* @__PURE__ */ jsx("span", { className: "max-w-40 truncate", children: p.displayName || p.email }),
|
|
248
|
+
/* @__PURE__ */ jsx(
|
|
249
|
+
"button",
|
|
250
|
+
{
|
|
251
|
+
type: "button",
|
|
252
|
+
"aria-label": labels?.remove ?? "Remove",
|
|
253
|
+
onClick: () => onRemove(p),
|
|
254
|
+
disabled,
|
|
255
|
+
className: "opacity-60 hover:opacity-100",
|
|
256
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3" })
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
]
|
|
260
|
+
},
|
|
261
|
+
principalKey(p)
|
|
262
|
+
)),
|
|
263
|
+
/* @__PURE__ */ jsx(
|
|
264
|
+
Input,
|
|
265
|
+
{
|
|
266
|
+
value: query,
|
|
267
|
+
disabled,
|
|
268
|
+
placeholder: value.length === 0 ? labels?.placeholder ?? "Add people" : void 0,
|
|
269
|
+
onChange: (e) => setQuery(e.target.value),
|
|
270
|
+
onKeyDown,
|
|
271
|
+
onPaste,
|
|
272
|
+
onFocus: () => setFocused(true),
|
|
273
|
+
onBlur: () => setTimeout(() => setFocused(false), 150),
|
|
274
|
+
className: "h-7 min-w-40 flex-1 border-0 bg-transparent px-1 shadow-none focus-visible:ring-0"
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
),
|
|
280
|
+
focused && (results.length > 0 || typedEmail) ? /* @__PURE__ */ jsxs("div", { className: "bg-popover max-h-64 overflow-auto rounded-md border p-1 shadow-md", children: [
|
|
281
|
+
typedEmail && !results.some((p) => p.email === typedEmail) ? /* @__PURE__ */ jsxs(
|
|
282
|
+
"button",
|
|
283
|
+
{
|
|
284
|
+
type: "button",
|
|
285
|
+
onClick: () => add({ email: typedEmail, kind: "person" }),
|
|
286
|
+
className: "hover:bg-accent flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left",
|
|
287
|
+
children: [
|
|
288
|
+
/* @__PURE__ */ jsx(PersonRow, { principal: { email: typedEmail, displayName: typedEmail } }),
|
|
289
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground ml-auto shrink-0 text-xs", children: labels?.useTyped ?? "Use this address" })
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
) : null,
|
|
293
|
+
results.map((p) => {
|
|
294
|
+
const off = excluded.has(principalKey(p));
|
|
295
|
+
return /* @__PURE__ */ jsxs(
|
|
296
|
+
"button",
|
|
297
|
+
{
|
|
298
|
+
type: "button",
|
|
299
|
+
disabled: off,
|
|
300
|
+
onClick: () => add(p),
|
|
301
|
+
className: cn(
|
|
302
|
+
"hover:bg-accent flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left",
|
|
303
|
+
off && "opacity-50"
|
|
304
|
+
),
|
|
305
|
+
children: [
|
|
306
|
+
/* @__PURE__ */ jsx(PersonRow, { principal: p }),
|
|
307
|
+
off ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground ml-auto shrink-0 text-xs", children: labels?.excluded ?? "already added" }) : null
|
|
308
|
+
]
|
|
309
|
+
},
|
|
310
|
+
principalKey(p)
|
|
311
|
+
);
|
|
312
|
+
})
|
|
313
|
+
] }) : null
|
|
314
|
+
] });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export { PeopleField, PeoplePicker, PersonAvatar, PersonRow, initials, looksLikeEmail, principalKey, usePrincipalSearch };
|
|
318
|
+
//# sourceMappingURL=index.js.map
|
|
319
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/people/types.ts","../../src/people/person-row.tsx","../../src/people/use-principal-search.ts","../../src/people/people-picker.tsx","../../src/people/people-field.tsx"],"names":["React2","jsxs","jsx","React3"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The shape the pickers render.\n *\n * Deliberately NOT contact's `Person` type. lyra must not know that a service called\n * contact exists: if it imported that type, every field contact adds would force a\n * lyra release and re-lock every app that consumes it. Each app maps its own DTO onto\n * this in about five lines, and only that app changes when its source changes.\n *\n * It covers people AND groups/orgs because the pickers are used to grant access as\n * often as to address a message, and \"share with the Legal team\" is the same gesture\n * as \"share with Ada\".\n */\nexport interface Principal<T = unknown> {\n /**\n * The caller's own object, handed straight back on select.\n *\n * A picker's job is to answer \"which one\", but the app almost always needs more\n * than what it renders — contact needs the contactId to open the right editor,\n * drive needs the grantee kind. Without this the app has to rebuild a lookup from\n * the key it just handed out, which is a map that exists only because the type\n * threw information away.\n */\n data?: T\n /** Stable id in the caller's world: a uid, a group id, an org id. */\n id?: string\n email?: string\n displayName?: string\n avatarUrl?: string\n /** One line under the name: a title, an org, \"in your contacts\". */\n subtitle?: string\n kind?: \"person\" | \"group\" | \"org\"\n}\n\n/**\n * How the app finds candidates. Injected, never implemented here.\n *\n * An EMPTY query is a legitimate call and should return the caller's recent or\n * frequent people — that is what makes a picker feel like it knows you rather than\n * like a search box you have to feed first.\n *\n * The signal is passed so a fast typist does not race their own requests; the picker\n * aborts the previous lookup on every keystroke.\n */\nexport type PrincipalSearch<T = unknown> = (\n query: string,\n signal?: AbortSignal,\n) => Promise<Principal<T>[]>\n\n/** Labels are injected: lyra ships no copy and no i18n runtime. */\nexport interface PeopleLabels {\n placeholder?: string\n /** Shown while the list is empty and nothing has been typed. */\n hint?: string\n noResults?: string\n /** Offer to use a typed address that matched nothing, e.g. \"Use {email}\". */\n useTyped?: string\n /** Marks an entry the caller excluded, e.g. \"already added\". */\n excluded?: string\n remove?: string\n}\n\n/** A plausible email. Not RFC-complete on purpose — this gates a UI affordance,\n * and the server validates for real. Being strict here only refuses addresses that\n * actually work. */\nexport function looksLikeEmail(s: string): boolean {\n const v = s.trim()\n return v.length >= 3 && !/\\s/.test(v) && /^[^@]+@[^@.]+\\.[^@]+$/.test(v)\n}\n\n/** The key a principal is deduped and excluded by. */\nexport function principalKey(p: Principal<unknown>): string {\n return (p.id || p.email || p.displayName || \"\").toLowerCase()\n}\n","\"use client\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"~/ui/primitives/avatar.js\"\nimport { cn } from \"~/utils/index.js\"\nimport type { Principal } from \"./types.js\"\n\n/**\n * Initials for the avatar fallback. Falls back through name → email → \"?\" so a\n * principal that is nothing but a typed address still renders as something.\n */\nexport function initials(p: Principal): string {\n const src = (p.displayName || \"\").trim() || p.email || \"?\"\n const parts = src.split(/\\s+/).filter(Boolean)\n if (parts.length >= 2 && parts[0] && parts[1]) {\n return (parts[0][0]! + parts[1][0]!).toUpperCase()\n }\n return src.slice(0, 2).toUpperCase()\n}\n\nexport function PersonAvatar({\n principal,\n className,\n}: {\n principal: Principal\n className?: string\n}) {\n return (\n <Avatar className={className}>\n {principal.avatarUrl ? (\n <AvatarImage src={principal.avatarUrl} alt={principal.displayName ?? \"\"} />\n ) : null}\n <AvatarFallback className=\"text-xs\">{initials(principal)}</AvatarFallback>\n </Avatar>\n )\n}\n\n/**\n * One line: avatar, name, and whatever the app chose to say underneath.\n *\n * Shared by the picker, the token field and any table that lists people — a person\n * rendered three different ways in three apps is how a suite stops feeling like one\n * product.\n */\nexport function PersonRow({\n principal,\n className,\n trailing,\n}: {\n principal: Principal\n className?: string\n /** Slot for whatever the caller attaches per person: a role select, a remove button. */\n trailing?: React.ReactNode\n}) {\n const name = principal.displayName || principal.email || \"—\"\n // The subtitle is dropped when it would just repeat the name — a row reading\n // \"ada@x.com / ada@x.com\" is noise.\n const sub = principal.subtitle && principal.subtitle !== name ? principal.subtitle : undefined\n\n return (\n <div className={cn(\"flex min-w-0 items-center gap-3\", className)}>\n <PersonAvatar principal={principal} className=\"size-8 shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm font-medium\">{name}</div>\n {sub ? <div className=\"text-muted-foreground truncate text-xs\">{sub}</div> : null}\n </div>\n {trailing}\n </div>\n )\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport type { Principal, PrincipalSearch } from \"./types.js\"\nimport { principalKey } from \"./types.js\"\n\nexport interface UsePrincipalSearchOptions<T = unknown> {\n search: PrincipalSearch<T>\n /** Keys (id or email) to mark as already chosen. */\n exclude?: string[]\n /** Milliseconds to wait after the last keystroke. */\n debounceMs?: number\n limit?: number\n}\n\nexport interface PrincipalSearchState<T = unknown> {\n query: string\n setQuery: (q: string) => void\n results: Principal<T>[]\n loading: boolean\n error: string | null\n /** Excluded keys, lowercased — the UI dims these rather than hiding them. */\n excluded: Set<string>\n}\n\n/**\n * The behaviour both pickers share: debounce, abort the previous request, keep the\n * last results visible while the next ones load.\n *\n * Two decisions worth keeping:\n *\n * - **Results are NOT cleared while loading.** Emptying the list on every keystroke\n * makes a picker flicker between \"here they are\" and \"nothing found\", and users\n * read that flash as a failed search.\n * - **Excluded entries stay in the list, dimmed.** Removing them makes the search\n * look broken to the one person who most expects a hit — \"I know Ada is in here,\n * why can't I find her?\" — when the truth is she is already added.\n */\nexport function usePrincipalSearch<T = unknown>({\n search,\n exclude,\n debounceMs = 200,\n limit,\n}: UsePrincipalSearchOptions<T>): PrincipalSearchState<T> {\n const [query, setQuery] = React.useState(\"\")\n const [results, setResults] = React.useState<Principal<T>[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const excluded = React.useMemo(\n () => new Set((exclude ?? []).filter(Boolean).map((k) => k.toLowerCase())),\n [exclude],\n )\n\n // Keep the latest search in a ref: a caller that re-creates the function every\n // render must not cancel and refire the request on every render.\n const searchRef = React.useRef(search)\n searchRef.current = search\n\n React.useEffect(() => {\n const controller = new AbortController()\n let cancelled = false\n\n const timer = setTimeout(() => {\n setLoading(true)\n setError(null)\n searchRef\n .current(query, controller.signal)\n .then((found) => {\n if (cancelled) return\n const seen = new Set<string>()\n const deduped = found.filter((p) => {\n const k = principalKey(p)\n if (!k || seen.has(k)) return false\n seen.add(k)\n return true\n })\n setResults(limit ? deduped.slice(0, limit) : deduped)\n })\n .catch((e: unknown) => {\n // An abort is the normal path on every keystroke, not a failure.\n if (cancelled || (e instanceof DOMException && e.name === \"AbortError\")) return\n setError(e instanceof Error ? e.message : String(e))\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n // An empty query still searches: that is how recents appear before typing.\n }, query ? debounceMs : 0)\n\n return () => {\n cancelled = true\n controller.abort()\n clearTimeout(timer)\n }\n }, [query, debounceMs, limit])\n\n return { query, setQuery, results, loading, error, excluded }\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronsUpDown } from \"lucide-react\"\nimport { Button } from \"~/ui/primitives/button.js\"\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"~/ui/primitives/command.js\"\nimport { Popover, PopoverContent, PopoverTrigger } from \"~/ui/primitives/popover.js\"\nimport { cn } from \"~/utils/index.js\"\nimport { PersonRow } from \"./person-row.js\"\nimport type { PeopleLabels, Principal, PrincipalSearch } from \"./types.js\"\nimport { looksLikeEmail, principalKey } from \"./types.js\"\nimport { usePrincipalSearch } from \"./use-principal-search.js\"\n\nexport interface PeoplePickerProps<T = unknown> {\n /** Injected by the app — lyra never knows where people come from. */\n search: PrincipalSearch<T>\n onSelect: (p: Principal<T>) => void\n /** Already-chosen keys (id or email): shown dimmed, not hidden. */\n exclude?: string[]\n /**\n * Offer a typed address that matched nothing. Essential wherever the other party\n * is a stranger by definition (a contract counterparty), and wrong wherever they\n * must already exist (adding somebody to an org).\n */\n allowFreeEntry?: boolean\n labels?: PeopleLabels\n disabled?: boolean\n className?: string\n}\n\n/**\n * Pick one person (or group, or org), from a popover.\n *\n * Filtering is SERVER-side — `shouldFilter={false}` — because ranking is the point:\n * the backend knows who this user actually works with, and cmdk's local fuzzy match\n * would reorder that into alphabetical noise.\n */\nexport function PeoplePicker<T = unknown>({\n search,\n onSelect,\n exclude,\n allowFreeEntry,\n labels,\n disabled,\n className,\n}: PeoplePickerProps<T>) {\n const [open, setOpen] = React.useState(false)\n const { query, setQuery, results, loading, excluded } = usePrincipalSearch({\n search,\n exclude,\n })\n\n const typedEmail = allowFreeEntry && looksLikeEmail(query) ? query.trim() : \"\"\n const alreadyListed = results.some((p) => p.email?.toLowerCase() === typedEmail.toLowerCase())\n const offerTyped = typedEmail !== \"\" && !alreadyListed\n\n function choose(p: Principal<T>) {\n onSelect(p)\n setOpen(false)\n setQuery(\"\")\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n disabled={disabled}\n className={cn(\"text-muted-foreground w-full justify-between font-normal\", className)}\n >\n {labels?.placeholder ?? \"Search people\"}\n <ChevronsUpDown className=\"size-4 opacity-50\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-[22rem] p-0\" align=\"start\">\n <Command shouldFilter={false}>\n <CommandInput\n value={query}\n onValueChange={setQuery}\n placeholder={labels?.placeholder ?? \"Search people\"}\n />\n <CommandList>\n {results.length === 0 && !offerTyped && !loading ? (\n <CommandEmpty>\n {query ? (labels?.noResults ?? \"No matches\") : (labels?.hint ?? \"Start typing\")}\n </CommandEmpty>\n ) : null}\n\n {offerTyped ? (\n <CommandGroup>\n <CommandItem\n value=\"__typed__\"\n onSelect={() => choose({ email: typedEmail, kind: \"person\" })}\n >\n <PersonRow\n principal={{ email: typedEmail, displayName: typedEmail }}\n // The affordance has to name the address, or it reads as a\n // generic \"add\" and people do not trust it with a contract.\n />\n <span className=\"text-muted-foreground ml-auto shrink-0 text-xs\">\n {labels?.useTyped ?? \"Use this address\"}\n </span>\n </CommandItem>\n </CommandGroup>\n ) : null}\n\n {results.length > 0 ? (\n <CommandGroup>\n {results.map((p, i) => {\n const off = excluded.has(principalKey(p))\n return (\n <CommandItem\n key={principalKey(p) || i}\n value={String(i)}\n disabled={off}\n onSelect={() => !off && choose(p)}\n className={cn(off && \"opacity-50\")}\n >\n <PersonRow principal={p} />\n {off ? (\n <span className=\"text-muted-foreground ml-auto shrink-0 text-xs\">\n {labels?.excluded ?? \"already added\"}\n </span>\n ) : null}\n </CommandItem>\n )\n })}\n </CommandGroup>\n ) : null}\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n )\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport { X } from \"lucide-react\"\nimport { Input } from \"~/ui/primitives/input.js\"\nimport { cn } from \"~/utils/index.js\"\nimport { PersonAvatar, PersonRow } from \"./person-row.js\"\nimport type { PeopleLabels, Principal, PrincipalSearch } from \"./types.js\"\nimport { looksLikeEmail, principalKey } from \"./types.js\"\nimport { usePrincipalSearch } from \"./use-principal-search.js\"\n\nexport interface PeopleFieldProps<T = unknown> {\n search: PrincipalSearch<T>\n /** Chosen so far — rendered as tokens. */\n value: Principal<T>[]\n onAdd: (p: Principal<T>) => void\n onRemove: (p: Principal<T>) => void\n allowFreeEntry?: boolean\n labels?: PeopleLabels\n disabled?: boolean\n className?: string\n}\n\n/**\n * A To:-style token field: type, pick, repeat.\n *\n * The popover picker and this are NOT the same component with a flag. A \"To\" line is\n * used dozens of times a minute and lives or dies on keyboard flow — type, Enter,\n * type, Enter, Backspace to undo — whereas a share dialog is opened once and clicked\n * through. Forcing one into the other's shape produces something that is bad at both.\n *\n * Paste is handled: a list copied out of a spreadsheet or another mail client arrives\n * as \"a@x.com, b@y.com; c@z.com\" and becomes three tokens, because the alternative is\n * the user hand-splitting it.\n */\nexport function PeopleField<T = unknown>({\n search,\n value,\n onAdd,\n onRemove,\n allowFreeEntry,\n labels,\n disabled,\n className,\n}: PeopleFieldProps<T>) {\n const [focused, setFocused] = React.useState(false)\n const exclude = React.useMemo(() => value.map(principalKey), [value])\n const { query, setQuery, results, excluded } = usePrincipalSearch({ search, exclude, limit: 6 })\n\n const chosen = React.useMemo(() => new Set(exclude), [exclude])\n const typedEmail = allowFreeEntry && looksLikeEmail(query) ? query.trim() : \"\"\n\n function add(p: Principal<T>) {\n const k = principalKey(p)\n if (!k || chosen.has(k)) return\n onAdd(p)\n setQuery(\"\")\n }\n\n function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {\n if ((e.key === \"Enter\" || e.key === \"Tab\" || e.key === \",\") && query.trim()) {\n // Enter takes the top-ranked match, or the typed address. Anything else would\n // make the common case — the person you meant is first — need the mouse.\n const pick = results.find((p) => !excluded.has(principalKey(p)))\n if (pick) {\n e.preventDefault()\n add(pick)\n } else if (typedEmail) {\n e.preventDefault()\n add({ email: typedEmail, kind: \"person\" })\n }\n return\n }\n // Backspace on an empty box removes the last token — the universal behaviour of\n // every mail client, and the first thing a user tries.\n if (e.key === \"Backspace\" && query === \"\" && value.length > 0) {\n onRemove(value[value.length - 1]!)\n }\n }\n\n function onPaste(e: React.ClipboardEvent<HTMLInputElement>) {\n const text = e.clipboardData.getData(\"text\")\n const parts = text.split(/[,;\\s]+/).map((s) => s.trim()).filter(Boolean)\n if (parts.length < 2) return // one token: let it type normally\n e.preventDefault()\n for (const part of parts) {\n if (looksLikeEmail(part)) add({ email: part, kind: \"person\" })\n }\n }\n\n return (\n <div className={cn(\"space-y-2\", className)}>\n <div\n className={cn(\n \"border-input focus-within:ring-ring flex min-h-10 flex-wrap items-center gap-1.5 rounded-md border px-2 py-1.5 focus-within:ring-1\",\n disabled && \"opacity-50\",\n )}\n >\n {value.map((p) => (\n <span\n key={principalKey(p)}\n className=\"bg-muted flex items-center gap-1.5 rounded-full py-0.5 pl-0.5 pr-2 text-xs\"\n >\n <PersonAvatar principal={p} className=\"size-5\" />\n <span className=\"max-w-40 truncate\">{p.displayName || p.email}</span>\n <button\n type=\"button\"\n aria-label={labels?.remove ?? \"Remove\"}\n onClick={() => onRemove(p)}\n disabled={disabled}\n className=\"opacity-60 hover:opacity-100\"\n >\n <X className=\"size-3\" />\n </button>\n </span>\n ))}\n <Input\n value={query}\n disabled={disabled}\n placeholder={value.length === 0 ? (labels?.placeholder ?? \"Add people\") : undefined}\n onChange={(e) => setQuery(e.target.value)}\n onKeyDown={onKeyDown}\n onPaste={onPaste}\n onFocus={() => setFocused(true)}\n // Delayed so a click on a suggestion lands before the list unmounts.\n onBlur={() => setTimeout(() => setFocused(false), 150)}\n className=\"h-7 min-w-40 flex-1 border-0 bg-transparent px-1 shadow-none focus-visible:ring-0\"\n />\n </div>\n\n {focused && (results.length > 0 || typedEmail) ? (\n <div className=\"bg-popover max-h-64 overflow-auto rounded-md border p-1 shadow-md\">\n {typedEmail && !results.some((p) => p.email === typedEmail) ? (\n <button\n type=\"button\"\n onClick={() => add({ email: typedEmail, kind: \"person\" })}\n className=\"hover:bg-accent flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left\"\n >\n <PersonRow principal={{ email: typedEmail, displayName: typedEmail }} />\n <span className=\"text-muted-foreground ml-auto shrink-0 text-xs\">\n {labels?.useTyped ?? \"Use this address\"}\n </span>\n </button>\n ) : null}\n {results.map((p) => {\n const off = excluded.has(principalKey(p))\n return (\n <button\n key={principalKey(p)}\n type=\"button\"\n disabled={off}\n onClick={() => add(p)}\n className={cn(\n \"hover:bg-accent flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left\",\n off && \"opacity-50\",\n )}\n >\n <PersonRow principal={p} />\n {off ? (\n <span className=\"text-muted-foreground ml-auto shrink-0 text-xs\">\n {labels?.excluded ?? \"already added\"}\n </span>\n ) : null}\n </button>\n )\n })}\n </div>\n ) : null}\n </div>\n )\n}\n"]}
|
package/dist/shell/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { SidebarInset, SidebarProvider, Sidebar, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, useSidebar, defaultAccountMenuLabels,
|
|
2
|
+
import { SidebarInset, SidebarProvider, Sidebar, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, useSidebar, defaultAccountMenuLabels, initialsOf, ScrollArea, ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ScrollBar, Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbPage, BreadcrumbLink, BreadcrumbSeparator } from '../chunk-F5HR244O.js';
|
|
3
3
|
import '../chunk-QYA2HGKW.js';
|
|
4
4
|
import { Card, CardTitle, CardDescription } from '../chunk-LZZ3UOPT.js';
|
|
5
|
-
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuLabel, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuItem } from '../chunk-
|
|
5
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuLabel, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuItem } from '../chunk-VEWKPOMS.js';
|
|
6
|
+
import { Avatar, AvatarImage, AvatarFallback, InputGroup, InputGroupAddon, InputGroupInput } from '../chunk-7GY2KYNW.js';
|
|
7
|
+
import '../chunk-PI6SANLG.js';
|
|
6
8
|
import { Button } from '../chunk-5Q4UTRYU.js';
|
|
7
9
|
import { cn } from '../chunk-XH55BHUU.js';
|
|
8
10
|
import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
|