@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,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
|
+
}
|