@adea-ai/ui 0.10.8
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/README.md +5 -0
- package/components.json +19 -0
- package/package.json +56 -0
- package/src/components/account-drawer.tsx +133 -0
- package/src/components/on-screen-controls.tsx +156 -0
- package/src/components/prop-catalog.tsx +583 -0
- package/src/components/scene-settings.tsx +202 -0
- package/src/components/theme-provider.tsx +30 -0
- package/src/components/theme-toggle.tsx +73 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/card.tsx +18 -0
- package/src/components/ui/dialog.tsx +129 -0
- package/src/components/ui/drawer.tsx +209 -0
- package/src/components/ui/dropdown-menu.tsx +258 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +224 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +20 -0
- package/src/components/ui/radio-group.tsx +38 -0
- package/src/components/ui/separator.tsx +21 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/tabs.tsx +75 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/version-dialog.tsx +366 -0
- package/src/components/workspace-brand.tsx +18 -0
- package/src/components/workspace-logo.tsx +17 -0
- package/src/index.tsx +55 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/version-notes.ts +30 -0
- package/src/styles/auth-shell.css +67 -0
- package/src/styles/conventional-workspace.css +2846 -0
- package/src/styles/globals.css +93 -0
- package/src/styles/theme.css +94 -0
- package/src/styles/workspace-shell.css +145 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
5
|
+
import { Box, Camera, Focus, Grid3X3, UserRoundPen } from "lucide-react";
|
|
6
|
+
import { AccountDrawer } from "./account-drawer";
|
|
7
|
+
import { Button } from "#components/ui/button";
|
|
8
|
+
|
|
9
|
+
export type SceneSettingsProps = {
|
|
10
|
+
cameraViewMode: "perspective" | "orthographic";
|
|
11
|
+
onCameraViewModeChange: (value: "perspective" | "orthographic") => void;
|
|
12
|
+
allowCameraViewModeChange?: boolean;
|
|
13
|
+
/** Standalone character selection and customization menu toggle. */
|
|
14
|
+
characterDesignerEnabled?: boolean;
|
|
15
|
+
onCharacterDesignerChange?: (value: boolean) => void;
|
|
16
|
+
/** DOM target for the character designer control in an app shell toolbar. */
|
|
17
|
+
characterDesignerTargetId?: string;
|
|
18
|
+
/** Open the scene-specific room layout and interior prop designer. */
|
|
19
|
+
onOpenRoomDesigner?: () => void;
|
|
20
|
+
/** DOM target for the account drawer trigger in an app shell toolbar. */
|
|
21
|
+
accountTargetId?: string;
|
|
22
|
+
accountLabel?: string;
|
|
23
|
+
accountAuthenticated?: boolean;
|
|
24
|
+
accountBusy?: boolean;
|
|
25
|
+
accountMusicControl?: ReactNode;
|
|
26
|
+
onAccountSignIn?: () => void;
|
|
27
|
+
onAccountSignOut?: () => void;
|
|
28
|
+
showAccountDrawer?: boolean;
|
|
29
|
+
/** DOM target for the compact camera controls in an app shell toolbar. */
|
|
30
|
+
cameraTargetId?: string;
|
|
31
|
+
/** DOM target for the room designer control in an app shell toolbar. */
|
|
32
|
+
roomDesignerTargetId?: string;
|
|
33
|
+
/** DOM target for the development scene editor control in an app shell toolbar. */
|
|
34
|
+
sceneEditorTargetId?: string;
|
|
35
|
+
/** Development-only scene editor toggle, available in perspective view. */
|
|
36
|
+
sceneEditorEnabled?: boolean;
|
|
37
|
+
onSceneEditorChange?: (value: boolean) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function usePortalTarget(targetId?: string) {
|
|
41
|
+
const [target, setTarget] = useState<HTMLElement | null>(null);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
setTarget(targetId ? document.getElementById(targetId) : null);
|
|
45
|
+
}, [targetId]);
|
|
46
|
+
|
|
47
|
+
return target;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderInTarget(content: ReactNode, target: HTMLElement | null) {
|
|
51
|
+
return target ? createPortal(content, target) : content;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Compact scene controls shared by the HQ shell and other scene hosts. */
|
|
55
|
+
export function SceneSettings({
|
|
56
|
+
cameraViewMode,
|
|
57
|
+
onCameraViewModeChange,
|
|
58
|
+
allowCameraViewModeChange = true,
|
|
59
|
+
characterDesignerEnabled,
|
|
60
|
+
onCharacterDesignerChange,
|
|
61
|
+
characterDesignerTargetId,
|
|
62
|
+
onOpenRoomDesigner,
|
|
63
|
+
accountTargetId,
|
|
64
|
+
accountLabel,
|
|
65
|
+
accountAuthenticated,
|
|
66
|
+
accountBusy,
|
|
67
|
+
accountMusicControl,
|
|
68
|
+
onAccountSignIn,
|
|
69
|
+
onAccountSignOut,
|
|
70
|
+
showAccountDrawer = true,
|
|
71
|
+
cameraTargetId,
|
|
72
|
+
roomDesignerTargetId,
|
|
73
|
+
sceneEditorTargetId,
|
|
74
|
+
sceneEditorEnabled,
|
|
75
|
+
onSceneEditorChange,
|
|
76
|
+
}: SceneSettingsProps) {
|
|
77
|
+
const cameraTarget = usePortalTarget(cameraTargetId);
|
|
78
|
+
const characterDesignerTarget = usePortalTarget(characterDesignerTargetId);
|
|
79
|
+
const roomDesignerTarget = usePortalTarget(roomDesignerTargetId);
|
|
80
|
+
const sceneEditorTarget = usePortalTarget(sceneEditorTargetId);
|
|
81
|
+
|
|
82
|
+
const cameraControl = (
|
|
83
|
+
<div className="workspace-camera-control" role="group" aria-label="Camera view">
|
|
84
|
+
<Button
|
|
85
|
+
type="button"
|
|
86
|
+
size="sm"
|
|
87
|
+
variant={cameraViewMode === "perspective" ? "default" : "outline"}
|
|
88
|
+
className="workspace-camera-button"
|
|
89
|
+
aria-label="Perspective camera"
|
|
90
|
+
title="Perspective camera"
|
|
91
|
+
aria-pressed={cameraViewMode === "perspective"}
|
|
92
|
+
onClick={() => onCameraViewModeChange("perspective")}
|
|
93
|
+
>
|
|
94
|
+
<Camera aria-hidden="true" />
|
|
95
|
+
Perspective
|
|
96
|
+
</Button>
|
|
97
|
+
<Button
|
|
98
|
+
type="button"
|
|
99
|
+
size="sm"
|
|
100
|
+
variant={cameraViewMode === "orthographic" ? "default" : "outline"}
|
|
101
|
+
className="workspace-camera-button"
|
|
102
|
+
aria-label="Top-down camera"
|
|
103
|
+
title="Top-down camera"
|
|
104
|
+
aria-pressed={cameraViewMode === "orthographic"}
|
|
105
|
+
onClick={() => onCameraViewModeChange("orthographic")}
|
|
106
|
+
>
|
|
107
|
+
<Focus aria-hidden="true" />
|
|
108
|
+
Top-down
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const characterDesignerControl = (
|
|
114
|
+
<Button
|
|
115
|
+
type="button"
|
|
116
|
+
variant={characterDesignerEnabled ? "default" : "outline"}
|
|
117
|
+
size="sm"
|
|
118
|
+
aria-haspopup="dialog"
|
|
119
|
+
aria-label={characterDesignerEnabled ? "Close character designer" : "Open character designer"}
|
|
120
|
+
title={characterDesignerEnabled ? "Close character designer" : "Open character designer"}
|
|
121
|
+
aria-pressed={characterDesignerEnabled}
|
|
122
|
+
onClick={() => onCharacterDesignerChange?.(!characterDesignerEnabled)}
|
|
123
|
+
>
|
|
124
|
+
<UserRoundPen className="size-4" aria-hidden="true" />
|
|
125
|
+
<span className="workspace-character-designer-label">Character</span>
|
|
126
|
+
</Button>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const roomDesignerControl = (
|
|
130
|
+
<Button
|
|
131
|
+
type="button"
|
|
132
|
+
variant="outline"
|
|
133
|
+
size="sm"
|
|
134
|
+
aria-haspopup="dialog"
|
|
135
|
+
aria-label="Open room designer"
|
|
136
|
+
title="Open room designer"
|
|
137
|
+
onClick={onOpenRoomDesigner}
|
|
138
|
+
>
|
|
139
|
+
<Grid3X3 className="size-4" aria-hidden="true" />
|
|
140
|
+
<span className="workspace-room-designer-label">Room designer</span>
|
|
141
|
+
</Button>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const sceneEditorControl = (
|
|
145
|
+
<Button
|
|
146
|
+
type="button"
|
|
147
|
+
variant={sceneEditorEnabled ? "default" : "outline"}
|
|
148
|
+
size="sm"
|
|
149
|
+
aria-haspopup="dialog"
|
|
150
|
+
aria-label={sceneEditorEnabled ? "Close scene editor" : "Open scene editor"}
|
|
151
|
+
title={sceneEditorEnabled ? "Close scene editor" : "Open scene editor"}
|
|
152
|
+
aria-pressed={sceneEditorEnabled}
|
|
153
|
+
onClick={() => onSceneEditorChange?.(!sceneEditorEnabled)}
|
|
154
|
+
>
|
|
155
|
+
<Box className="size-4" aria-hidden="true" />
|
|
156
|
+
<span className="workspace-scene-editor-label">Scene editor</span>
|
|
157
|
+
</Button>
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<>
|
|
162
|
+
{showAccountDrawer ? (
|
|
163
|
+
<AccountDrawer
|
|
164
|
+
accountLabel={accountLabel}
|
|
165
|
+
authenticated={accountAuthenticated}
|
|
166
|
+
busy={accountBusy}
|
|
167
|
+
musicControl={accountMusicControl}
|
|
168
|
+
triggerTargetId={accountTargetId}
|
|
169
|
+
onSignIn={onAccountSignIn}
|
|
170
|
+
onSignOut={onAccountSignOut}
|
|
171
|
+
/>
|
|
172
|
+
) : null}
|
|
173
|
+
{allowCameraViewModeChange ? renderInTarget(cameraControl, cameraTarget) : null}
|
|
174
|
+
{characterDesignerEnabled != null &&
|
|
175
|
+
!characterDesignerEnabled &&
|
|
176
|
+
onCharacterDesignerChange ? (
|
|
177
|
+
characterDesignerTarget ? (
|
|
178
|
+
createPortal(characterDesignerControl, characterDesignerTarget)
|
|
179
|
+
) : (
|
|
180
|
+
<div className="fixed right-4 top-16 z-40">{characterDesignerControl}</div>
|
|
181
|
+
)
|
|
182
|
+
) : null}
|
|
183
|
+
{onOpenRoomDesigner && cameraViewMode === "orthographic" ? (
|
|
184
|
+
roomDesignerTarget ? (
|
|
185
|
+
createPortal(roomDesignerControl, roomDesignerTarget)
|
|
186
|
+
) : (
|
|
187
|
+
<div className="fixed right-4 top-16 z-40">{roomDesignerControl}</div>
|
|
188
|
+
)
|
|
189
|
+
) : null}
|
|
190
|
+
{sceneEditorEnabled != null &&
|
|
191
|
+
!sceneEditorEnabled &&
|
|
192
|
+
onSceneEditorChange &&
|
|
193
|
+
cameraViewMode === "perspective" ? (
|
|
194
|
+
sceneEditorTarget ? (
|
|
195
|
+
createPortal(sceneEditorControl, sceneEditorTarget)
|
|
196
|
+
) : (
|
|
197
|
+
<div className="fixed right-4 top-16 z-40">{sceneEditorControl}</div>
|
|
198
|
+
)
|
|
199
|
+
) : null}
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
4
|
+
import type { ComponentProps, ReactElement, ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
// Bun 1.4 can resolve next-themes against a separate React type instance. The
|
|
7
|
+
// runtime component is compatible, but TypeScript then drops `children` from
|
|
8
|
+
// its JSX props. Keep the shared provider's public contract stable by
|
|
9
|
+
// normalizing the third-party component's props at this boundary.
|
|
10
|
+
type CompatibleNextThemesProviderProps = ComponentProps<typeof NextThemesProvider> & {
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const CompatibleNextThemesProvider = NextThemesProvider as unknown as (
|
|
15
|
+
props: CompatibleNextThemesProviderProps
|
|
16
|
+
) => ReactElement;
|
|
17
|
+
|
|
18
|
+
/** Shared theme provider used by each Next app in the workspace. */
|
|
19
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
20
|
+
return (
|
|
21
|
+
<CompatibleNextThemesProvider
|
|
22
|
+
attribute="class"
|
|
23
|
+
defaultTheme="system"
|
|
24
|
+
enableSystem
|
|
25
|
+
disableTransitionOnChange
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</CompatibleNextThemesProvider>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
import { Monitor, Moon, Sun } from "lucide-react";
|
|
5
|
+
import { useTheme } from "next-themes";
|
|
6
|
+
import { Button } from "#components/ui/button";
|
|
7
|
+
import { cn } from "#lib/utils";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compact sun/moon theme selector. Wired to the next-themes provider so the
|
|
11
|
+
* choice persists across the app (localStorage) and applies to every scene.
|
|
12
|
+
*/
|
|
13
|
+
export function ThemeToggle({ className }: { className?: string }) {
|
|
14
|
+
const { theme, setTheme } = useTheme();
|
|
15
|
+
const mounted = useSyncExternalStore(
|
|
16
|
+
() => () => undefined,
|
|
17
|
+
() => true,
|
|
18
|
+
() => false
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// next-themes resolves the saved/system theme after mount. Use the stable
|
|
22
|
+
// light presentation for both SSR and the first client render so the
|
|
23
|
+
// aria/class attributes cannot mismatch during hydration.
|
|
24
|
+
const selectedTheme = mounted ? theme : "system";
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn(
|
|
29
|
+
"inline-flex items-center gap-1.5 rounded-full border border-input bg-background p-1.5",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
role="radiogroup"
|
|
33
|
+
aria-label="Theme"
|
|
34
|
+
>
|
|
35
|
+
<Button
|
|
36
|
+
type="button"
|
|
37
|
+
role="radio"
|
|
38
|
+
aria-checked={selectedTheme === "system"}
|
|
39
|
+
aria-label="System theme"
|
|
40
|
+
variant={selectedTheme === "system" ? "default" : "ghost"}
|
|
41
|
+
size="icon-sm"
|
|
42
|
+
onClick={() => setTheme("system")}
|
|
43
|
+
className="rounded-full"
|
|
44
|
+
>
|
|
45
|
+
<Monitor className="size-4" aria-hidden="true" />
|
|
46
|
+
</Button>
|
|
47
|
+
<Button
|
|
48
|
+
type="button"
|
|
49
|
+
role="radio"
|
|
50
|
+
aria-checked={selectedTheme === "light"}
|
|
51
|
+
aria-label="Light theme"
|
|
52
|
+
variant={selectedTheme === "light" ? "default" : "ghost"}
|
|
53
|
+
size="icon-sm"
|
|
54
|
+
onClick={() => setTheme("light")}
|
|
55
|
+
className="rounded-full"
|
|
56
|
+
>
|
|
57
|
+
<Sun className="size-4" aria-hidden="true" />
|
|
58
|
+
</Button>
|
|
59
|
+
<Button
|
|
60
|
+
type="button"
|
|
61
|
+
role="radio"
|
|
62
|
+
aria-checked={selectedTheme === "dark"}
|
|
63
|
+
aria-label="Dark theme"
|
|
64
|
+
variant={selectedTheme === "dark" ? "default" : "ghost"}
|
|
65
|
+
size="icon-sm"
|
|
66
|
+
onClick={() => setTheme("dark")}
|
|
67
|
+
className="rounded-full"
|
|
68
|
+
>
|
|
69
|
+
<Moon className="size-4" aria-hidden="true" />
|
|
70
|
+
</Button>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
import { cn } from "#lib/utils";
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
13
|
+
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
14
|
+
destructive:
|
|
15
|
+
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
|
16
|
+
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
17
|
+
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
18
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: "default",
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function Badge({
|
|
28
|
+
className,
|
|
29
|
+
variant = "default",
|
|
30
|
+
render,
|
|
31
|
+
...props
|
|
32
|
+
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
|
33
|
+
return useRender({
|
|
34
|
+
defaultTagName: "span",
|
|
35
|
+
props: mergeProps<"span">(
|
|
36
|
+
{
|
|
37
|
+
className: cn(badgeVariants({ variant }), className),
|
|
38
|
+
},
|
|
39
|
+
props
|
|
40
|
+
),
|
|
41
|
+
render,
|
|
42
|
+
state: {
|
|
43
|
+
slot: "badge",
|
|
44
|
+
variant,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { cn } from "#lib/utils";
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
12
|
+
outline:
|
|
13
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
14
|
+
secondary:
|
|
15
|
+
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
16
|
+
ghost:
|
|
17
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
18
|
+
destructive:
|
|
19
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
20
|
+
success:
|
|
21
|
+
"bg-[#1a7f37]/10 text-[#1a7f37] hover:bg-[#1a7f37]/20 focus-visible:border-[#1a7f37]/40 focus-visible:ring-[#1a7f37]/20 dark:bg-[#3fb950]/20 dark:text-[#7ee787] dark:hover:bg-[#3fb950]/30 dark:focus-visible:ring-[#3fb950]/40",
|
|
22
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
default:
|
|
26
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
27
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
28
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
29
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
30
|
+
icon: "size-8",
|
|
31
|
+
"icon-xs":
|
|
32
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
33
|
+
"icon-sm":
|
|
34
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
35
|
+
"icon-lg": "size-9",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: {
|
|
39
|
+
variant: "default",
|
|
40
|
+
size: "default",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
function Button({
|
|
46
|
+
className,
|
|
47
|
+
variant = "default",
|
|
48
|
+
size = "default",
|
|
49
|
+
...props
|
|
50
|
+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
51
|
+
return (
|
|
52
|
+
<ButtonPrimitive
|
|
53
|
+
data-slot="button"
|
|
54
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "#lib/utils";
|
|
3
|
+
|
|
4
|
+
function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
data-slot="card"
|
|
8
|
+
className={cn("rounded-xl border bg-card text-card-foreground shadow-sm", className)}
|
|
9
|
+
{...props}
|
|
10
|
+
/>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function CardContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
15
|
+
return <div data-slot="card-content" className={cn("p-4", className)} {...props} />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Card, CardContent };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
|
5
|
+
import { X } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "#lib/utils";
|
|
8
|
+
|
|
9
|
+
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
|
10
|
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
|
14
|
+
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
|
18
|
+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
|
22
|
+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) {
|
|
26
|
+
return (
|
|
27
|
+
<DialogPrimitive.Backdrop
|
|
28
|
+
data-slot="dialog-overlay"
|
|
29
|
+
className={cn(
|
|
30
|
+
"fixed inset-0 z-[120] bg-slate-950/55 backdrop-blur-[2px] transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function DialogContent({
|
|
39
|
+
className,
|
|
40
|
+
children,
|
|
41
|
+
showCloseButton = true,
|
|
42
|
+
...props
|
|
43
|
+
}: DialogPrimitive.Popup.Props & { showCloseButton?: boolean }) {
|
|
44
|
+
return (
|
|
45
|
+
<DialogPortal>
|
|
46
|
+
<DialogOverlay />
|
|
47
|
+
<DialogPrimitive.Viewport
|
|
48
|
+
data-slot="dialog-viewport"
|
|
49
|
+
className="fixed inset-0 z-[120] flex items-center justify-center p-4"
|
|
50
|
+
>
|
|
51
|
+
<DialogPrimitive.Popup
|
|
52
|
+
data-slot="dialog-content"
|
|
53
|
+
className={cn(
|
|
54
|
+
"relative grid max-h-[min(44rem,calc(100dvh-2rem))] w-full max-w-2xl gap-0 overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-2xl outline-none transition-[opacity,transform] duration-200 data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-starting-style:scale-[0.98] data-starting-style:opacity-0",
|
|
55
|
+
className
|
|
56
|
+
)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
{showCloseButton && (
|
|
61
|
+
<DialogPrimitive.Close
|
|
62
|
+
data-slot="dialog-close"
|
|
63
|
+
className="absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-lg text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50"
|
|
64
|
+
aria-label="Close dialog"
|
|
65
|
+
>
|
|
66
|
+
<X className="size-4" aria-hidden="true" />
|
|
67
|
+
</DialogPrimitive.Close>
|
|
68
|
+
)}
|
|
69
|
+
</DialogPrimitive.Popup>
|
|
70
|
+
</DialogPrimitive.Viewport>
|
|
71
|
+
</DialogPortal>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
data-slot="dialog-header"
|
|
79
|
+
className={cn("flex flex-col gap-1.5 border-b px-6 py-5 pr-14", className)}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
data-slot="dialog-footer"
|
|
89
|
+
className={cn(
|
|
90
|
+
"flex flex-col-reverse gap-2 border-t px-6 py-4 sm:flex-row sm:justify-end",
|
|
91
|
+
className
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
|
99
|
+
return (
|
|
100
|
+
<DialogPrimitive.Title
|
|
101
|
+
data-slot="dialog-title"
|
|
102
|
+
className={cn("text-base font-semibold tracking-tight", className)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
|
|
109
|
+
return (
|
|
110
|
+
<DialogPrimitive.Description
|
|
111
|
+
data-slot="dialog-description"
|
|
112
|
+
className={cn("text-sm leading-5 text-muted-foreground", className)}
|
|
113
|
+
{...props}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export {
|
|
119
|
+
Dialog,
|
|
120
|
+
DialogClose,
|
|
121
|
+
DialogContent,
|
|
122
|
+
DialogDescription,
|
|
123
|
+
DialogFooter,
|
|
124
|
+
DialogHeader,
|
|
125
|
+
DialogOverlay,
|
|
126
|
+
DialogPortal,
|
|
127
|
+
DialogTitle,
|
|
128
|
+
DialogTrigger,
|
|
129
|
+
};
|