From 6afe1f7b671f3652041461c53f60706d1d6f6ad6 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Thu, 8 Oct 2026 09:39:38 +0200 Subject: [PATCH] 0.0.9 --- .gitea/workflows/publish.yml | 2 +- README.md | 9 +- bun.lock | 4 +- index.ts | 1 + .../_gitignore" | 24 +++++ .../app/app.vue" | 83 ++++++++++++++++++ .../nuxt.config.ts" | 7 ++ .../package.json" | 24 +++++ .../public/favicon.ico" | Bin 0 -> 4286 bytes .../public/robots.txt" | 2 + .../tsconfig.json" | 18 ++++ .../package.json" | 22 +++-- .../src/app.css" | 66 ++------------ .../src/app.tsx" | 52 +++++++---- .../tsconfig.json" | 11 ++- .../vite.config.ts" | 8 +- .../package.json" | 21 +++-- .../src/app.d.ts" | 13 +++ .../src/lib/index.ts" | 2 +- .../src/routes/+layout.svelte" | 5 +- .../src/routes/+page.svelte" | 56 +++++++++--- .../svelte.config.js" | 17 ---- .../tsconfig.json" | 21 +---- .../vite.config.ts" | 16 +++- template/services/api/server/todo/todo.go | 26 +++--- 25 files changed, 340 insertions(+), 170 deletions(-) create mode 100644 "template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/_gitignore" create mode 100644 "template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/app/app.vue" create mode 100644 "template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/nuxt.config.ts" create mode 100644 "template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/package.json" create mode 100644 "template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/public/favicon.ico" create mode 100644 "template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/public/robots.txt" create mode 100644 "template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/tsconfig.json" create mode 100644 "template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/app.d.ts" delete mode 100644 "template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/svelte.config.js" diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index 9c27121..c86bfc8 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -7,7 +7,7 @@ on: jobs: publish: - runs-on: x86 + runs-on: local steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index f133752..89f2342 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Supported web frontends: - [SvelteKit](https://kit.svelte.dev/) - [Solid Start](https://start.solidjs.com/) +- [Nuxt](https://nuxt.com/) Supported mobile frontends: @@ -20,7 +21,7 @@ bun create glstack The CLI asks for: - **Project name** -- lowercase letters, digits and dashes; used for the directory, the database and the `@/rpc` package. An existing non-empty directory is never overwritten -- **Web frontend** -- SvelteKit, Solid Start or none +- **Web frontend** -- SvelteKit, Solid Start, Nuxt or none - **Mobile frontend** -- React Native + Expo or none - **Bundle identifier** (Expo only) -- reverse-DNS id used as both the iOS bundle identifier and the Android package, e.g. `com.example.app` - **Go package prefix** -- e.g. `github.com/you`, giving the module path `github.com/you/` @@ -31,7 +32,7 @@ The CLI asks for: A monorepo with the following structure: - **`services/api`** -- Go backend using [Cobra](https://github.com/spf13/cobra) for CLI, [pgx](https://github.com/jackc/pgx) for Postgres, and [Connect RPC](https://connectrpc.com/) for the API layer -- **`apps/web`** -- Web frontend app (SvelteKit or Solid Start) +- **`apps/web`** -- Web frontend app (SvelteKit, Solid Start or Nuxt) - **`apps/mobile`** -- Optional [React Native + Expo](https://expo.dev/) mobile app sharing the same RPC client - **`packages/proto`** -- Protobuf service definitions with [Buf](https://buf.build/) for codegen (Go + TypeScript) - **`packages/rpc`** -- Generated TypeScript Connect RPC client shared across frontend apps @@ -64,7 +65,7 @@ The app reaches the API at `127.0.0.1:8080` everywhere: the iOS simulator shares The example apps poll the API every 2 seconds, so a change made on one device or in the web app shows up on the others. -The native `ios/` and `android/` folders are generated from `app.json` and not committed. After changing native config or upgrading Expo, delete them and they are regenerated on the next run. +The native `ios/` and `android/` folders are generated from `app.json` and not committed. After changing native config or upgrading Expo, delete them and they are regenerated on the next run. The same applies when a native build can't find files under `node_modules`: the folders hold absolute paths into `node_modules` from when they were generated. #### iOS on macOS @@ -81,7 +82,7 @@ CocoaPods comes from Nix. The iOS build uses `/Applications/Xcode.app` (or the X ### Starter example -The generated project includes a working Todo CRUD example wired end-to-end: protobuf schema, Go service implementation, SQL queries, and frontend UI. In the mobile app, rows animate into place with [Reanimated](https://docs.swmansion.com/react-native-reanimated/) when a todo is checked off and the list re-sorts. +The generated project includes a working Todo CRUD example wired end-to-end: protobuf schema, Go service implementation, SQL queries, and frontend UI. When a todo is checked off and the list re-sorts, rows animate into place: with [Reanimated](https://docs.swmansion.com/react-native-reanimated/) in the mobile app and the [GSAP](https://gsap.com/) Flip plugin in the web apps. ## License diff --git a/bun.lock b/bun.lock index ea2fa97..979b0a4 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "glstack", "dependencies": { "@clack/prompts": "^1.2.0", - "@gregorlohaus/tdir": "^0.2.0", + "@gregorlohaus/tdir": "^0.3.0", "zod": "^4.3.6", }, "devDependencies": { @@ -22,7 +22,7 @@ "@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="], - "@gregorlohaus/tdir": ["@gregorlohaus/tdir@0.2.0", "", { "peerDependencies": { "zod": "^4" }, "bin": { "tdir": "dist/cli.js" } }, "sha512-LwISQC7iARezu7SQZJk9TV8Sqp8I/fVMssH18lWL1KD/ETpWp2LwwTzm/j9aWcs+Eo5OBQctbqFTafo9Nfue+A=="], + "@gregorlohaus/tdir": ["@gregorlohaus/tdir@0.3.0", "", { "peerDependencies": { "zod": "^4" }, "bin": { "tdir": "dist/cli.js" } }, "sha512-b6l+yXdyFpPI573iuMCdVHkJjD3j+fN7LRHnHzorU0QGRh6OVrOZsiiqMA5/9v+RiAF5SDBrZckj8zp5WoqLtA=="], "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], diff --git a/index.ts b/index.ts index 9c4ea5b..8bda8b9 100644 --- a/index.ts +++ b/index.ts @@ -38,6 +38,7 @@ const project = await p.group( options: [ { value: "svelte-kit", label: "SvelteKit" }, { value: "solid-start", label: "SolidStart" }, + { value: "nuxt", label: "Nuxt" }, { value: "none", label: "None" } ] }) diff --git "a/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/_gitignore" "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/_gitignore" new file mode 100644 index 0000000..4a7f73a --- /dev/null +++ "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/_gitignore" @@ -0,0 +1,24 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example diff --git "a/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/app/app.vue" "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/app/app.vue" new file mode 100644 index 0000000..1a1ddca --- /dev/null +++ "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/app/app.vue" @@ -0,0 +1,83 @@ + + + + + diff --git "a/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/nuxt.config.ts" "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/nuxt.config.ts" new file mode 100644 index 0000000..5717321 --- /dev/null +++ "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/nuxt.config.ts" @@ -0,0 +1,7 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: '2025-07-15', + devtools: { enabled: true }, + // Same port as the other web templates, which the API allows via CORS. + devServer: { port: 5173 }, +}) diff --git "a/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/package.json" "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/package.json" new file mode 100644 index 0000000..6acbaca --- /dev/null +++ "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/package.json" @@ -0,0 +1,24 @@ +{ + "name": "web", + "type": "module", + "private": true, + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "postinstall": "nuxt prepare", + "check": "nuxt typecheck" + }, + "dependencies": { + "@<@var(context.project.name)>/rpc": "workspace:*", + "gsap": "^3.15.0", + "nuxt": "^4.6.0", + "vue": "^3.5.43", + "vue-router": "^5.3.1" + }, + "devDependencies": { + "typescript": "^6.0.3", + "vue-tsc": "^3.3.12" + } +} diff --git "a/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/public/favicon.ico" "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/public/favicon.ico" new file mode 100644 index 0000000000000000000000000000000000000000..18993ad91cfd43e03b074dd0b5cc3f37ab38e49c GIT binary patch literal 4286 zcmeHLOKuuL5PjK%MHWVi6lD zOGiREbCw`xmFozJ^aNatJY>w+g ze6a2@u~m#^BZm@8wco9#Crlli0uLb^3E$t2-WIc^#(?t)*@`UpuofJ(Uyh@F>b3Ph z$D^m8Xq~pTkGJ4Q`Q2)te3mgkWYZ^Ijq|hkiP^9`De={bQQ%heZC$QU2UpP(-tbl8 zPWD2abEew;oat@w`uP3J^YpsgT%~jT(Dk%oU}sa$7|n6hBjDj`+I;RX(>)%lm_7N{+B7Mu%H?422lE%MBJH!!YTN2oT7xr>>N-8OF$C&qU^ z>vLsa{$0X%q1fjOe3P1mCv#lN{xQ4_*HCSAZjTb1`}mlc+9rl8$B3OP%VT@mch_~G z7Y+4b{r>9e=M+7vSI;BgB?ryZDY4m>&wcHSn81VH1N~`0gvwH{ z8dv#hG|OK`>1;j7tM#B)Z7zDN?{6=dUal}$eweb/public/robots.txt" "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/public/robots.txt" new file mode 100644 index 0000000..0ad279c --- /dev/null +++ "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/public/robots.txt" @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: diff --git "a/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/tsconfig.json" "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/tsconfig.json" new file mode 100644 index 0000000..307b213 --- /dev/null +++ "b/template/apps/<@if(eq(context.project.frontend,\"nuxt\"))>web/tsconfig.json" @@ -0,0 +1,18 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "files": [], + "references": [ + { + "path": "./.nuxt/tsconfig.app.json" + }, + { + "path": "./.nuxt/tsconfig.server.json" + }, + { + "path": "./.nuxt/tsconfig.shared.json" + }, + { + "path": "./.nuxt/tsconfig.node.json" + } + ] +} diff --git "a/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/package.json" "b/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/package.json" index 1546f3b..b4a2d3c 100644 --- "a/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/package.json" +++ "b/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/package.json" @@ -1,21 +1,25 @@ { - "name": "example-bare", + "name": "web", "type": "module", "scripts": { "dev": "vite dev", "build": "vite build", - "start": "vite start", - "preview": "vite preview" + "start": "node .output/server/index.mjs", + "preview": "vite preview", + "check": "tsc --noEmit" }, "dependencies": { "@<@var(context.project.name)>/rpc": "workspace:*", - "@solidjs/start": "2.0.0-alpha.2", - "@solidjs/vite-plugin-nitro-2": "^0.1.0", - "solid-js": "^1.9.5", - "vite": "^7.0.0" + "@solidjs/start": "^2.0.0", + "solid-js": "^1.9.14", + "vite": "^8.0.0", + "nitro": "^3.0.260610-beta", + "gsap": "^3.15.0" }, "engines": { - "node": ">=22" + "node": ">=24" }, - "devDependencies": {} + "devDependencies": { + "typescript": "^6.0.3" + } } diff --git "a/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/src/app.css" "b/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/src/app.css" index a4d2e55..f4374c9 100644 --- "a/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/src/app.css" +++ "b/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/src/app.css" @@ -1,61 +1,5 @@ -body { - font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; -} - -a { - margin-right: 1rem; -} - -main { - text-align: center; - padding: 1em; - margin: 0 auto; -} - -h1 { - color: #335d92; - text-transform: uppercase; - font-size: 4rem; - font-weight: 100; - line-height: 1.1; - margin: 4rem auto; - max-width: 14rem; -} - -p { - max-width: 14rem; - margin: 2rem auto; - line-height: 1.35; -} - -@media (min-width: 480px) { - h1 { - max-width: none; - } - - p { - max-width: none; - } -} - -.increment { - font-family: inherit; - font-size: inherit; - padding: 1em 2em; - color: #335d92; - background-color: rgba(68, 107, 158, 0.1); - border-radius: 2em; - border: 2px solid rgba(68, 107, 158, 0); - outline: none; - width: 200px; - font-variant-numeric: tabular-nums; - cursor: pointer; -} - -.increment:focus { - border: 2px solid #335d92; -} - -.increment:active { - background-color: rgba(68, 107, 158, 0.2); -} +main { max-width: 32rem; margin: 2rem auto; font-family: system-ui, sans-serif; } +form, .todo-row { display: flex; gap: 0.75rem; align-items: center; margin-bottom: 0.75rem; } +input[type="text"] { flex: 1; padding: 0.5rem; border: 1px solid #8e8e93; border-radius: 8px; } +.todo-row span { flex: 1; } +.offline { color: #d32f2f; } diff --git "a/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/src/app.tsx" "b/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/src/app.tsx" index 8fd5136..f7c2ee1 100644 --- "a/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/src/app.tsx" +++ "b/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/src/app.tsx" @@ -1,7 +1,10 @@ import { createSignal, For, onCleanup, onMount, Show } from "solid-js"; +import { gsap } from "gsap"; +import { Flip } from "gsap/Flip"; import { createRouter, Todo } from "@<@var(context.project.name)>/rpc" import "./app.css"; +gsap.registerPlugin(Flip) const router = createRouter("http://127.0.0.1:8080") // Changes made elsewhere (another tab, the mobile app) show up within this interval. const syncIntervalMs = 2000 @@ -10,11 +13,25 @@ export default function App() { const [todos, setTodos] = createSignal([]); const [offline, setOffline] = createSignal(false); const [todoToCreateTask, setTodoToCreateTask] = createSignal(""); + + // Like the mobile app: rows spring into their new place when the list re-sorts, new rows fade in. + // Solid recreates rows for new objects; data-flip-id lets Flip match them to the old ones. + const showTodos = (next: Todo[]) => { + const rows = Flip.getState(".todo-row") + setTodos(next) + Flip.from(rows, { + targets: ".todo-row", + duration: 0.5, + ease: "back.out(1.4)", + onEnter: (entering) => gsap.fromTo(entering, { opacity: 0 }, { opacity: 1, duration: 0.3 }), + }) + } + const fetchTodos = () => { router.todos.listTodos({}).then((r) => { setOffline(false) - // Polling returns the same list most of the time; keep the old array so keeps its rows. - if (JSON.stringify(todos()) !== JSON.stringify(r.todos)) setTodos(r.todos) + // Polling returns the same list most of the time; skip re-rendering it. + if (JSON.stringify(todos()) !== JSON.stringify(r.todos)) showTodos(r.todos) }).catch(() => setOffline(true)) } onMount(() => { @@ -30,7 +47,8 @@ export default function App() { } const deleteTodo = (id: string) => { - return () => { + return async (event: Event & { currentTarget: HTMLElement }) => { + await gsap.to(event.currentTarget.closest(".todo-row"), { opacity: 0, duration: 0.3 }) router.todos.deleteTodo({ todo: { id } }).then(fetchTodos, () => setOffline(true)) } } @@ -45,20 +63,22 @@ export default function App() { } return ( -
-

Create Todo

-

Can't reach the API, retrying…

- setTodoToCreateTask(e.currentTarget.value)} type="text" placeholder="What needs to be done?"/> - +
+

