@parallelworks/ui 0.55.0 → 0.57.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 (43) hide show
  1. package/README.md +60 -24
  2. package/dist/access/AccessDrawer.d.ts +30 -19
  3. package/dist/access/AccessDrawer.js +92 -67
  4. package/dist/access/AccessList.d.ts +17 -16
  5. package/dist/access/AccessList.js +218 -114
  6. package/dist/access/GrantView.d.ts +9 -8
  7. package/dist/access/GrantView.js +34 -36
  8. package/dist/access/PrincipalPicker.d.ts +38 -0
  9. package/dist/access/{GroupPicker.js → PrincipalPicker.js} +173 -80
  10. package/dist/access/index.d.ts +1 -1
  11. package/dist/access/model.d.ts +67 -24
  12. package/dist/access/model.js +111 -47
  13. package/dist/access/strings.d.ts +63 -39
  14. package/dist/access/strings.js +114 -84
  15. package/dist/access/types.d.ts +32 -14
  16. package/dist/ai/adapter/mock/mockAdapter.js +1 -1
  17. package/dist/ai/core/useSSEStream.d.ts +1 -1
  18. package/dist/components/Checkbox.js +1 -1
  19. package/dist/components/Combobox.js +25 -20
  20. package/dist/components/DatePicker.js +58 -53
  21. package/dist/components/Drawer.js +3 -1
  22. package/dist/components/HoverCard.js +15 -10
  23. package/dist/components/Menu.js +3 -0
  24. package/dist/components/Modal.js +32 -27
  25. package/dist/components/Popover.js +30 -25
  26. package/dist/components/Radio.js +1 -1
  27. package/dist/components/Select.js +47 -42
  28. package/dist/components/Slider.js +3 -3
  29. package/dist/components/Switch.js +1 -1
  30. package/dist/components/ThemeEditor.js +1 -1
  31. package/dist/components/Tooltip.js +16 -11
  32. package/dist/components/fieldBox.js +3 -3
  33. package/dist/components/fullscreen.d.ts +4 -0
  34. package/dist/components/fullscreen.js +18 -0
  35. package/dist/graph/GraphEditorDialogs.d.ts +1 -1
  36. package/dist/icons.d.ts +2 -0
  37. package/dist/icons.js +22 -2
  38. package/dist/styles/theme.css +2 -1
  39. package/dist/styles.css +1 -1
  40. package/dist/theme/derive.d.ts +3 -1
  41. package/dist/theme/derive.js +17 -11
  42. package/package.json +1 -1
  43. package/dist/access/GroupPicker.d.ts +0 -35
@@ -1,26 +1,35 @@
1
1
  import { cx } from "../cx.js";
2
- import { AddIcon, ChevronRightIcon, OrganizationIcon, TrashIcon } from "../icons.js";
2
+ import { AddIcon, ChevronRightIcon, LockIcon, OrganizationIcon, TrashIcon } from "../icons.js";
3
3
  import { Loader } from "../components/Loader.js";
4
4
  import { Button } from "../components/Button.js";
5
5
  import { IconButton } from "../components/IconButton.js";
6
6
  import { Callout } from "../components/Callout.js";
7
7
  import { collapsePanelClassName } from "../components/Disclosure.js";
8
8
  import { StatusBadge } from "../components/StatusBadge.js";
9
- import { inheritedRoles, key, overlapOf, removalOf } from "./model.js";
10
- import { GroupTile } from "./GroupPicker.js";
9
+ import { alsoHas, firstOfEach, isOwner, key, overlapOf, removalOf } from "./model.js";
10
+ import { PrincipalTile } from "./PrincipalPicker.js";
11
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
12
  import { useEffect, useId, useRef } from "react";
13
13
  import { Collapsible } from "@base-ui/react/collapsible";
