workers-mini-admin 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/auth.d.ts +23 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +66 -0
- package/dist/auth.js.map +1 -0
- package/dist/css.d.ts +3 -0
- package/dist/css.d.ts.map +1 -0
- package/dist/css.js +3 -0
- package/dist/css.js.map +1 -0
- package/dist/escape.d.ts +5 -0
- package/dist/escape.d.ts.map +1 -0
- package/dist/escape.js +11 -0
- package/dist/escape.js.map +1 -0
- package/dist/form.d.ts +74 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +184 -0
- package/dist/form.js.map +1 -0
- package/dist/handler.d.ts +80 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +177 -0
- package/dist/handler.js.map +1 -0
- package/dist/html.d.ts +47 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +116 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/path.d.ts +28 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +82 -0
- package/dist/path.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jaredcat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# `workers-mini-admin`
|
|
2
|
+
|
|
3
|
+
Tiny single-user admin for Cloudflare Workers: password cookie / Bearer auth, HTML chrome, declarative forms, and a configurable mount path.
|
|
4
|
+
|
|
5
|
+
Declare fields (or pass your own HTML). Domain save/load stays in your Worker.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add workers-mini-admin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Local preview
|
|
14
|
+
|
|
15
|
+
From a clone of this repo:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cp playground/.dev.vars.example playground/.dev.vars
|
|
19
|
+
pnpm dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Open the URL Wrangler prints, then `/admin`. Login password is `ADMIN_SECRET` from `playground/.dev.vars` (default `dev`). See [playground/README.md](playground/README.md).
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createAdmin, parseAdminForm, type Field } from 'workers-mini-admin';
|
|
28
|
+
|
|
29
|
+
type Env = { ADMIN_SECRET?: string; SETTINGS: KVNamespace };
|
|
30
|
+
|
|
31
|
+
const admin = createAdmin<Env>({
|
|
32
|
+
basePath: '/admin',
|
|
33
|
+
title: 'My Admin',
|
|
34
|
+
getSecret: (env) => env.ADMIN_SECRET,
|
|
35
|
+
async render({ env }) {
|
|
36
|
+
const note = (await env.SETTINGS.get('note')) ?? '';
|
|
37
|
+
const fields: Field[] = [
|
|
38
|
+
{
|
|
39
|
+
type: 'textarea',
|
|
40
|
+
name: 'note',
|
|
41
|
+
label: 'Note',
|
|
42
|
+
value: note,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
return {
|
|
46
|
+
hint: 'Signed in. Edit settings below.',
|
|
47
|
+
fields,
|
|
48
|
+
submitLabel: 'Save',
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
async save({ form, env }) {
|
|
52
|
+
const values = parseAdminForm(form, [
|
|
53
|
+
{ type: 'textarea', name: 'note', label: 'Note' },
|
|
54
|
+
]);
|
|
55
|
+
await env.SETTINGS.put('note', String(values.note ?? ''));
|
|
56
|
+
return { flash: 'Saved.' };
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export default {
|
|
61
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
62
|
+
const res = await admin.fetch(request, env);
|
|
63
|
+
if (res) return res;
|
|
64
|
+
return new Response('Not found', { status: 404 });
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Mount path / reverse proxy
|
|
70
|
+
|
|
71
|
+
`basePath` defaults to `"/admin"`. Use `""` or `"/"` when the admin is the site root (e.g. `admin.example.com`):
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
createAdmin({
|
|
75
|
+
basePath: '',
|
|
76
|
+
title: 'My Admin',
|
|
77
|
+
// login at /login, logout at /logout, form at /
|
|
78
|
+
getSecret: (env) => env.ADMIN_SECRET,
|
|
79
|
+
render: async () => ({ fields: [] }),
|
|
80
|
+
save: async () => ({ flash: 'Saved.' }),
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Routes, form `action`s, redirects, and the auth cookie `Path` are derived from `basePath`. The Worker must see the same pathname the browser uses (normal on Cloudflare Workers). Path-stripping reverse proxies are not supported.
|
|
85
|
+
|
|
86
|
+
### Bring your own HTML
|
|
87
|
+
|
|
88
|
+
Return `body` instead of `fields`, or mix in `{ type: "html", html: "…" }` among fields:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
async render() {
|
|
92
|
+
return {
|
|
93
|
+
body: `<form method="post" action="/admin">…</form>`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For multi-panel UIs (wide layout, own cards, extra CSS):
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
async render() {
|
|
102
|
+
return {
|
|
103
|
+
shellMaxWidth: '1200px',
|
|
104
|
+
wrapBody: false,
|
|
105
|
+
extraCss: `.layout { display: grid; … }`,
|
|
106
|
+
bodyAttrs: { 'data-deployed-origin': origin },
|
|
107
|
+
body: `…`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- `shellMaxWidth` — simple CSS length for `.page-shell` (e.g. `1200px`)
|
|
113
|
+
- `wrapBody: false` — skip the default `.panel-card` wrapper
|
|
114
|
+
- `extraCss` — appended after shared styles
|
|
115
|
+
- `bodyAttrs` — attributes on `<body>` (escaped)
|
|
116
|
+
- `flashTone` — `'ok' | 'warn' | 'error'` (auto: `Error:` → error, `failed` → warn)
|
|
117
|
+
|
|
118
|
+
### Extra routes
|
|
119
|
+
|
|
120
|
+
For preview/upload endpoints under the mount:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
createAdmin({
|
|
124
|
+
basePath: '/admin',
|
|
125
|
+
title: 'My Admin',
|
|
126
|
+
getSecret: (env) => env.ADMIN_SECRET,
|
|
127
|
+
render: async () => ({ fields: [] }),
|
|
128
|
+
save: async () => ({}),
|
|
129
|
+
routes: {
|
|
130
|
+
'POST /preview': async (ctx) => {
|
|
131
|
+
const unauth = await ctx.requireAuth();
|
|
132
|
+
if (unauth) return unauth;
|
|
133
|
+
return Response.json({ ok: true });
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Unknown paths under a non-root mount return `404`. Paths outside the mount return `undefined` so your Worker can handle them. On a root mount (`basePath: ""`), unknown paths also return `undefined`.
|
|
140
|
+
|
|
141
|
+
### Low-level helpers
|
|
142
|
+
|
|
143
|
+
You can still wire auth and chrome yourself (`isAuthenticated`, `loginResponse`, `adminPage`, `adminForm`, …). Omitting `basePath` on those helpers keeps the legacy `/admin` defaults.
|
|
144
|
+
|
|
145
|
+
### Options
|
|
146
|
+
|
|
147
|
+
- `adminDisabledResponse(message?)` — optional custom 503 body (default mentions `ADMIN_SECRET`)
|
|
148
|
+
- `cookieName` on auth helpers and `createAdmin` — defaults to `admin_auth`
|
|
149
|
+
- `cookiePath` on `loginResponse` / `logoutResponse` / `createAdmin` — defaults to `/`. Logout clears `/`, the configured path, and the mount path so leftover cookies cannot stick around.
|
|
150
|
+
|
|
151
|
+
## API
|
|
152
|
+
|
|
153
|
+
| Export | Role |
|
|
154
|
+
| ------------------------------------ | ---------------------------------------------------- |
|
|
155
|
+
| `createAdmin` | Auth router + form page with configurable `basePath` |
|
|
156
|
+
| `adminForm` / `Field` | Declarative form HTML |
|
|
157
|
+
| `formText` / `parseAdminForm` | FormData helpers |
|
|
158
|
+
| `escapeHtml` | Safe HTML text interpolation |
|
|
159
|
+
| `hexSha256` / `timingSafeEqual` | Cookie token helpers |
|
|
160
|
+
| `isAuthenticated` | Cookie or `Authorization: Bearer` check |
|
|
161
|
+
| `adminAuthCookie` | `Set-Cookie` value builder |
|
|
162
|
+
| `loginResponse` / `logoutResponse` | Login POST / logout POST handlers |
|
|
163
|
+
| `html` / `redirect` | Small response helpers |
|
|
164
|
+
| `adminDisabledResponse` | 503 when admin secret is unset |
|
|
165
|
+
| `loginPageHtml` / `adminPage` | HTML shells |
|
|
166
|
+
| `joinBasePath` / `normalizeBasePath` | Mount path helpers |
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare function hexSha256(secret: string): Promise<string>;
|
|
2
|
+
export declare function timingSafeEqual(a: string, b: string): boolean;
|
|
3
|
+
export declare const DEFAULT_COOKIE_NAME = "admin_auth";
|
|
4
|
+
export declare function adminAuthCookie(token: string, isSecure: boolean, maxAge: number, cookieName?: string, path?: string): string;
|
|
5
|
+
export declare function isAuthenticated(request: Request, secret: string, cookieName?: string): Promise<boolean>;
|
|
6
|
+
export declare function loginResponse(options: {
|
|
7
|
+
secret: string;
|
|
8
|
+
password: string;
|
|
9
|
+
secureCookie: boolean;
|
|
10
|
+
successRedirect?: string;
|
|
11
|
+
loginHtml: (error?: string) => string;
|
|
12
|
+
cookieName?: string;
|
|
13
|
+
cookiePath?: string;
|
|
14
|
+
basePath?: string;
|
|
15
|
+
}): Promise<Response>;
|
|
16
|
+
export declare function logoutResponse(options: {
|
|
17
|
+
secureCookie: boolean;
|
|
18
|
+
redirectTo?: string;
|
|
19
|
+
cookieName?: string;
|
|
20
|
+
cookiePath?: string;
|
|
21
|
+
basePath?: string;
|
|
22
|
+
}): Response;
|
|
23
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ/D;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAS7D;AAED,eAAO,MAAM,mBAAmB,eAAe,CAAC;AAEhD,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,OAAO,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,GAAE,MAA4B,EACxC,IAAI,GAAE,MAAY,GACjB,MAAM,CAGR;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,UAAU,GAAE,MAA4B,GACvC,OAAO,CAAC,OAAO,CAAC,CAkBlB;AAMD,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoBpB;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE;IACtC,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,QAAQ,CAoBX"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { html, redirect } from './html.js';
|
|
2
|
+
import { cookiePathForBase, joinBasePath, resolveBasePath } from './path.js';
|
|
3
|
+
export async function hexSha256(secret) {
|
|
4
|
+
const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(secret));
|
|
5
|
+
return [...new Uint8Array(buffer)]
|
|
6
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
7
|
+
.join('');
|
|
8
|
+
}
|
|
9
|
+
export function timingSafeEqual(a, b) {
|
|
10
|
+
if (a.length !== b.length) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
let out = 0;
|
|
14
|
+
for (let index = 0; index < a.length; index++) {
|
|
15
|
+
out |= (a.codePointAt(index) ?? 0) ^ (b.codePointAt(index) ?? 0);
|
|
16
|
+
}
|
|
17
|
+
return out === 0;
|
|
18
|
+
}
|
|
19
|
+
export const DEFAULT_COOKIE_NAME = 'admin_auth';
|
|
20
|
+
export function adminAuthCookie(token, isSecure, maxAge, cookieName = DEFAULT_COOKIE_NAME, path = '/') {
|
|
21
|
+
const secureFlag = isSecure ? 'Secure; ' : '';
|
|
22
|
+
return `${cookieName}=${token}; HttpOnly; ${secureFlag}SameSite=Lax; Max-Age=${maxAge}; Path=${path}`;
|
|
23
|
+
}
|
|
24
|
+
export async function isAuthenticated(request, secret, cookieName = DEFAULT_COOKIE_NAME) {
|
|
25
|
+
const auth = request.headers.get('Authorization');
|
|
26
|
+
if (auth?.startsWith('Bearer ')) {
|
|
27
|
+
const token = auth.slice(7);
|
|
28
|
+
if (timingSafeEqual(token, secret)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const cookie = request.headers.get('Cookie') || '';
|
|
33
|
+
const match = new RegExp(String.raw `(?:^|;\s*)${escapeRegExp(cookieName)}=([^;]+)`).exec(cookie);
|
|
34
|
+
if (!match) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const expected = await hexSha256(secret);
|
|
38
|
+
return timingSafeEqual(match[1], expected);
|
|
39
|
+
}
|
|
40
|
+
function escapeRegExp(s) {
|
|
41
|
+
return s.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
|
|
42
|
+
}
|
|
43
|
+
export async function loginResponse(options) {
|
|
44
|
+
const { secret, password, secureCookie, loginHtml: renderLogin, cookieName = DEFAULT_COOKIE_NAME, cookiePath = '/', basePath, } = options;
|
|
45
|
+
if (!timingSafeEqual(password, secret)) {
|
|
46
|
+
return html(renderLogin('Invalid password'), 401);
|
|
47
|
+
}
|
|
48
|
+
const successRedirect = options.successRedirect ?? joinBasePath(resolveBasePath(basePath));
|
|
49
|
+
const token = await hexSha256(secret);
|
|
50
|
+
return redirect(successRedirect, adminAuthCookie(token, secureCookie, 86_400, cookieName, cookiePath));
|
|
51
|
+
}
|
|
52
|
+
export function logoutResponse(options) {
|
|
53
|
+
const { secureCookie, cookieName = DEFAULT_COOKIE_NAME, cookiePath = '/', basePath, } = options;
|
|
54
|
+
const resolvedBase = resolveBasePath(basePath);
|
|
55
|
+
const redirectTo = options.redirectTo ?? joinBasePath(resolvedBase, 'login');
|
|
56
|
+
// Browsers only drop a cookie when Path matches exactly, so clear the
|
|
57
|
+
// configured path, `/`, and the mount-scoped path (e.g. `/admin`).
|
|
58
|
+
const paths = new Set([
|
|
59
|
+
cookiePath,
|
|
60
|
+
'/',
|
|
61
|
+
cookiePathForBase(resolvedBase),
|
|
62
|
+
]);
|
|
63
|
+
const cookies = [...paths].map((path) => adminAuthCookie('', secureCookie, 0, cookieName, path));
|
|
64
|
+
return redirect(redirectTo, cookies);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/css.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Generated by scripts/embed-css.mjs — edit src/shared.css instead. */
|
|
2
|
+
export declare const SHARED_CSS = "* {\n box-sizing: border-box;\n}\n\n.admin-body {\n font-family:\n system-ui,\n -apple-system,\n 'Segoe UI',\n sans-serif;\n margin: 0;\n line-height: 1.45;\n color: #1a1a1a;\n background: #e4e6eb;\n min-height: 100vh;\n text-size-adjust: 100%;\n}\n\n.page-shell {\n max-width: 40rem;\n margin: 0 auto;\n padding: 0.75rem 1rem 2rem;\n}\n\n@media (width >= 640px) {\n .page-shell {\n padding: 1rem 1.25rem 2rem;\n }\n}\n\n.page-header {\n margin-bottom: 1rem;\n}\n\n.page-header h1 {\n font-size: clamp(1.25rem, 4vw, 1.5rem);\n margin: 0 0 0.5rem;\n}\n\n.panel-card {\n background: #fff;\n border-radius: 12px;\n padding: 1rem 1rem 1.25rem;\n box-shadow: 0 1px 3px rgb(0 0 0 / 7%);\n margin-bottom: 1rem;\n}\n\nlabel {\n display: block;\n margin-top: 1rem;\n font-weight: 600;\n font-size: 0.95rem;\n}\n\n.mode-row label {\n margin-top: 0;\n font-weight: normal;\n display: flex;\n align-items: center;\n gap: 0.35rem;\n}\n\n.row {\n display: flex;\n gap: 0.75rem;\n flex-wrap: wrap;\n}\n\n.row label {\n flex: 1;\n min-width: min(100%, 5.5rem);\n margin-top: 0;\n}\n\n@media (width <= 480px) {\n .row {\n flex-direction: column;\n }\n\n .row label {\n min-width: 100%;\n }\n}\n\nlabel:first-of-type,\n.panel-card > label:first-child {\n margin-top: 0;\n}\n\ninput[type='text'],\ninput[type='password'],\ninput[type='url'],\ninput[type='number'],\ntextarea,\nselect {\n width: 100%;\n padding: 0.55rem 0.65rem;\n font: inherit;\n border: 1px solid #c5c9d0;\n border-radius: 8px;\n background: #fafbfc;\n}\n\ntextarea {\n min-height: 5.5rem;\n resize: vertical;\n}\n\ninput:focus,\ntextarea:focus,\nselect:focus {\n outline: 2px solid #90cdf4;\n outline-offset: 1px;\n border-color: #3182ce;\n}\n\n.hint {\n font-size: 0.82rem;\n color: #4a5568;\n font-weight: normal;\n}\n\n.actions {\n margin-top: 1.5rem;\n display: flex;\n gap: 0.75rem;\n align-items: center;\n flex-wrap: wrap;\n}\n\n.btn-primary {\n padding: 0.6rem 1.1rem;\n border: none;\n border-radius: 8px;\n background: #2d3748;\n color: #fff;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n min-height: 44px;\n}\n\n.btn-primary:active {\n opacity: 0.94;\n}\n\n.btn-ghost {\n padding: 0.5rem 0.75rem;\n border: none;\n background: transparent;\n color: #4a5568;\n font: inherit;\n cursor: pointer;\n text-decoration: underline;\n min-height: 44px;\n}\n\n.ok {\n color: #276749;\n margin: 0 0 0.75rem;\n font-weight: 600;\n}\n\n.flash-err {\n color: #c53030;\n margin: 0 0 0.75rem;\n font-weight: 600;\n}\n\n.warn {\n color: #c05600;\n margin: 0 0 0.75rem;\n font-weight: 600;\n}\n\n.err {\n color: #b00;\n margin-bottom: 1rem;\n font-size: 0.95rem;\n}\n\n.btn-secondary {\n padding: 0.5rem 0.85rem;\n border: 1px solid #cbd5e0;\n border-radius: 8px;\n background: #f7fafc;\n font: inherit;\n cursor: pointer;\n min-height: 44px;\n font-size: 0.9rem;\n}\n\n.btn-secondary:active {\n background: #edf2f7;\n}\n\nfieldset {\n border: 1px solid #d8dee6;\n padding: 0.75rem 1rem;\n margin-top: 1rem;\n border-radius: 8px;\n background: #fafbfc;\n}\n\nlegend {\n padding: 0 0.35rem;\n font-weight: 600;\n}\n\n.mode-row {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem 1.25rem;\n margin-top: 0.5rem;\n}\n\n.mode-row input {\n width: auto;\n margin: 0;\n}\n\n.checkbox-row {\n margin-top: 1rem;\n font-weight: normal;\n display: flex;\n align-items: center;\n gap: 0.35rem;\n}\n\n.checkbox-row input {\n width: auto;\n margin: 0;\n}\n\n.login-shell {\n max-width: 24rem;\n margin: 0 auto;\n padding: 1.25rem;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.login-shell .panel-card {\n width: 100%;\n margin: 0;\n}\n\n.login-shell button {\n width: 100%;\n}";
|
|
3
|
+
//# sourceMappingURL=css.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../src/css.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,eAAO,MAAM,UAAU,+uHAA+uH,CAAC"}
|
package/dist/css.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Generated by scripts/embed-css.mjs — edit src/shared.css instead. */
|
|
2
|
+
export const SHARED_CSS = "* {\n box-sizing: border-box;\n}\n\n.admin-body {\n font-family:\n system-ui,\n -apple-system,\n 'Segoe UI',\n sans-serif;\n margin: 0;\n line-height: 1.45;\n color: #1a1a1a;\n background: #e4e6eb;\n min-height: 100vh;\n text-size-adjust: 100%;\n}\n\n.page-shell {\n max-width: 40rem;\n margin: 0 auto;\n padding: 0.75rem 1rem 2rem;\n}\n\n@media (width >= 640px) {\n .page-shell {\n padding: 1rem 1.25rem 2rem;\n }\n}\n\n.page-header {\n margin-bottom: 1rem;\n}\n\n.page-header h1 {\n font-size: clamp(1.25rem, 4vw, 1.5rem);\n margin: 0 0 0.5rem;\n}\n\n.panel-card {\n background: #fff;\n border-radius: 12px;\n padding: 1rem 1rem 1.25rem;\n box-shadow: 0 1px 3px rgb(0 0 0 / 7%);\n margin-bottom: 1rem;\n}\n\nlabel {\n display: block;\n margin-top: 1rem;\n font-weight: 600;\n font-size: 0.95rem;\n}\n\n.mode-row label {\n margin-top: 0;\n font-weight: normal;\n display: flex;\n align-items: center;\n gap: 0.35rem;\n}\n\n.row {\n display: flex;\n gap: 0.75rem;\n flex-wrap: wrap;\n}\n\n.row label {\n flex: 1;\n min-width: min(100%, 5.5rem);\n margin-top: 0;\n}\n\n@media (width <= 480px) {\n .row {\n flex-direction: column;\n }\n\n .row label {\n min-width: 100%;\n }\n}\n\nlabel:first-of-type,\n.panel-card > label:first-child {\n margin-top: 0;\n}\n\ninput[type='text'],\ninput[type='password'],\ninput[type='url'],\ninput[type='number'],\ntextarea,\nselect {\n width: 100%;\n padding: 0.55rem 0.65rem;\n font: inherit;\n border: 1px solid #c5c9d0;\n border-radius: 8px;\n background: #fafbfc;\n}\n\ntextarea {\n min-height: 5.5rem;\n resize: vertical;\n}\n\ninput:focus,\ntextarea:focus,\nselect:focus {\n outline: 2px solid #90cdf4;\n outline-offset: 1px;\n border-color: #3182ce;\n}\n\n.hint {\n font-size: 0.82rem;\n color: #4a5568;\n font-weight: normal;\n}\n\n.actions {\n margin-top: 1.5rem;\n display: flex;\n gap: 0.75rem;\n align-items: center;\n flex-wrap: wrap;\n}\n\n.btn-primary {\n padding: 0.6rem 1.1rem;\n border: none;\n border-radius: 8px;\n background: #2d3748;\n color: #fff;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n min-height: 44px;\n}\n\n.btn-primary:active {\n opacity: 0.94;\n}\n\n.btn-ghost {\n padding: 0.5rem 0.75rem;\n border: none;\n background: transparent;\n color: #4a5568;\n font: inherit;\n cursor: pointer;\n text-decoration: underline;\n min-height: 44px;\n}\n\n.ok {\n color: #276749;\n margin: 0 0 0.75rem;\n font-weight: 600;\n}\n\n.flash-err {\n color: #c53030;\n margin: 0 0 0.75rem;\n font-weight: 600;\n}\n\n.warn {\n color: #c05600;\n margin: 0 0 0.75rem;\n font-weight: 600;\n}\n\n.err {\n color: #b00;\n margin-bottom: 1rem;\n font-size: 0.95rem;\n}\n\n.btn-secondary {\n padding: 0.5rem 0.85rem;\n border: 1px solid #cbd5e0;\n border-radius: 8px;\n background: #f7fafc;\n font: inherit;\n cursor: pointer;\n min-height: 44px;\n font-size: 0.9rem;\n}\n\n.btn-secondary:active {\n background: #edf2f7;\n}\n\nfieldset {\n border: 1px solid #d8dee6;\n padding: 0.75rem 1rem;\n margin-top: 1rem;\n border-radius: 8px;\n background: #fafbfc;\n}\n\nlegend {\n padding: 0 0.35rem;\n font-weight: 600;\n}\n\n.mode-row {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem 1.25rem;\n margin-top: 0.5rem;\n}\n\n.mode-row input {\n width: auto;\n margin: 0;\n}\n\n.checkbox-row {\n margin-top: 1rem;\n font-weight: normal;\n display: flex;\n align-items: center;\n gap: 0.35rem;\n}\n\n.checkbox-row input {\n width: auto;\n margin: 0;\n}\n\n.login-shell {\n max-width: 24rem;\n margin: 0 auto;\n padding: 1.25rem;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.login-shell .panel-card {\n width: 100%;\n margin: 0;\n}\n\n.login-shell button {\n width: 100%;\n}";
|
|
3
|
+
//# sourceMappingURL=css.js.map
|
package/dist/css.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"css.js","sourceRoot":"","sources":["../src/css.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,MAAM,CAAC,MAAM,UAAU,GAAG,4uHAA4uH,CAAC"}
|
package/dist/escape.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA;;EAEE;AACF,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAM5C"}
|
package/dist/escape.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape.js","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA;;EAEE;AACF,MAAM,UAAU,UAAU,CAAC,CAAS;IAClC,OAAO,CAAC;SACL,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;SACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AAC/B,CAAC"}
|
package/dist/form.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type FieldOption = {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
};
|
|
5
|
+
type FieldBase = {
|
|
6
|
+
label?: string;
|
|
7
|
+
hint?: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
};
|
|
11
|
+
export type TextField = FieldBase & {
|
|
12
|
+
type: 'text' | 'url' | 'password';
|
|
13
|
+
name: string;
|
|
14
|
+
value?: string;
|
|
15
|
+
};
|
|
16
|
+
export type NumberField = FieldBase & {
|
|
17
|
+
type: 'number';
|
|
18
|
+
name: string;
|
|
19
|
+
value?: string | number;
|
|
20
|
+
min?: number;
|
|
21
|
+
max?: number;
|
|
22
|
+
step?: number | string;
|
|
23
|
+
};
|
|
24
|
+
export type TextareaField = FieldBase & {
|
|
25
|
+
type: 'textarea';
|
|
26
|
+
name: string;
|
|
27
|
+
value?: string;
|
|
28
|
+
};
|
|
29
|
+
export type SelectField = FieldBase & {
|
|
30
|
+
type: 'select';
|
|
31
|
+
name: string;
|
|
32
|
+
value?: string;
|
|
33
|
+
options: FieldOption[];
|
|
34
|
+
};
|
|
35
|
+
export type RadioField = FieldBase & {
|
|
36
|
+
type: 'radio';
|
|
37
|
+
name: string;
|
|
38
|
+
value?: string;
|
|
39
|
+
options: FieldOption[];
|
|
40
|
+
};
|
|
41
|
+
export type CheckboxField = FieldBase & {
|
|
42
|
+
type: 'checkbox';
|
|
43
|
+
name: string;
|
|
44
|
+
/**
|
|
45
|
+
Whether the checkbox is checked when rendering.
|
|
46
|
+
*/
|
|
47
|
+
checked?: boolean;
|
|
48
|
+
value?: string;
|
|
49
|
+
};
|
|
50
|
+
export type FieldsetField = {
|
|
51
|
+
type: 'fieldset';
|
|
52
|
+
legend: string;
|
|
53
|
+
hint?: string;
|
|
54
|
+
fields: Field[];
|
|
55
|
+
};
|
|
56
|
+
export type HtmlField = {
|
|
57
|
+
type: 'html';
|
|
58
|
+
html: string;
|
|
59
|
+
};
|
|
60
|
+
export type Field = TextField | NumberField | TextareaField | SelectField | RadioField | CheckboxField | FieldsetField | HtmlField;
|
|
61
|
+
export declare function adminForm(options: {
|
|
62
|
+
action: string;
|
|
63
|
+
fields: Field[];
|
|
64
|
+
method?: 'get' | 'post';
|
|
65
|
+
submitLabel?: string;
|
|
66
|
+
id?: string;
|
|
67
|
+
}): string;
|
|
68
|
+
export declare function formText(form: FormData, name: string): string;
|
|
69
|
+
/**
|
|
70
|
+
Named scalar values from a form matching the field schema (checkbox → boolean).
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseAdminForm(form: FormData, fields: Field[]): Record<string, string | boolean>;
|
|
73
|
+
export {};
|
|
74
|
+
//# sourceMappingURL=form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../src/form.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG;IAClC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,UAAU,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG;IACpC,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG;IACtC,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG;IACpC,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG;IACtC,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb;;QAEC;IACD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,KAAK,GACb,SAAS,GACT,WAAW,GACX,aAAa,GACb,WAAW,GACX,UAAU,GACV,aAAa,GACb,aAAa,GACb,SAAS,CAAC;AA2Id,wBAAgB,SAAS,CAAC,OAAO,EAAE;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,MAAM,CAUT;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7D;AAgCD;;EAEE;AACF,wBAAgB,cAAc,CAC5B,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,KAAK,EAAE,GACd,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAgBlC"}
|
package/dist/form.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { escapeHtml } from './escape.js';
|
|
2
|
+
function optionalHint(hint) {
|
|
3
|
+
return hint ? ` <span class="hint">${escapeHtml(hint)}</span>` : '';
|
|
4
|
+
}
|
|
5
|
+
function labelHtml(id, label, hint) {
|
|
6
|
+
return `<label for="${escapeHtml(id)}">${escapeHtml(label)}${optionalHint(hint)}</label>`;
|
|
7
|
+
}
|
|
8
|
+
function requiredAttribute(required) {
|
|
9
|
+
return required ? ' required' : '';
|
|
10
|
+
}
|
|
11
|
+
function renderField(field) {
|
|
12
|
+
switch (field.type) {
|
|
13
|
+
case 'html': {
|
|
14
|
+
return field.html;
|
|
15
|
+
}
|
|
16
|
+
case 'fieldset': {
|
|
17
|
+
return renderFieldset(field);
|
|
18
|
+
}
|
|
19
|
+
case 'radio': {
|
|
20
|
+
return renderRadio(field);
|
|
21
|
+
}
|
|
22
|
+
case 'checkbox': {
|
|
23
|
+
return renderCheckbox(field);
|
|
24
|
+
}
|
|
25
|
+
case 'textarea': {
|
|
26
|
+
return renderTextarea(field);
|
|
27
|
+
}
|
|
28
|
+
case 'select': {
|
|
29
|
+
return renderSelect(field);
|
|
30
|
+
}
|
|
31
|
+
case 'number': {
|
|
32
|
+
return renderNumber(field);
|
|
33
|
+
}
|
|
34
|
+
default: {
|
|
35
|
+
return renderTextInput(field);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function renderFieldset(field) {
|
|
40
|
+
const hint = field.hint
|
|
41
|
+
? `<p class="hint">${escapeHtml(field.hint)}</p>`
|
|
42
|
+
: '';
|
|
43
|
+
const inner = field.fields.map((child) => renderField(child)).join('\n');
|
|
44
|
+
return `<fieldset>
|
|
45
|
+
<legend>${escapeHtml(field.legend)}</legend>
|
|
46
|
+
${hint}
|
|
47
|
+
${inner}
|
|
48
|
+
</fieldset>`;
|
|
49
|
+
}
|
|
50
|
+
function renderRadio(field) {
|
|
51
|
+
const legend = field.label
|
|
52
|
+
? `<legend>${escapeHtml(field.label)}${optionalHint(field.hint)}</legend>`
|
|
53
|
+
: '';
|
|
54
|
+
const options = field.options
|
|
55
|
+
.map((option) => {
|
|
56
|
+
const id = `${field.name}_${option.value}`;
|
|
57
|
+
const checked = option.value === (field.value ?? '') ? ' checked' : '';
|
|
58
|
+
return `<label for="${escapeHtml(id)}"><input id="${escapeHtml(id)}" type="radio" name="${escapeHtml(field.name)}" value="${escapeHtml(option.value)}"${checked}${requiredAttribute(field.required)}> ${escapeHtml(option.label)}</label>`;
|
|
59
|
+
})
|
|
60
|
+
.join('\n ');
|
|
61
|
+
return `<fieldset>
|
|
62
|
+
${legend}
|
|
63
|
+
<div class="mode-row">
|
|
64
|
+
${options}
|
|
65
|
+
</div>
|
|
66
|
+
</fieldset>`;
|
|
67
|
+
}
|
|
68
|
+
function renderCheckbox(field) {
|
|
69
|
+
const id = field.name;
|
|
70
|
+
const checked = field.checked ? ' checked' : '';
|
|
71
|
+
const value = field.value ?? 'on';
|
|
72
|
+
return `<label class="checkbox-row" for="${escapeHtml(id)}">
|
|
73
|
+
<input id="${escapeHtml(id)}" type="checkbox" name="${escapeHtml(field.name)}" value="${escapeHtml(value)}"${checked}>
|
|
74
|
+
${escapeHtml(field.label ?? field.name)}${optionalHint(field.hint)}
|
|
75
|
+
</label>`;
|
|
76
|
+
}
|
|
77
|
+
function renderTextarea(field) {
|
|
78
|
+
const id = field.name;
|
|
79
|
+
const label = field.label ? labelHtml(id, field.label, field.hint) : '';
|
|
80
|
+
const placeholder = field.placeholder
|
|
81
|
+
? ` placeholder="${escapeHtml(field.placeholder)}"`
|
|
82
|
+
: '';
|
|
83
|
+
return `${label}
|
|
84
|
+
<textarea id="${escapeHtml(id)}" name="${escapeHtml(field.name)}"${placeholder}${requiredAttribute(field.required)}>${escapeHtml(field.value ?? '')}</textarea>`;
|
|
85
|
+
}
|
|
86
|
+
function renderSelect(field) {
|
|
87
|
+
const id = field.name;
|
|
88
|
+
const label = field.label ? labelHtml(id, field.label, field.hint) : '';
|
|
89
|
+
const options = field.options
|
|
90
|
+
.map((option) => {
|
|
91
|
+
const selected = option.value === (field.value ?? '') ? ' selected' : '';
|
|
92
|
+
return `<option value="${escapeHtml(option.value)}"${selected}>${escapeHtml(option.label)}</option>`;
|
|
93
|
+
})
|
|
94
|
+
.join('\n ');
|
|
95
|
+
return `${label}
|
|
96
|
+
<select id="${escapeHtml(id)}" name="${escapeHtml(field.name)}"${requiredAttribute(field.required)}>
|
|
97
|
+
${options}
|
|
98
|
+
</select>`;
|
|
99
|
+
}
|
|
100
|
+
function renderNumber(field) {
|
|
101
|
+
const id = field.name;
|
|
102
|
+
const label = field.label ? labelHtml(id, field.label, field.hint) : '';
|
|
103
|
+
const min = field.min === undefined ? '' : ` min="${field.min}"`;
|
|
104
|
+
const max = field.max === undefined ? '' : ` max="${field.max}"`;
|
|
105
|
+
const step = field.step === undefined ? '' : ` step="${escapeHtml(String(field.step))}"`;
|
|
106
|
+
const placeholder = field.placeholder
|
|
107
|
+
? ` placeholder="${escapeHtml(field.placeholder)}"`
|
|
108
|
+
: '';
|
|
109
|
+
const value = field.value !== undefined && field.value !== ''
|
|
110
|
+
? ` value="${escapeHtml(String(field.value))}"`
|
|
111
|
+
: '';
|
|
112
|
+
return `${label}
|
|
113
|
+
<input id="${escapeHtml(id)}" name="${escapeHtml(field.name)}" type="number"${value}${min}${max}${step}${placeholder}${requiredAttribute(field.required)}>`;
|
|
114
|
+
}
|
|
115
|
+
function renderTextInput(field) {
|
|
116
|
+
const id = field.name;
|
|
117
|
+
const label = field.label ? labelHtml(id, field.label, field.hint) : '';
|
|
118
|
+
const placeholder = field.placeholder
|
|
119
|
+
? ` placeholder="${escapeHtml(field.placeholder)}"`
|
|
120
|
+
: '';
|
|
121
|
+
const value = field.value === undefined ? '' : ` value="${escapeHtml(field.value)}"`;
|
|
122
|
+
return `${label}
|
|
123
|
+
<input id="${escapeHtml(id)}" name="${escapeHtml(field.name)}" type="${field.type}"${value}${placeholder}${requiredAttribute(field.required)}>`;
|
|
124
|
+
}
|
|
125
|
+
export function adminForm(options) {
|
|
126
|
+
const { action, fields, method = 'post', submitLabel = 'Save', id } = options;
|
|
127
|
+
const idAttribute = id ? ` id="${escapeHtml(id)}"` : '';
|
|
128
|
+
const body = fields.map((field) => renderField(field)).join('\n');
|
|
129
|
+
return `<form method="${method}" action="${escapeHtml(action)}"${idAttribute}>
|
|
130
|
+
${body}
|
|
131
|
+
<div class="actions">
|
|
132
|
+
<button type="submit" class="btn-primary">${escapeHtml(submitLabel)}</button>
|
|
133
|
+
</div>
|
|
134
|
+
</form>`;
|
|
135
|
+
}
|
|
136
|
+
export function formText(form, name) {
|
|
137
|
+
const value = form.get(name);
|
|
138
|
+
return typeof value === 'string' ? value : '';
|
|
139
|
+
}
|
|
140
|
+
const VALUE_FIELD_TYPES = new Set([
|
|
141
|
+
'text',
|
|
142
|
+
'url',
|
|
143
|
+
'password',
|
|
144
|
+
'number',
|
|
145
|
+
'textarea',
|
|
146
|
+
'select',
|
|
147
|
+
'radio',
|
|
148
|
+
]);
|
|
149
|
+
function isValueField(field) {
|
|
150
|
+
return VALUE_FIELD_TYPES.has(field.type);
|
|
151
|
+
}
|
|
152
|
+
function walkScalarFields(fields, out) {
|
|
153
|
+
for (const field of fields) {
|
|
154
|
+
if (field.type === 'html') {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (field.type === 'fieldset') {
|
|
158
|
+
walkScalarFields(field.fields, out);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
out.push(field);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
Named scalar values from a form matching the field schema (checkbox → boolean).
|
|
166
|
+
*/
|
|
167
|
+
export function parseAdminForm(form, fields) {
|
|
168
|
+
const scalars = [];
|
|
169
|
+
walkScalarFields(fields, scalars);
|
|
170
|
+
const result = {};
|
|
171
|
+
for (const field of scalars) {
|
|
172
|
+
if (field.type === 'checkbox') {
|
|
173
|
+
const raw = form.get(field.name);
|
|
174
|
+
const expected = field.value ?? 'on';
|
|
175
|
+
result[field.name] = typeof raw === 'string' && raw === expected;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (isValueField(field)) {
|
|
179
|
+
result[field.name] = formText(form, field.name);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=form.js.map
|
package/dist/form.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form.js","sourceRoot":"","sources":["../src/form.ts"],"names":[],"mappings":"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"}
|