From 3634917edb5e418857b4d65d567e73fb7773c9b1 Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Sat, 2 May 2026 10:54:48 -0400 Subject: [PATCH 01/26] chore: bump version to 1.1.0 and add changelog --- CHANGELOG.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- pnpm-lock.yaml | 6 ++--- 3 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..e886d6ae --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,67 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.0] - 2026-05-02 + +### Breaking Changes + +**Major Theming System Refactor** + +- Removed all shadcn-style theme variables in favor of pure LSD design tokens + - Removed: `--background`, `--foreground`, `--primary`, `--primary-foreground`, `--secondary`, `--secondary-foreground`, `--accent`, `--accent-foreground`, `--destructive`, `--destructive-foreground`, `--muted`, `--muted-foreground`, `--border`, `--input`, `--ring` +- Renamed design tokens for semantic clarity + - Double-prefix pattern (e.g., `text-lsd-text-secondary`) standardized to single prefix (e.g., `text-secondary`) + - Updated all component styling to use new variable naming convention +- Theme structure reorganized to support nested light/dark variants with accent themes +- Custom theme creation now requires using LSD-specific variable names + +### Migration + +If you have custom themes or override theme variables, you'll need to update them: + +1. Replace shadcn-style variables with LSD tokens +2. Update variable names to match the new single-prefix pattern +3. Review custom theme definitions against the new theme structure + +### Features + +- **New Monochrome Theme**: Added a pure monochrome accent theme for minimal design needs +- **Improved Theme System**: Redesigned CSS custom properties for better semantic clarity and consistency +- **Enhanced Theming Architecture**: Better support for nested light/dark variants with accent theme inheritance +- **Expanded Theme Options**: 5 accent themes available (Monochrome, Teal, Nord, Terracotta, Catppuccin) + +### Improvements + +- Standardized variable naming conventions across all 40+ components +- Improved theme inheritance and scoping behavior +- Enhanced accessibility with consistent semantic token names +- Better separation of concerns between color modes and accent themes + +### Deprecations + +- None removed (shadcn-style variables were fully removed, not deprecated) + +--- + +## Previous Versions + +### [1.0.0] - Initial Release + +- Initial release of LSD component library +- 38+ accessible UI components +- Built on React 19+ with Radix UI +- Support for light/dark modes and 4 accent themes (Teal, Nord, Terracotta, Catppuccin) +- Monochrome color palette with semantic styling +- Comprehensive documentation site +- Full TypeScript support + +--- + +## Version Reference + +- **1.1.0** - Current release (theming refactor) +- **1.0.0** - Initial release diff --git a/package.json b/package.json index 8eb3b358..22efb997 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nipsys/lsd-monorepo", - "version": "1.0.1", + "version": "1.1.0", "private": true, "description": "Monorepo for the @nipsys/lsd component library and documentation", "license": "MIT", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f87c5931..11bf025f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,7 +117,7 @@ importers: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwind-merge: - specifier: ^3.4.0 + specifier: ^3.5.0 version: 3.5.0 tw-animate-css: specifier: ^1.4.0 @@ -133,7 +133,7 @@ importers: specifier: ^20.4.1 version: 20.5.0 '@tailwindcss/vite': - specifier: ^4.1.18 + specifier: ^4.2.4 version: 4.2.4(vite@7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) '@testing-library/jest-dom': specifier: ^6.9.1 @@ -172,7 +172,7 @@ importers: specifier: ^2.1.0 version: 2.1.6 tailwindcss: - specifier: ^4.1.18 + specifier: ^4.2.4 version: 4.2.4 typescript: specifier: ~5.9.3 From 6baba4c18ccbc7fedfdb6372e8de985beb9b97e1 Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Sat, 2 May 2026 10:54:52 -0400 Subject: [PATCH 02/26] refactor(styles): implement multi-theme system with 5 accent themes --- packages/lsd/src/styles/core.css | 136 ++--------------- packages/lsd/src/styles/sonner.css | 30 ++-- packages/lsd/src/styles/themes.css | 229 ++++++++++++++++++----------- 3 files changed, 173 insertions(+), 222 deletions(-) diff --git a/packages/lsd/src/styles/core.css b/packages/lsd/src/styles/core.css index da95c6c2..144ac887 100644 --- a/packages/lsd/src/styles/core.css +++ b/packages/lsd/src/styles/core.css @@ -1,4 +1,5 @@ @custom-variant dark (&:is(.dark *)); +@custom-variant light (&:is(.light *)); @theme inline { --radius-sm: calc(var(--radius) - 4px); @@ -10,59 +11,23 @@ --animate-indeterminate-progress: indeterminate-progress 1.5s ease-in-out infinite; --animate-indeterminate-progress-slow: indeterminate-progress 3s ease-in-out infinite; --animate-indeterminate-progress-fast: indeterminate-progress 0.75s ease-in-out infinite; - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); /* LSD Design Tokens as Tailwind Colors */ - --color-lsd-text-primary: var(--lsd-text-primary); + --color-lsd-primary: var(--lsd-primary); + --color-lsd-primary-content: var(--lsd-primary-content); + --color-lsd-text-neutral: var(--lsd-text-neutral); --color-lsd-text-secondary: var(--lsd-text-secondary); --color-lsd-border: var(--lsd-border); - --color-lsd-icon-primary: var(--lsd-icon-primary); - --color-lsd-icon-secondary: var(--lsd-icon-secondary); - --color-lsd-surface: var(--lsd-surface); - --color-lsd-primary: var(--lsd-primary); + --color-lsd-background: var(--lsd-background); + --color-lsd-foreground: var(--lsd-foreground); --color-lsd-destructive: var(--lsd-destructive); - --color-lsd-destructive-text: var(--lsd-destructive-text); + --color-lsd-text-destructive: var(--lsd-text-destructive); --color-lsd-success: var(--lsd-success); - --color-lsd-success-text: var(--lsd-success-text); + --color-lsd-text-success: var(--lsd-text-success); --color-lsd-warning: var(--lsd-warning); - --color-lsd-warning-text: var(--lsd-warning-text); + --color-lsd-text-warning: var(--lsd-text-warning); --color-lsd-info: var(--lsd-info); - --color-lsd-info-text: var(--lsd-info-text); - --color-lsd-chart-1: #dc2626; - --color-lsd-chart-2: #2563eb; - --color-lsd-chart-3: #16a34a; - --color-lsd-chart-4: #f59e0b; - --color-lsd-chart-5: #a855f7; + --color-lsd-text-info: var(--lsd-text-info); } :root { @@ -76,91 +41,16 @@ --lsd-spacing-large: 1.25rem; --lsd-spacing-larger: 1.5rem; --lsd-spacing-largest: 1.75rem; - - /* Shadcn variables using LSD values (always defined on :root) */ - --background: var(--lsd-surface); - --foreground: var(--lsd-text-primary); - --card: var(--lsd-surface); - --card-foreground: var(--lsd-text-primary); - --popover: var(--lsd-surface); - --popover-foreground: var(--lsd-text-primary); - --primary: var(--lsd-primary); - --primary-foreground: var(--lsd-primary-foreground); - --secondary: var(--lsd-surface); - --secondary-foreground: var(--lsd-text-secondary); - --muted: var(--lsd-surface); - --muted-foreground: var(--lsd-text-primary); - --accent: var(--lsd-surface); - --accent-foreground: var(--lsd-text-secondary); - --destructive: var(--color-lsd-destructive); - --border: var(--lsd-border); - --input: var(--lsd-surface); - --ring: var(--lsd-text-primary); - --chart-1: var(--color-lsd-chart-1); - --chart-2: var(--color-lsd-chart-2); - --chart-3: var(--color-lsd-chart-3); - --chart-4: var(--color-lsd-chart-4); - --chart-5: var(--color-lsd-chart-5); - --sidebar: var(--lsd-surface); - --sidebar-foreground: var(--lsd-text-primary); - --sidebar-primary: var(--lsd-primary); - --sidebar-primary-foreground: var(--lsd-primary-foreground); - --sidebar-accent: var(--lsd-surface); - --sidebar-accent-foreground: var(--lsd-text-primary); - --sidebar-border: var(--lsd-border); - --sidebar-ring: var(--lsd-text-primary); -} - -:root:not([data-theme]):not([data-theme*=""]) { - /* LSD Design Tokens - Light Theme (defaults only) */ - --lsd-font-family: monospace; - --lsd-primary: #000000; - --lsd-primary-foreground: #ffffff; - --lsd-text-primary: #000000; - --lsd-text-secondary: #4a4a4a; - --lsd-border: #000000; - --lsd-icon-primary: #000000; - --lsd-icon-secondary: #ffffff; - --lsd-surface: #ffffff; - --lsd-destructive: #da0000; - --lsd-destructive-text: #c92d2d; - --lsd-success: #15803d; - --lsd-success-text: #1c9618; - --lsd-warning: #b45309; - --lsd-warning-text: #854d0e; - --lsd-info: #2563eb; - --lsd-info-text: #2563eb; -} - -.dark:not([data-theme]):not([data-theme*=""]) { - /* LSD Design Tokens - Dark Theme (defaults only) */ - --lsd-font-family: monospace; - --lsd-primary: #ffffff; - --lsd-primary-foreground: #000000; - --lsd-text-primary: #ffffff; - --lsd-text-secondary: #d2d2d2; - --lsd-border: #ffffff; - --lsd-icon-primary: #ffffff; - --lsd-icon-secondary: #000000; - --lsd-surface: #000000; - --lsd-destructive: #e5484d; - --lsd-destructive-text: #f27a7a; - --lsd-success: #22c55e; - --lsd-success-text: #30c82a; - --lsd-warning: #d97706; - --lsd-warning-text: #f59e0b; - --lsd-info: #2563eb; - --lsd-info-text: #2563eb; } @layer base { * { border-color: var(--lsd-border); - outline-color: var(--lsd-text-primary); + outline-color: var(--lsd-text-neutral); } body { - background-color: var(--background); - color: var(--foreground); + background-color: var(--lsd-background); + color: var(--lsd-text-neutral); } } diff --git a/packages/lsd/src/styles/sonner.css b/packages/lsd/src/styles/sonner.css index 0fa94ed6..e7fdac1d 100644 --- a/packages/lsd/src/styles/sonner.css +++ b/packages/lsd/src/styles/sonner.css @@ -1,20 +1,20 @@ [data-sonner-toaster], [data-sonner-toaster][data-style="true"] { - --normal-bg: var(--lsd-surface) !important; - --normal-text: var(--lsd-text-primary) !important; + --normal-bg: var(--lsd-foreground) !important; + --normal-text: var(--lsd-text-neutral) !important; --normal-border: var(--lsd-border) !important; - --success-bg: var(--lsd-surface) !important; + --success-bg: var(--lsd-foreground) !important; --success-border: var(--lsd-success) !important; - --success-text: var(--lsd-success-text) !important; - --error-bg: var(--lsd-surface) !important; + --success-text: var(--lsd-text-success) !important; + --error-bg: var(--lsd-foreground) !important; --error-border: var(--lsd-destructive) !important; - --error-text: var(--lsd-destructive-text) !important; - --warning-bg: var(--lsd-surface) !important; + --error-text: var(--lsd-text-destructive) !important; + --warning-bg: var(--lsd-foreground) !important; --warning-border: var(--lsd-warning) !important; - --warning-text: var(--lsd-warning-text) !important; - --info-bg: var(--lsd-surface) !important; + --warning-text: var(--lsd-text-warning) !important; + --info-bg: var(--lsd-foreground) !important; --info-border: var(--lsd-info) !important; - --info-text: var(--lsd-info-text) !important; + --info-text: var(--lsd-text-info) !important; font-family: var(--lsd-font-family) !important; } @@ -28,27 +28,27 @@ } [data-sonner-toaster] [data-sonner-toast] { - color: var(--lsd-text-primary); + color: var(--lsd-text-neutral); } [data-sonner-toaster] [data-sonner-toast][data-type="success"] { border-color: var(--lsd-success); - color: var(--lsd-success-text); + color: var(--lsd-text-success); } [data-sonner-toaster] [data-sonner-toast][data-type="error"] { border-color: var(--lsd-destructive); - color: var(--lsd-destructive-text); + color: var(--lsd-text-destructive); } [data-sonner-toaster] [data-sonner-toast][data-type="warning"] { border-color: var(--lsd-warning); - color: var(--lsd-warning-text); + color: var(--lsd-text-warning); } [data-sonner-toaster] [data-sonner-toast][data-type="info"] { border-color: var(--lsd-info); - color: var(--lsd-info-text); + color: var(--lsd-text-info); } [data-sonner-toaster] [data-sonner-toast] button { diff --git a/packages/lsd/src/styles/themes.css b/packages/lsd/src/styles/themes.css index 08751562..2afe0575 100644 --- a/packages/lsd/src/styles/themes.css +++ b/packages/lsd/src/styles/themes.css @@ -1,135 +1,196 @@ +[data-theme="monochrome"], +&:not([data-theme]) { + &, + &.light { + --lsd-primary: #000000; + --lsd-primary-content: #ffffff; + --lsd-text-neutral: #000000; + --lsd-text-secondary: #4a4a4a; + --lsd-background: #ffffff; + --lsd-foreground: #f8f8f8; + --lsd-border: #000000; + --lsd-destructive: #da0000; + --lsd-text-destructive: #c92d2d; + --lsd-success: #15803d; + --lsd-text-success: #1c9618; + --lsd-warning: #b45309; + --lsd-text-warning: #854d0e; + --lsd-info: #2563eb; + --lsd-text-info: #2563eb; + } + + &.dark, + .dark:not([data-theme]) { + --lsd-primary: #ffffff; + --lsd-primary-content: #000000; + --lsd-text-neutral: #ffffff; + --lsd-text-secondary: #d2d2d2; + --lsd-background: #0a0a0a; + --lsd-foreground: #191919; + --lsd-border: #ffffff; + --lsd-destructive: #e5484d; + --lsd-text-destructive: #f27a7a; + --lsd-success: #22c55e; + --lsd-text-success: #30c82a; + --lsd-warning: #d97706; + --lsd-text-warning: #f59e0b; + --lsd-info: #2563eb; + --lsd-text-info: #2563eb; + } +} + [data-theme="teal"] { &, &.light { --lsd-primary: #0d857e; - --lsd-text-primary: #103533; + --lsd-primary-content: #f8fcfb; + --lsd-text-neutral: #103533; --lsd-text-secondary: #3d6462; - --lsd-surface: #f2faf9; + --lsd-background: #f8fcfb; + --lsd-foreground: #f2faf9; --lsd-border: #9ec5c2; --lsd-destructive: #c4313e; - --lsd-destructive-text: #a22a35; + --lsd-text-destructive: #a22a35; --lsd-success: #18793a; - --lsd-success-text: #136530; + --lsd-text-success: #136530; --lsd-warning: #a16207; - --lsd-warning-text: #854d0e; + --lsd-text-warning: #854d0e; --lsd-info: #2760c4; - --lsd-info-text: #1e4d9e; + --lsd-text-info: #1e4d9e; + } + + &.dark { + --lsd-primary: #14b8a6; + --lsd-primary-content: #0f1615; + --lsd-text-neutral: #e8faf7; + --lsd-text-secondary: #8ac5bf; + --lsd-background: #0f1615; + --lsd-foreground: #092824; + --lsd-border: #1e6e67; + --lsd-destructive: #e5484d; + --lsd-text-destructive: #f27a7a; + --lsd-success: #22c55e; + --lsd-text-success: #4ade80; + --lsd-warning: #d97706; + --lsd-text-warning: #f59e0b; + --lsd-info: #3b82f6; + --lsd-text-info: #60a5fa; } -} -[data-theme="teal"].dark { - --lsd-primary: #14b8a6; - --lsd-text-primary: #e8faf7; - --lsd-text-secondary: #8ac5bf; - --lsd-surface: #071f1c; - --lsd-border: #1e6e67; - --lsd-destructive: #e5484d; - --lsd-destructive-text: #f27a7a; - --lsd-success: #22c55e; - --lsd-success-text: #4ade80; - --lsd-warning: #d97706; - --lsd-warning-text: #f59e0b; - --lsd-info: #3b82f6; - --lsd-info-text: #60a5fa; } [data-theme="nord"] { &, &.light { --lsd-primary: #3b6898; - --lsd-text-primary: #2e3440; + --lsd-primary-content: #e6e8eb; + --lsd-text-neutral: #2e3440; --lsd-text-secondary: #4c566a; - --lsd-surface: #eceff4; + --lsd-background: #e6e8eb; + --lsd-foreground: #eceff4; --lsd-border: #8e98aa; --lsd-destructive: #a3434d; - --lsd-destructive-text: #8c3841; + --lsd-text-destructive: #8c3841; --lsd-success: #4a7040; - --lsd-success-text: #3c5f33; + --lsd-text-success: #3c5f33; --lsd-warning: #a16207; - --lsd-warning-text: #7c5a12; + --lsd-text-warning: #7c5a12; --lsd-info: #2a6d86; - --lsd-info-text: #215a6f; + --lsd-text-info: #215a6f; + } + + &.dark { + --lsd-primary: #88c0d0; + --lsd-primary-content: #26313d; + --lsd-text-neutral: #eceff4; + --lsd-text-secondary: #8690a4; + --lsd-background: #26313d; + --lsd-foreground: #2e3440; + --lsd-border: #4c566a; + --lsd-destructive: #bf616a; + --lsd-text-destructive: #d4878e; + --lsd-success: #a3be8c; + --lsd-text-success: #b8ccaa; + --lsd-warning: #d08770; + --lsd-text-warning: #ebcb8b; + --lsd-info: #81a1c1; + --lsd-text-info: #9bb5cf; } -} -[data-theme="nord"].dark { - --lsd-primary: #88c0d0; - --lsd-text-primary: #eceff4; - --lsd-text-secondary: #8690a4; - --lsd-surface: #2e3440; - --lsd-border: #4c566a; - --lsd-destructive: #bf616a; - --lsd-destructive-text: #d4878e; - --lsd-success: #a3be8c; - --lsd-success-text: #b8ccaa; - --lsd-warning: #d08770; - --lsd-warning-text: #ebcb8b; - --lsd-info: #81a1c1; - --lsd-info-text: #9bb5cf; } [data-theme="terracotta"] { &, &.light { --lsd-primary: #b45a32; - --lsd-text-primary: #2a1c15; + --lsd-primary-content: #fefaf7; + --lsd-text-neutral: #2a1c15; --lsd-text-secondary: #6b5548; - --lsd-surface: #faf6f2; + --lsd-background: #fefaf7; + --lsd-foreground: #faf6f2; --lsd-border: #c8b5a6; --lsd-destructive: #b83a3a; - --lsd-destructive-text: #972f2f; + --lsd-text-destructive: #972f2f; --lsd-success: #4a7842; - --lsd-success-text: #3b6434; + --lsd-text-success: #3b6434; --lsd-warning: #9a7a18; - --lsd-warning-text: #7d6310; + --lsd-text-warning: #7d6310; --lsd-info: #30659e; - --lsd-info-text: #285485; + --lsd-text-info: #285485; + } + + &.dark { + --lsd-primary: #e0845a; + --lsd-primary-content: #16120f; + --lsd-text-neutral: #f5ebe4; + --lsd-text-secondary: #b09888; + --lsd-background: #16120f; + --lsd-foreground: #1c1410; + --lsd-border: #4d3c32; + --lsd-destructive: #e06060; + --lsd-text-destructive: #f08a8a; + --lsd-success: #6cb860; + --lsd-text-success: #92d486; + --lsd-warning: #d4a840; + --lsd-text-warning: #f0c860; + --lsd-info: #6a9fd8; + --lsd-text-info: #94bde6; } -} -[data-theme="terracotta"].dark { - --lsd-primary: #e0845a; - --lsd-text-primary: #f5ebe4; - --lsd-text-secondary: #b09888; - --lsd-surface: #1c1410; - --lsd-border: #4d3c32; - --lsd-destructive: #e06060; - --lsd-destructive-text: #f08a8a; - --lsd-success: #6cb860; - --lsd-success-text: #92d486; - --lsd-warning: #d4a840; - --lsd-warning-text: #f0c860; - --lsd-info: #6a9fd8; - --lsd-info-text: #94bde6; } [data-theme="catppuccin"] { &, &.light { --lsd-primary: #8839ef; - --lsd-text-primary: #4c4f69; + --lsd-primary-content: #f2f4f7; + --lsd-text-neutral: #4c4f69; --lsd-text-secondary: #6c6f85; - --lsd-surface: #eff1f5; + --lsd-background: #f2f4f7; + --lsd-foreground: #eff1f5; --lsd-border: #9ca0b0; --lsd-destructive: #d20f39; - --lsd-destructive-text: #a80c2e; + --lsd-text-destructive: #a80c2e; --lsd-success: #358923; - --lsd-success-text: #2b7320; + --lsd-text-success: #2b7320; --lsd-warning: #b07618; - --lsd-warning-text: #8f6012; + --lsd-text-warning: #8f6012; --lsd-info: #1e66f5; - --lsd-info-text: #1853c8; + --lsd-text-info: #1853c8; + } + + &.dark { + --lsd-primary: #cba6f7; + --lsd-primary-content: #181a24; + --lsd-text-neutral: #cdd6f4; + --lsd-text-secondary: #a6adc8; + --lsd-background: #181a24; + --lsd-foreground: #1e1e2e; + --lsd-border: #585b70; + --lsd-destructive: #f38ba8; + --lsd-text-destructive: #f38ba8; + --lsd-success: #a6e3a1; + --lsd-text-success: #a6e3a1; + --lsd-warning: #fab387; + --lsd-text-warning: #f9e2af; + --lsd-info: #89b4fa; + --lsd-text-info: #89b4fa; } -} -[data-theme="catppuccin"].dark { - --lsd-primary: #cba6f7; - --lsd-text-primary: #cdd6f4; - --lsd-text-secondary: #a6adc8; - --lsd-surface: #1e1e2e; - --lsd-border: #585b70; - --lsd-destructive: #f38ba8; - --lsd-destructive-text: #f38ba8; - --lsd-success: #a6e3a1; - --lsd-success-text: #a6e3a1; - --lsd-warning: #fab387; - --lsd-warning-text: #f9e2af; - --lsd-info: #89b4fa; - --lsd-info-text: #89b4fa; } From a9640b7d9e22d0c8ced466945eb0f06c065949a2 Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Sat, 2 May 2026 10:54:53 -0400 Subject: [PATCH 03/26] test: add comprehensive tests for theme switching and variants --- .../src/components/theme-switching.test.tsx | 1052 +++++++++++++++++ .../styles/__tests__/theme-variants.test.ts | 286 +++++ packages/lsd/src/styles/themes.test.ts | 350 ++++++ 3 files changed, 1688 insertions(+) create mode 100644 packages/lsd/src/components/theme-switching.test.tsx create mode 100644 packages/lsd/src/styles/__tests__/theme-variants.test.ts create mode 100644 packages/lsd/src/styles/themes.test.ts diff --git a/packages/lsd/src/components/theme-switching.test.tsx b/packages/lsd/src/components/theme-switching.test.tsx new file mode 100644 index 00000000..5c686ae3 --- /dev/null +++ b/packages/lsd/src/components/theme-switching.test.tsx @@ -0,0 +1,1052 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it } from 'vitest'; +import { Badge } from './ui/badge'; +import { Button } from './ui/button'; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from './ui/card'; + +afterEach(() => { + cleanup(); +}); + +describe('Theme Switching Tests', () => { + describe('Button component theme switching', () => { + it('renders with default theme (monochrome light)', () => { + render(); + const button = screen.getByTestId('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders in dark mode', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders with teal theme variant', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders with nord theme variant', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders with slate theme variant', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders with dark mode and nord theme', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders destructive variant in default theme', () => { + render( + + ); + const button = screen.getByTestId('button-destructive-default'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-destructive'); + }); + + it('renders destructive variant in teal theme', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button-destructive-teal'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-destructive'); + }); + + it('renders destructive variant in nord theme', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button-destructive-nord'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-destructive'); + }); + + it('renders destructive variant in slate theme', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button-destructive-slate'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-destructive'); + }); + + it('renders success variant in default dark mode', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button-success-dark'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-success'); + }); + + it('renders success variant in teal dark mode', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button-success-teal-dark'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-success'); + }); + + it('renders success variant in nord dark mode', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button-success-nord-dark'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-success'); + }); + + it('renders success variant in slate dark mode', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button-success-slate-dark'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-success'); + }); + + it('renders filled variant in teal theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-filled-teal')).toBeInTheDocument(); + }); + + it('renders outlined variant in teal theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-outlined-teal')).toBeInTheDocument(); + }); + + it('renders filled-rounded variant in teal theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-filled-rounded-teal')).toBeInTheDocument(); + }); + + it('renders outlined-rounded variant in teal theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-outlined-rounded-teal')).toBeInTheDocument(); + }); + + it('renders link variant in teal theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-link-teal')).toBeInTheDocument(); + }); + + it('renders ghost variant in teal theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-ghost-teal')).toBeInTheDocument(); + }); + + it('renders ghost-rounded variant in teal theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-ghost-rounded-teal')).toBeInTheDocument(); + }); + + it('renders destructive variant in nord dark mode', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button-destructive-nord-dark'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-destructive'); + }); + + it('renders success variant in nord dark mode', () => { + render( +
+ +
+ ); + const button = screen.getByTestId('button-success-nord-dark-2'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('lsd:bg-lsd-success'); + }); + + it('renders small size in default theme', () => { + render( + + ); + expect(screen.getByTestId('button-sm-default')).toBeInTheDocument(); + }); + + it('renders small size in teal theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-sm-teal')).toBeInTheDocument(); + }); + + it('renders medium size in nord theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-md-nord')).toBeInTheDocument(); + }); + + it('renders large size in slate theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-lg-slate')).toBeInTheDocument(); + }); + + it('renders square-sm size in teal theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-square-sm-teal')).toBeInTheDocument(); + }); + + it('renders square-md size in nord theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-square-md-nord')).toBeInTheDocument(); + }); + + it('renders square-lg size in slate theme', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('button-square-lg-slate')).toBeInTheDocument(); + }); + }); + + describe('Card component theme switching', () => { + it('renders with default theme (monochrome light)', () => { + render( + + + Test Title + + + ); + const card = screen.getByTestId('card'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('lsd:bg-lsd-foreground'); + expect(card).toHaveClass('lsd:border-lsd-border'); + }); + + it('renders in dark mode', () => { + render( +
+ + + Dark Mode Card + + +
+ ); + const card = screen.getByTestId('card'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('lsd:bg-lsd-foreground'); + expect(card).toHaveClass('lsd:border-lsd-border'); + }); + + it('renders with teal theme variant', () => { + render( +
+ + Teal Theme Content + +
+ ); + const card = screen.getByTestId('card'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('lsd:bg-lsd-foreground'); + }); + + it('renders with nord theme variant', () => { + render( +
+ + Nord Theme Content + +
+ ); + const card = screen.getByTestId('card'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('lsd:bg-lsd-foreground'); + }); + + it('renders with slate theme variant', () => { + render( +
+ + Slate Theme Content + +
+ ); + const card = screen.getByTestId('card'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('lsd:bg-lsd-foreground'); + }); + + it('renders with dark mode and slate theme', () => { + render( +
+ + + Dark Slate Card + Description + + Content + +
+ ); + const card = screen.getByTestId('card'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('lsd:bg-lsd-foreground'); + }); + + it('renders complete card structure in default theme', () => { + render( + + + Card Title + Card Description + + + + + Card Content + + + + + ); + const card = screen.getByTestId('card-default'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('lsd:bg-lsd-foreground'); + }); + + it('renders complete card structure in teal theme', () => { + render( +
+ + + Card Title + Card Description + + + + + Card Content + + + + +
+ ); + const card = screen.getByTestId('card-teal'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('lsd:bg-lsd-foreground'); + }); + + it('renders complete card structure in nord theme', () => { + render( +
+ + + Card Title + Card Description + + Card Content + +
+ ); + const card = screen.getByTestId('card-nord'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('lsd:bg-lsd-foreground'); + }); + + it('renders complete card structure in slate theme', () => { + render( +
+ + + Card Title + + Card Content + +
+ ); + const card = screen.getByTestId('card-slate'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('lsd:bg-lsd-foreground'); + }); + + it('renders card subcomponents in dark mode with nord theme', () => { + render( +
+ + + Title + Description + + Content + Footer + +
+ ); + expect(screen.getByTestId('card')).toBeInTheDocument(); + expect(screen.getByTestId('header')).toBeInTheDocument(); + expect(screen.getByTestId('content')).toBeInTheDocument(); + expect(screen.getByTestId('footer')).toBeInTheDocument(); + }); + + it('renders card with custom className in teal theme', () => { + render( +
+ + Content + +
+ ); + const card = screen.getByTestId('card-teal-custom'); + expect(card).toHaveClass('custom-class'); + }); + + it('renders card with custom className in nord theme', () => { + render( +
+ + Content + +
+ ); + const card = screen.getByTestId('card-nord-custom'); + expect(card).toHaveClass('custom-class'); + }); + }); + + describe('Badge component theme switching', () => { + it('renders with default theme (monochrome light)', () => { + render(Badge); + const badge = screen.getByTestId('badge'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders in dark mode', () => { + render( +
+ Dark Mode Badge +
+ ); + const badge = screen.getByTestId('badge'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders with teal theme variant', () => { + render( +
+ Teal Theme Badge +
+ ); + const badge = screen.getByTestId('badge'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders with nord theme variant', () => { + render( +
+ Nord Theme Badge +
+ ); + const badge = screen.getByTestId('badge'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders with slate theme', () => { + render( +
+ Slate Theme Badge +
+ ); + const badge = screen.getByTestId('badge'); + expect(badge).toBeInTheDocument(); + }); + + it('renders with dark mode and teal theme', () => { + render( +
+ Dark Teal Badge +
+ ); + const badge = screen.getByTestId('badge'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders filled variant in default theme', () => { + render( + + Filled + + ); + const badge = screen.getByTestId('badge-filled-default'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders filled variant in teal theme', () => { + render( +
+ + Filled + +
+ ); + const badge = screen.getByTestId('badge-filled-teal'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders filled variant in nord theme', () => { + render( +
+ + Filled + +
+ ); + const badge = screen.getByTestId('badge-filled-nord'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders filled variant in slate theme', () => { + render( +
+ + Filled + +
+ ); + const badge = screen.getByTestId('badge-filled-slate'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-lsd-primary'); + }); + + it('renders outlined variant in default dark mode', () => { + render( +
+ + Outlined + +
+ ); + const badge = screen.getByTestId('badge-outlined-dark'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-transparent'); + }); + + it('renders outlined variant in teal dark mode', () => { + render( +
+ + Outlined + +
+ ); + const badge = screen.getByTestId('badge-outlined-teal-dark'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-transparent'); + }); + + it('renders outlined variant in nord dark mode', () => { + render( +
+ + Outlined + +
+ ); + const badge = screen.getByTestId('badge-outlined-nord-dark'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:bg-transparent'); + }); + + it('renders small size in nord theme', () => { + render( +
+ + Small + +
+ ); + const badge = screen.getByTestId('badge-sm-nord'); + expect(badge).toBeInTheDocument(); + }); + + it('renders medium size in nord theme', () => { + render( +
+ + Medium + +
+ ); + const badge = screen.getByTestId('badge-md-nord'); + expect(badge).toBeInTheDocument(); + }); + + it('renders dot variant in default theme', () => { + render(); + const badge = screen.getByTestId('badge-dot-default'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:rounded-full'); + expect(badge).toHaveClass('lsd:p-0'); + }); + + it('renders dot variant in teal theme', () => { + render( +
+ +
+ ); + const badge = screen.getByTestId('badge-dot-teal'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:rounded-full'); + expect(badge).toHaveClass('lsd:p-0'); + }); + + it('renders dot variant in nord theme', () => { + render( +
+ +
+ ); + const badge = screen.getByTestId('badge-dot-nord'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:rounded-full'); + expect(badge).toHaveClass('lsd:p-0'); + }); + + it('renders dot variant in slate theme', () => { + render( +
+ +
+ ); + const badge = screen.getByTestId('badge-dot-slate'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass('lsd:rounded-full'); + expect(badge).toHaveClass('lsd:p-0'); + }); + + it('renders badge with icon in teal dark mode', () => { + render( +
+ ★} data-testid="badge-icon-teal"> + With Icon + +
+ ); + expect(screen.getByTestId('badge-icon-teal')).toBeInTheDocument(); + expect(screen.getByTestId('icon')).toBeInTheDocument(); + }); + + it('renders badge with icon in nord dark mode', () => { + render( +
+ ★} data-testid="badge-icon-nord"> + With Icon + +
+ ); + expect(screen.getByTestId('badge-icon-nord')).toBeInTheDocument(); + expect(screen.getByTestId('icon')).toBeInTheDocument(); + }); + + it('renders badge with icon in slate dark mode', () => { + render( +
+ ★} data-testid="badge-icon-slate"> + With Icon + +
+ ); + expect(screen.getByTestId('badge-icon-slate')).toBeInTheDocument(); + expect(screen.getByTestId('icon')).toBeInTheDocument(); + }); + + it('renders filled variant in slate dark mode', () => { + render( +
+ + Filled + +
+ ); + const badge = screen.getByTestId('badge-filled-slate-dark'); + expect(badge).toBeInTheDocument(); + }); + + it('renders outlined variant in slate dark mode', () => { + render( +
+ + Outlined + +
+ ); + const badge = screen.getByTestId('badge-outlined-slate-dark'); + expect(badge).toBeInTheDocument(); + }); + + it('renders dot variant in slate dark mode', () => { + render( +
+ +
+ ); + const badge = screen.getByTestId('badge-dot-slate-dark'); + expect(badge).toBeInTheDocument(); + }); + + it('renders clickable badge in default theme', () => { + render( + {}} data-testid="badge-clickable-default"> + Clickable + + ); + const badge = screen.getByTestId('badge-clickable-default'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveAttribute('role', 'button'); + }); + + it('renders clickable badge in teal theme', () => { + render( +
+ {}} data-testid="badge-clickable-teal"> + Clickable + +
+ ); + const badge = screen.getByTestId('badge-clickable-teal'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveAttribute('role', 'button'); + }); + + it('renders clickable badge in nord theme', () => { + render( +
+ {}} data-testid="badge-clickable-nord"> + Clickable + +
+ ); + const badge = screen.getByTestId('badge-clickable-nord'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveAttribute('role', 'button'); + }); + + it('renders clickable badge in slate theme', () => { + render( +
+ {}} data-testid="badge-clickable-slate"> + Clickable + +
+ ); + const badge = screen.getByTestId('badge-clickable-slate'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveAttribute('role', 'button'); + }); + }); + + describe('Cross-component theme consistency', () => { + it('renders Button, Card, and Badge together in teal theme', () => { + render( +
+ + + Card with Components + + + + Badge + + +
+ ); + expect(screen.getByTestId('card')).toBeInTheDocument(); + expect(screen.getByTestId('button')).toBeInTheDocument(); + expect(screen.getByTestId('badge')).toBeInTheDocument(); + }); + + it('renders all components in nord dark mode', () => { + render( +
+ + + Dark Nordic Card + + + + + Outlined Badge + + + +
+ ); + expect(screen.getByTestId('card')).toBeInTheDocument(); + expect(screen.getByTestId('button')).toBeInTheDocument(); + expect(screen.getByTestId('badge')).toBeInTheDocument(); + }); + + it('renders destructive button in default theme', () => { + render( + + ); + expect(screen.getByTestId('btn-destructive')).toHaveClass('lsd:bg-lsd-destructive'); + }); + + it('renders badge in default theme', () => { + render(Badge); + expect(screen.getByTestId('badge-default')).toBeInTheDocument(); + }); + + it('renders destructive button in teal theme', () => { + render( +
+ + Badge +
+ ); + expect(screen.getByTestId('btn-destructive-teal')).toHaveClass('lsd:bg-lsd-destructive'); + expect(screen.getByTestId('badge-teal')).toBeInTheDocument(); + }); + + it('renders destructive button in nord theme', () => { + render( +
+ + Badge +
+ ); + expect(screen.getByTestId('btn-destructive-nord')).toHaveClass('lsd:bg-lsd-destructive'); + expect(screen.getByTestId('badge-nord')).toBeInTheDocument(); + }); + + it('renders destructive button in slate theme', () => { + render( +
+ + Badge +
+ ); + expect(screen.getByTestId('btn-destructive-slate')).toHaveClass('lsd:bg-lsd-destructive'); + expect(screen.getByTestId('badge-slate')).toBeInTheDocument(); + }); + + it('handles theme switching from teal to nord', () => { + const { rerender } = render( +
+ + Teal Badge +
+ ); + expect(screen.getByTestId('button')).toBeInTheDocument(); + expect(screen.getByTestId('badge')).toBeInTheDocument(); + + rerender( +
+ + Nord Badge +
+ ); + expect(screen.getByTestId('button')).toBeInTheDocument(); + expect(screen.getByTestId('badge')).toBeInTheDocument(); + }); + + it('handles theme switching from light to dark mode', () => { + const { rerender } = render( +
+ + + Content + +
+ ); + expect(screen.getByTestId('button')).toBeInTheDocument(); + expect(screen.getByTestId('card')).toBeInTheDocument(); + + rerender( +
+ + + Content + +
+ ); + expect(screen.getByTestId('button')).toBeInTheDocument(); + expect(screen.getByTestId('card')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/lsd/src/styles/__tests__/theme-variants.test.ts b/packages/lsd/src/styles/__tests__/theme-variants.test.ts new file mode 100644 index 00000000..542142e7 --- /dev/null +++ b/packages/lsd/src/styles/__tests__/theme-variants.test.ts @@ -0,0 +1,286 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const themesCss = readFileSync(join(__dirname, '../themes.css'), 'utf-8'); + +interface ThemeVariables { + light: Record; + dark: Record; +} + +const EXPECTED_VARIABLES = [ + '--lsd-primary', + '--lsd-primary-content', + '--lsd-text-neutral', + '--lsd-text-secondary', + '--lsd-background', + '--lsd-foreground', + '--lsd-border', + '--lsd-destructive', + '--lsd-text-destructive', + '--lsd-success', + '--lsd-text-success', + '--lsd-warning', + '--lsd-text-warning', + '--lsd-info', + '--lsd-text-info', +]; + +const SEMANTIC_VARIABLES = [ + '--lsd-destructive', + '--lsd-text-destructive', + '--lsd-success', + '--lsd-text-success', + '--lsd-warning', + '--lsd-text-warning', + '--lsd-info', + '--lsd-text-info', +]; + +const TEXT_VARIABLES = ['--lsd-text-neutral', '--lsd-text-secondary']; + +const BACKGROUND_VARIABLES = ['--lsd-background', '--lsd-foreground']; + +function extractVariables(content: string): Record { + const vars: Record = {}; + const varRegex = /--lsd-[\w-]+:\s*([^;]+);/g; + let match: RegExpExecArray | null = varRegex.exec(content); + while (match !== null) { + vars[match[0].split(':')[0].trim()] = match[1].trim(); + match = varRegex.exec(content); + } + return vars; +} + +function parseThemeVariables(css: string, themeName: string): ThemeVariables { + const startPattern = `[data-theme="${themeName}"] {`; + const startIndex = css.indexOf(startPattern); + + if (startIndex === -1) { + return { light: {}, dark: {} }; + } + + let currentIndex = startIndex + startPattern.length; + let braceCount = 1; + let themeContent = ''; + + while (currentIndex < css.length && braceCount > 0) { + const char = css[currentIndex]; + if (char === '{') braceCount++; + else if (char === '}') braceCount--; + + if (braceCount > 0) { + themeContent += char; + } + currentIndex++; + } + + const lightVars: Record = {}; + const darkVars: Record = {}; + + const lightMatch = themeContent.match(/&,\s*&\.light\s*\{([^}]+)\}/s); + const darkMatch = themeContent.match(/&\.dark\s*\{([^}]+)\}/s); + + if (lightMatch) { + Object.assign(lightVars, extractVariables(lightMatch[1])); + } + + if (darkMatch) { + Object.assign(darkVars, extractVariables(darkMatch[1])); + } + + return { light: lightVars, dark: darkVars }; +} + +function parseMonochromeTheme(css: string): ThemeVariables { + const startPattern = `[data-theme="monochrome"]`; + const endPattern = `[data-theme="teal"]`; + + const startIndex = css.indexOf(startPattern); + const endIndex = css.indexOf(endPattern); + + if (startIndex === -1) { + return { light: {}, dark: {} }; + } + + const content = css.slice(startIndex, endIndex !== -1 ? endIndex : css.length); + + const lightVars: Record = {}; + const darkVars: Record = {}; + + const lightMatch = content.match(/&,\s*&\.light\s*\{([^}]+)\}/s); + const darkMatch = content.match(/&\.dark,\s*\.dark[^}]*\{([^}]+)\}/s); + + if (lightMatch) { + Object.assign(lightVars, extractVariables(lightMatch[1])); + } + + if (darkMatch) { + Object.assign(darkVars, extractVariables(darkMatch[1])); + } + + return { light: lightVars, dark: darkVars }; +} + +const THEME_NAMES = ['monochrome', 'teal', 'nord', 'terracotta', 'catppuccin'] as const; + +function getThemes(): Record { + return { + monochrome: parseMonochromeTheme(themesCss), + teal: parseThemeVariables(themesCss, 'teal'), + nord: parseThemeVariables(themesCss, 'nord'), + terracotta: parseThemeVariables(themesCss, 'terracotta'), + catppuccin: parseThemeVariables(themesCss, 'catppuccin'), + }; +} + +describe('theme-variants', () => { + describe('theme definitions', () => { + it.each(THEME_NAMES)('should have %s theme defined', themeName => { + const themeRegex = new RegExp(`\\[data-theme="${themeName}"\\]`); + expect(themesCss).toMatch(themeRegex); + }); + + it.each(THEME_NAMES)('should have light mode selector for %s theme', themeName => { + if (themeName === 'monochrome') { + expect(themesCss).toMatch(/\[data-theme="monochrome"]/); + expect(themesCss).toMatch(/&\.light/); + } else { + const lightRegex = new RegExp(`\\[data-theme="${themeName}"\\][\\s\\S]*?&\\.light`); + expect(themesCss).toMatch(lightRegex); + } + }); + + it.each(THEME_NAMES)('should have dark mode selector for %s theme', themeName => { + if (themeName === 'monochrome') { + expect(themesCss).toMatch(/&\.dark/); + } else { + const darkRegex = new RegExp(`\\[data-theme="${themeName}"\\][\\s\\S]*?&\\.dark`); + expect(themesCss).toMatch(darkRegex); + } + }); + }); + + describe('expected CSS custom properties', () => { + const themes = getThemes(); + + it.each(THEME_NAMES)('should have all expected variables in %s light mode', themeName => { + const vars = themes[themeName].light; + EXPECTED_VARIABLES.forEach(varName => { + expect(vars[varName]).toBeDefined(); + expect(vars[varName]).toMatch(/^#[0-9a-fA-F]{6}$/); + }); + }); + + it.each(THEME_NAMES)('should have all expected variables in %s dark mode', themeName => { + const vars = themes[themeName].dark; + EXPECTED_VARIABLES.forEach(varName => { + expect(vars[varName]).toBeDefined(); + expect(vars[varName]).toMatch(/^#[0-9a-fA-F]{6}$/); + }); + }); + }); + + describe('semantic colors', () => { + const themes = getThemes(); + + it.each(THEME_NAMES)('should have semantic colors defined in %s light mode', themeName => { + const vars = themes[themeName].light; + SEMANTIC_VARIABLES.forEach(varName => { + expect(vars[varName]).toBeDefined(); + }); + }); + + it.each(THEME_NAMES)('should have semantic colors defined in %s dark mode', themeName => { + const vars = themes[themeName].dark; + SEMANTIC_VARIABLES.forEach(varName => { + expect(vars[varName]).toBeDefined(); + }); + }); + }); + + describe('text colors', () => { + const themes = getThemes(); + + it.each(THEME_NAMES)('should have text colors defined in %s light mode', themeName => { + const vars = themes[themeName].light; + TEXT_VARIABLES.forEach(varName => { + expect(vars[varName]).toBeDefined(); + }); + }); + + it.each(THEME_NAMES)('should have text colors defined in %s dark mode', themeName => { + const vars = themes[themeName].dark; + TEXT_VARIABLES.forEach(varName => { + expect(vars[varName]).toBeDefined(); + }); + }); + }); + + describe('background/foreground variables', () => { + const themes = getThemes(); + + it.each(THEME_NAMES)('should have background/foreground in %s light mode', themeName => { + const vars = themes[themeName].light; + BACKGROUND_VARIABLES.forEach(varName => { + expect(vars[varName]).toBeDefined(); + }); + }); + + it.each(THEME_NAMES)('should have background/foreground in %s dark mode', themeName => { + const vars = themes[themeName].dark; + BACKGROUND_VARIABLES.forEach(varName => { + expect(vars[varName]).toBeDefined(); + }); + }); + }); + + describe('theme uniqueness', () => { + const themes = getThemes(); + + it('should have different primary colors between themes', () => { + const primaryColors = THEME_NAMES.map(name => themes[name].light['--lsd-primary']); + const uniqueColors = new Set(primaryColors); + expect(uniqueColors.size).toBeGreaterThan(1); + }); + + it('should have different background colors between themes', () => { + const bgColors = THEME_NAMES.map(name => themes[name].light['--lsd-background']); + const uniqueColors = new Set(bgColors); + expect(uniqueColors.size).toBeGreaterThan(1); + }); + + it('should have different primary colors in dark mode between themes', () => { + const primaryColors = THEME_NAMES.map(name => themes[name].dark['--lsd-primary']); + const uniqueColors = new Set(primaryColors); + expect(uniqueColors.size).toBeGreaterThan(1); + }); + + it('should have different text-neutral colors between themes', () => { + const textColors = THEME_NAMES.map(name => themes[name].light['--lsd-text-neutral']); + const uniqueColors = new Set(textColors); + expect(uniqueColors.size).toBeGreaterThan(1); + }); + }); + + describe('light/dark mode differences', () => { + const themes = getThemes(); + + it.each( + THEME_NAMES + )('should have different background colors between light/dark in %s', themeName => { + const lightBg = themes[themeName].light['--lsd-background']; + const darkBg = themes[themeName].dark['--lsd-background']; + expect(lightBg).not.toBe(darkBg); + }); + + it.each( + THEME_NAMES + )('should have different text-neutral between light/dark in %s', themeName => { + const lightText = themes[themeName].light['--lsd-text-neutral']; + const darkText = themes[themeName].dark['--lsd-text-neutral']; + expect(lightText).not.toBe(darkText); + }); + }); +}); diff --git a/packages/lsd/src/styles/themes.test.ts b/packages/lsd/src/styles/themes.test.ts new file mode 100644 index 00000000..1c89018e --- /dev/null +++ b/packages/lsd/src/styles/themes.test.ts @@ -0,0 +1,350 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { beforeAll, describe, expect, it } from 'vitest'; + +const THEMES_CSS_PATH = path.resolve(__dirname, 'themes.css'); + +const EXPECTED_THEMES = ['monochrome', 'teal', 'nord', 'terracotta', 'catppuccin'] as const; + +const EXPECTED_CSS_VARIABLES = [ + '--lsd-primary', + '--lsd-primary-content', + '--lsd-text-neutral', + '--lsd-text-secondary', + '--lsd-background', + '--lsd-foreground', + '--lsd-border', + '--lsd-destructive', + '--lsd-text-destructive', + '--lsd-success', + '--lsd-text-success', + '--lsd-warning', + '--lsd-text-warning', + '--lsd-info', + '--lsd-text-info', +] as const; + +type ThemeName = (typeof EXPECTED_THEMES)[number]; + +function extractThemeBlock(cssContent: string, theme: ThemeName): string | null { + const startMarker = `[data-theme="${theme}"]`; + const startIndex = cssContent.indexOf(startMarker); + + if (startIndex === -1) return null; + + let braceCount = 0; + let inBlock = false; + let endIndex = startIndex; + + for (let i = startIndex; i < cssContent.length; i++) { + if (cssContent[i] === '{') { + braceCount++; + inBlock = true; + } else if (cssContent[i] === '}') { + braceCount--; + if (braceCount === 0 && inBlock) { + endIndex = i + 1; + break; + } + } + } + + const fullThemeBlock = cssContent.slice(startIndex, endIndex); + + return fullThemeBlock; +} + +function extractVariablesFromBlock(block: string, mode: 'light' | 'dark'): Map { + const variables = new Map(); + + let modeStartIndex = -1; + + if (mode === 'light') { + const lightRegex = /&,\s*&\.light\s*\{/; + const match = lightRegex.exec(block); + if (match) { + modeStartIndex = match.index + match[0].length; + } + } else { + const darkRegex = /&\.dark[\s,]/; + const match = darkRegex.exec(block); + if (match) { + modeStartIndex = match.index + match[0].length; + } + } + + if (modeStartIndex === -1) return variables; + + let braceCount = 1; + let endIndex = modeStartIndex; + + for (let i = modeStartIndex; i < block.length; i++) { + if (block[i] === '{') { + braceCount++; + } else if (block[i] === '}') { + braceCount--; + if (braceCount === 0) { + endIndex = i; + break; + } + } + } + + const modeBlock = block.slice(modeStartIndex, endIndex); + + const varRegex = /(--lsd-[a-z-]+):\s*([^;]+);/g; + let varMatch: RegExpExecArray | null = varRegex.exec(modeBlock); + + while (varMatch !== null) { + variables.set(varMatch[1], varMatch[2].trim()); + varMatch = varRegex.exec(modeBlock); + } + + return variables; +} + +describe('themes.css', () => { + let cssContent: string; + + beforeAll(() => { + cssContent = fs.readFileSync(THEMES_CSS_PATH, 'utf-8'); + }); + + describe('file structure', () => { + it('file exists', () => { + expect(fs.existsSync(THEMES_CSS_PATH)).toBe(true); + }); + + it('file is not empty', () => { + expect(cssContent.trim().length).toBeGreaterThan(0); + }); + + it('file contains valid CSS syntax (balanced braces)', () => { + const openBraces = (cssContent.match(/\{/g) || []).length; + const closeBraces = (cssContent.match(/\}/g) || []).length; + + expect(openBraces).toBe(closeBraces); + expect(openBraces).toBeGreaterThan(0); + }); + }); + + describe('theme definitions', () => { + it('contains all 5 expected themes', () => { + for (const theme of EXPECTED_THEMES) { + const themeRegex = new RegExp(`\\[data-theme="${theme}"\\]`); + expect(cssContent).toMatch(themeRegex); + } + }); + + it('does not contain unexpected themes', () => { + const allThemeMatches = cssContent.match(/\[data-theme="[^"]+"\]/g) || []; + const foundThemes = allThemeMatches.map(match => match.match(/"([^"]+)"/)?.[1]); + + for (const foundTheme of foundThemes) { + if (foundTheme) { + expect(EXPECTED_THEMES).toContain(foundTheme as ThemeName); + } + } + }); + + it('each theme has both light and dark mode selectors', () => { + for (const theme of EXPECTED_THEMES) { + const themeBlock = extractThemeBlock(cssContent, theme); + + expect(themeBlock).not.toBeNull(); + expect(themeBlock).toMatch(/&,\s*&\.light/); + expect(themeBlock).toContain('&.dark'); + } + }); + }); + + describe('CSS variables', () => { + it('all themes define all expected variables in light mode', () => { + for (const theme of EXPECTED_THEMES) { + const themeBlock = extractThemeBlock(cssContent, theme); + + expect(themeBlock, `Theme ${theme} block should be extractable`).not.toBeNull(); + + const lightVars = extractVariablesFromBlock(themeBlock!, 'light'); + + for (const variable of EXPECTED_CSS_VARIABLES) { + expect(lightVars.has(variable), `${theme} light mode should have ${variable}`).toBe(true); + } + } + }); + + it('all themes define all expected variables in dark mode', () => { + for (const theme of EXPECTED_THEMES) { + const themeBlock = extractThemeBlock(cssContent, theme); + + expect(themeBlock, `Theme ${theme} block should be extractable`).not.toBeNull(); + + const darkVars = extractVariablesFromBlock(themeBlock!, 'dark'); + + for (const variable of EXPECTED_CSS_VARIABLES) { + expect(darkVars.has(variable), `${theme} dark mode should have ${variable}`).toBe(true); + } + } + }); + + it('light and dark mode have different values for primary color', () => { + for (const theme of EXPECTED_THEMES) { + const themeBlock = extractThemeBlock(cssContent, theme); + + expect(themeBlock).not.toBeNull(); + + const lightVars = extractVariablesFromBlock(themeBlock!, 'light'); + const darkVars = extractVariablesFromBlock(themeBlock!, 'dark'); + + const lightPrimary = lightVars.get('--lsd-primary'); + const darkPrimary = darkVars.get('--lsd-primary'); + + expect(lightPrimary).toBeDefined(); + expect(darkPrimary).toBeDefined(); + expect(lightPrimary).not.toBe(darkPrimary); + } + }); + + it('light and dark mode have different values for background color', () => { + for (const theme of EXPECTED_THEMES) { + const themeBlock = extractThemeBlock(cssContent, theme); + + expect(themeBlock).not.toBeNull(); + + const lightVars = extractVariablesFromBlock(themeBlock!, 'light'); + const darkVars = extractVariablesFromBlock(themeBlock!, 'dark'); + + const lightBackground = lightVars.get('--lsd-background'); + const darkBackground = darkVars.get('--lsd-background'); + + expect(lightBackground).toBeDefined(); + expect(darkBackground).toBeDefined(); + expect(lightBackground).not.toBe(darkBackground); + } + }); + + it('all color values are valid hex colors', () => { + const hexColorRegex = /^#[0-9a-fA-F]{6}$/; + + for (const theme of EXPECTED_THEMES) { + const themeBlock = extractThemeBlock(cssContent, theme); + + expect(themeBlock).not.toBeNull(); + + const lightVars = extractVariablesFromBlock(themeBlock!, 'light'); + const darkVars = extractVariablesFromBlock(themeBlock!, 'dark'); + + for (const variable of EXPECTED_CSS_VARIABLES) { + const lightValue = lightVars.get(variable); + const darkValue = darkVars.get(variable); + + expect(lightValue).toMatch(hexColorRegex); + expect(darkValue).toMatch(hexColorRegex); + } + } + }); + }); + + describe('data-theme attribute selectors', () => { + it('uses data-theme attribute for theme selection', () => { + for (const theme of EXPECTED_THEMES) { + expect(cssContent).toContain(`[data-theme="${theme}"]`); + } + }); + + it('monochrome theme handles missing data-theme attribute', () => { + expect(cssContent).toContain(':not([data-theme])'); + }); + }); + + describe('light/dark class selectors', () => { + it('light mode is activated with .light class', () => { + for (const theme of EXPECTED_THEMES) { + const themeBlock = extractThemeBlock(cssContent, theme); + + expect(themeBlock).not.toBeNull(); + expect(themeBlock).toMatch(/&\.light/); + } + }); + + it('dark mode is activated with .dark class', () => { + for (const theme of EXPECTED_THEMES) { + const themeBlock = extractThemeBlock(cssContent, theme); + + expect(themeBlock).not.toBeNull(); + expect(themeBlock).toContain('&.dark'); + } + }); + + it('default mode (no class) falls back to light mode', () => { + for (const theme of EXPECTED_THEMES) { + const themeBlock = extractThemeBlock(cssContent, theme); + + expect(themeBlock).not.toBeNull(); + + expect(themeBlock).toMatch(/&,\s*&\.light/); + } + }); + }); + + describe('theme-specific color values', () => { + it('each theme has unique primary color in light mode', () => { + const primaryColors = new Set(); + + for (const theme of EXPECTED_THEMES) { + const themeBlock = extractThemeBlock(cssContent, theme); + + expect(themeBlock).not.toBeNull(); + + const lightVars = extractVariablesFromBlock(themeBlock!, 'light'); + const primary = lightVars.get('--lsd-primary'); + + expect(primary).toBeDefined(); + primaryColors.add(primary!); + } + + expect(primaryColors.size).toBe(EXPECTED_THEMES.length); + }); + + it('each theme has unique primary color in dark mode', () => { + const primaryColors = new Set(); + + for (const theme of EXPECTED_THEMES) { + const themeBlock = extractThemeBlock(cssContent, theme); + + expect(themeBlock).not.toBeNull(); + + const darkVars = extractVariablesFromBlock(themeBlock!, 'dark'); + const primary = darkVars.get('--lsd-primary'); + + expect(primary).toBeDefined(); + primaryColors.add(primary!); + } + + expect(primaryColors.size).toBe(EXPECTED_THEMES.length); + }); + }); + + describe('negative cases', () => { + it('does not use invalid CSS variable names', () => { + const invalidVarRegex = /--[a-z]*[A-Z][a-z-]*:/; + + expect(cssContent).not.toMatch(invalidVarRegex); + }); + + it('does not have empty variable declarations', () => { + const emptyVarRegex = /--lsd-[a-z-]+:\s*;/; + + expect(cssContent).not.toMatch(emptyVarRegex); + }); + + it('does not have duplicate theme definitions', () => { + for (const theme of EXPECTED_THEMES) { + const themeRegex = new RegExp(`\\[data-theme="${theme}"\\]`, 'g'); + const matches = cssContent.match(themeRegex) || []; + + expect(matches.length).toBe(1); + } + }); + }); +}); From 85ea93fcae6195c153e458a8140e0171924fcd0c Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Sat, 2 May 2026 10:54:53 -0400 Subject: [PATCH 04/26] refactor(components): update all components to use new theme variable names --- .../ui/accordion/AccordionTrigger.tsx | 2 - .../lsd/src/components/ui/accordion/types.ts | 8 +-- .../ui/alert-dialog/AlertDialogContent.tsx | 2 +- .../ui/alert-dialog/AlertDialogTitle.tsx | 2 +- .../__tests__/alert-dialog-content.test.tsx | 2 +- .../lsd/src/components/ui/alert/Alert.tsx | 10 +-- .../components/ui/alert/AlertDescription.tsx | 8 +-- .../ui/autocomplete/Autocomplete.tsx | 16 ++--- .../ui/autocomplete/AutocompleteList.tsx | 6 +- .../__tests__/autocomplete.test.tsx | 12 +++- .../src/components/ui/avatar/AvatarBadge.tsx | 4 +- .../components/ui/avatar/AvatarFallback.tsx | 2 +- .../src/components/ui/avatar/AvatarGroup.tsx | 2 +- .../components/ui/avatar/AvatarGroupCount.tsx | 2 +- .../lsd/src/components/ui/badge/Badge.tsx | 2 +- .../src/components/ui/badge/badge.test.tsx | 6 +- packages/lsd/src/components/ui/badge/types.ts | 13 ++-- .../ui/button-group/ButtonGroupSeparator.tsx | 2 +- .../ui/button-group/ButtonGroupText.tsx | 2 +- .../__tests__/button-group-separator.test.tsx | 2 +- .../__tests__/button-group-text.test.tsx | 2 +- .../__tests__/button-group.test.tsx | 18 ++--- .../src/components/ui/button-group/types.ts | 6 +- .../lsd/src/components/ui/button/Button.tsx | 2 - .../src/components/ui/button/button.test.tsx | 69 ++++++------------- .../lsd/src/components/ui/button/types.ts | 26 ++++--- .../src/components/ui/calendar/Calendar.tsx | 8 +-- .../ui/calendar/CalendarDayButton.tsx | 12 ++-- .../ui/calendar/__tests__/calendar.test.tsx | 12 ---- packages/lsd/src/components/ui/card/Card.tsx | 3 +- .../ui/card/__tests__/card.test.tsx | 3 +- .../src/components/ui/checkbox/Checkbox.tsx | 6 +- .../components/ui/checkbox/checkbox.test.tsx | 2 +- .../lsd/src/components/ui/command/Command.tsx | 3 +- .../components/ui/command/CommandDialog.tsx | 2 - .../components/ui/command/CommandGroup.tsx | 2 - .../src/components/ui/command/CommandItem.tsx | 4 +- .../components/ui/command/CommandShortcut.tsx | 2 - .../command/__tests__/command-group.test.tsx | 1 - .../command/__tests__/command-item.test.tsx | 3 +- .../__tests__/command-shortcut.test.tsx | 1 - .../ui/command/__tests__/command.test.tsx | 3 +- .../components/ui/dialog/DialogContent.tsx | 5 +- .../ui/dialog/DialogDescription.tsx | 2 +- .../dialog/__tests__/dialog-content.test.tsx | 2 +- .../__tests__/dialog-description.test.tsx | 1 - .../DropdownMenuCheckboxItem.tsx | 2 - .../ui/dropdown-menu/DropdownMenuContent.tsx | 3 +- .../ui/dropdown-menu/DropdownMenuItem.tsx | 8 +-- .../ui/dropdown-menu/DropdownMenuLabel.tsx | 2 - .../dropdown-menu/DropdownMenuRadioItem.tsx | 2 - .../dropdown-menu/DropdownMenuSubContent.tsx | 3 +- .../dropdown-menu/DropdownMenuSubTrigger.tsx | 5 -- .../ui/dropdown-menu/dropdown-menu.test.tsx | 12 ++-- .../src/components/ui/field/FieldError.tsx | 2 +- .../src/components/ui/field/FieldLabel.tsx | 2 - .../src/components/ui/field/FieldLegend.tsx | 2 +- .../src/components/ui/field/field.test.tsx | 4 +- .../lsd/src/components/ui/form/FormLabel.tsx | 2 +- .../src/components/ui/form/FormMessage.tsx | 4 +- .../lsd/src/components/ui/form/form.test.tsx | 8 ++- .../components/ui/input-group/InputGroup.tsx | 2 - .../ui/input-group/InputGroupText.tsx | 4 +- .../lsd/src/components/ui/input/Input.tsx | 6 +- .../src/components/ui/input/input.test.tsx | 15 ++-- .../src/components/ui/label/label.test.tsx | 8 +-- packages/lsd/src/components/ui/label/types.ts | 2 +- .../ui/menubar/__tests__/menubar.test.tsx | 1 - .../lsd/src/components/ui/menubar/types.ts | 18 ++--- .../navigation-menu/NavigationMenuContent.tsx | 53 ++++++-------- .../ui/navigation-menu/NavigationMenuLink.tsx | 27 +++----- .../ui/navigation-menu/NavigationMenuList.tsx | 2 +- .../navigation-menu/NavigationMenuTrigger.tsx | 7 +- .../NavigationMenuViewport.tsx | 3 +- .../__tests__/navigation-menu-link.test.tsx | 19 ++--- .../navigation-menu/navigation-menu.test.tsx | 20 +++--- .../components/ui/popover/PopoverContent.tsx | 3 +- .../__tests__/popover-content.test.tsx | 4 +- .../src/components/ui/progress/Progress.tsx | 6 +- .../components/ui/progress/progress.test.tsx | 3 - .../components/ui/scroll-area/ScrollArea.tsx | 2 +- .../components/ui/select/SelectContent.tsx | 3 +- .../src/components/ui/select/SelectItem.tsx | 6 +- .../ui/select/SelectScrollDownButton.tsx | 2 - .../ui/select/SelectScrollUpButton.tsx | 2 - .../components/ui/select/SelectTrigger.tsx | 9 +-- .../select/__tests__/select-content.test.tsx | 5 +- .../ui/select/__tests__/select-item.test.tsx | 1 + .../src/components/ui/separator/Separator.tsx | 2 +- .../ui/separator/separator.test.tsx | 2 +- .../src/components/ui/sheet/SheetContent.tsx | 8 ++- .../src/components/ui/sheet/SheetHeader.tsx | 5 +- .../src/components/ui/sheet/SheetTitle.tsx | 2 +- .../ui/sheet/__tests__/sheet-content.test.tsx | 2 +- .../ui/sheet/__tests__/sheet-header.test.tsx | 1 - .../ui/sheet/__tests__/sheet-title.test.tsx | 1 - .../lsd/src/components/ui/sidebar/Sidebar.tsx | 10 +-- .../components/ui/sidebar/SidebarContent.tsx | 6 +- .../components/ui/sidebar/SidebarContext.tsx | 2 +- .../components/ui/sidebar/SidebarGroup.tsx | 4 +- .../src/components/ui/sidebar/SidebarMenu.tsx | 18 ++--- .../__tests__/sidebar-group-action.test.tsx | 8 ++- .../__tests__/sidebar-group-label.test.tsx | 3 +- .../ui/sidebar/__tests__/sidebar.test.tsx | 14 ++-- .../lsd/src/components/ui/sidebar/types.ts | 7 +- .../lsd/src/components/ui/slider/Slider.tsx | 2 +- .../ui/slider/__tests__/slider.test.tsx | 2 +- .../lsd/src/components/ui/sonner/Sonner.tsx | 20 +++--- .../src/components/ui/sonner/sonner.test.tsx | 4 +- .../lsd/src/components/ui/switch/Switch.tsx | 4 +- .../src/components/ui/switch/switch.test.tsx | 4 +- .../src/components/ui/table/TableFooter.tsx | 2 +- .../lsd/src/components/ui/table/TableHead.tsx | 2 +- .../lsd/src/components/ui/table/TableRow.tsx | 2 +- .../ui/table/__tests__/table.test.tsx | 9 +-- .../src/components/ui/tabs/TabsContent.tsx | 2 +- .../ui/tabs/__tests__/tabs-list.test.tsx | 1 - .../ui/tabs/__tests__/tabs-trigger.test.tsx | 2 - .../ui/tabs/__tests__/tabs.test.tsx | 9 ++- packages/lsd/src/components/ui/tabs/types.ts | 4 +- .../src/components/ui/toggle/toggle.test.tsx | 8 +-- .../lsd/src/components/ui/toggle/types.ts | 2 +- .../components/ui/tooltip/TooltipContent.tsx | 3 +- .../__tests__/tooltip-content.test.tsx | 4 +- .../lsd/src/components/ui/typography/types.ts | 10 +-- .../ui/typography/typography.test.tsx | 24 +++---- 126 files changed, 332 insertions(+), 482 deletions(-) diff --git a/packages/lsd/src/components/ui/accordion/AccordionTrigger.tsx b/packages/lsd/src/components/ui/accordion/AccordionTrigger.tsx index e5c5495c..8354aa85 100644 --- a/packages/lsd/src/components/ui/accordion/AccordionTrigger.tsx +++ b/packages/lsd/src/components/ui/accordion/AccordionTrigger.tsx @@ -65,8 +65,6 @@ function AccordionTrigger({ className, children, size = 'md', ...props }: Accord // Transitions & Animations 'lsd:transition-transform', 'lsd:duration-200', - // Colors & Backgrounds - 'lsd:text-lsd-icon-primary', // Layout & Positioning 'lsd:shrink-0', // Spacing diff --git a/packages/lsd/src/components/ui/accordion/types.ts b/packages/lsd/src/components/ui/accordion/types.ts index 13f0fbff..9f5ec814 100644 --- a/packages/lsd/src/components/ui/accordion/types.ts +++ b/packages/lsd/src/components/ui/accordion/types.ts @@ -148,9 +148,6 @@ export interface AccordionItemProps export const accordionTriggerVariants = cva( [ - // Colors & Backgrounds - 'lsd:text-lsd-text-primary', - 'lsd:bg-lsd-surface', // Layout & Positioning 'lsd:flex', 'lsd:flex-1', @@ -169,7 +166,7 @@ export const accordionTriggerVariants = cva( 'lsd:cursor-pointer', 'lsd:hover:underline', 'lsd:outline-none', - 'focus-visible:lsd:ring-lsd-text/50', + 'focus-visible:lsd:ring-lsd-text-neutral/50', 'focus-visible:lsd:ring-[3px]', 'focus-visible:lsd:border-lsd-border', 'lsd:disabled:pointer-events-none', @@ -209,9 +206,6 @@ export const accordionTriggerVariants = cva( export const accordionContentVariants = cva( [ - // Colors & Backgrounds - 'lsd:text-lsd-text-primary', - 'lsd:bg-lsd-surface', // Transitions & Animations 'lsd:data-[state=closed]:animate-accordion-up', 'lsd:data-[state=open]:animate-accordion-down', diff --git a/packages/lsd/src/components/ui/alert-dialog/AlertDialogContent.tsx b/packages/lsd/src/components/ui/alert-dialog/AlertDialogContent.tsx index 53dc5929..ab8ebfa0 100644 --- a/packages/lsd/src/components/ui/alert-dialog/AlertDialogContent.tsx +++ b/packages/lsd/src/components/ui/alert-dialog/AlertDialogContent.tsx @@ -15,7 +15,7 @@ function AlertDialogContent({ data-slot="alert-dialog-content" className={cn( // Colors & Backgrounds - 'lsd:bg-lsd-surface', + 'lsd:bg-lsd-foreground', // Layout & Positioning 'lsd:fixed', 'lsd:top-[50%]', diff --git a/packages/lsd/src/components/ui/alert-dialog/AlertDialogTitle.tsx b/packages/lsd/src/components/ui/alert-dialog/AlertDialogTitle.tsx index ec5d6085..bc7c3225 100644 --- a/packages/lsd/src/components/ui/alert-dialog/AlertDialogTitle.tsx +++ b/packages/lsd/src/components/ui/alert-dialog/AlertDialogTitle.tsx @@ -10,7 +10,7 @@ function AlertDialogTitle({ return ( ); diff --git a/packages/lsd/src/components/ui/alert-dialog/__tests__/alert-dialog-content.test.tsx b/packages/lsd/src/components/ui/alert-dialog/__tests__/alert-dialog-content.test.tsx index 302ee7bd..cbad1490 100644 --- a/packages/lsd/src/components/ui/alert-dialog/__tests__/alert-dialog-content.test.tsx +++ b/packages/lsd/src/components/ui/alert-dialog/__tests__/alert-dialog-content.test.tsx @@ -23,7 +23,7 @@ describe('AlertDialogContent', () => { ); const content = document.querySelector('[data-slot="alert-dialog-content"]'); - expect(content).toHaveClass('lsd:bg-lsd-surface'); + expect(content).toHaveClass('lsd:bg-lsd-foreground'); expect(content).toHaveClass('lsd:fixed'); expect(content).toHaveClass('lsd:top-[50%]'); expect(content).toHaveClass('lsd:left-[50%]'); diff --git a/packages/lsd/src/components/ui/alert/Alert.tsx b/packages/lsd/src/components/ui/alert/Alert.tsx index 0f62e2e8..f174b1bb 100644 --- a/packages/lsd/src/components/ui/alert/Alert.tsx +++ b/packages/lsd/src/components/ui/alert/Alert.tsx @@ -39,14 +39,14 @@ const alertVariants = cva( { variants: { variant: { - default: 'lsd:bg-lsd-surface lsd:text-lsd-text-primary lsd:border-lsd-border', + default: 'lsd:border-lsd-border', destructive: - 'lsd:bg-lsd-surface lsd:text-lsd-destructive-text lsd:border-lsd-destructive [&>svg]:lsd:text-current data-[variant=destructive]>*:data-[slot=alert-description]:lsd:text-lsd-destructive-text', - info: 'lsd:bg-lsd-surface lsd:text-lsd-info-text lsd:border-lsd-info [&>svg]:lsd:text-current data-[variant=info]>*:data-[slot=alert-description]:lsd:text-lsd-info-text', + 'lsd:text-lsd-text-destructive lsd:border-lsd-destructive [&>svg]:lsd:text-current data-[variant=destructive]>*:data-[slot=alert-description]:lsd:text-lsd-text-destructive', + info: 'lsd:text-lsd-text-info lsd:border-lsd-info [&>svg]:lsd:text-current data-[variant=info]>*:data-[slot=alert-description]:lsd:text-lsd-text-info', success: - 'lsd:bg-lsd-surface lsd:text-lsd-success-text lsd:border-lsd-success [&>svg]:lsd:text-current data-[variant=success]>*:data-[slot=alert-description]:lsd:text-lsd-success-text', + 'lsd:text-lsd-text-success lsd:border-lsd-success [&>svg]:lsd:text-current data-[variant=success]>*:data-[slot=alert-description]:lsd:text-lsd-text-success', warning: - 'lsd:bg-lsd-surface lsd:text-lsd-warning-text lsd:border-lsd-warning [&>svg]:lsd:text-current data-[variant=warning]>*:data-[slot=alert-description]:lsd:text-lsd-warning-text', + 'lsd:text-lsd-text-warning lsd:border-lsd-warning [&>svg]:lsd:text-current data-[variant=warning]>*:data-[slot=alert-description]:lsd:text-lsd-text-warning', }, }, defaultVariants: { diff --git a/packages/lsd/src/components/ui/alert/AlertDescription.tsx b/packages/lsd/src/components/ui/alert/AlertDescription.tsx index 8570cf06..b1318e10 100644 --- a/packages/lsd/src/components/ui/alert/AlertDescription.tsx +++ b/packages/lsd/src/components/ui/alert/AlertDescription.tsx @@ -32,10 +32,10 @@ function AlertDescription({ className, ...props }: AlertDescriptionProps) { // Pseudo-selectors & ARIA '[&_p]:lsd:leading-relaxed', // Colors & Backgrounds (variants) - 'data-[variant=destructive]:lsd:text-lsd-destructive-text/90', - 'data-[variant=info]:lsd:text-lsd-info-text/90', - 'data-[variant=success]:lsd:text-lsd-success-text/90', - 'data-[variant=warning]:lsd:text-lsd-warning-text/90', + 'data-[variant=destructive]:lsd:text-lsd-text-destructive/90', + 'data-[variant=info]:lsd:text-lsd-text-info/90', + 'data-[variant=success]:lsd:text-lsd-text-success/90', + 'data-[variant=warning]:lsd:text-lsd-text-warning/90', className )} {...props} diff --git a/packages/lsd/src/components/ui/autocomplete/Autocomplete.tsx b/packages/lsd/src/components/ui/autocomplete/Autocomplete.tsx index c924e4a4..a7c9bc5f 100644 --- a/packages/lsd/src/components/ui/autocomplete/Autocomplete.tsx +++ b/packages/lsd/src/components/ui/autocomplete/Autocomplete.tsx @@ -142,7 +142,7 @@ const Autocomplete = React.forwardRef( 'lsd:text-sm', currentSize.label, // Colors & Backgrounds - disabled ? 'lsd:text-lsd-text-secondary' : 'lsd:text-lsd-text-primary' + disabled && 'lsd:text-lsd-text-secondary' )} > {label} @@ -159,11 +159,11 @@ const Autocomplete = React.forwardRef( // Borders, Shapes & Effects variant === 'outlined' ? disabled - ? 'lsd:border lsd:border-lsd-border' - : 'lsd:border lsd:border-lsd-border' + ? 'lsd:border lsd:border-lsd-border/30' + : `lsd:border ${error ? 'lsd:border-lsd-destructive' : 'lsd:border-lsd-border'}` : disabled - ? 'lsd:border lsd:border-transparent lsd:border-b-lsd-border' - : 'lsd:border lsd:border-transparent lsd:border-b-lsd-border', + ? 'lsd:border lsd:border-transparent lsd:border-b-lsd-border/30' + : `lsd:border lsd:border-transparent ${error ? 'lsd:border-b-lsd-destructive' : 'lsd:border-b-lsd-border'}`, // Interactive States disabled ? 'lsd:cursor-not-allowed' : 'lsd:cursor-pointer' )} @@ -189,9 +189,7 @@ const Autocomplete = React.forwardRef( // Colors & Backgrounds disabled ? 'lsd:text-lsd-text-secondary' - : error - ? 'lsd:text-lsd-destructive' - : 'lsd:text-lsd-text-primary', + : error && 'lsd:text-lsd-text-destructive', // Interactive States disabled ? 'lsd:cursor-not-allowed' : '', // Typography @@ -230,7 +228,7 @@ const Autocomplete = React.forwardRef( // Sizing 'lsd:size-(--lsd-spacing-base)', // Colors & Backgrounds - disabled ? 'lsd:text-lsd-text-secondary' : 'lsd:text-lsd-icon-primary' + disabled && 'lsd:text-lsd-text-secondary' )} weight="duotone" /> diff --git a/packages/lsd/src/components/ui/autocomplete/AutocompleteList.tsx b/packages/lsd/src/components/ui/autocomplete/AutocompleteList.tsx index 2ad4858b..3a5e4ae3 100644 --- a/packages/lsd/src/components/ui/autocomplete/AutocompleteList.tsx +++ b/packages/lsd/src/components/ui/autocomplete/AutocompleteList.tsx @@ -74,7 +74,7 @@ export function AutocompleteContent({ // Spacing 'lsd:p-0', // Colors & Backgrounds - 'lsd:bg-lsd-surface', + 'lsd:bg-foreground', 'lsd:border-lsd-border', // Borders, Shapes & Effects 'lsd:data-[side=top]:border-b-0', @@ -121,8 +121,6 @@ export function AutocompleteContent({ keywords={[option.label]} onSelect={() => onSelect(option.value)} className={cn( - // Colors & Backgrounds - 'lsd:text-lsd-text-primary', // Interactive States 'lsd:hover:underline', 'lsd:focus:underline', @@ -144,8 +142,6 @@ export function AutocompleteContent({ {matchedPart} { it('applies error state', () => { render(); const input = screen.getByRole('textbox'); - expect(input).toHaveClass('lsd:text-lsd-destructive'); + expect(input).toHaveClass('lsd:text-lsd-text-destructive'); }); it('applies medium size classes by default', () => { @@ -250,6 +250,16 @@ describe('Autocomplete', () => { expect(screen.getByText('Option 1')).toBeInTheDocument(); }); + it('applies correct background to autocomplete list', () => { + render(); + const input = screen.getByRole('textbox'); + fireEvent.click(input); + const popover = screen.getByRole('dialog'); + expect(popover).toBeInTheDocument(); + // The popover content should have foreground background for overlay + expect(popover).toHaveClass('lsd:bg-foreground'); + }); + it('handles error when async fetch fails', async () => { const onOptionsFetch = vi.fn().mockRejectedValue(new Error('Fetch failed')); render(); diff --git a/packages/lsd/src/components/ui/avatar/AvatarBadge.tsx b/packages/lsd/src/components/ui/avatar/AvatarBadge.tsx index 9fd73040..4571a9ad 100644 --- a/packages/lsd/src/components/ui/avatar/AvatarBadge.tsx +++ b/packages/lsd/src/components/ui/avatar/AvatarBadge.tsx @@ -25,10 +25,10 @@ function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) { 'lsd:rounded-full', // Colors & Backgrounds 'lsd:bg-lsd-primary', - 'lsd:text-lsd-surface', + 'lsd:text-lsd-primary-content', // Ring 'lsd:ring-2', - 'lsd:ring-lsd-surface', + 'lsd:ring-lsd-background', // Other Utility Classes 'lsd:select-none', // Pseudo-selectors & ARIA diff --git a/packages/lsd/src/components/ui/avatar/AvatarFallback.tsx b/packages/lsd/src/components/ui/avatar/AvatarFallback.tsx index 4c7120e0..dfd391b6 100644 --- a/packages/lsd/src/components/ui/avatar/AvatarFallback.tsx +++ b/packages/lsd/src/components/ui/avatar/AvatarFallback.tsx @@ -26,7 +26,7 @@ function AvatarFallback({ // Borders, Shapes & Effects 'lsd:rounded-full', // Colors & Backgrounds - 'lsd:bg-lsd-muted', + 'lsd:bg-lsd-foreground', // Typography 'lsd:text-sm', 'lsd:text-lsd-text-secondary', diff --git a/packages/lsd/src/components/ui/avatar/AvatarGroup.tsx b/packages/lsd/src/components/ui/avatar/AvatarGroup.tsx index 7785196b..c24551e4 100644 --- a/packages/lsd/src/components/ui/avatar/AvatarGroup.tsx +++ b/packages/lsd/src/components/ui/avatar/AvatarGroup.tsx @@ -13,7 +13,7 @@ function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
)
svg]:size-(--lsd-spacing-base) lsd:group-has-data-[size=lg]/avatar-group:[&>svg]:size-(--lsd-spacing-large) lsd:group-has-data-[size=sm]/avatar-group:[&>svg]:size-(--lsd-spacing-small)', + 'lsd:relative lsd:flex lsd:size-(--lsd-spacing-large) lsd:shrink-0 lsd:items-center lsd:justify-center lsd:rounded-full lsd:bg-lsd-foreground lsd:text-sm lsd:text-lsd-text-secondary lsd:ring-2 lsd:ring-lsd-background lsd:group-has-data-[size=lg]/avatar-group:size-(--lsd-spacing-larger) lsd:group-has-data-[size=sm]/avatar-group:size-(--lsd-spacing-base) lsd:[&>svg]:size-(--lsd-spacing-base) lsd:group-has-data-[size=lg]/avatar-group:[&>svg]:size-(--lsd-spacing-large) lsd:group-has-data-[size=sm]/avatar-group:[&>svg]:size-(--lsd-spacing-small)', className )} {...props} diff --git a/packages/lsd/src/components/ui/badge/Badge.tsx b/packages/lsd/src/components/ui/badge/Badge.tsx index fee21ca8..2e0ec7ff 100644 --- a/packages/lsd/src/components/ui/badge/Badge.tsx +++ b/packages/lsd/src/components/ui/badge/Badge.tsx @@ -137,7 +137,7 @@ function Badge({ {onDismiss && ( ); const button = screen.getByRole('button'); - expect(button).toHaveClass('lsd:bg-primary'); - expect(button).toHaveClass('lsd:text-primary-foreground'); + expect(button).toHaveClass('lsd:bg-lsd-primary'); + expect(button).toHaveClass('lsd:text-lsd-primary-content'); }); it('applies outlined variant classes correctly', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('lsd:bg-transparent'); - expect(button).toHaveClass('lsd:text-foreground'); }); it('applies filled-rounded variant classes correctly', () => { render(); const button = screen.getByRole('button'); - expect(button).toHaveClass('lsd:bg-primary'); + expect(button).toHaveClass('lsd:bg-lsd-primary'); + expect(button).toHaveClass('lsd:text-lsd-primary-content'); expect(button).toHaveClass('lsd:rounded-full'); }); @@ -45,68 +45,56 @@ describe('Button', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('lsd:bg-transparent'); - expect(button).toHaveClass('lsd:text-foreground'); expect(button).toHaveClass('lsd:border-0'); - expect(button).toHaveClass('lsd:hover:underline'); }); it('applies ghost variant classes correctly', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('lsd:bg-transparent'); - expect(button).toHaveClass('lsd:text-foreground'); expect(button).toHaveClass('lsd:border-0'); - expect(button).toHaveClass('lsd:hover:bg-accent'); - expect(button).toHaveClass('lsd:hover:text-accent-foreground'); }); it('applies ghost-rounded variant classes correctly', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('lsd:bg-transparent'); - expect(button).toHaveClass('lsd:text-foreground'); expect(button).toHaveClass('lsd:border-0'); expect(button).toHaveClass('lsd:rounded-full'); - expect(button).toHaveClass('lsd:hover:bg-accent'); - expect(button).toHaveClass('lsd:hover:text-accent-foreground'); }); it('applies destructive variant classes correctly', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('lsd:bg-lsd-destructive'); - expect(button).toHaveClass('lsd:text-white'); + expect(button).toHaveClass('lsd:text-lsd-primary-content'); expect(button).toHaveClass('lsd:border-lsd-destructive'); - expect(button).toHaveClass('lsd:hover:bg-lsd-destructive/90'); }); it('applies destructive-rounded variant classes correctly', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('lsd:bg-lsd-destructive'); - expect(button).toHaveClass('lsd:text-white'); + expect(button).toHaveClass('lsd:text-lsd-primary-content'); expect(button).toHaveClass('lsd:border-lsd-destructive'); expect(button).toHaveClass('lsd:rounded-full'); - expect(button).toHaveClass('lsd:hover:bg-lsd-destructive/90'); }); it('applies success variant classes correctly', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('lsd:bg-lsd-success'); - expect(button).toHaveClass('lsd:text-white'); + expect(button).toHaveClass('lsd:text-lsd-primary-content'); expect(button).toHaveClass('lsd:border-lsd-success'); - expect(button).toHaveClass('lsd:hover:bg-lsd-success/90'); }); it('applies success-rounded variant classes correctly', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('lsd:bg-lsd-success'); - expect(button).toHaveClass('lsd:text-white'); + expect(button).toHaveClass('lsd:text-lsd-primary-content'); expect(button).toHaveClass('lsd:border-lsd-success'); expect(button).toHaveClass('lsd:rounded-full'); - expect(button).toHaveClass('lsd:hover:bg-lsd-success/90'); }); it('applies medium size classes correctly', () => { @@ -167,7 +155,7 @@ describe('Button', () => { it('uses default variant when not specified', () => { render(); const button = screen.getByRole('button'); - expect(button).toHaveClass('lsd:bg-primary'); + expect(button).toHaveClass('lsd:bg-lsd-primary'); }); it('uses default size when not specified', () => { @@ -211,10 +199,7 @@ describe('Button', () => { expect(button).toHaveClass('lsd:items-center'); expect(button).toHaveClass('lsd:justify-center'); expect(button).toHaveClass('lsd:border'); - expect(button).toHaveClass('lsd:transition-colors'); expect(button).toHaveClass('lsd:cursor-pointer'); - expect(button).toHaveClass('lsd:text-primary-foreground'); - expect(button).toHaveClass('lsd:hover:underline'); }); it('passes through additional props', () => { @@ -350,17 +335,17 @@ describe('Button', () => { describe('buttonVariants', () => { it('returns correct classes for filled variant', () => { - expect(buttonVariants({ variant: 'filled' })).toContain('lsd:bg-primary'); - expect(buttonVariants({ variant: 'filled' })).toContain('lsd:text-primary-foreground'); + expect(buttonVariants({ variant: 'filled' })).toContain('lsd:bg-lsd-primary'); + expect(buttonVariants({ variant: 'filled' })).toContain('lsd:text-lsd-primary-content'); }); it('returns correct classes for outlined variant', () => { expect(buttonVariants({ variant: 'outlined' })).toContain('lsd:bg-transparent'); - expect(buttonVariants({ variant: 'outlined' })).toContain('lsd:text-foreground'); }); it('returns correct classes for filled-rounded variant', () => { - expect(buttonVariants({ variant: 'filled-rounded' })).toContain('lsd:bg-primary'); + expect(buttonVariants({ variant: 'filled-rounded' })).toContain('lsd:bg-lsd-primary'); + expect(buttonVariants({ variant: 'filled-rounded' })).toContain('lsd:text-lsd-primary-content'); expect(buttonVariants({ variant: 'filled-rounded' })).toContain('lsd:rounded-full'); }); @@ -371,56 +356,44 @@ describe('buttonVariants', () => { it('returns correct classes for link variant', () => { expect(buttonVariants({ variant: 'link' })).toContain('lsd:bg-transparent'); - expect(buttonVariants({ variant: 'link' })).toContain('lsd:text-foreground'); expect(buttonVariants({ variant: 'link' })).toContain('lsd:border-0'); - expect(buttonVariants({ variant: 'link' })).toContain('lsd:underline'); }); it('returns correct classes for ghost variant', () => { expect(buttonVariants({ variant: 'ghost' })).toContain('lsd:bg-transparent'); - expect(buttonVariants({ variant: 'ghost' })).toContain('lsd:text-foreground'); expect(buttonVariants({ variant: 'ghost' })).toContain('lsd:border-0'); - expect(buttonVariants({ variant: 'ghost' })).toContain('lsd:hover:bg-accent'); - expect(buttonVariants({ variant: 'ghost' })).toContain('lsd:hover:text-accent-foreground'); }); it('returns correct classes for ghost-rounded variant', () => { expect(buttonVariants({ variant: 'ghost-rounded' })).toContain('lsd:bg-transparent'); - expect(buttonVariants({ variant: 'ghost-rounded' })).toContain('lsd:text-foreground'); expect(buttonVariants({ variant: 'ghost-rounded' })).toContain('lsd:border-0'); expect(buttonVariants({ variant: 'ghost-rounded' })).toContain('lsd:rounded-full'); - expect(buttonVariants({ variant: 'ghost-rounded' })).toContain('lsd:hover:bg-accent'); - expect(buttonVariants({ variant: 'ghost-rounded' })).toContain( - 'lsd:hover:text-accent-foreground' - ); }); it('returns correct classes for destructive variant', () => { expect(buttonVariants({ variant: 'destructive' })).toContain('lsd:bg-lsd-destructive'); - expect(buttonVariants({ variant: 'destructive' })).toContain('lsd:text-white'); - expect(buttonVariants({ variant: 'destructive' })).toContain('lsd:hover:bg-lsd-destructive/90'); + expect(buttonVariants({ variant: 'destructive' })).toContain('lsd:text-lsd-primary-content'); }); it('returns correct classes for destructive-rounded variant', () => { expect(buttonVariants({ variant: 'destructive-rounded' })).toContain('lsd:bg-lsd-destructive'); - expect(buttonVariants({ variant: 'destructive-rounded' })).toContain('lsd:text-white'); - expect(buttonVariants({ variant: 'destructive-rounded' })).toContain('lsd:rounded-full'); expect(buttonVariants({ variant: 'destructive-rounded' })).toContain( - 'lsd:hover:bg-lsd-destructive/90' + 'lsd:text-lsd-primary-content' ); + expect(buttonVariants({ variant: 'destructive-rounded' })).toContain('lsd:rounded-full'); }); it('returns correct classes for success variant', () => { expect(buttonVariants({ variant: 'success' })).toContain('lsd:bg-lsd-success'); - expect(buttonVariants({ variant: 'success' })).toContain('lsd:text-white'); - expect(buttonVariants({ variant: 'success' })).toContain('lsd:hover:bg-lsd-success/90'); + expect(buttonVariants({ variant: 'success' })).toContain('lsd:text-lsd-primary-content'); }); it('returns correct classes for success-rounded variant', () => { expect(buttonVariants({ variant: 'success-rounded' })).toContain('lsd:bg-lsd-success'); - expect(buttonVariants({ variant: 'success-rounded' })).toContain('lsd:text-white'); + expect(buttonVariants({ variant: 'success-rounded' })).toContain( + 'lsd:text-lsd-primary-content' + ); expect(buttonVariants({ variant: 'success-rounded' })).toContain('lsd:rounded-full'); - expect(buttonVariants({ variant: 'success-rounded' })).toContain('lsd:hover:bg-lsd-success/90'); }); it('returns correct classes for medium size', () => { @@ -465,7 +438,7 @@ describe('buttonVariants', () => { }); it('uses default variant when not specified', () => { - expect(buttonVariants({})).toContain('lsd:bg-primary'); + expect(buttonVariants({})).toContain('lsd:bg-lsd-primary'); }); it('uses default size when not specified', () => { diff --git a/packages/lsd/src/components/ui/button/types.ts b/packages/lsd/src/components/ui/button/types.ts index 0755dd3c..70e6598f 100644 --- a/packages/lsd/src/components/ui/button/types.ts +++ b/packages/lsd/src/components/ui/button/types.ts @@ -15,27 +15,25 @@ export type ButtonVariant = | 'success-rounded'; export const buttonVariants = cva( - 'lsd:inline-flex lsd:items-center lsd:justify-center lsd:border lsd:transition-colors lsd:cursor-pointer', + 'lsd:inline-flex lsd:items-center lsd:justify-center lsd:border lsd:cursor-pointer lsd:hover:underline', { variants: { variant: { - filled: 'lsd:bg-primary lsd:text-primary-foreground', - outlined: 'lsd:bg-transparent lsd:text-foreground', - 'filled-rounded': 'lsd:bg-primary lsd:text-primary-foreground lsd:rounded-full', - 'outlined-rounded': 'lsd:bg-transparent lsd:text-foreground lsd:rounded-full', - link: 'lsd:bg-transparent lsd:border-0 lsd:text-foreground lsd:underline', - ghost: - 'lsd:bg-transparent lsd:text-foreground lsd:border-0 lsd:hover:bg-accent lsd:hover:text-accent-foreground', - 'ghost-rounded': - 'lsd:bg-transparent lsd:text-foreground lsd:border-0 lsd:rounded-full lsd:hover:bg-accent lsd:hover:text-accent-foreground', + filled: 'lsd:bg-lsd-primary lsd:text-lsd-primary-content', + outlined: 'lsd:bg-transparent', + 'filled-rounded': 'lsd:bg-lsd-primary lsd:text-lsd-primary-content lsd:rounded-full', + 'outlined-rounded': 'lsd:bg-transparent lsd:rounded-full', + link: 'lsd:bg-transparent lsd:border-0 lsd:underline', + ghost: 'lsd:bg-transparent lsd:border-0', + 'ghost-rounded': 'lsd:bg-transparent lsd:border-0 lsd:rounded-full lsd:hover:border', destructive: - 'lsd:bg-lsd-destructive lsd:text-white lsd:border-lsd-destructive lsd:hover:bg-lsd-destructive/90', + 'lsd:bg-lsd-destructive lsd:text-lsd-primary-content lsd:border-lsd-destructive lsd:hover:bg-lsd-destructive/90', 'destructive-rounded': - 'lsd:bg-lsd-destructive lsd:text-white lsd:border-lsd-destructive lsd:rounded-full lsd:hover:bg-lsd-destructive/90', + 'lsd:bg-lsd-destructive lsd:text-lsd-primary-content lsd:border-lsd-destructive lsd:rounded-full lsd:hover:bg-lsd-destructive/90', success: - 'lsd:bg-lsd-success lsd:text-white lsd:border-lsd-success lsd:hover:bg-lsd-success/90', + 'lsd:bg-lsd-success lsd:text-lsd-primary-content lsd:border-lsd-success lsd:hover:bg-lsd-success/90', 'success-rounded': - 'lsd:bg-lsd-success lsd:text-white lsd:border-lsd-success lsd:rounded-full lsd:hover:bg-lsd-success/90', + 'lsd:bg-lsd-success lsd:text-lsd-primary-content lsd:border-lsd-success lsd:rounded-full lsd:hover:bg-lsd-success/90', }, size: { sm: 'lsd:h-8 lsd:px-[var(--lsd-spacing-small)] lsd:py-[var(--lsd-spacing-smaller)] lsd:text-sm', diff --git a/packages/lsd/src/components/ui/calendar/Calendar.tsx b/packages/lsd/src/components/ui/calendar/Calendar.tsx index 7ec93b7b..03fdf346 100644 --- a/packages/lsd/src/components/ui/calendar/Calendar.tsx +++ b/packages/lsd/src/components/ui/calendar/Calendar.tsx @@ -88,7 +88,7 @@ function Calendar({ buttonVariant = 'ghost', ...props }: CalendarProps) { svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, className @@ -140,15 +140,15 @@ function Calendar({ buttonVariant = 'ghost', ...props }: CalendarProps) { defaultClassNames.day ), range_start: cn( - 'lsd:rounded-l-md lsd:bg-lsd-primary lsd:text-lsd-primary-foreground', + 'lsd:rounded-l-md lsd:bg-lsd-primary lsd:text-lsd-primary-content', defaultClassNames.range_start ), range_middle: cn('lsd:rounded-none', defaultClassNames.range_middle), range_end: cn( - 'lsd:rounded-r-md lsd:bg-lsd-primary lsd:text-lsd-primary-foreground', + 'lsd:rounded-r-md lsd:bg-lsd-primary lsd:text-lsd-primary-content', defaultClassNames.range_end ), - today: cn('lsd:rounded-md lsd:bg-lsd-surface', defaultClassNames.today), + today: cn('lsd:border lsd:border-lsd-border/20', defaultClassNames.today), outside: cn( 'lsd:text-lsd-text-secondary lsd:aria-selected:lsd:text-lsd-text-secondary', defaultClassNames.outside diff --git a/packages/lsd/src/components/ui/calendar/CalendarDayButton.tsx b/packages/lsd/src/components/ui/calendar/CalendarDayButton.tsx index fb948912..892922c1 100644 --- a/packages/lsd/src/components/ui/calendar/CalendarDayButton.tsx +++ b/packages/lsd/src/components/ui/calendar/CalendarDayButton.tsx @@ -54,23 +54,21 @@ function CalendarDayButton({ className, day, modifiers, ...props }: CalendarDayB 'lsd:group-data-[focused=true]/day:lsd:z-10', 'lsd:group-data-[focused=true]/day:lsd:border-lsd-border', 'lsd:group-data-[focused=true]/day:lsd:ring-[3px]', - 'lsd:group-data-[focused=true]/day:lsd:ring-lsd-text-primary/50', + 'lsd:group-data-[focused=true]/day:lsd:ring-lsd-text-neutral/50', // Borders, Shapes & Effects (conditional) isRangeStart && cn('lsd:rounded-md', 'lsd:rounded-l-md'), // Colors & Backgrounds (conditional) - isRangeStart && cn('lsd:bg-lsd-primary', 'lsd:text-lsd-primary-foreground'), + isRangeStart && cn('lsd:bg-lsd-primary', 'lsd:text-lsd-primary-content'), // Borders, Shapes & Effects (conditional) isRangeEnd && cn('lsd:rounded-md', 'lsd:rounded-r-md'), // Colors & Backgrounds (conditional) - isRangeEnd && cn('lsd:bg-lsd-primary', 'lsd:text-lsd-primary-foreground'), + isRangeEnd && cn('lsd:bg-lsd-primary', 'lsd:text-lsd-primary-content'), // Borders, Shapes & Effects (conditional) - isRangeMiddle && cn('lsd:rounded-none'), + isRangeMiddle && 'lsd:rounded-none', // Colors & Backgrounds (conditional) - isRangeMiddle && cn('lsd:bg-lsd-surface', 'lsd:text-lsd-text-primary'), + isRangeMiddle && 'lsd:bg-lsd-background', // Borders, Shapes & Effects (conditional) isSelected && cn('lsd:border', 'lsd:border-lsd-border'), - // Dark Mode - 'dark:lsd:hover:lsd:text-lsd-text-primary', // Pseudo-selectors & ARIA '[&>span]:lsd:opacity-70', '[&>span]:lsd:lsd-typography-label1', diff --git a/packages/lsd/src/components/ui/calendar/__tests__/calendar.test.tsx b/packages/lsd/src/components/ui/calendar/__tests__/calendar.test.tsx index 13e4fe7d..5b36624e 100644 --- a/packages/lsd/src/components/ui/calendar/__tests__/calendar.test.tsx +++ b/packages/lsd/src/components/ui/calendar/__tests__/calendar.test.tsx @@ -22,12 +22,6 @@ describe('Calendar', () => { expect(root).toBeInTheDocument(); }); - it('applies LSD background token', () => { - const { container } = render(); - const root = container.querySelector('[data-slot="calendar"]') as HTMLElement; - expect(root).toHaveClass('lsd:bg-lsd-surface'); - }); - it('applies LSD padding token with CSS variable', () => { const { container } = render(); const root = container.querySelector('[data-slot="calendar"]') as HTMLElement; @@ -219,12 +213,6 @@ describe('Calendar', () => { }); describe('LSD Tokens Integration', () => { - it('uses LSD background token', () => { - const { container } = render(); - const root = container.querySelector('[data-slot="calendar"]') as HTMLElement; - expect(root).toHaveClass('lsd:bg-lsd-surface'); - }); - it('uses LSD spacing tokens with CSS variables for padding', () => { const { container } = render(); const root = container.querySelector('[data-slot="calendar"]') as HTMLElement; diff --git a/packages/lsd/src/components/ui/card/Card.tsx b/packages/lsd/src/components/ui/card/Card.tsx index 781ae657..1e7ba181 100644 --- a/packages/lsd/src/components/ui/card/Card.tsx +++ b/packages/lsd/src/components/ui/card/Card.tsx @@ -42,8 +42,7 @@ const Card = React.forwardRef>( data-slot="card" className={cn( // Colors & Backgrounds - 'lsd:bg-lsd-surface', - 'lsd:text-lsd-text-primary', + 'lsd:bg-lsd-foreground', // Layout & Positioning 'lsd:flex', 'lsd:flex-col', diff --git a/packages/lsd/src/components/ui/card/__tests__/card.test.tsx b/packages/lsd/src/components/ui/card/__tests__/card.test.tsx index cd04dae4..6ecd14b6 100644 --- a/packages/lsd/src/components/ui/card/__tests__/card.test.tsx +++ b/packages/lsd/src/components/ui/card/__tests__/card.test.tsx @@ -30,8 +30,7 @@ describe('Card', () => { it('applies default classes', () => { const { container } = render(); const card = queryByDataSlot(container, 'card'); - expect(card).toHaveClass('lsd:bg-lsd-surface'); - expect(card).toHaveClass('lsd:text-lsd-text-primary'); + expect(card).toHaveClass('lsd:bg-lsd-foreground'); expect(card).toHaveClass('lsd:flex'); expect(card).toHaveClass('lsd:flex-col'); expect(card).toHaveClass('lsd:border-lsd-border'); diff --git a/packages/lsd/src/components/ui/checkbox/Checkbox.tsx b/packages/lsd/src/components/ui/checkbox/Checkbox.tsx index f1910b45..31e72f5d 100644 --- a/packages/lsd/src/components/ui/checkbox/Checkbox.tsx +++ b/packages/lsd/src/components/ui/checkbox/Checkbox.tsx @@ -36,9 +36,9 @@ function Checkbox({ className, ...props }: React.ComponentProps { render(); const checkbox = screen.getByRole('checkbox'); expect(checkbox).toHaveClass('lsd:data-[state=checked]:bg-lsd-primary'); - expect(checkbox).toHaveClass('lsd:data-[state=checked]:text-lsd-surface'); + expect(checkbox).toHaveClass('lsd:data-[state=checked]:text-lsd-primary-content'); expect(checkbox).toHaveClass('lsd:data-[state=checked]:border-lsd-primary'); }); diff --git a/packages/lsd/src/components/ui/command/Command.tsx b/packages/lsd/src/components/ui/command/Command.tsx index 8d03f785..6873eb5d 100644 --- a/packages/lsd/src/components/ui/command/Command.tsx +++ b/packages/lsd/src/components/ui/command/Command.tsx @@ -56,8 +56,7 @@ export function Command({ className, ...props }: React.ComponentProps { ); const group = document.querySelector('[data-slot="command-group"]'); - expect(group).toHaveClass('lsd:text-lsd-text-primary'); expect(group).toHaveClass('lsd:overflow-hidden'); expect(group).toHaveClass('lsd:px-(--lsd-spacing-smallest)'); expect(group).toHaveClass('lsd:py-(--lsd-spacing-base)'); diff --git a/packages/lsd/src/components/ui/command/__tests__/command-item.test.tsx b/packages/lsd/src/components/ui/command/__tests__/command-item.test.tsx index 4275a764..8bc3a7f7 100644 --- a/packages/lsd/src/components/ui/command/__tests__/command-item.test.tsx +++ b/packages/lsd/src/components/ui/command/__tests__/command-item.test.tsx @@ -58,8 +58,7 @@ describe('CommandItem', () => { ); const item = document.querySelector('[data-slot="command-item"]'); - expect(item).toHaveClass('lsd:data-[selected=true]:bg-lsd-surface'); - expect(item).toHaveClass('lsd:data-[selected=true]:text-lsd-text-primary'); + expect(item).toHaveClass('lsd:data-[selected=true]:underline'); }); it('applies disabled state classes', () => { diff --git a/packages/lsd/src/components/ui/command/__tests__/command-shortcut.test.tsx b/packages/lsd/src/components/ui/command/__tests__/command-shortcut.test.tsx index be828d55..4e2d9509 100644 --- a/packages/lsd/src/components/ui/command/__tests__/command-shortcut.test.tsx +++ b/packages/lsd/src/components/ui/command/__tests__/command-shortcut.test.tsx @@ -44,7 +44,6 @@ describe('CommandShortcut', () => { ); const shortcut = document.querySelector('[data-slot="command-shortcut"]'); - expect(shortcut).toHaveClass('lsd:text-lsd-text-primary'); expect(shortcut).toHaveClass('lsd:ml-auto'); expect(shortcut).toHaveClass('lsd:text-xs'); expect(shortcut).toHaveClass('lsd:tracking-widest'); diff --git a/packages/lsd/src/components/ui/command/__tests__/command.test.tsx b/packages/lsd/src/components/ui/command/__tests__/command.test.tsx index e9340ada..a091ee55 100644 --- a/packages/lsd/src/components/ui/command/__tests__/command.test.tsx +++ b/packages/lsd/src/components/ui/command/__tests__/command.test.tsx @@ -46,8 +46,7 @@ describe('Command', () => { ); const command = document.querySelector('[data-slot="command"]'); - expect(command).toHaveClass('lsd:bg-lsd-surface'); - expect(command).toHaveClass('lsd:text-lsd-text-primary'); + expect(command).toHaveClass('lsd:bg-lsd-foreground'); expect(command).toHaveClass('lsd:flex'); expect(command).toHaveClass('lsd:h-full'); expect(command).toHaveClass('lsd:w-full'); diff --git a/packages/lsd/src/components/ui/dialog/DialogContent.tsx b/packages/lsd/src/components/ui/dialog/DialogContent.tsx index 36df32d7..bd0044b3 100644 --- a/packages/lsd/src/components/ui/dialog/DialogContent.tsx +++ b/packages/lsd/src/components/ui/dialog/DialogContent.tsx @@ -21,7 +21,7 @@ function DialogContent({ data-slot="dialog-content" className={cn( // Colors & Backgrounds - 'lsd:bg-lsd-surface', + 'lsd:bg-lsd-foreground', // Pseudo-selectors & ARIA - Animations 'lsd:data-[state=open]:animate-in', 'lsd:data-[state=closed]:animate-out', @@ -66,8 +66,7 @@ function DialogContent({ // Pseudo-selectors & ARIA - Focus 'focus:lsd:outline-hidden', // Pseudo-selectors & ARIA - State - 'lsd:data-[state=open]:bg-lsd-surface', - 'lsd:data-[state=open]:text-lsd-text-primary', + 'lsd:data-[state=open]:bg-lsd-foreground', // Positioning 'lsd:absolute', 'lsd:top-(--lsd-spacing-base)', diff --git a/packages/lsd/src/components/ui/dialog/DialogDescription.tsx b/packages/lsd/src/components/ui/dialog/DialogDescription.tsx index 956df021..3f12b7e8 100644 --- a/packages/lsd/src/components/ui/dialog/DialogDescription.tsx +++ b/packages/lsd/src/components/ui/dialog/DialogDescription.tsx @@ -10,7 +10,7 @@ function DialogDescription({ return ( ); diff --git a/packages/lsd/src/components/ui/dialog/__tests__/dialog-content.test.tsx b/packages/lsd/src/components/ui/dialog/__tests__/dialog-content.test.tsx index c270aa8a..b9ff4ea9 100644 --- a/packages/lsd/src/components/ui/dialog/__tests__/dialog-content.test.tsx +++ b/packages/lsd/src/components/ui/dialog/__tests__/dialog-content.test.tsx @@ -27,7 +27,7 @@ describe('DialogContent', () => { ); const content = document.querySelector('[data-slot="dialog-content"]'); - expect(content).toHaveClass('lsd:bg-lsd-surface'); + expect(content).toHaveClass('lsd:bg-lsd-foreground'); expect(content).toHaveClass('lsd:fixed'); expect(content).toHaveClass('lsd:top-[50%]'); expect(content).toHaveClass('lsd:left-[50%]'); diff --git a/packages/lsd/src/components/ui/dialog/__tests__/dialog-description.test.tsx b/packages/lsd/src/components/ui/dialog/__tests__/dialog-description.test.tsx index 4be496b2..ba0e41ba 100644 --- a/packages/lsd/src/components/ui/dialog/__tests__/dialog-description.test.tsx +++ b/packages/lsd/src/components/ui/dialog/__tests__/dialog-description.test.tsx @@ -29,7 +29,6 @@ describe('DialogDescription', () => { ); const description = screen.getByText('Description'); - expect(description).toHaveClass('lsd:text-lsd-text-primary'); expect(description).toHaveClass('lsd:text-sm'); }); diff --git a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.tsx b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.tsx index f1a4a5d2..47dace58 100644 --- a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.tsx +++ b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.tsx @@ -40,8 +40,6 @@ export function DropdownMenuCheckboxItem({ 'lsd:outline-none', 'lsd:select-none', // Interactive States - Focus - 'focus:lsd:bg-[var(--lsd-accent)]', - 'focus:lsd:text-[var(--lsd-accent-foreground)]', 'lsd:hover:underline', 'lsd:focus:underline', // Interactive States - Disabled diff --git a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuContent.tsx b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuContent.tsx index 74825dcf..dba0cd42 100644 --- a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuContent.tsx +++ b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuContent.tsx @@ -30,8 +30,7 @@ export function DropdownMenuContent({ sideOffset={sideOffset} className={cn( // Colors & Backgrounds - 'lsd:bg-lsd-surface', - 'lsd:text-lsd-text-primary', + 'lsd:bg-lsd-foreground', // Borders 'lsd:border', // Layout & Positioning diff --git a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuItem.tsx b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuItem.tsx index e0f181d3..e2ccc608 100644 --- a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuItem.tsx +++ b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuItem.tsx @@ -32,7 +32,7 @@ export function DropdownMenuItem({ data-variant={variant} className={cn( // Colors & Backgrounds - Destructive variant - variant === 'destructive' && 'lsd:text-lsd-destructive-text', + variant === 'destructive' && 'lsd:text-lsd-text-destructive', // Layout & Positioning 'lsd:relative', 'lsd:flex', @@ -49,8 +49,6 @@ export function DropdownMenuItem({ 'lsd:outline-none', 'lsd:select-none', // Interactive States - Focus/Hover - 'focus:lsd:bg-[var(--lsd-accent)]', - 'focus:lsd:text-[var(--lsd-accent-foreground)]', 'lsd:hover:underline', 'lsd:focus:underline', // Interactive States - Disabled @@ -58,7 +56,7 @@ export function DropdownMenuItem({ 'data-[disabled]:lsd:opacity-50', // Interactive States - Destructive variant focus variant === 'destructive' && 'focus:lsd:bg-[var(--lsd-destructive)]/10', - variant === 'destructive' && 'focus:lsd:text-[var(--lsd-destructive-text)]', + variant === 'destructive' && 'focus:lsd:text-[var(--lsd-text-destructive)]', variant === 'destructive' && 'dark:focus:lsd:bg-[var(--lsd-destructive)]/20', // Interactive States - Cursor 'lsd:cursor-pointer', @@ -71,7 +69,7 @@ export function DropdownMenuItem({ '[&_svg:not([class*=text-])]:lsd:text-[var(--lsd-text-secondary)]', // Pseudo-selectors & ARIA - Destructive variant SVG variant === 'destructive' && - '[&[data-variant=destructive]]:[&_svg]:lsd:text-[var(--lsd-destructive-text)]', + '[&[data-variant=destructive]]:[&_svg]:lsd:text-[var(--lsd-text-destructive)]', className )} {...props} diff --git a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuLabel.tsx b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuLabel.tsx index f76c8a2e..75312c0c 100644 --- a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuLabel.tsx +++ b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuLabel.tsx @@ -24,8 +24,6 @@ export function DropdownMenuLabel({ className, inset, ...props }: DropdownMenuLa // Typography Font 'lsd:font-medium', 'lsd:text-sm', - // Colors & Backgrounds - 'lsd:text-lsd-text-primary', // Spacing 'lsd:px-2', 'lsd:py-1.5', diff --git a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuRadioItem.tsx b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuRadioItem.tsx index 53608608..c8e90fcb 100644 --- a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuRadioItem.tsx +++ b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuRadioItem.tsx @@ -39,8 +39,6 @@ export function DropdownMenuRadioItem({ 'lsd:outline-none', 'lsd:select-none', // Interactive States - Focus - 'focus:lsd:bg-[var(--lsd-accent)]', - 'focus:lsd:text-[var(--lsd-accent-foreground)]', 'lsd:hover:underline', 'lsd:focus:underline', // Interactive States - Disabled diff --git a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuSubContent.tsx b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuSubContent.tsx index 05b9dd01..464dab55 100644 --- a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuSubContent.tsx +++ b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuSubContent.tsx @@ -11,8 +11,7 @@ export function DropdownMenuSubContent({ className, ...props }: DropdownMenuSubC data-slot="dropdown-menu-sub-content" className={cn( // Colors & Backgrounds - 'lsd:bg-lsd-surface', - 'lsd:text-lsd-text-primary', + 'lsd:bg-lsd-foreground', // Borders 'lsd:border', // Layout & Positioning diff --git a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.tsx b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.tsx index 7dcb381e..8d40bcb9 100644 --- a/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.tsx +++ b/packages/lsd/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.tsx @@ -42,13 +42,8 @@ export function DropdownMenuSubTrigger({ 'lsd:outline-none', 'lsd:select-none', // Interactive States - Focus/Hover - 'focus:lsd:bg-[var(--lsd-accent)]', - 'focus:lsd:text-[var(--lsd-accent-foreground)]', 'lsd:hover:underline', 'lsd:focus:underline', - // Interactive States - Open state - 'data-[state=open]:lsd:bg-[var(--lsd-accent)]', - 'data-[state=open]:lsd:text-[var(--lsd-accent-foreground)]', // Interactive States - Cursor 'lsd:cursor-pointer', // Pseudo-selectors & ARIA - Inset variant diff --git a/packages/lsd/src/components/ui/dropdown-menu/dropdown-menu.test.tsx b/packages/lsd/src/components/ui/dropdown-menu/dropdown-menu.test.tsx index 6b40f4c9..1065e9c9 100644 --- a/packages/lsd/src/components/ui/dropdown-menu/dropdown-menu.test.tsx +++ b/packages/lsd/src/components/ui/dropdown-menu/dropdown-menu.test.tsx @@ -135,12 +135,15 @@ describe('DropdownMenuContent', () => { ); const content = document.querySelector('[data-slot="dropdown-menu-content"]'); + expect(content).toHaveClass('lsd:bg-lsd-foreground'); + expect(content).toHaveClass('lsd:border'); expect(content).toHaveClass('lsd:z-50'); + expect(content).toHaveClass('lsd:max-h-(--radix-dropdown-menu-content-available-height)'); expect(content).toHaveClass('lsd:min-w-32'); expect(content).toHaveClass('lsd:rounded-none'); - expect(content).toHaveClass('lsd:border'); - expect(content).toHaveClass('lsd:bg-lsd-surface'); - expect(content).toHaveClass('lsd:p-(--lsd-spacing-smaller)'); + expect(content).toHaveClass('lsd:shadow-md'); + expect(content).toHaveClass('lsd:overflow-x-hidden'); + expect(content).toHaveClass('lsd:overflow-y-auto'); }); it('merges custom className with component classes', () => { @@ -258,7 +261,7 @@ describe('DropdownMenuItem', () => { ); const item = screen.getByText('Delete'); expect(item).toHaveAttribute('data-variant', 'destructive'); - expect(item).toHaveClass('lsd:text-lsd-destructive-text'); + expect(item).toHaveClass('lsd:text-lsd-text-destructive'); }); it('applies inset prop correctly', () => { @@ -780,7 +783,6 @@ describe('DropdownMenuLabel', () => { expect(label).toHaveClass('lsd:py-1.5'); expect(label).toHaveClass('lsd:text-sm'); expect(label).toHaveClass('lsd:font-medium'); - expect(label).toHaveClass('lsd:text-lsd-text-primary'); }); it('applies inset prop correctly', () => { diff --git a/packages/lsd/src/components/ui/field/FieldError.tsx b/packages/lsd/src/components/ui/field/FieldError.tsx index 1a4ca438..0e1cc1ee 100644 --- a/packages/lsd/src/components/ui/field/FieldError.tsx +++ b/packages/lsd/src/components/ui/field/FieldError.tsx @@ -59,7 +59,7 @@ const FieldError = React.forwardRef( // Typography Font 'lsd:font-normal', // Colors & Backgrounds - 'lsd:text-lsd-destructive-text', + 'lsd:text-lsd-text-destructive', className )} {...props} diff --git a/packages/lsd/src/components/ui/field/FieldLabel.tsx b/packages/lsd/src/components/ui/field/FieldLabel.tsx index fa0dd1ab..72d5aa5a 100644 --- a/packages/lsd/src/components/ui/field/FieldLabel.tsx +++ b/packages/lsd/src/components/ui/field/FieldLabel.tsx @@ -23,8 +23,6 @@ const FieldLabel = React.forwardRef( 'lsd:text-[0.875rem]', // Typography Font 'lsd:font-medium', - // Colors & Backgrounds - 'lsd:text-lsd-text-primary', // Spacing 'lsd:mb-(--lsd-spacing-smaller)', // Display diff --git a/packages/lsd/src/components/ui/field/FieldLegend.tsx b/packages/lsd/src/components/ui/field/FieldLegend.tsx index 5e133e0f..fdaa05bb 100644 --- a/packages/lsd/src/components/ui/field/FieldLegend.tsx +++ b/packages/lsd/src/components/ui/field/FieldLegend.tsx @@ -16,7 +16,7 @@ const FieldLegend = React.forwardRef( ); diff --git a/packages/lsd/src/components/ui/field/field.test.tsx b/packages/lsd/src/components/ui/field/field.test.tsx index e9e51d75..c9ab7d23 100644 --- a/packages/lsd/src/components/ui/field/field.test.tsx +++ b/packages/lsd/src/components/ui/field/field.test.tsx @@ -176,7 +176,6 @@ describe('FieldLegend', () => { render(Legend Text); const legend = screen.getByText('Legend Text'); expect(legend).toHaveClass('lsd:text-[1.5rem]'); - expect(legend).toHaveClass('lsd:text-lsd-text-primary'); expect(legend).toHaveClass('lsd:font-medium'); }); @@ -262,7 +261,6 @@ describe('FieldLabel', () => { const label = screen.getByText('Label Text'); expect(label).toHaveClass('lsd:text-[0.875rem]'); expect(label).toHaveClass('lsd:font-medium'); - expect(label).toHaveClass('lsd:text-lsd-text-primary'); expect(label).toHaveClass('lsd:mb-(--lsd-spacing-smaller)'); expect(label).toHaveClass('lsd:block'); }); @@ -448,7 +446,7 @@ describe('FieldError', () => { const error = screen.getByText('Error message'); expect(error).toHaveClass('lsd:text-sm'); expect(error).toHaveClass('lsd:font-normal'); - expect(error).toHaveClass('lsd:text-lsd-destructive-text'); + expect(error).toHaveClass('lsd:text-lsd-text-destructive'); }); it('applies role="alert" for accessibility', () => { diff --git a/packages/lsd/src/components/ui/form/FormLabel.tsx b/packages/lsd/src/components/ui/form/FormLabel.tsx index 351905f6..2f66cf49 100644 --- a/packages/lsd/src/components/ui/form/FormLabel.tsx +++ b/packages/lsd/src/components/ui/form/FormLabel.tsx @@ -20,7 +20,7 @@ function FormLabel({ className, ...props }: React.ComponentProps diff --git a/packages/lsd/src/components/ui/form/FormMessage.tsx b/packages/lsd/src/components/ui/form/FormMessage.tsx index 1c6dc8b7..91aef52d 100644 --- a/packages/lsd/src/components/ui/form/FormMessage.tsx +++ b/packages/lsd/src/components/ui/form/FormMessage.tsx @@ -29,9 +29,9 @@ function FormMessage({ className, ...props }: FormMessageProps) { // Typography Size 'lsd:text-sm', // Typography Line Height - 'lsd:leading-[1.25rem]', + 'lsd:leading-5', // Colors & Backgrounds - 'lsd:text-lsd-destructive-text', + 'lsd:text-lsd-text-destructive', className )} {...props} diff --git a/packages/lsd/src/components/ui/form/form.test.tsx b/packages/lsd/src/components/ui/form/form.test.tsx index fbc35b44..8f3ee54c 100644 --- a/packages/lsd/src/components/ui/form/form.test.tsx +++ b/packages/lsd/src/components/ui/form/form.test.tsx @@ -171,7 +171,7 @@ describe('Form Components', () => { }); const label = screen.getByTestId('form-label'); - expect(label).toHaveClass('lsd:text-lsd-destructive-text'); + expect(label).toHaveClass('lsd:text-lsd-text-destructive'); }); it('does not apply destructive class when there is no error', () => { @@ -195,7 +195,7 @@ describe('Form Components', () => { } render(); const label = screen.getByTestId('form-label'); - expect(label).not.toHaveClass('lsd:text-lsd-destructive-text'); + expect(label).not.toHaveClass('lsd:text-lsd-text-destructive'); }); }); @@ -421,7 +421,9 @@ describe('Form Components', () => { } render(); const message = screen.getByTestId('message'); - expect(message).toHaveClass('lsd:text-lsd-destructive-text'); + expect(message).toHaveClass('lsd:text-sm'); + expect(message).toHaveClass('lsd:leading-5'); + expect(message).toHaveClass('lsd:text-lsd-text-destructive'); }); it('does not render when no error or children', () => { diff --git a/packages/lsd/src/components/ui/input-group/InputGroup.tsx b/packages/lsd/src/components/ui/input-group/InputGroup.tsx index 2e6e3fc7..a25874fa 100644 --- a/packages/lsd/src/components/ui/input-group/InputGroup.tsx +++ b/packages/lsd/src/components/ui/input-group/InputGroup.tsx @@ -65,8 +65,6 @@ const InputGroup = React.forwardRef( 'lsd:transition-[color,box-shadow]', // Interactive States - Focus 'lsd:outline-none', - // Colors & Backgrounds - 'lsd:bg-lsd-surface', // Dynamic height classes getInputGroupHeightClasses(size), // Sizing - minimum width and textarea diff --git a/packages/lsd/src/components/ui/input-group/InputGroupText.tsx b/packages/lsd/src/components/ui/input-group/InputGroupText.tsx index cd4c4ce2..f61b66ed 100644 --- a/packages/lsd/src/components/ui/input-group/InputGroupText.tsx +++ b/packages/lsd/src/components/ui/input-group/InputGroupText.tsx @@ -28,8 +28,8 @@ const InputGroupText = React.forwardRef( // Colors & Backgrounds 'lsd:text-lsd-text-secondary', // Pseudo-selectors & ARIA - SVG styling - '[&_svg]:lsd:pointer-events-none', - "[&_svg:not([class*='size-'])]:lsd:size-4", + 'lsd:[&_svg]:pointer-events-none', + "lsd:[&_svg:not([class*='size-'])]:size-4", // Dynamic text size classes getInputGroupTextSizeClasses(size), className diff --git a/packages/lsd/src/components/ui/input/Input.tsx b/packages/lsd/src/components/ui/input/Input.tsx index af2dfda9..2381ffad 100644 --- a/packages/lsd/src/components/ui/input/Input.tsx +++ b/packages/lsd/src/components/ui/input/Input.tsx @@ -101,14 +101,11 @@ const Input = React.forwardRef( disabled={disabled} data-slot="input" className={cn( - // Pseudo-selectors & ARIA - File colors - 'file:lsd:text-lsd-text-primary', // Pseudo-selectors & ARIA - Placeholder colors - 'placeholder:lsd:text-lsd-text-primary', 'placeholder:lsd:opacity-30', // Pseudo-selectors & ARIA - Selection colors 'selection:lsd:bg-lsd-primary', - 'selection:lsd:text-lsd-surface', + 'selection:lsd:text-lsd-primary-content', // Borders 'lsd:border-none', // Interactive States - Focus @@ -116,7 +113,6 @@ const Input = React.forwardRef( 'focus-visible:lsd:outline-none', // Colors & Backgrounds 'lsd:bg-transparent', - 'lsd:text-lsd-text-primary', // Sizing 'lsd:w-full', 'lsd:h-full', diff --git a/packages/lsd/src/components/ui/input/input.test.tsx b/packages/lsd/src/components/ui/input/input.test.tsx index 2387ec83..1a49f2c0 100644 --- a/packages/lsd/src/components/ui/input/input.test.tsx +++ b/packages/lsd/src/components/ui/input/input.test.tsx @@ -167,22 +167,21 @@ describe('Input', () => { expect(input).toHaveValue('test value'); }); - it('applies focus-visible classes', () => { - render(); - const input = screen.getByRole('textbox'); - expect(input).toHaveClass('focus-visible:lsd:outline-none'); - expect(input).toHaveClass('lsd:px-(--lsd-spacing-base)'); - }); - it('applies base classes correctly', () => { render(); const input = screen.getByRole('textbox'); expect(input).toHaveClass('lsd:border-none'); expect(input).toHaveClass('lsd:outline-none'); expect(input).toHaveClass('lsd:bg-transparent'); - expect(input).toHaveClass('lsd:text-lsd-text-primary'); expect(input).toHaveClass('lsd:w-full'); expect(input).toHaveClass('lsd:h-full'); + expect(input).toHaveClass('focus-visible:lsd:outline-none'); + expect(input).toHaveClass('lsd:px-(--lsd-spacing-base)'); + expect(input).toHaveClass('lsd:py-[var(--lsd-spacing-small)]'); + expect(input).toHaveClass('lsd:text-base'); + expect(input).toHaveClass('placeholder:lsd:opacity-30'); + expect(input).toHaveClass('selection:lsd:bg-lsd-primary'); + expect(input).toHaveClass('selection:lsd:text-lsd-primary-content'); }); it('applies label size classes correctly', () => { diff --git a/packages/lsd/src/components/ui/label/label.test.tsx b/packages/lsd/src/components/ui/label/label.test.tsx index 5c3e4320..d78e8168 100644 --- a/packages/lsd/src/components/ui/label/label.test.tsx +++ b/packages/lsd/src/components/ui/label/label.test.tsx @@ -21,7 +21,7 @@ describe('Label', () => { it('applies default variant classes correctly', () => { render(); const label = screen.getByText('Label'); - expect(label).toHaveClass('lsd:text-lsd-text-primary'); + expect(label).toHaveClass('lsd:text-lsd-text-neutral'); }); it('applies secondary variant classes correctly', () => { @@ -54,7 +54,7 @@ describe('Label', () => { it('uses default variant when not specified', () => { render(); const label = screen.getByText('Label'); - expect(label).toHaveClass('lsd:text-lsd-text-primary'); + expect(label).toHaveClass('lsd:text-lsd-text-neutral'); }); it('uses default size when not specified', () => { @@ -103,7 +103,7 @@ describe('Label', () => { describe('labelVariants', () => { it('returns correct classes for default variant', () => { - expect(labelVariants({ variant: 'default' })).toContain('lsd:text-lsd-text-primary'); + expect(labelVariants({ variant: 'default' })).toContain('lsd:text-lsd-text-neutral'); }); it('returns correct classes for secondary variant', () => { @@ -126,7 +126,7 @@ describe('labelVariants', () => { }); it('uses default variant when not specified', () => { - expect(labelVariants({})).toContain('lsd:text-lsd-text-primary'); + expect(labelVariants({})).toContain('lsd:text-lsd-text-neutral'); }); it('uses default size when not specified', () => { diff --git a/packages/lsd/src/components/ui/label/types.ts b/packages/lsd/src/components/ui/label/types.ts index 289f3756..ef5495f2 100644 --- a/packages/lsd/src/components/ui/label/types.ts +++ b/packages/lsd/src/components/ui/label/types.ts @@ -8,7 +8,7 @@ export const labelVariants = cva( { variants: { variant: { - default: 'lsd:text-lsd-text-primary', + default: 'lsd:text-lsd-text-neutral', secondary: 'lsd:text-lsd-text-secondary', }, size: { diff --git a/packages/lsd/src/components/ui/menubar/__tests__/menubar.test.tsx b/packages/lsd/src/components/ui/menubar/__tests__/menubar.test.tsx index d7f8f221..8caef8f1 100644 --- a/packages/lsd/src/components/ui/menubar/__tests__/menubar.test.tsx +++ b/packages/lsd/src/components/ui/menubar/__tests__/menubar.test.tsx @@ -49,7 +49,6 @@ describe('Menubar', () => { expect(menubar).toHaveClass('lsd:items-center'); expect(menubar).toHaveClass('lsd:gap-[var(--lsd-spacing-smallest)]'); expect(menubar).toHaveClass('lsd:border'); - expect(menubar).toHaveClass('lsd:bg-lsd-surface'); }); it('merges custom className with component classes', () => { diff --git a/packages/lsd/src/components/ui/menubar/types.ts b/packages/lsd/src/components/ui/menubar/types.ts index 6fa218d7..e1b1668d 100644 --- a/packages/lsd/src/components/ui/menubar/types.ts +++ b/packages/lsd/src/components/ui/menubar/types.ts @@ -1,15 +1,15 @@ import { cva, type VariantProps } from 'class-variance-authority'; export const menubarVariants = cva( - 'lsd:flex lsd:h-9 lsd:items-center lsd:gap-[var(--lsd-spacing-smallest)] lsd:border lsd:border-lsd-border lsd:bg-lsd-surface lsd:p-[var(--lsd-spacing-smallest)] lsd:shadow-xs' + 'lsd:flex lsd:h-9 lsd:items-center lsd:gap-[var(--lsd-spacing-smallest)] lsd:border lsd:border-lsd-border lsd:p-[var(--lsd-spacing-smallest)] lsd:shadow-xs' ); export const menubarTriggerVariants = cva( - 'lsd:cursor-pointer lsd:flex lsd:items-center lsd:px-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smallest)] lsd:text-sm lsd:font-medium lsd:outline-none lsd:select-none lsd:text-lsd-text-primary lsd:bg-lsd-surface lsd:hover:underline lsd:focus:underline lsd:data-[state=open]:bg-lsd-surface-secondary lsd:data-[state=open]:text-lsd-text-primary lsd:transition-colors' + 'lsd:cursor-pointer lsd:flex lsd:items-center lsd:px-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smallest)] lsd:text-sm lsd:font-medium lsd:outline-none lsd:select-none lsd:hover:underline lsd:focus:underline lsd:data-[state=open]:bg-lsd-foreground lsd:transition-colors' ); export const menubarItemVariants = cva( - "lsd:relative lsd:flex lsd:cursor-pointer lsd:items-center lsd:gap-[var(--lsd-spacing-smaller)] lsd:px-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smaller)] lsd:text-sm lsd:outline-none lsd:select-none lsd:text-lsd-text-primary lsd:bg-lsd-surface lsd:disabled:pointer-events-none lsd:disabled:opacity-50 lsd:transition-colors lsd:[&_svg]:pointer-events-none lsd:[&_svg]:shrink-0 lsd:[&_svg:not([class*='size-'])]:size-4", + "lsd:relative lsd:flex lsd:cursor-pointer lsd:items-center lsd:gap-[var(--lsd-spacing-smaller)] lsd:px-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smaller)] lsd:text-sm lsd:outline-none lsd:select-none lsd:bg-lsd-foreground lsd:disabled:pointer-events-none lsd:disabled:opacity-50 lsd:transition-colors lsd:[&_svg]:pointer-events-none lsd:[&_svg]:shrink-0 lsd:[&_svg:not([class*='size-'])]:size-4", { variants: { variant: { @@ -33,7 +33,7 @@ export const menubarSeparatorVariants = cva( ); export const menubarLabelVariants = cva( - 'lsd:px-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smaller)] lsd:text-sm lsd:font-bold lsd:text-lsd-text-primary', + 'lsd:px-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smaller)] lsd:text-sm lsd:font-bold', { variants: { inset: { @@ -47,11 +47,11 @@ export const menubarLabelVariants = cva( ); export const menubarContentVariants = cva( - 'lsd:bg-lsd-surface lsd:text-lsd-text-primary lsd:z-50 lsd:min-w-[12rem] lsd:overflow-hidden lsd:border lsd:border-lsd-border lsd:p-[var(--lsd-spacing-smallest)] lsd:shadow-lg lsd:data-[state=open]:animate-in lsd:data-[state=closed]:fade-out-0 lsd:data-[state=open]:fade-in-0 lsd:data-[state=closed]:zoom-out-95 lsd:data-[state=open]:zoom-in-95 lsd:data-[side=bottom]:slide-in-from-top-2 lsd:data-[side=left]:slide-in-from-right-2 lsd:data-[side=right]:slide-in-from-left-2 lsd:data-[side=top]:slide-in-from-bottom-2' + 'lsd:bg-lsd-foreground lsd:z-50 lsd:min-w-[12rem] lsd:overflow-hidden lsd:border lsd:border-lsd-border lsd:p-[var(--lsd-spacing-smallest)] lsd:shadow-lg lsd:data-[state=open]:animate-in lsd:data-[state=closed]:fade-out-0 lsd:data-[state=open]:fade-in-0 lsd:data-[state=closed]:zoom-out-95 lsd:data-[state=open]:zoom-in-95 lsd:data-[side=bottom]:slide-in-from-top-2 lsd:data-[side=left]:slide-in-from-right-2 lsd:data-[side=right]:slide-in-from-left-2 lsd:data-[side=top]:slide-in-from-bottom-2' ); export const menubarSubTriggerVariants = cva( - 'lsd:flex lsd:cursor-pointer lsd:items-center lsd:px-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smaller)] lsd:text-sm lsd:outline-none lsd:select-none lsd:text-lsd-text-primary lsd:bg-lsd-surface lsd:data-[state=open]:bg-lsd-surface-secondary lsd:data-[state=open]:text-lsd-text-primary lsd:transition-colors', + 'lsd:flex lsd:cursor-pointer lsd:items-center lsd:px-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smaller)] lsd:text-sm lsd:outline-none lsd:select-none lsd:data-[state=open]:bg-lsd-foreground lsd:transition-colors', { variants: { inset: { @@ -65,15 +65,15 @@ export const menubarSubTriggerVariants = cva( ); export const menubarSubContentVariants = cva( - 'lsd:bg-lsd-surface lsd:text-lsd-text-primary lsd:z-50 lsd:min-w-[8rem] lsd:overflow-hidden lsd:border lsd:border-lsd-border lsd:p-[var(--lsd-spacing-smallest)] lsd:shadow-lg lsd:data-[state=open]:animate-in lsd:data-[state=closed]:animate-out lsd:data-[state=closed]:fade-out-0 lsd:data-[state=open]:fade-in-0 lsd:data-[state=closed]:zoom-out-95 lsd:data-[state=open]:zoom-in-95 lsd:data-[side=bottom]:slide-in-from-top-2 lsd:data-[side=left]:slide-in-from-right-2 lsd:data-[side=right]:slide-in-from-left-2 lsd:data-[side=top]:slide-in-from-bottom-2' + 'lsd:bg-lsd-foreground lsd:z-50 lsd:min-w-[8rem] lsd:overflow-hidden lsd:border lsd:border-lsd-border lsd:p-[var(--lsd-spacing-smallest)] lsd:shadow-lg lsd:data-[state=open]:animate-in lsd:data-[state=closed]:animate-out lsd:data-[state=closed]:fade-out-0 lsd:data-[state=open]:fade-in-0 lsd:data-[state=closed]:zoom-out-95 lsd:data-[state=open]:zoom-in-95 lsd:data-[side=bottom]:slide-in-from-top-2 lsd:data-[side=left]:slide-in-from-right-2 lsd:data-[side=right]:slide-in-from-left-2 lsd:data-[side=top]:slide-in-from-bottom-2' ); export const menubarCheckboxItemVariants = cva( - "lsd:relative lsd:flex lsd:cursor-pointer lsd:items-center lsd:gap-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smaller)] lsd:pr-[var(--lsd-spacing-smaller)] lsd:pl-8 lsd:text-sm lsd:outline-none lsd:select-none lsd:text-lsd-text-primary lsd:bg-lsd-surface lsd:disabled:pointer-events-none lsd:disabled:opacity-50 lsd:[&_svg]:pointer-events-none lsd:[&_svg]:shrink-0 lsd:[&_svg:not([class*='size-'])]:size-4" + "lsd:relative lsd:flex lsd:cursor-pointer lsd:items-center lsd:gap-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smaller)] lsd:pr-[var(--lsd-spacing-smaller)] lsd:pl-8 lsd:text-sm lsd:outline-none lsd:select-none lsd:bg-lsd-foreground lsd:disabled:pointer-events-none lsd:disabled:opacity-50 lsd:[&_svg]:pointer-events-none lsd:[&_svg]:shrink-0 lsd:[&_svg:not([class*='size-'])]:size-4" ); export const menubarRadioItemVariants = cva( - "lsd:relative lsd:flex lsd:cursor-pointer lsd:items-center lsd:gap-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smaller)] lsd:pr-[var(--lsd-spacing-smaller)] lsd:pl-8 lsd:text-sm lsd:outline-none lsd:select-none lsd:text-lsd-text-primary lsd:bg-lsd-surface lsd:disabled:pointer-events-none lsd:disabled:opacity-50 lsd:[&_svg]:pointer-events-none lsd:[&_svg]:shrink-0 lsd:[&_svg:not([class*='size-'])]:size-4" + "lsd:relative lsd:flex lsd:cursor-pointer lsd:items-center lsd:gap-[var(--lsd-spacing-smaller)] lsd:py-[var(--lsd-spacing-smaller)] lsd:pr-[var(--lsd-spacing-smaller)] lsd:pl-8 lsd:text-sm lsd:outline-none lsd:select-none lsd:bg-lsd-foreground lsd:disabled:pointer-events-none lsd:disabled:opacity-50 lsd:[&_svg]:pointer-events-none lsd:[&_svg]:shrink-0 lsd:[&_svg:not([class*='size-'])]:size-4" ); export type MenubarVariants = VariantProps; diff --git a/packages/lsd/src/components/ui/navigation-menu/NavigationMenuContent.tsx b/packages/lsd/src/components/ui/navigation-menu/NavigationMenuContent.tsx index 2431058f..987ec355 100644 --- a/packages/lsd/src/components/ui/navigation-menu/NavigationMenuContent.tsx +++ b/packages/lsd/src/components/ui/navigation-menu/NavigationMenuContent.tsx @@ -23,49 +23,36 @@ export function NavigationMenuContent({ className, ...props }: NavigationMenuCon // Sizing - Default 'lsd:w-full', // Spacing - Default - 'lsd:p-(--lsd-spacing-base)', - 'lsd:pr-(--lsd-spacing-large)', + 'lsd:pt-(--lsd-spacing-smallest)', // Layout & Positioning - Medium screens - 'md:lsd:absolute', + 'lsd:md:absolute', // Sizing - Medium screens - 'md:lsd:w-auto', - // Animations - Motion from end - 'data-[motion=from-end]:slide-in-from-right-52', - // Animations - Motion from start - 'data-[motion=from-start]:slide-in-from-left-52', - 'data-[motion^=from-]:animate-in', - 'data-[motion^=from-]:fade-in', - // Animations - Motion to end - 'data-[motion=to-end]:slide-out-to-right-52', - // Animations - Motion to start - 'data-[motion=to-start]:slide-out-to-left-52', - 'data-[motion^=to-]:animate-out', - 'data-[motion^=to-]:fade-out', + 'lsd:md:w-auto', // Viewport false state - Layout & Positioning - 'group-data-[viewport=false]/navigation-menu:lsd:top-full', - 'group-data-[viewport=false]/navigation-menu:lsd:overflow-hidden', + 'lsd:group-data-[viewport=false]/navigation-menu:top-full', + 'lsd:group-data-[viewport=false]/navigation-menu:overflow-hidden', // Viewport false state - Borders, Shapes & Effects - 'group-data-[viewport=false]/navigation-menu:lsd:rounded-md', - 'group-data-[viewport=false]/navigation-menu:lsd:border', - 'group-data-[viewport=false]/navigation-menu:lsd:shadow-md', + 'lsd:group-data-[viewport=false]/navigation-menu:rounded-md', + 'lsd:group-data-[viewport=false]/navigation-menu:border', + 'lsd:group-data-[viewport=false]/navigation-menu:shadow-md', // Viewport false state - Spacing - 'group-data-[viewport=false]/navigation-menu:lsd:mt-[1.5rem]', + 'lsd:group-data-[viewport=false]/navigation-menu:mt-6', // Viewport false state - Colors & Backgrounds - 'group-data-[viewport=false]/navigation-menu:lsd:bg-[var(--lsd-popover)]', - 'group-data-[viewport=false]/navigation-menu:lsd:text-[var(--lsd-popover-foreground)]', + 'lsd:group-data-[viewport=false]/navigation-menu:bg-lsd-foreground', // Viewport false state - Animations - 'group-data-[viewport=false]/navigation-menu:lsd:duration-200', + 'lsd:group-data-[viewport=false]/navigation-menu:duration-200', // Viewport false state - Open state animations - 'group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in', - 'group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0', - 'group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95', + 'lsd:group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in', + 'lsd:group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0', + 'lsd:group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95', // Viewport false state - Closed state animations - 'group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out', - 'group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0', - 'group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95', + 'lsd:group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out', + 'lsd:group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0', + 'lsd:group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95', // Pseudo-selectors & ARIA - Link focus states - '**:data-[slot=navigation-menu-link]:focus:lsd:ring-0', - '**:data-[slot=navigation-menu-link]:focus:lsd:outline-none', + 'lsd:**:data-[slot=navigation-menu-link]:border-b-0', + 'lsd:**:data-[slot=navigation-menu-link]:focus:ring-0', + 'lsd:**:data-[slot=navigation-menu-link]:focus:outline-none', className )} {...props} diff --git a/packages/lsd/src/components/ui/navigation-menu/NavigationMenuLink.tsx b/packages/lsd/src/components/ui/navigation-menu/NavigationMenuLink.tsx index c75fde49..99c29344 100644 --- a/packages/lsd/src/components/ui/navigation-menu/NavigationMenuLink.tsx +++ b/packages/lsd/src/components/ui/navigation-menu/NavigationMenuLink.tsx @@ -21,31 +21,20 @@ export function NavigationMenuLink({ className, ...props }: NavigationMenuLinkPr 'lsd:flex', 'lsd:flex-col', // Sizing - 'lsd:text-[0.875rem]', + 'lsd:h-9', + 'lsd:text-sm', // Spacing 'lsd:gap-(--lsd-spacing-smallest)', - 'lsd:p-(--lsd-spacing-base)', + 'lsd:px-(--lsd-spacing-base) lsd:py-(--lsd-spacing-smaller)', // Borders, Shapes & Effects - 'lsd:rounded-sm', + 'lsd:border', + 'lsd:cursor-pointer', 'lsd:transition-all', 'lsd:outline-none', - // Interactive States - Hover - 'hover:lsd:bg-[var(--lsd-accent)]', - 'hover:lsd:text-[var(--lsd-accent-foreground)]', - // Interactive States - Focus - 'focus:lsd:bg-[var(--lsd-accent)]', - 'focus:lsd:text-[var(--lsd-accent-foreground)]', - 'focus-visible:lsd:ring-[3px]', - 'focus-visible:lsd:ring-ring/50', - 'focus-visible:lsd:outline-1', - // Interactive States - Active state - 'data-[active=true]:lsd:bg-[var(--lsd-accent)]/50', - 'data-[active=true]:lsd:text-[var(--lsd-accent-foreground)]', - 'data-[active=true]:hover:lsd:bg-[var(--lsd-accent)]', - 'data-[active=true]:focus:lsd:bg-[var(--lsd-accent)]', // Pseudo-selectors & ARIA - SVG styling - "[&_svg:not([class*='size-']):lsd:size-4", - "[&_svg:not([class*='text-'):lsd:text-[var(--lsd-text-secondary)]", + 'lsd:hover:underline lsd:focus:underline', + "lsd:[&_svg:not([class*='size-']):size-4", + "lsd:[&_svg:not([class*='text-'):text-[var(--lsd-text-secondary)]", className )} {...props} diff --git a/packages/lsd/src/components/ui/navigation-menu/NavigationMenuList.tsx b/packages/lsd/src/components/ui/navigation-menu/NavigationMenuList.tsx index 9068bb2d..b8177a91 100644 --- a/packages/lsd/src/components/ui/navigation-menu/NavigationMenuList.tsx +++ b/packages/lsd/src/components/ui/navigation-menu/NavigationMenuList.tsx @@ -27,7 +27,7 @@ export function NavigationMenuList({ className, ...props }: NavigationMenuListPr 'lsd:items-start', 'lsd:justify-start', // Spacing - 'lsd:gap-(--lsd-spacing-1)', + 'lsd:gap-(--lsd-spacing-largest)', // Groups 'lsd:group', className diff --git a/packages/lsd/src/components/ui/navigation-menu/NavigationMenuTrigger.tsx b/packages/lsd/src/components/ui/navigation-menu/NavigationMenuTrigger.tsx index 1a0d7cfd..21b327f1 100644 --- a/packages/lsd/src/components/ui/navigation-menu/NavigationMenuTrigger.tsx +++ b/packages/lsd/src/components/ui/navigation-menu/NavigationMenuTrigger.tsx @@ -8,7 +8,7 @@ export interface NavigationMenuTriggerProps extends React.ComponentProps {} export const navigationMenuTriggerStyle = cva( - 'lsd:group lsd:inline-flex lsd:h-[2.25rem] lsd:w-max lsd:items-center lsd:justify-center lsd:rounded-md lsd:bg-[var(--lsd-background)] lsd:px-4 lsd:py-2 lsd:text-[0.875rem] lsd:font-medium lsd:transition-[color,box-shadow] lsd:outline-none hover:lsd:bg-[var(--lsd-accent)] hover:lsd:text-[var(--lsd-accent-foreground)] focus:lsd:bg-[var(--lsd-accent)] focus:lsd:text-[var(--lsd-accent-foreground)] focus-visible:lsd:ring-[3px] focus-visible:lsd:ring-ring/50 focus-visible:lsd:outline-1 disabled:lsd:pointer-events-none disabled:lsd:opacity-50 data-[state=open]:lsd:bg-[var(--lsd-accent)]/50 data-[state=open]:lsd:text-[var(--lsd-accent-foreground)] data-[state=open]:hover:lsd:bg-[var(--lsd-accent)] data-[state=open]:focus:lsd:bg-[var(--lsd-accent)]' + 'lsd:group lsd:inline-flex lsd:h-9 lsd:w-max lsd:items-center lsd:justify-center lsd:cursor-pointer lsd:px-(--lsd-spacing-base) lsd:py-(--lsd-spacing-smaller) lsd:border lsd:text-sm lsd:transition-[color,box-shadow] lsd:outline-none disabled:lsd:pointer-events-none disabled:lsd:opacity-50' ); /** @@ -31,18 +31,19 @@ export function NavigationMenuTrigger({ > {children}{' '}