@parallelworks/ui 0.53.0 → 0.55.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.
Files changed (55) hide show
  1. package/README.md +68 -9
  2. package/dist/access/AccessDrawer.d.ts +55 -0
  3. package/dist/access/AccessDrawer.js +445 -0
  4. package/dist/access/AccessList.d.ts +58 -0
  5. package/dist/access/AccessList.js +382 -0
  6. package/dist/access/GrantView.d.ts +35 -0
  7. package/dist/access/GrantView.js +188 -0
  8. package/dist/access/GroupPicker.d.ts +35 -0
  9. package/dist/access/GroupPicker.js +336 -0
  10. package/dist/access/focus.d.ts +10 -0
  11. package/dist/access/focus.js +27 -0
  12. package/dist/access/index.d.ts +3 -0
  13. package/dist/access/index.js +3 -0
  14. package/dist/access/model.d.ts +49 -0
  15. package/dist/access/model.js +129 -0
  16. package/dist/access/strings.d.ts +122 -0
  17. package/dist/access/strings.js +151 -0
  18. package/dist/access/types.d.ts +56 -0
  19. package/dist/ai/components/SlashMenu.js +1 -1
  20. package/dist/components/Combobox.d.ts +19 -0
  21. package/dist/components/Combobox.js +101 -50
  22. package/dist/components/DatePicker.js +4 -5
  23. package/dist/components/Drawer.d.ts +6 -1
  24. package/dist/components/Drawer.js +25 -16
  25. package/dist/components/Field.d.ts +8 -3
  26. package/dist/components/Field.js +32 -12
  27. package/dist/components/InfoTip.d.ts +3 -1
  28. package/dist/components/InfoTip.js +2 -1
  29. package/dist/components/Menu.js +4 -4
  30. package/dist/components/MenuEntries.js +1 -1
  31. package/dist/components/OptionDescription.d.ts +7 -0
  32. package/dist/components/OptionDescription.js +19 -0
  33. package/dist/components/Popover.d.ts +4 -1
  34. package/dist/components/Popover.js +2 -2
  35. package/dist/components/Provider.d.ts +7 -0
  36. package/dist/components/Provider.js +3 -0
  37. package/dist/components/Select.js +13 -11
  38. package/dist/components/comboboxItems.d.ts +10 -0
  39. package/dist/components/comboboxItems.js +8 -1
  40. package/dist/components/floating.d.ts +5 -2
  41. package/dist/components/floating.js +2 -2
  42. package/dist/editor/EditorField.js +1 -2
  43. package/dist/editor/Monaco.js +26 -2
  44. package/dist/editor/themes.d.ts +5 -0
  45. package/dist/editor/themes.js +18 -3
  46. package/dist/file-explorer/FileExplorer.js +2 -1
  47. package/dist/file-explorer/lib/paging.d.ts +1 -0
  48. package/dist/file-explorer/lib/paging.js +5 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +2 -2
  51. package/dist/styles/theme.css +82 -62
  52. package/dist/styles.css +1 -1
  53. package/dist/theme/derive.d.ts +5 -3
  54. package/dist/theme/derive.js +74 -4
  55. package/package.json +6 -2
