@parallelworks/ui 0.77.0 → 0.79.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 (268) hide show
  1. package/README.md +42 -6
  2. package/dist/access/AccessDrawer.d.ts +49 -7
  3. package/dist/access/AccessDrawer.js +243 -38
  4. package/dist/access/AccessList.d.ts +19 -1
  5. package/dist/access/AccessList.js +220 -9
  6. package/dist/access/GrantView.d.ts +3 -1
  7. package/dist/access/GrantView.js +17 -4
  8. package/dist/access/PrincipalPicker.js +5 -5
  9. package/dist/access/focus.js +1 -1
  10. package/dist/access/index.d.ts +2 -2
  11. package/dist/access/index.js +2 -2
  12. package/dist/access/strings.d.ts +25 -1
  13. package/dist/access/strings.js +27 -3
  14. package/dist/access/types.d.ts +26 -0
  15. package/dist/ai/components/AllocationSelector.js +1 -1
  16. package/dist/ai/components/AttachmentManager.js +5 -5
  17. package/dist/ai/components/AttachmentUpload.js +2 -2
  18. package/dist/ai/components/BranchNavigator.js +1 -1
  19. package/dist/ai/components/ChatActivity.js +1 -1
  20. package/dist/ai/components/ChatEmptyState.js +3 -3
  21. package/dist/ai/components/ChatInput.js +3 -5
  22. package/dist/ai/components/ChatLayout.js +1 -1
  23. package/dist/ai/components/ChatMessage.js +1 -1
  24. package/dist/ai/components/ChatMessageList.js +23 -8
  25. package/dist/ai/components/ChatSidebar.js +3 -3
  26. package/dist/ai/components/ChatThread.js +4 -4
  27. package/dist/ai/components/ComposerAddMenu.js +9 -20
  28. package/dist/ai/components/ModelSelector.js +3 -3
  29. package/dist/ai/components/PastedText.js +5 -5
  30. package/dist/ai/components/SessionView.js +1 -1
  31. package/dist/ai/components/ShareDialog.js +3 -3
  32. package/dist/ai/components/SlashMenu.d.ts +31 -4
  33. package/dist/ai/components/SlashMenu.js +36 -63
  34. package/dist/ai/components/agent/ActivityLine.js +1 -1
  35. package/dist/ai/components/agent/AgentMessageParts.js +13 -13
  36. package/dist/ai/components/conversationActions.js +3 -3
  37. package/dist/ai/core/ChatProvider.js +1 -1
  38. package/dist/ai/core/config.js +1 -1
  39. package/dist/ai/ui/ConversationSidebar.js +3 -3
  40. package/dist/ai/ui/sidebar.js +3 -3
  41. package/dist/assets/yaml.worker-DOBV2lND.js +884 -0
  42. package/dist/components/ActionMenu.d.ts +38 -9
  43. package/dist/components/ActionMenu.js +48 -22
  44. package/dist/components/Avatar.d.ts +66 -6
  45. package/dist/components/Avatar.js +130 -18
  46. package/dist/components/Breadcrumbs.d.ts +4 -0
  47. package/dist/components/Breadcrumbs.js +26 -10
  48. package/dist/components/Button.d.ts +3 -2
  49. package/dist/components/Button.js +11 -1
  50. package/dist/components/Callout.d.ts +3 -2
  51. package/dist/components/Callout.js +3 -2
  52. package/dist/components/Card.d.ts +33 -9
  53. package/dist/components/Card.js +24 -8
  54. package/dist/components/Checkbox.d.ts +3 -1
  55. package/dist/components/Checkbox.js +4 -2
  56. package/dist/components/ColorInput.js +1 -1
  57. package/dist/components/Combobox.d.ts +23 -0
  58. package/dist/components/Combobox.js +252 -86
  59. package/dist/components/ConfirmModal.d.ts +28 -2
  60. package/dist/components/ConfirmModal.js +100 -15
  61. package/dist/components/ContextMenu.d.ts +29 -2
  62. package/dist/components/ContextMenu.js +60 -8
  63. package/dist/components/CopyButton.d.ts +21 -8
  64. package/dist/components/CopyButton.js +29 -9
  65. package/dist/components/CreateModal.d.ts +20 -4
  66. package/dist/components/CreateModal.js +28 -17
  67. package/dist/components/DatePicker.d.ts +31 -1
  68. package/dist/components/DatePicker.js +178 -47
  69. package/dist/components/DescriptionList.d.ts +13 -3
  70. package/dist/components/DescriptionList.js +43 -11
  71. package/dist/components/Disclosure.d.ts +10 -2
  72. package/dist/components/Disclosure.js +18 -7
  73. package/dist/components/Drawer.js +5 -5
  74. package/dist/components/EmptyState.d.ts +12 -4
  75. package/dist/components/EmptyState.js +18 -10
  76. package/dist/components/ErrorPage.d.ts +40 -0
  77. package/dist/components/ErrorPage.js +101 -0
  78. package/dist/components/ExternalLink.d.ts +4 -1
  79. package/dist/components/ExternalLink.js +4 -5
  80. package/dist/components/Field.d.ts +21 -6
  81. package/dist/components/Field.js +32 -16
  82. package/dist/components/FileButton.d.ts +19 -0
  83. package/dist/components/FileButton.js +32 -0
  84. package/dist/components/HoverCard.d.ts +26 -6
  85. package/dist/components/HoverCard.js +11 -5
  86. package/dist/components/IconButton.js +4 -2
  87. package/dist/components/InfoTip.js +3 -3
  88. package/dist/components/Input.d.ts +33 -3
  89. package/dist/components/Input.js +149 -14
  90. package/dist/components/Kbd.d.ts +40 -0
  91. package/dist/components/Kbd.js +168 -0
  92. package/dist/components/Listbox.d.ts +103 -0
  93. package/dist/components/Listbox.js +190 -0
  94. package/dist/components/Loader.d.ts +8 -2
  95. package/dist/components/Loader.js +16 -1
  96. package/dist/components/Markdown.js +1 -1
  97. package/dist/components/MarkdownImpl.js +1 -1
  98. package/dist/components/Menu.d.ts +20 -2
  99. package/dist/components/Menu.js +48 -19
  100. package/dist/components/MenuEntries.d.ts +41 -4
  101. package/dist/components/MenuEntries.js +266 -39
  102. package/dist/components/Meter.d.ts +50 -11
  103. package/dist/components/Meter.js +71 -21
  104. package/dist/components/Modal.d.ts +9 -3
  105. package/dist/components/Modal.js +68 -29
  106. package/dist/components/OneTimeCodeInput.js +1 -1
  107. package/dist/components/PageHeader.js +111 -7
  108. package/dist/components/PageHeaderAction.d.ts +8 -3
  109. package/dist/components/PageHeaderAction.js +122 -34
  110. package/dist/components/Pagination.d.ts +13 -1
  111. package/dist/components/Pagination.js +90 -19
  112. package/dist/components/Popover.d.ts +16 -2
  113. package/dist/components/Popover.js +20 -21
  114. package/dist/components/Provider.d.ts +172 -5
  115. package/dist/components/Provider.js +157 -14
  116. package/dist/components/RefreshButton.d.ts +13 -6
  117. package/dist/components/RefreshButton.js +17 -9
  118. package/dist/components/RunStatusIcon.d.ts +16 -7
  119. package/dist/components/RunStatusIcon.js +21 -8
  120. package/dist/components/Section.d.ts +11 -4
  121. package/dist/components/Section.js +33 -22
  122. package/dist/components/SegmentedControl.d.ts +16 -3
  123. package/dist/components/SegmentedControl.js +7 -3
  124. package/dist/components/Select.d.ts +19 -3
  125. package/dist/components/Select.js +114 -68
  126. package/dist/components/SelectionCopy.d.ts +10 -0
  127. package/dist/components/SelectionCopy.js +105 -0
  128. package/dist/components/SettingModeOption.js +1 -1
  129. package/dist/components/SettingsCard.js +1 -1
  130. package/dist/components/Skeleton.js +1 -1
  131. package/dist/components/StatusBadge.d.ts +55 -10
  132. package/dist/components/StatusBadge.js +91 -15
  133. package/dist/components/StickyBar.js +1 -1
  134. package/dist/components/Table.d.ts +22 -4
  135. package/dist/components/Table.js +46 -24
  136. package/dist/components/Tabs.d.ts +9 -4
  137. package/dist/components/Tabs.js +9 -4
  138. package/dist/components/Tag.d.ts +49 -0
  139. package/dist/components/Tag.js +199 -0
  140. package/dist/components/ThemeEditor.js +1 -1
  141. package/dist/components/Toast.js +1 -1
  142. package/dist/components/ToggleGroup.d.ts +4 -1
  143. package/dist/components/ToggleGroup.js +30 -13
  144. package/dist/components/Tooltip.d.ts +9 -3
  145. package/dist/components/Tooltip.js +55 -39
  146. package/dist/components/buttonClasses.d.ts +5 -2
  147. package/dist/components/buttonClasses.js +11 -10
  148. package/dist/components/chart.d.ts +69 -0
  149. package/dist/components/chart.js +145 -0
  150. package/dist/components/comboboxItems.d.ts +7 -1
  151. package/dist/components/comboboxItems.js +6 -2
  152. package/dist/components/dismiss.d.ts +5 -1
  153. package/dist/components/dismiss.js +6 -2
  154. package/dist/components/floating.d.ts +4 -4
  155. package/dist/components/floating.js +5 -5
  156. package/dist/components/headerActionsFit.d.ts +37 -0
  157. package/dist/components/headerActionsFit.js +81 -0
  158. package/dist/components/pageHeaderStore.d.ts +3 -0
  159. package/dist/components/pageHeaderStore.js +3 -0
  160. package/dist/components/pathBreaks.js +2 -2
  161. package/dist/components/popupContainer.d.ts +17 -0
  162. package/dist/components/popupContainer.js +69 -0
  163. package/dist/components/relativeTime.d.ts +21 -10
  164. package/dist/components/relativeTime.js +50 -22
  165. package/dist/components/segmented.d.ts +18 -4
  166. package/dist/components/segmented.js +48 -6
  167. package/dist/components/typedDate.d.ts +5 -0
  168. package/dist/components/typedDate.js +50 -0
  169. package/dist/components/useClosingValue.d.ts +4 -0
  170. package/dist/components/useClosingValue.js +12 -0
  171. package/dist/components/useCopy.d.ts +6 -0
  172. package/dist/components/useCopy.js +3 -3
  173. package/dist/components/useNarrow.d.ts +2 -0
  174. package/dist/components/useNarrow.js +16 -0
  175. package/dist/editor/Editor.js +1 -1
  176. package/dist/editor/FormCodePanel.js +1 -1
  177. package/dist/editor/Monaco.js +1 -1
  178. package/dist/editor/workers.js +1 -1
  179. package/dist/file-explorer/FileExplorer.js +16 -15
  180. package/dist/file-explorer/FileExplorerContext.js +2 -2
  181. package/dist/file-explorer/FilePreview/Viewer.js +3 -3
  182. package/dist/file-explorer/FilePreview/index.js +2 -2
  183. package/dist/file-explorer/FileRowsWindow.js +3 -3
  184. package/dist/file-explorer/FileTable.js +1 -1
  185. package/dist/file-explorer/LoadMoreRow.js +1 -1
  186. package/dist/file-explorer/TreeView.js +5 -5
  187. package/dist/file-explorer/helpers.js +2 -2
  188. package/dist/file-explorer/toasts.js +2 -2
  189. package/dist/form/Fields/CheckboxGroupField.js +1 -1
  190. package/dist/form/Fields/DropdownField.js +1 -1
  191. package/dist/form/Fields/DurationField.js +2 -2
  192. package/dist/form/Fields/MultiDropdownField.js +1 -1
  193. package/dist/form/Fields/NumberField.js +1 -1
  194. package/dist/form/Fields/ObjectField.js +1 -1
  195. package/dist/form/Fields/RangeField.js +1 -1
  196. package/dist/form/Fields/StringField.js +1 -1
  197. package/dist/form/Form.js +3 -3
  198. package/dist/form/FormLayout.js +1 -1
  199. package/dist/form/Wizard/WizardContainer.js +1 -1
  200. package/dist/form/Wizard/WizardNavigation.js +1 -1
  201. package/dist/form/Wizard/WizardStepContent.js +1 -1
  202. package/dist/form/fieldRegistry.js +1 -1
  203. package/dist/form/formEditing.js +1 -1
  204. package/dist/format.d.ts +17 -7
  205. package/dist/format.js +39 -16
  206. package/dist/graph/AnnotationBanner.js +2 -2
  207. package/dist/graph/Connectors.js +5 -5
  208. package/dist/graph/DependencyGraph.js +10 -9
  209. package/dist/graph/GraphEditor.js +4 -4
  210. package/dist/graph/GraphEditorDialogs.js +13 -12
  211. package/dist/graph/GraphNode.js +1 -1
  212. package/dist/graph/InputDialog.js +5 -5
  213. package/dist/graph/InputsEditor.js +10 -9
  214. package/dist/graph/MatrixGroup.js +2 -2
  215. package/dist/graph/ProblemPageLink.d.ts +9 -0
  216. package/dist/graph/ProblemPageLink.js +21 -0
  217. package/dist/graph/ProblemsButton.js +26 -16
  218. package/dist/graph/Reveal.js +1 -1
  219. package/dist/graph/RunWarningsList.js +21 -12
  220. package/dist/graph/ShortcutsButton.js +25 -8
  221. package/dist/graph/StepSummaries.js +1 -1
  222. package/dist/graph/TreeView.js +2 -2
  223. package/dist/graph/WorkflowSettingsDialog.js +3 -3
  224. package/dist/graph/editorApi.d.ts +10 -0
  225. package/dist/graph/editorFields.js +3 -3
  226. package/dist/graph/editorPrimitives.js +1 -1
  227. package/dist/graph/editorRows.js +8 -6
  228. package/dist/graph/editorStrings.d.ts +1 -0
  229. package/dist/graph/editorStrings.js +1 -0
  230. package/dist/graph/fieldProblems.js +2 -2
  231. package/dist/graph/inputRefs.js +7 -6
  232. package/dist/graph/runWarnings.d.ts +12 -1
  233. package/dist/graph/stepWith.js +2 -2
  234. package/dist/index.d.ts +30 -21
  235. package/dist/index.js +20 -12
  236. package/dist/list/ActionToolbar.d.ts +4 -0
  237. package/dist/list/ActionToolbar.js +35 -12
  238. package/dist/list/ListFilterBar.d.ts +42 -0
  239. package/dist/list/ListFilterBar.js +789 -0
  240. package/dist/list/ListPage.d.ts +42 -4
  241. package/dist/list/ListPage.js +56 -22
  242. package/dist/list/ListTable.d.ts +27 -3
  243. package/dist/list/ListTable.js +48 -23
  244. package/dist/list/ListToolbar.d.ts +31 -0
  245. package/dist/list/ListToolbar.js +43 -0
  246. package/dist/list/ListViewControls.d.ts +8 -4
  247. package/dist/list/ListViewControls.js +22 -14
  248. package/dist/list/UserHoverCard.d.ts +8 -2
  249. package/dist/list/UserHoverCard.js +26 -13
  250. package/dist/list/filterModel.d.ts +122 -0
  251. package/dist/list/filterModel.js +308 -0
  252. package/dist/list/index.d.ts +6 -2
  253. package/dist/list/index.js +6 -2
  254. package/dist/list/listSearch.d.ts +1 -1
  255. package/dist/list/listSearch.js +8 -4
  256. package/dist/list/listView.d.ts +24 -2
  257. package/dist/list/listView.js +47 -45
  258. package/dist/list/rowMenu.d.ts +10 -5
  259. package/dist/list/rowMenu.js +17 -7
  260. package/dist/list/useListFilters.d.ts +68 -0
  261. package/dist/list/useListFilters.js +252 -0
  262. package/dist/logviewer/LogViewer.js +4 -4
  263. package/dist/styles/theme.css +94 -0
  264. package/dist/styles.css +1 -1
  265. package/package.json +11 -11
  266. package/dist/assets/yaml.worker-k9AdOqzf.js +0 -1441
  267. package/dist/components/fullscreen.d.ts +0 -4
  268. package/dist/components/fullscreen.js +0 -18
