From 1cbcb2051ff556d0df2f6071bc5883441df6f8eb Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Mon, 19 Jan 2026 23:41:56 +0000 Subject: [PATCH] feat: implement Material Design 3 styles for buttons and dialogs - Revamped button styles to align with Material Design 3 guidelines, including new variants (filled, tonal, elevated, outlined, text, and icon buttons). - Enhanced button states with hover, focus, and active effects, incorporating opacity transitions and background color changes. - Updated dialog styles to reflect MD3 design principles, including scrim overlay, dialog content, headers, footers, and close buttons. - Introduced animations for dialogs, dropdowns, and snackbars to improve user experience. - Refined dropdown menu and select component styles, ensuring consistency with MD3 aesthetics. --- CODE_STYLE.md | 153 +++++ src/app/globals.scss | 604 ++++++++++++++++-- .../features/python-runner/PythonOutput.tsx | 194 ++++-- .../layout/navigation/NavigationSidebar.tsx | 177 +++-- src/components/ui/badge.tsx | 115 +++- src/components/ui/bottom-navigation.tsx | 149 +++++ src/components/ui/button.tsx | 115 +++- src/components/ui/card.tsx | 91 ++- src/components/ui/chip.tsx | 166 +++++ src/components/ui/dropdown-menu.tsx | 116 +++- src/components/ui/fab.tsx | 172 +++++ src/components/ui/input.tsx | 204 +++++- src/components/ui/popover.tsx | 56 +- src/components/ui/switch.tsx | 42 +- src/components/ui/top-app-bar.tsx | 184 ++++++ src/lib/pyodide-runner.ts | 102 ++- src/styles/abstracts/_variables.scss | 429 +++++++++++-- src/styles/components/_buttons.scss | 348 ++++++++-- src/styles/components/_dialogs.scss | 466 +++++++++----- 19 files changed, 3395 insertions(+), 488 deletions(-) create mode 100644 CODE_STYLE.md create mode 100644 src/components/ui/bottom-navigation.tsx create mode 100644 src/components/ui/chip.tsx create mode 100644 src/components/ui/fab.tsx create mode 100644 src/components/ui/top-app-bar.tsx diff --git a/CODE_STYLE.md b/CODE_STYLE.md new file mode 100644 index 0000000..81678ed --- /dev/null +++ b/CODE_STYLE.md @@ -0,0 +1,153 @@ +Here’s a practical, engineering-grade set of coding principles that actually keep a codebase healthy over years—not just during the honeymoon phase. Think of this as preventative maintenance, not moral philosophy 🛠️🧠. + +⸻ + +1. Smallness Is a Feature + +Large things rot faster. + • Small files + • Small functions + • Small modules + • Small responsibilities + +If a file feels “important,” it’s probably doing too much. Decomposition beats cleverness every time. + +Size is friction. Friction accumulates. + +⸻ + +2. One Reason to Change + +This is the only part of SOLID that really matters in practice. + • Every file should exist for one reason + • If a change request makes you touch unrelated logic, the boundary is wrong + +Violations show up as: + • “While I’m here…” edits + • Fear-driven refactors + • Accidental breakage + +⸻ + +3. Make State Explicit (and Rare) + +Hidden state is technical debt wearing camouflage. + • Prefer pure functions + • Push IO to the edges + • Name state transitions clearly + • Avoid “ambient” globals, singletons, magic context + +If you can’t explain when state changes, it will betray you at scale 🧨. + +⸻ + +4. Clarity Beats Brevity + +Concise code is nice. Readable code survives. + • Prefer obvious over clever + • Use boring names that describe intent + • Avoid dense expressions that require mental simulation + +The best compliment for code: + +“I didn’t have to think.” + +⸻ + +5. Structure > Comments + +Comments decay. Structure persists. + • Encode intent in function names and types + • Let data shape explain behavior + • Use comments only for why, never what + +If you need a paragraph to explain a function, split the function. + +⸻ + +6. Types Are a Design Tool + +Even in dynamic languages. + • Types clarify contracts + • They force edge cases into daylight + • They prevent “just trust me” APIs + +A good type definition is executable documentation 📐. + +⸻ + +7. Tests Define Reality + +Untested code is hypothetical. + • Tests lock in behavior + • Refactors without tests are rewrites in disguise + • Focus on behavior, not implementation + +Healthy ratio: + • Few unit tests per function + • Strong integration tests per feature + +⸻ + +8. Errors Are First-Class + +Failure paths deserve the same respect as success paths. + • Handle errors explicitly + • Avoid silent fallbacks + • Fail fast, fail loud, fail usefully + +Most production bugs live in “this can’t happen” branches. + +⸻ + +9. Consistency Is More Valuable Than Correctness + +A consistent codebase is navigable—even if imperfect. + • Same patterns everywhere + • Same naming conventions + • Same folder logic + +Inconsistency multiplies cognitive load faster than any algorithmic inefficiency. + +⸻ + +10. Refactor Continuously, Not Heroically + +Big refactors are usually a smell. + • Refactor as you touch code + • Leave things slightly better than you found them + • Don’t let cleanup pile up into fear + +Entropy is real. You either fight it daily or lose spectacularly 🌪️. + +⸻ + +11. Design for Deletion + +The best code is easy to remove. + • Avoid tight coupling + • Prefer composition over inheritance + • Keep feature boundaries clean + +If you can’t delete a feature safely, it owns you—not the other way around. + +⸻ + +12. Tooling Is Part of the Codebase + +Linters, formatters, CI, and automation are not optional polish. + • Enforce rules mechanically + • Remove human judgment where possible + • Let tools be the bad cop + +Humans are bad at consistency. Machines love it 🤖. + +⸻ + +The Meta-Principle + +A good codebase optimizes for future humans, not present cleverness. + +Every line you write is a tiny act of communication across time. Write like Future-You is tired, busy, and slightly annoyed—but still clever enough to appreciate clean design. + +Entropy never sleeps. Engineers must 😄 diff --git a/src/app/globals.scss b/src/app/globals.scss index 9b559d9..970d8da 100644 --- a/src/app/globals.scss +++ b/src/app/globals.scss @@ -16,86 +16,596 @@ ::before, ::backdrop, ::file-selector-button { - border-color: var(--color-neutral-3, $border); + border-color: var(--md3-outline-variant); } -// Body and base styles +// Body and base styles - Android Material You feel body { background: $background; color: $foreground; - font-family: var(--font-inter), 'Inter', 'Bricolage Grotesque', sans-serif; - line-height: 1.5; - transition: background-color 0.2s ease, color 0.2s ease; + font-family: var(--font-inter), 'Inter', 'Roboto', system-ui, sans-serif; + font-size: $font-body-medium; + line-height: $line-height-body-medium; + letter-spacing: $letter-spacing-body; + transition: background-color $duration-medium-2 $easing-standard, + color $duration-medium-2 $easing-standard; overflow-x: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -// Typography base - detailed styles in components/_typography.scss +// ============================================ +// MD3 Typography Classes +// ============================================ + +// Display styles +.md3-display-large { + font-size: $font-display-large; + line-height: $line-height-display-large; + letter-spacing: $letter-spacing-display; + font-weight: 400; +} + +.md3-display-medium { + font-size: $font-display-medium; + line-height: $line-height-display-medium; + letter-spacing: $letter-spacing-display; + font-weight: 400; +} + +.md3-display-small { + font-size: $font-display-small; + line-height: $line-height-display-small; + letter-spacing: $letter-spacing-display; + font-weight: 400; +} + +// Headline styles +.md3-headline-large { + font-size: $font-headline-large; + line-height: $line-height-headline-large; + letter-spacing: $letter-spacing-headline; + font-weight: 400; +} + +.md3-headline-medium { + font-size: $font-headline-medium; + line-height: $line-height-headline-medium; + letter-spacing: $letter-spacing-headline; + font-weight: 400; +} + +.md3-headline-small { + font-size: $font-headline-small; + line-height: $line-height-headline-small; + letter-spacing: $letter-spacing-headline; + font-weight: 400; +} + +// Title styles +.md3-title-large { + font-size: $font-title-large; + line-height: $line-height-title-large; + letter-spacing: $letter-spacing-title; + font-weight: 400; +} + +.md3-title-medium { + font-size: $font-title-medium; + line-height: $line-height-title-medium; + letter-spacing: $letter-spacing-title; + font-weight: 500; +} + +.md3-title-small { + font-size: $font-title-small; + line-height: $line-height-title-small; + letter-spacing: $letter-spacing-title; + font-weight: 500; +} + +// Body styles +.md3-body-large { + font-size: $font-body-large; + line-height: $line-height-body-large; + letter-spacing: $letter-spacing-body; + font-weight: 400; +} + +.md3-body-medium { + font-size: $font-body-medium; + line-height: $line-height-body-medium; + letter-spacing: $letter-spacing-body; + font-weight: 400; +} + +.md3-body-small { + font-size: $font-body-small; + line-height: $line-height-body-small; + letter-spacing: $letter-spacing-body; + font-weight: 400; +} + +// Label styles +.md3-label-large { + font-size: $font-label-large; + line-height: $line-height-label-large; + letter-spacing: $letter-spacing-label; + font-weight: 500; +} + +.md3-label-medium { + font-size: $font-label-medium; + line-height: $line-height-label-medium; + letter-spacing: $letter-spacing-label; + font-weight: 500; +} + +.md3-label-small { + font-size: $font-label-small; + line-height: $line-height-label-small; + letter-spacing: $letter-spacing-label; + font-weight: 500; +} + +// Typography base for headings h1, h2, h3, h4, h5, h6 { - font-family: var(--font-inter), 'Inter', 'Bricolage Grotesque', sans-serif; - font-weight: 600; - line-height: 1.2; + font-family: var(--font-inter), 'Inter', 'Roboto', system-ui, sans-serif; + font-weight: 400; + letter-spacing: $letter-spacing-headline; +} + +h1 { + font-size: $font-headline-large; + line-height: $line-height-headline-large; +} + +h2 { + font-size: $font-headline-medium; + line-height: $line-height-headline-medium; +} + +h3 { + font-size: $font-headline-small; + line-height: $line-height-headline-small; +} + +h4 { + font-size: $font-title-large; + line-height: $line-height-title-large; +} + +h5 { + font-size: $font-title-medium; + line-height: $line-height-title-medium; + font-weight: 500; +} + +h6 { + font-size: $font-title-small; + line-height: $line-height-title-small; + font-weight: 500; } code, pre { - font-family: var(--font-jetbrains-mono), 'JetBrains Mono', monospace; + font-family: var(--font-jetbrains-mono), 'JetBrains Mono', 'Roboto Mono', monospace; } -// CSS Custom Properties for theme +// ============================================ +// MD3 CSS Custom Properties (Dark Theme) +// ============================================ :root { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 263 70% 50%; - --primary-foreground: 210 40% 98%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 195 100% 70%; - --accent-foreground: 222.2 84% 4.9%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 195 100% 70%; - --radius: 0.5rem; + // Primary + --md3-primary: #{$primary}; + --md3-on-primary: #{$on-primary}; + --md3-primary-container: #{$primary-container}; + --md3-on-primary-container: #{$on-primary-container}; - // Chart colors - --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); + // Secondary + --md3-secondary: #{$secondary}; + --md3-on-secondary: #{$on-secondary}; + --md3-secondary-container: #{$secondary-container}; + --md3-on-secondary-container: #{$on-secondary-container}; - // Sidebar colors - --sidebar: 217.2 32.6% 17.5%; - --sidebar-foreground: 210 40% 98%; - --sidebar-primary: 263 70% 50%; - --sidebar-primary-foreground: 210 40% 98%; - --sidebar-accent: 217.2 32.6% 17.5%; - --sidebar-accent-foreground: 210 40% 98%; - --sidebar-border: 217.2 32.6% 17.5%; - --sidebar-ring: 195 100% 70%; + // Tertiary + --md3-tertiary: #{$accent}; + --md3-on-tertiary: #{$on-tertiary}; + --md3-tertiary-container: #{$tertiary-container}; + --md3-on-tertiary-container: #{$on-tertiary-container}; + + // Error + --md3-error: #{$destructive}; + --md3-on-error: #{$on-error}; + --md3-error-container: #{$error-container}; + --md3-on-error-container: #{$on-error-container}; + + // Surface + --md3-surface: #{$surface}; + --md3-on-surface: #{$on-surface}; + --md3-surface-variant: #{$surface-variant}; + --md3-on-surface-variant: #{$on-surface-variant}; + --md3-surface-dim: #{$surface-dim}; + --md3-surface-bright: #{$surface-bright}; + + // Surface Containers (MD3 tonal elevation) + --md3-surface-container-lowest: #{$surface-container-lowest}; + --md3-surface-container-low: #{$surface-container-low}; + --md3-surface-container: #{$surface-container}; + --md3-surface-container-high: #{$surface-container-high}; + --md3-surface-container-highest: #{$surface-container-highest}; + + // Background + --md3-background: #{$background}; + --md3-on-background: #{$on-background}; + + // Outline + --md3-outline: #{$outline}; + --md3-outline-variant: #{$outline-variant}; + + // Inverse + --md3-inverse-surface: #{$inverse-surface}; + --md3-inverse-on-surface: #{$inverse-on-surface}; + --md3-inverse-primary: #{$inverse-primary}; + + // Scrim & Shadow + --md3-scrim: #{$scrim}; + --md3-shadow: #{$shadow}; + + // Semantic colors + --md3-success: #{$md3-success-80}; + --md3-on-success: #{$md3-success-40}; + --md3-warning: #{$md3-warning-80}; + --md3-on-warning: #{$md3-warning-40}; + --md3-info: #{$md3-info-80}; + --md3-on-info: #{$md3-info-40}; + + // State layers + --md3-state-hover: #{$state-hover-opacity}; + --md3-state-focus: #{$state-focus-opacity}; + --md3-state-pressed: #{$state-pressed-opacity}; + --md3-state-dragged: #{$state-dragged-opacity}; + --md3-state-disabled: #{$state-disabled-opacity}; + + // Elevation + --md3-elevation-1: #{$elevation-1}; + --md3-elevation-2: #{$elevation-2}; + --md3-elevation-3: #{$elevation-3}; + --md3-elevation-4: #{$elevation-4}; + --md3-elevation-5: #{$elevation-5}; + + // Motion + --md3-duration-short: #{$duration-short-4}; + --md3-duration-medium: #{$duration-medium-2}; + --md3-duration-long: #{$duration-long-2}; + --md3-easing-standard: #{$easing-standard}; + --md3-easing-emphasized: #{$easing-emphasized}; + --md3-easing-emphasized-decelerate: #{$easing-emphasized-decelerate}; + + // Shape + --md3-shape-extra-small: #{$radius-extra-small}; + --md3-shape-small: #{$radius-small}; + --md3-shape-medium: #{$radius-medium}; + --md3-shape-large: #{$radius-large}; + --md3-shape-extra-large: #{$radius-extra-large}; + --md3-shape-full: #{$radius-full}; + + // Legacy compatibility + --background: 260 10% 6%; + --foreground: 260 10% 90%; + --card: 260 10% 10%; + --card-foreground: 260 10% 90%; + --popover: 260 10% 17%; + --popover-foreground: 260 10% 90%; + --primary: 260 50% 80%; + --primary-foreground: 260 50% 20%; + --secondary: 260 15% 80%; + --secondary-foreground: 260 15% 20%; + --muted: 260 10% 12%; + --muted-foreground: 260 15% 80%; + --accent: 180 50% 80%; + --accent-foreground: 180 50% 20%; + --destructive: 0 75% 80%; + --destructive-foreground: 0 75% 20%; + --border: 260 15% 30%; + --input: 260 10% 22%; + --ring: 260 50% 80%; + --radius: 0.75rem; + + // Chart colors (MD3 inspired) + --chart-1: #{$md3-primary-60}; + --chart-2: #{$md3-secondary-60}; + --chart-3: #{$md3-tertiary-60}; + --chart-4: #{$md3-error-60}; + --chart-5: #{$md3-warning-80}; + + // Sidebar colors (MD3) + --sidebar: 260 10% 10%; + --sidebar-foreground: 260 10% 90%; + --sidebar-primary: 260 50% 80%; + --sidebar-primary-foreground: 260 50% 20%; + --sidebar-accent: 260 10% 17%; + --sidebar-accent-foreground: 260 10% 90%; + --sidebar-border: 260 15% 30%; + --sidebar-ring: 260 50% 80%; } -// Accordion animations +// ============================================ +// MD3 Surface Utility Classes +// ============================================ + +.md3-surface { + background-color: var(--md3-surface); + color: var(--md3-on-surface); +} + +.md3-surface-dim { + background-color: var(--md3-surface-dim); + color: var(--md3-on-surface); +} + +.md3-surface-bright { + background-color: var(--md3-surface-bright); + color: var(--md3-on-surface); +} + +.md3-surface-container-lowest { + background-color: var(--md3-surface-container-lowest); + color: var(--md3-on-surface); +} + +.md3-surface-container-low { + background-color: var(--md3-surface-container-low); + color: var(--md3-on-surface); +} + +.md3-surface-container { + background-color: var(--md3-surface-container); + color: var(--md3-on-surface); +} + +.md3-surface-container-high { + background-color: var(--md3-surface-container-high); + color: var(--md3-on-surface); +} + +.md3-surface-container-highest { + background-color: var(--md3-surface-container-highest); + color: var(--md3-on-surface); +} + +// ============================================ +// MD3 Elevation Classes +// ============================================ + +.md3-elevation-1 { + box-shadow: var(--md3-elevation-1); +} + +.md3-elevation-2 { + box-shadow: var(--md3-elevation-2); +} + +.md3-elevation-3 { + box-shadow: var(--md3-elevation-3); +} + +.md3-elevation-4 { + box-shadow: var(--md3-elevation-4); +} + +.md3-elevation-5 { + box-shadow: var(--md3-elevation-5); +} + +// ============================================ +// MD3 State Layer Mixin Classes +// ============================================ + +.md3-state-layer { + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + inset: 0; + background-color: currentColor; + opacity: 0; + transition: opacity $duration-short-4 $easing-standard; + pointer-events: none; + border-radius: inherit; + } + + &:hover::before { + opacity: var(--md3-state-hover); + } + + &:focus-visible::before { + opacity: var(--md3-state-focus); + } + + &:active::before { + opacity: var(--md3-state-pressed); + } +} + +// ============================================ +// MD3 Ripple Effect +// ============================================ + +@keyframes md3-ripple { + from { + transform: scale(0); + opacity: 0.12; + } + to { + transform: scale(4); + opacity: 0; + } +} + +.md3-ripple { + position: relative; + overflow: hidden; + + &::after { + content: ''; + position: absolute; + width: 100%; + height: 100%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0); + background: radial-gradient(circle, currentColor 10%, transparent 10.01%); + opacity: 0; + pointer-events: none; + border-radius: 50%; + } + + &:active::after { + animation: md3-ripple $duration-medium-4 $easing-standard; + } +} + +// ============================================ +// Animations (MD3 Motion) +// ============================================ + @keyframes accordion-down { from { height: 0; + opacity: 0; } to { height: var(--radix-accordion-content-height); + opacity: 1; } } @keyframes accordion-up { from { height: var(--radix-accordion-content-height); + opacity: 1; } to { height: 0; + opacity: 0; } } + +@keyframes md3-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes md3-fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes md3-scale-in { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes md3-slide-in-bottom { + from { + opacity: 0; + transform: translateY(100%); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes md3-slide-in-right { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +// ============================================ +// Android-style System UI +// ============================================ + +// Status bar simulation (optional) +.android-status-bar { + height: 24px; + background: var(--md3-surface-container-lowest); + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0 $size-4; + font-size: $font-label-small; + color: var(--md3-on-surface); +} + +// Navigation bar simulation (bottom gesture bar) +.android-nav-bar { + height: 20px; + background: transparent; + display: flex; + align-items: center; + justify-content: center; + padding-bottom: $size-2; + + &::after { + content: ''; + width: 134px; + height: 5px; + background: var(--md3-on-surface-variant); + border-radius: $radius-full; + opacity: 0.4; + } +} + +// Selection styling +::selection { + background: rgba($primary, 0.3); + color: $on-surface; +} + +// Scrollbar styling (Android-like thin scrollbar) +::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--md3-outline-variant); + border-radius: $radius-full; + + &:hover { + background: var(--md3-outline); + } +} + +// Focus visible styling (MD3 style) +:focus-visible { + outline: 2px solid var(--md3-primary); + outline-offset: 2px; +} diff --git a/src/components/features/python-runner/PythonOutput.tsx b/src/components/features/python-runner/PythonOutput.tsx index 9149e20..cc52802 100644 --- a/src/components/features/python-runner/PythonOutput.tsx +++ b/src/components/features/python-runner/PythonOutput.tsx @@ -1,10 +1,9 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useCallback } from 'react' import { motion } from 'framer-motion' -import { Play, CircleNotch, Terminal } from '@phosphor-icons/react' +import { Play, CircleNotch, ArrowClockwise, Warning } from '@phosphor-icons/react' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { runPythonCode, getPyodide, isPyodideReady } from '@/lib/pyodide-runner' +import { runPythonCode, getPyodide, isPyodideReady, getPyodideError, resetPyodide } from '@/lib/pyodide-runner' import { PythonTerminal } from '@/components/features/python-runner/PythonTerminal' import { toast } from 'sonner' @@ -17,30 +16,71 @@ export function PythonOutput({ code }: PythonOutputProps) { const [error, setError] = useState('') const [isRunning, setIsRunning] = useState(false) const [isInitializing, setIsInitializing] = useState(!isPyodideReady()) + const [initError, setInitError] = useState(null) const [hasInput, setHasInput] = useState(false) - useEffect(() => { - if (!isPyodideReady()) { - setIsInitializing(true) - getPyodide() - .then(() => { - setIsInitializing(false) - toast.success('Python environment ready!') - }) - .catch((err) => { - setIsInitializing(false) - toast.error('Failed to load Python environment') - console.error(err) - }) + const initializePyodide = useCallback(async () => { + setIsInitializing(true) + setInitError(null) + + try { + await getPyodide() + setIsInitializing(false) + toast.success('Python environment ready!') + } catch (err) { + setIsInitializing(false) + const errorMsg = err instanceof Error ? err.message : String(err) + setInitError(errorMsg) + toast.error('Failed to load Python environment') + console.error(err) } }, []) + const handleRetry = useCallback(() => { + resetPyodide() + setOutput('') + setError('') + setInitError(null) + initializePyodide() + }, [initializePyodide]) + + useEffect(() => { + // Check for existing initialization error + const existingError = getPyodideError() + if (existingError) { + setInitError(existingError.message) + setIsInitializing(false) + return + } + + if (!isPyodideReady()) { + initializePyodide() + } + }, [initializePyodide]) + useEffect(() => { const codeToCheck = code.toLowerCase() setHasInput(codeToCheck.includes('input(')) }, [code]) + const statusTone = initError + ? 'border-destructive/30 bg-destructive/10 text-destructive' + : isInitializing + ? 'border-border bg-primary/5 text-primary' + : 'border-primary/30 bg-primary/10 text-primary' + + const statusLabel = initError + ? 'Init failed' + : isInitializing + ? 'Loading' + : 'Ready' + const handleRun = async () => { + if (initError) { + toast.error('Python environment failed to start. Retry to load it again.') + return + } + if (isInitializing) { toast.info('Python environment is still loading...') return @@ -69,36 +109,112 @@ export function PythonOutput({ code }: PythonOutputProps) { return (
-
-

