feat(web): add credential management UI pages and components
Add credentials settings page, audit log page, CRUD dialog components (create, view, edit, rotate), credential card, dialog UI component, and API client for the M7-CredentialSecurity feature. Refs #346 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
128
apps/web/src/components/ui/dialog.tsx
Normal file
128
apps/web/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import * as React from "react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
export interface DialogProps {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface DialogTriggerProps {
|
||||
children?: React.ReactNode;
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export interface DialogContentProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface DialogHeaderProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface DialogFooterProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface DialogTitleProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface DialogDescriptionProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DialogContext = React.createContext<{
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}>({});
|
||||
|
||||
export function Dialog({ open, onOpenChange, children }: DialogProps): React.JSX.Element {
|
||||
const contextValue: { open?: boolean; onOpenChange?: (open: boolean) => void } = {};
|
||||
if (open !== undefined) {
|
||||
contextValue.open = open;
|
||||
}
|
||||
if (onOpenChange !== undefined) {
|
||||
contextValue.onOpenChange = onOpenChange;
|
||||
}
|
||||
|
||||
return <DialogContext.Provider value={contextValue}>{children}</DialogContext.Provider>;
|
||||
}
|
||||
|
||||
export function DialogTrigger({ children, asChild }: DialogTriggerProps): React.JSX.Element {
|
||||
const { onOpenChange } = React.useContext(DialogContext);
|
||||
|
||||
if (asChild && React.isValidElement(children)) {
|
||||
return React.cloneElement(children, {
|
||||
onClick: () => onOpenChange?.(true),
|
||||
} as React.HTMLAttributes<HTMLElement>);
|
||||
}
|
||||
|
||||
return <div onClick={() => onOpenChange?.(true)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function DialogContent({
|
||||
children,
|
||||
className = "",
|
||||
}: DialogContentProps): React.JSX.Element | null {
|
||||
const { open, onOpenChange } = React.useContext(DialogContext);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Overlay */}
|
||||
<div className="fixed inset-0 bg-black/50" onClick={() => onOpenChange?.(false)} />
|
||||
|
||||
{/* Dialog Content */}
|
||||
<div
|
||||
className={`relative z-50 w-full max-w-lg rounded-lg bg-white p-6 shadow-lg ${className}`}
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={() => onOpenChange?.(false)}
|
||||
className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogHeader({ children, className = "" }: DialogHeaderProps): React.JSX.Element {
|
||||
return <div className={`mb-4 ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
export function DialogFooter({ children, className = "" }: DialogFooterProps): React.JSX.Element {
|
||||
return <div className={`mt-4 flex justify-end gap-2 ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
export function DialogTitle({ children, className = "" }: DialogTitleProps): React.JSX.Element {
|
||||
return <h2 className={`text-lg font-semibold ${className}`}>{children}</h2>;
|
||||
}
|
||||
|
||||
export function DialogDescription({
|
||||
children,
|
||||
className = "",
|
||||
}: DialogDescriptionProps): React.JSX.Element {
|
||||
return <p className={`text-sm text-gray-600 ${className}`}>{children}</p>;
|
||||
}
|
||||
|
||||
export const DialogPortal = ({ children }: { children: React.ReactNode }): React.JSX.Element => (
|
||||
<>{children}</>
|
||||
);
|
||||
export const DialogOverlay = ({ children }: { children: React.ReactNode }): React.JSX.Element => (
|
||||
<>{children}</>
|
||||
);
|
||||
export const DialogClose = ({ children }: { children: React.ReactNode }): React.JSX.Element => (
|
||||
<>{children}</>
|
||||
);
|
||||
Reference in New Issue
Block a user