From 7004d798d6746f4d1d362baa15bc285124c544e3 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 24 Jul 2026 16:04:41 +0200 Subject: [PATCH] feat(ui): Primitives & Design Tokens fuer Cinematic Cinema OS --- docker/ui/src/components/ui/Badge.tsx | 39 ++++++++++ docker/ui/src/components/ui/Button.tsx | 43 +++++++++++ docker/ui/src/components/ui/Card.tsx | 47 ++++++++++++ docker/ui/src/components/ui/Input.tsx | 85 ++++++++++++++++++++++ docker/ui/src/components/ui/Modal.tsx | 66 +++++++++++++++++ docker/ui/src/components/ui/PageHeader.tsx | 19 +++++ docker/ui/src/index.css | 68 +++++------------ docker/ui/src/lib/design.ts | 85 ++++++++++++++++++++++ 8 files changed, 403 insertions(+), 49 deletions(-) create mode 100644 docker/ui/src/components/ui/Badge.tsx create mode 100644 docker/ui/src/components/ui/Button.tsx create mode 100644 docker/ui/src/components/ui/Card.tsx create mode 100644 docker/ui/src/components/ui/Input.tsx create mode 100644 docker/ui/src/components/ui/Modal.tsx create mode 100644 docker/ui/src/components/ui/PageHeader.tsx create mode 100644 docker/ui/src/lib/design.ts diff --git a/docker/ui/src/components/ui/Badge.tsx b/docker/ui/src/components/ui/Badge.tsx new file mode 100644 index 0000000..5028e28 --- /dev/null +++ b/docker/ui/src/components/ui/Badge.tsx @@ -0,0 +1,39 @@ +import { Disc } from 'lucide-react' +import { STATUS_STYLES, DISC_TYPE_STYLES } from '../../lib/design' + +interface StatusBadgeProps { + status: string + className?: string +} + +export function StatusBadge({ status, className = '' }: StatusBadgeProps) { + const config = STATUS_STYLES[status as keyof typeof STATUS_STYLES] || { + badge: 'bg-slate-500/15 text-slate-400 border-slate-500/30', + label: status, + } + + return ( + + {config.label} + + ) +} + +interface TypeBadgeProps { + type: string + className?: string +} + +export function TypeBadge({ type, className = '' }: TypeBadgeProps) { + const config = DISC_TYPE_STYLES[type as keyof typeof DISC_TYPE_STYLES] || { + badge: 'bg-slate-500/15 text-slate-400 border-slate-500/30', + label: type.toUpperCase(), + } + + return ( + + + {config.label} + + ) +} diff --git a/docker/ui/src/components/ui/Button.tsx b/docker/ui/src/components/ui/Button.tsx new file mode 100644 index 0000000..979da9e --- /dev/null +++ b/docker/ui/src/components/ui/Button.tsx @@ -0,0 +1,43 @@ +import { ButtonHTMLAttributes, ReactNode } from 'react' + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'amber' + size?: 'sm' | 'md' | 'lg' + children: ReactNode + className?: string +} + +export function Button({ + variant = 'primary', + size = 'md', + children, + className = '', + disabled, + ...props +}: ButtonProps) { + const base = 'inline-flex items-center justify-center font-medium rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed active:scale-[0.98]' + + const variants = { + primary: 'bg-indigo-600 hover:bg-indigo-500 text-white shadow-lg shadow-indigo-500/25 dark:shadow-indigo-900/40 focus:ring-indigo-500', + amber: 'bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-400 hover:to-amber-500 text-slate-950 font-semibold shadow-lg shadow-amber-500/20 focus:ring-amber-500', + secondary: 'bg-slate-100 dark:bg-slate-800/80 hover:bg-slate-200 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-700/80 focus:ring-slate-400', + ghost: 'bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800/60 text-slate-600 dark:text-slate-300 focus:ring-slate-400', + danger: 'bg-rose-600/90 hover:bg-rose-500 text-white shadow-lg shadow-rose-500/20 focus:ring-rose-500', + } + + const sizes = { + sm: 'px-2.5 py-1.5 text-xs gap-1.5', + md: 'px-4 py-2 text-sm gap-2', + lg: 'px-5 py-2.5 text-base gap-2.5', + } + + return ( + + ) +} diff --git a/docker/ui/src/components/ui/Card.tsx b/docker/ui/src/components/ui/Card.tsx new file mode 100644 index 0000000..6a2b277 --- /dev/null +++ b/docker/ui/src/components/ui/Card.tsx @@ -0,0 +1,47 @@ +import { ReactNode } from 'react' + +interface CardProps { + children: ReactNode + className?: string + glow?: boolean + onClick?: () => void +} + +export function Card({ children, className = '', glow = false, onClick }: CardProps) { + return ( +
+ {children} +
+ ) +} + +export function CardHeader({ children, className = '' }: { children: ReactNode; className?: string }) { + return ( +
+ {children} +
+ ) +} + +export function CardTitle({ children, className = '' }: { children: ReactNode; className?: string }) { + return ( +

+ {children} +

+ ) +} + +export function CardContent({ children, className = '' }: { children: ReactNode; className?: string }) { + return ( +
+ {children} +
+ ) +} diff --git a/docker/ui/src/components/ui/Input.tsx b/docker/ui/src/components/ui/Input.tsx new file mode 100644 index 0000000..061545a --- /dev/null +++ b/docker/ui/src/components/ui/Input.tsx @@ -0,0 +1,85 @@ +import { InputHTMLAttributes, SelectHTMLAttributes, ReactNode } from 'react' + +interface InputProps extends InputHTMLAttributes { + label?: string + error?: string + className?: string +} + +export function Input({ label, error, className = '', ...props }: InputProps) { + return ( +
+ {label && ( + + )} + + {error &&

{error}

} +
+ ) +} + +interface SelectProps extends SelectHTMLAttributes { + label?: string + children: ReactNode + className?: string +} + +export function Select({ label, children, className = '', ...props }: SelectProps) { + return ( +
+ {label && ( + + )} + +
+ ) +} + +interface ToggleProps { + checked: boolean + onChange: (checked: boolean) => void + label?: string + description?: string + disabled?: boolean +} + +export function Toggle({ checked, onChange, label, description, disabled }: ToggleProps) { + return ( +
+ {(label || description) && ( +
+ {label && {label}} + {description &&

{description}

} +
+ )} + +
+ ) +} diff --git a/docker/ui/src/components/ui/Modal.tsx b/docker/ui/src/components/ui/Modal.tsx new file mode 100644 index 0000000..0abe2e8 --- /dev/null +++ b/docker/ui/src/components/ui/Modal.tsx @@ -0,0 +1,66 @@ +import { ReactNode, useEffect } from 'react' +import { X } from 'lucide-react' + +interface ModalProps { + isOpen: boolean + onClose: () => void + title?: string + children: ReactNode + maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '4xl' +} + +export function Modal({ isOpen, onClose, title, children, maxWidth = 'lg' }: ModalProps) { + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + if (isOpen) { + document.body.style.overflow = 'hidden' + window.addEventListener('keydown', handleKeyDown) + } + return () => { + document.body.style.overflow = 'unset' + window.removeEventListener('keydown', handleKeyDown) + } + }, [isOpen, onClose]) + + if (!isOpen) return null + + const maxWidening = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + xl: 'max-w-xl', + '2xl': 'max-w-2xl', + '4xl': 'max-w-4xl', + } + + return ( +
+ {/* Glass Backdrop */} +
+ + {/* Modal Dialog */} +
+ {title && ( +
+

{title}

+ +
+ )} + +
+ {children} +
+
+
+ ) +} diff --git a/docker/ui/src/components/ui/PageHeader.tsx b/docker/ui/src/components/ui/PageHeader.tsx new file mode 100644 index 0000000..e0b05ad --- /dev/null +++ b/docker/ui/src/components/ui/PageHeader.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from 'react' + +interface PageHeaderProps { + title: string + subtitle?: string + action?: ReactNode +} + +export function PageHeader({ title, subtitle, action }: PageHeaderProps) { + return ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {action &&
{action}
} +
+ ) +} diff --git a/docker/ui/src/index.css b/docker/ui/src/index.css index 284e0ee..284ec59 100644 --- a/docker/ui/src/index.css +++ b/docker/ui/src/index.css @@ -2,57 +2,13 @@ @tailwind components; @tailwind utilities; -:root { - --bg-color: #f8fafc; - --bg-secondary: #f1f5f9; - --text-primary: #0f172a; - --text-secondary: #64748b; - --accent-color: #6366f1; - --accent-hover: #4f46e5; - --border-color: #e2e8f0; - --card-bg: #ffffff; - --input-bg: #ffffff; - --input-border: #e2e8f0; - --success-color: #22c55e; - --error-color: #ef4444; +@layer base { + body { + @apply m-0 bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100 font-sans antialiased selection:bg-amber-500/30 selection:text-amber-400; + } } -[data-theme="dark"] { - --bg-color: #0f172a; - --bg-secondary: #1e293b; - --text-primary: #f1f5f9; - --text-secondary: #94a3b8; - --accent-color: #818cf8; - --accent-hover: #6366f1; - --border-color: #334155; - --card-bg: #1e293b; - --input-bg: #0f172a; - --input-border: #334155; -} - -* { - transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; -} - -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-color: var(--bg-color); - color: var(--text-primary); -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; - background-color: var(--bg-secondary); - color: var(--text-primary); -} - -/* Toast-Einflug (ToastContext): von rechts reinploppen */ +/* Toast Einflug */ @keyframes toast-rein { from { opacity: 0; @@ -67,3 +23,17 @@ code { .toast-rein { animation: toast-rein 0.25s ease-out; } + +/* Subtile Disc-Rotation für Cinematic Spotlight */ +@keyframes disc-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.animate-disc-spin { + animation: disc-spin 14s linear infinite; +} diff --git a/docker/ui/src/lib/design.ts b/docker/ui/src/lib/design.ts new file mode 100644 index 0000000..ee62395 --- /dev/null +++ b/docker/ui/src/lib/design.ts @@ -0,0 +1,85 @@ +// Centralized Design System & Style Tokens for Rippy (Cinematic Cinema OS) + +export const STATUS_STYLES = { + pending: { + badge: 'bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/30', + label: 'Wartend', + }, + processing: { + badge: 'bg-sky-500/15 text-sky-600 dark:text-sky-400 border-sky-500/30 animate-pulse', + label: 'Rippen', + }, + transcoding: { + badge: 'bg-purple-500/15 text-purple-600 dark:text-purple-400 border-purple-500/30', + label: 'Komprimieren', + }, + canceling: { + badge: 'bg-orange-500/15 text-orange-600 dark:text-orange-400 border-orange-500/30', + label: 'Wird abgebrochen…', + }, + completed: { + badge: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/30', + label: 'Fertig', + }, + failed: { + badge: 'bg-rose-500/15 text-rose-600 dark:text-rose-400 border-rose-500/30', + label: 'Fehler', + }, +} as const + +export const DISC_TYPE_STYLES = { + cd: { + badge: 'bg-indigo-500/15 text-indigo-600 dark:text-indigo-400 border-indigo-500/30', + aura: 'from-indigo-500/20 via-indigo-900/10 to-transparent', + glow: 'shadow-[0_0_20px_-5px_rgba(99,102,241,0.2)]', + label: 'CD', + }, + dvd: { + badge: 'bg-purple-500/15 text-purple-600 dark:text-purple-400 border-purple-500/30', + aura: 'from-purple-500/20 via-purple-900/10 to-transparent', + glow: 'shadow-[0_0_20px_-5px_rgba(168,85,247,0.2)]', + label: 'DVD', + }, + bluray: { + badge: 'bg-sky-500/15 text-sky-600 dark:text-sky-400 border-sky-500/30', + aura: 'from-sky-500/20 via-sky-900/10 to-transparent', + glow: 'shadow-[0_0_20px_-5px_rgba(14,165,233,0.2)]', + label: 'BLU-RAY', + }, + uhd: { + badge: 'bg-amber-500/20 text-amber-700 dark:text-amber-300 border-amber-500/40 font-semibold shadow-sm shadow-amber-500/20', + aura: 'from-amber-500/25 via-amber-900/15 to-transparent', + glow: 'shadow-[0_0_25px_-5px_rgba(245,158,11,0.25)]', + label: '4K UHD', + }, +} as const + +export const LOG_LEVEL_STYLES = { + INFO: { + color: 'text-sky-600 dark:text-sky-400', + bg: 'bg-sky-500/10', + border: 'border-sky-500/20', + }, + WARNING: { + color: 'text-amber-600 dark:text-amber-400', + bg: 'bg-amber-500/10', + border: 'border-amber-500/20', + }, + ERROR: { + color: 'text-rose-600 dark:text-rose-400', + bg: 'bg-rose-500/10', + border: 'border-rose-500/20', + }, + DEBUG: { + color: 'text-slate-500 dark:text-slate-400', + bg: 'bg-slate-500/10', + border: 'border-slate-500/20', + }, +} as const + +export const ENCODER_BADGES: Record = { + 'cpu-x264': { label: 'H.264 (CPU)', highlight: false }, + 'cpu-x265': { label: 'H.265 (CPU)', highlight: false }, + 'vaapi': { label: 'VAAPI ⚡', highlight: true }, + 'nvenc': { label: 'NVENC ⚡', highlight: true }, +}