initial scaffold: vue 3 + vite 8 + shadcn-vue + tailwind 4
Wires pinia, vue-router, vue-i18n, vee-validate/zod, shadcn-vue (reka-ui), tailwind 4 via @tailwindcss/vite. Sample HomeView proves i18n + pinia + tailwind. Optional nostr/lnbits feature folders ship as documentation only — deps install on opt-in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
0054f3ab80
26 changed files with 3347 additions and 0 deletions
12
.editorconfig
Normal file
12
.editorconfig
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
.vite
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
*.log
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
.claude/
|
||||||
6
.prettierrc.json
Normal file
6
.prettierrc.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
74
README.md
Normal file
74
README.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# boilerplate-website
|
||||||
|
|
||||||
|
Opinionated Vue 3 starter — the base every new aiolabs website forks from.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Vue 3.5** + **Vite 8** + **TypeScript 6**
|
||||||
|
- **shadcn-vue** (via `reka-ui` + `class-variance-authority` + `tailwind-merge`) — components.json pre-wired, run `pnpm dlx shadcn-vue@latest add <name>` to copy components in
|
||||||
|
- **Tailwind CSS 4** (via `@tailwindcss/vite` — no `tailwind.config.js`)
|
||||||
|
- **Pinia 3** — sample `useCounterStore` in `src/stores/`
|
||||||
|
- **Vue Router 5** — file in `src/router/index.ts`, lazy-loaded views in `src/views/`
|
||||||
|
- **Vue I18n 11** — `src/i18n/locales/{en,es}.json`
|
||||||
|
- **vee-validate 4** + **zod 3** — form validation primitives (zod pinned to ^3 until `@vee-validate/zod` ships a v4-compatible resolver)
|
||||||
|
- **@lucide/vue** — icon set (`lucide-vue-next` is deprecated upstream)
|
||||||
|
- **ESLint 10** (flat config) + **Prettier 3**
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install
|
||||||
|
pnpm dev # vite dev server
|
||||||
|
pnpm build # type-check + production build
|
||||||
|
pnpm preview # serve dist/
|
||||||
|
pnpm lint
|
||||||
|
pnpm format
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├─ App.vue # router-view shell
|
||||||
|
├─ main.ts # plugin wiring
|
||||||
|
├─ style.css # tailwind + shadcn-vue CSS variables
|
||||||
|
├─ lib/utils.ts # cn() — shadcn-vue's class merger
|
||||||
|
├─ router/index.ts
|
||||||
|
├─ stores/counter.ts # example pinia store
|
||||||
|
├─ i18n/
|
||||||
|
│ ├─ index.ts
|
||||||
|
│ └─ locales/{en,es}.json
|
||||||
|
├─ views/HomeView.vue # proof-of-wiring page (i18n + pinia + tailwind)
|
||||||
|
└─ features/
|
||||||
|
├─ nostr/README.md # opt-in: nostr-tools wiring (see file)
|
||||||
|
└─ lnbits/README.md # opt-in: LNbits payments wiring (see file)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional features
|
||||||
|
|
||||||
|
Both nostr and LNbits live as **documentation-only** folders. The deps
|
||||||
|
aren't installed by default — bundle stays small for sites that don't
|
||||||
|
need them. Each folder's README walks you through enabling.
|
||||||
|
|
||||||
|
- **`src/features/nostr/`** — connect to relays, sign/publish events, contact forms that DM the site owner's npub
|
||||||
|
- **`src/features/lnbits/`** — create invoices, accept Lightning payments via an LNbits instance
|
||||||
|
|
||||||
|
## Versioning strategy
|
||||||
|
|
||||||
|
The boilerplate's `main` branch is **the "tools wired, no content" baseline**. New site = clone (or fork on Forgejo) → start adding content immediately.
|
||||||
|
|
||||||
|
To pull dep refreshes from the boilerplate into an existing site:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git remote add boilerplate forgejo@git.atitlan.io:aiolabs/boilerplate-website.git
|
||||||
|
git fetch boilerplate
|
||||||
|
git merge boilerplate/main
|
||||||
|
```
|
||||||
|
|
||||||
|
When a site diverges enough to no longer benefit from these merges, drop the remote — it becomes its own thing.
|
||||||
|
|
||||||
|
## Keeping deps current
|
||||||
|
|
||||||
|
- The boilerplate gets dep bumps via PRs (Renovate / Dependabot configurable; otherwise periodic `pnpm update --latest` + manual review).
|
||||||
|
- The Vue ecosystem (vue, pinia, router, i18n, vee-validate) versions in lockstep — when one majors, the others usually follow within weeks.
|
||||||
|
- Tailwind 4 + shadcn-vue + reka-ui is the current modern combo. shadcn-vue uses tw-animate-css (not the deprecated tailwindcss-animate plugin).
|
||||||
21
components.json
Normal file
21
components.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://shadcn-vue.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"typescript": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/style.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"framework": "vite",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"composables": "@/composables",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
1
env.d.ts
vendored
Normal file
1
env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
10
eslint.config.js
Normal file
10
eslint.config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
|
||||||
|
export default defineConfigWithVueTs(
|
||||||
|
{ ignores: ['dist', 'node_modules', '.vite'] },
|
||||||
|
pluginVue.configs['flat/recommended'],
|
||||||
|
vueTsConfigs.recommended,
|
||||||
|
skipFormatting,
|
||||||
|
)
|
||||||
13
index.html
Normal file
13
index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Boilerplate Website</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
45
package.json
Normal file
45
package.json
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "boilerplate-website",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --fix",
|
||||||
|
"format": "prettier --write \"src/**/*.{js,ts,vue,json,css,html,md}\"",
|
||||||
|
"type-check": "vue-tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@lucide/vue": "^1.16.0",
|
||||||
|
"@vee-validate/zod": "^4.15.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"reka-ui": "^2.9.8",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"vee-validate": "^4.15.1",
|
||||||
|
"vue": "^3.5.34",
|
||||||
|
"vue-i18n": "^11.4.4",
|
||||||
|
"vue-router": "^5.0.7",
|
||||||
|
"zod": "^3.25.76"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"@tsconfig/node22": "^22.0.5",
|
||||||
|
"@types/node": "^25.9.1",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
|
"@vue/eslint-config-typescript": "^14.7.0",
|
||||||
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"eslint": "^10.4.0",
|
||||||
|
"eslint-plugin-vue": "^10.9.1",
|
||||||
|
"prettier": "^3.8.3",
|
||||||
|
"tailwindcss": "^4.3.0",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
"vite": "^8.0.14",
|
||||||
|
"vue-tsc": "^3.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
2776
pnpm-lock.yaml
generated
Normal file
2776
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
7
src/App.vue
Normal file
7
src/App.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
67
src/features/lnbits/README.md
Normal file
67
src/features/lnbits/README.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# LNbits payments feature (opt-in)
|
||||||
|
|
||||||
|
The boilerplate ships **no payment code by default**. Enable when the
|
||||||
|
site needs to accept Lightning payments via LNbits.
|
||||||
|
|
||||||
|
## Enable
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm add light-bolt11-decoder qrcode
|
||||||
|
pnpm add -D @types/qrcode
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `src/features/lnbits/store.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const useLnbitsStore = defineStore('lnbits', () => {
|
||||||
|
const apiBase = ref<string>(import.meta.env.VITE_LNBITS_URL ?? '')
|
||||||
|
const invoiceKey = ref<string>(import.meta.env.VITE_LNBITS_INVOICE_KEY ?? '')
|
||||||
|
|
||||||
|
async function createInvoice(amountSats: number, memo: string) {
|
||||||
|
const res = await fetch(`${apiBase.value}/api/v1/payments`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Api-Key': invoiceKey.value,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ out: false, amount: amountSats, memo }),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`lnbits invoice creation failed: ${res.status}`)
|
||||||
|
return res.json() as Promise<{
|
||||||
|
payment_hash: string
|
||||||
|
payment_request: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
return { apiBase, invoiceKey, createInvoice }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Configure via `.env.local`:
|
||||||
|
|
||||||
|
```
|
||||||
|
VITE_LNBITS_URL=https://demo.lnbits.com
|
||||||
|
VITE_LNBITS_INVOICE_KEY=<your-invoice-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Use:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { useLnbitsStore } from '@/features/lnbits/store'
|
||||||
|
const lnbits = useLnbitsStore()
|
||||||
|
const inv = await lnbits.createInvoice(100, 'Tip')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The LNbits **invoice key** (read: "incoming payments only") is safe to
|
||||||
|
embed in a frontend build. Never embed the **admin key**.
|
||||||
|
- For polling/streaming payment status, hit `/api/v1/payments/<hash>`
|
||||||
|
(or use the websocket endpoint).
|
||||||
|
- Pair with the `nostr` feature for payment notifications to the site
|
||||||
|
owner's npub.
|
||||||
60
src/features/nostr/README.md
Normal file
60
src/features/nostr/README.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Nostr feature (opt-in)
|
||||||
|
|
||||||
|
The boilerplate ships **no nostr code by default** — bundle stays small.
|
||||||
|
Enable per-site when you need it.
|
||||||
|
|
||||||
|
## Enable
|
||||||
|
|
||||||
|
1. Install dependency:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm add nostr-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `src/features/nostr/store.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { SimplePool, type Event } from 'nostr-tools'
|
||||||
|
|
||||||
|
const DEFAULT_RELAYS = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://nos.lol',
|
||||||
|
'wss://relay.nostr.band',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const useNostrStore = defineStore('nostr', () => {
|
||||||
|
const pool = new SimplePool()
|
||||||
|
const relays = ref<string[]>(DEFAULT_RELAYS)
|
||||||
|
|
||||||
|
async function publish(event: Event) {
|
||||||
|
return Promise.allSettled(pool.publish(relays.value, event))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { relays, publish }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Use:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { useNostrStore } from '@/features/nostr/store'
|
||||||
|
const nostr = useNostrStore()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common patterns
|
||||||
|
|
||||||
|
- **Contact form → owner's npub**: build an event (NIP-04 DM or kind:1
|
||||||
|
with a `p` tag), sign with the visitor's NIP-07 extension (or an
|
||||||
|
ephemeral key for anonymous submissions), publish to the owner's
|
||||||
|
preferred relays.
|
||||||
|
- **Notifications without leaking infra**: pair with the `lnbits` feature
|
||||||
|
so payment events trigger nostr DMs to the owner without exposing the
|
||||||
|
LNbits admin URL.
|
||||||
|
- **NIP-46 remote signing**: see [`fiatjaf/nak`](https://github.com/fiatjaf/nak)
|
||||||
|
and [`kind-0/nsecbunkerd`](https://github.com/kind-0/nsecbunkerd) for
|
||||||
|
bunker-backed signing flows.
|
||||||
|
|
||||||
|
See [nbd-wtf/nostr-tools](https://github.com/nbd-wtf/nostr-tools) for
|
||||||
|
primitives (encryption, signing, relay pools).
|
||||||
10
src/i18n/index.ts
Normal file
10
src/i18n/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import en from './locales/en.json'
|
||||||
|
import es from './locales/es.json'
|
||||||
|
|
||||||
|
export default createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'en',
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
messages: { en, es },
|
||||||
|
})
|
||||||
11
src/i18n/locales/en.json
Normal file
11
src/i18n/locales/en.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"title": "Boilerplate Website"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"heading": "Welcome",
|
||||||
|
"intro": "Edit src/views/HomeView.vue to begin.",
|
||||||
|
"counter": "Count: {n}",
|
||||||
|
"increment": "Increment"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/i18n/locales/es.json
Normal file
11
src/i18n/locales/es.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"title": "Plantilla de Sitio Web"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"heading": "Bienvenido",
|
||||||
|
"intro": "Edita src/views/HomeView.vue para empezar.",
|
||||||
|
"counter": "Cuenta: {n}",
|
||||||
|
"increment": "Incrementar"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx, type ClassValue } from 'clsx'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
16
src/main.ts
Normal file
16
src/main.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import i18n from './i18n'
|
||||||
|
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(i18n)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
16
src/router/index.ts
Normal file
16
src/router/index.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: () => import('@/views/HomeView.vue'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
11
src/stores/counter.ts
Normal file
11
src/stores/counter.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
export const useCounterStore = defineStore('counter', () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const double = computed(() => count.value * 2)
|
||||||
|
function increment() {
|
||||||
|
count.value++
|
||||||
|
}
|
||||||
|
return { count, double, increment }
|
||||||
|
})
|
||||||
83
src/style.css
Normal file
83
src/style.css
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
@import 'tailwindcss';
|
||||||
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: hsl(0 0% 100%);
|
||||||
|
--foreground: hsl(0 0% 3.9%);
|
||||||
|
--card: hsl(0 0% 100%);
|
||||||
|
--card-foreground: hsl(0 0% 3.9%);
|
||||||
|
--popover: hsl(0 0% 100%);
|
||||||
|
--popover-foreground: hsl(0 0% 3.9%);
|
||||||
|
--primary: hsl(0 0% 9%);
|
||||||
|
--primary-foreground: hsl(0 0% 98%);
|
||||||
|
--secondary: hsl(0 0% 96.1%);
|
||||||
|
--secondary-foreground: hsl(0 0% 9%);
|
||||||
|
--muted: hsl(0 0% 96.1%);
|
||||||
|
--muted-foreground: hsl(0 0% 45.1%);
|
||||||
|
--accent: hsl(0 0% 96.1%);
|
||||||
|
--accent-foreground: hsl(0 0% 9%);
|
||||||
|
--destructive: hsl(0 84.2% 60.2%);
|
||||||
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
|
--border: hsl(0 0% 89.8%);
|
||||||
|
--input: hsl(0 0% 89.8%);
|
||||||
|
--ring: hsl(0 0% 3.9%);
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: hsl(0 0% 3.9%);
|
||||||
|
--foreground: hsl(0 0% 98%);
|
||||||
|
--card: hsl(0 0% 3.9%);
|
||||||
|
--card-foreground: hsl(0 0% 98%);
|
||||||
|
--popover: hsl(0 0% 3.9%);
|
||||||
|
--popover-foreground: hsl(0 0% 98%);
|
||||||
|
--primary: hsl(0 0% 98%);
|
||||||
|
--primary-foreground: hsl(0 0% 9%);
|
||||||
|
--secondary: hsl(0 0% 14.9%);
|
||||||
|
--secondary-foreground: hsl(0 0% 98%);
|
||||||
|
--muted: hsl(0 0% 14.9%);
|
||||||
|
--muted-foreground: hsl(0 0% 63.9%);
|
||||||
|
--accent: hsl(0 0% 14.9%);
|
||||||
|
--accent-foreground: hsl(0 0% 98%);
|
||||||
|
--destructive: hsl(0 62.8% 30.6%);
|
||||||
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
|
--border: hsl(0 0% 14.9%);
|
||||||
|
--input: hsl(0 0% 14.9%);
|
||||||
|
--ring: hsl(0 0% 83.1%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--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-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/views/HomeView.vue
Normal file
32
src/views/HomeView.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useCounterStore } from '@/stores/counter'
|
||||||
|
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
const counter = useCounterStore()
|
||||||
|
|
||||||
|
function toggleLocale() {
|
||||||
|
locale.value = locale.value === 'en' ? 'es' : 'en'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="mx-auto max-w-2xl space-y-6 p-8">
|
||||||
|
<h1 class="text-3xl font-bold">{{ t('home.heading') }}</h1>
|
||||||
|
<p class="text-muted-foreground">{{ t('home.intro') }}</p>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p>{{ t('home.counter', { n: counter.count }) }}</p>
|
||||||
|
<button
|
||||||
|
class="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:opacity-90"
|
||||||
|
@click="counter.increment"
|
||||||
|
>
|
||||||
|
{{ t('home.increment') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="text-sm underline" @click="toggleLocale">
|
||||||
|
Switch to {{ locale === 'en' ? 'Español' : 'English' }}
|
||||||
|
</button>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
11
tsconfig.app.json
Normal file
11
tsconfig.app.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.app.json" }]
|
||||||
|
}
|
||||||
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node22/tsconfig.json",
|
||||||
|
"include": ["vite.config.*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": ["node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
13
vite.config.ts
Normal file
13
vite.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue