commit f795cc48b54c1fabb8602b5144e9e950a1579742 Author: Nathan Leclercq Date: Thu Apr 9 12:49:55 2026 +0200 Homepage dashboard : horloge animée, todo, notes, RSS, pomodoro, recherche - Frontend Vue 3 (Composition API) + Vite + Vue Router - Backend Go (stdlib) : API REST todo/notes + proxy RSS + auth token - Docker Compose : SPA nginx + backend + Miniflux + Postgres - Widgets : horloge canvas météo, todo 3 colonnes, notes persistées, agrégateur RSS multi-feeds, pomodoro, recherche DuckDuckGo (Ctrl+K) - Auth : dashboard public, todo/notes protégés par token - Widgets expandables (mode agrandi centré) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d14c11c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +dist +.git +.gitignore +.vite +.env +.env.example +docker-compose.yml +backend +*.log +*.md +clock-lille.html diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e2fe9f2 --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +# Copie ce fichier en `.env` puis remplis les valeurs. +# Le `.env` est gitignored — il ne sera jamais commit. + +# Port hôte pour la homepage (SPA Vue, sert sur :80 dans le container) +HOMEPAGE_PORT=8080 + +# Port hôte pour Miniflux (sert sur :8080 dans le container) +MINIFLUX_PORT=8081 + +# Port hôte pour le backend Go (todos) +BACKEND_PORT=8082 + +# URL publique du backend, vue depuis le NAVIGATEUR de l'utilisateur. +# Utilisée à BUILD TIME par Vite (inlinée dans le bundle JS). +# En local : http://localhost:8082 — en prod : https://api.tondomaine.tld +VITE_BACKEND_URL=http://localhost:8082 + +# Token pour protéger les endpoints notes/todo (seul toi y accèdes) +# Générer avec : openssl rand -base64 24 +API_TOKEN=change-me-to-a-random-token + +# Postgres dédié à Miniflux +MINIFLUX_DB_PASSWORD=change-me-to-a-strong-password + +# Admin Miniflux créé au premier démarrage +MINIFLUX_ADMIN_USER=admin +MINIFLUX_ADMIN_PASSWORD=change-me-too diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0affd6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules +dist +.DS_Store +*.log +.vite +.env +*.tmp +backend/data/ +.claude/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b11cd2d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# --- build stage ----------------------------------------------------------- +FROM node:24-alpine AS build +WORKDIR /app + +# Build-time arg : URL publique du backend (inlinée dans le bundle Vite) +ARG VITE_BACKEND_URL=http://localhost:8082 +ENV VITE_BACKEND_URL=$VITE_BACKEND_URL + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# --- runtime stage --------------------------------------------------------- +FROM nginx:alpine AS runtime + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1270508 --- /dev/null +++ b/Makefile @@ -0,0 +1,68 @@ +.PHONY: help install dev build preview clean docker-build docker-run docker-dev up down restart logs ps backend-up backend-down backend-logs dev-stack + +NODE_VERSION := 24 +IMAGE := homepage:latest +PORT := 8080 + +help: ## Show this help + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +install: ## Install npm deps + npm install + +dev: ## Lance TOUT en mode dev : backend + miniflux + db (détachés) puis vite hot-reload + @test -f .env || (echo "→ create .env from .env.example first" && exit 1) + docker compose up -d --build backend miniflux miniflux-db + @echo "→ services up : backend :$${BACKEND_PORT:-8082}, miniflux :$${MINIFLUX_PORT:-8081}" + npm run dev + +dev-front: ## Uniquement Vite hot-reload (sans toucher aux containers) + npm run dev + +build: ## Build production bundle into dist/ + npm run build + +preview: build ## Serve the production build locally + npm run preview + +docker-build: ## Build the production Docker image + docker build -t $(IMAGE) . + +docker-run: docker-build ## Build + run the prod image on http://localhost:$(PORT) + docker run --rm -p $(PORT):80 $(IMAGE) + +docker-dev: ## Run the Vite dev server inside a throwaway Node container + docker run --rm -it -p 5173:5173 -v $(PWD):/app -w /app \ + node:$(NODE_VERSION)-alpine sh -c "npm install && npm run dev -- --host" + +## --- contrôle individuel des services --- + +backend-up: ## Lance uniquement le backend Go + docker compose up -d --build backend + +backend-down: ## Stoppe le backend Go + docker compose stop backend + +backend-logs: ## Tail des logs du backend + docker compose logs -f backend + +## --- docker compose stack complète (homepage + backend + miniflux + db) --- + +up: ## Lance toute la stack en arrière-plan + @test -f .env || (echo "→ create .env from .env.example first" && exit 1) + docker compose up -d --build + +down: ## Stoppe la stack + docker compose down + +restart: ## Redémarre la stack + docker compose restart + +logs: ## Suit les logs de la stack + docker compose logs -f + +ps: ## État des containers + docker compose ps + +clean: ## Remove node_modules and dist + rm -rf node_modules dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..7da16b9 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Homepage + +Dashboard personnel avec horloge animée, widgets todo/notes, agrégateur RSS, pomodoro et recherche rapide. + +## Stack + +- **Frontend** : Vue 3 (Composition API) + Vite + Vue Router +- **Backend** : Go (stdlib uniquement) — API REST pour todo, notes, proxy RSS +- **Déploiement** : Docker Compose (SPA nginx + backend Go + Miniflux + Postgres) + +## Démarrage rapide + +```bash +# 1. Configurer l'environnement +cp .env.example .env +# Éditer .env : changer les mots de passe et API_TOKEN + +# 2. Lancer tout (backend + frontend dev) +make dev + +# 3. Ouvrir http://localhost:5173 +``` + +## Commandes + +| Commande | Description | +|---|---| +| `make install` | Installer les dépendances npm | +| `make dev` | Lancer le backend (Docker) + frontend (Vite) | +| `make build` | Build de production dans `dist/` | +| `make up` | Lancer toute la stack Docker | +| `make down` | Arrêter la stack | + +## Widgets + +- **Horloge** : Canvas animé avec données météo (Open-Meteo), position solaire, 3 villes +- **Todo** : Vue 3 colonnes (hier/ajd/demain), persisté côté serveur, protégé par token +- **Notes** : Notes éphémères, persistées côté serveur, protégées par token +- **RSS** : Agrégateur multi-feeds (configurable via `src/data/feeds.json`) +- **Pomodoro** : Timer 25/5 min +- **Recherche** : Ctrl+K, DuckDuckGo avec support des bangs +- **Liens** : Panel de navigation (configurable via `src/data/links.json`) + +## Authentification + +Le dashboard est public. Les widgets todo et notes sont protégés par un token (`API_TOKEN` dans `.env`). Cliquer sur le cadenas en bas de page pour se connecter. + +## Configuration + +- **Liens** : `src/data/links.json` +- **Flux RSS** : `src/data/feeds.json` +- **Ports / tokens** : `.env` diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..712bb17 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,2 @@ +*.md +*.test diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..019c277 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,15 @@ +# --- build stage ----------------------------------------------------------- +FROM golang:1.23-alpine AS build +WORKDIR /src +COPY go.mod ./ +COPY *.go ./ +RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /backend . + +# --- runtime stage --------------------------------------------------------- +FROM alpine:latest +RUN mkdir -p /data && adduser -D -u 1000 app && chown app:app /data +USER app +COPY --from=build /backend /backend +EXPOSE 8080 +VOLUME ["/data"] +CMD ["/backend"] diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..7dd9e0b --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,3 @@ +module homepage-backend + +go 1.23 diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..8fa1bdd --- /dev/null +++ b/backend/main.go @@ -0,0 +1,404 @@ +// homepage-backend — tiny REST API for the homepage todo board. +// +// Storage: a single JSON file at $DATA_PATH (default /data/todos.json). +// No external deps — stdlib only. Mounted as a docker volume for persistence. +// +// Routes: +// +// GET /api/health +// GET /api/tasks?from=YYYY-MM-DD&to=YYYY-MM-DD +// POST /api/tasks { title, details?, due_date? } +// PATCH /api/tasks/{id} partial update +// DELETE /api/tasks/{id} +// +// On every list request, undone tasks whose due_date is in the past +// are auto-bumped to today (rollover). + +package main + +import ( + "encoding/json" + "io" + "log" + "net/http" + "os" + "strconv" + "strings" + "sync" + "time" +) + +type Task struct { + ID int64 `json:"id"` + Title string `json:"title"` + Details string `json:"details"` + DueDate string `json:"due_date"` // YYYY-MM-DD + Done bool `json:"done"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type Note struct { + ID int64 `json:"id"` + Text string `json:"text"` + CreatedAt string `json:"created_at"` +} + +type Store struct { + mu sync.Mutex + Tasks []Task `json:"tasks"` + Next int64 `json:"next_id"` + Notes []Note `json:"notes"` + NoteNext int64 `json:"note_next_id"` + path string +} + +func (s *Store) load() error { + s.mu.Lock() + defer s.mu.Unlock() + b, err := os.ReadFile(s.path) + if err != nil { + if os.IsNotExist(err) { + s.Next = 1 + return nil + } + return err + } + if err := json.Unmarshal(b, s); err != nil { + return err + } + if s.Next == 0 { + s.Next = 1 + } + if s.NoteNext == 0 { + s.NoteNext = 1 + } + return nil +} + +// save must be called with the mutex held. +func (s *Store) save() error { + b, err := json.MarshalIndent(s, "", " ") + if err != nil { + return err + } + tmp := s.path + ".tmp" + if err := os.WriteFile(tmp, b, 0644); err != nil { + return err + } + return os.Rename(tmp, s.path) +} + +// rollover must be called with the mutex held. +func (s *Store) rollover() { + today := time.Now().Format("2006-01-02") + now := time.Now().UTC().Format(time.RFC3339) + changed := false + for i := range s.Tasks { + if !s.Tasks[i].Done && s.Tasks[i].DueDate < today { + s.Tasks[i].DueDate = today + s.Tasks[i].UpdatedAt = now + changed = true + } + } + if changed { + _ = s.save() + } +} + +var store *Store +var apiToken string + +func main() { + path := getenv("DATA_PATH", "/data/todos.json") + apiToken = getenv("API_TOKEN", "") + if apiToken == "" { + log.Println("WARNING: API_TOKEN not set — notes/tasks endpoints are unprotected") + } + store = &Store{path: path} + if err := store.load(); err != nil { + log.Fatalf("failed to load store: %v", err) + } + + mux := http.NewServeMux() + mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) + }) + mux.HandleFunc("/api/auth", handleAuth) + mux.HandleFunc("/api/tasks", requireAuth(handleTasks)) + mux.HandleFunc("/api/tasks/", requireAuth(handleTask)) + mux.HandleFunc("/api/notes", requireAuth(handleNotes)) + mux.HandleFunc("/api/notes/", requireAuth(handleNote)) + mux.HandleFunc("/api/rss", handleRSS) + + addr := ":" + getenv("PORT", "8080") + log.Println("homepage-backend listening on", addr, "store:", path) + if err := http.ListenAndServe(addr, cors(mux)); err != nil { + log.Fatal(err) + } +} + +func getenv(k, def string) string { + if v := os.Getenv(k); v != "" { + return v + } + return def +} + +func checkAuth(r *http.Request) bool { + if apiToken == "" { + return true + } + h := r.Header.Get("Authorization") + return h == "Bearer "+apiToken +} + +func requireAuth(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !checkAuth(r) { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + next(w, r) + } +} + +func handleAuth(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var in struct { + Token string `json:"token"` + } + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if apiToken != "" && in.Token != apiToken { + http.Error(w, "invalid token", http.StatusUnauthorized) + return + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"ok":true}`)) +} + +func cors(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + h.ServeHTTP(w, r) + }) +} + +func writeJSON(w http.ResponseWriter, v any) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(v) +} + +func handleTasks(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + from := r.URL.Query().Get("from") + to := r.URL.Query().Get("to") + store.mu.Lock() + store.rollover() + out := make([]Task, 0, len(store.Tasks)) + for _, t := range store.Tasks { + if (from == "" || t.DueDate >= from) && (to == "" || t.DueDate <= to) { + out = append(out, t) + } + } + store.mu.Unlock() + writeJSON(w, out) + + case http.MethodPost: + var in Task + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if strings.TrimSpace(in.Title) == "" { + http.Error(w, "title required", http.StatusBadRequest) + return + } + if in.DueDate == "" { + in.DueDate = time.Now().Format("2006-01-02") + } + now := time.Now().UTC().Format(time.RFC3339) + store.mu.Lock() + in.ID = store.Next + store.Next++ + in.CreatedAt = now + in.UpdatedAt = now + in.Done = false + store.Tasks = append(store.Tasks, in) + _ = store.save() + store.mu.Unlock() + writeJSON(w, in) + + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +// handleRSS proxies a remote RSS/Atom feed (so the SPA can read it without +// hitting CORS). Basic guards: only http/https, response capped at 5MB, +// 10s timeout. +func handleRSS(w http.ResponseWriter, r *http.Request) { + target := r.URL.Query().Get("url") + if target == "" || !(strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://")) { + http.Error(w, "missing or invalid url", http.StatusBadRequest) + return + } + client := &http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + req.Header.Set("User-Agent", "homepage-rss-proxy/1.0") + resp, err := client.Do(req) + if err != nil { + http.Error(w, "upstream fetch failed: "+err.Error(), http.StatusBadGateway) + return + } + defer resp.Body.Close() + if resp.StatusCode >= 400 { + http.Error(w, "upstream "+resp.Status, http.StatusBadGateway) + return + } + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=300") + _, _ = io.Copy(w, io.LimitReader(resp.Body, 5*1024*1024)) +} + +func handleTask(w http.ResponseWriter, r *http.Request) { + idStr := strings.TrimPrefix(r.URL.Path, "/api/tasks/") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.NotFound(w, r) + return + } + + switch r.Method { + case http.MethodPatch: + var in struct { + Title *string `json:"title"` + Details *string `json:"details"` + DueDate *string `json:"due_date"` + Done *bool `json:"done"` + } + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + store.mu.Lock() + defer store.mu.Unlock() + for i := range store.Tasks { + if store.Tasks[i].ID != id { + continue + } + if in.Title != nil { + store.Tasks[i].Title = *in.Title + } + if in.Details != nil { + store.Tasks[i].Details = *in.Details + } + if in.DueDate != nil { + store.Tasks[i].DueDate = *in.DueDate + } + if in.Done != nil { + store.Tasks[i].Done = *in.Done + } + store.Tasks[i].UpdatedAt = time.Now().UTC().Format(time.RFC3339) + _ = store.save() + writeJSON(w, store.Tasks[i]) + return + } + http.NotFound(w, r) + + case http.MethodDelete: + store.mu.Lock() + defer store.mu.Unlock() + for i := range store.Tasks { + if store.Tasks[i].ID == id { + store.Tasks = append(store.Tasks[:i], store.Tasks[i+1:]...) + _ = store.save() + w.WriteHeader(http.StatusNoContent) + return + } + } + http.NotFound(w, r) + + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +// --- Notes --- + +func handleNotes(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + store.mu.Lock() + out := make([]Note, len(store.Notes)) + copy(out, store.Notes) + store.mu.Unlock() + writeJSON(w, out) + + case http.MethodPost: + var in Note + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if strings.TrimSpace(in.Text) == "" { + http.Error(w, "text required", http.StatusBadRequest) + return + } + store.mu.Lock() + in.ID = store.NoteNext + store.NoteNext++ + in.CreatedAt = time.Now().UTC().Format(time.RFC3339) + store.Notes = append([]Note{in}, store.Notes...) + if len(store.Notes) > 50 { + store.Notes = store.Notes[:50] + } + _ = store.save() + store.mu.Unlock() + writeJSON(w, in) + + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func handleNote(w http.ResponseWriter, r *http.Request) { + idStr := strings.TrimPrefix(r.URL.Path, "/api/notes/") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.NotFound(w, r) + return + } + if r.Method != http.MethodDelete { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + store.mu.Lock() + defer store.mu.Unlock() + for i := range store.Notes { + if store.Notes[i].ID == id { + store.Notes = append(store.Notes[:i], store.Notes[i+1:]...) + _ = store.save() + w.WriteHeader(http.StatusNoContent) + return + } + } + http.NotFound(w, r) +} diff --git a/clock-lille.html b/clock-lille.html new file mode 100644 index 0000000..d4901aa --- /dev/null +++ b/clock-lille.html @@ -0,0 +1,717 @@ + + + + + +The Clock — Lille + + + + + +
+ + +
+ anneau météo — 24h glissantes +
+ +
+ +
+ +
+ +
+
+
+
+
+ précip. mm/h — extérieur +
+
+
+ temp. + nuages — centre +
+
+
+ rafales km/h — intérieur +
+
+
+
+ +
+ +
+ −10° +
+ 40°C + température +
+
+ 0 → 100 % + + + + nuages (voile) +
+
+ + + + + + précipitations mm/h +
+
+ 30 +
+ 80 km/h + rafales +
+
+ 970 +
+
+ ··· +
+
+ 1040 hPa + pression (centre) +
+
+
+
+
+
+ sec +
+
+
+ min +
+
+
+ heure +
+
+
+ météo +
+
+
+ jour +
+
+ + soleil +
+
+
+ année +
+
+
+
+ +
+
+ + +
+ + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ba7b299 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,68 @@ +# Stack homepage : SPA Vue + Miniflux (RSS) + Postgres +# +# Lance avec : make up +# Stop avec : make down +# Logs avec : make logs +# +# Configure les variables dans .env (voir .env.example). + +services: + homepage: + build: + context: . + args: + VITE_BACKEND_URL: ${VITE_BACKEND_URL:-http://localhost:8082} + image: homepage:latest + container_name: homepage + restart: unless-stopped + ports: + - "${HOMEPAGE_PORT:-8080}:80" + + backend: + build: ./backend + image: homepage-backend:latest + container_name: homepage-backend + restart: unless-stopped + ports: + - "${BACKEND_PORT:-8082}:8080" + environment: + API_TOKEN: ${API_TOKEN:-} + volumes: + - backend-data:/data + + miniflux: + image: miniflux/miniflux:latest + container_name: miniflux + restart: unless-stopped + depends_on: + miniflux-db: + condition: service_healthy + environment: + DATABASE_URL: postgres://miniflux:${MINIFLUX_DB_PASSWORD}@miniflux-db/miniflux?sslmode=disable + RUN_MIGRATIONS: "1" + CREATE_ADMIN: "1" + ADMIN_USERNAME: ${MINIFLUX_ADMIN_USER} + ADMIN_PASSWORD: ${MINIFLUX_ADMIN_PASSWORD} + ports: + - "${MINIFLUX_PORT:-8081}:8080" + + miniflux-db: + image: postgres:16-alpine + container_name: miniflux-db + restart: unless-stopped + environment: + POSTGRES_USER: miniflux + POSTGRES_PASSWORD: ${MINIFLUX_DB_PASSWORD} + POSTGRES_DB: miniflux + volumes: + - miniflux-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "miniflux"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 20s + +volumes: + miniflux-db-data: + backend-data: diff --git a/index.html b/index.html new file mode 100644 index 0000000..daa1266 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Homepage + + +
+ + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..c5aee9b --- /dev/null +++ b/nginx.conf @@ -0,0 +1,17 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA fallback for Vue Router (history mode) + location / { + try_files $uri $uri/ /index.html; + } + + # Long-term cache for built assets (hashed filenames) + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f935ecd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1546 @@ +{ + "name": "homepage", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "homepage", + "version": "0.1.0", + "dependencies": { + "vue": "^3.5.32", + "vue-router": "^5.0.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.5", + "vite": "^8.0.7" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.123.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.123.0.tgz", + "integrity": "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.13.tgz", + "integrity": "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.13.tgz", + "integrity": "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.13.tgz", + "integrity": "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.13.tgz", + "integrity": "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.13.tgz", + "integrity": "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.13.tgz", + "integrity": "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.13.tgz", + "integrity": "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.13.tgz", + "integrity": "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.13.tgz", + "integrity": "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.1", + "@emnapi/runtime": "1.9.1", + "@napi-rs/wasm-runtime": "^1.1.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.13.tgz", + "integrity": "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.13.tgz", + "integrity": "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue-macros/common": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz", + "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==", + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz", + "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.1" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", + "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", + "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "vue": "3.5.32" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz", + "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "ast-kit": "^2.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.13.tgz", + "integrity": "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.123.0", + "@rolldown/pluginutils": "1.0.0-rc.13" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.13", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.13", + "@rolldown/binding-darwin-x64": "1.0.0-rc.13", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.13", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.13", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.13", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.13", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.13", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.13", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.13" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", + "dev": true, + "license": "MIT" + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/unplugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/vite": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.7.tgz", + "integrity": "sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.13", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.4.tgz", + "integrity": "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.28.6", + "@vue-macros/common": "^3.1.1", + "@vue/devtools-api": "^8.0.6", + "ast-walker-scope": "^0.8.3", + "chokidar": "^5.0.0", + "json5": "^2.2.3", + "local-pkg": "^1.1.2", + "magic-string": "^0.30.21", + "mlly": "^1.8.0", + "muggle-string": "^0.4.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "scule": "^1.3.0", + "tinyglobby": "^0.2.15", + "unplugin": "^3.0.0", + "unplugin-utils": "^0.3.1", + "yaml": "^2.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.17", + "pinia": "^3.0.4", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + } + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..94508ea --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "homepage", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview --host" + }, + "dependencies": { + "vue": "^3.5.32", + "vue-router": "^5.0.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.5", + "vite": "^8.0.7" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..692742b --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..13ee9ea --- /dev/null +++ b/src/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/src/components/ClockCanvas.vue b/src/components/ClockCanvas.vue new file mode 100644 index 0000000..2e5fdca --- /dev/null +++ b/src/components/ClockCanvas.vue @@ -0,0 +1,388 @@ + + + + + diff --git a/src/components/ClockControls.vue b/src/components/ClockControls.vue new file mode 100644 index 0000000..a42049c --- /dev/null +++ b/src/components/ClockControls.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/src/components/ClockLegend.vue b/src/components/ClockLegend.vue new file mode 100644 index 0000000..95fe61f --- /dev/null +++ b/src/components/ClockLegend.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/src/components/EphemeralNotes.vue b/src/components/EphemeralNotes.vue new file mode 100644 index 0000000..a10bd91 --- /dev/null +++ b/src/components/EphemeralNotes.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/src/components/NavPanel.vue b/src/components/NavPanel.vue new file mode 100644 index 0000000..0eff065 --- /dev/null +++ b/src/components/NavPanel.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/src/components/PomodoroTimer.vue b/src/components/PomodoroTimer.vue new file mode 100644 index 0000000..6f5676f --- /dev/null +++ b/src/components/PomodoroTimer.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/src/components/QuickSearch.vue b/src/components/QuickSearch.vue new file mode 100644 index 0000000..9b00cb1 --- /dev/null +++ b/src/components/QuickSearch.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/src/components/RSSFeed.vue b/src/components/RSSFeed.vue new file mode 100644 index 0000000..f9f37d7 --- /dev/null +++ b/src/components/RSSFeed.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/src/components/TodoBoard.vue b/src/components/TodoBoard.vue new file mode 100644 index 0000000..7a3d03b --- /dev/null +++ b/src/components/TodoBoard.vue @@ -0,0 +1,518 @@ + + + + + diff --git a/src/composables/useAuth.js b/src/composables/useAuth.js new file mode 100644 index 0000000..645d6ee --- /dev/null +++ b/src/composables/useAuth.js @@ -0,0 +1,63 @@ +// useAuth — simple token-based auth for private widgets (notes, todo). +// Token stored in localStorage. All authenticated API calls must use getHeaders(). + +import { ref } from 'vue' + +const BACKEND = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8082' +const STORAGE_KEY = 'homepage:token' + +const authenticated = ref(false) +const token = ref('') + +// Restore from localStorage and verify token is still valid +const saved = localStorage.getItem(STORAGE_KEY) +if (saved) { + token.value = saved + // Verify asynchronously — authenticated stays false until confirmed + fetch(`${BACKEND}/api/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token: saved }), + }) + .then((r) => { + if (r.ok) { + authenticated.value = true + } else { + token.value = '' + localStorage.removeItem(STORAGE_KEY) + } + }) + .catch(() => { + // Backend unreachable — keep token, try later + authenticated.value = true + }) +} + +async function login(pwd) { + const r = await fetch(`${BACKEND}/api/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token: pwd }), + }) + if (!r.ok) { + throw new Error('invalid') + } + token.value = pwd + authenticated.value = true + localStorage.setItem(STORAGE_KEY, pwd) +} + +function logout() { + token.value = '' + authenticated.value = false + localStorage.removeItem(STORAGE_KEY) +} + +function getHeaders() { + if (!token.value) return {} + return { Authorization: `Bearer ${token.value}` } +} + +export function useAuth() { + return { authenticated, token, login, logout, getHeaders } +} diff --git a/src/composables/useExpandable.js b/src/composables/useExpandable.js new file mode 100644 index 0000000..4a07d62 --- /dev/null +++ b/src/composables/useExpandable.js @@ -0,0 +1,28 @@ +// useExpandable — toggle between compact and expanded widget state. +// Escape key collapses. Returns { expanded, toggle, collapse }. + +import { ref, onMounted, onBeforeUnmount } from 'vue' + +export function useExpandable() { + const expanded = ref(false) + + function toggle() { + expanded.value = !expanded.value + } + + function collapse() { + expanded.value = false + } + + function onKey(e) { + if (expanded.value && e.key === 'Escape') { + e.preventDefault() + collapse() + } + } + + onMounted(() => window.addEventListener('keydown', onKey)) + onBeforeUnmount(() => window.removeEventListener('keydown', onKey)) + + return { expanded, toggle, collapse } +} diff --git a/src/composables/useNotes.js b/src/composables/useNotes.js new file mode 100644 index 0000000..de0394b --- /dev/null +++ b/src/composables/useNotes.js @@ -0,0 +1,48 @@ +// useNotes — client for the homepage-backend notes API. +// Persistent server-side storage (shared across devices). + +import { ref } from 'vue' +import { useAuth } from './useAuth.js' + +const BACKEND = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8082' +const { getHeaders } = useAuth() + +const notes = ref([]) +const loading = ref(false) +const error = ref(null) + +async function fetchNotes() { + loading.value = true + try { + const r = await fetch(`${BACKEND}/api/notes`, { headers: getHeaders() }) + if (!r.ok) throw new Error(`HTTP ${r.status}`) + notes.value = await r.json() + error.value = null + } catch (e) { + error.value = e.message + } finally { + loading.value = false + } +} + +async function createNote(text) { + const r = await fetch(`${BACKEND}/api/notes`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...getHeaders() }, + body: JSON.stringify({ text }), + }) + if (!r.ok) throw new Error(`HTTP ${r.status}`) + const created = await r.json() + notes.value.unshift(created) + return created +} + +async function deleteNote(id) { + const r = await fetch(`${BACKEND}/api/notes/${id}`, { method: 'DELETE', headers: getHeaders() }) + if (!r.ok && r.status !== 204) throw new Error(`HTTP ${r.status}`) + notes.value = notes.value.filter((n) => n.id !== id) +} + +export function useNotes() { + return { notes, loading, error, fetchNotes, createNote, deleteNote } +} diff --git a/src/composables/useParisTime.js b/src/composables/useParisTime.js new file mode 100644 index 0000000..b54f70c --- /dev/null +++ b/src/composables/useParisTime.js @@ -0,0 +1,88 @@ +// Paris timezone helpers + simulated/live clock state. +// +// State is module-level (singleton) so all components share the same +// `simDate` / `isLive` without prop drilling. + +import { ref, computed } from 'vue' + +export const TZ = 'Europe/Paris' + +export function parisComponents(d) { + const parts = new Intl.DateTimeFormat('fr-FR', { + timeZone: TZ, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }).formatToParts(d) + const get = (t) => parseInt(parts.find((x) => x.type === t).value, 10) + return { + year: get('year'), + month: get('month'), + day: get('day'), + hour: get('hour'), + minute: get('minute'), + second: get('second'), + } +} + +export function parisDateStr(d) { + const p = parisComponents(d) + const pad = (n) => String(n).padStart(2, '0') + return `${p.year}-${pad(p.month)}-${pad(p.day)}` +} + +export function toInputStr(d) { + const p = parisComponents(d) + const pad = (n) => String(n).padStart(2, '0') + return `${p.year}-${pad(p.month)}-${pad(p.day)}T${pad(p.hour)}:${pad(p.minute)}:${pad(p.second)}` +} + +export function getParisOffsetMs(d) { + const fmt = (tz) => + new Intl.DateTimeFormat('en-GB', { + timeZone: tz, + hour: '2-digit', + minute: '2-digit', + hour12: false, + }).format(d) + const [uh, um] = fmt('UTC').split(':').map(Number) + const [ph, pm] = fmt(TZ).split(':').map(Number) + return ((ph * 60 + pm) - (uh * 60 + um)) * 60000 +} + +export function parisStrToDate(val) { + const naive = new Date(val + 'Z') + return new Date(naive.getTime() - getParisOffsetMs(naive)) +} + +// --- Shared singleton state --- +const simDate = ref(null) +const isLive = ref(true) + +function getNow() { + return isLive.value ? new Date() : simDate.value +} + +function setSimulated(date) { + simDate.value = date + isLive.value = false +} + +function goLive() { + simDate.value = null + isLive.value = true +} + +export function useClockState() { + return { + simDate: computed(() => simDate.value), + isLive: computed(() => isLive.value), + getNow, + setSimulated, + goLive, + } +} diff --git a/src/composables/useRSS.js b/src/composables/useRSS.js new file mode 100644 index 0000000..e92fcf4 --- /dev/null +++ b/src/composables/useRSS.js @@ -0,0 +1,96 @@ +// useRSS — fetch RSS/Atom feeds via the backend proxy and parse them +// in the browser using DOMParser. Handles RSS 2.0, RSS 1.0 (RDF), Atom. + +import { ref } from 'vue' + +const BACKEND = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8082' + +function pickText(el, ...selectors) { + for (const sel of selectors) { + const node = el.querySelector(sel) + if (node?.textContent) return node.textContent.trim() + } + return '' +} + +function pickLink(el) { + // Atom : + const atomLink = [...el.querySelectorAll('link')].find((l) => l.getAttribute('href')) + if (atomLink) return atomLink.getAttribute('href') + // RSS 2.0 / 1.0 : text + const rssLink = el.querySelector('link') + return rssLink?.textContent?.trim() || '' +} + +export function parseFeed(xmlText, label) { + // Some servers return HTML error pages instead of XML + let trimmed = xmlText.trimStart() + if (trimmed.startsWith('', '', '']) { + const idx = trimmed.indexOf(end) + if (idx !== -1) { + trimmed = trimmed.slice(0, idx + end.length) + break + } + } + const doc = new DOMParser().parseFromString(trimmed, 'application/xml') + const err = doc.querySelector('parsererror') + if (err) { + throw new Error(`${label || 'feed'}: XML parse error — ${err.textContent.slice(0, 80)}`) + } + // Items can be (RSS) or (Atom) + const nodes = [...doc.querySelectorAll('item, entry')] + return nodes.map((n) => ({ + title: pickText(n, 'title'), + link: pickLink(n), + date: pickText(n, 'pubDate', 'published', 'updated', 'date'), + })) +} + +async function fetchOne(feed) { + const r = await fetch(`${BACKEND}/api/rss?url=${encodeURIComponent(feed.url)}`) + if (!r.ok) throw new Error(`${feed.label || feed.url}: HTTP ${r.status}`) + const text = await r.text() + return parseFeed(text, feed.label || feed.url).map((it) => ({ ...it, source: feed.label || '' })) +} + +export function useRSS() { + const items = ref([]) + const loading = ref(false) + const error = ref(null) + + async function load(url) { + return loadMultiple([{ url, label: '' }]) + } + + async function loadMultiple(feeds) { + loading.value = true + error.value = null + try { + const results = await Promise.allSettled(feeds.map(fetchOne)) + const merged = [] + const errors = [] + for (let i = 0; i < results.length; i++) { + if (results[i].status === 'fulfilled') { + merged.push(...results[i].value) + } else { + errors.push(results[i].reason.message) + } + } + merged.sort((a, b) => { + const da = new Date(a.date).getTime() || 0 + const db = new Date(b.date).getTime() || 0 + return db - da + }) + items.value = merged + if (errors.length) error.value = errors.join(' · ') + } finally { + loading.value = false + } + } + + return { items, loading, error, load, loadMultiple } +} diff --git a/src/composables/useTodos.js b/src/composables/useTodos.js new file mode 100644 index 0000000..e2d2994 --- /dev/null +++ b/src/composables/useTodos.js @@ -0,0 +1,63 @@ +// useTodos — small client for the homepage-backend tasks API. +// +// Module-level reactive state shared across components. + +import { ref } from 'vue' +import { useAuth } from './useAuth.js' + +const BACKEND = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8082' +const { getHeaders } = useAuth() + +const tasks = ref([]) +const loading = ref(false) +const error = ref(null) + +async function fetchRange(from, to) { + loading.value = true + try { + const r = await fetch(`${BACKEND}/api/tasks?from=${from}&to=${to}`, { headers: getHeaders() }) + if (!r.ok) throw new Error(`HTTP ${r.status}`) + tasks.value = await r.json() + error.value = null + } catch (e) { + error.value = e.message + tasks.value = [] + } finally { + loading.value = false + } +} + +async function createTask(payload) { + const r = await fetch(`${BACKEND}/api/tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...getHeaders() }, + body: JSON.stringify(payload), + }) + if (!r.ok) throw new Error(`HTTP ${r.status}`) + const created = await r.json() + tasks.value.push(created) + return created +} + +async function updateTask(id, patch) { + const r = await fetch(`${BACKEND}/api/tasks/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', ...getHeaders() }, + body: JSON.stringify(patch), + }) + if (!r.ok) throw new Error(`HTTP ${r.status}`) + const updated = await r.json() + const idx = tasks.value.findIndex((t) => t.id === id) + if (idx >= 0) tasks.value[idx] = updated + return updated +} + +async function deleteTask(id) { + const r = await fetch(`${BACKEND}/api/tasks/${id}`, { method: 'DELETE', headers: getHeaders() }) + if (!r.ok && r.status !== 204) throw new Error(`HTTP ${r.status}`) + tasks.value = tasks.value.filter((t) => t.id !== id) +} + +export function useTodos() { + return { tasks, loading, error, fetchRange, createTask, updateTask, deleteTask } +} diff --git a/src/composables/useWeather.js b/src/composables/useWeather.js new file mode 100644 index 0000000..3bcacd4 --- /dev/null +++ b/src/composables/useWeather.js @@ -0,0 +1,178 @@ +// Weather state for Lille (Open-Meteo). +// Module-level singleton: targets are fetched, smoothed values are +// interpolated each frame and read by the canvas. + +import { reactive } from 'vue' +import { TZ, parisComponents } from './useParisTime.js' + +const LSPEED = 0.04 // interpolation speed per frame (~60fps → ~1.5s) +const lerp = (a, b, t) => a + (b - a) * t + +const weatherTarget = { + temp: null, + cloudcover: 0, + precip: 0, + pressure: 1013, + precipForecast: [], + tempForecast: [], + cloudForecast: [], + gustForecast: [], + sunriseHour: 6, + sunsetHour: 20, +} + +// Smoothed (drawn) values — reactive so the legend can read them. +const weatherSmooth = reactive({ + temp: null, + cloudcover: 0, + precip: 0, + pressure: 1013, + precipForecast: Array(24).fill(0), + tempForecast: Array(24).fill(null), + cloudForecast: Array(24).fill(0), + gustForecast: Array(24).fill(0), + sunriseHour: 6, + sunsetHour: 20, +}) + +let lastFetchKey = null +let lastFetchTs = 0 + +export function resetFetchCache() { + lastFetchKey = null +} + +export function smoothWeather() { + const s = weatherSmooth + const tg = weatherTarget + if (tg.temp !== null) { + s.temp = s.temp === null ? tg.temp : lerp(s.temp, tg.temp, LSPEED) + } + s.cloudcover = lerp(s.cloudcover, tg.cloudcover, LSPEED) + s.precip = lerp(s.precip, tg.precip, LSPEED) + s.pressure = lerp(s.pressure, tg.pressure, LSPEED) + s.sunriseHour = lerp(s.sunriseHour, tg.sunriseHour, LSPEED) + s.sunsetHour = lerp(s.sunsetHour, tg.sunsetHour, LSPEED) + for (let h = 0; h < 24; h++) { + s.precipForecast[h] = lerp(s.precipForecast[h] || 0, tg.precipForecast[h] || 0, LSPEED) + s.cloudForecast[h] = lerp(s.cloudForecast[h] || 0, tg.cloudForecast[h] || 0, LSPEED) + s.gustForecast[h] = lerp(s.gustForecast[h] || 0, tg.gustForecast[h] || 0, LSPEED) + if (tg.tempForecast[h] !== null && tg.tempForecast[h] !== undefined) { + s.tempForecast[h] = + s.tempForecast[h] === null + ? tg.tempForecast[h] + : lerp(s.tempForecast[h], tg.tempForecast[h], LSPEED) + } + } +} + +export async function fetchWeather(d) { + const p = parisComponents(d) + const pad = (n) => String(n).padStart(2, '0') + const dateStr = `${p.year}-${pad(p.month)}-${pad(p.day)}` + const key = `${dateStr}T${pad(p.hour)}` + const now10 = Date.now() + if (key === lastFetchKey && now10 - lastFetchTs < 10 * 60 * 1000) return + lastFetchKey = key + lastFetchTs = now10 + + const diffDays = (d.getTime() - Date.now()) / 86400000 + const tomorrow = new Date(d.getTime() + 86400000) + const tp = parisComponents(tomorrow) + const tDateStr = `${tp.year}-${pad(tp.month)}-${pad(tp.day)}` + + try { + let url, hourly, timeArr, daily + if (diffDays > 1) { + url = + `https://api.open-meteo.com/v1/forecast?latitude=50.633&longitude=3.067` + + `&hourly=temperature_2m,cloudcover,precipitation,surface_pressure,windgusts_10m` + + `&daily=sunrise,sunset` + + `&start_date=${dateStr}&end_date=${tDateStr}&timezone=${encodeURIComponent(TZ)}` + const r = await fetch(url) + const dd = await r.json() + hourly = dd.hourly + timeArr = dd.hourly.time + daily = dd.daily + weatherTarget.temp = hourly.temperature_2m?.[p.hour] ?? null + weatherTarget.cloudcover = hourly.cloudcover?.[p.hour] ?? 0 + weatherTarget.precip = hourly.precipitation?.[p.hour] ?? 0 + weatherTarget.pressure = hourly.surface_pressure?.[p.hour] ?? 1013 + } else if (diffDays < -1) { + url = + `https://archive-api.open-meteo.com/v1/archive?latitude=50.633&longitude=3.067` + + `&hourly=temperature_2m,cloudcover,precipitation,surface_pressure,windgusts_10m` + + `&daily=sunrise,sunset` + + `&start_date=${dateStr}&end_date=${tDateStr}&timezone=${encodeURIComponent(TZ)}` + const r = await fetch(url) + const dd = await r.json() + hourly = dd.hourly + timeArr = dd.hourly.time + daily = dd.daily + weatherTarget.temp = hourly.temperature_2m?.[p.hour] ?? null + weatherTarget.cloudcover = hourly.cloudcover?.[p.hour] ?? 0 + weatherTarget.precip = hourly.precipitation?.[p.hour] ?? 0 + weatherTarget.pressure = hourly.surface_pressure?.[p.hour] ?? 1013 + } else { + url = + `https://api.open-meteo.com/v1/forecast?latitude=50.633&longitude=3.067` + + `¤t_weather=true` + + `&hourly=temperature_2m,cloudcover,precipitation,surface_pressure,windgusts_10m` + + `&daily=sunrise,sunset` + + `&forecast_days=3&timezone=${encodeURIComponent(TZ)}` + const r = await fetch(url) + const dd = await r.json() + hourly = dd.hourly + timeArr = dd.hourly.time + daily = dd.daily + weatherTarget.temp = + dd.current_weather?.temperature ?? hourly.temperature_2m?.[p.hour] ?? null + weatherTarget.cloudcover = hourly.cloudcover?.[p.hour] ?? 0 + weatherTarget.precip = hourly.precipitation?.[p.hour] ?? 0 + weatherTarget.pressure = hourly.surface_pressure?.[p.hour] ?? 1013 + } + + if (daily?.sunrise?.[0]) { + const sr = daily.sunrise[0].split('T')[1] || daily.sunrise[0] + const [sh, sm] = sr.split(':').map(Number) + weatherTarget.sunriseHour = sh + sm / 60 + } + if (daily?.sunset?.[0]) { + const ss = daily.sunset[0].split('T')[1] || daily.sunset[0] + const [sh, sm] = ss.split(':').map(Number) + weatherTarget.sunsetHour = sh + sm / 60 + } + + const currentTimeStr = `${dateStr}T${pad(p.hour)}:00` + const startIdx = timeArr ? timeArr.indexOf(currentTimeStr) : -1 + if (startIdx >= 0) { + weatherTarget.precipForecast = Array.from( + { length: 24 }, + (_, i) => hourly.precipitation?.[startIdx + i] ?? 0 + ) + weatherTarget.tempForecast = Array.from( + { length: 24 }, + (_, i) => hourly.temperature_2m?.[startIdx + i] ?? null + ) + weatherTarget.cloudForecast = Array.from( + { length: 24 }, + (_, i) => hourly.cloudcover?.[startIdx + i] ?? 0 + ) + weatherTarget.gustForecast = Array.from( + { length: 24 }, + (_, i) => hourly.windgusts_10m?.[startIdx + i] ?? 0 + ) + } + } catch (e) { + console.warn('weather fetch failed', e) + } +} + +export function useWeather() { + return { + weatherSmooth, + fetchWeather, + smoothWeather, + resetFetchCache, + } +} diff --git a/src/data/feeds.json b/src/data/feeds.json new file mode 100644 index 0000000..a714673 --- /dev/null +++ b/src/data/feeds.json @@ -0,0 +1,89 @@ +{ + "feeds": [ + { + "label": "zwindler", + "name": "Zwindler's Reflection", + "url": "https://blog.zwindler.fr/index.xml" + }, + { + "label": "jvns", + "name": "Julia Evans", + "url": "https://jvns.ca/atom.xml" + }, + { + "label": "vicki", + "name": "Vicki Boykis", + "url": "https://vickiboykis.com/index.xml" + }, + { + "label": "eli", + "name": "Eli Bendersky", + "url": "https://eli.thegreenplace.net/feeds/all.atom.xml" + }, + { + "label": "noted", + "name": "Noted - Self-Hosting", + "url": "https://noted.lol/rss/" + }, + { + "label": "mcorbin", + "name": "mcorbin", + "url": "https://www.mcorbin.fr/feed.xml" + }, + { + "label": "seb666", + "name": "Seboss666", + "url": "https://blog.seboss666.info/feed" + }, + { + "label": "iximiuz", + "name": "Ivan Velichko", + "url": "https://iximiuz.com/feed.rss" + }, + { + "label": "chollinger", + "name": "Christian Hollinger", + "url": "https://chollinger.com/blog/index.xml" + }, + { + "label": "rmoff", + "name": "Robin Moffatt", + "url": "https://rmoff.net/index.xml" + }, + { + "label": "ssp", + "name": "Simon Spati", + "url": "https://www.ssp.sh/index.xml" + }, + { + "label": "geerling", + "name": "Jeff Geerling", + "url": "https://www.jeffgeerling.com/blog.xml" + }, + { + "label": "dataguy", + "name": "Confessions of a Data Guy", + "url": "https://www.confessionsofadataguy.com/feed/" + }, + { + "label": "fasterth", + "name": "fasterthanlime", + "url": "https://fasterthanli.me/index.xml" + }, + { + "label": "bitfield", + "name": "Bitfield Consulting (Go)", + "url": "https://bitfieldconsulting.com/posts?format=rss" + }, + { + "label": "kobzol", + "name": "Kobzol (Rust)", + "url": "https://kobzol.github.io/feed.xml" + }, + { + "label": "momjian", + "name": "Bruce Momjian (PostgreSQL)", + "url": "https://momjian.us/main/rss/pgblog.xml" + } + ] +} diff --git a/src/data/links.json b/src/data/links.json new file mode 100644 index 0000000..57e1c89 --- /dev/null +++ b/src/data/links.json @@ -0,0 +1,63 @@ +{ + "groups": [ + { + "title": "site", + "items": [ + { + "label": "accueil", + "url": "https://nathan.leclercq.spacesheep.ovh/" + }, + { + "label": "projets", + "url": "https://nathan.leclercq.spacesheep.ovh/projets/" + }, + { + "label": "blog", + "url": "https://nathan.leclercq.spacesheep.ovh/posts/" + }, + { + "label": "about", + "url": "https://nathan.leclercq.spacesheep.ovh/about/" + }, + { + "label": "cv", + "url": "https://nathan.leclercq.spacesheep.ovh/cv_fr/" + } + ] + }, + { + "title": "services", + "items": [ + { + "label": "gitea", + "url": "https://git.spacesheep.ovh/" + }, + { + "label": "vaultwarden", + "url": "https://vaultwarden.spacesheep.ovh/" + } + ] + }, + { + "title": "social", + "items": [ + { + "label": "github", + "url": "https://github.com/nathanlq" + }, + { + "label": "linkedin", + "url": "https://www.linkedin.com/in/nathan-leclercq-51292014b/" + }, + { + "label": "email", + "url": "mailto:nathan.leclercq9@protonmail.com" + } + ] + }, + { + "title": "shortcuts", + "items": [] + } + ] +} \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..745b80d --- /dev/null +++ b/src/main.js @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' +import './styles/global.css' + +createApp(App).use(router).mount('#app') diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..c46c50f --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,17 @@ +import { createRouter, createWebHistory } from 'vue-router' +import HomeView from '../views/HomeView.vue' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + name: 'home', + component: HomeView, + }, + // Ajouter ici de nouvelles pages : + // { path: '/about', name: 'about', component: () => import('../views/AboutView.vue') }, + ], +}) + +export default router diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..6077d6c --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,42 @@ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body, +#app { + width: 100%; + height: 100%; + overflow: hidden; + font-family: 'Courier New', monospace; +} + + +/* --- minimal scrollbars to match the dashboard aesthetic --- */ + +* { + scrollbar-width: thin; + scrollbar-color: rgba(128, 128, 128, 0.3) transparent; +} + +*::-webkit-scrollbar { + width: 6px; + height: 6px; +} +*::-webkit-scrollbar-track { + background: transparent; +} +*::-webkit-scrollbar-thumb { + background: rgba(128, 128, 128, 0.28); + border-radius: 3px; +} +*::-webkit-scrollbar-thumb:hover { + background: rgba(128, 128, 128, 0.55); +} +*::-webkit-scrollbar-corner { + background: transparent; +} diff --git a/src/utils/astro.js b/src/utils/astro.js new file mode 100644 index 0000000..60f762c --- /dev/null +++ b/src/utils/astro.js @@ -0,0 +1,16 @@ +// Astro helpers + city list +// Extracted as-is from clock-lille.html + +export const CITIES = [ + { label: 'LIL', lon: 3.067, lat: 50.63 }, + { label: 'TYO', lon: 139.69, lat: 35.68 }, + { label: 'NYC', lon: -74.006, lat: 40.71 }, +] + +export function solarDeclination(doy) { + return (-23.45 * Math.cos((2 * Math.PI / 365) * (doy + 10)) * Math.PI) / 180 +} + +export function dayOfYear(d) { + return Math.floor((d - new Date(d.getFullYear(), 0, 0)) / 86400000) +} diff --git a/src/utils/colors.js b/src/utils/colors.js new file mode 100644 index 0000000..73a0a1e --- /dev/null +++ b/src/utils/colors.js @@ -0,0 +1,62 @@ +// Color helpers (time of day, temperature) +// Extracted as-is from clock-lille.html + +export function timeColor(sh, alpha) { + const h = ((sh % 24) + 24) % 24 + let r, g, b + if (h < 6) { + const t = h / 6 + r = 20 + t * 210 + g = 30 + t * 100 + b = 80 - t * 30 + } else if (h < 12) { + const t = (h - 6) / 6 + r = 230 - t * 30 + g = 130 + t * 90 + b = 50 + t * 100 + } else if (h < 18) { + const t = (h - 12) / 6 + r = 200 + t * 50 + g = 220 - t * 90 + b = 150 - t * 100 + } else { + const t = (h - 18) / 6 + r = 250 - t * 230 + g = 130 - t * 100 + b = 50 + t * 30 + } + return `rgba(${Math.round(r)},${Math.round(g)},${Math.round(b)},${alpha})` +} + +export function tempColor(temp, alpha) { + if (temp === null) return `rgba(128,128,128,${alpha})` + const t = Math.max(0, Math.min(1, (temp + 10) / 50)) + let r, g, b + if (t < 0.2) { + const u = t / 0.2 + r = Math.round(120 - u * 80) + g = Math.round(u * 20) + b = Math.round(180 + u * 75) + } else if (t < 0.4) { + const u = (t - 0.2) / 0.2 + r = Math.round(40 - u * 40) + g = Math.round(20 + u * 180) + b = Math.round(255 - u * 60) + } else if (t < 0.6) { + const u = (t - 0.4) / 0.2 + r = Math.round(u * 200) + g = Math.round(200 + u * 55) + b = Math.round(195 - u * 195) + } else if (t < 0.8) { + const u = (t - 0.6) / 0.2 + r = Math.round(200 + u * 40) + g = Math.round(255 - u * 130) + b = 0 + } else { + const u = (t - 0.8) / 0.2 + r = Math.round(240 + u * 15) + g = Math.round(125 - u * 105) + b = 0 + } + return `rgba(${Math.round(r)},${Math.round(g)},${Math.round(b)},${alpha})` +} diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue new file mode 100644 index 0000000..4575d66 --- /dev/null +++ b/src/views/HomeView.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..08c9d6f --- /dev/null +++ b/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + host: true, + port: 5173, + }, +})