Python Output

- )} - + +
-
- {!output && !error && ( +
+ {isInitializing && ( + +
+
+ +
+
+

Preparing Python environment

+

+ This takes a few seconds the first time while Pyodide downloads. +

+
+
+
+ )} + + {initError && ( + +
+
+ +
+
+
+

Python environment failed to start

+

{initError}

+
+
+ + +
+
+
+
+ )} + + {!isInitializing && !initError && !output && !error && (
Click "Run" to execute the Python code
)} - {output && ( + {!isInitializing && !initError && output && ( -
-
-

Navigation

- -
- + aria-hidden="true" + /> -
-

- CodeSnippet Library -

-
-
- + {/* Navigation Drawer */} + + {/* Header */} +
+ + CodeSnippet + + +
+ + {/* Navigation Items */} + + + {/* Footer */} +
+

+ Material Design 3 +

+
+ + {/* Android gesture bar indicator */} +
+
+
+ + + )} + ); } diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 35161b7..89ceaff 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -4,23 +4,80 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" +/** + * Material Design 3 Badge Component + * + * Used for: + * - Status indicators + * - Labels + * - Small counts (notification badges use a different pattern) + */ const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden", + // Base styles + [ + "inline-flex items-center justify-center gap-1", + "rounded-full px-2.5 py-0.5", + "text-xs font-medium", + "whitespace-nowrap shrink-0 w-fit", + "transition-colors duration-200", + "[&>svg]:size-3 [&>svg]:pointer-events-none", + "outline-none", + "focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2", + ].join(" "), { variants: { variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + // Filled badge (primary) + default: [ + "border-transparent bg-primary text-primary-foreground", + "[a&]:hover:bg-primary/90", + ].join(" "), + + // Tonal badge (secondary container) + secondary: [ + "border-transparent bg-secondary text-secondary-foreground", + "[a&]:hover:bg-secondary/90", + ].join(" "), + + // Error badge + destructive: [ + "border-transparent bg-destructive text-destructive-foreground", + "[a&]:hover:bg-destructive/90", + ].join(" "), + + // Outlined badge + outline: [ + "border border-border bg-transparent text-foreground", + "[a&]:hover:bg-muted", + ].join(" "), + + // Success badge + success: [ + "border-transparent bg-[hsl(145,60%,35%)] text-white", + "[a&]:hover:opacity-90", + ].join(" "), + + // Warning badge + warning: [ + "border-transparent bg-[hsl(40,80%,45%)] text-white", + "[a&]:hover:opacity-90", + ].join(" "), + + // Info badge + info: [ + "border-transparent bg-[hsl(210,70%,45%)] text-white", + "[a&]:hover:opacity-90", + ].join(" "), + }, + size: { + default: "h-5 px-2.5 text-xs", + sm: "h-4 px-2 text-[10px]", + lg: "h-6 px-3 text-sm", }, }, defaultVariants: { variant: "default", + size: "default", }, } ) @@ -28,6 +85,7 @@ const badgeVariants = cva( function Badge({ className, variant, + size, asChild = false, ...props }: ComponentProps<"span"> & @@ -37,10 +95,45 @@ function Badge({ return ( ) } -export { Badge, badgeVariants } +/** + * MD3 Small Badge (for notification counts) + * Can be a dot or show a number + */ +function SmallBadge({ + className, + count, + max = 99, + ...props +}: ComponentProps<"span"> & { + count?: number + max?: number +}) { + const showDot = count === undefined || count === 0 + const displayCount = count && count > max ? `${max}+` : count + + return ( + + {!showDot && displayCount} + + ) +} + +export { Badge, SmallBadge, badgeVariants } diff --git a/src/components/ui/bottom-navigation.tsx b/src/components/ui/bottom-navigation.tsx new file mode 100644 index 0000000..18a940d --- /dev/null +++ b/src/components/ui/bottom-navigation.tsx @@ -0,0 +1,149 @@ +'use client' + +import { ComponentProps, createContext, forwardRef, useContext } from "react" +import { cn } from "@/lib/utils" + +/** + * Material Design 3 Bottom Navigation Bar + * + * Android-style bottom navigation with: + * - 3-5 destinations + * - Active indicator pill + * - Icon + label layout + * - Badge support + */ + +interface BottomNavigationContextValue { + activeValue?: string + onValueChange?: (value: string) => void +} + +const BottomNavigationContext = createContext({}) + +interface BottomNavigationProps extends ComponentProps<"nav"> { + value?: string + onValueChange?: (value: string) => void +} + +const BottomNavigation = forwardRef( + ({ className, value, onValueChange, children, ...props }, ref) => { + return ( + + + + ) + } +) +BottomNavigation.displayName = "BottomNavigation" + +interface BottomNavigationItemProps extends ComponentProps<"button"> { + value: string + icon: React.ReactNode + activeIcon?: React.ReactNode + label: string + badge?: number | boolean +} + +const BottomNavigationItem = forwardRef( + ({ className, value, icon, activeIcon, label, badge, ...props }, ref) => { + const context = useContext(BottomNavigationContext) + const isActive = context.activeValue === value + + const handleClick = () => { + context.onValueChange?.(value) + } + + return ( + + ) + } +) +BottomNavigationItem.displayName = "BottomNavigationItem" + +export { BottomNavigation, BottomNavigationItem } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 1e8e06f..ba38048 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -4,28 +4,111 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" +/** + * Material Design 3 Button Component + * + * Variants: + * - filled (default): High emphasis, primary actions (MD3 Filled Button) + * - tonal: Medium emphasis, secondary actions (MD3 Filled Tonal Button) + * - elevated: Medium emphasis with shadow (MD3 Elevated Button) + * - outlined: Low emphasis, secondary actions (MD3 Outlined Button) + * - text: Lowest emphasis, tertiary actions (MD3 Text Button) + * - destructive: Error/danger actions + * - ghost: Legacy support + * - link: Text link style + */ const buttonVariants = cva( - "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", + // Base styles - MD3 common button properties + [ + "inline-flex items-center justify-center gap-2 whitespace-nowrap", + "rounded-full text-sm font-medium", + "transition-all duration-200", + "disabled:pointer-events-none disabled:opacity-[0.38]", + "[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-[18px]", + "shrink-0 [&_svg]:shrink-0", + "outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background", + "relative overflow-hidden select-none", + // State layer using pseudo-element + "before:absolute before:inset-0 before:rounded-[inherit] before:bg-current before:opacity-0 before:transition-opacity before:duration-200", + "hover:before:opacity-[0.08] focus-visible:before:opacity-[0.12] active:before:opacity-[0.12]", + ].join(" "), { variants: { variant: { - default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "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: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", + // MD3 Filled Button - Primary, highest emphasis + default: [ + "bg-primary text-primary-foreground", + "shadow-sm", + "hover:shadow-md", + "active:shadow-sm", + ].join(" "), + + // MD3 Filled Tonal Button - Secondary, medium emphasis + tonal: [ + "bg-secondary text-secondary-foreground", + "hover:shadow-sm", + ].join(" "), + + // MD3 Elevated Button - Medium emphasis with elevation + elevated: [ + "bg-card text-primary", + "shadow-md", + "hover:shadow-lg", + "active:shadow-md", + ].join(" "), + + // MD3 Outlined Button - Low emphasis + outline: [ + "border border-border bg-transparent text-primary", + "hover:bg-primary/[0.08]", + ].join(" "), + + // MD3 Text Button - Lowest emphasis + text: [ + "bg-transparent text-primary", + "hover:bg-primary/[0.08]", + "px-3", + ].join(" "), + + // Destructive/Error variant + destructive: [ + "bg-destructive text-destructive-foreground", + "shadow-sm", + "hover:shadow-md", + "focus-visible:ring-destructive", + ].join(" "), + + // Legacy secondary (maps to tonal) + secondary: [ + "bg-secondary text-secondary-foreground", + "hover:shadow-sm", + ].join(" "), + + // Ghost - for icon buttons and minimal UI + ghost: [ + "bg-transparent text-muted-foreground", + "hover:bg-muted hover:text-foreground", + ].join(" "), + + // Link style + link: [ + "text-primary underline-offset-4 hover:underline", + "bg-transparent", + ].join(" "), }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", + // MD3 standard button height is 40dp + default: "h-10 px-6 py-2.5 min-w-[64px] has-[>svg]:px-4", + // Compact size + sm: "h-8 px-4 gap-1.5 text-xs has-[>svg]:px-3", + // Large for prominent actions + lg: "h-12 px-8 text-base has-[>svg]:px-6", + // Icon button (MD3 standard icon button is 40x40) + icon: "size-10 p-0 min-w-0 rounded-full", + // Small icon button + "icon-sm": "size-8 p-0 min-w-0 rounded-full", + // Large icon button + "icon-lg": "size-12 p-0 min-w-0 rounded-full", }, }, defaultVariants: { diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index a48fc33..ca92fbb 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,15 +1,58 @@ import { ComponentProps } from "react" +import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" -function Card({ className, ...props }: ComponentProps<"div">) { +/** + * Material Design 3 Card Component + * + * Variants: + * - elevated: Default card with shadow (MD3 Elevated Card) + * - filled: Card with tonal surface color (MD3 Filled Card) + * - outlined: Card with outline border (MD3 Outlined Card) + */ +const cardVariants = cva( + // Base styles + [ + "flex flex-col gap-4 rounded-xl text-card-foreground", + "transition-all duration-200", + "relative overflow-hidden", + ].join(" "), + { + variants: { + variant: { + // MD3 Elevated Card - default surface with shadow + elevated: [ + "bg-card shadow-md", + "hover:shadow-lg", + ].join(" "), + + // MD3 Filled Card - surface container color, no shadow + filled: [ + "bg-secondary/50", + ].join(" "), + + // MD3 Outlined Card - transparent with border + outlined: [ + "bg-card border border-border", + ].join(" "), + }, + }, + defaultVariants: { + variant: "elevated", + }, + } +) + +function Card({ + className, + variant, + ...props +}: ComponentProps<"div"> & VariantProps) { return (
) @@ -20,7 +63,7 @@ function CardHeader({ className, ...props }: ComponentProps<"div">) {
) { return (
) @@ -42,7 +88,7 @@ function CardDescription({ className, ...props }: ComponentProps<"div">) { return (
) @@ -65,7 +111,7 @@ function CardContent({ className, ...props }: ComponentProps<"div">) { return (
) @@ -75,7 +121,30 @@ function CardFooter({ className, ...props }: ComponentProps<"div">) { return (
+ ) +} + +/** + * MD3 Card Media - for images/videos at top of card + */ +function CardMedia({ + className, + ...props +}: ComponentProps<"div">) { + return ( +
) @@ -89,4 +158,6 @@ export { CardAction, CardDescription, CardContent, + CardMedia, + cardVariants, } diff --git a/src/components/ui/chip.tsx b/src/components/ui/chip.tsx new file mode 100644 index 0000000..430e64a --- /dev/null +++ b/src/components/ui/chip.tsx @@ -0,0 +1,166 @@ +'use client' + +import { ComponentProps, forwardRef } from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "@phosphor-icons/react" +import { cn } from "@/lib/utils" + +/** + * Material Design 3 Chip Component + * + * Types: + * - assist: Actions in context (Assist Chip) + * - filter: Toggleable filters (Filter Chip) + * - input: User input, removable (Input Chip) + * - suggestion: Dynamic suggestions (Suggestion Chip) + */ +const chipVariants = cva( + // Base styles + [ + "inline-flex items-center justify-center gap-2", + "h-8 px-4 rounded-lg", + "text-sm font-medium", + "transition-all duration-200", + "cursor-pointer select-none", + "outline-none", + "relative overflow-hidden", + // State layer + "before:absolute before:inset-0 before:rounded-[inherit] before:bg-current before:opacity-0 before:transition-opacity before:duration-200", + "hover:before:opacity-[0.08] focus-visible:before:opacity-[0.12] active:before:opacity-[0.12]", + // Focus ring + "focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background", + // Disabled + "disabled:pointer-events-none disabled:opacity-[0.38]", + // Icon sizing + "[&_svg]:size-[18px] [&_svg]:shrink-0", + ].join(" "), + { + variants: { + variant: { + // Assist Chip - outlined, for contextual actions + assist: [ + "border border-border bg-transparent text-foreground", + "hover:bg-muted/50", + ].join(" "), + + // Filter Chip - toggleable, outlined or filled when selected + filter: [ + "border border-border bg-transparent text-foreground", + "data-[selected=true]:bg-secondary data-[selected=true]:border-transparent data-[selected=true]:text-secondary-foreground", + ].join(" "), + + // Input Chip - user input, removable + input: [ + "border border-border bg-transparent text-foreground", + "pr-2", + ].join(" "), + + // Suggestion Chip - outlined, for suggestions + suggestion: [ + "border border-border bg-transparent text-foreground", + "hover:bg-muted/50", + ].join(" "), + + // Filled variant + filled: [ + "bg-secondary text-secondary-foreground border-transparent", + ].join(" "), + }, + size: { + default: "h-8 px-4 text-sm", + sm: "h-6 px-3 text-xs", + }, + }, + defaultVariants: { + variant: "assist", + size: "default", + }, + } +) + +interface ChipProps + extends ComponentProps<"button">, + VariantProps { + leadingIcon?: React.ReactNode + selected?: boolean + onRemove?: () => void +} + +const Chip = forwardRef( + ( + { + className, + variant, + size, + leadingIcon, + selected, + onRemove, + children, + ...props + }, + ref + ) => { + const isInput = variant === "input" + const isFilter = variant === "filter" + + return ( + + )} + + ) + } +) +Chip.displayName = "Chip" + +/** + * Chip Group container + */ +function ChipGroup({ className, ...props }: ComponentProps<"div">) { + return ( +
+ ) +} + +export { Chip, ChipGroup, chipVariants } diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index a757339..8f181a9 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -8,6 +8,15 @@ import CircleIcon from "lucide-react/dist/esm/icons/circle" import { cn } from "@/lib/utils" +/** + * Material Design 3 Dropdown Menu + * + * Android-style menu with: + * - 48dp minimum touch targets + * - Proper state layers + * - Clear focus indicators + */ + function DropdownMenu({ ...props }: ComponentProps) { @@ -35,7 +44,7 @@ function DropdownMenuTrigger({ function DropdownMenuContent({ className, - sideOffset = 4, + sideOffset = 8, ...props }: ComponentProps) { return ( @@ -44,7 +53,22 @@ function DropdownMenuContent({ 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", + // MD3 surface container + "bg-[hsl(var(--popover))] text-[hsl(var(--popover-foreground))]", + // Shape and shadow + "z-50 min-w-[180px] overflow-hidden rounded-lg border-0 p-1", + "shadow-lg", + // Constrain height and enable scrolling + "max-h-[min(var(--radix-dropdown-menu-content-available-height),400px)]", + "overflow-y-auto", + // Animations + "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", + // Transform origin + "origin-[var(--radix-dropdown-menu-content-transform-origin)]", className )} {...props} @@ -76,7 +100,29 @@ function DropdownMenuItem({ 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", + // MD3 menu item - minimum 48dp height for touch + "relative flex items-center gap-3", + "min-h-[48px] px-3 py-2", + "text-sm font-normal", + "rounded-md cursor-pointer select-none outline-none", + // State layer + "transition-colors duration-150", + // Default colors + "text-[hsl(var(--popover-foreground))]", + // Hover/focus state + "hover:bg-[hsl(var(--muted))]", + "focus:bg-[hsl(var(--muted))]", + // Destructive variant + "data-[variant=destructive]:text-destructive", + "data-[variant=destructive]:hover:bg-destructive/10", + "data-[variant=destructive]:focus:bg-destructive/10", + // Disabled state + "data-[disabled]:pointer-events-none data-[disabled]:opacity-[0.38]", + // Inset for items without icons + "data-[inset]:pl-10", + // Icon styling + "[&_svg]:size-5 [&_svg]:shrink-0 [&_svg]:text-[hsl(var(--muted-foreground))]", + "[&_svg]:pointer-events-none", className )} {...props} @@ -94,15 +140,24 @@ function DropdownMenuCheckboxItem({ - + - + {children} @@ -130,14 +185,23 @@ function DropdownMenuRadioItem({ - + - + {children} @@ -157,7 +221,10 @@ function DropdownMenuLabel({ data-slot="dropdown-menu-label" data-inset={inset} className={cn( - "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", + // MD3 label style + "px-3 py-2 text-xs font-medium uppercase tracking-wider", + "text-[hsl(var(--muted-foreground))]", + "data-[inset]:pl-10", className )} {...props} @@ -172,7 +239,7 @@ function DropdownMenuSeparator({ return ( ) @@ -186,7 +253,7 @@ function DropdownMenuShortcut({ {children} - + ) } @@ -232,7 +308,17 @@ function DropdownMenuSubContent({ , + VariantProps { + icon: React.ReactNode +} + +const FAB = forwardRef( + ({ className, variant, size, icon, ...props }, ref) => { + return ( + + ) + } +) +FAB.displayName = "FAB" + +/** + * Extended FAB with label + */ +interface ExtendedFABProps + extends ComponentProps<"button">, + VariantProps { + icon?: React.ReactNode + label: string +} + +const extendedFabVariants = cva( + // Base styles + [ + "inline-flex items-center justify-center gap-3", + "h-14 px-4 rounded-2xl font-medium text-sm", + "transition-all duration-200", + "disabled:pointer-events-none disabled:opacity-[0.38]", + "[&_svg]:pointer-events-none [&_svg]:size-6", + "shrink-0 [&_svg]:shrink-0", + "outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background", + "relative overflow-hidden select-none", + "shadow-lg hover:shadow-xl active:shadow-md", + // State layer + "before:absolute before:inset-0 before:rounded-[inherit] before:bg-current before:opacity-0 before:transition-opacity before:duration-200", + "hover:before:opacity-[0.08] focus-visible:before:opacity-[0.12] active:before:opacity-[0.12]", + ].join(" "), + { + variants: { + variant: { + primary: [ + "bg-primary text-primary-foreground", + "focus-visible:ring-primary", + ].join(" "), + secondary: [ + "bg-secondary text-secondary-foreground", + "focus-visible:ring-secondary", + ].join(" "), + tertiary: [ + "bg-accent text-accent-foreground", + "focus-visible:ring-accent", + ].join(" "), + surface: [ + "bg-card text-primary", + "focus-visible:ring-primary", + ].join(" "), + }, + }, + defaultVariants: { + variant: "primary", + }, + } +) + +const ExtendedFAB = forwardRef( + ({ className, variant, icon, label, ...props }, ref) => { + return ( + + ) + } +) +ExtendedFAB.displayName = "ExtendedFAB" + +export { FAB, ExtendedFAB, fabVariants, extendedFabVariants } diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 18186bc..d526c0e 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,16 +1,42 @@ -import { ComponentProps } from "react" +import { ComponentProps, forwardRef, useId, useState } from "react" import { cn } from "@/lib/utils" +/** + * Material Design 3 Text Field Component + * + * Standard input with MD3 styling including: + * - Filled and outlined variants + * - Proper focus states with animated indicator + * - Support for leading/trailing icons + * - Helper text and error states + */ function Input({ className, type, ...props }: ComponentProps<"input">) { return ( ) { ) } -export { Input } +/** + * MD3 Outlined Text Field + */ +function InputOutlined({ className, type, ...props }: ComponentProps<"input">) { + return ( + + ) +} + +/** + * MD3 Text Field with Floating Label + */ +interface TextFieldProps extends ComponentProps<"input"> { + label: string + helperText?: string + error?: boolean + errorText?: string + leadingIcon?: React.ReactNode + trailingIcon?: React.ReactNode + variant?: "filled" | "outlined" +} + +const TextField = forwardRef( + ( + { + className, + label, + helperText, + error, + errorText, + leadingIcon, + trailingIcon, + variant = "filled", + id, + ...props + }, + ref + ) => { + const generatedId = useId() + const inputId = id || generatedId + const [isFocused, setIsFocused] = useState(false) + const [hasValue, setHasValue] = useState( + Boolean(props.value || props.defaultValue) + ) + + const isLabelFloating = isFocused || hasValue + + const handleFocus = (e: React.FocusEvent) => { + setIsFocused(true) + props.onFocus?.(e) + } + + const handleBlur = (e: React.FocusEvent) => { + setIsFocused(false) + props.onBlur?.(e) + } + + const handleChange = (e: React.ChangeEvent) => { + setHasValue(Boolean(e.target.value)) + props.onChange?.(e) + } + + const isFilled = variant === "filled" + + return ( +
+
+ {leadingIcon && ( + {leadingIcon} + )} +
+ + +
+ {trailingIcon && ( + {trailingIcon} + )} +
+ {(helperText || errorText) && ( +

+ {error ? errorText : helperText} +

+ )} +
+ ) + } +) +TextField.displayName = "TextField" + +export { Input, InputOutlined, TextField } diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index e67eb5a..56dec46 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -5,6 +5,15 @@ import * as PopoverPrimitive from "@radix-ui/react-popover" import { cn } from "@/lib/utils" +/** + * Material Design 3 Popover + * + * Android-style popover with: + * - Proper elevation and shadow + * - Smooth animations + * - Accessible focus management + */ + function Popover({ ...props }: ComponentProps) { @@ -20,7 +29,7 @@ function PopoverTrigger({ function PopoverContent({ className, align = "center", - sideOffset = 4, + sideOffset = 8, ...props }: ComponentProps) { return ( @@ -30,7 +39,23 @@ function PopoverContent({ align={align} 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 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden", + // MD3 surface container high + "bg-[hsl(var(--popover))] text-[hsl(var(--popover-foreground))]", + // Shape and shadow + "z-50 w-72 rounded-xl border-0 p-4", + "shadow-lg", + // Animations + "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", + // Transform origin + "origin-[var(--radix-popover-content-transform-origin)]", + // Focus + "outline-none", className )} {...props} @@ -45,4 +70,29 @@ function PopoverAnchor({ return } -export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } +/** + * PopoverClose - Close button for popover + */ +function PopoverClose({ + className, + ...props +}: ComponentProps) { + return ( + + ) +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverClose } diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx index 3367d08..dc239de 100644 --- a/src/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -5,6 +5,15 @@ import * as SwitchPrimitive from "@radix-ui/react-switch" import { cn } from "@/lib/utils" +/** + * Material Design 3 Switch Component + * + * Android-style toggle switch with: + * - 52x32 track (MD3 spec) + * - 24x24 handle when unchecked, 28x28 when checked + * - State layer on handle + * - Proper focus ring + */ function Switch({ className, ...props @@ -13,7 +22,20 @@ function Switch({ diff --git a/src/components/ui/top-app-bar.tsx b/src/components/ui/top-app-bar.tsx new file mode 100644 index 0000000..2631a80 --- /dev/null +++ b/src/components/ui/top-app-bar.tsx @@ -0,0 +1,184 @@ +'use client' + +import { ComponentProps, forwardRef } from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +/** + * Material Design 3 Top App Bar + * + * Variants: + * - center-aligned: Logo/title centered (default) + * - small: Standard top app bar + * - medium: Medium with larger title + * - large: Large with prominent title + */ + +const topAppBarVariants = cva( + // Base styles + [ + "w-full", + "bg-[hsl(var(--card))]", + "transition-all duration-200", + ].join(" "), + { + variants: { + variant: { + "center-aligned": "h-16", + small: "h-16", + medium: "h-28", + large: "h-36", + }, + scrolled: { + true: "shadow-md bg-[hsl(var(--card))]/95 backdrop-blur-sm", + false: "shadow-none", + }, + }, + defaultVariants: { + variant: "small", + scrolled: false, + }, + } +) + +interface TopAppBarProps + extends ComponentProps<"header">, + VariantProps { + navigationIcon?: React.ReactNode + onNavigationClick?: () => void + title?: string + actions?: React.ReactNode +} + +const TopAppBar = forwardRef( + ( + { + className, + variant, + scrolled, + navigationIcon, + onNavigationClick, + title, + actions, + children, + ...props + }, + ref + ) => { + const isCenterAligned = variant === "center-aligned" + const isMediumOrLarge = variant === "medium" || variant === "large" + + return ( +
+
+ {/* Leading navigation icon */} + {navigationIcon && ( + + )} + + {/* Title - for small and center-aligned */} + {!isMediumOrLarge && title && ( +

+ {title} +

+ )} + + {/* Trailing actions */} + {actions && ( +
+ {actions} +
+ )} +
+ + {/* Title for medium and large variants */} + {isMediumOrLarge && title && ( +
+

+ {title} +

+
+ )} + + {children} +
+ ) + } +) +TopAppBar.displayName = "TopAppBar" + +/** + * Top App Bar Action Button + */ +interface TopAppBarActionProps extends ComponentProps<"button"> { + icon: React.ReactNode +} + +const TopAppBarAction = forwardRef( + ({ className, icon, ...props }, ref) => { + return ( + + ) + } +) +TopAppBarAction.displayName = "TopAppBarAction" + +export { TopAppBar, TopAppBarAction, topAppBarVariants } diff --git a/src/lib/pyodide-runner.ts b/src/lib/pyodide-runner.ts index 3948752..7f4322a 100644 --- a/src/lib/pyodide-runner.ts +++ b/src/lib/pyodide-runner.ts @@ -2,8 +2,14 @@ import { loadPyodide, PyodideInterface } from 'pyodide' let pyodideInstance: PyodideInterface | null = null let pyodideLoading: Promise | null = null +let initializationError: Error | null = null export async function getPyodide(): Promise { + // If we had an initialization error, throw it + if (initializationError) { + throw initializationError + } + if (pyodideInstance) { return pyodideInstance } @@ -13,10 +19,15 @@ export async function getPyodide(): Promise { } pyodideLoading = loadPyodide({ - indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.29.1/full/', + indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.27.0/full/', }).then((pyodide) => { pyodideInstance = pyodide + initializationError = null return pyodide + }).catch((err) => { + initializationError = err instanceof Error ? err : new Error(String(err)) + pyodideLoading = null + throw initializationError }) return pyodideLoading @@ -25,7 +36,8 @@ export async function getPyodide(): Promise { export async function runPythonCode(code: string): Promise<{ output?: string; error?: string }> { try { const pyodide = await getPyodide() - + + // Reset stdout/stderr for each run pyodide.runPython(` import sys from io import StringIO @@ -33,19 +45,39 @@ sys.stdout = StringIO() sys.stderr = StringIO() `) - const result = pyodide.runPython(code) - const stdout = pyodide.runPython('sys.stdout.getvalue()') - - let output = stdout || '' - if (result !== undefined && result !== null) { - output += (output ? '\n' : '') + String(result) - } + try { + const result = pyodide.runPython(code) + const stdout = pyodide.runPython('sys.stdout.getvalue()') + const stderr = pyodide.runPython('sys.stderr.getvalue()') - return { output: output || '(no output)' } + let output = stdout || '' + if (stderr) { + output += (output ? '\n' : '') + stderr + } + if (result !== undefined && result !== null && String(result) !== 'None') { + output += (output ? '\n' : '') + String(result) + } + + return { output: output || '(no output)' } + } catch (runErr) { + // Get any stderr output that might have been captured + let stderr = '' + try { + stderr = pyodide.runPython('sys.stderr.getvalue()') + } catch { + // Ignore errors getting stderr + } + + const errorMessage = runErr instanceof Error ? runErr.message : String(runErr) + return { + output: '', + error: stderr ? `${stderr}\n${errorMessage}` : errorMessage + } + } } catch (err) { return { output: '', - error: err instanceof Error ? err.message : String(err) + error: `Python environment error: ${err instanceof Error ? err.message : String(err)}` } } } @@ -62,6 +94,7 @@ export async function runPythonCodeInteractive( ): Promise { const pyodide = await getPyodide() + // Set up interactive stdout/stderr handlers pyodide.runPython(` import sys from io import StringIO @@ -70,7 +103,7 @@ class InteractiveStdout: def __init__(self, callback): self.callback = callback self.buffer = "" - + def write(self, text): self.buffer += text if "\\n" in text: @@ -80,7 +113,7 @@ class InteractiveStdout: self.callback(line) self.buffer = lines[-1] return len(text) - + def flush(self): if self.buffer: self.callback(self.buffer) @@ -90,7 +123,7 @@ class InteractiveStderr: def __init__(self, callback): self.callback = callback self.buffer = "" - + def write(self, text): self.buffer += text if "\\n" in text: @@ -100,7 +133,7 @@ class InteractiveStderr: self.callback(line) self.buffer = lines[-1] return len(text) - + def flush(self): if self.buffer: self.callback(self.buffer) @@ -137,33 +170,38 @@ sys.stderr = InteractiveStderr(__error_callback__) pyodide.globals.set('js_input_handler', customInput) + // Set up custom input function using asyncio.run() which is the modern approach await pyodide.runPythonAsync(` import builtins from pyodide.ffi import to_js import asyncio -def custom_input(prompt=""): +async def async_input(prompt=""): import sys sys.stdout.write(prompt) sys.stdout.flush() - - async def get_input(): - result = await js_input_handler(prompt) - return result - - loop = asyncio.get_event_loop() - if loop.is_running(): + result = await js_input_handler(prompt) + return result + +def custom_input(prompt=""): + import asyncio + try: + # Try to get the running loop (works in async context) + loop = asyncio.get_running_loop() + # If we're in an async context, we need to use a different approach import pyodide.webloop - return pyodide.webloop.WebLoop().run_until_complete(get_input()) - else: - return loop.run_until_complete(get_input()) + future = asyncio.ensure_future(async_input(prompt)) + return pyodide.webloop.WebLoop().run_until_complete(future) + except RuntimeError: + # No running loop, create a new one + return asyncio.run(async_input(prompt)) builtins.input = custom_input `) try { await pyodide.runPythonAsync(code) - + pyodide.runPython('sys.stdout.flush()') pyodide.runPython('sys.stderr.flush()') } catch (err) { @@ -177,3 +215,13 @@ builtins.input = custom_input export function isPyodideReady(): boolean { return pyodideInstance !== null } + +export function getPyodideError(): Error | null { + return initializationError +} + +export function resetPyodide(): void { + pyodideInstance = null + pyodideLoading = null + initializationError = null +} diff --git a/src/styles/abstracts/_variables.scss b/src/styles/abstracts/_variables.scss index dbd2eaf..9d7b52d 100644 --- a/src/styles/abstracts/_variables.scss +++ b/src/styles/abstracts/_variables.scss @@ -1,49 +1,370 @@ -// Color Variables - Material Design 3 (Material You) inspired -$background: hsl(222.2, 84%, 4.9%); -$foreground: hsl(210, 15%, 88%); // MD3 on-surface -$card: hsl(222.2, 40%, 8%); // MD3 surface-container -$card-foreground: hsl(210, 15%, 88%); -$popover: hsl(222.2, 40%, 10%); // MD3 surface-container-high -$popover-foreground: hsl(210, 15%, 88%); -$primary: hsl(260, 45%, 55%); // MD3 primary - softer purple -$primary-foreground: hsl(210, 15%, 95%); -$secondary: hsl(217.2, 25%, 20%); // MD3 secondary-container -$secondary-foreground: hsl(210, 15%, 88%); -$muted: hsl(217.2, 25%, 22%); -$muted-foreground: hsl(215, 12%, 60%); // MD3 on-surface-variant -$accent: hsl(195, 50%, 50%); // MD3 tertiary - balanced cyan -$accent-foreground: hsl(222.2, 84%, 4.9%); -$destructive: hsl(0, 50%, 50%); // MD3 error -$destructive-foreground: hsl(210, 15%, 95%); -$border: hsl(217.2, 20%, 25%); // MD3 outline -$input: hsl(217.2, 25%, 18%); -$ring: hsl(195, 50%, 50%); // Match accent +// ============================================ +// Material Design 3 (Material You) Color System +// Android Smartphone UI Theme +// ============================================ -// Size scale variables (for direct use in component styles) +// === MD3 Primary Tonal Palette === +// Purple/Violet primary color with full tonal range +$md3-primary-0: hsl(260, 50%, 0%); +$md3-primary-10: hsl(260, 50%, 10%); +$md3-primary-20: hsl(260, 50%, 20%); +$md3-primary-25: hsl(260, 50%, 25%); +$md3-primary-30: hsl(260, 50%, 30%); +$md3-primary-35: hsl(260, 50%, 35%); +$md3-primary-40: hsl(260, 50%, 40%); +$md3-primary-50: hsl(260, 50%, 50%); +$md3-primary-60: hsl(260, 50%, 60%); +$md3-primary-70: hsl(260, 50%, 70%); +$md3-primary-80: hsl(260, 50%, 80%); +$md3-primary-90: hsl(260, 50%, 90%); +$md3-primary-95: hsl(260, 50%, 95%); +$md3-primary-99: hsl(260, 50%, 99%); +$md3-primary-100: hsl(260, 50%, 100%); + +// === MD3 Secondary Tonal Palette === +// Desaturated purple for secondary elements +$md3-secondary-0: hsl(260, 15%, 0%); +$md3-secondary-10: hsl(260, 15%, 10%); +$md3-secondary-20: hsl(260, 15%, 20%); +$md3-secondary-25: hsl(260, 15%, 25%); +$md3-secondary-30: hsl(260, 15%, 30%); +$md3-secondary-35: hsl(260, 15%, 35%); +$md3-secondary-40: hsl(260, 15%, 40%); +$md3-secondary-50: hsl(260, 15%, 50%); +$md3-secondary-60: hsl(260, 15%, 60%); +$md3-secondary-70: hsl(260, 15%, 70%); +$md3-secondary-80: hsl(260, 15%, 80%); +$md3-secondary-90: hsl(260, 15%, 90%); +$md3-secondary-95: hsl(260, 15%, 95%); +$md3-secondary-99: hsl(260, 15%, 99%); +$md3-secondary-100: hsl(260, 15%, 100%); + +// === MD3 Tertiary Tonal Palette === +// Cyan/Teal for tertiary accents +$md3-tertiary-0: hsl(180, 50%, 0%); +$md3-tertiary-10: hsl(180, 50%, 10%); +$md3-tertiary-20: hsl(180, 50%, 20%); +$md3-tertiary-25: hsl(180, 50%, 25%); +$md3-tertiary-30: hsl(180, 50%, 30%); +$md3-tertiary-35: hsl(180, 50%, 35%); +$md3-tertiary-40: hsl(180, 50%, 40%); +$md3-tertiary-50: hsl(180, 50%, 50%); +$md3-tertiary-60: hsl(180, 50%, 60%); +$md3-tertiary-70: hsl(180, 50%, 70%); +$md3-tertiary-80: hsl(180, 50%, 80%); +$md3-tertiary-90: hsl(180, 50%, 90%); +$md3-tertiary-95: hsl(180, 50%, 95%); +$md3-tertiary-99: hsl(180, 50%, 99%); +$md3-tertiary-100: hsl(180, 50%, 100%); + +// === MD3 Error Tonal Palette === +$md3-error-0: hsl(0, 75%, 0%); +$md3-error-10: hsl(0, 75%, 10%); +$md3-error-20: hsl(0, 75%, 20%); +$md3-error-30: hsl(0, 75%, 30%); +$md3-error-40: hsl(0, 75%, 40%); +$md3-error-50: hsl(0, 75%, 50%); +$md3-error-60: hsl(0, 75%, 60%); +$md3-error-70: hsl(0, 75%, 70%); +$md3-error-80: hsl(0, 75%, 80%); +$md3-error-90: hsl(0, 75%, 90%); +$md3-error-95: hsl(0, 75%, 95%); +$md3-error-100: hsl(0, 75%, 100%); + +// === MD3 Neutral Tonal Palette === +// For surfaces and backgrounds +$md3-neutral-0: hsl(260, 10%, 0%); +$md3-neutral-4: hsl(260, 10%, 4%); +$md3-neutral-6: hsl(260, 10%, 6%); +$md3-neutral-10: hsl(260, 10%, 10%); +$md3-neutral-12: hsl(260, 10%, 12%); +$md3-neutral-17: hsl(260, 10%, 17%); +$md3-neutral-20: hsl(260, 10%, 20%); +$md3-neutral-22: hsl(260, 10%, 22%); +$md3-neutral-24: hsl(260, 10%, 24%); +$md3-neutral-25: hsl(260, 10%, 25%); +$md3-neutral-30: hsl(260, 10%, 30%); +$md3-neutral-35: hsl(260, 10%, 35%); +$md3-neutral-40: hsl(260, 10%, 40%); +$md3-neutral-50: hsl(260, 10%, 50%); +$md3-neutral-60: hsl(260, 10%, 60%); +$md3-neutral-70: hsl(260, 10%, 70%); +$md3-neutral-80: hsl(260, 10%, 80%); +$md3-neutral-87: hsl(260, 10%, 87%); +$md3-neutral-90: hsl(260, 10%, 90%); +$md3-neutral-92: hsl(260, 10%, 92%); +$md3-neutral-94: hsl(260, 10%, 94%); +$md3-neutral-95: hsl(260, 10%, 95%); +$md3-neutral-96: hsl(260, 10%, 96%); +$md3-neutral-98: hsl(260, 10%, 98%); +$md3-neutral-99: hsl(260, 10%, 99%); +$md3-neutral-100: hsl(260, 10%, 100%); + +// === MD3 Neutral Variant Tonal Palette === +// For outline and surface variants +$md3-neutral-variant-0: hsl(260, 15%, 0%); +$md3-neutral-variant-10: hsl(260, 15%, 10%); +$md3-neutral-variant-20: hsl(260, 15%, 20%); +$md3-neutral-variant-25: hsl(260, 15%, 25%); +$md3-neutral-variant-30: hsl(260, 15%, 30%); +$md3-neutral-variant-35: hsl(260, 15%, 35%); +$md3-neutral-variant-40: hsl(260, 15%, 40%); +$md3-neutral-variant-50: hsl(260, 15%, 50%); +$md3-neutral-variant-60: hsl(260, 15%, 60%); +$md3-neutral-variant-70: hsl(260, 15%, 70%); +$md3-neutral-variant-80: hsl(260, 15%, 80%); +$md3-neutral-variant-90: hsl(260, 15%, 90%); +$md3-neutral-variant-95: hsl(260, 15%, 95%); +$md3-neutral-variant-99: hsl(260, 15%, 99%); +$md3-neutral-variant-100: hsl(260, 15%, 100%); + +// === MD3 Semantic Colors (Success, Warning, Info) === +$md3-success-40: hsl(145, 60%, 35%); +$md3-success-80: hsl(145, 60%, 75%); +$md3-success-90: hsl(145, 60%, 90%); +$md3-warning-40: hsl(40, 80%, 45%); +$md3-warning-80: hsl(40, 80%, 75%); +$md3-warning-90: hsl(40, 80%, 90%); +$md3-info-40: hsl(210, 70%, 45%); +$md3-info-80: hsl(210, 70%, 75%); +$md3-info-90: hsl(210, 70%, 90%); + +// ============================================ +// MD3 Dark Theme Color Scheme +// ============================================ + +// Primary colors +$primary: $md3-primary-80; +$primary-foreground: $md3-primary-20; +$on-primary: $md3-primary-20; +$primary-container: $md3-primary-30; +$on-primary-container: $md3-primary-90; + +// Secondary colors +$secondary: $md3-secondary-80; +$secondary-foreground: $md3-secondary-20; +$on-secondary: $md3-secondary-20; +$secondary-container: $md3-secondary-30; +$on-secondary-container: $md3-secondary-90; + +// Tertiary colors +$accent: $md3-tertiary-80; +$accent-foreground: $md3-tertiary-20; +$on-tertiary: $md3-tertiary-20; +$tertiary-container: $md3-tertiary-30; +$on-tertiary-container: $md3-tertiary-90; + +// Error colors +$destructive: $md3-error-80; +$destructive-foreground: $md3-error-20; +$on-error: $md3-error-20; +$error-container: $md3-error-30; +$on-error-container: $md3-error-90; + +// Surface colors (MD3 Tonal Elevation) +$background: $md3-neutral-6; +$foreground: $md3-neutral-90; +$on-background: $md3-neutral-90; + +$surface: $md3-neutral-6; +$on-surface: $md3-neutral-90; +$surface-dim: $md3-neutral-6; +$surface-bright: $md3-neutral-24; + +// Surface containers for MD3 tonal elevation +$surface-container-lowest: $md3-neutral-4; +$surface-container-low: $md3-neutral-10; +$surface-container: $md3-neutral-12; +$surface-container-high: $md3-neutral-17; +$surface-container-highest: $md3-neutral-22; + +// Surface variant +$surface-variant: $md3-neutral-variant-30; +$on-surface-variant: $md3-neutral-variant-80; + +// Outline colors +$outline: $md3-neutral-variant-60; +$outline-variant: $md3-neutral-variant-30; + +// Legacy compatibility mappings +$card: $surface-container-low; +$card-foreground: $on-surface; +$popover: $surface-container-high; +$popover-foreground: $on-surface; +$muted: $surface-container; +$muted-foreground: $on-surface-variant; +$border: $outline-variant; +$input: $surface-container-highest; +$ring: $primary; + +// Inverse colors +$inverse-surface: $md3-neutral-90; +$inverse-on-surface: $md3-neutral-20; +$inverse-primary: $md3-primary-40; + +// Scrim and shadow +$scrim: $md3-neutral-0; +$shadow: $md3-neutral-0; + +// State layer opacities (MD3 spec) +$state-hover-opacity: 0.08; +$state-focus-opacity: 0.12; +$state-pressed-opacity: 0.12; +$state-dragged-opacity: 0.16; +$state-disabled-opacity: 0.38; +$state-disabled-container-opacity: 0.12; + +// ============================================ +// MD3 Typography Scale +// ============================================ + +// Display +$font-display-large: 3.5625rem; // 57px +$font-display-medium: 2.8125rem; // 45px +$font-display-small: 2.25rem; // 36px + +// Headline +$font-headline-large: 2rem; // 32px +$font-headline-medium: 1.75rem; // 28px +$font-headline-small: 1.5rem; // 24px + +// Title +$font-title-large: 1.375rem; // 22px +$font-title-medium: 1rem; // 16px +$font-title-small: 0.875rem; // 14px + +// Body +$font-body-large: 1rem; // 16px +$font-body-medium: 0.875rem; // 14px +$font-body-small: 0.75rem; // 12px + +// Label +$font-label-large: 0.875rem; // 14px +$font-label-medium: 0.75rem; // 12px +$font-label-small: 0.6875rem; // 11px + +// Line heights for MD3 +$line-height-display-large: 4rem; // 64px +$line-height-display-medium: 3.25rem; // 52px +$line-height-display-small: 2.75rem; // 44px +$line-height-headline-large: 2.5rem; // 40px +$line-height-headline-medium: 2.25rem; // 36px +$line-height-headline-small: 2rem; // 32px +$line-height-title-large: 1.75rem; // 28px +$line-height-title-medium: 1.5rem; // 24px +$line-height-title-small: 1.25rem; // 20px +$line-height-body-large: 1.5rem; // 24px +$line-height-body-medium: 1.25rem; // 20px +$line-height-body-small: 1rem; // 16px +$line-height-label-large: 1.25rem; // 20px +$line-height-label-medium: 1rem; // 16px +$line-height-label-small: 1rem; // 16px + +// Letter spacing for MD3 +$letter-spacing-display: -0.25px; +$letter-spacing-headline: 0; +$letter-spacing-title: 0; +$letter-spacing-body: 0.25px; +$letter-spacing-label: 0.1px; + +// ============================================ +// MD3 Border Radius (Shape Scale) +// ============================================ +$radius-factor: 1; $size-scale: 1; + +$radius-none: 0; +$radius-extra-small: calc(4px * $radius-factor); // MD3 extra-small +$radius-small: calc(8px * $radius-factor); // MD3 small +$radius-medium: calc(12px * $radius-factor); // MD3 medium +$radius-large: calc(16px * $radius-factor); // MD3 large +$radius-extra-large: calc(28px * $radius-factor); // MD3 extra-large +$radius-full: 9999px; + +// Legacy mappings +$radius-sm: $radius-extra-small; +$radius-md: $radius-small; +$radius-lg: $radius-medium; +$radius-xl: $radius-large; + +// ============================================ +// MD3 Elevation System +// ============================================ + +// Elevation shadows (with tonal overlay for dark theme) +$elevation-1: 0 1px 2px rgba($shadow, 0.3), 0 1px 3px 1px rgba($shadow, 0.15); +$elevation-2: 0 1px 2px rgba($shadow, 0.3), 0 2px 6px 2px rgba($shadow, 0.15); +$elevation-3: 0 4px 8px 3px rgba($shadow, 0.15), 0 1px 3px rgba($shadow, 0.3); +$elevation-4: 0 6px 10px 4px rgba($shadow, 0.15), 0 2px 3px rgba($shadow, 0.3); +$elevation-5: 0 8px 12px 6px rgba($shadow, 0.15), 0 4px 4px rgba($shadow, 0.3); + +// Tonal elevation overlays (for dark theme surfaces) +$tonal-elevation-1: rgba($primary, 0.05); +$tonal-elevation-2: rgba($primary, 0.08); +$tonal-elevation-3: rgba($primary, 0.11); +$tonal-elevation-4: rgba($primary, 0.12); +$tonal-elevation-5: rgba($primary, 0.14); + +// Legacy shadow mappings +$shadow-accent: rgba($primary, 0.08); +$shadow-accent-md: rgba($primary, 0.12); +$shadow-focus: rgba($primary, 0.10); +$accent-hover: $md3-tertiary-70; + +// ============================================ +// MD3 Motion/Animation +// ============================================ + +// Duration tokens +$duration-short-1: 50ms; +$duration-short-2: 100ms; +$duration-short-3: 150ms; +$duration-short-4: 200ms; +$duration-medium-1: 250ms; +$duration-medium-2: 300ms; +$duration-medium-3: 350ms; +$duration-medium-4: 400ms; +$duration-long-1: 450ms; +$duration-long-2: 500ms; +$duration-long-3: 550ms; +$duration-long-4: 600ms; +$duration-extra-long-1: 700ms; +$duration-extra-long-2: 800ms; +$duration-extra-long-3: 900ms; +$duration-extra-long-4: 1000ms; + +// Easing tokens (MD3 curves) +$easing-standard: cubic-bezier(0.2, 0, 0, 1); +$easing-standard-decelerate: cubic-bezier(0, 0, 0, 1); +$easing-standard-accelerate: cubic-bezier(0.3, 0, 1, 1); +$easing-emphasized: cubic-bezier(0.2, 0, 0, 1); +$easing-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1); +$easing-emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15); +$easing-legacy: cubic-bezier(0.4, 0, 0.2, 1); + +// ============================================ +// Size Scale (4dp grid system) +// ============================================ $size-0: 0px; $size-px: 1px; -$size-0-5: calc(0.125rem * $size-scale); -$size-1: calc(0.25rem * $size-scale); -$size-2: calc(0.5rem * $size-scale); -$size-3: calc(0.75rem * $size-scale); -$size-4: calc(1rem * $size-scale); -$size-5: calc(1.25rem * $size-scale); -$size-6: calc(1.5rem * $size-scale); -$size-8: calc(2rem * $size-scale); -$size-10: calc(2.5rem * $size-scale); -$size-12: calc(3rem * $size-scale); -$size-16: calc(4rem * $size-scale); -$size-20: calc(5rem * $size-scale); -$size-24: calc(6rem * $size-scale); +$size-0-5: calc(0.125rem * $size-scale); // 2px +$size-1: calc(0.25rem * $size-scale); // 4px +$size-2: calc(0.5rem * $size-scale); // 8px +$size-3: calc(0.75rem * $size-scale); // 12px +$size-4: calc(1rem * $size-scale); // 16px +$size-5: calc(1.25rem * $size-scale); // 20px +$size-6: calc(1.5rem * $size-scale); // 24px +$size-8: calc(2rem * $size-scale); // 32px +$size-10: calc(2.5rem * $size-scale); // 40px +$size-12: calc(3rem * $size-scale); // 48px +$size-14: calc(3.5rem * $size-scale); // 56px +$size-16: calc(4rem * $size-scale); // 64px +$size-20: calc(5rem * $size-scale); // 80px +$size-24: calc(6rem * $size-scale); // 96px -// Border radii variables (for direct use) -$radius-factor: 1; -$radius-sm: calc(2px * $radius-factor * $size-scale); -$radius-md: calc(6px * $radius-factor * $size-scale); -$radius-lg: calc(8px * $radius-factor * $size-scale); -$radius-xl: calc(12px * $radius-factor * $size-scale); -$radius-full: 9999px; +// ============================================ +// Legacy Compatibility +// ============================================ // Spacing Scale $spacing: ( @@ -81,20 +402,20 @@ $spacing: ( 96: 24rem, ); -// Border Radius +// Border Radius (legacy map) $radius: ( none: 0, - sm: 0.125rem, - base: 0.25rem, - md: 0.375rem, - lg: 0.5rem, - xl: 0.75rem, - 2xl: 1rem, - 3xl: 1.5rem, + sm: 0.25rem, + base: 0.5rem, + md: 0.75rem, + lg: 1rem, + xl: 1.5rem, + 2xl: 1.75rem, + 3xl: 2rem, full: 9999px, ); -// Font Sizes +// Font Sizes (legacy map) $font-sizes: ( xs: 0.75rem, sm: 0.875rem, @@ -143,11 +464,3 @@ $z-index: ( 50: 50, auto: auto, ); - -// Shadow values - Material Design 3 elevation system -$shadow-accent: rgba(99, 150, 180, 0.08); // MD3 elevation tint -$shadow-accent-md: rgba(99, 150, 180, 0.12); -$shadow-focus: rgba(99, 150, 180, 0.10); - -// Hover colors - MD3 state layers -$accent-hover: hsl(195, 50%, 55%); diff --git a/src/styles/components/_buttons.scss b/src/styles/components/_buttons.scss index 81b9cf4..f6c0f3b 100644 --- a/src/styles/components/_buttons.scss +++ b/src/styles/components/_buttons.scss @@ -1,92 +1,360 @@ @use '../abstracts' as *; -// Button component styles -// These complement the existing Tailwind-based button components +// ============================================ +// Material Design 3 Button Styles +// ============================================ + +// Base button styles (MD3) .btn { display: inline-flex; align-items: center; justify-content: center; gap: $size-2; - padding: $size-2 $size-4; + padding: $size-2-5 $size-6; + min-width: 64px; + min-height: 40px; border: none; - border-radius: $radius-md; + border-radius: $radius-full; + font-family: inherit; font-weight: 500; - font-size: 0.875rem; + font-size: $font-label-large; + line-height: $line-height-label-large; + letter-spacing: $letter-spacing-label; cursor: pointer; - transition: all 0.2s ease; + user-select: none; + text-decoration: none; + position: relative; + overflow: hidden; + transition: + background-color $duration-short-4 $easing-standard, + box-shadow $duration-short-4 $easing-standard, + color $duration-short-4 $easing-standard; outline: none; - &:hover:not(:disabled) { - transform: translateY(-2px); + // State layer + &::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background-color: currentColor; + opacity: 0; + transition: opacity $duration-short-4 $easing-standard; + pointer-events: none; + } + + &:hover::before { + opacity: $state-hover-opacity; + } + + &:focus-visible::before { + opacity: $state-focus-opacity; + } + + &:active::before { + opacity: $state-pressed-opacity; } &:focus-visible { - box-shadow: 0 0 0 3px $secondary, 0 0 0 6px $ring; + outline: 2px solid $ring; + outline-offset: 2px; } &:disabled { - opacity: 0.5; + opacity: $state-disabled-opacity; cursor: not-allowed; + pointer-events: none; } - // Primary variant - MD3 filled button - &-primary { - background: $primary; - color: $primary-foreground; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12); // MD3 elevation-1 + svg { + width: 18px; + height: 18px; + flex-shrink: 0; + } + + // ============================================ + // MD3 Filled Button (Primary) + // ============================================ + &-primary, + &-filled { + background-color: $primary; + color: $on-primary; + box-shadow: $elevation-1; &:hover:not(:disabled) { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14); // MD3 elevation-2 - background: hsl(260, 45%, 58%); // Slightly lighter on hover + box-shadow: $elevation-2; } &:active:not(:disabled) { - transform: translateY(0); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12); + box-shadow: $elevation-1; } } - // Secondary variant - &-secondary { - background-color: $secondary; - color: $secondary-foreground; - border: 1px solid $border; + // ============================================ + // MD3 Filled Tonal Button + // ============================================ + &-tonal { + background-color: $secondary-container; + color: $on-secondary-container; &:hover:not(:disabled) { - background-color: hsl(217.2, 32.6%, 20%); + box-shadow: $elevation-1; } } - // Ghost variant + // ============================================ + // MD3 Elevated Button + // ============================================ + &-elevated { + background-color: $surface-container-low; + color: $primary; + box-shadow: $elevation-1; + + &:hover:not(:disabled) { + box-shadow: $elevation-2; + } + + &:active:not(:disabled) { + box-shadow: $elevation-1; + } + } + + // ============================================ + // MD3 Outlined Button + // ============================================ + &-outline, + &-outlined { + background-color: transparent; + color: $primary; + border: 1px solid $outline; + + &:hover:not(:disabled) { + background-color: rgba($primary, $state-hover-opacity); + } + } + + // ============================================ + // MD3 Text Button + // ============================================ + &-text, &-ghost { background-color: transparent; - color: $accent; - border: 1px solid $accent; + color: $primary; + padding-left: $size-3; + padding-right: $size-3; &:hover:not(:disabled) { - background-color: rgba(99, 242, 255, 0.1); + background-color: rgba($primary, $state-hover-opacity); } } - // Outline variant - &-outline { - background-color: transparent; - color: $foreground; - border: 1px solid $border; + // ============================================ + // Destructive/Error Button + // ============================================ + &-destructive, + &-error { + background-color: $destructive; + color: $on-error; + box-shadow: $elevation-1; &:hover:not(:disabled) { - background-color: $secondary; + box-shadow: $elevation-2; + } + + &:focus-visible { + outline-color: $destructive; } } - // Size variants + // ============================================ + // Secondary Button (maps to tonal) + // ============================================ + &-secondary { + background-color: $secondary-container; + color: $on-secondary-container; + + &:hover:not(:disabled) { + box-shadow: $elevation-1; + } + } + + // ============================================ + // Size Variants + // ============================================ &-sm { - padding: calc($size-1 * 0.75) $size-2; - font-size: 0.75rem; + padding: $size-1-5 $size-4; + min-height: 32px; + font-size: $font-label-medium; + gap: $size-1-5; + + svg { + width: 16px; + height: 16px; + } } &-lg { - padding: $size-3 $size-6; - font-size: 1rem; + padding: $size-3 $size-8; + min-height: 48px; + font-size: $font-label-large; + + svg { + width: 20px; + height: 20px; + } + } + + // ============================================ + // Icon Button + // ============================================ + &-icon { + padding: 0; + min-width: 40px; + width: 40px; + height: 40px; + border-radius: $radius-full; + + &.btn-sm { + min-width: 32px; + width: 32px; + height: 32px; + } + + &.btn-lg { + min-width: 48px; + width: 48px; + height: 48px; + } + } +} + +// ============================================ +// MD3 Icon Button (standalone) +// ============================================ +.icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + padding: 0; + border: none; + border-radius: $radius-full; + background-color: transparent; + color: $on-surface-variant; + cursor: pointer; + position: relative; + overflow: hidden; + transition: color $duration-short-4 $easing-standard; + outline: none; + + &::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background-color: currentColor; + opacity: 0; + transition: opacity $duration-short-4 $easing-standard; + pointer-events: none; + } + + &:hover::before { + opacity: $state-hover-opacity; + } + + &:focus-visible::before { + opacity: $state-focus-opacity; + } + + &:active::before { + opacity: $state-pressed-opacity; + } + + &:focus-visible { + outline: 2px solid $ring; + outline-offset: 2px; + } + + &:disabled { + opacity: $state-disabled-opacity; + cursor: not-allowed; + } + + svg { + width: 24px; + height: 24px; + } + + // Filled variant + &-filled { + background-color: $primary; + color: $on-primary; + } + + // Tonal variant + &-tonal { + background-color: $secondary-container; + color: $on-secondary-container; + } + + // Outlined variant + &-outlined { + border: 1px solid $outline; + color: $on-surface-variant; + } +} + +// ============================================ +// Navigation Button (burger menu) +// ============================================ +.nav-burger-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + padding: 0; + border: none; + border-radius: $radius-full; + background-color: transparent; + color: $on-surface; + cursor: pointer; + position: relative; + overflow: hidden; + transition: color $duration-short-4 $easing-standard; + outline: none; + + &::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background-color: currentColor; + opacity: 0; + transition: opacity $duration-short-4 $easing-standard; + pointer-events: none; + } + + &:hover::before { + opacity: $state-hover-opacity; + } + + &:focus-visible::before { + opacity: $state-focus-opacity; + } + + &:active::before { + opacity: $state-pressed-opacity; + } + + &:focus-visible { + outline: 2px solid $ring; + outline-offset: 2px; + } + + svg { + width: 24px; + height: 24px; } } diff --git a/src/styles/components/_dialogs.scss b/src/styles/components/_dialogs.scss index a5db0bc..31f7694 100644 --- a/src/styles/components/_dialogs.scss +++ b/src/styles/components/_dialogs.scss @@ -1,16 +1,20 @@ @use '../abstracts' as *; -// Dialog/Modal styling - Material Design 3 +// ============================================ +// Material Design 3 Dialog/Modal Styles +// ============================================ + +// Scrim/Overlay [data-slot="dialog-overlay"], .dialog-overlay { position: fixed; inset: 0; z-index: 50; - background-color: rgba(0, 0, 0, 0.5); // MD3 scrim opacity - backdrop-filter: blur(4px); // Subtle blur - animation: fadeIn 0.2s ease-out; + background-color: rgba($scrim, 0.32); // MD3 scrim opacity + animation: md3-fade-in $duration-medium-2 $easing-emphasized-decelerate; } +// Dialog Content (MD3 Basic Dialog) [data-slot="dialog"], .dialog-content { position: fixed; @@ -18,139 +22,239 @@ top: 50%; z-index: 50; transform: translate(-50%, -50%); - width: 90%; - max-width: 32rem; - max-height: 85vh; + width: calc(100% - 48px); + max-width: 560px; + max-height: calc(100vh - 96px); overflow-y: auto; - background: $popover; // MD3 surface-container-high - border: 1px solid rgba($border, 0.3); - border-radius: $radius-xl; // MD3 extra-large shape (28px) - box-shadow: - 0 8px 16px rgba(0, 0, 0, 0.18), // MD3 elevation-3 - 0 4px 8px rgba(0, 0, 0, 0.12); - animation: dialogSlideIn 0.25s cubic-bezier(0.2, 0, 0, 1); // MD3 emphasized easing - + background: $surface-container-high; + border: none; + border-radius: $radius-extra-large; + box-shadow: $elevation-3; + padding: $size-6; + animation: md3-dialog-enter $duration-medium-2 $easing-emphasized-decelerate; + &:focus { outline: none; } } -// Dropdown menu styling - Material Design 3 +// Dialog Header +[data-slot="dialog-header"], +.dialog-header { + display: flex; + flex-direction: column; + gap: $size-4; + margin-bottom: $size-6; +} + +// Dialog Title (MD3 Headline Small) +[data-slot="dialog-title"], +.dialog-title { + font-size: $font-headline-small; + line-height: $line-height-headline-small; + font-weight: 400; + color: $on-surface; + margin: 0; +} + +// Dialog Description (MD3 Body Medium) +[data-slot="dialog-description"], +.dialog-description { + font-size: $font-body-medium; + line-height: $line-height-body-medium; + color: $on-surface-variant; + margin: 0; +} + +// Dialog Footer (Actions) +[data-slot="dialog-footer"], +.dialog-footer { + display: flex; + justify-content: flex-end; + gap: $size-2; + margin-top: $size-6; +} + +// Dialog Close Button +[data-slot="dialog-close"], +.dialog-close { + position: absolute; + top: $size-4; + right: $size-4; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: $radius-full; + border: none; + background: transparent; + color: $on-surface-variant; + cursor: pointer; + transition: background-color $duration-short-4 $easing-standard; + + &:hover { + background-color: rgba($on-surface, $state-hover-opacity); + } + + &:focus-visible { + background-color: rgba($on-surface, $state-focus-opacity); + outline: none; + } +} + +// ============================================ +// MD3 Dropdown Menu +// ============================================ [data-slot="dropdown-menu-content"], .dropdown-content { z-index: 50; - min-width: 12rem; + min-width: 112px; + max-width: 280px; overflow: hidden; - border-radius: $radius-lg; // MD3 large shape - border: 1px solid rgba($border, 0.3); - background: $popover; - box-shadow: - 0 4px 8px rgba(0, 0, 0, 0.14), // MD3 elevation-2 - 0 2px 4px rgba(0, 0, 0, 0.10); - padding: 0.5rem; - animation: dropdownSlideIn 0.2s cubic-bezier(0.2, 0, 0, 1); - + border-radius: $radius-extra-small; + border: none; + background: $surface-container; + box-shadow: $elevation-2; + padding: $size-2 0; + animation: md3-menu-enter $duration-short-4 $easing-emphasized-decelerate; + &:focus { outline: none; } } +// Dropdown Menu Item [data-slot="dropdown-menu-item"], .dropdown-item { position: relative; display: flex; align-items: center; - gap: 0.75rem; - padding: 0.625rem 0.75rem; - font-size: 0.875rem; - color: $foreground; - border-radius: $radius-md; // MD3 medium shape + gap: $size-3; + min-height: 48px; + padding: 0 $size-3; + font-size: $font-body-large; + line-height: $line-height-body-large; + color: $on-surface; cursor: pointer; - transition: background-color 0.15s ease; + transition: background-color $duration-short-4 $easing-standard; user-select: none; outline: none; &:hover { - background-color: rgba($accent, 0.08); // MD3 state layer + background-color: rgba($on-surface, $state-hover-opacity); } &:focus { - background-color: rgba($accent, 0.12); + background-color: rgba($on-surface, $state-focus-opacity); + } + + &:active { + background-color: rgba($on-surface, $state-pressed-opacity); } &[data-disabled] { pointer-events: none; - opacity: 0.38; // MD3 disabled opacity + opacity: $state-disabled-opacity; + } + + svg { + width: 24px; + height: 24px; + color: $on-surface-variant; } } -// Popover styling - refined and elegant +// Dropdown Menu Separator +[data-slot="dropdown-menu-separator"], +.dropdown-separator { + height: 1px; + background: $outline-variant; + margin: $size-2 0; +} + +// Dropdown Menu Label +[data-slot="dropdown-menu-label"], +.dropdown-label { + padding: $size-2 $size-3; + font-size: $font-label-large; + font-weight: 500; + color: $on-surface-variant; +} + +// ============================================ +// MD3 Popover +// ============================================ [data-slot="popover-content"], .popover-content { z-index: 50; width: auto; - border-radius: $radius-lg; - border: 1px solid rgba($border, 0.5); - background: rgba($popover, 0.95); - backdrop-filter: blur(12px) saturate(150%); - padding: 1rem; - box-shadow: - 0 10px 25px rgba(0, 0, 0, 0.2), - 0 4px 10px rgba(0, 0, 0, 0.15); - animation: popoverSlideIn 0.2s cubic-bezier(0.4, 0, 0.2, 1); - + min-width: 112px; + border-radius: $radius-medium; + border: none; + background: $surface-container; + box-shadow: $elevation-2; + padding: $size-4; + animation: md3-menu-enter $duration-short-4 $easing-emphasized-decelerate; + &:focus { outline: none; } } -// Select dropdown styling - beautiful custom select +// ============================================ +// MD3 Select +// ============================================ [data-slot="select-trigger"], .select-trigger { display: flex; align-items: center; justify-content: space-between; width: 100%; - padding: 0.5rem 0.75rem; - font-size: 0.875rem; - border-radius: $radius-md; - border: 1px solid rgba($border, 0.5); - background: rgba($input, 0.5); - color: $foreground; + min-height: 56px; + padding: $size-4; + font-size: $font-body-large; + border-radius: $radius-extra-small; + border: 1px solid $outline; + background: transparent; + color: $on-surface; cursor: pointer; - transition: all 0.2s ease; + transition: border-color $duration-short-4 $easing-standard; &:hover { - background: rgba($input, 0.7); - border-color: rgba($accent, 0.5); + border-color: $on-surface; } &:focus { outline: none; - border-color: $ring; - box-shadow: 0 0 0 3px rgba($ring, 0.1); + border-color: $primary; + border-width: 2px; + padding: calc(#{$size-4} - 1px); } &[data-disabled] { pointer-events: none; - opacity: 0.5; + opacity: $state-disabled-opacity; + } + + svg { + width: 24px; + height: 24px; + color: $on-surface-variant; } } [data-slot="select-content"], .select-content { z-index: 50; - min-width: 8rem; + min-width: 112px; overflow: hidden; - border-radius: $radius-lg; - border: 1px solid rgba($border, 0.5); - background: rgba($popover, 0.95); - backdrop-filter: blur(12px) saturate(150%); - box-shadow: - 0 10px 25px rgba(0, 0, 0, 0.2), - 0 4px 10px rgba(0, 0, 0, 0.15); - padding: 0.375rem; - animation: selectSlideIn 0.2s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: $radius-extra-small; + border: none; + background: $surface-container; + box-shadow: $elevation-2; + padding: $size-2 0; + animation: md3-menu-enter $duration-short-4 $easing-emphasized-decelerate; } [data-slot="select-item"], @@ -158,45 +262,46 @@ position: relative; display: flex; align-items: center; - padding: 0.5rem 0.75rem 0.5rem 2rem; - font-size: 0.875rem; - color: $foreground; - border-radius: $radius-sm; + min-height: 48px; + padding: 0 $size-3; + font-size: $font-body-large; + color: $on-surface; cursor: pointer; - transition: all 0.15s ease; + transition: background-color $duration-short-4 $easing-standard; outline: none; user-select: none; &:hover { - background-color: rgba($accent, 0.12); - color: $accent; + background-color: rgba($on-surface, $state-hover-opacity); } &:focus { - background-color: rgba($accent, 0.15); - color: $accent; + background-color: rgba($on-surface, $state-focus-opacity); } &[data-state="checked"] { - background-color: rgba($accent, 0.1); - color: $accent; - font-weight: 500; + background-color: rgba($primary, 0.12); &::before { - content: '✓'; + content: ''; position: absolute; - left: 0.5rem; - font-size: 0.875rem; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: $primary; } } &[data-disabled] { pointer-events: none; - opacity: 0.5; + opacity: $state-disabled-opacity; } } -// Alert Dialog styling - attention-grabbing +// ============================================ +// MD3 Alert Dialog +// ============================================ [data-slot="alert-dialog-content"], .alert-dialog-content { position: fixed; @@ -204,24 +309,115 @@ top: 50%; z-index: 50; transform: translate(-50%, -50%); - width: 90%; - max-width: 28rem; - background: linear-gradient(135deg, rgba($card, 0.95) 0%, rgba($card, 0.98) 100%); - border: 1px solid rgba($destructive, 0.3); - border-radius: $radius-xl; - box-shadow: - 0 20px 40px rgba($destructive, 0.2), - 0 10px 20px rgba(0, 0, 0, 0.2); - padding: 1.5rem; - animation: alertSlideIn 0.25s cubic-bezier(0.4, 0, 0.2, 1); - + width: calc(100% - 48px); + max-width: 560px; + background: $surface-container-high; + border: none; + border-radius: $radius-extra-large; + box-shadow: $elevation-3; + padding: $size-6; + animation: md3-dialog-enter $duration-medium-2 $easing-emphasized-decelerate; + &:focus { outline: none; } } +// Alert Dialog with icon (optional) +.alert-dialog-icon { + display: flex; + justify-content: center; + margin-bottom: $size-4; + + svg { + width: 24px; + height: 24px; + color: $on-surface-variant; + } + + &--error svg { + color: $destructive; + } +} + +// ============================================ +// MD3 Snackbar/Toast +// ============================================ +.snackbar, +[data-slot="toast"] { + display: flex; + align-items: center; + gap: $size-2; + min-height: 48px; + padding: $size-3-5 $size-4; + background: $inverse-surface; + color: $inverse-on-surface; + border-radius: $radius-extra-small; + box-shadow: $elevation-3; + font-size: $font-body-medium; + animation: md3-snackbar-enter $duration-medium-1 $easing-emphasized-decelerate; +} + +.snackbar-action { + margin-left: auto; + padding: 0 $size-2; + font-size: $font-label-large; + font-weight: 500; + color: $inverse-primary; + background: transparent; + border: none; + cursor: pointer; + transition: opacity $duration-short-4 $easing-standard; + + &:hover { + opacity: 0.8; + } +} + +// ============================================ +// Namespace Selector (MD3 styled) +// ============================================ +.namespace-select { + display: inline-flex; + align-items: center; + gap: $size-2; + padding: $size-2 $size-4; + font-size: $font-label-large; + font-weight: 500; + border-radius: $radius-small; + border: 1px solid $outline-variant; + background: $surface-container; + color: $on-surface; + cursor: pointer; + transition: all $duration-short-4 $easing-standard; + + svg { + width: 18px; + height: 18px; + color: $on-surface-variant; + transition: color $duration-short-4 $easing-standard; + } + + &:hover { + background: $surface-container-high; + border-color: $outline; + + svg { + color: $on-surface; + } + } + + &:focus { + outline: none; + border-color: $primary; + box-shadow: 0 0 0 2px rgba($primary, 0.2); + } +} + +// ============================================ // Animations -@keyframes fadeIn { +// ============================================ +@keyframes md3-fade-in { from { opacity: 0; } @@ -230,10 +426,10 @@ } } -@keyframes dialogSlideIn { +@keyframes md3-dialog-enter { from { opacity: 0; - transform: translate(-50%, -48%) scale(0.96); + transform: translate(-50%, -48%) scale(0.95); } to { opacity: 1; @@ -241,21 +437,10 @@ } } -@keyframes dropdownSlideIn { +@keyframes md3-menu-enter { from { opacity: 0; - transform: translateY(-8px) scale(0.96); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -@keyframes popoverSlideIn { - from { - opacity: 0; - transform: scale(0.96); + transform: scale(0.95); } to { opacity: 1; @@ -263,64 +448,13 @@ } } -@keyframes selectSlideIn { +@keyframes md3-snackbar-enter { from { opacity: 0; - transform: translateY(-4px); + transform: translateY(100%); } to { opacity: 1; transform: translateY(0); } } - -@keyframes alertSlideIn { - from { - opacity: 0; - transform: translate(-50%, -45%) scale(0.9); - } - to { - opacity: 1; - transform: translate(-50%, -50%) scale(1); - } -} - -// Namespace selector specific styling -.namespace-select { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - font-size: 0.875rem; - font-weight: 500; - border-radius: $radius-lg; - border: 1px solid rgba($border, 0.5); - background: rgba($secondary, 0.5); - color: $foreground; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - - svg { - width: 1rem; - height: 1rem; - color: $muted-foreground; - transition: color 0.2s ease; - } - - &:hover { - background: rgba($secondary, 0.7); - border-color: rgba($accent, 0.4); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba($accent, 0.1); - - svg { - color: $accent; - } - } - - &:focus { - outline: none; - border-color: $ring; - box-shadow: 0 0 0 3px rgba($ring, 0.1); - } -}