14
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
15
+ /** The id the owner's section has among the roles' sections. */
16
+ var OWNER_SECTION = "__owner";
17
+ var principalLabel = (principal, t) => principal.kind === "everyone" ? t.everyoneInOrganization : principal.name;
18
+ /** The line under a name: who a person is, or how many are in a group. */
19
+ var aboutOf = (principal, t) => principal.kind === "user" ? principal.detail : principal.memberCount === void 0 ? void 0 : principal.kind === "everyone" ? t.people(principal.memberCount) : t.members(principal.memberCount);
20
+ /** Where a role someone has elsewhere comes from, as "through" reads it. */
21
+ var sourceOf = (holding) => holding.through?.name ?? holding.from ?? "";
22
+ /** Stands where a remove button would, on a row that can't be removed here. */
23
+ function Locked() {
24
+ return /* @__PURE__ */ jsx("span", {
25
+ className: "flex size-7 items-center justify-center text-(--theme-muted-text-color)",
26
+ children: /* @__PURE__ */ jsx(LockIcon, {
27
+ "aria-hidden": "true",
28
+ className: "size-4"
29
+ })
21
30
  });
22
31
  }
23
- /** A role or group whose rows fold under it. */
32
+ /** A role or principal whose rows fold under it. */
24
33
  function Section({ id, open, onOpenChange, disabled, lead, title, description, aside, narrowAside, notice, children }) {
25
34
  return /* @__PURE__ */ jsxs(Collapsible.Root, {
26
35
  open: open && !disabled,
@@ -30,7 +39,7 @@ function Section({ id, open, onOpenChange, disabled, lead, title, description, a
30
39
  children: [
31
40
  /* @__PURE__ */ jsxs(Collapsible.Trigger, {
32
41
  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)"),
42
+ className: cx("group/section flex w-full items-center gap-3 px-4 py-2.5 text-left focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--theme-focus)", disabled ? "cursor-default" : "cursor-pointer hover:bg-(--theme-hover)"),
34
43
  children: [
35
44
  /* @__PURE__ */ jsx(ChevronRightIcon, {
36
45
  "aria-hidden": "true",
@@ -43,22 +52,22 @@ function Section({ id, open, onOpenChange, disabled, lead, title, description, a
43
52
  /* @__PURE__ */ jsx("span", {
44
53
  className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5",
45
54
  children: /* @__PURE__ */ jsx("span", {
46
- className: "font-medium wrap-break-word",
55
+ className: "text-sm font-medium wrap-anywhere",
47
56
  children: title
48
57
  })
49
58
  }),
50
59
  description && /* @__PURE__ */ jsx("span", {
51
- className: "hidden text-sm text-(--theme-muted-text-color) @min-[30rem]/access:block",
60
+ className: "hidden text-xs wrap-anywhere text-(--theme-muted-text-color) @min-[30rem]/access:block",
52
61
  children: description
53
62
  }),
54
63
  narrowAside && /* @__PURE__ */ jsx("span", {
55
- className: "text-sm text-(--theme-muted-text-color) @min-[30rem]/access:hidden",
64
+ className: "text-xs text-(--theme-muted-text-color) @min-[30rem]/access:hidden",
56
65
  children: narrowAside
57
66
  })
58
67
  ]
59
68
  }),
60
69
  aside && /* @__PURE__ */ jsx("span", {
61
- className: "hidden shrink-0 items-center gap-1 text-sm text-(--theme-muted-text-color) @min-[30rem]/access:flex",
70
+ className: "hidden shrink-0 items-center gap-1 text-xs text-(--theme-muted-text-color) @min-[30rem]/access:flex",
62
71
  children: aside
63
72
  })
64
73
  ]
@@ -77,7 +86,7 @@ function Section({ id, open, onOpenChange, disabled, lead, title, description, a
77
86
  ]
78
87
  });
79
88
  }
80
- /** A line under a section: a group under a role, or a role under a group. */
89
+ /** A line under a section: a principal under a role, or a role under a principal. */
81
90
  function Row({ lead, title, meta, action, muted }) {
82
91
  return /* @__PURE__ */ jsxs("div", {
83
92
  className: "flex items-center gap-3 py-1.5 ps-11 pe-4 @min-[30rem]/access:ps-11",
@@ -86,10 +95,10 @@ function Row({ lead, title, meta, action, muted }) {
86
95
  /* @__PURE__ */ jsxs("div", {
87
96
  className: cx("min-w-0 flex-1", muted && "text-(--theme-muted-text-color)"),
88
97
  children: [/* @__PURE__ */ jsx("div", {
89
- className: "flex min-w-0 flex-wrap items-center gap-x-2",
98
+ className: "flex min-w-0 flex-wrap items-center gap-x-2 text-sm wrap-anywhere",
90
99
  children: title
91
100
  }), meta && /* @__PURE__ */ jsx("div", {
92
- className: "text-sm text-(--theme-muted-text-color)",
101
+ className: "text-xs wrap-anywhere text-(--theme-muted-text-color)",
93
102
  children: meta
94
103
  })]
95
104
  }),
@@ -112,21 +121,38 @@ function RemoveButton({ label, saving, removal, onAsk }) {
112
121
  onClick: () => onAsk(removal)
113
122
  });
114
123
  }
115
- /** Asks before a group loses roles, saying exactly what it loses and what it keeps. */
124
+ /** What someone keeps elsewhere, by where it comes from, everyone's apart. */
125
+ function keptElsewhere(holdings) {
126
+ const throughEveryone = [];
127
+ const through = /* @__PURE__ */ new Map();
128
+ for (const holding of holdings) if (holding.through?.kind === "everyone") throughEveryone.push(holding.role.name);
129
+ else {
130
+ const from = sourceOf(holding);
131
+ through.set(from, [...through.get(from) ?? [], holding.role.name]);
132
+ }
133
+ return {
134
+ throughEveryone,
135
+ through: [...through].map(([from, roles]) => ({
136
+ from,
137
+ roles
138
+ }))
139
+ };
140
+ }
141
+ /** Asks before a principal loses roles, saying exactly what it loses and what it keeps. */
116
142
  function RemoveConfirm({ model, removal, saving, onCancel, onConfirm, t }) {
117
143
  const titleId = useId();
118
144
  const cancel = useRef(null);
119
145
  useEffect(() => cancel.current?.focus(), []);
120
- const group = model.groupById.get(removal.groupId);
121
- if (!group) return null;
122
- const everyone = group.kind === "everyone";
146
+ const principal = model.principalById.get(removal.principalId);
147
+ if (!principal) return null;
123
148
  const roleNames = removal.roleIds.map((id) => model.roleById.get(id)?.name ?? id);
124
- const impact = removalOf(model, removal.groupId, removal.roleIds);
149
+ const impact = removalOf(model, removal.principalId, removal.roleIds);
125
150
  const own = impact.keep.map((role) => role.name);
126
- const throughEveryone = impact.keepThroughEveryone.map((role) => role.name);
151
+ const { throughEveryone, through } = keptElsewhere(impact.keepElsewhere);
152
+ const kept = t.keeps(own, throughEveryone, through);
127
153
  return /* @__PURE__ */ jsx("fieldset", {
128
154
  "aria-labelledby": titleId,
129
- className: "m-0 min-w-0 border-0 px-4 py-2",
155
+ className: "m-0 min-w-0 border-0 px-4 py-2 text-sm",
130
156
  onKeyDown: (e) => {
131
157
  if (e.key === "Escape" && !e.defaultPrevented) {
132
158
  e.stopPropagation();
@@ -140,26 +166,33 @@ function RemoveConfirm({ model, removal, saving, onCancel, onConfirm, t }) {
140
166
  /* @__PURE__ */ jsx("p", {
141
167
  id: titleId,
142
168
  className: "m-0 font-semibold text-(--theme-app)",
143
- children: removal.all ? t.removeAllTitle(groupLabel(group, t)) : t.removeTitle(groupLabel(group, t), roleNames)
169
+ children: removal.all ? t.removeAllTitle(principalLabel(principal, t)) : t.removeTitle(principalLabel(principal, t), roleNames)
144
170
  }),
145
171
  /* @__PURE__ */ jsx("div", {
146
172
  className: "mt-1 text-(--theme-app)",
147
- children: impact.lost.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
173
+ children: impact.owner ? /* @__PURE__ */ jsx("p", {
174
+ className: "m-0 text-(--theme-muted-text-color)",
175
+ children: t.ownerLosesNothing
176
+ }) : impact.lost.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
148
177
  /* @__PURE__ */ jsx("p", {
149
178
  className: "m-0 text-(--theme-muted-text-color)",
150
- children: t.losesIntro(group.memberCount, everyone)
179
+ children: t.losesIntro(principal.kind, principal.memberCount)
151
180
  }),
152
181
  /* @__PURE__ */ jsx("ul", {
153
182
  className: "my-2 grid list-disc gap-x-6 gap-y-1 ps-5 @min-[30rem]/access:grid-cols-2",
154
183
  children: impact.lost.map((permission) => /* @__PURE__ */ jsx("li", { children: model.permissionLabel(permission) }, permission))
155
184
  }),
156
- /* @__PURE__ */ jsx("p", {
185
+ kept && /* @__PURE__ */ jsx("p", {
157
186
  className: "m-0 text-(--theme-muted-text-color)",
158
- children: t.keeps(own, throughEveryone)
187
+ children: kept
159
188
  })
160
189
  ] }) : /* @__PURE__ */ jsx("p", {
161
190
  className: "m-0 text-(--theme-muted-text-color)",
162
- children: t.losesNothing(own, throughEveryone)
191
+ children: t.losesNothing([
192
+ ...own,
193
+ ...through.flatMap((source) => source.roles),
194
+ ...throughEveryone
195
+ ])
163
196
  })
164
197
  }),
165
198
  /* @__PURE__ */ jsxs("div", {
@@ -182,18 +215,51 @@ function RemoveConfirm({ model, removal, saving, onCancel, onConfirm, t }) {
182
215
  });
183
216
  }
184
217
  /** 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. */
218
+ var removeKey = (removal) => `${removal.principalId}\u0000${removal.all ? "*" : removal.roleIds.join(",")}`;
219
+ var sameRemoval = (a, principalId, roleId) => a !== void 0 && a.principalId === principalId && (roleId === void 0 ? a.all : !a.all && a.roleIds.includes(roleId));
220
+ /** The hint beside someone holding a role: what else already gives it to them. */
221
+ function overlapHint(overlap, t) {
222
+ if (overlap.owner) return t.ownerHasAll;
223
+ if (overlap.coveredBy.length > 0) return t.coveredBy(overlap.coveredBy.map((other) => other.name));
224
+ const from = [...new Set(overlap.elsewhere.filter((holding) => holding.through?.kind !== "everyone").map((holding) => sourceOf(holding)))];
225
+ if (from.length > 0) return t.throughToo(from);
226
+ return overlap.elsewhere.length > 0 ? t.everyoneHasThisToo : void 0;
227
+ }
228
+ /** Who has each role, role by role, after the owner. */
188
229
  function ByRole({ model, shown, order, expanded, onExpandedChange, filtering, actions, t, sectionId }) {
189
230
  return order.map((roleId) => {
231
+ if (roleId === "__owner") {
232
+ const owner = model.owner;
233
+ return owner && /* @__PURE__ */ jsx(Section, {
234
+ id: sectionId(roleId),
235
+ open: expanded.has(roleId),
236
+ onOpenChange: (open) => onExpandedChange(roleId, open),
237
+ title: t.owner,
238
+ description: t.ownerDescription,
239
+ aside: principalLabel(owner, t),
240
+ narrowAside: principalLabel(owner, t),
241
+ children: /* @__PURE__ */ jsx(Row, {
242
+ lead: /* @__PURE__ */ jsx(PrincipalTile, {
243
+ principal: owner,
244
+ compact: true,
245
+ className: "hidden @min-[30rem]/access:flex"
246
+ }),
247
+ title: principalLabel(owner, t),
248
+ meta: aboutOf(owner, t),
249
+ action: /* @__PURE__ */ jsx(Locked, {})
250
+ })
251
+ }, roleId);
252
+ }
190
253
  const role = model.roleById.get(roleId);
191
254
  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;
255
+ const direct = model.holders.get(roleId) ?? [];
256
+ const inherited = model.inheritedHolders.get(roleId) ?? [];
257
+ const all = new Map([...direct, ...inherited.map((holder) => holder.principal)].map((p) => [p.id, p]));
258
+ const principalIds = shown.get(roleId) ?? [];
259
+ const everyone = [...all.values()].some((principal) => principal.kind === "everyone");
260
+ const people = [...all.values()].filter((principal) => principal.kind === "user").length;
261
+ const groups = [...all.values()].filter((principal) => principal.kind === "group").length;
262
+ const count = filtering && principalIds.length !== all.size ? t.holdersShown(principalIds.length, all.size) : people + groups > 0 ? t.holders(people, groups) : void 0;
197
263
  const everyoneTag = everyone && /* @__PURE__ */ jsxs(StatusBadge, {
198
264
  variant: "info",
199
265
  size: "md",
@@ -207,98 +273,120 @@ function ByRole({ model, shown, order, expanded, onExpandedChange, filtering, ac
207
273
  id: sectionId(roleId),
208
274
  open: expanded.has(roleId),
209
275
  onOpenChange: (open) => onExpandedChange(roleId, open),
210
- disabled: holders.length === 0,
276
+ disabled: all.size === 0,
211
277
  notice: failure,
212
- title: /* @__PURE__ */ jsxs(Fragment, { children: [role.name, role.highImpact && /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(HighImpact, { t })] })] }),
278
+ title: role.name,
213
279
  description: role.description,
214
280
  aside: /* @__PURE__ */ jsxs(Fragment, { children: [everyoneTag, count] }),
215
281
  narrowAside: everyoneTag && !count ? /* @__PURE__ */ jsx("span", {
216
282
  className: "mt-1 inline-flex",
217
283
  children: everyoneTag
218
284
  }) : 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, {
285
+ children: principalIds.map((principalId) => {
286
+ const principal = all.get(principalId);
287
+ if (!principal) return null;
288
+ const lead = /* @__PURE__ */ jsx(PrincipalTile, {
289
+ principal,
290
+ compact: true,
291
+ className: "hidden @min-[30rem]/access:flex"
292
+ });
293
+ if (!direct.some((holder) => holder.id === principalId)) {
294
+ const from = inherited.find((holder) => holder.principal.id === principalId)?.from;
295
+ return /* @__PURE__ */ jsx(Row, {
296
+ lead,
297
+ title: principalLabel(principal, t),
298
+ meta: [aboutOf(principal, t), from !== void 0 && t.inheritedFrom(from)].filter(Boolean).join(" · "),
299
+ action: /* @__PURE__ */ jsx(Locked, {})
300
+ }, principalId);
301
+ }
302
+ if (sameRemoval(actions.removal, principalId, roleId) && actions.removal) return /* @__PURE__ */ jsx(RemoveConfirm, {
223
303
  model,
224
304
  removal: actions.removal,
225
305
  saving: actions.saving.size > 0,
226
306
  onCancel: actions.onCancelRemove,
227
307
  onConfirm: () => actions.removal && actions.onRemove(actions.removal),
228
308
  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);
309
+ }, principalId);
310
+ const hint = overlapHint(overlapOf(model, principalId, role), t);
311
+ const label = t.removeRole(principalLabel(principal, t), role.name);
233
312
  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(" · "),
313
+ lead,
314
+ title: principalLabel(principal, t),
315
+ meta: [aboutOf(principal, t), hint].filter(Boolean).join(" · "),
240
316
  action: !actions.readOnly && /* @__PURE__ */ jsx(RemoveButton, {
241
317
  label,
242
318
  saving: actions.saving.has(key({
243
- groupId,
319
+ principalId,
244
320
  roleId
245
321
  })),
246
322
  removal: {
247
- groupId,
323
+ principalId,
248
324
  roleIds: [roleId],
249
325
  all: false
250
326
  },
251
327
  onAsk: actions.onAskRemove
252
328
  })
253
- }, groupId);
329
+ }, principalId);
254
330
  })
255
331
  }, roleId);
256
332
  });
257
333
  }
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));
334
+ /** Which roles each person and group has, one by one. */
335
+ function ByPrincipal({ model, shown, order, expanded, onExpandedChange, actions, t, sectionId }) {
336
+ return order.map((principalId) => {
337
+ const principal = model.principalById.get(principalId);
338
+ if (!principal) return null;
339
+ const owner = isOwner(model, principalId);
340
+ const held = model.held.get(principalId) ?? [];
341
+ const ownInherited = model.inherited.get(principalId) ?? [];
342
+ const roleIds = new Set(shown.get(principalId) ?? [...held, ...ownInherited.map((h) => h.role)].map((r) => r.id));
265
343
  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
- })] });
344
+ const inherited = ownInherited.filter((holding) => roleIds.has(holding.role.id) && !held.some((role) => role.id === holding.role.id));
345
+ const through = firstOfEach(alsoHas(model, principalId)).filter((holding) => holding.through !== void 0 && !held.some((role) => role.id === holding.role.id) && !ownInherited.some((own) => own.role.id === holding.role.id));
346
+ const failure = actions.failure?.principalId === principalId ? actions.failure.node : void 0;
347
+ const everyone = principal.kind === "everyone";
348
+ const badgeRoles = [...held, ...ownInherited.map((holding) => holding.role).filter((role) => !held.includes(role))];
349
+ const room = owner ? 1 : 2;
350
+ const badges = badgeRoles.length <= room ? badgeRoles : badgeRoles.slice(0, room - 1);
351
+ const hidden = badgeRoles.slice(badges.length);
352
+ const roleBadges = /* @__PURE__ */ jsxs(Fragment, { children: [
353
+ owner && /* @__PURE__ */ jsx(StatusBadge, {
354
+ size: "md",
355
+ variant: "info",
356
+ children: t.owner
357
+ }),
358
+ badges.map((role) => /* @__PURE__ */ jsx(StatusBadge, {
359
+ size: "md",
360
+ children: role.name
361
+ }, role.id)),
362
+ hidden.length > 0 && /* @__PURE__ */ jsxs(StatusBadge, {
363
+ size: "md",
364
+ children: [/* @__PURE__ */ jsx("span", {
365
+ "aria-hidden": "true",
366
+ children: t.moreRoles(hidden.length)
367
+ }), /* @__PURE__ */ jsx("span", {
368
+ className: "sr-only",
369
+ children: t.moreRolesLabel(hidden.map((role) => role.name))
370
+ })]
371
+ })
372
+ ] });
285
373
  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,
374
+ id: sectionId(principalId),
375
+ open: expanded.has(principalId),
376
+ onOpenChange: (open) => onExpandedChange(principalId, open),
377
+ lead: /* @__PURE__ */ jsx(PrincipalTile, {
378
+ principal,
291
379
  className: "hidden @min-[30rem]/access:flex"
292
380
  }),
293
381
  notice: failure,
294
- title: groupLabel(group, t),
295
- description: everyone ? t.everyoneJoinLater : sizeOf(group, t),
382
+ title: principalLabel(principal, t),
383
+ description: everyone ? t.everyoneJoinLater : aboutOf(principal, t),
296
384
  aside: roleBadges,
297
385
  narrowAside: /* @__PURE__ */ jsx("span", {
298
386
  className: "mt-1 flex flex-wrap gap-1",
299
387
  children: roleBadges
300
388
  }),
301
- children: sameRemoval(actions.removal, groupId) && actions.removal ? /* @__PURE__ */ jsx(RemoveConfirm, {
389
+ children: sameRemoval(actions.removal, principalId) && actions.removal ? /* @__PURE__ */ jsx(RemoveConfirm, {
302
390
  model,
303
391
  removal: actions.removal,
304
392
  saving: actions.saving.size > 0,
@@ -306,7 +394,15 @@ function ByGroup({ model, shown, order, expanded, onExpandedChange, actions, t,
306
394
  onConfirm: () => actions.removal && actions.onRemove(actions.removal),
307
395
  t
308
396
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [
309
- roles.map((role) => sameRemoval(actions.removal, groupId, role.id) && actions.removal ? /* @__PURE__ */ jsx(RemoveConfirm, {
397
+ owner && /* @__PURE__ */ jsx(Row, {
398
+ title: /* @__PURE__ */ jsx("span", {
399
+ className: "font-medium",
400
+ children: t.owner
401
+ }),
402
+ meta: t.ownerDescription,
403
+ action: /* @__PURE__ */ jsx(Locked, {})
404
+ }),
405
+ roles.map((role) => sameRemoval(actions.removal, principalId, role.id) && actions.removal ? /* @__PURE__ */ jsx(RemoveConfirm, {
310
406
  model,
311
407
  removal: actions.removal,
312
408
  saving: actions.saving.size > 0,
@@ -315,35 +411,43 @@ function ByGroup({ model, shown, order, expanded, onExpandedChange, actions, t,
315
411
  t
316
412
  }, role.id) : /* @__PURE__ */ jsx(RoleRow, {
317
413
  model,
318
- group,
414
+ principal,
319
415
  role,
320
416
  actions,
321
417
  t
322
418
  }, role.id)),
323
- inherited.map((role) => /* @__PURE__ */ jsx(Row, {
324
- muted: true,
325
- title: role.name,
326
- meta: t.throughEveryone
419
+ inherited.map(({ role, from }) => /* @__PURE__ */ jsx(Row, {
420
+ title: /* @__PURE__ */ jsx("span", {
421
+ className: "font-medium",
422
+ children: role.name
423
+ }),
424
+ meta: t.inheritedFrom(from),
425
+ action: /* @__PURE__ */ jsx(Locked, {})
327
426
  }, role.id)),
328
- !actions.readOnly && /* @__PURE__ */ jsxs("div", {
427
+ through.map((holding) => /* @__PURE__ */ jsx(Row, {
428
+ muted: true,
429
+ title: holding.role.name,
430
+ meta: holding.through?.kind === "everyone" ? t.throughEveryone : t.through(sourceOf(holding))
431
+ }, holding.role.id)),
432
+ !actions.readOnly && (!owner || held.length > 0) && /* @__PURE__ */ jsxs("div", {
329
433
  className: "flex flex-wrap items-center gap-2 ps-11 pe-4 pt-2",
330
- children: [/* @__PURE__ */ jsx(Button, {
434
+ children: [!owner && /* @__PURE__ */ jsx(Button, {
331
435
  variant: "secondary",
332
436
  size: "sm",
333
437
  icon: /* @__PURE__ */ jsx(AddIcon, {}),
334
- onClick: () => actions.onGrantMore(groupId),
438
+ onClick: () => actions.onGrantMore(principalId),
335
439
  children: t.grantMoreRoles
336
- }), /* @__PURE__ */ jsx(Button, {
440
+ }), held.length > 0 && /* @__PURE__ */ jsx(Button, {
337
441
  variant: "ghost",
338
442
  size: "sm",
339
443
  "data-remove": removeKey({
340
- groupId,
444
+ principalId,
341
445
  roleIds: [],
342
446
  all: true
343
447
  }),
344
448
  className: "text-(--theme-error-text)",
345
449
  onClick: () => actions.onAskRemove({
346
- groupId,
450
+ principalId,
347
451
  roleIds: held.map((role) => role.id),
348
452
  all: true
349
453
  }),
@@ -351,26 +455,26 @@ function ByGroup({ model, shown, order, expanded, onExpandedChange, actions, t,
351
455
  })]
352
456
  })
353
457
  ] })
354
- }, groupId);
458
+ }, principalId);
355
459
  });
356
460
  }
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;
461
+ function RoleRow({ model, principal, role, actions, t }) {
462
+ const overlap = overlapOf(model, principal.id, role);
463
+ const meta = overlap.owner ? t.redundantOwner : overlap.coveredBy.length > 0 ? t.redundant(overlap.coveredBy.map((other) => other.name)) : role.description;
360
464
  return /* @__PURE__ */ jsx(Row, {
361
- title: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
465
+ title: /* @__PURE__ */ jsx("span", {
362
466
  className: "font-medium",
363
467
  children: role.name
364
- }), role.highImpact && /* @__PURE__ */ jsx(HighImpact, { t })] }),
468
+ }),
365
469
  meta,
366
470
  action: !actions.readOnly && /* @__PURE__ */ jsx(RemoveButton, {
367
- label: t.removeRole(groupLabel(group, t), role.name),
471
+ label: t.removeRole(principalLabel(principal, t), role.name),
368
472
  saving: actions.saving.has(key({
369
- groupId: group.id,
473
+ principalId: principal.id,
370
474
  roleId: role.id
371
475
  })),
372
476
  removal: {
373
- groupId: group.id,
477
+ principalId: principal.id,
374
478
  roleIds: [role.id],
375
479
  all: false
376
480
  },
@@ -379,4 +483,4 @@ function RoleRow({ model, group, role, actions, t }) {
379
483
  });
380
484
  }
381
485
  //#endregion
382
- export { ByGroup, ByRole, HighImpact, RemoveConfirm, removeKey };
486
+ export { ByPrincipal, ByRole, OWNER_SECTION, RemoveConfirm, principalLabel, removeKey };
@@ -1,24 +1,25 @@
1
1
  import { type ReactNode } from 'react';
2
- import { type DescribeGroup, type DetailsCache, type SearchGroups } from './GroupPicker.js';
3
2
  import { type AccessModel } from './model.js';
3
+ import { type DescribeGroup, type DetailsCache, type SearchPrincipals } from './PrincipalPicker.js';
4
4
  import type { AccessStrings } from './strings.js';
5
- import type { AccessChange, AccessGroup } from './types.js';
5
+ import type { AccessChange, AccessPrincipal } from './types.js';
6
6
  export interface GrantSelection {
7
- groupIds: string[];
7
+ principalIds: string[];
8
8
  roleIds: string[];
9
9
  }
10
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, }: {
11
+ export declare function useGrantView({ model, selection, onSelectionChange, resourceName, describeGroup, searchPrincipals, groupCount, peopleCount, recentPrincipalIds, cache, seen, saving, failure, onGrant, onCancel, focusLater, boundary, t, }: {
12
12
  model: AccessModel;
13
13
  selection: GrantSelection;
14
14
  onSelectionChange: (next: GrantSelection) => void;
15
15
  resourceName: string;
16
16
  describeGroup?: DescribeGroup | undefined;
17
- searchGroups?: SearchGroups | undefined;
17
+ searchPrincipals?: SearchPrincipals | undefined;
18
18
  groupCount?: number | undefined;
19
- recentGroupIds?: readonly string[] | undefined;
19
+ peopleCount?: number | undefined;
20
+ recentPrincipalIds?: readonly string[] | undefined;
20
21
  cache: DetailsCache;
21
- seen: Map<string, AccessGroup>;
22
+ seen: Map<string, AccessPrincipal>;
22
23
  saving: boolean;
23
24
  /** The failed grant's callout, which stays in the form so the choices can be retried. */
24
25
  failure: ReactNode;
@@ -26,7 +27,7 @@ export declare function useGrantView({ model, selection, onSelectionChange, reso
26
27
  onCancel: () => void;
27
28
  /** Sends focus somewhere once the change it goes with has rendered. */
28
29
  focusLater: (find: () => HTMLElement | null | undefined) => void;
29
- /** The drawer's body, above its footer, which the open group list fills. */
30
+ /** The drawer's body, above its footer, which the open list fills. */
30
31
  boundary: Element | null;
31
32
  t: AccessStrings;
32
33
  }): {