@volter/world-ui 0.1.37 → 1.0.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/client/payment.tsx +1 -0
- package/dist/client/payment.tsx +1 -0
- package/dist/src/index.d.ts +1 -19
- package/dist/src/index.js +1 -41
- package/dist/src/session.d.ts +13 -0
- package/dist/src/session.js +33 -0
- package/package.json +5 -6
- package/src/index.ts +4 -54
- package/src/session.ts +38 -0
- package/client/kit.css +0 -55
- package/client/kit.tsx +0 -312
- package/dist/client/kit.bundle.js +0 -235
- package/dist/client/kit.css +0 -55
- package/dist/client/kit.d.ts +0 -101
- package/dist/client/kit.js +0 -160
- package/dist/client/kit.tsx +0 -312
package/client/payment.tsx
CHANGED
|
@@ -90,6 +90,7 @@ export function Payment(props: PaymentProps) {
|
|
|
90
90
|
<button type="submit" className="pay-submit SubmitButton" data-testid={props.submit.testId}>{props.submit.label}</button>
|
|
91
91
|
{props.note ? <p className="pay-note">{props.note}</p> : null}
|
|
92
92
|
</form>
|
|
93
|
+
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: FORMAT_SCRIPT is this module's own literal, never data */}
|
|
93
94
|
{props.sections.some((s) => s.fields.some((f) => f.format)) ? <script dangerouslySetInnerHTML={{ __html: FORMAT_SCRIPT }} /> : null}
|
|
94
95
|
</main>
|
|
95
96
|
</div>
|
package/dist/client/payment.tsx
CHANGED
|
@@ -90,6 +90,7 @@ export function Payment(props: PaymentProps) {
|
|
|
90
90
|
<button type="submit" className="pay-submit SubmitButton" data-testid={props.submit.testId}>{props.submit.label}</button>
|
|
91
91
|
{props.note ? <p className="pay-note">{props.note}</p> : null}
|
|
92
92
|
</form>
|
|
93
|
+
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: FORMAT_SCRIPT is this module's own literal, never data */}
|
|
93
94
|
{props.sections.some((s) => s.fields.some((f) => f.format)) ? <script dangerouslySetInnerHTML={{ __html: FORMAT_SCRIPT }} /> : null}
|
|
94
95
|
</main>
|
|
95
96
|
</div>
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
1
|
import type { ReactElement } from 'react';
|
|
2
|
-
export type TwinUiOptions = {
|
|
3
|
-
title: string;
|
|
4
|
-
/** the pack's client entry (a .tsx that calls mountTwinUi), resolved when first served */
|
|
5
|
-
clientEntry: () => string;
|
|
6
|
-
/** the pack's own stylesheet: its skin over the kit's */
|
|
7
|
-
css?: () => string;
|
|
8
|
-
/** the pack's fetch: every request that is not the UI's own is the vendor API */
|
|
9
|
-
api: (request: Request) => Promise<Response>;
|
|
10
|
-
};
|
|
11
|
-
export declare function twinUiHtml(title: string): string;
|
|
12
|
-
/** The UI's routes (the shell, the client, the stylesheets) over the pack's API. */
|
|
13
|
-
export declare function twinUiFetch(options: TwinUiOptions): (request: Request) => Promise<Response>;
|
|
14
|
-
/** The UI on its own loopback port. Loopback-specific, so the port the kernel hands out is free on 127.0.0.1. */
|
|
15
|
-
export declare function serveTwinUi(options: TwinUiOptions & {
|
|
16
|
-
port?: number;
|
|
17
|
-
}): Promise<{
|
|
18
|
-
port: number;
|
|
19
|
-
stop: () => void;
|
|
20
|
-
}>;
|
|
21
2
|
/** A hosted flow's page (docs/contributing/architecture.md, "Screens"): rendered on the server at the
|
|
22
3
|
* vendor's own host and path, its styles inline, so it needs no asset routes beside it. */
|
|
23
4
|
export declare function flowPage(options: {
|
|
@@ -31,3 +12,4 @@ export { Consent, CONSENT_CSS, type ConsentPermission, type ConsentProps } from
|
|
|
31
12
|
export { FORMAT_SCRIPT, Payment, PAYMENT_CSS, type PaymentField, type PaymentProps } from '../client/payment.js';
|
|
32
13
|
export { Portal, PORTAL_CSS, type PortalAction, type PortalForm, type PortalItem, type PortalProps, type PortalSection } from '../client/portal.js';
|
|
33
14
|
export { cookieOf, SignIn, SignInCode, SIGN_IN_CSS, type SignInCodeProps, type SignInProps } from '../client/sign-in.js';
|
|
15
|
+
export { formOf, redirect, signedIn, startSession, toSignIn } from './session.js';
|
package/dist/src/index.js
CHANGED
|
@@ -1,45 +1,4 @@
|
|
|
1
1
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
2
|
-
import { brandTokensResponse, bundleClient, fileResponse, serveHttp, filePathOf } from '@volter/world-core';
|
|
3
|
-
const KIT_CSS = () => filePathOf(new URL('../client/kit.css', import.meta.url));
|
|
4
|
-
/** The brand's tokens and faces the kit's roles read (company decision 0018), fetched at build into client/brand/ (gitignored). */
|
|
5
|
-
const BRAND_DIR = () => filePathOf(new URL('../client/brand', import.meta.url));
|
|
6
|
-
export function twinUiHtml(title) {
|
|
7
|
-
const safe = title.replace(/[<>&"]/g, '');
|
|
8
|
-
return `<!doctype html>
|
|
9
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
10
|
-
<base href="/"><title>${safe}</title><link rel="stylesheet" href="assets/brand/tokens.css"><link rel="stylesheet" href="assets/kit.css"><link rel="stylesheet" href="assets/styles.css"></head>
|
|
11
|
-
<body><div id="root"></div><script type="module" src="assets/app.js"></script></body></html>`;
|
|
12
|
-
}
|
|
13
|
-
/** The UI's routes (the shell, the client, the stylesheets) over the pack's API. */
|
|
14
|
-
export function twinUiFetch(options) {
|
|
15
|
-
return async (request) => {
|
|
16
|
-
const url = new URL(request.url);
|
|
17
|
-
if (request.method === 'GET') {
|
|
18
|
-
if (url.pathname === '/' || url.pathname === '')
|
|
19
|
-
return new Response(twinUiHtml(options.title), { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
20
|
-
if (url.pathname === '/assets/app.js') {
|
|
21
|
-
try {
|
|
22
|
-
return new Response(await bundleClient(options.clientEntry()), { headers: { 'content-type': 'text/javascript; charset=utf-8' } });
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
return new Response(String(error), { status: 500 });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
if (url.pathname.startsWith('/assets/brand/'))
|
|
29
|
-
return brandTokensResponse(BRAND_DIR(), url.pathname.slice('/assets/brand/'.length));
|
|
30
|
-
if (url.pathname === '/assets/kit.css')
|
|
31
|
-
return fileResponse(KIT_CSS(), { headers: { 'content-type': 'text/css; charset=utf-8' } });
|
|
32
|
-
if (url.pathname === '/assets/styles.css')
|
|
33
|
-
return options.css ? fileResponse(options.css(), { headers: { 'content-type': 'text/css; charset=utf-8' } }) : new Response('', { headers: { 'content-type': 'text/css; charset=utf-8' } });
|
|
34
|
-
}
|
|
35
|
-
return options.api(request);
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
/** The UI on its own loopback port. Loopback-specific, so the port the kernel hands out is free on 127.0.0.1. */
|
|
39
|
-
export async function serveTwinUi(options) {
|
|
40
|
-
const server = await serveHttp({ hostname: '127.0.0.1', port: options.port ?? 0, idleTimeout: 60, fetch: twinUiFetch(options) });
|
|
41
|
-
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
42
|
-
}
|
|
43
2
|
/** A hosted flow's page (docs/contributing/architecture.md, "Screens"): rendered on the server at the
|
|
44
3
|
* vendor's own host and path, its styles inline, so it needs no asset routes beside it. */
|
|
45
4
|
export function flowPage(options) {
|
|
@@ -54,3 +13,4 @@ export { Consent, CONSENT_CSS } from "../client/consent.js";
|
|
|
54
13
|
export { FORMAT_SCRIPT, Payment, PAYMENT_CSS } from "../client/payment.js";
|
|
55
14
|
export { Portal, PORTAL_CSS } from "../client/portal.js";
|
|
56
15
|
export { cookieOf, SignIn, SignInCode, SIGN_IN_CSS } from "../client/sign-in.js";
|
|
16
|
+
export { formOf, redirect, signedIn, startSession, toSignIn } from "./session.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type HandlerContext, type Person } from '@volter/world-core';
|
|
2
|
+
/** A form a page posted, by field. */
|
|
3
|
+
export declare const formOf: (ctx: HandlerContext) => Record<string, string>;
|
|
4
|
+
/** The person the request's session cookie names, or undefined when nobody is signed in; with `scope`, only a session
|
|
5
|
+
* begun in it (a vendor's sandbox dashboard and its production one sign a person in apart). */
|
|
6
|
+
export declare function signedIn(ctx: HandlerContext, cookie: string, scope?: string): Person | undefined;
|
|
7
|
+
/** A redirect, setting a cookie when given one. */
|
|
8
|
+
export declare function redirect(location: string, cookie?: string): Response;
|
|
9
|
+
/** The person signed in: a session recorded (in `scope`, when the vendor signs a person in apart per dashboard), its
|
|
10
|
+
* cookie set, and the page they asked for next. */
|
|
11
|
+
export declare function startSession(ctx: HandlerContext, email: string, cookie: string, next: string, scope?: string): Promise<Response>;
|
|
12
|
+
/** To the vendor's sign-in page, which returns the person to `path` (its parameter the vendor's: `next`, `return_to`). */
|
|
13
|
+
export declare const toSignIn: (signIn: string, path: string, param?: string) => Response;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The session kit (docs/contributing/architecture.md, "Screens", "Who is on a screen"): how every dashboard a pack builds
|
|
2
|
+
// knows who is signed in, the same way in every pack. A person is the kernel's (`recordPerson`, `personWith`); signing
|
|
3
|
+
// in records a `_web_session` row and sets the vendor's session cookie on the answer; a page asks who the request's
|
|
4
|
+
// cookie names. Everything is written and read through the screen's context.
|
|
5
|
+
import { personOf } from '@volter/world-core';
|
|
6
|
+
import { cookieOf } from "../client/sign-in.js";
|
|
7
|
+
const SESSION = '_web_session';
|
|
8
|
+
/** A form a page posted, by field. */
|
|
9
|
+
export const formOf = (ctx) => Object.fromEntries(new URLSearchParams(ctx.text));
|
|
10
|
+
/** The person the request's session cookie names, or undefined when nobody is signed in; with `scope`, only a session
|
|
11
|
+
* begun in it (a vendor's sandbox dashboard and its production one sign a person in apart). */
|
|
12
|
+
export function signedIn(ctx, cookie, scope) {
|
|
13
|
+
const token = cookieOf(ctx.call.request, cookie);
|
|
14
|
+
const session = token ? ctx.rowsRaw(SESSION).find((s) => s.token === token && (scope === undefined || s.scope === scope)) : undefined;
|
|
15
|
+
return session ? personOf(ctx, String(session.email)) : undefined;
|
|
16
|
+
}
|
|
17
|
+
/** A redirect, setting a cookie when given one. */
|
|
18
|
+
export function redirect(location, cookie) {
|
|
19
|
+
const headers = new Headers({ location });
|
|
20
|
+
if (cookie)
|
|
21
|
+
headers.append('set-cookie', cookie);
|
|
22
|
+
return new Response(null, { status: 302, headers });
|
|
23
|
+
}
|
|
24
|
+
/** The person signed in: a session recorded (in `scope`, when the vendor signs a person in apart per dashboard), its
|
|
25
|
+
* cookie set, and the page they asked for next. */
|
|
26
|
+
export async function startSession(ctx, email, cookie, next, scope) {
|
|
27
|
+
const issued = await ctx.record('_web_session_issue', { email, ...(scope !== undefined ? { scope } : {}) });
|
|
28
|
+
const token = await ctx.secret(`web-session:${issued}`);
|
|
29
|
+
await ctx.record(SESSION, { token, email, ...(scope !== undefined ? { scope } : {}), created_at: ctx.occurredAt }, `websession:${token}`);
|
|
30
|
+
return redirect(next, `${cookie}=${token}; path=/; HttpOnly; Secure; SameSite=Lax`);
|
|
31
|
+
}
|
|
32
|
+
/** To the vendor's sign-in page, which returns the person to `path` (its parameter the vendor's: `next`, `return_to`). */
|
|
33
|
+
export const toSignIn = (signIn, path, param = 'next') => redirect(`${signIn}?${param}=${encodeURIComponent(path)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-ui",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Shared pieces and serving for derived packs' screens (hosted flows and workspaces), through each pack's own API; see docs/contributing/architecture.md, Screens.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Volter (https://github.com/volter-ai)",
|
|
@@ -10,15 +10,14 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "node ../../scripts/publish/build.mjs",
|
|
13
|
-
"prepack": "
|
|
13
|
+
"prepack": "node ../../scripts/publish/prepare-publish.mjs prepack",
|
|
14
14
|
"postpack": "node ../../scripts/publish/prepare-publish.mjs postpack"
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
18
|
"types": "./dist/src/index.d.ts",
|
|
19
19
|
"default": "./dist/src/index.js"
|
|
20
|
-
}
|
|
21
|
-
"./kit": "./client/kit.tsx"
|
|
20
|
+
}
|
|
22
21
|
},
|
|
23
22
|
"files": [
|
|
24
23
|
"src",
|
|
@@ -29,13 +28,13 @@
|
|
|
29
28
|
"dist"
|
|
30
29
|
],
|
|
31
30
|
"peerDependencies": {
|
|
32
|
-
"@volter/world-core": "
|
|
31
|
+
"@volter/world-core": "3.0.1",
|
|
33
32
|
"react": "^19.2.7",
|
|
34
33
|
"react-dom": "^19.2.7"
|
|
35
34
|
},
|
|
36
35
|
"repository": {
|
|
37
36
|
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/volter-ai/twin.git",
|
|
37
|
+
"url": "git+https://github.com/volter-ai/twin-world.git",
|
|
39
38
|
"directory": "packages/world-ui"
|
|
40
39
|
},
|
|
41
40
|
"homepage": "https://world.volter.ai/docs/README",
|
package/src/index.ts
CHANGED
|
@@ -1,59 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// its title, its client entry and stylesheet, and its fetch; nothing here knows a vendor.
|
|
1
|
+
// @volter/world-ui — what a pack's screens are built from (docs/contributing/architecture.md, "Screens"): the page a
|
|
2
|
+
// hosted flow renders on the server, the pieces its pages are made of (sign-in, a portal, consent, payment) and the
|
|
3
|
+
// session kit every dashboard signs its person in with. It imports the kernel's pack contract alone, as a pack does.
|
|
5
4
|
import type { ReactElement } from 'react';
|
|
6
5
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
7
|
-
import { brandTokensResponse, bundleClient, fileResponse, serveHttp, filePathOf } from '@volter/world-core';
|
|
8
|
-
|
|
9
|
-
export type TwinUiOptions = {
|
|
10
|
-
title: string;
|
|
11
|
-
/** the pack's client entry (a .tsx that calls mountTwinUi), resolved when first served */
|
|
12
|
-
clientEntry: () => string;
|
|
13
|
-
/** the pack's own stylesheet: its skin over the kit's */
|
|
14
|
-
css?: () => string;
|
|
15
|
-
/** the pack's fetch: every request that is not the UI's own is the vendor API */
|
|
16
|
-
api: (request: Request) => Promise<Response>;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const KIT_CSS = (): string => filePathOf(new URL('../client/kit.css', import.meta.url));
|
|
20
|
-
/** The brand's tokens and faces the kit's roles read (company decision 0018), fetched at build into client/brand/ (gitignored). */
|
|
21
|
-
const BRAND_DIR = (): string => filePathOf(new URL('../client/brand', import.meta.url));
|
|
22
|
-
|
|
23
|
-
export function twinUiHtml(title: string): string {
|
|
24
|
-
const safe = title.replace(/[<>&"]/g, '');
|
|
25
|
-
return `<!doctype html>
|
|
26
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
27
|
-
<base href="/"><title>${safe}</title><link rel="stylesheet" href="assets/brand/tokens.css"><link rel="stylesheet" href="assets/kit.css"><link rel="stylesheet" href="assets/styles.css"></head>
|
|
28
|
-
<body><div id="root"></div><script type="module" src="assets/app.js"></script></body></html>`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** The UI's routes (the shell, the client, the stylesheets) over the pack's API. */
|
|
32
|
-
export function twinUiFetch(options: TwinUiOptions): (request: Request) => Promise<Response> {
|
|
33
|
-
return async (request) => {
|
|
34
|
-
const url = new URL(request.url);
|
|
35
|
-
if (request.method === 'GET') {
|
|
36
|
-
if (url.pathname === '/' || url.pathname === '') return new Response(twinUiHtml(options.title), { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
37
|
-
if (url.pathname === '/assets/app.js') {
|
|
38
|
-
try {
|
|
39
|
-
return new Response(await bundleClient(options.clientEntry()), { headers: { 'content-type': 'text/javascript; charset=utf-8' } });
|
|
40
|
-
} catch (error) {
|
|
41
|
-
return new Response(String(error), { status: 500 });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (url.pathname.startsWith('/assets/brand/')) return brandTokensResponse(BRAND_DIR(), url.pathname.slice('/assets/brand/'.length));
|
|
45
|
-
if (url.pathname === '/assets/kit.css') return fileResponse(KIT_CSS(), { headers: { 'content-type': 'text/css; charset=utf-8' } });
|
|
46
|
-
if (url.pathname === '/assets/styles.css') return options.css ? fileResponse(options.css(), { headers: { 'content-type': 'text/css; charset=utf-8' } }) : new Response('', { headers: { 'content-type': 'text/css; charset=utf-8' } });
|
|
47
|
-
}
|
|
48
|
-
return options.api(request);
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** The UI on its own loopback port. Loopback-specific, so the port the kernel hands out is free on 127.0.0.1. */
|
|
53
|
-
export async function serveTwinUi(options: TwinUiOptions & { port?: number }): Promise<{ port: number; stop: () => void }> {
|
|
54
|
-
const server = await serveHttp({ hostname: '127.0.0.1', port: options.port ?? 0, idleTimeout: 60, fetch: twinUiFetch(options) });
|
|
55
|
-
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
56
|
-
}
|
|
57
6
|
|
|
58
7
|
/** A hosted flow's page (docs/contributing/architecture.md, "Screens"): rendered on the server at the
|
|
59
8
|
* vendor's own host and path, its styles inline, so it needs no asset routes beside it. */
|
|
@@ -70,3 +19,4 @@ export { Consent, CONSENT_CSS, type ConsentPermission, type ConsentProps } from
|
|
|
70
19
|
export { FORMAT_SCRIPT, Payment, PAYMENT_CSS, type PaymentField, type PaymentProps } from '../client/payment.tsx';
|
|
71
20
|
export { Portal, PORTAL_CSS, type PortalAction, type PortalForm, type PortalItem, type PortalProps, type PortalSection } from '../client/portal.tsx';
|
|
72
21
|
export { cookieOf, SignIn, SignInCode, SIGN_IN_CSS, type SignInCodeProps, type SignInProps } from '../client/sign-in.tsx';
|
|
22
|
+
export { formOf, redirect, signedIn, startSession, toSignIn } from './session.ts';
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The session kit (docs/contributing/architecture.md, "Screens", "Who is on a screen"): how every dashboard a pack builds
|
|
2
|
+
// knows who is signed in, the same way in every pack. A person is the kernel's (`recordPerson`, `personWith`); signing
|
|
3
|
+
// in records a `_web_session` row and sets the vendor's session cookie on the answer; a page asks who the request's
|
|
4
|
+
// cookie names. Everything is written and read through the screen's context.
|
|
5
|
+
import { personOf, type HandlerContext, type Person } from '@volter/world-core';
|
|
6
|
+
import { cookieOf } from '../client/sign-in.tsx';
|
|
7
|
+
|
|
8
|
+
const SESSION = '_web_session';
|
|
9
|
+
|
|
10
|
+
/** A form a page posted, by field. */
|
|
11
|
+
export const formOf = (ctx: HandlerContext): Record<string, string> => Object.fromEntries(new URLSearchParams(ctx.text));
|
|
12
|
+
|
|
13
|
+
/** The person the request's session cookie names, or undefined when nobody is signed in; with `scope`, only a session
|
|
14
|
+
* begun in it (a vendor's sandbox dashboard and its production one sign a person in apart). */
|
|
15
|
+
export function signedIn(ctx: HandlerContext, cookie: string, scope?: string): Person | undefined {
|
|
16
|
+
const token = cookieOf(ctx.call.request, cookie);
|
|
17
|
+
const session = token ? ctx.rowsRaw(SESSION).find((s) => s.token === token && (scope === undefined || s.scope === scope)) : undefined;
|
|
18
|
+
return session ? personOf(ctx, String(session.email)) : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A redirect, setting a cookie when given one. */
|
|
22
|
+
export function redirect(location: string, cookie?: string): Response {
|
|
23
|
+
const headers = new Headers({ location });
|
|
24
|
+
if (cookie) headers.append('set-cookie', cookie);
|
|
25
|
+
return new Response(null, { status: 302, headers });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The person signed in: a session recorded (in `scope`, when the vendor signs a person in apart per dashboard), its
|
|
29
|
+
* cookie set, and the page they asked for next. */
|
|
30
|
+
export async function startSession(ctx: HandlerContext, email: string, cookie: string, next: string, scope?: string): Promise<Response> {
|
|
31
|
+
const issued = await ctx.record('_web_session_issue', { email, ...(scope !== undefined ? { scope } : {}) });
|
|
32
|
+
const token = await ctx.secret(`web-session:${issued}`);
|
|
33
|
+
await ctx.record(SESSION, { token, email, ...(scope !== undefined ? { scope } : {}), created_at: ctx.occurredAt }, `websession:${token}`);
|
|
34
|
+
return redirect(next, `${cookie}=${token}; path=/; HttpOnly; Secure; SameSite=Lax`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** To the vendor's sign-in page, which returns the person to `path` (its parameter the vendor's: `next`, `return_to`). */
|
|
38
|
+
export const toSignIn = (signIn: string, path: string, param = 'next'): Response => redirect(`${signIn}?${param}=${encodeURIComponent(path)}`);
|
package/client/kit.css
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/* The twin UI kit's base: layout and pieces. A pack's own stylesheet is its skin: it sets the
|
|
2
|
-
variables below (and anything else of the vendor's look) and is loaded after this one. Unskinned,
|
|
3
|
-
the kit wears the Volter brand's roles (company decision 0018): the shell links the brand's tokens
|
|
4
|
-
(assets/brand/tokens.css, fetched at build) before this sheet, and the pre-commit hook refuses a
|
|
5
|
-
literal colour here. */
|
|
6
|
-
:root {
|
|
7
|
-
--tw-accent: var(--volter-action-default);
|
|
8
|
-
--tw-bg: var(--volter-surface-page);
|
|
9
|
-
--tw-nav-bg: var(--volter-surface-subtle);
|
|
10
|
-
--tw-text: var(--volter-text-primary);
|
|
11
|
-
--tw-muted: var(--volter-text-muted);
|
|
12
|
-
--tw-border: var(--volter-border-default);
|
|
13
|
-
--tw-ok: var(--volter-status-healthy-text);
|
|
14
|
-
--tw-warn: var(--volter-status-attention-text);
|
|
15
|
-
--tw-bad: var(--volter-status-danger-text);
|
|
16
|
-
--tw-font: var(--volter-font-ui);
|
|
17
|
-
}
|
|
18
|
-
* { box-sizing: border-box; }
|
|
19
|
-
body { margin: 0; font-family: var(--tw-font); color: var(--tw-text); background: var(--tw-bg); }
|
|
20
|
-
button { font: inherit; }
|
|
21
|
-
.tw-app { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; }
|
|
22
|
-
.tw-nav { background: var(--tw-nav-bg); border-right: 1px solid var(--tw-border); padding: 12px 8px; display: flex; flex-direction: column; gap: 2px; overflow-y: auto; }
|
|
23
|
-
.tw-nav button { text-align: left; background: none; border: 0; padding: 6px 10px; border-radius: 6px; color: var(--tw-text); cursor: pointer; display: flex; justify-content: space-between; }
|
|
24
|
-
.tw-nav button:hover { background: var(--volter-overlay-hover); }
|
|
25
|
-
.tw-nav .tw-on, .tw-nav .tw-on:hover { background: var(--tw-accent); color: var(--volter-text-on-strong); }
|
|
26
|
-
.tw-brand { font-weight: 700; font-size: 16px; margin-bottom: 8px; flex-direction: column; }
|
|
27
|
-
.tw-brand small { font-weight: 400; color: var(--tw-muted); font-size: 12px; }
|
|
28
|
-
.tw-count { color: var(--tw-muted); font-size: 12px; margin-left: 8px; }
|
|
29
|
-
.tw-main { padding: 20px 28px; overflow: auto; }
|
|
30
|
-
.tw-main h1 { font-size: 20px; display: flex; align-items: center; gap: 8px; }
|
|
31
|
-
.tw-list { list-style: none; padding: 0; margin: 0; border: 1px solid var(--tw-border); border-radius: 6px; }
|
|
32
|
-
.tw-list li + li { border-top: 1px solid var(--tw-border); }
|
|
33
|
-
.tw-list button { width: 100%; background: none; border: 0; padding: 10px 14px; display: grid; grid-template-columns: 1fr 1fr auto; gap: 12px; text-align: left; cursor: pointer; }
|
|
34
|
-
.tw-list button:hover { background: var(--tw-nav-bg); }
|
|
35
|
-
.tw-list span { color: var(--tw-muted); }
|
|
36
|
-
.tw-empty { padding: 12px 14px; color: var(--tw-muted); }
|
|
37
|
-
.tw-pill { font-size: 12px; padding: 1px 8px; border-radius: 10px; border: 1px solid currentColor; white-space: nowrap; }
|
|
38
|
-
.tw-pill-ok { color: var(--tw-ok); }
|
|
39
|
-
.tw-pill-warn { color: var(--tw-warn); }
|
|
40
|
-
.tw-pill-bad { color: var(--tw-bad); }
|
|
41
|
-
.tw-pill-muted { color: var(--tw-muted); }
|
|
42
|
-
.tw-ref { font: inherit; font-size: 13px; background: none; border: 1px solid var(--tw-border); border-radius: 4px; padding: 0 6px; color: var(--tw-accent); cursor: pointer; }
|
|
43
|
-
.tw-fields { margin: 12px 0; }
|
|
44
|
-
.tw-field { display: grid; grid-template-columns: 220px 1fr; gap: 12px; padding: 3px 0; border-bottom: 1px dashed var(--tw-border); }
|
|
45
|
-
.tw-field dt { color: var(--tw-muted); }
|
|
46
|
-
.tw-field dd { margin: 0; word-break: break-all; }
|
|
47
|
-
.tw-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
|
|
48
|
-
.tw-actions button, .tw-create button { background: var(--tw-accent); color: var(--volter-text-on-strong); border: 0; border-radius: 6px; padding: 6px 12px; cursor: pointer; text-transform: capitalize; }
|
|
49
|
-
.tw-create { display: flex; gap: 8px; flex-wrap: wrap; align-items: end; margin-bottom: 12px; }
|
|
50
|
-
.tw-create label { display: flex; flex-direction: column; font-size: 12px; color: var(--tw-muted); }
|
|
51
|
-
.tw-create input { border: 1px solid var(--tw-border); border-radius: 6px; padding: 5px 8px; font: inherit; }
|
|
52
|
-
.tw-back { background: none; border: 0; color: var(--tw-accent); cursor: pointer; padding: 0; }
|
|
53
|
-
.tw-error { color: var(--tw-bad); width: 100%; }
|
|
54
|
-
.tw-home ul { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; }
|
|
55
|
-
.tw-home button { width: 100%; text-align: left; background: var(--tw-nav-bg); border: 1px solid var(--tw-border); border-radius: 6px; padding: 12px; cursor: pointer; }
|