@jinaan/uikit 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/card.d.ts.map +1 -1
- package/dist/code-block.d.ts.map +1 -1
- package/dist/dialog-motion.d.ts +18 -0
- package/dist/dialog-motion.d.ts.map +1 -0
- package/dist/dialog.d.ts +4 -3
- package/dist/dialog.d.ts.map +1 -1
- package/dist/dropdown-menu.d.ts.map +1 -1
- package/dist/empty-state.d.ts.map +1 -1
- package/dist/index.js +1092 -903
- package/dist/index.js.map +1 -1
- package/dist/select.d.ts +6 -1
- package/dist/select.d.ts.map +1 -1
- package/dist/table-surface.d.ts +5 -1
- package/dist/table-surface.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent-prompt.tsx +1 -1
- package/src/button.tsx +1 -1
- package/src/card.tsx +83 -40
- package/src/checkbox.tsx +4 -4
- package/src/code-block.tsx +22 -21
- package/src/dialog-motion.ts +38 -0
- package/src/dialog.tsx +288 -33
- package/src/dropdown-menu.tsx +5 -3
- package/src/empty-state.tsx +15 -18
- package/src/label.tsx +1 -1
- package/src/radio-group.tsx +2 -2
- package/src/select.tsx +50 -27
- package/src/table-surface.tsx +23 -1
- package/styles.css +107 -4
package/src/dialog.tsx
CHANGED
|
@@ -1,46 +1,289 @@
|
|
|
1
1
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
2
2
|
import { X } from "@phosphor-icons/react";
|
|
3
3
|
import {
|
|
4
|
+
createContext,
|
|
4
5
|
forwardRef,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
5
11
|
type ComponentPropsWithoutRef,
|
|
6
12
|
type ElementRef,
|
|
7
13
|
type HTMLAttributes,
|
|
8
14
|
} from "react";
|
|
9
15
|
|
|
16
|
+
import { computeDialogFlight, type DialogOriginRect } from "./dialog-motion";
|
|
10
17
|
import { cn } from "./utils";
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
const SOURCE_SELECTOR =
|
|
20
|
+
'button, a, [role="button"], [role="menuitem"], [data-dialog-origin]';
|
|
21
|
+
|
|
22
|
+
let lastSource: { rect: DialogOriginRect; at: number } | null = null;
|
|
23
|
+
let trackerInstalled = false;
|
|
24
|
+
|
|
25
|
+
function rectOf(element: Element): DialogOriginRect | null {
|
|
26
|
+
const rect = element.getBoundingClientRect();
|
|
27
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
28
|
+
return {
|
|
29
|
+
left: rect.left,
|
|
30
|
+
top: rect.top,
|
|
31
|
+
width: rect.width,
|
|
32
|
+
height: rect.height,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rememberSource(target: EventTarget | null) {
|
|
37
|
+
if (!(target instanceof Element)) return;
|
|
38
|
+
const element = target.closest(SOURCE_SELECTOR);
|
|
39
|
+
if (!element) return;
|
|
40
|
+
const rect = rectOf(element);
|
|
41
|
+
if (rect) lastSource = { rect, at: Date.now() };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function installSourceTracker() {
|
|
45
|
+
if (trackerInstalled || typeof document === "undefined") return;
|
|
46
|
+
trackerInstalled = true;
|
|
47
|
+
document.addEventListener(
|
|
48
|
+
"pointerdown",
|
|
49
|
+
(event) => rememberSource(event.target),
|
|
50
|
+
true,
|
|
51
|
+
);
|
|
52
|
+
document.addEventListener(
|
|
53
|
+
"keydown",
|
|
54
|
+
(event) => {
|
|
55
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
56
|
+
rememberSource(document.activeElement);
|
|
57
|
+
},
|
|
58
|
+
true,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveOrigin(content: HTMLElement): DialogOriginRect | null {
|
|
63
|
+
if (lastSource && Date.now() - lastSource.at < 2000) {
|
|
64
|
+
const rect = lastSource.rect;
|
|
65
|
+
lastSource = null;
|
|
66
|
+
return rect;
|
|
67
|
+
}
|
|
68
|
+
lastSource = null;
|
|
69
|
+
|
|
70
|
+
const active = document.activeElement;
|
|
71
|
+
if (
|
|
72
|
+
active instanceof Element &&
|
|
73
|
+
active !== content &&
|
|
74
|
+
!content.contains(active) &&
|
|
75
|
+
active.matches(SOURCE_SELECTOR)
|
|
76
|
+
) {
|
|
77
|
+
return rectOf(active);
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const DIALOG_CLOSE_MS = 250;
|
|
83
|
+
const DialogClosingContext = createContext(false);
|
|
84
|
+
|
|
85
|
+
/** Dialog root; owns the delayed close so the fly-out can finish. */
|
|
86
|
+
export function Dialog({
|
|
87
|
+
open: controlledOpen,
|
|
88
|
+
defaultOpen,
|
|
89
|
+
onOpenChange,
|
|
90
|
+
children,
|
|
91
|
+
...props
|
|
92
|
+
}: ComponentPropsWithoutRef<typeof DialogPrimitive.Root>) {
|
|
93
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(
|
|
94
|
+
defaultOpen ?? false,
|
|
95
|
+
);
|
|
96
|
+
const [closing, setClosing] = useState(false);
|
|
97
|
+
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
98
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
99
|
+
const openRef = useRef(open);
|
|
100
|
+
openRef.current = open;
|
|
101
|
+
const controlled = controlledOpen !== undefined;
|
|
102
|
+
|
|
103
|
+
useEffect(() => installSourceTracker(), []);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (controlledOpen) setClosing(false);
|
|
107
|
+
}, [controlledOpen]);
|
|
108
|
+
|
|
109
|
+
useEffect(
|
|
110
|
+
() => () => {
|
|
111
|
+
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
112
|
+
},
|
|
113
|
+
[],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const handleOpenChange = (next: boolean) => {
|
|
117
|
+
if (next) {
|
|
118
|
+
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
119
|
+
setClosing(false);
|
|
120
|
+
if (!controlled) setUncontrolledOpen(true);
|
|
121
|
+
onOpenChange?.(true);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!open || closing) return;
|
|
125
|
+
|
|
126
|
+
const reduced =
|
|
127
|
+
typeof window !== "undefined" &&
|
|
128
|
+
typeof window.matchMedia === "function" &&
|
|
129
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
130
|
+
if (reduced) {
|
|
131
|
+
if (!controlled) setUncontrolledOpen(false);
|
|
132
|
+
onOpenChange?.(false);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setClosing(true);
|
|
137
|
+
closeTimer.current = setTimeout(() => {
|
|
138
|
+
if (!controlled) setUncontrolledOpen(false);
|
|
139
|
+
onOpenChange?.(false);
|
|
140
|
+
requestAnimationFrame(() => {
|
|
141
|
+
// Restore the overlay only when a controlled parent vetoed the close.
|
|
142
|
+
if (openRef.current) setClosing(false);
|
|
143
|
+
});
|
|
144
|
+
}, DIALOG_CLOSE_MS);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<DialogClosingContext.Provider value={closing}>
|
|
149
|
+
<DialogPrimitive.Root
|
|
150
|
+
open={open}
|
|
151
|
+
onOpenChange={handleOpenChange}
|
|
152
|
+
{...props}
|
|
153
|
+
>
|
|
154
|
+
{children}
|
|
155
|
+
</DialogPrimitive.Root>
|
|
156
|
+
</DialogClosingContext.Provider>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const DialogTrigger = forwardRef<
|
|
161
|
+
ElementRef<typeof DialogPrimitive.Trigger>,
|
|
162
|
+
ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
|
|
163
|
+
>(({ onClick, ...props }, ref) => (
|
|
164
|
+
<DialogPrimitive.Trigger
|
|
165
|
+
ref={ref}
|
|
166
|
+
onClick={(event) => {
|
|
167
|
+
rememberSource(event.currentTarget);
|
|
168
|
+
onClick?.(event);
|
|
169
|
+
}}
|
|
170
|
+
{...props}
|
|
171
|
+
/>
|
|
172
|
+
));
|
|
173
|
+
DialogTrigger.displayName = "DialogTrigger";
|
|
174
|
+
|
|
14
175
|
export const DialogClose = DialogPrimitive.Close;
|
|
15
176
|
|
|
177
|
+
function startDialogMotion(content: HTMLDivElement, retry = true) {
|
|
178
|
+
if (content.dataset.dialogMotionStarted === "true") return;
|
|
179
|
+
if (
|
|
180
|
+
typeof window !== "undefined" &&
|
|
181
|
+
typeof window.matchMedia === "function" &&
|
|
182
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
183
|
+
) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const target = rectOf(content);
|
|
188
|
+
if (!target) {
|
|
189
|
+
if (retry) {
|
|
190
|
+
window.requestAnimationFrame(() => startDialogMotion(content, false));
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
content.dataset.dialogMotionStarted = "true";
|
|
196
|
+
const origin = resolveOrigin(content);
|
|
197
|
+
const flight = origin ? computeDialogFlight(origin, target) : null;
|
|
198
|
+
if (flight) {
|
|
199
|
+
content.style.setProperty("--dialog-dx", `${flight.dx}px`);
|
|
200
|
+
content.style.setProperty("--dialog-dy", `${flight.dy}px`);
|
|
201
|
+
content.style.setProperty("--dialog-sx", String(flight.sx));
|
|
202
|
+
content.style.setProperty("--dialog-sy", String(flight.sy));
|
|
203
|
+
content.classList.add("dialog-fly-in");
|
|
204
|
+
} else {
|
|
205
|
+
content.classList.add("dialog-pop-in");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const clear = () => {
|
|
209
|
+
content.classList.remove("dialog-fly-in", "dialog-pop-in");
|
|
210
|
+
content.style.removeProperty("--dialog-dx");
|
|
211
|
+
content.style.removeProperty("--dialog-dy");
|
|
212
|
+
content.style.removeProperty("--dialog-sx");
|
|
213
|
+
content.style.removeProperty("--dialog-sy");
|
|
214
|
+
};
|
|
215
|
+
const onEnd = (event: AnimationEvent) => {
|
|
216
|
+
if (event.target !== content) return;
|
|
217
|
+
if (
|
|
218
|
+
event.animationName !== "dialog-fly-in" &&
|
|
219
|
+
event.animationName !== "dialog-pop-in"
|
|
220
|
+
) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
content.removeEventListener("animationend", onEnd);
|
|
224
|
+
clear();
|
|
225
|
+
};
|
|
226
|
+
content.addEventListener("animationend", onEnd);
|
|
227
|
+
}
|
|
228
|
+
|
|
16
229
|
export const DialogContent = forwardRef<
|
|
17
230
|
ElementRef<typeof DialogPrimitive.Content>,
|
|
18
231
|
ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
19
|
-
>(({ className, children, ...props }, ref) =>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
232
|
+
>(({ className, children, ...props }, ref) => {
|
|
233
|
+
const closing = useContext(DialogClosingContext);
|
|
234
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
235
|
+
const setRef = useCallback(
|
|
236
|
+
(node: HTMLDivElement | null) => {
|
|
237
|
+
contentRef.current = node;
|
|
238
|
+
if (node) startDialogMotion(node);
|
|
239
|
+
if (typeof ref === "function") {
|
|
240
|
+
ref(node);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (ref) {
|
|
244
|
+
(ref as { current: HTMLDivElement | null }).current = node;
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
[ref],
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<DialogPrimitive.Portal>
|
|
252
|
+
<DialogPrimitive.Overlay
|
|
253
|
+
className={cn(
|
|
254
|
+
"fixed inset-0 z-50 overflow-hidden bg-overlay data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
|
255
|
+
!closing &&
|
|
256
|
+
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
|
|
257
|
+
closing && "opacity-0 transition-opacity duration-200",
|
|
258
|
+
)}
|
|
259
|
+
>
|
|
260
|
+
<span
|
|
261
|
+
aria-hidden="true"
|
|
262
|
+
className="dialog-noise pointer-events-none absolute inset-0"
|
|
263
|
+
/>
|
|
264
|
+
</DialogPrimitive.Overlay>
|
|
265
|
+
<div className="pointer-events-none fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4">
|
|
266
|
+
<DialogPrimitive.Content
|
|
267
|
+
ref={setRef}
|
|
268
|
+
className={cn(
|
|
269
|
+
"dialog-content pointer-events-auto relative flex max-h-[min(85vh,44rem)] w-full max-w-[22.4rem] flex-col gap-0 overflow-hidden will-change-[transform,opacity]",
|
|
270
|
+
"rounded-2xl border border-(--dialog-shell-border) bg-(--dialog-shell-bg) pb-3 shadow-(--dialog-shell-shadow) sm:pb-4",
|
|
271
|
+
closing && "dialog-closing",
|
|
272
|
+
"focus:outline-none",
|
|
273
|
+
className,
|
|
274
|
+
)}
|
|
275
|
+
{...props}
|
|
276
|
+
>
|
|
277
|
+
{children}
|
|
278
|
+
<DialogPrimitive.Close className="absolute right-3.5 top-3.5 z-10 rounded-sm text-subtle transition-colors hover:text-fg focus-visible:outline-none">
|
|
279
|
+
<X className="size-4" />
|
|
280
|
+
<span className="sr-only">Close</span>
|
|
281
|
+
</DialogPrimitive.Close>
|
|
282
|
+
</DialogPrimitive.Content>
|
|
283
|
+
</div>
|
|
284
|
+
</DialogPrimitive.Portal>
|
|
285
|
+
);
|
|
286
|
+
});
|
|
44
287
|
DialogContent.displayName = "DialogContent";
|
|
45
288
|
|
|
46
289
|
/**
|
|
@@ -48,12 +291,15 @@ DialogContent.displayName = "DialogContent";
|
|
|
48
291
|
* and right edges (the shell clips its top corners); the footer stays below on
|
|
49
292
|
* the shell so the two surfaces read as distinct layers.
|
|
50
293
|
*/
|
|
51
|
-
export function DialogPanel({
|
|
294
|
+
export function DialogPanel({
|
|
295
|
+
className,
|
|
296
|
+
...props
|
|
297
|
+
}: HTMLAttributes<HTMLDivElement>) {
|
|
52
298
|
return (
|
|
53
299
|
<div
|
|
54
300
|
data-slot="panel"
|
|
55
301
|
className={cn(
|
|
56
|
-
"flex min-h-0 flex-1 flex-col overflow-hidden rounded-b-2xl border-x-0 border-t-0 border-b border-
|
|
302
|
+
"flex min-h-0 flex-1 flex-col overflow-hidden rounded-b-2xl border-x-0 border-t-0 border-b border-(--dialog-panel-border) bg-(--dialog-panel-bg) shadow-(--dialog-panel-shadow)",
|
|
57
303
|
className,
|
|
58
304
|
)}
|
|
59
305
|
{...props}
|
|
@@ -62,7 +308,10 @@ export function DialogPanel({ className, ...props }: HTMLAttributes<HTMLDivEleme
|
|
|
62
308
|
}
|
|
63
309
|
|
|
64
310
|
/** Scrollable region inside `DialogPanel`; keeps the footer pinned. */
|
|
65
|
-
export function DialogBody({
|
|
311
|
+
export function DialogBody({
|
|
312
|
+
className,
|
|
313
|
+
...props
|
|
314
|
+
}: HTMLAttributes<HTMLDivElement>) {
|
|
66
315
|
return (
|
|
67
316
|
<div
|
|
68
317
|
data-slot="body"
|
|
@@ -72,12 +321,15 @@ export function DialogBody({ className, ...props }: HTMLAttributes<HTMLDivElemen
|
|
|
72
321
|
);
|
|
73
322
|
}
|
|
74
323
|
|
|
75
|
-
export function DialogHeader({
|
|
324
|
+
export function DialogHeader({
|
|
325
|
+
className,
|
|
326
|
+
...props
|
|
327
|
+
}: HTMLAttributes<HTMLDivElement>) {
|
|
76
328
|
return (
|
|
77
329
|
<div
|
|
78
330
|
data-slot="header"
|
|
79
331
|
className={cn(
|
|
80
|
-
"flex shrink-0 flex-col gap-1.5 border-b border-
|
|
332
|
+
"flex shrink-0 flex-col gap-1.5 border-b border-(--dialog-panel-border) px-5 py-4",
|
|
81
333
|
className,
|
|
82
334
|
)}
|
|
83
335
|
{...props}
|
|
@@ -85,7 +337,10 @@ export function DialogHeader({ className, ...props }: HTMLAttributes<HTMLDivElem
|
|
|
85
337
|
);
|
|
86
338
|
}
|
|
87
339
|
|
|
88
|
-
export function DialogFooter({
|
|
340
|
+
export function DialogFooter({
|
|
341
|
+
className,
|
|
342
|
+
...props
|
|
343
|
+
}: HTMLAttributes<HTMLDivElement>) {
|
|
89
344
|
return (
|
|
90
345
|
<div
|
|
91
346
|
data-slot="footer"
|
package/src/dropdown-menu.tsx
CHANGED
|
@@ -16,13 +16,13 @@ export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
|
16
16
|
export const DropdownMenuContent = forwardRef<
|
|
17
17
|
ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
18
18
|
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
19
|
-
>(({ className, sideOffset =
|
|
19
|
+
>(({ className, sideOffset = 8, ...props }, ref) => (
|
|
20
20
|
<DropdownMenuPrimitive.Portal>
|
|
21
21
|
<DropdownMenuPrimitive.Content
|
|
22
22
|
ref={ref}
|
|
23
23
|
sideOffset={sideOffset}
|
|
24
24
|
className={cn(
|
|
25
|
-
"z-50 min-w-48 overflow-hidden rounded-lg border border-line bg-[var(--button-secondary-bg)] p-1
|
|
25
|
+
"menu-surface btn-inner-border z-50 min-w-48 overflow-hidden rounded-lg border border-line bg-[var(--button-secondary-bg)] p-1",
|
|
26
26
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
27
27
|
"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
|
|
28
28
|
"data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
|
|
@@ -39,7 +39,9 @@ DropdownMenuContent.displayName = "DropdownMenuContent";
|
|
|
39
39
|
|
|
40
40
|
export const DropdownMenuItem = forwardRef<
|
|
41
41
|
ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
42
|
-
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
42
|
+
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
43
|
+
inset?: boolean;
|
|
44
|
+
}
|
|
43
45
|
>(({ className, inset, ...props }, ref) => (
|
|
44
46
|
<DropdownMenuPrimitive.Item
|
|
45
47
|
ref={ref}
|
package/src/empty-state.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { TableSurface } from "./table-surface";
|
|
4
4
|
|
|
5
5
|
interface EmptyStateProps {
|
|
6
6
|
icon?: ReactNode;
|
|
@@ -12,24 +12,21 @@ interface EmptyStateProps {
|
|
|
12
12
|
|
|
13
13
|
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
|
|
14
14
|
return (
|
|
15
|
-
<
|
|
16
|
-
className=
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{icon ? (
|
|
22
|
-
<div className="flex size-11 items-center justify-center rounded-xl border border-line bg-elevated text-subtle [&_svg]:size-6">
|
|
23
|
-
{icon}
|
|
24
|
-
</div>
|
|
25
|
-
) : null}
|
|
26
|
-
<div className="space-y-1">
|
|
27
|
-
<p className="text-sm font-medium text-fg">{title}</p>
|
|
28
|
-
{description ? (
|
|
29
|
-
<p className="mx-auto max-w-sm text-sm text-muted">{description}</p>
|
|
15
|
+
<TableSurface className={className}>
|
|
16
|
+
<div className="flex flex-col items-center justify-center gap-4 px-6 py-16 text-center">
|
|
17
|
+
{icon ? (
|
|
18
|
+
<div className="flex size-11 items-center justify-center rounded-xl border border-line bg-elevated text-subtle [&_svg]:size-6">
|
|
19
|
+
{icon}
|
|
20
|
+
</div>
|
|
30
21
|
) : null}
|
|
22
|
+
<div className="space-y-1">
|
|
23
|
+
<p className="text-sm font-medium text-fg">{title}</p>
|
|
24
|
+
{description ? (
|
|
25
|
+
<p className="mx-auto max-w-sm text-sm text-muted">{description}</p>
|
|
26
|
+
) : null}
|
|
27
|
+
</div>
|
|
28
|
+
{action}
|
|
31
29
|
</div>
|
|
32
|
-
|
|
33
|
-
</div>
|
|
30
|
+
</TableSurface>
|
|
34
31
|
);
|
|
35
32
|
}
|
package/src/label.tsx
CHANGED
|
@@ -10,7 +10,7 @@ export const Label = forwardRef<
|
|
|
10
10
|
<LabelPrimitive.Root
|
|
11
11
|
ref={ref}
|
|
12
12
|
className={cn(
|
|
13
|
-
"text-sm font-medium text-fg leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
13
|
+
"mb-1 text-sm font-medium text-fg leading-none last:mb-0 peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
14
14
|
className,
|
|
15
15
|
)}
|
|
16
16
|
{...props}
|
package/src/radio-group.tsx
CHANGED
|
@@ -113,8 +113,8 @@ export function RadioCard({
|
|
|
113
113
|
<span
|
|
114
114
|
aria-hidden
|
|
115
115
|
className={cn(
|
|
116
|
-
"mt-
|
|
117
|
-
"peer-checked:border-
|
|
116
|
+
"mt-[5px] grid size-3.5 shrink-0 place-items-center rounded-full border-2 border-transparent bg-line-strong transition-colors",
|
|
117
|
+
"peer-checked:border-4 peer-checked:border-primary peer-checked:bg-primary-fg",
|
|
118
118
|
"peer-focus-visible:ring-2 peer-focus-visible:ring-focus/40 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-canvas",
|
|
119
119
|
)}
|
|
120
120
|
/>
|
package/src/select.tsx
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
2
2
|
import { CaretDown, Check } from "@phosphor-icons/react";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
forwardRef,
|
|
5
|
+
type ComponentPropsWithoutRef,
|
|
6
|
+
type ElementRef,
|
|
7
|
+
} from "react";
|
|
4
8
|
|
|
5
9
|
import { cn } from "./utils";
|
|
6
10
|
|
|
@@ -8,16 +12,27 @@ export const Select = SelectPrimitive.Root;
|
|
|
8
12
|
export const SelectGroup = SelectPrimitive.Group;
|
|
9
13
|
export const SelectValue = SelectPrimitive.Value;
|
|
10
14
|
|
|
15
|
+
export interface SelectTriggerProps extends ComponentPropsWithoutRef<
|
|
16
|
+
typeof SelectPrimitive.Trigger
|
|
17
|
+
> {
|
|
18
|
+
/** `field` fills a form; `inline` mimics the compact app dropdown. */
|
|
19
|
+
variant?: "field" | "inline";
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
export const SelectTrigger = forwardRef<
|
|
12
23
|
ElementRef<typeof SelectPrimitive.Trigger>,
|
|
13
|
-
|
|
14
|
-
>(({ className, children, ...props }, ref) => (
|
|
24
|
+
SelectTriggerProps
|
|
25
|
+
>(({ className, children, variant = "field", ...props }, ref) => (
|
|
15
26
|
<SelectPrimitive.Trigger
|
|
16
27
|
ref={ref}
|
|
17
28
|
className={cn(
|
|
18
|
-
"flex
|
|
19
|
-
"focus:outline-none
|
|
29
|
+
"flex items-center justify-between gap-2 text-sm text-fg",
|
|
30
|
+
"focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
20
31
|
"[&>span]:truncate",
|
|
32
|
+
variant === "field" &&
|
|
33
|
+
"h-9 w-full rounded-md border border-line bg-surface px-3 focus:border-primary focus:ring-2 focus:ring-focus/30",
|
|
34
|
+
variant === "inline" &&
|
|
35
|
+
"btn-fill btn-inner-border btn-interactive h-8 w-auto rounded-(--control-radius) border-0 bg-(--button-secondary-bg) px-(--control-pad-x-sm) font-medium text-fg hover:bg-(--button-surface-hover) active:bg-(--button-surface-active) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-line-strong",
|
|
21
36
|
className,
|
|
22
37
|
)}
|
|
23
38
|
{...props}
|
|
@@ -33,27 +48,35 @@ SelectTrigger.displayName = "SelectTrigger";
|
|
|
33
48
|
export const SelectContent = forwardRef<
|
|
34
49
|
ElementRef<typeof SelectPrimitive.Content>,
|
|
35
50
|
ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
36
|
-
>(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
>(
|
|
52
|
+
(
|
|
53
|
+
{ className, children, position = "popper", sideOffset = 8, ...props },
|
|
54
|
+
ref,
|
|
55
|
+
) => (
|
|
56
|
+
<SelectPrimitive.Portal>
|
|
57
|
+
<SelectPrimitive.Content
|
|
58
|
+
ref={ref}
|
|
59
|
+
position={position}
|
|
60
|
+
sideOffset={sideOffset}
|
|
61
|
+
className={cn(
|
|
62
|
+
"menu-surface btn-inner-border z-50 min-w-48 overflow-hidden rounded-lg border border-line bg-(--button-secondary-bg) p-1",
|
|
63
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
64
|
+
"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
|
|
65
|
+
"data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
|
|
66
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
|
67
|
+
"data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
68
|
+
"duration-150 ease-out",
|
|
69
|
+
className,
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
<SelectPrimitive.Viewport className="p-0">
|
|
74
|
+
{children}
|
|
75
|
+
</SelectPrimitive.Viewport>
|
|
76
|
+
</SelectPrimitive.Content>
|
|
77
|
+
</SelectPrimitive.Portal>
|
|
78
|
+
),
|
|
79
|
+
);
|
|
57
80
|
SelectContent.displayName = "SelectContent";
|
|
58
81
|
|
|
59
82
|
export const SelectItem = forwardRef<
|
|
@@ -63,7 +86,7 @@ export const SelectItem = forwardRef<
|
|
|
63
86
|
<SelectPrimitive.Item
|
|
64
87
|
ref={ref}
|
|
65
88
|
className={cn(
|
|
66
|
-
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-fg outline-none focus:bg-
|
|
89
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-fg outline-none focus:bg-(--menu-highlight) data-disabled:pointer-events-none data-disabled:opacity-50",
|
|
67
90
|
className,
|
|
68
91
|
)}
|
|
69
92
|
{...props}
|
package/src/table-surface.tsx
CHANGED
|
@@ -16,6 +16,10 @@ export interface TableSurfaceAction {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
interface TableSurfaceProps {
|
|
19
|
+
/** Title above the header row. */
|
|
20
|
+
title?: ReactNode;
|
|
21
|
+
/** Supporting text under the title. */
|
|
22
|
+
description?: ReactNode;
|
|
19
23
|
/** Header row content (a single row of column cells). */
|
|
20
24
|
header?: ReactNode;
|
|
21
25
|
/** Panel content: the body table, or an empty / loading / error state. */
|
|
@@ -65,6 +69,8 @@ function isActionConfig(
|
|
|
65
69
|
* `tone` plus `headerClassName` / `panelClassName`.
|
|
66
70
|
*/
|
|
67
71
|
export function TableSurface({
|
|
72
|
+
title,
|
|
73
|
+
description,
|
|
68
74
|
header,
|
|
69
75
|
children,
|
|
70
76
|
action,
|
|
@@ -74,6 +80,7 @@ export function TableSurface({
|
|
|
74
80
|
panelClassName,
|
|
75
81
|
minHeight,
|
|
76
82
|
}: TableSurfaceProps) {
|
|
83
|
+
const hasTitle = Boolean(title || description);
|
|
77
84
|
const actionNode = isActionConfig(action) ? (
|
|
78
85
|
<Button
|
|
79
86
|
variant={action.variant ?? "primary"}
|
|
@@ -99,9 +106,24 @@ export function TableSurface({
|
|
|
99
106
|
return (
|
|
100
107
|
<TableContainer className={cn("relative", SHELL_TONE[tone], className)}>
|
|
101
108
|
<div className="overflow-x-auto">
|
|
102
|
-
{header ? (
|
|
109
|
+
{header || hasTitle ? (
|
|
103
110
|
// Header rows must not inherit the body row hover surface.
|
|
104
111
|
<div className={cn("[--table-row-hover:transparent]", headerClassName)}>
|
|
112
|
+
{hasTitle ? (
|
|
113
|
+
<div
|
|
114
|
+
className={cn(
|
|
115
|
+
"px-(--table-pad-x) pb-3 pt-4",
|
|
116
|
+
actionNode && "pr-28",
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
{title ? (
|
|
120
|
+
<div className="text-sm font-semibold text-fg">{title}</div>
|
|
121
|
+
) : null}
|
|
122
|
+
{description ? (
|
|
123
|
+
<div className="mt-0.5 text-sm text-muted">{description}</div>
|
|
124
|
+
) : null}
|
|
125
|
+
</div>
|
|
126
|
+
) : null}
|
|
105
127
|
{header}
|
|
106
128
|
</div>
|
|
107
129
|
) : null}
|