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>
|
||||
<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>
|
||||
|
|
|
|||
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
|
||||
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
1
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
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