@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.
Files changed (221) hide show
  1. package/GENERATIVE.md +101 -0
  2. package/INTERACTIONS.md +51 -0
  3. package/LICENSE +21 -0
  4. package/README.md +57 -0
  5. package/REUSE.md +170 -0
  6. package/REVIEW.md +68 -0
  7. package/THIRD-PARTY-NOTICES.md +28 -0
  8. package/ai-sdk/index.js +96 -0
  9. package/assets/ibm-plex-mono-cyrillic-400-normal.woff2 +0 -0
  10. package/assets/ibm-plex-mono-cyrillic-500-normal.woff2 +0 -0
  11. package/assets/ibm-plex-mono-cyrillic-ext-400-normal.woff2 +0 -0
  12. package/assets/ibm-plex-mono-cyrillic-ext-500-normal.woff2 +0 -0
  13. package/assets/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  14. package/assets/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  15. package/assets/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  16. package/assets/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  17. package/assets/ibm-plex-mono-vietnamese-400-normal.woff2 +0 -0
  18. package/assets/ibm-plex-mono-vietnamese-500-normal.woff2 +0 -0
  19. package/assets/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
  20. package/assets/instrument-sans-latin-wght-normal.woff2 +0 -0
  21. package/assistant-ui/index.js +139 -0
  22. package/chunks/callout-DKMHSnjc.js +342 -0
  23. package/chunks/canvas-BJ15-XxE.js +598 -0
  24. package/chunks/controller-Cfpg0C7v.js +180 -0
  25. package/chunks/core-BCqVRLUy.js +312 -0
  26. package/chunks/text-field-BLtoaZFn.js +41 -0
  27. package/chunks/types-BGF7r1my.js +115 -0
  28. package/chunks/ui-9DyNsYXY.js +967 -0
  29. package/core/index.js +4 -0
  30. package/custom/index.js +64 -0
  31. package/examples/ai-sdk-backend/package.json +11 -0
  32. package/examples/ai-sdk-backend/server.mjs +316 -0
  33. package/examples/custom-backend/copy.mjs +7 -0
  34. package/examples/custom-backend/server.mjs +118 -0
  35. package/examples/generative/specs.json +2094 -0
  36. package/examples/review-backend/copy.mjs +27 -0
  37. package/examples/review-backend/package.json +11 -0
  38. package/examples/review-backend/server.mjs +276 -0
  39. package/examples/web-ai-sdk/index.html +12 -0
  40. package/examples/web-ai-sdk/package.json +21 -0
  41. package/examples/web-ai-sdk/src/copy.ts +1 -0
  42. package/examples/web-ai-sdk/src/main.tsx +64 -0
  43. package/examples/web-ai-sdk/tsconfig.json +13 -0
  44. package/examples/web-generative/index.html +12 -0
  45. package/examples/web-generative/package.json +23 -0
  46. package/examples/web-generative/src/copy.ts +42 -0
  47. package/examples/web-generative/src/main.tsx +74 -0
  48. package/examples/web-generative/tsconfig.json +14 -0
  49. package/examples/web-independent/index.html +12 -0
  50. package/examples/web-independent/package.json +20 -0
  51. package/examples/web-independent/src/main.tsx +32 -0
  52. package/examples/web-independent/tsconfig.json +14 -0
  53. package/examples/web-interactions/index.html +12 -0
  54. package/examples/web-interactions/package.json +21 -0
  55. package/examples/web-interactions/src/copy.ts +215 -0
  56. package/examples/web-interactions/src/examples.tsx +628 -0
  57. package/examples/web-interactions/src/main.tsx +116 -0
  58. package/examples/web-interactions/tsconfig.json +14 -0
  59. package/examples/web-review/index.html +12 -0
  60. package/examples/web-review/package.json +20 -0
  61. package/examples/web-review/src/main.tsx +59 -0
  62. package/examples/web-review/tsconfig.json +14 -0
  63. package/fonts.css +117 -0
  64. package/generative/index.js +1560 -0
  65. package/interactions/index.js +1591 -0
  66. package/langgraph-review/index.js +36 -0
  67. package/licenses/ai-sdk.txt +13 -0
  68. package/licenses/assistant-ui-generative.txt +21 -0
  69. package/licenses/assistant-ui.txt +23 -0
  70. package/licenses/ibm-plex-mono.txt +93 -0
  71. package/licenses/instrument-sans.txt +93 -0
  72. package/licenses/internationalized-date.txt +201 -0
  73. package/licenses/langgraph.txt +21 -0
  74. package/licenses/lucide.txt +43 -0
  75. package/licenses/react-markdown.txt +21 -0
  76. package/licenses/remark-gfm.txt +22 -0
  77. package/licenses/zod.txt +21 -0
  78. package/package.json +138 -0
  79. package/review/index.js +31 -0
  80. package/source/components/avatar.tsx +50 -0
  81. package/source/components/button.tsx +155 -0
  82. package/source/components/callout.tsx +65 -0
  83. package/source/components/canvas/connector.tsx +14 -0
  84. package/source/components/canvas/copy.ts +34 -0
  85. package/source/components/canvas/index.tsx +257 -0
  86. package/source/components/canvas/recorder.ts +36 -0
  87. package/source/components/date-time-field.tsx +77 -0
  88. package/source/components/flow-motion.tsx +109 -0
  89. package/source/components/icons.tsx +275 -0
  90. package/source/components/responsive-overlay.tsx +91 -0
  91. package/source/components/scenario-picker/copy.ts +5 -0
  92. package/source/components/scenario-picker/index.tsx +142 -0
  93. package/source/components/scroll-area.tsx +59 -0
  94. package/source/components/search-picker.tsx +127 -0
  95. package/source/components/segmented-control.tsx +123 -0
  96. package/source/components/skeleton.tsx +11 -0
  97. package/source/components/spinner.tsx +19 -0
  98. package/source/components/superxd-arrow.tsx +22 -0
  99. package/source/components/switch.tsx +45 -0
  100. package/source/components/text-field.tsx +81 -0
  101. package/source/components/thinking.tsx +16 -0
  102. package/source/components/ui/copy.ts +12 -0
  103. package/source/components/ui/display.tsx +276 -0
  104. package/source/components/ui/navigation.tsx +346 -0
  105. package/source/components/ui/portal-theme.tsx +28 -0
  106. package/source/components/ui/selection.tsx +336 -0
  107. package/source/lib/cn.ts +23 -0
  108. package/source/reusable/ai-sdk/index.ts +114 -0
  109. package/source/reusable/assistant-ui/index.tsx +266 -0
  110. package/source/reusable/core/attachments.ts +163 -0
  111. package/source/reusable/core/index.ts +417 -0
  112. package/source/reusable/custom/index.ts +90 -0
  113. package/source/reusable/generative/actions.ts +188 -0
  114. package/source/reusable/generative/copy.ts +47 -0
  115. package/source/reusable/generative/data.tsx +126 -0
  116. package/source/reusable/generative/index.tsx +517 -0
  117. package/source/reusable/generative/schema.ts +327 -0
  118. package/source/reusable/generative/timeline.tsx +214 -0
  119. package/source/reusable/generative/vocabulary.tsx +847 -0
  120. package/source/reusable/interactions/agent.ts +259 -0
  121. package/source/reusable/interactions/artifact.ts +236 -0
  122. package/source/reusable/interactions/context.tsx +38 -0
  123. package/source/reusable/interactions/copy.ts +117 -0
  124. package/source/reusable/interactions/feedback.ts +73 -0
  125. package/source/reusable/interactions/index.ts +11 -0
  126. package/source/reusable/interactions/presentation.tsx +483 -0
  127. package/source/reusable/interactions/store.ts +74 -0
  128. package/source/reusable/interactions/threads.ts +126 -0
  129. package/source/reusable/interactions/web.tsx +574 -0
  130. package/source/reusable/langgraph-review/index.ts +73 -0
  131. package/source/reusable/review/controller.ts +263 -0
  132. package/source/reusable/review/index.ts +47 -0
  133. package/source/reusable/review/types.ts +189 -0
  134. package/source/reusable/ui/index.ts +25 -0
  135. package/source/reusable/uploads/index.ts +48 -0
  136. package/source/reusable/web/attachments/copy.ts +23 -0
  137. package/source/reusable/web/attachments/index.tsx +233 -0
  138. package/source/reusable/web/copy.ts +22 -0
  139. package/source/reusable/web/index.tsx +470 -0
  140. package/source/reusable/web/message/copy.ts +26 -0
  141. package/source/reusable/web/message/index.tsx +388 -0
  142. package/source/reusable/web/message/streaming.ts +64 -0
  143. package/source/reusable/web/message/styles.css +21 -0
  144. package/source/reusable/web/review/copy.ts +78 -0
  145. package/source/reusable/web/review/flow.ts +24 -0
  146. package/source/reusable/web/review/index.tsx +506 -0
  147. package/source/reusable/web/styles.ts +2 -0
  148. package/source/styles/library.css +612 -0
  149. package/source/styles/theme.css +228 -0
  150. package/styles.css +3 -0
  151. package/styles.d.ts +1 -0
  152. package/types/components/avatar.d.ts +14 -0
  153. package/types/components/button.d.ts +36 -0
  154. package/types/components/callout.d.ts +19 -0
  155. package/types/components/canvas/connector.d.ts +2 -0
  156. package/types/components/canvas/copy.d.ts +34 -0
  157. package/types/components/canvas/index.d.ts +33 -0
  158. package/types/components/canvas/recorder.d.ts +10 -0
  159. package/types/components/date-time-field.d.ts +11 -0
  160. package/types/components/flow-motion.d.ts +38 -0
  161. package/types/components/icons.d.ts +138 -0
  162. package/types/components/responsive-overlay.d.ts +15 -0
  163. package/types/components/scenario-picker/copy.d.ts +5 -0
  164. package/types/components/scenario-picker/index.d.ts +14 -0
  165. package/types/components/scroll-area.d.ts +14 -0
  166. package/types/components/search-picker.d.ts +27 -0
  167. package/types/components/segmented-control.d.ts +23 -0
  168. package/types/components/skeleton.d.ts +4 -0
  169. package/types/components/spinner.d.ts +4 -0
  170. package/types/components/superxd-arrow.d.ts +5 -0
  171. package/types/components/switch.d.ts +8 -0
  172. package/types/components/text-field.d.ts +19 -0
  173. package/types/components/thinking.d.ts +8 -0
  174. package/types/components/ui/copy.d.ts +12 -0
  175. package/types/components/ui/display.d.ts +50 -0
  176. package/types/components/ui/navigation.d.ts +63 -0
  177. package/types/components/ui/portal-theme.d.ts +8 -0
  178. package/types/components/ui/selection.d.ts +37 -0
  179. package/types/lib/cn.d.ts +3 -0
  180. package/types/reusable/ai-sdk/index.d.ts +15 -0
  181. package/types/reusable/assistant-ui/index.d.ts +12 -0
  182. package/types/reusable/core/attachments.d.ts +54 -0
  183. package/types/reusable/core/index.d.ts +107 -0
  184. package/types/reusable/custom/index.d.ts +14 -0
  185. package/types/reusable/generative/actions.d.ts +45 -0
  186. package/types/reusable/generative/copy.d.ts +48 -0
  187. package/types/reusable/generative/data.d.ts +4 -0
  188. package/types/reusable/generative/index.d.ts +30 -0
  189. package/types/reusable/generative/schema.d.ts +303 -0
  190. package/types/reusable/generative/timeline.d.ts +21 -0
  191. package/types/reusable/generative/vocabulary.d.ts +34 -0
  192. package/types/reusable/interactions/agent.d.ts +98 -0
  193. package/types/reusable/interactions/artifact.d.ts +118 -0
  194. package/types/reusable/interactions/context.d.ts +130 -0
  195. package/types/reusable/interactions/copy.d.ts +115 -0
  196. package/types/reusable/interactions/feedback.d.ts +34 -0
  197. package/types/reusable/interactions/index.d.ts +10 -0
  198. package/types/reusable/interactions/presentation.d.ts +97 -0
  199. package/types/reusable/interactions/store.d.ts +29 -0
  200. package/types/reusable/interactions/threads.d.ts +38 -0
  201. package/types/reusable/interactions/web.d.ts +22 -0
  202. package/types/reusable/langgraph-review/index.d.ts +5 -0
  203. package/types/reusable/review/controller.d.ts +52 -0
  204. package/types/reusable/review/index.d.ts +12 -0
  205. package/types/reusable/review/types.d.ts +61 -0
  206. package/types/reusable/ui/index.d.ts +19 -0
  207. package/types/reusable/uploads/index.d.ts +11 -0
  208. package/types/reusable/web/attachments/copy.d.ts +25 -0
  209. package/types/reusable/web/attachments/index.d.ts +24 -0
  210. package/types/reusable/web/copy.d.ts +24 -0
  211. package/types/reusable/web/index.d.ts +41 -0
  212. package/types/reusable/web/message/copy.d.ts +27 -0
  213. package/types/reusable/web/message/index.d.ts +56 -0
  214. package/types/reusable/web/message/streaming.d.ts +27 -0
  215. package/types/reusable/web/review/copy.d.ts +72 -0
  216. package/types/reusable/web/review/flow.d.ts +3 -0
  217. package/types/reusable/web/review/index.d.ts +38 -0
  218. package/types/reusable/web/styles.d.ts +2 -0
  219. package/ui/index.js +4 -0
  220. package/uploads/index.js +34 -0
  221. 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
+ }
@@ -0,0 +1,2 @@
1
+ import "../../styles/library.css";
2
+ import "./message/styles.css";