1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 20:39:57 +01:00

feat(frontend): Tauri Apps, Shared UI Library und Nix Flake

- packages/ui: Shared React Component Library (Button, Card, Badge, Input)
  mit Tailwind v4 @theme Design Tokens (oklch)
- apps/web: ERP Web-UI (Vite + React + Tailwind v4, API-Proxy :8080)
- apps/scanner: Tauri v2 Mobile App mit Barcode-Scanner Plugin
  (cfg(mobile) für Desktop-Kompatibilität)
- flake.nix: Nix Flake mit rust-overlay, Tauri System-Deps (GTK,
  WebKitGTK, libsoup, OpenSSL), ersetzt shell.nix
- justfile: Dev-Befehle für alle Projekte (backend, cli, web, scanner)
- frontend/CLAUDE.md: Agent Guide mit Base UI Docs Referenz
This commit is contained in:
Sebastian Frick 2026-03-19 16:45:47 +01:00
parent b9b89e3f0e
commit ef50eb8279
96 changed files with 11682 additions and 16 deletions

View file

@ -0,0 +1,34 @@
import { forwardRef, type HTMLAttributes } from 'react';
import { cn } from '../lib/cn';
type BadgeVariant = 'success' | 'warning' | 'danger' | 'info' | 'neutral';
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
variant?: BadgeVariant;
}
const variantStyles: Record<BadgeVariant, string> = {
success: 'bg-success-100 text-success-800 border-success-300',
warning: 'bg-warning-100 text-warning-800 border-warning-300',
danger: 'bg-danger-100 text-danger-800 border-danger-300',
info: 'bg-info-100 text-info-800 border-info-300',
neutral: 'bg-warm-100 text-warm-700 border-warm-300',
};
export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
({ className, variant = 'neutral', ...props }, ref) => {
return (
<span
ref={ref}
className={cn(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium',
variantStyles[variant],
className
)}
{...props}
/>
);
}
);
Badge.displayName = 'Badge';

View file

@ -0,0 +1,48 @@
import { forwardRef, type ButtonHTMLAttributes } from 'react';
import { cn } from '../lib/cn';
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
}
const variantStyles: Record<ButtonVariant, string> = {
primary:
'bg-brand-600 text-white hover:bg-brand-700 active:bg-brand-800 focus-visible:ring-brand-500',
secondary:
'bg-warm-100 text-warm-800 hover:bg-warm-200 active:bg-warm-300 focus-visible:ring-warm-400 border border-warm-300',
ghost: 'text-warm-700 hover:bg-warm-100 active:bg-warm-200 focus-visible:ring-warm-400',
danger:
'bg-danger-600 text-white hover:bg-danger-700 active:bg-danger-800 focus-visible:ring-danger-500',
};
const sizeStyles: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-5 py-2.5 text-base',
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'primary', size = 'md', disabled, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
'inline-flex items-center justify-center font-medium rounded-md transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
'disabled:opacity-50 disabled:pointer-events-none',
variantStyles[variant],
sizeStyles[size],
className
)}
disabled={disabled}
{...props}
/>
);
}
);
Button.displayName = 'Button';

View file

@ -0,0 +1,19 @@
import { forwardRef, type HTMLAttributes } from 'react';
import { cn } from '../lib/cn';
export interface CardProps extends HTMLAttributes<HTMLDivElement> {}
export const Card = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
'rounded-lg border border-warm-200 bg-white p-6 shadow-sm',
className
)}
{...props}
/>
);
});
Card.displayName = 'Card';

View file

@ -0,0 +1,22 @@
import { forwardRef, type InputHTMLAttributes } from 'react';
import { cn } from '../lib/cn';
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
export const Input = forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
return (
<input
ref={ref}
className={cn(
'block w-full rounded-md border border-warm-300 bg-white px-3 py-2 text-sm text-warm-900',
'placeholder:text-warm-400',
'focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20',
'disabled:cursor-not-allowed disabled:bg-warm-50 disabled:opacity-50',
className
)}
{...props}
/>
);
});
Input.displayName = 'Input';

View file

@ -0,0 +1,5 @@
export { Button, type ButtonProps } from './components/Button';
export { Card, type CardProps } from './components/Card';
export { Badge, type BadgeProps } from './components/Badge';
export { Input, type InputProps } from './components/Input';
export { cn } from './lib/cn';

