@murumets-ee/forms-ui 0.0.0-stage → 0.97.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,94 @@
1
+ Elastic License 2.0 (ELv2)
2
+
3
+ URL: https://www.elastic.co/licensing/elastic-license
4
+
5
+ ## Acceptance
6
+
7
+ By using the software, you agree to all of the terms and conditions below.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
12
+ non-sublicensable, non-transferable license to use, copy, distribute, make
13
+ available, and prepare derivative works of the software, in each case subject
14
+ to the limitations and conditions below.
15
+
16
+ ## Limitations
17
+
18
+ You may not provide the software to third parties as a hosted or managed
19
+ service, where the service provides users with access to any substantial set
20
+ of the features or functionality of the software.
21
+
22
+ You may not move, change, disable, or circumvent the license key functionality
23
+ in the software, and you may not remove or obscure any functionality in the
24
+ software that is protected by the license key.
25
+
26
+ You may not alter, remove, or obscure any licensing, copyright, or other
27
+ notices of the licensor in the software. Any use of the licensor's trademarks
28
+ is subject to applicable law.
29
+
30
+ ## Patents
31
+
32
+ The licensor grants you a license, under any patent claims the licensor can
33
+ license, or becomes able to license, to make, have made, use, sell, offer for
34
+ sale, import and have imported the software, in each case subject to the
35
+ limitations and conditions in this license. This license does not cover any
36
+ patent claims that you cause to be infringed by modifications or additions to
37
+ the software. If you or your company make any written claim that the software
38
+ infringes or contributes to infringement of any patent, your patent license
39
+ for the software granted under these terms ends immediately. If your company
40
+ makes such a claim, your patent license ends immediately for work on behalf
41
+ of your company.
42
+
43
+ ## Notices
44
+
45
+ You must ensure that anyone who gets a copy of any part of the software from
46
+ you also gets a copy of these terms.
47
+
48
+ If you modify the software, you must include in any modified copies of the
49
+ software prominent notices stating that you have modified the software.
50
+
51
+ ## No Other Rights
52
+
53
+ These terms do not imply any licenses other than those expressly granted in
54
+ these terms.
55
+
56
+ ## Termination
57
+
58
+ If you use the software in violation of these terms, such use is not licensed,
59
+ and your licenses will automatically terminate. If the licensor provides you
60
+ with a notice of your violation, and you cease all violation of this license
61
+ no later than 30 days after you receive that notice, your licenses will be
62
+ reinstated retroactively. However, if you violate these terms after such
63
+ reinstatement, any additional violation of these terms will cause your
64
+ licenses to terminate automatically and permanently.
65
+
66
+ ## No Liability
67
+
68
+ As far as the law allows, the software comes as is, without any warranty or
69
+ condition, and the licensor will not be liable to you for any damages arising
70
+ out of these terms or the use or nature of the software, under any kind of
71
+ legal claim.
72
+
73
+ ## Definitions
74
+
75
+ The **licensor** is the entity offering these terms, and the **software** is
76
+ the software the licensor makes available under these terms, including any
77
+ portion of it.
78
+
79
+ **you** refers to the individual or entity agreeing to these terms.
80
+
81
+ **your company** is any legal entity, sole proprietorship, or other kind of
82
+ organization that you work for, plus all organizations that have control over,
83
+ are under the control of, or are under common control with that organization.
84
+ **control** means ownership of substantially all the assets of an entity, or
85
+ the power to direct the management and policies of an entity (for example, by
86
+ voting right, contract, or otherwise). Control can be direct or indirect.
87
+
88
+ **your licenses** are all the licenses granted to you for the software under
89
+ these terms.
90
+
91
+ **use** means anything you do with the software requiring one of your
92
+ licenses.
93
+
94
+ **trademark** means trademarks, service marks, and similar rights.
@@ -0,0 +1,25 @@
1
+
2
+ import { ReactElement } from "react";
3
+
4
+ //#region src/new-form-dialog.d.ts
5
+ interface NewFormDialogProps {
6
+ readonly apiBasePath: string;
7
+ /** Where the form canvas lives — `<admin base>/forms`. */
8
+ readonly formsBasePath: string;
9
+ }
10
+ declare function NewFormDialog({
11
+ apiBasePath,
12
+ formsBasePath
13
+ }: NewFormDialogProps): ReactElement;
14
+ //#endregion
15
+ //#region src/register.d.ts
16
+ declare function registerFormBlocks(): void;
17
+ /**
18
+ * Render it, do not import it for effect: a component in the layout is what
19
+ * guarantees the registration runs before any canvas resolves its vocabulary
20
+ * (`resolveBlockVocabulary` runs during render). Same shape as menus-ui's.
21
+ */
22
+ declare function FormBlocksRegistration(): null;
23
+ //#endregion
24
+ export { FormBlocksRegistration, NewFormDialog, type NewFormDialogProps, registerFormBlocks };
25
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/new-form-dialog.tsx","../src/register.tsx"],"mappings":";;;;UA8BiB,kBAAA;EAAA,SACN,WAAA;EADM;EAAA,SAGN,aAAa;AAAA;AAAA,iBAGR,aAAA,CAAA;EAAgB,WAAA;EAAa;AAAA,GAAiB,kBAAA,GAAqB,YAAA;;;iBCLnE,kBAAA,CAAA;;;ADDhB;;;iBCkCgB,sBAAA,CAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ "use client";
2
+ import{FORMS_SOURCE as e,FORM_ENVIRONMENT as t,FORM_NAME_MAX as n,PAGE_ENVIRONMENT as r,formEnvironmentBlocks as i,formEnvironmentTraits as a,formSiteBlocks as o}from"@murumets-ee/forms/schema";import{Button as s,Dialog as c,DialogContent as l,DialogHeader as u,DialogTitle as d,Input as f,Label as p}from"@murumets-ee/ui";import{useRouter as m}from"next/navigation";import{useTranslations as h}from"next-intl";import{useState as g}from"react";import{Fragment as _,jsx as v,jsxs as y}from"react/jsx-runtime";import{registerBlockEnvironment as b,registerBlockEnvironmentBase as x,registerBlockTheme as S,widenFieldType as C}from"@murumets-ee/blocks/core";import{formFieldKeyType as w,formPickerType as T,formSettingsTabsType as E}from"@murumets-ee/forms/editor";import{FORMS_THEME as D,formsTheme as O}from"@murumets-ee/forms/render";function k({apiBasePath:e,formsBasePath:t}){let r=h(`Forms.newDialog`),i=m(),[a,o]=g(!1),[b,x]=g(``),[S,C]=g(!1),[w,T]=g(null),E=b.trim(),D=E!==``&&E.length<=n&&!S;return y(_,{children:[v(s,{type:`button`,size:`sm`,onClick:()=>o(!0),children:r(`title`)}),v(c,{open:a,onOpenChange:o,children:y(l,{className:`max-w-md`,children:[v(u,{children:v(d,{children:r(`title`)})}),y(`form`,{onSubmit:async n=>{if(n.preventDefault(),D){C(!0),T(null);try{let n=await fetch(`${e}/form`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:E})});if(!n.ok)throw Error(r(`failed`,{status:n.status}));let a=await n.json();if(typeof a.id!=`string`)throw Error(r(`noId`));o(!1),C(!1),i.push(`${t}/${a.id}`)}catch(e){T(e instanceof Error?e.message:r(`failed`,{status:0})),C(!1)}}},className:`flex flex-col gap-3`,children:[y(`div`,{className:`flex flex-col gap-1.5`,children:[v(p,{htmlFor:`new-form-name`,children:r(`name`)}),v(f,{id:`new-form-name`,value:b,maxLength:n,onChange:e=>x(e.target.value),placeholder:r(`namePlaceholder`),"aria-describedby":`new-form-name-help`}),v(`p`,{id:`new-form-name-help`,className:`text-xs text-muted-foreground`,children:r(`help`)})]}),w!==null&&v(`p`,{role:`alert`,className:`text-sm text-destructive`,children:w}),y(`div`,{className:`flex justify-end gap-2 pt-1`,children:[v(s,{type:`button`,variant:`outline`,onClick:()=>o(!1),children:r(`cancel`)}),v(s,{type:`submit`,disabled:!D,loading:S,children:r(`create`)})]})]})]})})]})}function A(){S(O),x({name:t,blocks:i,traits:a,fieldTypes:[C(w),C(E)],defaultTheme:D}),b({source:e,name:r,blocks:o,fieldTypes:[C(T),C(w),C(E)]})}function j(){return A(),null}export{j as FormBlocksRegistration,k as NewFormDialog,A as registerFormBlocks};
3
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/new-form-dialog.tsx","../src/register.tsx"],"sourcesContent":["'use client'\n\n/**\n * \"New form\" on the Forms list (W1): ask for a name, create the form, and land\n * on its canvas (W2).\n *\n * It sends the NAME and nothing else. The server gives a form created without\n * a tree its starting contact form, in the request's content language\n * (`formStarterTree` in `@murumets-ee/forms`) — so the browser shapes no part\n * of the tree, and an agent creating a form gets the same one.\n *\n * It posts to the generic entity API (`POST /api/admin/form`), where the\n * firewall sits: `form:create`, CSRF, and the site of the request (the\n * admin's patched `fetch` adds it).\n */\n\nimport { FORM_NAME_MAX } from '@murumets-ee/forms/schema'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\n} from '@murumets-ee/ui'\nimport { useRouter } from 'next/navigation'\nimport { useTranslations } from 'next-intl'\nimport { type FormEvent, type ReactElement, useState } from 'react'\n\nexport interface NewFormDialogProps {\n readonly apiBasePath: string\n /** Where the form canvas lives — `<admin base>/forms`. */\n readonly formsBasePath: string\n}\n\nexport function NewFormDialog({ apiBasePath, formsBasePath }: NewFormDialogProps): ReactElement {\n const t = useTranslations('Forms.newDialog')\n const router = useRouter()\n const [open, setOpen] = useState(false)\n const [name, setName] = useState('')\n const [submitting, setSubmitting] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const trimmed = name.trim()\n const canSubmit = trimmed !== '' && trimmed.length <= FORM_NAME_MAX && !submitting\n\n const onSubmit = async (event: FormEvent) => {\n event.preventDefault()\n if (!canSubmit) return\n setSubmitting(true)\n setError(null)\n try {\n const res = await fetch(`${apiBasePath}/form`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ name: trimmed }),\n })\n if (!res.ok) throw new Error(t('failed', { status: res.status }))\n const created = (await res.json()) as { id?: unknown }\n if (typeof created.id !== 'string') throw new Error(t('noId'))\n setOpen(false)\n setSubmitting(false)\n router.push(`${formsBasePath}/${created.id}`)\n } catch (err) {\n setError(err instanceof Error ? err.message : t('failed', { status: 0 }))\n setSubmitting(false)\n }\n }\n\n return (\n <>\n <Button type=\"button\" size=\"sm\" onClick={() => setOpen(true)}>\n {t('title')}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{t('title')}</DialogTitle>\n </DialogHeader>\n <form onSubmit={onSubmit} className=\"flex flex-col gap-3\">\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"new-form-name\">{t('name')}</Label>\n <Input\n id=\"new-form-name\"\n value={name}\n maxLength={FORM_NAME_MAX}\n onChange={(e) => setName(e.target.value)}\n placeholder={t('namePlaceholder')}\n aria-describedby=\"new-form-name-help\"\n />\n <p id=\"new-form-name-help\" className=\"text-xs text-muted-foreground\">\n {t('help')}\n </p>\n </div>\n {error !== null && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {error}\n </p>\n )}\n <div className=\"flex justify-end gap-2 pt-1\">\n <Button type=\"button\" variant=\"outline\" onClick={() => setOpen(false)}>\n {t('cancel')}\n </Button>\n <Button type=\"submit\" disabled={!canSubmit} loading={submitting}>\n {t('create')}\n </Button>\n </div>\n </form>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n","'use client'\n\n/**\n * Puts the `form` environment into the browser's block registry — the one\n * client-side step a canvas needs, MOUNTED by the admin layout beside\n * `<MenuBlocksRegistration />` (see CLAUDE.md's standing exception for why the\n * browser half is a registry call and not a `Plugin.contributes` slot).\n *\n * The server learns the same vocabulary declaratively from the plugin\n * (`blocks.blocks` + `blocks.environmentBases`), so the two agree on every\n * block name; this half adds what only the browser can hold — the renders and\n * the panel's two field editors.\n */\n\nimport {\n registerBlockEnvironment,\n registerBlockEnvironmentBase,\n registerBlockTheme,\n widenFieldType,\n} from '@murumets-ee/blocks/core'\nimport { formFieldKeyType, formPickerType, formSettingsTabsType } from '@murumets-ee/forms/editor'\nimport { FORMS_THEME, formsTheme } from '@murumets-ee/forms/render'\nimport {\n FORM_ENVIRONMENT,\n FORMS_SOURCE,\n formEnvironmentBlocks,\n formEnvironmentTraits,\n formSiteBlocks,\n PAGE_ENVIRONMENT,\n} from '@murumets-ee/forms/schema'\n\nexport function registerFormBlocks(): void {\n registerBlockTheme(formsTheme)\n registerBlockEnvironmentBase({\n name: FORM_ENVIRONMENT,\n blocks: formEnvironmentBlocks,\n traits: formEnvironmentTraits,\n fieldTypes: [widenFieldType(formFieldKeyType), widenFieldType(formSettingsTabsType)],\n defaultTheme: FORMS_THEME,\n })\n // The page canvas (D002): the Form block in the palette, and — because the\n // saved form it shows is drawn and edited IN PLACE on the page — every block\n // a form is made of, with their panel editors. A field is still offered only\n // inside a form (`ancestor`), so the page itself gains one entry: \"Form\".\n // The server's `page` environment holds the same blocks\n // (`formPageEnvironmentExtension`); the fields are edited in the form's own\n // document, judged against the `form` environment.\n registerBlockEnvironment({\n source: FORMS_SOURCE,\n name: PAGE_ENVIRONMENT,\n blocks: formSiteBlocks,\n fieldTypes: [\n widenFieldType(formPickerType),\n widenFieldType(formFieldKeyType),\n widenFieldType(formSettingsTabsType),\n ],\n })\n}\n\n/**\n * Render it, do not import it for effect: a component in the layout is what\n * guarantees the registration runs before any canvas resolves its vocabulary\n * (`resolveBlockVocabulary` runs during render). Same shape as menus-ui's.\n */\nexport function FormBlocksRegistration(): null {\n registerFormBlocks()\n return null\n}\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { PageProps } from "@murumets-ee/core";
2
+ import { ReactNode } from "react";
3
+
4
+ //#region src/pages/form-canvas-page.d.ts
5
+ declare function FormCanvasPage({
6
+ params,
7
+ searchParams
8
+ }: PageProps<{
9
+ locale: string;
10
+ id: string;
11
+ }>): Promise<ReactNode>;
12
+ //#endregion
13
+ //#region src/pages/forms-list-page.d.ts
14
+ declare function FormsListPage({
15
+ params
16
+ }: PageProps<{
17
+ locale: string;
18
+ }>): Promise<ReactNode>;
19
+ //#endregion
20
+ export { FormCanvasPage, FormsListPage };
21
+ //# sourceMappingURL=pages.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.d.mts","names":[],"sources":["../src/pages/form-canvas-page.tsx","../src/pages/forms-list-page.tsx"],"mappings":";;;;iBAqBsB,cAAA,CAAA;EACpB,MAAA;EACA;AAAA,GACC,SAAA;EAAY,MAAA;EAAgB,EAAA;AAAA,KAAgB,OAAA,CAAQ,SAAA;;;iBCSjC,aAAA,CAAA;EAAgB;AAAA,GAAU,SAAA;EAAY,MAAA;AAAA,KAAoB,OAAA,CAAQ,SAAA"}
package/dist/pages.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{getRegisteredAdminConfig as e}from"@murumets-ee/admin-ui/config-registry";import{hasEntityPermission as t,hasResourcePermission as n,renderEntityEditFallback as r}from"@murumets-ee/admin-ui/pages";import{FORM_ENTITY as i,Form as a}from"@murumets-ee/forms";import{notFound as o}from"next/navigation";import{fetchEntityList as s}from"@murumets-ee/admin-ui/server";import{WorkflowStatusBadge as c}from"@murumets-ee/admin-ui/workflow";import{countEmbedUsages as l}from"@murumets-ee/blocks/server";import{NewFormDialog as u}from"@murumets-ee/forms-ui";import{Table as d,TableBody as f,TableCell as p,TableHead as m,TableHeader as h,TableRow as g}from"@murumets-ee/ui";import _ from"next/link";import{getTranslations as v,setRequestLocale as y}from"next-intl/server";import{jsx as b,jsxs as x}from"react/jsx-runtime";const S=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function C({params:t,searchParams:n}){let{id:i}=await t;return S.test(i)||o(),r(a,e(),{params:t,searchParams:n})}async function w({params:r}){let S=e(),{locale:C}=await r;y(C);let w=await v(`Forms.list`),T=`${S.adminBasePath.replace(`:locale`,C)}/forms`;return S.withAdminContext(C,async()=>{await t(a,`view`,S)||o();let[{items:e,total:r},v]=await Promise.all([s(a,{sortField:`name`,sortDirection:`asc`,limit:200}),n(`form`,`create`,S)]),y=await l(i,e.map(e=>e.id)).catch(()=>null);return x(`div`,{className:`space-y-4 p-6`,children:[x(`div`,{className:`flex items-start justify-between gap-4`,children:[x(`div`,{children:[b(`h1`,{className:`text-2xl font-bold`,children:w(`title`)}),b(`p`,{className:`text-sm text-muted-foreground`,children:w(`description`)})]}),v&&b(u,{apiBasePath:S.apiBasePath??`/api/admin`,formsBasePath:T})]}),e.length===0?b(`p`,{className:`rounded-md border border-dashed p-6 text-sm text-muted-foreground`,children:w(`empty`)}):x(d,{children:[b(h,{children:x(g,{children:[b(m,{children:w(`name`)}),b(m,{children:w(`usedOn`)}),b(m,{className:`text-right`,children:w(`submissions`)}),b(m,{className:`text-right`,children:w(`new`)}),b(m,{className:`text-right`,children:w(`blocked`)}),b(m,{children:w(`status`)})]})}),b(f,{children:e.map(e=>x(g,{children:[b(p,{className:`font-medium`,children:b(_,{href:`${T}/${e.id}`,className:`text-primary hover:underline`,children:e.name})}),b(p,{className:`text-muted-foreground`,"data-forms-used-on":e.id,children:y===null?w(`notYet`):w(`usedOnPages`,{count:y.get(e.id)??0})}),b(p,{className:`text-right text-muted-foreground`,children:w(`notYet`)}),b(p,{className:`text-right text-muted-foreground`,children:w(`notYet`)}),b(p,{className:`text-right text-muted-foreground`,children:w(`notYet`)}),b(p,{children:b(c,{status:e.status===`published`?`published`:`draft`})})]},e.id))})]}),r>e.length&&b(`p`,{className:`text-xs text-muted-foreground`,children:w(`truncated`,{count:e.length})})]})})}export{C as FormCanvasPage,w as FormsListPage};
2
+ //# sourceMappingURL=pages.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.mjs","names":[],"sources":["../src/pages/form-canvas-page.tsx","../src/pages/forms-list-page.tsx"],"sourcesContent":["/**\n * The form canvas (W2) — a form open on its own canvas, in the `form`\n * environment.\n *\n * It owns `/admin/forms/:id` (and the singular `/admin/form/:id`), the URLs\n * the generic entity edit page would have answered, so a form has one editor and it is gated by the\n * job permission: the route's `forms:manage`, then — inside\n * `renderEntityEditFallback` — the entity's own `form:update`, which is also\n * what every write from the canvas is checked against. The editor itself is\n * the toolkit's: drafts, publish, locks, undo — nothing re-implemented.\n */\n\nimport { getRegisteredAdminConfig } from '@murumets-ee/admin-ui/config-registry'\nimport { renderEntityEditFallback } from '@murumets-ee/admin-ui/pages'\nimport type { PageProps } from '@murumets-ee/core'\nimport { Form } from '@murumets-ee/forms'\nimport { notFound } from 'next/navigation'\nimport type { ReactNode } from 'react'\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nexport async function FormCanvasPage({\n params,\n searchParams,\n}: PageProps<{ locale: string; id: string }>): Promise<ReactNode> {\n const { id } = await params\n // `forms/new` lands here too (this route shadows the entity family); a form\n // is only ever created from the Forms list, so anything but an id is a 404.\n if (!UUID_RE.test(id)) notFound()\n return renderEntityEditFallback(Form, getRegisteredAdminConfig(), {\n params,\n searchParams,\n })\n}\n","/**\n * Forms (W1) — the selected site's forms, and the way into each one's canvas.\n *\n * Gated by `forms:manage` on its route (the plugin's `defaultRoutes`), which\n * is also what the sidebar link inherits. Lists THIS site's forms only: `form`\n * is site-bound, so the entity read is scoped by the request's site like\n * every other bound entity (C2).\n *\n * The columns are the approved wireframe's. \"Used on\" counts the PUBLISHED\n * pages that show the form (forms D002 — from `entity_refs`, which a page's\n * Form block feeds when the page is published); \"Submissions\", \"New\" and\n * \"Blocked\" fill in with the submission path (PR04/PR05). Until then each of\n * those shows a dash rather than a zero — a zero would claim a count that\n * nothing has taken yet.\n */\n\nimport { getRegisteredAdminConfig } from '@murumets-ee/admin-ui/config-registry'\nimport { hasEntityPermission, hasResourcePermission } from '@murumets-ee/admin-ui/pages'\nimport { fetchEntityList } from '@murumets-ee/admin-ui/server'\nimport { WorkflowStatusBadge } from '@murumets-ee/admin-ui/workflow'\nimport { countEmbedUsages } from '@murumets-ee/blocks/server'\nimport type { PageProps } from '@murumets-ee/core'\nimport { FORM_ENTITY, Form } from '@murumets-ee/forms'\nimport { NewFormDialog } from '@murumets-ee/forms-ui'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@murumets-ee/ui'\nimport Link from 'next/link'\nimport { notFound } from 'next/navigation'\nimport { getTranslations, setRequestLocale } from 'next-intl/server'\nimport type { ReactNode } from 'react'\n\n/** A site's forms are a handful; the bound keeps one page load one query. */\nconst MAX_FORMS = 200\n\nexport async function FormsListPage({ params }: PageProps<{ locale: string }>): Promise<ReactNode> {\n const config = getRegisteredAdminConfig()\n const { locale } = await params\n setRequestLocale(locale)\n const t = await getTranslations('Forms.list')\n const formsBasePath = `${config.adminBasePath.replace(':locale', locale)}/forms`\n\n return config.withAdminContext(locale, async () => {\n // The route's gate is the job (`forms:manage`); reading the rows is the\n // entity's own `form:view`. A role holding the first without the second\n // gets the same no-leak 404 as the gate, not a failed read.\n if (!(await hasEntityPermission(Form, 'view', config))) notFound()\n const [{ items, total }, canCreate] = await Promise.all([\n fetchEntityList(Form, { sortField: 'name', sortDirection: 'asc', limit: MAX_FORMS }),\n hasResourcePermission('form', 'create', config),\n ])\n // One grouped query for every listed form. A failure costs the column its\n // numbers (a dash), never the list.\n const usedOn = await countEmbedUsages(\n FORM_ENTITY,\n items.map((form) => form.id),\n ).catch(() => null)\n\n return (\n <div className=\"space-y-4 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-bold\">{t('title')}</h1>\n <p className=\"text-sm text-muted-foreground\">{t('description')}</p>\n </div>\n {canCreate && (\n <NewFormDialog\n apiBasePath={config.apiBasePath ?? '/api/admin'}\n formsBasePath={formsBasePath}\n />\n )}\n </div>\n\n {items.length === 0 ? (\n <p className=\"rounded-md border border-dashed p-6 text-sm text-muted-foreground\">\n {t('empty')}\n </p>\n ) : (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{t('name')}</TableHead>\n <TableHead>{t('usedOn')}</TableHead>\n <TableHead className=\"text-right\">{t('submissions')}</TableHead>\n <TableHead className=\"text-right\">{t('new')}</TableHead>\n <TableHead className=\"text-right\">{t('blocked')}</TableHead>\n <TableHead>{t('status')}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {items.map((form) => (\n <TableRow key={form.id}>\n <TableCell className=\"font-medium\">\n <Link\n href={`${formsBasePath}/${form.id}`}\n className=\"text-primary hover:underline\"\n >\n {form.name}\n </Link>\n </TableCell>\n <TableCell className=\"text-muted-foreground\" data-forms-used-on={form.id}>\n {usedOn === null\n ? t('notYet')\n : t('usedOnPages', { count: usedOn.get(form.id) ?? 0 })}\n </TableCell>\n <TableCell className=\"text-right text-muted-foreground\">{t('notYet')}</TableCell>\n <TableCell className=\"text-right text-muted-foreground\">{t('notYet')}</TableCell>\n <TableCell className=\"text-right text-muted-foreground\">{t('notYet')}</TableCell>\n <TableCell>\n <WorkflowStatusBadge\n status={form.status === 'published' ? 'published' : 'draft'}\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n )}\n {total > items.length && (\n <p className=\"text-xs text-muted-foreground\">{t('truncated', { count: items.length })}</p>\n )}\n </div>\n )\n })\n}\n"],"mappings":"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"}
@@ -0,0 +1,184 @@
1
+ import { getFormsMessages } from "@murumets-ee/forms/i18n";
2
+
3
+ //#region ../admin-route/dist/index.d.mts
4
+ /**
5
+ * Spec passed to {@link defineFeature}.
6
+ *
7
+ * Plugin-level NON-CRUD resources whose permission grants aren't tied to a
8
+ * single route or entity — e.g. `ticketing.bulk-edit` with actions
9
+ * `['view', 'execute']`, or `commerce.imports` with `['view', 'export',
10
+ * 'cancel']`. Use `defineAdminRoute` for routes and `defineAdminPage` for
11
+ * pages; `defineFeature` is the catalog hook for everything that's neither.
12
+ *
13
+ * The resource string is the same shape as a `defineAdminRoute` permission's
14
+ * left side. Actions are the right side — one catalog entry is emitted per
15
+ * `(resource, action)` pair, exactly as if `defineAdminRoute` had been
16
+ * called once per action with a no-op handler.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * defineFeature({
21
+ * resource: 'ticketing.bulk-edit',
22
+ * actions: ['view', 'execute'],
23
+ * defaultRoles: ['admin', 'agent'],
24
+ * description: 'Operate ticketing bulk-edit tools',
25
+ * })
26
+ * ```
27
+ */
28
+ interface DefineFeatureSpec {
29
+ /**
30
+ * Resource string — left side of the permission. Must be non-empty and
31
+ * not contain `:` (the action separator). Convention: dot-notation for
32
+ * plugin-prefixed names (`'ticketing.bulk-edit'`, `'commerce.imports'`).
33
+ */
34
+ resource: string;
35
+ /**
36
+ * Actions on this resource. Each becomes a catalog entry
37
+ * `<resource>:<action>`. Must be non-empty; each action must be a
38
+ * non-empty string without `:`. Duplicates within the same call are
39
+ * rejected (would be silently deduped by `registerPermission` but the
40
+ * call-site shape suggests programmer error).
41
+ */
42
+ actions: readonly string[];
43
+ /**
44
+ * Built-in roles granted EVERY action on this resource when the role row is
45
+ * first written, and granted the RESOURCE on a later boot if the saved row
46
+ * does not carry it yet.
47
+ *
48
+ * ⚠️ Not "on every `upsertBuiltInRoles`": its step 2 merges at RESOURCE
49
+ * granularity, so adding a new ACTION to a resource a role already holds
50
+ * reaches fresh installs and NOT existing ones. That asymmetry is deliberate
51
+ * — it is what stops an upgrade from undoing an operator's per-action edits —
52
+ * and it means a new action needs a migration or an operator, not a redeploy.
53
+ *
54
+ * ✅ **This is the field that actually seeds** — it is the only one. A
55
+ * `defineAdminRoute`/`defineAdminPage` `defaultRoles` carrying the same role
56
+ * names grants nothing (see their docblocks), so a permission that should be
57
+ * held by a non-admin role needs an entry HERE, reached declaratively via
58
+ * `Plugin.shared.features[]`. Read off the plugin objects rather than the
59
+ * catalog, because `registerPermission` keeps the first registrant's
60
+ * `source` and a permission registered by a route first would look like a
61
+ * route contribution afterwards (editor plan F111/R046).
62
+ *
63
+ * ⚠️ **Exactly three role names are honoured: `authenticated`, `editor`,
64
+ * `agent`** — `PLUGIN_SEEDABLE_ROLES` in `@murumets-ee/auth`. Anything else
65
+ * here is silently ignored, which is worth knowing BEFORE you write it:
66
+ *
67
+ * - `admin` is a bypass flag in `buildPermissionChecker`, not a row, and
68
+ * `PATCH /permissions` refuses to modify one — a grant would be redundant
69
+ * and uneditable. Most declarations name it and lose nothing by doing so.
70
+ * - `customer` must never reach `/api/admin/*` at all, per
71
+ * `buildInitialRoleDefinitions`' own docblock.
72
+ * - `public` is the anonymous visitor; changing what a site publishes is not
73
+ * a decision a plugin makes on an operator's behalf.
74
+ * - `viewer` is named in 20+ `defaultRoles` arrays repo-wide and is not in
75
+ * `BUILT_IN_ROLES`. It does not exist (editor plan F113).
76
+ *
77
+ * A permission that genuinely needs one of those roles wants a review, not an
78
+ * array entry. Ask for it.
79
+ *
80
+ * Defaults to `[]`. The same default-roles set applies to every action — if
81
+ * different actions need different default grants, register separate
82
+ * `defineFeature` calls.
83
+ */
84
+ defaultRoles?: readonly string[];
85
+ /**
86
+ * Optional description for the Permission Matrix UI (PR-D) and audit
87
+ * logs. Plain-text, no markdown. Applied to every catalog entry the
88
+ * call produces; per-action descriptions need separate `defineFeature`
89
+ * calls (one per action).
90
+ */
91
+ description?: string;
92
+ }
93
+ /**
94
+ * Register a non-CRUD plugin resource in the permission catalog.
95
+ *
96
+ * **Plugin authors should NOT call this directly.** Contribute features
97
+ * declaratively via `Plugin.shared.features: DefineFeatureSpec[]` — the
98
+ * framework's merge step at boot iterates that array and calls this
99
+ * factory for each spec. The declarative path is the supported plugin-
100
+ * authoring API; direct invocation is reserved for tests / scripts /
101
+ * non-plugin call sites that need to populate the catalog explicitly.
102
+ *
103
+ * The returned `PermissionCatalogEntry[]` contains ONE entry per `(resource,
104
+ * action)` pair — the same shape every other catalog consumer (Permission
105
+ * Matrix UI, `buildResourceCatalog`) already understands. `upsertBuiltInRoles`
106
+ * is NOT among them: it reads `Plugin.shared.features[]` off the plugin
107
+ * objects, never the catalog, because `registerPermission` keeps the FIRST
108
+ * registrant's `source` and unions `defaultRoles` across all of them — so a
109
+ * feature registered by a route first is indistinguishable here from a route
110
+ * (editor plan R046). Each returned entry reflects the AUTHORITATIVE
111
+ * post-merge catalog state: on a fresh registration it equals the input
112
+ * spec's fields; on a repeat registration (something else already
113
+ * contributed the same `(resource, action)` pair) the entry carries the
114
+ * first registrant's `source` + `description` and the union of every
115
+ * registrant's `defaultRoles`. Callers consume the return value as a
116
+ * truthful snapshot, not the draft this call would have registered.
117
+ *
118
+ * Unlike {@link defineAdminRoute}, `defineFeature` does NOT produce a
119
+ * handler — its only contribution is the catalog registration. Use this
120
+ * for non-route, non-entity-action permissions that the toolkit's auth
121
+ * layer should know about: feature flags an admin can grant per-role,
122
+ * bulk operations that aren't a discrete HTTP endpoint, scheduled-job
123
+ * "run-now" capabilities, etc.
124
+ *
125
+ * **Why a wrapper exists when `registerPermission` already does the job:**
126
+ * the wrapper validates input shape (non-empty resource, non-empty
127
+ * actions, no `:` in either, no duplicate actions in the same call),
128
+ * splits a multi-action declaration into the right number of catalog
129
+ * entries, and sets `source: 'feature'` so the Matrix UI can group by
130
+ * declaration site. `registerPermission` is the lower-level primitive
131
+ * shared with `defineAdminRoute`; plugin code should reach for
132
+ * `defineFeature` instead.
133
+ *
134
+ * **Idempotency:** repeat calls for the same `(resource, action)` pair
135
+ * union their `defaultRoles` (same behavior as `defineAdminRoute`
136
+ * registering the same permission twice). Two plugins both declaring
137
+ * `'ticketing.bulk-edit:execute'` produce one catalog entry with the
138
+ * combined defaults.
139
+ *
140
+ * @example Plugin authoring (declarative — preferred):
141
+ * ```ts
142
+ * import type { Plugin } from '@murumets-ee/core'
143
+ *
144
+ * export function ticketingPlugin(): Plugin {
145
+ * return {
146
+ * name: '@app/ticketing',
147
+ * shared: {
148
+ * features: [
149
+ * {
150
+ * resource: 'ticketing.bulk-edit',
151
+ * actions: ['view', 'execute'],
152
+ * defaultRoles: ['admin', 'agent'],
153
+ * description: 'Operate ticketing bulk-edit tools',
154
+ * },
155
+ * ],
156
+ * },
157
+ * }
158
+ * }
159
+ * ```
160
+ *
161
+ * @example Tests / scripts (direct invocation):
162
+ * ```ts
163
+ * import { defineFeature } from '@murumets-ee/core'
164
+ *
165
+ * const entries = defineFeature({
166
+ * resource: 'ticketing.bulk-edit',
167
+ * actions: ['view', 'execute'],
168
+ * defaultRoles: ['admin', 'agent'],
169
+ * })
170
+ * // entries[0].source === 'feature'
171
+ * ```
172
+ */
173
+ //#endregion
174
+ //#region src/plugin.d.ts
175
+ declare function formsPlugin(): Pick<import("@murumets-ee/core").Plugin, "server" | "adminUi" | "postInstall" | "requires" | "contributes"> & {
176
+ readonly name: "@murumets-ee/forms";
177
+ readonly shared?: {
178
+ readonly features: readonly DefineFeatureSpec[];
179
+ readonly messages: typeof getFormsMessages;
180
+ };
181
+ };
182
+ //#endregion
183
+ export { formsPlugin };
184
+ //# sourceMappingURL=plugin.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.mts","names":["PermissionString","AdminRouteMethod","SegmentPath","P","DefineAdminRouteSpec","TApp","AdminRouteHandler","AdminRouteMcpSpec","prefix","path","method","permission","defaultRoles","description","descriptionKey","handler","matchAnyPath","tokens","mcp","confirm","JsonValue","key","McpInputSchema","type","properties","required","additionalProperties","inputSchema","ENTRY_GATED","AdminRouteEntry","resource","action","guardedHandler","acceptsTokens","requiresConfirmation","PermissionCatalogEntry","source","registerPermission","entry","getPermissionCatalog","Map","_resetPermissionCatalog","clearFeaturePermissions","safeAudit","Parameters","NonNullable","ctx","safeAuditMany","Promise","entries","PermissionDenialContext","Record","extraMetadata","emitPermissionDenied","spec","permissionDeniedResponse","Response","role","defineAdminRoute","DefineFeatureSpec","actions","defineFeature","combineAdminRoutes","AdminRoute","AuthDoor","TOKEN_DERIVED_DOORS","ReadonlySet","isTokenDerivedDoor","door","CredentialNarrowing","Readonly","CallerCredential","id","narrowing","rateLimitPerMinute","CREDENTIAL_RATE_BUDGET_MAX","parseCredentialRateBudget","raw","AdminApiCaller","U","ConfirmationPolicy","name","email","user","credential","confirmation","narrowCheck","base","GATED","AuthUser","AuditLogFn","entityType","entityId","userId","userName","changes","metadata","AuditLogManyFn","PermissionChecker","Request","req","segments","locale","defaultLocale","audit","auditMany","checkPermission","app","TokenPolicyEntry","handlers","GET","POST","PATCH","DELETE","tokenPolicy","matchTokenPolicyEntry","Pick","route","routeRequiresConfirmation","routeAcceptsTokens","isGatedRoute","value","AGENT_APPROVALS_RESOURCE","APPROVAL_STATUSES","ApprovalStatus","isApprovalStatus","DEFAULT_APPROVAL_TTL_MS","MAX_PARKED_BODY_BYTES","MAX_PENDING_PER_PRINCIPAL","APPROVAL_ID_PATTERN","RegExp","ApprovalPreview","kind","entity","ids","snapshot","ParkedOperation","Date","principalId","credentialId","query","body","preview","status","requestedAt","expiresAt","decidedBy","decidedAt","resultStatus","result","ParkInput","ParkOutcome","ok","reason","AgentApprovalQueue","park","input","now","get","list","limit","offset","options","items","total","countPending","claim","reject","finish","outcome","resumeCaller","op","sweep","staleExecutingMs","expired","interrupted","BuiltInEntityAction","suffix","behavior","requires","needsContentClient","BUILT_IN_ENTITY_ACTIONS","ADMIN_SITE_HEADER","ADMIN_SITE_QUERY_PARAM","ADMIN_SITE_SEGMENT_PREFIX","isAdminSiteHandleShape","parseAdminSiteSegment","segment","isAdminSiteSegment","adminSiteSegment","site","siteFromAdminPath","pathname","withAdminSite","href","stripAdminSite"],"sources":["../../admin-route/dist/index.d.mts","../src/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;UAqvBU2D,iBAAAA;;;;;;EAMR7B,QAAAA;;;;;;;;EAQA8B,OAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0CAhD,YAAAA;;;;;;;EAOAC,WAAAA;AAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBCtwBc,WAAA,CAAA,GAAW,IAAA,6BAAA,MAAA;EAAA;;gCAAA,iBAAA;IAAA"}
@@ -0,0 +1,2 @@
1
+ import{definePlugin as e}from"@murumets-ee/core";import{FORMS_MANAGE as t,Form as n,formsFeatures as r,formsRoutes as i}from"@murumets-ee/forms";import{getFormsMessages as a}from"@murumets-ee/forms/i18n";import{FormEmbedBlock as o,formEnvironment as s,formPageEnvironmentExtension as c,formSchemaBlocks as l}from"@murumets-ee/forms/schema";const u=async e=>(await import(`@murumets-ee/forms-ui/pages`)).FormsListPage(e),d=async e=>(await import(`@murumets-ee/forms-ui/pages`)).FormCanvasPage(e);function f(){return e({name:`@murumets-ee/forms`,requires:[`@murumets-ee/content`],server:{entities:[n],routes:[...i()]},contributes:{"blocks.blocks":[...l,o],"blocks.environmentBases":[s],"blocks.environments":c},shared:{features:r,messages:a},adminUi:{pages:{FormsListPage:u,FormCanvasPage:d},sidebar:[{id:`forms`,group:`Content`,label:`Forms`,href:`/admin/forms`,iconName:`clipboard-list`,requires:t}],defaultRoutes:[{path:`forms`,factory:`FormsListPage`,page:{requires:t,defaultRoles:[`admin`,`editor`],description:`List this site’s forms and create new ones.`,descriptionKey:`Forms.permissions.list`,tokens:`accept`}},{path:`forms/[id]`,factory:`FormCanvasPage`,page:{requires:t,defaultRoles:[`admin`,`editor`],description:`Edit a form on its canvas.`,descriptionKey:`Forms.permissions.canvas`,tokens:`accept`}},{path:`form`,factory:`FormsListPage`},{path:`form/[id]`,factory:`FormCanvasPage`}]}})}export{f as formsPlugin};
2
+ //# sourceMappingURL=plugin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * `formsPlugin()` — everything `@murumets-ee/forms` contributes, declared as\n * data on one Plugin object (CLAUDE.md \"all plugin contributions are\n * declarative\"). Drop it into `plugins:` and the framework wires:\n *\n * - the `form` entity (site-bound canvas document);\n * - the block vocabulary to the SERVER — `blocks.blocks` (so the tree walk\n * that judges every write knows `ancestor` / `uniqueKey`), the `form`\n * environment base (so the canvas resolves its palette), and the Form block\n * in the site's `page` environment (D002 — a saved form on any page);\n * - the admin route behind \"Detach a copy\";\n * - the permissions — `forms:manage` and the `form` entity grants — to the\n * Permission Matrix, seeded for `editor`;\n * - the catalogue (`shared.messages`);\n * - the Forms list and the form canvas as pages, and the sidebar link.\n *\n * The browser half — renders and the panel's field editors — is\n * `<FormBlocksRegistration />` from `@murumets-ee/forms-ui`, mounted by the\n * admin layout (the standing client-registry exception in CLAUDE.md).\n *\n * jiti-safe: this module is reached from `lumi.config.ts`, so the pages load\n * lazily and nothing here imports JSX or the blocks root.\n */\n\n// The `blocks.*` contribution slots are declared by module augmentation in\n// `@murumets-ee/blocks`; the type-only import is what brings them into scope.\nimport type {} from '@murumets-ee/blocks'\nimport { definePlugin, type PageFactory } from '@murumets-ee/core'\nimport { FORMS_MANAGE, Form, formsFeatures, formsRoutes } from '@murumets-ee/forms'\nimport { getFormsMessages } from '@murumets-ee/forms/i18n'\nimport {\n FormEmbedBlock,\n formEnvironment,\n formPageEnvironmentExtension,\n formSchemaBlocks,\n} from '@murumets-ee/forms/schema'\n\nconst FormsListPage: PageFactory<{ locale: string }> = async (props) => {\n const mod = await import('@murumets-ee/forms-ui/pages')\n return mod.FormsListPage(props)\n}\nconst FormCanvasPage: PageFactory<{ locale: string; id: string }> = async (props) => {\n const mod = await import('@murumets-ee/forms-ui/pages')\n return mod.FormCanvasPage(props)\n}\n\nexport function formsPlugin() {\n return definePlugin({\n name: '@murumets-ee/forms',\n // `publishable()` keeps its drafts in tables the content plugin owns.\n requires: ['@murumets-ee/content'],\n server: {\n entities: [Form],\n // `POST /api/admin/form-copy` — the Form block's \"Detach a copy\".\n routes: [...formsRoutes()],\n },\n contributes: {\n // The form document's blocks, and the page's Form block (D002).\n 'blocks.blocks': [...formSchemaBlocks, FormEmbedBlock],\n 'blocks.environmentBases': [formEnvironment],\n // The site's pages may hold a Form block, and the page environment\n // holds the form's blocks too, because a page DRAWS an embedded form\n // with the site theme (see `formPageEnvironmentExtension`).\n 'blocks.environments': formPageEnvironmentExtension,\n },\n shared: {\n features: formsFeatures,\n messages: getFormsMessages,\n },\n adminUi: {\n pages: {\n FormsListPage,\n FormCanvasPage,\n },\n sidebar: [\n {\n id: 'forms',\n group: 'Content',\n label: 'Forms',\n href: '/admin/forms',\n iconName: 'clipboard-list',\n requires: FORMS_MANAGE,\n },\n ],\n defaultRoutes: [\n {\n // Owns the `form` entity's list URL — the generic table never shows.\n path: 'forms',\n factory: 'FormsListPage',\n page: {\n requires: FORMS_MANAGE,\n defaultRoles: ['admin', 'editor'],\n description: 'List this site’s forms and create new ones.',\n descriptionKey: 'Forms.permissions.list',\n // An agent's browser session may open it (agent-access D020) —\n // the same reach its key already has to `form` through the API,\n // still behind its role's `forms:manage`.\n tokens: 'accept',\n },\n },\n {\n // Owns the `form` entity's edit URL — the canvas, gated by the job.\n path: 'forms/[id]',\n factory: 'FormCanvasPage',\n page: {\n requires: FORMS_MANAGE,\n defaultRoles: ['admin', 'editor'],\n description: 'Edit a form on its canvas.',\n descriptionKey: 'Forms.permissions.canvas',\n tokens: 'accept',\n },\n },\n // The SINGULAR slugs resolve to the same entity (`/admin/form/<id>`),\n // so they are owned too — otherwise the generic editor would answer\n // them behind `form:*` alone, and a form would have two addresses.\n // No `page` spec: the gate wraps the FACTORY once (above), so every\n // path that renders it is gated the same way.\n { path: 'form', factory: 'FormsListPage' },\n { path: 'form/[id]', factory: 'FormCanvasPage' },\n ],\n },\n })\n}\n"],"mappings":"oVAqCA,MAAM,EAAiD,KAAO,KAErD,MADW,OAAO,+BAAA,CACd,cAAc,CAAK,EAE1B,EAA8D,KAAO,KAElE,MADW,OAAO,+BAAA,CACd,eAAe,CAAK,EAGjC,SAAgB,GAAc,CAC5B,OAAO,EAAa,CAClB,KAAM,qBAEN,SAAU,CAAC,sBAAsB,EACjC,OAAQ,CACN,SAAU,CAAC,CAAI,EAEf,OAAQ,CAAC,GAAG,EAAY,CAAC,CAC3B,EACA,YAAa,CAEX,gBAAiB,CAAC,GAAG,EAAkB,CAAc,EACrD,0BAA2B,CAAC,CAAe,EAI3C,sBAAuB,CACzB,EACA,OAAQ,CACN,SAAU,EACV,SAAU,CACZ,EACA,QAAS,CACP,MAAO,CACL,gBACA,gBACF,EACA,QAAS,CACP,CACE,GAAI,QACJ,MAAO,UACP,MAAO,QACP,KAAM,eACN,SAAU,iBACV,SAAU,CACZ,CACF,EACA,cAAe,CACb,CAEE,KAAM,QACN,QAAS,gBACT,KAAM,CACJ,SAAU,EACV,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,8CACb,eAAgB,yBAIhB,OAAQ,QACV,CACF,EACA,CAEE,KAAM,aACN,QAAS,iBACT,KAAM,CACJ,SAAU,EACV,aAAc,CAAC,QAAS,QAAQ,EAChC,YAAa,6BACb,eAAgB,2BAChB,OAAQ,QACV,CACF,EAMA,CAAE,KAAM,OAAQ,QAAS,eAAgB,EACzC,CAAE,KAAM,YAAa,QAAS,gBAAiB,CACjD,CACF,CACF,CAAC,CACH"}
package/package.json CHANGED
@@ -1,6 +1,73 @@
1
1
  {
2
2
  "name": "@murumets-ee/forms-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.97.0",
4
+ "license": "Elastic-2.0",
5
+ "type": "module",
6
+ "description": "Admin half of @murumets-ee/forms: the form canvas registration, the Forms list, the New form dialog and the plugin.",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.mts",
10
+ "import": "./dist/index.mjs"
11
+ },
12
+ "./pages": {
13
+ "types": "./dist/pages.d.mts",
14
+ "import": "./dist/pages.mjs"
15
+ },
16
+ "./plugin": {
17
+ "types": "./dist/plugin.d.mts",
18
+ "import": "./dist/plugin.mjs"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "dependencies": {
25
+ "@murumets-ee/admin-ui": "0.97.0",
26
+ "@murumets-ee/content": "0.97.0",
27
+ "@murumets-ee/forms": "0.97.0",
28
+ "@murumets-ee/core": "0.97.0",
29
+ "@murumets-ee/blocks": "0.97.0",
30
+ "@murumets-ee/ui": "0.97.0"
31
+ },
32
+ "peerDependencies": {
33
+ "next": ">=16.0.0 <17",
34
+ "next-intl": ">=4.0.0 <5",
35
+ "react": ">=19.0.0 <20",
36
+ "react-dom": ">=19.0.0 <20"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "next": {
40
+ "optional": true
41
+ },
42
+ "next-intl": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "devDependencies": {
47
+ "@testing-library/react": "^16.3.2",
48
+ "@types/react": "^19",
49
+ "@types/react-dom": "^19",
50
+ "happy-dom": "^20.10.1",
51
+ "next": "16.3.0",
52
+ "next-intl": "^4.13.0",
53
+ "react": "19.2.7",
54
+ "react-dom": "19.2.7",
55
+ "tsdown": "^0.22.2",
56
+ "typescript": "^5.7.3",
57
+ "vitest": "^2.1.8"
58
+ },
59
+ "typeCoverage": {
60
+ "atLeast": 100
61
+ },
62
+ "bundleBudget": {
63
+ "dist/index.mjs": 16384,
64
+ "dist/pages.mjs": 8192,
65
+ "dist/plugin.mjs": 5120
66
+ },
67
+ "scripts": {
68
+ "build": "tsdown",
69
+ "dev": "tsdown --watch",
70
+ "test": "vitest run",
71
+ "test:watch": "vitest"
72
+ }
6
73
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.