Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion packages/SimpleModule.Theme.Default/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@
pointer-events: auto;
}

/* --- Nav Link --- */
/* --- Nav Link (public layout) --- */
.nav-link-active,
.nav-link-inactive {
@apply block px-4 py-2.5 rounded-xl text-sm no-underline transition-all duration-150;
Expand All @@ -384,6 +384,33 @@
.nav-link-inactive:hover {
@apply bg-surface-raised text-text;
}

/* --- Sidebar Nav Link (app layout, with icon) --- */
.sidebar-nav-link-active,
.sidebar-nav-link-inactive {
@apply flex items-center gap-3 px-3 py-2 rounded-xl text-sm no-underline transition-all duration-150 outline-none focus-visible:ring-4 focus-visible:ring-primary-ring;
}
.sidebar-nav-link-active {
@apply font-medium text-primary bg-primary-subtle;
}
.sidebar-nav-link-inactive {
@apply text-text-secondary;
}
.sidebar-nav-link-inactive:hover {
@apply bg-surface-raised text-text;
}
}

/* ============================================================
Focus Ring Utilities
============================================================ */
@layer utilities {
.focus-ring {
@apply outline-none focus-visible:ring-4 focus-visible:ring-primary-ring focus-visible:border-primary;
}
.focus-ring-danger {
@apply outline-none focus-visible:ring-4 focus-visible:ring-danger-bg focus-visible:border-danger;
}
}

