acrylblends 0.7.8 → 0.8.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 (141) hide show
  1. package/dist/index.d.ts +13 -5
  2. package/dist/index.js +72 -13
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/README.md +2 -2
  5. package/docs/framework/UPSTREAM.md +1 -1
  6. package/docs/framework/getting-started/basic.md +1 -2
  7. package/docs/framework/getting-started/desktop.md +1 -1
  8. package/docs/framework/guides/bring-your-own-tool.md +216 -0
  9. package/docs/framework/guides/create-acrylblends.md +5 -4
  10. package/docs/framework/guides/data.md +2 -1
  11. package/docs/framework/upstream.json +3 -3
  12. package/lib/node-server.js +1 -0
  13. package/package.json +1 -1
  14. package/plugins/acryl-db/plugin.json +6 -1
  15. package/plugins/acryl-db/src/conformance.ts +224 -0
  16. package/plugins/acryl-db/upstream.json +1 -1
  17. package/plugins/acryl-db-sqlite/plugin.json +4 -1
  18. package/plugins/acryl-db-sqlite/upstream.json +1 -1
  19. package/plugins/acryl-door/LICENSE +47 -0
  20. package/plugins/acryl-door/plugin.json +14 -0
  21. package/plugins/acryl-door/src/cli/acryl.ts +207 -0
  22. package/plugins/acryl-door/src/host/api.ts +9 -0
  23. package/plugins/acryl-door/src/host/audit.ts +64 -0
  24. package/plugins/acryl-door/src/host/channel.ts +142 -0
  25. package/plugins/acryl-door/src/host/chat-tools.ts +39 -0
  26. package/plugins/acryl-door/src/host/door-file.ts +94 -0
  27. package/plugins/acryl-door/src/host/door.ts +195 -0
  28. package/plugins/acryl-door/src/host/http.ts +80 -0
  29. package/plugins/acryl-door/src/host/mcp-routes.ts +83 -0
  30. package/plugins/acryl-door/src/host/operator-routes.ts +38 -0
  31. package/plugins/acryl-door/src/host/page-routes.ts +65 -0
  32. package/plugins/acryl-door/src/host/shim.ts +65 -0
  33. package/plugins/acryl-door/src/host/verbs.ts +109 -0
  34. package/plugins/acryl-door/src/index.ts +133 -0
  35. package/plugins/acryl-door/src/page/api.ts +9 -0
  36. package/plugins/acryl-door/src/page/channel.ts +135 -0
  37. package/plugins/acryl-door/src/page/driver/accessibility.ts +163 -0
  38. package/plugins/acryl-door/src/page/driver/actions.ts +186 -0
  39. package/plugins/acryl-door/src/page/driver/driver.ts +153 -0
  40. package/plugins/acryl-door/src/page/driver/ref-table.ts +78 -0
  41. package/plugins/acryl-door/src/page/driver/sensitivity.ts +49 -0
  42. package/plugins/acryl-door/src/page/driver/snapshot.ts +132 -0
  43. package/plugins/acryl-door/src/page/index.ts +40 -0
  44. package/plugins/acryl-door/src/page/indicator.ts +72 -0
  45. package/plugins/acryl-door/src/page/main.ts +7 -0
  46. package/plugins/acryl-door/src/page/protocol.ts +213 -0
  47. package/plugins/acryl-door/src/page/snapshot-text.ts +18 -0
  48. package/plugins/acryl-door/src/page/user-input.ts +24 -0
  49. package/plugins/acryl-door/upstream.json +28 -0
  50. package/plugins/acryl-inspect/upstream.json +1 -1
  51. package/plugins/acryl-kv/plugin.json +6 -1
  52. package/plugins/acryl-kv/src/conformance.ts +226 -0
  53. package/plugins/acryl-kv/upstream.json +1 -1
  54. package/plugins/acryl-kv-db/plugin.json +4 -1
  55. package/plugins/acryl-kv-db/upstream.json +1 -1
  56. package/plugins/acryl-kv-json/plugin.json +4 -1
  57. package/plugins/acryl-kv-json/upstream.json +1 -1
  58. package/plugins/acryl-node-server/src/adapter/listener.ts +5 -0
  59. package/plugins/acryl-node-server/upstream.json +2 -2
  60. package/plugins/acryl-router/src/service.ts +3 -0
  61. package/plugins/acryl-router/upstream.json +2 -2
  62. package/plugins/acryl-router-http/upstream.json +1 -1
  63. package/plugins/acryl-state/upstream.json +1 -1
  64. package/plugins/agent-panel/client/main.ts +159 -0
  65. package/plugins/agent-panel/index.ts +232 -0
  66. package/plugins/agent-panel/plugin.json +21 -0
  67. package/plugins/desktop/upstream.json +1 -1
  68. package/plugins/desktop-web/upstream.json +1 -1
  69. package/plugins/extensions/resolve.ts +16 -2
  70. package/plugins/tsconfig.json +6 -0
  71. package/shared/DEFAULTS.md +11 -6
  72. package/shared/SKILL.md +1 -1
  73. package/templates/agent/blend.yaml +3 -0
  74. package/templates/agent/src/app.ts +18 -2
  75. package/templates/agent/src/routes.ts +14 -1
  76. package/templates/agent/template.json +2 -1
  77. package/templates/agent/test/agent.test.ts +10 -4
  78. package/templates/agent/test/app.test.ts +29 -8
  79. package/templates/agent/test/builder.test.ts +34 -23
  80. package/templates/agent/test/model.test.ts +24 -19
  81. package/templates/blank/src/app.ts +25 -5
  82. package/templates/blank/test/app.test.ts +28 -9
  83. package/templates/chat/AGENTS.md +16 -0
  84. package/templates/chat/blend.yaml +29 -0
  85. package/templates/chat/gitignore +6 -0
  86. package/templates/chat/migrations/001-chat.sql +12 -0
  87. package/templates/chat/package.json +33 -0
  88. package/templates/chat/src/app.ts +106 -0
  89. package/templates/chat/src/chat-app.ts +101 -0
  90. package/templates/chat/src/client/chat.tsx +115 -0
  91. package/templates/chat/src/client/tsconfig.json +26 -0
  92. package/templates/chat/src/db.ts +43 -0
  93. package/templates/chat/src/door.ts +7 -0
  94. package/templates/chat/src/llm.ts +42 -0
  95. package/templates/chat/src/main.ts +28 -0
  96. package/templates/chat/src/page.tsx +40 -0
  97. package/templates/chat/src/routes.ts +15 -0
  98. package/templates/chat/src/runtime.ts +17 -0
  99. package/templates/chat/template.json +11 -0
  100. package/templates/chat/test/chat.test.ts +198 -0
  101. package/templates/chat/test/server.test.ts +78 -0
  102. package/templates/chat/tsconfig.json +20 -0
  103. package/templates/nodejs/AGENTS.md +9 -0
  104. package/templates/nodejs/blend.yaml +26 -0
  105. package/templates/nodejs/gitignore +3 -0
  106. package/templates/nodejs/package.json +30 -0
  107. package/templates/nodejs/src/app.ts +92 -0
  108. package/templates/nodejs/src/client/counter.tsx +12 -0
  109. package/templates/nodejs/src/client/tsconfig.json +26 -0
  110. package/templates/nodejs/src/door.ts +7 -0
  111. package/templates/nodejs/src/main.ts +25 -0
  112. package/templates/nodejs/src/page.tsx +29 -0
  113. package/templates/nodejs/src/routes.ts +40 -0
  114. package/templates/nodejs/template.json +3 -0
  115. package/templates/nodejs/test/app.test.ts +78 -0
  116. package/templates/nodejs/test/server.test.ts +83 -0
  117. package/templates/nodejs/tsconfig.json +20 -0
  118. package/templates/notes/AGENTS.md +26 -0
  119. package/templates/notes/blend.yaml +29 -0
  120. package/templates/notes/gitignore +6 -0
  121. package/templates/notes/migrations/001-notes.sql +18 -0
  122. package/templates/notes/package.json +33 -0
  123. package/templates/notes/src/app.ts +106 -0
  124. package/templates/notes/src/auth.ts +57 -0
  125. package/templates/notes/src/client/counter.tsx +21 -0
  126. package/templates/notes/src/client/tsconfig.json +26 -0
  127. package/templates/notes/src/db.ts +50 -0
  128. package/templates/notes/src/door.ts +7 -0
  129. package/templates/notes/src/main.ts +28 -0
  130. package/templates/notes/src/notes-app.ts +205 -0
  131. package/templates/notes/src/routes.ts +14 -0
  132. package/templates/notes/src/runtime.ts +17 -0
  133. package/templates/notes/src/schema.ts +38 -0
  134. package/templates/notes/src/views.tsx +136 -0
  135. package/templates/notes/template.json +11 -0
  136. package/templates/notes/test/notes.test.ts +280 -0
  137. package/templates/notes/test/server.test.ts +78 -0
  138. package/templates/notes/tsconfig.json +20 -0
  139. package/templates/todo/src/app.ts +27 -7
  140. package/templates/todo/test/todo.test.ts +47 -22
  141. package/templates/todo/upstream.json +1 -1
