@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.
Files changed (197) hide show
  1. package/dist/lib/biome-host/agent-validation.d.ts +22 -0
  2. package/dist/lib/biome-host/agent-validation.d.ts.map +1 -0
  3. package/dist/lib/biome-host/agent-validation.js +127 -0
  4. package/dist/lib/biome-host/agent-validation.js.map +1 -0
  5. package/dist/lib/biome-host/create-biome-orval-config.d.ts +14 -0
  6. package/dist/lib/biome-host/create-biome-orval-config.d.ts.map +1 -0
  7. package/dist/lib/biome-host/create-biome-orval-config.js +22 -0
  8. package/dist/lib/biome-host/create-biome-orval-config.js.map +1 -0
  9. package/dist/lib/biome-host/frontend-biome.d.ts +3 -1
  10. package/dist/lib/biome-host/frontend-biome.d.ts.map +1 -1
  11. package/dist/lib/biome-host/host-bridge.d.ts +2 -0
  12. package/dist/lib/biome-host/host-bridge.d.ts.map +1 -1
  13. package/dist/lib/biome-host/host-bridge.js.map +1 -1
  14. package/dist/lib/biome-host/host-sources.d.ts +2 -0
  15. package/dist/lib/biome-host/host-sources.d.ts.map +1 -1
  16. package/dist/lib/biome-host/index.d.ts +2 -1
  17. package/dist/lib/biome-host/index.d.ts.map +1 -1
  18. package/dist/lib/biome-host/index.js +2 -1
  19. package/dist/lib/biome-host/index.js.map +1 -1
  20. package/dist/registry/index.d.ts +1 -1
  21. package/dist/registry/index.d.ts.map +1 -1
  22. package/dist/registry/index.js +1 -1
  23. package/dist/registry/index.js.map +1 -1
  24. package/dist/registry/lib/agent-validation-host.d.ts +3 -0
  25. package/dist/registry/lib/agent-validation-host.d.ts.map +1 -0
  26. package/dist/registry/lib/agent-validation-host.js +10 -0
  27. package/dist/registry/lib/agent-validation-host.js.map +1 -0
  28. package/dist/session-kit/approvals/ApprovalButton.d.ts +14 -0
  29. package/dist/session-kit/approvals/ApprovalButton.d.ts.map +1 -0
  30. package/dist/session-kit/approvals/ApprovalButton.js +45 -0
  31. package/dist/session-kit/approvals/ApprovalButton.js.map +1 -0
  32. package/dist/session-kit/approvals/ApprovalCard.d.ts +12 -0
  33. package/dist/session-kit/approvals/ApprovalCard.d.ts.map +1 -0
  34. package/dist/session-kit/approvals/ApprovalCard.js +117 -0
  35. package/dist/session-kit/approvals/ApprovalCard.js.map +1 -0
  36. package/dist/session-kit/approvals/ApprovalsCenter.d.ts +11 -0
  37. package/dist/session-kit/approvals/ApprovalsCenter.d.ts.map +1 -0
  38. package/dist/session-kit/approvals/ApprovalsCenter.js +127 -0
  39. package/dist/session-kit/approvals/ApprovalsCenter.js.map +1 -0
  40. package/dist/session-kit/approvals/CapabilityApprovalStickyBar.d.ts +12 -0
  41. package/dist/session-kit/approvals/CapabilityApprovalStickyBar.d.ts.map +1 -0
  42. package/dist/session-kit/approvals/CapabilityApprovalStickyBar.js +36 -0
  43. package/dist/session-kit/approvals/CapabilityApprovalStickyBar.js.map +1 -0
  44. package/dist/session-kit/approvals/Obligation.d.ts +15 -0
  45. package/dist/session-kit/approvals/Obligation.d.ts.map +1 -0
  46. package/dist/session-kit/approvals/Obligation.js +42 -0
  47. package/dist/session-kit/approvals/Obligation.js.map +1 -0
  48. package/dist/session-kit/approvals/ScopedBody.d.ts +9 -0
  49. package/dist/session-kit/approvals/ScopedBody.d.ts.map +1 -0
  50. package/dist/session-kit/approvals/ScopedBody.js +145 -0
  51. package/dist/session-kit/approvals/ScopedBody.js.map +1 -0
  52. package/dist/session-kit/approvals/approval-icons.d.ts +15 -0
  53. package/dist/session-kit/approvals/approval-icons.d.ts.map +1 -0
  54. package/dist/session-kit/approvals/approval-icons.js +3 -0
  55. package/dist/session-kit/approvals/approval-icons.js.map +1 -0
  56. package/dist/session-kit/approvals/approval-model.d.ts +83 -0
  57. package/dist/session-kit/approvals/approval-model.d.ts.map +1 -0
  58. package/dist/session-kit/approvals/approval-model.js +25 -0
  59. package/dist/session-kit/approvals/approval-model.js.map +1 -0
  60. package/dist/session-kit/approvals/index.d.ts +12 -0
  61. package/dist/session-kit/approvals/index.d.ts.map +1 -0
  62. package/dist/session-kit/approvals/index.js +28 -0
  63. package/dist/session-kit/approvals/index.js.map +1 -0
  64. package/dist/session-kit/approvals/obligation-display.d.ts +17 -0
  65. package/dist/session-kit/approvals/obligation-display.d.ts.map +1 -0
  66. package/dist/session-kit/approvals/obligation-display.js +58 -0
  67. package/dist/session-kit/approvals/obligation-display.js.map +1 -0
  68. package/dist/session-kit/approvals/risk-accent.d.ts +8 -0
  69. package/dist/session-kit/approvals/risk-accent.d.ts.map +1 -0
  70. package/dist/session-kit/approvals/risk-accent.js +28 -0
  71. package/dist/session-kit/approvals/risk-accent.js.map +1 -0
  72. package/dist/session-kit/approvals/scope-icons.d.ts +12 -0
  73. package/dist/session-kit/approvals/scope-icons.d.ts.map +1 -0
  74. package/dist/session-kit/approvals/scope-icons.js +14 -0
  75. package/dist/session-kit/approvals/scope-icons.js.map +1 -0
  76. package/dist/session-kit/combobox/Combobox.d.ts +46 -0
  77. package/dist/session-kit/combobox/Combobox.d.ts.map +1 -0
  78. package/dist/session-kit/combobox/Combobox.js +113 -0
  79. package/dist/session-kit/combobox/Combobox.js.map +1 -0
  80. package/dist/session-kit/combobox/use-click-outside.d.ts +3 -0
  81. package/dist/session-kit/combobox/use-click-outside.d.ts.map +1 -0
  82. package/dist/session-kit/combobox/use-click-outside.js +18 -0
  83. package/dist/session-kit/combobox/use-click-outside.js.map +1 -0
  84. package/dist/session-kit/display/ContextHeader.d.ts +27 -0
  85. package/dist/session-kit/display/ContextHeader.d.ts.map +1 -0
  86. package/dist/session-kit/display/ContextHeader.js +47 -0
  87. package/dist/session-kit/display/ContextHeader.js.map +1 -0
  88. package/dist/session-kit/display/FileDiffCard.d.ts +18 -0
  89. package/dist/session-kit/display/FileDiffCard.d.ts.map +1 -0
  90. package/dist/session-kit/display/FileDiffCard.js +58 -0
  91. package/dist/session-kit/display/FileDiffCard.js.map +1 -0
  92. package/dist/session-kit/display/MD.d.ts +7 -0
  93. package/dist/session-kit/display/MD.d.ts.map +1 -0
  94. package/dist/session-kit/display/MD.js +89 -0
  95. package/dist/session-kit/display/MD.js.map +1 -0
  96. package/dist/session-kit/display/MessageTurn.d.ts +21 -0
  97. package/dist/session-kit/display/MessageTurn.d.ts.map +1 -0
  98. package/dist/session-kit/display/MessageTurn.js +62 -0
  99. package/dist/session-kit/display/MessageTurn.js.map +1 -0
  100. package/dist/session-kit/display/ThinkingPanel.d.ts +12 -0
  101. package/dist/session-kit/display/ThinkingPanel.d.ts.map +1 -0
  102. package/dist/session-kit/display/ThinkingPanel.js +30 -0
  103. package/dist/session-kit/display/ThinkingPanel.js.map +1 -0
  104. package/dist/session-kit/display/TodoChecklist.d.ts +17 -0
  105. package/dist/session-kit/display/TodoChecklist.d.ts.map +1 -0
  106. package/dist/session-kit/display/TodoChecklist.js +50 -0
  107. package/dist/session-kit/display/TodoChecklist.js.map +1 -0
  108. package/dist/session-kit/display/TokenMeter.d.ts +15 -0
  109. package/dist/session-kit/display/TokenMeter.d.ts.map +1 -0
  110. package/dist/session-kit/display/TokenMeter.js +35 -0
  111. package/dist/session-kit/display/TokenMeter.js.map +1 -0
  112. package/dist/session-kit/display/ToolStrip.d.ts +31 -0
  113. package/dist/session-kit/display/ToolStrip.d.ts.map +1 -0
  114. package/dist/session-kit/display/ToolStrip.js +99 -0
  115. package/dist/session-kit/display/ToolStrip.js.map +1 -0
  116. package/dist/session-kit/display/TypingDots.d.ts +3 -0
  117. package/dist/session-kit/display/TypingDots.d.ts.map +1 -0
  118. package/dist/session-kit/display/TypingDots.js +14 -0
  119. package/dist/session-kit/display/TypingDots.js.map +1 -0
  120. package/dist/session-kit/index.d.ts +21 -0
  121. package/dist/session-kit/index.d.ts.map +1 -0
  122. package/dist/session-kit/index.js +37 -0
  123. package/dist/session-kit/index.js.map +1 -0
  124. package/dist/session-kit/lib/enums.d.ts +34 -0
  125. package/dist/session-kit/lib/enums.d.ts.map +1 -0
  126. package/dist/session-kit/lib/enums.js +44 -0
  127. package/dist/session-kit/lib/enums.js.map +1 -0
  128. package/dist/session-kit/lib/portal-accent.d.ts +3 -0
  129. package/dist/session-kit/lib/portal-accent.d.ts.map +1 -0
  130. package/dist/session-kit/lib/portal-accent.js +9 -0
  131. package/dist/session-kit/lib/portal-accent.js.map +1 -0
  132. package/dist/session-kit/lib/status-dot.d.ts +10 -0
  133. package/dist/session-kit/lib/status-dot.d.ts.map +1 -0
  134. package/dist/session-kit/lib/status-dot.js +43 -0
  135. package/dist/session-kit/lib/status-dot.js.map +1 -0
  136. package/dist/session-kit/primitives/Avatar.d.ts +10 -0
  137. package/dist/session-kit/primitives/Avatar.d.ts.map +1 -0
  138. package/dist/session-kit/primitives/Avatar.js +21 -0
  139. package/dist/session-kit/primitives/Avatar.js.map +1 -0
  140. package/dist/session-kit/primitives/PortalGlyph.d.ts +12 -0
  141. package/dist/session-kit/primitives/PortalGlyph.d.ts.map +1 -0
  142. package/dist/session-kit/primitives/PortalGlyph.js +21 -0
  143. package/dist/session-kit/primitives/PortalGlyph.js.map +1 -0
  144. package/dist/session-kit/primitives/RiskPill.d.ts +12 -0
  145. package/dist/session-kit/primitives/RiskPill.d.ts.map +1 -0
  146. package/dist/session-kit/primitives/RiskPill.js +53 -0
  147. package/dist/session-kit/primitives/RiskPill.js.map +1 -0
  148. package/dist/session-kit/primitives/ScopeDot.d.ts +9 -0
  149. package/dist/session-kit/primitives/ScopeDot.d.ts.map +1 -0
  150. package/dist/session-kit/primitives/ScopeDot.js +23 -0
  151. package/dist/session-kit/primitives/ScopeDot.js.map +1 -0
  152. package/dist/session-kit/primitives/Segmented.d.ts +16 -0
  153. package/dist/session-kit/primitives/Segmented.d.ts.map +1 -0
  154. package/dist/session-kit/primitives/Segmented.js +31 -0
  155. package/dist/session-kit/primitives/Segmented.js.map +1 -0
  156. package/package.json +2 -2
  157. package/src/lib/biome-host/create-biome-orval-config.ts +76 -0
  158. package/src/lib/biome-host/frontend-biome.ts +22 -2
  159. package/src/lib/biome-host/host-bridge.ts +22 -0
  160. package/src/lib/biome-host/host-sources.ts +13 -0
  161. package/src/lib/biome-host/index.ts +2 -1
  162. package/src/registry/index.ts +1 -1
  163. package/src/registry/lib/extension-points.ts +1 -1
  164. package/src/session-kit/approvals/ApprovalButton.tsx +89 -0
  165. package/src/session-kit/approvals/ApprovalCard.tsx +336 -0
  166. package/src/session-kit/approvals/ApprovalsCenter.tsx +327 -0
  167. package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +118 -0
  168. package/src/session-kit/approvals/Obligation.tsx +111 -0
  169. package/src/session-kit/approvals/ScopedBody.tsx +392 -0
  170. package/src/session-kit/approvals/approval-icons.ts +31 -0
  171. package/src/session-kit/approvals/approval-model.ts +205 -0
  172. package/src/session-kit/approvals/index.ts +22 -0
  173. package/src/session-kit/approvals/obligation-display.ts +100 -0
  174. package/src/session-kit/approvals/risk-accent.ts +47 -0
  175. package/src/session-kit/approvals/scope-icons.ts +19 -0
  176. package/src/session-kit/combobox/Combobox.tsx +327 -0
  177. package/src/session-kit/combobox/use-click-outside.ts +21 -0
  178. package/src/session-kit/display/ContextHeader.tsx +148 -0
  179. package/src/session-kit/display/FileDiffCard.tsx +140 -0
  180. package/src/session-kit/display/MD.tsx +153 -0
  181. package/src/session-kit/display/MessageTurn.tsx +157 -0
  182. package/src/session-kit/display/ThinkingPanel.tsx +78 -0
  183. package/src/session-kit/display/TodoChecklist.tsx +120 -0
  184. package/src/session-kit/display/TokenMeter.tsx +89 -0
  185. package/src/session-kit/display/ToolStrip.tsx +278 -0
  186. package/src/session-kit/display/TypingDots.tsx +24 -0
  187. package/src/session-kit/index.ts +44 -0
  188. package/src/session-kit/lib/enums.ts +66 -0
  189. package/src/session-kit/lib/portal-accent.ts +30 -0
  190. package/src/session-kit/lib/status-dot.ts +68 -0
  191. package/src/session-kit/primitives/Avatar.tsx +44 -0
  192. package/src/session-kit/primitives/PortalGlyph.tsx +51 -0
  193. package/src/session-kit/primitives/RiskPill.tsx +95 -0
  194. package/src/session-kit/primitives/ScopeDot.tsx +47 -0
  195. package/src/session-kit/primitives/Segmented.tsx +71 -0
  196. /package/src/lib/biome-host/{composition-validation.ts → agent-validation.ts} +0 -0
  197. /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
+ }