@@ -0,0 +1,382 @@
1
+ import { cx } from "../cx.js";
2
+ import { AddIcon, ChevronRightIcon, OrganizationIcon, TrashIcon } from "../icons.js";
3
+ import { Loader } from "../components/Loader.js";
4
+ import { Button } from "../components/Button.js";
5
+ import { IconButton } from "../components/IconButton.js";
6
+ import { Callout } from "../components/Callout.js";
7
+ import { collapsePanelClassName } from "../components/Disclosure.js";
8
+ import { StatusBadge } from "../components/StatusBadge.js";
9
+ import { inheritedRoles, key, overlapOf, removalOf } from "./model.js";
10
+ import { GroupTile } from "./GroupPicker.js";
11
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
+ import { useEffect, useId, useRef } from "react";
13
+ import { Collapsible } from "@base-ui/react/collapsible";
14
+ //#region src/access/AccessList.tsx
15
+ var groupLabel = (group, t) => group.kind === "everyone" ? t.everyoneInOrganization : group.name;
16
+ var sizeOf = (group, t) => group.memberCount === void 0 ? void 0 : group.kind === "everyone" ? t.people(group.memberCount) : t.members(group.memberCount);
17
+ function HighImpact({ t }) {
18
+ return /* @__PURE__ */ jsx(StatusBadge, {
19
+ variant: "warning",
20
+ children: t.highImpact
21
+ });
22
+ }
23
+ /** A role or group whose rows fold under it. */
24
+ function Section({ id, open, onOpenChange, disabled, lead, title, description, aside, narrowAside, notice, children }) {
25
+ return /* @__PURE__ */ jsxs(Collapsible.Root, {
26
+ open: open && !disabled,
27
+ onOpenChange,
28
+ disabled,
29
+ className: "border-b border-(--theme-border)",
30
+ children: [
31
+ /* @__PURE__ */ jsxs(Collapsible.Trigger, {
32
+ id,
33
+ className: cx("group/section flex w-full items-center gap-3 px-4 py-3 text-left outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--theme-focus)", disabled ? "cursor-default" : "cursor-pointer hover:bg-(--theme-hover)"),
34
+ children: [
35
+ /* @__PURE__ */ jsx(ChevronRightIcon, {
36
+ "aria-hidden": "true",
37
+ className: cx("size-4 shrink-0 text-(--theme-muted-text-color) transition-transform group-data-panel-open/section:rotate-90 motion-reduce:transition-none", disabled && "invisible")
38
+ }),
39
+ lead,
40
+ /* @__PURE__ */ jsxs("span", {
41
+ className: "flex min-w-0 flex-1 flex-col",
42
+ children: [
43
+ /* @__PURE__ */ jsx("span", {
44
+ className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5",
45
+ children: /* @__PURE__ */ jsx("span", {
46
+ className: "font-medium wrap-break-word",
47
+ children: title
48
+ })
49
+ }),
50
+ description && /* @__PURE__ */ jsx("span", {
51
+ className: "hidden text-sm text-(--theme-muted-text-color) @min-[30rem]/access:block",
52
+ children: description
53
+ }),
54
+ narrowAside && /* @__PURE__ */ jsx("span", {
55
+ className: "text-sm text-(--theme-muted-text-color) @min-[30rem]/access:hidden",
56
+ children: narrowAside
57
+ })
58
+ ]
59
+ }),
60
+ aside && /* @__PURE__ */ jsx("span", {
61
+ className: "hidden shrink-0 items-center gap-1 text-sm text-(--theme-muted-text-color) @min-[30rem]/access:flex",
62
+ children: aside
63
+ })
64
+ ]
65
+ }),
66
+ /* @__PURE__ */ jsx(Collapsible.Panel, {
67
+ className: collapsePanelClassName,
68
+ children: /* @__PURE__ */ jsx("div", {
69
+ className: "pb-3",
70
+ children
71
+ })
72
+ }),
73
+ notice && /* @__PURE__ */ jsx("div", {
74
+ className: "px-4 pb-3",
75
+ children: notice
76
+ })
77
+ ]
78
+ });
79
+ }
80
+ /** A line under a section: a group under a role, or a role under a group. */
81
+ function Row({ lead, title, meta, action, muted }) {
82
+ return /* @__PURE__ */ jsxs("div", {
83
+ className: "flex items-center gap-3 py-1.5 ps-11 pe-4 @min-[30rem]/access:ps-11",
84
+ children: [
85
+ lead,
86
+ /* @__PURE__ */ jsxs("div", {
87
+ className: cx("min-w-0 flex-1", muted && "text-(--theme-muted-text-color)"),
88
+ children: [/* @__PURE__ */ jsx("div", {
89
+ className: "flex min-w-0 flex-wrap items-center gap-x-2",
90
+ children: title
91
+ }), meta && /* @__PURE__ */ jsx("div", {
92
+ className: "text-sm text-(--theme-muted-text-color)",
93
+ children: meta
94
+ })]
95
+ }),
96
+ action
97
+ ]
98
+ });
99
+ }
100
+ function RemoveButton({ label, saving, removal, onAsk }) {
101
+ return saving ? /* @__PURE__ */ jsx("span", {
102
+ className: "flex size-7 items-center justify-center",
103
+ children: /* @__PURE__ */ jsx(Loader, {
104
+ inline: true,
105
+ size: "1em",
106
+ label
107
+ })
108
+ }) : /* @__PURE__ */ jsx(IconButton, {
109
+ icon: /* @__PURE__ */ jsx(TrashIcon, {}),
110
+ label,
111
+ "data-remove": removeKey(removal),
112
+ onClick: () => onAsk(removal)
113
+ });
114
+ }
115
+ /** Asks before a group loses roles, saying exactly what it loses and what it keeps. */
116
+ function RemoveConfirm({ model, removal, saving, onCancel, onConfirm, t }) {
117
+ const titleId = useId();
118
+ const cancel = useRef(null);
119
+ useEffect(() => cancel.current?.focus(), []);
120
+ const group = model.groupById.get(removal.groupId);
121
+ if (!group) return null;
122
+ const everyone = group.kind === "everyone";
123
+ const roleNames = removal.roleIds.map((id) => model.roleById.get(id)?.name ?? id);
124
+ const impact = removalOf(model, removal.groupId, removal.roleIds);
125
+ const own = impact.keep.map((role) => role.name);
126
+ const throughEveryone = impact.keepThroughEveryone.map((role) => role.name);
127
+ return /* @__PURE__ */ jsx("fieldset", {
128
+ "aria-labelledby": titleId,
129
+ className: "m-0 min-w-0 border-0 px-4 py-2",
130
+ onKeyDown: (e) => {
131
+ if (e.key === "Escape" && !e.defaultPrevented) {
132
+ e.stopPropagation();
133
+ onCancel();
134
+ }
135
+ },
136
+ children: /* @__PURE__ */ jsxs(Callout, {
137
+ tone: "error",
138
+ icon: false,
139
+ children: [
140
+ /* @__PURE__ */ jsx("p", {
141
+ id: titleId,
142
+ className: "m-0 font-semibold text-(--theme-app)",
143
+ children: removal.all ? t.removeAllTitle(groupLabel(group, t)) : t.removeTitle(groupLabel(group, t), roleNames)
144
+ }),
145
+ /* @__PURE__ */ jsx("div", {
146
+ className: "mt-1 text-(--theme-app)",
147
+ children: impact.lost.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
148
+ /* @__PURE__ */ jsx("p", {
149
+ className: "m-0 text-(--theme-muted-text-color)",
150
+ children: t.losesIntro(group.memberCount, everyone)
151
+ }),
152
+ /* @__PURE__ */ jsx("ul", {
153
+ className: "my-2 grid list-disc gap-x-6 gap-y-1 ps-5 @min-[30rem]/access:grid-cols-2",
154
+ children: impact.lost.map((permission) => /* @__PURE__ */ jsx("li", { children: model.permissionLabel(permission) }, permission))
155
+ }),
156
+ /* @__PURE__ */ jsx("p", {
157
+ className: "m-0 text-(--theme-muted-text-color)",
158
+ children: t.keeps(own, throughEveryone)
159
+ })
160
+ ] }) : /* @__PURE__ */ jsx("p", {
161
+ className: "m-0 text-(--theme-muted-text-color)",
162
+ children: t.losesNothing(own, throughEveryone)
163
+ })
164
+ }),
165
+ /* @__PURE__ */ jsxs("div", {
166
+ className: "mt-3 flex justify-end gap-2",
167
+ children: [/* @__PURE__ */ jsx(Button, {
168
+ ref: cancel,
169
+ variant: "ghost",
170
+ onClick: onCancel,
171
+ disabled: saving,
172
+ children: t.common.cancel
173
+ }), /* @__PURE__ */ jsx(Button, {
174
+ variant: "danger",
175
+ loading: saving,
176
+ onClick: onConfirm,
177
+ children: removal.all ? t.removeAllAccess : t.confirmRemove(roleNames)
178
+ })]
179
+ })
180
+ ]
181
+ })
182
+ });
183
+ }
184
+ /** Marks the button that asked for a removal, so focus can go back to it. */
185
+ var removeKey = (removal) => `${removal.groupId}\u0000${removal.all ? "*" : removal.roleIds.join(",")}`;
186
+ var sameRemoval = (a, groupId, roleId) => a !== void 0 && a.groupId === groupId && (roleId === void 0 ? a.all : !a.all && a.roleIds.includes(roleId));
187
+ /** Who has each role, role by role. */
188
+ function ByRole({ model, shown, order, expanded, onExpandedChange, filtering, actions, t, sectionId }) {
189
+ return order.map((roleId) => {
190
+ const role = model.roleById.get(roleId);
191
+ if (!role) return null;
192
+ const holders = model.holders.get(roleId) ?? [];
193
+ const groupIds = shown.get(roleId) ?? [];
194
+ const everyone = holders.some((group) => group.kind === "everyone");
195
+ const others = holders.filter((group) => group.kind !== "everyone").length;
196
+ const count = filtering && groupIds.length !== holders.length ? t.groupsShown(groupIds.length, holders.length) : others > 0 || !everyone ? t.groups(everyone ? others : holders.length) : void 0;
197
+ const everyoneTag = everyone && /* @__PURE__ */ jsxs(StatusBadge, {
198
+ variant: "info",
199
+ size: "md",
200
+ children: [/* @__PURE__ */ jsx(OrganizationIcon, {
201
+ "aria-hidden": "true",
202
+ className: "size-3"
203
+ }), t.everyone]
204
+ });
205
+ const failure = actions.failure?.roleId === roleId ? actions.failure.node : void 0;
206
+ return /* @__PURE__ */ jsx(Section, {
207
+ id: sectionId(roleId),
208
+ open: expanded.has(roleId),
209
+ onOpenChange: (open) => onExpandedChange(roleId, open),
210
+ disabled: holders.length === 0,
211
+ notice: failure,
212
+ title: /* @__PURE__ */ jsxs(Fragment, { children: [role.name, role.highImpact && /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(HighImpact, { t })] })] }),
213
+ description: role.description,
214
+ aside: /* @__PURE__ */ jsxs(Fragment, { children: [everyoneTag, count] }),
215
+ narrowAside: everyoneTag && !count ? /* @__PURE__ */ jsx("span", {
216
+ className: "mt-1 inline-flex",
217
+ children: everyoneTag
218
+ }) : count,
219
+ children: groupIds.map((groupId) => {
220
+ const group = model.groupById.get(groupId);
221
+ if (!group) return null;
222
+ if (sameRemoval(actions.removal, groupId, roleId) && actions.removal) return /* @__PURE__ */ jsx(RemoveConfirm, {
223
+ model,
224
+ removal: actions.removal,
225
+ saving: actions.saving.size > 0,
226
+ onCancel: actions.onCancelRemove,
227
+ onConfirm: () => actions.removal && actions.onRemove(actions.removal),
228
+ t
229
+ }, groupId);
230
+ const overlap = overlapOf(model, groupId, role);
231
+ const hint = overlap.coveredBy.length > 0 ? t.coveredBy(overlap.coveredBy.map((other) => other.name)) : overlap.everyone ? t.everyoneHasThisToo : void 0;
232
+ const label = t.removeRole(groupLabel(group, t), role.name);
233
+ return /* @__PURE__ */ jsx(Row, {
234
+ lead: /* @__PURE__ */ jsx(GroupTile, {
235
+ everyone: group.kind === "everyone",
236
+ className: "hidden @min-[30rem]/access:flex"
237
+ }),
238
+ title: groupLabel(group, t),
239
+ meta: [sizeOf(group, t), hint].filter(Boolean).join(" · "),
240
+ action: !actions.readOnly && /* @__PURE__ */ jsx(RemoveButton, {
241
+ label,
242
+ saving: actions.saving.has(key({
243
+ groupId,
244
+ roleId
245
+ })),
246
+ removal: {
247
+ groupId,
248
+ roleIds: [roleId],
249
+ all: false
250
+ },
251
+ onAsk: actions.onAskRemove
252
+ })
253
+ }, groupId);
254
+ })
255
+ }, roleId);
256
+ });
257
+ }
258
+ /** Which roles each group has, group by group. */
259
+ function ByGroup({ model, shown, order, expanded, onExpandedChange, actions, t, sectionId }) {
260
+ return order.map((groupId) => {
261
+ const group = model.groupById.get(groupId);
262
+ if (!group) return null;
263
+ const held = model.held.get(groupId) ?? [];
264
+ const roleIds = new Set(shown.get(groupId) ?? held.map((role) => role.id));
265
+ const roles = held.filter((role) => roleIds.has(role.id));
266
+ const inherited = inheritedRoles(model, groupId).filter((role) => !held.some((own) => own.id === role.id));
267
+ const everyone = group.kind === "everyone";
268
+ const failure = actions.failure?.groupId === groupId ? actions.failure.node : void 0;
269
+ const badges = held.length <= 2 ? held : held.slice(0, 1);
270
+ const hidden = held.slice(badges.length);
271
+ const roleBadges = /* @__PURE__ */ jsxs(Fragment, { children: [badges.map((role) => /* @__PURE__ */ jsx(StatusBadge, {
272
+ size: "md",
273
+ variant: role.highImpact ? "warning" : "default",
274
+ children: role.name
275
+ }, role.id)), hidden.length > 0 && /* @__PURE__ */ jsxs(StatusBadge, {
276
+ size: "md",
277
+ children: [/* @__PURE__ */ jsx("span", {
278
+ "aria-hidden": "true",
279
+ children: t.moreRoles(hidden.length)
280
+ }), /* @__PURE__ */ jsx("span", {
281
+ className: "sr-only",
282
+ children: t.moreRolesLabel(hidden.map((role) => role.name))
283
+ })]
284
+ })] });
285
+ return /* @__PURE__ */ jsx(Section, {
286
+ id: sectionId(groupId),
287
+ open: expanded.has(groupId),
288
+ onOpenChange: (open) => onExpandedChange(groupId, open),
289
+ lead: /* @__PURE__ */ jsx(GroupTile, {
290
+ everyone,
291
+ className: "hidden @min-[30rem]/access:flex"
292
+ }),
293
+ notice: failure,
294
+ title: groupLabel(group, t),
295
+ description: everyone ? t.everyoneJoinLater : sizeOf(group, t),
296
+ aside: roleBadges,
297
+ narrowAside: /* @__PURE__ */ jsx("span", {
298
+ className: "mt-1 flex flex-wrap gap-1",
299
+ children: roleBadges
300
+ }),
301
+ children: sameRemoval(actions.removal, groupId) && actions.removal ? /* @__PURE__ */ jsx(RemoveConfirm, {
302
+ model,
303
+ removal: actions.removal,
304
+ saving: actions.saving.size > 0,
305
+ onCancel: actions.onCancelRemove,
306
+ onConfirm: () => actions.removal && actions.onRemove(actions.removal),
307
+ t
308
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
309
+ roles.map((role) => sameRemoval(actions.removal, groupId, role.id) && actions.removal ? /* @__PURE__ */ jsx(RemoveConfirm, {
310
+ model,
311
+ removal: actions.removal,
312
+ saving: actions.saving.size > 0,
313
+ onCancel: actions.onCancelRemove,
314
+ onConfirm: () => actions.removal && actions.onRemove(actions.removal),
315
+ t
316
+ }, role.id) : /* @__PURE__ */ jsx(RoleRow, {
317
+ model,
318
+ group,
319
+ role,
320
+ actions,
321
+ t
322
+ }, role.id)),
323
+ inherited.map((role) => /* @__PURE__ */ jsx(Row, {
324
+ muted: true,
325
+ title: role.name,
326
+ meta: t.throughEveryone
327
+ }, role.id)),
328
+ !actions.readOnly && /* @__PURE__ */ jsxs("div", {
329
+ className: "flex flex-wrap items-center gap-2 ps-11 pe-4 pt-2",
330
+ children: [/* @__PURE__ */ jsx(Button, {
331
+ variant: "secondary",
332
+ size: "sm",
333
+ icon: /* @__PURE__ */ jsx(AddIcon, {}),
334
+ onClick: () => actions.onGrantMore(groupId),
335
+ children: t.grantMoreRoles
336
+ }), /* @__PURE__ */ jsx(Button, {
337
+ variant: "ghost",
338
+ size: "sm",
339
+ "data-remove": removeKey({
340
+ groupId,
341
+ roleIds: [],
342
+ all: true
343
+ }),
344
+ className: "text-(--theme-error-text)",
345
+ onClick: () => actions.onAskRemove({
346
+ groupId,
347
+ roleIds: held.map((role) => role.id),
348
+ all: true
349
+ }),
350
+ children: t.removeAllAccess
351
+ })]
352
+ })
353
+ ] })
354
+ }, groupId);
355
+ });
356
+ }
357
+ function RoleRow({ model, group, role, actions, t }) {
358
+ const overlap = overlapOf(model, group.id, role);
359
+ const meta = overlap.coveredBy.length > 0 ? t.redundant(overlap.coveredBy.map((other) => other.name)) : role.description;
360
+ return /* @__PURE__ */ jsx(Row, {
361
+ title: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
362
+ className: "font-medium",
363
+ children: role.name
364
+ }), role.highImpact && /* @__PURE__ */ jsx(HighImpact, { t })] }),
365
+ meta,
366
+ action: !actions.readOnly && /* @__PURE__ */ jsx(RemoveButton, {
367
+ label: t.removeRole(groupLabel(group, t), role.name),
368
+ saving: actions.saving.has(key({
369
+ groupId: group.id,
370
+ roleId: role.id
371
+ })),
372
+ removal: {
373
+ groupId: group.id,
374
+ roleIds: [role.id],
375
+ all: false
376
+ },
377
+ onAsk: actions.onAskRemove
378
+ })
379
+ });
380
+ }
381
+ //#endregion
382
+ export { ByGroup, ByRole, HighImpact, RemoveConfirm, removeKey };
@@ -0,0 +1,35 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type DescribeGroup, type DetailsCache, type SearchGroups } from './GroupPicker.js';
3
+ import { type AccessModel } from './model.js';
4
+ import type { AccessStrings } from './strings.js';
5
+ import type { AccessChange, AccessGroup } from './types.js';
6
+ export interface GrantSelection {
7
+ groupIds: string[];
8
+ roleIds: string[];
9
+ }
10
+ /** The grant form's body and footer, which the drawer lays out in its own body and footer. */
11
+ export declare function useGrantView({ model, selection, onSelectionChange, resourceName, describeGroup, searchGroups, groupCount, recentGroupIds, cache, seen, saving, failure, onGrant, onCancel, focusLater, boundary, t, }: {
12
+ model: AccessModel;
13
+ selection: GrantSelection;
14
+ onSelectionChange: (next: GrantSelection) => void;
15
+ resourceName: string;
16
+ describeGroup?: DescribeGroup | undefined;
17
+ searchGroups?: SearchGroups | undefined;
18
+ groupCount?: number | undefined;
19
+ recentGroupIds?: readonly string[] | undefined;
20
+ cache: DetailsCache;
21
+ seen: Map<string, AccessGroup>;
22
+ saving: boolean;
23
+ /** The failed grant's callout, which stays in the form so the choices can be retried. */
24
+ failure: ReactNode;
25
+ onGrant: (change: AccessChange) => void;
26
+ onCancel: () => void;
27
+ /** Sends focus somewhere once the change it goes with has rendered. */
28
+ focusLater: (find: () => HTMLElement | null | undefined) => void;
29
+ /** The drawer's body, above its footer, which the open group list fills. */
30
+ boundary: Element | null;
31
+ t: AccessStrings;
32
+ }): {
33
+ body: ReactNode;
34
+ footer: ReactNode;
35
+ };
@@ -0,0 +1,188 @@
1
+ import { Button } from "../components/Button.js";
2
+ import { Callout } from "../components/Callout.js";
3
+ import { Combobox } from "../components/Combobox.js";
4
+ import { StatusBadge } from "../components/StatusBadge.js";
5
+ import { covers, grantPlan, overlapOf } from "./model.js";
6
+ import { GroupPicker } from "./GroupPicker.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { useId, useRef, useState } from "react";
9
+ //#region src/access/GrantView.tsx
10
+ /** The grant form's body and footer, which the drawer lays out in its own body and footer. */
11
+ function useGrantView({ model, selection, onSelectionChange, resourceName, describeGroup, searchGroups, groupCount, recentGroupIds, cache, seen, saving, failure, onGrant, onCancel, focusLater, boundary, t }) {
12
+ const uid = useId();
13
+ const [confirming, setConfirming] = useState(false);
14
+ const keepButton = useRef(null);
15
+ const grantId = `${uid}grant`;
16
+ const { groupIds, roleIds } = selection;
17
+ const roles = roleIds.map((id) => model.roleById.get(id)).filter((role) => role !== void 0);
18
+ const roleNames = roles.map((role) => role.name);
19
+ const groupName = (id) => {
20
+ const group = seen.get(id);
21
+ return group?.kind === "everyone" ? t.everyone : group?.name ?? id;
22
+ };
23
+ const plan = grantPlan(model, groupIds, roleIds);
24
+ const everyone = model.everyone;
25
+ const toEveryone = everyone !== void 0 && groupIds.includes(everyone.id);
26
+ const people = everyone?.memberCount;
27
+ const roleItems = model.roles.map((role) => {
28
+ const chosen = roleIds.filter((id) => id !== role.id).map((id) => model.roleById.get(id)).filter((other) => other !== void 0);
29
+ const holders = groupIds.filter((id) => (model.held.get(id) ?? []).some((held) => held.id === role.id));
30
+ const coveredBy = chosen.filter((other) => covers(other, role)).map((r) => r.name);
31
+ const everyoneHas = everyone !== void 0 && !groupIds.includes(everyone.id) && (overlapOf(model, everyone.id, role).coveredBy.length > 0 || (model.held.get(everyone.id) ?? []).some((held) => held.id === role.id));
32
+ const hints = [
33
+ holders.length > 0 && t.alreadyHave(holders.map(groupName)),
34
+ coveredBy.length > 0 && t.alreadyCovered(coveredBy),
35
+ everyoneHas && t.everyoneHasIt
36
+ ].filter((hint) => typeof hint === "string");
37
+ return {
38
+ value: role.id,
39
+ label: role.name,
40
+ description: [role.description, ...hints].filter(Boolean).join(" "),
41
+ badge: role.highImpact ? /* @__PURE__ */ jsx(StatusBadge, {
42
+ variant: "warning",
43
+ children: t.highImpact
44
+ }) : null
45
+ };
46
+ });
47
+ const getting = [...new Set(plan.grant.map((grant) => grant.groupId))].map(groupName);
48
+ const status = groupIds.length === 0 && roleIds.length === 0 ? {
49
+ text: t.pickGroupsAndRoles,
50
+ ready: false
51
+ } : groupIds.length === 0 ? {
52
+ text: t.pickGroups,
53
+ ready: false
54
+ } : roleIds.length === 0 ? {
55
+ text: t.pickRoles,
56
+ ready: false
57
+ } : plan.grant.length === 0 ? {
58
+ text: t.nothingToGrant(groupIds.map(groupName), roleNames),
59
+ ready: false
60
+ } : {
61
+ text: t.grantSummary(getting, [...new Set(plan.grant.map((grant) => grant.roleId))].map((id) => model.roleById.get(id)?.name ?? id)),
62
+ ready: true
63
+ };
64
+ const grant = () => {
65
+ setConfirming(false);
66
+ onGrant({
67
+ grant: plan.grant,
68
+ revoke: []
69
+ });
70
+ };
71
+ const statusId = `${uid}status`;
72
+ const body = /* @__PURE__ */ jsxs("div", {
73
+ className: "flex flex-col gap-5 p-4",
74
+ children: [
75
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
76
+ htmlFor: `${uid}groups`,
77
+ className: "mb-1.5 block text-sm font-medium",
78
+ children: t.groupsLabel
79
+ }), /* @__PURE__ */ jsx(GroupPicker, {
80
+ id: `${uid}groups`,
81
+ describedBy: `${uid}groupsHint`,
82
+ model,
83
+ value: groupIds,
84
+ onValueChange: (next) => onSelectionChange({
85
+ groupIds: next,
86
+ roleIds
87
+ }),
88
+ granting: roles,
89
+ resourceName,
90
+ describeGroup,
91
+ searchGroups,
92
+ groupCount,
93
+ recentGroupIds,
94
+ cache,
95
+ seen,
96
+ boundary,
97
+ t
98
+ })] }),
99
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
100
+ htmlFor: `${uid}roles`,
101
+ className: "mb-1.5 block text-sm font-medium",
102
+ children: t.rolesLabel
103
+ }), /* @__PURE__ */ jsx(Combobox, {
104
+ multiple: true,
105
+ id: `${uid}roles`,
106
+ items: roleItems,
107
+ value: roleIds,
108
+ onValueChange: (next) => onSelectionChange({
109
+ groupIds,
110
+ roleIds: next
111
+ }),
112
+ placeholder: t.searchRoles
113
+ })] }),
114
+ toEveryone && roles.length > 0 && /* @__PURE__ */ jsx(Callout, {
115
+ tone: "warning",
116
+ title: t.everyoneWarningTitle(roleNames, people),
117
+ children: t.everyoneWarning(resourceName)
118
+ }),
119
+ failure
120
+ ]
121
+ });
122
+ const stopConfirming = () => {
123
+ setConfirming(false);
124
+ focusLater(() => document.getElementById(grantId));
125
+ };
126
+ return {
127
+ body,
128
+ footer: confirming ? /* @__PURE__ */ jsxs("fieldset", {
129
+ "aria-labelledby": statusId,
130
+ className: "m-0 flex w-full min-w-0 flex-wrap items-center gap-x-4 gap-y-2 border-0 p-0",
131
+ onKeyDown: (e) => {
132
+ if (e.key === "Escape") {
133
+ e.stopPropagation();
134
+ stopConfirming();
135
+ }
136
+ },
137
+ children: [/* @__PURE__ */ jsx("p", {
138
+ id: statusId,
139
+ className: "m-0 min-w-0 flex-1 basis-48 text-sm font-medium",
140
+ children: t.confirmEveryoneTitle(roleNames, people)
141
+ }), /* @__PURE__ */ jsxs("div", {
142
+ className: "ms-auto flex shrink-0 gap-2",
143
+ children: [/* @__PURE__ */ jsx(Button, {
144
+ ref: keepButton,
145
+ variant: "secondary",
146
+ onClick: stopConfirming,
147
+ disabled: saving,
148
+ children: t.common.cancel
149
+ }), /* @__PURE__ */ jsx(Button, {
150
+ variant: roles.some((role) => role.highImpact) ? "danger" : "primary",
151
+ loading: saving,
152
+ onClick: grant,
153
+ children: t.grantToEveryone
154
+ })]
155
+ })]
156
+ }) : /* @__PURE__ */ jsxs("div", {
157
+ className: "flex w-full flex-wrap items-center gap-x-4 gap-y-2",
158
+ children: [/* @__PURE__ */ jsx("p", {
159
+ id: statusId,
160
+ className: "m-0 min-w-0 flex-1 basis-48 text-sm text-(--theme-muted-text-color)",
161
+ children: status.text
162
+ }), /* @__PURE__ */ jsxs("div", {
163
+ className: "ms-auto flex shrink-0 gap-2",
164
+ children: [/* @__PURE__ */ jsx(Button, {
165
+ variant: "secondary",
166
+ onClick: onCancel,
167
+ children: t.common.cancel
168
+ }), /* @__PURE__ */ jsx(Button, {
169
+ id: grantId,
170
+ variant: "primary",
171
+ focusableWhenDisabled: true,
172
+ disabled: !status.ready,
173
+ "aria-describedby": statusId,
174
+ loading: saving,
175
+ onClick: () => {
176
+ if (toEveryone) {
177
+ setConfirming(true);
178
+ focusLater(() => keepButton.current);
179
+ } else grant();
180
+ },
181
+ children: toEveryone ? t.grantToEveryone : t.grant(roleNames)
182
+ })]
183
+ })]
184
+ })
185
+ };
186
+ }
187
+ //#endregion
188
+ export { useGrantView };
@@ -0,0 +1,35 @@
1
+ import { type AccessModel } from './model.js';
2
+ import type { AccessStrings } from './strings.js';
3
+ import type { AccessGroup, AccessGroupDetails, AccessRole } from './types.js';
4
+ export type DescribeGroup = (groupId: string, signal: AbortSignal) => Promise<AccessGroupDetails>;
5
+ export type SearchGroups = (query: string, signal: AbortSignal) => Promise<readonly AccessGroup[]>;
6
+ /** Details already loaded, kept for as long as the drawer is open. */
7
+ export type DetailsCache = Map<string, AccessGroupDetails>;
8
+ export declare function GroupTile({ everyone, className }: {
9
+ everyone: boolean;
10
+ className?: string;
11
+ }): import("react").JSX.Element;
12
+ interface GroupPickerProps {
13
+ model: AccessModel;
14
+ value: readonly string[];
15
+ onValueChange: (ids: string[]) => void;
16
+ /** The roles being granted, which the details weigh against what the group holds. */
17
+ granting: readonly AccessRole[];
18
+ resourceName: string;
19
+ describeGroup?: DescribeGroup | undefined;
20
+ searchGroups?: SearchGroups | undefined;
21
+ groupCount?: number | undefined;
22
+ recentGroupIds?: readonly string[] | undefined;
23
+ cache: DetailsCache;
24
+ /** Every group the picker has seen, by id, so a chosen one found by a search keeps its name. */
25
+ seen: Map<string, AccessGroup>;
26
+ id: string;
27
+ describedBy: string;
28
+ /** The drawer's body, above its footer, which the open list fills. */
29
+ boundary: Element | null;
30
+ t: AccessStrings;
31
+ }
32
+ /** The groups field: everyone pinned first, then recent groups, then all of them; a pane beside
33
+ * the list says who is in the highlighted group and what it holds here. */
34
+ export declare function GroupPicker({ model, value, onValueChange, granting, resourceName, describeGroup, searchGroups, groupCount, recentGroupIds, cache, seen, id, describedBy, boundary, t, }: GroupPickerProps): import("react").JSX.Element;
35
+ export {};