@xemahq/ui-kernel 0.1.6 → 0.1.8
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/agent-validation.d.ts +22 -0
- package/dist/lib/biome-host/agent-validation.d.ts.map +1 -0
- package/dist/lib/biome-host/agent-validation.js +127 -0
- package/dist/lib/biome-host/agent-validation.js.map +1 -0
- 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/frontend-biome.d.ts +3 -1
- package/dist/lib/biome-host/frontend-biome.d.ts.map +1 -1
- 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 +2 -1
- package/dist/lib/biome-host/index.d.ts.map +1 -1
- package/dist/lib/biome-host/index.js +2 -1
- package/dist/lib/biome-host/index.js.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/lib/agent-validation-host.d.ts +3 -0
- package/dist/registry/lib/agent-validation-host.d.ts.map +1 -0
- package/dist/registry/lib/agent-validation-host.js +10 -0
- package/dist/registry/lib/agent-validation-host.js.map +1 -0
- 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/frontend-biome.ts +22 -2
- 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 +2 -1
- package/src/registry/index.ts +1 -1
- package/src/registry/lib/extension-points.ts +1 -1
- 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
- /package/src/lib/biome-host/{composition-validation.ts → agent-validation.ts} +0 -0
- /package/src/registry/lib/{composition-validation-host.ts → agent-validation-host.ts} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sticky CAPABILITY-approval bar (HANDOFF §6) — pinned above the composer
|
|
3
|
+
* so a pending capability approval is always actionable without scrolling
|
|
4
|
+
* back to the inline card.
|
|
5
|
+
*
|
|
6
|
+
* NOTE on naming: this is DISTINCT from the clarification-gate
|
|
7
|
+
* `StickyApprovalBar` (`src/components/clarification/StickyApprovalBar.tsx`),
|
|
8
|
+
* which approves a clarification CONTRACT (attempt counters, reject
|
|
9
|
+
* reasons, override rationale). This bar approves a suspended CAPABILITY
|
|
10
|
+
* CALL (risk tier + biome + Review/Deny/Approve). The two concerns are
|
|
11
|
+
* deliberately separate components so neither regresses the other.
|
|
12
|
+
*
|
|
13
|
+
* Title + RiskPill + biome + Review / Deny / Approve. The bar surface is
|
|
14
|
+
* tinted with the risk accent; critical/high force the risk colour on the
|
|
15
|
+
* approve button.
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
ApprovalDecision,
|
|
19
|
+
isExplicitApprovalTier,
|
|
20
|
+
type ApprovalRequest,
|
|
21
|
+
} from './approval-model';
|
|
22
|
+
import { ApprovalButton } from './ApprovalButton';
|
|
23
|
+
import { RISK_ACCENT } from './risk-accent';
|
|
24
|
+
import { RiskPill } from '../primitives/RiskPill';
|
|
25
|
+
|
|
26
|
+
import type { ApprovalIcons } from './approval-icons';
|
|
27
|
+
import type { ReactElement } from 'react';
|
|
28
|
+
|
|
29
|
+
export interface CapabilityApprovalStickyBarProps {
|
|
30
|
+
readonly request: ApprovalRequest;
|
|
31
|
+
readonly icons: Pick<ApprovalIcons, 'shield' | 'check'>;
|
|
32
|
+
readonly onResolve: (
|
|
33
|
+
decision: ApprovalDecision,
|
|
34
|
+
request: ApprovalRequest,
|
|
35
|
+
) => void;
|
|
36
|
+
/** "Review" scrolls/opens the inline card; optional. */
|
|
37
|
+
readonly onReview?: () => void;
|
|
38
|
+
/** Portal/brand accent for the (non-critical) approve button. */
|
|
39
|
+
readonly accent?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function CapabilityApprovalStickyBar({
|
|
43
|
+
request,
|
|
44
|
+
icons,
|
|
45
|
+
onResolve,
|
|
46
|
+
onReview,
|
|
47
|
+
accent = 'hsl(var(--primary))',
|
|
48
|
+
}: CapabilityApprovalStickyBarProps): ReactElement {
|
|
49
|
+
const r = RISK_ACCENT[request.riskTier];
|
|
50
|
+
const explicit = isExplicitApprovalTier(request.riskTier);
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
display: 'flex',
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
gap: 11,
|
|
57
|
+
padding: '9px 13px',
|
|
58
|
+
borderRadius: 11,
|
|
59
|
+
border: `1px solid ${r.bd}`,
|
|
60
|
+
background: r.bg,
|
|
61
|
+
marginBottom: 9,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<span style={{ color: r.fg, flexShrink: 0, display: 'inline-flex' }}>
|
|
65
|
+
{icons.shield(17)}
|
|
66
|
+
</span>
|
|
67
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
68
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
|
69
|
+
<span
|
|
70
|
+
style={{
|
|
71
|
+
fontSize: 13,
|
|
72
|
+
fontWeight: 600,
|
|
73
|
+
color: 'hsl(var(--ink))',
|
|
74
|
+
overflow: 'hidden',
|
|
75
|
+
textOverflow: 'ellipsis',
|
|
76
|
+
whiteSpace: 'nowrap',
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{request.title}
|
|
80
|
+
</span>
|
|
81
|
+
<RiskPill tier={request.riskTier} size="xs" />
|
|
82
|
+
</div>
|
|
83
|
+
<div
|
|
84
|
+
style={{
|
|
85
|
+
fontSize: 11.5,
|
|
86
|
+
color: 'hsl(var(--ink-3))',
|
|
87
|
+
overflow: 'hidden',
|
|
88
|
+
textOverflow: 'ellipsis',
|
|
89
|
+
whiteSpace: 'nowrap',
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{request.biomeName} · awaiting your approval
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
{onReview && (
|
|
96
|
+
<ApprovalButton variant="ghost" size="sm" onClick={onReview}>
|
|
97
|
+
Review
|
|
98
|
+
</ApprovalButton>
|
|
99
|
+
)}
|
|
100
|
+
<ApprovalButton
|
|
101
|
+
variant="ghost"
|
|
102
|
+
size="sm"
|
|
103
|
+
onClick={() => onResolve(ApprovalDecision.Denied, request)}
|
|
104
|
+
>
|
|
105
|
+
Deny
|
|
106
|
+
</ApprovalButton>
|
|
107
|
+
<ApprovalButton
|
|
108
|
+
variant="primary"
|
|
109
|
+
size="sm"
|
|
110
|
+
icon={icons.check(13)}
|
|
111
|
+
accent={explicit ? r.fg : accent}
|
|
112
|
+
onClick={() => onResolve(ApprovalDecision.Approved, request)}
|
|
113
|
+
>
|
|
114
|
+
Approve
|
|
115
|
+
</ApprovalButton>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Obligation chip + quorum dots (HANDOFF §6).
|
|
3
|
+
*
|
|
4
|
+
* `Obligation` renders one obligation as a chip: host-supplied icon (via
|
|
5
|
+
* `renderIcon` from the stable `ObligationIcon` key) + label + optional
|
|
6
|
+
* pre-formatted mono value. An obligation whose `kind` is outside the
|
|
7
|
+
* known `PolicyObligationKind` map fails SAFE to a visible "unknown"
|
|
8
|
+
* chip (shield glyph + the raw kind) — never a silent drop.
|
|
9
|
+
*
|
|
10
|
+
* `QuorumDots` renders `have/need` approver dots (filled = collected).
|
|
11
|
+
*/
|
|
12
|
+
import { OBLIGATION_DISPLAY, ObligationIcon } from './obligation-display';
|
|
13
|
+
|
|
14
|
+
import type { ApprovalObligation } from './approval-model';
|
|
15
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
16
|
+
|
|
17
|
+
export interface ObligationProps {
|
|
18
|
+
readonly obligation: ApprovalObligation;
|
|
19
|
+
/** Host icon renderer for a stable `ObligationIcon` key (kit is icon-agnostic). */
|
|
20
|
+
readonly renderIcon: (key: ObligationIcon, size: number) => ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Obligation({
|
|
24
|
+
obligation,
|
|
25
|
+
renderIcon,
|
|
26
|
+
}: ObligationProps): ReactElement {
|
|
27
|
+
// Known kinds resolve to a label + icon key; an unknown discriminant
|
|
28
|
+
// (a not-yet-mapped kernel kind) renders a visible fallback rather than
|
|
29
|
+
// disappearing — fail-fast, but on screen.
|
|
30
|
+
const meta = OBLIGATION_DISPLAY[obligation.kind];
|
|
31
|
+
const iconKey = meta?.iconKey ?? ObligationIcon.ShieldCheck;
|
|
32
|
+
const label = meta?.label ?? `unknown: ${obligation.kind}`;
|
|
33
|
+
return (
|
|
34
|
+
<span
|
|
35
|
+
style={{
|
|
36
|
+
display: 'inline-flex',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
gap: 5,
|
|
39
|
+
padding: '3px 8px',
|
|
40
|
+
borderRadius: 6,
|
|
41
|
+
whiteSpace: 'nowrap',
|
|
42
|
+
background: 'hsl(var(--muted))',
|
|
43
|
+
border: '1px solid hsl(var(--rule))',
|
|
44
|
+
fontSize: 11.5,
|
|
45
|
+
color: 'hsl(var(--ink-2))',
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<span style={{ color: 'hsl(var(--ink-3))', display: 'inline-flex' }}>
|
|
49
|
+
{renderIcon(iconKey, 12)}
|
|
50
|
+
</span>
|
|
51
|
+
<span style={{ color: 'hsl(var(--ink-3))' }}>{label}</span>
|
|
52
|
+
{obligation.value && (
|
|
53
|
+
<span
|
|
54
|
+
className="mono"
|
|
55
|
+
style={{ fontSize: 11, color: 'hsl(var(--ink))', fontWeight: 500 }}
|
|
56
|
+
>
|
|
57
|
+
{obligation.value}
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
</span>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface QuorumDotsProps {
|
|
65
|
+
readonly have: number;
|
|
66
|
+
readonly need: number;
|
|
67
|
+
/** Host icon for the leading "users" glyph. */
|
|
68
|
+
readonly usersIcon?: ReactNode;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function QuorumDots({
|
|
72
|
+
have,
|
|
73
|
+
need,
|
|
74
|
+
usersIcon,
|
|
75
|
+
}: QuorumDotsProps): ReactElement {
|
|
76
|
+
return (
|
|
77
|
+
<span
|
|
78
|
+
style={{
|
|
79
|
+
display: 'inline-flex',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
gap: 6,
|
|
82
|
+
fontSize: 12,
|
|
83
|
+
color: 'hsl(var(--ink-3))',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{usersIcon}
|
|
87
|
+
<span style={{ display: 'inline-flex', gap: 3 }}>
|
|
88
|
+
{Array.from({ length: need }).map((_, i) => {
|
|
89
|
+
const filled = i < have;
|
|
90
|
+
return (
|
|
91
|
+
<span
|
|
92
|
+
key={`quorum-${i}`}
|
|
93
|
+
style={{
|
|
94
|
+
width: 7,
|
|
95
|
+
height: 7,
|
|
96
|
+
borderRadius: '50%',
|
|
97
|
+
background: filled ? 'hsl(var(--success))' : 'transparent',
|
|
98
|
+
border: `1.5px solid ${
|
|
99
|
+
filled ? 'hsl(var(--success))' : 'hsl(var(--rule-2))'
|
|
100
|
+
}`,
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</span>
|
|
106
|
+
<span className="mono" style={{ fontSize: 11 }}>
|
|
107
|
+
{have}/{need} approvers
|
|
108
|
+
</span>
|
|
109
|
+
</span>
|
|
110
|
+
);
|
|
111
|
+
}
|