aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.thicket/tickets.jsonl7
-rw-r--r--Makefile5
-rw-r--r--frontend-vanilla/.gitignore24
-rw-r--r--frontend-vanilla/index.html13
-rw-r--r--frontend-vanilla/package-lock.json1055
-rw-r--r--frontend-vanilla/package.json15
-rw-r--r--frontend-vanilla/public/vite.svg1
-rw-r--r--frontend-vanilla/src/counter.ts9
-rw-r--r--frontend-vanilla/src/main.ts24
-rw-r--r--frontend-vanilla/src/style.css96
-rw-r--r--frontend-vanilla/src/typescript.svg1
-rw-r--r--frontend-vanilla/tsconfig.json26
-rw-r--r--frontend-vanilla/vite.config.ts17
-rw-r--r--vanilla_frontend_proposal.md144
-rw-r--r--web/dist/v3/assets/index-BmeGit54.css1
-rw-r--r--web/dist/v3/assets/index-DfsH-YDt.js17
-rw-r--r--web/dist/v3/index.html14
-rw-r--r--web/dist/v3/vite.svg1
-rw-r--r--web/frontend.go69
-rw-r--r--web/web.go9
-rw-r--r--web/web_test.go8
21 files changed, 1512 insertions, 44 deletions
diff --git a/.thicket/tickets.jsonl b/.thicket/tickets.jsonl
index d5d256d..18bc8bd 100644
--- a/.thicket/tickets.jsonl
+++ b/.thicket/tickets.jsonl
@@ -48,7 +48,7 @@
{"id":"NK-ca9t70","title":"Vanilla JS: Add Feed UI","description":"Add UI to add a new feed by URL in vanilla JS prototype.","type":"feature","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T04:47:41.764330544Z","updated":"2026-02-14T04:47:41.764330544Z"}
{"id":"NK-chns2b","title":"reach parity between vanilla js and react v2 ui","description":"Continue implementing the vanilla js one with minimal overhad/depdnencies to be fast and lean. Make sure there are tests and rely on the v2 ui and legacy version as references.","type":"epic","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T04:45:06.813453353Z","updated":"2026-02-14T04:45:06.813453353Z"}
{"id":"NK-ck4co9","title":"Refactor E2E tests to use page objects","description":"The E2E tests are getting complex. Refactor them to use the Page Object Model pattern for better maintainability.","type":"task","status":"open","priority":4,"labels":null,"assignee":"","created":"2026-02-15T02:21:34.96843041Z","updated":"2026-02-15T19:14:31.660189629Z"}
-{"id":"NK-d2be57","title":"Persist sidebar state across reloads","description":"Currently sidebar state resets on reload. It should persist in localStorage like the theme.","type":"feature","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-15T22:23:06.847360465Z","updated":"2026-02-15T22:23:06.847360465Z"}
+{"id":"NK-d2be57","title":"Persist sidebar state across reloads","description":"Currently sidebar state resets on reload. It should persist in localStorage like the theme.","type":"feature","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-15T22:23:06.847360465Z","updated":"2026-02-15T22:23:06.847360465Z"}
{"id":"NK-d4c8jv","title":"Vanilla JS Parity: Read/Star/Filter","description":"Implement read/unread toggle, star/unstar, and special filters (All, Unread, Starred) in vanilla JS prototype.","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T04:46:32.113504545Z","updated":"2026-02-14T04:47:46.412290355Z"}
{"id":"NK-dbcl6t","title":"Create Python Compliance Suite","description":"","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-15T00:21:53.997204693Z","updated":"2026-02-15T00:44:41.52830766Z"}
{"id":"NK-doss0v","title":"v2 ui: change title fonts to Helvetica Neue","description":"to match style in legacy change font to match Helventic Neue where applicable","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T06:27:57.270935467Z","updated":"2026-02-14T06:31:42.798620609Z"}
@@ -63,7 +63,7 @@
{"id":"NK-fnbaaf","title":"stress test with a REALLY big database","description":"I have been using a version of neko for over a decade and my prod database is 1.4GB sqlite file. it's in big_neko.db\n\nrun some tests with that big database and see if anything messes up! DO NOT CHECK IN THAT BIG DATABASE EVER.\n\nif there are bugs or performance issues with such a big db, file some tickets for us to evaluate and we can create some synthetic data or issues later on.","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-15T02:58:54.859954523Z","updated":"2026-02-15T02:58:54.859954523Z"}
{"id":"NK-fpzx66","title":"v2 ui - title styling","description":"The title of the article stays blue and bold regardless of read state.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T03:22:55.339956853Z","updated":"2026-02-14T03:28:01.555909701Z"}
{"id":"NK-fzjyay","title":"github CI fails","description":"Both the backend and frontend failed the CI jobs failed the lint checks.\n\nare we running those properly locally before submitting\n\nalso the frontent consistency check failed","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-15T02:41:02.747760248Z","updated":"2026-02-15T05:11:46.097784637Z"}
-{"id":"NK-g4n0sq","title":"write tests based on closed CLs","description":"Look at the recent CLs and closed tickets over the past 48 hours and identify areas where we need regression tests or other tests and write tickets for them as additional testing","type":"epic","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-15T21:46:39.050460849Z","updated":"2026-02-15T21:46:39.050460849Z"}
+{"id":"NK-g4n0sq","title":"write tests based on closed CLs","description":"Look at the recent CLs and closed tickets over the past 48 hours and identify areas where we need regression tests or other tests and write tickets for them as additional testing","type":"epic","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-15T21:46:39.050460849Z","updated":"2026-02-15T21:46:39.050460849Z"}
{"id":"NK-g7ya0n","title":"Style Settings page to match Glass sidebar","description":"The settings page currently uses the old retro aesthetic. It should be updated to match the new minimalist 'glass' sidebar style for visual consistency. This should include updating form elements, backgrounds, and typography.","type":"feature","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T23:53:46.590222566Z","updated":"2026-02-15T00:51:14.771780934Z"}
{"id":"NK-g818qn","title":"Improve mobile responsiveness of React UI","description":"The React UI should be fully responsive and work well on small screens. Now that the vanilla JS prototype is removed, we should ensure the main interface is a great experience for mobile users.","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T22:46:32.850472479Z","updated":"2026-02-14T22:49:01.411224187Z"}
{"id":"NK-gdf99z","title":"TUI is terrible and needs fixing","description":"The TUI doesn't really work and doesn't make sense. Think very hard and look at the v2 HTML UI implementation and make something cool like that. Probably needs tests too.","type":"epic","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T03:51:59.882212859Z","updated":"2026-02-14T04:31:28.290051717Z"}
@@ -88,7 +88,7 @@
{"id":"NK-kqt9oc","title":"docker support","description":"add support so people can self-host this in docker and (maybe) test it yourself. maybe keep it in a docker directory with separate docs etc.","type":"epic","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T20:19:10.70328135Z","updated":"2026-02-14T01:03:35.363466842Z"}
{"id":"NK-kra45a","title":"enhance github ci/cq","description":"Make sure we have the right CI/CQ things in the github workflow. Can it test our docker compose for us too maybe","type":"feature","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-15T00:08:25.732991582Z","updated":"2026-02-15T01:04:54.350079542Z"}
{"id":"NK-lrew5z","title":"UI Styling: Global Typography \u0026 Layout (Fixed Width)","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:04:57.254341682Z","updated":"2026-02-13T18:11:31.436752093Z"}
-{"id":"NK-lrihov","title":"Re-enable and fix errcheck lints","description":"Re-enable 'errcheck' in .golangci.yml and fix the remaining ~140 issues, mostly related to unchecked Writes and DB operations.","type":"cleanup","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-15T16:41:08.395284346Z","updated":"2026-02-15T16:41:08.395284346Z"}
+{"id":"NK-lrihov","title":"Re-enable and fix errcheck lints","description":"Re-enable 'errcheck' in .golangci.yml and fix the remaining ~140 issues, mostly related to unchecked Writes and DB operations.","type":"cleanup","status":"open","priority":4,"labels":null,"assignee":"","created":"2026-02-15T16:41:08.395284346Z","updated":"2026-02-15T16:41:08.395284346Z"}
{"id":"NK-m8bya7","title":"Fix and Re-enable Playwright E2E Tests","description":"E2E tests were crashing the VM and timing out. Disabled them in package.json. Need to investigate resource usage and re-enable.","type":"bug","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T03:08:18.501189975Z","updated":"2026-02-14T04:00:03.995357386Z"}
{"id":"NK-mbuw7q","title":"v2 ui bug - panel open by default","description":"Panel is closed by default, it should be open by default on desktop.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T02:46:56.870671639Z","updated":"2026-02-14T03:08:17.322841854Z"}
{"id":"NK-mgmn5m","title":"serve \"legacy\" version UI at /v1/ instead of /","description":"Let's \"softly\" start to deprecated the legacy version by moving it to /v1/ -- ideally this won't require any changes but there may be some relative/absolute URLs to adjust in the static files there or in rouoting","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T16:41:04.710679944Z","updated":"2026-02-14T17:38:25.35292336Z"}
@@ -126,6 +126,7 @@
{"id":"NK-thq2oq","title":"v2 ui - font size adjustments","description":"Move font-size: 18px to :root so rem units resolve correctly. Adjust title size to ~24px.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T03:30:58.751447802Z","updated":"2026-02-14T03:31:56.358775833Z"}
{"id":"NK-tw0nga","title":"E2E Testing","description":"Set up E2E testing with Playwright or Cypress to verify full flows: Login -\u003e View Feeds -\u003e View Items -\u003e Logout","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T15:01:33.817314728Z","updated":"2026-02-13T15:46:57.094062908Z"}
{"id":"NK-ucckki","title":"security changes broke legacy","description":"I think some of the security policies make it so the old legacy one doesn't work. this may just be WAI but have a look\n\n[Warning] jQuery.Deferred exception: Refused to evaluate a string as JavaScript because 'unsafe-eval' or 'trusted-types-eval' is not an allowed source of script in the following Content Security Policy directive: \"script-src 'self'\". (jquery-3.3.1.min.js, line 2)\n (2)\n\"Function@[native code]\no@http://localhost:4994/static/jquery.tmpl.min.js:10:3543\ntemplate@http://localhost:4994/static/jquery.tmpl.min.js:10:1914\ntmpl@http://localhost:4994/static/jquery.tmpl.min.js:10:1422\nrender@http://localhost:4994/static/ui.js:208:23\nnr@http://localhost:4994/static/underscore-1.13.1.min.js:6:7308\n@http://localhost:4994/static/underscore-1.13.1.min.js:6:7733\n@http://localhost:4994/static/underscore-1.13.1.min.js:6:786\nboot@http://localhost:4994/static/ui.js:598:28\n@http://localhost:4994/static/ui.js:8:9\nl@http://localhost:4994/static/jquery-3.3.1.min.js:2:29380\n@http://localhost:4994/static/jquery-3.3.1.min.js:2:29678\"\nundefined","type":"bug","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T17:41:02.255772514Z","updated":"2026-02-14T17:41:02.255772514Z"}
+{"id":"NK-uxnbu7","title":"Scaffold Vanilla JS Frontend (v3)","description":"","type":"feature","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-16T01:26:12.132325593Z","updated":"2026-02-16T01:26:12.132325593Z"}
{"id":"NK-uy90he","title":"UI Styling: Feed Items (Spacing, Dateline)","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:05:17.289457994Z","updated":"2026-02-13T18:11:46.255816698Z"}
{"id":"NK-uywybr","title":"https://computer.rip/rss.xml fails to importa","description":"running neko -a https://computer.rip/rss.xml gave an error. debug it and add test case to catch.","type":"bug","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T20:12:28.54350403Z","updated":"2026-02-14T01:03:02.755247954Z"}
{"id":"NK-v9e7r3","title":"consistency in sidebar","description":"With the new sidebar styling, SETTINGS and LOGOUT and the light/dark look really different than the rest. Let's make them more consistent from a style perspective.","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T23:55:51.554786606Z","updated":"2026-02-15T00:22:33.826814528Z"}
diff --git a/Makefile b/Makefile
index 4a9bba4..c3076f6 100644
--- a/Makefile
+++ b/Makefile
@@ -13,7 +13,7 @@ LDFLAGS=-ldflags "-X main.Version=${VERSION} -X main.Build=${BUILD}"
default: build
-all: clean ui build docs
+all: clean ui ui-vanilla build docs
clean:
rm -f ${BINARY}
@@ -25,6 +25,9 @@ ui:
mkdir -p web/dist/v2
cp -r frontend/dist/* web/dist/v2/
+ui-vanilla:
+ cd frontend-vanilla && ${NPM} install && ${NPM} run build
+
build:
${GO} build ${LDFLAGS} -o ${BINARY} ./cmd/neko
diff --git a/frontend-vanilla/.gitignore b/frontend-vanilla/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend-vanilla/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend-vanilla/index.html b/frontend-vanilla/index.html
new file mode 100644
index 0000000..0038daa
--- /dev/null
+++ b/frontend-vanilla/index.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>frontend-vanilla</title>
+ </head>
+ <body>
+ <div id="app"></div>
+ <script type="module" src="/src/main.ts"></script>
+ </body>
+</html>
diff --git a/frontend-vanilla/package-lock.json b/frontend-vanilla/package-lock.json
new file mode 100644
index 0000000..6f6c92e
--- /dev/null
+++ b/frontend-vanilla/package-lock.json
@@ -0,0 +1,1055 @@
+{
+ "name": "frontend-vanilla",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend-vanilla",
+ "version": "0.0.0",
+ "devDependencies": {
+ "typescript": "~5.9.3",
+ "vite": "^7.3.1"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+ "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
+ "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
+ "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
+ "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+ "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
+ "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
+ "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
+ "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
+ "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
+ "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
+ "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
+ "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
+ "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
+ "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
+ "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
+ "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
+ "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
+ "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
+ "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
+ "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
+ "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
+ "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
+ "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+ "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.3",
+ "@esbuild/android-arm": "0.27.3",
+ "@esbuild/android-arm64": "0.27.3",
+ "@esbuild/android-x64": "0.27.3",
+ "@esbuild/darwin-arm64": "0.27.3",
+ "@esbuild/darwin-x64": "0.27.3",
+ "@esbuild/freebsd-arm64": "0.27.3",
+ "@esbuild/freebsd-x64": "0.27.3",
+ "@esbuild/linux-arm": "0.27.3",
+ "@esbuild/linux-arm64": "0.27.3",
+ "@esbuild/linux-ia32": "0.27.3",
+ "@esbuild/linux-loong64": "0.27.3",
+ "@esbuild/linux-mips64el": "0.27.3",
+ "@esbuild/linux-ppc64": "0.27.3",
+ "@esbuild/linux-riscv64": "0.27.3",
+ "@esbuild/linux-s390x": "0.27.3",
+ "@esbuild/linux-x64": "0.27.3",
+ "@esbuild/netbsd-arm64": "0.27.3",
+ "@esbuild/netbsd-x64": "0.27.3",
+ "@esbuild/openbsd-arm64": "0.27.3",
+ "@esbuild/openbsd-x64": "0.27.3",
+ "@esbuild/openharmony-arm64": "0.27.3",
+ "@esbuild/sunos-x64": "0.27.3",
+ "@esbuild/win32-arm64": "0.27.3",
+ "@esbuild/win32-ia32": "0.27.3",
+ "@esbuild/win32-x64": "0.27.3"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "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,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "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"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.57.1",
+ "@rollup/rollup-android-arm64": "4.57.1",
+ "@rollup/rollup-darwin-arm64": "4.57.1",
+ "@rollup/rollup-darwin-x64": "4.57.1",
+ "@rollup/rollup-freebsd-arm64": "4.57.1",
+ "@rollup/rollup-freebsd-x64": "4.57.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.1",
+ "@rollup/rollup-linux-arm64-musl": "4.57.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.1",
+ "@rollup/rollup-linux-loong64-musl": "4.57.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-musl": "4.57.1",
+ "@rollup/rollup-openbsd-x64": "4.57.1",
+ "@rollup/rollup-openharmony-arm64": "4.57.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.1",
+ "@rollup/rollup-win32-x64-gnu": "4.57.1",
+ "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "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==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "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",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.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
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/frontend-vanilla/package.json b/frontend-vanilla/package.json
new file mode 100644
index 0000000..ee57be1
--- /dev/null
+++ b/frontend-vanilla/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "frontend-vanilla",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "typescript": "~5.9.3",
+ "vite": "^7.3.1"
+ }
+}
diff --git a/frontend-vanilla/public/vite.svg b/frontend-vanilla/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/frontend-vanilla/public/vite.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> \ No newline at end of file
diff --git a/frontend-vanilla/src/counter.ts b/frontend-vanilla/src/counter.ts
new file mode 100644
index 0000000..09e5afd
--- /dev/null
+++ b/frontend-vanilla/src/counter.ts
@@ -0,0 +1,9 @@
+export function setupCounter(element: HTMLButtonElement) {
+ let counter = 0
+ const setCounter = (count: number) => {
+ counter = count
+ element.innerHTML = `count is ${counter}`
+ }
+ element.addEventListener('click', () => setCounter(counter + 1))
+ setCounter(0)
+}
diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts
new file mode 100644
index 0000000..6396b50
--- /dev/null
+++ b/frontend-vanilla/src/main.ts
@@ -0,0 +1,24 @@
+import './style.css'
+import typescriptLogo from './typescript.svg'
+import viteLogo from '/vite.svg'
+import { setupCounter } from './counter.ts'
+
+document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
+ <div>
+ <a href="https://vite.dev" target="_blank">
+ <img src="${viteLogo}" class="logo" alt="Vite logo" />
+ </a>
+ <a href="https://www.typescriptlang.org/" target="_blank">
+ <img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
+ </a>
+ <h1>Vite + TypeScript</h1>
+ <div class="card">
+ <button id="counter" type="button"></button>
+ </div>
+ <p class="read-the-docs">
+ Click on the Vite and TypeScript logos to learn more
+ </p>
+ </div>
+`
+
+setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
diff --git a/frontend-vanilla/src/style.css b/frontend-vanilla/src/style.css
new file mode 100644
index 0000000..3bcdbd0
--- /dev/null
+++ b/frontend-vanilla/src/style.css
@@ -0,0 +1,96 @@
+:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+#app {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.vanilla:hover {
+ filter: drop-shadow(0 0 2em #3178c6aa);
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/frontend-vanilla/src/typescript.svg b/frontend-vanilla/src/typescript.svg
new file mode 100644
index 0000000..d91c910
--- /dev/null
+++ b/frontend-vanilla/src/typescript.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg> \ No newline at end of file
diff --git a/frontend-vanilla/tsconfig.json b/frontend-vanilla/tsconfig.json
new file mode 100644
index 0000000..4ba8dd9
--- /dev/null
+++ b/frontend-vanilla/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend-vanilla/vite.config.ts b/frontend-vanilla/vite.config.ts
new file mode 100644
index 0000000..58b4f81
--- /dev/null
+++ b/frontend-vanilla/vite.config.ts
@@ -0,0 +1,17 @@
+
+import { defineConfig } from 'vite';
+
+// https://vite.dev/config/
+export default defineConfig({
+ base: '/v3/',
+ server: {
+ proxy: {
+ '/api': 'http://127.0.0.1:4994',
+ '/image': 'http://127.0.0.1:4994',
+ },
+ },
+ build: {
+ outDir: '../web/dist/v3',
+ emptyOutDir: true
+ }
+});
diff --git a/vanilla_frontend_proposal.md b/vanilla_frontend_proposal.md
new file mode 100644
index 0000000..de7e1dc
--- /dev/null
+++ b/vanilla_frontend_proposal.md
@@ -0,0 +1,144 @@
+# Vanilla JS Frontend Implementation Proposal
+
+## 1. Executive Summary
+This proposal outlines the strategy to build a lightweight, high-performance "v3" frontend for Neko using modern Vanilla JavaScript (ES Modules, Web Components). The goal is to achieve the instant responsiveness of the legacy Backbone version while maintaining code maintainability and modern best practices. This version will coexist with the current React ("v2") frontend, allowing for A/B testing and gradual migration.
+
+## 2. Architecture
+
+### 2.1 Core Philosophy
+- **No Frameworks**: Remove the overhead of React, Virtual DOM, and hydration.
+- **Platform First**: Use standard Web APIs (Custom Elements, IntersectionObserver, Fetch).
+- **Direct DOM Manipulation**: Update only what changes. State changes map 1:1 to DOM class/attribute updates.
+
+### 2.2 Directory Structure
+A new directory `frontend-vanilla` will be created alongside the existing `frontend`:
+```
+neko/
+├── frontend/ # Current React V2
+├── frontend-vanilla/ # New Vanilla V3
+│ ├── index.html
+│ ├── src/
+│ │ ├── components/
+│ │ ├── store.ts # State management
+│ │ ├── router.ts # History API wrapper
+│ │ └── main.ts
+│ └── vite.config.ts
+└── web/ # Go backend serving logic
+```
+
+### 2.3 Technology Stack
+- **Build Tool**: Vite (Vanilla preset). Extremely fast dev server and optimized bundling.
+- **Language**: TypeScript (sharing types with `frontend` where possible `../frontend/src/types`).
+- **CSS**: Standard CSS Variables (sharing `index.css` concepts from v2).
+- **State**: Custom lightweight `Pub/Sub` Store.
+
+## 3. Coexistence Strategy
+
+To support both versions simultaneously during development and review:
+
+### 3.1 Backend Serving (`web/web.go`)
+1. **Build Output**: `frontend-vanilla` will build to `web/dist/v3`.
+2. **Embedding**: Update `web.go` to embed the new directory.
+ ```go
+ //go:embed dist/v2/* dist/v3/*
+ frontendFiles embed.FS
+ ```
+3. **Routing**:
+ - Host v3 at `/v3/`.
+ - Users can switch via a simple link in the Settings menu (or a UI toggle).
+ - Feature flag (cookie) can serve v3 at `/` if desired later.
+
+### 3.2 Development Helper
+- Add a `make dev-vanilla` target that runs the Vite dev server for the vanilla app, proxying API requests to the Go backend.
+
+## 4. Implementation Details
+
+### 4.1 State Management (The Store)
+A simple `Store` class that extends `EventTarget` or uses a basic subscription model.
+```typescript
+class Store {
+ items: Item[] = [];
+ events = new EventTarget();
+
+ setItems(items: Item[]) {
+ this.items = items;
+ this.events.dispatchEvent(new CustomEvent('update'));
+ }
+
+ markRead(id: number) {
+ const item = this.items.find(i => i._id === id);
+ if (item) {
+ item.read = true;
+ // Emit specific event for precise DOM update
+ this.events.dispatchEvent(new CustomEvent('item-change', { detail: { id, change: 'read' } }));
+ // Sync with API...
+ }
+ }
+}
+```
+
+### 4.2 Efficient Rendering
+- **Initial Render**: Use Template Literals mapped to arrays. This is the fastest way to generate HTML strings in JS.
+- **Updates**: Components subscribe to the Store. When `item-change` fires for ID 101, the `<feed-item id="101">` component updates its *own* class list directly. No parent re-render.
+
+### 4.3 Web Components
+Encapsulate logic in Custom Elements to keep code organized.
+```typescript
+class FeedItem extends HTMLElement {
+ connectedCallback() {
+ // Render content
+ // Subscribe to store updates for self
+ }
+ // ...
+}
+customElements.define('feed-item', FeedItem);
+```
+
+## 5. Testing Strategy
+
+### 5.1 Unit Testing
+- **Tool**: Vitest.
+- **Approach**: Test "business logic" (Store, Router) in isolation. Test components using `JSDOM`.
+- **Benefit**: Fastest feedback loop.
+
+### 5.2 E2E Testing (The "Compliance" Suite)
+- **Tool**: Playwright (Reuse existing suite).
+- **Strategy**:
+ - Update existing tests to use agnostic data-attributes (e.g., `data-test-id="feed-item"`).
+ - Run the *same* test suite against both `/v2/` (React) and `/v3/` (Vanilla) endpoints.
+ - This guarantees feature parity.
+
+```typescript
+// example-spec.ts
+const uiVersion = process.env.UI_VERSION || 'v2'; // or v3
+
+test('loads items', async ({ page }) => {
+ await page.goto(`/${uiVersion}/`);
+ // assertions...
+});
+```
+
+## 6. Work Plan
+
+1. **Phase 1: Scaffold & Embed (1 Day)**
+ - Initialize `frontend-vanilla`.
+ - Update `web.go` to serve it.
+ - Verify "Hello World" at `/v3/`.
+
+2. **Phase 2: Read-Only Implementation (2 Days)**
+ - Implement fetching feeds/items.
+ - Implement Store and Template rendering.
+ - **Milestone**: Benchmark loading execution time vs React.
+
+3. **Phase 3: Interactive Features (2 Days)**
+ - Implement Keyboard navigation (j/k).
+ - Implement Mark as Read / Star.
+ - Implement Infinite Scroll.
+
+4. **Phase 4: Parity & Polish (Ongoing)**
+ - Settings, Mobile Sidebar, etc.
+ - Passing Compliance Tests.
+
+## 7. Next Steps for Approval
+- Review and approve this proposal.
+- Authorize creation of `frontend-vanilla` directory.
diff --git a/web/dist/v3/assets/index-BmeGit54.css b/web/dist/v3/assets/index-BmeGit54.css
new file mode 100644
index 0000000..a9ec929
--- /dev/null
+++ b/web/dist/v3/assets/index-BmeGit54.css
@@ -0,0 +1 @@
+:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}#app{max-width:1280px;margin:0 auto;padding:2rem;text-align:center}.logo{height:6em;padding:1.5em;will-change:filter;transition:filter .3s}.logo:hover{filter:drop-shadow(0 0 2em #646cffaa)}.logo.vanilla:hover{filter:drop-shadow(0 0 2em #3178c6aa)}.card{padding:2em}.read-the-docs{color:#888}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media(prefers-color-scheme:light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}
diff --git a/web/dist/v3/assets/index-DfsH-YDt.js b/web/dist/v3/assets/index-DfsH-YDt.js
new file mode 100644
index 0000000..681443b
--- /dev/null
+++ b/web/dist/v3/assets/index-DfsH-YDt.js
@@ -0,0 +1,17 @@
+(function(){const c=document.createElement("link").relList;if(c&&c.supports&&c.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))o(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const s of t.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&o(s)}).observe(document,{childList:!0,subtree:!0});function r(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function o(e){if(e.ep)return;e.ep=!0;const t=r(e);fetch(e.href,t)}})();const n="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%20aria-hidden='true'%20role='img'%20class='iconify%20iconify--logos'%20width='32'%20height='32'%20preserveAspectRatio='xMidYMid%20meet'%20viewBox='0%200%20256%20256'%3e%3cpath%20fill='%23007ACC'%20d='M0%20128v128h256V0H0z'%3e%3c/path%3e%3cpath%20fill='%23FFF'%20d='m56.612%20128.85l-.081%2010.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121%2010.443Zm149.955-10.742c6.501%201.625%2011.459%204.51%2016.01%209.224c2.357%202.52%205.851%207.111%206.136%208.208c.08.325-11.053%207.802-17.798%2011.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759%203.535-12.718%2010.321c0%201.992.284%203.17%201.097%204.795c1.707%203.536%204.876%205.649%2014.832%209.956c18.326%207.883%2026.168%2013.084%2031.045%2020.48c5.445%208.249%206.664%2021.415%202.966%2031.208c-4.063%2010.646-14.14%2017.879-28.323%2020.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163%201.178-.813%202.356-1.504c1.138-.65%205.446-3.129%209.509-5.485l7.355-4.267l1.544%202.276c2.154%203.29%206.867%207.801%209.712%209.305c8.167%204.307%2019.383%203.698%2024.909-1.26c2.357-2.153%203.332-4.388%203.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76%2012.353-21.659%2026.25-24.3c4.51-.853%2014.994-.528%2019.424.569Z'%3e%3c/path%3e%3c/svg%3e",l="/v3/vite.svg";function a(i){let c=0;const r=o=>{c=o,i.innerHTML=`count is ${c}`};i.addEventListener("click",()=>r(c+1)),r(0)}document.querySelector("#app").innerHTML=`
+ <div>
+ <a href="https://vite.dev" target="_blank">
+ <img src="${l}" class="logo" alt="Vite logo" />
+ </a>
+ <a href="https://www.typescriptlang.org/" target="_blank">
+ <img src="${n}" class="logo vanilla" alt="TypeScript logo" />
+ </a>
+ <h1>Vite + TypeScript</h1>
+ <div class="card">
+ <button id="counter" type="button"></button>
+ </div>
+ <p class="read-the-docs">
+ Click on the Vite and TypeScript logos to learn more
+ </p>
+ </div>
+`;a(document.querySelector("#counter"));
diff --git a/web/dist/v3/index.html b/web/dist/v3/index.html
new file mode 100644
index 0000000..bd98fe7
--- /dev/null
+++ b/web/dist/v3/index.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="icon" type="image/svg+xml" href="/v3/vite.svg" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>frontend-vanilla</title>
+ <script type="module" crossorigin src="/v3/assets/index-DfsH-YDt.js"></script>
+ <link rel="stylesheet" crossorigin href="/v3/assets/index-BmeGit54.css">
+ </head>
+ <body>
+ <div id="app"></div>
+ </body>
+</html>
diff --git a/web/dist/v3/vite.svg b/web/dist/v3/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/web/dist/v3/vite.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> \ No newline at end of file
diff --git a/web/frontend.go b/web/frontend.go
index d77c974..00074ec 100644
--- a/web/frontend.go
+++ b/web/frontend.go
@@ -8,46 +8,49 @@ import (
"strings"
)
-func ServeFrontend(w http.ResponseWriter, r *http.Request) {
- // Use fs.Sub to treat dist/v2 as the root
- box, err := fs.Sub(frontendFiles, "dist/v2")
- if err != nil {
- http.Error(w, "frontend not found", http.StatusNotFound)
- return
- }
+func ServeFrontend(distDir string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Use fs.Sub to treat distDir as the root
+ box, err := fs.Sub(frontendFiles, distDir)
+ if err != nil {
+ http.Error(w, "frontend not found", http.StatusNotFound)
+ return
+ }
- // Get the file path from the URL
- path := r.URL.Path
- path = strings.TrimPrefix(path, "/")
+ // Get the file path from the URL
+ path := r.URL.Path
+ path = strings.TrimPrefix(path, "/")
- // If path is empty, it's index.html
- if path == "" {
- path = "index.html"
- }
+ // If path is empty, it's index.html
+ if path == "" {
+ path = "index.html"
+ }
- // Try to open the file
- f, err := box.Open(path)
- if err != nil {
- // If file not found, serve index.html for client-side routing
- if !strings.Contains(filepath.Base(path), ".") {
- f, err = box.Open("index.html")
- if err != nil {
- http.Error(w, "frontend not found", http.StatusNotFound)
+ // Try to open the file
+ f, err := box.Open(path)
+ if err != nil {
+ // If file not found, serve index.html for client-side routing
+ // but only if it looks like a route (no extension)
+ if !strings.Contains(filepath.Base(path), ".") {
+ f, err = box.Open("index.html")
+ if err != nil {
+ http.Error(w, "frontend not found", http.StatusNotFound)
+ return
+ }
+ path = "index.html"
+ } else {
+ http.Error(w, "not found", http.StatusNotFound)
return
}
- path = "index.html"
- } else {
- http.Error(w, "not found", http.StatusNotFound)
+ }
+ defer func() { _ = f.Close() }()
+
+ d, err := f.Stat()
+ if err != nil {
+ http.Error(w, "internal error", http.StatusInternalServerError)
return
}
- }
- defer func() { _ = f.Close() }()
- d, err := f.Stat()
- if err != nil {
- http.Error(w, "internal error", http.StatusInternalServerError)
- return
+ http.ServeContent(w, r, path, d.ModTime(), f.(io.ReadSeeker))
}
-
- http.ServeContent(w, r, path, d.ModTime(), f.(io.ReadSeeker))
}
diff --git a/web/web.go b/web/web.go
index 1f1bd62..bb00773 100644
--- a/web/web.go
+++ b/web/web.go
@@ -35,7 +35,7 @@ var (
//go:embed static/*
staticFiles embed.FS
- //go:embed dist/v2/*
+ //go:embed dist/v2/* dist/v3/*
frontendFiles embed.FS
)
@@ -225,9 +225,12 @@ func NewRouter(cfg *config.Settings) http.Handler {
// New Frontend (React/Vite) from web/dist/v2
// Default route
- mux.Handle("/", GzipMiddleware(http.HandlerFunc(ServeFrontend)))
+ mux.Handle("/", GzipMiddleware(ServeFrontend("dist/v2")))
// Also keep /v2/ for explicit access
- mux.Handle("/v2/", GzipMiddleware(http.StripPrefix("/v2/", http.HandlerFunc(ServeFrontend))))
+ mux.Handle("/v2/", GzipMiddleware(http.StripPrefix("/v2/", ServeFrontend("dist/v2"))))
+
+ // Vanilla JS (v3)
+ mux.Handle("/v3/", GzipMiddleware(http.StripPrefix("/v3/", ServeFrontend("dist/v3"))))
// Legacy UI at /v1/
mux.Handle("/v1/", GzipMiddleware(http.StripPrefix("/v1/", AuthWrap(http.HandlerFunc(indexHandler)))))
diff --git a/web/web_test.go b/web/web_test.go
index ad79581..a208155 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -422,7 +422,7 @@ func TestServeFrontend(t *testing.T) {
rr := httptest.NewRecorder()
// Mimic the routing in Serve()
- handler := http.StripPrefix("/v2/", http.HandlerFunc(ServeFrontend))
+ handler := http.StripPrefix("/v2/", ServeFrontend("dist/v2"))
handler.ServeHTTP(rr, req)
// We expect 200 if built, or maybe panic if box not found (rice.MustFindBox)
@@ -517,7 +517,7 @@ func TestServeFrontendEdgeCases(t *testing.T) {
// 1. Missing file with extension should 404
req := httptest.NewRequest("GET", "/v2/missing.js", nil)
rr := httptest.NewRecorder()
- handler := http.StripPrefix("/v2/", http.HandlerFunc(ServeFrontend))
+ handler := http.StripPrefix("/v2/", ServeFrontend("dist/v2"))
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("Expected %d for missing asset, got %d", http.StatusNotFound, rr.Code)
@@ -632,7 +632,7 @@ func TestImageProxyHandlerInvalidBase64(t *testing.T) {
func TestServeFrontendNotFound(t *testing.T) {
req := httptest.NewRequest("GET", "/not-actually-a-file", nil)
rr := httptest.NewRecorder()
- ServeFrontend(rr, req)
+ ServeFrontend("dist/v2")(rr, req)
// Should fallback to index.html if it's not a dot-extension file
if rr.Code != http.StatusOK {
t.Errorf("Expected %d (fallback to index.html), got %d", http.StatusOK, rr.Code)
@@ -665,7 +665,7 @@ func TestImageProxyHeaders(t *testing.T) {
func TestServeFrontendAssetNotFound(t *testing.T) {
req := httptest.NewRequest("GET", "/static/missing.js", nil)
rr := httptest.NewRecorder()
- ServeFrontend(rr, req)
+ ServeFrontend("dist/v2")(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("Expected %d for missing asset, got %d", http.StatusNotFound, rr.Code)
}