chore: add @tailwindcss/postcss and PostCSS setup for Tailwind v4
This commit is contained in:
parent
40c249264c
commit
1cee60953e
17 changed files with 263 additions and 7 deletions
7
app.config.ts
Normal file
7
app.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default defineAppConfig({
|
||||||
|
ui: {
|
||||||
|
primary: "violet",
|
||||||
|
gray: "neutral",
|
||||||
|
strategy: "class",
|
||||||
|
},
|
||||||
|
});
|
||||||
20
app/app.vue
20
app/app.vue
|
|
@ -1,6 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<UApp>
|
||||||
<NuxtRouteAnnouncer />
|
<UNotifications />
|
||||||
<NuxtWelcome />
|
<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>
|
</div>
|
||||||
|
<ColorModeToggle />
|
||||||
|
</header>
|
||||||
|
<NuxtPage />
|
||||||
|
</UContainer>
|
||||||
|
<NuxtRouteAnnouncer />
|
||||||
|
</UApp>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// noop
|
||||||
|
</script>
|
||||||
|
|
|
||||||
17
app/components/ColorModeToggle.vue
Normal file
17
app/components/ColorModeToggle.vue
Normal 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
12
assets/css/main.css
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--ui-primary: 237 85% 62%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--ui-primary: 237 85% 70%;
|
||||||
|
}
|
||||||
|
|
||||||
25
composables/useFixtureIO.ts
Normal file
25
composables/useFixtureIO.ts
Normal 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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,5 +1,27 @@
|
||||||
|
import { defineNuxtConfig } from "nuxt/config";
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: "2025-07-15",
|
||||||
devtools: { enabled: true }
|
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
1
package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.54.2",
|
"@playwright/test": "^1.54.2",
|
||||||
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"test": "vitest --run",
|
||||||
|
"test:ui": "vitest",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/ui": "^3.3.0",
|
"@nuxt/ui": "^3.3.0",
|
||||||
|
|
@ -22,6 +25,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.54.2",
|
"@playwright/test": "^1.54.2",
|
||||||
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
|
|
|
||||||
62
pages/index.vue
Normal file
62
pages/index.vue
Normal 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
15
playwright.config.ts
Normal 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'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
7
plugins/piniaPersistedState.client.ts
Normal file
7
plugins/piniaPersistedState.client.ts
Normal 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());
|
||||||
|
});
|
||||||
10
server/api/fixtures.get.ts
Normal file
10
server/api/fixtures.get.ts
Normal 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()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
11
server/api/fixtures.post.ts
Normal file
11
server/api/fixtures.post.ts
Normal 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
18
stores/counter.ts
Normal 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
10
tests/e2e/example.spec.ts
Normal 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
9
tests/example.spec.ts
Normal 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
12
vitest.config.ts
Normal 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'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue