create-tinyjoin 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +70 -4
  2. package/cli.js +4 -4
  3. package/package.json +1 -1
  4. package/templates/AGENTS.md.hbs +67 -12
  5. package/templates/README.md.hbs +74 -13
  6. package/templates/client/index.html.hbs +48 -45
  7. package/templates/client/package.json.hbs +4 -2
  8. package/templates/client/public/favicon.svg +6 -0
  9. package/templates/client/public/js.svg +5 -0
  10. package/templates/client/public/ts.svg +6 -0
  11. package/templates/client/src/app.ts.hbs +37 -0
  12. package/templates/client/src/button.css.hbs +30 -0
  13. package/templates/client/src/button.ts.hbs +19 -0
  14. package/templates/client/src/database.ts.hbs +62 -25
  15. package/templates/client/src/error.css.hbs +6 -0
  16. package/templates/client/src/error.ts.hbs +39 -0
  17. package/templates/client/src/index.ts.hbs +7 -0
  18. package/templates/client/src/info.css.hbs +93 -0
  19. package/templates/client/src/info.ts.hbs +42 -0
  20. package/templates/client/src/input.css.hbs +22 -0
  21. package/templates/client/src/input.ts.hbs +16 -0
  22. package/templates/client/src/loading.css.hbs +19 -0
  23. package/templates/client/src/loading.ts.hbs +12 -0
  24. package/templates/client/src/title.css.hbs +16 -0
  25. package/templates/client/src/title.ts.hbs +16 -0
  26. package/templates/client/src/todoInput.css.hbs +16 -0
  27. package/templates/client/src/todoInput.ts.hbs +50 -0
  28. package/templates/client/src/todoItem.css.hbs +33 -0
  29. package/templates/client/src/todoItem.ts.hbs +31 -0
  30. package/templates/client/src/todoList.css.hbs +15 -0
  31. package/templates/client/src/todoList.ts.hbs +89 -0
  32. package/templates/client/src/topBar.css.hbs +13 -0
  33. package/templates/client/src/topBar.ts.hbs +13 -0
  34. package/templates/client/vite.config.ts.hbs +6 -0
  35. package/templates/client/src/main.ts.hbs +0 -159
  36. package/templates/client/src/style.css.hbs +0 -199
package/README.md CHANGED
@@ -1,20 +1,36 @@
1
1
  # create-tinyjoin
2
2
 
3
- Scaffold a small local todo app with TinyJoin, TypeScript, and Vite:
3
+ Scaffold a small local todo app with TinyJoin and Vite:
4
4
 
5
5
  ```sh
6
6
  npm create tinyjoin@latest
7
7
  ```
8
8
 
9
9
  The generated project is ready to run at its root and needs no database server,
