feat: wire up Tailwind v4 and shadcn-vue token mapping alongside PrimeVue

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Matthew McPeak 2026-07-11 14:15:37 -04:00
parent 8751c150cd
commit 106c214b16
10 changed files with 965 additions and 22 deletions

732
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "reka-nova",
"font": "geist-sans",
"typescript": true,
"tailwind": {
"config": "",
"css": "src/assets/css/tailwind.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"pointer": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"composables": "@/composables"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

View file

@ -38,5 +38,16 @@
"@vitest/coverage-v8": "^2.0.0", "@vitest/coverage-v8": "^2.0.0",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"vite-plugin-dts": "^5.0.2" "vite-plugin-dts": "^5.0.2"
},
"dependencies": {
"@lucide/vue": "^1.24.0",
"@tailwindcss/vite": "^4.3.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"reka-ui": "^2.10.1",
"shadcn-vue": "^2.7.4",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.2",
"tw-animate-css": "^1.4.0"
} }
} }

View file

@ -0,0 +1,174 @@
/* ============================================================
tailwind.css
Toggle themes by setting data-theme on <html>:
<html data-theme="hades">
<html data-theme="apollo">
============================================================ */
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500&display=swap');
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn-vue/tailwind.css";
@theme inline {
--font-sans: var(--font-sans);
--font-serif: var(--font-serif);
--font-mono: var(--font-mono);
--color-background: var(--surface-0);
--color-foreground: var(--text-body);
--color-card: var(--surface-1);
--color-card-foreground: var(--text-body);
--color-popover: var(--surface-2);
--color-popover-foreground: var(--text-body);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-fg);
--color-secondary: var(--neutral);
--color-secondary-foreground: var(--neutral-fg);
--color-muted: var(--surface-2);
--color-muted-foreground: var(--text-muted);
--color-accent: var(--surface-3);
--color-accent-foreground: var(--text-high);
--color-destructive: var(--danger);
--color-destructive-foreground: var(--danger-fg);
--color-info: var(--info);
--color-info-foreground: var(--info-fg);
--color-success: var(--success);
--color-success-foreground: var(--success-fg);
--color-warning: var(--warn);
--color-warning-foreground: var(--warn-fg);
--color-border: var(--border);
--color-input: var(--border);
--color-ring: var(--primary);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.3125rem;
}
/* ── HADES ───────────────────────────────────────────────── */
[data-theme='hades'] {
--surface-0: #18160f;
--surface-1: #28241a;
--surface-2: #2e2a1e;
--surface-3: #322e22;
--track: #32302a;
--border: #4a4538;
--border-hi: #5a5040;
--border-lo: #38342a;
--primary: #d4b278;
--primary-fg: #0e0c08;
--warn: #e08840;
--warn-subtle: #281808;
--warn-border: #4a2c10;
--warn-fg: #fff8e8;
--danger: #7a1a1a;
--danger-subtle: #200808;
--danger-border: #3a1010;
--danger-fg: #fff8e8;
--info: #88aec8;
--info-subtle: #101e2c;
--info-border: #1e3850;
--info-fg: #0a1828;
--neutral: #7a6e60;
--neutral-fg: #fff8e8;
--success: #6aaa7a;
--success-subtle: #0e2214;
--success-border: #1e4228;
--success-fg: #fff8e8;
--text-high: #f4f0e8;
--text-body: #d0c8b8;
--text-muted: #908878;
--text-dim: #807868;
--text-label: #c0b8a8;
--focus-ring: 0 0 0 3px rgba(212, 178, 120, 0.65);
--font-serif: 'Cinzel', Georgia, serif;
--font-sans: 'IBM Plex Sans', system-ui, sans-serif;
--font-mono: 'IBM Plex Mono', monospace;
}
/* ── APOLLO ──────────────────────────────────────────────── */
[data-theme='apollo'] {
--surface-0: #faf7f2;
--surface-1: #f4f0e8;
--surface-2: #eee9df;
--surface-3: #e4ddd0;
--track: #e4ddd0;
--border: #bab2a0;
--border-hi: #c0b8a8;
--border-lo: #ccc4b4;
--primary: #b8860b;
--primary-fg: #fff8e8;
--warn: #9a6020;
--warn-subtle: #fdf3e0;
--warn-border: #e8d0a0;
--warn-fg: #fff8e8;
--danger: #7a1a1a;
--danger-subtle: #faeaea;
--danger-border: #c8a0a0;
--danger-fg: #fff8e8;
--info: #3a5a8a;
--info-subtle: #e8eef8;
--info-border: #b0c4e0;
--info-fg: #fff8e8;
--neutral: #8a8070;
--neutral-fg: #fff8e8;
--success: #5a7a40;
--success-subtle: #eef4e8;
--success-border: #c0d4a8;
--success-fg: #fff8e8;
--text-high: #1a1610;
--text-body: #3a3020;
--text-muted: #5a5040;
--text-dim: #8a8070;
--text-label: #5a5040;
--focus-ring: 0 0 0 3px rgba(184, 134, 11, 0.65);
--font-serif: 'Cinzel', Georgia, serif;
--font-sans: 'IBM Plex Sans', system-ui, sans-serif;
--font-mono: 'IBM Plex Mono', monospace;
}
@layer base {
* {
@apply border-border;
box-sizing: border-box;
}
html {
font-family: var(--font-sans);
font-size: 14px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
@apply bg-background text-foreground;
min-height: 100vh;
}
:focus-visible {
outline: 2px solid transparent;
box-shadow: var(--focus-ring);
}
}

