diff --git a/HANDOFF.md b/HANDOFF.md deleted file mode 100644 index 3010fb9..0000000 --- a/HANDOFF.md +++ /dev/null @@ -1,108 +0,0 @@ -# Handoff Notes - -## Where we left off - -Auth is working — `register` and `login` both return a JWT. The item functions (`listItems`, `getItem`, `createItem`, `updateItem`, `deleteItem`) still have **no auth protection**. Next task is adding `verifyToken` + `requireRole` to each of them. - ---- - -## Next immediate step — protect item functions - -Add this to the top of each item function's `index.js`, right inside the `try` block before the DB call: - -```js -const { verifyToken, requireRole } = require("../auth/middleware") - -// inside the function: -const decoded = verifyToken(req) -if (decoded.status) return decoded - -const err = requireRole(decoded, "admin", "employee") // adjust roles per function -if (err) return err -``` - -### Role rules (from PLAN.md) -| Function | Allowed roles | -| ------------ | ---------------------- | -| listItems | admin, employee, guest | -| getItem | admin, employee, guest | -| createItem | admin, employee | -| updateItem | admin | -| deleteItem | admin | - ---- - -## After that — frontend - -Install deps first: -```bash -bun add vue-router@4 pinia axios -``` - -Files to create (in order): -1. `src/api/client.js` — axios instance that reads JWT from localStorage and attaches it as `Authorization: Bearer ` header -2. `src/router/index.js` — routes for `/login` and `/inventory`, redirect to login if no token -3. `src/stores/auth.js` — pinia store for login/register, saves token to localStorage -4. `src/stores/inventory.js` — pinia store for item CRUD -5. `src/pages/LoginPage.vue` -6. `src/pages/InventoryPage.vue` + `src/components/ItemTable.vue` + `src/components/ItemForm.vue` -7. Update `src/main.js` and `src/App.vue` - ---- - -## Useful commands - -```bash -# Start local environment (SQL Server + Azurite + Functions) -docker-compose up - -# Rebuild after code/package changes -docker-compose up --build - -# Run DB migrations -node api/db/migrate.js - -# Start frontend -bun dev - -# Deploy to Azure -func azure functionapp publish testneas --javascript --build remote -``` - -Local functions: `http://localhost:7071/api/...` -Frontend dev server: `http://localhost:5173` - ---- - -## How auth works - -- `POST /api/auth/register` — creates user, returns JWT -- `POST /api/auth/login` — verifies password with bcrypt, returns JWT -- Token payload: `{ id, username, role }`, expires in 1 hour -- Send token on protected requests: `Authorization: Bearer ` -- Roles: `admin` (full access), `employee` (view + create), `guest` (view only) - ---- - -## Environment variables - -**Local** — stored in `.env` (gitignored, never commit): -- `SA_PASSWORD` — SQL Server SA password for Docker -- `SQL_SERVER`, `SQL_PORT`, `SQL_DATABASE`, `SQL_USER`, `SQL_PASSWORD` — DB connection -- `JWT_SECRET` — secret used to sign tokens - -**Azure** — set in Function App > Settings > Environment variables (same names as above, plus `JWT_SECRET`) - ---- - -## Key files - -| File | Purpose | -| ---- | ------- | -| `api/db/connection.js` | Shared MSSQL connection pool | -| `api/db/migrate.js` | Runs all `.sql` files in `api/db/migrations/` | -| `api/auth/middleware.js` | `verifyToken` and `requireRole` helpers | -| `api/register/` | POST /api/auth/register | -| `api/login/` | POST /api/auth/login | -| `docker-compose.yml` | Local dev services | -| `vite.config.js` | Proxies `/api` → `http://localhost:7071` | diff --git a/bun.lock b/bun.lock index 0118a92..c0131df 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "@vitejs/plugin-vue": "^6.0.5", "appwrite": "^25.0.0", "axios": "^1.15.2", + "html2canvas": "^1.4.1", "pinia": "^3.0.4", "tailwindcss": "^4.2.2", "vite": "^8.0.3", @@ -296,6 +297,8 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA=="], "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], @@ -322,6 +325,8 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -432,6 +437,8 @@ "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -602,6 +609,8 @@ "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], + "text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], @@ -618,6 +627,8 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="], + "vite": ["vite@8.0.8", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "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" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="], "vite-dev-rpc": ["vite-dev-rpc@1.1.0", "", { "dependencies": { "birpc": "^2.4.0", "vite-hot-client": "^2.1.0" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" } }, "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A=="], diff --git a/index.html b/index.html index ccaca7d..e9e0a5c 100644 --- a/index.html +++ b/index.html @@ -1,17 +1,6 @@ - diff --git a/package-lock.json b/package-lock.json index cb59b30..73ce81b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,18 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.2.2", + "@vitejs/plugin-vue": "^6.0.5", + "appwrite": "^25.0.0", "axios": "^1.15.2", + "html2canvas": "^1.4.1", "pinia": "^3.0.4", "tailwindcss": "^4.2.2", + "vite": "^8.0.3", "vue": "^3.5.31", "vue-router": "4" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@vitejs/plugin-vue": "^6.0.5", "eslint": "^10.1.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-oxlint": "~1.57.0", @@ -26,7 +29,6 @@ "npm-run-all2": "^8.0.4", "oxlint": "~1.57.0", "prettier": "3.8.1", - "vite": "^8.0.3", "vite-plugin-vue-devtools": "^8.1.1" }, "engines": { @@ -58,7 +60,6 @@ "version": "7.29.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -453,6 +454,27 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -1213,6 +1235,27 @@ "node": ">=14.0.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.0-rc.15", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", @@ -1245,7 +1288,6 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.2", - "dev": true, "license": "MIT" }, "node_modules/@tailwindcss/node": { @@ -1524,7 +1566,6 @@ }, "node_modules/@vitejs/plugin-vue": { "version": "6.0.5", - "dev": true, "license": "MIT", "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" @@ -1729,7 +1770,6 @@ "version": "8.16.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1779,6 +1819,18 @@ "node": ">=14" } }, + "node_modules/appwrite": { + "version": "25.1.1", + "resolved": "https://registry.npmjs.org/appwrite/-/appwrite-25.1.1.tgz", + "integrity": "sha512-h+vVPErfCLZ9FCnWH1cZOphgX8MsXNNN0PvxCxoOCbqyX6XubtvvOEpp/BpVOAHp7jIDFEJu72ingBu9kh1vDQ==", + "license": "BSD-3-Clause", + "dependencies": { + "json-bigint": "1.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "license": "MIT" @@ -1800,6 +1852,15 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.16", "dev": true, @@ -1811,6 +1872,15 @@ "node": ">=6.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/birpc": { "version": "2.9.0", "license": "MIT", @@ -1852,7 +1922,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1971,6 +2040,15 @@ "dev": true, "license": "ISC" }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "dev": true, @@ -2164,7 +2242,6 @@ "version": "10.2.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2596,6 +2673,19 @@ "version": "5.5.3", "license": "MIT" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "dev": true, @@ -2717,6 +2807,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "dev": true, @@ -3229,7 +3328,6 @@ "version": "1.57.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "oxlint": "bin/oxlint" }, @@ -3597,6 +3695,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "license": "MIT", @@ -3694,10 +3801,18 @@ "dev": true, "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/vite": { "version": "8.0.8", "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -3867,7 +3982,6 @@ "node_modules/vue": { "version": "3.5.32", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.32", "@vue/compiler-sfc": "3.5.32", diff --git a/package.json b/package.json index 64ce74f..55c2fc7 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,12 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", + "@vitejs/plugin-vue": "^6.0.5", "appwrite": "^25.0.0", "axios": "^1.15.2", + "html2canvas": "^1.4.1", "pinia": "^3.0.4", "tailwindcss": "^4.2.2", - "@vitejs/plugin-vue": "^6.0.5", "vite": "^8.0.3", "vue": "^3.5.31", "vue-router": "4" diff --git a/src/assets/main.css b/src/assets/main.css index 297ac65..0abb525 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -1,33 +1,33 @@ -@import "tailwindcss"; +@import 'tailwindcss'; @custom-variant dark (&:where(.dark, .dark *)); @font-face { - font-family: "Mabry Pro"; - src: url("/mabry-light-pro.woff2") format("woff2"); + font-family: 'Mabry Pro'; + src: url('/mabry-light-pro.woff2') format('woff2'); font-weight: 300; font-style: normal; font-display: swap; } @font-face { - font-family: "Mabry Pro"; - src: url("/mabry-regular-pro.woff2") format("woff2"); + font-family: 'Mabry Pro'; + src: url('/mabry-regular-pro.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; } @font-face { - font-family: "Mabry Pro"; - src: url("/mabry-regular-pro.woff2") format("woff2"); + font-family: 'Mabry Pro'; + src: url('/mabry-regular-pro.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; } @theme { - --font-sans: "Mabry Pro", Arial, Helvetica, ui-sans-serif, system-ui, sans-serif; + --font-sans: 'Mabry Pro', Arial, Helvetica, ui-sans-serif, system-ui, sans-serif; --color-neas-pine: #003d2d; --color-neas-moss: #95c672; --color-neas-sun: #f1e967; @@ -58,3 +58,311 @@ color: #e8f0ec; } } + +@layer base { + :root { + --neas-grid-line: rgba(0, 61, 45, 0.08); + --neas-grid-line-dark: rgba(232, 240, 236, 0.08); + --neas-glow: rgba(149, 198, 114, 0.22); + --neas-sun-glow: rgba(241, 233, 103, 0.3); + } +} + +@layer components { + .neas-page { + background: linear-gradient(180deg, rgba(241, 246, 244, 0.9) 0%, #f1f6f4 60%, #eef3f1 100%); + } + + .dark .neas-page { + background: linear-gradient(180deg, #061512 0%, #071a15 60%, #0a1f19 100%); + } + + .neas-grid { + background-image: + linear-gradient(to right, var(--neas-grid-line) 1px, transparent 1px), + linear-gradient(to bottom, var(--neas-grid-line) 1px, transparent 1px); + background-size: 80px 80px; + background-position: var(--neas-grid-x, 0px) var(--neas-grid-y, 0px); + transition: background-position 700ms cubic-bezier(0.16, 1, 0.3, 1); + will-change: background-position; + } + + .dark .neas-grid { + background-image: + linear-gradient(to right, var(--neas-grid-line-dark) 1px, transparent 1px), + linear-gradient(to bottom, var(--neas-grid-line-dark) 1px, transparent 1px); + background-position: var(--neas-grid-x, 0px) var(--neas-grid-y, 0px); + } + + .neas-hero { + position: relative; + overflow: hidden; + border-radius: 24px; + background: linear-gradient(135deg, #003d2d 0%, #014a37 40%, #0c6b4f 100%); + color: var(--color-neas-white); + } + + .neas-hero-animate::before { + animation: neas-glow 10s ease-in-out infinite; + } + + .neas-hero-animate::after { + animation: neas-grid-drift 18s linear infinite; + } + + .neas-hero::before { + content: ''; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 12% 18%, rgba(241, 233, 103, 0.28), transparent 42%), + radial-gradient(circle at 80% 20%, rgba(149, 198, 114, 0.3), transparent 45%); + opacity: 0.9; + pointer-events: none; + } + + .neas-hero::after { + content: ''; + position: absolute; + inset: 0; + background-image: + linear-gradient(to right, rgba(255, 255, 255, 0.08) 1px, transparent 1px), + linear-gradient(to bottom, rgba(255, 255, 255, 0.08) 1px, transparent 1px); + background-size: 96px 96px; + opacity: 0.35; + pointer-events: none; + } + + .neas-panel { + border-radius: 22px; + border: 1px solid rgba(0, 61, 45, 0.12); + background: rgba(255, 255, 255, 0.92); + box-shadow: + 0 24px 60px rgba(0, 61, 45, 0.08), + 0 2px 0 rgba(0, 61, 45, 0.06); + backdrop-filter: blur(12px); + } + + .neas-glass { + border: 1px solid rgba(0, 61, 45, 0.1); + background: rgba(255, 255, 255, 0.72); + box-shadow: 0 20px 50px rgba(0, 61, 45, 0.12); + backdrop-filter: blur(18px); + } + + .neas-glass-strong { + border: 1px solid rgba(0, 61, 45, 0.12); + background: rgba(255, 255, 255, 0.86); + box-shadow: 0 24px 70px rgba(0, 61, 45, 0.16); + backdrop-filter: blur(22px); + } + + .dark .neas-panel { + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(15, 36, 29, 0.92); + box-shadow: + 0 28px 70px rgba(0, 0, 0, 0.35), + 0 2px 0 rgba(255, 255, 255, 0.04); + } + + .neas-kpi { + border-radius: 18px; + border: 1px solid rgba(0, 61, 45, 0.12); + background: rgba(255, 255, 255, 0.88); + box-shadow: 0 18px 40px rgba(0, 61, 45, 0.1); + } + + .dark .neas-kpi { + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(7, 26, 21, 0.9); + box-shadow: 0 18px 45px rgba(0, 0, 0, 0.35); + } + + .neas-chip { + display: inline-flex; + align-items: center; + gap: 0.4rem; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.16); + padding: 0.35rem 0.75rem; + font-size: 0.72rem; + letter-spacing: 0.12em; + text-transform: uppercase; + } + + .neas-input { + border-radius: 14px; + border: 1px solid rgba(0, 61, 45, 0.18); + background: rgba(241, 246, 244, 0.9); + transition: + border-color 200ms ease, + box-shadow 200ms ease, + background-color 200ms ease; + } + + .neas-input:focus { + border-color: rgba(0, 61, 45, 0.6); + box-shadow: 0 0 0 3px rgba(149, 198, 114, 0.28); + background: #ffffff; + outline: none; + } + + .dark .neas-input { + border: 1px solid rgba(255, 255, 255, 0.18); + background: rgba(6, 21, 18, 0.95); + color: #e8f0ec; + } + + .dark .neas-input:focus { + border-color: rgba(149, 198, 114, 0.7); + box-shadow: 0 0 0 3px rgba(149, 198, 114, 0.18); + background: #0a1814; + } + + .neas-button { + border-radius: 999px; + padding: 0.85rem 1.75rem; + font-size: 0.78rem; + letter-spacing: 0.14em; + text-transform: uppercase; + font-weight: 500; + background: #f1e967; + color: #003d2d; + transition: + transform 200ms ease, + box-shadow 200ms ease, + background-color 200ms ease; + box-shadow: 0 16px 26px rgba(241, 233, 103, 0.35); + } + + .neas-button:hover { + background: #f6f07a; + transform: translateY(-1px); + } + + .neas-button-dark { + background: #003d2d; + color: #ffffff; + box-shadow: 0 14px 24px rgba(0, 61, 45, 0.3); + } + + .neas-button-dark:hover { + background: #002f24; + } + + .neas-outline { + border-radius: 999px; + border: 1.6px solid rgba(0, 61, 45, 0.5); + padding: 0.7rem 1.4rem; + font-size: 0.75rem; + letter-spacing: 0.14em; + text-transform: uppercase; + font-weight: 500; + transition: + border-color 200ms ease, + color 200ms ease, + background-color 200ms ease; + } + + .neas-outline:hover { + background: rgba(0, 61, 45, 0.08); + } + + .dark .neas-outline { + border-color: rgba(232, 240, 236, 0.5); + color: #e8f0ec; + } + + .dark .neas-outline:hover { + background: rgba(232, 240, 236, 0.12); + } + + .neas-table-head { + background: linear-gradient(90deg, #003d2d 0%, #0b5a44 100%); + } + + .neas-reveal { + opacity: 0; + animation: neas-rise 700ms ease forwards; + } + + .neas-delay-1 { + animation-delay: 120ms; + } + + .neas-delay-2 { + animation-delay: 220ms; + } + + .neas-delay-3 { + animation-delay: 320ms; + } + + .neas-delay-4 { + animation-delay: 420ms; + } + + .neas-delay-5 { + animation-delay: 520ms; + } + + .neas-float { + animation: neas-float 6s ease-in-out infinite; + } +} + +@keyframes neas-rise { + 0% { + opacity: 0; + transform: translateY(14px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes neas-float { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(-6px); + } + 100% { + transform: translateY(0); + } +} + +@keyframes neas-grid-drift { + 0% { + background-position: 0 0; + } + 100% { + background-position: 96px 96px; + } +} + +@keyframes neas-glow { + 0% { + opacity: 0.85; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.85; + } +} + +@media (prefers-reduced-motion: reduce) { + .neas-reveal, + .neas-hero-animate::before, + .neas-hero-animate::after, + .neas-float { + animation: none !important; + opacity: 1 !important; + transform: none !important; + } +} diff --git a/src/components/ItemForm.vue b/src/components/ItemForm.vue index bae8356..4d6533d 100644 --- a/src/components/ItemForm.vue +++ b/src/components/ItemForm.vue @@ -52,7 +52,7 @@ async function handleSubmit() {