/* ============================================================
Expand Down
65 changes: 62 additions & 3 deletions packages/SimpleModule.UI/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as React from 'react';
import { cn } from '../lib/utils';

const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 rounded-xl text-sm font-semibold transition-all duration-200 active:scale-[0.97] cursor-pointer disabled:pointer-events-none disabled:opacity-50',
'inline-flex items-center justify-center gap-2 rounded-xl text-sm font-semibold transition-all duration-200 active:scale-[0.97] cursor-pointer disabled:pointer-events-none disabled:opacity-50 outline-none focus-visible:ring-4 focus-visible:ring-primary-ring',
{
variants: {
variant: {
Expand All @@ -22,6 +22,7 @@ const buttonVariants = cva(
sm: 'px-3.5 py-1.5 text-xs rounded-lg',
default: 'px-5 py-2.5',
lg: 'px-8 py-3.5 text-base',
icon: 'h-9 w-9 p-0',
},
},
defaultVariants: {
Expand All @@ -31,17 +32,75 @@ const buttonVariants = cva(
},
);

const spinnerSizeMap = {
sm: 'w-3 h-3',
default: 'w-4 h-4',
lg: 'w-4 h-4',
icon: 'w-4 h-4',
} as const;

interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
isLoading?: boolean;
loadingText?: React.ReactNode;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
(
{
className,
variant,
size,
asChild = false,
isLoading = false,
loadingText,
disabled,
children,
...props
},
ref,
) => {
const Comp = asChild ? Slot : 'button';
const spinnerClass = spinnerSizeMap[size ?? 'default'];

if (asChild) {
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
aria-busy={isLoading || undefined}
{...props}
>
{children}
</Comp>
);
}

return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
disabled={disabled || isLoading}
aria-busy={isLoading || undefined}
{...props}
>
{isLoading ? (
<>
<span
aria-hidden="true"
className={cn(
'inline-block border-2 border-current/30 border-t-current rounded-full animate-spin',
spinnerClass,
)}
/>
{loadingText ?? children}
</>
) : (
children
)}
</Comp>
);
},
);
Expand Down
42 changes: 31 additions & 11 deletions packages/SimpleModule.UI/components/card.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { cn } from '../lib/utils';

const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'bg-surface border border-border rounded-2xl p-5 m-2 transition-all duration-200 hover:border-border-strong',
className,
)}
{...props}
/>
const cardVariants = cva('rounded-2xl m-2 transition-all duration-200', {
variants: {
variant: {
default: 'bg-surface border border-border hover:border-border-strong',
flat: 'bg-surface border border-border',
elevated: 'bg-surface border border-border shadow-lg hover:shadow-xl',
ghost: 'bg-transparent',
},
padding: {
none: 'p-0',
sm: 'p-3',
default: 'p-5',
lg: 'p-6 sm:p-8',
},
},
defaultVariants: {
variant: 'default',
padding: 'default',
},
});

interface CardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof cardVariants> {}

const Card = React.forwardRef<HTMLDivElement, CardProps>(
({ className, variant, padding, ...props }, ref) => (
<div ref={ref} className={cn(cardVariants({ variant, padding }), className)} {...props} />
),
);
Card.displayName = 'Card';
Expand Down Expand Up @@ -53,4 +72,5 @@ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDiv
);
CardFooter.displayName = 'CardFooter';

export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
export type { CardProps };
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cardVariants };
34 changes: 28 additions & 6 deletions packages/SimpleModule.UI/components/dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { cn } from '../lib/utils';

Expand All @@ -22,18 +23,37 @@ const DialogOverlay = React.forwardRef<
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

const dialogContentVariants = cva(
'fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-surface p-6 shadow-lg rounded-2xl 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',
{
variants: {
size: {
sm: 'max-w-sm',
default: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
full: 'max-w-[calc(100vw-2rem)] max-h-[calc(100vh-2rem)] overflow-y-auto',
},
},
defaultVariants: {
size: 'default',
},
},
);

interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
VariantProps<typeof dialogContentVariants> {}

const DialogContent = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
DialogContentProps
>(({ className, size, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-surface p-6 shadow-lg rounded-2xl 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',
className,
)}
className={cn(dialogContentVariants({ size }), className)}
{...props}
>
{children}
Expand Down Expand Up @@ -92,6 +112,7 @@ const DialogDescription = React.forwardRef<
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;

export type { DialogContentProps };
export {
Dialog,
DialogClose,
Expand All @@ -103,4 +124,5 @@ export {
DialogPortal,
DialogTitle,
DialogTrigger,
dialogContentVariants,
};
13 changes: 12 additions & 1 deletion packages/SimpleModule.UI/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ export {
} from './breadcrumb';
export { Button, buttonVariants } from './button';
export { Calendar, type CalendarProps } from './calendar';
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './card';
export {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
type CardProps,
CardTitle,
cardVariants,
} from './card';
export {
type ChartConfig,
ChartContainer,
Expand Down Expand Up @@ -44,13 +53,15 @@ export {
Dialog,
DialogClose,
DialogContent,
type DialogContentProps,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
dialogContentVariants,
} from './dialog';
export {
DropdownMenu,
Expand Down
50 changes: 41 additions & 9 deletions packages/SimpleModule.UI/components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,50 @@ const inputVariants = cva(
);

interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement>,
VariantProps<typeof inputVariants> {}
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'>,
VariantProps<typeof inputVariants> {
prefix?: React.ReactNode;
suffix?: React.ReactNode;
wrapperClassName?: string;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, variant, type, ...props }, ref) => {
({ className, variant, type, prefix, suffix, wrapperClassName, ...props }, ref) => {
if (prefix == null && suffix == null) {
return (
<input
type={type}
className={cn(inputVariants({ variant, className }))}
ref={ref}
{...props}
/>
);
}

return (
<input
type={type}
className={cn(inputVariants({ variant, className }))}
ref={ref}
{...props}
/>
<div
className={cn(
'relative flex items-center w-full',
// make adornments inherit muted color and not steal focus
'[&>[data-slot=prefix]]:absolute [&>[data-slot=prefix]]:left-3 [&>[data-slot=prefix]]:text-text-muted [&>[data-slot=prefix]]:pointer-events-none',
'[&>[data-slot=suffix]]:absolute [&>[data-slot=suffix]]:right-3 [&>[data-slot=suffix]]:text-text-muted',
wrapperClassName,
)}
>
{prefix != null && <span data-slot="prefix">{prefix}</span>}
<input
type={type}
className={cn(
inputVariants({ variant }),
prefix != null && 'pl-10',
suffix != null && 'pr-10',
className,
)}
ref={ref}
{...props}
/>
{suffix != null && <span data-slot="suffix">{suffix}</span>}
</div>
);
},
);
Expand Down
11 changes: 5 additions & 6 deletions packages/SimpleModule.UI/components/layouts/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ import { DarkModeToggle } from './dark-mode-toggle';
import type { MenuItem, SharedProps } from './types';
import { UserDropdown } from './user-dropdown';

const ACTIVE_CLASS =
'flex items-center gap-3 px-3 py-2 rounded-xl text-sm font-medium text-primary bg-primary-subtle no-underline transition-all duration-150';
const INACTIVE_CLASS =
'flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-text-secondary no-underline hover:bg-surface-raised hover:text-text transition-all duration-150';

function isActiveUrl(url: string, pathname: string): boolean {
if (url === '/') return pathname === '/';
return pathname.toLowerCase().startsWith(url.toLowerCase());
Expand All @@ -30,7 +25,11 @@ function NavLink({
}) {
const active = isActiveUrl(item.url, pathname);
return (
<Link href={item.url} className={active ? ACTIVE_CLASS : INACTIVE_CLASS} onClick={onClick}>
<Link
href={item.url}
className={active ? 'sidebar-nav-link-active' : 'sidebar-nav-link-inactive'}
onClick={onClick}
>
<SidebarIcon icon={item.icon} />
<span className="sidebar-label">{item.label}</span>
</Link>
Expand Down
2 changes: 1 addition & 1 deletion template/SimpleModule.Host/wwwroot/css/app.css

Large diffs are not rendered by default.

Loading