@parallelworks/ui 0.53.0 → 0.54.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 +55 -5
- package/dist/access/AccessDrawer.d.ts +55 -0
- package/dist/access/AccessDrawer.js +445 -0
- package/dist/access/AccessList.d.ts +58 -0
- package/dist/access/AccessList.js +382 -0
- package/dist/access/GrantView.d.ts +35 -0
- package/dist/access/GrantView.js +188 -0
- package/dist/access/GroupPicker.d.ts +35 -0
- package/dist/access/GroupPicker.js +336 -0
- package/dist/access/focus.d.ts +10 -0
- package/dist/access/focus.js +27 -0
- package/dist/access/index.d.ts +3 -0
- package/dist/access/index.js +3 -0
- package/dist/access/model.d.ts +49 -0
- package/dist/access/model.js +129 -0
- package/dist/access/strings.d.ts +122 -0
- package/dist/access/strings.js +151 -0
- package/dist/access/types.d.ts +56 -0
- package/dist/ai/components/SlashMenu.js +1 -1
- package/dist/components/Combobox.d.ts +19 -0
- package/dist/components/Combobox.js +101 -50
- package/dist/components/Drawer.d.ts +6 -1
- package/dist/components/Drawer.js +25 -16
- package/dist/components/Menu.js +4 -4
- package/dist/components/MenuEntries.js +1 -1
- package/dist/components/OptionDescription.d.ts +7 -0
- package/dist/components/OptionDescription.js +19 -0
- package/dist/components/Provider.d.ts +7 -0
- package/dist/components/Provider.js +3 -0
- package/dist/components/Select.js +13 -11
- package/dist/components/comboboxItems.d.ts +10 -0
- package/dist/components/comboboxItems.js +8 -1
- package/dist/components/floating.d.ts +5 -2
- package/dist/components/floating.js +2 -2
- package/dist/editor/themes.d.ts +5 -0
- package/dist/editor/themes.js +17 -2
- package/dist/file-explorer/FileExplorer.js +2 -1
- package/dist/file-explorer/lib/paging.d.ts +1 -0
- package/dist/file-explorer/lib/paging.js +5 -1
- package/dist/styles/theme.css +82 -62
- package/dist/styles.css +1 -1
- package/dist/theme/derive.d.ts +5 -3
- package/dist/theme/derive.js +61 -3
- package/package.json +5 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type UIStrings } from '../components/Provider.js';
|
|
3
|
+
export declare const ACCESS_STRINGS: {
|
|
4
|
+
title: string;
|
|
5
|
+
/** Under the title: the resource, and how many groups have a role on it. */
|
|
6
|
+
summary: (resource: string, groups: number, everyone: boolean) => string;
|
|
7
|
+
/** Names the switch between the two ways of listing who has access. */
|
|
8
|
+
arrange: string;
|
|
9
|
+
byRole: string;
|
|
10
|
+
byGroup: string;
|
|
11
|
+
filter: string;
|
|
12
|
+
grantAccess: string;
|
|
13
|
+
highImpact: string;
|
|
14
|
+
loading: string;
|
|
15
|
+
groups: (n: number) => string;
|
|
16
|
+
/** How many of a role's groups a filter left. */
|
|
17
|
+
groupsShown: (shown: number, total: number) => string;
|
|
18
|
+
members: (n: number) => string;
|
|
19
|
+
people: (n: number) => string;
|
|
20
|
+
/** Everyone, where it is short: a tag or a picker row. */
|
|
21
|
+
everyone: string;
|
|
22
|
+
/** Everyone, where it stands as a row of its own. */
|
|
23
|
+
everyoneInOrganization: string;
|
|
24
|
+
everyoneJoinLater: string;
|
|
25
|
+
/** Read with the badges a group row folds away: "+2". */
|
|
26
|
+
moreRoles: (n: number) => string;
|
|
27
|
+
moreRolesLabel: (roles: readonly string[]) => string;
|
|
28
|
+
/** Beside a group whose other role gives everything this one does. */
|
|
29
|
+
coveredBy: (roles: readonly string[]) => string;
|
|
30
|
+
/** Beside a group that has this through everyone as well. */
|
|
31
|
+
everyoneHasThisToo: string;
|
|
32
|
+
/** A role a group holds that another of its roles already gives. */
|
|
33
|
+
redundant: (roles: readonly string[]) => string;
|
|
34
|
+
/** A role a group has only because everyone does. */
|
|
35
|
+
throughEveryone: string;
|
|
36
|
+
removeRole: (group: string, role: string) => string;
|
|
37
|
+
grantMoreRoles: string;
|
|
38
|
+
removeAllAccess: string;
|
|
39
|
+
removeTitle: (group: string, roles: readonly string[]) => string;
|
|
40
|
+
removeAllTitle: (group: string) => string;
|
|
41
|
+
/** Leads the list of what a group loses; `members` is undefined when the count isn't known. */
|
|
42
|
+
losesIntro: (members: number | undefined, everyone: boolean) => string;
|
|
43
|
+
/** After the list: why these are lost, and what they keep. */
|
|
44
|
+
keeps: (own: readonly string[], throughEveryone: readonly string[]) => string;
|
|
45
|
+
/** When the roles it keeps give everything the removed ones did. */
|
|
46
|
+
losesNothing: (own: readonly string[], throughEveryone: readonly string[]) => string;
|
|
47
|
+
confirmRemove: (roles: readonly string[]) => string;
|
|
48
|
+
removeFailed: (group: string, roles: readonly string[]) => string;
|
|
49
|
+
grantFailed: (groups: readonly string[], roles: readonly string[]) => string;
|
|
50
|
+
undoFailed: string;
|
|
51
|
+
/** Under a failed save; `reason` is the host's error message, when it gave one. */
|
|
52
|
+
nothingChanged: (reason: string | undefined) => string;
|
|
53
|
+
/** The toast after a grant. */
|
|
54
|
+
granted: (groups: readonly string[], roles: readonly string[]) => string;
|
|
55
|
+
/** The toast after a removal. */
|
|
56
|
+
removed: (groups: readonly string[], roles: readonly string[]) => string;
|
|
57
|
+
undone: string;
|
|
58
|
+
readOnly: string;
|
|
59
|
+
/** After `readOnly` when the host gives no reason. */
|
|
60
|
+
readOnlyReason: string;
|
|
61
|
+
emptyTitle: string;
|
|
62
|
+
emptyDescription: (resource: string) => string;
|
|
63
|
+
noMatches: string;
|
|
64
|
+
clearFilter: string;
|
|
65
|
+
back: string;
|
|
66
|
+
groupsLabel: string;
|
|
67
|
+
addGroup: string;
|
|
68
|
+
/** Under the groups field; `count` is undefined when the host doesn't say how many there are. */
|
|
69
|
+
groupsHint: (count: number | undefined, everyone: boolean) => string;
|
|
70
|
+
everyoneCoversGroups: string;
|
|
71
|
+
organization: string;
|
|
72
|
+
recent: string;
|
|
73
|
+
allGroups: string;
|
|
74
|
+
/** On a group's row in the picker: the roles it holds here. */
|
|
75
|
+
hasRoles: (roles: readonly string[]) => string;
|
|
76
|
+
/** Read with a group's row: its size, where it comes from and its roles here. */
|
|
77
|
+
groupSummary: (members: number | undefined, everyone: boolean, source: 'synced' | 'manual' | undefined, roles: readonly string[]) => string;
|
|
78
|
+
rolesLabel: string;
|
|
79
|
+
searchRoles: string;
|
|
80
|
+
/** On a role's row in the picker. */
|
|
81
|
+
alreadyHave: (groups: readonly string[]) => string;
|
|
82
|
+
alreadyCovered: (roles: readonly string[]) => string;
|
|
83
|
+
everyoneHasIt: string;
|
|
84
|
+
/** The footer's account of what pressing Grant does. */
|
|
85
|
+
grantSummary: (groups: readonly string[], roles: readonly string[]) => ReactNode;
|
|
86
|
+
/** Why Grant can't be pressed: every group has every role already. */
|
|
87
|
+
nothingToGrant: (groups: readonly string[], roles: readonly string[]) => string;
|
|
88
|
+
pickGroupsAndRoles: string;
|
|
89
|
+
pickGroups: string;
|
|
90
|
+
pickRoles: string;
|
|
91
|
+
grant: (roles: readonly string[]) => string;
|
|
92
|
+
grantToEveryone: string;
|
|
93
|
+
everyoneWarningTitle: (roles: readonly string[], people: number | undefined) => string;
|
|
94
|
+
everyoneWarning: (resource: string) => string;
|
|
95
|
+
confirmEveryoneTitle: (roles: readonly string[], people: number | undefined) => string;
|
|
96
|
+
synced: string;
|
|
97
|
+
syncedShort: string;
|
|
98
|
+
manual: string;
|
|
99
|
+
wholeOrganization: string;
|
|
100
|
+
joinLater: string;
|
|
101
|
+
/** The first few members by name, and how many more there are. */
|
|
102
|
+
memberNames: (names: readonly string[], more: number) => string;
|
|
103
|
+
moreMembers: (n: number) => string;
|
|
104
|
+
onResource: (resource: string) => string;
|
|
105
|
+
noRolesHere: string;
|
|
106
|
+
/** When a role being granted gives everything one the group holds does. */
|
|
107
|
+
becomesRedundant: (granting: readonly string[], held: readonly string[]) => string;
|
|
108
|
+
alreadyHas: (roles: readonly string[]) => string;
|
|
109
|
+
everyoneReach: (roles: readonly string[], people: number | undefined) => string;
|
|
110
|
+
updated: (ago: string) => string;
|
|
111
|
+
detailsFailed: string;
|
|
112
|
+
loadingDetails: string;
|
|
113
|
+
noHighlight: string;
|
|
114
|
+
};
|
|
115
|
+
/** The drawer's own text, which a host translates through UIProvider's `access`. */
|
|
116
|
+
export type AccessText = typeof ACCESS_STRINGS;
|
|
117
|
+
/** The drawer's text with the shared words, such as Cancel and Retry, beside it. */
|
|
118
|
+
export type AccessStrings = AccessText & {
|
|
119
|
+
common: UIStrings['common'];
|
|
120
|
+
};
|
|
121
|
+
/** The access drawer's strings: the host's translations over the English defaults. */
|
|
122
|
+
export declare function useAccessStrings(): AccessStrings;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useStrings } from "../components/Provider.js";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
//#region src/access/strings.tsx
|
|
5
|
+
var plural = new Intl.PluralRules("en");
|
|
6
|
+
var list = new Intl.ListFormat("en", { type: "conjunction" });
|
|
7
|
+
/** A count and its noun: "1 group", "1,024 groups". */
|
|
8
|
+
var countOf = (n, one, other) => `${n.toLocaleString()} ${plural.select(n) === "one" ? one : other}`;
|
|
9
|
+
var and = (names) => list.format(names);
|
|
10
|
+
var has = (names) => names.length === 1 ? "has" : "have";
|
|
11
|
+
var ACCESS_STRINGS = {
|
|
12
|
+
title: "Access",
|
|
13
|
+
/** Under the title: the resource, and how many groups have a role on it. */
|
|
14
|
+
summary: (resource, groups, everyone) => everyone ? groups === 0 ? `${resource} · Everyone has access` : `${resource} · Everyone and ${countOf(groups, "group have access", "groups have access")}` : groups === 0 ? `${resource} · No groups have access` : `${resource} · ${countOf(groups, "group has access", "groups have access")}`,
|
|
15
|
+
/** Names the switch between the two ways of listing who has access. */
|
|
16
|
+
arrange: "List access",
|
|
17
|
+
byRole: "By role",
|
|
18
|
+
byGroup: "By group",
|
|
19
|
+
filter: "Filter by role or group",
|
|
20
|
+
grantAccess: "Grant access",
|
|
21
|
+
highImpact: "High impact",
|
|
22
|
+
loading: "Loading who has access",
|
|
23
|
+
groups: (n) => countOf(n, "group", "groups"),
|
|
24
|
+
/** How many of a role's groups a filter left. */
|
|
25
|
+
groupsShown: (shown, total) => `${shown.toLocaleString()} of ${countOf(total, "group", "groups")}`,
|
|
26
|
+
members: (n) => countOf(n, "member", "members"),
|
|
27
|
+
people: (n) => countOf(n, "person", "people"),
|
|
28
|
+
/** Everyone, where it is short: a tag or a picker row. */
|
|
29
|
+
everyone: "Everyone",
|
|
30
|
+
/** Everyone, where it stands as a row of its own. */
|
|
31
|
+
everyoneInOrganization: "Everyone in your organization",
|
|
32
|
+
everyoneJoinLater: "Including people who join later",
|
|
33
|
+
/** Read with the badges a group row folds away: "+2". */
|
|
34
|
+
moreRoles: (n) => `+${n.toLocaleString()}`,
|
|
35
|
+
moreRolesLabel: (roles) => `Also ${and(roles)}`,
|
|
36
|
+
/** Beside a group whose other role gives everything this one does. */
|
|
37
|
+
coveredBy: (roles) => `${and(roles)} covers this too`,
|
|
38
|
+
/** Beside a group that has this through everyone as well. */
|
|
39
|
+
everyoneHasThisToo: "Everyone has this too",
|
|
40
|
+
/** A role a group holds that another of its roles already gives. */
|
|
41
|
+
redundant: (roles) => `Redundant: ${and(roles)} already covers this.`,
|
|
42
|
+
/** A role a group has only because everyone does. */
|
|
43
|
+
throughEveryone: "Everyone in your organization has it",
|
|
44
|
+
removeRole: (group, role) => `Remove ${group} from ${role}`,
|
|
45
|
+
grantMoreRoles: "Grant more roles",
|
|
46
|
+
removeAllAccess: "Remove all access",
|
|
47
|
+
removeTitle: (group, roles) => `Remove ${group} from ${and(roles)}?`,
|
|
48
|
+
removeAllTitle: (group) => `Remove all of ${group}’s access?`,
|
|
49
|
+
/** Leads the list of what a group loses; `members` is undefined when the count isn't known. */
|
|
50
|
+
losesIntro: (members, everyone) => everyone ? "People who have it only through everyone will no longer be able to:" : members === void 0 ? "Its members will no longer be able to:" : `Its ${countOf(members, "member", "members")} will no longer be able to:`,
|
|
51
|
+
/** After the list: why these are lost, and what they keep. */
|
|
52
|
+
keeps: (own, throughEveryone) => [
|
|
53
|
+
"No other role of theirs gives these.",
|
|
54
|
+
own.length > 0 && `They keep ${and(own)}.`,
|
|
55
|
+
throughEveryone.length > 0 && `They keep ${and(throughEveryone)}, which everyone has.`
|
|
56
|
+
].filter(Boolean).join(" "),
|
|
57
|
+
/** When the roles it keeps give everything the removed ones did. */
|
|
58
|
+
losesNothing: (own, throughEveryone) => `They lose nothing: ${and([...own, ...throughEveryone])} still ${own.length + throughEveryone.length === 1 ? "gives" : "give"} them all of it.`,
|
|
59
|
+
confirmRemove: (roles) => roles.length === 1 ? `Remove ${roles[0]}` : "Remove all access",
|
|
60
|
+
removeFailed: (group, roles) => `Couldn’t remove ${group} from ${and(roles)}`,
|
|
61
|
+
grantFailed: (groups, roles) => `Couldn’t grant ${and(roles)} to ${and(groups)}`,
|
|
62
|
+
undoFailed: "Couldn’t undo the change",
|
|
63
|
+
/** Under a failed save; `reason` is the host's error message, when it gave one. */
|
|
64
|
+
nothingChanged: (reason) => reason ? `${reason.replace(/\.?$/, ".")} Nothing changed.` : "Nothing changed.",
|
|
65
|
+
/** The toast after a grant. */
|
|
66
|
+
granted: (groups, roles) => `Gave ${and(groups)} ${and(roles)}.`,
|
|
67
|
+
/** The toast after a removal. */
|
|
68
|
+
removed: (groups, roles) => `Removed ${and(groups)} from ${and(roles)}.`,
|
|
69
|
+
undone: "Change undone.",
|
|
70
|
+
readOnly: "You can see who has access.",
|
|
71
|
+
/** After `readOnly` when the host gives no reason. */
|
|
72
|
+
readOnlyReason: "Only people who manage access here can change it.",
|
|
73
|
+
emptyTitle: "No groups have access yet",
|
|
74
|
+
emptyDescription: (resource) => `Give a group a role so its members can see, use or manage ${resource}.`,
|
|
75
|
+
noMatches: "No roles or groups match",
|
|
76
|
+
clearFilter: "Clear filter",
|
|
77
|
+
back: "Back to who has access",
|
|
78
|
+
groupsLabel: "Groups",
|
|
79
|
+
addGroup: "Add a group",
|
|
80
|
+
/** Under the groups field; `count` is undefined when the host doesn't say how many there are. */
|
|
81
|
+
groupsHint: (count, everyone) => count === void 0 ? everyone ? "Search groups, or pick Everyone in your organization." : "Search groups." : `Search ${countOf(count, "group", "groups")}${everyone ? ", or pick Everyone in your organization" : ""}.`,
|
|
82
|
+
everyoneCoversGroups: "Everyone covers every group, so there’s no need to add others.",
|
|
83
|
+
organization: "Organization",
|
|
84
|
+
recent: "Recent",
|
|
85
|
+
allGroups: "All groups",
|
|
86
|
+
/** On a group's row in the picker: the roles it holds here. */
|
|
87
|
+
hasRoles: (roles) => `has ${and(roles)}`,
|
|
88
|
+
/** Read with a group's row: its size, where it comes from and its roles here. */
|
|
89
|
+
groupSummary: (members, everyone, source, roles) => [
|
|
90
|
+
members !== void 0 && (everyone ? countOf(members, "person", "people") : countOf(members, "member", "members")),
|
|
91
|
+
source === "synced" && "synced",
|
|
92
|
+
roles.length > 0 ? `has ${and(roles)} here` : "no roles here"
|
|
93
|
+
].filter(Boolean).join(", "),
|
|
94
|
+
rolesLabel: "Roles",
|
|
95
|
+
searchRoles: "Search roles",
|
|
96
|
+
/** On a role's row in the picker. */
|
|
97
|
+
alreadyHave: (groups) => `${and(groups)} already ${has(groups)} it.`,
|
|
98
|
+
alreadyCovered: (roles) => `${and(roles)} already covers this.`,
|
|
99
|
+
everyoneHasIt: "Everyone already has it.",
|
|
100
|
+
/** The footer's account of what pressing Grant does. */
|
|
101
|
+
grantSummary: (groups, roles) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
102
|
+
/* @__PURE__ */ jsx("strong", {
|
|
103
|
+
className: "font-medium text-(--theme-app)",
|
|
104
|
+
children: and(groups)
|
|
105
|
+
}),
|
|
106
|
+
" ",
|
|
107
|
+
groups.length === 1 ? "gets" : "get",
|
|
108
|
+
" ",
|
|
109
|
+
and(roles),
|
|
110
|
+
"."
|
|
111
|
+
] }),
|
|
112
|
+
/** Why Grant can't be pressed: every group has every role already. */
|
|
113
|
+
nothingToGrant: (groups, roles) => `${and(groups)} already ${has(groups)} ${and(roles)}, so there’s nothing to grant.`,
|
|
114
|
+
pickGroupsAndRoles: "Pick the groups and the roles to grant.",
|
|
115
|
+
pickGroups: "Pick a group to grant to.",
|
|
116
|
+
pickRoles: "Pick a role to grant.",
|
|
117
|
+
grant: (roles) => roles.length === 0 ? "Grant" : roles.length === 1 ? `Grant ${roles[0]}` : `Grant ${countOf(roles.length, "role", "roles")}`,
|
|
118
|
+
grantToEveryone: "Grant to everyone",
|
|
119
|
+
everyoneWarningTitle: (roles, people) => people === void 0 ? `${and(roles)} for everyone` : `${and(roles)} for all ${countOf(people, "person", "people")}`,
|
|
120
|
+
everyoneWarning: (resource) => `Everyone in your organization, and everyone who joins later, will have it on ${resource}.`,
|
|
121
|
+
confirmEveryoneTitle: (roles, people) => people === void 0 ? `Grant ${and(roles)} to everyone?` : `Grant ${and(roles)} to all ${countOf(people, "person", "people")}?`,
|
|
122
|
+
synced: "Synced from your identity provider",
|
|
123
|
+
syncedShort: "Synced",
|
|
124
|
+
manual: "Members added by hand",
|
|
125
|
+
wholeOrganization: "Your whole organization",
|
|
126
|
+
joinLater: "And anyone who joins later gets it automatically.",
|
|
127
|
+
/** The first few members by name, and how many more there are. */
|
|
128
|
+
memberNames: (names, more) => more > 0 ? and([...names, `${more.toLocaleString()} more`]) : and(names),
|
|
129
|
+
moreMembers: (n) => `+${n.toLocaleString()}`,
|
|
130
|
+
onResource: (resource) => `On ${resource}`,
|
|
131
|
+
noRolesHere: "No roles here yet.",
|
|
132
|
+
/** When a role being granted gives everything one the group holds does. */
|
|
133
|
+
becomesRedundant: (granting, held) => `${and(granting)} also ${granting.length === 1 ? "gives" : "give"} them ${and(held)}, so ${and(held)} becomes redundant. Nothing is removed.`,
|
|
134
|
+
alreadyHas: (roles) => `Already has ${and(roles)}.`,
|
|
135
|
+
everyoneReach: (roles, people) => `${and(roles)} for everyone reaches ${people === void 0 ? "every person" : `all ${countOf(people, "person", "people")}`} in your organization, and anyone who joins later. You’ll be asked to confirm.`,
|
|
136
|
+
updated: (ago) => `Membership updated ${ago}`,
|
|
137
|
+
detailsFailed: "Couldn’t load this group’s details.",
|
|
138
|
+
loadingDetails: "Loading the group’s details",
|
|
139
|
+
noHighlight: "Highlight a group to see who is in it."
|
|
140
|
+
};
|
|
141
|
+
/** The access drawer's strings: the host's translations over the English defaults. */
|
|
142
|
+
function useAccessStrings() {
|
|
143
|
+
const { access: overrides, common } = useStrings();
|
|
144
|
+
return useMemo(() => ({
|
|
145
|
+
...ACCESS_STRINGS,
|
|
146
|
+
...overrides,
|
|
147
|
+
common
|
|
148
|
+
}), [overrides, common]);
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
export { ACCESS_STRINGS, useAccessStrings };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { TimeValue } from '../format.js';
|
|
2
|
+
/** A named set of permissions. Groups are granted roles, never permissions one by one. */
|
|
3
|
+
export interface AccessRole {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
/** What it lets someone do, in a sentence. */
|
|
7
|
+
description?: string | undefined;
|
|
8
|
+
/** The permissions it gives. Two roles that share some are allowed, and only redundant: the
|
|
9
|
+
* drawer works out the overlap from these and says so as a hint. */
|
|
10
|
+
permissions: readonly string[];
|
|
11
|
+
/** Marks a role that reaches far, such as root on every node. It carries a tag, and granting
|
|
12
|
+
* it to everyone says so before asking to confirm. */
|
|
13
|
+
highImpact?: boolean | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** A permission's name in words, for what a group loses when a role is taken away. */
|
|
16
|
+
export interface AccessPermission {
|
|
17
|
+
id: string;
|
|
18
|
+
/** What it lets someone do, as a phrase that follows "will no longer be able to": "Sign in". */
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
/** Who a role can be granted to. */
|
|
22
|
+
export interface AccessGroup {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
/** How many people are in it; for everyone, the organization's size. */
|
|
26
|
+
memberCount?: number | undefined;
|
|
27
|
+
/** `everyone` is the whole organization, people who join later included. At most one group
|
|
28
|
+
* is; leave it out where a resource can't be granted to everyone. */
|
|
29
|
+
kind?: 'everyone' | 'group' | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** A role held by a group. */
|
|
32
|
+
export interface AccessGrant {
|
|
33
|
+
groupId: string;
|
|
34
|
+
roleId: string;
|
|
35
|
+
}
|
|
36
|
+
/** What one change grants and takes away; neither list holds a grant that changes nothing. */
|
|
37
|
+
export interface AccessChange {
|
|
38
|
+
grant: AccessGrant[];
|
|
39
|
+
revoke: AccessGrant[];
|
|
40
|
+
}
|
|
41
|
+
/** A person, as a group's details show its first few members. */
|
|
42
|
+
export interface AccessMember {
|
|
43
|
+
name: string;
|
|
44
|
+
avatarUrl?: string | undefined;
|
|
45
|
+
}
|
|
46
|
+
/** More about a group than the list needs, loaded when it is highlighted in the group picker. */
|
|
47
|
+
export interface AccessGroupDetails {
|
|
48
|
+
description?: string | undefined;
|
|
49
|
+
/** `synced` comes from an identity provider; `manual` is kept by hand. */
|
|
50
|
+
source?: 'synced' | 'manual' | undefined;
|
|
51
|
+
/** The first few, in the order to show them. */
|
|
52
|
+
members: readonly AccessMember[];
|
|
53
|
+
memberCount: number;
|
|
54
|
+
/** When its membership last changed. */
|
|
55
|
+
updatedAt?: TimeValue;
|
|
56
|
+
}
|
|
@@ -83,7 +83,7 @@ function SlashMenu({ label, listId, matches, active, onChoose }) {
|
|
|
83
83
|
role: "listbox",
|
|
84
84
|
"aria-label": label,
|
|
85
85
|
"data-testid": "slash-menu",
|
|
86
|
-
className: cx("absolute bottom-full left-0 right-0 z-20 mb-2 overflow-hidden
|
|
86
|
+
className: cx("absolute bottom-full left-0 right-0 z-20 mb-2 overflow-hidden", "p-1", panelClassName),
|
|
87
87
|
children: matches.map((c, i) => /* @__PURE__ */ jsxs("div", {
|
|
88
88
|
id: optionId(listId, i),
|
|
89
89
|
role: "option",
|
|
@@ -46,6 +46,25 @@ export type ComboboxProps<Value = string> = FieldProps & SelectionProps<Value> &
|
|
|
46
46
|
trigger?: ReactElement | undefined;
|
|
47
47
|
/** A hint on the trigger button. */
|
|
48
48
|
triggerTooltip?: ReactNode;
|
|
49
|
+
/** A pane beside the list about the highlighted item, such as who is in a group; it gets
|
|
50
|
+
* undefined while nothing is highlighted. It follows the highlight from the keyboard and
|
|
51
|
+
* the pointer alike, and is hidden from assistive technology, so each item's
|
|
52
|
+
* `accessibleDescription` must say what matters in it. Where the list is narrower than
|
|
53
|
+
* 30rem the pane has no room, and `detailsSummary` shows under the highlighted item. */
|
|
54
|
+
details?: ((value: Value | undefined) => ReactNode) | undefined;
|
|
55
|
+
/** Called as the highlight moves, from the keyboard or the pointer, such as to load what
|
|
56
|
+
* `details` shows; undefined once nothing is highlighted. */
|
|
57
|
+
onItemHighlighted?: ((value: Value | undefined) => void) | undefined;
|
|
58
|
+
/** A line about the highlighted item, under it, where the list has no room for `details`. */
|
|
59
|
+
detailsSummary?: ((value: Value) => ReactNode) | undefined;
|
|
60
|
+
/** Called as the typed text changes. With it, the host does the filtering: the list shows
|
|
61
|
+
* `items` as given, such as the results of a search on the server. */
|
|
62
|
+
onQueryChange?: ((query: string) => void) | undefined;
|
|
63
|
+
/** The host's search is under way. */
|
|
64
|
+
loading?: boolean | undefined;
|
|
65
|
+
/** The area the open list stays inside, such as a drawer's body above its footer; by
|
|
66
|
+
* default, the viewport. With `details`, the list grows to fill it. */
|
|
67
|
+
collisionBoundary?: Element | null | undefined;
|
|
49
68
|
};
|
|
50
69
|
export interface AutocompleteProps extends FieldProps {
|
|
51
70
|
/** Suggestions; any typed text is also a valid value. */
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { cx } from "../cx.js";
|
|
2
2
|
import { AddIcon, CheckIcon, ChevronDownIcon, RemoveIcon } from "../icons.js";
|
|
3
3
|
import { panelClassName, positionerClassName } from "./floating.js";
|
|
4
|
+
import { Loader } from "./Loader.js";
|
|
4
5
|
import { Tooltip } from "./Tooltip.js";
|
|
5
6
|
import { useStrings } from "./Provider.js";
|
|
6
7
|
import { useQueryMatch } from "./search.js";
|
|
7
8
|
import { fieldBox } from "./fieldBox.js";
|
|
8
|
-
import { isComboboxGroup, pickerItemClassName } from "./comboboxItems.js";
|
|
9
|
+
import { isComboboxGroup, optionNaming, pickerItemClassName } from "./comboboxItems.js";
|
|
10
|
+
import { OptionDescription } from "./OptionDescription.js";
|
|
9
11
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
12
|
import { useCallback, useId, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
11
13
|
import { dequal } from "dequal";
|
|
@@ -79,13 +81,14 @@ var triggerClassName = "flex shrink-0 items-center self-stretch px-2 text-(--the
|
|
|
79
81
|
var popupInputClassName = fieldBox({ size: "sm" });
|
|
80
82
|
/** A pick-from-the-list field: one value, or several with `multiple`. Typing filters the list. */
|
|
81
83
|
function Combobox$1(props) {
|
|
82
|
-
const { items, isItemEqualToValue = dequal, itemToStringLabel = String, className, placeholder, disabled, required, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, trigger, triggerTooltip, size = "md" } = props;
|
|
84
|
+
const { items, isItemEqualToValue = dequal, itemToStringLabel = String, className, placeholder, disabled, required, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, trigger, triggerTooltip, size = "md", details, detailsSummary, onItemHighlighted, onQueryChange, loading = false, collisionBoundary } = props;
|
|
83
85
|
const entries = useEntries(items);
|
|
84
86
|
const filter = useEntryFilter();
|
|
85
87
|
const t = useStrings().dropdown;
|
|
86
88
|
const list = useRef(null);
|
|
87
89
|
const uid = useId();
|
|
88
90
|
const [inputValue, setInputValue] = useState(null);
|
|
91
|
+
const [highlighted, setHighlighted] = useState(void 0);
|
|
89
92
|
const entryOf = (value) => entries.flat.find((entry) => isItemEqualToValue(entry.value, value)) ?? {
|
|
90
93
|
value,
|
|
91
94
|
label: itemToStringLabel(value)
|
|
@@ -125,15 +128,21 @@ function Combobox$1(props) {
|
|
|
125
128
|
autoComplete: "off",
|
|
126
129
|
className: trigger ? popupInputClassName : inputClassName(size)
|
|
127
130
|
});
|
|
131
|
+
const isHighlighted = (entry) => highlighted !== void 0 && !entry.created && !highlighted.created && isItemEqualToValue(entry.value, highlighted.value);
|
|
128
132
|
const itemRow = (entry, rowProps) => /* @__PURE__ */ jsxs(Combobox.Item, {
|
|
129
133
|
value: entry,
|
|
130
134
|
disabled: entry.disabled || full && !isChosen(entry),
|
|
131
135
|
...rowProps.props,
|
|
132
|
-
...optionNaming(uid
|
|
136
|
+
...optionNaming(`${uid}${rowProps.props.index}`, entry),
|
|
133
137
|
className: pickerItemClassName,
|
|
134
138
|
children: [/* @__PURE__ */ jsx(ItemContent, {
|
|
135
139
|
entry,
|
|
136
|
-
id: `${uid}${rowProps.props.index}
|
|
140
|
+
id: `${uid}${rowProps.props.index}`,
|
|
141
|
+
children: detailsSummary && isHighlighted(entry) && /* @__PURE__ */ jsx("span", {
|
|
142
|
+
"aria-hidden": "true",
|
|
143
|
+
className: "mt-0.5 text-xs leading-snug text-(--theme-muted-text-color) @min-[30rem]/picker:hidden",
|
|
144
|
+
children: detailsSummary(entry.value)
|
|
145
|
+
})
|
|
137
146
|
}), /* @__PURE__ */ jsx(Combobox.ItemIndicator, {
|
|
138
147
|
className: "ml-auto flex shrink-0 items-center",
|
|
139
148
|
children: /* @__PURE__ */ jsx(CheckIcon, {
|
|
@@ -162,7 +171,10 @@ function Combobox$1(props) {
|
|
|
162
171
|
className: "border-b border-(--theme-border) p-1.5",
|
|
163
172
|
children: input
|
|
164
173
|
}),
|
|
165
|
-
className: "w-max min-w-(--anchor-width) max-w-[min(28rem,var(--available-width))]",
|
|
174
|
+
className: details ? "w-[min(36rem,var(--available-width))]" : "w-max min-w-(--anchor-width) max-w-[min(28rem,var(--available-width))]",
|
|
175
|
+
loading: loading ? t.searching : void 0,
|
|
176
|
+
details: details?.(highlighted?.created ? void 0 : highlighted?.value),
|
|
177
|
+
collisionBoundary,
|
|
166
178
|
renderItem: itemRow
|
|
167
179
|
})] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Combobox.InputGroup, {
|
|
168
180
|
"data-testid": "combobox",
|
|
@@ -198,15 +210,25 @@ function Combobox$1(props) {
|
|
|
198
210
|
empty: t.noOptionsFound,
|
|
199
211
|
statusGroups: entries.statusGroups,
|
|
200
212
|
query: inputValue ?? "",
|
|
213
|
+
loading: loading ? t.searching : void 0,
|
|
214
|
+
details: details?.(highlighted?.created ? void 0 : highlighted?.value),
|
|
215
|
+
collisionBoundary,
|
|
201
216
|
renderItem: itemRow
|
|
202
217
|
})] });
|
|
203
218
|
const root = {
|
|
204
219
|
items: listItems,
|
|
205
|
-
filter,
|
|
206
220
|
isItemEqualToValue: (a, b) => isItemEqualToValue(a.value, b.value),
|
|
207
221
|
itemToStringLabel: entryLabel,
|
|
208
|
-
|
|
209
|
-
|
|
222
|
+
filter: onQueryChange ? null : filter,
|
|
223
|
+
onInputValueChange: (next) => {
|
|
224
|
+
setInputValue(next);
|
|
225
|
+
onQueryChange?.(next);
|
|
226
|
+
},
|
|
227
|
+
onItemHighlighted: (entry, { index, reason }) => {
|
|
228
|
+
if (details || detailsSummary) setHighlighted(entry);
|
|
229
|
+
onItemHighlighted?.(entry && !entry.created ? entry.value : void 0);
|
|
230
|
+
list.current?.reveal(index, reason);
|
|
231
|
+
},
|
|
210
232
|
disabled,
|
|
211
233
|
required,
|
|
212
234
|
virtualized: true,
|
|
@@ -218,7 +240,12 @@ function Combobox$1(props) {
|
|
|
218
240
|
multiple: true,
|
|
219
241
|
value: toEntries(props.value),
|
|
220
242
|
defaultValue: toEntries(props.defaultValue),
|
|
221
|
-
onValueChange: (next) => {
|
|
243
|
+
onValueChange: (next, details) => {
|
|
244
|
+
if (details.reason === "escape-key") {
|
|
245
|
+
details.cancel();
|
|
246
|
+
details.allowPropagation();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
222
249
|
setUncontrolledCount(next.length);
|
|
223
250
|
props.onValueChange?.(next.map((entry) => entry.value));
|
|
224
251
|
}
|
|
@@ -275,7 +302,7 @@ function Autocomplete$1({ items, value, defaultValue, onValueChange, className,
|
|
|
275
302
|
value: entry,
|
|
276
303
|
disabled: entry.disabled,
|
|
277
304
|
...rowProps.props,
|
|
278
|
-
...optionNaming(uid
|
|
305
|
+
...optionNaming(`${uid}${rowProps.props.index}`, entry),
|
|
279
306
|
className: pickerItemClassName,
|
|
280
307
|
children: /* @__PURE__ */ jsx(ItemContent, {
|
|
281
308
|
entry,
|
|
@@ -285,33 +312,31 @@ function Autocomplete$1({ items, value, defaultValue, onValueChange, className,
|
|
|
285
312
|
})]
|
|
286
313
|
});
|
|
287
314
|
}
|
|
288
|
-
|
|
289
|
-
* its name. */
|
|
290
|
-
function optionNaming(uid, index, entry) {
|
|
291
|
-
return {
|
|
292
|
-
"aria-labelledby": `${uid}${index}label`,
|
|
293
|
-
"aria-describedby": entry.description ? `${uid}${index}description` : void 0
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
function ItemContent({ entry, id }) {
|
|
315
|
+
function ItemContent({ entry, id, children }) {
|
|
297
316
|
return /* @__PURE__ */ jsxs(Fragment, { children: [entry.icon && /* @__PURE__ */ jsx("span", {
|
|
298
317
|
className: "flex shrink-0 items-center",
|
|
299
318
|
children: entry.icon
|
|
300
319
|
}), /* @__PURE__ */ jsxs("span", {
|
|
301
320
|
className: "flex min-w-0 flex-col",
|
|
302
|
-
children: [
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
321
|
+
children: [
|
|
322
|
+
/* @__PURE__ */ jsxs("span", {
|
|
323
|
+
className: "flex min-w-0 items-center gap-2",
|
|
324
|
+
children: [/* @__PURE__ */ jsx(Tooltip, {
|
|
325
|
+
content: entry.label,
|
|
326
|
+
overflowOnly: true,
|
|
327
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
328
|
+
id: `${id}label`,
|
|
329
|
+
className: "truncate",
|
|
330
|
+
children: entry.label
|
|
331
|
+
})
|
|
332
|
+
}), entry.badge]
|
|
333
|
+
}),
|
|
334
|
+
/* @__PURE__ */ jsx(OptionDescription, {
|
|
335
|
+
id: `${id}description`,
|
|
336
|
+
item: entry
|
|
337
|
+
}),
|
|
338
|
+
children
|
|
339
|
+
]
|
|
315
340
|
})] });
|
|
316
341
|
}
|
|
317
342
|
function GroupHeading({ group }) {
|
|
@@ -335,26 +360,52 @@ function GroupHeading({ group }) {
|
|
|
335
360
|
] });
|
|
336
361
|
}
|
|
337
362
|
/** Portal, positioner and popup shared by Combobox and Autocomplete; both use the same parts. */
|
|
338
|
-
function ListPopup({ listRef, empty, statusGroups = [], query = "", header, className = "w-(--anchor-width) max-w-(--available-width)", renderItem }) {
|
|
363
|
+
function ListPopup({ listRef, empty, statusGroups = [], query = "", header, className = "w-(--anchor-width) max-w-(--available-width)", loading, details, collisionBoundary, renderItem }) {
|
|
364
|
+
const withDetails = details !== void 0;
|
|
365
|
+
const list = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
366
|
+
loading && /* @__PURE__ */ jsxs("div", {
|
|
367
|
+
role: "status",
|
|
368
|
+
className: "flex items-center gap-2 px-3 py-2 text-(--theme-muted-text-color)",
|
|
369
|
+
children: [/* @__PURE__ */ jsx(Loader, {
|
|
370
|
+
inline: true,
|
|
371
|
+
decorative: true,
|
|
372
|
+
size: "1em"
|
|
373
|
+
}), loading]
|
|
374
|
+
}),
|
|
375
|
+
empty && !loading && statusGroups.length === 0 && /* @__PURE__ */ jsx(Combobox.Empty, {
|
|
376
|
+
className: "px-3 py-2 text-(--theme-muted-text-color) empty:hidden",
|
|
377
|
+
children: empty
|
|
378
|
+
}),
|
|
379
|
+
/* @__PURE__ */ jsx(Combobox.List, {
|
|
380
|
+
className: cx(withDetails && "min-h-0 flex-1"),
|
|
381
|
+
children: /* @__PURE__ */ jsx(VirtualList, {
|
|
382
|
+
ref: listRef,
|
|
383
|
+
statusGroups,
|
|
384
|
+
query,
|
|
385
|
+
tall: withDetails,
|
|
386
|
+
renderItem
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
] });
|
|
339
390
|
return /* @__PURE__ */ jsx(Combobox.Portal, { children: /* @__PURE__ */ jsx(Combobox.Positioner, {
|
|
340
391
|
align: "start",
|
|
341
392
|
sideOffset: 4,
|
|
342
393
|
className: positionerClassName,
|
|
394
|
+
...collisionBoundary && { collisionBoundary },
|
|
343
395
|
children: /* @__PURE__ */ jsxs(Combobox.Popup, {
|
|
344
|
-
className: cx("overflow-hidden", className, panelClassName),
|
|
345
|
-
children: [
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
className: "
|
|
349
|
-
children:
|
|
350
|
-
}),
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
]
|
|
396
|
+
className: cx("overflow-hidden", withDetails && "@container/picker flex h-[min(40rem,var(--available-height))] flex-col", className, panelClassName),
|
|
397
|
+
children: [header, withDetails ? /* @__PURE__ */ jsxs("div", {
|
|
398
|
+
className: "flex min-h-0 flex-1",
|
|
399
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
400
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
401
|
+
children: list
|
|
402
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
403
|
+
"aria-hidden": "true",
|
|
404
|
+
"data-testid": "combobox-details",
|
|
405
|
+
className: "hidden w-1/2 shrink-0 overflow-y-auto overscroll-contain border-l border-(--theme-border) bg-(--theme-panel-bg) @min-[30rem]/picker:block",
|
|
406
|
+
children: [details, /* @__PURE__ */ jsx("div", { className: "pointer-events-none sticky bottom-0 -mt-6 h-6 bg-linear-to-t from-(--theme-panel-bg) to-transparent" })]
|
|
407
|
+
})]
|
|
408
|
+
}) : list]
|
|
358
409
|
})
|
|
359
410
|
}) });
|
|
360
411
|
}
|
|
@@ -382,7 +433,7 @@ function toRows(filtered, statusGroups) {
|
|
|
382
433
|
rowOfItem
|
|
383
434
|
};
|
|
384
435
|
}
|
|
385
|
-
function VirtualList({ ref, statusGroups, query, renderItem }) {
|
|
436
|
+
function VirtualList({ ref, statusGroups, query, tall, renderItem }) {
|
|
386
437
|
const filtered = Combobox.useFilteredItems();
|
|
387
438
|
const match = useQueryMatch();
|
|
388
439
|
const { rows, rowOfItem } = useMemo(() => {
|
|
@@ -420,7 +471,7 @@ function VirtualList({ ref, statusGroups, query, renderItem }) {
|
|
|
420
471
|
return /* @__PURE__ */ jsx("div", {
|
|
421
472
|
role: "presentation",
|
|
422
473
|
ref: scrollerRef,
|
|
423
|
-
className: "max-h-[min(15rem,var(--available-height))]
|
|
474
|
+
className: cx("overflow-auto overscroll-contain px-1", tall ? "max-h-full" : "max-h-[min(15rem,var(--available-height))]"),
|
|
424
475
|
style: { height: total },
|
|
425
476
|
children: /* @__PURE__ */ jsx("div", {
|
|
426
477
|
role: "presentation",
|
|
@@ -441,7 +492,7 @@ function VirtualList({ ref, statusGroups, query, renderItem }) {
|
|
|
441
492
|
"data-index": virtual.index,
|
|
442
493
|
ref: virtualizer.measureElement,
|
|
443
494
|
style,
|
|
444
|
-
className: "flex items-center gap-1.5 px-
|
|
495
|
+
className: "flex items-center gap-1.5 px-2 pt-2 pb-1 text-xs text-(--theme-muted-text-color)",
|
|
445
496
|
children: /* @__PURE__ */ jsx(GroupHeading, { group: row.group })
|
|
446
497
|
}, virtual.key);
|
|
447
498
|
return renderItem(row.entry, {
|
|
@@ -7,6 +7,11 @@ export interface DrawerProps extends Omit<BaseDrawer.Popup.Props, 'className' |
|
|
|
7
7
|
title?: ReactNode;
|
|
8
8
|
/** A line under the title. */
|
|
9
9
|
description?: ReactNode;
|
|
10
|
+
/** Adds a back arrow before the title, for a view the drawer opened over its first, such as
|
|
11
|
+
* a form; the drawer stays one drawer rather than another stacking on it. */
|
|
12
|
+
onBack?: (() => void) | undefined;
|
|
13
|
+
/** Names the back arrow; defaults to the provider's `back`. */
|
|
14
|
+
backLabel?: string | undefined;
|
|
10
15
|
/** Actions along the bottom edge, such as Cancel and Save; they stay in view as the body scrolls. */
|
|
11
16
|
footer?: ReactNode;
|
|
12
17
|
/** The edge it slides in from, and the way a swipe dismisses it. */
|
|
@@ -44,4 +49,4 @@ export interface DrawerProps extends Omit<BaseDrawer.Popup.Props, 'className' |
|
|
|
44
49
|
* `--app-inset-top` (a top bar's height), and at its widest leaves the strip
|
|
45
50
|
* `--app-inset-start` (a start-side sidebar's width, at least 2.5rem). Set both
|
|
46
51
|
* on the document root. */
|
|
47
|
-
export declare function Drawer({ open, onClose, title, description, footer, side, width, resizable, minWidth, dismissible, modal, closeButton, contained, keepMounted, padded, className, children, ...popupProps }: DrawerProps): import("react").JSX.Element;
|
|
52
|
+
export declare function Drawer({ open, onClose, title, description, onBack, backLabel, footer, side, width, resizable, minWidth, dismissible, modal, closeButton, contained, keepMounted, padded, className, children, ...popupProps }: DrawerProps): import("react").JSX.Element;
|