@entifix/react-controls 0.20.0 → 0.20.2

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.
@@ -2,4 +2,5 @@ export * from './action-surfaces.js';
2
2
  export * from './error-actions.js';
3
3
  export * from './render-link.js';
4
4
  export * from './shortcut.js';
5
+ export * from './use-verb-invoker.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC"}
@@ -2,5 +2,6 @@ export * from './action-surfaces.js';
2
2
  export * from './error-actions.js';
3
3
  export * from './render-link.js';
4
4
  export * from './shortcut.js';
5
+ export * from './use-verb-invoker.js';
5
6
 
6
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/actions/index.ts"],"sourcesContent":["export * from './action-surfaces.js';\nexport * from './error-actions.js';\nexport * from './render-link.js';\nexport * from './shortcut.js';\n"],"names":[],"mappings":"AAAA,cAAc,uBAAuB;AACrC,cAAc,qBAAqB;AACnC,cAAc,mBAAmB;AACjC,cAAc,gBAAgB"}
1
+ {"version":3,"sources":["../../../src/ui/actions/index.ts"],"sourcesContent":["export * from './action-surfaces.js';\nexport * from './error-actions.js';\nexport * from './render-link.js';\nexport * from './shortcut.js';\nexport * from './use-verb-invoker.js';\n"],"names":[],"mappings":"AAAA,cAAc,uBAAuB;AACrC,cAAc,qBAAqB;AACnC,cAAc,mBAAmB;AACjC,cAAc,gBAAgB;AAC9B,cAAc,wBAAwB"}
@@ -45,6 +45,11 @@ export interface VerbInvoker<TTarget> {
45
45
  * count, and the button repeats the verb with the count;
46
46
  * - `typed`, or a `matching` selection over {@link TYPED_SELECTION_THRESHOLD}
47
47
  * — the same dialog, and the name or the count has to be typed first.
48
+ *
49
+ * Exported for a host's own surface — a screen that is neither a form nor a
50
+ * table, rendering declared verbs on an `ActionBar` — so it reaches a verb
51
+ * through the same funnel EntityForm and EntityTable use rather than
52
+ * re-implementing the dialog's rules.
48
53
  */
49
54
  export declare function useVerbInvoker<TTarget>({ run, busy, typedThreshold, served, }: VerbInvokerOptions<TTarget>): VerbInvoker<TTarget>;
50
55
  //# sourceMappingURL=use-verb-invoker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-verb-invoker.d.ts","sourceRoot":"","sources":["../../../src/ui/actions/use-verb-invoker.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAC;AAMjD;;;GAGG;AACH,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,kDAAkD;AAClD,MAAM,WAAW,WAAW,CAAC,OAAO;IAClC,QAAQ,CAAC,UAAU,EAAE,IAAI,CACvB,iBAAiB,EACjB,KAAK,GAAG,UAAU,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAC1D,CAAC;IACF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB,CAAC,OAAO;IACzC,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACrD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,IAAI,CAAC,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;CAC7D;AAED,MAAM,WAAW,WAAW,CAAC,OAAO;IAClC,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IACzD,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,EACtC,GAAG,EACH,IAAY,EACZ,cAA0C,EAC1C,MAAM,GACP,EAAE,kBAAkB,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,CAyEpD"}
1
+ {"version":3,"file":"use-verb-invoker.d.ts","sourceRoot":"","sources":["../../../src/ui/actions/use-verb-invoker.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAC;AAMjD;;;GAGG;AACH,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,kDAAkD;AAClD,MAAM,WAAW,WAAW,CAAC,OAAO;IAClC,QAAQ,CAAC,UAAU,EAAE,IAAI,CACvB,iBAAiB,EACjB,KAAK,GAAG,UAAU,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAC1D,CAAC;IACF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB,CAAC,OAAO;IACzC,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACrD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,IAAI,CAAC,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;CAC7D;AAED,MAAM,WAAW,WAAW,CAAC,OAAO;IAClC,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IACzD,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,EACtC,GAAG,EACH,IAAY,EACZ,cAA0C,EAC1C,MAAM,GACP,EAAE,kBAAkB,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,CAyEpD"}
@@ -20,6 +20,11 @@ import { useToasts } from '../molecules/toast-region/index.js';
20
20
  * count, and the button repeats the verb with the count;
21
21
  * - `typed`, or a `matching` selection over {@link TYPED_SELECTION_THRESHOLD}
22
22
  * — the same dialog, and the name or the count has to be typed first.
23
+ *
24
+ * Exported for a host's own surface — a screen that is neither a form nor a
25
+ * table, rendering declared verbs on an `ActionBar` — so it reaches a verb
26
+ * through the same funnel EntityForm and EntityTable use rather than
27
+ * re-implementing the dialog's rules.
23
28
  */ export function useVerbInvoker({ run, busy = false, typedThreshold = TYPED_SELECTION_THRESHOLD, served }) {
24
29
  var _confirm_confirmLabelKey;
25
30
  const t = useT();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/actions/use-verb-invoker.tsx"],"sourcesContent":["'use client';\n\nimport type { UseCaseDescriptor } from '@entifix/core';\nimport { type ReactNode, useState } from 'react';\n\nimport { useT, useTranslateKey } from '../../i18n/index.js';\nimport { ConfirmDialog } from '../molecules/confirm-dialog/index.js';\nimport { useToasts } from '../molecules/toast-region/index.js';\n\n/**\n * A `matching` selection larger than this asks for the count to be typed\n * before a destructive verb runs over it.\n */\nexport const TYPED_SELECTION_THRESHOLD = 1000;\n\n/** One verb about to run, and what it runs on. */\nexport interface VerbRequest<TTarget> {\n readonly descriptor: Pick<\n UseCaseDescriptor,\n 'key' | 'labelKey' | 'confirm' | 'reversible' | 'inverse'\n >;\n readonly target: TTarget;\n /** What it acts on, as the dialog names it: \"Acme\", \"12 invoices\". */\n readonly object: string;\n /** How many records; 1 for a single record. */\n readonly count: number;\n /** A selection of everything a filter matches, rather than ticked rows. */\n readonly matching?: boolean;\n}\n\nexport interface VerbInvokerOptions<TTarget> {\n /** Runs a verb — the descriptor's own, or its inverse on Undo — by key. */\n readonly run: (key: string, target: TTarget) => void;\n readonly busy?: boolean;\n readonly typedThreshold?: number;\n /**\n * The verbs this caller may run — the served document's `useCases`, which\n * leave out what the caller holds no grant for. Undo is offered only when\n * its `inverse` is one of them. Without it, every verb may run.\n */\n readonly served?: readonly Pick<UseCaseDescriptor, 'key'>[];\n}\n\nexport interface VerbInvoker<TTarget> {\n readonly invoke: (request: VerbRequest<TTarget>) => void;\n /** The confirmation, while one is open. Render it once, anywhere. */\n readonly dialog: ReactNode;\n}\n\n/**\n * Where every surface sends a verb, so none can reach one around its safety\n * ([ADR 0026](../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)):\n *\n * - no `confirm` — it runs;\n * - `reversible: 'undo'` — it runs, and a toast offers Undo, which runs its\n * `inverse`. Without a toast region to offer Undo in, or an inverse this\n * caller may run, it asks instead;\n * - `confirm` — an alert dialog asks a question naming the object and the\n * count, and the button repeats the verb with the count;\n * - `typed`, or a `matching` selection over {@link TYPED_SELECTION_THRESHOLD}\n * — the same dialog, and the name or the count has to be typed first.\n */\nexport function useVerbInvoker<TTarget>({\n run,\n busy = false,\n typedThreshold = TYPED_SELECTION_THRESHOLD,\n served,\n}: VerbInvokerOptions<TTarget>): VerbInvoker<TTarget> {\n const t = useT();\n const translateKey = useTranslateKey();\n const toasts = useToasts();\n const [pending, setPending] = useState<VerbRequest<TTarget> | undefined>(\n undefined,\n );\n\n const invoke = (request: VerbRequest<TTarget>) => {\n const { descriptor, target, object } = request;\n const { confirm, inverse } = descriptor;\n if (confirm === undefined) {\n run(descriptor.key, target);\n return;\n }\n if (\n descriptor.reversible === 'undo' &&\n inverse !== undefined &&\n (served === undefined ||\n served.some(useCase => useCase.key === inverse)) &&\n toasts !== undefined\n ) {\n run(descriptor.key, target);\n const verb = translateKey(descriptor.labelKey);\n toasts.show({\n tone: 'undo',\n message:\n confirm.doneKey === undefined\n ? t('confirm.done', { verb, object })\n : translateKey(confirm.doneKey, { object, count: request.count }),\n action: {\n label: t('confirm.undo'),\n onAction: () => run(inverse, target),\n },\n });\n return;\n }\n setPending(request);\n };\n\n if (pending === undefined) return { invoke, dialog: undefined };\n\n const { descriptor, object, count } = pending;\n const confirm = descriptor.confirm as NonNullable<\n UseCaseDescriptor['confirm']\n >;\n const verb = translateKey(confirm.confirmLabelKey ?? descriptor.labelKey);\n const typed =\n descriptor.reversible === 'typed' ||\n (pending.matching === true && count > typedThreshold);\n\n return {\n invoke,\n dialog: (\n <ConfirmDialog\n open\n tone={confirm.tone}\n title={t('confirm.question', {\n verb: translateKey(descriptor.labelKey),\n object,\n })}\n message={translateKey(confirm.messageKey, { object, count })}\n confirmLabel={t('confirm.verbCount', { verb, count })}\n typed={typed ? (count > 1 ? String(count) : object) : undefined}\n busy={busy}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n setPending(undefined);\n run(descriptor.key, pending.target);\n }}\n />\n ),\n };\n}\n"],"names":["useState","useT","useTranslateKey","ConfirmDialog","useToasts","TYPED_SELECTION_THRESHOLD","useVerbInvoker","run","busy","typedThreshold","served","confirm","t","translateKey","toasts","pending","setPending","undefined","invoke","request","descriptor","target","object","inverse","key","reversible","some","useCase","verb","labelKey","show","tone","message","doneKey","count","action","label","onAction","dialog","confirmLabelKey","typed","matching","open","title","messageKey","confirmLabel","String","onCancel","onConfirm"],"mappings":"AAAA;;AAGA,SAAyBA,QAAQ,QAAQ,QAAQ;AAEjD,SAASC,IAAI,EAAEC,eAAe,QAAQ,sBAAsB;AAC5D,SAASC,aAAa,QAAQ,uCAAuC;AACrE,SAASC,SAAS,QAAQ,qCAAqC;AAE/D;;;CAGC,GACD,OAAO,MAAMC,4BAA4B,KAAK;AAoC9C;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,eAAwB,EACtCC,GAAG,EACHC,OAAO,KAAK,EACZC,iBAAiBJ,yBAAyB,EAC1CK,MAAM,EACsB;QA8CFC;IA7C1B,MAAMC,IAAIX;IACV,MAAMY,eAAeX;IACrB,MAAMY,SAASV;IACf,MAAM,CAACW,SAASC,WAAW,GAAGhB,SAC5BiB;IAGF,MAAMC,SAAS,CAACC;QACd,MAAM,EAAEC,UAAU,EAAEC,MAAM,EAAEC,MAAM,EAAE,GAAGH;QACvC,MAAM,EAAER,OAAO,EAAEY,OAAO,EAAE,GAAGH;QAC7B,IAAIT,YAAYM,WAAW;YACzBV,IAAIa,WAAWI,GAAG,EAAEH;YACpB;QACF;QACA,IACED,WAAWK,UAAU,KAAK,UAC1BF,YAAYN,aACXP,CAAAA,WAAWO,aACVP,OAAOgB,IAAI,CAACC,CAAAA,UAAWA,QAAQH,GAAG,KAAKD,QAAO,KAChDT,WAAWG,WACX;YACAV,IAAIa,WAAWI,GAAG,EAAEH;YACpB,MAAMO,OAAOf,aAAaO,WAAWS,QAAQ;YAC7Cf,OAAOgB,IAAI,CAAC;gBACVC,MAAM;gBACNC,SACErB,QAAQsB,OAAO,KAAKhB,YAChBL,EAAE,gBAAgB;oBAAEgB;oBAAMN;gBAAO,KACjCT,aAAaF,QAAQsB,OAAO,EAAE;oBAAEX;oBAAQY,OAAOf,QAAQe,KAAK;gBAAC;gBACnEC,QAAQ;oBACNC,OAAOxB,EAAE;oBACTyB,UAAU,IAAM9B,IAAIgB,SAASF;gBAC/B;YACF;YACA;QACF;QACAL,WAAWG;IACb;IAEA,IAAIJ,YAAYE,WAAW,OAAO;QAAEC;QAAQoB,QAAQrB;IAAU;IAE9D,MAAM,EAAEG,UAAU,EAAEE,MAAM,EAAEY,KAAK,EAAE,GAAGnB;IACtC,MAAMJ,UAAUS,WAAWT,OAAO;IAGlC,MAAMiB,OAAOf,cAAaF,2BAAAA,QAAQ4B,eAAe,YAAvB5B,2BAA2BS,WAAWS,QAAQ;IACxE,MAAMW,QACJpB,WAAWK,UAAU,KAAK,WACzBV,QAAQ0B,QAAQ,KAAK,QAAQP,QAAQzB;IAExC,OAAO;QACLS;QACAoB,sBACE,KAACnC;YACCuC,IAAI;YACJX,MAAMpB,QAAQoB,IAAI;YAClBY,OAAO/B,EAAE,oBAAoB;gBAC3BgB,MAAMf,aAAaO,WAAWS,QAAQ;gBACtCP;YACF;YACAU,SAASnB,aAAaF,QAAQiC,UAAU,EAAE;gBAAEtB;gBAAQY;YAAM;YAC1DW,cAAcjC,EAAE,qBAAqB;gBAAEgB;gBAAMM;YAAM;YACnDM,OAAOA,QAASN,QAAQ,IAAIY,OAAOZ,SAASZ,SAAUL;YACtDT,MAAMA;YACNuC,UAAU,IAAM/B,WAAWC;YAC3B+B,WAAW;gBACThC,WAAWC;gBACXV,IAAIa,WAAWI,GAAG,EAAET,QAAQM,MAAM;YACpC;;IAGN;AACF"}
1
+ {"version":3,"sources":["../../../src/ui/actions/use-verb-invoker.tsx"],"sourcesContent":["'use client';\n\nimport type { UseCaseDescriptor } from '@entifix/core';\nimport { type ReactNode, useState } from 'react';\n\nimport { useT, useTranslateKey } from '../../i18n/index.js';\nimport { ConfirmDialog } from '../molecules/confirm-dialog/index.js';\nimport { useToasts } from '../molecules/toast-region/index.js';\n\n/**\n * A `matching` selection larger than this asks for the count to be typed\n * before a destructive verb runs over it.\n */\nexport const TYPED_SELECTION_THRESHOLD = 1000;\n\n/** One verb about to run, and what it runs on. */\nexport interface VerbRequest<TTarget> {\n readonly descriptor: Pick<\n UseCaseDescriptor,\n 'key' | 'labelKey' | 'confirm' | 'reversible' | 'inverse'\n >;\n readonly target: TTarget;\n /** What it acts on, as the dialog names it: \"Acme\", \"12 invoices\". */\n readonly object: string;\n /** How many records; 1 for a single record. */\n readonly count: number;\n /** A selection of everything a filter matches, rather than ticked rows. */\n readonly matching?: boolean;\n}\n\nexport interface VerbInvokerOptions<TTarget> {\n /** Runs a verb — the descriptor's own, or its inverse on Undo — by key. */\n readonly run: (key: string, target: TTarget) => void;\n readonly busy?: boolean;\n readonly typedThreshold?: number;\n /**\n * The verbs this caller may run — the served document's `useCases`, which\n * leave out what the caller holds no grant for. Undo is offered only when\n * its `inverse` is one of them. Without it, every verb may run.\n */\n readonly served?: readonly Pick<UseCaseDescriptor, 'key'>[];\n}\n\nexport interface VerbInvoker<TTarget> {\n readonly invoke: (request: VerbRequest<TTarget>) => void;\n /** The confirmation, while one is open. Render it once, anywhere. */\n readonly dialog: ReactNode;\n}\n\n/**\n * Where every surface sends a verb, so none can reach one around its safety\n * ([ADR 0026](../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)):\n *\n * - no `confirm` — it runs;\n * - `reversible: 'undo'` — it runs, and a toast offers Undo, which runs its\n * `inverse`. Without a toast region to offer Undo in, or an inverse this\n * caller may run, it asks instead;\n * - `confirm` — an alert dialog asks a question naming the object and the\n * count, and the button repeats the verb with the count;\n * - `typed`, or a `matching` selection over {@link TYPED_SELECTION_THRESHOLD}\n * — the same dialog, and the name or the count has to be typed first.\n *\n * Exported for a host's own surface — a screen that is neither a form nor a\n * table, rendering declared verbs on an `ActionBar` — so it reaches a verb\n * through the same funnel EntityForm and EntityTable use rather than\n * re-implementing the dialog's rules.\n */\nexport function useVerbInvoker<TTarget>({\n run,\n busy = false,\n typedThreshold = TYPED_SELECTION_THRESHOLD,\n served,\n}: VerbInvokerOptions<TTarget>): VerbInvoker<TTarget> {\n const t = useT();\n const translateKey = useTranslateKey();\n const toasts = useToasts();\n const [pending, setPending] = useState<VerbRequest<TTarget> | undefined>(\n undefined,\n );\n\n const invoke = (request: VerbRequest<TTarget>) => {\n const { descriptor, target, object } = request;\n const { confirm, inverse } = descriptor;\n if (confirm === undefined) {\n run(descriptor.key, target);\n return;\n }\n if (\n descriptor.reversible === 'undo' &&\n inverse !== undefined &&\n (served === undefined ||\n served.some(useCase => useCase.key === inverse)) &&\n toasts !== undefined\n ) {\n run(descriptor.key, target);\n const verb = translateKey(descriptor.labelKey);\n toasts.show({\n tone: 'undo',\n message:\n confirm.doneKey === undefined\n ? t('confirm.done', { verb, object })\n : translateKey(confirm.doneKey, { object, count: request.count }),\n action: {\n label: t('confirm.undo'),\n onAction: () => run(inverse, target),\n },\n });\n return;\n }\n setPending(request);\n };\n\n if (pending === undefined) return { invoke, dialog: undefined };\n\n const { descriptor, object, count } = pending;\n const confirm = descriptor.confirm as NonNullable<\n UseCaseDescriptor['confirm']\n >;\n const verb = translateKey(confirm.confirmLabelKey ?? descriptor.labelKey);\n const typed =\n descriptor.reversible === 'typed' ||\n (pending.matching === true && count > typedThreshold);\n\n return {\n invoke,\n dialog: (\n <ConfirmDialog\n open\n tone={confirm.tone}\n title={t('confirm.question', {\n verb: translateKey(descriptor.labelKey),\n object,\n })}\n message={translateKey(confirm.messageKey, { object, count })}\n confirmLabel={t('confirm.verbCount', { verb, count })}\n typed={typed ? (count > 1 ? String(count) : object) : undefined}\n busy={busy}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n setPending(undefined);\n run(descriptor.key, pending.target);\n }}\n />\n ),\n };\n}\n"],"names":["useState","useT","useTranslateKey","ConfirmDialog","useToasts","TYPED_SELECTION_THRESHOLD","useVerbInvoker","run","busy","typedThreshold","served","confirm","t","translateKey","toasts","pending","setPending","undefined","invoke","request","descriptor","target","object","inverse","key","reversible","some","useCase","verb","labelKey","show","tone","message","doneKey","count","action","label","onAction","dialog","confirmLabelKey","typed","matching","open","title","messageKey","confirmLabel","String","onCancel","onConfirm"],"mappings":"AAAA;;AAGA,SAAyBA,QAAQ,QAAQ,QAAQ;AAEjD,SAASC,IAAI,EAAEC,eAAe,QAAQ,sBAAsB;AAC5D,SAASC,aAAa,QAAQ,uCAAuC;AACrE,SAASC,SAAS,QAAQ,qCAAqC;AAE/D;;;CAGC,GACD,OAAO,MAAMC,4BAA4B,KAAK;AAoC9C;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASC,eAAwB,EACtCC,GAAG,EACHC,OAAO,KAAK,EACZC,iBAAiBJ,yBAAyB,EAC1CK,MAAM,EACsB;QA8CFC;IA7C1B,MAAMC,IAAIX;IACV,MAAMY,eAAeX;IACrB,MAAMY,SAASV;IACf,MAAM,CAACW,SAASC,WAAW,GAAGhB,SAC5BiB;IAGF,MAAMC,SAAS,CAACC;QACd,MAAM,EAAEC,UAAU,EAAEC,MAAM,EAAEC,MAAM,EAAE,GAAGH;QACvC,MAAM,EAAER,OAAO,EAAEY,OAAO,EAAE,GAAGH;QAC7B,IAAIT,YAAYM,WAAW;YACzBV,IAAIa,WAAWI,GAAG,EAAEH;YACpB;QACF;QACA,IACED,WAAWK,UAAU,KAAK,UAC1BF,YAAYN,aACXP,CAAAA,WAAWO,aACVP,OAAOgB,IAAI,CAACC,CAAAA,UAAWA,QAAQH,GAAG,KAAKD,QAAO,KAChDT,WAAWG,WACX;YACAV,IAAIa,WAAWI,GAAG,EAAEH;YACpB,MAAMO,OAAOf,aAAaO,WAAWS,QAAQ;YAC7Cf,OAAOgB,IAAI,CAAC;gBACVC,MAAM;gBACNC,SACErB,QAAQsB,OAAO,KAAKhB,YAChBL,EAAE,gBAAgB;oBAAEgB;oBAAMN;gBAAO,KACjCT,aAAaF,QAAQsB,OAAO,EAAE;oBAAEX;oBAAQY,OAAOf,QAAQe,KAAK;gBAAC;gBACnEC,QAAQ;oBACNC,OAAOxB,EAAE;oBACTyB,UAAU,IAAM9B,IAAIgB,SAASF;gBAC/B;YACF;YACA;QACF;QACAL,WAAWG;IACb;IAEA,IAAIJ,YAAYE,WAAW,OAAO;QAAEC;QAAQoB,QAAQrB;IAAU;IAE9D,MAAM,EAAEG,UAAU,EAAEE,MAAM,EAAEY,KAAK,EAAE,GAAGnB;IACtC,MAAMJ,UAAUS,WAAWT,OAAO;IAGlC,MAAMiB,OAAOf,cAAaF,2BAAAA,QAAQ4B,eAAe,YAAvB5B,2BAA2BS,WAAWS,QAAQ;IACxE,MAAMW,QACJpB,WAAWK,UAAU,KAAK,WACzBV,QAAQ0B,QAAQ,KAAK,QAAQP,QAAQzB;IAExC,OAAO;QACLS;QACAoB,sBACE,KAACnC;YACCuC,IAAI;YACJX,MAAMpB,QAAQoB,IAAI;YAClBY,OAAO/B,EAAE,oBAAoB;gBAC3BgB,MAAMf,aAAaO,WAAWS,QAAQ;gBACtCP;YACF;YACAU,SAASnB,aAAaF,QAAQiC,UAAU,EAAE;gBAAEtB;gBAAQY;YAAM;YAC1DW,cAAcjC,EAAE,qBAAqB;gBAAEgB;gBAAMM;YAAM;YACnDM,OAAOA,QAASN,QAAQ,IAAIY,OAAOZ,SAASZ,SAAUL;YACtDT,MAAMA;YACNuC,UAAU,IAAM/B,WAAWC;YAC3B+B,WAAW;gBACThC,WAAWC;gBACXV,IAAIa,WAAWI,GAAG,EAAET,QAAQM,MAAM;YACpC;;IAGN;AACF"}
@@ -1,8 +1,6 @@
1
- export interface QuantityInputProps {
1
+ interface QuantityInputBase {
2
2
  /** What is being counted; names the spinbutton and both of its buttons. */
3
3
  label: string;
4
- value: number;
5
- onChange: (value: number) => void;
6
4
  min?: number;
7
5
  max?: number;
8
6
  step?: number;
@@ -17,6 +15,24 @@ export interface QuantityInputProps {
17
15
  name?: string;
18
16
  className?: string;
19
17
  }
18
+ /** The caller owns the count: every committed change comes back through `onChange`. */
19
+ export interface ControlledQuantityInputProps extends QuantityInputBase {
20
+ value: number;
21
+ onChange: (value: number) => void;
22
+ defaultValue?: never;
23
+ }
24
+ /**
25
+ * The field owns the count, starting at `defaultValue` (or `min`). This is the
26
+ * shape a server-rendered form takes: a server component cannot hand a client
27
+ * one a function, and with `name` the count posts with the form, so the page
28
+ * needs no client parent of its own. `onChange` still reports each commit.
29
+ */
30
+ export interface UncontrolledQuantityInputProps extends QuantityInputBase {
31
+ value?: never;
32
+ defaultValue?: number;
33
+ onChange?: (value: number) => void;
34
+ }
35
+ export type QuantityInputProps = ControlledQuantityInputProps | UncontrolledQuantityInputProps;
20
36
  /**
21
37
  * A count, on the WAI-ARIA spinbutton pattern: Up and Down step it, Home and
22
38
  * End jump to its bounds, and what is typed is committed on Enter or on
@@ -24,6 +40,11 @@ export interface QuantityInputProps {
24
40
  * "Decrease {label}" and "Increase {label}" and sit at `--target-min`; they are
25
41
  * out of the Tab order, as the pattern has them, because the arrow keys
26
42
  * already do what they do.
43
+ *
44
+ * Controlled with `value` and `onChange`, or uncontrolled with `defaultValue`
45
+ * — the second is how a Server Action form uses it, since a server component
46
+ * can pass neither a state nor a handler.
27
47
  */
28
- export declare function QuantityInput({ label, value, onChange, min, max, step, disabled, id, name, className, }: QuantityInputProps): import("react").JSX.Element;
48
+ export declare function QuantityInput({ label, value: controlled, defaultValue, onChange, min, max, step, disabled, id, name, className, }: QuantityInputProps): import("react").JSX.Element;
49
+ export {};
29
50
  //# sourceMappingURL=quantity-input.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"quantity-input.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,GAAO,EACP,GAAG,EACH,IAAQ,EACR,QAAgB,EAChB,EAAE,EACF,IAAI,EACJ,SAAS,GACV,EAAE,kBAAkB,+BA8EpB"}
1
+ {"version":3,"file":"quantity-input.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"names":[],"mappings":"AAOA,UAAU,iBAAiB;IACzB,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,uFAAuF;AACvF,MAAM,WAAW,4BAA6B,SAAQ,iBAAiB;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,YAAY,CAAC,EAAE,KAAK,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,8BAA+B,SAAQ,iBAAiB;IACvE,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED,MAAM,MAAM,kBAAkB,GAC5B,4BAA4B,GAAG,8BAA8B,CAAC;AAKhE;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,KAAK,EAAE,UAAU,EACjB,YAAY,EACZ,QAAQ,EACR,GAAO,EACP,GAAG,EACH,IAAQ,EACR,QAAgB,EAChB,EAAE,EACF,IAAI,EACJ,SAAS,GACV,EAAE,kBAAkB,+BAkFpB"}
@@ -11,8 +11,14 @@ const STEP_BUTTON = 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 ite
11
11
  * "Decrease {label}" and "Increase {label}" and sit at `--target-min`; they are
12
12
  * out of the Tab order, as the pattern has them, because the arrow keys
13
13
  * already do what they do.
14
- */ export function QuantityInput({ label, value, onChange, min = 0, max, step = 1, disabled = false, id, name, className }) {
14
+ *
15
+ * Controlled with `value` and `onChange`, or uncontrolled with `defaultValue`
16
+ * — the second is how a Server Action form uses it, since a server component
17
+ * can pass neither a state nor a handler.
18
+ */ export function QuantityInput({ label, value: controlled, defaultValue, onChange, min = 0, max, step = 1, disabled = false, id, name, className }) {
15
19
  const t = useT();
20
+ const [own, setOwn] = useState(defaultValue != null ? defaultValue : min);
21
+ const value = controlled != null ? controlled : own;
16
22
  const [text, setText] = useState(String(value));
17
23
  useEffect(()=>{
18
24
  // eslint-disable-next-line react-hooks/set-state-in-effect -- the value moved from outside (a step, a reset)
@@ -24,7 +30,9 @@ const STEP_BUTTON = 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 ite
24
30
  const set = (next)=>{
25
31
  const clamped = clamp(next);
26
32
  setText(String(clamped));
27
- if (clamped !== value) onChange(clamped);
33
+ if (clamped === value) return;
34
+ setOwn(clamped);
35
+ onChange == null ? void 0 : onChange(clamped);
28
36
  };
29
37
  const commit = ()=>{
30
38
  const typed = Number.parseInt(text, 10);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"sourcesContent":["'use client';\n\nimport { type KeyboardEvent, useEffect, useState } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface QuantityInputProps {\n /** What is being counted; names the spinbutton and both of its buttons. */\n label: string;\n value: number;\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n id?: string;\n /**\n * Posts the count with the form it sits in, under this name — a server\n * action's form works without JavaScript, where the field is the only part\n * a reader can change, so the field itself carries it. The server clamps\n * what it receives: a typed value is only clamped here once committed.\n */\n name?: string;\n className?: string;\n}\n\nconst STEP_BUTTON =\n 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 items-center justify-center rounded-md border border-border-control bg-surface-elevated text-content hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50';\n\n/**\n * A count, on the WAI-ARIA spinbutton pattern: Up and Down step it, Home and\n * End jump to its bounds, and what is typed is committed on Enter or on\n * leaving the field, clamped to the bounds. The − and + beside it are named\n * \"Decrease {label}\" and \"Increase {label}\" and sit at `--target-min`; they are\n * out of the Tab order, as the pattern has them, because the arrow keys\n * already do what they do.\n */\nexport function QuantityInput({\n label,\n value,\n onChange,\n min = 0,\n max,\n step = 1,\n disabled = false,\n id,\n name,\n className,\n}: QuantityInputProps) {\n const t = useT();\n const [text, setText] = useState(String(value));\n\n useEffect(() => {\n // eslint-disable-next-line react-hooks/set-state-in-effect -- the value moved from outside (a step, a reset)\n setText(String(value));\n }, [value]);\n\n const clamp = (next: number) =>\n Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min, next));\n const set = (next: number) => {\n const clamped = clamp(next);\n setText(String(clamped));\n if (clamped !== value) onChange(clamped);\n };\n const commit = () => {\n const typed = Number.parseInt(text, 10);\n set(Number.isNaN(typed) ? value : typed);\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const next = {\n ArrowUp: value + step,\n ArrowDown: value - step,\n Home: min,\n End: max,\n }[event.key];\n if (event.key === 'Enter') {\n commit();\n return;\n }\n if (next === undefined) return;\n event.preventDefault();\n set(next);\n };\n\n return (\n <span className={cn('inline-flex items-center gap-3xs', className)}>\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.decrease', { label })}\n disabled={disabled || value <= min}\n onClick={() => set(value - step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">−</span>\n </button>\n <input\n id={id}\n name={name}\n type=\"text\"\n role=\"spinbutton\"\n inputMode=\"numeric\"\n aria-label={label}\n aria-valuenow={value}\n aria-valuemin={min}\n aria-valuemax={max}\n disabled={disabled}\n value={text}\n onChange={event => setText(event.currentTarget.value)}\n onBlur={commit}\n onKeyDown={onKeyDown}\n className=\"w-16 min-h-(--target-min) rounded-lg border border-border-control bg-surface-elevated px-2xs py-3xs text-center text-step-sm text-content tabular-nums focus-ring disabled:cursor-not-allowed disabled:opacity-50\"\n />\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.increase', { label })}\n disabled={disabled || (max !== undefined && value >= max)}\n onClick={() => set(value + step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">+</span>\n </button>\n </span>\n );\n}\n"],"names":["useEffect","useState","useT","cn","STEP_BUTTON","QuantityInput","label","value","onChange","min","max","step","disabled","id","name","className","t","text","setText","String","clamp","next","Math","Number","POSITIVE_INFINITY","set","clamped","commit","typed","parseInt","isNaN","onKeyDown","event","ArrowUp","ArrowDown","Home","End","key","undefined","preventDefault","span","button","type","tabIndex","aria-label","onClick","aria-hidden","input","role","inputMode","aria-valuenow","aria-valuemin","aria-valuemax","currentTarget","onBlur"],"mappings":"AAAA;;AAEA,SAA6BA,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAEhE,SAASC,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,EAAE,QAAQ,oBAAoB;AAsBvC,MAAMC,cACJ;AAEF;;;;;;;CAOC,GACD,OAAO,SAASC,cAAc,EAC5BC,KAAK,EACLC,KAAK,EACLC,QAAQ,EACRC,MAAM,CAAC,EACPC,GAAG,EACHC,OAAO,CAAC,EACRC,WAAW,KAAK,EAChBC,EAAE,EACFC,IAAI,EACJC,SAAS,EACU;IACnB,MAAMC,IAAId;IACV,MAAM,CAACe,MAAMC,QAAQ,GAAGjB,SAASkB,OAAOZ;IAExCP,UAAU;QACR,6GAA6G;QAC7GkB,QAAQC,OAAOZ;IACjB,GAAG;QAACA;KAAM;IAEV,MAAMa,QAAQ,CAACC,OACbC,KAAKb,GAAG,CAACC,cAAAA,MAAOa,OAAOC,iBAAiB,EAAEF,KAAKZ,GAAG,CAACD,KAAKY;IAC1D,MAAMI,MAAM,CAACJ;QACX,MAAMK,UAAUN,MAAMC;QACtBH,QAAQC,OAAOO;QACf,IAAIA,YAAYnB,OAAOC,SAASkB;IAClC;IACA,MAAMC,SAAS;QACb,MAAMC,QAAQL,OAAOM,QAAQ,CAACZ,MAAM;QACpCQ,IAAIF,OAAOO,KAAK,CAACF,SAASrB,QAAQqB;IACpC;IAEA,MAAMG,YAAY,CAACC;QACjB,MAAMX,OAAO;YACXY,SAAS1B,QAAQI;YACjBuB,WAAW3B,QAAQI;YACnBwB,MAAM1B;YACN2B,KAAK1B;QACP,CAAC,CAACsB,MAAMK,GAAG,CAAC;QACZ,IAAIL,MAAMK,GAAG,KAAK,SAAS;YACzBV;YACA;QACF;QACA,IAAIN,SAASiB,WAAW;QACxBN,MAAMO,cAAc;QACpBd,IAAIJ;IACN;IAEA,qBACE,MAACmB;QAAKzB,WAAWZ,GAAG,oCAAoCY;;0BACtD,KAAC0B;gBACCC,MAAK;gBACLC,UAAU,CAAC;gBACXC,cAAY5B,EAAE,qBAAqB;oBAAEV;gBAAM;gBAC3CM,UAAUA,YAAYL,SAASE;gBAC/BoC,SAAS,IAAMpB,IAAIlB,QAAQI;gBAC3BI,WAAWX;0BAEX,cAAA,KAACoC;oBAAKM,eAAY;8BAAO;;;0BAE3B,KAACC;gBACClC,IAAIA;gBACJC,MAAMA;gBACN4B,MAAK;gBACLM,MAAK;gBACLC,WAAU;gBACVL,cAAYtC;gBACZ4C,iBAAe3C;gBACf4C,iBAAe1C;gBACf2C,iBAAe1C;gBACfE,UAAUA;gBACVL,OAAOU;gBACPT,UAAUwB,CAAAA,QAASd,QAAQc,MAAMqB,aAAa,CAAC9C,KAAK;gBACpD+C,QAAQ3B;gBACRI,WAAWA;gBACXhB,WAAU;;0BAEZ,KAAC0B;gBACCC,MAAK;gBACLC,UAAU,CAAC;gBACXC,cAAY5B,EAAE,qBAAqB;oBAAEV;gBAAM;gBAC3CM,UAAUA,YAAaF,QAAQ4B,aAAa/B,SAASG;gBACrDmC,SAAS,IAAMpB,IAAIlB,QAAQI;gBAC3BI,WAAWX;0BAEX,cAAA,KAACoC;oBAAKM,eAAY;8BAAO;;;;;AAIjC"}
1
+ {"version":3,"sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"sourcesContent":["'use client';\n\nimport { type KeyboardEvent, useEffect, useState } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { cn } from '../../utils/cn.js';\n\ninterface QuantityInputBase {\n /** What is being counted; names the spinbutton and both of its buttons. */\n label: string;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n id?: string;\n /**\n * Posts the count with the form it sits in, under this name — a server\n * action's form works without JavaScript, where the field is the only part\n * a reader can change, so the field itself carries it. The server clamps\n * what it receives: a typed value is only clamped here once committed.\n */\n name?: string;\n className?: string;\n}\n\n/** The caller owns the count: every committed change comes back through `onChange`. */\nexport interface ControlledQuantityInputProps extends QuantityInputBase {\n value: number;\n onChange: (value: number) => void;\n defaultValue?: never;\n}\n\n/**\n * The field owns the count, starting at `defaultValue` (or `min`). This is the\n * shape a server-rendered form takes: a server component cannot hand a client\n * one a function, and with `name` the count posts with the form, so the page\n * needs no client parent of its own. `onChange` still reports each commit.\n */\nexport interface UncontrolledQuantityInputProps extends QuantityInputBase {\n value?: never;\n defaultValue?: number;\n onChange?: (value: number) => void;\n}\n\nexport type QuantityInputProps =\n ControlledQuantityInputProps | UncontrolledQuantityInputProps;\n\nconst STEP_BUTTON =\n 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 items-center justify-center rounded-md border border-border-control bg-surface-elevated text-content hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50';\n\n/**\n * A count, on the WAI-ARIA spinbutton pattern: Up and Down step it, Home and\n * End jump to its bounds, and what is typed is committed on Enter or on\n * leaving the field, clamped to the bounds. The − and + beside it are named\n * \"Decrease {label}\" and \"Increase {label}\" and sit at `--target-min`; they are\n * out of the Tab order, as the pattern has them, because the arrow keys\n * already do what they do.\n *\n * Controlled with `value` and `onChange`, or uncontrolled with `defaultValue`\n * — the second is how a Server Action form uses it, since a server component\n * can pass neither a state nor a handler.\n */\nexport function QuantityInput({\n label,\n value: controlled,\n defaultValue,\n onChange,\n min = 0,\n max,\n step = 1,\n disabled = false,\n id,\n name,\n className,\n}: QuantityInputProps) {\n const t = useT();\n const [own, setOwn] = useState(defaultValue ?? min);\n const value = controlled ?? own;\n const [text, setText] = useState(String(value));\n\n useEffect(() => {\n // eslint-disable-next-line react-hooks/set-state-in-effect -- the value moved from outside (a step, a reset)\n setText(String(value));\n }, [value]);\n\n const clamp = (next: number) =>\n Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min, next));\n const set = (next: number) => {\n const clamped = clamp(next);\n setText(String(clamped));\n if (clamped === value) return;\n setOwn(clamped);\n onChange?.(clamped);\n };\n const commit = () => {\n const typed = Number.parseInt(text, 10);\n set(Number.isNaN(typed) ? value : typed);\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const next = {\n ArrowUp: value + step,\n ArrowDown: value - step,\n Home: min,\n End: max,\n }[event.key];\n if (event.key === 'Enter') {\n commit();\n return;\n }\n if (next === undefined) return;\n event.preventDefault();\n set(next);\n };\n\n return (\n <span className={cn('inline-flex items-center gap-3xs', className)}>\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.decrease', { label })}\n disabled={disabled || value <= min}\n onClick={() => set(value - step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">−</span>\n </button>\n <input\n id={id}\n name={name}\n type=\"text\"\n role=\"spinbutton\"\n inputMode=\"numeric\"\n aria-label={label}\n aria-valuenow={value}\n aria-valuemin={min}\n aria-valuemax={max}\n disabled={disabled}\n value={text}\n onChange={event => setText(event.currentTarget.value)}\n onBlur={commit}\n onKeyDown={onKeyDown}\n className=\"w-16 min-h-(--target-min) rounded-lg border border-border-control bg-surface-elevated px-2xs py-3xs text-center text-step-sm text-content tabular-nums focus-ring disabled:cursor-not-allowed disabled:opacity-50\"\n />\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.increase', { label })}\n disabled={disabled || (max !== undefined && value >= max)}\n onClick={() => set(value + step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">+</span>\n </button>\n </span>\n );\n}\n"],"names":["useEffect","useState","useT","cn","STEP_BUTTON","QuantityInput","label","value","controlled","defaultValue","onChange","min","max","step","disabled","id","name","className","t","own","setOwn","text","setText","String","clamp","next","Math","Number","POSITIVE_INFINITY","set","clamped","commit","typed","parseInt","isNaN","onKeyDown","event","ArrowUp","ArrowDown","Home","End","key","undefined","preventDefault","span","button","type","tabIndex","aria-label","onClick","aria-hidden","input","role","inputMode","aria-valuenow","aria-valuemin","aria-valuemax","currentTarget","onBlur"],"mappings":"AAAA;;AAEA,SAA6BA,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAEhE,SAASC,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,EAAE,QAAQ,oBAAoB;AA0CvC,MAAMC,cACJ;AAEF;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,cAAc,EAC5BC,KAAK,EACLC,OAAOC,UAAU,EACjBC,YAAY,EACZC,QAAQ,EACRC,MAAM,CAAC,EACPC,GAAG,EACHC,OAAO,CAAC,EACRC,WAAW,KAAK,EAChBC,EAAE,EACFC,IAAI,EACJC,SAAS,EACU;IACnB,MAAMC,IAAIhB;IACV,MAAM,CAACiB,KAAKC,OAAO,GAAGnB,SAASQ,uBAAAA,eAAgBE;IAC/C,MAAMJ,QAAQC,qBAAAA,aAAcW;IAC5B,MAAM,CAACE,MAAMC,QAAQ,GAAGrB,SAASsB,OAAOhB;IAExCP,UAAU;QACR,6GAA6G;QAC7GsB,QAAQC,OAAOhB;IACjB,GAAG;QAACA;KAAM;IAEV,MAAMiB,QAAQ,CAACC,OACbC,KAAKf,GAAG,CAACC,cAAAA,MAAOe,OAAOC,iBAAiB,EAAEF,KAAKd,GAAG,CAACD,KAAKc;IAC1D,MAAMI,MAAM,CAACJ;QACX,MAAMK,UAAUN,MAAMC;QACtBH,QAAQC,OAAOO;QACf,IAAIA,YAAYvB,OAAO;QACvBa,OAAOU;QACPpB,4BAAAA,SAAWoB;IACb;IACA,MAAMC,SAAS;QACb,MAAMC,QAAQL,OAAOM,QAAQ,CAACZ,MAAM;QACpCQ,IAAIF,OAAOO,KAAK,CAACF,SAASzB,QAAQyB;IACpC;IAEA,MAAMG,YAAY,CAACC;QACjB,MAAMX,OAAO;YACXY,SAAS9B,QAAQM;YACjByB,WAAW/B,QAAQM;YACnB0B,MAAM5B;YACN6B,KAAK5B;QACP,CAAC,CAACwB,MAAMK,GAAG,CAAC;QACZ,IAAIL,MAAMK,GAAG,KAAK,SAAS;YACzBV;YACA;QACF;QACA,IAAIN,SAASiB,WAAW;QACxBN,MAAMO,cAAc;QACpBd,IAAIJ;IACN;IAEA,qBACE,MAACmB;QAAK3B,WAAWd,GAAG,oCAAoCc;;0BACtD,KAAC4B;gBACCC,MAAK;gBACLC,UAAU,CAAC;gBACXC,cAAY9B,EAAE,qBAAqB;oBAAEZ;gBAAM;gBAC3CQ,UAAUA,YAAYP,SAASI;gBAC/BsC,SAAS,IAAMpB,IAAItB,QAAQM;gBAC3BI,WAAWb;0BAEX,cAAA,KAACwC;oBAAKM,eAAY;8BAAO;;;0BAE3B,KAACC;gBACCpC,IAAIA;gBACJC,MAAMA;gBACN8B,MAAK;gBACLM,MAAK;gBACLC,WAAU;gBACVL,cAAY1C;gBACZgD,iBAAe/C;gBACfgD,iBAAe5C;gBACf6C,iBAAe5C;gBACfE,UAAUA;gBACVP,OAAOc;gBACPX,UAAU0B,CAAAA,QAASd,QAAQc,MAAMqB,aAAa,CAAClD,KAAK;gBACpDmD,QAAQ3B;gBACRI,WAAWA;gBACXlB,WAAU;;0BAEZ,KAAC4B;gBACCC,MAAK;gBACLC,UAAU,CAAC;gBACXC,cAAY9B,EAAE,qBAAqB;oBAAEZ;gBAAM;gBAC3CQ,UAAUA,YAAaF,QAAQ8B,aAAanC,SAASK;gBACrDqC,SAAS,IAAMpB,IAAItB,QAAQM;gBAC3BI,WAAWb;0BAEX,cAAA,KAACwC;oBAAKM,eAAY;8BAAO;;;;;AAIjC"}
@@ -1,4 +1,4 @@
1
1
  import { type Entity } from '@entifix/core';
2
2
  import type { EntityFormProps } from './entity-form.types.js';
3
- export declare function EntityForm<TEntity extends Entity>({ entityConstructor, entity, values, onFieldChange, linkSources, onLinkChange, mode: modeProp, defaultMode, onModeChange, errors, formError, onSubmit, onDelete, deletePolicy, embedded, isLoading, skeleton, isSaving, isPending, isDeleting, error, onRetry, backHref, renderLink, title, metadata, isMetadataLoading, onUseCase, onClone, children, }: EntityFormProps<TEntity>): import("react").JSX.Element;
3
+ export declare function EntityForm<TEntity extends Entity>({ entityConstructor, entity, values, onFieldChange, linkSources, onLinkChange, mode: modeProp, defaultMode, onModeChange, errors, formError, onSubmit, onDelete, deletePolicy, recordLabel: hostRecordLabel, embedded, isLoading, skeleton, isSaving, isPending, isDeleting, error, onRetry, backHref, renderLink, title, metadata, isMetadataLoading, onUseCase, onClone, children, }: EntityFormProps<TEntity>): import("react").JSX.Element;
4
4
  //# sourceMappingURL=entity-form.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entity-form.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-form/entity-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,MAAM,EAWZ,MAAM,eAAe,CAAC;AA+BvB,OAAO,KAAK,EAGV,eAAe,EAChB,MAAM,wBAAwB,CAAC;AAqDhC,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,EACjD,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,aAAa,EACb,WAAW,EACX,YAAY,EACZ,IAAI,EAAE,QAAQ,EACd,WAAW,EACX,YAAY,EACZ,MAAM,EACN,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,YAAwB,EACxB,QAAgB,EAChB,SAAiB,EACjB,QAAe,EACf,QAAgB,EAChB,SAAiB,EACjB,UAAkB,EAClB,KAAK,EACL,OAAO,EACP,QAAQ,EACR,UAAyB,EACzB,KAAK,EACL,QAAQ,EACR,iBAAyB,EACzB,SAAS,EACT,OAAO,EACP,QAAQ,GACT,EAAE,eAAe,CAAC,OAAO,CAAC,+BAqc1B"}
1
+ {"version":3,"file":"entity-form.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-form/entity-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,MAAM,EAWZ,MAAM,eAAe,CAAC;AA+BvB,OAAO,KAAK,EAGV,eAAe,EAChB,MAAM,wBAAwB,CAAC;AAqDhC,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,EACjD,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,aAAa,EACb,WAAW,EACX,YAAY,EACZ,IAAI,EAAE,QAAQ,EACd,WAAW,EACX,YAAY,EACZ,MAAM,EACN,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,YAAwB,EACxB,WAAW,EAAE,eAAe,EAC5B,QAAgB,EAChB,SAAiB,EACjB,QAAe,EACf,QAAgB,EAChB,SAAiB,EACjB,UAAkB,EAClB,KAAK,EACL,OAAO,EACP,QAAQ,EACR,UAAyB,EACzB,KAAK,EACL,QAAQ,EACR,iBAAyB,EACzB,SAAS,EACT,OAAO,EACP,QAAQ,GACT,EAAE,eAAe,CAAC,OAAO,CAAC,+BAsc1B"}
@@ -66,7 +66,7 @@ import { resolveEntityFormFields } from './use-entity-form-fields.js';
66
66
  */ /** The key the built-in Delete runs under, which no declared verb can take. */ const DELETE_KEY = '$delete';
67
67
  /** Save, from anywhere in the form — the caret in a field included. */ const SAVE_SHORTCUT = 'Mod+S';
68
68
  /** The footer's declared primary, when it is not Save. */ const PRIMARY_SHORTCUT = 'Mod+Enter';
69
- export function EntityForm({ entityConstructor, entity, values, onFieldChange, linkSources, onLinkChange, mode: modeProp, defaultMode, onModeChange, errors, formError, onSubmit, onDelete, deletePolicy = 'confirm', embedded = false, isLoading = false, skeleton = true, isSaving = false, isPending = false, isDeleting = false, error, onRetry, backHref, renderLink = renderAnchor, title, metadata, isMetadataLoading = false, onUseCase, onClone, children }) {
69
+ export function EntityForm({ entityConstructor, entity, values, onFieldChange, linkSources, onLinkChange, mode: modeProp, defaultMode, onModeChange, errors, formError, onSubmit, onDelete, deletePolicy = 'confirm', recordLabel: hostRecordLabel, embedded = false, isLoading = false, skeleton = true, isSaving = false, isPending = false, isDeleting = false, error, onRetry, backHref, renderLink = renderAnchor, title, metadata, isMetadataLoading = false, onUseCase, onClone, children }) {
70
70
  const formId = useId();
71
71
  const slots = readEntityFormFields(children);
72
72
  // The *field* descriptors, which stay local and synchronous — a column is a
@@ -158,7 +158,7 @@ export function EntityForm({ entityConstructor, entity, values, onFieldChange, l
158
158
  // filled-in field — or, for a record with nothing filled in, the entity.
159
159
  const metaEntity = extractMetaEntity(entityConstructor);
160
160
  const named = fields.find((field)=>field.type !== 'id' && readDraftString(draft, field.name) !== '');
161
- const recordLabel = named === undefined ? entityNoun(metaEntity) : readDraftString(draft, named.name);
161
+ const recordLabel = hostRecordLabel != null ? hostRecordLabel : named === undefined ? entityNoun(metaEntity) : readDraftString(draft, named.name);
162
162
  const invoke = (descriptor)=>verbs.invoke({
163
163
  descriptor,
164
164
  target: undefined,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/organisms/entity-form/entity-form.tsx"],"sourcesContent":["'use client';\n\nimport {\n cloneEntityDraft,\n describeEntityColumns,\n type Entity,\n type EntityAction,\n type EntityDraft,\n type EntityDraftValue,\n type EntityLinkSource,\n extractMetaEntity,\n type MetaAccessorType,\n readDraftString,\n readLocalizedDraft,\n readRowDrafts,\n type UseCaseDescriptor,\n} from '@entifix/core';\nimport { type ReactNode, useEffect, useId, useRef, useState } from 'react';\n\nimport {\n useEntityNoun,\n useLocalizedDescriptors,\n useT,\n useTranslateKey,\n} from '../../../i18n/index.js';\nimport { renderAnchor, useCasesForSurface } from '../../actions/index.js';\nimport { useVerbInvoker } from '../../actions/use-verb-invoker.js';\nimport { Button, button } from '../../atoms/button/index.js';\nimport { CellValue } from '../../atoms/cell-value/index.js';\nimport { FieldControl } from '../../atoms/field-control/index.js';\nimport { LocalizedFieldControl } from '../../atoms/localized-field-control/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport {\n ActionBar,\n type ActionBarItem,\n itemsForUseCases,\n} from '../../molecules/action-bar/index.js';\nimport { Card } from '../../molecules/card/index.js';\nimport { EntityLinkInput } from '../../molecules/entity-link-input/index.js';\nimport { LoadingBoundary } from '../../molecules/loading-boundary/index.js';\nimport { RegionState } from '../../molecules/region-state/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport { useLiveUpdates } from '../../utils/live-updates.js';\nimport { useFocusOnArrival } from '../../utils/use-focus-on-arrival.js';\nimport { EntityDetailGrid } from '../entity-detail-grid/index.js';\nimport type {\n EntityFormField,\n EntityFormMode,\n EntityFormProps,\n} from './entity-form.types.js';\nimport { readEntityFormFields } from './entity-form-slots.js';\nimport { resolveEntityFormFields } from './use-entity-form-fields.js';\n\n/**\n * The member types a `linkSources` entry can actually produce an editor for.\n *\n * `link` is the obvious one. `string` is here because a foreign key does not\n * have to be a typed relation: when the target lives in **another slice's\n * store**, an `EntityLink` would be an illegal edge and a storage-layer join\n * across a store boundary, so the member is a plain id and the relation exists\n * only in the operator's head — a cross-store reference is an id, never a\n * link. Nothing about the editor cares which of the two it is —\n * `EntityLinkInput` writes `String(target.id)` into the draft either way, and\n * the *only* place `link`-ness still matters is `applyEntityLinks` at submit,\n * which skips a non-`link` descriptor and so leaves the id as the truth.\n *\n * Everything else is excluded on purpose rather than by omission: a `boolean`,\n * `enum`, `number` or `date` member cannot name another record, so a source\n * pointed at one is a wiring mistake — see {@link assertLinkSourcesAreEditable}.\n */\nconst PICKABLE_TYPES: ReadonlySet<MetaAccessorType> = new Set<MetaAccessorType>(\n ['link', 'string'],\n);\n\n/**\n * A form that builds itself from an entity's metadata: fields, labels and value\n * formatting all come from `@accessor()` declarations, so editing a new entity\n * needs no bespoke form. The read/edit toggle is the whole point — the same\n * component shows a record as text or as inputs.\n *\n * Two things layer on top of that default:\n * - **modes** — `read` renders each member through `CellValue`; `edit` renders\n * the matching `FieldControl`. A member in {@link PICKABLE_TYPES} with an\n * entry in `linkSources` gets the full picker instead — a to-one `link`, or a\n * `string` holding a foreign key into another slice's store. Without a source,\n * a relation stays read-only, and a `linkCollection` always does, the to-many\n * editor being a follow-up. A source aimed at a member that can render no\n * picker is a wiring mistake rather than a no-op, and throws.\n * - **slots** — `<EntityField>` children override one field's label, control or\n * read display, or add a computed field.\n *\n * It is presentational: the draft, its errors and the save/delete actions are\n * all props, so the same form hosts on a plain route and inside a workspace tab.\n */\n/** The key the built-in Delete runs under, which no declared verb can take. */\nconst DELETE_KEY = '$delete';\n\n/** Save, from anywhere in the form — the caret in a field included. */\nconst SAVE_SHORTCUT = 'Mod+S';\n/** The footer's declared primary, when it is not Save. */\nconst PRIMARY_SHORTCUT = 'Mod+Enter';\n\nexport function EntityForm<TEntity extends Entity>({\n entityConstructor,\n entity,\n values,\n onFieldChange,\n linkSources,\n onLinkChange,\n mode: modeProp,\n defaultMode,\n onModeChange,\n errors,\n formError,\n onSubmit,\n onDelete,\n deletePolicy = 'confirm',\n embedded = false,\n isLoading = false,\n skeleton = true,\n isSaving = false,\n isPending = false,\n isDeleting = false,\n error,\n onRetry,\n backHref,\n renderLink = renderAnchor,\n title,\n metadata,\n isMetadataLoading = false,\n onUseCase,\n onClone,\n children,\n}: EntityFormProps<TEntity>) {\n const formId = useId();\n const slots = readEntityFormFields<TEntity>(children);\n // The *field* descriptors, which stay local and synchronous — a column is a\n // property of the class every caller holds. Only an action's availability is\n // a property of the caller, which is why that half arrives as `metadata`.\n const columnDescriptors = describeEntityColumns(\n entityConstructor,\n entity,\n ) as Array<EntityFormField<TEntity>>;\n // Before slots resolve, so an `<EntityField label>` override still wins.\n const described = useLocalizedDescriptors(columnDescriptors);\n const resolved = resolveEntityFormFields(described, slots.fields);\n assertLinkSourcesAreEditable(resolved, linkSources);\n\n // **Form above, table below**, and it is a partition rather than an ordering\n // hint. A grid rendered between two labelled inputs reads as a field, which\n // it is not — it is a second record list — so every owned collection is\n // pulled out of the field stack whatever `order` it declared, and rendered\n // full width beneath it. A composition that named no child has nothing to\n // render and stays where it was, showing its row count as before.\n const fields = resolved.filter(field => !isOwnedCollection(field));\n const ownedCollections = resolved.filter(isOwnedCollection);\n\n /**\n * `true`/omitted → the built-in default, which draws one label+control pair\n * per resolved field; a node → that node; `false` → nothing. The field list\n * comes from the constructor's metadata, not from the record, so it is already\n * complete while the record is still in flight — which is what lets the\n * placeholder match the form it is standing in for.\n */\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n const showSkeleton = isLoading && skeleton !== false;\n\n const [internalMode, setInternalMode] = useState<EntityFormMode>(\n defaultMode ?? (entity ? 'read' : 'edit'),\n );\n const mode = modeProp ?? internalMode;\n const editing = mode === 'edit';\n\n const draft: EntityDraft = values ?? {};\n const setField = (name: string, value: EntityDraftValue) =>\n onFieldChange?.(name, value);\n\n const toggleMode = () => {\n const next: EntityFormMode = editing ? 'read' : 'edit';\n setInternalMode(next);\n onModeChange?.(next);\n };\n\n const t = useT();\n const translateKey = useTranslateKey();\n const entityNoun = useEntityNoun();\n const live = useLiveUpdates();\n // A form reached by a write — saved, handed off by a wizard — takes focus on\n // its heading, so it never rests on `<body>` (ADR 0028).\n const heading = useRef<HTMLHeadingElement>(null);\n useFocusOnArrival(heading);\n const busy = isSaving || isPending || isDeleting;\n\n // A draft restored from storage holds relation ids and no instances, so every\n // picker is resolving its target for a moment after mount. Saving inside that\n // window is exactly the case `applyEntityLinks` refuses: an `embedded` member\n // with an id and nothing to inline. Gating the save is the honest answer —\n // the form is still assembling itself, and the wait is one lookup long.\n //\n // Deliberately not narrowed to `embedded` members: a descriptor's\n // serialization is not what makes an in-flight lookup a bad moment to submit,\n // and a form that saves half-resolved is confusing whichever shape it writes.\n const resolvingLinks = Object.values(linkSources ?? {}).some(\n source => source.selected.isLoading,\n );\n\n // Absent metadata keeps the pre-ADR-0026 behaviour, so an un-migrated call\n // site renders exactly as before. Present metadata is authoritative: it is\n // what the service already decided this caller may do.\n const may = (action: EntityAction) =>\n metadata === undefined || metadata.actions.includes(action);\n\n // A single-record form shows the verbs that act on *this* record. Which cell\n // lands where is `action-surfaces`' business and nobody else's — a\n // `context-dependent` entity verb belongs to a row's overflow menu and a\n // `collection`-bound one to the bulk bar, and resolving that here as well is\n // how the two surfaces drifted apart in the first place.\n //\n // ⚠️ **Only when there is somewhere for them to go.** Every cell these two\n // surfaces render is `binding: 'entity'`, so without an `onUseCase` the\n // button has no handler and clicking it does nothing at all. That is not a\n // hypothetical: `makeEntityCrud` withholds the handler on a create, because\n // there is no record for a verb to act on — and Publish and Unpublish\n // rendered on a new `ProductOffering` and were inert, which reads as a broken\n // feature rather than an inapplicable one.\n //\n // The hooks are called unconditionally and their *results* are gated: React's\n // hook count must stay fixed across renders.\n const declaredHeaderUseCases = useCasesForSurface(\n 'form-header',\n metadata?.useCases,\n );\n const declaredFooterUseCases = useCasesForSurface(\n 'form-footer',\n metadata?.useCases,\n );\n const headerUseCases = onUseCase ? declaredHeaderUseCases : [];\n const footerUseCases = onUseCase ? declaredFooterUseCases : [];\n\n // Every verb on the form, and its own Delete, goes through one invoker: a\n // destructive verb asks, asks for the name, or runs and offers Undo, as it\n // declared — `revoke-sessions` ends every session a user holds.\n const verbs = useVerbInvoker<undefined>({\n run: key => (key === DELETE_KEY ? onDelete?.() : onUseCase?.(key)),\n busy,\n served: metadata?.useCases,\n });\n // What a confirmation names: the record as the reader knows it — its first\n // filled-in field — or, for a record with nothing filled in, the entity.\n const metaEntity = extractMetaEntity(entityConstructor);\n const named = fields.find(\n field => field.type !== 'id' && readDraftString(draft, field.name) !== '',\n );\n const recordLabel =\n named === undefined\n ? entityNoun(metaEntity)\n : readDraftString(draft, named.name);\n const invoke = (descriptor: UseCaseDescriptor) =>\n verbs.invoke({\n descriptor,\n target: undefined,\n object: recordLabel,\n count: 1,\n });\n const requestDelete = () => {\n // Undo is the host's to offer: only it can put a deleted record back.\n if (deletePolicy === 'undo') {\n onDelete?.();\n return;\n }\n invoke({\n key: DELETE_KEY,\n binding: 'entity',\n placement: 'determining',\n labelKey: 'controls:form.delete',\n confirm: {\n tone: 'destructive',\n messageKey: 'controls:confirm.deleteMessage',\n },\n reversible: deletePolicy,\n });\n };\n\n const headerItems = itemsForUseCases(\n headerUseCases,\n translateKey,\n invoke,\n busy,\n );\n const footerItems = itemsForUseCases(\n footerUseCases,\n translateKey,\n invoke,\n busy,\n );\n /**\n * A save attempt. Counted, so a refused one moves focus to the error summary\n * once — not again on every keystroke that leaves an error standing.\n */\n const [attempt, setAttempt] = useState(0);\n const save = () => {\n setAttempt(current => current + 1);\n onSubmit?.(draft);\n };\n const summary = useRef<HTMLDivElement>(null);\n const summaryId = useId();\n const invalid = editing\n ? fields.filter(field => errors?.[field.name] !== undefined)\n : [];\n const focusedAttempt = useRef(0);\n useEffect(() => {\n if (attempt === focusedAttempt.current || invalid.length === 0) return;\n focusedAttempt.current = attempt;\n summary.current?.focus();\n }, [attempt, invalid.length]);\n\n // Save is the footer's primary unless the entity declared one of its own — a\n // determining \"Publish\" is what that page is for, and Save steps aside.\n const declaresFooterPrimary = footerItems.some(\n item => item.emphasis === 'primary',\n );\n const footerActions: ActionBarItem[] = [\n ...(may('write')\n ? [\n {\n key: 'save',\n // A pending write waits on the push stream; while that is down\n // it says so, rather than waiting without a word.\n label: isPending\n ? t(live.connection === 'open' ? 'form.pending' : 'live.waiting')\n : isSaving\n ? t('form.saving')\n : t('form.save'),\n emphasis: declaresFooterPrimary\n ? ('default' as const)\n : ('primary' as const),\n disabled: busy || resolvingLinks,\n submit: true,\n shortcut: SAVE_SHORTCUT,\n onSelect: save,\n },\n ]\n : []),\n // The page's main act answers Mod+Enter, unless it declared its own.\n ...footerItems.map(item =>\n item.emphasis === 'primary' && item.shortcut === undefined\n ? { ...item, shortcut: PRIMARY_SHORTCUT }\n : item,\n ),\n // Clone is the CRUD triple's, not a declared verb's: every entity can be\n // copied, so giving it a `@useCase()` would mean declaring the same verb on\n // all 28 of them. It needs `write`, because what it produces is a create.\n ...(onClone && entity && may('write')\n ? [\n {\n key: 'clone',\n label: t('form.clone'),\n disabled: busy,\n onSelect: () => onClone(cloneEntityDraft(entityConstructor, draft)),\n },\n ]\n : []),\n ...(onDelete && may('delete')\n ? [\n {\n key: 'delete',\n label: isDeleting ? t('form.deleting') : t('form.delete'),\n destructive: true,\n disabled: busy,\n onSelect: requestDelete,\n },\n ]\n : []),\n ];\n\n // A real form while editing, so Enter in a field saves; an embedded one\n // belongs to its host's flow, and a caller who may not write has no Save.\n const asForm = editing && !embedded && may('write');\n const rows = fields.map(field => (\n <FieldRow\n key={field.name}\n field={field}\n entity={entity}\n draft={draft}\n value={readDraftString(draft, field.name)}\n editing={editing}\n error={editing ? errors?.[field.name] : undefined}\n setField={setField}\n linkSource={linkSources?.[field.name]}\n onLinkChange={onLinkChange}\n id={`${formId}-${field.name}`}\n />\n ));\n\n const body = (\n <Stack gap=\"s\">\n {/* An embedded form brings only its fields: the heading and the action\n row belong to whatever is hosting it — a wizard step sits under the\n wizard's own heading and advances with the wizard's own footer, so\n rendering a second of each is two controls for one job. */}\n {!embedded && (\n <Stack direction=\"row\" gap=\"xs\" align=\"center\">\n <Text\n as=\"h2\"\n ref={heading}\n tabIndex={-1}\n step={1}\n weight=\"semibold\"\n className=\"focus-ring\"\n >\n {/* `entity` is undefined while the record is still in flight, so\n testing it alone made a loading edit form announce itself as\n \"New\" for as long as the fetch took, then relabel when the\n record landed. A form that is loading is never a create. */}\n {title ?? (entity || isLoading ? t('form.details') : t('form.new'))}\n </Text>\n {/* The built-in toggle only appears when the form owns its mode and\n there is a record to view — a create form has nothing to read. */}\n {modeProp === undefined && entity && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={toggleMode}\n >\n {editing ? t('form.view') : t('form.edit')}\n </Button>\n )}\n <LoadingBoundary isLoading={isMetadataLoading} lines={0}>\n <>\n {headerItems.length > 0 && (\n <ActionBar surface=\"form-header\" items={headerItems} />\n )}\n {slots.headerActions}\n </>\n </LoadingBoundary>\n </Stack>\n )}\n\n {/* One announcement for the form; the shimmer below is aria-hidden. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {isLoading ? t('form.loading') : ''}\n </span>\n\n <RegionState\n plural={entityNoun(metaEntity, 'plural')}\n error={error}\n onRetry={onRetry}\n // Only a load that can be retried leaves an out-of-date record; a\n // failed save leaves what the reader typed, which is current.\n hasStaleData={entity !== undefined && onRetry !== undefined}\n />\n\n {/* A refused save, as one summary of links to what needs fixing: the\n only alert in the form, and where focus goes after the attempt. */}\n {invalid.length > 0 && (\n <div\n ref={summary}\n tabIndex={-1}\n role=\"alert\"\n aria-labelledby={summaryId}\n data-testid=\"entity-form-error-summary\"\n className=\"rounded-lg border-l-4 border-danger bg-danger-subtle px-s py-xs text-step-sm text-content focus-ring\"\n >\n <p id={summaryId} className=\"m-0 font-semibold\">\n {t('form.errorSummary', { count: invalid.length })}\n </p>\n <ul className=\"m-0 ps-s\">\n {invalid.map(field => (\n <li key={field.name}>\n <a\n href={`#${formId}-${field.name}`}\n // The content colour: primary on the danger surface misses\n // 4.5:1.\n className=\"font-medium text-content underline underline-offset-2 focus-ring\"\n onClick={event => {\n event.preventDefault();\n document.getElementById(`${formId}-${field.name}`)?.focus();\n }}\n >\n {errors?.[field.name]}\n </a>\n </li>\n ))}\n </ul>\n </div>\n )}\n\n {editing && fields.some(field => field.required && !field.readonly) && (\n <Text step={-1} tone=\"muted\">\n {t('form.requiredHint')}\n </Text>\n )}\n\n {/* The placeholder *replaces* the rows rather than sitting above them.\n Rendering both meant a shimmer stacked on a full set of empty fields,\n which is two loading signals for one region and twice the height the\n loaded form settles at. */}\n {showSkeleton\n ? (customSkeleton ??\n fields.map(field => (\n <Stack key={field.name} gap=\"3xs\">\n <Skeleton shape=\"line\" className=\"w-24\" />\n <Skeleton className=\"h-9 w-full\" />\n </Stack>\n )))\n : editing\n ? rows\n : // Read mode is a description list: a label points at nothing\n // when there is no control, and a term and its value is what\n // the pair is.\n rows.length > 0 && (\n <dl className=\"m-0 flex flex-col gap-s\">{rows}</dl>\n )}\n\n {/* The rows the record owns, full width and below every field. Each\n grid is handed the *whole* error map and picks out the keys naming\n its own member, so a record owning two collections needs no\n bookkeeping here. */}\n {ownedCollections.map(field => (\n <EntityDetailGrid\n key={field.name}\n descriptor={field}\n rows={readRowDrafts(draft[field.name])}\n onRowsChange={rows => setField(field.name, rows)}\n errors={editing ? errors : undefined}\n editing={editing}\n isLoading={isLoading}\n skeleton={skeleton}\n />\n ))}\n\n {/* A cross-field rule has no row to sit under, so it sits with the\n actions it blocks. */}\n {editing && formError && (\n <span role=\"alert\" className=\"text-step-sm text-danger\">\n {formError}\n </span>\n )}\n\n {editing && !embedded && (\n <Stack direction=\"row\" gap=\"xs\" align=\"center\">\n <ActionBar surface=\"form-footer\" size=\"md\" items={footerActions} />\n {slots.footerActions}\n {/* A link styled as a button, not a button inside a link: the old\n nesting was two interactive elements for one action, and its\n `disabled` stopped nothing, because the anchor around it still\n navigated. While busy the link is inert instead. */}\n {backHref &&\n renderLink({\n href: backHref,\n className: cn(\n button({ variant: 'ghost' }),\n busy && 'pointer-events-none opacity-50',\n ),\n children: t('form.back'),\n intent: { kind: 'back' },\n })}\n </Stack>\n )}\n\n {slots.rest}\n </Stack>\n );\n\n return (\n <Card>\n {asForm ? (\n <form\n noValidate\n // Enter submits through Save, the form's default button, which is\n // disabled while a save is in flight — so a busy form stays put.\n onSubmit={event => {\n event.preventDefault();\n save();\n }}\n >\n {body}\n </form>\n ) : (\n body\n )}\n\n {verbs.dialog}\n </Card>\n );\n}\n\n/**\n * A member the detail grid renders rather than the field stack.\n *\n * The `childType` half is not defensive: a `composition` that declared no child\n * has no columns, so there is nothing for a grid to draw. It keeps its place in\n * the field stack and reads as its row count, which is what it did before this\n * control existed — an honest \"some rows\" rather than an empty table implying\n * there are none.\n */\nfunction isOwnedCollection<TEntity extends Entity>(\n field: EntityFormField<TEntity>,\n): boolean {\n return field.type === 'composition' && field.childType !== undefined;\n}\n\n/**\n * Refuse a `linkSources` entry that names a member no picker can be built for.\n *\n * `FieldRow` asks for {@link PICKABLE_TYPES} before it builds an editor, so a\n * source handed to anything else used to fall through to the read-only display\n * or a plain `FieldControl` with nothing said: the caller wired a picker, no\n * picker appeared, and the field was indistinguishable from one the entity had\n * declared read-only. That is a wiring mistake rather than a state — the entry\n * is dead in *every* shape such a field can render, since an\n * `<EntityField render>` slot is handed the draft and never the source — so it\n * costs the render instead of a warning nobody reads. `useEntityLinkSource`\n * already throws at render for the sibling mistake (a search\n * property the target does not declare `filterable`); this is the same fault\n * caught one level up.\n *\n * Two messages, because the two faults are not the same thing. A\n * `linkCollection` is a member the editor has simply not been built for yet\n * (#26), so the fix is to wait or to point at the to-one you meant. Any other\n * type is a member that can never name another record at all, so the fix is to\n * remove the entry.\n *\n * Here rather than inside `FieldRow`, and that is the whole reason it is a\n * separate pass: a row only reaches its editor branch in **edit** mode, so a\n * per-row check would stay silent on a form that opens in `read` — the default\n * whenever there is a record — and then throw on a click, minutes later. One\n * pass over the registry fires on the first render either way.\n *\n * Against the resolved fields rather than the raw metadata: a slot may drop a\n * member, and a source left behind for a field that no longer renders is not\n * worth a page.\n */\nfunction assertLinkSourcesAreEditable<TEntity extends Entity>(\n fields: Array<EntityFormField<TEntity>>,\n // Same reason as the prop's own declaration: a source's target is another\n // entity entirely and cannot be expressed in terms of `TEntity`.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n linkSources: Record<string, EntityLinkSource<any>> | undefined,\n): void {\n if (linkSources === undefined) return;\n\n const sourced = fields.filter(\n field =>\n linkSources[field.name] !== undefined && !PICKABLE_TYPES.has(field.type),\n );\n if (sourced.length === 0) return;\n\n const toMany = sourced\n .filter(field => field.type === 'linkCollection')\n .map(field => field.name);\n if (toMany.length > 0) {\n throw new Error(\n `EntityForm was given a link source for a to-many member: ${toMany.join(', ')}. ` +\n 'A `linkCollection` has no editor yet, so the source can only be dropped ' +\n 'and the field would render read-only with no sign anything was wired. ' +\n 'Remove the entry, or point it at the to-one `link` you meant.',\n );\n }\n\n const owned = sourced\n .filter(field => field.type === 'composition')\n .map(field => field.name);\n if (owned.length > 0) {\n throw new Error(\n `EntityForm was given a link source for an owned collection: ${owned.join(', ')}. ` +\n 'A `composition` is not a reference — its rows have no life outside this ' +\n 'record and are never picked from existing ones, so there is nothing for a ' +\n 'picker to look up. The detail grid is what edits them.',\n );\n }\n\n const unpickable = sourced.map(field => `${field.name} (${field.type})`);\n throw new Error(\n `EntityForm was given a link source for a member that cannot hold one: ${unpickable.join(', ')}. ` +\n 'A picker writes the target’s id into the draft, so the member has to be a ' +\n '`link` or the `string` that carries a foreign key. Remove the entry, or ' +\n 'point it at the member that actually holds the reference.',\n );\n}\n\ninterface FieldRowProps<TEntity extends Entity> {\n field: EntityFormField<TEntity>;\n entity: TEntity | undefined;\n draft: EntityDraft;\n value: string;\n editing: boolean;\n error: string | undefined;\n setField: (name: string, value: EntityDraftValue) => void;\n // The target of a relation is a different entity than the form's own.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n linkSource: EntityLinkSource<any> | undefined;\n onLinkChange:\n ((name: string, entity: Entity | undefined) => void) | undefined;\n id: string;\n}\n\n/**\n * One label + control (or value) row. A `readonly` member renders its read\n * display even in edit mode, and so does a relation with no source; everything\n * else edits through `FieldControl` — or, when a source was supplied for a\n * member in {@link PICKABLE_TYPES}, through `EntityLinkInput`. A boolean edited\n * inline carries its own label, so the row does not add a second one.\n *\n * The source branch sits above the `virtual || isRelation` fallback and below\n * `field.render`, which is what lets a slot still override a picked field.\n */\nfunction FieldRow<TEntity extends Entity>({\n field,\n entity,\n draft,\n value,\n editing,\n error,\n setField,\n linkSource,\n onLinkChange,\n id,\n}: FieldRowProps<TEntity>) {\n const t = useT();\n const errorId = `${id}-error`;\n const readNode: ReactNode = field.readRender ? (\n field.readRender(entity)\n ) : (\n <CellValue\n value={\n entity ? (entity as Record<string, unknown>)[field.name] : undefined\n }\n descriptor={field}\n record={entity as Record<string, unknown> | undefined}\n />\n );\n\n // Members with no editor of their own here: a to-one `link` whose picker was\n // never wired, a `linkCollection` (#26) and a `composition`. All three\n // fall back to the read display, which is honest — a disabled text box\n // holding `[object Object]` is not.\n const isRelation =\n field.type === 'link' ||\n field.type === 'linkCollection' ||\n field.type === 'composition';\n\n let control: ReactNode;\n if (!editing) {\n control = readNode;\n } else if (field.render) {\n control = field.render({ draft, value, setField, id });\n } else if (linkSource && PICKABLE_TYPES.has(field.type)) {\n control = (\n <EntityLinkInput\n descriptor={field}\n value={value}\n source={linkSource}\n id={id}\n disabled={field.readonly}\n onSelect={target => {\n setField(field.name, target.id == null ? '' : String(target.id));\n onLinkChange?.(field.name, target);\n }}\n onClear={() => {\n setField(field.name, '');\n onLinkChange?.(field.name, undefined);\n }}\n />\n );\n } else if (field.type === 'localized' && !field.virtual) {\n control = (\n <LocalizedFieldControl\n descriptor={field}\n value={readLocalizedDraft(draft, field.name)}\n onChange={next => setField(field.name, next)}\n id={id}\n />\n );\n } else if (field.virtual || isRelation) {\n // A relation with no source has no input to offer, and a virtual field has\n // no member to edit unless a `render` was supplied — both fall back to the\n // read display.\n control = readNode;\n } else {\n control = (\n <FieldControl\n descriptor={field}\n value={value}\n onChange={next => setField(field.name, next)}\n id={id}\n record={draft}\n aria-invalid={error !== undefined || undefined}\n aria-describedby={error === undefined ? undefined : errorId}\n aria-required={field.required || undefined}\n />\n );\n }\n\n if (!editing)\n return (\n <div className=\"flex flex-col gap-3xs\">\n <dt className=\"text-step-sm text-content-muted\">{field.label}</dt>\n <dd className=\"m-0\">{control}</dd>\n </div>\n );\n\n const ownsLabel = field.type === 'boolean' && !field.render && !field.virtual;\n // Marked before anyone submits, not discovered after.\n const marked = field.required && !field.readonly;\n\n return (\n <Stack gap=\"3xs\">\n {!ownsLabel && (\n // The marker sits beside the label, not in it: the name stays the\n // field's own, and `aria-required` is what says it to a reader.\n <span className=\"flex items-baseline gap-3xs\">\n <label htmlFor={id} className=\"text-step-sm text-content-muted\">\n {field.label}\n </label>\n {marked && (\n <span aria-hidden=\"true\" className=\"text-step-sm text-danger\">\n {t('form.requiredMarker')}\n </span>\n )}\n </span>\n )}\n {control}\n {/* Not an alert of its own: the summary above the fields is the one\n announcement, and this is what the control's description reads. */}\n {error && (\n <span id={errorId} className=\"text-step-sm text-danger\">\n {error}\n </span>\n )}\n </Stack>\n );\n}\n"],"names":["cloneEntityDraft","describeEntityColumns","extractMetaEntity","readDraftString","readLocalizedDraft","readRowDrafts","useEffect","useId","useRef","useState","useEntityNoun","useLocalizedDescriptors","useT","useTranslateKey","renderAnchor","useCasesForSurface","useVerbInvoker","Button","button","CellValue","FieldControl","LocalizedFieldControl","Skeleton","Text","ActionBar","itemsForUseCases","Card","EntityLinkInput","LoadingBoundary","RegionState","Stack","cn","useLiveUpdates","useFocusOnArrival","EntityDetailGrid","readEntityFormFields","resolveEntityFormFields","PICKABLE_TYPES","Set","DELETE_KEY","SAVE_SHORTCUT","PRIMARY_SHORTCUT","EntityForm","entityConstructor","entity","values","onFieldChange","linkSources","onLinkChange","mode","modeProp","defaultMode","onModeChange","errors","formError","onSubmit","onDelete","deletePolicy","embedded","isLoading","skeleton","isSaving","isPending","isDeleting","error","onRetry","backHref","renderLink","title","metadata","isMetadataLoading","onUseCase","onClone","children","formId","slots","columnDescriptors","described","resolved","fields","assertLinkSourcesAreEditable","filter","field","isOwnedCollection","ownedCollections","customSkeleton","undefined","showSkeleton","internalMode","setInternalMode","editing","draft","setField","name","value","toggleMode","next","t","translateKey","entityNoun","live","heading","busy","resolvingLinks","Object","some","source","selected","may","action","actions","includes","declaredHeaderUseCases","useCases","declaredFooterUseCases","headerUseCases","footerUseCases","verbs","run","key","served","metaEntity","named","find","type","recordLabel","invoke","descriptor","target","object","count","requestDelete","binding","placement","labelKey","confirm","tone","messageKey","reversible","headerItems","footerItems","attempt","setAttempt","save","current","summary","summaryId","invalid","focusedAttempt","length","focus","declaresFooterPrimary","item","emphasis","footerActions","label","connection","disabled","submit","shortcut","onSelect","map","destructive","asForm","rows","FieldRow","linkSource","id","body","gap","direction","align","as","ref","tabIndex","step","weight","className","variant","size","onClick","lines","surface","items","headerActions","span","role","aria-live","plural","hasStaleData","div","aria-labelledby","data-testid","p","ul","li","a","href","event","document","preventDefault","getElementById","required","readonly","shape","dl","onRowsChange","intent","kind","rest","form","noValidate","dialog","childType","sourced","has","toMany","Error","join","owned","unpickable","errorId","readNode","readRender","record","isRelation","control","render","String","onClear","virtual","onChange","aria-invalid","aria-describedby","aria-required","dt","dd","ownsLabel","marked","htmlFor","aria-hidden"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/ui/organisms/entity-form/entity-form.tsx"],"sourcesContent":["'use client';\n\nimport {\n cloneEntityDraft,\n describeEntityColumns,\n type Entity,\n type EntityAction,\n type EntityDraft,\n type EntityDraftValue,\n type EntityLinkSource,\n extractMetaEntity,\n type MetaAccessorType,\n readDraftString,\n readLocalizedDraft,\n readRowDrafts,\n type UseCaseDescriptor,\n} from '@entifix/core';\nimport { type ReactNode, useEffect, useId, useRef, useState } from 'react';\n\nimport {\n useEntityNoun,\n useLocalizedDescriptors,\n useT,\n useTranslateKey,\n} from '../../../i18n/index.js';\nimport { renderAnchor, useCasesForSurface } from '../../actions/index.js';\nimport { useVerbInvoker } from '../../actions/use-verb-invoker.js';\nimport { Button, button } from '../../atoms/button/index.js';\nimport { CellValue } from '../../atoms/cell-value/index.js';\nimport { FieldControl } from '../../atoms/field-control/index.js';\nimport { LocalizedFieldControl } from '../../atoms/localized-field-control/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport {\n ActionBar,\n type ActionBarItem,\n itemsForUseCases,\n} from '../../molecules/action-bar/index.js';\nimport { Card } from '../../molecules/card/index.js';\nimport { EntityLinkInput } from '../../molecules/entity-link-input/index.js';\nimport { LoadingBoundary } from '../../molecules/loading-boundary/index.js';\nimport { RegionState } from '../../molecules/region-state/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport { useLiveUpdates } from '../../utils/live-updates.js';\nimport { useFocusOnArrival } from '../../utils/use-focus-on-arrival.js';\nimport { EntityDetailGrid } from '../entity-detail-grid/index.js';\nimport type {\n EntityFormField,\n EntityFormMode,\n EntityFormProps,\n} from './entity-form.types.js';\nimport { readEntityFormFields } from './entity-form-slots.js';\nimport { resolveEntityFormFields } from './use-entity-form-fields.js';\n\n/**\n * The member types a `linkSources` entry can actually produce an editor for.\n *\n * `link` is the obvious one. `string` is here because a foreign key does not\n * have to be a typed relation: when the target lives in **another slice's\n * store**, an `EntityLink` would be an illegal edge and a storage-layer join\n * across a store boundary, so the member is a plain id and the relation exists\n * only in the operator's head — a cross-store reference is an id, never a\n * link. Nothing about the editor cares which of the two it is —\n * `EntityLinkInput` writes `String(target.id)` into the draft either way, and\n * the *only* place `link`-ness still matters is `applyEntityLinks` at submit,\n * which skips a non-`link` descriptor and so leaves the id as the truth.\n *\n * Everything else is excluded on purpose rather than by omission: a `boolean`,\n * `enum`, `number` or `date` member cannot name another record, so a source\n * pointed at one is a wiring mistake — see {@link assertLinkSourcesAreEditable}.\n */\nconst PICKABLE_TYPES: ReadonlySet<MetaAccessorType> = new Set<MetaAccessorType>(\n ['link', 'string'],\n);\n\n/**\n * A form that builds itself from an entity's metadata: fields, labels and value\n * formatting all come from `@accessor()` declarations, so editing a new entity\n * needs no bespoke form. The read/edit toggle is the whole point — the same\n * component shows a record as text or as inputs.\n *\n * Two things layer on top of that default:\n * - **modes** — `read` renders each member through `CellValue`; `edit` renders\n * the matching `FieldControl`. A member in {@link PICKABLE_TYPES} with an\n * entry in `linkSources` gets the full picker instead — a to-one `link`, or a\n * `string` holding a foreign key into another slice's store. Without a source,\n * a relation stays read-only, and a `linkCollection` always does, the to-many\n * editor being a follow-up. A source aimed at a member that can render no\n * picker is a wiring mistake rather than a no-op, and throws.\n * - **slots** — `<EntityField>` children override one field's label, control or\n * read display, or add a computed field.\n *\n * It is presentational: the draft, its errors and the save/delete actions are\n * all props, so the same form hosts on a plain route and inside a workspace tab.\n */\n/** The key the built-in Delete runs under, which no declared verb can take. */\nconst DELETE_KEY = '$delete';\n\n/** Save, from anywhere in the form — the caret in a field included. */\nconst SAVE_SHORTCUT = 'Mod+S';\n/** The footer's declared primary, when it is not Save. */\nconst PRIMARY_SHORTCUT = 'Mod+Enter';\n\nexport function EntityForm<TEntity extends Entity>({\n entityConstructor,\n entity,\n values,\n onFieldChange,\n linkSources,\n onLinkChange,\n mode: modeProp,\n defaultMode,\n onModeChange,\n errors,\n formError,\n onSubmit,\n onDelete,\n deletePolicy = 'confirm',\n recordLabel: hostRecordLabel,\n embedded = false,\n isLoading = false,\n skeleton = true,\n isSaving = false,\n isPending = false,\n isDeleting = false,\n error,\n onRetry,\n backHref,\n renderLink = renderAnchor,\n title,\n metadata,\n isMetadataLoading = false,\n onUseCase,\n onClone,\n children,\n}: EntityFormProps<TEntity>) {\n const formId = useId();\n const slots = readEntityFormFields<TEntity>(children);\n // The *field* descriptors, which stay local and synchronous — a column is a\n // property of the class every caller holds. Only an action's availability is\n // a property of the caller, which is why that half arrives as `metadata`.\n const columnDescriptors = describeEntityColumns(\n entityConstructor,\n entity,\n ) as Array<EntityFormField<TEntity>>;\n // Before slots resolve, so an `<EntityField label>` override still wins.\n const described = useLocalizedDescriptors(columnDescriptors);\n const resolved = resolveEntityFormFields(described, slots.fields);\n assertLinkSourcesAreEditable(resolved, linkSources);\n\n // **Form above, table below**, and it is a partition rather than an ordering\n // hint. A grid rendered between two labelled inputs reads as a field, which\n // it is not — it is a second record list — so every owned collection is\n // pulled out of the field stack whatever `order` it declared, and rendered\n // full width beneath it. A composition that named no child has nothing to\n // render and stays where it was, showing its row count as before.\n const fields = resolved.filter(field => !isOwnedCollection(field));\n const ownedCollections = resolved.filter(isOwnedCollection);\n\n /**\n * `true`/omitted → the built-in default, which draws one label+control pair\n * per resolved field; a node → that node; `false` → nothing. The field list\n * comes from the constructor's metadata, not from the record, so it is already\n * complete while the record is still in flight — which is what lets the\n * placeholder match the form it is standing in for.\n */\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n const showSkeleton = isLoading && skeleton !== false;\n\n const [internalMode, setInternalMode] = useState<EntityFormMode>(\n defaultMode ?? (entity ? 'read' : 'edit'),\n );\n const mode = modeProp ?? internalMode;\n const editing = mode === 'edit';\n\n const draft: EntityDraft = values ?? {};\n const setField = (name: string, value: EntityDraftValue) =>\n onFieldChange?.(name, value);\n\n const toggleMode = () => {\n const next: EntityFormMode = editing ? 'read' : 'edit';\n setInternalMode(next);\n onModeChange?.(next);\n };\n\n const t = useT();\n const translateKey = useTranslateKey();\n const entityNoun = useEntityNoun();\n const live = useLiveUpdates();\n // A form reached by a write — saved, handed off by a wizard — takes focus on\n // its heading, so it never rests on `<body>` (ADR 0028).\n const heading = useRef<HTMLHeadingElement>(null);\n useFocusOnArrival(heading);\n const busy = isSaving || isPending || isDeleting;\n\n // A draft restored from storage holds relation ids and no instances, so every\n // picker is resolving its target for a moment after mount. Saving inside that\n // window is exactly the case `applyEntityLinks` refuses: an `embedded` member\n // with an id and nothing to inline. Gating the save is the honest answer —\n // the form is still assembling itself, and the wait is one lookup long.\n //\n // Deliberately not narrowed to `embedded` members: a descriptor's\n // serialization is not what makes an in-flight lookup a bad moment to submit,\n // and a form that saves half-resolved is confusing whichever shape it writes.\n const resolvingLinks = Object.values(linkSources ?? {}).some(\n source => source.selected.isLoading,\n );\n\n // Absent metadata keeps the pre-ADR-0026 behaviour, so an un-migrated call\n // site renders exactly as before. Present metadata is authoritative: it is\n // what the service already decided this caller may do.\n const may = (action: EntityAction) =>\n metadata === undefined || metadata.actions.includes(action);\n\n // A single-record form shows the verbs that act on *this* record. Which cell\n // lands where is `action-surfaces`' business and nobody else's — a\n // `context-dependent` entity verb belongs to a row's overflow menu and a\n // `collection`-bound one to the bulk bar, and resolving that here as well is\n // how the two surfaces drifted apart in the first place.\n //\n // ⚠️ **Only when there is somewhere for them to go.** Every cell these two\n // surfaces render is `binding: 'entity'`, so without an `onUseCase` the\n // button has no handler and clicking it does nothing at all. That is not a\n // hypothetical: `makeEntityCrud` withholds the handler on a create, because\n // there is no record for a verb to act on — and Publish and Unpublish\n // rendered on a new `ProductOffering` and were inert, which reads as a broken\n // feature rather than an inapplicable one.\n //\n // The hooks are called unconditionally and their *results* are gated: React's\n // hook count must stay fixed across renders.\n const declaredHeaderUseCases = useCasesForSurface(\n 'form-header',\n metadata?.useCases,\n );\n const declaredFooterUseCases = useCasesForSurface(\n 'form-footer',\n metadata?.useCases,\n );\n const headerUseCases = onUseCase ? declaredHeaderUseCases : [];\n const footerUseCases = onUseCase ? declaredFooterUseCases : [];\n\n // Every verb on the form, and its own Delete, goes through one invoker: a\n // destructive verb asks, asks for the name, or runs and offers Undo, as it\n // declared — `revoke-sessions` ends every session a user holds.\n const verbs = useVerbInvoker<undefined>({\n run: key => (key === DELETE_KEY ? onDelete?.() : onUseCase?.(key)),\n busy,\n served: metadata?.useCases,\n });\n // What a confirmation names: the record as the reader knows it — its first\n // filled-in field — or, for a record with nothing filled in, the entity.\n const metaEntity = extractMetaEntity(entityConstructor);\n const named = fields.find(\n field => field.type !== 'id' && readDraftString(draft, field.name) !== '',\n );\n const recordLabel =\n hostRecordLabel ??\n (named === undefined\n ? entityNoun(metaEntity)\n : readDraftString(draft, named.name));\n const invoke = (descriptor: UseCaseDescriptor) =>\n verbs.invoke({\n descriptor,\n target: undefined,\n object: recordLabel,\n count: 1,\n });\n const requestDelete = () => {\n // Undo is the host's to offer: only it can put a deleted record back.\n if (deletePolicy === 'undo') {\n onDelete?.();\n return;\n }\n invoke({\n key: DELETE_KEY,\n binding: 'entity',\n placement: 'determining',\n labelKey: 'controls:form.delete',\n confirm: {\n tone: 'destructive',\n messageKey: 'controls:confirm.deleteMessage',\n },\n reversible: deletePolicy,\n });\n };\n\n const headerItems = itemsForUseCases(\n headerUseCases,\n translateKey,\n invoke,\n busy,\n );\n const footerItems = itemsForUseCases(\n footerUseCases,\n translateKey,\n invoke,\n busy,\n );\n /**\n * A save attempt. Counted, so a refused one moves focus to the error summary\n * once — not again on every keystroke that leaves an error standing.\n */\n const [attempt, setAttempt] = useState(0);\n const save = () => {\n setAttempt(current => current + 1);\n onSubmit?.(draft);\n };\n const summary = useRef<HTMLDivElement>(null);\n const summaryId = useId();\n const invalid = editing\n ? fields.filter(field => errors?.[field.name] !== undefined)\n : [];\n const focusedAttempt = useRef(0);\n useEffect(() => {\n if (attempt === focusedAttempt.current || invalid.length === 0) return;\n focusedAttempt.current = attempt;\n summary.current?.focus();\n }, [attempt, invalid.length]);\n\n // Save is the footer's primary unless the entity declared one of its own — a\n // determining \"Publish\" is what that page is for, and Save steps aside.\n const declaresFooterPrimary = footerItems.some(\n item => item.emphasis === 'primary',\n );\n const footerActions: ActionBarItem[] = [\n ...(may('write')\n ? [\n {\n key: 'save',\n // A pending write waits on the push stream; while that is down\n // it says so, rather than waiting without a word.\n label: isPending\n ? t(live.connection === 'open' ? 'form.pending' : 'live.waiting')\n : isSaving\n ? t('form.saving')\n : t('form.save'),\n emphasis: declaresFooterPrimary\n ? ('default' as const)\n : ('primary' as const),\n disabled: busy || resolvingLinks,\n submit: true,\n shortcut: SAVE_SHORTCUT,\n onSelect: save,\n },\n ]\n : []),\n // The page's main act answers Mod+Enter, unless it declared its own.\n ...footerItems.map(item =>\n item.emphasis === 'primary' && item.shortcut === undefined\n ? { ...item, shortcut: PRIMARY_SHORTCUT }\n : item,\n ),\n // Clone is the CRUD triple's, not a declared verb's: every entity can be\n // copied, so giving it a `@useCase()` would mean declaring the same verb on\n // all 28 of them. It needs `write`, because what it produces is a create.\n ...(onClone && entity && may('write')\n ? [\n {\n key: 'clone',\n label: t('form.clone'),\n disabled: busy,\n onSelect: () => onClone(cloneEntityDraft(entityConstructor, draft)),\n },\n ]\n : []),\n ...(onDelete && may('delete')\n ? [\n {\n key: 'delete',\n label: isDeleting ? t('form.deleting') : t('form.delete'),\n destructive: true,\n disabled: busy,\n onSelect: requestDelete,\n },\n ]\n : []),\n ];\n\n // A real form while editing, so Enter in a field saves; an embedded one\n // belongs to its host's flow, and a caller who may not write has no Save.\n const asForm = editing && !embedded && may('write');\n const rows = fields.map(field => (\n <FieldRow\n key={field.name}\n field={field}\n entity={entity}\n draft={draft}\n value={readDraftString(draft, field.name)}\n editing={editing}\n error={editing ? errors?.[field.name] : undefined}\n setField={setField}\n linkSource={linkSources?.[field.name]}\n onLinkChange={onLinkChange}\n id={`${formId}-${field.name}`}\n />\n ));\n\n const body = (\n <Stack gap=\"s\">\n {/* An embedded form brings only its fields: the heading and the action\n row belong to whatever is hosting it — a wizard step sits under the\n wizard's own heading and advances with the wizard's own footer, so\n rendering a second of each is two controls for one job. */}\n {!embedded && (\n <Stack direction=\"row\" gap=\"xs\" align=\"center\">\n <Text\n as=\"h2\"\n ref={heading}\n tabIndex={-1}\n step={1}\n weight=\"semibold\"\n className=\"focus-ring\"\n >\n {/* `entity` is undefined while the record is still in flight, so\n testing it alone made a loading edit form announce itself as\n \"New\" for as long as the fetch took, then relabel when the\n record landed. A form that is loading is never a create. */}\n {title ?? (entity || isLoading ? t('form.details') : t('form.new'))}\n </Text>\n {/* The built-in toggle only appears when the form owns its mode and\n there is a record to view — a create form has nothing to read. */}\n {modeProp === undefined && entity && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={toggleMode}\n >\n {editing ? t('form.view') : t('form.edit')}\n </Button>\n )}\n <LoadingBoundary isLoading={isMetadataLoading} lines={0}>\n <>\n {headerItems.length > 0 && (\n <ActionBar surface=\"form-header\" items={headerItems} />\n )}\n {slots.headerActions}\n </>\n </LoadingBoundary>\n </Stack>\n )}\n\n {/* One announcement for the form; the shimmer below is aria-hidden. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {isLoading ? t('form.loading') : ''}\n </span>\n\n <RegionState\n plural={entityNoun(metaEntity, 'plural')}\n error={error}\n onRetry={onRetry}\n // Only a load that can be retried leaves an out-of-date record; a\n // failed save leaves what the reader typed, which is current.\n hasStaleData={entity !== undefined && onRetry !== undefined}\n />\n\n {/* A refused save, as one summary of links to what needs fixing: the\n only alert in the form, and where focus goes after the attempt. */}\n {invalid.length > 0 && (\n <div\n ref={summary}\n tabIndex={-1}\n role=\"alert\"\n aria-labelledby={summaryId}\n data-testid=\"entity-form-error-summary\"\n className=\"rounded-lg border-l-4 border-danger bg-danger-subtle px-s py-xs text-step-sm text-content focus-ring\"\n >\n <p id={summaryId} className=\"m-0 font-semibold\">\n {t('form.errorSummary', { count: invalid.length })}\n </p>\n <ul className=\"m-0 ps-s\">\n {invalid.map(field => (\n <li key={field.name}>\n <a\n href={`#${formId}-${field.name}`}\n // The content colour: primary on the danger surface misses\n // 4.5:1.\n className=\"font-medium text-content underline underline-offset-2 focus-ring\"\n onClick={event => {\n event.preventDefault();\n document.getElementById(`${formId}-${field.name}`)?.focus();\n }}\n >\n {errors?.[field.name]}\n </a>\n </li>\n ))}\n </ul>\n </div>\n )}\n\n {editing && fields.some(field => field.required && !field.readonly) && (\n <Text step={-1} tone=\"muted\">\n {t('form.requiredHint')}\n </Text>\n )}\n\n {/* The placeholder *replaces* the rows rather than sitting above them.\n Rendering both meant a shimmer stacked on a full set of empty fields,\n which is two loading signals for one region and twice the height the\n loaded form settles at. */}\n {showSkeleton\n ? (customSkeleton ??\n fields.map(field => (\n <Stack key={field.name} gap=\"3xs\">\n <Skeleton shape=\"line\" className=\"w-24\" />\n <Skeleton className=\"h-9 w-full\" />\n </Stack>\n )))\n : editing\n ? rows\n : // Read mode is a description list: a label points at nothing\n // when there is no control, and a term and its value is what\n // the pair is.\n rows.length > 0 && (\n <dl className=\"m-0 flex flex-col gap-s\">{rows}</dl>\n )}\n\n {/* The rows the record owns, full width and below every field. Each\n grid is handed the *whole* error map and picks out the keys naming\n its own member, so a record owning two collections needs no\n bookkeeping here. */}\n {ownedCollections.map(field => (\n <EntityDetailGrid\n key={field.name}\n descriptor={field}\n rows={readRowDrafts(draft[field.name])}\n onRowsChange={rows => setField(field.name, rows)}\n errors={editing ? errors : undefined}\n editing={editing}\n isLoading={isLoading}\n skeleton={skeleton}\n />\n ))}\n\n {/* A cross-field rule has no row to sit under, so it sits with the\n actions it blocks. */}\n {editing && formError && (\n <span role=\"alert\" className=\"text-step-sm text-danger\">\n {formError}\n </span>\n )}\n\n {editing && !embedded && (\n <Stack direction=\"row\" gap=\"xs\" align=\"center\">\n <ActionBar surface=\"form-footer\" size=\"md\" items={footerActions} />\n {slots.footerActions}\n {/* A link styled as a button, not a button inside a link: the old\n nesting was two interactive elements for one action, and its\n `disabled` stopped nothing, because the anchor around it still\n navigated. While busy the link is inert instead. */}\n {backHref &&\n renderLink({\n href: backHref,\n className: cn(\n button({ variant: 'ghost' }),\n busy && 'pointer-events-none opacity-50',\n ),\n children: t('form.back'),\n intent: { kind: 'back' },\n })}\n </Stack>\n )}\n\n {slots.rest}\n </Stack>\n );\n\n return (\n <Card>\n {asForm ? (\n <form\n noValidate\n // Enter submits through Save, the form's default button, which is\n // disabled while a save is in flight — so a busy form stays put.\n onSubmit={event => {\n event.preventDefault();\n save();\n }}\n >\n {body}\n </form>\n ) : (\n body\n )}\n\n {verbs.dialog}\n </Card>\n );\n}\n\n/**\n * A member the detail grid renders rather than the field stack.\n *\n * The `childType` half is not defensive: a `composition` that declared no child\n * has no columns, so there is nothing for a grid to draw. It keeps its place in\n * the field stack and reads as its row count, which is what it did before this\n * control existed — an honest \"some rows\" rather than an empty table implying\n * there are none.\n */\nfunction isOwnedCollection<TEntity extends Entity>(\n field: EntityFormField<TEntity>,\n): boolean {\n return field.type === 'composition' && field.childType !== undefined;\n}\n\n/**\n * Refuse a `linkSources` entry that names a member no picker can be built for.\n *\n * `FieldRow` asks for {@link PICKABLE_TYPES} before it builds an editor, so a\n * source handed to anything else used to fall through to the read-only display\n * or a plain `FieldControl` with nothing said: the caller wired a picker, no\n * picker appeared, and the field was indistinguishable from one the entity had\n * declared read-only. That is a wiring mistake rather than a state — the entry\n * is dead in *every* shape such a field can render, since an\n * `<EntityField render>` slot is handed the draft and never the source — so it\n * costs the render instead of a warning nobody reads. `useEntityLinkSource`\n * already throws at render for the sibling mistake (a search\n * property the target does not declare `filterable`); this is the same fault\n * caught one level up.\n *\n * Two messages, because the two faults are not the same thing. A\n * `linkCollection` is a member the editor has simply not been built for yet\n * (#26), so the fix is to wait or to point at the to-one you meant. Any other\n * type is a member that can never name another record at all, so the fix is to\n * remove the entry.\n *\n * Here rather than inside `FieldRow`, and that is the whole reason it is a\n * separate pass: a row only reaches its editor branch in **edit** mode, so a\n * per-row check would stay silent on a form that opens in `read` — the default\n * whenever there is a record — and then throw on a click, minutes later. One\n * pass over the registry fires on the first render either way.\n *\n * Against the resolved fields rather than the raw metadata: a slot may drop a\n * member, and a source left behind for a field that no longer renders is not\n * worth a page.\n */\nfunction assertLinkSourcesAreEditable<TEntity extends Entity>(\n fields: Array<EntityFormField<TEntity>>,\n // Same reason as the prop's own declaration: a source's target is another\n // entity entirely and cannot be expressed in terms of `TEntity`.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n linkSources: Record<string, EntityLinkSource<any>> | undefined,\n): void {\n if (linkSources === undefined) return;\n\n const sourced = fields.filter(\n field =>\n linkSources[field.name] !== undefined && !PICKABLE_TYPES.has(field.type),\n );\n if (sourced.length === 0) return;\n\n const toMany = sourced\n .filter(field => field.type === 'linkCollection')\n .map(field => field.name);\n if (toMany.length > 0) {\n throw new Error(\n `EntityForm was given a link source for a to-many member: ${toMany.join(', ')}. ` +\n 'A `linkCollection` has no editor yet, so the source can only be dropped ' +\n 'and the field would render read-only with no sign anything was wired. ' +\n 'Remove the entry, or point it at the to-one `link` you meant.',\n );\n }\n\n const owned = sourced\n .filter(field => field.type === 'composition')\n .map(field => field.name);\n if (owned.length > 0) {\n throw new Error(\n `EntityForm was given a link source for an owned collection: ${owned.join(', ')}. ` +\n 'A `composition` is not a reference — its rows have no life outside this ' +\n 'record and are never picked from existing ones, so there is nothing for a ' +\n 'picker to look up. The detail grid is what edits them.',\n );\n }\n\n const unpickable = sourced.map(field => `${field.name} (${field.type})`);\n throw new Error(\n `EntityForm was given a link source for a member that cannot hold one: ${unpickable.join(', ')}. ` +\n 'A picker writes the target’s id into the draft, so the member has to be a ' +\n '`link` or the `string` that carries a foreign key. Remove the entry, or ' +\n 'point it at the member that actually holds the reference.',\n );\n}\n\ninterface FieldRowProps<TEntity extends Entity> {\n field: EntityFormField<TEntity>;\n entity: TEntity | undefined;\n draft: EntityDraft;\n value: string;\n editing: boolean;\n error: string | undefined;\n setField: (name: string, value: EntityDraftValue) => void;\n // The target of a relation is a different entity than the form's own.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n linkSource: EntityLinkSource<any> | undefined;\n onLinkChange:\n ((name: string, entity: Entity | undefined) => void) | undefined;\n id: string;\n}\n\n/**\n * One label + control (or value) row. A `readonly` member renders its read\n * display even in edit mode, and so does a relation with no source; everything\n * else edits through `FieldControl` — or, when a source was supplied for a\n * member in {@link PICKABLE_TYPES}, through `EntityLinkInput`. A boolean edited\n * inline carries its own label, so the row does not add a second one.\n *\n * The source branch sits above the `virtual || isRelation` fallback and below\n * `field.render`, which is what lets a slot still override a picked field.\n */\nfunction FieldRow<TEntity extends Entity>({\n field,\n entity,\n draft,\n value,\n editing,\n error,\n setField,\n linkSource,\n onLinkChange,\n id,\n}: FieldRowProps<TEntity>) {\n const t = useT();\n const errorId = `${id}-error`;\n const readNode: ReactNode = field.readRender ? (\n field.readRender(entity)\n ) : (\n <CellValue\n value={\n entity ? (entity as Record<string, unknown>)[field.name] : undefined\n }\n descriptor={field}\n record={entity as Record<string, unknown> | undefined}\n />\n );\n\n // Members with no editor of their own here: a to-one `link` whose picker was\n // never wired, a `linkCollection` (#26) and a `composition`. All three\n // fall back to the read display, which is honest — a disabled text box\n // holding `[object Object]` is not.\n const isRelation =\n field.type === 'link' ||\n field.type === 'linkCollection' ||\n field.type === 'composition';\n\n let control: ReactNode;\n if (!editing) {\n control = readNode;\n } else if (field.render) {\n control = field.render({ draft, value, setField, id });\n } else if (linkSource && PICKABLE_TYPES.has(field.type)) {\n control = (\n <EntityLinkInput\n descriptor={field}\n value={value}\n source={linkSource}\n id={id}\n disabled={field.readonly}\n onSelect={target => {\n setField(field.name, target.id == null ? '' : String(target.id));\n onLinkChange?.(field.name, target);\n }}\n onClear={() => {\n setField(field.name, '');\n onLinkChange?.(field.name, undefined);\n }}\n />\n );\n } else if (field.type === 'localized' && !field.virtual) {\n control = (\n <LocalizedFieldControl\n descriptor={field}\n value={readLocalizedDraft(draft, field.name)}\n onChange={next => setField(field.name, next)}\n id={id}\n />\n );\n } else if (field.virtual || isRelation) {\n // A relation with no source has no input to offer, and a virtual field has\n // no member to edit unless a `render` was supplied — both fall back to the\n // read display.\n control = readNode;\n } else {\n control = (\n <FieldControl\n descriptor={field}\n value={value}\n onChange={next => setField(field.name, next)}\n id={id}\n record={draft}\n aria-invalid={error !== undefined || undefined}\n aria-describedby={error === undefined ? undefined : errorId}\n aria-required={field.required || undefined}\n />\n );\n }\n\n if (!editing)\n return (\n <div className=\"flex flex-col gap-3xs\">\n <dt className=\"text-step-sm text-content-muted\">{field.label}</dt>\n <dd className=\"m-0\">{control}</dd>\n </div>\n );\n\n const ownsLabel = field.type === 'boolean' && !field.render && !field.virtual;\n // Marked before anyone submits, not discovered after.\n const marked = field.required && !field.readonly;\n\n return (\n <Stack gap=\"3xs\">\n {!ownsLabel && (\n // The marker sits beside the label, not in it: the name stays the\n // field's own, and `aria-required` is what says it to a reader.\n <span className=\"flex items-baseline gap-3xs\">\n <label htmlFor={id} className=\"text-step-sm text-content-muted\">\n {field.label}\n </label>\n {marked && (\n <span aria-hidden=\"true\" className=\"text-step-sm text-danger\">\n {t('form.requiredMarker')}\n </span>\n )}\n </span>\n )}\n {control}\n {/* Not an alert of its own: the summary above the fields is the one\n announcement, and this is what the control's description reads. */}\n {error && (\n <span id={errorId} className=\"text-step-sm text-danger\">\n {error}\n </span>\n )}\n </Stack>\n );\n}\n"],"names":["cloneEntityDraft","describeEntityColumns","extractMetaEntity","readDraftString","readLocalizedDraft","readRowDrafts","useEffect","useId","useRef","useState","useEntityNoun","useLocalizedDescriptors","useT","useTranslateKey","renderAnchor","useCasesForSurface","useVerbInvoker","Button","button","CellValue","FieldControl","LocalizedFieldControl","Skeleton","Text","ActionBar","itemsForUseCases","Card","EntityLinkInput","LoadingBoundary","RegionState","Stack","cn","useLiveUpdates","useFocusOnArrival","EntityDetailGrid","readEntityFormFields","resolveEntityFormFields","PICKABLE_TYPES","Set","DELETE_KEY","SAVE_SHORTCUT","PRIMARY_SHORTCUT","EntityForm","entityConstructor","entity","values","onFieldChange","linkSources","onLinkChange","mode","modeProp","defaultMode","onModeChange","errors","formError","onSubmit","onDelete","deletePolicy","recordLabel","hostRecordLabel","embedded","isLoading","skeleton","isSaving","isPending","isDeleting","error","onRetry","backHref","renderLink","title","metadata","isMetadataLoading","onUseCase","onClone","children","formId","slots","columnDescriptors","described","resolved","fields","assertLinkSourcesAreEditable","filter","field","isOwnedCollection","ownedCollections","customSkeleton","undefined","showSkeleton","internalMode","setInternalMode","editing","draft","setField","name","value","toggleMode","next","t","translateKey","entityNoun","live","heading","busy","resolvingLinks","Object","some","source","selected","may","action","actions","includes","declaredHeaderUseCases","useCases","declaredFooterUseCases","headerUseCases","footerUseCases","verbs","run","key","served","metaEntity","named","find","type","invoke","descriptor","target","object","count","requestDelete","binding","placement","labelKey","confirm","tone","messageKey","reversible","headerItems","footerItems","attempt","setAttempt","save","current","summary","summaryId","invalid","focusedAttempt","length","focus","declaresFooterPrimary","item","emphasis","footerActions","label","connection","disabled","submit","shortcut","onSelect","map","destructive","asForm","rows","FieldRow","linkSource","id","body","gap","direction","align","as","ref","tabIndex","step","weight","className","variant","size","onClick","lines","surface","items","headerActions","span","role","aria-live","plural","hasStaleData","div","aria-labelledby","data-testid","p","ul","li","a","href","event","document","preventDefault","getElementById","required","readonly","shape","dl","onRowsChange","intent","kind","rest","form","noValidate","dialog","childType","sourced","has","toMany","Error","join","owned","unpickable","errorId","readNode","readRender","record","isRelation","control","render","String","onClear","virtual","onChange","aria-invalid","aria-describedby","aria-required","dt","dd","ownsLabel","marked","htmlFor","aria-hidden"],"mappings":"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"}
@@ -89,6 +89,14 @@ export interface EntityFormProps<TEntity extends Entity> {
89
89
  * at once — the host offers Undo, since only it can put the record back.
90
90
  */
91
91
  deletePolicy?: 'confirm' | 'typed' | 'undo';
92
+ /**
93
+ * What a confirmation calls this record — the object in "Delete {record}?"
94
+ * and the text the `typed` policy asks for. Defaults to the first filled-in
95
+ * field, which names the record only when that field is its name: for a
96
+ * record whose first field is a grouping (a service, a category) pass the
97
+ * member that identifies it, or the dialog names the group.
98
+ */
99
+ recordLabel?: string;
92
100
  /**
93
101
  * What this caller may do with the entity, as served by
94
102
  * `GET /api/<entity>/$metadata` and already filtered against the verified
@@ -1 +1 @@
1
- {"version":3,"file":"entity-form.types.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-form/entity-form.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"entity-form.types.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-form/entity-form.types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/organisms/entity-form/entity-form.types.ts"],"sourcesContent":["import type {\n EntifixFailure,\n Entity,\n EntityConstructor,\n EntityDraft,\n EntityDraftValue,\n EntityFieldDescriptor,\n EntityLinkSource,\n EntityMetadataDocument,\n} from '@entifix/core';\nimport type { ReactNode } from 'react';\n\nimport type { RenderLink } from '../../actions/index.js';\n\n/** Whether the form shows values as text (`read`) or as inputs (`edit`). */\nexport type EntityFormMode = 'read' | 'edit';\n\n/** What a custom edit control for one field is handed. */\nexport interface EntityFieldRenderContext {\n draft: EntityDraft;\n value: string;\n setField: (name: string, value: EntityDraftValue) => void;\n /** The id the field's label points at — put it on your control. */\n id: string;\n}\n\n/**\n * A field as the form finally renders it: the metadata descriptor plus the\n * per-field overrides a caller supplied through an `<EntityField>` slot.\n */\nexport interface EntityFormField<\n TEntity extends Entity,\n> extends EntityFieldDescriptor {\n /** Replaces the default edit control. */\n render?: (context: EntityFieldRenderContext) => ReactNode;\n /** Replaces the default read display. */\n readRender?: (entity: TEntity | undefined) => ReactNode;\n /** True when the field has no backing entity member (slot-only field). */\n virtual?: boolean;\n}\n\nexport interface EntityFormProps<TEntity extends Entity> {\n /** Metadata source. Fields, labels and types derive from its accessors. */\n entityConstructor: EntityConstructor<TEntity>;\n /** The record being viewed/edited. The read display and, in a wrapper, the\n * initial draft both come from it; omit for a create form. */\n entity?: TEntity;\n\n /** The current draft. The form is controlled — a host (the `useEntityForm`\n * hook or a `useState`) owns this and updates it via `onFieldChange`. */\n values?: EntityDraft;\n /**\n * Called when one field changes.\n *\n * The value is a union rather than a string because a `composition`\n * member's is its list of rows — the one member with no lossless string form.\n * Every scalar editor still writes a string, so a host that only edits\n * scalars is unaffected.\n */\n onFieldChange?: (name: string, value: EntityDraftValue) => void;\n\n /**\n * A search/browse source per reference, keyed by accessor name. A `link` field\n * with a source gets the full editor; one without keeps its read-only display,\n * which is what a form that only shows a relation still wants.\n *\n * A **`string`** field with a source gets the same editor, because a foreign\n * key whose target lives in another slice's store cannot be a typed `link` at\n * all and is a plain id member. The editor does not know the\n * difference — it writes the target's id into the draft either way.\n *\n * A key naming any other member throws: `linkCollection` has no editor\n * anywhere yet, and a `boolean`/`enum`/`number`/`date` can never name another\n * record, so the entry could only be dropped — and a dropped source looks\n * exactly like a member the entity declared read-only.\n *\n * A registry rather than a slot per relation: the whole point is that an entity\n * declaring a reference needs no bespoke control. The sources are built by the\n * caller because fetching them is the integration layer's job, and this package\n * cannot reach it.\n */\n // A source's target is another entity entirely, so it cannot be expressed in\n // terms of `TEntity`; the input it feeds is instantiated per field anyway.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n linkSources?: Record<string, EntityLinkSource<any>>;\n /**\n * A relation was set or cleared. The instance travels alongside the id the\n * draft holds, so a host can remember it — an id alone cannot be written as an\n * embedded relation.\n */\n onLinkChange?: (name: string, entity: Entity | undefined) => void;\n\n /** Force a mode. Omit to let the form own it (with the built-in toggle). */\n mode?: EntityFormMode;\n /** Initial mode when uncontrolled. Defaults to `read` with an `entity`, else\n * `edit` (a create form opens ready to type). */\n defaultMode?: EntityFormMode;\n onModeChange?: (mode: EntityFormMode) => void;\n\n /** Per-field validation messages, keyed by field name. */\n errors?: Record<string, string>;\n /**\n * A validation message that belongs to no single field — a rule spanning two\n * of them. Rendered above the actions, since no row can host it.\n */\n formError?: string;\n\n onSubmit?: (draft: EntityDraft) => void;\n onDelete?: () => void;\n /**\n * How the built-in Delete is made safe: `confirm` (the default) asks in an\n * alert dialog, `typed` asks for the record's name first, and `undo` deletes\n * at once — the host offers Undo, since only it can put the record back.\n */\n deletePolicy?: 'confirm' | 'typed' | 'undo';\n\n /**\n * What this caller may do with the entity, as served by\n * `GET /api/<entity>/$metadata` and already filtered against the verified\n * principal.\n *\n * **Optional, and absent means \"as before\".** Without it the form renders Save\n * and Delete exactly as it always has, so a call site that has not migrated is\n * unaffected. This is not a security control — hiding a button protects\n * nothing; the route guard does. It is what stops a screen offering an action\n * the service will refuse.\n */\n metadata?: EntityMetadataDocument;\n /** The document is still in flight; the action row renders as a skeleton. */\n isMetadataLoading?: boolean;\n /**\n * A declared use case was invoked, by its `key`. The form renders the button\n * and asks for confirmation where the descriptor says to; performing the act\n * is the caller's — the browser must never import a use-case class.\n */\n onUseCase?: (key: string) => void;\n\n /**\n * Copies the record into a new draft, dropping the members declared\n * `@accessor({ resetOnClone: true })` and always the id.\n *\n * A prop rather than something the form does by itself, because the *result*\n * of a clone is a navigation — a create form holding the copy — and this\n * package knows no router. The form supplies the cleaned draft\n * (`cloneEntityDraft`); where it lands is the caller's.\n *\n * Absent renders no Clone action.\n */\n onClone?: (draft: EntityDraft) => void;\n\n /**\n * Render the **fields and nothing else** — no heading, no action row.\n *\n * For a form hosted inside something that already owns those controls: a\n * wizard step sits under the wizard's own heading and advances with the\n * wizard's own footer, so a second heading and a Save button beside Siguiente\n * would be two controls for one job, and the Save would commit a partial\n * record mid-flow.\n *\n * Opt-in and defaulted off, so every existing form is untouched.\n */\n embedded?: boolean;\n isLoading?: boolean;\n /**\n * What holds the form's shape while the record is in flight.\n *\n * `true` (the default) renders the built-in placeholder — one label+control\n * pair per resolved field, so the swap to the real rows shifts nothing. A node\n * replaces that default; `false` renders no placeholder at all.\n *\n * The placeholder *replaces* the field rows; it never stacks above them.\n */\n skeleton?: boolean | ReactNode;\n isSaving?: boolean;\n /**\n * A write the server handed to a flow that has not settled yet. The fields\n * keep what was typed, and Save waits, until the flow's outcome arrives.\n */\n isPending?: boolean;\n isDeleting?: boolean;\n /** The failure of the last load/save, shown as an alert. */\n error?: EntifixFailure;\n /** Loads the record again — what a failed load offers as Retry. */\n onRetry?: () => void;\n\n /** Link to return to, rendered as a `Back` action. Omit to hide it. */\n backHref?: string;\n /**\n * Renders the `Back` link. Defaults to a plain anchor, which a client-routed\n * host experiences as a full document load. See {@link RenderLink}.\n */\n renderLink?: RenderLink;\n /** Heading for the card. Defaults to a generic label. */\n title?: ReactNode;\n\n /** Customization slots — see `entity-form-slots`. */\n children?: ReactNode;\n}\n"],"names":[],"mappings":"AAyCA,WA4JC"}
1
+ {"version":3,"sources":["../../../../src/ui/organisms/entity-form/entity-form.types.ts"],"sourcesContent":["import type {\n EntifixFailure,\n Entity,\n EntityConstructor,\n EntityDraft,\n EntityDraftValue,\n EntityFieldDescriptor,\n EntityLinkSource,\n EntityMetadataDocument,\n} from '@entifix/core';\nimport type { ReactNode } from 'react';\n\nimport type { RenderLink } from '../../actions/index.js';\n\n/** Whether the form shows values as text (`read`) or as inputs (`edit`). */\nexport type EntityFormMode = 'read' | 'edit';\n\n/** What a custom edit control for one field is handed. */\nexport interface EntityFieldRenderContext {\n draft: EntityDraft;\n value: string;\n setField: (name: string, value: EntityDraftValue) => void;\n /** The id the field's label points at — put it on your control. */\n id: string;\n}\n\n/**\n * A field as the form finally renders it: the metadata descriptor plus the\n * per-field overrides a caller supplied through an `<EntityField>` slot.\n */\nexport interface EntityFormField<\n TEntity extends Entity,\n> extends EntityFieldDescriptor {\n /** Replaces the default edit control. */\n render?: (context: EntityFieldRenderContext) => ReactNode;\n /** Replaces the default read display. */\n readRender?: (entity: TEntity | undefined) => ReactNode;\n /** True when the field has no backing entity member (slot-only field). */\n virtual?: boolean;\n}\n\nexport interface EntityFormProps<TEntity extends Entity> {\n /** Metadata source. Fields, labels and types derive from its accessors. */\n entityConstructor: EntityConstructor<TEntity>;\n /** The record being viewed/edited. The read display and, in a wrapper, the\n * initial draft both come from it; omit for a create form. */\n entity?: TEntity;\n\n /** The current draft. The form is controlled — a host (the `useEntityForm`\n * hook or a `useState`) owns this and updates it via `onFieldChange`. */\n values?: EntityDraft;\n /**\n * Called when one field changes.\n *\n * The value is a union rather than a string because a `composition`\n * member's is its list of rows — the one member with no lossless string form.\n * Every scalar editor still writes a string, so a host that only edits\n * scalars is unaffected.\n */\n onFieldChange?: (name: string, value: EntityDraftValue) => void;\n\n /**\n * A search/browse source per reference, keyed by accessor name. A `link` field\n * with a source gets the full editor; one without keeps its read-only display,\n * which is what a form that only shows a relation still wants.\n *\n * A **`string`** field with a source gets the same editor, because a foreign\n * key whose target lives in another slice's store cannot be a typed `link` at\n * all and is a plain id member. The editor does not know the\n * difference — it writes the target's id into the draft either way.\n *\n * A key naming any other member throws: `linkCollection` has no editor\n * anywhere yet, and a `boolean`/`enum`/`number`/`date` can never name another\n * record, so the entry could only be dropped — and a dropped source looks\n * exactly like a member the entity declared read-only.\n *\n * A registry rather than a slot per relation: the whole point is that an entity\n * declaring a reference needs no bespoke control. The sources are built by the\n * caller because fetching them is the integration layer's job, and this package\n * cannot reach it.\n */\n // A source's target is another entity entirely, so it cannot be expressed in\n // terms of `TEntity`; the input it feeds is instantiated per field anyway.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n linkSources?: Record<string, EntityLinkSource<any>>;\n /**\n * A relation was set or cleared. The instance travels alongside the id the\n * draft holds, so a host can remember it — an id alone cannot be written as an\n * embedded relation.\n */\n onLinkChange?: (name: string, entity: Entity | undefined) => void;\n\n /** Force a mode. Omit to let the form own it (with the built-in toggle). */\n mode?: EntityFormMode;\n /** Initial mode when uncontrolled. Defaults to `read` with an `entity`, else\n * `edit` (a create form opens ready to type). */\n defaultMode?: EntityFormMode;\n onModeChange?: (mode: EntityFormMode) => void;\n\n /** Per-field validation messages, keyed by field name. */\n errors?: Record<string, string>;\n /**\n * A validation message that belongs to no single field — a rule spanning two\n * of them. Rendered above the actions, since no row can host it.\n */\n formError?: string;\n\n onSubmit?: (draft: EntityDraft) => void;\n onDelete?: () => void;\n /**\n * How the built-in Delete is made safe: `confirm` (the default) asks in an\n * alert dialog, `typed` asks for the record's name first, and `undo` deletes\n * at once — the host offers Undo, since only it can put the record back.\n */\n deletePolicy?: 'confirm' | 'typed' | 'undo';\n /**\n * What a confirmation calls this record — the object in \"Delete {record}?\"\n * and the text the `typed` policy asks for. Defaults to the first filled-in\n * field, which names the record only when that field is its name: for a\n * record whose first field is a grouping (a service, a category) pass the\n * member that identifies it, or the dialog names the group.\n */\n recordLabel?: string;\n\n /**\n * What this caller may do with the entity, as served by\n * `GET /api/<entity>/$metadata` and already filtered against the verified\n * principal.\n *\n * **Optional, and absent means \"as before\".** Without it the form renders Save\n * and Delete exactly as it always has, so a call site that has not migrated is\n * unaffected. This is not a security control — hiding a button protects\n * nothing; the route guard does. It is what stops a screen offering an action\n * the service will refuse.\n */\n metadata?: EntityMetadataDocument;\n /** The document is still in flight; the action row renders as a skeleton. */\n isMetadataLoading?: boolean;\n /**\n * A declared use case was invoked, by its `key`. The form renders the button\n * and asks for confirmation where the descriptor says to; performing the act\n * is the caller's — the browser must never import a use-case class.\n */\n onUseCase?: (key: string) => void;\n\n /**\n * Copies the record into a new draft, dropping the members declared\n * `@accessor({ resetOnClone: true })` and always the id.\n *\n * A prop rather than something the form does by itself, because the *result*\n * of a clone is a navigation — a create form holding the copy — and this\n * package knows no router. The form supplies the cleaned draft\n * (`cloneEntityDraft`); where it lands is the caller's.\n *\n * Absent renders no Clone action.\n */\n onClone?: (draft: EntityDraft) => void;\n\n /**\n * Render the **fields and nothing else** — no heading, no action row.\n *\n * For a form hosted inside something that already owns those controls: a\n * wizard step sits under the wizard's own heading and advances with the\n * wizard's own footer, so a second heading and a Save button beside Siguiente\n * would be two controls for one job, and the Save would commit a partial\n * record mid-flow.\n *\n * Opt-in and defaulted off, so every existing form is untouched.\n */\n embedded?: boolean;\n isLoading?: boolean;\n /**\n * What holds the form's shape while the record is in flight.\n *\n * `true` (the default) renders the built-in placeholder — one label+control\n * pair per resolved field, so the swap to the real rows shifts nothing. A node\n * replaces that default; `false` renders no placeholder at all.\n *\n * The placeholder *replaces* the field rows; it never stacks above them.\n */\n skeleton?: boolean | ReactNode;\n isSaving?: boolean;\n /**\n * A write the server handed to a flow that has not settled yet. The fields\n * keep what was typed, and Save waits, until the flow's outcome arrives.\n */\n isPending?: boolean;\n isDeleting?: boolean;\n /** The failure of the last load/save, shown as an alert. */\n error?: EntifixFailure;\n /** Loads the record again — what a failed load offers as Retry. */\n onRetry?: () => void;\n\n /** Link to return to, rendered as a `Back` action. Omit to hide it. */\n backHref?: string;\n /**\n * Renders the `Back` link. Defaults to a plain anchor, which a client-routed\n * host experiences as a full document load. See {@link RenderLink}.\n */\n renderLink?: RenderLink;\n /** Heading for the card. Defaults to a generic label. */\n title?: ReactNode;\n\n /** Customization slots — see `entity-form-slots`. */\n children?: ReactNode;\n}\n"],"names":[],"mappings":"AAyCA,WAoKC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/react-controls",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "Entity-agnostic React UI — table, form, filters, wizard, command palette — rendered from entity metadata, plus layout primitives.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,7 +19,7 @@
19
19
  "@headlessui/react": "^2.2.10",
20
20
  "class-variance-authority": "^0.7.1",
21
21
  "idb": "^8.0.4",
22
- "@entifix/core": "0.20.0"
22
+ "@entifix/core": "0.20.2"
23
23
  },
24
24
  "exports": {
25
25
  "./package.json": "./package.json",
@@ -55,8 +55,8 @@
55
55
  "react": "19.3.1",
56
56
  "react-i18next": "17.1.10",
57
57
  "storybook": "^10.6.5",
58
- "@entifix/style": "0.20.0",
59
- "@entifix/i18n": "0.20.0"
58
+ "@entifix/i18n": "0.20.2",
59
+ "@entifix/style": "0.20.2"
60
60
  },
61
61
  "nx": {
62
62
  "tags": [
@@ -111,7 +111,7 @@
111
111
  "i18next": ">=26 <27",
112
112
  "react": ">=19 <20",
113
113
  "react-i18next": ">=17 <18",
114
- "@entifix/i18n": "0.20.0"
114
+ "@entifix/i18n": "0.20.2"
115
115
  },
116
116
  "peerDependenciesMeta": {
117
117
  "@entifix/i18n": {