Create Todo

+

Can't reach the API, retrying…

+
{ event.preventDefault(); createTodo() }}> + setTodoToCreateTask(e.currentTarget.value)} type="text" placeholder="What needs to be done?"/> + +
- {(item:Todo) => -
- {item.task} - - -
- } + {(todo: Todo) => +
+ {todo.task} + + +
+ }
-
+ ); } diff --git "a/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/tsconfig.json" "b/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/tsconfig.json" index 7692957..561169a 100644 --- "a/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/tsconfig.json" +++ "b/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/tsconfig.json" @@ -10,10 +10,15 @@ "allowJs": true, "strict": true, "noEmit": true, - "types": ["vite/client"], + "types": [ + "vite/client" + ], "isolatedModules": true, "paths": { - "~/*": ["./src/*"] - } + "~/*": [ + "./src/*" + ] + }, + "skipLibCheck": true } } diff --git "a/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/vite.config.ts" "b/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/vite.config.ts" index 4c93687..d172098 100644 --- "a/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/vite.config.ts" +++ "b/template/apps/<@if(eq(context.project.frontend,\"solid-start\"))>web/vite.config.ts" @@ -1,10 +1,14 @@ import { defineConfig } from "vite"; -import { nitroV2Plugin as nitro } from "@solidjs/vite-plugin-nitro-2"; +import { nitro } from "nitro/vite"; import { solidStart } from "@solidjs/start/config"; export default defineConfig({ plugins: [solidStart(), nitro() - ] + ], + // Same port as the other web templates, which the API allows via CORS. + server: { port: 5173 }, + // SolidStart's dev toolbar imports this CommonJS package in the browser, which fails unless prebundled. + optimizeDeps: { include: ["@solidjs/start > @jridgewell/trace-mapping"] }, }); diff --git "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/package.json" "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/package.json" index 322e3f3..a3bc759 100644 --- "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/package.json" +++ "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/package.json" @@ -12,15 +12,20 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" }, "devDependencies": { - "@sveltejs/adapter-auto": "^7.0.1", - "@sveltejs/kit": "^2.57.0", - "@sveltejs/vite-plugin-svelte": "^7.0.0", - "svelte": "^5.55.2", - "svelte-check": "^4.4.6", - "typescript": "^6.0.2", - "vite": "^8.0.7" + "@sveltejs/adapter-auto": "^8.0.0", + "@sveltejs/kit": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^7.2.0", + "svelte": "^5.57.1", + "svelte-check": "^4.6.0", + "typescript": "^6.0.3", + "vite": "^8.3.0" + }, + "imports": { + "#lib": "./src/lib/index.js", + "#lib/*": "./src/lib/*" }, "dependencies": { - "@<@var(context.project.name)>/rpc": "workspace:*" + "@<@var(context.project.name)>/rpc": "workspace:*", + "gsap": "^3.15.0" } } diff --git "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/app.d.ts" "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/app.d.ts" new file mode 100644 index 0000000..da08e6d --- /dev/null +++ "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/app.d.ts" @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/lib/index.ts" "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/lib/index.ts" index 856f2b6..73c55ef 100644 --- "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/lib/index.ts" +++ "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/lib/index.ts" @@ -1 +1 @@ -// place files you want to import through the `$lib` alias in this folder. +// place files you want to import through the `#lib` alias in this folder. diff --git "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/routes/+layout.svelte" "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/routes/+layout.svelte" index 9cebde5..7a8b5d1 100644 --- "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/routes/+layout.svelte" +++ "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/routes/+layout.svelte" @@ -1,7 +1,8 @@ diff --git "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/routes/+page.svelte" "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/routes/+page.svelte" index b9636ce..0c802d6 100644 --- "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/routes/+page.svelte" +++ "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/src/routes/+page.svelte" @@ -1,16 +1,35 @@ -
+

Create Todo

- {#if offline}

Can't reach the API, retrying…

{/if} - - + {#if offline}

Can't reach the API, retrying…

{/if} +
{ event.preventDefault(); createTodo() }}> + + +
{#each todos as todo (todo.id)} -
- {todo.task} - - -
+
+ {todo.task} + + +
{/each} -
+ + + diff --git "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/svelte.config.js" "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/svelte.config.js" deleted file mode 100644 index 0c3412e..0000000 --- "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/svelte.config.js" +++ /dev/null @@ -1,17 +0,0 @@ -import adapter from '@sveltejs/adapter-auto'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - compilerOptions: { - // Force runes mode for the project, except for libraries. Can be removed in svelte 6. - runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) - }, - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter() - } -}; - -export default config; diff --git "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/tsconfig.json" "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/tsconfig.json" index 2c2ed3c..62f2df9 100644 --- "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/tsconfig.json" +++ "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/tsconfig.json" @@ -1,20 +1,7 @@ { - "extends": "./.svelte-kit/tsconfig.json", + "extends": "$app/tsconfig", "compilerOptions": { - "rewriteRelativeImportExtensions": true, - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // To make changes to top-level options such as include and exclude, we recommend extending - // the generated config; see https://svelte.dev/docs/kit/configuration#typescript + "strict": true + }, + "include": ["src", "vite.config.ts"] } diff --git "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/vite.config.ts" "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/vite.config.ts" index bbf8c7d..cb76b81 100644 --- "a/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/vite.config.ts" +++ "b/template/apps/<@if(eq(context.project.frontend,\"svelte-kit\"))>web/vite.config.ts" @@ -1,6 +1,20 @@ +import adapter from '@sveltejs/adapter-auto'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()] + plugins: [ + sveltekit({ + compilerOptions: { + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => + filename.split(/[/\\]/).includes('node_modules') ? undefined : true + }, + + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + }) + ] }); diff --git a/template/services/api/server/todo/todo.go b/template/services/api/server/todo/todo.go index 0efafad..3668850 100644 --- a/template/services/api/server/todo/todo.go +++ b/template/services/api/server/todo/todo.go @@ -59,20 +59,20 @@ func (srv *TodoServer) ListTodos(ctx context.Context, req *connect.Request[todov Done: BoolPtr(todo.Done.Bool), }) } - slices.SortFunc(responseTodos, func(a, b *todov1.Todo) int { - dateCmp := cmp.Compare(a.GetCreatedAt(), b.GetCreatedAt()) - return dateCmp - }) - slices.SortFunc(responseTodos, func(a, b *todov1.Todo) int { - var boolToInt = func(b bool) int { - if b { - return 1 - } else { - return 0 - } + // Open todos first, each group oldest first. A single total order (SortFunc isn't stable) + // keeps rows from swapping places between polls. + boolToInt := func(b bool) int { + if b { + return 1 } - doneCmp := cmp.Compare(boolToInt(*a.Done), boolToInt(*b.Done)) - return doneCmp + return 0 + } + slices.SortFunc(responseTodos, func(a, b *todov1.Todo) int { + return cmp.Or( + cmp.Compare(boolToInt(a.GetDone()), boolToInt(b.GetDone())), + cmp.Compare(a.GetCreatedAt(), b.GetCreatedAt()), + cmp.Compare(a.GetId(), b.GetId()), + ) }) return &connect.Response[todov1.ListTodosResponse]{