@rsc-kit/press 0.0.0-stage → 0.30.42
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/README.md +117 -2
- package/dist/admin/actions.d.ts +30 -0
- package/dist/admin/actions.js +171 -0
- package/dist/admin/actions.js.map +1 -0
- package/dist/admin/index.d.ts +5 -0
- package/dist/admin/index.js +2 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/panel.d.ts +96 -0
- package/dist/admin/panel.js +354 -0
- package/dist/admin/panel.js.map +1 -0
- package/dist/admin/registry.d.ts +58 -0
- package/dist/admin/registry.js +90 -0
- package/dist/admin/registry.js.map +1 -0
- package/dist/admin/theme.d.ts +251 -0
- package/dist/admin/theme.js +2 -0
- package/dist/admin/theme.js.map +1 -0
- package/dist/admin/url.d.ts +24 -0
- package/dist/admin/url.js +45 -0
- package/dist/admin/url.js.map +1 -0
- package/dist/blocks.d.ts +46 -0
- package/dist/blocks.js +33 -0
- package/dist/blocks.js.map +1 -0
- package/dist/forms/actions.d.ts +7 -0
- package/dist/forms/actions.js +26 -0
- package/dist/forms/actions.js.map +1 -0
- package/dist/forms/form.d.ts +42 -0
- package/dist/forms/form.js +44 -0
- package/dist/forms/form.js.map +1 -0
- package/dist/forms/honeypot.d.ts +2 -0
- package/dist/forms/honeypot.js +3 -0
- package/dist/forms/honeypot.js.map +1 -0
- package/dist/forms/index.d.ts +3 -0
- package/dist/forms/index.js +2 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/forms/registry.d.ts +10 -0
- package/dist/forms/registry.js +17 -0
- package/dist/forms/registry.js.map +1 -0
- package/dist/forms/theme.d.ts +29 -0
- package/dist/forms/theme.js +2 -0
- package/dist/forms/theme.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/installable.d.ts +173 -0
- package/dist/installable.js +490 -0
- package/dist/installable.js.map +1 -0
- package/dist/memory.d.ts +7 -0
- package/dist/memory.js +50 -0
- package/dist/memory.js.map +1 -0
- package/dist/pages/index.d.ts +2 -0
- package/dist/pages/index.js +2 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/pages.d.ts +78 -0
- package/dist/pages/pages.js +183 -0
- package/dist/pages/pages.js.map +1 -0
- package/dist/plugin.d.ts +51 -0
- package/dist/plugin.js +20 -0
- package/dist/plugin.js.map +1 -0
- package/dist/resource.d.ts +320 -0
- package/dist/resource.js +415 -0
- package/dist/resource.js.map +1 -0
- package/dist/shadcn/breadcrumbs.d.ts +6 -0
- package/dist/shadcn/breadcrumbs.js +21 -0
- package/dist/shadcn/breadcrumbs.js.map +1 -0
- package/dist/shadcn/components/ui/alert-dialog.d.ts +18 -0
- package/dist/shadcn/components/ui/alert-dialog.js +44 -0
- package/dist/shadcn/components/ui/alert-dialog.js.map +1 -0
- package/dist/shadcn/components/ui/avatar.d.ts +11 -0
- package/dist/shadcn/components/ui/avatar.js +25 -0
- package/dist/shadcn/components/ui/avatar.js.map +1 -0
- package/dist/shadcn/components/ui/badge.d.ts +7 -0
- package/dist/shadcn/components/ui/badge.js +34 -0
- package/dist/shadcn/components/ui/badge.js.map +1 -0
- package/dist/shadcn/components/ui/breadcrumb.d.ts +10 -0
- package/dist/shadcn/components/ui/breadcrumb.js +38 -0
- package/dist/shadcn/components/ui/breadcrumb.js.map +1 -0
- package/dist/shadcn/components/ui/button.d.ts +8 -0
- package/dist/shadcn/components/ui/button.js +35 -0
- package/dist/shadcn/components/ui/button.js.map +1 -0
- package/dist/shadcn/components/ui/card.d.ts +11 -0
- package/dist/shadcn/components/ui/card.js +26 -0
- package/dist/shadcn/components/ui/card.js.map +1 -0
- package/dist/shadcn/components/ui/checkbox.d.ts +3 -0
- package/dist/shadcn/components/ui/checkbox.js +10 -0
- package/dist/shadcn/components/ui/checkbox.js.map +1 -0
- package/dist/shadcn/components/ui/dropdown-menu.d.ts +29 -0
- package/dist/shadcn/components/ui/dropdown-menu.js +53 -0
- package/dist/shadcn/components/ui/dropdown-menu.js.map +1 -0
- package/dist/shadcn/components/ui/field.d.ts +24 -0
- package/dist/shadcn/components/ui/field.js +69 -0
- package/dist/shadcn/components/ui/field.js.map +1 -0
- package/dist/shadcn/components/ui/input.d.ts +3 -0
- package/dist/shadcn/components/ui/input.js +9 -0
- package/dist/shadcn/components/ui/input.js.map +1 -0
- package/dist/shadcn/components/ui/label.d.ts +3 -0
- package/dist/shadcn/components/ui/label.js +9 -0
- package/dist/shadcn/components/ui/label.js.map +1 -0
- package/dist/shadcn/components/ui/select.d.ts +15 -0
- package/dist/shadcn/components/ui/select.js +36 -0
- package/dist/shadcn/components/ui/select.js.map +1 -0
- package/dist/shadcn/components/ui/separator.d.ts +3 -0
- package/dist/shadcn/components/ui/separator.js +9 -0
- package/dist/shadcn/components/ui/separator.js.map +1 -0
- package/dist/shadcn/components/ui/sheet.d.ts +14 -0
- package/dist/shadcn/components/ui/sheet.js +39 -0
- package/dist/shadcn/components/ui/sheet.js.map +1 -0
- package/dist/shadcn/components/ui/sidebar.d.ts +63 -0
- package/dist/shadcn/components/ui/sidebar.js +257 -0
- package/dist/shadcn/components/ui/sidebar.js.map +1 -0
- package/dist/shadcn/components/ui/skeleton.d.ts +2 -0
- package/dist/shadcn/components/ui/skeleton.js +7 -0
- package/dist/shadcn/components/ui/skeleton.js.map +1 -0
- package/dist/shadcn/components/ui/sonner.d.ts +3 -0
- package/dist/shadcn/components/ui/sonner.js +38 -0
- package/dist/shadcn/components/ui/sonner.js.map +1 -0
- package/dist/shadcn/components/ui/switch.d.ts +5 -0
- package/dist/shadcn/components/ui/switch.js +9 -0
- package/dist/shadcn/components/ui/switch.js.map +1 -0
- package/dist/shadcn/components/ui/table.d.ts +10 -0
- package/dist/shadcn/components/ui/table.js +30 -0
- package/dist/shadcn/components/ui/table.js.map +1 -0
- package/dist/shadcn/components/ui/textarea.d.ts +3 -0
- package/dist/shadcn/components/ui/textarea.js +8 -0
- package/dist/shadcn/components/ui/textarea.js.map +1 -0
- package/dist/shadcn/components/ui/tooltip.d.ts +6 -0
- package/dist/shadcn/components/ui/tooltip.js +18 -0
- package/dist/shadcn/components/ui/tooltip.js.map +1 -0
- package/dist/shadcn/data-table.d.ts +12 -0
- package/dist/shadcn/data-table.js +195 -0
- package/dist/shadcn/data-table.js.map +1 -0
- package/dist/shadcn/fields.d.ts +20 -0
- package/dist/shadcn/fields.js +179 -0
- package/dist/shadcn/fields.js.map +1 -0
- package/dist/shadcn/flash-toasts.d.ts +2 -0
- package/dist/shadcn/flash-toasts.js +11 -0
- package/dist/shadcn/flash-toasts.js.map +1 -0
- package/dist/shadcn/hooks/use-mobile.d.ts +1 -0
- package/dist/shadcn/hooks/use-mobile.js +16 -0
- package/dist/shadcn/hooks/use-mobile.js.map +1 -0
- package/dist/shadcn/index.d.ts +10 -0
- package/dist/shadcn/index.js +31 -0
- package/dist/shadcn/index.js.map +1 -0
- package/dist/shadcn/mode-toggle.d.ts +7 -0
- package/dist/shadcn/mode-toggle.js +52 -0
- package/dist/shadcn/mode-toggle.js.map +1 -0
- package/dist/shadcn/nav.d.ts +5 -0
- package/dist/shadcn/nav.js +19 -0
- package/dist/shadcn/nav.js.map +1 -0
- package/dist/shadcn/pages.d.ts +15 -0
- package/dist/shadcn/pages.js +59 -0
- package/dist/shadcn/pages.js.map +1 -0
- package/dist/shadcn/plugin-forms.d.ts +16 -0
- package/dist/shadcn/plugin-forms.js +25 -0
- package/dist/shadcn/plugin-forms.js.map +1 -0
- package/dist/shadcn/public-form.d.ts +8 -0
- package/dist/shadcn/public-form.js +26 -0
- package/dist/shadcn/public-form.js.map +1 -0
- package/dist/shadcn/resource-form.d.ts +19 -0
- package/dist/shadcn/resource-form.js +30 -0
- package/dist/shadcn/resource-form.js.map +1 -0
- package/dist/shadcn/styles.css +12 -0
- package/dist/shadcn/tenant-switcher.d.ts +3 -0
- package/dist/shadcn/tenant-switcher.js +17 -0
- package/dist/shadcn/tenant-switcher.js.map +1 -0
- package/dist/shadcn/theme-script.d.ts +7 -0
- package/dist/shadcn/theme-script.js +10 -0
- package/dist/shadcn/theme-script.js.map +1 -0
- package/dist/shadcn/user-menu.d.ts +3 -0
- package/dist/shadcn/user-menu.js +23 -0
- package/dist/shadcn/user-menu.js.map +1 -0
- package/dist/signing.d.ts +43 -0
- package/dist/signing.js +82 -0
- package/dist/signing.js.map +1 -0
- package/dist/tenant.d.ts +12 -0
- package/dist/tenant.js +29 -0
- package/dist/tenant.js.map +1 -0
- package/dist/uploads.d.ts +32 -0
- package/dist/uploads.js +28 -0
- package/dist/uploads.js.map +1 -0
- package/package.json +74 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/admin/registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { callPlugin, manifestProblems, publicHttps, resourcesOf, wantsChange } from '../installable.js'\nimport type { Installs } from '../installable.js'\nimport type { PanelPlugin, RecordChange } from '../plugin.js'\nimport { tell } from '../plugin.js'\nimport type { Resource, SourceContext, Tenant } from '../resource.js'\n\n/**\n * Tenants: whose records the panel is showing. The panel never stores which\n * one is current - the app already knows, in its session, a cookie, the\n * user's row - so it asks, and tells the app when the visitor switches.\n */\nexport interface Tenancy {\n /** What a tenant is called in the switcher: 'Team', 'Store', 'Workspace'. */\n label?: string\n /** The tenants this visitor belongs to. */\n tenants: () => Tenant[] | Promise<Tenant[]>\n /** The one they are working in. Null - none chosen, or no longer theirs - is a 404. */\n current: () => Tenant | null | Promise<Tenant | null>\n /** Remember the visitor's choice. Runs in a server action, so it may set a cookie. */\n switch: (id: string) => void | Promise<void>\n}\n\n/** What an action needs of a panel: its resources, who may use them, and whose rows they are. */\nexport interface PanelHandle {\n path: string\n resources: Resource[]\n authorize: () => boolean | Promise<boolean>\n plugins: PanelPlugin[]\n tenancy: Tenancy | null\n signOut: (() => void | Promise<void>) | null\n /** Plugins installed while it runs, or null when the panel takes none. */\n installs: Installs | null\n}\n\n/** Where a panel may send a plugin's calls: the app's rule, or https to a public name. */\nexport function allowOf(panel: PanelHandle): (url: URL) => boolean {\n return panel.installs?.allow ?? publicHttps\n}\n\n/**\n * The panel's resources for this request: its own, then each installed\n * plugin's for this tenant. A plugin whose manifest no longer fits - the app\n * renamed a resource it reads, or added one with its name - is left out and\n * said so, rather than failing every page.\n */\nexport async function resourcesFor(panel: PanelHandle, context: SourceContext): Promise<Resource[]> {\n if (!panel.installs) return panel.resources\n\n const host = panel.resources.map((resource) => resource.name)\n const installed: Resource[] = []\n const taken = new Set(host)\n\n for (const installation of await panel.installs.store.list(context.tenant)) {\n const problems = manifestProblems(installation.manifest, installation.manifestUrl, host)\n const names = installation.manifest.resources.map((resource) => resource.name)\n\n if (problems.length > 0 || names.some((name) => taken.has(name))) {\n console.error(`[admin] the installed plugin ${installation.manifest.name} is left out: ${problems.join('; ') || 'a resource name is taken'}`)\n continue\n }\n\n names.forEach((name) => taken.add(name))\n installed.push(...resourcesOf(installation, allowOf(panel)))\n }\n\n return [...panel.resources, ...installed]\n}\n\n/**\n * A change, told to the build-time plugins and to each installed plugin that\n * asked for it. An installed plugin is told within three seconds or not at\n * all: it is somebody else's server, and the person saving is not waiting\n * for it.\n */\nexport async function tellAll(panel: PanelHandle, change: RecordChange, context: SourceContext): Promise<void> {\n await tell(panel.plugins, change)\n\n if (!panel.installs) return\n\n const listening = (await panel.installs.store.list(context.tenant)).filter((installation) => wantsChange(installation, change))\n\n await Promise.all(\n listening.map((installation) =>\n callPlugin(installation, { type: 'change', change: { ...change, tenant: context.tenant } }, context, allowOf(panel), 3000).catch((error: unknown) =>\n console.error(`[admin] the installed plugin ${installation.manifest.name} was not told of ${change.change} ${change.resource} ${change.id}`, error),\n ),\n ),\n )\n}\n\n/**\n * Panels by path. An action receives a path, not a panel - only what can be\n * serialised crosses to the browser and back - so the panel is looked up here.\n * On globalThis because a dev server can evaluate this module more than once,\n * and a second copy of the map would hold no panels.\n */\nconst KEY = Symbol.for('@rsc-kit/press.panels')\nconst panels: Map<string, PanelHandle> = ((globalThis as Record<symbol, unknown>)[KEY] ??= new Map()) as Map<string, PanelHandle>\n\nexport function registerPanel(panel: PanelHandle): void {\n panels.set(panel.path, panel)\n}\n\nexport function panelAt(path: string): PanelHandle {\n const panel = panels.get(path)\n\n if (!panel) throw new Error(`[admin] no panel at ${path}; definePanel({ path: ${JSON.stringify(path)} }) has not run`)\n\n return panel\n}\n\n/**\n * Who is asking, as a data source is told. In a panel with tenants, the\n * current tenant must be one of the visitor's - an app whose current() reads\n * a cookie would otherwise hand anyone the tenant the cookie names.\n */\nexport async function contextOf(panel: PanelHandle): Promise<SourceContext | null> {\n if (!panel.tenancy) return { tenant: null }\n\n const tenant = await panel.tenancy.current()\n\n if (!tenant) return null\n\n const theirs = await panel.tenancy.tenants()\n\n return theirs.some((candidate) => candidate.id === tenant.id) ? { tenant } : null\n}\n\n/** The resource and its context, once the visitor is allowed into the panel - and allowed to change it. */\nexport async function allowedResource(path: string, name: string): Promise<{ resource: Resource; context: SourceContext }> {\n const panel = panelAt(path)\n\n if (!(await panel.authorize())) throw new Error('[admin] not allowed')\n\n const context = await contextOf(panel)\n\n if (!context) throw new Error('[admin] no tenant chosen, or not one of this visitor\\'s')\n\n const resource = (await resourcesFor(panel, context)).find((candidate) => candidate.name === name)\n\n if (!resource) throw new Error(`[admin] no resource ${name} in the panel at ${path}`)\n\n if (resource.readOnly) throw new Error(`[admin] ${name} is read-only`)\n\n return { resource, context }\n}\n"]}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
+
import type { Field, Filter, Format, TableMode, Tenant } from '../resource.js';
|
|
3
|
+
/**
|
|
4
|
+
* What a theme draws for an admin panel. The panel decides everything else:
|
|
5
|
+
* who may see a page, which rows, which tenant, what a button does. A theme
|
|
6
|
+
* is handed the result as props and draws it.
|
|
7
|
+
*
|
|
8
|
+
* Every prop is data, a server action already bound to its record, or an
|
|
9
|
+
* element already drawn - never a component. A theme's component may be a
|
|
10
|
+
* client component, and a component cannot cross to the browser; the
|
|
11
|
+
* element it draws can.
|
|
12
|
+
*
|
|
13
|
+
* The panel owns the Suspense boundaries, because it knows which parts read
|
|
14
|
+
* the request; a theme draws what shows meanwhile, as `Loading`.
|
|
15
|
+
*/
|
|
16
|
+
export interface AdminTheme {
|
|
17
|
+
/** The frame every page sits in: navigation, header, the page itself. */
|
|
18
|
+
Shell: ComponentType<ShellProps>;
|
|
19
|
+
/** The panel's name and mark, where a panel with tenants has its switcher. */
|
|
20
|
+
Brand: ComponentType<BrandProps>;
|
|
21
|
+
TenantSwitcher: ComponentType<TenantSwitcherProps>;
|
|
22
|
+
/** The panel's links, lit up for the page the visitor is on - which only the browser knows. */
|
|
23
|
+
Nav: ComponentType<{
|
|
24
|
+
items: NavItem[];
|
|
25
|
+
}>;
|
|
26
|
+
/** Where the visitor is, read from the url: Admin / Products / Edit. */
|
|
27
|
+
Breadcrumbs: ComponentType<BreadcrumbsProps>;
|
|
28
|
+
UserMenu: ComponentType<UserMenuProps>;
|
|
29
|
+
Dashboard: ComponentType<DashboardProps>;
|
|
30
|
+
/** A resource's list page around its table. */
|
|
31
|
+
Index: ComponentType<IndexProps>;
|
|
32
|
+
/** A resource's rows: searching, sorting, paging, filters and bulk actions, in either mode. */
|
|
33
|
+
Table: ComponentType<TableProps>;
|
|
34
|
+
/** A create or edit page, and its form. */
|
|
35
|
+
Form: ComponentType<FormProps>;
|
|
36
|
+
Plugins: ComponentType<PluginsProps>;
|
|
37
|
+
/** A value in a column with a format: money, a badge, yes or no, a date. A column's own cell() is drawn instead. */
|
|
38
|
+
Cell: ComponentType<{
|
|
39
|
+
format: Exclude<Format, 'text' | 'relation'>;
|
|
40
|
+
value: unknown;
|
|
41
|
+
}>;
|
|
42
|
+
/** What shows while a page, or a part of the shell, waits for the request. */
|
|
43
|
+
Loading: ComponentType<{
|
|
44
|
+
kind: 'dashboard' | 'table' | 'form' | 'menu';
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
export interface ShellProps {
|
|
48
|
+
title: string;
|
|
49
|
+
/** The Brand, or the TenantSwitcher. */
|
|
50
|
+
brand: ReactNode;
|
|
51
|
+
/** The Nav. */
|
|
52
|
+
nav: ReactNode;
|
|
53
|
+
/** The UserMenu, in a panel that knows who is signed in. */
|
|
54
|
+
user: ReactNode | null;
|
|
55
|
+
/** The Breadcrumbs. */
|
|
56
|
+
crumbs: ReactNode;
|
|
57
|
+
/** The light and dark switch: off when the app has its own. */
|
|
58
|
+
modeToggle: boolean;
|
|
59
|
+
children: ReactNode;
|
|
60
|
+
}
|
|
61
|
+
export interface BrandProps {
|
|
62
|
+
href: string;
|
|
63
|
+
title: string;
|
|
64
|
+
/** The app's mark, drawn; absent when the panel was given none. */
|
|
65
|
+
logo?: ReactNode;
|
|
66
|
+
}
|
|
67
|
+
export interface TenantSwitcherProps {
|
|
68
|
+
tenants: Tenant[];
|
|
69
|
+
current: Tenant;
|
|
70
|
+
/** What the tenants are called: Teams. */
|
|
71
|
+
label: string;
|
|
72
|
+
choose: (id: string) => Promise<unknown>;
|
|
73
|
+
}
|
|
74
|
+
export interface NavItem {
|
|
75
|
+
href: string;
|
|
76
|
+
label: string;
|
|
77
|
+
/**
|
|
78
|
+
* What the link leads to, for the theme's default icon. `installed` is a
|
|
79
|
+
* resource a plugin installed while the panel ran.
|
|
80
|
+
*/
|
|
81
|
+
kind: 'dashboard' | 'resource' | 'installed' | 'plugins';
|
|
82
|
+
/** The resource's own icon, drawn. */
|
|
83
|
+
icon?: ReactNode;
|
|
84
|
+
/** Exact for the dashboard, so it is not lit up on every page below it. */
|
|
85
|
+
exact?: boolean;
|
|
86
|
+
}
|
|
87
|
+
export interface BreadcrumbsProps {
|
|
88
|
+
/** The panel's path. */
|
|
89
|
+
root: string;
|
|
90
|
+
title: string;
|
|
91
|
+
/** Each first segment's label: { products: 'Products' }. */
|
|
92
|
+
labels: Record<string, string>;
|
|
93
|
+
}
|
|
94
|
+
export interface PanelUser {
|
|
95
|
+
name: string;
|
|
96
|
+
email?: string;
|
|
97
|
+
avatar?: string;
|
|
98
|
+
}
|
|
99
|
+
export interface UserMenuProps {
|
|
100
|
+
user: PanelUser;
|
|
101
|
+
signOut?: () => Promise<unknown>;
|
|
102
|
+
}
|
|
103
|
+
export interface DashboardProps {
|
|
104
|
+
description: string;
|
|
105
|
+
counts: {
|
|
106
|
+
name: string;
|
|
107
|
+
label: string;
|
|
108
|
+
href: string;
|
|
109
|
+
/** Null when the source could not be counted: an installed plugin's server that is down. */
|
|
110
|
+
total: number | null;
|
|
111
|
+
icon?: ReactNode;
|
|
112
|
+
}[];
|
|
113
|
+
/** Each plugin widget, already rendered for this tenant. */
|
|
114
|
+
widgets: {
|
|
115
|
+
key: string;
|
|
116
|
+
title: string;
|
|
117
|
+
description?: string;
|
|
118
|
+
content: ReactNode;
|
|
119
|
+
}[];
|
|
120
|
+
}
|
|
121
|
+
export interface IndexProps {
|
|
122
|
+
title: string;
|
|
123
|
+
description?: string;
|
|
124
|
+
/** The new-record link, absent for a read-only resource. */
|
|
125
|
+
create?: {
|
|
126
|
+
href: string;
|
|
127
|
+
label: string;
|
|
128
|
+
};
|
|
129
|
+
/** The Table, as a section the panel refreshes on its own. */
|
|
130
|
+
table: ReactNode;
|
|
131
|
+
}
|
|
132
|
+
/** A column as the browser needs it: what to call it, and what it may do. The cells come drawn. */
|
|
133
|
+
export interface TableColumn {
|
|
134
|
+
key: string;
|
|
135
|
+
label: string;
|
|
136
|
+
sortable: boolean;
|
|
137
|
+
searchable: boolean;
|
|
138
|
+
visible: boolean;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A row, drawn on the server: each cell is already the element it shows -
|
|
142
|
+
* a money format, a badge, a column's own cell() - and `values` are the raw
|
|
143
|
+
* values the browser sorts and searches by in a client-mode table.
|
|
144
|
+
*/
|
|
145
|
+
export interface TableRowData {
|
|
146
|
+
id: string;
|
|
147
|
+
cells: Record<string, ReactNode>;
|
|
148
|
+
values: Record<string, string | number | boolean | null>;
|
|
149
|
+
edit?: string;
|
|
150
|
+
remove?: () => Promise<unknown>;
|
|
151
|
+
}
|
|
152
|
+
/** A bulk action as the table offers it; the work is the server's. */
|
|
153
|
+
export interface TableBulkAction {
|
|
154
|
+
name: string;
|
|
155
|
+
label: string;
|
|
156
|
+
destructive: boolean;
|
|
157
|
+
}
|
|
158
|
+
/** What a server-mode table is showing: the url's state, as the page read it. */
|
|
159
|
+
export interface TableQuery {
|
|
160
|
+
q: string;
|
|
161
|
+
sort?: string;
|
|
162
|
+
dir: 'asc' | 'desc';
|
|
163
|
+
page: number;
|
|
164
|
+
filters: Record<string, string>;
|
|
165
|
+
}
|
|
166
|
+
export interface TableProps {
|
|
167
|
+
/**
|
|
168
|
+
* server the url is the state. Typing, a filter, a header and the pager
|
|
169
|
+
* each visit a url, the page asks the source for one page, and a
|
|
170
|
+
* link works with no javascript and can be shared.
|
|
171
|
+
* client every row arrived at once; search, filters, sort and the pager
|
|
172
|
+
* happen in the browser, with no request.
|
|
173
|
+
*/
|
|
174
|
+
mode: TableMode;
|
|
175
|
+
/** The list's url, which a server-mode table visits with its state. */
|
|
176
|
+
href: string;
|
|
177
|
+
columns: TableColumn[];
|
|
178
|
+
rows: TableRowData[];
|
|
179
|
+
total: number;
|
|
180
|
+
perPage: number;
|
|
181
|
+
query: TableQuery;
|
|
182
|
+
filters: Filter[];
|
|
183
|
+
noun: {
|
|
184
|
+
one: string;
|
|
185
|
+
many: string;
|
|
186
|
+
};
|
|
187
|
+
/** Ticking rows, and what can be done to them. Absent for a read-only resource. */
|
|
188
|
+
bulk?: {
|
|
189
|
+
actions: TableBulkAction[];
|
|
190
|
+
run: (action: string, ids: string[]) => Promise<unknown>;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export interface FormProps {
|
|
194
|
+
title: string;
|
|
195
|
+
description: string;
|
|
196
|
+
/** Saves the record. A refusal is a 422 whose messages belong under their fields. */
|
|
197
|
+
action: (form: FormData) => Promise<unknown>;
|
|
198
|
+
/** Each field, a relation's with its choices. */
|
|
199
|
+
fields: Field[];
|
|
200
|
+
/** The record's values; empty on a create page. */
|
|
201
|
+
values: Record<string, unknown>;
|
|
202
|
+
/** The submit button's words: Create product, Save changes. */
|
|
203
|
+
submit: string;
|
|
204
|
+
/** Where cancelling goes. */
|
|
205
|
+
cancel: string;
|
|
206
|
+
}
|
|
207
|
+
export interface PluginsProps {
|
|
208
|
+
description: string;
|
|
209
|
+
/** Whether this visitor may install and uninstall; one who may not sees what is installed. */
|
|
210
|
+
allowed: boolean;
|
|
211
|
+
/** Reads a manifest url, sent as the field `manifest`, and puts it under review. */
|
|
212
|
+
review: (form: FormData) => Promise<unknown>;
|
|
213
|
+
/** A manifest under review, or why it cannot be installed. */
|
|
214
|
+
reviewing: null | {
|
|
215
|
+
problem: string;
|
|
216
|
+
} | PluginReview;
|
|
217
|
+
installed: InstalledPlugin[];
|
|
218
|
+
}
|
|
219
|
+
export interface PluginReview {
|
|
220
|
+
label: string;
|
|
221
|
+
version?: string;
|
|
222
|
+
/** Its description, or its server's host. */
|
|
223
|
+
description: string;
|
|
224
|
+
/** The labels of the resources it adds. */
|
|
225
|
+
adds: string[];
|
|
226
|
+
/** What it may reach, in words; empty when it reaches nothing beyond its own records. */
|
|
227
|
+
can: string[];
|
|
228
|
+
endpoint: string;
|
|
229
|
+
install: () => Promise<unknown>;
|
|
230
|
+
/** Where cancelling the review goes. */
|
|
231
|
+
cancel: string;
|
|
232
|
+
}
|
|
233
|
+
export interface InstalledPlugin {
|
|
234
|
+
id: string;
|
|
235
|
+
label: string;
|
|
236
|
+
description: string;
|
|
237
|
+
/** The labels of the resources it added. */
|
|
238
|
+
resources: string[];
|
|
239
|
+
scopes: string[];
|
|
240
|
+
/** Absent for a visitor who may not uninstall. */
|
|
241
|
+
uninstall?: () => Promise<unknown>;
|
|
242
|
+
/**
|
|
243
|
+
* What the plugin needs to tell this panel's calls from anyone else's,
|
|
244
|
+
* shown until someone confirms they copied it.
|
|
245
|
+
*/
|
|
246
|
+
secret?: {
|
|
247
|
+
installation: string;
|
|
248
|
+
secret: string;
|
|
249
|
+
copied: () => Promise<unknown>;
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/admin/theme.ts"],"names":[],"mappings":"","sourcesContent":["import type { ComponentType, ReactNode } from 'react'\nimport type { Field, Filter, Format, TableMode, Tenant } from '../resource.js'\n\n/**\n * What a theme draws for an admin panel. The panel decides everything else:\n * who may see a page, which rows, which tenant, what a button does. A theme\n * is handed the result as props and draws it.\n *\n * Every prop is data, a server action already bound to its record, or an\n * element already drawn - never a component. A theme's component may be a\n * client component, and a component cannot cross to the browser; the\n * element it draws can.\n *\n * The panel owns the Suspense boundaries, because it knows which parts read\n * the request; a theme draws what shows meanwhile, as `Loading`.\n */\nexport interface AdminTheme {\n /** The frame every page sits in: navigation, header, the page itself. */\n Shell: ComponentType<ShellProps>\n /** The panel's name and mark, where a panel with tenants has its switcher. */\n Brand: ComponentType<BrandProps>\n TenantSwitcher: ComponentType<TenantSwitcherProps>\n /** The panel's links, lit up for the page the visitor is on - which only the browser knows. */\n Nav: ComponentType<{ items: NavItem[] }>\n /** Where the visitor is, read from the url: Admin / Products / Edit. */\n Breadcrumbs: ComponentType<BreadcrumbsProps>\n UserMenu: ComponentType<UserMenuProps>\n\n Dashboard: ComponentType<DashboardProps>\n /** A resource's list page around its table. */\n Index: ComponentType<IndexProps>\n /** A resource's rows: searching, sorting, paging, filters and bulk actions, in either mode. */\n Table: ComponentType<TableProps>\n /** A create or edit page, and its form. */\n Form: ComponentType<FormProps>\n Plugins: ComponentType<PluginsProps>\n\n /** A value in a column with a format: money, a badge, yes or no, a date. A column's own cell() is drawn instead. */\n Cell: ComponentType<{ format: Exclude<Format, 'text' | 'relation'>; value: unknown }>\n /** What shows while a page, or a part of the shell, waits for the request. */\n Loading: ComponentType<{ kind: 'dashboard' | 'table' | 'form' | 'menu' }>\n}\n\nexport interface ShellProps {\n title: string\n /** The Brand, or the TenantSwitcher. */\n brand: ReactNode\n /** The Nav. */\n nav: ReactNode\n /** The UserMenu, in a panel that knows who is signed in. */\n user: ReactNode | null\n /** The Breadcrumbs. */\n crumbs: ReactNode\n /** The light and dark switch: off when the app has its own. */\n modeToggle: boolean\n children: ReactNode\n}\n\nexport interface BrandProps {\n href: string\n title: string\n /** The app's mark, drawn; absent when the panel was given none. */\n logo?: ReactNode\n}\n\nexport interface TenantSwitcherProps {\n tenants: Tenant[]\n current: Tenant\n /** What the tenants are called: Teams. */\n label: string\n choose: (id: string) => Promise<unknown>\n}\n\nexport interface NavItem {\n href: string\n label: string\n /**\n * What the link leads to, for the theme's default icon. `installed` is a\n * resource a plugin installed while the panel ran.\n */\n kind: 'dashboard' | 'resource' | 'installed' | 'plugins'\n /** The resource's own icon, drawn. */\n icon?: ReactNode\n /** Exact for the dashboard, so it is not lit up on every page below it. */\n exact?: boolean\n}\n\nexport interface BreadcrumbsProps {\n /** The panel's path. */\n root: string\n title: string\n /** Each first segment's label: { products: 'Products' }. */\n labels: Record<string, string>\n}\n\nexport interface PanelUser {\n name: string\n email?: string\n avatar?: string\n}\n\nexport interface UserMenuProps {\n user: PanelUser\n signOut?: () => Promise<unknown>\n}\n\nexport interface DashboardProps {\n description: string\n counts: {\n name: string\n label: string\n href: string\n /** Null when the source could not be counted: an installed plugin's server that is down. */\n total: number | null\n icon?: ReactNode\n }[]\n /** Each plugin widget, already rendered for this tenant. */\n widgets: { key: string; title: string; description?: string; content: ReactNode }[]\n}\n\nexport interface IndexProps {\n title: string\n description?: string\n /** The new-record link, absent for a read-only resource. */\n create?: { href: string; label: string }\n /** The Table, as a section the panel refreshes on its own. */\n table: ReactNode\n}\n\n/** A column as the browser needs it: what to call it, and what it may do. The cells come drawn. */\nexport interface TableColumn {\n key: string\n label: string\n sortable: boolean\n searchable: boolean\n visible: boolean\n}\n\n/**\n * A row, drawn on the server: each cell is already the element it shows -\n * a money format, a badge, a column's own cell() - and `values` are the raw\n * values the browser sorts and searches by in a client-mode table.\n */\nexport interface TableRowData {\n id: string\n cells: Record<string, ReactNode>\n values: Record<string, string | number | boolean | null>\n edit?: string\n remove?: () => Promise<unknown>\n}\n\n/** A bulk action as the table offers it; the work is the server's. */\nexport interface TableBulkAction {\n name: string\n label: string\n destructive: boolean\n}\n\n/** What a server-mode table is showing: the url's state, as the page read it. */\nexport interface TableQuery {\n q: string\n sort?: string\n dir: 'asc' | 'desc'\n page: number\n filters: Record<string, string>\n}\n\nexport interface TableProps {\n /**\n * server the url is the state. Typing, a filter, a header and the pager\n * each visit a url, the page asks the source for one page, and a\n * link works with no javascript and can be shared.\n * client every row arrived at once; search, filters, sort and the pager\n * happen in the browser, with no request.\n */\n mode: TableMode\n /** The list's url, which a server-mode table visits with its state. */\n href: string\n columns: TableColumn[]\n rows: TableRowData[]\n total: number\n perPage: number\n query: TableQuery\n filters: Filter[]\n noun: { one: string; many: string }\n /** Ticking rows, and what can be done to them. Absent for a read-only resource. */\n bulk?: { actions: TableBulkAction[]; run: (action: string, ids: string[]) => Promise<unknown> }\n}\n\nexport interface FormProps {\n title: string\n description: string\n /** Saves the record. A refusal is a 422 whose messages belong under their fields. */\n action: (form: FormData) => Promise<unknown>\n /** Each field, a relation's with its choices. */\n fields: Field[]\n /** The record's values; empty on a create page. */\n values: Record<string, unknown>\n /** The submit button's words: Create product, Save changes. */\n submit: string\n /** Where cancelling goes. */\n cancel: string\n}\n\nexport interface PluginsProps {\n description: string\n /** Whether this visitor may install and uninstall; one who may not sees what is installed. */\n allowed: boolean\n /** Reads a manifest url, sent as the field `manifest`, and puts it under review. */\n review: (form: FormData) => Promise<unknown>\n /** A manifest under review, or why it cannot be installed. */\n reviewing: null | { problem: string } | PluginReview\n installed: InstalledPlugin[]\n}\n\nexport interface PluginReview {\n label: string\n version?: string\n /** Its description, or its server's host. */\n description: string\n /** The labels of the resources it adds. */\n adds: string[]\n /** What it may reach, in words; empty when it reaches nothing beyond its own records. */\n can: string[]\n endpoint: string\n install: () => Promise<unknown>\n /** Where cancelling the review goes. */\n cancel: string\n}\n\nexport interface InstalledPlugin {\n id: string\n label: string\n description: string\n /** The labels of the resources it added. */\n resources: string[]\n scopes: string[]\n /** Absent for a visitor who may not uninstall. */\n uninstall?: () => Promise<unknown>\n /**\n * What the plugin needs to tell this panel's calls from anyone else's,\n * shown until someone confirms they copied it.\n */\n secret?: { installation: string; secret: string; copied: () => Promise<unknown> }\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The url schemas a panel's route files export. Standard Schemas written by
|
|
3
|
+
* hand, so the panel depends on no validation library: the engine reads a
|
|
4
|
+
* page's params and searchParams through `~standard`, whichever made it.
|
|
5
|
+
*/
|
|
6
|
+
import type { StandardSchemaV1 } from '@rsc-kit/core/standardSchema';
|
|
7
|
+
export type ListSearch = {
|
|
8
|
+
q?: string;
|
|
9
|
+
sort?: string;
|
|
10
|
+
dir: 'asc' | 'desc';
|
|
11
|
+
page: number;
|
|
12
|
+
/** A filter: any other key. */
|
|
13
|
+
[filter: string]: string | number | undefined;
|
|
14
|
+
};
|
|
15
|
+
/** A resource's list page reads these from its url. A filter is any other key. */
|
|
16
|
+
export declare const listSearch: StandardSchemaV1<Record<string, unknown>, ListSearch>;
|
|
17
|
+
export declare const resourceParams: StandardSchemaV1<Record<string, unknown>, Record<"resource", string>>;
|
|
18
|
+
export declare const recordParams: StandardSchemaV1<Record<string, unknown>, Record<"id" | "resource", string>>;
|
|
19
|
+
export type PluginsSearch = {
|
|
20
|
+
/** A manifest url under review. */
|
|
21
|
+
review?: string;
|
|
22
|
+
};
|
|
23
|
+
/** The plugins page reads the manifest it is reviewing from its url. */
|
|
24
|
+
export declare const pluginsSearch: StandardSchemaV1<Record<string, unknown>, PluginsSearch>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function schema(validate) {
|
|
2
|
+
return {
|
|
3
|
+
'~standard': {
|
|
4
|
+
version: 1,
|
|
5
|
+
vendor: '@rsc-kit/press',
|
|
6
|
+
validate: (value) => typeof value === 'object' && value !== null ? validate(value) : { issues: [{ message: 'Expected an object', path: [] }] },
|
|
7
|
+
types: undefined,
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const text = (value) => (typeof value === 'string' ? value : Array.isArray(value) && typeof value[0] === 'string' ? value[0] : undefined);
|
|
12
|
+
/** A resource's list page reads these from its url. A filter is any other key. */
|
|
13
|
+
export const listSearch = schema((value) => {
|
|
14
|
+
const out = { dir: 'asc', page: 1 };
|
|
15
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
16
|
+
const given = text(raw);
|
|
17
|
+
if (given === undefined)
|
|
18
|
+
continue;
|
|
19
|
+
if (key === 'dir')
|
|
20
|
+
out.dir = given === 'desc' ? 'desc' : 'asc';
|
|
21
|
+
else if (key === 'page') {
|
|
22
|
+
const page = Number(given);
|
|
23
|
+
// A page that is not one is the first, rather than a 400: an old link
|
|
24
|
+
// or a hand-edited url should still show the table.
|
|
25
|
+
out.page = Number.isInteger(page) && page >= 1 ? page : 1;
|
|
26
|
+
}
|
|
27
|
+
else
|
|
28
|
+
out[key] = given;
|
|
29
|
+
}
|
|
30
|
+
return { value: out };
|
|
31
|
+
});
|
|
32
|
+
const params = (...keys) => schema((value) => {
|
|
33
|
+
const missing = keys.filter((key) => text(value[key]) === undefined);
|
|
34
|
+
if (missing.length)
|
|
35
|
+
return { issues: missing.map((key) => ({ message: 'Required', path: [key] })) };
|
|
36
|
+
return { value: Object.fromEntries(keys.map((key) => [key, text(value[key])])) };
|
|
37
|
+
});
|
|
38
|
+
export const resourceParams = params('resource');
|
|
39
|
+
export const recordParams = params('resource', 'id');
|
|
40
|
+
/** The plugins page reads the manifest it is reviewing from its url. */
|
|
41
|
+
export const pluginsSearch = schema((value) => {
|
|
42
|
+
const review = text(value.review);
|
|
43
|
+
return { value: review ? { review } : {} };
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.js","sourceRoot":"","sources":["../../src/admin/url.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The url schemas a panel's route files export. Standard Schemas written by\n * hand, so the panel depends on no validation library: the engine reads a\n * page's params and searchParams through `~standard`, whichever made it.\n */\nimport type { StandardSchemaV1 } from '@rsc-kit/core/standardSchema'\n\ntype Result<T> = { value: T } | { issues: { message: string; path: string[] }[] }\n\nfunction schema<T>(validate: (value: Record<string, unknown>) => Result<T>): StandardSchemaV1<Record<string, unknown>, T> {\n return {\n '~standard': {\n version: 1,\n vendor: '@rsc-kit/press',\n validate: (value) =>\n typeof value === 'object' && value !== null ? validate(value as Record<string, unknown>) : { issues: [{ message: 'Expected an object', path: [] }] },\n types: undefined as never,\n },\n }\n}\n\nconst text = (value: unknown) => (typeof value === 'string' ? value : Array.isArray(value) && typeof value[0] === 'string' ? value[0] : undefined)\n\nexport type ListSearch = {\n q?: string\n sort?: string\n dir: 'asc' | 'desc'\n page: number\n /** A filter: any other key. */\n [filter: string]: string | number | undefined\n}\n\n/** A resource's list page reads these from its url. A filter is any other key. */\nexport const listSearch = schema<ListSearch>((value) => {\n const out: ListSearch = { dir: 'asc', page: 1 }\n\n for (const [key, raw] of Object.entries(value)) {\n const given = text(raw)\n\n if (given === undefined) continue\n\n if (key === 'dir') out.dir = given === 'desc' ? 'desc' : 'asc'\n else if (key === 'page') {\n const page = Number(given)\n\n // A page that is not one is the first, rather than a 400: an old link\n // or a hand-edited url should still show the table.\n out.page = Number.isInteger(page) && page >= 1 ? page : 1\n } else out[key] = given\n }\n\n return { value: out }\n})\n\nconst params = <K extends string>(...keys: K[]) =>\n schema<Record<K, string>>((value) => {\n const missing = keys.filter((key) => text(value[key]) === undefined)\n\n if (missing.length) return { issues: missing.map((key) => ({ message: 'Required', path: [key] })) }\n\n return { value: Object.fromEntries(keys.map((key) => [key, text(value[key])!])) as Record<K, string> }\n })\n\nexport const resourceParams = params('resource')\n\nexport const recordParams = params('resource', 'id')\n\nexport type PluginsSearch = {\n /** A manifest url under review. */\n review?: string\n}\n\n/** The plugins page reads the manifest it is reviewing from its url. */\nexport const pluginsSearch = schema<PluginsSearch>((value) => {\n const review = text(value.review)\n\n return { value: review ? { review } : {} }\n})\n"]}
|
package/dist/blocks.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { Field, FieldOverride, FormSchema, ValuesOf } from './resource.js';
|
|
3
|
+
/**
|
|
4
|
+
* A kind of content a page is built from - a hero, a feature grid, a call to
|
|
5
|
+
* action - as a schema for what an editor fills in, and a component that
|
|
6
|
+
* draws it. The component is the app's: a block looks like the site it is on.
|
|
7
|
+
*/
|
|
8
|
+
export interface Block<Schema extends FormSchema = FormSchema> {
|
|
9
|
+
name: string;
|
|
10
|
+
label: string;
|
|
11
|
+
schema: Schema;
|
|
12
|
+
fields: Field[];
|
|
13
|
+
render: ComponentType<{
|
|
14
|
+
data: ValuesOf<Schema>;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export interface BlockDefinition<Schema extends FormSchema> {
|
|
18
|
+
/** What it is saved as: lowercase letters, digits and dashes. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** What an editor picks it by. Humanized from the name by default. */
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Any Standard Schema of an object. Its fields are what an editor fills in. */
|
|
23
|
+
form: Schema;
|
|
24
|
+
fields?: Partial<Record<keyof ValuesOf<Schema> & string, FieldOverride>>;
|
|
25
|
+
/** Draws it, given what passed the schema. May be an async server component. */
|
|
26
|
+
render: ComponentType<{
|
|
27
|
+
data: ValuesOf<Schema>;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
/** One block as it is saved: which kind, and what was filled in. */
|
|
31
|
+
export interface BlockValue {
|
|
32
|
+
type: string;
|
|
33
|
+
data: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
export declare function defineBlock<Schema extends FormSchema>(definition: BlockDefinition<Schema>): Block<Schema>;
|
|
36
|
+
/**
|
|
37
|
+
* Saved blocks, drawn in order. A block whose kind is no longer defined is
|
|
38
|
+
* left out rather than failing the page: content outlives the code that drew
|
|
39
|
+
* it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function Blocks({ blocks, value }: {
|
|
42
|
+
blocks: Block<any>[];
|
|
43
|
+
value: unknown;
|
|
44
|
+
}): (import("react").ReactElement<{
|
|
45
|
+
data: any;
|
|
46
|
+
}, string | import("react").JSXElementConstructor<any>> | null)[];
|
package/dist/blocks.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { fieldsOf, humanize } from './resource.js';
|
|
3
|
+
export function defineBlock(definition) {
|
|
4
|
+
if (!/^[a-z0-9-]+$/.test(definition.name)) {
|
|
5
|
+
throw new Error(`[press] a block's name is lowercase letters, digits and dashes: ${JSON.stringify(definition.name)}`);
|
|
6
|
+
}
|
|
7
|
+
const fields = fieldsOf(definition.form, definition.fields ?? {});
|
|
8
|
+
if (fields.some((field) => field.type === 'blocks')) {
|
|
9
|
+
throw new Error(`[press] the block ${definition.name} has a blocks field; blocks do not nest`);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
name: definition.name,
|
|
13
|
+
label: definition.label ?? humanize(definition.name),
|
|
14
|
+
schema: definition.form,
|
|
15
|
+
fields,
|
|
16
|
+
render: definition.render,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Saved blocks, drawn in order. A block whose kind is no longer defined is
|
|
21
|
+
* left out rather than failing the page: content outlives the code that drew
|
|
22
|
+
* it.
|
|
23
|
+
*/
|
|
24
|
+
export function Blocks({ blocks, value }) {
|
|
25
|
+
const list = Array.isArray(value) ? value : [];
|
|
26
|
+
return list.map((item, index) => {
|
|
27
|
+
const block = blocks.find((candidate) => candidate.name === item?.type);
|
|
28
|
+
if (!block)
|
|
29
|
+
return null;
|
|
30
|
+
return createElement(block.render, { key: index, data: item.data });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=blocks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocks.js","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAErC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAkClD,MAAM,UAAU,WAAW,CAA4B,UAAmC;IACxF,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,mEAAmE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACvH,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,MAAM,IAAI,EAAE,CAAC,CAAA;IAEjE,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,qBAAqB,UAAU,CAAC,IAAI,yCAAyC,CAAC,CAAA;IAChG,CAAC;IAED,OAAO;QACL,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,KAAK,EAAE,UAAU,CAAC,KAAK,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC;QACpD,MAAM,EAAE,UAAU,CAAC,IAAI;QACvB,MAAM;QACN,MAAM,EAAE,UAAU,CAAC,MAAM;KAC1B,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAA4C;IAChF,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAAsB,CAAC,CAAC,CAAC,EAAE,CAAA;IAEhE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC9B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,IAAI,EAAE,IAAI,CAAC,CAAA;QAEvE,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAA;QAEvB,OAAO,aAAa,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IACrE,CAAC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { createElement } from 'react'\nimport type { ComponentType } from 'react'\nimport { fieldsOf, humanize } from './resource.js'\nimport type { Field, FieldOverride, FormSchema, ValuesOf } from './resource.js'\n\n/**\n * A kind of content a page is built from - a hero, a feature grid, a call to\n * action - as a schema for what an editor fills in, and a component that\n * draws it. The component is the app's: a block looks like the site it is on.\n */\nexport interface Block<Schema extends FormSchema = FormSchema> {\n name: string\n label: string\n schema: Schema\n fields: Field[]\n render: ComponentType<{ data: ValuesOf<Schema> }>\n}\n\nexport interface BlockDefinition<Schema extends FormSchema> {\n /** What it is saved as: lowercase letters, digits and dashes. */\n name: string\n /** What an editor picks it by. Humanized from the name by default. */\n label?: string\n /** Any Standard Schema of an object. Its fields are what an editor fills in. */\n form: Schema\n fields?: Partial<Record<keyof ValuesOf<Schema> & string, FieldOverride>>\n /** Draws it, given what passed the schema. May be an async server component. */\n render: ComponentType<{ data: ValuesOf<Schema> }>\n}\n\n/** One block as it is saved: which kind, and what was filled in. */\nexport interface BlockValue {\n type: string\n data: Record<string, unknown>\n}\n\nexport function defineBlock<Schema extends FormSchema>(definition: BlockDefinition<Schema>): Block<Schema> {\n if (!/^[a-z0-9-]+$/.test(definition.name)) {\n throw new Error(`[press] a block's name is lowercase letters, digits and dashes: ${JSON.stringify(definition.name)}`)\n }\n\n const fields = fieldsOf(definition.form, definition.fields ?? {})\n\n if (fields.some((field) => field.type === 'blocks')) {\n throw new Error(`[press] the block ${definition.name} has a blocks field; blocks do not nest`)\n }\n\n return {\n name: definition.name,\n label: definition.label ?? humanize(definition.name),\n schema: definition.form,\n fields,\n render: definition.render,\n }\n}\n\n/**\n * Saved blocks, drawn in order. A block whose kind is no longer defined is\n * left out rather than failing the page: content outlives the code that drew\n * it.\n */\nexport function Blocks({ blocks, value }: { blocks: Block<any>[]; value: unknown }) {\n const list = Array.isArray(value) ? (value as BlockValue[]) : []\n\n return list.map((item, index) => {\n const block = blocks.find((candidate) => candidate.name === item?.type)\n\n if (!block) return null\n\n return createElement(block.render, { key: index, data: item.data })\n })\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One submit for every form, bound by its component:
|
|
3
|
+
* submitForm.bind(null, 'contact'). The form's own schema is the check, on
|
|
4
|
+
* the server, every time - the browser's checks are a courtesy to the
|
|
5
|
+
* visitor, and anyone can post without them.
|
|
6
|
+
*/
|
|
7
|
+
export declare function submitForm(name: string, form: FormData): Promise<undefined>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
import { fieldErrors } from '@rsc-kit/core/action';
|
|
3
|
+
import { redirect } from '@rsc-kit/core/redirect';
|
|
4
|
+
import { parse } from '../resource.js';
|
|
5
|
+
import { formNamed } from './registry.js';
|
|
6
|
+
import { HONEYPOT } from './honeypot.js';
|
|
7
|
+
/**
|
|
8
|
+
* One submit for every form, bound by its component:
|
|
9
|
+
* submitForm.bind(null, 'contact'). The form's own schema is the check, on
|
|
10
|
+
* the server, every time - the browser's checks are a courtesy to the
|
|
11
|
+
* visitor, and anyone can post without them.
|
|
12
|
+
*/
|
|
13
|
+
export async function submitForm(name, form) {
|
|
14
|
+
const definition = formNamed(name);
|
|
15
|
+
// Filled in, so not a person. Answered as a success, so a bot learns
|
|
16
|
+
// nothing from the reply, and dropped.
|
|
17
|
+
if (String(form.get(HONEYPOT) ?? '') !== '')
|
|
18
|
+
return;
|
|
19
|
+
const parsed = await parse(definition, form);
|
|
20
|
+
if (!parsed.ok)
|
|
21
|
+
return fieldErrors(parsed.errors);
|
|
22
|
+
await definition.handle(parsed.values);
|
|
23
|
+
if (definition.redirect)
|
|
24
|
+
redirect(definition.redirect);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/forms/actions.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAEZ,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAExC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY,EAAE,IAAc;IAC3D,MAAM,UAAU,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAElC,qEAAqE;IACrE,uCAAuC;IACvC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE;QAAE,OAAM;IAEnD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,UAAU,EAAE,IAAI,CAAC,CAAA;IAE5C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAEjD,MAAM,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAEtC,IAAI,UAAU,CAAC,QAAQ;QAAE,QAAQ,CAAC,UAAU,CAAC,QAAiB,CAAC,CAAA;AACjE,CAAC","sourcesContent":["'use server'\n\nimport { fieldErrors } from '@rsc-kit/core/action'\nimport { redirect } from '@rsc-kit/core/redirect'\nimport { parse } from '../resource.js'\nimport { formNamed } from './registry.js'\nimport { HONEYPOT } from './honeypot.js'\n\n/**\n * One submit for every form, bound by its component:\n * submitForm.bind(null, 'contact'). The form's own schema is the check, on\n * the server, every time - the browser's checks are a courtesy to the\n * visitor, and anyone can post without them.\n */\nexport async function submitForm(name: string, form: FormData) {\n const definition = formNamed(name)\n\n // Filled in, so not a person. Answered as a success, so a bot learns\n // nothing from the reply, and dropped.\n if (String(form.get(HONEYPOT) ?? '') !== '') return\n\n const parsed = await parse(definition, form)\n\n if (!parsed.ok) return fieldErrors(parsed.errors)\n\n await definition.handle(parsed.values)\n\n if (definition.redirect) redirect(definition.redirect as never)\n}\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FieldOverride, FormSchema, ValuesOf } from '../resource.js';
|
|
2
|
+
import type { FormsTheme } from './theme.js';
|
|
3
|
+
export interface FormDefinition<Schema extends FormSchema> {
|
|
4
|
+
/** Unique in the app: the form is found by it when it is sent. Lowercase letters, digits and dashes. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Any Standard Schema of an object: z.object(), v.object(), type({...}). It checks every submission, on the server. */
|
|
7
|
+
form: Schema;
|
|
8
|
+
/** Labels, descriptions and controls the schema cannot say - or every field, for a schema that does not describe itself as JSON Schema. */
|
|
9
|
+
fields?: Partial<Record<keyof ValuesOf<Schema> & string, FieldOverride>>;
|
|
10
|
+
/** What draws it: import { shadcn } from '@rsc-kit/press/shadcn'. */
|
|
11
|
+
theme: FormsTheme;
|
|
12
|
+
/**
|
|
13
|
+
* What a submission that passed the schema does: store it, send it, call a
|
|
14
|
+
* backend. Throw an ActionValidationError - or let a backend's 422 through -
|
|
15
|
+
* and its messages land under the fields.
|
|
16
|
+
*/
|
|
17
|
+
handle: (values: ValuesOf<Schema>) => void | Promise<void>;
|
|
18
|
+
/** The submit button's words. Send by default. */
|
|
19
|
+
submit?: string;
|
|
20
|
+
/** What the visitor reads in place of the form once it is sent. */
|
|
21
|
+
done?: string;
|
|
22
|
+
/** Where the visitor goes once it is sent, instead of `done`. */
|
|
23
|
+
redirect?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A form anyone may fill in - a contact form, a sign-up, a survey - from a
|
|
27
|
+
* Standard Schema, checked by the browser first and by the schema on the
|
|
28
|
+
* server always. Place it in any page:
|
|
29
|
+
*
|
|
30
|
+
* export default function Contact() {
|
|
31
|
+
* return <main><h1>Contact us</h1><contact.Form /></main>
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* The form reads nothing from the request, so the page around it can still
|
|
35
|
+
* be stored by the build.
|
|
36
|
+
*/
|
|
37
|
+
export declare function defineForm<Schema extends FormSchema>(definition: FormDefinition<Schema>): {
|
|
38
|
+
name: string;
|
|
39
|
+
fields: import("../resource.js").Field[];
|
|
40
|
+
Form: () => import("react").JSX.Element;
|
|
41
|
+
};
|
|
42
|
+
export type PublicForm = ReturnType<typeof defineForm>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { fieldsOf } from '../resource.js';
|
|
3
|
+
import { submitForm } from './actions.js';
|
|
4
|
+
import { HONEYPOT } from './honeypot.js';
|
|
5
|
+
import { registerForm } from './registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* A form anyone may fill in - a contact form, a sign-up, a survey - from a
|
|
8
|
+
* Standard Schema, checked by the browser first and by the schema on the
|
|
9
|
+
* server always. Place it in any page:
|
|
10
|
+
*
|
|
11
|
+
* export default function Contact() {
|
|
12
|
+
* return <main><h1>Contact us</h1><contact.Form /></main>
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* The form reads nothing from the request, so the page around it can still
|
|
16
|
+
* be stored by the build.
|
|
17
|
+
*/
|
|
18
|
+
export function defineForm(definition) {
|
|
19
|
+
if (!/^[a-z0-9-]+$/.test(definition.name)) {
|
|
20
|
+
throw new Error(`[forms] a form's name is lowercase letters, digits and dashes: ${JSON.stringify(definition.name)}`);
|
|
21
|
+
}
|
|
22
|
+
if (definition.done !== undefined && definition.redirect !== undefined) {
|
|
23
|
+
throw new Error(`[forms] the form ${definition.name} has both done and redirect; a sent form shows one or goes to the other`);
|
|
24
|
+
}
|
|
25
|
+
const fields = fieldsOf(definition.form, definition.fields ?? {});
|
|
26
|
+
if (fields.some((field) => field.name === HONEYPOT)) {
|
|
27
|
+
throw new Error(`[forms] the form ${definition.name} has a field named ${HONEYPOT}, which is the name its bot trap uses`);
|
|
28
|
+
}
|
|
29
|
+
registerForm({
|
|
30
|
+
name: definition.name,
|
|
31
|
+
schema: definition.form,
|
|
32
|
+
fields,
|
|
33
|
+
handle: definition.handle,
|
|
34
|
+
redirect: definition.redirect ?? null,
|
|
35
|
+
});
|
|
36
|
+
const theme = definition.theme;
|
|
37
|
+
const submit = definition.submit ?? 'Send';
|
|
38
|
+
const done = definition.redirect === undefined ? (definition.done ?? 'Thank you. It was sent.') : undefined;
|
|
39
|
+
function Form() {
|
|
40
|
+
return (_jsx(theme.PublicForm, { action: submitForm.bind(null, definition.name), fields: fields, submit: submit, ...(done === undefined ? {} : { done }), honeypot: HONEYPOT }));
|
|
41
|
+
}
|
|
42
|
+
return { name: definition.name, fields, Form };
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form.js","sourceRoot":"","sources":["../../src/forms/form.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AA0B5C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAA4B,UAAkC;IACtF,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,kEAAkE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACtH,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,KAAK,SAAS,IAAI,UAAU,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,oBAAoB,UAAU,CAAC,IAAI,yEAAyE,CAAC,CAAA;IAC/H,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,MAAM,IAAI,EAAE,CAAC,CAAA;IAEjE,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,oBAAoB,UAAU,CAAC,IAAI,sBAAsB,QAAQ,uCAAuC,CAAC,CAAA;IAC3H,CAAC;IAED,YAAY,CAAC;QACX,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,MAAM,EAAE,UAAU,CAAC,IAAI;QACvB,MAAM;QACN,MAAM,EAAE,UAAU,CAAC,MAAmE;QACtF,QAAQ,EAAE,UAAU,CAAC,QAAQ,IAAI,IAAI;KACtC,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAA;IAC9B,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,IAAI,MAAM,CAAA;IAC1C,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,IAAI,yBAAyB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAE3G,SAAS,IAAI;QACX,OAAO,CACL,KAAC,KAAK,CAAC,UAAU,IACf,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,EAC9C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,KACV,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EACxC,QAAQ,EAAE,QAAQ,GAClB,CACH,CAAA;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;AAChD,CAAC","sourcesContent":["import { fieldsOf } from '../resource.js'\nimport type { FieldOverride, FormSchema, ValuesOf } from '../resource.js'\nimport { submitForm } from './actions.js'\nimport { HONEYPOT } from './honeypot.js'\nimport { registerForm } from './registry.js'\nimport type { FormsTheme } from './theme.js'\n\nexport interface FormDefinition<Schema extends FormSchema> {\n /** Unique in the app: the form is found by it when it is sent. Lowercase letters, digits and dashes. */\n name: string\n /** Any Standard Schema of an object: z.object(), v.object(), type({...}). It checks every submission, on the server. */\n form: Schema\n /** Labels, descriptions and controls the schema cannot say - or every field, for a schema that does not describe itself as JSON Schema. */\n fields?: Partial<Record<keyof ValuesOf<Schema> & string, FieldOverride>>\n /** What draws it: import { shadcn } from '@rsc-kit/press/shadcn'. */\n theme: FormsTheme\n /**\n * What a submission that passed the schema does: store it, send it, call a\n * backend. Throw an ActionValidationError - or let a backend's 422 through -\n * and its messages land under the fields.\n */\n handle: (values: ValuesOf<Schema>) => void | Promise<void>\n /** The submit button's words. Send by default. */\n submit?: string\n /** What the visitor reads in place of the form once it is sent. */\n done?: string\n /** Where the visitor goes once it is sent, instead of `done`. */\n redirect?: string\n}\n\n/**\n * A form anyone may fill in - a contact form, a sign-up, a survey - from a\n * Standard Schema, checked by the browser first and by the schema on the\n * server always. Place it in any page:\n *\n * export default function Contact() {\n * return <main><h1>Contact us</h1><contact.Form /></main>\n * }\n *\n * The form reads nothing from the request, so the page around it can still\n * be stored by the build.\n */\nexport function defineForm<Schema extends FormSchema>(definition: FormDefinition<Schema>) {\n if (!/^[a-z0-9-]+$/.test(definition.name)) {\n throw new Error(`[forms] a form's name is lowercase letters, digits and dashes: ${JSON.stringify(definition.name)}`)\n }\n\n if (definition.done !== undefined && definition.redirect !== undefined) {\n throw new Error(`[forms] the form ${definition.name} has both done and redirect; a sent form shows one or goes to the other`)\n }\n\n const fields = fieldsOf(definition.form, definition.fields ?? {})\n\n if (fields.some((field) => field.name === HONEYPOT)) {\n throw new Error(`[forms] the form ${definition.name} has a field named ${HONEYPOT}, which is the name its bot trap uses`)\n }\n\n registerForm({\n name: definition.name,\n schema: definition.form,\n fields,\n handle: definition.handle as (values: Record<string, unknown>) => void | Promise<void>,\n redirect: definition.redirect ?? null,\n })\n\n const theme = definition.theme\n const submit = definition.submit ?? 'Send'\n const done = definition.redirect === undefined ? (definition.done ?? 'Thank you. It was sent.') : undefined\n\n function Form() {\n return (\n <theme.PublicForm\n action={submitForm.bind(null, definition.name)}\n fields={fields}\n submit={submit}\n {...(done === undefined ? {} : { done })}\n honeypot={HONEYPOT}\n />\n )\n }\n\n return { name: definition.name, fields, Form }\n}\n\nexport type PublicForm = ReturnType<typeof defineForm>\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"honeypot.js","sourceRoot":"","sources":["../../src/forms/honeypot.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAC3G,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAA","sourcesContent":["/** The field only bots fill in. Named like one a form might really have, which is what a bot looks for. */\nexport const HONEYPOT = 'homepage'\n"]}
|