View file

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

View file

@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const cssPath = resolve(process.cwd(), 'src/assets/css/tailwind.css')
describe('tailwind token mapping', () => {
const css = readFileSync(cssPath, 'utf-8')
it('maps --color-primary onto the existing --primary token', () => {
expect(css).toMatch(/--color-primary:\s*var\(--primary\)/)
})
it('maps --color-destructive onto the existing --danger token', () => {
expect(css).toMatch(/--color-destructive:\s*var\(--danger\)/)
})
it('defines the extra info/success/warning color slots', () => {
expect(css).toMatch(/--color-info:\s*var\(--info\)/)
expect(css).toMatch(/--color-success:\s*var\(--success\)/)
expect(css).toMatch(/--color-warning:\s*var\(--warn\)/)
})
it('keeps both Hades and Apollo theme selectors', () => {
expect(css).toContain("[data-theme='hades']")
expect(css).toContain("[data-theme='apollo']")
})
})

View file

@ -2,12 +2,14 @@ import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts' import dts from 'vite-plugin-dts'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ export default defineConfig({
plugins: [vue(), dts({ tsconfigPath: './tsconfig.json', include: ['src/**/*'] })], plugins: [vue(), tailwindcss(), dts({ tsconfigPath: './tsconfig.json', include: ['src/**/*'] })],
resolve: { resolve: {
alias: { alias: {
'~': fileURLToPath(new URL('./src', import.meta.url)), '~': fileURLToPath(new URL('./src', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },
build: { build: {

View file

@ -7,6 +7,7 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
'~': fileURLToPath(new URL('./src', import.meta.url)), '~': fileURLToPath(new URL('./src', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },
test: { test: {

View file

@ -9,6 +9,7 @@
}, },
"dependencies": { "dependencies": {
"@nychthemeron/library": "workspace:*", "@nychthemeron/library": "workspace:*",
"@tailwindcss/vite": "^4.3.2",
"primevue": "^4.5.0", "primevue": "^4.5.0",
"vue": "^3.5.32" "vue": "^3.5.32"
}, },

View file

@ -1,9 +1,10 @@
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue(), tailwindcss()],
resolve: { resolve: {
alias: { alias: {
'~': fileURLToPath(new URL('./src', import.meta.url)), '~': fileURLToPath(new URL('./src', import.meta.url)),