View file

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}

View file

@ -0,0 +1,88 @@
@theme {
/* Brand warm orange/amber */
--color-brand-50: oklch(0.98 0.02 75);
--color-brand-100: oklch(0.95 0.04 75);
--color-brand-200: oklch(0.9 0.08 75);
--color-brand-300: oklch(0.83 0.12 75);
--color-brand-400: oklch(0.75 0.16 70);
--color-brand-500: oklch(0.65 0.19 55);
--color-brand-600: oklch(0.58 0.2 50);
--color-brand-700: oklch(0.5 0.18 50);
--color-brand-800: oklch(0.42 0.14 50);
--color-brand-900: oklch(0.35 0.1 50);
--color-brand-950: oklch(0.25 0.07 50);
/* Warm neutrals */
--color-warm-50: oklch(0.98 0.005 75);
--color-warm-100: oklch(0.95 0.01 75);
--color-warm-200: oklch(0.91 0.015 75);
--color-warm-300: oklch(0.85 0.02 75);
--color-warm-400: oklch(0.7 0.02 75);
--color-warm-500: oklch(0.55 0.02 75);
--color-warm-600: oklch(0.45 0.02 75);
--color-warm-700: oklch(0.37 0.015 75);
--color-warm-800: oklch(0.3 0.01 75);
--color-warm-900: oklch(0.22 0.008 75);
--color-warm-950: oklch(0.15 0.005 75);
/* Success green */
--color-success-50: oklch(0.97 0.03 145);
--color-success-100: oklch(0.93 0.06 145);
--color-success-200: oklch(0.87 0.1 145);
--color-success-300: oklch(0.78 0.15 145);
--color-success-400: oklch(0.68 0.18 145);
--color-success-500: oklch(0.6 0.17 145);
--color-success-600: oklch(0.52 0.15 145);
--color-success-700: oklch(0.45 0.12 145);
--color-success-800: oklch(0.38 0.1 145);
--color-success-900: oklch(0.32 0.07 145);
--color-success-950: oklch(0.22 0.05 145);
/* Warning yellow/amber */
--color-warning-50: oklch(0.98 0.03 95);
--color-warning-100: oklch(0.95 0.07 95);
--color-warning-200: oklch(0.9 0.12 90);
--color-warning-300: oklch(0.84 0.17 85);
--color-warning-400: oklch(0.78 0.18 80);
--color-warning-500: oklch(0.72 0.17 75);
--color-warning-600: oklch(0.62 0.16 65);
--color-warning-700: oklch(0.52 0.13 60);
--color-warning-800: oklch(0.44 0.1 60);
--color-warning-900: oklch(0.37 0.07 60);
--color-warning-950: oklch(0.27 0.05 60);
/* Danger red */
--color-danger-50: oklch(0.97 0.02 25);
--color-danger-100: oklch(0.93 0.05 25);
--color-danger-200: oklch(0.87 0.1 25);
--color-danger-300: oklch(0.78 0.15 25);
--color-danger-400: oklch(0.68 0.18 25);
--color-danger-500: oklch(0.6 0.2 25);
--color-danger-600: oklch(0.52 0.19 25);
--color-danger-700: oklch(0.45 0.16 25);
--color-danger-800: oklch(0.38 0.12 25);
--color-danger-900: oklch(0.32 0.09 25);
--color-danger-950: oklch(0.22 0.06 25);
/* Info blue */
--color-info-50: oklch(0.97 0.02 245);
--color-info-100: oklch(0.93 0.05 245);
--color-info-200: oklch(0.87 0.09 245);
--color-info-300: oklch(0.78 0.14 245);
--color-info-400: oklch(0.68 0.17 245);
--color-info-500: oklch(0.6 0.18 245);
--color-info-600: oklch(0.52 0.17 245);
--color-info-700: oklch(0.45 0.14 245);
--color-info-800: oklch(0.38 0.11 245);
--color-info-900: oklch(0.32 0.08 245);
--color-info-950: oklch(0.22 0.06 245);
/* Font */
--font-sans: 'Poppins', ui-sans-serif, system-ui, sans-serif;
/* Radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
}