package/README.md CHANGED
@@ -24,7 +24,7 @@ Hosts bind strings, navigation, data hooks and the workflow engine through
24
24
  ```tsx
25
25
  <UIProvider
26
26
  strings={{ common: { save: 'Speichern' }, fileExplorer: { preview: { open: 'Öffnen' } } }}
27
- navigation={{ goTo: navigate }}
27
+ navigation={{ goTo: navigate, goBack: router.history.back, canGoBack: router.history.canGoBack }}
28
28
  data={{ useRunFile, usePersistedState }}
29
29
  slots={{ link: RouterLink }}
30
30
  engine={() => import('./engine').then((m) => m.engine)}
@@ -47,6 +47,33 @@ rule a select or scroll area would inject ships in `theme.css`. A host that
47
47
  renders on the server passes its policy's `nonce`, which the few components
48
48
  that emit a script to place themselves before hydration put on it.
49
49
 
50
+ Pass `locale`, the app's language as a BCP 47 tag such as `de-DE`, so relative
51
+ times, exact times, durations and the date picker read as the app's own text
52
+ does. Without it they follow the date library's default locale, or the browser's.
53
+ `useLocale()` reads it, for a host formatting with `formatDateTime`.
54
+
55
+ Pass `avatarColoring="kind"` for avatars that tell people from agents at a
56
+ glance: every person shares one tint and every agent another, with two initials
57
+ even for a one-word name. The host sets the tints with `--avatar-person`,
58
+ `--avatar-person-text`, `--avatar-agent`, `--avatar-agent-text` and the agent's
59
+ edge `--avatar-agent-ring`. An agent's avatar (`kind="agent"`) is a rounded
60
+ square under either coloring, so its shape and not only its color sets it apart.
61
+
62
+ A run's warnings and the workflow editor's problems may carry a problem `code`, `params` and
63
+ `type`, as a server's problem details do. Bind `data.useProblemMessage` to `useCodeMessage` from
64
+ `@parallelworks/problem/react`, and they read in the app's language: the message at
65
+ `apiErrors.<code>` in the app's use-intl catalog, or the shared one. It gets each one's `type` too:
66
+ for a type that names an application, such as `/problems/workflow/undefined_input`, the message at
67
+ `apiErrors.workflow.undefined_input` comes first. The app's own codes can stay at the top level of
68
+ `apiErrors`; another registry's go under its name, so a code both use keeps each one's message and
69
+ params. A code with no message, or no `useProblemMessage`, shows the `message` the server sent.
70
+
71
+ Menus, selects, pickers, popovers, tooltips and hover cards open over the page,
72
+ or over the fullscreen element while there is one. Opened inside a `<dialog>`
73
+ shown with `showModal()`, they open inside that dialog instead: the browser
74
+ draws a modal dialog above everything else and leaves the page behind it inert,
75
+ so a popup over the page would open under it, out of reach.
76
+
50
77
  ## Which component?
51
78
 
52
79
  Each need has one component. Reach for it before styling markup by hand.
@@ -59,6 +86,8 @@ Each need has one component. Reach for it before styling markup by hand.
59
86
  | A link out of the app | `ExternalLink` |
60
87
  | Work under way | `Loader`; `Skeleton` in a `LoadingRegion` for the shape of content still loading |
61
88
  | Nothing to show yet, or nothing matches | `EmptyState` |
89
+ | A page, card or section that couldn't show what it was asked for, such as a 404 | `ErrorPage`, its words chosen by the status |
90
+ | A list page's toolbar and its loading, error, empty and no-match states | `ListToolbar` in `ListPage` with `total`, `matches`, `empty` and `noMatches` (from `./list`) |
62
91
  | A text field | `Input`, `Textarea`, or a control framed by `Field`, whose `actions` hold buttons that act on it |
63
92
  | A number, a date, a color | `NumberInput`, `DatePicker`, `ColorInput` |
64
93
  | A one-time code, such as a sign-in check | `OneTimeCodeInput` |
@@ -92,11 +121,15 @@ Each need has one component. Reach for it before styling markup by hand.
92
121
  | How full something is, how far a task has come | `Meter`, `ProgressBar` |
93
122
  | Pages of a long list | `Pagination` |
94
123
  | A grouped page of settings | `SettingsCard` with `SettingRow`s |
95
- | A person | `Avatar`, colored by name without a picture |
124
+ | A person or an agent | `Avatar`, colored by name without a picture, or by kind; `AvatarGroup` for an overlapping stack with a "+N" chip |
96
125
  | A status in words | `StatusBadge`, or `StatusDot` beside text that says it |
126
+ | A thing's tags or labels | `Tag`; `TagList` for several, wrapped or on one line ending in "+N" |
127
+ | Keys to press | `Kbd`, with `Mod` for Command or Control; `ariaKeyShortcuts` for the control the keys press |
128
+ | A chart's colors, legend and tooltip | `seriesColors`, `ChartLegendList`, `ChartTooltipContent` and `useSeriesHighlight`, around a chart drawn by hand or by a library |
97
129
  | A run's, job's or step's status beside its name | `RunStatusIcon`; `StatusBadge` with `runStatusTone` to say it in words |
98
- | A copy action | `CopyButton`; `CopyCodeBlock` for a command or snippet; `useCopy` for a menu row or a control of your own |
99
- | A live relative time or duration | `RelativeTime`, `Elapsed` |
130
+ | A copy action | `CopyButton`; `CopyCodeBlock` for a command or snippet, with `secret` for a value shown once; `SelectionCopy` to copy part of a block by selecting it; `useCopy` for a menu row or a control of your own |
131
+ | A live relative time or duration | `RelativeTime` (with a `fallback`, an `exact` style for its tooltip, or `show="absolute"`), `Elapsed` |
132
+ | Fetching again on demand | `RefreshButton`; `variant="inline"` in a line of details; `loading` on an `ActionToolbar` action |
100
133
  | Rendered Markdown | `Markdown` |
101
134
  | Code or YAML to read or edit | `Editor` from `./editor`; `EditorField` as a form field, `FormCodePanel` for a form's values as JSON |
102
135
  | A run's jobs, steps, logs, annotations and warnings | `DependencyGraph` from `./graph`, as a graph or a list |
@@ -331,7 +364,8 @@ all at one lightness and chroma so none reads louder, and each
331
364
  `--theme-category-N-text` is solved to secondary text's targets on its fill. An
332
365
  `Avatar` without a picture takes one, picked by an FNV-1a hash of its name (or
333
366
  its `colorKey`), so the same name has the same color on every page; a nameless
334
- one keeps the accent. `categoryOf(key)` gives the same pick, 1–8, and
367
+ one keeps the accent. Under `avatarColoring="kind"` it takes its kind's tint
368
+ instead. `categoryOf(key)` gives the same pick, 1–8, and
335
369
  `categoryClasses(key)` its fill and text classes, for anything else drawn
336
370
  beside a name, such as a group's icon.
337
371
 
@@ -781,7 +815,9 @@ each linking to its job or step: the link opens the subworkflows, matrix and job
781
815
  it and what it is in into view below the graph's view bar, readable, and moves focus to it. Each job and step a warning is about, and each one it sits
782
816
  inside, carries a mark that lists the warnings on hover, so one skipped without a log is still
783
817
  flagged. A job name a matrix ran as several jobs marks each of them. `dag.warningCount` words the
784
- count.
818
+ count. A warning with a `code` reads as the host's `useProblemMessage` words it, and one whose `type`
819
+ names a page, such as `/problems/workflow/undefined_input`, links to that page in a new tab
820
+ (`dag.warningAbout`); the editor's problems list does the same (`problemAbout`).
785
821
 
786
822
  The editor checks YAML against the schemas a host registers with `configureEditorYaml` from
787
823
  `@parallelworks/ui/editor`. The job, step, input and settings dialogs edit their YAML at their own
@@ -2,11 +2,10 @@ import { type ReactNode } from 'react';
2
2
  import { type AccessModel } from './model.js';
3
3
  import type { SearchPrincipals } from './PrincipalPicker.js';
4
4
  import { type AccessStrings } from './strings.js';
5
- import type { AccessChange, AccessGrant, AccessGroupDetails, AccessInheritedGrant, AccessPermission, AccessPrincipal, AccessRole } from './types.js';
5
+ import type { AccessChange, AccessGrant, AccessGroupDetails, AccessInheritedGrant, AccessPermission, AccessPrincipal, AccessRole, AccessRule, AccessVisibility } from './types.js';
6
6
  export type AccessArrangement = 'role' | 'principal';
7
- export interface AccessDrawerProps {
8
- open: boolean;
9
- onClose: () => void;
7
+ /** What the drawer and the inline panel share: everything but where they sit. */
8
+ export interface AccessEditorProps {
10
9
  /** What access is to, as the reader knows it: "gpu-cluster-east". */
11
10
  resourceName: string;
12
11
  /** Every role that can be granted here, in the order to list them, and any held here only
@@ -48,16 +47,59 @@ export interface AccessDrawerProps {
48
47
  peopleCount?: number | undefined;
49
48
  /** People and groups the reader granted to lately, listed first in the picker. */
50
49
  recentPrincipalIds?: readonly string[] | undefined;
51
- /** How the list starts out: role by role, or person and group one by one. */
50
+ /** How the list starts out: role by role, or person and group one by one. Person by person
51
+ * where each holds one role, role by role otherwise. */
52
52
  defaultArrangement?: AccessArrangement | undefined;
53
+ /** A switch above the list between open to everyone who can see what the resource is in
54
+ * and only those listed. It saves as a grant does, with Undo in a toast. */
55
+ visibility?: AccessVisibility | undefined;
56
+ /** Reasons someone has access that no grant here changes, such as admins who always do:
57
+ * locked rows ahead of the grants. */
58
+ rules?: readonly AccessRule[] | undefined;
59
+ /** Each person and group holds one role here. Their row picks it from a list, a change
60
+ * swaps the old role for the new one in a single change, and the grant form picks one. */
61
+ singleRole?: boolean | undefined;
62
+ /** Why a save failed, in the reader's words, under "Couldn’t grant…": a fixed sentence, or
63
+ * one made from what `onChange` rejected with, such as a localized message for its error
64
+ * code. Without it, a generic sentence; the error's own message never shows, as a
65
+ * server's words aren't the reader's. */
66
+ errorMessage?: string | ((error: unknown) => string | undefined) | undefined;
67
+ /** Access here is set somewhere else, such as on what the resource is part of: the list
68
+ * shows, read-only, under a note that says where, with a link there. */
69
+ managedElsewhere?: AccessManagedElsewhere | undefined;
70
+ }
71
+ export interface AccessManagedElsewhere {
72
+ /** Where access is set and why: "This team follows the workspace’s access." Without it, a
73
+ * generic sentence. */
74
+ reason?: ReactNode;
75
+ /** Where to change it, through the host's router. */
76
+ href?: string | undefined;
77
+ /** The link's text, which says where it goes: "Workspace access". Without it, "Manage
78
+ * access". */
79
+ linkLabel?: string | undefined;
80
+ }
81
+ export interface AccessDrawerProps extends AccessEditorProps {
82
+ open: boolean;
83
+ onClose: () => void;
53
84
  /** As `Drawer`'s. */
54
85
  width?: number | string | undefined;
55
86
  }
87
+ export interface AccessPanelProps extends AccessEditorProps {
88
+ /** On the panel's frame. */
89
+ className?: string | undefined;
90
+ /** Under the list, inside the frame and ruled off from it: a note or actions about access
91
+ * as a whole. The grant form takes its place while it shows. */
92
+ footer?: ReactNode;
93
+ }
56
94
  /** Who has access to one resource, and the place to change it: roles granted to people and
57
95
  * groups. Overlapping roles are allowed and only redundant, which the drawer points out; taking
58
96
  * a role away first says exactly what is lost. The owner and roles granted above the resource
59
- * show, locked. Each change is saved as it is made, with Undo in a toast. */
60
- export declare function AccessDrawer({ open, onClose, resourceName, roles, principals, grants, owner, inherited, permissions, onChange, loading, readOnly, readOnlyReason, describeGroup, searchPrincipals, groupCount, peopleCount, recentPrincipalIds, defaultArrangement, width, }: AccessDrawerProps): import("react").JSX.Element;
97
+ * show, locked. Each change is saved as it is made, with Undo in a toast. `AccessPanel` is the
98
+ * same editor inline on a page. */
99
+ export declare function AccessDrawer({ open, onClose, width, ...props }: AccessDrawerProps): import("react").JSX.Element;
100
+ /** The access editor inline on a page, such as a settings section, rather than in a drawer: the
101
+ * list in a frame, which the grant form takes over with a back arrow in its own header. */
102
+ export declare function AccessPanel({ className, footer, ...props }: AccessPanelProps): import("react").JSX.Element;
61
103
  type Match = (query: string, ...texts: (string | undefined)[]) => boolean;
62
104
  export declare function filterAccess(model: AccessModel, arrangement: AccessArrangement, query: string, match: Match, t: AccessStrings): {
63
105
  shown: Map<string, readonly string[]>;
@@ -1,32 +1,37 @@
1
- import { AddIcon, LockIcon, UsersIcon } from "../icons.js";
2
- import { toast } from "../components/Toast.js";
1
+ import { cx } from "../cx.js";
3
2
  import { EmptyState } from "../components/EmptyState.js";
3
+ import { AddIcon, ArrowLeftIcon, LockIcon, UsersIcon } from "../icons.js";
4
+ import { useOpenMenu } from "../components/ContextMenu.js";
5
+ import { Loader } from "../components/Loader.js";
6
+ import { toast } from "../components/Toast.js";
4
7
  import { useQueryMatch } from "../components/search.js";
5
8
  import { LoadingRegion, Skeleton } from "../components/Skeleton.js";
6
9
  import { Button } from "../components/Button.js";
10
+ import { IconButton } from "../components/IconButton.js";
7
11
  import { Callout } from "../components/Callout.js";
12
+ import { dialogFooterClassName } from "../components/dismiss.js";
8
13
  import { Drawer } from "../components/Drawer.js";
14
+ import { Select } from "../components/Select.js";
9
15
  import { SearchInput } from "../components/SearchInput.js";
10
16
  import { SegmentedControl } from "../components/SegmentedControl.js";
11
17
  import { buildModel, invert, key, principalsWithAccess, warnAboutMisuse } from "./model.js";
12
- import { ByPrincipal, ByRole, OWNER_SECTION, principalLabel, removeKey } from "./AccessList.js";
18
+ import { ByPrincipal, ByPrincipalSingle, ByRole, OWNER_SECTION, RuleRow, principalLabel, removeKey } from "./AccessList.js";
13
19
  import { holdFocus, useFocusAfterRender } from "./focus.js";
14
20
  import { useGrantView } from "./GrantView.js";
15
21
  import { useAccessStrings } from "./strings.js";
16
- import { jsx, jsxs } from "react/jsx-runtime";
17
22
  import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
23
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
18
24
  //#region src/access/AccessDrawer.tsx
19
25
  var EMPTY_SELECTION = {
20
26
  principalIds: [],
21
27
  roleIds: []
22
28
  };
23
- /** Who has access to one resource, and the place to change it: roles granted to people and
24
- * groups. Overlapping roles are allowed and only redundant, which the drawer points out; taking
25
- * a role away first says exactly what is lost. The owner and roles granted above the resource
26
- * show, locked. Each change is saved as it is made, with Undo in a toast. */
27
- function AccessDrawer({ open, onClose, resourceName, roles, principals, grants, owner, inherited, permissions, onChange, loading = false, readOnly = false, readOnlyReason, describeGroup, searchPrincipals, groupCount, peopleCount, recentPrincipalIds, defaultArrangement = "role", width = 560 }) {
29
+ /** The list and the grant form, and the saving, failing and undoing they share, for the drawer
30
+ * and the inline panel to lay out. */
31
+ function useAccessEditor({ resourceName, roles, principals, grants, owner, inherited, permissions, onChange, loading = false, readOnly = false, readOnlyReason, describeGroup, searchPrincipals, groupCount, peopleCount, recentPrincipalIds, singleRole = false, defaultArrangement = singleRole ? "principal" : "role", visibility, rules, errorMessage, managedElsewhere }, inline) {
28
32
  const t = useAccessStrings();
29
33
  const uid = useId();
34
+ const openMenu = useOpenMenu();
30
35
  const model = useMemo(() => buildModel({
31
36
  roles,
32
37
  principals,
@@ -71,6 +76,8 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
71
76
  const [removal, setRemoval] = useState();
72
77
  const [saving, setSaving] = useState();
73
78
  const [failure, setFailure] = useState();
79
+ const [visibilitySaving, setVisibilitySaving] = useState(false);
80
+ const [visibilityFailure, setVisibilityFailure] = useState();
74
81
  const cache = useRef(/* @__PURE__ */ new Map()).current;
75
82
  const seen = useRef(/* @__PURE__ */ new Map()).current;
76
83
  const body = useRef(null);
@@ -82,9 +89,12 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
82
89
  const focusLater = useFocusAfterRender();
83
90
  const change = useRef(onChange);
84
91
  change.current = onChange;
92
+ const visibilityChange = useRef(visibility?.onChange);
93
+ visibilityChange.current = visibility?.onChange;
85
94
  const match = useQueryMatch();
86
95
  const filtered = useFiltered(model, arrangement, query.trim(), match, t);
87
- const editable = !readOnly && onChange !== void 0;
96
+ const editable = !readOnly && !managedElsewhere && onChange !== void 0;
97
+ const messageOf = (error) => (typeof errorMessage === "function" ? errorMessage(error) : errorMessage) || t.failedReason;
88
98
  const sectionId = (id) => `${uid}${arrangement}${id}`;
89
99
  const names = (changed) => {
90
100
  const all = [...changed.grant, ...changed.revoke];
@@ -99,10 +109,14 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
99
109
  const undo = (done) => {
100
110
  Promise.resolve(change.current?.(invert(done))).then(() => toast.info(t.undone), (error) => toast.error(t.nothingChanged(messageOf(error)), { title: t.undoFailed }));
101
111
  };
112
+ /** One role swapped for another, as a row's role picker sends it. */
113
+ const swapped = (next) => next.grant.length === 1 && next.revoke.length > 0 && next.revoke.every((revoked) => revoked.principalId === next.grant[0]?.principalId);
102
114
  /** Saves a change and reports how it went; resolves true once saved. */
103
115
  const save = async (next, where) => {
104
116
  if (!onChange || next.grant.length === 0 && next.revoke.length === 0) return false;
105
117
  const { principalNames, roleNames } = names(next);
118
+ const swap = swapped(next);
119
+ const newRole = model.roleById.get(next.grant[0]?.roleId ?? "")?.name ?? "";
106
120
  setSaving(next);
107
121
  setFailure(void 0);
108
122
  try {
@@ -110,7 +124,7 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
110
124
  } catch (error) {
111
125
  setFailure({
112
126
  change: next,
113
- title: next.grant.length > 0 ? t.grantFailed(principalNames, roleNames) : t.removeFailed(principalNames[0] ?? "", roleNames),
127
+ title: swap ? t.roleChangeFailed(principalNames[0] ?? "", newRole) : next.grant.length > 0 ? t.grantFailed(principalNames, roleNames) : t.removeFailed(principalNames[0] ?? "", roleNames),
114
128
  message: messageOf(error),
115
129
  ...where
116
130
  });
@@ -118,12 +132,36 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
118
132
  } finally {
119
133
  setSaving(void 0);
120
134
  }
121
- toast.success(next.grant.length > 0 ? t.granted(principalNames, roleNames) : t.removed(principalNames, roleNames), { action: {
135
+ toast.success(swap ? t.roleChanged(principalNames[0] ?? "", newRole) : next.grant.length > 0 ? t.granted(principalNames, roleNames) : t.removed(principalNames, roleNames), { action: {
122
136
  label: t.common.undo,
123
137
  onClick: () => undo(next)
124
138
  } });
125
139
  return true;
126
140
  };
141
+ const openLabel = visibility?.openLabel ?? "";
142
+ const restrictedLabel = visibility?.restrictedLabel ?? t.restrictedLabel;
143
+ const choiceLabel = (restricted) => restricted ? restrictedLabel : openLabel;
144
+ const saveVisibility = async (restricted) => {
145
+ const save = visibilityChange.current;
146
+ if (!save) return;
147
+ setVisibilitySaving(true);
148
+ setVisibilityFailure(void 0);
149
+ try {
150
+ await save(restricted);
151
+ } catch (error) {
152
+ setVisibilityFailure({
153
+ restricted,
154
+ message: messageOf(error)
155
+ });
156
+ return;
157
+ } finally {
158
+ setVisibilitySaving(false);
159
+ }
160
+ toast.success(t.visibilityChanged(choiceLabel(restricted)), { action: {
161
+ label: t.common.undo,
162
+ onClick: () => Promise.resolve(visibilityChange.current?.(!restricted)).then(() => toast.info(t.undone), (error) => toast.error(t.nothingChanged(messageOf(error)), { title: t.undoFailed }))
163
+ } });
164
+ };
127
165
  const toList = () => {
128
166
  setView("list");
129
167
  setFailure((current) => current?.inGrant ? void 0 : current);
@@ -167,6 +205,23 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
167
205
  setRemoval(void 0);
168
206
  focusLater(() => document.getElementById(sectionId(anchor ?? pending.principalId)) ?? body.current?.querySelector("input[type=\"search\"]"));
169
207
  };
208
+ const changeRole = (principalId, roleId) => {
209
+ const held = (model.held.get(principalId) ?? []).filter((role) => role.id !== roleId);
210
+ const has = (model.held.get(principalId) ?? []).some((role) => role.id === roleId);
211
+ save({
212
+ grant: has ? [] : [{
213
+ principalId,
214
+ roleId
215
+ }],
216
+ revoke: held.map((role) => ({
217
+ principalId,
218
+ roleId: role.id
219
+ }))
220
+ }, {
221
+ inGrant: false,
222
+ principalId
223
+ });
224
+ };
170
225
  const retry = (failed) => failed.inGrant ? grant(failed.change) : save(failed.change, {
171
226
  inGrant: false,
172
227
  principalId: failed.principalId,
@@ -185,11 +240,10 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
185
240
  onDismiss: () => setFailure(void 0),
186
241
  children: t.nothingChanged(failure.message)
187
242
  });
188
- const close = () => {
243
+ const reset = () => {
189
244
  setView("list");
190
245
  setRemoval(void 0);
191
246
  setQuery("");
192
- onClose();
193
247
  };
194
248
  const grantView = useGrantView({
195
249
  model,
@@ -209,12 +263,15 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
209
263
  onCancel: toList,
210
264
  focusLater,
211
265
  boundary: bodyElement,
266
+ singleRole,
212
267
  t
213
268
  });
214
269
  const savingKeys = useMemo(() => new Set(saving?.revoke.map(key) ?? []), [saving]);
215
270
  const withAccess = principalsWithAccess(model);
216
271
  const empty = !loading && withAccess.length === 0;
217
- const View = arrangement === "role" ? ByRole : ByPrincipal;
272
+ const View = arrangement === "role" ? ByRole : singleRole ? ByPrincipalSingle : ByPrincipal;
273
+ const needle = query.trim();
274
+ const shownRules = (rules ?? []).filter((rule) => !needle || match(needle, rule.title, rule.detail));
218
275
  const grantButton = (className) => /* @__PURE__ */ jsx(Button, {
219
276
  variant: "primary",
220
277
  icon: /* @__PURE__ */ jsx(AddIcon, {}),
@@ -224,10 +281,92 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
224
281
  className,
225
282
  children: t.grantAccess
226
283
  });
284
+ const visibilityValue = visibility?.restricted ? "restricted" : "open";
285
+ const visibilityOptions = [{
286
+ value: "open",
287
+ label: openLabel
288
+ }, {
289
+ value: "restricted",
290
+ label: restrictedLabel
291
+ }];
292
+ const visibilityRow = visibility && /* @__PURE__ */ jsxs("div", {
293
+ className: "flex flex-col gap-3 border-b border-(--theme-border) px-4 py-3",
294
+ children: [/* @__PURE__ */ jsxs("div", {
295
+ className: "flex items-center gap-2",
296
+ children: [
297
+ /* @__PURE__ */ jsx("div", {
298
+ className: "hidden @min-[30rem]/access:block",
299
+ children: /* @__PURE__ */ jsx(SegmentedControl, {
300
+ label: t.visibility,
301
+ value: visibilityValue,
302
+ disabled: !editable || visibilitySaving,
303
+ onValueChange: (next) => {
304
+ saveVisibility(next === "restricted");
305
+ },
306
+ options: visibilityOptions
307
+ })
308
+ }),
309
+ /* @__PURE__ */ jsx(Select, {
310
+ "aria-label": t.visibility,
311
+ items: visibilityOptions,
312
+ value: visibilityValue,
313
+ disabled: !editable || visibilitySaving,
314
+ onValueChange: (next) => {
315
+ if (next !== null && next !== visibilityValue) saveVisibility(next === "restricted");
316
+ },
317
+ className: "min-w-0 flex-1 @min-[30rem]/access:hidden"
318
+ }),
319
+ visibilitySaving && /* @__PURE__ */ jsx(Loader, {
320
+ inline: true,
321
+ size: "1em",
322
+ label: t.common.loading
323
+ })
324
+ ]
325
+ }), visibilityFailure && /* @__PURE__ */ jsx(Callout, {
326
+ tone: "error",
327
+ live: true,
328
+ title: t.visibilityFailed(choiceLabel(visibilityFailure.restricted)),
329
+ action: /* @__PURE__ */ jsx(Button, {
330
+ size: "sm",
331
+ variant: "secondary",
332
+ onClick: () => saveVisibility(visibilityFailure.restricted),
333
+ children: t.common.retry
334
+ }),
335
+ onDismiss: () => setVisibilityFailure(void 0),
336
+ children: t.nothingChanged(visibilityFailure.message)
337
+ })]
338
+ });
339
+ const ruleRows = shownRules.map((rule) => /* @__PURE__ */ jsx(RuleRow, {
340
+ lead: arrangement === "role" ? "chevron" : singleRole ? "tile" : "both",
341
+ rule,
342
+ roleName: rule.roleId ? model.roleById.get(rule.roleId)?.name : void 0,
343
+ t
344
+ }, rule.id));
227
345
  const list = /* @__PURE__ */ jsxs("div", {
228
346
  className: "@container/access flex min-h-0 flex-1 flex-col",
229
347
  children: [
230
- readOnly && /* @__PURE__ */ jsx("div", {
348
+ managedElsewhere ? /* @__PURE__ */ jsx("div", {
349
+ className: cx("px-4 pt-3", empty && !visibility && "pb-3"),
350
+ children: /* @__PURE__ */ jsx(Callout, {
351
+ tone: "info",
352
+ icon: /* @__PURE__ */ jsx(LockIcon, {}),
353
+ children: /* @__PURE__ */ jsxs("div", {
354
+ className: "flex flex-col items-start gap-2 @min-[30rem]/access:flex-row @min-[30rem]/access:gap-3",
355
+ children: [/* @__PURE__ */ jsx("div", {
356
+ className: "min-w-0 flex-1",
357
+ children: managedElsewhere.reason ?? t.managedElsewhere
358
+ }), managedElsewhere.href !== void 0 && /* @__PURE__ */ jsx("div", {
359
+ className: "flex shrink-0 items-center @min-[30rem]/access:h-5",
360
+ children: /* @__PURE__ */ jsx(Button, {
361
+ size: "sm",
362
+ variant: "secondary",
363
+ href: managedElsewhere.href,
364
+ children: managedElsewhere.linkLabel ?? t.manageAccess
365
+ })
366
+ })]
367
+ })
368
+ })
369
+ }) : readOnly && /* @__PURE__ */ jsx("div", {
231
370
  className: "px-4 pt-3",
232
371
  children: /* @__PURE__ */ jsxs(Callout, {
233
372
  tone: "info",
@@ -239,10 +378,11 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
239
378
  ]
240
379
  })
241
380
  }),
381
+ visibilityRow,
242
382
  !empty && /* @__PURE__ */ jsxs("div", {
243
383
  className: "flex flex-wrap items-center gap-2 border-b border-(--theme-border) px-4 py-3",
244
384
  children: [
245
- /* @__PURE__ */ jsx("div", {
385
+ !singleRole && /* @__PURE__ */ jsx("div", {
246
386
  className: "w-full @min-[30rem]/access:w-auto",
247
387
  children: /* @__PURE__ */ jsx(SegmentedControl, {
248
388
  fill: true,
@@ -258,7 +398,7 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
258
398
  }]
259
399
  })
260
400
  }),
261
- editable && grantButton("ms-auto hidden @min-[30rem]/access:inline-flex"),
401
+ editable && grantButton(singleRole ? "order-last hidden @min-[30rem]/access:inline-flex" : "ms-auto hidden @min-[30rem]/access:inline-flex"),
262
402
  /* @__PURE__ */ jsx(SearchInput, {
263
403
  label: t.filter,
264
404
  value: query,
@@ -266,7 +406,7 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
266
406
  setQuery(next);
267
407
  setExpanded(expandedFor(model, next.trim(), match, t));
268
408
  },
269
- className: "min-w-0 basis-full"
409
+ className: singleRole ? "min-w-0 flex-1" : "min-w-0 basis-full"
270
410
  })
271
411
  ]
272
412
  }),
@@ -275,18 +415,18 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
275
415
  children: failureNode
276
416
  }),
277
417
  /* @__PURE__ */ jsx("div", {
278
- className: "min-h-0 flex-1 overflow-y-auto",
418
+ className: cx("min-h-0 flex-1", inline ? "[&>:last-child]:border-b-0" : "overflow-y-auto"),
279
419
  children: loading ? /* @__PURE__ */ jsx(LoadingRegion, {
280
420
  label: t.loading,
281
421
  children: /* @__PURE__ */ jsx(RowsSkeleton, {})
282
- }) : empty ? /* @__PURE__ */ jsx(EmptyState, {
422
+ }) : empty && managedElsewhere ? ruleRows : empty ? /* @__PURE__ */ jsxs(Fragment$1, { children: [ruleRows, /* @__PURE__ */ jsx(EmptyState, {
283
423
  variant: "bare",
284
424
  icon: /* @__PURE__ */ jsx(UsersIcon, {}),
285
425
  title: t.emptyTitle,
286
426
  description: t.emptyDescription(resourceName),
287
427
  action: editable && grantButton(""),
288
- className: "py-16"
289
- }) : filtered.order.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
428
+ className: ruleRows.length > 0 ? "py-8" : "py-16"
429
+ })] }) : filtered.order.length === 0 && shownRules.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
290
430
  variant: "bare",
291
431
  title: t.noMatches,
292
432
  action: /* @__PURE__ */ jsx(Button, {
@@ -295,7 +435,7 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
295
435
  onClick: () => setQuery(""),
296
436
  children: t.clearFilter
297
437
  })
298
- }) : /* @__PURE__ */ jsx(View, {
438
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [ruleRows, /* @__PURE__ */ jsx(View, {
299
439
  model,
300
440
  shown: filtered.shown,
301
441
  order: filtered.order,
@@ -309,13 +449,15 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
309
449
  [arrangement]: next
310
450
  };
311
451
  }),
312
- filtering: query.trim() !== "",
452
+ filtering: needle !== "",
313
453
  sectionId,
314
454
  t,
315
455
  actions: {
316
456
  readOnly: !editable,
317
457
  saving: savingKeys,
318
458
  removal,
459
+ singleRole,
460
+ openMenu,
319
461
  onAskRemove: (next) => {
320
462
  holdFocus();
321
463
  setFailure(void 0);
@@ -328,13 +470,14 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
328
470
  },
329
471
  onRemove: remove,
330
472
  onGrantMore: (principalId) => toGrant([principalId]),
473
+ onChangeRole: changeRole,
331
474
  failure: failure && !failure.inGrant ? {
332
475
  node: failureNode,
333
476
  principalId: failure.principalId,
334
477
  roleId: failure.roleId
335
478
  } : void 0
336
479
  }
337
- })
480
+ })] })
338
481
  }),
339
482
  editable && !empty && /* @__PURE__ */ jsx("div", {
340
483
  className: "border-t border-(--theme-border) p-4 @min-[30rem]/access:hidden",
@@ -343,32 +486,94 @@ function AccessDrawer({ open, onClose, resourceName, roles, principals, grants,
343
486
  ]
344
487
  });
345
488
  const granting = view === "grant";
346
- return /* @__PURE__ */ jsx(Drawer, {
347
- open,
348
- onClose: close,
349
- width,
350
- padded: false,
489
+ return {
490
+ granting,
351
491
  title: granting ? t.grantAccess : t.title,
352
- description: granting ? resourceName : loading ? resourceName : t.summary(resourceName, withAccess.filter((principal) => principal.kind === "user").length, withAccess.filter((principal) => principal.kind === "group").length, withAccess.some((principal) => principal.kind === "everyone")),
353
- onBack: granting ? toList : void 0,
354
- backLabel: t.back,
492
+ description: granting || loading ? resourceName : t.summary(resourceName, withAccess.filter((principal) => principal.kind === "user").length, withAccess.filter((principal) => principal.kind === "group").length, withAccess.some((principal) => principal.kind === "everyone")),
493
+ toList,
494
+ reset,
355
495
  footer: granting ? grantView.footer : void 0,
356
496
  onKeyDown: (e) => {
357
497
  const target = e.target;
358
498
  if (granting && e.key === "Escape" && !e.defaultPrevented && target.getAttribute("aria-expanded") !== "true") {
359
499
  e.stopPropagation();
360
- e.preventBaseUIHandler();
500
+ e.preventBaseUIHandler?.();
361
501
  toList();
362
502
  }
363
503
  },
364
- children: /* @__PURE__ */ jsx("div", {
504
+ body: /* @__PURE__ */ jsx("div", {
365
505
  ref: bodyRef,
366
506
  className: "flex min-h-0 flex-1 flex-col",
367
507
  children: granting ? grantView.body : list
368
508
  })
509
+ };
510
+ }
511
+ /** Who has access to one resource, and the place to change it: roles granted to people and
512
+ * groups. Overlapping roles are allowed and only redundant, which the drawer points out; taking
513
+ * a role away first says exactly what is lost. The owner and roles granted above the resource
514
+ * show, locked. Each change is saved as it is made, with Undo in a toast. `AccessPanel` is the
515
+ * same editor inline on a page. */
516
+ function AccessDrawer({ open, onClose, width = 560, ...props }) {
517
+ const t = useAccessStrings();
518
+ const editor = useAccessEditor(props, false);
519
+ return /* @__PURE__ */ jsx(Drawer, {
520
+ open,
521
+ onClose: () => {
522
+ editor.reset();
523
+ onClose();
524
+ },
525
+ width,
526
+ padded: false,
527
+ title: editor.title,
528
+ description: editor.description,
529
+ onBack: editor.granting ? editor.toList : void 0,
530
+ backLabel: t.back,
531
+ footer: editor.footer,
532
+ onKeyDown: editor.onKeyDown,
533
+ children: editor.body
534
+ });
535
+ }
536
+ /** The access editor inline on a page, such as a settings section, rather than in a drawer: the
537
+ * list in a frame, which the grant form takes over with a back arrow in its own header. */
538
+ function AccessPanel({ className, footer, ...props }) {
539
+ const t = useAccessStrings();
540
+ const editor = useAccessEditor(props, true);
541
+ return /* @__PURE__ */ jsxs("section", {
542
+ "data-access-panel": "",
543
+ tabIndex: -1,
544
+ "aria-label": editor.granting ? t.grantAccess : t.title,
545
+ onKeyDown: editor.onKeyDown,
546
+ className: cx("flex min-w-0 flex-col overflow-hidden rounded-xl border border-(--theme-border) bg-(--theme-panel-bg) text-(--theme-app) outline-none", className),
547
+ children: [
548
+ editor.granting && /* @__PURE__ */ jsxs("div", {
549
+ className: "flex shrink-0 items-start gap-3 border-b border-(--theme-border) px-4 py-3",
550
+ children: [/* @__PURE__ */ jsx(IconButton, {
551
+ icon: /* @__PURE__ */ jsx(ArrowLeftIcon, {}),
552
+ label: t.back,
553
+ onClick: editor.toList,
554
+ className: "-my-1 -ms-1.5"
555
+ }), /* @__PURE__ */ jsxs("div", {
556
+ className: "min-w-0 flex-1",
557
+ children: [/* @__PURE__ */ jsx("h3", {
558
+ className: "m-0 text-sm font-semibold wrap-break-word",
559
+ children: editor.title
560
+ }), /* @__PURE__ */ jsx("p", {
561
+ className: "m-0 mt-0.5 text-xs text-(--theme-muted-text-color)",
562
+ children: editor.description
563
+ })]
564
+ })]
565
+ }),
566
+ editor.body,
567
+ editor.footer ? /* @__PURE__ */ jsx("div", {
568
+ className: dialogFooterClassName,
569
+ children: editor.footer
570
+ }) : footer !== void 0 && footer !== null && footer !== false && /* @__PURE__ */ jsx("div", {
571
+ className: "border-t border-(--theme-border) px-4 py-3 text-sm text-(--theme-muted-text-color)",
572
+ children: footer
573
+ })
574
+ ]
369
575
  });
370
576
  }
371
- var messageOf = (error) => error instanceof Error && error.message ? error.message : void 0;
372
577
  var anchorOf = (failure, arrangement) => arrangement === "role" ? failure.roleId ?? "" : failure.principalId;
373
578
  /** The one of `elements` that is laid out, such as the one of two buttons a width shows, or the
374
579
  * first where nothing is (as in tests). */
@@ -467,4 +672,4 @@ function RowsSkeleton() {
467
672
  }, row));
468
673
  }
469
674
  //#endregion
470
- export { AccessDrawer, filterAccess };
675
+ export { AccessDrawer, AccessPanel, filterAccess };