@xemahq/ui-kernel 0.1.6 → 0.1.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/dist/lib/biome-host/create-biome-orval-config.d.ts +14 -0
- package/dist/lib/biome-host/create-biome-orval-config.d.ts.map +1 -0
- package/dist/lib/biome-host/create-biome-orval-config.js +22 -0
- package/dist/lib/biome-host/create-biome-orval-config.js.map +1 -0
- package/dist/lib/biome-host/host-bridge.d.ts +2 -0
- package/dist/lib/biome-host/host-bridge.d.ts.map +1 -1
- package/dist/lib/biome-host/host-bridge.js.map +1 -1
- package/dist/lib/biome-host/host-sources.d.ts +2 -0
- package/dist/lib/biome-host/host-sources.d.ts.map +1 -1
- package/dist/lib/biome-host/index.d.ts +1 -0
- package/dist/lib/biome-host/index.d.ts.map +1 -1
- package/dist/lib/biome-host/index.js +1 -0
- package/dist/lib/biome-host/index.js.map +1 -1
- package/dist/session-kit/approvals/ApprovalButton.d.ts +14 -0
- package/dist/session-kit/approvals/ApprovalButton.d.ts.map +1 -0
- package/dist/session-kit/approvals/ApprovalButton.js +45 -0
- package/dist/session-kit/approvals/ApprovalButton.js.map +1 -0
- package/dist/session-kit/approvals/ApprovalCard.d.ts +12 -0
- package/dist/session-kit/approvals/ApprovalCard.d.ts.map +1 -0
- package/dist/session-kit/approvals/ApprovalCard.js +117 -0
- package/dist/session-kit/approvals/ApprovalCard.js.map +1 -0
- package/dist/session-kit/approvals/ApprovalsCenter.d.ts +11 -0
- package/dist/session-kit/approvals/ApprovalsCenter.d.ts.map +1 -0
- package/dist/session-kit/approvals/ApprovalsCenter.js +127 -0
- package/dist/session-kit/approvals/ApprovalsCenter.js.map +1 -0
- package/dist/session-kit/approvals/CapabilityApprovalStickyBar.d.ts +12 -0
- package/dist/session-kit/approvals/CapabilityApprovalStickyBar.d.ts.map +1 -0
- package/dist/session-kit/approvals/CapabilityApprovalStickyBar.js +36 -0
- package/dist/session-kit/approvals/CapabilityApprovalStickyBar.js.map +1 -0
- package/dist/session-kit/approvals/Obligation.d.ts +15 -0
- package/dist/session-kit/approvals/Obligation.d.ts.map +1 -0
- package/dist/session-kit/approvals/Obligation.js +42 -0
- package/dist/session-kit/approvals/Obligation.js.map +1 -0
- package/dist/session-kit/approvals/ScopedBody.d.ts +9 -0
- package/dist/session-kit/approvals/ScopedBody.d.ts.map +1 -0
- package/dist/session-kit/approvals/ScopedBody.js +145 -0
- package/dist/session-kit/approvals/ScopedBody.js.map +1 -0
- package/dist/session-kit/approvals/approval-icons.d.ts +15 -0
- package/dist/session-kit/approvals/approval-icons.d.ts.map +1 -0
- package/dist/session-kit/approvals/approval-icons.js +3 -0
- package/dist/session-kit/approvals/approval-icons.js.map +1 -0
- package/dist/session-kit/approvals/approval-model.d.ts +83 -0
- package/dist/session-kit/approvals/approval-model.d.ts.map +1 -0
- package/dist/session-kit/approvals/approval-model.js +25 -0
- package/dist/session-kit/approvals/approval-model.js.map +1 -0
- package/dist/session-kit/approvals/index.d.ts +12 -0
- package/dist/session-kit/approvals/index.d.ts.map +1 -0
- package/dist/session-kit/approvals/index.js +28 -0
- package/dist/session-kit/approvals/index.js.map +1 -0
- package/dist/session-kit/approvals/obligation-display.d.ts +17 -0
- package/dist/session-kit/approvals/obligation-display.d.ts.map +1 -0
- package/dist/session-kit/approvals/obligation-display.js +58 -0
- package/dist/session-kit/approvals/obligation-display.js.map +1 -0
- package/dist/session-kit/approvals/risk-accent.d.ts +8 -0
- package/dist/session-kit/approvals/risk-accent.d.ts.map +1 -0
- package/dist/session-kit/approvals/risk-accent.js +28 -0
- package/dist/session-kit/approvals/risk-accent.js.map +1 -0
- package/dist/session-kit/approvals/scope-icons.d.ts +12 -0
- package/dist/session-kit/approvals/scope-icons.d.ts.map +1 -0
- package/dist/session-kit/approvals/scope-icons.js +14 -0
- package/dist/session-kit/approvals/scope-icons.js.map +1 -0
- package/dist/session-kit/combobox/Combobox.d.ts +46 -0
- package/dist/session-kit/combobox/Combobox.d.ts.map +1 -0
- package/dist/session-kit/combobox/Combobox.js +113 -0
- package/dist/session-kit/combobox/Combobox.js.map +1 -0
- package/dist/session-kit/combobox/use-click-outside.d.ts +3 -0
- package/dist/session-kit/combobox/use-click-outside.d.ts.map +1 -0
- package/dist/session-kit/combobox/use-click-outside.js +18 -0
- package/dist/session-kit/combobox/use-click-outside.js.map +1 -0
- package/dist/session-kit/display/ContextHeader.d.ts +27 -0
- package/dist/session-kit/display/ContextHeader.d.ts.map +1 -0
- package/dist/session-kit/display/ContextHeader.js +47 -0
- package/dist/session-kit/display/ContextHeader.js.map +1 -0
- package/dist/session-kit/display/FileDiffCard.d.ts +18 -0
- package/dist/session-kit/display/FileDiffCard.d.ts.map +1 -0
- package/dist/session-kit/display/FileDiffCard.js +58 -0
- package/dist/session-kit/display/FileDiffCard.js.map +1 -0
- package/dist/session-kit/display/MD.d.ts +7 -0
- package/dist/session-kit/display/MD.d.ts.map +1 -0
- package/dist/session-kit/display/MD.js +89 -0
- package/dist/session-kit/display/MD.js.map +1 -0
- package/dist/session-kit/display/MessageTurn.d.ts +21 -0
- package/dist/session-kit/display/MessageTurn.d.ts.map +1 -0
- package/dist/session-kit/display/MessageTurn.js +62 -0
- package/dist/session-kit/display/MessageTurn.js.map +1 -0
- package/dist/session-kit/display/ThinkingPanel.d.ts +12 -0
- package/dist/session-kit/display/ThinkingPanel.d.ts.map +1 -0
- package/dist/session-kit/display/ThinkingPanel.js +30 -0
- package/dist/session-kit/display/ThinkingPanel.js.map +1 -0
- package/dist/session-kit/display/TodoChecklist.d.ts +17 -0
- package/dist/session-kit/display/TodoChecklist.d.ts.map +1 -0
- package/dist/session-kit/display/TodoChecklist.js +50 -0
- package/dist/session-kit/display/TodoChecklist.js.map +1 -0
- package/dist/session-kit/display/TokenMeter.d.ts +15 -0
- package/dist/session-kit/display/TokenMeter.d.ts.map +1 -0
- package/dist/session-kit/display/TokenMeter.js +35 -0
- package/dist/session-kit/display/TokenMeter.js.map +1 -0
- package/dist/session-kit/display/ToolStrip.d.ts +31 -0
- package/dist/session-kit/display/ToolStrip.d.ts.map +1 -0
- package/dist/session-kit/display/ToolStrip.js +99 -0
- package/dist/session-kit/display/ToolStrip.js.map +1 -0
- package/dist/session-kit/display/TypingDots.d.ts +3 -0
- package/dist/session-kit/display/TypingDots.d.ts.map +1 -0
- package/dist/session-kit/display/TypingDots.js +14 -0
- package/dist/session-kit/display/TypingDots.js.map +1 -0
- package/dist/session-kit/index.d.ts +21 -0
- package/dist/session-kit/index.d.ts.map +1 -0
- package/dist/session-kit/index.js +37 -0
- package/dist/session-kit/index.js.map +1 -0
- package/dist/session-kit/lib/enums.d.ts +34 -0
- package/dist/session-kit/lib/enums.d.ts.map +1 -0
- package/dist/session-kit/lib/enums.js +44 -0
- package/dist/session-kit/lib/enums.js.map +1 -0
- package/dist/session-kit/lib/portal-accent.d.ts +3 -0
- package/dist/session-kit/lib/portal-accent.d.ts.map +1 -0
- package/dist/session-kit/lib/portal-accent.js +9 -0
- package/dist/session-kit/lib/portal-accent.js.map +1 -0
- package/dist/session-kit/lib/status-dot.d.ts +10 -0
- package/dist/session-kit/lib/status-dot.d.ts.map +1 -0
- package/dist/session-kit/lib/status-dot.js +43 -0
- package/dist/session-kit/lib/status-dot.js.map +1 -0
- package/dist/session-kit/primitives/Avatar.d.ts +10 -0
- package/dist/session-kit/primitives/Avatar.d.ts.map +1 -0
- package/dist/session-kit/primitives/Avatar.js +21 -0
- package/dist/session-kit/primitives/Avatar.js.map +1 -0
- package/dist/session-kit/primitives/PortalGlyph.d.ts +12 -0
- package/dist/session-kit/primitives/PortalGlyph.d.ts.map +1 -0
- package/dist/session-kit/primitives/PortalGlyph.js +21 -0
- package/dist/session-kit/primitives/PortalGlyph.js.map +1 -0
- package/dist/session-kit/primitives/RiskPill.d.ts +12 -0
- package/dist/session-kit/primitives/RiskPill.d.ts.map +1 -0
- package/dist/session-kit/primitives/RiskPill.js +53 -0
- package/dist/session-kit/primitives/RiskPill.js.map +1 -0
- package/dist/session-kit/primitives/ScopeDot.d.ts +9 -0
- package/dist/session-kit/primitives/ScopeDot.d.ts.map +1 -0
- package/dist/session-kit/primitives/ScopeDot.js +23 -0
- package/dist/session-kit/primitives/ScopeDot.js.map +1 -0
- package/dist/session-kit/primitives/Segmented.d.ts +16 -0
- package/dist/session-kit/primitives/Segmented.d.ts.map +1 -0
- package/dist/session-kit/primitives/Segmented.js +31 -0
- package/dist/session-kit/primitives/Segmented.js.map +1 -0
- package/package.json +2 -2
- package/src/lib/biome-host/create-biome-orval-config.ts +76 -0
- package/src/lib/biome-host/host-bridge.ts +22 -0
- package/src/lib/biome-host/host-sources.ts +13 -0
- package/src/lib/biome-host/index.ts +1 -0
- package/src/session-kit/approvals/ApprovalButton.tsx +89 -0
- package/src/session-kit/approvals/ApprovalCard.tsx +336 -0
- package/src/session-kit/approvals/ApprovalsCenter.tsx +327 -0
- package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +118 -0
- package/src/session-kit/approvals/Obligation.tsx +111 -0
- package/src/session-kit/approvals/ScopedBody.tsx +392 -0
- package/src/session-kit/approvals/approval-icons.ts +31 -0
- package/src/session-kit/approvals/approval-model.ts +205 -0
- package/src/session-kit/approvals/index.ts +22 -0
- package/src/session-kit/approvals/obligation-display.ts +100 -0
- package/src/session-kit/approvals/risk-accent.ts +47 -0
- package/src/session-kit/approvals/scope-icons.ts +19 -0
- package/src/session-kit/combobox/Combobox.tsx +327 -0
- package/src/session-kit/combobox/use-click-outside.ts +21 -0
- package/src/session-kit/display/ContextHeader.tsx +148 -0
- package/src/session-kit/display/FileDiffCard.tsx +140 -0
- package/src/session-kit/display/MD.tsx +153 -0
- package/src/session-kit/display/MessageTurn.tsx +157 -0
- package/src/session-kit/display/ThinkingPanel.tsx +78 -0
- package/src/session-kit/display/TodoChecklist.tsx +120 -0
- package/src/session-kit/display/TokenMeter.tsx +89 -0
- package/src/session-kit/display/ToolStrip.tsx +278 -0
- package/src/session-kit/display/TypingDots.tsx +24 -0
- package/src/session-kit/index.ts +44 -0
- package/src/session-kit/lib/enums.ts +66 -0
- package/src/session-kit/lib/portal-accent.ts +30 -0
- package/src/session-kit/lib/status-dot.ts +68 -0
- package/src/session-kit/primitives/Avatar.tsx +44 -0
- package/src/session-kit/primitives/PortalGlyph.tsx +51 -0
- package/src/session-kit/primitives/RiskPill.tsx +95 -0
- package/src/session-kit/primitives/ScopeDot.tsx +47 -0
- package/src/session-kit/primitives/Segmented.tsx +71 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GENERIC approval frame (HANDOFF §6) — identical platform-wide.
|
|
3
|
+
*
|
|
4
|
+
* Header: "Approval required" + RiskPill (reused from the kit) + mono
|
|
5
|
+
* capability ref + serif title + summary. Then the biome-supplied
|
|
6
|
+
* `ScopedBody`. Then obligations chips (closed `PolicyObligationKind`
|
|
7
|
+
* taxonomy), approver role, and quorum dots. Actions: Deny / Allow once /
|
|
8
|
+
* Approve — low risk gets "Approve & remember"; high & critical are
|
|
9
|
+
* explicit Approve only (no allow-once-remember). The accent rail uses the
|
|
10
|
+
* SAME risk→colour mapping as the pill.
|
|
11
|
+
*
|
|
12
|
+
* Resolved state collapses to a compact confirmation row; a
|
|
13
|
+
* quorum-approved request reads "approved · waiting on N more approver".
|
|
14
|
+
*
|
|
15
|
+
* Controlled OR uncontrolled: pass `decision` to control the resolved
|
|
16
|
+
* state from the host (the real flow), or omit it and the card tracks its
|
|
17
|
+
* own optimistic resolved state. `onResolve(decision, request)` always
|
|
18
|
+
* fires.
|
|
19
|
+
*/
|
|
20
|
+
import { useState, type ReactElement } from 'react';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
ApprovalDecision,
|
|
24
|
+
isExplicitApprovalTier,
|
|
25
|
+
isQuorum,
|
|
26
|
+
type ApprovalRequest,
|
|
27
|
+
} from './approval-model';
|
|
28
|
+
import { ApprovalButton } from './ApprovalButton';
|
|
29
|
+
import { Obligation, QuorumDots } from './Obligation';
|
|
30
|
+
import { RISK_ACCENT } from './risk-accent';
|
|
31
|
+
import { ScopedBody } from './ScopedBody';
|
|
32
|
+
import { RiskPill } from '../primitives/RiskPill';
|
|
33
|
+
|
|
34
|
+
import type { ApprovalIcons } from './approval-icons';
|
|
35
|
+
|
|
36
|
+
export interface ApprovalCardProps {
|
|
37
|
+
readonly request: ApprovalRequest;
|
|
38
|
+
readonly icons: ApprovalIcons;
|
|
39
|
+
/** Resolution callback. Fires before/after the host updates `decision`. */
|
|
40
|
+
readonly onResolve: (
|
|
41
|
+
decision: ApprovalDecision,
|
|
42
|
+
request: ApprovalRequest,
|
|
43
|
+
) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Controlled resolved state. Omit for uncontrolled (the card collapses
|
|
46
|
+
* optimistically on click). The real host flow passes the server-known
|
|
47
|
+
* decision so the collapse reflects committed state.
|
|
48
|
+
*/
|
|
49
|
+
readonly decision?: ApprovalDecision | null;
|
|
50
|
+
/** Portal/brand accent for the (non-critical) primary approve button. */
|
|
51
|
+
readonly accent?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ApprovalCard({
|
|
55
|
+
request,
|
|
56
|
+
icons,
|
|
57
|
+
onResolve,
|
|
58
|
+
decision,
|
|
59
|
+
accent = 'hsl(var(--primary))',
|
|
60
|
+
}: ApprovalCardProps): ReactElement {
|
|
61
|
+
const [localDecision, setLocalDecision] = useState<ApprovalDecision | null>(
|
|
62
|
+
null,
|
|
63
|
+
);
|
|
64
|
+
const resolved = decision ?? localDecision;
|
|
65
|
+
const r = RISK_ACCENT[request.riskTier];
|
|
66
|
+
const explicit = isExplicitApprovalTier(request.riskTier);
|
|
67
|
+
const quorum = isQuorum(request);
|
|
68
|
+
const approveLabel = resolveApproveLabel(request, quorum, explicit);
|
|
69
|
+
|
|
70
|
+
function resolve(d: ApprovalDecision): void {
|
|
71
|
+
if (decision === undefined) setLocalDecision(d);
|
|
72
|
+
onResolve(d, request);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (resolved) {
|
|
76
|
+
const approved = resolved === ApprovalDecision.Approved;
|
|
77
|
+
const remaining =
|
|
78
|
+
(request.requireApproverCount ?? 1) - ((request.approvalsIn ?? 0) + 1);
|
|
79
|
+
const approvedLabel =
|
|
80
|
+
quorum && remaining > 0
|
|
81
|
+
? `approved · waiting on ${remaining} more approver${remaining === 1 ? '' : 's'}`
|
|
82
|
+
: 'approved, capability resumed';
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
style={{
|
|
86
|
+
marginTop: 9,
|
|
87
|
+
display: 'flex',
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
gap: 10,
|
|
90
|
+
padding: '11px 14px',
|
|
91
|
+
borderRadius: 11,
|
|
92
|
+
border: `1px solid ${
|
|
93
|
+
approved ? 'hsl(var(--success) / 0.3)' : 'hsl(var(--rule))'
|
|
94
|
+
}`,
|
|
95
|
+
background: approved
|
|
96
|
+
? 'hsl(var(--success) / 0.06)'
|
|
97
|
+
: 'hsl(var(--muted))',
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<span
|
|
101
|
+
style={{
|
|
102
|
+
color: approved ? 'hsl(var(--success))' : 'hsl(var(--ink-3))',
|
|
103
|
+
display: 'inline-flex',
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{approved ? icons.approved(17) : icons.denied(17)}
|
|
107
|
+
</span>
|
|
108
|
+
<span style={{ fontSize: 13, color: 'hsl(var(--ink-2))' }}>
|
|
109
|
+
<strong style={{ color: 'hsl(var(--ink))', fontWeight: 600 }}>
|
|
110
|
+
{request.title}
|
|
111
|
+
</strong>{' '}
|
|
112
|
+
— {approved ? approvedLabel : 'denied'}
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
style={{
|
|
121
|
+
marginTop: 9,
|
|
122
|
+
borderRadius: 12,
|
|
123
|
+
overflow: 'hidden',
|
|
124
|
+
border: `1px solid ${explicit ? r.bd : 'hsl(var(--rule-2))'}`,
|
|
125
|
+
boxShadow: 'var(--shadow-2)',
|
|
126
|
+
background: 'hsl(var(--paper-elev))',
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<div style={{ display: 'flex' }}>
|
|
130
|
+
{/* accent rail — same risk→colour mapping as the pill */}
|
|
131
|
+
<div
|
|
132
|
+
style={{
|
|
133
|
+
width: 3,
|
|
134
|
+
flexShrink: 0,
|
|
135
|
+
background: explicit ? r.fg : 'hsl(var(--rule-2))',
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
139
|
+
{/* header — generic */}
|
|
140
|
+
<div
|
|
141
|
+
style={{
|
|
142
|
+
padding: '11px 14px 9px',
|
|
143
|
+
borderBottom: '1px solid hsl(var(--rule))',
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<div
|
|
147
|
+
style={{
|
|
148
|
+
display: 'flex',
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
gap: 8,
|
|
151
|
+
marginBottom: 6,
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<span
|
|
155
|
+
style={{
|
|
156
|
+
color: explicit ? r.fg : 'hsl(var(--primary))',
|
|
157
|
+
display: 'inline-flex',
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
{icons.shield(16)}
|
|
161
|
+
</span>
|
|
162
|
+
<span
|
|
163
|
+
className="cap"
|
|
164
|
+
style={{ color: 'hsl(var(--ink-3))', letterSpacing: '0.1em' }}
|
|
165
|
+
>
|
|
166
|
+
Approval required
|
|
167
|
+
</span>
|
|
168
|
+
<RiskPill tier={request.riskTier} size="xs" />
|
|
169
|
+
<span style={{ flex: 1 }} />
|
|
170
|
+
<span
|
|
171
|
+
className="mono"
|
|
172
|
+
style={{
|
|
173
|
+
fontSize: 10.5,
|
|
174
|
+
color: 'hsl(var(--ink-4))',
|
|
175
|
+
display: 'inline-flex',
|
|
176
|
+
alignItems: 'center',
|
|
177
|
+
gap: 4,
|
|
178
|
+
overflow: 'hidden',
|
|
179
|
+
textOverflow: 'ellipsis',
|
|
180
|
+
whiteSpace: 'nowrap',
|
|
181
|
+
maxWidth: '50%',
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
{icons.capability(11)}
|
|
185
|
+
{request.capability}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
<div
|
|
189
|
+
className="serif"
|
|
190
|
+
style={{
|
|
191
|
+
fontSize: 15,
|
|
192
|
+
fontWeight: 600,
|
|
193
|
+
color: 'hsl(var(--ink))',
|
|
194
|
+
lineHeight: 1.3,
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
{request.title}
|
|
198
|
+
</div>
|
|
199
|
+
<div
|
|
200
|
+
style={{
|
|
201
|
+
fontSize: 12.5,
|
|
202
|
+
color: 'hsl(var(--ink-3))',
|
|
203
|
+
marginTop: 2,
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
{request.summary}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* SCOPED biome body */}
|
|
211
|
+
<div
|
|
212
|
+
style={{ padding: '10px 14px', background: 'hsl(var(--paper) / 0.5)' }}
|
|
213
|
+
>
|
|
214
|
+
<div
|
|
215
|
+
style={{
|
|
216
|
+
display: 'flex',
|
|
217
|
+
alignItems: 'center',
|
|
218
|
+
gap: 6,
|
|
219
|
+
marginBottom: 8,
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
<span
|
|
223
|
+
className="cap"
|
|
224
|
+
style={{ fontSize: 9.5, color: 'hsl(var(--ink-4))' }}
|
|
225
|
+
>
|
|
226
|
+
Details from {request.biomeName}
|
|
227
|
+
</span>
|
|
228
|
+
<span
|
|
229
|
+
style={{ flex: 1, height: 1, background: 'hsl(var(--rule))' }}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
<ScopedBody scope={request.scope} renderIcon={icons.scope} />
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{/* obligations + approver requirement — generic */}
|
|
236
|
+
<div
|
|
237
|
+
style={{
|
|
238
|
+
padding: '10px 14px',
|
|
239
|
+
borderTop: '1px solid hsl(var(--rule))',
|
|
240
|
+
display: 'flex',
|
|
241
|
+
flexDirection: 'column',
|
|
242
|
+
gap: 9,
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{request.obligations && request.obligations.length > 0 && (
|
|
246
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
|
247
|
+
{request.obligations.map((o) => (
|
|
248
|
+
<Obligation
|
|
249
|
+
key={o.kind}
|
|
250
|
+
obligation={o}
|
|
251
|
+
renderIcon={icons.obligation}
|
|
252
|
+
/>
|
|
253
|
+
))}
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
<div
|
|
257
|
+
style={{
|
|
258
|
+
display: 'flex',
|
|
259
|
+
alignItems: 'center',
|
|
260
|
+
gap: 10,
|
|
261
|
+
flexWrap: 'wrap',
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
{request.requireRole && (
|
|
265
|
+
<span
|
|
266
|
+
style={{
|
|
267
|
+
display: 'inline-flex',
|
|
268
|
+
alignItems: 'center',
|
|
269
|
+
gap: 5,
|
|
270
|
+
fontSize: 12,
|
|
271
|
+
color: 'hsl(var(--ink-3))',
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
{icons.user(13)}approver role{' '}
|
|
275
|
+
<span className="mono" style={{ color: 'hsl(var(--ink-2))' }}>
|
|
276
|
+
{request.requireRole}
|
|
277
|
+
</span>
|
|
278
|
+
</span>
|
|
279
|
+
)}
|
|
280
|
+
{quorum && (
|
|
281
|
+
<QuorumDots
|
|
282
|
+
have={request.approvalsIn ?? 0}
|
|
283
|
+
need={request.requireApproverCount ?? 1}
|
|
284
|
+
usersIcon={icons.users(13)}
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
287
|
+
<span style={{ flex: 1 }} />
|
|
288
|
+
<ApprovalButton
|
|
289
|
+
variant="ghost"
|
|
290
|
+
size="sm"
|
|
291
|
+
onClick={() => resolve(ApprovalDecision.Denied)}
|
|
292
|
+
>
|
|
293
|
+
Deny
|
|
294
|
+
</ApprovalButton>
|
|
295
|
+
{!explicit && (
|
|
296
|
+
<ApprovalButton
|
|
297
|
+
variant="soft"
|
|
298
|
+
size="sm"
|
|
299
|
+
icon={icons.check(13)}
|
|
300
|
+
onClick={() => resolve(ApprovalDecision.Approved)}
|
|
301
|
+
>
|
|
302
|
+
Allow once
|
|
303
|
+
</ApprovalButton>
|
|
304
|
+
)}
|
|
305
|
+
<ApprovalButton
|
|
306
|
+
variant="primary"
|
|
307
|
+
size="sm"
|
|
308
|
+
icon={icons.shield(13)}
|
|
309
|
+
accent={explicit ? r.fg : accent}
|
|
310
|
+
onClick={() => resolve(ApprovalDecision.Approved)}
|
|
311
|
+
>
|
|
312
|
+
{approveLabel}
|
|
313
|
+
</ApprovalButton>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The primary-button label: a quorum step ("Approve (1 of 2)"), a plain
|
|
324
|
+
* "Approve" for explicit (high/critical) tiers, or "Approve & remember"
|
|
325
|
+
* for low/medium where allow-once-remember is offered.
|
|
326
|
+
*/
|
|
327
|
+
function resolveApproveLabel(
|
|
328
|
+
request: ApprovalRequest,
|
|
329
|
+
quorum: boolean,
|
|
330
|
+
explicit: boolean,
|
|
331
|
+
): string {
|
|
332
|
+
if (quorum) {
|
|
333
|
+
return `Approve (${(request.approvalsIn ?? 0) + 1} of ${request.requireApproverCount})`;
|
|
334
|
+
}
|
|
335
|
+
return explicit ? 'Approve' : 'Approve & remember';
|
|
336
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approvals Center body (HANDOFF §6) — the cross-portal batch-review list.
|
|
3
|
+
*
|
|
4
|
+
* This is the GENERIC, prop-driven body (toolbar + rows + empty state). It
|
|
5
|
+
* does NOT own the drawer chrome: the host wraps it in its own `Sheet`
|
|
6
|
+
* (the host carries the overlay/focus-trap/portal primitive). That keeps
|
|
7
|
+
* the kit free of any host UI dependency while the batching/selection
|
|
8
|
+
* logic stays generic and reusable.
|
|
9
|
+
*
|
|
10
|
+
* Batches approvals ACROSS all portals/sessions: each row carries its
|
|
11
|
+
* portal + biome + quorum and an "Open session" affordance. "Select
|
|
12
|
+
* low-risk" selects every low/medium item; bulk "Approve N" resolves the
|
|
13
|
+
* selection. The row model (`ApprovalCenterItem`) already carries
|
|
14
|
+
* portalId/name/accent so Pillar 4 can group by portal without a reshape.
|
|
15
|
+
*/
|
|
16
|
+
import { CapabilityRiskTier } from '@xemahq/kernel-contracts/capability';
|
|
17
|
+
import { useCallback, useState, type ReactElement } from 'react';
|
|
18
|
+
|
|
19
|
+
import { ApprovalDecision, type ApprovalCenterItem } from './approval-model';
|
|
20
|
+
import { ApprovalButton } from './ApprovalButton';
|
|
21
|
+
import { RiskPill } from '../primitives/RiskPill';
|
|
22
|
+
|
|
23
|
+
import type { ApprovalIcons } from './approval-icons';
|
|
24
|
+
|
|
25
|
+
export interface ApprovalsCenterProps {
|
|
26
|
+
readonly items: readonly ApprovalCenterItem[];
|
|
27
|
+
readonly icons: Pick<ApprovalIcons, 'check' | 'approved'>;
|
|
28
|
+
/** Resolve a single item (also used by the bulk action per-item). */
|
|
29
|
+
readonly onResolve: (
|
|
30
|
+
decision: ApprovalDecision,
|
|
31
|
+
item: ApprovalCenterItem,
|
|
32
|
+
) => void;
|
|
33
|
+
/** "Open session" — host navigates to the suspended capability's session. */
|
|
34
|
+
readonly onOpen?: (item: ApprovalCenterItem) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const LOW_RISK: ReadonlySet<CapabilityRiskTier> = new Set([
|
|
38
|
+
CapabilityRiskTier.Low,
|
|
39
|
+
CapabilityRiskTier.Medium,
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export function ApprovalsCenter({
|
|
43
|
+
items,
|
|
44
|
+
icons,
|
|
45
|
+
onResolve,
|
|
46
|
+
onOpen,
|
|
47
|
+
}: ApprovalsCenterProps): ReactElement {
|
|
48
|
+
const [selected, setSelected] = useState<ReadonlySet<string>>(
|
|
49
|
+
() => new Set(),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const toggle = useCallback((id: string) => {
|
|
53
|
+
setSelected((prev) => {
|
|
54
|
+
const next = new Set(prev);
|
|
55
|
+
if (next.has(id)) next.delete(id);
|
|
56
|
+
else next.add(id);
|
|
57
|
+
return next;
|
|
58
|
+
});
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const selectLowRisk = useCallback(() => {
|
|
62
|
+
setSelected(
|
|
63
|
+
new Set(
|
|
64
|
+
items
|
|
65
|
+
.filter((it) => LOW_RISK.has(it.request.riskTier))
|
|
66
|
+
.map((it) => it.id),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
}, [items]);
|
|
70
|
+
|
|
71
|
+
const approveSelected = useCallback(() => {
|
|
72
|
+
for (const id of selected) {
|
|
73
|
+
const it = items.find((x) => x.id === id);
|
|
74
|
+
if (it) onResolve(ApprovalDecision.Approved, it);
|
|
75
|
+
}
|
|
76
|
+
setSelected(new Set());
|
|
77
|
+
}, [selected, items, onResolve]);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
style={{
|
|
82
|
+
display: 'flex',
|
|
83
|
+
flexDirection: 'column',
|
|
84
|
+
height: '100%',
|
|
85
|
+
minHeight: 0,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{/* selection toolbar */}
|
|
89
|
+
<div
|
|
90
|
+
style={{
|
|
91
|
+
padding: '9px 16px',
|
|
92
|
+
borderBottom: '1px solid hsl(var(--rule))',
|
|
93
|
+
display: 'flex',
|
|
94
|
+
alignItems: 'center',
|
|
95
|
+
gap: 9,
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<span style={{ fontSize: 12, color: 'hsl(var(--ink-3))' }}>
|
|
99
|
+
{selected.size} selected
|
|
100
|
+
</span>
|
|
101
|
+
<span style={{ flex: 1 }} />
|
|
102
|
+
<ApprovalButton
|
|
103
|
+
variant="ghost"
|
|
104
|
+
size="xs"
|
|
105
|
+
onClick={selectLowRisk}
|
|
106
|
+
disabled={items.length === 0}
|
|
107
|
+
>
|
|
108
|
+
Select low-risk
|
|
109
|
+
</ApprovalButton>
|
|
110
|
+
<ApprovalButton
|
|
111
|
+
variant="primary"
|
|
112
|
+
size="xs"
|
|
113
|
+
icon={icons.check(12)}
|
|
114
|
+
disabled={selected.size === 0}
|
|
115
|
+
onClick={approveSelected}
|
|
116
|
+
>
|
|
117
|
+
Approve {selected.size || ''}
|
|
118
|
+
</ApprovalButton>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* rows */}
|
|
122
|
+
<div
|
|
123
|
+
style={{
|
|
124
|
+
flex: 1,
|
|
125
|
+
minHeight: 0,
|
|
126
|
+
overflowY: 'auto',
|
|
127
|
+
padding: 12,
|
|
128
|
+
display: 'flex',
|
|
129
|
+
flexDirection: 'column',
|
|
130
|
+
gap: 9,
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{items.length === 0 ? (
|
|
134
|
+
<div
|
|
135
|
+
style={{
|
|
136
|
+
textAlign: 'center',
|
|
137
|
+
padding: '48px 0',
|
|
138
|
+
color: 'hsl(var(--ink-3))',
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
<span
|
|
142
|
+
style={{
|
|
143
|
+
color: 'hsl(var(--success))',
|
|
144
|
+
display: 'inline-flex',
|
|
145
|
+
marginBottom: 10,
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{icons.approved(26)}
|
|
149
|
+
</span>
|
|
150
|
+
<div style={{ fontSize: 13.5 }}>Nothing waiting on you.</div>
|
|
151
|
+
</div>
|
|
152
|
+
) : (
|
|
153
|
+
items.map((it) => (
|
|
154
|
+
<ApprovalRow
|
|
155
|
+
key={it.id}
|
|
156
|
+
item={it}
|
|
157
|
+
checked={selected.has(it.id)}
|
|
158
|
+
onToggle={() => toggle(it.id)}
|
|
159
|
+
onResolve={onResolve}
|
|
160
|
+
{...(onOpen ? { onOpen } : {})}
|
|
161
|
+
icons={icons}
|
|
162
|
+
/>
|
|
163
|
+
))
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface ApprovalRowProps {
|
|
171
|
+
readonly item: ApprovalCenterItem;
|
|
172
|
+
readonly checked: boolean;
|
|
173
|
+
readonly onToggle: () => void;
|
|
174
|
+
readonly onResolve: (
|
|
175
|
+
decision: ApprovalDecision,
|
|
176
|
+
item: ApprovalCenterItem,
|
|
177
|
+
) => void;
|
|
178
|
+
readonly onOpen?: (item: ApprovalCenterItem) => void;
|
|
179
|
+
readonly icons: Pick<ApprovalIcons, 'check'>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function ApprovalRow({
|
|
183
|
+
item,
|
|
184
|
+
checked,
|
|
185
|
+
onToggle,
|
|
186
|
+
onResolve,
|
|
187
|
+
onOpen,
|
|
188
|
+
icons,
|
|
189
|
+
}: ApprovalRowProps): ReactElement {
|
|
190
|
+
const { request } = item;
|
|
191
|
+
const quorum = (request.requireApproverCount ?? 1) > 1;
|
|
192
|
+
return (
|
|
193
|
+
<div
|
|
194
|
+
style={{
|
|
195
|
+
border: `1px solid ${
|
|
196
|
+
checked ? 'hsl(var(--primary) / 0.4)' : 'hsl(var(--rule))'
|
|
197
|
+
}`,
|
|
198
|
+
borderRadius: 10,
|
|
199
|
+
background: 'hsl(var(--paper-elev))',
|
|
200
|
+
boxShadow: checked
|
|
201
|
+
? '0 0 0 1px hsl(var(--primary) / 0.2)'
|
|
202
|
+
: 'var(--shadow-2)',
|
|
203
|
+
overflow: 'hidden',
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<div style={{ display: 'flex', gap: 10, padding: '11px 12px' }}>
|
|
207
|
+
<button
|
|
208
|
+
type="button"
|
|
209
|
+
onClick={onToggle}
|
|
210
|
+
aria-pressed={checked}
|
|
211
|
+
aria-label={checked ? 'Deselect' : 'Select'}
|
|
212
|
+
style={{
|
|
213
|
+
width: 18,
|
|
214
|
+
height: 18,
|
|
215
|
+
borderRadius: 5,
|
|
216
|
+
marginTop: 1,
|
|
217
|
+
flexShrink: 0,
|
|
218
|
+
display: 'grid',
|
|
219
|
+
placeItems: 'center',
|
|
220
|
+
border: checked ? 'none' : '1.5px solid hsl(var(--rule-2))',
|
|
221
|
+
background: checked ? 'hsl(var(--primary))' : 'transparent',
|
|
222
|
+
color: '#fff',
|
|
223
|
+
cursor: 'pointer',
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
{checked && icons.check(12)}
|
|
227
|
+
</button>
|
|
228
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
229
|
+
<div
|
|
230
|
+
style={{
|
|
231
|
+
display: 'flex',
|
|
232
|
+
alignItems: 'center',
|
|
233
|
+
gap: 7,
|
|
234
|
+
marginBottom: 3,
|
|
235
|
+
}}
|
|
236
|
+
>
|
|
237
|
+
<span
|
|
238
|
+
style={{
|
|
239
|
+
fontSize: 13.5,
|
|
240
|
+
fontWeight: 600,
|
|
241
|
+
color: 'hsl(var(--ink))',
|
|
242
|
+
overflow: 'hidden',
|
|
243
|
+
textOverflow: 'ellipsis',
|
|
244
|
+
whiteSpace: 'nowrap',
|
|
245
|
+
minWidth: 0,
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
{request.title}
|
|
249
|
+
</span>
|
|
250
|
+
<RiskPill tier={request.riskTier} size="xs" />
|
|
251
|
+
</div>
|
|
252
|
+
<div
|
|
253
|
+
style={{
|
|
254
|
+
display: 'flex',
|
|
255
|
+
alignItems: 'center',
|
|
256
|
+
gap: 7,
|
|
257
|
+
fontSize: 11.5,
|
|
258
|
+
color: 'hsl(var(--ink-3))',
|
|
259
|
+
flexWrap: 'wrap',
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
{(item.portalName || item.portalAccentVar) && (
|
|
263
|
+
<span
|
|
264
|
+
style={{
|
|
265
|
+
display: 'inline-flex',
|
|
266
|
+
alignItems: 'center',
|
|
267
|
+
gap: 4,
|
|
268
|
+
}}
|
|
269
|
+
>
|
|
270
|
+
{item.portalAccentVar && (
|
|
271
|
+
<span
|
|
272
|
+
style={{
|
|
273
|
+
width: 7,
|
|
274
|
+
height: 7,
|
|
275
|
+
borderRadius: 2,
|
|
276
|
+
background: `hsl(${item.portalAccentVar})`,
|
|
277
|
+
}}
|
|
278
|
+
/>
|
|
279
|
+
)}
|
|
280
|
+
{item.portalName}
|
|
281
|
+
</span>
|
|
282
|
+
)}
|
|
283
|
+
{item.portalName && <span>·</span>}
|
|
284
|
+
<span>{request.biomeName}</span>
|
|
285
|
+
{quorum && (
|
|
286
|
+
<>
|
|
287
|
+
<span>·</span>
|
|
288
|
+
<span className="mono">
|
|
289
|
+
{request.approvalsIn ?? 0}/{request.requireApproverCount}
|
|
290
|
+
</span>
|
|
291
|
+
</>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
<div
|
|
297
|
+
style={{
|
|
298
|
+
display: 'flex',
|
|
299
|
+
gap: 7,
|
|
300
|
+
padding: '0 12px 11px',
|
|
301
|
+
justifyContent: 'flex-end',
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
{onOpen && (
|
|
305
|
+
<ApprovalButton variant="ghost" size="xs" onClick={() => onOpen(item)}>
|
|
306
|
+
Open session
|
|
307
|
+
</ApprovalButton>
|
|
308
|
+
)}
|
|
309
|
+
<ApprovalButton
|
|
310
|
+
variant="ghost"
|
|
311
|
+
size="xs"
|
|
312
|
+
onClick={() => onResolve(ApprovalDecision.Denied, item)}
|
|
313
|
+
>
|
|
314
|
+
Deny
|
|
315
|
+
</ApprovalButton>
|
|
316
|
+
<ApprovalButton
|
|
317
|
+
variant="soft"
|
|
318
|
+
size="xs"
|
|
319
|
+
icon={icons.check(12)}
|
|
320
|
+
onClick={() => onResolve(ApprovalDecision.Approved, item)}
|
|
321
|
+
>
|
|
322
|
+
Approve
|
|
323
|
+
</ApprovalButton>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|