@@ -0,0 +1,205 @@
1
+ import type { Db } from '../plugins/acryl-db/src/index.ts'
2
+ import {
3
+ createMiddleware,
4
+ deleteCookie,
5
+ getCookie,
6
+ schemaValidator,
7
+ setCookie,
8
+ } from '../plugins/acryl-router/src/helpers/index.ts'
9
+ import type { Context } from '../plugins/acryl-router/src/index.ts'
10
+ import { Acryl, HTTPException } from '../plugins/acryl-router/src/index.ts'
11
+ import { csrf } from '../plugins/acryl-router/src/middleware/index.ts'
12
+ import { authStore, verifyPassword } from './auth.ts'
13
+ import { notesStore } from './db.ts'
14
+ import { checkCredentials, checkNote, NoteSchema } from './schema.ts'
15
+ import { authPage, formPage, listPage } from './views.tsx'
16
+
17
+ type Env = { Variables: { user: { id: number; email: string } } }
18
+
19
+ const COOKIE = 'sid'
20
+ const MONTH = 60 * 60 * 24 * 30
21
+
22
+ export interface AppOptions {
23
+ /** Believe `X-Forwarded-Proto` (set it when a reverse proxy you run terminates HTTPS in front of the app). Default: the TRUST_PROXY=1 environment variable. */
24
+ trustProxy?: boolean
25
+ /** The address people use in the browser, for example `https://notes.example.com`. Behind a proxy the app sees `http://127.0.0.1:3000`, so csrf needs it. Default: PUBLIC_ORIGIN. */
26
+ publicOrigin?: string
27
+ }
28
+
29
+ export function createApi(db: Db) {
30
+ const auth = authStore(db)
31
+ const notes = notesStore(db)
32
+ return new Acryl<Env>()
33
+ .use(async (c, next) => {
34
+ const user = await auth.userFor(getCookie(c, COOKIE))
35
+ if (!user) return c.json({ error: 'Log in first.' }, 401)
36
+ c.set('user', user)
37
+ await next()
38
+ })
39
+ .get('/', async (c) =>
40
+ c.json({ notes: await notes.list(c.get('user').id, c.req.query('q') ?? '') }, 200),
41
+ )
42
+ .post('/', schemaValidator('json', NoteSchema), async (c) =>
43
+ c.json(
44
+ {
45
+ note: await notes.create(
46
+ c.get('user').id,
47
+ c.req.valid('json').title,
48
+ c.req.valid('json').body,
49
+ ),
50
+ },
51
+ 201,
52
+ ),
53
+ )
54
+ .get('/:id', async (c) => {
55
+ const note = await notes.get(c.get('user').id, Number(c.req.param('id')))
56
+ return note ? c.json({ note }, 200) : c.json({ error: 'No such note.' }, 404)
57
+ })
58
+ .put('/:id', schemaValidator('json', NoteSchema), async (c) => {
59
+ const { title, body } = c.req.valid('json')
60
+ const note = await notes.update(c.get('user').id, Number(c.req.param('id')), title, body)
61
+ return note ? c.json({ note }, 200) : c.json({ error: 'No such note.' }, 404)
62
+ })
63
+ .delete('/:id', async (c) =>
64
+ (await notes.remove(c.get('user').id, Number(c.req.param('id'))))
65
+ ? c.json({ ok: true }, 200)
66
+ : c.json({ error: 'No such note.' }, 404),
67
+ )
68
+ }
69
+
70
+ export type Api = ReturnType<typeof createApi>
71
+
72
+ export function createApp(
73
+ db: Db,
74
+ {
75
+ trustProxy = process.env.TRUST_PROXY === '1',
76
+ publicOrigin = process.env.PUBLIC_ORIGIN,
77
+ }: AppOptions = {},
78
+ ) {
79
+ const auth = authStore(db)
80
+ const notes = notesStore(db)
81
+ const app = new Acryl<Env>()
82
+ // Refuses a form POST that does not come from this site (checks Origin and Sec-Fetch-Site); JSON requests are not form-shaped and pass.
83
+ app.use(csrf(publicOrigin ? { origin: [publicOrigin] } : undefined))
84
+ // A secure cookie is only sent over HTTPS: set when the request is HTTPS, or a proxy we trust says it was.
85
+ const session = (c: Context<Env>) => ({
86
+ path: '/',
87
+ httpOnly: true,
88
+ sameSite: 'Lax' as const,
89
+ maxAge: MONTH,
90
+ secure:
91
+ new URL(c.req.url).protocol === 'https:' ||
92
+ (trustProxy && c.req.header('x-forwarded-proto') === 'https'),
93
+ })
94
+ // The log has the stack; the page never does.
95
+ app.onError((error, c) => {
96
+ if (error instanceof HTTPException) return error.getResponse()
97
+ console.error(error)
98
+ return c.text('Internal Server Error', 500)
99
+ })
100
+
101
+ const requireUser = createMiddleware<Env>(async (c, next) => {
102
+ const user = await auth.userFor(getCookie(c, COOKIE))
103
+ if (!user) return c.redirect('/login')
104
+ c.set('user', user)
105
+ await next()
106
+ })
107
+
108
+ app.get('/', (c) => c.redirect('/notes'))
109
+ app.get('/signup', (c) => c.html(authPage('signup')))
110
+ app.get('/login', (c) => c.html(authPage('login')))
111
+
112
+ app.post('/signup', async (c) => {
113
+ const form = await c.req.parseBody()
114
+ const email = String(form.email ?? '')
115
+ .trim()
116
+ .toLowerCase()
117
+ const errors = checkCredentials(email, form.password, true)
118
+ if (errors.length === 0 && (await auth.findUser(email)))
119
+ errors.push('That email already has an account. Log in instead.')
120
+ if (errors.length > 0) return c.html(authPage('signup', errors, email), 400)
121
+ const id = await auth.createUser(email, String(form.password))
122
+ setCookie(c, COOKIE, await auth.startSession(id), session(c))
123
+ return c.redirect('/notes', 303)
124
+ })
125
+
126
+ app.post('/login', async (c) => {
127
+ const form = await c.req.parseBody()
128
+ const email = String(form.email ?? '')
129
+ .trim()
130
+ .toLowerCase()
131
+ const errors = checkCredentials(email, form.password, false)
132
+ if (errors.length > 0) return c.html(authPage('login', errors, email), 400)
133
+ const user = await auth.findUser(email)
134
+ if (!user || !(await verifyPassword(String(form.password), user.password_hash)))
135
+ return c.html(authPage('login', ['Wrong email or password.'], email), 401)
136
+ setCookie(c, COOKIE, await auth.startSession(user.id), session(c))
137
+ return c.redirect('/notes', 303)
138
+ })
139
+
140
+ app.post('/logout', async (c) => {
141
+ const token = getCookie(c, COOKIE)
142
+ if (token) await auth.endSession(token)
143
+ deleteCookie(c, COOKIE, { path: '/' })
144
+ return c.redirect('/login', 303)
145
+ })
146
+
147
+ app.use('/notes/*', requireUser)
148
+ app.use('/notes', requireUser)
149
+
150
+ app.get('/notes', async (c) => {
151
+ const q = (c.req.query('q') ?? '').trim()
152
+ return c.html(listPage(c.get('user').email, await notes.list(c.get('user').id, q), q))
153
+ })
154
+ app.get('/notes/new', (c) =>
155
+ c.html(formPage(c.get('user').email, '/notes', { title: '', body: '' })),
156
+ )
157
+ app.post('/notes', async (c) => {
158
+ const form = await c.req.parseBody()
159
+ const checked = checkNote(form)
160
+ if ('issues' in checked)
161
+ return c.html(
162
+ formPage(
163
+ c.get('user').email,
164
+ '/notes',
165
+ { title: String(form.title ?? ''), body: String(form.body ?? '') },
166
+ checked.issues.map((i) => i.message),
167
+ ),
168
+ 400,
169
+ )
170
+ await notes.create(c.get('user').id, checked.value.title, checked.value.body)
171
+ return c.redirect('/notes', 303)
172
+ })
173
+ app.get('/notes/:id/edit', async (c) => {
174
+ const note = await notes.get(c.get('user').id, Number(c.req.param('id')))
175
+ return note
176
+ ? c.html(formPage(c.get('user').email, `/notes/${note.id}`, note))
177
+ : c.html(listPage(c.get('user').email, [], ''), 404)
178
+ })
179
+ app.post('/notes/:id', async (c) => {
180
+ const id = Number(c.req.param('id'))
181
+ const form = await c.req.parseBody()
182
+ const checked = checkNote(form)
183
+ if ('issues' in checked)
184
+ return c.html(
185
+ formPage(
186
+ c.get('user').email,
187
+ `/notes/${id}`,
188
+ { title: String(form.title ?? ''), body: String(form.body ?? '') },
189
+ checked.issues.map((i) => i.message),
190
+ ),
191
+ 400,
192
+ )
193
+ if (!(await notes.update(c.get('user').id, id, checked.value.title, checked.value.body)))
194
+ return c.text('No such note.', 404)
195
+ return c.redirect('/notes', 303)
196
+ })
197
+ app.post('/notes/:id/delete', async (c) => {
198
+ if (!(await notes.remove(c.get('user').id, Number(c.req.param('id')))))
199
+ return c.text('No such note.', 404)
200
+ return c.redirect('/notes', 303)
201
+ })
202
+
203
+ app.route('/api/notes', createApi(db))
204
+ return app
205
+ }
@@ -0,0 +1,14 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import type {} from '../plugins/acryl-db/src/cordis.ts'
3
+ import { createApp } from './notes-app.ts'
4
+ import { createClient } from './runtime.ts'
5
+
6
+ /** The app's own plugin: it builds the app on the `db` port and mounts it on the router. The mount is removed when the plugin is disposed. */
7
+ export const routes = {
8
+ name: 'routes',
9
+ inject: ['router', 'db'],
10
+ apply(ctx: Context) {
11
+ ctx.router.route('/', createApp(ctx.db))
12
+ ctx.router.route('/client', createClient())
13
+ },
14
+ }
@@ -0,0 +1,17 @@
1
+ import { createRequire } from 'node:module'
2
+ import { dirname, join } from 'node:path'
3
+ import { clientModules } from '../plugins/acryl-node-server/src/client-modules.ts'
4
+
5
+ /** The browser builds of the DOM renderer and the typed client, shipped by the `acrylblends` package this app depends on (`clientModules` serves them). */
6
+ const runtimeDir = join(
7
+ dirname(createRequire(import.meta.url).resolve('acrylblends/package.json')),
8
+ 'lib',
9
+ 'browser',
10
+ )
11
+
12
+ /**
13
+ * Client components without a bundler: `src/client` is compiled when the browser asks; `npm run build` compiles it once for production
14
+ * (`NODE_ENV=production` then serves only `dist/client`). `src/routes.ts` mounts it at `/client` on the router, next to the app.
15
+ */
16
+ export const createClient = () =>
17
+ clientModules({ root: 'src/client', compiled: 'dist/client', runtimeDir })
@@ -0,0 +1,38 @@
1
+ import type { StandardSchema } from '../plugins/acryl-router/src/helpers/index.ts'
2
+
3
+ export interface NoteInput {
4
+ title: string
5
+ body: string
6
+ }
7
+
8
+ export const TITLE_MAX = 80
9
+ export const BODY_MAX = 2000
10
+
11
+ /** The one place a note's rules live: the HTML forms and the JSON API both use it. */
12
+ export function checkNote(
13
+ value: unknown,
14
+ ): { value: NoteInput } | { issues: { message: string; path: string[] }[] } {
15
+ const input = (value ?? {}) as Record<string, unknown>
16
+ const title = typeof input.title === 'string' ? input.title.trim() : ''
17
+ const body = typeof input.body === 'string' ? input.body : ''
18
+ const issues: { message: string; path: string[] }[] = []
19
+ if (title === '') issues.push({ message: 'Give the note a title.', path: ['title'] })
20
+ else if (title.length > TITLE_MAX)
21
+ issues.push({ message: `The title is at most ${TITLE_MAX} characters.`, path: ['title'] })
22
+ if (body.length > BODY_MAX)
23
+ issues.push({ message: `The note is at most ${BODY_MAX} characters.`, path: ['body'] })
24
+ return issues.length > 0 ? { issues } : { value: { title, body } }
25
+ }
26
+
27
+ export const NoteSchema: StandardSchema<NoteInput> = {
28
+ '~standard': { validate: (value) => checkNote(value) },
29
+ }
30
+
31
+ export function checkCredentials(email: unknown, password: unknown, signup: boolean): string[] {
32
+ const errors: string[] = []
33
+ if (typeof email !== 'string' || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email))
34
+ errors.push('Enter a valid email address.')
35
+ if (typeof password !== 'string' || password === '') errors.push('Enter a password.')
36
+ else if (signup && password.length < 8) errors.push('The password needs at least 8 characters.')
37
+ return errors
38
+ }
@@ -0,0 +1,136 @@
1
+ import type { Note } from './db.ts'
2
+ import { doorEnabled } from './door.ts'
3
+ import { BODY_MAX, TITLE_MAX } from './schema.ts'
4
+
5
+ const counterScript = `
6
+ const filter = document.getElementById('filter')
7
+ if (filter) filter.addEventListener('input', () => {
8
+ const q = filter.value.toLowerCase()
9
+ for (const li of document.querySelectorAll('[data-note]'))
10
+ li.hidden = !li.textContent.toLowerCase().includes(q)
11
+ })
12
+ `
13
+
14
+ const Layout = ({
15
+ title,
16
+ user,
17
+ children,
18
+ }: {
19
+ title: string
20
+ user?: string
21
+ children?: unknown
22
+ }) => (
23
+ <html lang="en">
24
+ <head>
25
+ <meta charset="utf-8" />
26
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
27
+ <title>{title}</title>
28
+ <style
29
+ dangerouslySetInnerHTML={{
30
+ __html:
31
+ 'body{font:16px system-ui;max-width:42rem;margin:2rem auto;padding:0 1rem}label{display:block;margin:.75rem 0 .25rem}input,textarea{width:100%;box-sizing:border-box;padding:.5rem}.error{color:#b00020}nav{display:flex;gap:1rem;align-items:center}nav form{margin-left:auto}li{margin:.5rem 0}',
32
+ }}
33
+ />
34
+ </head>
35
+ <body>
36
+ <nav>
37
+ <strong>Notes</strong>
38
+ {user ? (
39
+ <>
40
+ <a href="/notes">All notes</a>
41
+ <a href="/notes/new">New note</a>
42
+ <form method="post" action="/logout">
43
+ <span>{user} </span>
44
+ <button type="submit">Log out</button>
45
+ </form>
46
+ </>
47
+ ) : (
48
+ <>
49
+ <a href="/login">Log in</a>
50
+ <a href="/signup">Sign up</a>
51
+ </>
52
+ )}
53
+ </nav>
54
+ {children}
55
+ <script dangerouslySetInnerHTML={{ __html: counterScript }} />
56
+ <script type="module" src="/client/counter.tsx" />
57
+ {doorEnabled() ? <script type="module" src="/api/acryl-door/page.js" /> : null}
58
+ </body>
59
+ </html>
60
+ )
61
+
62
+ const Errors = ({ errors }: { errors: string[] }) =>
63
+ errors.length > 0 ? (
64
+ <ul class="error" role="alert">
65
+ {errors.map((e) => (
66
+ <li>{e}</li>
67
+ ))}
68
+ </ul>
69
+ ) : null
70
+
71
+ export const authPage = (mode: 'login' | 'signup', errors: string[] = [], email = ''): string =>
72
+ (
73
+ <Layout title={mode === 'login' ? 'Log in' : 'Sign up'}>
74
+ <h1>{mode === 'login' ? 'Log in' : 'Sign up'}</h1>
75
+ <Errors errors={errors} />
76
+ <form method="post" action={`/${mode}`}>
77
+ <label for="email">Email</label>
78
+ <input id="email" name="email" type="email" value={email} />
79
+ <label for="password">Password</label>
80
+ <input id="password" name="password" type="password" />
81
+ <p>
82
+ <button type="submit">{mode === 'login' ? 'Log in' : 'Create account'}</button>
83
+ </p>
84
+ </form>
85
+ </Layout>
86
+ ).toString()
87
+
88
+ export const listPage = (user: string, notes: Note[], q: string): string =>
89
+ (
90
+ <Layout title="Notes" user={user}>
91
+ <h1>Your notes</h1>
92
+ <form method="get" action="/notes">
93
+ <label for="filter">Search</label>
94
+ <input id="filter" name="q" type="search" value={q} />
95
+ <button type="submit">Search</button>
96
+ </form>
97
+ {notes.length === 0 ? <p>{q ? `No note matches "${q}".` : 'No notes yet.'}</p> : null}
98
+ <ul>
99
+ {notes.map((n) => (
100
+ <li data-note>
101
+ <a href={`/notes/${n.id}/edit`}>{n.title}</a>
102
+ <form method="post" action={`/notes/${n.id}/delete`} style="display:inline">
103
+ <button type="submit">Delete</button>
104
+ </form>
105
+ </li>
106
+ ))}
107
+ </ul>
108
+ </Layout>
109
+ ).toString()
110
+
111
+ export const formPage = (
112
+ user: string,
113
+ action: string,
114
+ note: { title: string; body: string },
115
+ errors: string[] = [],
116
+ ): string =>
117
+ (
118
+ <Layout title="Note" user={user}>
119
+ <h1>{action === '/notes' ? 'New note' : 'Edit note'}</h1>
120
+ <Errors errors={errors} />
121
+ <form method="post" action={action}>
122
+ <label for="title">Title</label>
123
+ <input id="title" name="title" value={note.title} />
124
+ <label for="body">Note</label>
125
+ <textarea id="body" name="body" rows={8} maxlength={BODY_MAX}>
126
+ {note.body}
127
+ </textarea>
128
+ <small id="count" aria-live="polite" data-island="counter" />
129
+ <p>
130
+ <button type="submit">Save</button> <a href="/notes">Cancel</a>
131
+ </p>
132
+ </form>
133
+ </Layout>
134
+ ).toString()
135
+
136
+ export { TITLE_MAX }
@@ -0,0 +1,11 @@
1
+ {
2
+ "plugins": [
3
+ "instance",
4
+ "acryl-router",
5
+ "acryl-node-server",
6
+ "acryl-db-sqlite",
7
+ "manifest",
8
+ "save",
9
+ "acryl-door"
10
+ ]
11
+ }