@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
|
@@ -58,6 +58,28 @@ export interface HostBridgeAuth {
|
|
|
58
58
|
getProjectId(): string | null;
|
|
59
59
|
/** Returns the current actor's user id (subject), or null. */
|
|
60
60
|
getUserId(): string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Proactively refresh the actor token if it is within `minValiditySeconds`
|
|
63
|
+
* of expiry, then return a guaranteed-valid bearer. The refresh + any
|
|
64
|
+
* redirect-on-failure stays HOST-owned (the host owns the auth client and
|
|
65
|
+
* the single-flight login guard) — biomes only reach it through this method
|
|
66
|
+
* so an Orval client's `getAuthToken` can be wired against the SAME refresh
|
|
67
|
+
* path the host uses, instead of each biome re-implementing token freshness.
|
|
68
|
+
*
|
|
69
|
+
* @param minValiditySeconds Refresh when fewer than this many seconds of
|
|
70
|
+
* validity remain. Defaults to a host-chosen safe window.
|
|
71
|
+
* @returns A valid bearer token string. Implementations that cannot produce
|
|
72
|
+
* one (session expired) trigger the host login redirect and reject.
|
|
73
|
+
*/
|
|
74
|
+
ensureFreshToken(minValiditySeconds?: number): Promise<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Handle a 401 from a downstream API call: force-refresh the token and, if
|
|
77
|
+
* that fails, redirect to login. HOST-owned for the same reason as
|
|
78
|
+
* {@link HostBridgeAuth.ensureFreshToken} — there is ONE login-redirect
|
|
79
|
+
* single-flight guard and one refresh-dedup path in the host; biomes funnel
|
|
80
|
+
* 401 recovery through here rather than forking that logic.
|
|
81
|
+
*/
|
|
82
|
+
onUnauthorized(): Promise<void>;
|
|
61
83
|
}
|
|
62
84
|
|
|
63
85
|
export interface HostBridgeToast {
|
|
@@ -25,6 +25,19 @@ export interface AuthSource {
|
|
|
25
25
|
getProjectId(): string | null;
|
|
26
26
|
/** Subject (user) id of the actor, or null when unauthenticated. */
|
|
27
27
|
getUserId(): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Proactively refresh the token if it is within `minValiditySeconds` of
|
|
30
|
+
* expiry and return a valid bearer. Backed by the host's auth client
|
|
31
|
+
* (keycloak-js `updateToken`, …) and its single-flight login redirect.
|
|
32
|
+
* Mirrors {@link HostBridgeAuth.ensureFreshToken} — the bridge delegates
|
|
33
|
+
* straight through to this source.
|
|
34
|
+
*/
|
|
35
|
+
ensureFreshToken(minValiditySeconds?: number): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Force-refresh on a 401 and redirect to login on failure. Backed by the
|
|
38
|
+
* host's auth-guard. Mirrors {@link HostBridgeAuth.onUnauthorized}.
|
|
39
|
+
*/
|
|
40
|
+
onUnauthorized(): Promise<void>;
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
/**
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export * from './frontend-biome';
|
|
16
16
|
export * from './host-bridge';
|
|
17
|
+
export * from './create-biome-orval-config';
|
|
17
18
|
export * from './biome-registry';
|
|
18
19
|
export * from './session-contributions';
|
|
19
20
|
export * from './session-profiles';
|
|
20
21
|
export * from './host-sources';
|
|
21
22
|
export * from './nav';
|
|
22
23
|
export * from './biome-mode';
|
|
23
|
-
export * from './
|
|
24
|
+
export * from './agent-validation';
|
package/src/registry/index.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* readable via the host bridge's location").
|
|
19
19
|
* 2. Mount `<BiomeSlot name={HostExtensionSlots.X} />` in the host
|
|
20
20
|
* page.
|
|
21
|
-
* 3. Document the slot in `
|
|
21
|
+
* 3. Document the slot in the xema-docs repo (`content/biomes/04-frontend-extensions.md`).
|
|
22
22
|
*
|
|
23
23
|
* Removing a host slot is a breaking change — bump the kernel package
|
|
24
24
|
* major version.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal action button used by the approval surfaces (card / sticky bar /
|
|
3
|
+
* Center). The kit carries no button primitive, so this is a small,
|
|
4
|
+
* inline-styled control matching the design's `Btn` (ghost / soft /
|
|
5
|
+
* primary, with an optional accent override for critical-tier approve).
|
|
6
|
+
*
|
|
7
|
+
* Kept inside the approvals folder because it is only the approval
|
|
8
|
+
* actions — it is NOT a general-purpose kit button.
|
|
9
|
+
*/
|
|
10
|
+
import type { CSSProperties, ReactElement, ReactNode } from 'react';
|
|
11
|
+
|
|
12
|
+
export type ApprovalButtonVariant = 'ghost' | 'soft' | 'primary';
|
|
13
|
+
export type ApprovalButtonSize = 'xs' | 'sm';
|
|
14
|
+
|
|
15
|
+
export interface ApprovalButtonProps {
|
|
16
|
+
readonly variant?: ApprovalButtonVariant;
|
|
17
|
+
readonly size?: ApprovalButtonSize;
|
|
18
|
+
/** Accent for the `primary` variant background (e.g. portal/risk accent). */
|
|
19
|
+
readonly accent?: string;
|
|
20
|
+
readonly icon?: ReactNode;
|
|
21
|
+
readonly disabled?: boolean;
|
|
22
|
+
readonly onClick?: () => void;
|
|
23
|
+
readonly children: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PAD: Record<ApprovalButtonSize, string> = {
|
|
27
|
+
xs: '3px 9px',
|
|
28
|
+
sm: '5px 11px',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const FONT: Record<ApprovalButtonSize, number> = { xs: 12, sm: 12.5 };
|
|
32
|
+
|
|
33
|
+
export function ApprovalButton({
|
|
34
|
+
variant = 'ghost',
|
|
35
|
+
size = 'sm',
|
|
36
|
+
accent = 'hsl(var(--primary))',
|
|
37
|
+
icon,
|
|
38
|
+
disabled = false,
|
|
39
|
+
onClick,
|
|
40
|
+
children,
|
|
41
|
+
}: ApprovalButtonProps): ReactElement {
|
|
42
|
+
const base: CSSProperties = {
|
|
43
|
+
display: 'inline-flex',
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
gap: 5,
|
|
46
|
+
padding: PAD[size],
|
|
47
|
+
fontSize: FONT[size],
|
|
48
|
+
fontWeight: 550,
|
|
49
|
+
borderRadius: 7,
|
|
50
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
51
|
+
opacity: disabled ? 0.5 : 1,
|
|
52
|
+
whiteSpace: 'nowrap',
|
|
53
|
+
lineHeight: 1.2,
|
|
54
|
+
};
|
|
55
|
+
const variantStyle = resolveVariantStyle(variant, accent);
|
|
56
|
+
return (
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
onClick={onClick}
|
|
61
|
+
style={{ ...base, ...variantStyle }}
|
|
62
|
+
>
|
|
63
|
+
{icon}
|
|
64
|
+
{children}
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveVariantStyle(
|
|
70
|
+
variant: ApprovalButtonVariant,
|
|
71
|
+
accent: string,
|
|
72
|
+
): CSSProperties {
|
|
73
|
+
switch (variant) {
|
|
74
|
+
case 'primary':
|
|
75
|
+
return { background: accent, color: '#fff', border: '1px solid transparent' };
|
|
76
|
+
case 'soft':
|
|
77
|
+
return {
|
|
78
|
+
background: 'hsl(var(--muted))',
|
|
79
|
+
color: 'hsl(var(--ink))',
|
|
80
|
+
border: '1px solid hsl(var(--rule))',
|
|
81
|
+
};
|
|
82
|
+
case 'ghost':
|
|
83
|
+
return {
|
|
84
|
+
background: 'transparent',
|
|
85
|
+
color: 'hsl(var(--ink-2))',
|
|
86
|
+
border: '1px solid transparent',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -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
|
+
}
|