@ttt-productions/ui-core 0.11.14 → 0.11.16
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/react/components/alert-dialog.js +2 -2
- package/dist/react/components/alert-dialog.js.map +1 -1
- package/dist/react/components/avatar.js +1 -1
- package/dist/react/components/avatar.js.map +1 -1
- package/dist/react/components/badge.d.ts +2 -2
- package/dist/react/components/button.d.ts +11 -2
- package/dist/react/components/button.d.ts.map +1 -1
- package/dist/react/components/button.js +20 -7
- package/dist/react/components/button.js.map +1 -1
- package/dist/react/components/card.js +1 -1
- package/dist/react/components/card.js.map +1 -1
- package/dist/react/components/chunk-error-recovery.js +2 -2
- package/dist/react/components/chunk-error-recovery.js.map +1 -1
- package/dist/react/components/consequence-dialog.d.ts +1 -1
- package/dist/react/components/consequence-dialog.d.ts.map +1 -1
- package/dist/react/components/consequence-dialog.js +2 -3
- package/dist/react/components/consequence-dialog.js.map +1 -1
- package/dist/react/components/dialog.js +2 -2
- package/dist/react/components/dialog.js.map +1 -1
- package/dist/react/components/dropdown-menu.d.ts +8 -0
- package/dist/react/components/dropdown-menu.d.ts.map +1 -1
- package/dist/react/components/dropdown-menu.js +5 -3
- package/dist/react/components/dropdown-menu.js.map +1 -1
- package/dist/react/components/end-of-list-indicator.js +1 -1
- package/dist/react/components/end-of-list-indicator.js.map +1 -1
- package/dist/react/components/input.js +1 -1
- package/dist/react/components/input.js.map +1 -1
- package/dist/react/components/list-pagination.d.ts.map +1 -1
- package/dist/react/components/list-pagination.js +19 -1
- package/dist/react/components/list-pagination.js.map +1 -1
- package/dist/react/components/menubar.js +1 -1
- package/dist/react/components/menubar.js.map +1 -1
- package/dist/react/components/popover.js +1 -1
- package/dist/react/components/popover.js.map +1 -1
- package/dist/react/components/scroll-to-top-button.js +1 -1
- package/dist/react/components/scroll-to-top-button.js.map +1 -1
- package/dist/react/components/search-dropdown.d.ts.map +1 -1
- package/dist/react/components/search-dropdown.js +3 -2
- package/dist/react/components/search-dropdown.js.map +1 -1
- package/dist/react/components/select.d.ts +8 -1
- package/dist/react/components/select.d.ts.map +1 -1
- package/dist/react/components/select.js +3 -2
- package/dist/react/components/select.js.map +1 -1
- package/dist/react/components/sheet.js +2 -2
- package/dist/react/components/sheet.js.map +1 -1
- package/dist/react/components/spinner.d.ts +23 -0
- package/dist/react/components/spinner.d.ts.map +1 -0
- package/dist/react/components/spinner.js +27 -0
- package/dist/react/components/spinner.js.map +1 -0
- package/dist/react/components/switch.d.ts +8 -1
- package/dist/react/components/switch.d.ts.map +1 -1
- package/dist/react/components/switch.js +2 -1
- package/dist/react/components/switch.js.map +1 -1
- package/dist/react/components/table.js +1 -1
- package/dist/react/components/table.js.map +1 -1
- package/dist/react/components/tabs.js +1 -1
- package/dist/react/components/tabs.js.map +1 -1
- package/dist/react/components/textarea.js +1 -1
- package/dist/react/components/textarea.js.map +1 -1
- package/dist/react/components/toast.js +5 -5
- package/dist/react/components/toast.js.map +1 -1
- package/dist/react/components/tooltip.js +1 -1
- package/dist/react/components/tooltip.js.map +1 -1
- package/dist/react/hooks/use-async-action.d.ts +30 -0
- package/dist/react/hooks/use-async-action.d.ts.map +1 -0
- package/dist/react/hooks/use-async-action.js +39 -0
- package/dist/react/hooks/use-async-action.js.map +1 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -1
- package/package.json +8 -4
- package/src/react/components/alert-dialog.tsx +2 -2
- package/src/react/components/avatar.tsx +1 -1
- package/src/react/components/button.tsx +48 -12
- package/src/react/components/card.tsx +1 -1
- package/src/react/components/chunk-error-recovery.tsx +2 -2
- package/src/react/components/consequence-dialog.tsx +4 -6
- package/src/react/components/dialog.tsx +2 -2
- package/src/react/components/dropdown-menu.tsx +21 -4
- package/src/react/components/end-of-list-indicator.tsx +1 -1
- package/src/react/components/input.tsx +1 -1
- package/src/react/components/list-pagination.tsx +24 -4
- package/src/react/components/menubar.tsx +1 -1
- package/src/react/components/popover.tsx +1 -1
- package/src/react/components/scroll-to-top-button.tsx +1 -1
- package/src/react/components/search-dropdown.tsx +5 -4
- package/src/react/components/select.tsx +20 -6
- package/src/react/components/sheet.tsx +2 -2
- package/src/react/components/spinner.tsx +49 -0
- package/src/react/components/switch.tsx +16 -4
- package/src/react/components/table.tsx +1 -1
- package/src/react/components/tabs.tsx +2 -2
- package/src/react/components/textarea.tsx +1 -1
- package/src/react/components/toast.tsx +5 -5
- package/src/react/components/tooltip.tsx +1 -1
- package/src/react/hooks/use-async-action.ts +63 -0
- package/src/react/index.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/ui-core",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.16",
|
|
4
4
|
"description": "Shared UI components for TTT Productions projects",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"react-dom": ">=19.0.0",
|
|
50
50
|
"react-hook-form": ">=7.0.0",
|
|
51
51
|
"@hookform/resolvers": ">=3.0.0",
|
|
52
|
-
"@ttt-productions/firebase-helpers": "^0.12.
|
|
52
|
+
"@ttt-productions/firebase-helpers": "^0.12.9",
|
|
53
|
+
"lucide-react": "^1.30.0"
|
|
53
54
|
},
|
|
54
55
|
"peerDependenciesMeta": {
|
|
55
56
|
"react": {
|
|
@@ -63,6 +64,9 @@
|
|
|
63
64
|
},
|
|
64
65
|
"@hookform/resolvers": {
|
|
65
66
|
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"lucide-react": {
|
|
69
|
+
"optional": true
|
|
66
70
|
}
|
|
67
71
|
},
|
|
68
72
|
"dependencies": {
|
|
@@ -88,7 +92,6 @@
|
|
|
88
92
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
89
93
|
"class-variance-authority": "^0.7.0",
|
|
90
94
|
"clsx": "^2.1.1",
|
|
91
|
-
"lucide-react": "^0.562.0",
|
|
92
95
|
"tailwind-merge": "^3.6.0"
|
|
93
96
|
},
|
|
94
97
|
"devDependencies": {
|
|
@@ -99,7 +102,8 @@
|
|
|
99
102
|
"typescript": "^5.8.3",
|
|
100
103
|
"react-hook-form": "^7.69.0",
|
|
101
104
|
"@hookform/resolvers": "^5.2.2",
|
|
102
|
-
"@ttt-productions/firebase-helpers": "^0.12.
|
|
105
|
+
"@ttt-productions/firebase-helpers": "^0.12.9",
|
|
106
|
+
"lucide-react": "^1.30.0"
|
|
103
107
|
},
|
|
104
108
|
"author": "DJ (TTT Productions)",
|
|
105
109
|
"license": "MIT"
|
|
@@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
|
|
|
16
16
|
>(({ className, ...props }, ref) => (
|
|
17
17
|
<AlertDialogPrimitive.Overlay
|
|
18
18
|
className={cn(
|
|
19
|
-
"fixed inset-0 z-50 bg-
|
|
19
|
+
"fixed inset-0 z-50 bg-[color:var(--scrim)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
20
20
|
className
|
|
21
21
|
)}
|
|
22
22
|
{...props}
|
|
@@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
|
|
|
34
34
|
<AlertDialogPrimitive.Content
|
|
35
35
|
ref={ref}
|
|
36
36
|
className={cn(
|
|
37
|
-
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6
|
|
37
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 elevation-raised duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg [&>*]:min-w-0",
|
|
38
38
|
className
|
|
39
39
|
)}
|
|
40
40
|
{...props}
|
|
@@ -40,7 +40,7 @@ const AvatarFallback = React.forwardRef<
|
|
|
40
40
|
<AvatarPrimitive.Fallback
|
|
41
41
|
ref={ref}
|
|
42
42
|
className={cn(
|
|
43
|
-
"flex h-full w-full items-center justify-center rounded-full bg-
|
|
43
|
+
"flex h-full w-full items-center justify-center rounded-full bg-[color:var(--avatar-fallback)] text-primary font-semibold",
|
|
44
44
|
className
|
|
45
45
|
)}
|
|
46
46
|
{...props}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
-
import { Slot } from "@radix-ui/react-slot"
|
|
4
|
+
import { Slot, Slottable } from "@radix-ui/react-slot"
|
|
5
5
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
6
6
|
|
|
7
7
|
import { cn } from "../../lib/utils.js"
|
|
8
|
+
import { Spinner } from "./spinner.js"
|
|
8
9
|
|
|
9
10
|
const buttonVariants = cva(
|
|
10
11
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-bold ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
@@ -14,9 +15,9 @@ const buttonVariants = cva(
|
|
|
14
15
|
default:
|
|
15
16
|
"button-default bg-primary text-primary-foreground hover:bg-primary/90 border-2 border-[hsl(var(--brand-primary-deep))]",
|
|
16
17
|
destructive:
|
|
17
|
-
"bg-destructive text-destructive-foreground hover:bg-destructive/90 border-2 border-[hsl(var(--
|
|
18
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90 border-2 border-[hsl(var(--destructive-border))]",
|
|
18
19
|
success:
|
|
19
|
-
"bg-
|
|
20
|
+
"bg-[color:var(--button-success)] text-primary-foreground hover:bg-[color:var(--button-success)]/90 border-2 border-[hsl(var(--status-success-border))]",
|
|
20
21
|
outline:
|
|
21
22
|
"border-2 border-border bg-background hover:bg-accent hover:text-accent-foreground text-foreground",
|
|
22
23
|
secondary:
|
|
@@ -44,19 +45,54 @@ export interface ButtonProps
|
|
|
44
45
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
45
46
|
VariantProps<typeof buttonVariants> {
|
|
46
47
|
asChild?: boolean
|
|
48
|
+
/**
|
|
49
|
+
* Async work this button started is in flight. The button disables itself — so
|
|
50
|
+
* neither a repeat click nor an implicit form resubmit can fire — sets
|
|
51
|
+
* `aria-busy`, and shows the canonical spinner: an icon button (`size="icon"`)
|
|
52
|
+
* swaps its icon for it; any other button shows it in place of its `icon`.
|
|
53
|
+
*/
|
|
54
|
+
pending?: boolean
|
|
55
|
+
/** Leading icon, rendered before the label. While `pending`, the spinner takes its place. */
|
|
56
|
+
icon?: React.ReactNode
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
50
|
-
({ className, variant, size, asChild = false, children, ...props }, ref) => {
|
|
51
|
-
const
|
|
60
|
+
({ className, variant, size, asChild = false, pending = false, icon, disabled, children, ...props }, ref) => {
|
|
61
|
+
const shared = {
|
|
62
|
+
...props,
|
|
63
|
+
...(pending ? { "aria-busy": true, "data-pending": "" } : {}),
|
|
64
|
+
disabled: disabled || pending || undefined,
|
|
65
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
66
|
+
}
|
|
67
|
+
const spinner = <Spinner size={size === "lg" ? "sm" : "xs"} />
|
|
68
|
+
const leading = pending ? spinner : icon
|
|
69
|
+
|
|
70
|
+
if (asChild) {
|
|
71
|
+
// Slot finds the Slottable among its DIRECT children, so the leading slot and the
|
|
72
|
+
// Slottable must be siblings here — never wrapped in a fragment.
|
|
73
|
+
return leading ? (
|
|
74
|
+
<Slot ref={ref} {...shared}>
|
|
75
|
+
{leading}
|
|
76
|
+
<Slottable>{children}</Slottable>
|
|
77
|
+
</Slot>
|
|
78
|
+
) : (
|
|
79
|
+
<Slot ref={ref} {...shared}>
|
|
80
|
+
{children}
|
|
81
|
+
</Slot>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
52
85
|
return (
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
86
|
+
<button ref={ref} {...shared}>
|
|
87
|
+
{size === "icon" ? (
|
|
88
|
+
pending ? spinner : children
|
|
89
|
+
) : (
|
|
90
|
+
<>
|
|
91
|
+
{leading}
|
|
92
|
+
{children}
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
</button>
|
|
60
96
|
)
|
|
61
97
|
}
|
|
62
98
|
)
|
|
@@ -7,7 +7,7 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
|
|
|
7
7
|
({ className, ...props }, ref) => (
|
|
8
8
|
<div
|
|
9
9
|
ref={ref}
|
|
10
|
-
className={cn("rounded-lg border-2 card-border bg-card text-card-foreground
|
|
10
|
+
className={cn("rounded-lg border-2 card-border bg-card text-card-foreground", className)}
|
|
11
11
|
{...props}
|
|
12
12
|
/>
|
|
13
13
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { Spinner } from "./spinner.js";
|
|
5
5
|
|
|
6
6
|
export interface ChunkErrorRecoveryProps {
|
|
7
7
|
children: React.ReactNode;
|
|
@@ -48,7 +48,7 @@ export function ChunkErrorRecovery({
|
|
|
48
48
|
if (hasError) {
|
|
49
49
|
return (
|
|
50
50
|
<div className="flex flex-col h-screen w-screen items-center justify-center gap-4 bg-background">
|
|
51
|
-
<
|
|
51
|
+
<Spinner size="xl" />
|
|
52
52
|
<p className="text-muted-foreground">{loadingMessage}</p>
|
|
53
53
|
</div>
|
|
54
54
|
);
|
|
@@ -14,7 +14,7 @@ import { Button } from './button.js';
|
|
|
14
14
|
import { Textarea } from './textarea.js';
|
|
15
15
|
import { Input } from './input.js';
|
|
16
16
|
import { Label } from './label.js';
|
|
17
|
-
import {
|
|
17
|
+
import { Zap, Clock, RotateCcw } from 'lucide-react';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* The inline required-reason config. The caller OWNS the reason value/onChange (draft state lives at
|
|
@@ -71,7 +71,7 @@ export interface ConsequenceDialogProps {
|
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Runs on confirm. May be async — while it is pending the dialog STAYS OPEN, both buttons disable,
|
|
74
|
-
* and
|
|
74
|
+
* and the confirm button shows its pending spinner. Closes on resolve. If it rejects the dialog
|
|
75
75
|
* stays open so the caller's own surface (error toast, TOTP step-up) can show and the operator can
|
|
76
76
|
* retry or cancel. This component takes no Sentry/monitoring dependency — every error path
|
|
77
77
|
* belongs to the caller's `onConfirm`.
|
|
@@ -184,7 +184,6 @@ export function ConsequenceDialog({
|
|
|
184
184
|
(reason.maxLength === undefined || reason.value.length <= reason.maxLength);
|
|
185
185
|
|
|
186
186
|
const typedOk = !typedConfirmation ? true : typedValue === typedConfirmation.phrase;
|
|
187
|
-
const confirmDisabled = pending || !reasonOk || !typedOk;
|
|
188
187
|
|
|
189
188
|
return (
|
|
190
189
|
<AlertDialog open={actualOpen} onOpenChange={handleOpenChange}>
|
|
@@ -263,10 +262,9 @@ export function ConsequenceDialog({
|
|
|
263
262
|
<Button
|
|
264
263
|
variant={destructive ? 'destructive' : 'default'}
|
|
265
264
|
onClick={handleConfirm}
|
|
266
|
-
disabled={
|
|
267
|
-
|
|
265
|
+
disabled={!reasonOk || !typedOk}
|
|
266
|
+
pending={pending}
|
|
268
267
|
>
|
|
269
|
-
{pending ? <Loader2 className="mr-2 spinner-xs" aria-hidden="true" /> : null}
|
|
270
268
|
{confirmLabel}
|
|
271
269
|
</Button>
|
|
272
270
|
</AlertDialogFooter>
|
|
@@ -18,7 +18,7 @@ const DialogOverlay = React.forwardRef<
|
|
|
18
18
|
<DialogPrimitive.Overlay
|
|
19
19
|
ref={ref}
|
|
20
20
|
className={cn(
|
|
21
|
-
"fixed inset-0 z-50 bg-
|
|
21
|
+
"fixed inset-0 z-50 bg-[color:var(--scrim)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
22
22
|
className
|
|
23
23
|
)}
|
|
24
24
|
{...props}
|
|
@@ -35,7 +35,7 @@ const DialogContent = React.forwardRef<
|
|
|
35
35
|
<DialogPrimitive.Content
|
|
36
36
|
ref={ref}
|
|
37
37
|
className={cn(
|
|
38
|
-
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6
|
|
38
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 elevation-raised duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
39
39
|
"max-h-[90vh] overflow-y-auto [&>*]:min-w-0",
|
|
40
40
|
className
|
|
41
41
|
)}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
5
|
+
import { Slottable } from "@radix-ui/react-slot"
|
|
5
6
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
6
7
|
|
|
7
8
|
import { cn } from "../../lib/utils.js"
|
|
9
|
+
import { Spinner } from "./spinner.js"
|
|
8
10
|
|
|
9
11
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
10
12
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
@@ -41,7 +43,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|
|
41
43
|
<DropdownMenuPrimitive.SubContent
|
|
42
44
|
ref={ref}
|
|
43
45
|
className={cn(
|
|
44
|
-
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground
|
|
46
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground elevation-raised data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
45
47
|
className
|
|
46
48
|
)}
|
|
47
49
|
{...props}
|
|
@@ -58,7 +60,7 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
58
60
|
ref={ref}
|
|
59
61
|
sideOffset={sideOffset}
|
|
60
62
|
className={cn(
|
|
61
|
-
"z-50 min-w-[8rem] overflow-hidden rounded-md border !bg-card p-1 text-popover-foreground
|
|
63
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border !bg-card p-1 text-popover-foreground elevation-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
62
64
|
className
|
|
63
65
|
)}
|
|
64
66
|
{...props}
|
|
@@ -71,8 +73,16 @@ const DropdownMenuItem = React.forwardRef<
|
|
|
71
73
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
72
74
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
73
75
|
inset?: boolean
|
|
76
|
+
/**
|
|
77
|
+
* The action this item started is in flight: the item disables itself, sets
|
|
78
|
+
* `aria-busy`, and shows the spinner in place of its `icon`. Keeping the menu open
|
|
79
|
+
* meanwhile is the caller's `onSelect` (`event.preventDefault()`, close on settle).
|
|
80
|
+
*/
|
|
81
|
+
pending?: boolean
|
|
82
|
+
/** Leading icon. While `pending`, the spinner takes its place. */
|
|
83
|
+
icon?: React.ReactNode
|
|
74
84
|
}
|
|
75
|
-
>(({ className, inset, ...props }, ref) => (
|
|
85
|
+
>(({ className, inset, pending = false, icon, disabled, children, ...props }, ref) => (
|
|
76
86
|
<DropdownMenuPrimitive.Item
|
|
77
87
|
ref={ref}
|
|
78
88
|
className={cn(
|
|
@@ -81,7 +91,14 @@ const DropdownMenuItem = React.forwardRef<
|
|
|
81
91
|
className
|
|
82
92
|
)}
|
|
83
93
|
{...props}
|
|
84
|
-
|
|
94
|
+
{...(pending ? { "aria-busy": true, "data-pending": "" } : {})}
|
|
95
|
+
disabled={disabled || pending || undefined}
|
|
96
|
+
>
|
|
97
|
+
{/* Direct siblings, never a fragment: under `asChild` Radix Slot finds the Slottable
|
|
98
|
+
among its direct children and renders the icon/spinner inside the child element. */}
|
|
99
|
+
{pending ? <Spinner size="xs" /> : icon}
|
|
100
|
+
<Slottable>{children}</Slottable>
|
|
101
|
+
</DropdownMenuPrimitive.Item>
|
|
85
102
|
))
|
|
86
103
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
87
104
|
|
|
@@ -20,7 +20,7 @@ export function EndOfListIndicator({
|
|
|
20
20
|
className,
|
|
21
21
|
)}
|
|
22
22
|
>
|
|
23
|
-
{icon ?? <CheckCircle2 className="h-6 w-6 text-
|
|
23
|
+
{icon ?? <CheckCircle2 className="h-6 w-6 text-[color:var(--success-icon)]" />}
|
|
24
24
|
<p className="text-sm">{message}</p>
|
|
25
25
|
</div>
|
|
26
26
|
);
|
|
@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|
|
9
9
|
<input
|
|
10
10
|
type={type}
|
|
11
11
|
className={cn(
|
|
12
|
-
"flex h-10 w-full rounded-md border-2 border-
|
|
12
|
+
"flex h-10 w-full rounded-md border-2 border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
13
13
|
className
|
|
14
14
|
)}
|
|
15
15
|
ref={ref}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
5
|
import { Button } from "./button.js";
|
|
5
6
|
import { cn } from "../../lib/utils.js";
|
|
@@ -43,6 +44,17 @@ export function ListPagination({ pagination, busy = false, className }: ListPagi
|
|
|
43
44
|
const { currentPage, totalPages, canPreviousPage, canNextPage, goToPreviousPage, goToNextPage } =
|
|
44
45
|
pagination;
|
|
45
46
|
|
|
47
|
+
// The control that started the in-flight page shows the spinner. Cleared when `busy`
|
|
48
|
+
// ends — a render-phase adjustment, so there is no effect and no stale frame.
|
|
49
|
+
const [clicked, setClicked] = useState<"previous" | "next" | null>(null);
|
|
50
|
+
const [wasBusy, setWasBusy] = useState(busy);
|
|
51
|
+
if (busy !== wasBusy) {
|
|
52
|
+
setWasBusy(busy);
|
|
53
|
+
if (!busy) setClicked(null);
|
|
54
|
+
}
|
|
55
|
+
const previousPending = busy && clicked === "previous";
|
|
56
|
+
const nextPending = busy && clicked === "next";
|
|
57
|
+
|
|
46
58
|
// One visibility rule for both flavors. For a known total this is exactly
|
|
47
59
|
// `totalPages > 1`: page state is clamped into `1..totalPages`, so a
|
|
48
60
|
// single-page list has no reachable neighbour in either direction, and a
|
|
@@ -56,11 +68,15 @@ export function ListPagination({ pagination, busy = false, className }: ListPagi
|
|
|
56
68
|
<Button
|
|
57
69
|
variant="outline"
|
|
58
70
|
size="sm"
|
|
59
|
-
onClick={
|
|
71
|
+
onClick={() => {
|
|
72
|
+
setClicked("previous");
|
|
73
|
+
goToPreviousPage();
|
|
74
|
+
}}
|
|
60
75
|
disabled={busy || !canPreviousPage}
|
|
76
|
+
pending={previousPending}
|
|
77
|
+
icon={<ChevronLeft className="icon-xs" />}
|
|
61
78
|
className="h-11 min-w-11"
|
|
62
79
|
>
|
|
63
|
-
<ChevronLeft className="icon-xs mr-1" />
|
|
64
80
|
Previous
|
|
65
81
|
</Button>
|
|
66
82
|
<span className="text-counter" role="status">
|
|
@@ -69,12 +85,16 @@ export function ListPagination({ pagination, busy = false, className }: ListPagi
|
|
|
69
85
|
<Button
|
|
70
86
|
variant="outline"
|
|
71
87
|
size="sm"
|
|
72
|
-
onClick={
|
|
88
|
+
onClick={() => {
|
|
89
|
+
setClicked("next");
|
|
90
|
+
goToNextPage();
|
|
91
|
+
}}
|
|
73
92
|
disabled={busy || !canNextPage}
|
|
93
|
+
pending={nextPending}
|
|
74
94
|
className="h-11 min-w-11"
|
|
75
95
|
>
|
|
76
96
|
Next
|
|
77
|
-
<ChevronRight className="icon-xs
|
|
97
|
+
{nextPending ? null : <ChevronRight className="icon-xs" />}
|
|
78
98
|
</Button>
|
|
79
99
|
</div>
|
|
80
100
|
);
|
|
@@ -98,7 +98,7 @@ const MenubarContent = React.forwardRef<
|
|
|
98
98
|
alignOffset={alignOffset}
|
|
99
99
|
sideOffset={sideOffset}
|
|
100
100
|
className={cn(
|
|
101
|
-
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground
|
|
101
|
+
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground elevation-popover data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
102
102
|
className
|
|
103
103
|
)}
|
|
104
104
|
{...props}
|
|
@@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
|
|
|
19
19
|
align={align}
|
|
20
20
|
sideOffset={sideOffset}
|
|
21
21
|
className={cn(
|
|
22
|
-
"z-50 w-72 rounded-md border !bg-card p-4 text-popover-foreground
|
|
22
|
+
"z-50 w-72 rounded-md border !bg-card p-4 text-popover-foreground elevation-popover outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
23
23
|
className
|
|
24
24
|
)}
|
|
25
25
|
{...props}
|
|
@@ -74,7 +74,7 @@ export function ScrollToTopButton({
|
|
|
74
74
|
onClick={onClick}
|
|
75
75
|
aria-label={ariaLabel}
|
|
76
76
|
className={cn(
|
|
77
|
-
"fixed bottom-24 left-1/2 -translate-x-1/2 z-50 rounded-full
|
|
77
|
+
"fixed bottom-24 left-1/2 -translate-x-1/2 z-50 rounded-full elevation-raised transition-opacity duration-300",
|
|
78
78
|
isVisible ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
79
79
|
className,
|
|
80
80
|
)}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { useState, useRef, useEffect } from 'react';
|
|
4
4
|
import { Input } from './input.js';
|
|
5
5
|
import { Label } from './label.js';
|
|
6
|
-
import {
|
|
6
|
+
import { X, Search } from 'lucide-react';
|
|
7
|
+
import { Spinner } from './spinner.js';
|
|
7
8
|
import { cn } from "../../lib/utils.js";
|
|
8
9
|
|
|
9
10
|
export interface SearchDropdownProps<T> {
|
|
@@ -173,7 +174,7 @@ export function SearchDropdown<T>({
|
|
|
173
174
|
|
|
174
175
|
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
175
176
|
{isLoading ? (
|
|
176
|
-
<
|
|
177
|
+
<Spinner size="xs" className="text-muted-foreground" />
|
|
177
178
|
) : value ? (
|
|
178
179
|
<button
|
|
179
180
|
type="button"
|
|
@@ -188,14 +189,14 @@ export function SearchDropdown<T>({
|
|
|
188
189
|
</div>
|
|
189
190
|
|
|
190
191
|
{showDropdown && (
|
|
191
|
-
<div className="absolute z-50 w-full mt-1 bg-popover border rounded-md
|
|
192
|
+
<div className="absolute z-50 w-full mt-1 bg-popover border rounded-md elevation-raised max-h-60 overflow-auto">
|
|
192
193
|
{error ? (
|
|
193
194
|
<div className="px-3 py-2 text-sm text-destructive">
|
|
194
195
|
{error}
|
|
195
196
|
</div>
|
|
196
197
|
) : isLoading ? (
|
|
197
198
|
<div className="px-3 py-2 text-sm text-muted-foreground flex items-center gap-2">
|
|
198
|
-
<
|
|
199
|
+
<Spinner size="xs" />
|
|
199
200
|
Searching...
|
|
200
201
|
</div>
|
|
201
202
|
) : results.length === 0 ? (
|
|
@@ -5,6 +5,7 @@ import * as SelectPrimitive from "@radix-ui/react-select"
|
|
|
5
5
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
|
6
6
|
|
|
7
7
|
import { cn } from "../../lib/utils.js"
|
|
8
|
+
import { Spinner } from "./spinner.js"
|
|
8
9
|
|
|
9
10
|
const Select = SelectPrimitive.Root
|
|
10
11
|
const SelectGroup = SelectPrimitive.Group
|
|
@@ -12,8 +13,15 @@ const SelectValue = SelectPrimitive.Value
|
|
|
12
13
|
|
|
13
14
|
const SelectTrigger = React.forwardRef<
|
|
14
15
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
15
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
16
|
-
|
|
16
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
|
17
|
+
/**
|
|
18
|
+
* The change this select started is in flight: the trigger disables itself, sets
|
|
19
|
+
* `aria-busy`, and shows the spinner in place of its chevron. It keeps showing the
|
|
20
|
+
* COMMITTED value — the caller changes `value` only once the write lands.
|
|
21
|
+
*/
|
|
22
|
+
pending?: boolean
|
|
23
|
+
}
|
|
24
|
+
>(({ className, children, pending = false, disabled, ...props }, ref) => (
|
|
17
25
|
<SelectPrimitive.Trigger
|
|
18
26
|
ref={ref}
|
|
19
27
|
className={cn(
|
|
@@ -21,11 +29,17 @@ const SelectTrigger = React.forwardRef<
|
|
|
21
29
|
className
|
|
22
30
|
)}
|
|
23
31
|
{...props}
|
|
32
|
+
{...(pending ? { "aria-busy": true, "data-pending": "" } : {})}
|
|
33
|
+
disabled={disabled || pending || undefined}
|
|
24
34
|
>
|
|
25
35
|
{children}
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
|
|
36
|
+
{pending ? (
|
|
37
|
+
<Spinner size="xs" />
|
|
38
|
+
) : (
|
|
39
|
+
<SelectPrimitive.Icon asChild>
|
|
40
|
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
41
|
+
</SelectPrimitive.Icon>
|
|
42
|
+
)}
|
|
29
43
|
</SelectPrimitive.Trigger>
|
|
30
44
|
))
|
|
31
45
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
@@ -66,7 +80,7 @@ const SelectContent = React.forwardRef<
|
|
|
66
80
|
<SelectPrimitive.Content
|
|
67
81
|
ref={ref}
|
|
68
82
|
className={cn(
|
|
69
|
-
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground
|
|
83
|
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground elevation-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
70
84
|
position === "popper" &&
|
|
71
85
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
72
86
|
className
|
|
@@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
|
|
|
21
21
|
>(({ className, ...props }, ref) => (
|
|
22
22
|
<SheetPrimitive.Overlay
|
|
23
23
|
className={cn(
|
|
24
|
-
"fixed inset-0 z-50 bg-
|
|
24
|
+
"fixed inset-0 z-50 bg-[color:var(--scrim)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
25
25
|
className
|
|
26
26
|
)}
|
|
27
27
|
{...props}
|
|
@@ -31,7 +31,7 @@ const SheetOverlay = React.forwardRef<
|
|
|
31
31
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
|
32
32
|
|
|
33
33
|
const sheetVariants = cva(
|
|
34
|
-
"fixed z-50 gap-4 bg-background p-6
|
|
34
|
+
"fixed z-50 gap-4 bg-background p-6 elevation-raised transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
35
35
|
{
|
|
36
36
|
variants: {
|
|
37
37
|
side: {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Loader2 } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils.js"
|
|
6
|
+
|
|
7
|
+
export type SpinnerSize = "xs" | "sm" | "md" | "lg" | "xl"
|
|
8
|
+
|
|
9
|
+
export interface SpinnerProps {
|
|
10
|
+
/** Maps onto the theme-core `spinner-*` classes. Defaults to `xs` (the in-control size). */
|
|
11
|
+
size?: SpinnerSize
|
|
12
|
+
/**
|
|
13
|
+
* Screen-reader text. When set, the spinner becomes a `status` live region that
|
|
14
|
+
* announces it. Omit it for a spinner inside a control that already carries its
|
|
15
|
+
* own busy state (`aria-busy`) — the control speaks for it.
|
|
16
|
+
*/
|
|
17
|
+
label?: string
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SIZE_CLASS: Record<SpinnerSize, string> = {
|
|
22
|
+
xs: "spinner-xs",
|
|
23
|
+
sm: "spinner-sm",
|
|
24
|
+
md: "spinner-md",
|
|
25
|
+
lg: "spinner-lg",
|
|
26
|
+
xl: "spinner-xl",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The ONE loading spinner. Every in-progress indicator renders through this
|
|
31
|
+
* component (or through a control's `pending` prop, which renders it), so the
|
|
32
|
+
* spinner's look, size scale, and color rule have one change point. The
|
|
33
|
+
* theme-core `spinner-*` classes own size, animation, and color — including the
|
|
34
|
+
* `button .spinner-*` rule that makes an in-button spinner inherit the button's
|
|
35
|
+
* text color so it is never primary-on-primary.
|
|
36
|
+
*/
|
|
37
|
+
function Spinner({ size = "xs", label, className }: SpinnerProps) {
|
|
38
|
+
if (!label) {
|
|
39
|
+
return <Loader2 className={cn(SIZE_CLASS[size], className)} aria-hidden="true" />
|
|
40
|
+
}
|
|
41
|
+
return (
|
|
42
|
+
<span role="status" className={cn("inline-flex items-center justify-center", className)}>
|
|
43
|
+
<Loader2 className={SIZE_CLASS[size]} aria-hidden="true" />
|
|
44
|
+
<span className="sr-only">{label}</span>
|
|
45
|
+
</span>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { Spinner }
|
|
@@ -4,24 +4,36 @@ import * as React from "react"
|
|
|
4
4
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
|
5
5
|
|
|
6
6
|
import { cn } from "../../lib/utils.js"
|
|
7
|
+
import { Spinner } from "./spinner.js"
|
|
7
8
|
|
|
8
9
|
const Switch = React.forwardRef<
|
|
9
10
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
|
10
|
-
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
|
11
|
-
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & {
|
|
12
|
+
/**
|
|
13
|
+
* The change this switch started is in flight: the switch disables itself, sets
|
|
14
|
+
* `aria-busy`, and spins inside its thumb. It keeps showing the COMMITTED state —
|
|
15
|
+
* the caller flips `checked` only once the write lands.
|
|
16
|
+
*/
|
|
17
|
+
pending?: boolean
|
|
18
|
+
}
|
|
19
|
+
>(({ className, pending = false, disabled, ...props }, ref) => (
|
|
12
20
|
<SwitchPrimitives.Root
|
|
13
21
|
className={cn(
|
|
14
22
|
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
|
15
23
|
className
|
|
16
24
|
)}
|
|
17
25
|
{...props}
|
|
26
|
+
{...(pending ? { "aria-busy": true, "data-pending": "" } : {})}
|
|
27
|
+
disabled={disabled || pending || undefined}
|
|
18
28
|
ref={ref}
|
|
19
29
|
>
|
|
20
30
|
<SwitchPrimitives.Thumb
|
|
21
31
|
className={cn(
|
|
22
|
-
"pointer-events-none
|
|
32
|
+
"pointer-events-none flex h-5 w-5 items-center justify-center rounded-full bg-background elevation-raised transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
|
23
33
|
)}
|
|
24
|
-
|
|
34
|
+
>
|
|
35
|
+
{pending ? <Spinner size="xs" /> : null}
|
|
36
|
+
</SwitchPrimitives.Thumb>
|
|
25
37
|
</SwitchPrimitives.Root>
|
|
26
38
|
))
|
|
27
39
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
|
@@ -59,7 +59,7 @@ const TableRow = React.forwardRef<
|
|
|
59
59
|
<tr
|
|
60
60
|
ref={ref}
|
|
61
61
|
className={cn(
|
|
62
|
-
"border-b transition-colors hover:bg-
|
|
62
|
+
"border-b transition-colors hover:bg-[color:var(--table-row-hover)] data-[state=selected]:bg-muted",
|
|
63
63
|
className
|
|
64
64
|
)}
|
|
65
65
|
{...props}
|