@superxd-studio/adptr 0.5.0
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/GENERATIVE.md +101 -0
- package/INTERACTIONS.md +51 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/REUSE.md +170 -0
- package/REVIEW.md +68 -0
- package/THIRD-PARTY-NOTICES.md +28 -0
- package/ai-sdk/index.js +96 -0
- package/assets/ibm-plex-mono-cyrillic-400-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-cyrillic-500-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-cyrillic-ext-400-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-cyrillic-ext-500-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-vietnamese-400-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-vietnamese-500-normal.woff2 +0 -0
- package/assets/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
- package/assets/instrument-sans-latin-wght-normal.woff2 +0 -0
- package/assistant-ui/index.js +139 -0
- package/chunks/callout-DKMHSnjc.js +342 -0
- package/chunks/canvas-BJ15-XxE.js +598 -0
- package/chunks/controller-Cfpg0C7v.js +180 -0
- package/chunks/core-BCqVRLUy.js +312 -0
- package/chunks/text-field-BLtoaZFn.js +41 -0
- package/chunks/types-BGF7r1my.js +115 -0
- package/chunks/ui-9DyNsYXY.js +967 -0
- package/core/index.js +4 -0
- package/custom/index.js +64 -0
- package/examples/ai-sdk-backend/package.json +11 -0
- package/examples/ai-sdk-backend/server.mjs +316 -0
- package/examples/custom-backend/copy.mjs +7 -0
- package/examples/custom-backend/server.mjs +118 -0
- package/examples/generative/specs.json +2094 -0
- package/examples/review-backend/copy.mjs +27 -0
- package/examples/review-backend/package.json +11 -0
- package/examples/review-backend/server.mjs +276 -0
- package/examples/web-ai-sdk/index.html +12 -0
- package/examples/web-ai-sdk/package.json +21 -0
- package/examples/web-ai-sdk/src/copy.ts +1 -0
- package/examples/web-ai-sdk/src/main.tsx +64 -0
- package/examples/web-ai-sdk/tsconfig.json +13 -0
- package/examples/web-generative/index.html +12 -0
- package/examples/web-generative/package.json +23 -0
- package/examples/web-generative/src/copy.ts +42 -0
- package/examples/web-generative/src/main.tsx +74 -0
- package/examples/web-generative/tsconfig.json +14 -0
- package/examples/web-independent/index.html +12 -0
- package/examples/web-independent/package.json +20 -0
- package/examples/web-independent/src/main.tsx +32 -0
- package/examples/web-independent/tsconfig.json +14 -0
- package/examples/web-interactions/index.html +12 -0
- package/examples/web-interactions/package.json +21 -0
- package/examples/web-interactions/src/copy.ts +215 -0
- package/examples/web-interactions/src/examples.tsx +628 -0
- package/examples/web-interactions/src/main.tsx +116 -0
- package/examples/web-interactions/tsconfig.json +14 -0
- package/examples/web-review/index.html +12 -0
- package/examples/web-review/package.json +20 -0
- package/examples/web-review/src/main.tsx +59 -0
- package/examples/web-review/tsconfig.json +14 -0
- package/fonts.css +117 -0
- package/generative/index.js +1560 -0
- package/interactions/index.js +1591 -0
- package/langgraph-review/index.js +36 -0
- package/licenses/ai-sdk.txt +13 -0
- package/licenses/assistant-ui-generative.txt +21 -0
- package/licenses/assistant-ui.txt +23 -0
- package/licenses/ibm-plex-mono.txt +93 -0
- package/licenses/instrument-sans.txt +93 -0
- package/licenses/internationalized-date.txt +201 -0
- package/licenses/langgraph.txt +21 -0
- package/licenses/lucide.txt +43 -0
- package/licenses/react-markdown.txt +21 -0
- package/licenses/remark-gfm.txt +22 -0
- package/licenses/zod.txt +21 -0
- package/package.json +138 -0
- package/review/index.js +31 -0
- package/source/components/avatar.tsx +50 -0
- package/source/components/button.tsx +155 -0
- package/source/components/callout.tsx +65 -0
- package/source/components/canvas/connector.tsx +14 -0
- package/source/components/canvas/copy.ts +34 -0
- package/source/components/canvas/index.tsx +257 -0
- package/source/components/canvas/recorder.ts +36 -0
- package/source/components/date-time-field.tsx +77 -0
- package/source/components/flow-motion.tsx +109 -0
- package/source/components/icons.tsx +275 -0
- package/source/components/responsive-overlay.tsx +91 -0
- package/source/components/scenario-picker/copy.ts +5 -0
- package/source/components/scenario-picker/index.tsx +142 -0
- package/source/components/scroll-area.tsx +59 -0
- package/source/components/search-picker.tsx +127 -0
- package/source/components/segmented-control.tsx +123 -0
- package/source/components/skeleton.tsx +11 -0
- package/source/components/spinner.tsx +19 -0
- package/source/components/superxd-arrow.tsx +22 -0
- package/source/components/switch.tsx +45 -0
- package/source/components/text-field.tsx +81 -0
- package/source/components/thinking.tsx +16 -0
- package/source/components/ui/copy.ts +12 -0
- package/source/components/ui/display.tsx +276 -0
- package/source/components/ui/navigation.tsx +346 -0
- package/source/components/ui/portal-theme.tsx +28 -0
- package/source/components/ui/selection.tsx +336 -0
- package/source/lib/cn.ts +23 -0
- package/source/reusable/ai-sdk/index.ts +114 -0
- package/source/reusable/assistant-ui/index.tsx +266 -0
- package/source/reusable/core/attachments.ts +163 -0
- package/source/reusable/core/index.ts +417 -0
- package/source/reusable/custom/index.ts +90 -0
- package/source/reusable/generative/actions.ts +188 -0
- package/source/reusable/generative/copy.ts +47 -0
- package/source/reusable/generative/data.tsx +126 -0
- package/source/reusable/generative/index.tsx +517 -0
- package/source/reusable/generative/schema.ts +327 -0
- package/source/reusable/generative/timeline.tsx +214 -0
- package/source/reusable/generative/vocabulary.tsx +847 -0
- package/source/reusable/interactions/agent.ts +259 -0
- package/source/reusable/interactions/artifact.ts +236 -0
- package/source/reusable/interactions/context.tsx +38 -0
- package/source/reusable/interactions/copy.ts +117 -0
- package/source/reusable/interactions/feedback.ts +73 -0
- package/source/reusable/interactions/index.ts +11 -0
- package/source/reusable/interactions/presentation.tsx +483 -0
- package/source/reusable/interactions/store.ts +74 -0
- package/source/reusable/interactions/threads.ts +126 -0
- package/source/reusable/interactions/web.tsx +574 -0
- package/source/reusable/langgraph-review/index.ts +73 -0
- package/source/reusable/review/controller.ts +263 -0
- package/source/reusable/review/index.ts +47 -0
- package/source/reusable/review/types.ts +189 -0
- package/source/reusable/ui/index.ts +25 -0
- package/source/reusable/uploads/index.ts +48 -0
- package/source/reusable/web/attachments/copy.ts +23 -0
- package/source/reusable/web/attachments/index.tsx +233 -0
- package/source/reusable/web/copy.ts +22 -0
- package/source/reusable/web/index.tsx +470 -0
- package/source/reusable/web/message/copy.ts +26 -0
- package/source/reusable/web/message/index.tsx +388 -0
- package/source/reusable/web/message/streaming.ts +64 -0
- package/source/reusable/web/message/styles.css +21 -0
- package/source/reusable/web/review/copy.ts +78 -0
- package/source/reusable/web/review/flow.ts +24 -0
- package/source/reusable/web/review/index.tsx +506 -0
- package/source/reusable/web/styles.ts +2 -0
- package/source/styles/library.css +612 -0
- package/source/styles/theme.css +228 -0
- package/styles.css +3 -0
- package/styles.d.ts +1 -0
- package/types/components/avatar.d.ts +14 -0
- package/types/components/button.d.ts +36 -0
- package/types/components/callout.d.ts +19 -0
- package/types/components/canvas/connector.d.ts +2 -0
- package/types/components/canvas/copy.d.ts +34 -0
- package/types/components/canvas/index.d.ts +33 -0
- package/types/components/canvas/recorder.d.ts +10 -0
- package/types/components/date-time-field.d.ts +11 -0
- package/types/components/flow-motion.d.ts +38 -0
- package/types/components/icons.d.ts +138 -0
- package/types/components/responsive-overlay.d.ts +15 -0
- package/types/components/scenario-picker/copy.d.ts +5 -0
- package/types/components/scenario-picker/index.d.ts +14 -0
- package/types/components/scroll-area.d.ts +14 -0
- package/types/components/search-picker.d.ts +27 -0
- package/types/components/segmented-control.d.ts +23 -0
- package/types/components/skeleton.d.ts +4 -0
- package/types/components/spinner.d.ts +4 -0
- package/types/components/superxd-arrow.d.ts +5 -0
- package/types/components/switch.d.ts +8 -0
- package/types/components/text-field.d.ts +19 -0
- package/types/components/thinking.d.ts +8 -0
- package/types/components/ui/copy.d.ts +12 -0
- package/types/components/ui/display.d.ts +50 -0
- package/types/components/ui/navigation.d.ts +63 -0
- package/types/components/ui/portal-theme.d.ts +8 -0
- package/types/components/ui/selection.d.ts +37 -0
- package/types/lib/cn.d.ts +3 -0
- package/types/reusable/ai-sdk/index.d.ts +15 -0
- package/types/reusable/assistant-ui/index.d.ts +12 -0
- package/types/reusable/core/attachments.d.ts +54 -0
- package/types/reusable/core/index.d.ts +107 -0
- package/types/reusable/custom/index.d.ts +14 -0
- package/types/reusable/generative/actions.d.ts +45 -0
- package/types/reusable/generative/copy.d.ts +48 -0
- package/types/reusable/generative/data.d.ts +4 -0
- package/types/reusable/generative/index.d.ts +30 -0
- package/types/reusable/generative/schema.d.ts +303 -0
- package/types/reusable/generative/timeline.d.ts +21 -0
- package/types/reusable/generative/vocabulary.d.ts +34 -0
- package/types/reusable/interactions/agent.d.ts +98 -0
- package/types/reusable/interactions/artifact.d.ts +118 -0
- package/types/reusable/interactions/context.d.ts +130 -0
- package/types/reusable/interactions/copy.d.ts +115 -0
- package/types/reusable/interactions/feedback.d.ts +34 -0
- package/types/reusable/interactions/index.d.ts +10 -0
- package/types/reusable/interactions/presentation.d.ts +97 -0
- package/types/reusable/interactions/store.d.ts +29 -0
- package/types/reusable/interactions/threads.d.ts +38 -0
- package/types/reusable/interactions/web.d.ts +22 -0
- package/types/reusable/langgraph-review/index.d.ts +5 -0
- package/types/reusable/review/controller.d.ts +52 -0
- package/types/reusable/review/index.d.ts +12 -0
- package/types/reusable/review/types.d.ts +61 -0
- package/types/reusable/ui/index.d.ts +19 -0
- package/types/reusable/uploads/index.d.ts +11 -0
- package/types/reusable/web/attachments/copy.d.ts +25 -0
- package/types/reusable/web/attachments/index.d.ts +24 -0
- package/types/reusable/web/copy.d.ts +24 -0
- package/types/reusable/web/index.d.ts +41 -0
- package/types/reusable/web/message/copy.d.ts +27 -0
- package/types/reusable/web/message/index.d.ts +56 -0
- package/types/reusable/web/message/streaming.d.ts +27 -0
- package/types/reusable/web/review/copy.d.ts +72 -0
- package/types/reusable/web/review/flow.d.ts +3 -0
- package/types/reusable/web/review/index.d.ts +38 -0
- package/types/reusable/web/styles.d.ts +2 -0
- package/ui/index.js +4 -0
- package/uploads/index.js +34 -0
- package/web/index.js +931 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import { CanvasSequence, CanvasStage } from "../../../components/canvas/index.js";
|
|
2
|
+
import { canvasCopy } from "../../../components/canvas/copy.js";
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
useSyncExternalStore,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { Checkbox, Disclosure, DisclosurePanel, Button as AriaButton } from "react-aria-components";
|
|
13
|
+
import { Button } from "../../../components/button.js";
|
|
14
|
+
import { Callout } from "../../../components/callout.js";
|
|
15
|
+
import { Spinner } from "../../../components/spinner.js";
|
|
16
|
+
import { IconCheck, IconChevronDown } from "../../../components/icons.js";
|
|
17
|
+
import { FlowFrame, FlowStep, runFlow } from "../../../components/flow-motion.js";
|
|
18
|
+
import { cn } from "../../../lib/cn.js";
|
|
19
|
+
import type { ReviewChange, ReviewSnapshot } from "../../review/types.js";
|
|
20
|
+
import type { ReviewController } from "../../review/controller.js";
|
|
21
|
+
import { reviewCopy, type ReviewCopy } from "./copy.js";
|
|
22
|
+
import { reviewFlow } from "./flow.js";
|
|
23
|
+
export { reviewCopy } from "./copy.js";
|
|
24
|
+
export type { ReviewCopy } from "./copy.js";
|
|
25
|
+
export interface ReviewChangeCardProps {
|
|
26
|
+
change: ReviewChange;
|
|
27
|
+
isSelected?: boolean;
|
|
28
|
+
onSelectionChange?: (selected: boolean) => void;
|
|
29
|
+
isDisabled?: boolean;
|
|
30
|
+
isApplied?: boolean;
|
|
31
|
+
isExpanded?: boolean;
|
|
32
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
33
|
+
/** Side-by-side comparison on a roomy canvas. The content remains the same. */
|
|
34
|
+
comparison?: "columns" | "stacked";
|
|
35
|
+
copy?: ReviewCopy;
|
|
36
|
+
}
|
|
37
|
+
export function ReviewChangeCard({
|
|
38
|
+
change,
|
|
39
|
+
isSelected = false,
|
|
40
|
+
onSelectionChange,
|
|
41
|
+
isDisabled = false,
|
|
42
|
+
isApplied = false,
|
|
43
|
+
isExpanded = true,
|
|
44
|
+
onExpandedChange,
|
|
45
|
+
comparison = "stacked",
|
|
46
|
+
copy = reviewCopy,
|
|
47
|
+
}: ReviewChangeCardProps) {
|
|
48
|
+
return (
|
|
49
|
+
<Disclosure
|
|
50
|
+
isExpanded={isExpanded}
|
|
51
|
+
onExpandedChange={onExpandedChange}
|
|
52
|
+
className={cn(
|
|
53
|
+
"min-w-0 overflow-hidden rounded-surface border bg-surface shadow-control",
|
|
54
|
+
isSelected ? "border-line-emphasis" : "border-line-subtle",
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
<div className="flex min-w-0 items-center gap-3 px-5 py-4">
|
|
58
|
+
<Checkbox
|
|
59
|
+
isSelected={isApplied || isSelected}
|
|
60
|
+
onChange={onSelectionChange}
|
|
61
|
+
isDisabled={isDisabled || isApplied || !onSelectionChange}
|
|
62
|
+
aria-label={copy.selectChange(change.title)}
|
|
63
|
+
className="group flex min-h-control-md min-w-0 flex-1 cursor-pointer items-center gap-3 rounded-control type-label text-fg-emphasis outline-none data-disabled:cursor-default data-focus-visible:focus-ring"
|
|
64
|
+
>
|
|
65
|
+
<span
|
|
66
|
+
className={cn(
|
|
67
|
+
"grid size-5 shrink-0 place-items-center rounded-control border",
|
|
68
|
+
isSelected
|
|
69
|
+
? "border-brand bg-brand text-on-brand"
|
|
70
|
+
: "border-line-emphasis bg-surface",
|
|
71
|
+
isApplied && "border-success-line bg-success-subtle text-success",
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{isApplied || isSelected ? <IconCheck className="size-3.5" /> : null}
|
|
75
|
+
</span>
|
|
76
|
+
<span className="min-w-0 break-words">{change.title}</span>
|
|
77
|
+
</Checkbox>
|
|
78
|
+
{isApplied ? (
|
|
79
|
+
<span className="rounded-pill bg-success-subtle px-2.5 py-1 type-caption text-success">
|
|
80
|
+
{copy.applied}
|
|
81
|
+
</span>
|
|
82
|
+
) : null}
|
|
83
|
+
<AriaButton
|
|
84
|
+
slot="trigger"
|
|
85
|
+
aria-label={copy.viewChange(change.title)}
|
|
86
|
+
className="grid size-control-md shrink-0 place-items-center rounded-control text-fg-subtle outline-none data-focus-visible:focus-ring data-hovered:bg-surface-muted"
|
|
87
|
+
>
|
|
88
|
+
<IconChevronDown
|
|
89
|
+
className={cn(
|
|
90
|
+
"transition-transform duration-(--duration-fast) ease-standard",
|
|
91
|
+
isExpanded && "rotate-180",
|
|
92
|
+
)}
|
|
93
|
+
/>
|
|
94
|
+
</AriaButton>
|
|
95
|
+
</div>
|
|
96
|
+
<DisclosurePanel className="border-t border-line-subtle">
|
|
97
|
+
<div className={cn("grid min-w-0", comparison === "columns" && "grid-cols-2")}>
|
|
98
|
+
<Comparison label={copy.before} muted>
|
|
99
|
+
{change.before}
|
|
100
|
+
</Comparison>
|
|
101
|
+
<Comparison label={copy.after}>{change.after}</Comparison>
|
|
102
|
+
</div>
|
|
103
|
+
<p className="border-t border-line-subtle px-5 py-3 type-caption text-fg-subtle">
|
|
104
|
+
<span className="sr-only">{copy.reason}: </span>
|
|
105
|
+
{change.reason}
|
|
106
|
+
</p>
|
|
107
|
+
</DisclosurePanel>
|
|
108
|
+
</Disclosure>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
function Comparison({
|
|
112
|
+
label,
|
|
113
|
+
children,
|
|
114
|
+
muted = false,
|
|
115
|
+
}: {
|
|
116
|
+
label: string;
|
|
117
|
+
children: ReactNode;
|
|
118
|
+
muted?: boolean;
|
|
119
|
+
}) {
|
|
120
|
+
return (
|
|
121
|
+
<div className={cn("min-w-0 px-5 py-4", muted ? "bg-surface-muted" : "bg-surface")}>
|
|
122
|
+
<p className="mb-2 font-mono type-caption text-fg-subtle">{label}</p>
|
|
123
|
+
<p className="type-body break-words text-fg">{children}</p>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
export interface ReviewDecisionPanelProps {
|
|
128
|
+
snapshot: ReviewSnapshot;
|
|
129
|
+
headingLevel?: 2 | 3;
|
|
130
|
+
onReview: () => void;
|
|
131
|
+
onDecline: () => void;
|
|
132
|
+
onConfirm: () => void;
|
|
133
|
+
onBack: () => void;
|
|
134
|
+
onRecover: () => void;
|
|
135
|
+
onReload: () => void;
|
|
136
|
+
copy?: ReviewCopy;
|
|
137
|
+
}
|
|
138
|
+
/** Provider-free decision display. The controller and the server, not this panel, own authority. */
|
|
139
|
+
export function ReviewDecisionPanel({
|
|
140
|
+
snapshot: s,
|
|
141
|
+
headingLevel = 2,
|
|
142
|
+
onReview,
|
|
143
|
+
onDecline,
|
|
144
|
+
onConfirm,
|
|
145
|
+
onBack,
|
|
146
|
+
onRecover,
|
|
147
|
+
onReload,
|
|
148
|
+
copy = reviewCopy,
|
|
149
|
+
}: ReviewDecisionPanelProps) {
|
|
150
|
+
const Heading = headingLevel === 2 ? "h2" : "h3";
|
|
151
|
+
const { record, decision, status } = s;
|
|
152
|
+
const selected = decision?.changeIds ?? s.selectedIds;
|
|
153
|
+
const total = record?.remainingIds.length ?? 0;
|
|
154
|
+
const confirming = status === "confirming" || status === "submitting";
|
|
155
|
+
const deny = decision?.choice === "deny";
|
|
156
|
+
const title = confirming
|
|
157
|
+
? deny
|
|
158
|
+
? record?.appliedIds.length
|
|
159
|
+
? copy.declineRemainingTitle
|
|
160
|
+
: copy.denyConfirm
|
|
161
|
+
: copy.confirm
|
|
162
|
+
: status === "complete"
|
|
163
|
+
? copy.complete(record?.appliedIds.length ?? 0)
|
|
164
|
+
: status === "denied"
|
|
165
|
+
? copy.denied
|
|
166
|
+
: copy.summary;
|
|
167
|
+
const ready = status === "ready" || status === "partial";
|
|
168
|
+
const done = status === "complete";
|
|
169
|
+
// The panel's own steps: choosing, confirming, and the outcome. Moving between them inks
|
|
170
|
+
// the new content in while the panel resizes (see flow-motion.tsx).
|
|
171
|
+
const step = confirming ? "confirm" : done || status === "denied" ? "outcome" : "choose";
|
|
172
|
+
return (
|
|
173
|
+
<FlowFrame>
|
|
174
|
+
<section
|
|
175
|
+
aria-label={copy.summary}
|
|
176
|
+
className="min-w-0 rounded-surface border border-line-subtle bg-surface p-5"
|
|
177
|
+
>
|
|
178
|
+
<FlowStep key={step}>
|
|
179
|
+
<div className="flex min-w-0 flex-col gap-5">
|
|
180
|
+
<div className="flex items-center gap-3">
|
|
181
|
+
{/* Done earns SuperXD's ultramarine: the mark pops in, then the check lands */}
|
|
182
|
+
{done ? (
|
|
183
|
+
<span className="flex size-8 shrink-0 animate-done-in items-center justify-center rounded-pill bg-done text-on-done">
|
|
184
|
+
<IconCheck className="animate-done-in [animation-delay:var(--duration-pop)]" />
|
|
185
|
+
</span>
|
|
186
|
+
) : null}
|
|
187
|
+
<Heading
|
|
188
|
+
tabIndex={-1}
|
|
189
|
+
data-review-focus
|
|
190
|
+
className="type-heading text-fg-emphasis outline-none"
|
|
191
|
+
>
|
|
192
|
+
{title}
|
|
193
|
+
</Heading>
|
|
194
|
+
</div>
|
|
195
|
+
{record && !["complete", "denied"].includes(status) ? (
|
|
196
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
197
|
+
<p className="font-mono type-caption text-fg-subtle">{copy.target}</p>
|
|
198
|
+
<p className="type-label break-words text-fg-emphasis">
|
|
199
|
+
{record.proposal.destination}
|
|
200
|
+
</p>
|
|
201
|
+
</div>
|
|
202
|
+
) : null}
|
|
203
|
+
{ready || (confirming && !deny) ? (
|
|
204
|
+
<div className="flex flex-col gap-3">
|
|
205
|
+
<p className="type-label text-fg-emphasis">
|
|
206
|
+
{copy.selected(selected.length, total)}
|
|
207
|
+
</p>
|
|
208
|
+
{selected.length ? (
|
|
209
|
+
<ul className="flex flex-col gap-2 type-body text-fg">
|
|
210
|
+
{record?.proposal.changes
|
|
211
|
+
.filter((c) => selected.includes(c.id))
|
|
212
|
+
.map((c) => (
|
|
213
|
+
<li key={c.id} className="flex items-start gap-2">
|
|
214
|
+
<IconCheck className="mt-0.5" />
|
|
215
|
+
<span className="min-w-0 break-words">{c.title}</span>
|
|
216
|
+
</li>
|
|
217
|
+
))}
|
|
218
|
+
</ul>
|
|
219
|
+
) : (
|
|
220
|
+
<p className="type-body text-fg-subtle">{copy.noSelection}</p>
|
|
221
|
+
)}
|
|
222
|
+
{confirming ? (
|
|
223
|
+
<p className="type-body text-fg-subtle">{copy.approvalNote}</p>
|
|
224
|
+
) : null}
|
|
225
|
+
</div>
|
|
226
|
+
) : null}
|
|
227
|
+
{confirming && deny ? (
|
|
228
|
+
<p className="type-body text-fg-subtle">
|
|
229
|
+
{record?.appliedIds.length ? copy.declineRemainingNote : copy.denyNote}
|
|
230
|
+
</p>
|
|
231
|
+
) : null}
|
|
232
|
+
{s.issue === "not-applied" ? (
|
|
233
|
+
<Callout tone="attention" title={copy.notApplied}>
|
|
234
|
+
{copy.notAppliedNote}
|
|
235
|
+
</Callout>
|
|
236
|
+
) : null}
|
|
237
|
+
{status === "uncertain" || status === "checking" ? (
|
|
238
|
+
<Callout tone="attention" title={copy.uncertain}>
|
|
239
|
+
{copy.uncertainNote}
|
|
240
|
+
</Callout>
|
|
241
|
+
) : null}
|
|
242
|
+
{status === "conflict" ? (
|
|
243
|
+
<Callout tone="attention" title={copy.conflict}>
|
|
244
|
+
{copy.conflictNote}
|
|
245
|
+
</Callout>
|
|
246
|
+
) : null}
|
|
247
|
+
{status === "blocked" ? (
|
|
248
|
+
<Callout title={copy.blocked}>{copy.blockedNote}</Callout>
|
|
249
|
+
) : null}
|
|
250
|
+
{status === "partial" ? (
|
|
251
|
+
<Callout tone="attention" title={copy.partial(record?.appliedIds.length ?? 0, total)}>
|
|
252
|
+
{copy.partialNote}
|
|
253
|
+
</Callout>
|
|
254
|
+
) : null}
|
|
255
|
+
{status === "complete" ? (
|
|
256
|
+
<Callout tone="success">
|
|
257
|
+
{copy.kept(
|
|
258
|
+
(record?.proposal.changes.length ?? 0) - (record?.appliedIds.length ?? 0),
|
|
259
|
+
)}
|
|
260
|
+
</Callout>
|
|
261
|
+
) : null}
|
|
262
|
+
{status === "denied" ? (
|
|
263
|
+
<p className="type-body text-fg-subtle">{copy.deniedNote}</p>
|
|
264
|
+
) : null}
|
|
265
|
+
<div className="flex flex-col gap-2">
|
|
266
|
+
{ready ? (
|
|
267
|
+
<>
|
|
268
|
+
<Button variant="primary" isDisabled={!s.selectedIds.length} onPress={onReview}>
|
|
269
|
+
{copy.review(s.selectedIds.length)}
|
|
270
|
+
</Button>
|
|
271
|
+
<Button variant="ghost" onPress={onDecline}>
|
|
272
|
+
{record?.appliedIds.length ? copy.declineRemaining : copy.deny}
|
|
273
|
+
</Button>
|
|
274
|
+
</>
|
|
275
|
+
) : null}
|
|
276
|
+
{confirming ? (
|
|
277
|
+
<>
|
|
278
|
+
<Button
|
|
279
|
+
variant="primary"
|
|
280
|
+
isPending={status === "submitting"}
|
|
281
|
+
pendingLabel={copy.applying}
|
|
282
|
+
onPress={onConfirm}
|
|
283
|
+
>
|
|
284
|
+
{deny ? copy.denyAction : copy.apply(selected.length)}
|
|
285
|
+
</Button>
|
|
286
|
+
{s.issue === "not-applied" ? (
|
|
287
|
+
<Button variant="secondary" onPress={onRecover}>
|
|
288
|
+
{copy.check}
|
|
289
|
+
</Button>
|
|
290
|
+
) : (
|
|
291
|
+
<Button variant="ghost" isDisabled={status === "submitting"} onPress={onBack}>
|
|
292
|
+
{copy.back}
|
|
293
|
+
</Button>
|
|
294
|
+
)}
|
|
295
|
+
</>
|
|
296
|
+
) : null}
|
|
297
|
+
{status === "uncertain" || status === "checking" ? (
|
|
298
|
+
<Button
|
|
299
|
+
isPending={status === "checking"}
|
|
300
|
+
pendingLabel={copy.checking}
|
|
301
|
+
onPress={onRecover}
|
|
302
|
+
>
|
|
303
|
+
{copy.check}
|
|
304
|
+
</Button>
|
|
305
|
+
) : null}
|
|
306
|
+
{status === "conflict" ? <Button onPress={onReload}>{copy.reload}</Button> : null}
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</FlowStep>
|
|
310
|
+
</section>
|
|
311
|
+
</FlowFrame>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
export function useReview(controller: ReviewController) {
|
|
315
|
+
const snapshot = useSyncExternalStore(
|
|
316
|
+
controller.subscribe,
|
|
317
|
+
controller.getSnapshot,
|
|
318
|
+
controller.getSnapshot,
|
|
319
|
+
);
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
controller.load();
|
|
322
|
+
}, [controller]);
|
|
323
|
+
return snapshot;
|
|
324
|
+
}
|
|
325
|
+
export interface DraftReviewProps {
|
|
326
|
+
controller: ReviewController;
|
|
327
|
+
copy?: ReviewCopy;
|
|
328
|
+
className?: string;
|
|
329
|
+
}
|
|
330
|
+
/** The task survives container changes. A comparison becomes stacked disclosure on smaller canvases. */
|
|
331
|
+
/**
|
|
332
|
+
* Like useReview, but a change of step runs as a flow transition. (A store read with
|
|
333
|
+
* useSyncExternalStore always updates at once, so it can't take part in one.)
|
|
334
|
+
*/
|
|
335
|
+
function useReviewFlow(controller: ReviewController) {
|
|
336
|
+
const [snapshot, setSnapshot] = useState(controller.getSnapshot);
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
let last = controller.getSnapshot();
|
|
339
|
+
const update = () => {
|
|
340
|
+
const next = controller.getSnapshot();
|
|
341
|
+
const type = reviewFlow(last.status, next.status);
|
|
342
|
+
last = next;
|
|
343
|
+
runFlow(type, () => setSnapshot(next));
|
|
344
|
+
};
|
|
345
|
+
const unsubscribe = controller.subscribe(update);
|
|
346
|
+
update();
|
|
347
|
+
controller.load();
|
|
348
|
+
return unsubscribe;
|
|
349
|
+
}, [controller]);
|
|
350
|
+
return snapshot;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function DraftReview(props: DraftReviewProps) {
|
|
354
|
+
return (
|
|
355
|
+
<CanvasSequence>
|
|
356
|
+
<ReviewSession {...props} />
|
|
357
|
+
</CanvasSequence>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
function ReviewSession({ controller, copy = reviewCopy, className }: DraftReviewProps) {
|
|
361
|
+
const snapshot = useReviewFlow(controller);
|
|
362
|
+
const root = useRef<HTMLDivElement>(null);
|
|
363
|
+
const focusAfterAction = useRef(false);
|
|
364
|
+
const [roomy, setRoomy] = useState(false);
|
|
365
|
+
const [expanded, setExpanded] = useState<Readonly<Record<string, boolean>>>({});
|
|
366
|
+
const titleId = useId();
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (!root.current) return;
|
|
369
|
+
const element = root.current;
|
|
370
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
371
|
+
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
372
|
+
const breakpoint =
|
|
373
|
+
Number.parseFloat(getComputedStyle(element).getPropertyValue("--breakpoint-md")) || 48;
|
|
374
|
+
setRoomy((entry?.contentRect.width ?? 0) >= breakpoint * rem);
|
|
375
|
+
});
|
|
376
|
+
observer.observe(element);
|
|
377
|
+
return () => observer.disconnect();
|
|
378
|
+
}, []);
|
|
379
|
+
useLayoutEffect(() => {
|
|
380
|
+
if (
|
|
381
|
+
focusAfterAction.current &&
|
|
382
|
+
!["submitting", "checking", "loading"].includes(snapshot.status)
|
|
383
|
+
) {
|
|
384
|
+
// Restore within this task only; moving to another part of the page is respected.
|
|
385
|
+
if (
|
|
386
|
+
root.current?.contains(document.activeElement) ||
|
|
387
|
+
document.activeElement === document.body
|
|
388
|
+
)
|
|
389
|
+
root.current?.querySelector<HTMLElement>("[data-review-focus]")?.focus();
|
|
390
|
+
focusAfterAction.current = false;
|
|
391
|
+
}
|
|
392
|
+
}, [snapshot.status]);
|
|
393
|
+
function action(callback: () => boolean) {
|
|
394
|
+
if (callback()) focusAfterAction.current = true;
|
|
395
|
+
}
|
|
396
|
+
const { record, status } = snapshot;
|
|
397
|
+
// Loading, a failed load and the review itself are separate steps: the review inks in
|
|
398
|
+
const phase =
|
|
399
|
+
status === "idle" || status === "loading"
|
|
400
|
+
? "loading"
|
|
401
|
+
: status === "load-error"
|
|
402
|
+
? "error"
|
|
403
|
+
: "review";
|
|
404
|
+
const busy = ["idle", "loading", "submitting", "checking"].includes(status);
|
|
405
|
+
const label = {
|
|
406
|
+
idle: canvasCopy.loading,
|
|
407
|
+
loading: canvasCopy.loading,
|
|
408
|
+
ready: canvasCopy.review,
|
|
409
|
+
confirming: canvasCopy.confirm,
|
|
410
|
+
submitting: canvasCopy.applying,
|
|
411
|
+
checking: canvasCopy.checking,
|
|
412
|
+
"load-error": canvasCopy.failed,
|
|
413
|
+
uncertain: canvasCopy.uncertain,
|
|
414
|
+
conflict: canvasCopy.conflict,
|
|
415
|
+
blocked: canvasCopy.blocked,
|
|
416
|
+
partial: canvasCopy.partial,
|
|
417
|
+
complete: canvasCopy.complete,
|
|
418
|
+
denied: canvasCopy.denied,
|
|
419
|
+
}[status];
|
|
420
|
+
return (
|
|
421
|
+
<CanvasStage
|
|
422
|
+
phase={`review-${status}-${snapshot.issue ?? ""}`}
|
|
423
|
+
|
|
424
|
+
frameClassName={className}
|
|
425
|
+
label={label}
|
|
426
|
+
terminal={status === "complete" || status === "denied"}
|
|
427
|
+
>
|
|
428
|
+
<div
|
|
429
|
+
ref={root}
|
|
430
|
+
className={cn("@container w-full min-w-0 text-fg", className)}
|
|
431
|
+
aria-busy={busy}
|
|
432
|
+
data-review-status={status}
|
|
433
|
+
>
|
|
434
|
+
<p className="sr-only" role="status" aria-live="polite">
|
|
435
|
+
{copy.announcement[status]}
|
|
436
|
+
</p>
|
|
437
|
+
<header className="mb-6 flex flex-col gap-2">
|
|
438
|
+
<p className="font-mono type-caption text-fg-subtle">{copy.proposed}</p>
|
|
439
|
+
<h2 id={titleId} className="type-title text-fg-emphasis">
|
|
440
|
+
{record?.proposal.title ?? copy.loading}
|
|
441
|
+
</h2>
|
|
442
|
+
</header>
|
|
443
|
+
<FlowStep key={phase}>
|
|
444
|
+
<div className="min-w-0">
|
|
445
|
+
{status === "idle" || status === "loading" ? (
|
|
446
|
+
<div className="flex min-h-64 items-center justify-center gap-3 rounded-surface border border-line-subtle bg-surface">
|
|
447
|
+
<Spinner />
|
|
448
|
+
<p className="type-body text-fg-subtle">{copy.loadingNote}</p>
|
|
449
|
+
</div>
|
|
450
|
+
) : status === "load-error" ? (
|
|
451
|
+
<Callout
|
|
452
|
+
tone="error"
|
|
453
|
+
title={copy.loadError}
|
|
454
|
+
actions={<Button onPress={() => action(controller.load)}>{copy.retry}</Button>}
|
|
455
|
+
>
|
|
456
|
+
{copy.loadErrorNote}
|
|
457
|
+
</Callout>
|
|
458
|
+
) : record && !record.proposal.changes.length ? (
|
|
459
|
+
<Callout title={copy.empty}>{copy.emptyNote}</Callout>
|
|
460
|
+
) : (
|
|
461
|
+
<div className="grid min-w-0 items-start gap-6 @3xl:grid-cols-3">
|
|
462
|
+
<div className="flex min-w-0 flex-col gap-3 @3xl:col-span-2">
|
|
463
|
+
<ul aria-labelledby={titleId} className="flex min-w-0 flex-col gap-3">
|
|
464
|
+
{record?.proposal.changes.map((change) => (
|
|
465
|
+
<li key={`${record.proposal.id}-${change.id}`}>
|
|
466
|
+
<ReviewChangeCard
|
|
467
|
+
change={change}
|
|
468
|
+
isSelected={snapshot.selectedIds.includes(change.id)}
|
|
469
|
+
isApplied={record.appliedIds.includes(change.id)}
|
|
470
|
+
isDisabled={
|
|
471
|
+
!["ready", "partial"].includes(status) ||
|
|
472
|
+
!record.remainingIds.includes(change.id)
|
|
473
|
+
}
|
|
474
|
+
onSelectionChange={(selected) => controller.select(change.id, selected)}
|
|
475
|
+
isExpanded={expanded[change.id] ?? true}
|
|
476
|
+
onExpandedChange={(open) =>
|
|
477
|
+
setExpanded((value) => ({ ...value, [change.id]: open }))
|
|
478
|
+
}
|
|
479
|
+
comparison={roomy ? "columns" : "stacked"}
|
|
480
|
+
copy={copy}
|
|
481
|
+
/>
|
|
482
|
+
</li>
|
|
483
|
+
))}
|
|
484
|
+
</ul>
|
|
485
|
+
</div>
|
|
486
|
+
<div className="min-w-0 @3xl:sticky @3xl:top-6">
|
|
487
|
+
<ReviewDecisionPanel
|
|
488
|
+
snapshot={snapshot}
|
|
489
|
+
headingLevel={3}
|
|
490
|
+
onReview={() => action(() => controller.prepare("approve"))}
|
|
491
|
+
onDecline={() => action(() => controller.prepare("deny"))}
|
|
492
|
+
onConfirm={() => action(controller.submit)}
|
|
493
|
+
onBack={() => action(controller.back)}
|
|
494
|
+
onRecover={() => action(controller.recover)}
|
|
495
|
+
onReload={() => action(controller.load)}
|
|
496
|
+
copy={copy}
|
|
497
|
+
/>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
501
|
+
</div>
|
|
502
|
+
</FlowStep>
|
|
503
|
+
</div>
|
|
504
|
+
</CanvasStage>
|
|
505
|
+
);
|
|
506
|
+
}
|