@stigmer/react 3.0.6 → 3.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.js +2 -9
- package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
- package/agent-instance/AgentInstanceList.d.ts.map +1 -1
- package/agent-instance/AgentInstanceList.js +2 -9
- package/agent-instance/AgentInstanceList.js.map +1 -1
- package/agent-instance/CreateAgentInstanceDialog.d.ts.map +1 -1
- package/agent-instance/CreateAgentInstanceDialog.js +1 -1
- package/agent-instance/CreateAgentInstanceDialog.js.map +1 -1
- package/composer/SessionComposer.d.ts +14 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +15 -9
- package/composer/SessionComposer.js.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js.map +1 -1
- package/library/InstanceVisibilitySelector.d.ts +23 -9
- package/library/InstanceVisibilitySelector.d.ts.map +1 -1
- package/library/InstanceVisibilitySelector.js +14 -9
- package/library/InstanceVisibilitySelector.js.map +1 -1
- package/library/VisibilityOptionRow.d.ts +52 -0
- package/library/VisibilityOptionRow.d.ts.map +1 -0
- package/library/VisibilityOptionRow.js +92 -0
- package/library/VisibilityOptionRow.js.map +1 -0
- package/library/VisibilitySelector.d.ts +47 -24
- package/library/VisibilitySelector.d.ts.map +1 -1
- package/library/VisibilitySelector.js +137 -115
- package/library/VisibilitySelector.js.map +1 -1
- package/library/visibilityLevels.d.ts +25 -3
- package/library/visibilityLevels.d.ts.map +1 -1
- package/library/visibilityLevels.js +8 -2
- package/library/visibilityLevels.js.map +1 -1
- package/package.json +4 -4
- package/session/NewSessionViewer.d.ts +32 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +20 -9
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +24 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +18 -12
- package/session/SessionViewer.js.map +1 -1
- package/session/audience.d.ts +21 -0
- package/session/audience.d.ts.map +1 -0
- package/session/audience.js +2 -0
- package/session/audience.js.map +1 -0
- package/session/index.d.ts +2 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js.map +1 -1
- package/session/runtime-env.d.ts +47 -0
- package/session/runtime-env.d.ts.map +1 -0
- package/session/runtime-env.js +20 -0
- package/session/runtime-env.js.map +1 -0
- package/session/useNewSessionFlow.d.ts +25 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +20 -8
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +27 -2
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +34 -13
- package/session/useSessionPageFlow.js.map +1 -1
- package/src/agent-instance/AgentInstanceDetailPanel.tsx +7 -27
- package/src/agent-instance/AgentInstanceList.tsx +7 -27
- package/src/agent-instance/CreateAgentInstanceDialog.tsx +1 -0
- package/src/composer/SessionComposer.tsx +30 -8
- package/src/composer/__tests__/SessionComposer-lockAgent.test.tsx +150 -0
- package/src/index.ts +2 -0
- package/src/library/InstanceVisibilitySelector.tsx +27 -9
- package/src/library/VisibilityOptionRow.tsx +244 -0
- package/src/library/VisibilitySelector.tsx +303 -260
- package/src/library/__tests__/VisibilitySelector.test.tsx +256 -0
- package/src/library/visibilityLevels.ts +35 -5
- package/src/session/NewSessionViewer.tsx +61 -12
- package/src/session/SessionViewer.tsx +51 -15
- package/src/session/__tests__/audienceWiring.test.tsx +274 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +122 -0
- package/src/session/__tests__/useSessionPageFlow.runtimeEnv.test.tsx +170 -0
- package/src/session/audience.ts +20 -0
- package/src/session/index.ts +3 -0
- package/src/session/runtime-env.ts +57 -0
- package/src/session/useNewSessionFlow.ts +44 -9
- package/src/session/useSessionPageFlow.ts +65 -17
- package/src/workflow/instance/CreateWorkflowInstanceDialog.tsx +1 -0
- package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +7 -27
- package/src/workflow/instance/WorkflowInstanceList.tsx +7 -27
- package/styles.css +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.d.ts.map +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.js +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.js.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.js +2 -9
- package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
- package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceList.js +2 -9
- package/workflow/instance/WorkflowInstanceList.js.map +1 -1
|
@@ -1,54 +1,94 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Popover } from "@base-ui/react/popover";
|
|
4
5
|
import { cn } from "@stigmer/theme";
|
|
5
6
|
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
7
|
+
import { useStigmerPortalContainer } from "../portal-container";
|
|
8
|
+
import { ConfirmDialog } from "../resource-detail/ConfirmDialog";
|
|
9
|
+
import { useConfirmAction } from "../resource-detail/useConfirmAction";
|
|
10
|
+
import {
|
|
11
|
+
PROMPT_STYLES,
|
|
12
|
+
VISIBILITY_CHIP_CLASS,
|
|
13
|
+
VisibilityIcon,
|
|
14
|
+
VisibilityOptionRow,
|
|
15
|
+
} from "./VisibilityOptionRow";
|
|
6
16
|
import {
|
|
7
17
|
visibilityOption,
|
|
8
18
|
type VisibilityLevelOption,
|
|
9
19
|
} from "./visibilityLevels";
|
|
10
20
|
|
|
21
|
+
/** How the selector presents itself and how it confirms escalations. */
|
|
22
|
+
export type VisibilitySelectorMode = "manage" | "create";
|
|
23
|
+
|
|
11
24
|
/** Props for {@link VisibilitySelector}. */
|
|
12
25
|
export interface VisibilitySelectorProps {
|
|
13
26
|
/** Current visibility of the resource. */
|
|
14
27
|
readonly visibility: ApiResourceVisibility;
|
|
15
28
|
/**
|
|
16
|
-
* Levels to offer, in escalation order (
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
29
|
+
* Levels to offer, in escalation order (least to most exposed). Selecting
|
|
30
|
+
* a level later in the list than the current one is an escalation and is
|
|
31
|
+
* confirmed by severity (see {@link mode}); de-escalation applies
|
|
32
|
+
* immediately (revoking access is always safe).
|
|
20
33
|
*/
|
|
21
34
|
readonly options: readonly VisibilityLevelOption[];
|
|
22
35
|
/** Called when the user selects (and, for escalations, confirms) a level. */
|
|
23
36
|
readonly onVisibilityChange: (v: ApiResourceVisibility) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Presentation + confirmation mode.
|
|
39
|
+
*
|
|
40
|
+
* - `"manage"` (default) — a compact current-state chip opens a popover
|
|
41
|
+
* ladder of levels. Escalations are confirmed by severity: a light
|
|
42
|
+
* inline prompt for levels carrying {@link VisibilityLevelOption.confirmPrompt}
|
|
43
|
+
* (e.g. Organization), and a blocking modal for levels carrying
|
|
44
|
+
* {@link VisibilityLevelOption.confirmDialog} (Platform, Public). This is
|
|
45
|
+
* the live-resource case.
|
|
46
|
+
* - `"create"` — an inline radio list that applies immediately, with no
|
|
47
|
+
* escalation confirmation. Used to pick an initial value while creating
|
|
48
|
+
* a resource (typically inside a native `<dialog>`, where a portaled
|
|
49
|
+
* popover would render beneath the modal's top layer).
|
|
50
|
+
*
|
|
51
|
+
* @default "manage"
|
|
52
|
+
*/
|
|
53
|
+
readonly mode?: VisibilitySelectorMode;
|
|
24
54
|
/** Shows a spinner/disabled state while the RPC is in flight. */
|
|
25
55
|
readonly isPending?: boolean;
|
|
26
56
|
/** Disables all interaction (e.g., when the user lacks can_edit). */
|
|
27
57
|
readonly disabled?: boolean;
|
|
28
|
-
/** Accessible name for the
|
|
58
|
+
/** Accessible name for the control. Defaults to "Resource visibility". */
|
|
29
59
|
readonly ariaLabel?: string;
|
|
30
60
|
/** Additional CSS classes applied to the root element. */
|
|
31
61
|
readonly className?: string;
|
|
32
62
|
}
|
|
33
63
|
|
|
34
64
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
65
|
+
* The single control for resource visibility across blueprints AND
|
|
66
|
+
* instances. Offered levels are pure data ({@link VisibilityLevelOption});
|
|
67
|
+
* per-kind level sets live in `visibilityLevels.ts`, so this component
|
|
68
|
+
* carries no kind-specific logic.
|
|
69
|
+
*
|
|
70
|
+
* In `"manage"` mode it renders a current-state chip (icon + label + caret)
|
|
71
|
+
* that opens a popover listing each offered level with its own description —
|
|
72
|
+
* scaling cleanly to four levels without the layout shift of a segmented
|
|
73
|
+
* control, and explaining every choice at a glance (Recognition over
|
|
74
|
+
* Recall). Escalation is confirmed in proportion to how far access expands:
|
|
75
|
+
* de-escalation applies instantly, an Organization escalation shows a light
|
|
76
|
+
* inline prompt, and Platform/Public escalations open a blocking
|
|
77
|
+
* {@link ConfirmDialog} that names the exact audience. Confirmation is owned
|
|
78
|
+
* here so every consumer — blueprint detail, instance detail, and any
|
|
79
|
+
* standalone embed — behaves identically.
|
|
39
80
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* access is consequential. De-escalating applies immediately.
|
|
81
|
+
* In `"create"` mode it renders an inline radio list that applies
|
|
82
|
+
* immediately (initial value selection has no escalation semantics).
|
|
43
83
|
*
|
|
44
84
|
* If the current visibility is not among the offered options (e.g. a
|
|
45
85
|
* platform-shared blueprint whose org no longer operates an
|
|
46
86
|
* IdentityProvider), its canonical option is rendered in place so the
|
|
47
87
|
* state stays legible and the user can still move to an offered level.
|
|
48
88
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
89
|
+
* All visual properties flow through `--stgm-*` design tokens; portaled
|
|
90
|
+
* content targets the {@link useStigmerPortalContainer} so it inherits the
|
|
91
|
+
* active theme.
|
|
52
92
|
*
|
|
53
93
|
* @example
|
|
54
94
|
* ```tsx
|
|
@@ -64,19 +104,27 @@ export function VisibilitySelector({
|
|
|
64
104
|
visibility,
|
|
65
105
|
options,
|
|
66
106
|
onVisibilityChange,
|
|
107
|
+
mode = "manage",
|
|
67
108
|
isPending = false,
|
|
68
109
|
disabled = false,
|
|
69
110
|
ariaLabel = "Resource visibility",
|
|
70
111
|
className,
|
|
71
112
|
}: VisibilitySelectorProps) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
const portalContainer = useStigmerPortalContainer();
|
|
114
|
+
const { confirmState, confirm, handleConfirm, handleCancel } =
|
|
115
|
+
useConfirmAction();
|
|
116
|
+
|
|
117
|
+
const [open, setOpen] = useState(false);
|
|
118
|
+
// The Organization-style escalation awaiting its light inline confirm.
|
|
119
|
+
const [pendingInline, setPendingInline] =
|
|
120
|
+
useState<ApiResourceVisibility | null>(null);
|
|
121
|
+
const [highlightIdx, setHighlightIdx] = useState(-1);
|
|
75
122
|
const optionRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
123
|
+
|
|
76
124
|
const effectivelyDisabled = disabled || isPending;
|
|
77
125
|
|
|
78
126
|
// Keep the current state legible even when it is not offerable in the
|
|
79
|
-
// current context: render its canonical option as an extra
|
|
127
|
+
// current context: render its canonical option as an extra row.
|
|
80
128
|
const effectiveOptions = useMemo(() => {
|
|
81
129
|
if (options.some((o) => o.value === visibility)) return options;
|
|
82
130
|
return [...options, visibilityOption(visibility)];
|
|
@@ -90,274 +138,273 @@ export function VisibilitySelector({
|
|
|
90
138
|
[effectiveOptions, visibility],
|
|
91
139
|
);
|
|
92
140
|
|
|
93
|
-
const
|
|
141
|
+
const apply = useCallback(
|
|
94
142
|
(value: ApiResourceVisibility) => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (isEscalation(value)) {
|
|
98
|
-
setConfirming(value);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
setConfirming(null);
|
|
143
|
+
setOpen(false);
|
|
144
|
+
setPendingInline(null);
|
|
103
145
|
onVisibilityChange(value);
|
|
104
146
|
},
|
|
105
|
-
[
|
|
147
|
+
[onVisibilityChange],
|
|
106
148
|
);
|
|
107
149
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
150
|
+
const handleSelect = useCallback(
|
|
151
|
+
(option: VisibilityLevelOption) => {
|
|
152
|
+
const value = option.value;
|
|
153
|
+
if (value === visibility) {
|
|
154
|
+
setPendingInline(null);
|
|
155
|
+
setOpen(false);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
113
158
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
159
|
+
// De-escalation (and create mode) never confirm — narrowing access is
|
|
160
|
+
// always safe, and an initial pick has no escalation semantics.
|
|
161
|
+
if (mode === "create" || !isEscalation(value)) {
|
|
162
|
+
apply(value);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
117
165
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
166
|
+
if (option.confirmDialog) {
|
|
167
|
+
setOpen(false);
|
|
168
|
+
setPendingInline(null);
|
|
169
|
+
void confirm({
|
|
170
|
+
title: option.confirmDialog.title,
|
|
171
|
+
description: option.confirmDialog.description,
|
|
172
|
+
confirmLabel: `Make ${option.label}`,
|
|
173
|
+
// Exposure is reversible, so this is a primary (not destructive)
|
|
174
|
+
// confirmation; the audience-naming copy carries the caution.
|
|
175
|
+
variant: "default",
|
|
176
|
+
}).then((ok) => {
|
|
177
|
+
if (ok) onVisibilityChange(value);
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
121
181
|
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
126
|
-
e.preventDefault();
|
|
127
|
-
nextIndex =
|
|
128
|
-
(index - 1 + effectiveOptions.length) % effectiveOptions.length;
|
|
182
|
+
if (option.confirmPrompt) {
|
|
183
|
+
setPendingInline(value);
|
|
184
|
+
return;
|
|
129
185
|
}
|
|
130
186
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
187
|
+
apply(value);
|
|
188
|
+
},
|
|
189
|
+
[visibility, mode, isEscalation, apply, confirm, onVisibilityChange],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const moveFocus = useCallback(
|
|
193
|
+
(from: number, delta: number) => {
|
|
194
|
+
const count = effectiveOptions.length;
|
|
195
|
+
const next = (from + delta + count) % count;
|
|
196
|
+
setHighlightIdx(next);
|
|
197
|
+
optionRefs.current[next]?.focus();
|
|
198
|
+
},
|
|
199
|
+
[effectiveOptions.length],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const handleRowKeyDown = useCallback(
|
|
203
|
+
(e: React.KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
204
|
+
switch (e.key) {
|
|
205
|
+
case "ArrowDown":
|
|
206
|
+
case "ArrowRight":
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
moveFocus(index, 1);
|
|
209
|
+
break;
|
|
210
|
+
case "ArrowUp":
|
|
211
|
+
case "ArrowLeft":
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
moveFocus(index, -1);
|
|
214
|
+
break;
|
|
215
|
+
case "Home":
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
setHighlightIdx(0);
|
|
218
|
+
optionRefs.current[0]?.focus();
|
|
219
|
+
break;
|
|
220
|
+
case "End": {
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
const last = effectiveOptions.length - 1;
|
|
223
|
+
setHighlightIdx(last);
|
|
224
|
+
optionRefs.current[last]?.focus();
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
134
227
|
}
|
|
135
228
|
},
|
|
136
|
-
[
|
|
229
|
+
[moveFocus, effectiveOptions.length],
|
|
137
230
|
);
|
|
138
231
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
232
|
+
// On open, focus the current level so keyboard users land on a sensible row.
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (!open) {
|
|
235
|
+
setPendingInline(null);
|
|
236
|
+
setHighlightIdx(-1);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const current = effectiveOptions.findIndex((o) => o.value === visibility);
|
|
240
|
+
const start = current >= 0 ? current : 0;
|
|
241
|
+
setHighlightIdx(start);
|
|
242
|
+
const raf = requestAnimationFrame(() => optionRefs.current[start]?.focus());
|
|
243
|
+
return () => cancelAnimationFrame(raf);
|
|
244
|
+
}, [open, effectiveOptions, visibility]);
|
|
143
245
|
|
|
144
|
-
|
|
145
|
-
<
|
|
246
|
+
const dialog = (
|
|
247
|
+
<ConfirmDialog
|
|
248
|
+
state={confirmState}
|
|
249
|
+
onConfirm={handleConfirm}
|
|
250
|
+
onCancel={handleCancel}
|
|
251
|
+
/>
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const renderRows = (role: "option" | "radio") =>
|
|
255
|
+
effectiveOptions.map((option, index) => (
|
|
256
|
+
<VisibilityOptionRow
|
|
257
|
+
key={option.value}
|
|
258
|
+
ref={(el) => {
|
|
259
|
+
optionRefs.current[index] = el;
|
|
260
|
+
}}
|
|
261
|
+
option={option}
|
|
262
|
+
role={role}
|
|
263
|
+
isSelected={visibility === option.value}
|
|
264
|
+
isHighlighted={role === "option" && highlightIdx === index}
|
|
265
|
+
tabIndex={
|
|
266
|
+
role === "radio"
|
|
267
|
+
? visibility === option.value
|
|
268
|
+
? 0
|
|
269
|
+
: -1
|
|
270
|
+
: highlightIdx === index
|
|
271
|
+
? 0
|
|
272
|
+
: -1
|
|
273
|
+
}
|
|
274
|
+
disabled={effectivelyDisabled}
|
|
275
|
+
onSelect={() => handleSelect(option)}
|
|
276
|
+
onMouseEnter={
|
|
277
|
+
role === "option" ? () => setHighlightIdx(index) : undefined
|
|
278
|
+
}
|
|
279
|
+
onKeyDown={(e) => handleRowKeyDown(e, index)}
|
|
280
|
+
/>
|
|
281
|
+
));
|
|
282
|
+
|
|
283
|
+
// ---- Create mode: inline radio list, applies immediately ---------------
|
|
284
|
+
if (mode === "create") {
|
|
285
|
+
return (
|
|
146
286
|
<div
|
|
147
287
|
role="radiogroup"
|
|
148
288
|
aria-label={ariaLabel}
|
|
149
289
|
aria-disabled={effectivelyDisabled || undefined}
|
|
150
290
|
className={cn(
|
|
151
|
-
"
|
|
291
|
+
"flex flex-col gap-0.5 rounded-md border border-border p-1",
|
|
152
292
|
effectivelyDisabled && "pointer-events-none opacity-50",
|
|
293
|
+
className,
|
|
153
294
|
)}
|
|
154
295
|
>
|
|
155
|
-
{
|
|
156
|
-
const isSelected = visibility === option.value;
|
|
157
|
-
|
|
158
|
-
return (
|
|
159
|
-
<button
|
|
160
|
-
key={option.value}
|
|
161
|
-
ref={(el) => {
|
|
162
|
-
optionRefs.current[index] = el;
|
|
163
|
-
}}
|
|
164
|
-
type="button"
|
|
165
|
-
role="radio"
|
|
166
|
-
aria-checked={isSelected}
|
|
167
|
-
aria-label={`${option.label}: ${option.description}`}
|
|
168
|
-
tabIndex={isSelected ? 0 : -1}
|
|
169
|
-
disabled={effectivelyDisabled}
|
|
170
|
-
onClick={() => handleSelect(option.value)}
|
|
171
|
-
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
172
|
-
className={cn(
|
|
173
|
-
"inline-flex cursor-pointer items-center gap-1 rounded-sm px-2.5 py-1 text-xs font-medium transition-colors",
|
|
174
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
175
|
-
isSelected
|
|
176
|
-
? SELECTED_STYLES[option.tone]
|
|
177
|
-
: "text-muted-foreground hover:text-foreground",
|
|
178
|
-
)}
|
|
179
|
-
>
|
|
180
|
-
{isPending && isSelected ? (
|
|
181
|
-
<span
|
|
182
|
-
className="inline-block size-3 animate-spin rounded-full border-2 border-current border-t-transparent"
|
|
183
|
-
aria-hidden="true"
|
|
184
|
-
/>
|
|
185
|
-
) : (
|
|
186
|
-
<VisibilityIcon tone={option.tone} className="size-3" />
|
|
187
|
-
)}
|
|
188
|
-
{option.label}
|
|
189
|
-
</button>
|
|
190
|
-
);
|
|
191
|
-
})}
|
|
296
|
+
{renderRows("radio")}
|
|
192
297
|
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
193
300
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
301
|
+
// ---- Manage mode: current-state chip + popover ladder ------------------
|
|
302
|
+
const current = visibilityOption(visibility);
|
|
303
|
+
const pendingOption =
|
|
304
|
+
pendingInline !== null
|
|
305
|
+
? effectiveOptions.find((o) => o.value === pendingInline)
|
|
306
|
+
: undefined;
|
|
200
307
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
308
|
+
return (
|
|
309
|
+
<>
|
|
310
|
+
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
311
|
+
<Popover.Trigger
|
|
312
|
+
disabled={effectivelyDisabled}
|
|
313
|
+
aria-label={`${ariaLabel}: ${current.label}`}
|
|
204
314
|
className={cn(
|
|
205
|
-
|
|
206
|
-
|
|
315
|
+
VISIBILITY_CHIP_CLASS,
|
|
316
|
+
"transition-colors hover:bg-accent-hover hover:text-foreground",
|
|
317
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
318
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
319
|
+
className,
|
|
207
320
|
)}
|
|
208
|
-
role="alert"
|
|
209
321
|
>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
className=
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
>
|
|
222
|
-
Confirm
|
|
223
|
-
</button>
|
|
224
|
-
<button
|
|
225
|
-
type="button"
|
|
226
|
-
onClick={cancelConfirm}
|
|
227
|
-
className={cn(
|
|
228
|
-
"rounded px-2 py-0.5 text-xs font-medium",
|
|
229
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
230
|
-
PROMPT_STYLES[confirmingOption.tone].cancel,
|
|
231
|
-
)}
|
|
232
|
-
>
|
|
233
|
-
Cancel
|
|
234
|
-
</button>
|
|
235
|
-
</div>
|
|
236
|
-
)}
|
|
237
|
-
</div>
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ---------------------------------------------------------------------------
|
|
242
|
-
// Tone styling — one row per visibility tone, keyed by VisibilityLevelOption
|
|
243
|
-
// ---------------------------------------------------------------------------
|
|
244
|
-
|
|
245
|
-
type Tone = VisibilityLevelOption["tone"];
|
|
246
|
-
|
|
247
|
-
const SELECTED_STYLES: Record<Tone, string> = {
|
|
248
|
-
private:
|
|
249
|
-
"bg-amber-50 text-amber-800 shadow-sm dark:bg-amber-900/30 dark:text-amber-300",
|
|
250
|
-
org: "bg-blue-100 text-blue-800 shadow-sm dark:bg-blue-900/40 dark:text-blue-300",
|
|
251
|
-
platform:
|
|
252
|
-
"bg-violet-100 text-violet-800 shadow-sm dark:bg-violet-900/40 dark:text-violet-300",
|
|
253
|
-
public:
|
|
254
|
-
"bg-emerald-100 text-emerald-800 shadow-sm dark:bg-emerald-900/40 dark:text-emerald-300",
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const PROMPT_STYLES: Record<
|
|
258
|
-
Tone,
|
|
259
|
-
{ container: string; text: string; confirm: string; cancel: string }
|
|
260
|
-
> = {
|
|
261
|
-
// Private never escalates, but the row keeps the Record total.
|
|
262
|
-
private: {
|
|
263
|
-
container:
|
|
264
|
-
"border-amber-200 bg-amber-50 dark:border-amber-800/50 dark:bg-amber-950/30",
|
|
265
|
-
text: "text-amber-800 dark:text-amber-200",
|
|
266
|
-
confirm:
|
|
267
|
-
"bg-amber-600 hover:bg-amber-700 dark:bg-amber-600 dark:hover:bg-amber-500",
|
|
268
|
-
cancel:
|
|
269
|
-
"text-amber-700 hover:text-amber-900 dark:text-amber-300 dark:hover:text-amber-100",
|
|
270
|
-
},
|
|
271
|
-
org: {
|
|
272
|
-
container:
|
|
273
|
-
"border-blue-200 bg-blue-50 dark:border-blue-800/50 dark:bg-blue-950/30",
|
|
274
|
-
text: "text-blue-800 dark:text-blue-200",
|
|
275
|
-
confirm:
|
|
276
|
-
"bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500",
|
|
277
|
-
cancel:
|
|
278
|
-
"text-blue-700 hover:text-blue-900 dark:text-blue-300 dark:hover:text-blue-100",
|
|
279
|
-
},
|
|
280
|
-
platform: {
|
|
281
|
-
container:
|
|
282
|
-
"border-violet-200 bg-violet-50 dark:border-violet-800/50 dark:bg-violet-950/30",
|
|
283
|
-
text: "text-violet-800 dark:text-violet-200",
|
|
284
|
-
confirm:
|
|
285
|
-
"bg-violet-600 hover:bg-violet-700 dark:bg-violet-600 dark:hover:bg-violet-500",
|
|
286
|
-
cancel:
|
|
287
|
-
"text-violet-700 hover:text-violet-900 dark:text-violet-300 dark:hover:text-violet-100",
|
|
288
|
-
},
|
|
289
|
-
public: {
|
|
290
|
-
container:
|
|
291
|
-
"border-amber-200 bg-amber-50 dark:border-amber-800/50 dark:bg-amber-950/30",
|
|
292
|
-
text: "text-amber-800 dark:text-amber-200",
|
|
293
|
-
confirm:
|
|
294
|
-
"bg-amber-600 hover:bg-amber-700 dark:bg-amber-600 dark:hover:bg-amber-500",
|
|
295
|
-
cancel:
|
|
296
|
-
"text-amber-700 hover:text-amber-900 dark:text-amber-300 dark:hover:text-amber-100",
|
|
297
|
-
},
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// ---------------------------------------------------------------------------
|
|
301
|
-
// Icons — inline SVGs following the SDK pattern (no icon library dependency)
|
|
302
|
-
// ---------------------------------------------------------------------------
|
|
303
|
-
|
|
304
|
-
/** Icon for a visibility tone; shared with {@link VisibilityBadge}. */
|
|
305
|
-
export function VisibilityIcon({
|
|
306
|
-
tone,
|
|
307
|
-
className,
|
|
308
|
-
}: {
|
|
309
|
-
readonly tone: Tone;
|
|
310
|
-
readonly className?: string;
|
|
311
|
-
}) {
|
|
312
|
-
switch (tone) {
|
|
313
|
-
case "org":
|
|
314
|
-
return <UsersIcon className={className} />;
|
|
315
|
-
case "platform":
|
|
316
|
-
return <BuildingsIcon className={className} />;
|
|
317
|
-
case "public":
|
|
318
|
-
return <GlobeIcon className={className} />;
|
|
319
|
-
default:
|
|
320
|
-
return <LockIcon className={className} />;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function LockIcon({ className }: { readonly className?: string }) {
|
|
325
|
-
return (
|
|
326
|
-
<svg className={className} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
327
|
-
<rect x="3.5" y="7" width="9" height="7" rx="1.5" />
|
|
328
|
-
<path d="M5.5 7V5a2.5 2.5 0 0 1 5 0v2" />
|
|
329
|
-
</svg>
|
|
330
|
-
);
|
|
331
|
-
}
|
|
322
|
+
{isPending ? (
|
|
323
|
+
<span
|
|
324
|
+
className="inline-block size-2.5 animate-spin rounded-full border-2 border-current border-t-transparent"
|
|
325
|
+
aria-hidden="true"
|
|
326
|
+
/>
|
|
327
|
+
) : (
|
|
328
|
+
<VisibilityIcon tone={current.tone} className="size-2.5" />
|
|
329
|
+
)}
|
|
330
|
+
{current.label}
|
|
331
|
+
<CaretIcon />
|
|
332
|
+
</Popover.Trigger>
|
|
332
333
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
334
|
+
<Popover.Portal container={portalContainer}>
|
|
335
|
+
<Popover.Positioner sideOffset={4} align="start">
|
|
336
|
+
<Popover.Popup
|
|
337
|
+
role="listbox"
|
|
338
|
+
aria-label={ariaLabel}
|
|
339
|
+
className={cn(
|
|
340
|
+
"z-popover w-72 rounded-lg border border-border bg-popover p-1 shadow-md",
|
|
341
|
+
"text-popover-foreground animate-in fade-in-0 zoom-in-95",
|
|
342
|
+
)}
|
|
343
|
+
>
|
|
344
|
+
{renderRows("option")}
|
|
343
345
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
346
|
+
{pendingOption?.confirmPrompt && (
|
|
347
|
+
<div
|
|
348
|
+
role="alert"
|
|
349
|
+
className={cn(
|
|
350
|
+
"mt-1 flex items-center gap-2 rounded-md border px-2.5 py-1.5",
|
|
351
|
+
PROMPT_STYLES[pendingOption.tone].container,
|
|
352
|
+
)}
|
|
353
|
+
>
|
|
354
|
+
<span
|
|
355
|
+
className={cn(
|
|
356
|
+
"flex-1 text-[0.65rem] leading-snug",
|
|
357
|
+
PROMPT_STYLES[pendingOption.tone].text,
|
|
358
|
+
)}
|
|
359
|
+
>
|
|
360
|
+
{pendingOption.confirmPrompt}
|
|
361
|
+
</span>
|
|
362
|
+
<button
|
|
363
|
+
type="button"
|
|
364
|
+
onClick={() => setPendingInline(null)}
|
|
365
|
+
className={cn(
|
|
366
|
+
"rounded px-2 py-0.5 text-[0.65rem] font-medium",
|
|
367
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
368
|
+
PROMPT_STYLES[pendingOption.tone].cancel,
|
|
369
|
+
)}
|
|
370
|
+
>
|
|
371
|
+
Cancel
|
|
372
|
+
</button>
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
375
|
+
onClick={() => apply(pendingOption.value)}
|
|
376
|
+
className={cn(
|
|
377
|
+
"rounded px-2 py-0.5 text-[0.65rem] font-medium text-white",
|
|
378
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
379
|
+
PROMPT_STYLES[pendingOption.tone].confirm,
|
|
380
|
+
)}
|
|
381
|
+
>
|
|
382
|
+
Confirm
|
|
383
|
+
</button>
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
</Popover.Popup>
|
|
387
|
+
</Popover.Positioner>
|
|
388
|
+
</Popover.Portal>
|
|
389
|
+
</Popover.Root>
|
|
390
|
+
{dialog}
|
|
391
|
+
</>
|
|
352
392
|
);
|
|
353
393
|
}
|
|
354
394
|
|
|
355
|
-
function
|
|
395
|
+
function CaretIcon() {
|
|
356
396
|
return (
|
|
357
|
-
<svg
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
397
|
+
<svg
|
|
398
|
+
className="size-2.5 shrink-0 text-muted-foreground"
|
|
399
|
+
viewBox="0 0 12 12"
|
|
400
|
+
fill="none"
|
|
401
|
+
stroke="currentColor"
|
|
402
|
+
strokeWidth="1.5"
|
|
403
|
+
strokeLinecap="round"
|
|
404
|
+
strokeLinejoin="round"
|
|
405
|
+
aria-hidden="true"
|
|
406
|
+
>
|
|
407
|
+
<path d="M3 4.5 6 7.5 9 4.5" />
|
|
361
408
|
</svg>
|
|
362
409
|
);
|
|
363
410
|
}
|
|
@@ -369,7 +416,8 @@ function GlobeIcon({ className }: { readonly className?: string }) {
|
|
|
369
416
|
* Rendered wherever the interactive {@link VisibilitySelector} is not
|
|
370
417
|
* available — for viewers who lack `can_edit`, and while a permission check
|
|
371
418
|
* is in flight — so a resource's visibility is always legible rather than
|
|
372
|
-
* silently blank.
|
|
419
|
+
* silently blank. Shares the chip styling with the selector trigger so the
|
|
420
|
+
* read-only and editable states are visually consistent.
|
|
373
421
|
*/
|
|
374
422
|
export function VisibilityBadge({
|
|
375
423
|
visibility,
|
|
@@ -380,12 +428,7 @@ export function VisibilityBadge({
|
|
|
380
428
|
}) {
|
|
381
429
|
const option = visibilityOption(visibility);
|
|
382
430
|
return (
|
|
383
|
-
<span
|
|
384
|
-
className={cn(
|
|
385
|
-
"inline-flex shrink-0 items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground",
|
|
386
|
-
className,
|
|
387
|
-
)}
|
|
388
|
-
>
|
|
431
|
+
<span className={cn(VISIBILITY_CHIP_CLASS, className)}>
|
|
389
432
|
<VisibilityIcon tone={option.tone} className="size-2.5" />
|
|
390
433
|
{option.label}
|
|
391
434
|
</span>
|