10
- account, or credentials. Choose whether its todos should remain after reloads or
11
- start fresh each time.
10
+ account, or credentials. Choose TypeScript or JavaScript, and whether its todos
11
+ should remain after reloads or start fresh each time.
12
+
13
+ Your app should look something like this:
14
+
15
+ ![Todo App](screenshots/todos.png)
16
+
17
+ - Task list with add/complete/delete
18
+ - Single TinyJoin database with one `todos` table
19
+ - Demonstrates basic CRUD operations in SQL
20
+ - Automatic multi-tab access for saved data
21
+ - Offline production app loading after the first online visit
22
+ - Perfect starter example
23
+
24
+ It shares its markup and styling with the equivalent
25
+ [create-tinybase](https://github.com/tinyplex/create-tinybase) vanilla
26
+ TypeScript starter, with the TinyJoin accent color and system fonts.
12
27
 
13
28
  For agents and CI, every option can be supplied non-interactively:
14
29
 
15
30
  ```sh
16
31
  npm create tinyjoin@latest -- --non-interactive \
17
32
  --projectName my-tinyjoin-app \
33
+ --language typescript \
18
34
  --storage opfs \
19
35
  --installAndRun false
20
36
  ```
@@ -25,15 +41,65 @@ usage.
25
41
  `npm run build` assembles the publishable `create-tinyjoin` package under
26
42
  `dist/`.
27
43
 
44
+ ## Generated structure
45
+
46
+ The generated project is a single Vite application at its root (`.ts` below
47
+ becomes `.js` when JavaScript is chosen):
48
+
49
+ - `index.html` loads the app and defines its theme variables.
50
+ - `vite.config.ts` enables production offline loading through `tinyjoinOffline()`.
51
+ - `src/index.ts` bootstraps the app on load.
52
+ - `src/app.ts` builds the app shell, showing a spinner until the database opens.
53
+ - `src/database.ts` opens the database, defines the `todos` table, and adds the
54
+ starter todos on the first visit.
55
+ - `src/todoInput.ts`, `src/todoList.ts`, and `src/todoItem.ts` are the todo
56
+ interface, each with a colocated stylesheet.
57
+ - `src/topBar.ts`, `src/title.ts`, `src/info.ts`, `src/loading.ts`,
58
+ `src/button.ts`, and `src/input.ts` are the shared interface pieces.
59
+
60
+ ## TypeScript and JavaScript
61
+
62
+ The templates are written once, in TypeScript. Choosing JavaScript strips their
63
+ types with `ts-blank-space` and renames the outputs to `.js`, exactly as
64
+ `create-tinybase` does: `getFiles` marks each `.ts` template `transpile` when
65
+ the JavaScript language is chosen, and `tinycreate` blanks the types during
66
+ post-processing. A JavaScript project also drops `tsconfig.json`, the
67
+ `typescript` devDependency, and the `tsc --noEmit` step from its build script.
68
+
69
+ ## Offline builds
70
+
71
+ Generated applications use `tinyjoinOffline()` from `tinyjoin/vite`. The
72
+ production build includes the application and complete database runtime in
73
+ its offline copy, including files loaded only when persistent storage opens.
74
+ There are no remote font requests. Development remains a normal Vite session;
75
+ use `npm run build` and `npm run preview` to test offline behavior.
76
+
77
+ New application versions wait for open tabs to close before activating. This
78
+ keeps existing pages on their matching build; it does not migrate database
79
+ schemas. Applications with their own service worker can use the plugin's
80
+ manifest mode. Offline loading and saved local data do not synchronize with
81
+ a server or another device.
82
+
28
83
  ## Development
29
84
 
30
85
  ```sh
86
+ npm run spell # cspell over the sources, templates, and docs
31
87
  npm run typecheck # generator and test TypeScript
32
- npm test # CLI, template, and package-boundary tests
88
+ npm test # CLI, template, snapshot, and package-boundary tests
33
89
  npm run test:generated # generated apps against published TinyJoin
34
90
  npm run test:e2e # real generated-app behavior in Chromium
35
91
  ```
36
92
 
93
+ `npm run publishPackage` runs all of the above before publishing. Add project
94
+ vocabulary to `cspell.json` rather than disabling the check.
95
+
96
+ `npm test` compares every generated file, for both languages and both storage
97
+ modes, against the snapshots in `test/__snapshots__`. Run
98
+ `npx vitest run test/cli.test.ts -u` to accept intentional template changes.
99
+ `npm run test:generated` installs and builds all four combinations, and
100
+ `npm run test:e2e` then drives the two saved apps in Chromium and tests offline
101
+ production loading for all four combinations.
102
+
37
103
  Run the local generator from this repository's parent directory with:
38
104
 
39
105
  ```sh
package/cli.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import{existsSync as c}from"node:fs";import{dirname as l,join as r}from"node:path";import{fileURLToPath as p}from"node:url";import{createCLI as u,detectPackageManager as m}from"tinycreate";const g=r(l(p(import.meta.url)),"templates"),s=process.argv.slice(2),i=[{title:"Save data across reloads (recommended)",value:"opfs"},{title:"Start fresh each time",value:"memory"}],d={command:"npm create tinyjoin@latest --",nonInteractiveFlag:"--non-interactive",options:{projectName:{type:"string",required:!0},storage:{values:i.map(({value:e})=>e),required:!0,default:"opfs"},installAndRun:{values:[!0,!1],required:!0,recommendedForAgents:!1}}};(s.includes("--help")||s.includes("-h"))&&(console.log(`create-tinyjoin
2
+ import{existsSync as g}from"node:fs";import{dirname as m,join as a}from"node:path";import{fileURLToPath as d}from"node:url";import{createCLI as f,detectPackageManager as y}from"tinycreate";const h=a(m(d(import.meta.url)),"templates"),r=process.argv.slice(2),c=[{title:"TypeScript",value:"typescript"},{title:"JavaScript",value:"javascript"}],p=[{title:"Save data across reloads (recommended)",value:"opfs"},{title:"Start fresh each time",value:"memory"}],v={command:"npm create tinyjoin@latest --",nonInteractiveFlag:"--non-interactive",options:{projectName:{type:"string",required:!0},language:{values:c.map(({value:t})=>t),required:!0,default:"typescript"},storage:{values:p.map(({value:t})=>t),required:!0,default:"opfs"},installAndRun:{values:[!0,!1],required:!0,recommendedForAgents:!1}}};(r.includes("--help")||r.includes("-h"))&&(console.log(`create-tinyjoin
3
3
 
4
4
  Interactively scaffold a TinyJoin application:
5
5
  npm create tinyjoin@latest
6
6
 
7
7
  Run non-interactively:
8
8
  npm create tinyjoin@latest -- --non-interactive \\
9
- --projectName my-tinyjoin-app --storage opfs \\
9
+ --projectName my-tinyjoin-app --language typescript --storage opfs \\
10
10
  --installAndRun false
11
11
 
12
12
  Agent and automation commands:
13
13
  --list-options Print the current option catalog as JSON
14
- --help Show this help`),process.exit(0)),s.includes("--list-options")&&(console.log(JSON.stringify(d,null,2)),process.exit(0));const f={welcomeMessage:`\u{1F389} Welcome to TinyJoin!
15
- `,questions:[{type:"text",name:"projectName",message:"Project name:",initial:"my-tinyjoin-app",validate:a},{type:"select",name:"storage",message:"Todo data:",choices:[...i],initial:0},{type:"confirm",name:"installAndRun",message:"Install dependencies and start the app?",initial:!0}],createContext:e=>{const t=String(e.projectName??"").trim(),n=a(t);if(n!==!0)throw new TypeError(n);const o=h(e.storage===0?"opfs":e.storage??"opfs","storage",["opfs","memory"]);return{projectName:t,installAndRun:e.installAndRun===!0||e.installAndRun==="true",storage:o,usesOpfs:o==="opfs",storageName:y(t),tinyjoinDependency:process.env.CREATE_TINYJOIN_DEPENDENCY??"^0.0.5"}},getFiles:()=>[{template:"README.md.hbs",output:"README.md",prettier:!0},{template:"AGENTS.md.hbs",output:"AGENTS.md",prettier:!0},{template:"client/package.json.hbs",output:"package.json",prettier:!0},{template:"client/.gitignore.hbs",output:".gitignore"},{template:"client/index.html.hbs",output:"index.html",prettier:!0},{template:"client/tsconfig.json.hbs",output:"tsconfig.json",prettier:!0},{template:"client/src/database.ts.hbs",output:"src/database.ts",prettier:!0},{template:"client/src/main.ts.hbs",output:"src/main.ts",prettier:!0},{template:"client/src/style.css.hbs",output:"src/style.css",prettier:!0}],templateRoot:g,installCommand:"{pm} install",devCommand:"{pm} run dev",workingDirectory:".",onSuccess:e=>{const t=m();console.log("Next steps:"),console.log(` cd ${e}`),console.log(` ${t} install`),console.log(` ${t} run dev`)}};u(f).catch(e=>{console.error(e instanceof Error?e.message:String(e)),process.exit(1)});function a(e){return!/^[a-z0-9][a-z0-9._-]*$/i.test(e)||e==="."||e===".."?"Use letters, numbers, dots, dashes, or underscores without path separators.":c(r(process.cwd(),e))?`Directory "${e}" already exists. Choose a different name.`:!0}function h(e,t,n){const o=typeof e=="string"?e:"";if(!n.includes(o))throw new TypeError(`${j(t)} must be one of: ${n.join(", ")}.`);return o}function y(e){const t=e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"app",n="tinyjoin-",o="-db-v1";return`${n}${t.slice(0,64-n.length-o.length)}${o}`}function j(e){return`${e.charAt(0).toUpperCase()}${e.slice(1)}`}
14
+ --help Show this help`),process.exit(0)),r.includes("--list-options")&&(console.log(JSON.stringify(v,null,2)),process.exit(0));const j={welcomeMessage:`\u{1F389} Welcome to TinyJoin!
15
+ `,questions:[{type:"text",name:"projectName",message:"Project name:",initial:"my-tinyjoin-app",validate:l},{type:"select",name:"language",message:"Language:",choices:[...c],initial:0},{type:"select",name:"storage",message:"Todo data:",choices:[...p],initial:0},{type:"confirm",name:"installAndRun",message:"Install dependencies and start the app?",initial:!0}],createContext:t=>{const n=String(t.projectName??"").trim(),o=l(n);if(o!==!0)throw new TypeError(o);const e=u(t.language===0?"typescript":t.language??"typescript","language",["typescript","javascript"]),s=u(t.storage===0?"opfs":t.storage??"opfs","storage",["opfs","memory"]),i=e==="typescript";return{projectName:n,installAndRun:t.installAndRun===!0||t.installAndRun==="true",language:e,typescript:i,javascript:!i,scriptExt:i?"ts":"js",ext:i?"ts":"js",storage:s,usesOpfs:s==="opfs",storageName:b(n),tinyjoinDependency:process.env.CREATE_TINYJOIN_DEPENDENCY??"^0.1.0"}},getFiles:t=>{const{ext:n,typescript:o}=t,e=i=>({template:`client/src/${i}.ts.hbs`,output:`src/${i}.${n}`,prettier:!0,transpile:!o}),s=i=>({template:`client/src/${i}.css.hbs`,output:`src/${i}.css`,prettier:!0});return[{template:"README.md.hbs",output:"README.md",prettier:!0},{template:"AGENTS.md.hbs",output:"AGENTS.md",prettier:!0},{template:"client/package.json.hbs",output:"package.json",prettier:!0},{template:"client/.gitignore.hbs",output:".gitignore"},{template:"client/index.html.hbs",output:"index.html",prettier:!0},{template:"client/vite.config.ts.hbs",output:`vite.config.${n}`,prettier:!0,transpile:!o},...o?[{template:"client/tsconfig.json.hbs",output:"tsconfig.json",prettier:!0}]:[],{template:"client/public/favicon.svg",output:"public/favicon.svg"},o?{template:"client/public/ts.svg",output:"public/ts.svg"}:{template:"client/public/js.svg",output:"public/js.svg"},e("index"),e("app"),e("topBar"),s("topBar"),e("title"),s("title"),e("info"),s("info"),e("loading"),s("loading"),e("error"),s("error"),e("button"),s("button"),e("input"),s("input"),e("database"),e("todoInput"),s("todoInput"),e("todoList"),s("todoList"),e("todoItem"),s("todoItem")]},templateRoot:h,installCommand:"{pm} install",devCommand:"{pm} run dev",workingDirectory:".",onSuccess:t=>{const n=y();console.log("Next steps:"),console.log(` cd ${t}`),console.log(` ${n} install`),console.log(` ${n} run dev`)}};f(j).catch(t=>{console.error(t instanceof Error?t.message:String(t)),process.exit(1)});function l(t){return!/^[a-z0-9][a-z0-9._-]*$/i.test(t)||t==="."||t===".."?"Use letters, numbers, dots, dashes, or underscores without path separators.":g(a(process.cwd(),t))?`Directory "${t}" already exists. Choose a different name.`:!0}function u(t,n,o){const e=typeof t=="string"?t:"";if(!o.includes(e))throw new TypeError(`${x(n)} must be one of: ${o.join(", ")}.`);return e}function b(t){const n=t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"app",o="tinyjoin-",e="-db-v2";return`${o}${n.slice(0,64-o.length-e.length)}${e}`}function x(t){return`${t.charAt(0).toUpperCase()}${t.slice(1)}`}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-tinyjoin",
3
- "version": "0.0.5",
3
+ "version": "0.1.0",
4
4
  "author": "jamesgpearce",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,15 +1,70 @@
1
- # {{projectName}}
1
+ # TinyJoin Application Guide
2
2
 
3
- This is a generated TinyJoin TypeScript/Vite starter.
3
+ This project was generated by `create-tinyjoin`. Preserve its working data
4
+ lifecycle while adapting the application to the user's requirements.
4
5
 
5
- - Run the app from this directory with `npm run dev`.
6
- - Keep database setup in `src/database.ts` and application UI in `src/main.ts`.
7
- - Keep schema setup safe to run again when the app reloads.
8
- - Use TinyJoin's `sql` tagged template or parameter arrays for application
9
- values. Do not concatenate user input into SQL.
10
- - Generate stable row identifiers in application code.
11
- - Keep the database client open while the app is running and close it during
12
- page teardown.
13
- - Validate changes with `npm run build`.
6
+ ## Selected Architecture
14
7
 
15
- Current TinyJoin documentation: https://tinyjoin.org
8
+ - **Application**: todo list
9
+ - **Language**: {{#if typescript}}TypeScript{{else}}JavaScript{{/if}}
10
+ - **UI**: Vanilla JS
11
+ - **Data**: TinyJoin relational database in the browser
12
+ - **Persistence**: {{#if usesOpfs}}saved in this browser across reloads{{else}}kept for the current visit only{{/if}}
13
+
14
+ The database client is opened once in `src/database.{{ext}}` and shared through
15
+ the `databaseReady` promise. Reuse it within the application.{{#if usesOpfs}}
16
+ Tabs share the saved database automatically; keep its name stable.{{/if}}
17
+
18
+ One atomic script creates the schema and inserts starter rows. Only
19
+ `TABLE_ALREADY_EXISTS` is ignored, so concurrent first visits cannot seed
20
+ twice and an existing empty table stays empty. Preserve this behavior.
21
+
22
+ Await writes, preserve drafts on failure, and keep errors visible. Retry at
23
+ startup reloads the page to create a fresh client. Do not automatically replay
24
+ a failed write: a storage failure can leave its commit outcome uncertain.
25
+ If the browser restores a cached page after teardown, the `pageshow` handler
26
+ reloads it so the app creates a fresh client instead of reusing a closed one.
27
+
28
+ ## Key Files
29
+
30
+ - `README.md` explains how to run the generated project.
31
+ - `src/database.{{ext}}` opens the database and defines the `todos` table.
32
+ - `src/app.{{ext}}` contains the main application UI.
33
+ - `src/todoList.{{ext}}` reads the `todos` table and subscribes to its changes.
34
+ - `vite.config.{{ext}}` enables offline production builds with `tinyjoinOffline()`.
35
+
36
+ The production app caches its complete build after an online visit. Keep the
37
+ offline plugin configured when changing build output. It waits for open tabs
38
+ to close before activating an update; do not force updates into running tabs.
39
+ For an existing service worker, use the plugin's manifest mode and integrate
40
+ its assets there instead of adding a competing registration. Offline caching
41
+ does not migrate a saved schema or synchronize data to a server.
42
+
43
+ ## Working Method
44
+
45
+ 1. Read `README.md` and `src/database.{{ext}}` before making changes.
46
+ 2. Keep application data in TinyJoin rather than duplicating it in UI state.
47
+ 3. Use `$1` parameter arrays for application
48
+ values. Do not concatenate user input into SQL.
49
+ 4. Generate stable row identifiers in application code.
50
+ 5. Keep the schema safe to run again when the app reloads.
51
+ 6. Keep the database client open while the app is running and close it during
52
+ page teardown.
53
+ 7. Run the build after edits.
54
+
55
+ ## Verification
56
+
57
+ Run:
58
+
59
+ ```sh
60
+ npm run build
61
+ ```
62
+
63
+ Change data in the running app, reload the page, and confirm the change
64
+ {{#if usesOpfs}}remains{{else}}resets, as this starter was generated without persistence{{/if}}.
65
+ For offline verification, use `npm run preview`, wait for the browser's offline
66
+ copy to finish installing, close the page, disconnect the network, and reopen
67
+ the same URL. Verify loading, edits, and reloads; the development server does
68
+ not install offline support.
69
+
70
+ Current TinyJoin documentation: https://tinyjoin.org/llms.txt
@@ -1,29 +1,90 @@
1
1
  # {{projectName}}
2
2
 
3
- A small local todo app built with TinyJoin, TypeScript, and Vite.
3
+ A TinyJoin todo app built with {{#if typescript}}TypeScript{{else}}JavaScript{{/if}} and Vanilla JS.
4
4
 
5
- ## Start it
5
+ ## Getting Started
6
6
 
7
- ```sh
7
+ To start the web client with Vite, run the following commands:
8
+
9
+ ```bash
8
10
  npm install
9
11
  npm run dev
10
12
  ```
11
13
 
12
- Your app will open at the address Vite prints in the terminal. It needs no
13
- database server, account, or credentials.
14
+ PNPM, Yarn, and Bun should also work. Your app will be available at
15
+ `http://localhost:5173`, or whichever port Vite specifies in the console. It
16
+ needs no database server, account, or credentials.
17
+
18
+ After that, start hacking!
19
+
20
+ ## Structure
21
+
22
+ This project is a single web application, built with Vite and
23
+ {{#if typescript}}TypeScript{{else}}JavaScript{{/if}}.
24
+
25
+ ### Key Files
26
+
27
+ - **`index.html`**: The main HTML file that loads your app
28
+ - **`vite.config.{{ext}}`**: Production build and offline support
29
+ - **`src/index.{{ext}}`**: Entry point that bootstraps the app
30
+ - **`src/app.{{ext}}`**: Main application logic
31
+ - **`src/database.{{ext}}`**: TinyJoin database and `todos` table setup
32
+ - **`src/todoInput.{{ext}}`**, **`src/todoList.{{ext}}`**,
33
+ **`src/todoItem.{{ext}}`**: The todo interface
34
+ - **`src/topBar.{{ext}}`**, **`src/button.{{ext}}`**, **`src/input.{{ext}}`**,
35
+ **`src/loading.{{ext}}`**: Shared interface pieces
36
+
37
+ #### How It Works
38
+
39
+ The app uses **TinyJoin** to keep application state in a small relational
40
+ database that runs entirely in the browser. The interface reads rows with
41
+ ordinary SQL queries, writes them with parameterized statements, and subscribes
42
+ to the `todos` table so that every change re-renders the list.
14
43
 
15
44
  {{#if usesOpfs}}
16
- Todos are saved locally in this browser and remain after a reload.
45
+ **Persistence** is enabled. Your data is automatically saved and restored when
46
+ you reload the page. Tabs share the same saved database, and changes appear
47
+ in every open tab.
48
+
49
+ The browser can clear or evict saved data, so keep a separate copy of anything
50
+ irreplaceable. Deleting all todos leaves the list empty after a reload; the
51
+ example rows are added only when the table is first created.
17
52
  {{else}}
18
- Todos are kept for the current visit and start fresh after a reload.
53
+ **Persistence** is not enabled. Your todos are kept for the current visit and
54
+ start fresh after a reload.
19
55
  {{/if}}
20
56
 
21
- ## Start editing
57
+ Startup failures replace the loading spinner with a message and **Retry**.
58
+ Failed changes show an error; a failed addition keeps the text you entered.
59
+ Writes are not automatically retried.
60
+
61
+ ## Offline Use
62
+
63
+ Build and preview the app to try offline support:
64
+
65
+ ```sh
66
+ npm run build
67
+ npm run preview
68
+ ```
69
+
70
+ The production app downloads a local copy on its first online visit. Once
71
+ that download finishes, you can close it, reopen it without a connection,
72
+ and keep using your todos.{{#unless usesOpfs}} Your selected data mode still
73
+ starts with a fresh todo list each visit.{{/unless}} The development server
74
+ does not install an offline copy.
75
+
76
+ Deploy the contents of `dist/` to an HTTPS host. Offline loading is set up by
77
+ `tinyjoinOffline()` in `vite.config.{{ext}}`; no extra application code is needed.
78
+ New versions wait until all open tabs close before taking over, so a working
79
+ tab keeps its matching application files. A new deployment does not migrate
80
+ saved database schemas. Offline use does not synchronize data with a server
81
+ or another device.
22
82
 
23
- - `src/database.ts` opens the database and defines the `todos` table.
24
- - `src/main.ts` contains the todo UI and its queries.
25
- - `src/style.css` contains the app styles.
83
+ If you later add your own service worker, use the plugin's manifest mode to
84
+ integrate its cached files with that worker instead of registering two for
85
+ the same application. See the [offline guide](https://tinyjoin.org/guides/offline/).
26
86
 
27
- Run `npm run build` to check the TypeScript and create a production build.
87
+ ## Learn More
28
88
 
29
- Learn more at [tinyjoin.org](https://tinyjoin.org).
89
+ - [TinyJoin Documentation](https://tinyjoin.org)
90
+ - [TinyJoin Demos](https://tinyjoin.org/demos/)
@@ -1,51 +1,54 @@
1
1
  <!doctype html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <meta
7
- name="description"
8
- content="A small local todo app built with TinyJoin"
9
- />
10
- <title>{{projectName}}</title>
11
- </head>
12
- <body>
13
- <main>
14
- <header>
15
- <p class="eyebrow">TinyJoin starter</p>
16
- <h1>Todos</h1>
17
- <p class="intro">
18
- {{#if usesOpfs}}Your list is saved locally in this browser.{{else}}Your list starts fresh each time you load the app.{{/if}}
19
- </p>
20
- </header>
21
3
 
22
- <form data-testid="todo-form">
23
- <label for="todo-title">What needs doing?</label>
24
- <div class="composer">
25
- <input
26
- id="todo-title"
27
- data-testid="todo-title"
28
- name="title"
29
- type="text"
30
- maxlength="120"
31
- placeholder="Add a todo"
32
- autocomplete="off"
33
- required
34
- />
35
- <button type="submit">Add</button>
36
- </div>
37
- </form>
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
+ <title>TinyJoin Todos</title>
9
+ <style>
10
+ * {
11
+ box-sizing: border-box;
12
+ }
38
13
 
39
- <p class="status" data-testid="status" role="status" aria-live="polite">
40
- Opening your todos…
41
- </p>
42
- <p class="error" data-testid="error" role="alert" hidden></p>
43
- <ul data-testid="todos" aria-live="polite" aria-busy="true"></ul>
14
+ body {
15
+ user-select: none;
16
+ margin: 0;
17
+ padding: 0;
18
+ font-family: system-ui, sans-serif;
19
+ }
20
+
21
+ #root {
22
+ display: flex;
23
+ flex-direction: column;
24
+ min-height: 100vh;
25
+ }
26
+
27
+ #app {
28
+ max-width: 60rem;
29
+ width: 100%;
30
+ margin: 0 auto;
31
+ padding: 2rem 1.5rem;
32
+ }
33
+
34
+ body {
35
+ --hue: 270;
36
+ --fg: oklch(85% 0.01 var(--hue));
37
+ --fg2: oklch(60% 0.01 var(--hue));
38
+ --bg: oklch(20% 0.01 var(--hue));
39
+ --bg2: oklch(25% 0.01 var(--hue));
40
+ --border: oklch(30% 0.01 var(--hue));
41
+ --bg-header: oklch(30% 0.008 var(--hue) / 0.5);
42
+ --accent: #7c3aed;
43
+ color: var(--fg);
44
+ background: var(--bg);
45
+ }
46
+ </style>
47
+ </head>
48
+
49
+ <body>
50
+ <div id="root"></div>
51
+ <script type="module" src="/src/index.{{ext}}"></script>
52
+ </body>
44
53
 
45
- <footer>
46
- Built with <a href="https://tinyjoin.org">TinyJoin</a>.
47
- </footer>
48
- </main>
49
- <script type="module" src="/src/main.ts"></script>
50
- </body>
51
54
  </html>
@@ -8,14 +8,16 @@
8
8
  },
9
9
  "scripts": {
10
10
  "dev": "vite",
11
- "build": "tsc --noEmit && vite build",
11
+ "build": "{{#if typescript}}tsc --noEmit && {{/if}}vite build",
12
12
  "preview": "vite preview"
13
13
  },
14
14
  "dependencies": {
15
15
  "tinyjoin": "{{tinyjoinDependency}}"
16
16
  },
17
17
  "devDependencies": {
18
+ {{#if typescript}}
18
19
  "typescript": "^7.0.2",
19
- "vite": "^8.2.1"
20
+ {{/if}}
21
+ "vite": "^8.3.0"
20
22
  }
21
23
  }
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-100 -100 1200 1200" stroke-linejoin="round" stroke="#fff" stroke-width="50">
2
+ <path d="M0 150a500 150 0 00223 125 100 100 0 0179 84l48 291v250a100 100 0 00100 100h100a100 100 0 00100-100V650l48-291a100 100 0 0179-84 500 150 0 00223-125A500 150 0 000 150z" fill="#7C3AED" />
3
+ <path
4
+ d="M0 150a500 150 0 00223 125 100 100 0 0179 84 700 700 0 01-12.5 160A500 150 0 010 383zm0 233a500 150 0 00289.5 136A700 700 0 01206 738.5 500 150 0 010 617zm0 234a500 150 0 00206 121.5 700 700 0 01-81 81.5 500 500 0 00225-90v170a100 100 0 00100 100A500 150 0 010 850zm1000-467a500 150 0 01-223 125 100 100 0 00-79 84 700 700 0 0012.5 160A500 150 0 001000 383zm0 233a500 150 0 01-289.5 136A700 700 0 00794 738.5 500 150 0 001000 617zm0 234a500 150 0 01-206 121.5 700 700 0 0081 81.5 500 500 0 01-225-90v170a100 100 0 01-100 100 500 150 0 00450-150z" />
5
+ <path d="M350 730a500 500 0 01-225 90 700 700 0 00177-461l48 291zm300 0a500 500 0 00225 90 700 700 0 01-177-461l-48 291z" fill="#fff" />
6
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 630 630">
2
+ <rect width="630" height="630" fill="#f7df1e" />
3
+ <path
4
+ d="m423.2 492.19c12.69 20.72 29.2 35.95 58.4 35.95 24.53 0 40.2-12.26 40.2-29.2 0-20.3-16.1-27.49-43.1-39.3l-14.8-6.35c-42.72-18.2-71.1-41-71.1-89.2 0-44.4 33.83-78.2 86.7-78.2 37.64 0 64.7 13.1 84.2 47.4l-46.1 29.6c-10.15-18.2-21.1-25.37-38.1-25.37-17.34 0-28.33 11-28.33 25.37 0 17.76 11 24.95 36.4 35.95l14.8 6.34c50.3 21.57 78.7 43.56 78.7 93 0 53.3-41.87 82.5-98.1 82.5-54.98 0-90.5-26.2-107.88-60.54zm-209.13 5.13c9.3 16.5 17.76 30.45 38.1 30.45 19.45 0 31.72-7.61 31.72-37.2v-201.3h59.2v202.1c0 61.3-35.94 89.2-88.4 89.2-47.4 0-74.85-24.53-88.81-54.075z" />
5
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg fill="none" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg">
2
+ <rect fill="#3178c6" height="512" width="512" />
3
+ <path clip-rule="evenodd"
4
+ d="m316.939 407.424v50.061c8.138 4.172 17.763 7.3 28.875 9.386s22.823 3.129 35.135 3.129c11.999 0 23.397-1.147 34.196-3.442 10.799-2.294 20.268-6.075 28.406-11.342 8.138-5.266 14.581-12.15 19.328-20.65s7.121-19.007 7.121-31.522c0-9.074-1.356-17.026-4.069-23.857s-6.625-12.906-11.738-18.225c-5.112-5.319-11.242-10.091-18.389-14.315s-15.207-8.213-24.18-11.967c-6.573-2.712-12.468-5.345-17.685-7.9-5.217-2.556-9.651-5.163-13.303-7.822-3.652-2.66-6.469-5.476-8.451-8.448-1.982-2.973-2.974-6.336-2.974-10.091 0-3.441.887-6.544 2.661-9.308s4.278-5.136 7.512-7.118c3.235-1.981 7.199-3.52 11.894-4.615 4.696-1.095 9.912-1.642 15.651-1.642 4.173 0 8.581.313 13.224.938 4.643.626 9.312 1.591 14.008 2.894 4.695 1.304 9.259 2.947 13.694 4.928 4.434 1.982 8.529 4.276 12.285 6.884v-46.776c-7.616-2.92-15.937-5.084-24.962-6.492s-19.381-2.112-31.066-2.112c-11.895 0-23.163 1.278-33.805 3.833s-20.006 6.544-28.093 11.967c-8.086 5.424-14.476 12.333-19.171 20.729-4.695 8.395-7.043 18.433-7.043 30.114 0 14.914 4.304 27.638 12.912 38.172 8.607 10.533 21.675 19.45 39.204 26.751 6.886 2.816 13.303 5.579 19.25 8.291s11.086 5.528 15.415 8.448c4.33 2.92 7.747 6.101 10.252 9.543 2.504 3.441 3.756 7.352 3.756 11.733 0 3.233-.783 6.231-2.348 8.995s-3.939 5.162-7.121 7.196-7.147 3.624-11.894 4.771c-4.748 1.148-10.303 1.721-16.668 1.721-10.851 0-21.597-1.903-32.24-5.71-10.642-3.806-20.502-9.516-29.579-17.13zm-84.159-123.342h64.22v-41.082h-179v41.082h63.906v182.918h50.874z"
5
+ fill="#fff" fill-rule="evenodd" />
6
+ </svg>
@@ -0,0 +1,37 @@
1
+ import {databaseReady} from './database';
2
+ import {createTopBar} from './topBar';
3
+ import {showLoading, hideLoading} from './loading';
4
+ import {createTodoInput} from './todoInput';
5
+ import {createTodoList} from './todoList';
6
+ import {createButton} from './button';
7
+ import {createError, showError} from './error';
8
+
9
+ export const app = async (root: HTMLElement) => {
10
+ const appElement = document.createElement('div');
11
+ appElement.id = 'app';
12
+ root.replaceChildren(createTopBar(), appElement);
13
+
14
+ // Show loading spinner
15
+ const loadingDiv = showLoading(appElement);
16
+
17
+ try {
18
+ const database = await databaseReady;
19
+ const todoInputContainer = createTodoInput(database);
20
+ appElement.appendChild(todoInputContainer);
21
+
22
+ const input = todoInputContainer.querySelector('input')!;
23
+ input.focus();
24
+
25
+ appElement.appendChild(createTodoList(database));
26
+ } catch (error) {
27
+ const message = createError();
28
+ message.id = 'startupError';
29
+ showError(message, error, 'Could not open your todos.');
30
+ appElement.append(
31
+ message,
32
+ createButton('Retry', () => location.reload(), 'primary'),
33
+ );
34
+ } finally {
35
+ hideLoading(loadingDiv);
36
+ }
37
+ };
@@ -0,0 +1,30 @@
1
+ button {
2
+ padding: 0.5rem 1rem;
3
+ border: 1px solid #666;
4
+ border-radius: 0.25rem;
5
+ background: #222;
6
+ color: #fff;
7
+ font-family: inherit;
8
+ font-weight: 800;
9
+ cursor: pointer;
10
+ margin: 0.5rem;
11
+ }
12
+
13
+ button:hover {
14
+ border-color: #7c3aed;
15
+ }
16
+
17
+ button:disabled {
18
+ cursor: wait;
19
+ opacity: 0.6;
20
+ }
21
+
22
+ button.primary {
23
+ background: #7c3aed;
24
+ border: none;
25
+ padding: 0.75rem 1.5rem;
26
+ }
27
+
28
+ button.primary:hover {
29
+ background: #6d28d9;
30
+ }
@@ -0,0 +1,19 @@
1
+ import './button.css';
2
+
3
+ export const createButton = (
4
+ text: string,
5
+ onClick: (() => void) | null,
6
+ variant: 'default' | 'primary' = 'default',
7
+ type: 'button' | 'submit' = 'button',
8
+ ): HTMLButtonElement => {
9
+ const button = document.createElement('button');
10
+ button.textContent = text;
11
+ button.type = type;
12
+ if (variant === 'primary') {
13
+ button.className = 'primary';
14
+ }
15
+ if (onClick) {
16
+ button.addEventListener('click', onClick);
17
+ }
18
+ return button;
19
+ };