@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,276 @@
1
+ import { useState, type ReactNode, type HTMLAttributes } from "react";
2
+ import {
3
+ ProgressBar,
4
+ Separator as AriaSeparator,
5
+ Table,
6
+ TableHeader,
7
+ Column,
8
+ TableBody,
9
+ Row,
10
+ Cell,
11
+ Text,
12
+ UNSTABLE_ToastQueue as ToastQueue,
13
+ UNSTABLE_ToastRegion as AriaToastRegion,
14
+ UNSTABLE_Toast as AriaToast,
15
+ UNSTABLE_ToastContent as ToastContent,
16
+ type ProgressBarProps,
17
+ type SeparatorProps,
18
+ type SortDescriptor,
19
+ type Selection,
20
+ } from "react-aria-components";
21
+ import { Button } from "../button";
22
+ import { IconX } from "../icons";
23
+ import { cn } from "../../lib/cn";
24
+ import { uiCopy } from "./copy";
25
+
26
+ export function Card({ children, className, ...props }: HTMLAttributes<HTMLElement>) {
27
+ return (
28
+ <section
29
+ {...props}
30
+ className={cn(
31
+ "min-w-0 rounded-surface border border-line-subtle bg-surface p-6 text-fg shadow-surface",
32
+ className,
33
+ )}
34
+ >
35
+ {children}
36
+ </section>
37
+ );
38
+ }
39
+ export type BadgeTone = "neutral" | "info" | "success" | "attention" | "error";
40
+ const tones: Record<BadgeTone, string> = {
41
+ neutral: "bg-surface-subtle text-fg-subtle",
42
+ info: "bg-info-subtle text-info",
43
+ success: "bg-success-subtle text-success",
44
+ attention: "bg-attention-subtle text-attention",
45
+ error: "bg-error-subtle text-error",
46
+ };
47
+ export function Badge({ children, tone = "neutral" }: { children: ReactNode; tone?: BadgeTone }) {
48
+ return (
49
+ <span
50
+ className={cn(
51
+ "inline-flex max-w-full items-center gap-1.5 rounded-cell px-2 py-1 type-caption",
52
+ tones[tone],
53
+ )}
54
+ >
55
+ {children}
56
+ </span>
57
+ );
58
+ }
59
+ export function Progress({
60
+ label,
61
+ className,
62
+ ...props
63
+ }: Omit<ProgressBarProps, "children"> & { label: string }) {
64
+ return (
65
+ <ProgressBar
66
+ {...props}
67
+ aria-label={label}
68
+ className={cn(
69
+ "flex w-full min-w-0 flex-col gap-2",
70
+ typeof className === "string" && className,
71
+ )}
72
+ >
73
+ {({ percentage, valueText, isIndeterminate }) => (
74
+ <>
75
+ <div className="flex items-center justify-between gap-3">
76
+ <span className="type-label">{label}</span>
77
+ {!isIndeterminate && (
78
+ <span className="type-caption text-fg-subtle numeric">{valueText}</span>
79
+ )}
80
+ </div>
81
+ <div className="h-2 overflow-hidden rounded-pill bg-surface-subtle">
82
+ <div
83
+ style={{ width: isIndeterminate ? "100%" : `${percentage}%` }}
84
+ className={cn(
85
+ "h-full rounded-pill bg-brand transition-[width] duration-(--duration-base) motion-reduce:transition-none",
86
+ isIndeterminate && "animate-pulse-soft motion-reduce:animate-none",
87
+ )}
88
+ />
89
+ </div>
90
+ </>
91
+ )}
92
+ </ProgressBar>
93
+ );
94
+ }
95
+ export function Separator({ className, ...props }: SeparatorProps) {
96
+ return (
97
+ <AriaSeparator
98
+ {...props}
99
+ className={cn(
100
+ "border-0 bg-line-subtle",
101
+ props.orientation === "vertical" ? "h-6 w-px" : "h-px w-full",
102
+ typeof className === "string" && className,
103
+ )}
104
+ />
105
+ );
106
+ }
107
+ export function AspectRatio({
108
+ ratio = 16 / 9,
109
+ children,
110
+ className,
111
+ }: {
112
+ ratio?: number;
113
+ children: ReactNode;
114
+ className?: string;
115
+ }) {
116
+ return (
117
+ <div
118
+ style={{ aspectRatio: Number.isFinite(ratio) && ratio > 0 ? ratio : 1 }}
119
+ className={cn("relative w-full min-w-0 overflow-hidden rounded-surface", className)}
120
+ >
121
+ {children}
122
+ </div>
123
+ );
124
+ }
125
+ export function KeyboardHint({ children }: { children: ReactNode }) {
126
+ return (
127
+ <kbd className="inline-flex rounded-cell border border-line-subtle bg-surface-muted px-1.5 py-0.5 font-mono type-caption text-fg-subtle">
128
+ {children}
129
+ </kbd>
130
+ );
131
+ }
132
+ export interface DataColumn {
133
+ id: string;
134
+ label: string;
135
+ sortable?: boolean;
136
+ isRowHeader?: boolean;
137
+ }
138
+ export interface DataRow {
139
+ id: string;
140
+ values: Readonly<Record<string, string | number>>;
141
+ }
142
+ export function sortRows(rows: readonly DataRow[], sort?: SortDescriptor) {
143
+ if (!sort) return [...rows];
144
+ const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
145
+ return [...rows].sort((a, b) => {
146
+ const x = a.values[String(sort.column)] ?? "",
147
+ y = b.values[String(sort.column)] ?? "";
148
+ const value =
149
+ typeof x === "number" && typeof y === "number"
150
+ ? x - y
151
+ : collator.compare(String(x), String(y));
152
+ return sort.direction === "descending" ? -value : value;
153
+ });
154
+ }
155
+ /** Local sorting and selection. Server pagination and virtualisation belong to the host. */
156
+ export function DataTable({
157
+ label,
158
+ columns,
159
+ rows,
160
+ empty = uiCopy.noRows,
161
+ selectionMode = "none",
162
+ selectedKeys,
163
+ onSelectionChange,
164
+ }: {
165
+ label: string;
166
+ columns: readonly DataColumn[];
167
+ rows: readonly DataRow[];
168
+ empty?: string;
169
+ selectionMode?: "none" | "single" | "multiple";
170
+ selectedKeys?: Selection;
171
+ onSelectionChange?: (keys: Selection) => void;
172
+ }) {
173
+ const [sort, setSort] = useState<SortDescriptor>();
174
+ return (
175
+ <div className="max-w-full overflow-x-auto rounded-surface border border-line-subtle bg-surface">
176
+ <Table
177
+ aria-label={label}
178
+ sortDescriptor={sort}
179
+ onSortChange={setSort}
180
+ selectionMode={selectionMode}
181
+ selectedKeys={selectedKeys}
182
+ onSelectionChange={onSelectionChange}
183
+ className="w-full outline-none"
184
+ >
185
+ <TableHeader columns={columns}>
186
+ {(column) => (
187
+ <Column
188
+ id={column.id}
189
+ isRowHeader={column.isRowHeader ?? column === columns[0]}
190
+ allowsSorting={column.sortable}
191
+ className="border-b border-line-subtle bg-surface-muted p-3 text-left type-label text-fg-emphasis outline-none data-focus-visible:focus-ring"
192
+ >
193
+ {({ sortDirection }) => (
194
+ <span className="inline-flex items-center gap-2">
195
+ {column.label}
196
+ {column.sortable && (
197
+ <span aria-hidden="true">
198
+ {sortDirection === "ascending"
199
+ ? "↑"
200
+ : sortDirection === "descending"
201
+ ? "↓"
202
+ : "↕"}
203
+ </span>
204
+ )}
205
+ </span>
206
+ )}
207
+ </Column>
208
+ )}
209
+ </TableHeader>
210
+ <TableBody
211
+ items={sortRows(rows, sort)}
212
+ renderEmptyState={() => <p className="p-6 type-body text-fg-subtle">{empty}</p>}
213
+ >
214
+ {(row) => (
215
+ <Row
216
+ id={row.id}
217
+ columns={columns}
218
+ className="border-b border-line-subtle outline-none last:border-b-0 data-focus-visible:focus-ring data-selected:bg-surface-subtle"
219
+ >
220
+ {(column) => (
221
+ <Cell className="max-w-80 p-3 type-body break-words text-fg outline-none data-focus-visible:focus-ring">
222
+ {row.values[column.id] ?? ""}
223
+ </Cell>
224
+ )}
225
+ </Row>
226
+ )}
227
+ </TableBody>
228
+ </Table>
229
+ </div>
230
+ );
231
+ }
232
+ export interface Notification {
233
+ title: string;
234
+ description?: string;
235
+ }
236
+ export function createNotificationQueue() {
237
+ return new ToastQueue<Notification>({ maxVisibleToasts: 3 });
238
+ }
239
+ export function ToastRegion({
240
+ queue,
241
+ label = uiCopy.notifications,
242
+ }: {
243
+ queue: ToastQueue<Notification>;
244
+ label?: string;
245
+ }) {
246
+ return (
247
+ <AriaToastRegion
248
+ queue={queue}
249
+ aria-label={label}
250
+ className="fixed right-4 bottom-4 z-50 flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-3 outline-none"
251
+ >
252
+ {({ toast }) => (
253
+ <AriaToast
254
+ toast={toast}
255
+ className="rounded-surface border border-line-subtle bg-surface p-4 text-fg shadow-popover outline-none data-entering:animate-toast-in data-exiting:animate-fade-out data-focus-visible:focus-ring"
256
+ >
257
+ <ToastContent>
258
+ <div className="flex items-start justify-between gap-3">
259
+ <Text slot="title" className="type-label text-fg-emphasis">
260
+ {toast.content.title}
261
+ </Text>
262
+ <Button slot="close" size="icon-sm" variant="ghost" aria-label={uiCopy.dismiss}>
263
+ <IconX />
264
+ </Button>
265
+ </div>
266
+ {toast.content.description && (
267
+ <Text slot="description" className="mt-2 block type-body text-fg-subtle">
268
+ {toast.content.description}
269
+ </Text>
270
+ )}
271
+ </ToastContent>
272
+ </AriaToast>
273
+ )}
274
+ </AriaToastRegion>
275
+ );
276
+ }
@@ -0,0 +1,346 @@
1
+ import { PortalTheme } from "./portal-theme";
2
+ import type { ReactNode } from "react";
3
+ import {
4
+ MenuTrigger,
5
+ Menu as AriaMenu,
6
+ MenuItem,
7
+ Popover,
8
+ Text,
9
+ DisclosureGroup,
10
+ Disclosure,
11
+ DisclosurePanel,
12
+ Heading,
13
+ Tabs as AriaTabs,
14
+ TabList,
15
+ Tab,
16
+ TabPanel,
17
+ SelectionIndicator,
18
+ TooltipTrigger,
19
+ Tooltip as AriaTooltip,
20
+ DialogTrigger,
21
+ ModalOverlay,
22
+ Modal,
23
+ Dialog,
24
+ Breadcrumbs as AriaBreadcrumbs,
25
+ Breadcrumb,
26
+ Link,
27
+ type DisclosureGroupProps,
28
+ type TabsProps,
29
+ type TooltipTriggerComponentProps,
30
+ } from "react-aria-components";
31
+ import { Button } from "../button";
32
+ import { IconChevronDown, IconChevronRight, IconX } from "../icons";
33
+ import { cn } from "../../lib/cn";
34
+ import { uiCopy } from "./copy";
35
+
36
+ export interface MenuCommand {
37
+ id: string;
38
+ label: string;
39
+ description?: string;
40
+ isDisabled?: boolean;
41
+ isDestructive?: boolean;
42
+ }
43
+ export function Menu({
44
+ label,
45
+ items,
46
+ onAction,
47
+ isDisabled = false,
48
+ }: {
49
+ label: string;
50
+ items: readonly MenuCommand[];
51
+ onAction: (id: string) => void;
52
+ isDisabled?: boolean;
53
+ }) {
54
+ return (
55
+ <PortalTheme>
56
+ {(theme) => (
57
+ <MenuTrigger>
58
+ <Button isDisabled={isDisabled} className="group/trigger">
59
+ {label}
60
+ <IconChevronDown className="transition-[rotate] duration-(--duration-settle) ease-settle group-aria-expanded/trigger:rotate-180 motion-reduce:transition-none" />
61
+ </Button>
62
+ <Popover
63
+ style={theme.style}
64
+ className={cn(
65
+ theme.className,
66
+ "z-50 max-w-[calc(100vw-2rem)] origin-(--trigger-anchor-point) rounded-surface border border-line-subtle bg-surface p-1 text-fg shadow-popover outline-none data-entering:animate-pop-in data-exiting:animate-pop-out",
67
+ )}
68
+ >
69
+ <AriaMenu
70
+ aria-label={label}
71
+ items={items}
72
+ disabledKeys={items.filter((i) => i.isDisabled).map((i) => i.id)}
73
+ onAction={(key) => onAction(String(key))}
74
+ className="max-w-80 min-w-48 outline-none"
75
+ >
76
+ {(item) => (
77
+ <MenuItem
78
+ id={item.id}
79
+ textValue={item.label}
80
+ className={cn(
81
+ "cursor-pointer rounded-cell px-3 py-2 outline-none data-disabled:opacity-50 data-focused:bg-surface-subtle",
82
+ item.isDestructive ? "text-error" : "text-fg-emphasis",
83
+ )}
84
+ >
85
+ <Text slot="label" className="block type-label">
86
+ {item.label}
87
+ </Text>
88
+ {item.description && (
89
+ <Text slot="description" className="mt-1 block type-caption text-fg-subtle">
90
+ {item.description}
91
+ </Text>
92
+ )}
93
+ </MenuItem>
94
+ )}
95
+ </AriaMenu>
96
+ </Popover>
97
+ </MenuTrigger>
98
+ )}
99
+ </PortalTheme>
100
+ );
101
+ }
102
+ export interface DisclosureItem {
103
+ id: string;
104
+ title: string;
105
+ content: ReactNode;
106
+ isDisabled?: boolean;
107
+ }
108
+ export function Accordion({
109
+ label,
110
+ items,
111
+ className,
112
+ ...props
113
+ }: Omit<DisclosureGroupProps, "children"> & { label: string; items: readonly DisclosureItem[] }) {
114
+ return (
115
+ <DisclosureGroup
116
+ {...props}
117
+ aria-label={label}
118
+ className={cn(
119
+ "w-full min-w-0 divide-y divide-line-subtle",
120
+ typeof className === "string" && className,
121
+ )}
122
+ >
123
+ {items.map((item) => (
124
+ <Disclosure
125
+ key={item.id}
126
+ id={item.id}
127
+ data-accordion-item={item.id}
128
+ isDisabled={item.isDisabled}
129
+ className="group min-w-0 py-1"
130
+ >
131
+ {/* A little room above and below, so the hover and pressed fill floats as its own
132
+ squircle instead of running into the dividers */}
133
+ <Heading>
134
+ <Button
135
+ slot="trigger"
136
+ variant="ghost"
137
+ className="h-auto min-h-control-md w-full justify-between px-control-md-x py-3 text-left whitespace-normal"
138
+ >
139
+ <span className="min-w-0 flex-1 break-words">{item.title}</span>
140
+ <IconChevronDown className="transition-[rotate] duration-(--duration-settle) ease-settle group-data-expanded:rotate-180 motion-reduce:transition-none" />
141
+ </Button>
142
+ </Heading>
143
+ {/* hidden="until-found" preserves the panel box. Keep spacing on its
144
+ contents so a collapsed panel does not leave an empty strip. */}
145
+ <DisclosurePanel className="min-w-0 type-body text-fg">
146
+ <div className="animate-reveal-in px-control-md-x pt-3 pb-4">{item.content}</div>
147
+ </DisclosurePanel>
148
+ </Disclosure>
149
+ ))}
150
+ </DisclosureGroup>
151
+ );
152
+ }
153
+ export interface ContentTab {
154
+ id: string;
155
+ label: string;
156
+ content: ReactNode;
157
+ isDisabled?: boolean;
158
+ }
159
+ export function Tabs({
160
+ label,
161
+ items,
162
+ className,
163
+ ...props
164
+ }: Omit<TabsProps, "children"> & { label: string; items: readonly ContentTab[] }) {
165
+ return (
166
+ <AriaTabs
167
+ {...props}
168
+ className={cn("w-full min-w-0", typeof className === "string" && className)}
169
+ >
170
+ <TabList aria-label={label} className="flex flex-wrap gap-1 border-b border-line-subtle">
171
+ {items.map((item) => (
172
+ <Tab
173
+ key={item.id}
174
+ id={item.id}
175
+ isDisabled={item.isDisabled}
176
+ className="relative cursor-pointer rounded-t-control border-b-2 border-transparent px-3 py-2 type-label text-fg-subtle transition-colors duration-(--duration-snap) ease-snap outline-none data-disabled:opacity-50 data-focus-visible:focus-ring data-hovered:text-fg-emphasis data-selected:text-fg-emphasis"
177
+ >
178
+ {item.label}
179
+ {/* The underline slides between tabs and settles */}
180
+ <SelectionIndicator className="absolute inset-x-0 -bottom-0.5 h-0.5 bg-brand transition-[translate,width] duration-(--duration-settle) ease-settle" />
181
+ </Tab>
182
+ ))}
183
+ </TabList>
184
+ {items.map((item) => (
185
+ <TabPanel
186
+ key={item.id}
187
+ id={item.id}
188
+ className="min-w-0 py-4 outline-none data-focus-visible:focus-ring"
189
+ >
190
+ {item.content}
191
+ </TabPanel>
192
+ ))}
193
+ </AriaTabs>
194
+ );
195
+ }
196
+ export function Tooltip({
197
+ children,
198
+ content,
199
+ ...props
200
+ }: Omit<TooltipTriggerComponentProps, "children"> & { children: ReactNode; content: ReactNode }) {
201
+ return (
202
+ <PortalTheme>
203
+ {(theme) => (
204
+ <TooltipTrigger {...props}>
205
+ {children}
206
+ <AriaTooltip
207
+ style={theme.style}
208
+ placement="top"
209
+ className={cn(
210
+ theme.className,
211
+ "z-50 max-w-64 origin-(--trigger-anchor-point) rounded-control border border-line-subtle bg-surface px-3 py-2 type-caption text-fg-emphasis shadow-popover data-entering:animate-pop-in data-exiting:animate-pop-out",
212
+ )}
213
+ >
214
+ {content}
215
+ </AriaTooltip>
216
+ </TooltipTrigger>
217
+ )}
218
+ </PortalTheme>
219
+ );
220
+ }
221
+ /** A labelled, controlled modal. The host can prevent dismissal while a mutation settles. */
222
+ export function ModalDialog({
223
+ title,
224
+ children,
225
+ isOpen,
226
+ onOpenChange,
227
+ isDismissDisabled = false,
228
+ trigger,
229
+ }: {
230
+ title: string;
231
+ children: ReactNode;
232
+ isOpen: boolean;
233
+ onOpenChange: (open: boolean) => void;
234
+ isDismissDisabled?: boolean;
235
+ trigger: ReactNode;
236
+ }) {
237
+ return (
238
+ <PortalTheme>
239
+ {(theme) => (
240
+ <DialogTrigger isOpen={isOpen} onOpenChange={onOpenChange}>
241
+ {trigger}
242
+ <ModalOverlay
243
+ style={theme.style}
244
+ isDismissable={!isDismissDisabled}
245
+ isKeyboardDismissDisabled={isDismissDisabled}
246
+ className={cn(
247
+ theme.className,
248
+ "fixed inset-0 z-50 flex items-center justify-center bg-overlay p-4 data-entering:animate-fade-in data-exiting:animate-fade-out",
249
+ )}
250
+ >
251
+ <Modal className="max-h-[calc(100dvh-2rem)] w-full max-w-lg overflow-y-auto rounded-surface border border-line-subtle bg-surface p-6 text-fg shadow-dialog data-entering:animate-dialog-settle data-exiting:animate-pop-out">
252
+ <Dialog className="min-w-0 outline-none">
253
+ {({ close }) => (
254
+ <>
255
+ <div className="mb-4 flex items-start justify-between gap-4">
256
+ <Heading slot="title" className="type-heading text-fg-emphasis">
257
+ {title}
258
+ </Heading>
259
+ <Button
260
+ variant="ghost"
261
+ size="icon-sm"
262
+ aria-label={uiCopy.close}
263
+ isDisabled={isDismissDisabled}
264
+ onPress={close}
265
+ >
266
+ <IconX />
267
+ </Button>
268
+ </div>
269
+ {children}
270
+ </>
271
+ )}
272
+ </Dialog>
273
+ </Modal>
274
+ </ModalOverlay>
275
+ </DialogTrigger>
276
+ )}
277
+ </PortalTheme>
278
+ );
279
+ }
280
+ export function Pagination({
281
+ label,
282
+ page,
283
+ total,
284
+ onChange,
285
+ isDisabled = false,
286
+ }: {
287
+ label: string;
288
+ page: number;
289
+ total: number;
290
+ onChange: (page: number) => void;
291
+ isDisabled?: boolean;
292
+ }) {
293
+ const count = Math.max(1, Math.floor(Number.isFinite(total) ? total : 1));
294
+ const current = Math.min(count, Math.max(1, Math.floor(Number.isFinite(page) ? page : 1)));
295
+ return (
296
+ <nav aria-label={label} className="flex flex-wrap items-center justify-between gap-3">
297
+ <Button
298
+ size="sm"
299
+ aria-label={uiCopy.previous}
300
+ isDisabled={isDisabled || current === 1}
301
+ onPress={() => onChange(current - 1)}
302
+ >
303
+ <IconChevronRight className="rotate-180" />
304
+ {uiCopy.previous}
305
+ </Button>
306
+ <span aria-current="page" className="type-caption text-fg-subtle numeric">
307
+ {uiCopy.page(current, count)}
308
+ </span>
309
+ <Button
310
+ size="sm"
311
+ aria-label={uiCopy.next}
312
+ isDisabled={isDisabled || current === count}
313
+ onPress={() => onChange(current + 1)}
314
+ >
315
+ {uiCopy.next}
316
+ <IconChevronRight />
317
+ </Button>
318
+ </nav>
319
+ );
320
+ }
321
+ export function Breadcrumbs({
322
+ label,
323
+ items,
324
+ }: {
325
+ label: string;
326
+ items: readonly { id: string; label: string; href?: string }[];
327
+ }) {
328
+ return (
329
+ <AriaBreadcrumbs
330
+ aria-label={label}
331
+ className="flex flex-wrap items-center gap-2 type-caption text-fg-subtle"
332
+ >
333
+ {items.map((item, index) => (
334
+ <Breadcrumb key={item.id} className="inline-flex items-center gap-2">
335
+ <Link
336
+ href={item.href}
337
+ className="rounded-cell outline-none data-focus-visible:focus-ring data-hovered:underline"
338
+ >
339
+ {item.label}
340
+ </Link>
341
+ {index < items.length - 1 && <IconChevronRight className="size-3" />}
342
+ </Breadcrumb>
343
+ ))}
344
+ </AriaBreadcrumbs>
345
+ );
346
+ }
@@ -0,0 +1,28 @@
1
+ import { useRef, useState, type ReactNode, type CSSProperties } from "react";
2
+ /** Portals leave scoped preview/host themes. Copy their computed variables at the trigger. */
3
+ export function PortalTheme({
4
+ children,
5
+ }: {
6
+ children: (theme: { className: string; style: CSSProperties }) => ReactNode;
7
+ }) {
8
+ const root = useRef<HTMLDivElement>(null);
9
+ const [theme, setTheme] = useState<{ className: string; style: CSSProperties }>({
10
+ className: "sxd",
11
+ style: {},
12
+ });
13
+ const sync = () => {
14
+ if (!root.current) return;
15
+ const computed = getComputedStyle(root.current);
16
+ const variables: Record<string, string> = {};
17
+ for (let index = 0; index < computed.length; index++) {
18
+ const name = computed.item(index);
19
+ if (name.startsWith("--")) variables[name] = computed.getPropertyValue(name);
20
+ }
21
+ setTheme({ className: root.current.closest(".dark") ? "sxd dark" : "sxd", style: variables });
22
+ };
23
+ return (
24
+ <div ref={root} className="contents" onPointerDownCapture={sync} onFocusCapture={sync}>
25
+ {children(theme)}
26
+ </div>
27
+ );
28
+ }