refactor: update UI components and styles for improved consistency and functionality
- Refactored Skeleton component to include data-slot attribute and updated class names. - Enhanced Switch component with size prop and improved styling. - Updated Tooltip components to include data-slot attributes and improved styling. - Refactored global CSS to use custom properties for theming and improved dark mode support. - Added useIsMobile hook for responsive design handling. - Updated IPC link implementation for tRPC in Electron. - Adjusted ProjectsPage layout for better responsiveness. - Removed outdated Tailwind configuration file and integrated Tailwind CSS with Vite.
This commit is contained in:
11
.mcp.json
Normal file
11
.mcp.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"shadcn": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"shadcn@latest",
|
||||||
|
"mcp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
.vscode/mcp.json
vendored
Normal file
11
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"shadcn": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"shadcn@latest",
|
||||||
|
"mcp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"rsc": false,
|
"rsc": false,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
"config": "",
|
||||||
"css": "src/renderer/globals.css",
|
"css": "src/renderer/globals.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
|
|||||||
4992
package-lock.json
generated
4992
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -33,29 +33,20 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"autoprefixer": "^10.4.24",
|
|
||||||
"drizzle-kit": "^0.31.9",
|
"drizzle-kit": "^0.31.9",
|
||||||
"electron": "40.6.0",
|
"electron": "40.6.0",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.19",
|
"shadcn": "^3.8.5",
|
||||||
|
"tailwindcss": "^4.2.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^5.4.21"
|
"vite": "^5.4.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/geist": "^5.2.8",
|
"@fontsource/geist": "^5.2.8",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@tailwindcss/vite": "^4.2.0",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
|
||||||
"@radix-ui/react-visually-hidden": "^1.2.4",
|
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@tanstack/react-router": "^1.161.1",
|
"@tanstack/react-router": "^1.161.1",
|
||||||
"@trpc/client": "^11.10.0",
|
"@trpc/client": "^11.10.0",
|
||||||
@@ -67,9 +58,9 @@
|
|||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
"electron-trpc": "^0.7.1",
|
|
||||||
"framer-motion": "^12.34.2",
|
"framer-motion": "^12.34.2",
|
||||||
"lucide-react": "^0.575.0",
|
"lucide-react": "^0.575.0",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { app, BrowserWindow } from 'electron';
|
import { app, BrowserWindow } from 'electron';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import started from 'electron-squirrel-startup';
|
import started from 'electron-squirrel-startup';
|
||||||
import { createIPCHandler } from 'electron-trpc/main';
|
|
||||||
import { initDb } from './db';
|
import { initDb } from './db';
|
||||||
import { appRouter } from './router';
|
import { appRouter } from './router';
|
||||||
|
import { createIPCHandler } from './ipc';
|
||||||
|
|
||||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||||
if (started) {
|
if (started) {
|
||||||
|
|||||||
89
src/main/ipc.ts
Normal file
89
src/main/ipc.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Custom electron IPC ↔ tRPC v11 bridge.
|
||||||
|
*
|
||||||
|
* Replaces the incompatible electron-trpc package (v0.7.x bundles tRPC v10's
|
||||||
|
* callProcedure which checks `procedure._def[type]`, while tRPC v11 uses
|
||||||
|
* `procedure._def.type`).
|
||||||
|
*/
|
||||||
|
import { ipcMain, type BrowserWindow } from 'electron';
|
||||||
|
import {
|
||||||
|
callTRPCProcedure,
|
||||||
|
getErrorShape,
|
||||||
|
getTRPCErrorFromUnknown,
|
||||||
|
transformTRPCResponse,
|
||||||
|
type AnyRouter,
|
||||||
|
} from '@trpc/server';
|
||||||
|
|
||||||
|
export const IPC_CHANNEL = 'trpc';
|
||||||
|
|
||||||
|
interface IPCRequest {
|
||||||
|
method: 'request';
|
||||||
|
operation: {
|
||||||
|
id: number;
|
||||||
|
type: 'query' | 'mutation' | 'subscription';
|
||||||
|
path: string;
|
||||||
|
input?: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createIPCHandler<TRouter extends AnyRouter>({
|
||||||
|
router,
|
||||||
|
windows = [],
|
||||||
|
}: {
|
||||||
|
router: TRouter;
|
||||||
|
windows?: BrowserWindow[];
|
||||||
|
}) {
|
||||||
|
const config = router._def._config;
|
||||||
|
|
||||||
|
ipcMain.on(IPC_CHANNEL, async (event, message: IPCRequest) => {
|
||||||
|
if (message.method !== 'request') return;
|
||||||
|
|
||||||
|
const { id, type, path, input } = message.operation;
|
||||||
|
|
||||||
|
// Deserialize input through transformer (identity by default)
|
||||||
|
const rawInput = input !== undefined
|
||||||
|
? config.transformer.input.deserialize(input)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const respond = (response: any) => {
|
||||||
|
if (event.sender.isDestroyed()) return;
|
||||||
|
const serialised = transformTRPCResponse(config, response);
|
||||||
|
event.reply(IPC_CHANNEL, serialised);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await callTRPCProcedure({
|
||||||
|
router,
|
||||||
|
path,
|
||||||
|
getRawInput: async () => rawInput,
|
||||||
|
ctx: {},
|
||||||
|
type,
|
||||||
|
signal: undefined as unknown as AbortSignal,
|
||||||
|
batchIndex: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
respond({ id, result: { type: 'data', data: result } });
|
||||||
|
} catch (cause) {
|
||||||
|
const error = getTRPCErrorFromUnknown(cause);
|
||||||
|
respond({
|
||||||
|
id,
|
||||||
|
error: getErrorShape({
|
||||||
|
config,
|
||||||
|
error,
|
||||||
|
type,
|
||||||
|
path,
|
||||||
|
input: rawInput,
|
||||||
|
ctx: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach additional windows later if needed
|
||||||
|
return {
|
||||||
|
attachWindow(_win: BrowserWindow) {
|
||||||
|
// No per-window setup needed — ipcMain.on is global
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
// See the Electron documentation for details on how to use preload scripts:
|
// See the Electron documentation for details on how to use preload scripts:
|
||||||
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
||||||
import { exposeElectronTRPC } from 'electron-trpc/main';
|
import './trpc';
|
||||||
|
|
||||||
process.once('loaded', () => {
|
|
||||||
exposeElectronTRPC();
|
|
||||||
});
|
|
||||||
|
|||||||
20
src/preload/trpc.ts
Normal file
20
src/preload/trpc.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Preload script — expose a minimal IPC transport to the renderer.
|
||||||
|
*
|
||||||
|
* Replaces electron-trpc's exposeElectronTRPC with a compatible shim
|
||||||
|
* that works with our custom IPC handler + tRPC v11.
|
||||||
|
*/
|
||||||
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
const IPC_CHANNEL = 'trpc';
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronTRPC', {
|
||||||
|
sendMessage: (msg: unknown) => ipcRenderer.send(IPC_CHANNEL, msg),
|
||||||
|
onMessage: (cb: (data: unknown) => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, data: unknown) => cb(data);
|
||||||
|
ipcRenderer.on(IPC_CHANNEL, handler);
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener(IPC_CHANNEL, handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -57,7 +57,7 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
return (
|
return (
|
||||||
<SidebarProvider open={open} onOpenChange={handleOpenChange}>
|
<SidebarProvider open={open} onOpenChange={handleOpenChange}>
|
||||||
<AppSidebar currentPath={currentPath} />
|
<AppSidebar currentPath={currentPath} />
|
||||||
<SidebarInset className="overflow-hidden">
|
<SidebarInset>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{/* Right-edge vertical 'keep scrolling for AI' affordance (non-interactive) */}
|
{/* Right-edge vertical 'keep scrolling for AI' affordance (non-interactive) */}
|
||||||
|
|||||||
@@ -80,13 +80,54 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
|||||||
const [editDialog, setEditDialog] = useState<{ id: string; name: string; clientId: string | null } | null>(null);
|
const [editDialog, setEditDialog] = useState<{ id: string; name: string; clientId: string | null } | null>(null);
|
||||||
const [editClientValue, setEditClientValue] = useState<string>(NO_CLIENT_KEY);
|
const [editClientValue, setEditClientValue] = useState<string>(NO_CLIENT_KEY);
|
||||||
|
|
||||||
|
// New-project dialog state
|
||||||
|
const [newProjectOpen, setNewProjectOpen] = useState(false);
|
||||||
|
const [newProjectName, setNewProjectName] = useState('');
|
||||||
|
const [newProjectClientId, setNewProjectClientId] = useState<string>(NO_CLIENT_KEY);
|
||||||
|
const [newProjectSubClientId, setNewProjectSubClientId] = useState<string>(NO_CLIENT_KEY);
|
||||||
|
// Inline client creation
|
||||||
|
const [creatingClient, setCreatingClient] = useState(false);
|
||||||
|
const [newClientName, setNewClientName] = useState('');
|
||||||
|
// Inline sub-client creation
|
||||||
|
const [creatingSubClient, setCreatingSubClient] = useState(false);
|
||||||
|
const [newSubClientName, setNewSubClientName] = useState('');
|
||||||
|
|
||||||
const { data: projectList = [] } = trpc.projects.list.useQuery(
|
const { data: projectList = [] } = trpc.projects.list.useQuery(
|
||||||
{ includeArchived: showArchived },
|
{ includeArchived: showArchived },
|
||||||
);
|
);
|
||||||
const { data: clientList = [] } = trpc.clients.list.useQuery();
|
const { data: clientList = [] } = trpc.clients.list.useQuery();
|
||||||
|
|
||||||
|
// Derived: top-level clients and sub-clients grouped by parentId
|
||||||
|
const topLevelClients = useMemo(
|
||||||
|
() => clientList.filter((c) => !c.parentId),
|
||||||
|
[clientList],
|
||||||
|
);
|
||||||
|
const subClientsByParent = useMemo(() => {
|
||||||
|
const m = new Map<string, typeof clientList>();
|
||||||
|
for (const c of clientList) {
|
||||||
|
if (c.parentId) {
|
||||||
|
const arr = m.get(c.parentId);
|
||||||
|
if (arr) arr.push(c);
|
||||||
|
else m.set(c.parentId, [c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}, [clientList]);
|
||||||
|
|
||||||
|
const createClientMutation = trpc.clients.create.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
void utils.clients.list.invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const createMutation = trpc.projects.create.useMutation({
|
const createMutation = trpc.projects.create.useMutation({
|
||||||
onSuccess: () => { void utils.projects.list.invalidate(); },
|
onSuccess: (data, variables) => {
|
||||||
|
// Auto-expand the matching client group
|
||||||
|
const groupKey = variables.clientId ?? NO_CLIENT_KEY;
|
||||||
|
setExpanded((prev) => new Set([...prev, groupKey]));
|
||||||
|
onSelectProject(data.id);
|
||||||
|
void utils.projects.list.invalidate();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMutation = trpc.projects.update.useMutation({
|
const updateMutation = trpc.projects.update.useMutation({
|
||||||
@@ -140,8 +181,58 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNewProject() {
|
function handleOpenNewProject() {
|
||||||
createMutation.mutate({ name: 'New Project' });
|
setNewProjectName('');
|
||||||
|
setNewProjectClientId(NO_CLIENT_KEY);
|
||||||
|
setNewProjectSubClientId(NO_CLIENT_KEY);
|
||||||
|
setCreatingClient(false);
|
||||||
|
setNewClientName('');
|
||||||
|
setCreatingSubClient(false);
|
||||||
|
setNewSubClientName('');
|
||||||
|
setNewProjectOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateProject() {
|
||||||
|
const name = newProjectName.trim();
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
let resolvedClientId: string | undefined;
|
||||||
|
|
||||||
|
// If creating a brand-new client first
|
||||||
|
if (creatingClient && newClientName.trim()) {
|
||||||
|
const result = await createClientMutation.mutateAsync({ name: newClientName.trim() });
|
||||||
|
resolvedClientId = result.id;
|
||||||
|
|
||||||
|
// If also creating a sub-client under the new client
|
||||||
|
if (creatingSubClient && newSubClientName.trim()) {
|
||||||
|
const subResult = await createClientMutation.mutateAsync({
|
||||||
|
name: newSubClientName.trim(),
|
||||||
|
parentId: resolvedClientId,
|
||||||
|
});
|
||||||
|
resolvedClientId = subResult.id;
|
||||||
|
}
|
||||||
|
} else if (newProjectClientId !== NO_CLIENT_KEY) {
|
||||||
|
// User picked an existing client
|
||||||
|
if (creatingSubClient && newSubClientName.trim()) {
|
||||||
|
// Create a new sub-client under the selected client
|
||||||
|
const subResult = await createClientMutation.mutateAsync({
|
||||||
|
name: newSubClientName.trim(),
|
||||||
|
parentId: newProjectClientId,
|
||||||
|
});
|
||||||
|
resolvedClientId = subResult.id;
|
||||||
|
} else if (newProjectSubClientId !== NO_CLIENT_KEY) {
|
||||||
|
// User picked an existing sub-client
|
||||||
|
resolvedClientId = newProjectSubClientId;
|
||||||
|
} else {
|
||||||
|
// Just the parent client, no sub-client
|
||||||
|
resolvedClientId = newProjectClientId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createMutation.mutate(
|
||||||
|
{ name, clientId: resolvedClientId },
|
||||||
|
{ onSuccess: () => setNewProjectOpen(false) },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleArchiveToggle(id: string, currentStatus: string | null) {
|
function handleArchiveToggle(id: string, currentStatus: string | null) {
|
||||||
@@ -186,7 +277,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleNewProject}
|
onClick={handleOpenNewProject}
|
||||||
disabled={createMutation.isPending}
|
disabled={createMutation.isPending}
|
||||||
aria-label="New Project"
|
aria-label="New Project"
|
||||||
>
|
>
|
||||||
@@ -237,7 +328,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleNewProject}
|
onClick={handleOpenNewProject}
|
||||||
disabled={createMutation.isPending}
|
disabled={createMutation.isPending}
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-4 w-4" />
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
@@ -286,7 +377,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
|||||||
<div
|
<div
|
||||||
key={project.id}
|
key={project.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex items-center h-7 rounded-md text-sm cursor-pointer hover:bg-accent transition-colors',
|
'group flex items-center h-7 rounded-md text-sm cursor-pointer hover:bg-accent transition-colors',
|
||||||
selectedProjectId === project.id && 'bg-sidebar-accent',
|
selectedProjectId === project.id && 'bg-sidebar-accent',
|
||||||
project.status === 'archived' && 'opacity-60',
|
project.status === 'archived' && 'opacity-60',
|
||||||
)}
|
)}
|
||||||
@@ -303,9 +394,8 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'size-5 p-0 ml-1 text-muted-foreground hover:bg-muted shrink-0',
|
'relative z-10 h-5 w-5 min-h-0 min-w-0 p-0 ml-1 text-muted-foreground hover:bg-muted shrink-0',
|
||||||
'opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100',
|
'opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100',
|
||||||
)}
|
)}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
@@ -348,7 +438,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-destructive focus:text-destructive"
|
variant="destructive"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setDeleteProjectId({ id: project.id, name: project.name });
|
setDeleteProjectId({ id: project.id, name: project.name });
|
||||||
@@ -399,6 +489,159 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
|
{/* New Project dialog */}
|
||||||
|
<Dialog open={newProjectOpen} onOpenChange={setNewProjectOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>New Project</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Give your project a name and optionally assign it to a client.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 py-2">
|
||||||
|
{/* Project name */}
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label htmlFor="new-project-name" className="text-sm font-medium">Project Name</label>
|
||||||
|
<Input
|
||||||
|
id="new-project-name"
|
||||||
|
placeholder="e.g. Website Redesign"
|
||||||
|
value={newProjectName}
|
||||||
|
onChange={(e) => setNewProjectName(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Client selection */}
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-sm font-medium">Client <span className="text-muted-foreground font-normal">(optional)</span></label>
|
||||||
|
{creatingClient ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="New client name"
|
||||||
|
value={newClientName}
|
||||||
|
onChange={(e) => setNewClientName(e.target.value)}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setCreatingClient(false);
|
||||||
|
setNewClientName('');
|
||||||
|
setCreatingSubClient(false);
|
||||||
|
setNewSubClientName('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
value={newProjectClientId}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setNewProjectClientId(v);
|
||||||
|
setNewProjectSubClientId(NO_CLIENT_KEY);
|
||||||
|
setCreatingSubClient(false);
|
||||||
|
setNewSubClientName('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="flex-1">
|
||||||
|
<SelectValue placeholder="Select a client" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={NO_CLIENT_KEY}>None (Internal)</SelectItem>
|
||||||
|
{topLevelClients.map((c) => (
|
||||||
|
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCreatingClient(true)}
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5 mr-1" />New
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sub-client selection — only when a client is selected or being created */}
|
||||||
|
{(newProjectClientId !== NO_CLIENT_KEY || (creatingClient && newClientName.trim())) && (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-sm font-medium">Sub-client <span className="text-muted-foreground font-normal">(optional)</span></label>
|
||||||
|
{creatingSubClient ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="New sub-client name"
|
||||||
|
value={newSubClientName}
|
||||||
|
onChange={(e) => setNewSubClientName(e.target.value)}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setCreatingSubClient(false);
|
||||||
|
setNewSubClientName('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : creatingClient ? (
|
||||||
|
/* When creating a new client there are no existing sub-clients to pick */
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-fit"
|
||||||
|
onClick={() => setCreatingSubClient(true)}
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5 mr-1" />New Sub-client
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
value={newProjectSubClientId}
|
||||||
|
onValueChange={setNewProjectSubClientId}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="flex-1">
|
||||||
|
<SelectValue placeholder="Select a sub-client" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={NO_CLIENT_KEY}>None</SelectItem>
|
||||||
|
{(subClientsByParent.get(newProjectClientId) ?? []).map((sc) => (
|
||||||
|
<SelectItem key={sc.id} value={sc.id}>{sc.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCreatingSubClient(true)}
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5 mr-1" />New
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setNewProjectOpen(false)}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateProject}
|
||||||
|
disabled={!newProjectName.trim() || createMutation.isPending || createClientMutation.isPending}
|
||||||
|
>
|
||||||
|
{createMutation.isPending || createClientMutation.isPending ? 'Creating…' : 'Create Project'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Edit client dialog */}
|
{/* Edit client dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={!!editDialog}
|
open={!!editDialog}
|
||||||
|
|||||||
@@ -1,139 +1,194 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
const AlertDialog = AlertDialogPrimitive.Root
|
function AlertDialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||||
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
function AlertDialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
function AlertDialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogOverlay = React.forwardRef<
|
function AlertDialogOverlay({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
<AlertDialogPrimitive.Overlay
|
return (
|
||||||
className={cn(
|
<AlertDialogPrimitive.Overlay
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
data-slot="alert-dialog-overlay"
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const AlertDialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay />
|
|
||||||
<AlertDialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"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 shadow-lg 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",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</AlertDialogPortal>
|
)
|
||||||
))
|
}
|
||||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const AlertDialogHeader = ({
|
function AlertDialogContent({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
||||||
|
size?: "default" | "sm"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"bg-background 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 group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogHeader({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<"div">) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<div
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
data-slot="alert-dialog-header"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
||||||
{...props}
|
className
|
||||||
/>
|
)}
|
||||||
)
|
{...props}
|
||||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogFooter = ({
|
function AlertDialogFooter({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<"div">) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<div
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
data-slot="alert-dialog-footer"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||||
{...props}
|
className
|
||||||
/>
|
)}
|
||||||
)
|
{...props}
|
||||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogTitle = React.forwardRef<
|
function AlertDialogTitle({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
<AlertDialogPrimitive.Title
|
return (
|
||||||
ref={ref}
|
<AlertDialogPrimitive.Title
|
||||||
className={cn("text-lg font-semibold", className)}
|
data-slot="alert-dialog-title"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"text-lg font-semibold sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
||||||
))
|
className
|
||||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogDescription = React.forwardRef<
|
function AlertDialogDescription({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
<AlertDialogPrimitive.Description
|
return (
|
||||||
ref={ref}
|
<AlertDialogPrimitive.Description
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
data-slot="alert-dialog-description"
|
||||||
{...props}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
AlertDialogDescription.displayName =
|
)
|
||||||
AlertDialogPrimitive.Description.displayName
|
}
|
||||||
|
|
||||||
const AlertDialogAction = React.forwardRef<
|
function AlertDialogMedia({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<"div">) {
|
||||||
<AlertDialogPrimitive.Action
|
return (
|
||||||
ref={ref}
|
<div
|
||||||
className={cn(buttonVariants(), className)}
|
data-slot="alert-dialog-media"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"bg-muted mb-2 inline-flex size-16 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
|
||||||
))
|
className
|
||||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogCancel = React.forwardRef<
|
function AlertDialogAction({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
variant = "default",
|
||||||
>(({ className, ...props }, ref) => (
|
size = "default",
|
||||||
<AlertDialogPrimitive.Cancel
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
|
||||||
className={cn(
|
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||||
buttonVariants({ variant: "outline" }),
|
return (
|
||||||
"mt-2 sm:mt-0",
|
<Button variant={variant} size={size} asChild>
|
||||||
className
|
<AlertDialogPrimitive.Action
|
||||||
)}
|
data-slot="alert-dialog-action"
|
||||||
{...props}
|
className={cn(className)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogCancel({
|
||||||
|
className,
|
||||||
|
variant = "outline",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
|
||||||
|
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||||
|
return (
|
||||||
|
<Button variant={variant} size={size} asChild>
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
data-slot="alert-dialog-cancel"
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogPortal,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogMedia,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,34 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
lg: "h-10 rounded-md px-8",
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
icon: "h-9 w-9",
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -34,24 +38,27 @@ const buttonVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
function Button({
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
className,
|
||||||
VariantProps<typeof buttonVariants> {
|
variant = "default",
|
||||||
asChild?: boolean
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot.Root : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Button.displayName = "Button"
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants }
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
||||||
|
|
||||||
const Collapsible = CollapsiblePrimitive.Root
|
function Collapsible({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
function CollapsibleTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
function CollapsibleContent({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||||
|
|||||||
@@ -1,120 +1,156 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import { XIcon } from "lucide-react"
|
||||||
import { X } from "lucide-react"
|
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogPortal = DialogPrimitive.Portal
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogClose = DialogPrimitive.Close
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
function DialogOverlay({
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
<DialogPrimitive.Overlay
|
return (
|
||||||
ref={ref}
|
<DialogPrimitive.Overlay
|
||||||
className={cn(
|
data-slot="dialog-overlay"
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DialogPortal>
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"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 shadow-lg 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",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({
|
||||||
|
className,
|
||||||
|
showCloseButton = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
{showCloseButton && (
|
||||||
<X className="h-4 w-4" />
|
<DialogPrimitive.Close asChild>
|
||||||
<span className="sr-only">Close</span>
|
<Button variant="outline">Close</Button>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
)}
|
||||||
</DialogPortal>
|
</div>
|
||||||
))
|
)
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
const DialogHeader = ({
|
function DialogTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<DialogPrimitive.Title
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
data-slot="dialog-title"
|
||||||
className
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
DialogHeader.displayName = "DialogHeader"
|
|
||||||
|
|
||||||
const DialogFooter = ({
|
function DialogDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<DialogPrimitive.Description
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
data-slot="dialog-description"
|
||||||
className
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
DialogFooter.displayName = "DialogFooter"
|
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"text-lg font-semibold leading-none tracking-tight",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPortal,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,199 +1,257 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
function DropdownMenu({
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
))
|
|
||||||
DropdownMenuSubTrigger.displayName =
|
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 origin-[--radix-dropdown-menu-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSubContent.displayName =
|
|
||||||
DropdownMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
||||||
"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 origin-[--radix-dropdown-menu-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
))
|
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
))
|
|
||||||
DropdownMenuCheckboxItem.displayName =
|
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuSubContent,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,20 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
({ className, type, ...props }, ref) => {
|
return (
|
||||||
return (
|
<input
|
||||||
<input
|
type={type}
|
||||||
type={type}
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
)}
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
ref={ref}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
)
|
/>
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
Input.displayName = "Input"
|
|
||||||
|
|
||||||
export { Input }
|
export { Input }
|
||||||
|
|||||||
@@ -1,159 +1,188 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
import { Select as SelectPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
function SelectTrigger({
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
size = "default",
|
||||||
>(({ className, children, ...props }, ref) => (
|
children,
|
||||||
<SelectPrimitive.Trigger
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
className={cn(
|
size?: "sm" | "default"
|
||||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
}) {
|
||||||
className
|
return (
|
||||||
)}
|
<SelectPrimitive.Trigger
|
||||||
{...props}
|
data-slot="select-trigger"
|
||||||
>
|
data-size={size}
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
))
|
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
))
|
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
))
|
|
||||||
SelectScrollDownButton.displayName =
|
|
||||||
SelectPrimitive.ScrollDownButton.displayName
|
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
||||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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 origin-[--radix-select-content-transform-origin]",
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
position === "popper" &&
|
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
position={position}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SelectScrollUpButton />
|
{children}
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "item-aligned",
|
||||||
|
align = "center",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-1",
|
"bg-popover text-popover-foreground 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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
|
position={position}
|
||||||
|
align={align}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
<SelectScrollUpButton />
|
||||||
</SelectPrimitive.Viewport>
|
<SelectPrimitive.Viewport
|
||||||
<SelectScrollDownButton />
|
className={cn(
|
||||||
</SelectPrimitive.Content>
|
"p-1",
|
||||||
</SelectPrimitive.Portal>
|
position === "popper" &&
|
||||||
))
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
function SelectLabel({
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
<SelectPrimitive.Label
|
return (
|
||||||
ref={ref}
|
<SelectPrimitive.Label
|
||||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
data-slot="select-label"
|
||||||
{...props}
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
function SelectItem({
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
<SelectPrimitive.Item
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
ref={ref}
|
return (
|
||||||
className={cn(
|
<SelectPrimitive.Item
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
data-slot="select-item"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
{...props}
|
className
|
||||||
>
|
)}
|
||||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
{...props}
|
||||||
<SelectPrimitive.ItemIndicator>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<span
|
||||||
</SelectPrimitive.ItemIndicator>
|
data-slot="select-item-indicator"
|
||||||
</span>
|
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
>
|
||||||
</SelectPrimitive.Item>
|
<SelectPrimitive.ItemIndicator>
|
||||||
))
|
<CheckIcon className="size-4" />
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
function SelectSeparator({
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
<SelectPrimitive.Separator
|
return (
|
||||||
ref={ref}
|
<SelectPrimitive.Separator
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
data-slot="select-separator"
|
||||||
{...props}
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectLabel,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectLabel,
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,26 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
function Separator({
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
orientation = "horizontal",
|
||||||
>(
|
decorative = true,
|
||||||
(
|
...props
|
||||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
ref
|
return (
|
||||||
) => (
|
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
ref={ref}
|
data-slot="separator"
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 bg-border",
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Separator }
|
export { Separator }
|
||||||
|
|||||||
@@ -1,135 +1,138 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
import { XIcon } from "lucide-react"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||||
import { X } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Sheet = SheetPrimitive.Root
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetTrigger = SheetPrimitive.Trigger
|
function SheetTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetClose = SheetPrimitive.Close
|
function SheetClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetPortal = SheetPrimitive.Portal
|
function SheetPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetOverlay = React.forwardRef<
|
function SheetOverlay({
|
||||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||||
<SheetPrimitive.Overlay
|
return (
|
||||||
className={cn(
|
<SheetPrimitive.Overlay
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
data-slot="sheet-overlay"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
{...props}
|
className
|
||||||
ref={ref}
|
)}
|
||||||
/>
|
|
||||||
))
|
|
||||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const sheetVariants = cva(
|
|
||||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
side: {
|
|
||||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
||||||
bottom:
|
|
||||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
||||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
|
||||||
right:
|
|
||||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
side: "right",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
interface SheetContentProps
|
|
||||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
|
||||||
VariantProps<typeof sheetVariants> {}
|
|
||||||
|
|
||||||
const SheetContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
|
||||||
SheetContentProps
|
|
||||||
>(({ side = "right", className, children, ...props }, ref) => (
|
|
||||||
<SheetPortal>
|
|
||||||
<SheetOverlay />
|
|
||||||
<SheetPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(sheetVariants({ side }), className)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
/>
|
||||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
)
|
||||||
<X className="h-4 w-4" />
|
}
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</SheetPrimitive.Close>
|
|
||||||
{children}
|
|
||||||
</SheetPrimitive.Content>
|
|
||||||
</SheetPortal>
|
|
||||||
))
|
|
||||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const SheetHeader = ({
|
function SheetContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
side = "right",
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
data-slot="sheet-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
|
side === "right" &&
|
||||||
|
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||||
|
side === "left" &&
|
||||||
|
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||||
|
side === "top" &&
|
||||||
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||||
|
side === "bottom" &&
|
||||||
|
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<SheetPrimitive.Title
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
data-slot="sheet-title"
|
||||||
className
|
className={cn("text-foreground font-semibold", className)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
SheetHeader.displayName = "SheetHeader"
|
|
||||||
|
|
||||||
const SheetFooter = ({
|
function SheetDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<SheetPrimitive.Description
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
data-slot="sheet-description"
|
||||||
className
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
SheetFooter.displayName = "SheetFooter"
|
|
||||||
|
|
||||||
const SheetTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-lg font-semibold text-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const SheetDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetPortal,
|
|
||||||
SheetOverlay,
|
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
SheetClose,
|
SheetClose,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,10 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Skeleton({
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
data-slot="skeleton"
|
||||||
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,27 +1,35 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
import { Switch as SwitchPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Switch = React.forwardRef<
|
function Switch({
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
size = "default",
|
||||||
>(({ className, ...props }, ref) => (
|
...props
|
||||||
<SwitchPrimitives.Root
|
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
||||||
className={cn(
|
size?: "sm" | "default"
|
||||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none 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",
|
}) {
|
||||||
className
|
return (
|
||||||
)}
|
<SwitchPrimitive.Root
|
||||||
{...props}
|
data-slot="switch"
|
||||||
ref={ref}
|
data-size={size}
|
||||||
>
|
|
||||||
<SwitchPrimitives.Thumb
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
/>
|
{...props}
|
||||||
</SwitchPrimitives.Root>
|
>
|
||||||
))
|
<SwitchPrimitive.Thumb
|
||||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
data-slot="switch-thumb"
|
||||||
|
className={cn(
|
||||||
|
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { Switch }
|
export { Switch }
|
||||||
|
|||||||
@@ -1,30 +1,55 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider
|
function TooltipProvider({
|
||||||
|
delayDuration = 0,
|
||||||
const Tooltip = TooltipPrimitive.Root
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
return (
|
||||||
|
<TooltipPrimitive.Provider
|
||||||
const TooltipContent = React.forwardRef<
|
data-slot="tooltip-provider"
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
delayDuration={delayDuration}
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 origin-[--radix-tooltip-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</TooltipPrimitive.Portal>
|
)
|
||||||
))
|
}
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
|
||||||
|
function Tooltip({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
|
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
data-slot="tooltip-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
|
|||||||
@@ -2,64 +2,131 @@
|
|||||||
@import '@fontsource/geist/500.css';
|
@import '@fontsource/geist/500.css';
|
||||||
@import '@fontsource/geist/600.css';
|
@import '@fontsource/geist/600.css';
|
||||||
|
|
||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer base {
|
@custom-variant dark (&:is(.dark *));
|
||||||
:root {
|
|
||||||
--background: 0 0% 100%;
|
|
||||||
--foreground: 0 0% 3.9%;
|
|
||||||
--primary: 0 0% 9%;
|
|
||||||
--primary-foreground: 0 0% 98%;
|
|
||||||
--secondary: 0 0% 96.1%;
|
|
||||||
--secondary-foreground: 0 0% 9%;
|
|
||||||
--muted: 0 0% 96.1%;
|
|
||||||
--muted-foreground: 0 0% 45.1%;
|
|
||||||
--border: 0 0% 89.8%;
|
|
||||||
--input: 0 0% 89.8%;
|
|
||||||
--ring: 0 0% 83.1%;
|
|
||||||
--radius: 0.5rem;
|
|
||||||
|
|
||||||
/* Sidebar tokens — matching Figma exactly */
|
@theme inline {
|
||||||
--sidebar: 0 0% 98%;
|
--color-background: var(--background);
|
||||||
--sidebar-border: 220 13% 91%;
|
--color-foreground: var(--foreground);
|
||||||
--sidebar-accent: 240 4.8% 95.9%;
|
--color-card: var(--card);
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--color-card-foreground: var(--card-foreground);
|
||||||
--sidebar-background: 0 0% 98%;
|
--color-popover: var(--popover);
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
--color-primary: var(--primary);
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--color-secondary: var(--secondary);
|
||||||
}
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
* {
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
border-color: hsl(var(--border));
|
--color-accent: var(--accent);
|
||||||
}
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
body {
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
background-color: hsl(var(--background));
|
--color-border: var(--border);
|
||||||
color: hsl(var(--foreground));
|
--color-input: var(--input);
|
||||||
font-family: 'Geist', 'Inter', system-ui, sans-serif;
|
--color-ring: var(--ring);
|
||||||
-webkit-font-smoothing: antialiased;
|
--color-chart-1: var(--chart-1);
|
||||||
margin: 0;
|
--color-chart-2: var(--chart-2);
|
||||||
overflow: hidden; /* Electron: no OS scrollbars */
|
--color-chart-3: var(--chart-3);
|
||||||
}
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
#root {
|
--color-sidebar: var(--sidebar);
|
||||||
height: 100vh;
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
width: 100vw;
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
display: flex;
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
overflow: hidden;
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
}
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
.dark {
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--sidebar-background: 240 5.9% 10%;
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
--radius-lg: var(--radius);
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
--font-sans: 'Geist', 'Inter', system-ui, sans-serif;
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
}
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
||||||
}
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.269 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.371 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.439 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Geist', 'Inter', system-ui, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden; /* Electron: no OS scrollbars */
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { StrictMode, useState } from 'react';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { RouterProvider } from '@tanstack/react-router';
|
import { RouterProvider } from '@tanstack/react-router';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ipcLink } from 'electron-trpc/renderer';
|
import { ipcLink } from './lib/ipcLink';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
import { trpc } from './lib/trpc';
|
import { trpc } from './lib/trpc';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|||||||
76
src/renderer/lib/ipcLink.ts
Normal file
76
src/renderer/lib/ipcLink.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Renderer-side tRPC IPC link for Electron.
|
||||||
|
*
|
||||||
|
* Replaces electron-trpc's ipcLink with a custom implementation that
|
||||||
|
* works with our custom IPC handler + tRPC v11.
|
||||||
|
*/
|
||||||
|
import { observable } from '@trpc/server/observable';
|
||||||
|
import type { TRPCLink } from '@trpc/client';
|
||||||
|
import type { AnyRouter } from '@trpc/server';
|
||||||
|
|
||||||
|
interface ElectronTRPC {
|
||||||
|
sendMessage: (msg: unknown) => void;
|
||||||
|
onMessage: (cb: (data: unknown) => void) => (() => void) | void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electronTRPC: ElectronTRPC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TRPCResponse = {
|
||||||
|
id: number | null;
|
||||||
|
result?: { type: string; data?: unknown };
|
||||||
|
error?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
let nextId = 0;
|
||||||
|
|
||||||
|
export function ipcLink<TRouter extends AnyRouter>(): TRPCLink<TRouter> {
|
||||||
|
return () =>
|
||||||
|
({ op }) =>
|
||||||
|
observable((observer) => {
|
||||||
|
const id = ++nextId;
|
||||||
|
const { electronTRPC } = window;
|
||||||
|
|
||||||
|
if (!electronTRPC) {
|
||||||
|
observer.error(
|
||||||
|
new Error(
|
||||||
|
'Could not find `electronTRPC` global. ' +
|
||||||
|
'Check that the preload script has been loaded.',
|
||||||
|
) as any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribe = electronTRPC.onMessage((response: unknown) => {
|
||||||
|
const msg = response as TRPCResponse;
|
||||||
|
if (msg.id !== id) return;
|
||||||
|
|
||||||
|
if ('error' in msg) {
|
||||||
|
observer.error(msg.error as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.next({
|
||||||
|
result: msg.result as { type: 'data'; data: unknown },
|
||||||
|
});
|
||||||
|
observer.complete();
|
||||||
|
});
|
||||||
|
|
||||||
|
electronTRPC.sendMessage({
|
||||||
|
method: 'request',
|
||||||
|
operation: {
|
||||||
|
id,
|
||||||
|
type: op.type,
|
||||||
|
path: op.path,
|
||||||
|
input: op.input,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (typeof unsubscribe === 'function') unsubscribe();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ function ProjectsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full overflow-hidden">
|
<div className="flex h-full min-h-0">
|
||||||
<ProjectSidebar
|
<ProjectSidebar
|
||||||
selectedProjectId={projectId}
|
selectedProjectId={projectId}
|
||||||
onSelectProject={handleSelectProject}
|
onSelectProject={handleSelectProject}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
darkMode: ['class'],
|
|
||||||
content: ['./index.html', './src/renderer/**/*.{ts,tsx}'],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
fontFamily: {
|
|
||||||
sans: [
|
|
||||||
'Geist',
|
|
||||||
'Inter',
|
|
||||||
'system-ui',
|
|
||||||
'sans-serif'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
border: 'hsl(var(--border))',
|
|
||||||
input: 'hsl(var(--input))',
|
|
||||||
ring: 'hsl(var(--ring))',
|
|
||||||
background: 'hsl(var(--background))',
|
|
||||||
foreground: 'hsl(var(--foreground))',
|
|
||||||
primary: {
|
|
||||||
DEFAULT: 'hsl(var(--primary))',
|
|
||||||
foreground: 'hsl(var(--primary-foreground))'
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
DEFAULT: 'hsl(var(--secondary))',
|
|
||||||
foreground: 'hsl(var(--secondary-foreground))'
|
|
||||||
},
|
|
||||||
muted: {
|
|
||||||
DEFAULT: 'hsl(var(--muted))',
|
|
||||||
foreground: 'hsl(var(--muted-foreground))'
|
|
||||||
},
|
|
||||||
sidebar: {
|
|
||||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
|
||||||
border: 'hsl(var(--sidebar-border))',
|
|
||||||
accent: 'hsl(var(--sidebar-accent))',
|
|
||||||
foreground: 'hsl(var(--sidebar-foreground))',
|
|
||||||
primary: 'hsl(var(--sidebar-primary))',
|
|
||||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
|
||||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
|
||||||
ring: 'hsl(var(--sidebar-ring))'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
borderRadius: {
|
|
||||||
lg: 'var(--radius)',
|
|
||||||
md: 'calc(var(--radius) - 2px)',
|
|
||||||
sm: 'calc(var(--radius) - 4px)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import { TanStackRouterVite } from '@tanstack/router-vite-plugin';
|
import { TanStackRouterVite } from '@tanstack/router-vite-plugin';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
// https://vitejs.dev/config
|
// https://vitejs.dev/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
react(),
|
react(),
|
||||||
TanStackRouterVite({
|
TanStackRouterVite({
|
||||||
routesDirectory: './src/renderer/routes',
|
routesDirectory: './src/renderer/routes',
|
||||||
|
|||||||
Reference in New Issue
Block a user