chore: add @tailwindcss/postcss and PostCSS setup for Tailwind v4

This commit is contained in:
Jennie Robinson Faber 2025-08-09 13:12:54 +01:00
parent 40c249264c
commit 1cee60953e
17 changed files with 263 additions and 7 deletions

7
app.config.ts Normal file
View file

@ -0,0 +1,7 @@
export default defineAppConfig({
ui: {
primary: "violet",
gray: "neutral",
strategy: "class",
},
});

View file

@ -1,6 +1,20 @@
<template>
<div>
<UApp>
<UNotifications />
<UContainer>
<header class="py-4 flex items-center justify-between">
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-rocket-launch" class="text-primary-500" />
<h1 class="font-semibold">Urgent Tools</h1>
</div>
<ColorModeToggle />
</header>
<NuxtPage />
</UContainer>
<NuxtRouteAnnouncer />
<NuxtWelcome />
</div>
</UApp>
</template>
<script setup lang="ts">
// noop
</script>

View file

@ -0,0 +1,17 @@
<template>
<UButton color="gray" variant="ghost" @click="toggle">
<UIcon :name="icon" class="w-5 h-5" />
</UButton>
</template>
<script setup lang="ts">
const colorMode = useColorMode();
const icon = computed(() =>
colorMode.value === "dark"
? "i-heroicons-moon-20-solid"
: "i-heroicons-sun-20-solid"
);
const toggle = () => {
colorMode.preference = colorMode.value === "dark" ? "light" : "dark";
};
</script>

12
assets/css/main.css Normal file
View file

@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--ui-primary: 237 85% 62%;
}
.dark {
--ui-primary: 237 85% 70%;
}

View file

@ -0,0 +1,25 @@
import { useCounterStore } from '~/stores/counter'
export type AppSnapshot = {
counter: { count: number }
}
export function useFixtureIO() {
const exportAll = (): AppSnapshot => {
const counter = useCounterStore()
return {
counter: { count: counter.count }
}
}
const importAll = (snapshot: AppSnapshot) => {
const counter = useCounterStore()
if (snapshot?.counter) {
counter.$patch({ count: snapshot.counter.count ?? 0 })
}
}
return { exportAll, importAll }
}

View file

@ -1,5 +1,27 @@
import { defineNuxtConfig } from "nuxt/config";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true }
})
compatibilityDate: "2025-07-15",
devtools: { enabled: true },
// Keep SSR on (default) with Nitro server
ssr: true,
// Strict TypeScript
typescript: {
strict: true,
},
// Global CSS (Tailwind for @nuxt/ui)
css: ["~/assets/css/main.css"],
modules: [
"@pinia/nuxt",
"@nuxt/ui",
"@nuxtjs/color-mode",
"@nuxtjs/tailwindcss",
],
// Nuxt UI minimal theme customizations live in app.config.ts
});

1
package-lock.json generated
View file

@ -19,6 +19,7 @@
},
"devDependencies": {
"@playwright/test": "^1.54.2",
"@tailwindcss/postcss": "^4.1.11",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/test-utils": "^2.4.6",
"typescript": "^5.9.2",

View file

@ -7,7 +7,10 @@
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
"postinstall": "nuxt prepare",
"test": "vitest --run",
"test:ui": "vitest",
"test:e2e": "playwright test"
},
"dependencies": {
"@nuxt/ui": "^3.3.0",
@ -22,6 +25,7 @@
},
"devDependencies": {
"@playwright/test": "^1.54.2",
"@tailwindcss/postcss": "^4.1.11",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/test-utils": "^2.4.6",
"typescript": "^5.9.2",

62
pages/index.vue Normal file
View file

@ -0,0 +1,62 @@
<template>
<section class="py-8 space-y-6">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-semibold">Welcome</h2>
<div class="flex gap-2">
<UButton
icon="i-heroicons-arrow-down-tray"
color="gray"
@click="onExport"
>Export JSON</UButton
>
<UButton icon="i-heroicons-arrow-up-tray" color="gray" @click="onImport"
>Import JSON</UButton
>
</div>
</div>
<UCard>
<div class="flex items-center gap-3">
<UButton icon="i-heroicons-plus" @click="store.increment"
>Increment</UButton
>
<div class="text-sm text-gray-500">
Count: {{ store.count }} (x2: {{ store.doubleCount }})
</div>
</div>
</UCard>
</section>
</template>
<script setup lang="ts">
import { useCounterStore } from "~/stores/counter";
const store = useCounterStore();
const onExport = () => {
const data = exportAll();
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "urgent-tools.json";
a.click();
URL.revokeObjectURL(url);
};
const onImport = async () => {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.onchange = async () => {
const file = input.files?.[0];
if (!file) return;
const text = await file.text();
importAll(JSON.parse(text));
};
input.click();
};
const { exportAll, importAll } = useFixtureIO();
</script>

15
playwright.config.ts Normal file
View file

@ -0,0 +1,15 @@
import { defineConfig } from '@playwright/test'
export default defineConfig({
webServer: {
command: 'npm run dev',
port: 3000,
timeout: 60_000,
reuseExistingServer: true
},
use: {
baseURL: 'http://localhost:3000'
}
})

View file

@ -0,0 +1,7 @@
import { defineNuxtPlugin } from "#app";
import { createPersistedState } from "pinia-plugin-persistedstate";
export default defineNuxtPlugin((nuxtApp) => {
// Register persisted state plugin for Pinia on client
nuxtApp.$pinia.use(createPersistedState());
});

View file

@ -0,0 +1,10 @@
import { defineEventHandler } from 'h3'
import { useFixtureIO } from '~/composables/useFixtureIO'
export default defineEventHandler(() => {
// Export snapshot of in-memory state
const { exportAll } = useFixtureIO()
return exportAll()
})

View file

@ -0,0 +1,11 @@
import { defineEventHandler, readBody } from 'h3'
import { useFixtureIO, type AppSnapshot } from '~/composables/useFixtureIO'
export default defineEventHandler(async (event) => {
const body = (await readBody(event)) as AppSnapshot
const { importAll } = useFixtureIO()
importAll(body)
return { ok: true }
})

18
stores/counter.ts Normal file
View file

@ -0,0 +1,18 @@
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count += 1;
},
},
persist: {
paths: ["count"],
},
});

10
tests/e2e/example.spec.ts Normal file
View file

@ -0,0 +1,10 @@
import { test, expect } from '@playwright/test'
test('happy path: home loads and increments', async ({ page }) => {
await page.goto('/')
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible()
await page.getByRole('button', { name: /Increment/ }).click()
await expect(page.getByText(/Count: 1/)).toBeVisible()
})

9
tests/example.spec.ts Normal file
View file

@ -0,0 +1,9 @@
import { describe, it, expect } from 'vitest'
describe('math', () => {
it('adds', () => {
expect(1 + 1).toBe(2)
})
})

12
vitest.config.ts Normal file
View file

@ -0,0 +1,12 @@
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom'
}
})