@entifix/react-controls 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/i18n/entity-labels.d.ts +17 -5
  2. package/dist/i18n/entity-labels.d.ts.map +1 -1
  3. package/dist/i18n/entity-labels.js +30 -6
  4. package/dist/i18n/entity-labels.js.map +1 -1
  5. package/dist/preferences/indexeddb-ui-preferences-state.d.ts +1 -1
  6. package/dist/preferences/indexeddb-ui-preferences-state.d.ts.map +1 -1
  7. package/dist/preferences/indexeddb-ui-preferences-state.js +5 -3
  8. package/dist/preferences/indexeddb-ui-preferences-state.js.map +1 -1
  9. package/dist/preferences/local-storage-ui-preferences-state.d.ts.map +1 -1
  10. package/dist/preferences/local-storage-ui-preferences-state.js +10 -4
  11. package/dist/preferences/local-storage-ui-preferences-state.js.map +1 -1
  12. package/dist/preferences/ui-preferences-state.d.ts +4 -4
  13. package/dist/preferences/ui-preferences-state.d.ts.map +1 -1
  14. package/dist/preferences/ui-preferences-state.js.map +1 -1
  15. package/dist/ui/actions/action-surfaces.d.ts.map +1 -1
  16. package/dist/ui/actions/action-surfaces.js +1 -6
  17. package/dist/ui/actions/action-surfaces.js.map +1 -1
  18. package/dist/ui/organisms/entity-form/entity-form.d.ts.map +1 -1
  19. package/dist/ui/organisms/entity-form/entity-form.js +5 -11
  20. package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
  21. package/dist/ui/organisms/entity-form/entity-form.types.d.ts +2 -2
  22. package/dist/ui/organisms/entity-form/entity-form.types.d.ts.map +1 -1
  23. package/dist/ui/organisms/entity-form/entity-form.types.js.map +1 -1
  24. package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
  25. package/dist/ui/organisms/entity-table/entity-table.js +2 -2
  26. package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
  27. package/dist/ui/organisms/entity-table/entity-table.types.d.ts +2 -2
  28. package/dist/ui/organisms/entity-table/entity-table.types.d.ts.map +1 -1
  29. package/dist/ui/organisms/entity-table/entity-table.types.js.map +1 -1
  30. package/package.json +7 -7
@@ -1,4 +1,4 @@
1
- import type { EntityFieldDescriptor } from '@entifix/core';
1
+ import { type EntityFieldDescriptor } from '@entifix/core';
2
2
  /**
3
3
  * Resolves each descriptor's `labelKey` against the active catalog, leaving the
4
4
  * declared `label` in place for members that have no key yet.
@@ -15,16 +15,28 @@ export declare function useLocalizedDescriptors<TDescriptor extends EntityFieldD
15
15
  * used to render as everywhere.
16
16
  */
17
17
  export declare function useEnumLabel(): (descriptor: EntityFieldDescriptor, value: string) => string;
18
+ /**
19
+ * A failure's own fields — `slug` on a `DuplicateSlug`, `entity` on a
20
+ * `NotFound` — as a catalog sentence interpolates them: what its problem body
21
+ * carries beside the envelope. Anything that is not a typed failure has none.
22
+ */
23
+ export declare const failureFields: (failure: unknown) => Record<string, unknown>;
24
+ /** {@link failureFields}, memoised on the failure. */
25
+ export declare function useFailureFields(failure: unknown): Record<string, unknown>;
18
26
  /**
19
27
  * Renders a failed request in the reader's language.
20
28
  *
21
- * Services answer with `{ error, code, detail }`, and the rest client carries
22
- * the `code` onto `EntifixError.details`. Falling back to `message` keeps a
23
- * failure that predates the vocabulary — or one raised client-side — readable
24
- * rather than blank.
29
+ * A typed failure carries its `code` on itself
30
+ * ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)),
31
+ * and its own fields are handed to the sentence, so `errors:duplicateSlug`
32
+ * can say which slug. An error from a service that still answers
33
+ * `{ error, code, detail }` carries its code on `details`. Falling back to
34
+ * `message` keeps a failure that predates the vocabulary — or one raised
35
+ * client-side — readable rather than blank.
25
36
  */
26
37
  export declare function useErrorMessage(): (error: {
27
38
  message: string;
39
+ code?: unknown;
28
40
  details?: Record<string, unknown>;
29
41
  }) => string;
30
42
  //# sourceMappingURL=entity-labels.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entity-labels.d.ts","sourceRoot":"","sources":["../../src/i18n/entity-labels.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAK3D;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,SAAS,qBAAqB,EACzC,WAAW,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,EAAE,CAYpD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,CAC9B,UAAU,EAAE,qBAAqB,EACjC,KAAK,EAAE,MAAM,KACV,MAAM,CAUV;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,IAAI,CAAC,KAAK,EAAE;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,KAAK,MAAM,CAYX"}
1
+ {"version":3,"file":"entity-labels.d.ts","sourceRoot":"","sources":["../../src/i18n/entity-labels.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,qBAAqB,EAG3B,MAAM,eAAe,CAAC;AAKvB;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,SAAS,qBAAqB,EACzC,WAAW,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,EAAE,CAYpD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,CAC9B,UAAU,EAAE,qBAAqB,EACjC,KAAK,EAAE,MAAM,KACV,MAAM,CAUV;AAaD;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS,OAAO,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAO/D,CAAC;AAET,sDAAsD;AACtD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE1E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,IAAI,CAAC,KAAK,EAAE;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,KAAK,MAAM,CAaX"}
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { _ as _extends } from "@swc/helpers/_/_extends";
3
+ import { isFailure, problemOf } from '@entifix/core';
3
4
  import { useCallback, useMemo } from 'react';
4
5
  import { useTranslateKey } from './i18n-context.js';
5
6
  /**
@@ -29,19 +30,42 @@ import { useTranslateKey } from './i18n-context.js';
29
30
  translate
30
31
  ]);
31
32
  }
33
+ const ENVELOPE = new Set([
34
+ 'type',
35
+ 'title',
36
+ 'status',
37
+ 'code',
38
+ 'category',
39
+ 'detail',
40
+ 'instance',
41
+ 'traceId'
42
+ ]);
43
+ /**
44
+ * A failure's own fields — `slug` on a `DuplicateSlug`, `entity` on a
45
+ * `NotFound` — as a catalog sentence interpolates them: what its problem body
46
+ * carries beside the envelope. Anything that is not a typed failure has none.
47
+ */ export const failureFields = (failure)=>isFailure(failure) ? Object.fromEntries(Object.entries(problemOf(failure)).filter(([key])=>!ENVELOPE.has(key))) : {};
48
+ /** {@link failureFields}, memoised on the failure. */ export function useFailureFields(failure) {
49
+ return useMemo(()=>failureFields(failure), [
50
+ failure
51
+ ]);
52
+ }
32
53
  /**
33
54
  * Renders a failed request in the reader's language.
34
55
  *
35
- * Services answer with `{ error, code, detail }`, and the rest client carries
36
- * the `code` onto `EntifixError.details`. Falling back to `message` keeps a
37
- * failure that predates the vocabulary — or one raised client-side — readable
38
- * rather than blank.
56
+ * A typed failure carries its `code` on itself
57
+ * ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)),
58
+ * and its own fields are handed to the sentence, so `errors:duplicateSlug`
59
+ * can say which slug. An error from a service that still answers
60
+ * `{ error, code, detail }` carries its code on `details`. Falling back to
61
+ * `message` keeps a failure that predates the vocabulary — or one raised
62
+ * client-side — readable rather than blank.
39
63
  */ export function useErrorMessage() {
40
64
  const translate = useTranslateKey();
41
65
  return useCallback((error)=>{
42
66
  var _error_details;
43
- const code = (_error_details = error.details) == null ? void 0 : _error_details['code'];
44
- return typeof code === 'string' ? translate(`errors:${code}`) : error.message;
67
+ const code = typeof error.code === 'string' ? error.code : (_error_details = error.details) == null ? void 0 : _error_details['code'];
68
+ return typeof code === 'string' ? translate(`errors:${code}`, failureFields(error)) : error.message;
45
69
  }, [
46
70
  translate
47
71
  ]);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/i18n/entity-labels.ts"],"sourcesContent":["'use client';\n\nimport type { EntityFieldDescriptor } from '@entifix/core';\nimport { useCallback, useMemo } from 'react';\n\nimport { useTranslateKey } from './i18n-context.js';\n\n/**\n * Resolves each descriptor's `labelKey` against the active catalog, leaving the\n * declared `label` in place for members that have no key yet.\n *\n * Done here rather than in `describeEntityColumns` because the same descriptors\n * are the server-side filter allowlist, where a translated label would be\n * meaningless — and because `@entifix/core` (T1) cannot reach\n * `@entifix/i18n` (T2).\n */\nexport function useLocalizedDescriptors<\n TDescriptor extends EntityFieldDescriptor,\n>(descriptors: readonly TDescriptor[]): TDescriptor[] {\n const translate = useTranslateKey();\n\n return useMemo(\n () =>\n descriptors.map(descriptor =>\n descriptor.labelKey === undefined\n ? descriptor\n : { ...descriptor, label: translate(descriptor.labelKey) },\n ),\n [descriptors, translate],\n );\n}\n\n/**\n * Reads one value of an enum member. Falls back to the raw value when the\n * member declared no vocabulary prefix, which is what a `status` of `active`\n * used to render as everywhere.\n */\nexport function useEnumLabel(): (\n descriptor: EntityFieldDescriptor,\n value: string,\n) => string {\n const translate = useTranslateKey();\n\n return useCallback(\n (descriptor, value) =>\n descriptor.enumLabelKey === undefined\n ? value\n : translate(`${descriptor.enumLabelKey}.${value}`),\n [translate],\n );\n}\n\n/**\n * Renders a failed request in the reader's language.\n *\n * Services answer with `{ error, code, detail }`, and the rest client carries\n * the `code` onto `EntifixError.details`. Falling back to `message` keeps a\n * failure that predates the vocabulary — or one raised client-side — readable\n * rather than blank.\n */\nexport function useErrorMessage(): (error: {\n message: string;\n details?: Record<string, unknown>;\n}) => string {\n const translate = useTranslateKey();\n\n return useCallback(\n error => {\n const code = error.details?.['code'];\n return typeof code === 'string'\n ? translate(`errors:${code}`)\n : error.message;\n },\n [translate],\n );\n}\n"],"names":["useCallback","useMemo","useTranslateKey","useLocalizedDescriptors","descriptors","translate","map","descriptor","labelKey","undefined","label","useEnumLabel","value","enumLabelKey","useErrorMessage","error","code","details","message"],"mappings":"AAAA;;AAGA,SAASA,WAAW,EAAEC,OAAO,QAAQ,QAAQ;AAE7C,SAASC,eAAe,QAAQ,oBAAoB;AAEpD;;;;;;;;CAQC,GACD,OAAO,SAASC,wBAEdC,WAAmC;IACnC,MAAMC,YAAYH;IAElB,OAAOD,QACL,IACEG,YAAYE,GAAG,CAACC,CAAAA,aACdA,WAAWC,QAAQ,KAAKC,YACpBF,aACA,aAAKA;gBAAYG,OAAOL,UAAUE,WAAWC,QAAQ;iBAE7D;QAACJ;QAAaC;KAAU;AAE5B;AAEA;;;;CAIC,GACD,OAAO,SAASM;IAId,MAAMN,YAAYH;IAElB,OAAOF,YACL,CAACO,YAAYK,QACXL,WAAWM,YAAY,KAAKJ,YACxBG,QACAP,UAAU,GAAGE,WAAWM,YAAY,CAAC,CAAC,EAAED,OAAO,GACrD;QAACP;KAAU;AAEf;AAEA;;;;;;;CAOC,GACD,OAAO,SAASS;IAId,MAAMT,YAAYH;IAElB,OAAOF,YACLe,CAAAA;YACeA;QAAb,MAAMC,QAAOD,iBAAAA,MAAME,OAAO,qBAAbF,cAAe,CAAC,OAAO;QACpC,OAAO,OAAOC,SAAS,WACnBX,UAAU,CAAC,OAAO,EAAEW,MAAM,IAC1BD,MAAMG,OAAO;IACnB,GACA;QAACb;KAAU;AAEf"}
1
+ {"version":3,"sources":["../../src/i18n/entity-labels.ts"],"sourcesContent":["'use client';\n\nimport {\n type EntityFieldDescriptor,\n isFailure,\n problemOf,\n} from '@entifix/core';\nimport { useCallback, useMemo } from 'react';\n\nimport { useTranslateKey } from './i18n-context.js';\n\n/**\n * Resolves each descriptor's `labelKey` against the active catalog, leaving the\n * declared `label` in place for members that have no key yet.\n *\n * Done here rather than in `describeEntityColumns` because the same descriptors\n * are the server-side filter allowlist, where a translated label would be\n * meaningless — and because `@entifix/core` (T1) cannot reach\n * `@entifix/i18n` (T2).\n */\nexport function useLocalizedDescriptors<\n TDescriptor extends EntityFieldDescriptor,\n>(descriptors: readonly TDescriptor[]): TDescriptor[] {\n const translate = useTranslateKey();\n\n return useMemo(\n () =>\n descriptors.map(descriptor =>\n descriptor.labelKey === undefined\n ? descriptor\n : { ...descriptor, label: translate(descriptor.labelKey) },\n ),\n [descriptors, translate],\n );\n}\n\n/**\n * Reads one value of an enum member. Falls back to the raw value when the\n * member declared no vocabulary prefix, which is what a `status` of `active`\n * used to render as everywhere.\n */\nexport function useEnumLabel(): (\n descriptor: EntityFieldDescriptor,\n value: string,\n) => string {\n const translate = useTranslateKey();\n\n return useCallback(\n (descriptor, value) =>\n descriptor.enumLabelKey === undefined\n ? value\n : translate(`${descriptor.enumLabelKey}.${value}`),\n [translate],\n );\n}\n\nconst ENVELOPE: ReadonlySet<string> = new Set([\n 'type',\n 'title',\n 'status',\n 'code',\n 'category',\n 'detail',\n 'instance',\n 'traceId',\n]);\n\n/**\n * A failure's own fields — `slug` on a `DuplicateSlug`, `entity` on a\n * `NotFound` — as a catalog sentence interpolates them: what its problem body\n * carries beside the envelope. Anything that is not a typed failure has none.\n */\nexport const failureFields = (failure: unknown): Record<string, unknown> =>\n isFailure(failure)\n ? Object.fromEntries(\n Object.entries(problemOf(failure)).filter(\n ([key]) => !ENVELOPE.has(key),\n ),\n )\n : {};\n\n/** {@link failureFields}, memoised on the failure. */\nexport function useFailureFields(failure: unknown): Record<string, unknown> {\n return useMemo(() => failureFields(failure), [failure]);\n}\n\n/**\n * Renders a failed request in the reader's language.\n *\n * A typed failure carries its `code` on itself\n * ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)),\n * and its own fields are handed to the sentence, so `errors:duplicateSlug`\n * can say which slug. An error from a service that still answers\n * `{ error, code, detail }` carries its code on `details`. Falling back to\n * `message` keeps a failure that predates the vocabulary — or one raised\n * client-side — readable rather than blank.\n */\nexport function useErrorMessage(): (error: {\n message: string;\n code?: unknown;\n details?: Record<string, unknown>;\n}) => string {\n const translate = useTranslateKey();\n\n return useCallback(\n error => {\n const code =\n typeof error.code === 'string' ? error.code : error.details?.['code'];\n return typeof code === 'string'\n ? translate(`errors:${code}`, failureFields(error))\n : error.message;\n },\n [translate],\n );\n}\n"],"names":["isFailure","problemOf","useCallback","useMemo","useTranslateKey","useLocalizedDescriptors","descriptors","translate","map","descriptor","labelKey","undefined","label","useEnumLabel","value","enumLabelKey","ENVELOPE","Set","failureFields","failure","Object","fromEntries","entries","filter","key","has","useFailureFields","useErrorMessage","error","code","details","message"],"mappings":"AAAA;;AAEA,SAEEA,SAAS,EACTC,SAAS,QACJ,gBAAgB;AACvB,SAASC,WAAW,EAAEC,OAAO,QAAQ,QAAQ;AAE7C,SAASC,eAAe,QAAQ,oBAAoB;AAEpD;;;;;;;;CAQC,GACD,OAAO,SAASC,wBAEdC,WAAmC;IACnC,MAAMC,YAAYH;IAElB,OAAOD,QACL,IACEG,YAAYE,GAAG,CAACC,CAAAA,aACdA,WAAWC,QAAQ,KAAKC,YACpBF,aACA,aAAKA;gBAAYG,OAAOL,UAAUE,WAAWC,QAAQ;iBAE7D;QAACJ;QAAaC;KAAU;AAE5B;AAEA;;;;CAIC,GACD,OAAO,SAASM;IAId,MAAMN,YAAYH;IAElB,OAAOF,YACL,CAACO,YAAYK,QACXL,WAAWM,YAAY,KAAKJ,YACxBG,QACAP,UAAU,GAAGE,WAAWM,YAAY,CAAC,CAAC,EAAED,OAAO,GACrD;QAACP;KAAU;AAEf;AAEA,MAAMS,WAAgC,IAAIC,IAAI;IAC5C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED;;;;CAIC,GACD,OAAO,MAAMC,gBAAgB,CAACC,UAC5BnB,UAAUmB,WACNC,OAAOC,WAAW,CAChBD,OAAOE,OAAO,CAACrB,UAAUkB,UAAUI,MAAM,CACvC,CAAC,CAACC,IAAI,GAAK,CAACR,SAASS,GAAG,CAACD,SAG7B,CAAC,EAAE;AAET,oDAAoD,GACpD,OAAO,SAASE,iBAAiBP,OAAgB;IAC/C,OAAOhB,QAAQ,IAAMe,cAAcC,UAAU;QAACA;KAAQ;AACxD;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASQ;IAKd,MAAMpB,YAAYH;IAElB,OAAOF,YACL0B,CAAAA;YAEkDA;QADhD,MAAMC,OACJ,OAAOD,MAAMC,IAAI,KAAK,WAAWD,MAAMC,IAAI,IAAGD,iBAAAA,MAAME,OAAO,qBAAbF,cAAe,CAAC,OAAO;QACvE,OAAO,OAAOC,SAAS,WACnBtB,UAAU,CAAC,OAAO,EAAEsB,MAAM,EAAEX,cAAcU,UAC1CA,MAAMG,OAAO;IACnB,GACA;QAACxB;KAAU;AAEf"}
@@ -8,7 +8,7 @@ import { type UiPreferencesState, UiPreferencesStateTag } from './ui-preferences
8
8
  *
9
9
  * The port is unchanged: every method closes over the namespace, so each is
10
10
  * `R = never`. Values are stored via structured clone (no JSON round-trip), and
11
- * a failed I/O surfaces as an {@link EntifixConnError} the caller can fall back
11
+ * a failed I/O surfaces as an `Unavailable` the caller can fall back
12
12
  * from — a stale view should degrade to defaults, not break.
13
13
  */
14
14
  export declare function makeIndexedDbUiPreferencesState(namespace?: string): UiPreferencesState;
@@ -1 +1 @@
1
- {"version":3,"file":"indexeddb-ui-preferences-state.d.ts","sourceRoot":"","sources":["../../src/preferences/indexeddb-ui-preferences-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAIvC,OAAO,EACL,KAAK,kBAAkB,EACvB,qBAAqB,EACtB,MAAM,2BAA2B,CAAC;AAUnC;;;;;;;;;;GAUG;AACH,wBAAgB,+BAA+B,CAC7C,SAAS,GAAE,MAAyC,GACnD,kBAAkB,CA8CpB;AAED,eAAO,MAAM,2BAA2B,kDAGvC,CAAC"}
1
+ {"version":3,"file":"indexeddb-ui-preferences-state.d.ts","sourceRoot":"","sources":["../../src/preferences/indexeddb-ui-preferences-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAIvC,OAAO,EACL,KAAK,kBAAkB,EACvB,qBAAqB,EACtB,MAAM,2BAA2B,CAAC;AAUnC;;;;;;;;;;GAUG;AACH,wBAAgB,+BAA+B,CAC7C,SAAS,GAAE,MAAyC,GACnD,kBAAkB,CA0CpB;AAED,eAAO,MAAM,2BAA2B,kDAGvC,CAAC"}
@@ -1,4 +1,4 @@
1
- import { EntifixConnError } from '@entifix/core';
1
+ import { Unavailable } from '@entifix/core';
2
2
  import { Effect, Layer } from 'effect';
3
3
  import { openDB } from 'idb';
4
4
  import { DEFAULT_UI_PREFERENCES_NAMESPACE } from './local-storage-ui-preferences-state.js';
@@ -16,7 +16,7 @@ const STORE_NAME = 'preferences';
16
16
  *
17
17
  * The port is unchanged: every method closes over the namespace, so each is
18
18
  * `R = never`. Values are stored via structured clone (no JSON round-trip), and
19
- * a failed I/O surfaces as an {@link EntifixConnError} the caller can fall back
19
+ * a failed I/O surfaces as an `Unavailable` the caller can fall back
20
20
  * from — a stale view should degrade to defaults, not break.
21
21
  */ export function makeIndexedDbUiPreferencesState(namespace = DEFAULT_UI_PREFERENCES_NAMESPACE) {
22
22
  const storageKey = (key)=>`${namespace}:${key}`;
@@ -28,7 +28,9 @@ const STORE_NAME = 'preferences';
28
28
  });
29
29
  const io = (action, key, run)=>Effect.tryPromise({
30
30
  try: run,
31
- catch: (cause)=>new EntifixConnError(`Unable to ${action} UI preference "${key}"`, cause)
31
+ catch: ()=>new Unavailable({
32
+ dependency: 'indexeddb'
33
+ })
32
34
  });
33
35
  return {
34
36
  read: (key)=>io('read', key, async ()=>{
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/preferences/indexeddb-ui-preferences-state.ts"],"sourcesContent":["import { EntifixConnError, type EntifixError } from '@entifix/core';\nimport { Effect, Layer } from 'effect';\nimport { type IDBPDatabase, openDB } from 'idb';\n\nimport { DEFAULT_UI_PREFERENCES_NAMESPACE } from './local-storage-ui-preferences-state.js';\nimport {\n type UiPreferencesState,\n UiPreferencesStateTag,\n} from './ui-preferences-state.js';\n\nconst DB_NAME = 'entifix-ui-preferences';\nconst STORE_NAME = 'preferences';\n\n/** No IndexedDB during SSR — reads resolve empty and writes are dropped. */\nfunction hasIndexedDb(): boolean {\n return typeof indexedDB !== 'undefined';\n}\n\n/**\n * `IndexedDB`-backed {@link UiPreferencesState} — the persistence seam for all\n * client UI state (column layout, sidebar collapse, and now the tab workspace),\n * unifying it with the tab/draft stores in one datastore instead of splitting\n * between `localStorage` and IndexedDB.\n *\n * The port is unchanged: every method closes over the namespace, so each is\n * `R = never`. Values are stored via structured clone (no JSON round-trip), and\n * a failed I/O surfaces as an {@link EntifixConnError} the caller can fall back\n * from — a stale view should degrade to defaults, not break.\n */\nexport function makeIndexedDbUiPreferencesState(\n namespace: string = DEFAULT_UI_PREFERENCES_NAMESPACE,\n): UiPreferencesState {\n const storageKey = (key: string) => `${namespace}:${key}`;\n\n let database: Promise<IDBPDatabase> | undefined;\n const open = () =>\n (database ??= openDB(DB_NAME, 1, {\n upgrade(db) {\n db.createObjectStore(STORE_NAME);\n },\n }));\n\n const io = <TValue>(\n action: string,\n key: string,\n run: () => Promise<TValue>,\n ): Effect.Effect<TValue, EntifixError> =>\n Effect.tryPromise({\n try: run,\n catch: cause =>\n new EntifixConnError(\n `Unable to ${action} UI preference \"${key}\"`,\n cause,\n ) as EntifixError,\n });\n\n return {\n read: <TValue>(key: string) =>\n io<TValue | undefined>('read', key, async () => {\n if (!hasIndexedDb()) return undefined;\n return (await open()).get(STORE_NAME, storageKey(key)) as Promise<\n TValue | undefined\n >;\n }),\n\n write: <TValue>(key: string, value: TValue) =>\n io<void>('write', key, async () => {\n if (!hasIndexedDb()) return;\n await (await open()).put(STORE_NAME, value, storageKey(key));\n }),\n\n remove: (key: string) =>\n io<void>('remove', key, async () => {\n if (!hasIndexedDb()) return;\n await (await open()).delete(STORE_NAME, storageKey(key));\n }),\n };\n}\n\nexport const IndexedDbUiPreferencesLayer = Layer.succeed(\n UiPreferencesStateTag,\n makeIndexedDbUiPreferencesState(),\n);\n"],"names":["EntifixConnError","Effect","Layer","openDB","DEFAULT_UI_PREFERENCES_NAMESPACE","UiPreferencesStateTag","DB_NAME","STORE_NAME","hasIndexedDb","indexedDB","makeIndexedDbUiPreferencesState","namespace","storageKey","key","database","open","upgrade","db","createObjectStore","io","action","run","tryPromise","try","catch","cause","read","undefined","get","write","value","put","remove","delete","IndexedDbUiPreferencesLayer","succeed"],"mappings":"AAAA,SAASA,gBAAgB,QAA2B,gBAAgB;AACpE,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AACvC,SAA4BC,MAAM,QAAQ,MAAM;AAEhD,SAASC,gCAAgC,QAAQ,0CAA0C;AAC3F,SAEEC,qBAAqB,QAChB,4BAA4B;AAEnC,MAAMC,UAAU;AAChB,MAAMC,aAAa;AAEnB,0EAA0E,GAC1E,SAASC;IACP,OAAO,OAAOC,cAAc;AAC9B;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,gCACdC,YAAoBP,gCAAgC;IAEpD,MAAMQ,aAAa,CAACC,MAAgB,GAAGF,UAAU,CAAC,EAAEE,KAAK;IAEzD,IAAIC;IACJ,MAAMC,OAAO,IACVD,mBAAAA,WAAAA,WAAaX,OAAOG,SAAS,GAAG;YAC/BU,SAAQC,EAAE;gBACRA,GAAGC,iBAAiB,CAACX;YACvB;QACF;IAEF,MAAMY,KAAK,CACTC,QACAP,KACAQ,MAEApB,OAAOqB,UAAU,CAAC;YAChBC,KAAKF;YACLG,OAAOC,CAAAA,QACL,IAAIzB,iBACF,CAAC,UAAU,EAAEoB,OAAO,gBAAgB,EAAEP,IAAI,CAAC,CAAC,EAC5CY;QAEN;IAEF,OAAO;QACLC,MAAM,CAASb,MACbM,GAAuB,QAAQN,KAAK;gBAClC,IAAI,CAACL,gBAAgB,OAAOmB;gBAC5B,OAAO,AAAC,CAAA,MAAMZ,MAAK,EAAGa,GAAG,CAACrB,YAAYK,WAAWC;YAGnD;QAEFgB,OAAO,CAAShB,KAAaiB,QAC3BX,GAAS,SAASN,KAAK;gBACrB,IAAI,CAACL,gBAAgB;gBACrB,MAAM,AAAC,CAAA,MAAMO,MAAK,EAAGgB,GAAG,CAACxB,YAAYuB,OAAOlB,WAAWC;YACzD;QAEFmB,QAAQ,CAACnB,MACPM,GAAS,UAAUN,KAAK;gBACtB,IAAI,CAACL,gBAAgB;gBACrB,MAAM,AAAC,CAAA,MAAMO,MAAK,EAAGkB,MAAM,CAAC1B,YAAYK,WAAWC;YACrD;IACJ;AACF;AAEA,OAAO,MAAMqB,8BAA8BhC,MAAMiC,OAAO,CACtD9B,uBACAK,mCACA"}
1
+ {"version":3,"sources":["../../src/preferences/indexeddb-ui-preferences-state.ts"],"sourcesContent":["import { Unavailable } from '@entifix/core';\nimport { Effect, Layer } from 'effect';\nimport { type IDBPDatabase, openDB } from 'idb';\n\nimport { DEFAULT_UI_PREFERENCES_NAMESPACE } from './local-storage-ui-preferences-state.js';\nimport {\n type UiPreferencesState,\n UiPreferencesStateTag,\n} from './ui-preferences-state.js';\n\nconst DB_NAME = 'entifix-ui-preferences';\nconst STORE_NAME = 'preferences';\n\n/** No IndexedDB during SSR — reads resolve empty and writes are dropped. */\nfunction hasIndexedDb(): boolean {\n return typeof indexedDB !== 'undefined';\n}\n\n/**\n * `IndexedDB`-backed {@link UiPreferencesState} — the persistence seam for all\n * client UI state (column layout, sidebar collapse, and now the tab workspace),\n * unifying it with the tab/draft stores in one datastore instead of splitting\n * between `localStorage` and IndexedDB.\n *\n * The port is unchanged: every method closes over the namespace, so each is\n * `R = never`. Values are stored via structured clone (no JSON round-trip), and\n * a failed I/O surfaces as an `Unavailable` the caller can fall back\n * from — a stale view should degrade to defaults, not break.\n */\nexport function makeIndexedDbUiPreferencesState(\n namespace: string = DEFAULT_UI_PREFERENCES_NAMESPACE,\n): UiPreferencesState {\n const storageKey = (key: string) => `${namespace}:${key}`;\n\n let database: Promise<IDBPDatabase> | undefined;\n const open = () =>\n (database ??= openDB(DB_NAME, 1, {\n upgrade(db) {\n db.createObjectStore(STORE_NAME);\n },\n }));\n\n const io = <TValue>(\n action: string,\n key: string,\n run: () => Promise<TValue>,\n ): Effect.Effect<TValue, Unavailable> =>\n Effect.tryPromise({\n try: run,\n catch: () => new Unavailable({ dependency: 'indexeddb' }),\n });\n\n return {\n read: <TValue>(key: string) =>\n io<TValue | undefined>('read', key, async () => {\n if (!hasIndexedDb()) return undefined;\n return (await open()).get(STORE_NAME, storageKey(key)) as Promise<\n TValue | undefined\n >;\n }),\n\n write: <TValue>(key: string, value: TValue) =>\n io<void>('write', key, async () => {\n if (!hasIndexedDb()) return;\n await (await open()).put(STORE_NAME, value, storageKey(key));\n }),\n\n remove: (key: string) =>\n io<void>('remove', key, async () => {\n if (!hasIndexedDb()) return;\n await (await open()).delete(STORE_NAME, storageKey(key));\n }),\n };\n}\n\nexport const IndexedDbUiPreferencesLayer = Layer.succeed(\n UiPreferencesStateTag,\n makeIndexedDbUiPreferencesState(),\n);\n"],"names":["Unavailable","Effect","Layer","openDB","DEFAULT_UI_PREFERENCES_NAMESPACE","UiPreferencesStateTag","DB_NAME","STORE_NAME","hasIndexedDb","indexedDB","makeIndexedDbUiPreferencesState","namespace","storageKey","key","database","open","upgrade","db","createObjectStore","io","action","run","tryPromise","try","catch","dependency","read","undefined","get","write","value","put","remove","delete","IndexedDbUiPreferencesLayer","succeed"],"mappings":"AAAA,SAASA,WAAW,QAAQ,gBAAgB;AAC5C,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AACvC,SAA4BC,MAAM,QAAQ,MAAM;AAEhD,SAASC,gCAAgC,QAAQ,0CAA0C;AAC3F,SAEEC,qBAAqB,QAChB,4BAA4B;AAEnC,MAAMC,UAAU;AAChB,MAAMC,aAAa;AAEnB,0EAA0E,GAC1E,SAASC;IACP,OAAO,OAAOC,cAAc;AAC9B;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,gCACdC,YAAoBP,gCAAgC;IAEpD,MAAMQ,aAAa,CAACC,MAAgB,GAAGF,UAAU,CAAC,EAAEE,KAAK;IAEzD,IAAIC;IACJ,MAAMC,OAAO,IACVD,mBAAAA,WAAAA,WAAaX,OAAOG,SAAS,GAAG;YAC/BU,SAAQC,EAAE;gBACRA,GAAGC,iBAAiB,CAACX;YACvB;QACF;IAEF,MAAMY,KAAK,CACTC,QACAP,KACAQ,MAEApB,OAAOqB,UAAU,CAAC;YAChBC,KAAKF;YACLG,OAAO,IAAM,IAAIxB,YAAY;oBAAEyB,YAAY;gBAAY;QACzD;IAEF,OAAO;QACLC,MAAM,CAASb,MACbM,GAAuB,QAAQN,KAAK;gBAClC,IAAI,CAACL,gBAAgB,OAAOmB;gBAC5B,OAAO,AAAC,CAAA,MAAMZ,MAAK,EAAGa,GAAG,CAACrB,YAAYK,WAAWC;YAGnD;QAEFgB,OAAO,CAAShB,KAAaiB,QAC3BX,GAAS,SAASN,KAAK;gBACrB,IAAI,CAACL,gBAAgB;gBACrB,MAAM,AAAC,CAAA,MAAMO,MAAK,EAAGgB,GAAG,CAACxB,YAAYuB,OAAOlB,WAAWC;YACzD;QAEFmB,QAAQ,CAACnB,MACPM,GAAS,UAAUN,KAAK;gBACtB,IAAI,CAACL,gBAAgB;gBACrB,MAAM,AAAC,CAAA,MAAMO,MAAK,EAAGkB,MAAM,CAAC1B,YAAYK,WAAWC;YACrD;IACJ;AACF;AAEA,OAAO,MAAMqB,8BAA8BhC,MAAMiC,OAAO,CACtD9B,uBACAK,mCACA"}
@@ -1 +1 @@
1
- {"version":3,"file":"local-storage-ui-preferences-state.d.ts","sourceRoot":"","sources":["../../src/preferences/local-storage-ui-preferences-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEvC,OAAO,EACL,KAAK,kBAAkB,EACvB,qBAAqB,EACtB,MAAM,2BAA2B,CAAC;AAEnC,eAAO,MAAM,gCAAgC,eAAe,CAAC;AAO7D;;;;;;;;;GASG;AACH,wBAAgB,kCAAkC,CAChD,SAAS,GAAE,MAAyC,GACnD,kBAAkB,CAiDpB;AAED,eAAO,MAAM,8BAA8B,kDAG1C,CAAC"}
1
+ {"version":3,"file":"local-storage-ui-preferences-state.d.ts","sourceRoot":"","sources":["../../src/preferences/local-storage-ui-preferences-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEvC,OAAO,EACL,KAAK,kBAAkB,EACvB,qBAAqB,EACtB,MAAM,2BAA2B,CAAC;AAEnC,eAAO,MAAM,gCAAgC,eAAe,CAAC;AAO7D;;;;;;;;;GASG;AACH,wBAAgB,kCAAkC,CAChD,SAAS,GAAE,MAAyC,GACnD,kBAAkB,CAqCpB;AAED,eAAO,MAAM,8BAA8B,kDAG1C,CAAC"}
@@ -1,4 +1,4 @@
1
- import { EntifixConnError } from '@entifix/core';
1
+ import { Unavailable } from '@entifix/core';
2
2
  import { Effect, Layer } from 'effect';
3
3
  import { UiPreferencesStateTag } from './ui-preferences-state.js';
4
4
  export const DEFAULT_UI_PREFERENCES_NAMESPACE = 'entifix-ui';
@@ -28,21 +28,27 @@ export const DEFAULT_UI_PREFERENCES_NAMESPACE = 'entifix-ui';
28
28
  return undefined;
29
29
  }
30
30
  },
31
- catch: (cause)=>new EntifixConnError(`Unable to read UI preference "${key}"`, cause)
31
+ catch: ()=>new Unavailable({
32
+ dependency: 'localStorage'
33
+ })
32
34
  }),
33
35
  write: (key, value)=>Effect.try({
34
36
  try: ()=>{
35
37
  if (!isBrowser()) return;
36
38
  window.localStorage.setItem(storageKey(key), JSON.stringify(value));
37
39
  },
38
- catch: (cause)=>new EntifixConnError(`Unable to write UI preference "${key}"`, cause)
40
+ catch: ()=>new Unavailable({
41
+ dependency: 'localStorage'
42
+ })
39
43
  }),
40
44
  remove: (key)=>Effect.try({
41
45
  try: ()=>{
42
46
  if (!isBrowser()) return;
43
47
  window.localStorage.removeItem(storageKey(key));
44
48
  },
45
- catch: (cause)=>new EntifixConnError(`Unable to remove UI preference "${key}"`, cause)
49
+ catch: ()=>new Unavailable({
50
+ dependency: 'localStorage'
51
+ })
46
52
  })
47
53
  };
48
54
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/preferences/local-storage-ui-preferences-state.ts"],"sourcesContent":["import { EntifixConnError } from '@entifix/core';\nimport { Effect, Layer } from 'effect';\n\nimport {\n type UiPreferencesState,\n UiPreferencesStateTag,\n} from './ui-preferences-state.js';\n\nexport const DEFAULT_UI_PREFERENCES_NAMESPACE = 'entifix-ui';\n\n/** No `window` during SSR — reads resolve empty and writes are dropped. */\nfunction isBrowser(): boolean {\n return typeof window !== 'undefined' && !!window.localStorage;\n}\n\n/**\n * `localStorage`-backed {@link UiPreferencesState}. Every method closes over the\n * namespace, so each one is `R = never` and the object satisfies the interface\n * without leaking a requirement into callers (the same shape\n * `makeMongoRepository` uses).\n *\n * Values are JSON-encoded. Unparseable entries resolve to `undefined` rather\n * than failing: a stale or hand-edited key should fall back to defaults, not\n * break the view.\n */\nexport function makeLocalStorageUiPreferencesState(\n namespace: string = DEFAULT_UI_PREFERENCES_NAMESPACE,\n): UiPreferencesState {\n const storageKey = (key: string) => `${namespace}:${key}`;\n\n return {\n read: <TValue>(key: string) =>\n Effect.try({\n try: () => {\n if (!isBrowser()) return undefined;\n const raw = window.localStorage.getItem(storageKey(key));\n if (raw === null) return undefined;\n try {\n return JSON.parse(raw) as TValue;\n } catch {\n return undefined;\n }\n },\n catch: cause =>\n new EntifixConnError(\n `Unable to read UI preference \"${key}\"`,\n cause,\n ) as EntifixConnError,\n }),\n\n write: <TValue>(key: string, value: TValue) =>\n Effect.try({\n try: () => {\n if (!isBrowser()) return;\n window.localStorage.setItem(storageKey(key), JSON.stringify(value));\n },\n catch: cause =>\n new EntifixConnError(\n `Unable to write UI preference \"${key}\"`,\n cause,\n ) as EntifixConnError,\n }),\n\n remove: (key: string) =>\n Effect.try({\n try: () => {\n if (!isBrowser()) return;\n window.localStorage.removeItem(storageKey(key));\n },\n catch: cause =>\n new EntifixConnError(\n `Unable to remove UI preference \"${key}\"`,\n cause,\n ) as EntifixConnError,\n }),\n };\n}\n\nexport const LocalStorageUiPreferencesLayer = Layer.succeed(\n UiPreferencesStateTag,\n makeLocalStorageUiPreferencesState(),\n);\n"],"names":["EntifixConnError","Effect","Layer","UiPreferencesStateTag","DEFAULT_UI_PREFERENCES_NAMESPACE","isBrowser","window","localStorage","makeLocalStorageUiPreferencesState","namespace","storageKey","key","read","try","undefined","raw","getItem","JSON","parse","catch","cause","write","value","setItem","stringify","remove","removeItem","LocalStorageUiPreferencesLayer","succeed"],"mappings":"AAAA,SAASA,gBAAgB,QAAQ,gBAAgB;AACjD,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AAEvC,SAEEC,qBAAqB,QAChB,4BAA4B;AAEnC,OAAO,MAAMC,mCAAmC,aAAa;AAE7D,yEAAyE,GACzE,SAASC;IACP,OAAO,OAAOC,WAAW,eAAe,CAAC,CAACA,OAAOC,YAAY;AAC/D;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,mCACdC,YAAoBL,gCAAgC;IAEpD,MAAMM,aAAa,CAACC,MAAgB,GAAGF,UAAU,CAAC,EAAEE,KAAK;IAEzD,OAAO;QACLC,MAAM,CAASD,MACbV,OAAOY,GAAG,CAAC;gBACTA,KAAK;oBACH,IAAI,CAACR,aAAa,OAAOS;oBACzB,MAAMC,MAAMT,OAAOC,YAAY,CAACS,OAAO,CAACN,WAAWC;oBACnD,IAAII,QAAQ,MAAM,OAAOD;oBACzB,IAAI;wBACF,OAAOG,KAAKC,KAAK,CAACH;oBACpB,EAAE,eAAM;wBACN,OAAOD;oBACT;gBACF;gBACAK,OAAOC,CAAAA,QACL,IAAIpB,iBACF,CAAC,8BAA8B,EAAEW,IAAI,CAAC,CAAC,EACvCS;YAEN;QAEFC,OAAO,CAASV,KAAaW,QAC3BrB,OAAOY,GAAG,CAAC;gBACTA,KAAK;oBACH,IAAI,CAACR,aAAa;oBAClBC,OAAOC,YAAY,CAACgB,OAAO,CAACb,WAAWC,MAAMM,KAAKO,SAAS,CAACF;gBAC9D;gBACAH,OAAOC,CAAAA,QACL,IAAIpB,iBACF,CAAC,+BAA+B,EAAEW,IAAI,CAAC,CAAC,EACxCS;YAEN;QAEFK,QAAQ,CAACd,MACPV,OAAOY,GAAG,CAAC;gBACTA,KAAK;oBACH,IAAI,CAACR,aAAa;oBAClBC,OAAOC,YAAY,CAACmB,UAAU,CAAChB,WAAWC;gBAC5C;gBACAQ,OAAOC,CAAAA,QACL,IAAIpB,iBACF,CAAC,gCAAgC,EAAEW,IAAI,CAAC,CAAC,EACzCS;YAEN;IACJ;AACF;AAEA,OAAO,MAAMO,iCAAiCzB,MAAM0B,OAAO,CACzDzB,uBACAK,sCACA"}
1
+ {"version":3,"sources":["../../src/preferences/local-storage-ui-preferences-state.ts"],"sourcesContent":["import { Unavailable } from '@entifix/core';\nimport { Effect, Layer } from 'effect';\n\nimport {\n type UiPreferencesState,\n UiPreferencesStateTag,\n} from './ui-preferences-state.js';\n\nexport const DEFAULT_UI_PREFERENCES_NAMESPACE = 'entifix-ui';\n\n/** No `window` during SSR — reads resolve empty and writes are dropped. */\nfunction isBrowser(): boolean {\n return typeof window !== 'undefined' && !!window.localStorage;\n}\n\n/**\n * `localStorage`-backed {@link UiPreferencesState}. Every method closes over the\n * namespace, so each one is `R = never` and the object satisfies the interface\n * without leaking a requirement into callers (the same shape\n * `makeMongoRepository` uses).\n *\n * Values are JSON-encoded. Unparseable entries resolve to `undefined` rather\n * than failing: a stale or hand-edited key should fall back to defaults, not\n * break the view.\n */\nexport function makeLocalStorageUiPreferencesState(\n namespace: string = DEFAULT_UI_PREFERENCES_NAMESPACE,\n): UiPreferencesState {\n const storageKey = (key: string) => `${namespace}:${key}`;\n\n return {\n read: <TValue>(key: string) =>\n Effect.try({\n try: () => {\n if (!isBrowser()) return undefined;\n const raw = window.localStorage.getItem(storageKey(key));\n if (raw === null) return undefined;\n try {\n return JSON.parse(raw) as TValue;\n } catch {\n return undefined;\n }\n },\n catch: () => new Unavailable({ dependency: 'localStorage' }),\n }),\n\n write: <TValue>(key: string, value: TValue) =>\n Effect.try({\n try: () => {\n if (!isBrowser()) return;\n window.localStorage.setItem(storageKey(key), JSON.stringify(value));\n },\n catch: () => new Unavailable({ dependency: 'localStorage' }),\n }),\n\n remove: (key: string) =>\n Effect.try({\n try: () => {\n if (!isBrowser()) return;\n window.localStorage.removeItem(storageKey(key));\n },\n catch: () => new Unavailable({ dependency: 'localStorage' }),\n }),\n };\n}\n\nexport const LocalStorageUiPreferencesLayer = Layer.succeed(\n UiPreferencesStateTag,\n makeLocalStorageUiPreferencesState(),\n);\n"],"names":["Unavailable","Effect","Layer","UiPreferencesStateTag","DEFAULT_UI_PREFERENCES_NAMESPACE","isBrowser","window","localStorage","makeLocalStorageUiPreferencesState","namespace","storageKey","key","read","try","undefined","raw","getItem","JSON","parse","catch","dependency","write","value","setItem","stringify","remove","removeItem","LocalStorageUiPreferencesLayer","succeed"],"mappings":"AAAA,SAASA,WAAW,QAAQ,gBAAgB;AAC5C,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AAEvC,SAEEC,qBAAqB,QAChB,4BAA4B;AAEnC,OAAO,MAAMC,mCAAmC,aAAa;AAE7D,yEAAyE,GACzE,SAASC;IACP,OAAO,OAAOC,WAAW,eAAe,CAAC,CAACA,OAAOC,YAAY;AAC/D;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,mCACdC,YAAoBL,gCAAgC;IAEpD,MAAMM,aAAa,CAACC,MAAgB,GAAGF,UAAU,CAAC,EAAEE,KAAK;IAEzD,OAAO;QACLC,MAAM,CAASD,MACbV,OAAOY,GAAG,CAAC;gBACTA,KAAK;oBACH,IAAI,CAACR,aAAa,OAAOS;oBACzB,MAAMC,MAAMT,OAAOC,YAAY,CAACS,OAAO,CAACN,WAAWC;oBACnD,IAAII,QAAQ,MAAM,OAAOD;oBACzB,IAAI;wBACF,OAAOG,KAAKC,KAAK,CAACH;oBACpB,EAAE,eAAM;wBACN,OAAOD;oBACT;gBACF;gBACAK,OAAO,IAAM,IAAInB,YAAY;wBAAEoB,YAAY;oBAAe;YAC5D;QAEFC,OAAO,CAASV,KAAaW,QAC3BrB,OAAOY,GAAG,CAAC;gBACTA,KAAK;oBACH,IAAI,CAACR,aAAa;oBAClBC,OAAOC,YAAY,CAACgB,OAAO,CAACb,WAAWC,MAAMM,KAAKO,SAAS,CAACF;gBAC9D;gBACAH,OAAO,IAAM,IAAInB,YAAY;wBAAEoB,YAAY;oBAAe;YAC5D;QAEFK,QAAQ,CAACd,MACPV,OAAOY,GAAG,CAAC;gBACTA,KAAK;oBACH,IAAI,CAACR,aAAa;oBAClBC,OAAOC,YAAY,CAACmB,UAAU,CAAChB,WAAWC;gBAC5C;gBACAQ,OAAO,IAAM,IAAInB,YAAY;wBAAEoB,YAAY;oBAAe;YAC5D;IACJ;AACF;AAEA,OAAO,MAAMO,iCAAiCzB,MAAM0B,OAAO,CACzDzB,uBACAK,sCACA"}
@@ -1,4 +1,4 @@
1
- import type { EntifixError } from '@entifix/core';
1
+ import type { Unavailable } from '@entifix/core';
2
2
  import { Context, type Effect } from 'effect';
3
3
  /**
4
4
  * Where a user's UI personalization lives — column order and visibility today,
@@ -12,9 +12,9 @@ import { Context, type Effect } from 'effect';
12
12
  * Values are stored and returned as-is; adapters own their own encoding.
13
13
  */
14
14
  export interface UiPreferencesState {
15
- read<TValue>(key: string): Effect.Effect<TValue | undefined, EntifixError>;
16
- write<TValue>(key: string, value: TValue): Effect.Effect<void, EntifixError>;
17
- remove(key: string): Effect.Effect<void, EntifixError>;
15
+ read<TValue>(key: string): Effect.Effect<TValue | undefined, Unavailable>;
16
+ write<TValue>(key: string, value: TValue): Effect.Effect<void, Unavailable>;
17
+ remove(key: string): Effect.Effect<void, Unavailable>;
18
18
  }
19
19
  declare const UiPreferencesStateTag_base: Context.ServiceClass<UiPreferencesStateTag, "UiPreferencesState", UiPreferencesState>;
20
20
  export declare class UiPreferencesStateTag extends UiPreferencesStateTag_base {
@@ -1 +1 @@
1
- {"version":3,"file":"ui-preferences-state.d.ts","sourceRoot":"","sources":["../../src/preferences/ui-preferences-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C;;;;;;;;;;GAUG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,SAAS,EAAE,YAAY,CAAC,CAAC;IAC3E,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAC7E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;CACxD;;AAED,qBAAa,qBAAsB,SAAQ,0BAGlB;CAAG"}
1
+ {"version":3,"file":"ui-preferences-state.d.ts","sourceRoot":"","sources":["../../src/preferences/ui-preferences-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C;;;;;;;;;;GAUG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,SAAS,EAAE,WAAW,CAAC,CAAC;IAC1E,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC5E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;CACvD;;AAED,qBAAa,qBAAsB,SAAQ,0BAGlB;CAAG"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/preferences/ui-preferences-state.ts"],"sourcesContent":["import type { EntifixError } from '@entifix/core';\nimport { Context, type Effect } from 'effect';\n\n/**\n * Where a user's UI personalization lives — column order and visibility today,\n * any other per-user view state later.\n *\n * The port is Effect-returning and therefore free to be asynchronous: the\n * shipped adapter writes to `localStorage` synchronously, but a server-backed\n * store (per-user preferences fetched over HTTP) is a drop-in replacement that\n * no component has to be rewritten for.\n *\n * Values are stored and returned as-is; adapters own their own encoding.\n */\nexport interface UiPreferencesState {\n read<TValue>(key: string): Effect.Effect<TValue | undefined, EntifixError>;\n write<TValue>(key: string, value: TValue): Effect.Effect<void, EntifixError>;\n remove(key: string): Effect.Effect<void, EntifixError>;\n}\n\nexport class UiPreferencesStateTag extends Context.Service<\n UiPreferencesStateTag,\n UiPreferencesState\n>()('UiPreferencesState') {}\n"],"names":["Context","UiPreferencesStateTag","Service"],"mappings":"AACA,SAASA,OAAO,QAAqB,SAAS;AAmB9C,OAAO,MAAMC,8BAA8BD,QAAQE,OAAO,GAGtD;AAAuB"}
1
+ {"version":3,"sources":["../../src/preferences/ui-preferences-state.ts"],"sourcesContent":["import type { Unavailable } from '@entifix/core';\nimport { Context, type Effect } from 'effect';\n\n/**\n * Where a user's UI personalization lives — column order and visibility today,\n * any other per-user view state later.\n *\n * The port is Effect-returning and therefore free to be asynchronous: the\n * shipped adapter writes to `localStorage` synchronously, but a server-backed\n * store (per-user preferences fetched over HTTP) is a drop-in replacement that\n * no component has to be rewritten for.\n *\n * Values are stored and returned as-is; adapters own their own encoding.\n */\nexport interface UiPreferencesState {\n read<TValue>(key: string): Effect.Effect<TValue | undefined, Unavailable>;\n write<TValue>(key: string, value: TValue): Effect.Effect<void, Unavailable>;\n remove(key: string): Effect.Effect<void, Unavailable>;\n}\n\nexport class UiPreferencesStateTag extends Context.Service<\n UiPreferencesStateTag,\n UiPreferencesState\n>()('UiPreferencesState') {}\n"],"names":["Context","UiPreferencesStateTag","Service"],"mappings":"AACA,SAASA,OAAO,QAAqB,SAAS;AAmB9C,OAAO,MAAMC,8BAA8BD,QAAQE,OAAO,GAGtD;AAAuB"}
@@ -1 +1 @@
1
- {"version":3,"file":"action-surfaces.d.ts","sourceRoot":"","sources":["../../../src/ui/actions/action-surfaces.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,iBAAiB,EAEvB,MAAM,eAAe,CAAC;AAEvB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,qGAOlB,CAAC;AACX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAyB7D;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,WAAW,CAAC;AAyBvC;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,iBAAiB,GAC5B,SAAS,aAAa,EAAE,CAa1B;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,iBAAiB,GAAG,aAAa,CAMvE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,SAAS,GACjD,iBAAiB,EAAE,CAIrB"}
1
+ {"version":3,"file":"action-surfaces.d.ts","sourceRoot":"","sources":["../../../src/ui/actions/action-surfaces.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,eAAe,CAAC;AAEvB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,qGAOlB,CAAC;AACX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAyB7D;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,WAAW,CAAC;AAuBvC;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,iBAAiB,GAC5B,SAAS,aAAa,EAAE,CAa1B;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,iBAAiB,GAAG,aAAa,CAMvE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,SAAS,GACjD,iBAAiB,EAAE,CAIrB"}
@@ -1,4 +1,3 @@
1
- import { EntifixLogicError } from '@entifix/core';
2
1
  /**
3
2
  * Where a declared verb is rendered.
4
3
  *
@@ -50,11 +49,7 @@ import { EntifixLogicError } from '@entifix/core';
50
49
  /** Which surface renders one cell. Throws on a cell no surface owns. */ function surfaceForCell(key, binding, placement) {
51
50
  const surface = SURFACE_BY_CELL[`${binding}:${placement}`];
52
51
  if (surface === undefined) {
53
- throw new EntifixLogicError(`The use case "${key}" is declared ${binding} + ${placement}, which no surface renders. ` + 'A `determining` action finalizes a page — it is an object page’s footer — and a list ' + 'screen has no page to finalize, so a collection-bound verb cannot be determining. ' + 'Declare it `context-dependent` to reach the bulk bar, or `context-independent` to sit ' + 'in the table toolbar.', undefined, {
54
- key,
55
- binding,
56
- placement
57
- });
52
+ throw new Error(`The use case "${key}" is declared ${binding} + ${placement}, which no surface renders. ` + 'A `determining` action finalizes a page — it is an object page’s footer — and a list ' + 'screen has no page to finalize, so a collection-bound verb cannot be determining. ' + 'Declare it `context-dependent` to reach the bulk bar, or `context-independent` to sit ' + 'in the table toolbar.');
58
53
  }
59
54
  return surface;
60
55
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/actions/action-surfaces.ts"],"sourcesContent":["import {\n EntifixLogicError,\n type UseCaseBinding,\n type UseCaseDescriptor,\n type UseCasePlacement,\n} from '@entifix/core';\n\n/**\n * Where a declared verb is rendered.\n *\n * The use-case descriptor has two independent axes, and at first only one\n * surface, so four of the nine cells had nowhere to go: `EntityForm` filtered\n * `binding === 'entity' && placement !== 'context-dependent'` and everything\n * else was dropped without a word. This is the map that closes it —\n * **placement decides the surface, binding decides the payload** — and it lives\n * in one module so no surface re-derives it and the two axes cannot drift\n * apart again.\n */\nexport const ACTION_SURFACES = [\n 'form-header',\n 'form-footer',\n 'row-menu',\n 'bulk-bar',\n 'table-toolbar',\n 'command-palette',\n] as const;\nexport type ActionSurface = (typeof ACTION_SURFACES)[number];\n\n/**\n * The nine cells, exhaustively.\n *\n * `collection` + `determining` is deliberately absent, and asking for it\n * throws rather than returning `undefined`. A determining action *finalizes a\n * page* — it is the footer that ends an object page — and a list screen has no\n * such thing to finalize, which is Fiori's own rule. Dropping it silently is\n * the fault this repo already writes an assertion against elsewhere: the verb\n * would be declared, granted, exported, pass every check a register runs,\n * and simply never appear, which reads to its author as \"the permission is\n * wrong\" and sends them looking in the wrong place entirely.\n */\nconst SURFACE_BY_CELL: Record<string, ActionSurface> = {\n 'entity:context-independent': 'form-header',\n 'entity:determining': 'form-footer',\n 'entity:context-dependent': 'row-menu',\n 'collection:context-dependent': 'bulk-bar',\n 'collection:context-independent': 'table-toolbar',\n 'unbound:context-dependent': 'command-palette',\n 'unbound:context-independent': 'command-palette',\n 'unbound:determining': 'command-palette',\n};\n\n/**\n * The overflow trigger's glyph.\n *\n * A constant rather than a literal in JSX, because `react/jsx-no-literals`\n * fails the build on written-in copy and is right to: it cannot tell a\n * typographic glyph from a forgotten Spanish string. The trigger's *name* is a\n * catalog key (`form.moreActions`, `table.actions`) — this is only the mark,\n * and it is never read aloud.\n */\nexport const OVERFLOW_GLYPH = '\\u22ef';\n\n/** Which surface renders one cell. Throws on a cell no surface owns. */\nfunction surfaceForCell(\n key: string,\n binding: UseCaseBinding,\n placement: UseCasePlacement,\n): ActionSurface {\n const surface = SURFACE_BY_CELL[`${binding}:${placement}`];\n\n if (surface === undefined) {\n throw new EntifixLogicError(\n `The use case \"${key}\" is declared ${binding} + ${placement}, which no surface renders. ` +\n 'A `determining` action finalizes a page — it is an object page’s footer — and a list ' +\n 'screen has no page to finalize, so a collection-bound verb cannot be determining. ' +\n 'Declare it `context-dependent` to reach the bulk bar, or `context-independent` to sit ' +\n 'in the table toolbar.',\n undefined,\n { key, binding, placement },\n );\n }\n\n return surface;\n}\n\n/**\n * Every surface this descriptor renders on.\n *\n * ⚠️ **A verb may name more than one cell**, and that is what makes publishing\n * reachable from a form header, a row menu and a selection without becoming\n * three permissions. The primary `binding` + `placement` come first, then\n * whatever `alsoAt` adds, and each is validated the same way — an illegal cell\n * in `alsoAt` throws exactly as one in the primary pair does.\n *\n * Duplicates are collapsed. A descriptor that named the same cell twice would\n * otherwise render the verb twice in one menu, which reads as a bug in the\n * screen rather than in the declaration.\n */\nexport function surfacesFor(\n descriptor: UseCaseDescriptor,\n): readonly ActionSurface[] {\n const cells = [\n { binding: descriptor.binding, placement: descriptor.placement },\n ...(descriptor.alsoAt ?? []),\n ];\n\n return [\n ...new Set(\n cells.map(cell =>\n surfaceForCell(descriptor.key, cell.binding, cell.placement),\n ),\n ),\n ];\n}\n\n/**\n * The single surface this descriptor renders on.\n *\n * Kept for the callers that genuinely have one — the command palette asks \"is\n * this mine?\" of an `unbound` verb, which can only ever be in one place. Any\n * caller that renders a *list* of verbs wants {@link surfacesFor}.\n */\nexport function surfaceFor(descriptor: UseCaseDescriptor): ActionSurface {\n return surfaceForCell(\n descriptor.key,\n descriptor.binding,\n descriptor.placement,\n );\n}\n\n/**\n * The descriptors one surface should render, in declaration order.\n *\n * Every descriptor is passed through {@link surfacesFor}, so an unrenderable\n * cell throws on the **first render of any surface** rather than only on the\n * one that would have shown it — the same reason\n * `assertLinkSourcesAreEditable` runs as one pass over the registry instead of\n * per row: a check that only fires on the screen that was already looking for\n * the verb is a check that fires after the bug is reported. A cell declared in\n * `alsoAt` is validated on that same first render, not on the surface it names.\n */\nexport function useCasesForSurface(\n surface: ActionSurface,\n useCases: readonly UseCaseDescriptor[] | undefined,\n): UseCaseDescriptor[] {\n return (useCases ?? []).filter(descriptor =>\n surfacesFor(descriptor).includes(surface),\n );\n}\n"],"names":["EntifixLogicError","ACTION_SURFACES","SURFACE_BY_CELL","OVERFLOW_GLYPH","surfaceForCell","key","binding","placement","surface","undefined","surfacesFor","descriptor","cells","alsoAt","Set","map","cell","surfaceFor","useCasesForSurface","useCases","filter","includes"],"mappings":"AAAA,SACEA,iBAAiB,QAIZ,gBAAgB;AAEvB;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,kBAAkB;IAC7B;IACA;IACA;IACA;IACA;IACA;CACD,CAAU;AAGX;;;;;;;;;;;CAWC,GACD,MAAMC,kBAAiD;IACrD,8BAA8B;IAC9B,sBAAsB;IACtB,4BAA4B;IAC5B,gCAAgC;IAChC,kCAAkC;IAClC,6BAA6B;IAC7B,+BAA+B;IAC/B,uBAAuB;AACzB;AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMC,iBAAiB,SAAS;AAEvC,sEAAsE,GACtE,SAASC,eACPC,GAAW,EACXC,OAAuB,EACvBC,SAA2B;IAE3B,MAAMC,UAAUN,eAAe,CAAC,GAAGI,QAAQ,CAAC,EAAEC,WAAW,CAAC;IAE1D,IAAIC,YAAYC,WAAW;QACzB,MAAM,IAAIT,kBACR,CAAC,cAAc,EAAEK,IAAI,cAAc,EAAEC,QAAQ,GAAG,EAAEC,UAAU,4BAA4B,CAAC,GACvF,0FACA,uFACA,2FACA,yBACFE,WACA;YAAEJ;YAAKC;YAASC;QAAU;IAE9B;IAEA,OAAOC;AACT;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASE,YACdC,UAA6B;QAIvBA;IAFN,MAAMC,QAAQ;QACZ;YAAEN,SAASK,WAAWL,OAAO;YAAEC,WAAWI,WAAWJ,SAAS;QAAC;YAC3DI,qBAAAA,WAAWE,MAAM,YAAjBF,qBAAqB,EAAE;KAC5B;IAED,OAAO;WACF,IAAIG,IACLF,MAAMG,GAAG,CAACC,CAAAA,OACRZ,eAAeO,WAAWN,GAAG,EAAEW,KAAKV,OAAO,EAAEU,KAAKT,SAAS;KAGhE;AACH;AAEA;;;;;;CAMC,GACD,OAAO,SAASU,WAAWN,UAA6B;IACtD,OAAOP,eACLO,WAAWN,GAAG,EACdM,WAAWL,OAAO,EAClBK,WAAWJ,SAAS;AAExB;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASW,mBACdV,OAAsB,EACtBW,QAAkD;IAElD,OAAO,CAACA,mBAAAA,WAAY,EAAE,EAAEC,MAAM,CAACT,CAAAA,aAC7BD,YAAYC,YAAYU,QAAQ,CAACb;AAErC"}
1
+ {"version":3,"sources":["../../../src/ui/actions/action-surfaces.ts"],"sourcesContent":["import {\n type UseCaseBinding,\n type UseCaseDescriptor,\n type UseCasePlacement,\n} from '@entifix/core';\n\n/**\n * Where a declared verb is rendered.\n *\n * The use-case descriptor has two independent axes, and at first only one\n * surface, so four of the nine cells had nowhere to go: `EntityForm` filtered\n * `binding === 'entity' && placement !== 'context-dependent'` and everything\n * else was dropped without a word. This is the map that closes it —\n * **placement decides the surface, binding decides the payload** — and it lives\n * in one module so no surface re-derives it and the two axes cannot drift\n * apart again.\n */\nexport const ACTION_SURFACES = [\n 'form-header',\n 'form-footer',\n 'row-menu',\n 'bulk-bar',\n 'table-toolbar',\n 'command-palette',\n] as const;\nexport type ActionSurface = (typeof ACTION_SURFACES)[number];\n\n/**\n * The nine cells, exhaustively.\n *\n * `collection` + `determining` is deliberately absent, and asking for it\n * throws rather than returning `undefined`. A determining action *finalizes a\n * page* — it is the footer that ends an object page — and a list screen has no\n * such thing to finalize, which is Fiori's own rule. Dropping it silently is\n * the fault this repo already writes an assertion against elsewhere: the verb\n * would be declared, granted, exported, pass every check a register runs,\n * and simply never appear, which reads to its author as \"the permission is\n * wrong\" and sends them looking in the wrong place entirely.\n */\nconst SURFACE_BY_CELL: Record<string, ActionSurface> = {\n 'entity:context-independent': 'form-header',\n 'entity:determining': 'form-footer',\n 'entity:context-dependent': 'row-menu',\n 'collection:context-dependent': 'bulk-bar',\n 'collection:context-independent': 'table-toolbar',\n 'unbound:context-dependent': 'command-palette',\n 'unbound:context-independent': 'command-palette',\n 'unbound:determining': 'command-palette',\n};\n\n/**\n * The overflow trigger's glyph.\n *\n * A constant rather than a literal in JSX, because `react/jsx-no-literals`\n * fails the build on written-in copy and is right to: it cannot tell a\n * typographic glyph from a forgotten Spanish string. The trigger's *name* is a\n * catalog key (`form.moreActions`, `table.actions`) — this is only the mark,\n * and it is never read aloud.\n */\nexport const OVERFLOW_GLYPH = '\\u22ef';\n\n/** Which surface renders one cell. Throws on a cell no surface owns. */\nfunction surfaceForCell(\n key: string,\n binding: UseCaseBinding,\n placement: UseCasePlacement,\n): ActionSurface {\n const surface = SURFACE_BY_CELL[`${binding}:${placement}`];\n\n if (surface === undefined) {\n throw new Error(\n `The use case \"${key}\" is declared ${binding} + ${placement}, which no surface renders. ` +\n 'A `determining` action finalizes a page — it is an object page’s footer — and a list ' +\n 'screen has no page to finalize, so a collection-bound verb cannot be determining. ' +\n 'Declare it `context-dependent` to reach the bulk bar, or `context-independent` to sit ' +\n 'in the table toolbar.',\n );\n }\n\n return surface;\n}\n\n/**\n * Every surface this descriptor renders on.\n *\n * ⚠️ **A verb may name more than one cell**, and that is what makes publishing\n * reachable from a form header, a row menu and a selection without becoming\n * three permissions. The primary `binding` + `placement` come first, then\n * whatever `alsoAt` adds, and each is validated the same way — an illegal cell\n * in `alsoAt` throws exactly as one in the primary pair does.\n *\n * Duplicates are collapsed. A descriptor that named the same cell twice would\n * otherwise render the verb twice in one menu, which reads as a bug in the\n * screen rather than in the declaration.\n */\nexport function surfacesFor(\n descriptor: UseCaseDescriptor,\n): readonly ActionSurface[] {\n const cells = [\n { binding: descriptor.binding, placement: descriptor.placement },\n ...(descriptor.alsoAt ?? []),\n ];\n\n return [\n ...new Set(\n cells.map(cell =>\n surfaceForCell(descriptor.key, cell.binding, cell.placement),\n ),\n ),\n ];\n}\n\n/**\n * The single surface this descriptor renders on.\n *\n * Kept for the callers that genuinely have one — the command palette asks \"is\n * this mine?\" of an `unbound` verb, which can only ever be in one place. Any\n * caller that renders a *list* of verbs wants {@link surfacesFor}.\n */\nexport function surfaceFor(descriptor: UseCaseDescriptor): ActionSurface {\n return surfaceForCell(\n descriptor.key,\n descriptor.binding,\n descriptor.placement,\n );\n}\n\n/**\n * The descriptors one surface should render, in declaration order.\n *\n * Every descriptor is passed through {@link surfacesFor}, so an unrenderable\n * cell throws on the **first render of any surface** rather than only on the\n * one that would have shown it — the same reason\n * `assertLinkSourcesAreEditable` runs as one pass over the registry instead of\n * per row: a check that only fires on the screen that was already looking for\n * the verb is a check that fires after the bug is reported. A cell declared in\n * `alsoAt` is validated on that same first render, not on the surface it names.\n */\nexport function useCasesForSurface(\n surface: ActionSurface,\n useCases: readonly UseCaseDescriptor[] | undefined,\n): UseCaseDescriptor[] {\n return (useCases ?? []).filter(descriptor =>\n surfacesFor(descriptor).includes(surface),\n );\n}\n"],"names":["ACTION_SURFACES","SURFACE_BY_CELL","OVERFLOW_GLYPH","surfaceForCell","key","binding","placement","surface","undefined","Error","surfacesFor","descriptor","cells","alsoAt","Set","map","cell","surfaceFor","useCasesForSurface","useCases","filter","includes"],"mappings":"AAMA;;;;;;;;;;CAUC,GACD,OAAO,MAAMA,kBAAkB;IAC7B;IACA;IACA;IACA;IACA;IACA;CACD,CAAU;AAGX;;;;;;;;;;;CAWC,GACD,MAAMC,kBAAiD;IACrD,8BAA8B;IAC9B,sBAAsB;IACtB,4BAA4B;IAC5B,gCAAgC;IAChC,kCAAkC;IAClC,6BAA6B;IAC7B,+BAA+B;IAC/B,uBAAuB;AACzB;AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMC,iBAAiB,SAAS;AAEvC,sEAAsE,GACtE,SAASC,eACPC,GAAW,EACXC,OAAuB,EACvBC,SAA2B;IAE3B,MAAMC,UAAUN,eAAe,CAAC,GAAGI,QAAQ,CAAC,EAAEC,WAAW,CAAC;IAE1D,IAAIC,YAAYC,WAAW;QACzB,MAAM,IAAIC,MACR,CAAC,cAAc,EAAEL,IAAI,cAAc,EAAEC,QAAQ,GAAG,EAAEC,UAAU,4BAA4B,CAAC,GACvF,0FACA,uFACA,2FACA;IAEN;IAEA,OAAOC;AACT;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASG,YACdC,UAA6B;QAIvBA;IAFN,MAAMC,QAAQ;QACZ;YAAEP,SAASM,WAAWN,OAAO;YAAEC,WAAWK,WAAWL,SAAS;QAAC;YAC3DK,qBAAAA,WAAWE,MAAM,YAAjBF,qBAAqB,EAAE;KAC5B;IAED,OAAO;WACF,IAAIG,IACLF,MAAMG,GAAG,CAACC,CAAAA,OACRb,eAAeQ,WAAWP,GAAG,EAAEY,KAAKX,OAAO,EAAEW,KAAKV,SAAS;KAGhE;AACH;AAEA;;;;;;CAMC,GACD,OAAO,SAASW,WAAWN,UAA6B;IACtD,OAAOR,eACLQ,WAAWP,GAAG,EACdO,WAAWN,OAAO,EAClBM,WAAWL,SAAS;AAExB;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASY,mBACdX,OAAsB,EACtBY,QAAkD;IAElD,OAAO,CAACA,mBAAAA,WAAY,EAAE,EAAEC,MAAM,CAACT,CAAAA,aAC7BD,YAAYC,YAAYU,QAAQ,CAACd;AAErC"}
@@ -1 +1 @@
1
- {"version":3,"file":"entity-form.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-form/entity-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,MAAM,EAUZ,MAAM,eAAe,CAAC;AA4BvB,OAAO,KAAK,EAGV,eAAe,EAChB,MAAM,wBAAwB,CAAC;AA6DhC,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,QAAgB,EAChB,SAAiB,EACjB,QAAe,EACf,QAAgB,EAChB,UAAkB,EAClB,KAAK,EACL,QAAQ,EACR,UAAyB,EACzB,KAAK,EACL,QAAQ,EACR,iBAAyB,EACzB,SAAS,EACT,OAAO,EACP,QAAQ,GACT,EAAE,eAAe,CAAC,OAAO,CAAC,+BA8W1B"}
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,EAUZ,MAAM,eAAe,CAAC;AA4BvB,OAAO,KAAK,EAGV,eAAe,EAChB,MAAM,wBAAwB,CAAC;AA6DhC,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,QAAgB,EAChB,SAAiB,EACjB,QAAe,EACf,QAAgB,EAChB,UAAkB,EAClB,KAAK,EACL,QAAQ,EACR,UAAyB,EACzB,KAAK,EACL,QAAQ,EACR,iBAAyB,EACzB,SAAS,EACT,OAAO,EACP,QAAQ,GACT,EAAE,eAAe,CAAC,OAAO,CAAC,+BA8W1B"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { cloneEntityDraft, describeEntityColumns, EntifixLogicError, readDraftString, readLocalizedDraft, readRowDrafts } from '@entifix/core';
3
+ import { cloneEntityDraft, describeEntityColumns, readDraftString, readLocalizedDraft, readRowDrafts } from '@entifix/core';
4
4
  import { useId, useState } from 'react';
5
5
  import { useErrorMessage, useLocalizedDescriptors, useT, useTranslateKey } from '../../../i18n/index.js';
6
6
  import { OVERFLOW_GLYPH, renderAnchor, useCasesForSurface } from '../../actions/index.js';
@@ -374,7 +374,7 @@ export function EntityForm({ entityConstructor, entity, values, onFieldChange, l
374
374
  * is dead in *every* shape such a field can render, since an
375
375
  * `<EntityField render>` slot is handed the draft and never the source — so it
376
376
  * costs the render instead of a warning nobody reads. `useEntityLinkSource`
377
- * already throws `EntifixLogicError` at render for the sibling mistake (a search
377
+ * already throws at render for the sibling mistake (a search
378
378
  * property the target does not declare `filterable`); this is the same fault
379
379
  * caught one level up.
380
380
  *
@@ -402,20 +402,14 @@ linkSources) {
402
402
  if (sourced.length === 0) return;
403
403
  const toMany = sourced.filter((field)=>field.type === 'linkCollection').map((field)=>field.name);
404
404
  if (toMany.length > 0) {
405
- throw new EntifixLogicError(`EntityForm was given a link source for a to-many member: ${toMany.join(', ')}. ` + 'A `linkCollection` has no editor yet, so the source can only be dropped ' + 'and the field would render read-only with no sign anything was wired. ' + 'Remove the entry, or point it at the to-one `link` you meant.', undefined, {
406
- fields: toMany
407
- });
405
+ throw new Error(`EntityForm was given a link source for a to-many member: ${toMany.join(', ')}. ` + 'A `linkCollection` has no editor yet, so the source can only be dropped ' + 'and the field would render read-only with no sign anything was wired. ' + 'Remove the entry, or point it at the to-one `link` you meant.');
408
406
  }
409
407
  const owned = sourced.filter((field)=>field.type === 'composition').map((field)=>field.name);
410
408
  if (owned.length > 0) {
411
- throw new EntifixLogicError(`EntityForm was given a link source for an owned collection: ${owned.join(', ')}. ` + 'A `composition` is not a reference — its rows have no life outside this ' + 'record and are never picked from existing ones, so there is nothing for a ' + 'picker to look up. The detail grid is what edits them.', undefined, {
412
- fields: owned
413
- });
409
+ throw new Error(`EntityForm was given a link source for an owned collection: ${owned.join(', ')}. ` + 'A `composition` is not a reference — its rows have no life outside this ' + 'record and are never picked from existing ones, so there is nothing for a ' + 'picker to look up. The detail grid is what edits them.');
414
410
  }
415
411
  const unpickable = sourced.map((field)=>`${field.name} (${field.type})`);
416
- throw new EntifixLogicError(`EntityForm was given a link source for a member that cannot hold one: ${unpickable.join(', ')}. ` + 'A picker writes the target’s id into the draft, so the member has to be a ' + '`link` or the `string` that carries a foreign key. Remove the entry, or ' + 'point it at the member that actually holds the reference.', undefined, {
417
- fields: sourced.map((field)=>field.name)
418
- });
412
+ throw new Error(`EntityForm was given a link source for a member that cannot hold one: ${unpickable.join(', ')}. ` + 'A picker writes the target’s id into the draft, so the member has to be a ' + '`link` or the `string` that carries a foreign key. Remove the entry, or ' + 'point it at the member that actually holds the reference.');
419
413
  }
420
414
  /**
421
415
  * One label + control (or value) row. A `readonly` member renders its read
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/organisms/entity-form/entity-form.tsx"],"sourcesContent":["'use client';\n\nimport {\n cloneEntityDraft,\n describeEntityColumns,\n EntifixLogicError,\n type Entity,\n type EntityAction,\n type EntityDraft,\n type EntityDraftValue,\n type EntityLinkSource,\n type MetaAccessorType,\n readDraftString,\n readLocalizedDraft,\n readRowDrafts,\n type UseCaseDescriptor,\n} from '@entifix/core';\nimport { type ReactNode, useId, useState } from 'react';\n\nimport {\n useErrorMessage,\n useLocalizedDescriptors,\n useT,\n useTranslateKey,\n} from '../../../i18n/index.js';\nimport {\n OVERFLOW_GLYPH,\n renderAnchor,\n useCasesForSurface,\n} from '../../actions/index.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 { Card } from '../../molecules/card/index.js';\nimport { ConfirmDialog } from '../../molecules/confirm-dialog/index.js';\nimport { EntityLinkInput } from '../../molecules/entity-link-input/index.js';\nimport { LoadingBoundary } from '../../molecules/loading-boundary/index.js';\nimport { Menu } from '../../molecules/menu/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.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/**\n * How many verbs a surface shows before the rest fold into an overflow menu.\n *\n * Four, because that is roughly what fits beside a title at a laptop width\n * without the row wrapping — the same number Fiori's object-page header settles\n * on. It is a layout constant, not a policy: raising it does not grant anything.\n */\nconst VISIBLE_ACTION_LIMIT = 4;\n\n/** A verb waiting on its confirmation — narrowed so `confirm` is not optional. */\ninterface PendingConfirmation {\n key: string;\n labelKey: string;\n confirm: NonNullable<UseCaseDescriptor['confirm']>;\n}\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 embedded = false,\n isLoading = false,\n skeleton = true,\n isSaving = false,\n isDeleting = false,\n error,\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 errorMessage = useErrorMessage();\n const busy = isSaving || 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 // A descriptor carrying `confirm` must be asked about before it fires;\n // `revoke-sessions` ends every session a user holds. The state holds the\n // confirmation itself rather than the descriptor, so the dialog cannot be\n // opened for a verb that never asked for one.\n const [pending, setPending] = useState<PendingConfirmation | undefined>(\n undefined,\n );\n const invoke = (descriptor: UseCaseDescriptor) => {\n const confirm = descriptor.confirm;\n if (confirm) {\n setPending({\n key: descriptor.key,\n labelKey: descriptor.labelKey,\n confirm,\n });\n return;\n }\n onUseCase?.(descriptor.key);\n };\n\n const useCaseButton = (descriptor: UseCaseDescriptor) => (\n <Button\n key={descriptor.key}\n type=\"button\"\n variant={\n descriptor.confirm?.tone === 'destructive' ? 'destructive' : 'secondary'\n }\n size={descriptor.placement === 'context-independent' ? 'sm' : 'md'}\n disabled={busy}\n onClick={() => invoke(descriptor)}\n >\n {/* A descriptor's labels are *runtime* catalog keys — they never resolve\n at compile time, which is why a host's catalog check is what catches a\n typo here rather than the type system. */}\n {translateKey(descriptor.labelKey)}\n </Button>\n );\n\n const useCaseMenuItem = (descriptor: UseCaseDescriptor) => (\n <Menu.Item\n key={descriptor.key}\n tone={\n descriptor.confirm?.tone === 'destructive' ? 'destructive' : 'neutral'\n }\n disabled={busy}\n onClick={() => invoke(descriptor)}\n >\n {translateKey(descriptor.labelKey)}\n </Menu.Item>\n );\n\n /**\n * Four actions fit a row; twelve do not.\n *\n * So a surface renders its first {@link VISIBLE_ACTION_LIMIT} verbs as\n * buttons and folds the rest behind one overflow trigger, in **declaration\n * order** — the entity's author decided which verbs matter by writing them\n * first, and a rule that sorted by anything else (alphabetical, destructive\n * last) would reorder the row every time a verb was added.\n *\n * The split is only applied when there is something to fold: a fifth action\n * is the point at which a menu is cheaper than a wider row, and a menu\n * holding one entry is strictly worse than the button it replaced.\n */\n const withOverflow = (descriptors: UseCaseDescriptor[]) => {\n if (descriptors.length <= VISIBLE_ACTION_LIMIT) {\n return descriptors.map(useCaseButton);\n }\n\n const visible = descriptors.slice(0, VISIBLE_ACTION_LIMIT);\n const folded = descriptors.slice(VISIBLE_ACTION_LIMIT);\n\n return [\n ...visible.map(useCaseButton),\n <Menu key=\"overflow\">\n <Menu.Trigger aria-label={t('form.moreActions')}>\n {OVERFLOW_GLYPH}\n </Menu.Trigger>\n <Menu.Items>{folded.map(useCaseMenuItem)}</Menu.Items>\n </Menu>,\n ];\n };\n\n return (\n <Card>\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 as=\"h2\" step={1} weight=\"semibold\">\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 ??\n (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 {withOverflow(headerUseCases)}\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 {error && (\n <p\n role=\"alert\"\n data-testid=\"entity-form-error\"\n className=\"rounded-sm border border-danger bg-danger-subtle px-s py-2xs text-step-sm text-danger\"\n >\n {errorMessage(error)}\n </p>\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 : 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 {/* 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\">\n {may('write') && (\n <Button\n type=\"button\"\n onClick={() => onSubmit?.(draft)}\n disabled={busy || resolvingLinks}\n >\n {isSaving ? t('form.saving') : t('form.save')}\n </Button>\n )}\n {onDelete && may('delete') && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={onDelete}\n disabled={busy}\n >\n {isDeleting ? t('form.deleting') : t('form.delete')}\n </Button>\n )}\n {withOverflow(footerUseCases)}\n {/* Clone is the CRUD triple's, not a declared verb's: every entity\n can be copied, so giving it a `@useCase()` would mean declaring\n the same verb on all 28 of them. It needs `write`, because what\n it produces is a create. */}\n {onClone && entity && may('write') && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n disabled={busy}\n onClick={() =>\n onClone(cloneEntityDraft(entityConstructor, draft))\n }\n >\n {t('form.clone')}\n </Button>\n )}\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 {pending && (\n <ConfirmDialog\n open\n tone={pending.confirm.tone}\n title={translateKey(pending.labelKey)}\n message={translateKey(pending.confirm.messageKey)}\n busy={busy}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n const key = pending.key;\n setPending(undefined);\n onUseCase?.(key);\n }}\n />\n )}\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 `EntifixLogicError` 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 EntifixLogicError(\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 undefined,\n { fields: toMany },\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 EntifixLogicError(\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 undefined,\n { fields: owned },\n );\n }\n\n const unpickable = sourced.map(field => `${field.name} (${field.type})`);\n throw new EntifixLogicError(\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 undefined,\n { fields: sourced.map(field => field.name) },\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 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 />\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 />\n );\n }\n\n const ownsLabel =\n editing && field.type === 'boolean' && !field.render && !field.virtual;\n\n return (\n <Stack gap=\"3xs\">\n {!ownsLabel && (\n <label htmlFor={id} className=\"text-step-sm text-content-muted\">\n {field.label}\n </label>\n )}\n {control}\n {error && (\n <span role=\"alert\" className=\"text-step-sm text-danger\">\n {error}\n </span>\n )}\n </Stack>\n );\n}\n"],"names":["cloneEntityDraft","describeEntityColumns","EntifixLogicError","readDraftString","readLocalizedDraft","readRowDrafts","useId","useState","useErrorMessage","useLocalizedDescriptors","useT","useTranslateKey","OVERFLOW_GLYPH","renderAnchor","useCasesForSurface","Button","button","CellValue","FieldControl","LocalizedFieldControl","Skeleton","Text","Card","ConfirmDialog","EntityLinkInput","LoadingBoundary","Menu","Stack","cn","EntityDetailGrid","readEntityFormFields","resolveEntityFormFields","PICKABLE_TYPES","Set","VISIBLE_ACTION_LIMIT","EntityForm","entityConstructor","entity","values","onFieldChange","linkSources","onLinkChange","mode","modeProp","defaultMode","onModeChange","errors","formError","onSubmit","onDelete","embedded","isLoading","skeleton","isSaving","isDeleting","error","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","errorMessage","busy","resolvingLinks","Object","some","source","selected","may","action","actions","includes","declaredHeaderUseCases","useCases","declaredFooterUseCases","headerUseCases","footerUseCases","pending","setPending","invoke","descriptor","confirm","key","labelKey","useCaseButton","type","variant","tone","size","placement","disabled","onClick","useCaseMenuItem","Item","withOverflow","descriptors","length","map","visible","slice","folded","Trigger","aria-label","Items","gap","direction","align","as","step","weight","lines","headerActions","span","role","aria-live","className","p","data-testid","shape","FieldRow","linkSource","id","rows","onRowsChange","footerActions","href","intent","kind","rest","open","message","messageKey","onCancel","onConfirm","childType","sourced","has","toMany","join","owned","unpickable","readNode","readRender","isRelation","control","render","readonly","onSelect","target","String","onClear","virtual","onChange","ownsLabel","label","htmlFor"],"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 type MetaAccessorType,\n readDraftString,\n readLocalizedDraft,\n readRowDrafts,\n type UseCaseDescriptor,\n} from '@entifix/core';\nimport { type ReactNode, useId, useState } from 'react';\n\nimport {\n useErrorMessage,\n useLocalizedDescriptors,\n useT,\n useTranslateKey,\n} from '../../../i18n/index.js';\nimport {\n OVERFLOW_GLYPH,\n renderAnchor,\n useCasesForSurface,\n} from '../../actions/index.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 { Card } from '../../molecules/card/index.js';\nimport { ConfirmDialog } from '../../molecules/confirm-dialog/index.js';\nimport { EntityLinkInput } from '../../molecules/entity-link-input/index.js';\nimport { LoadingBoundary } from '../../molecules/loading-boundary/index.js';\nimport { Menu } from '../../molecules/menu/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.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/**\n * How many verbs a surface shows before the rest fold into an overflow menu.\n *\n * Four, because that is roughly what fits beside a title at a laptop width\n * without the row wrapping — the same number Fiori's object-page header settles\n * on. It is a layout constant, not a policy: raising it does not grant anything.\n */\nconst VISIBLE_ACTION_LIMIT = 4;\n\n/** A verb waiting on its confirmation — narrowed so `confirm` is not optional. */\ninterface PendingConfirmation {\n key: string;\n labelKey: string;\n confirm: NonNullable<UseCaseDescriptor['confirm']>;\n}\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 embedded = false,\n isLoading = false,\n skeleton = true,\n isSaving = false,\n isDeleting = false,\n error,\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 errorMessage = useErrorMessage();\n const busy = isSaving || 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 // A descriptor carrying `confirm` must be asked about before it fires;\n // `revoke-sessions` ends every session a user holds. The state holds the\n // confirmation itself rather than the descriptor, so the dialog cannot be\n // opened for a verb that never asked for one.\n const [pending, setPending] = useState<PendingConfirmation | undefined>(\n undefined,\n );\n const invoke = (descriptor: UseCaseDescriptor) => {\n const confirm = descriptor.confirm;\n if (confirm) {\n setPending({\n key: descriptor.key,\n labelKey: descriptor.labelKey,\n confirm,\n });\n return;\n }\n onUseCase?.(descriptor.key);\n };\n\n const useCaseButton = (descriptor: UseCaseDescriptor) => (\n <Button\n key={descriptor.key}\n type=\"button\"\n variant={\n descriptor.confirm?.tone === 'destructive' ? 'destructive' : 'secondary'\n }\n size={descriptor.placement === 'context-independent' ? 'sm' : 'md'}\n disabled={busy}\n onClick={() => invoke(descriptor)}\n >\n {/* A descriptor's labels are *runtime* catalog keys — they never resolve\n at compile time, which is why a host's catalog check is what catches a\n typo here rather than the type system. */}\n {translateKey(descriptor.labelKey)}\n </Button>\n );\n\n const useCaseMenuItem = (descriptor: UseCaseDescriptor) => (\n <Menu.Item\n key={descriptor.key}\n tone={\n descriptor.confirm?.tone === 'destructive' ? 'destructive' : 'neutral'\n }\n disabled={busy}\n onClick={() => invoke(descriptor)}\n >\n {translateKey(descriptor.labelKey)}\n </Menu.Item>\n );\n\n /**\n * Four actions fit a row; twelve do not.\n *\n * So a surface renders its first {@link VISIBLE_ACTION_LIMIT} verbs as\n * buttons and folds the rest behind one overflow trigger, in **declaration\n * order** — the entity's author decided which verbs matter by writing them\n * first, and a rule that sorted by anything else (alphabetical, destructive\n * last) would reorder the row every time a verb was added.\n *\n * The split is only applied when there is something to fold: a fifth action\n * is the point at which a menu is cheaper than a wider row, and a menu\n * holding one entry is strictly worse than the button it replaced.\n */\n const withOverflow = (descriptors: UseCaseDescriptor[]) => {\n if (descriptors.length <= VISIBLE_ACTION_LIMIT) {\n return descriptors.map(useCaseButton);\n }\n\n const visible = descriptors.slice(0, VISIBLE_ACTION_LIMIT);\n const folded = descriptors.slice(VISIBLE_ACTION_LIMIT);\n\n return [\n ...visible.map(useCaseButton),\n <Menu key=\"overflow\">\n <Menu.Trigger aria-label={t('form.moreActions')}>\n {OVERFLOW_GLYPH}\n </Menu.Trigger>\n <Menu.Items>{folded.map(useCaseMenuItem)}</Menu.Items>\n </Menu>,\n ];\n };\n\n return (\n <Card>\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 as=\"h2\" step={1} weight=\"semibold\">\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 ??\n (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 {withOverflow(headerUseCases)}\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 {error && (\n <p\n role=\"alert\"\n data-testid=\"entity-form-error\"\n className=\"rounded-sm border border-danger bg-danger-subtle px-s py-2xs text-step-sm text-danger\"\n >\n {errorMessage(error)}\n </p>\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 : 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 {/* 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\">\n {may('write') && (\n <Button\n type=\"button\"\n onClick={() => onSubmit?.(draft)}\n disabled={busy || resolvingLinks}\n >\n {isSaving ? t('form.saving') : t('form.save')}\n </Button>\n )}\n {onDelete && may('delete') && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={onDelete}\n disabled={busy}\n >\n {isDeleting ? t('form.deleting') : t('form.delete')}\n </Button>\n )}\n {withOverflow(footerUseCases)}\n {/* Clone is the CRUD triple's, not a declared verb's: every entity\n can be copied, so giving it a `@useCase()` would mean declaring\n the same verb on all 28 of them. It needs `write`, because what\n it produces is a create. */}\n {onClone && entity && may('write') && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n disabled={busy}\n onClick={() =>\n onClone(cloneEntityDraft(entityConstructor, draft))\n }\n >\n {t('form.clone')}\n </Button>\n )}\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 {pending && (\n <ConfirmDialog\n open\n tone={pending.confirm.tone}\n title={translateKey(pending.labelKey)}\n message={translateKey(pending.confirm.messageKey)}\n busy={busy}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n const key = pending.key;\n setPending(undefined);\n onUseCase?.(key);\n }}\n />\n )}\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 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 />\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 />\n );\n }\n\n const ownsLabel =\n editing && field.type === 'boolean' && !field.render && !field.virtual;\n\n return (\n <Stack gap=\"3xs\">\n {!ownsLabel && (\n <label htmlFor={id} className=\"text-step-sm text-content-muted\">\n {field.label}\n </label>\n )}\n {control}\n {error && (\n <span role=\"alert\" className=\"text-step-sm text-danger\">\n {error}\n </span>\n )}\n </Stack>\n );\n}\n"],"names":["cloneEntityDraft","describeEntityColumns","readDraftString","readLocalizedDraft","readRowDrafts","useId","useState","useErrorMessage","useLocalizedDescriptors","useT","useTranslateKey","OVERFLOW_GLYPH","renderAnchor","useCasesForSurface","Button","button","CellValue","FieldControl","LocalizedFieldControl","Skeleton","Text","Card","ConfirmDialog","EntityLinkInput","LoadingBoundary","Menu","Stack","cn","EntityDetailGrid","readEntityFormFields","resolveEntityFormFields","PICKABLE_TYPES","Set","VISIBLE_ACTION_LIMIT","EntityForm","entityConstructor","entity","values","onFieldChange","linkSources","onLinkChange","mode","modeProp","defaultMode","onModeChange","errors","formError","onSubmit","onDelete","embedded","isLoading","skeleton","isSaving","isDeleting","error","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","errorMessage","busy","resolvingLinks","Object","some","source","selected","may","action","actions","includes","declaredHeaderUseCases","useCases","declaredFooterUseCases","headerUseCases","footerUseCases","pending","setPending","invoke","descriptor","confirm","key","labelKey","useCaseButton","type","variant","tone","size","placement","disabled","onClick","useCaseMenuItem","Item","withOverflow","descriptors","length","map","visible","slice","folded","Trigger","aria-label","Items","gap","direction","align","as","step","weight","lines","headerActions","span","role","aria-live","className","p","data-testid","shape","FieldRow","linkSource","id","rows","onRowsChange","footerActions","href","intent","kind","rest","open","message","messageKey","onCancel","onConfirm","childType","sourced","has","toMany","Error","join","owned","unpickable","readNode","readRender","isRelation","control","render","readonly","onSelect","target","String","onClear","virtual","onChange","ownsLabel","label","htmlFor"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { EntifixError, Entity, EntityConstructor, EntityDraft, EntityDraftValue, EntityFieldDescriptor, EntityLinkSource, EntityMetadataDocument } from '@entifix/core';
1
+ import type { Entity, EntityConstructor, EntityDraft, EntityDraftValue, EntityFieldDescriptor, EntityLinkSource, EntityMetadataDocument, TransitionalFailure } from '@entifix/core';
2
2
  import type { ReactNode } from 'react';
3
3
  import type { RenderLink } from '../../actions/index.js';
4
4
  /** Whether the form shows values as text (`read`) or as inputs (`edit`). */
@@ -141,7 +141,7 @@ export interface EntityFormProps<TEntity extends Entity> {
141
141
  isSaving?: boolean;
142
142
  isDeleting?: boolean;
143
143
  /** The failure of the last load/save, shown as an alert. */
144
- error?: EntifixError;
144
+ error?: TransitionalFailure;
145
145
  /** Link to return to, rendered as a `Back` action. Omit to hide it. */
146
146
  backHref?: string;
147
147
  /**
@@ -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 EntifixError,\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 /**\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 isDeleting?: boolean;\n /** The failure of the last load/save, shown as an alert. */\n error?: EntifixError;\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,WA+IC"}
1
+ {"version":3,"sources":["../../../../src/ui/organisms/entity-form/entity-form.types.ts"],"sourcesContent":["import type {\n Entity,\n EntityConstructor,\n EntityDraft,\n EntityDraftValue,\n EntityFieldDescriptor,\n EntityLinkSource,\n EntityMetadataDocument,\n TransitionalFailure,\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 /**\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 isDeleting?: boolean;\n /** The failure of the last load/save, shown as an alert. */\n error?: TransitionalFailure;\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,WA+IC"}
@@ -1 +1 @@
1
- {"version":3,"file":"entity-table.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-table/entity-table.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,MAAM,EAWZ,MAAM,eAAe,CAAC;AAkCvB,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,yBAAyB,CAAC;AAyDjC,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,EAClD,iBAAiB,EACjB,SAAS,EACT,QAAe,EACf,KAAK,EACL,KAAK,EACL,UAAU,EACV,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,OAAO,EACP,OAAO,EACP,UAAyB,EACzB,QAAQ,EACR,cAAc,EACd,YAAmB,EACnB,eAAsB,EACtB,SAAS,EACT,OAAO,EACP,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,iBAAyB,EACzB,aAAa,EACb,SAAS,EACT,YAAY,EACZ,aAAa,EACb,WAAW,EACX,aAAqB,EACrB,QAAQ,GACT,EAAE,gBAAgB,CAAC,OAAO,CAAC,+BAysB3B"}
1
+ {"version":3,"file":"entity-table.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-table/entity-table.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,MAAM,EAWZ,MAAM,eAAe,CAAC;AAkCvB,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,yBAAyB,CAAC;AAyDjC,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,EAClD,iBAAiB,EACjB,SAAS,EACT,QAAe,EACf,KAAK,EACL,KAAK,EACL,UAAU,EACV,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,OAAO,EACP,OAAO,EACP,UAAyB,EACzB,QAAQ,EACR,cAAc,EACd,YAAmB,EACnB,eAAsB,EACtB,SAAS,EACT,OAAO,EACP,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,iBAAyB,EACzB,aAAa,EACb,SAAS,EACT,YAAY,EACZ,aAAa,EACb,WAAW,EACX,aAAqB,EACrB,QAAQ,GACT,EAAE,gBAAgB,CAAC,OAAO,CAAC,+BAysB3B"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { emptySelection, EntifixLogicError, extractMetaEntity, hierarchyOf, isSelected, selectionSize, toggleSelected } from '@entifix/core';
3
+ import { emptySelection, extractMetaEntity, hierarchyOf, isSelected, selectionSize, toggleSelected } from '@entifix/core';
4
4
  import { Fragment, useMemo, useRef, useState } from 'react';
5
5
  import { useErrorMessage, useT, useTranslateKey } from '../../../i18n/index.js';
6
6
  import { OVERFLOW_GLYPH, renderAnchor, useCasesForSurface } from '../../actions/index.js';
@@ -87,7 +87,7 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
87
87
  // than a state, so it costs the render — the same call `EntityForm` makes
88
88
  // for a link source aimed at a member no picker can edit.
89
89
  if (onSelect !== undefined && selection !== undefined) {
90
- throw new EntifixLogicError('EntityTable was given both `onSelect` and `selection`. A picker picks one ' + 'row and a selection accumulates many, so the row control cannot be both. ' + 'Drop `selection` inside a picker, or drop `onSelect` outside one.');
90
+ throw new Error('EntityTable was given both `onSelect` and `selection`. A picker picks one ' + 'row and a selection accumulates many, so the row control cannot be both. ' + 'Drop `selection` inside a picker, or drop `onSelect` outside one.');
91
91
  }
92
92
  /**
93
93
  * The selection, or `undefined` when the table is not selectable at all.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/organisms/entity-table/entity-table.tsx"],"sourcesContent":["'use client';\n\nimport {\n emptySelection,\n EntifixLogicError,\n type Entity,\n type EntityId,\n type EntitySelection,\n type EntitySorting,\n extractMetaEntity,\n type FilterGroup,\n hierarchyOf,\n isSelected,\n selectionSize,\n toggleSelected,\n type UseCaseDescriptor,\n} from '@entifix/core';\nimport { Fragment, type ReactNode, useMemo, useRef, useState } from 'react';\n\nimport { useErrorMessage, useT, useTranslateKey } from '../../../i18n/index.js';\nimport {\n OVERFLOW_GLYPH,\n renderAnchor,\n useCasesForSurface,\n} from '../../actions/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { CellValue } from '../../atoms/cell-value/index.js';\nimport { Checkbox } from '../../atoms/field/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeaderCell,\n TableMessageRow,\n TableRow,\n} from '../../atoms/table/index.js';\nimport { linkClassName } from '../../atoms/text/index.js';\nimport { BulkActionBar } from '../../molecules/bulk-action-bar/index.js';\nimport { BulkResult } from '../../molecules/bulk-result/index.js';\nimport { ColumnSettings } from '../../molecules/column-settings/index.js';\nimport { ConfirmDialog } from '../../molecules/confirm-dialog/index.js';\nimport { EntityRecordCard } from '../../molecules/entity-record-card/index.js';\nimport { FilterBuilder } from '../../molecules/filter-builder/index.js';\nimport { LoadingBoundary } from '../../molecules/loading-boundary/index.js';\nimport { Menu } from '../../molecules/menu/index.js';\nimport { Pagination } from '../../molecules/pagination/index.js';\nimport { SortBuilder } from '../../molecules/sort-builder/index.js';\nimport { TableToolbar } from '../../molecules/table-toolbar/index.js';\nimport type {\n EntityTableColumn,\n EntityTablePivotBreakpoint,\n EntityTableProps,\n} from './entity-table.types.js';\nimport { readEntityTableSlots } from './entity-table-slots.js';\nimport { shownTreeRows, TreeCell, treeRows } from './entity-table-tree.js';\nimport { useEntityTableColumns } from './use-entity-table-columns.js';\n\n/**\n * The grid and the card list are both rendered, and CSS picks one. A JS\n * breakpoint hook would have to guess during SSR and correct after mount —\n * a hydration mismatch on every page load. Classes are written out in full\n * because Tailwind cannot see a class name that is concatenated at runtime.\n */\nconst PIVOT_CLASS: Record<\n EntityTablePivotBreakpoint,\n { grid: string; cards: string }\n> = {\n sm: { grid: 'hidden sm:block', cards: 'sm:hidden' },\n md: { grid: 'hidden md:block', cards: 'md:hidden' },\n lg: { grid: 'hidden lg:block', cards: 'lg:hidden' },\n};\n\ntype Panel = 'none' | 'filters' | 'sorting';\n\n/**\n * How many placeholder rows the built-in skeleton draws.\n *\n * The column count is taken from the real geometry, because that is the axis a\n * mismatch shifts; the row count is capped instead of tracking `pageSize`, since\n * a fifty-row page would otherwise paint fifty shimmer rows to stand in for\n * content that arrives below the fold anyway.\n */\nconst SKELETON_ROW_CAP = 5;\n\n/**\n * A table that builds itself from an entity's metadata: columns, labels, value\n * formatting and the filter/sort controls all come from `@accessor()`\n * declarations, so listing a new entity needs no bespoke table.\n *\n * Three things layer on top of that default:\n * - **personalization** — column order and visibility, persisted through the\n * UI-preferences port (so it survives reloads and can later move server-side);\n * - **responsiveness** — below `pivotBreakpoint` rows pivot into label/value\n * cards instead of scrolling sideways;\n * - **slots** — `<EntityColumn>` / `<EntityTableHeader>` / `<EntityTableRow>` /\n * `<EntityTableToolbar>` children override any part of the default rendering.\n */\n/**\n * What a pending verb will act on once it is confirmed.\n *\n * A form has one record; a table has a row or a selection, and those reach two\n * different handlers with two different payloads. Carrying the selection itself\n * rather than reading it back at confirm time also removes a branch that could\n * not happen: the bulk bar only renders inside a truthy selection.\n */\ntype PendingTarget<TEntity extends Entity> =\n | { kind: 'row'; item: TEntity }\n | { kind: 'selection'; selection: EntitySelection<TEntity> };\n\nexport function EntityTable<TEntity extends Entity>({\n entityConstructor,\n isLoading,\n skeleton = true,\n error,\n items,\n totalItems,\n currentPage,\n pageSize,\n onPageChange,\n onPageSizeChange,\n hrefFor,\n newHref,\n renderLink = renderAnchor,\n onSelect,\n preferencesKey,\n showControls = true,\n pivotBreakpoint = 'md',\n filtering,\n sorting,\n onFilteringChange,\n onSortingChange,\n selection,\n onSelectionChange,\n metadata,\n isMetadataLoading = false,\n onBulkUseCase,\n onUseCase,\n bulkOutcomes,\n onBulkDismiss,\n onBulkRetry,\n isBulkRunning = false,\n children,\n}: EntityTableProps<TEntity>) {\n const t = useT();\n // A descriptor's label is a runtime catalog key — the typed gate cannot see\n // it, which is why a host's catalog check is what catches a typo.\n const translateKey = useTranslateKey();\n const errorMessage = useErrorMessage();\n const metaEntity = extractMetaEntity(entityConstructor);\n const slots = readEntityTableSlots<TEntity>(children);\n const [panel, setPanel] = useState<Panel>('none');\n\n /**\n * A verb waiting to be confirmed, and what it will act on.\n *\n * ⚠️ **The table had no confirmation flow at all** until a change moved a\n * destructive verb onto it. It read `confirm.tone` — only to pick a tone for\n * a button or a menu item — and then fired on the first click. `unpublish` is\n * declared `destructive`, so a verb reachable from a row menu without this\n * would have been a one-click unpublish with a red label explaining what had\n * already happened.\n *\n * The state holds the confirmation itself rather than the descriptor, so the\n * dialog cannot open for a verb that never asked for one — the same shape\n * `EntityForm` uses, and for the same reason.\n *\n * It carries the **target** too, which the form's does not need: a form has\n * one record and a table has a row or a selection, and those run through two\n * different handlers with two different payloads.\n */\n const [pending, setPending] = useState<\n | {\n key: string;\n labelKey: string;\n confirm: NonNullable<UseCaseDescriptor['confirm']>;\n target: PendingTarget<TEntity>;\n }\n | undefined\n >(undefined);\n\n const { columns, visibleColumns, hidden, setPersonalization } =\n useEntityTableColumns<TEntity>(\n entityConstructor,\n slots.columns,\n items[0],\n `entity-table:${preferencesKey ?? metaEntity.key ?? metaEntity.name}`,\n );\n\n const pivot = PIVOT_CLASS[pivotBreakpoint];\n\n // A picker's table is choosing *one* value; a multi-selection there would\n // offer a set to a form member that holds a single reference, and the two\n // controls would fight over the same row. This is a wiring mistake rather\n // than a state, so it costs the render — the same call `EntityForm` makes\n // for a link source aimed at a member no picker can edit.\n if (onSelect !== undefined && selection !== undefined) {\n throw new EntifixLogicError(\n 'EntityTable was given both `onSelect` and `selection`. A picker picks one ' +\n 'row and a selection accumulates many, so the row control cannot be both. ' +\n 'Drop `selection` inside a picker, or drop `onSelect` outside one.',\n );\n }\n\n /**\n * The selection, or `undefined` when the table is not selectable at all.\n *\n * One value rather than a `selection` prop beside a `selectable` boolean:\n * two of them means every render site has to re-test both, and the second\n * test is unreachable — which reads as a defensive check and is really dead\n * code the coverage gate then cannot close.\n */\n const selectionState =\n selection !== undefined && onSelectionChange !== undefined\n ? selection\n : undefined;\n const selectable = selectionState !== undefined;\n const rowUseCases = useCasesForSurface('row-menu', metadata?.useCases);\n const bulkUseCases = useCasesForSurface('bulk-bar', metadata?.useCases);\n const toolbarUseCases = useCasesForSurface(\n 'table-toolbar',\n metadata?.useCases,\n );\n const hasRowMenu = rowUseCases.length > 0 && onUseCase !== undefined;\n\n /**\n * What a `context-independent` collection verb acts on: everything the\n * current filter matches.\n *\n * Not an empty `ids` selection, which would say \"no rows\" — the verb is\n * available *without* a selection precisely because its subject is the\n * collection, so the honest payload is the same filter the listing is\n * showing, and the same one a \"select all matching\" escalation would send.\n */\n const everythingMatching: EntitySelection<TEntity> = {\n mode: 'matching',\n filtering,\n total: totalItems,\n excluded: new Set<EntityId>(),\n };\n\n const hasRowAction =\n onSelect !== undefined || hrefFor !== undefined || hasRowMenu;\n const columnCount =\n visibleColumns.length + (hasRowAction ? 1 : 0) + (selectable ? 1 : 0);\n\n /**\n * The first load has nothing to show and is held with a skeleton; a refetch\n * already has rows, so it keeps them and only marks the region busy. Splitting\n * the two is what stops every pagination click flashing grey.\n */\n const isFirstLoad = isLoading && items.length === 0;\n const isRefetching = isLoading && items.length > 0;\n\n /** `true`/omitted → the built-in default; a node → that node; `false` → none. */\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n const showSkeleton = isFirstLoad && skeleton !== false;\n const skeletonRows = Array.from(\n { length: Math.min(pageSize, SKELETON_ROW_CAP) },\n (_, index) => index,\n );\n\n /**\n * The refetch hint, and it is deliberately the only one: rows stay readable\n * and simply dim, because a second skeleton over live data would be two\n * loading signals in one region.\n */\n const busyClass = isRefetching\n ? 'opacity-60 transition-opacity duration-200 ease-smooth'\n : '';\n\n const renderCell = (\n column: EntityTableColumn<TEntity>,\n item: TEntity,\n ): ReactNode =>\n column.render ? (\n column.render(item)\n ) : (\n <CellValue\n value={(item as Record<string, unknown>)[column.name]}\n descriptor={column}\n />\n );\n\n /**\n * The row a shift-click ranges *from* — the last box the user actually\n * toggled. Held in a ref rather than state because it changes nothing on\n * screen, and re-rendering the whole table on every click to remember an\n * anchor is a cost with no visible return.\n */\n const anchorIndex = useRef<number | undefined>(undefined);\n\n /**\n * An entity that declares a parent renders as a tree: the page arranged\n * depth first, its first column indented, and a subtree hidden while its\n * record is collapsed. Collapsing is view state, so it lives here and is\n * forgotten with the table, like the shift-click anchor above.\n */\n const treeKeys = useMemo(\n () => hierarchyOf(entityConstructor),\n [entityConstructor],\n );\n const [collapsed, setCollapsed] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const rows = useMemo(() => treeRows(items, treeKeys), [items, treeKeys]);\n const shown = shownTreeRows(rows, collapsed);\n const shownItems = shown.map(row => row.item);\n const toggleCollapsed = (key: string) =>\n setCollapsed(current => {\n const next = new Set(current);\n if (next.has(key)) next.delete(key);\n else next.add(key);\n return next;\n });\n\n const setSelection = (next: EntitySelection<TEntity>) =>\n onSelectionChange?.(next);\n\n const toggleRow = (\n index: number,\n checked: boolean,\n shiftKey: boolean,\n current: EntitySelection<TEntity>,\n ) => {\n // A shift-click extends the last toggle across the rows *on this page*, in\n // the direction the user dragged, and applies the new state to all of them.\n const from = shiftKey ? (anchorIndex.current ?? index) : index;\n const [start, end] = from <= index ? [from, index] : [index, from];\n\n let next = current;\n // The rows as shown: in a tree the order on screen is not the load's.\n for (const row of shownItems.slice(start, end + 1)) {\n next = toggleSelected(next, row.id, checked);\n }\n\n anchorIndex.current = index;\n setSelection(next);\n };\n\n /**\n * The header box, and it has three states rather than two: none of this\n * page's rows selected, all of them, or some. Without the third the box\n * reads as \"nothing is selected\" while a bulk bar sits below it saying\n * otherwise.\n */\n const pageIds = items.map(item => item.id);\n const selectedOnPage = selectionState\n ? pageIds.filter(id => isSelected(selectionState, id)).length\n : 0;\n const allOnPageSelected =\n pageIds.length > 0 && selectedOnPage === pageIds.length;\n const someOnPageSelected = selectedOnPage > 0 && !allOnPageSelected;\n\n const toggleAllOnPage = (\n checked: boolean,\n current: EntitySelection<TEntity>,\n ) => {\n let next = current;\n for (const id of pageIds) next = toggleSelected(next, id, checked);\n anchorIndex.current = undefined;\n setSelection(next);\n };\n\n /**\n * How a row names itself to a screen reader.\n *\n * The first visible column's value, because that is what the user reads the\n * row *as* — \"Seleccionar Acme\" rather than \"Seleccionar fila 3\", which tells\n * them the position they can already see and not the thing they cannot.\n */\n const rowLabel = (item: TEntity): string => {\n // The first non-identifier column **that this row actually filled in**.\n //\n // An id names a row the way \"row 3\" does: it repeats what the user can\n // already see. But skipping only the id is not enough — measured against\n // the seeded catalog, where `code` is the first non-id column and is empty\n // on every brand, so the label fell straight through to the id and never\n // reached `name`. A column that is blank on this row names it no better\n // than its position does, so the search continues past it.\n //\n // The id remains the last resort, because a row with nothing rendered\n // still has to announce itself.\n const named = visibleColumns.find(column => {\n if (column.type === 'id') return false;\n const value = (item as Record<string, unknown>)[column.name];\n return value != null && String(value) !== '';\n });\n\n return named\n ? String((item as Record<string, unknown>)[named.name])\n : String(item.id);\n };\n\n const selectionCell = (\n item: TEntity,\n index: number,\n // Passed in rather than closed over: this is only ever called under\n // `selectable`, which already proves the selection exists, and reading the\n // optional prop here would add a fallback arm that nothing can reach.\n current: EntitySelection<TEntity>,\n ) => (\n <Checkbox\n // Deliberately no `id`: both pivots are always in the DOM, so an id here\n // would be emitted twice per row. `aria-label` names the control instead,\n // and it names the *record* rather than the position.\n aria-label={t('table.selectRow', { record: rowLabel(item) })}\n checked={isSelected(current, item.id)}\n disabled={isBulkRunning}\n onChange={event =>\n toggleRow(\n index,\n event.currentTarget.checked,\n (event.nativeEvent as MouseEvent).shiftKey === true,\n current,\n )\n }\n />\n );\n\n const runTarget = (key: string, target: PendingTarget<TEntity>) => {\n if (target.kind === 'row') onUseCase?.(key, target.item);\n else onBulkUseCase?.(key, target.selection);\n };\n\n const invoke = (\n useCase: UseCaseDescriptor,\n target: PendingTarget<TEntity>,\n ) => {\n const confirm = useCase.confirm;\n if (confirm) {\n setPending({\n key: useCase.key,\n labelKey: useCase.labelKey,\n confirm,\n target,\n });\n return;\n }\n runTarget(useCase.key, target);\n };\n\n const rowMenu = (item: TEntity) =>\n hasRowMenu ? (\n <Menu>\n <Menu.Trigger aria-label={t('table.actions')}>\n {OVERFLOW_GLYPH}\n </Menu.Trigger>\n <Menu.Items>\n {rowUseCases.map(useCase => (\n <Menu.Item\n key={useCase.key}\n tone={\n useCase.confirm?.tone === 'destructive'\n ? 'destructive'\n : 'neutral'\n }\n onClick={() => invoke(useCase, { kind: 'row', item })}\n >\n {translateKey(useCase.labelKey)}\n </Menu.Item>\n ))}\n </Menu.Items>\n </Menu>\n ) : undefined;\n\n /**\n * Selection wins over navigation: a picker's row must set the value, not send\n * the user away from the form they came from.\n *\n * The row menu sits *beside* whichever of the two applies rather than\n * replacing it — \"open this record\" and \"run a verb on it\" are both true at\n * once, and folding the first into the menu would bury the action every row\n * of a list exists for.\n */\n const recordAction = (item: TEntity) => {\n const primary = onSelect ? (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => onSelect(item)}\n >\n {t('table.select')}\n </Button>\n ) : hrefFor ? (\n renderLink({\n href: hrefFor(item.id),\n className: linkClassName,\n children: t('table.open'),\n intent: { kind: 'open', id: item.id },\n })\n ) : undefined;\n\n const menu = rowMenu(item);\n if (!menu) return primary;\n\n return (\n <span className=\"flex items-center gap-2xs\">\n {primary}\n {menu}\n </span>\n );\n };\n\n /**\n * \"No records\" is a statement about the data; after a failed load it would be\n * a lie about it. The distinction matters most to the user who is deciding\n * whether their catalog is empty or their backend is down.\n */\n const emptyMessage = error ? t('table.error') : t('table.empty');\n\n return (\n <div className=\"flex flex-col gap-s\">\n {/*\n One announcement for the whole table, not one per layout. Both the grid\n and the card list are always in the DOM — CSS picks which is visible —\n so a live region inside each meant assistive tech was told twice that\n the table was loading. The shimmer itself is aria-hidden, so this is the\n only thing that carries the news.\n */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {isFirstLoad ? t('table.loading') : ''}\n </span>\n\n {showControls && (\n <TableToolbar\n start={\n <>\n {newHref &&\n renderLink({\n href: newHref,\n className: linkClassName,\n children: t('table.new'),\n intent: { kind: 'new' },\n })}\n {/* Collection-bound verbs that need no selection. They sit with\n `New` rather than in the bulk bar, because the bulk bar only\n exists while rows are ticked and these are always available. */}\n <LoadingBoundary isLoading={isMetadataLoading} lines={0}>\n <>\n {toolbarUseCases.map(useCase => (\n <Button\n key={useCase.key}\n type=\"button\"\n variant={\n useCase.confirm?.tone === 'destructive'\n ? 'destructive'\n : 'secondary'\n }\n size=\"sm\"\n disabled={isBulkRunning}\n onClick={() =>\n onBulkUseCase?.(useCase.key, everythingMatching)\n }\n >\n {translateKey(useCase.labelKey)}\n </Button>\n ))}\n </>\n </LoadingBoundary>\n {slots.toolbar}\n </>\n }\n end={\n <>\n <Button\n type=\"button\"\n variant={panel === 'filters' ? 'primary' : 'secondary'}\n size=\"sm\"\n onClick={() =>\n setPanel(current =>\n current === 'filters' ? 'none' : 'filters',\n )\n }\n >\n {t('table.filters')}\n </Button>\n <Button\n type=\"button\"\n variant={panel === 'sorting' ? 'primary' : 'secondary'}\n size=\"sm\"\n onClick={() =>\n setPanel(current =>\n current === 'sorting' ? 'none' : 'sorting',\n )\n }\n >\n {t('table.sorting')}\n </Button>\n <ColumnSettings\n columns={columns}\n hidden={hidden}\n onChange={setPersonalization}\n />\n </>\n }\n panel={\n panel === 'filters' ? (\n <FilterBuilder<TEntity>\n descriptors={columns.filter(column => column.filterable)}\n value={filtering}\n onChange={(applied: FilterGroup<TEntity>) =>\n onFilteringChange?.(applied)\n }\n />\n ) : panel === 'sorting' ? (\n <SortBuilder<TEntity>\n descriptors={columns.filter(column => column.sortable)}\n value={sorting}\n onChange={(applied: EntitySorting<TEntity>) =>\n onSortingChange?.(applied)\n }\n />\n ) : undefined\n }\n />\n )}\n\n {/*\n The bar appears when there is something selected and something to do\n with it. It sits above the rows rather than floating over them: a fixed\n overlay covers the last row of a short page, which is the row the user\n most likely just ticked.\n */}\n {selectionState && selectionSize(selectionState) > 0 && (\n <BulkActionBar\n count={selectionSize(selectionState)}\n useCases={bulkUseCases}\n busy={isBulkRunning}\n // Offered only when this page is fully ticked and the store holds\n // more than the page — the escalation has somewhere to escalate to.\n matchingTotal={\n selectionState.mode === 'ids' &&\n allOnPageSelected &&\n totalItems > items.length\n ? totalItems\n : undefined\n }\n onSelectAllMatching={() => setSelection(everythingMatching)}\n onClear={() => setSelection(emptySelection<TEntity>())}\n onUseCase={useCase =>\n invoke(useCase, { kind: 'selection', selection: selectionState })\n }\n />\n )}\n\n {pending && (\n <ConfirmDialog\n open\n tone={pending.confirm.tone}\n title={translateKey(pending.labelKey)}\n message={translateKey(pending.confirm.messageKey)}\n busy={isBulkRunning}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n const { key, target } = pending;\n setPending(undefined);\n runTarget(key, target);\n }}\n />\n )}\n\n {bulkOutcomes && bulkOutcomes.length > 0 && onBulkDismiss && (\n <BulkResult\n outcomes={bulkOutcomes}\n busy={isBulkRunning}\n labelFor={id => {\n const row = items.find(item => item.id === id);\n return row ? rowLabel(row) : undefined;\n }}\n onRetryFailed={onBulkRetry}\n onDismiss={onBulkDismiss}\n />\n )}\n\n {/*\n Rendered once, outside both layouts, and independently of whether there\n are rows: a refetch that fails leaves the previous page on screen, and\n without this the user would read stale data as current.\n */}\n {error && (\n <p\n role=\"alert\"\n data-testid=\"entity-table-error\"\n className=\"rounded-sm border border-danger bg-danger-subtle px-s py-2xs text-step-sm text-danger\"\n >\n {errorMessage(error)}\n </p>\n )}\n\n {/* Wide viewports: a grid. */}\n <div className={`${pivot.grid} ${busyClass}`}>\n <Table>\n <TableHead>\n {slots.header ? (\n slots.header.render(visibleColumns)\n ) : (\n <tr>\n {selectable && (\n <TableHeaderCell>\n <Checkbox\n aria-label={t('table.selectAllOnPage')}\n checked={allOnPageSelected}\n indeterminate={someOnPageSelected}\n disabled={isBulkRunning || items.length === 0}\n onChange={event =>\n toggleAllOnPage(\n event.currentTarget.checked,\n selectionState,\n )\n }\n />\n </TableHeaderCell>\n )}\n {visibleColumns.map(column => (\n <TableHeaderCell key={column.name}>\n {column.header ?? column.label}\n </TableHeaderCell>\n ))}\n {hasRowAction && (\n <TableHeaderCell>{t('table.actions')}</TableHeaderCell>\n )}\n </tr>\n )}\n </TableHead>\n <TableBody aria-busy={isLoading || undefined}>\n {/* Skeleton rows rather than the word \"Loading\", and one per column\n rather than a single full-width blob: the placeholder occupies\n the real grid, so the swap to rows shifts nothing sideways.\n `TableMessageRow` is deliberately bypassed — it is one centred\n full-span cell and cannot express the geometry. */}\n {showSkeleton &&\n (customSkeleton ? (\n <TableMessageRow colSpan={columnCount}>\n {customSkeleton}\n </TableMessageRow>\n ) : (\n skeletonRows.map(row => (\n <TableRow key={row}>\n {selectable && (\n <TableCell>\n <Skeleton shape=\"line\" className=\"w-4\" />\n </TableCell>\n )}\n {visibleColumns.map(column => (\n <TableCell key={column.name}>\n <Skeleton shape=\"line\" className=\"w-full\" />\n </TableCell>\n ))}\n {hasRowAction && (\n <TableCell>\n <Skeleton shape=\"line\" className=\"w-16\" />\n </TableCell>\n )}\n </TableRow>\n ))\n ))}\n {!isLoading && items.length === 0 && (\n <TableMessageRow colSpan={columnCount}>\n {emptyMessage}\n </TableMessageRow>\n )}\n {shown.map(({ item, depth, hasChildren }, index) =>\n slots.row ? (\n // A Fragment carries the key without wrapping the custom row in\n // an element `<tbody>` would reject.\n <Fragment key={String(item.id ?? index)}>\n {slots.row.render(item, visibleColumns)}\n </Fragment>\n ) : (\n <TableRow key={String(item.id ?? index)}>\n {selectionState && (\n <TableCell>\n {selectionCell(item, index, selectionState)}\n </TableCell>\n )}\n {visibleColumns.map((column, columnIndex) => (\n <TableCell key={column.name}>\n {treeKeys !== undefined && columnIndex === 0 ? (\n <TreeCell\n depth={depth}\n hasChildren={hasChildren}\n expanded={!collapsed.has(treeKeys.idOf(item))}\n label={t(\n collapsed.has(treeKeys.idOf(item))\n ? 'table.expandRow'\n : 'table.collapseRow',\n { record: rowLabel(item) },\n )}\n onToggle={() => toggleCollapsed(treeKeys.idOf(item))}\n >\n {renderCell(column, item)}\n </TreeCell>\n ) : (\n renderCell(column, item)\n )}\n </TableCell>\n ))}\n {hasRowAction && <TableCell>{recordAction(item)}</TableCell>}\n </TableRow>\n ),\n )}\n </TableBody>\n </Table>\n </div>\n\n {/* Narrow viewports: the same columns pivoted into cards. */}\n <div\n className={`${pivot.cards} flex flex-col gap-2xs ${busyClass}`}\n aria-busy={isLoading || undefined}\n >\n {/* The card pivot gets card-shaped placeholders, not a bare shimmer\n block: the border, radius and padding are the geometry here. */}\n {showSkeleton &&\n (customSkeleton ??\n skeletonRows.map(row => (\n <EntityRecordCard\n key={row}\n columns={visibleColumns}\n renderCell={() => <Skeleton shape=\"line\" className=\"w-24\" />}\n />\n )))}\n {!isLoading && items.length === 0 && (\n <p className=\"text-step-sm text-content-muted\">{emptyMessage}</p>\n )}\n {shown.map(({ item }, index) => (\n <EntityRecordCard\n key={String(item.id ?? index)}\n columns={visibleColumns}\n // The card is handed the resolved columns themselves, so the\n // narrow layout goes through the exact same renderers as the grid.\n renderCell={column =>\n renderCell(column as EntityTableColumn<TEntity>, item)\n }\n leading={\n selectionState\n ? selectionCell(item, index, selectionState)\n : undefined\n }\n actions={recordAction(item)}\n />\n ))}\n </div>\n\n <Pagination\n currentPage={currentPage}\n pageSize={pageSize}\n totalItems={totalItems}\n onPageChange={onPageChange}\n onPageSizeChange={onPageSizeChange}\n />\n\n {slots.rest}\n </div>\n );\n}\n"],"names":["emptySelection","EntifixLogicError","extractMetaEntity","hierarchyOf","isSelected","selectionSize","toggleSelected","Fragment","useMemo","useRef","useState","useErrorMessage","useT","useTranslateKey","OVERFLOW_GLYPH","renderAnchor","useCasesForSurface","Button","CellValue","Checkbox","Skeleton","Table","TableBody","TableCell","TableHead","TableHeaderCell","TableMessageRow","TableRow","linkClassName","BulkActionBar","BulkResult","ColumnSettings","ConfirmDialog","EntityRecordCard","FilterBuilder","LoadingBoundary","Menu","Pagination","SortBuilder","TableToolbar","readEntityTableSlots","shownTreeRows","TreeCell","treeRows","useEntityTableColumns","PIVOT_CLASS","sm","grid","cards","md","lg","SKELETON_ROW_CAP","EntityTable","entityConstructor","isLoading","skeleton","error","items","totalItems","currentPage","pageSize","onPageChange","onPageSizeChange","hrefFor","newHref","renderLink","onSelect","preferencesKey","showControls","pivotBreakpoint","filtering","sorting","onFilteringChange","onSortingChange","selection","onSelectionChange","metadata","isMetadataLoading","onBulkUseCase","onUseCase","bulkOutcomes","onBulkDismiss","onBulkRetry","isBulkRunning","children","t","translateKey","errorMessage","metaEntity","slots","panel","setPanel","pending","setPending","undefined","columns","visibleColumns","hidden","setPersonalization","key","name","pivot","selectionState","selectable","rowUseCases","useCases","bulkUseCases","toolbarUseCases","hasRowMenu","length","everythingMatching","mode","total","excluded","Set","hasRowAction","columnCount","isFirstLoad","isRefetching","customSkeleton","showSkeleton","skeletonRows","Array","from","Math","min","_","index","busyClass","renderCell","column","item","render","value","descriptor","anchorIndex","treeKeys","collapsed","setCollapsed","rows","shown","shownItems","map","row","toggleCollapsed","current","next","has","delete","add","setSelection","toggleRow","checked","shiftKey","start","end","slice","id","pageIds","selectedOnPage","filter","allOnPageSelected","someOnPageSelected","toggleAllOnPage","rowLabel","named","find","type","String","selectionCell","aria-label","record","disabled","onChange","event","currentTarget","nativeEvent","runTarget","target","kind","invoke","useCase","confirm","labelKey","rowMenu","Trigger","Items","Item","tone","onClick","recordAction","primary","variant","size","href","className","intent","menu","span","emptyMessage","div","role","aria-live","lines","toolbar","descriptors","filterable","applied","sortable","count","busy","matchingTotal","onSelectAllMatching","onClear","open","title","message","messageKey","onCancel","onConfirm","outcomes","labelFor","onRetryFailed","onDismiss","p","data-testid","header","tr","indeterminate","label","aria-busy","colSpan","shape","depth","hasChildren","columnIndex","expanded","idOf","onToggle","leading","actions","rest"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/ui/organisms/entity-table/entity-table.tsx"],"sourcesContent":["'use client';\n\nimport {\n emptySelection,\n type Entity,\n type EntityId,\n type EntitySelection,\n type EntitySorting,\n extractMetaEntity,\n type FilterGroup,\n hierarchyOf,\n isSelected,\n selectionSize,\n toggleSelected,\n type UseCaseDescriptor,\n} from '@entifix/core';\nimport { Fragment, type ReactNode, useMemo, useRef, useState } from 'react';\n\nimport { useErrorMessage, useT, useTranslateKey } from '../../../i18n/index.js';\nimport {\n OVERFLOW_GLYPH,\n renderAnchor,\n useCasesForSurface,\n} from '../../actions/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { CellValue } from '../../atoms/cell-value/index.js';\nimport { Checkbox } from '../../atoms/field/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeaderCell,\n TableMessageRow,\n TableRow,\n} from '../../atoms/table/index.js';\nimport { linkClassName } from '../../atoms/text/index.js';\nimport { BulkActionBar } from '../../molecules/bulk-action-bar/index.js';\nimport { BulkResult } from '../../molecules/bulk-result/index.js';\nimport { ColumnSettings } from '../../molecules/column-settings/index.js';\nimport { ConfirmDialog } from '../../molecules/confirm-dialog/index.js';\nimport { EntityRecordCard } from '../../molecules/entity-record-card/index.js';\nimport { FilterBuilder } from '../../molecules/filter-builder/index.js';\nimport { LoadingBoundary } from '../../molecules/loading-boundary/index.js';\nimport { Menu } from '../../molecules/menu/index.js';\nimport { Pagination } from '../../molecules/pagination/index.js';\nimport { SortBuilder } from '../../molecules/sort-builder/index.js';\nimport { TableToolbar } from '../../molecules/table-toolbar/index.js';\nimport type {\n EntityTableColumn,\n EntityTablePivotBreakpoint,\n EntityTableProps,\n} from './entity-table.types.js';\nimport { readEntityTableSlots } from './entity-table-slots.js';\nimport { shownTreeRows, TreeCell, treeRows } from './entity-table-tree.js';\nimport { useEntityTableColumns } from './use-entity-table-columns.js';\n\n/**\n * The grid and the card list are both rendered, and CSS picks one. A JS\n * breakpoint hook would have to guess during SSR and correct after mount —\n * a hydration mismatch on every page load. Classes are written out in full\n * because Tailwind cannot see a class name that is concatenated at runtime.\n */\nconst PIVOT_CLASS: Record<\n EntityTablePivotBreakpoint,\n { grid: string; cards: string }\n> = {\n sm: { grid: 'hidden sm:block', cards: 'sm:hidden' },\n md: { grid: 'hidden md:block', cards: 'md:hidden' },\n lg: { grid: 'hidden lg:block', cards: 'lg:hidden' },\n};\n\ntype Panel = 'none' | 'filters' | 'sorting';\n\n/**\n * How many placeholder rows the built-in skeleton draws.\n *\n * The column count is taken from the real geometry, because that is the axis a\n * mismatch shifts; the row count is capped instead of tracking `pageSize`, since\n * a fifty-row page would otherwise paint fifty shimmer rows to stand in for\n * content that arrives below the fold anyway.\n */\nconst SKELETON_ROW_CAP = 5;\n\n/**\n * A table that builds itself from an entity's metadata: columns, labels, value\n * formatting and the filter/sort controls all come from `@accessor()`\n * declarations, so listing a new entity needs no bespoke table.\n *\n * Three things layer on top of that default:\n * - **personalization** — column order and visibility, persisted through the\n * UI-preferences port (so it survives reloads and can later move server-side);\n * - **responsiveness** — below `pivotBreakpoint` rows pivot into label/value\n * cards instead of scrolling sideways;\n * - **slots** — `<EntityColumn>` / `<EntityTableHeader>` / `<EntityTableRow>` /\n * `<EntityTableToolbar>` children override any part of the default rendering.\n */\n/**\n * What a pending verb will act on once it is confirmed.\n *\n * A form has one record; a table has a row or a selection, and those reach two\n * different handlers with two different payloads. Carrying the selection itself\n * rather than reading it back at confirm time also removes a branch that could\n * not happen: the bulk bar only renders inside a truthy selection.\n */\ntype PendingTarget<TEntity extends Entity> =\n | { kind: 'row'; item: TEntity }\n | { kind: 'selection'; selection: EntitySelection<TEntity> };\n\nexport function EntityTable<TEntity extends Entity>({\n entityConstructor,\n isLoading,\n skeleton = true,\n error,\n items,\n totalItems,\n currentPage,\n pageSize,\n onPageChange,\n onPageSizeChange,\n hrefFor,\n newHref,\n renderLink = renderAnchor,\n onSelect,\n preferencesKey,\n showControls = true,\n pivotBreakpoint = 'md',\n filtering,\n sorting,\n onFilteringChange,\n onSortingChange,\n selection,\n onSelectionChange,\n metadata,\n isMetadataLoading = false,\n onBulkUseCase,\n onUseCase,\n bulkOutcomes,\n onBulkDismiss,\n onBulkRetry,\n isBulkRunning = false,\n children,\n}: EntityTableProps<TEntity>) {\n const t = useT();\n // A descriptor's label is a runtime catalog key — the typed gate cannot see\n // it, which is why a host's catalog check is what catches a typo.\n const translateKey = useTranslateKey();\n const errorMessage = useErrorMessage();\n const metaEntity = extractMetaEntity(entityConstructor);\n const slots = readEntityTableSlots<TEntity>(children);\n const [panel, setPanel] = useState<Panel>('none');\n\n /**\n * A verb waiting to be confirmed, and what it will act on.\n *\n * ⚠️ **The table had no confirmation flow at all** until a change moved a\n * destructive verb onto it. It read `confirm.tone` — only to pick a tone for\n * a button or a menu item — and then fired on the first click. `unpublish` is\n * declared `destructive`, so a verb reachable from a row menu without this\n * would have been a one-click unpublish with a red label explaining what had\n * already happened.\n *\n * The state holds the confirmation itself rather than the descriptor, so the\n * dialog cannot open for a verb that never asked for one — the same shape\n * `EntityForm` uses, and for the same reason.\n *\n * It carries the **target** too, which the form's does not need: a form has\n * one record and a table has a row or a selection, and those run through two\n * different handlers with two different payloads.\n */\n const [pending, setPending] = useState<\n | {\n key: string;\n labelKey: string;\n confirm: NonNullable<UseCaseDescriptor['confirm']>;\n target: PendingTarget<TEntity>;\n }\n | undefined\n >(undefined);\n\n const { columns, visibleColumns, hidden, setPersonalization } =\n useEntityTableColumns<TEntity>(\n entityConstructor,\n slots.columns,\n items[0],\n `entity-table:${preferencesKey ?? metaEntity.key ?? metaEntity.name}`,\n );\n\n const pivot = PIVOT_CLASS[pivotBreakpoint];\n\n // A picker's table is choosing *one* value; a multi-selection there would\n // offer a set to a form member that holds a single reference, and the two\n // controls would fight over the same row. This is a wiring mistake rather\n // than a state, so it costs the render — the same call `EntityForm` makes\n // for a link source aimed at a member no picker can edit.\n if (onSelect !== undefined && selection !== undefined) {\n throw new Error(\n 'EntityTable was given both `onSelect` and `selection`. A picker picks one ' +\n 'row and a selection accumulates many, so the row control cannot be both. ' +\n 'Drop `selection` inside a picker, or drop `onSelect` outside one.',\n );\n }\n\n /**\n * The selection, or `undefined` when the table is not selectable at all.\n *\n * One value rather than a `selection` prop beside a `selectable` boolean:\n * two of them means every render site has to re-test both, and the second\n * test is unreachable — which reads as a defensive check and is really dead\n * code the coverage gate then cannot close.\n */\n const selectionState =\n selection !== undefined && onSelectionChange !== undefined\n ? selection\n : undefined;\n const selectable = selectionState !== undefined;\n const rowUseCases = useCasesForSurface('row-menu', metadata?.useCases);\n const bulkUseCases = useCasesForSurface('bulk-bar', metadata?.useCases);\n const toolbarUseCases = useCasesForSurface(\n 'table-toolbar',\n metadata?.useCases,\n );\n const hasRowMenu = rowUseCases.length > 0 && onUseCase !== undefined;\n\n /**\n * What a `context-independent` collection verb acts on: everything the\n * current filter matches.\n *\n * Not an empty `ids` selection, which would say \"no rows\" — the verb is\n * available *without* a selection precisely because its subject is the\n * collection, so the honest payload is the same filter the listing is\n * showing, and the same one a \"select all matching\" escalation would send.\n */\n const everythingMatching: EntitySelection<TEntity> = {\n mode: 'matching',\n filtering,\n total: totalItems,\n excluded: new Set<EntityId>(),\n };\n\n const hasRowAction =\n onSelect !== undefined || hrefFor !== undefined || hasRowMenu;\n const columnCount =\n visibleColumns.length + (hasRowAction ? 1 : 0) + (selectable ? 1 : 0);\n\n /**\n * The first load has nothing to show and is held with a skeleton; a refetch\n * already has rows, so it keeps them and only marks the region busy. Splitting\n * the two is what stops every pagination click flashing grey.\n */\n const isFirstLoad = isLoading && items.length === 0;\n const isRefetching = isLoading && items.length > 0;\n\n /** `true`/omitted → the built-in default; a node → that node; `false` → none. */\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n const showSkeleton = isFirstLoad && skeleton !== false;\n const skeletonRows = Array.from(\n { length: Math.min(pageSize, SKELETON_ROW_CAP) },\n (_, index) => index,\n );\n\n /**\n * The refetch hint, and it is deliberately the only one: rows stay readable\n * and simply dim, because a second skeleton over live data would be two\n * loading signals in one region.\n */\n const busyClass = isRefetching\n ? 'opacity-60 transition-opacity duration-200 ease-smooth'\n : '';\n\n const renderCell = (\n column: EntityTableColumn<TEntity>,\n item: TEntity,\n ): ReactNode =>\n column.render ? (\n column.render(item)\n ) : (\n <CellValue\n value={(item as Record<string, unknown>)[column.name]}\n descriptor={column}\n />\n );\n\n /**\n * The row a shift-click ranges *from* — the last box the user actually\n * toggled. Held in a ref rather than state because it changes nothing on\n * screen, and re-rendering the whole table on every click to remember an\n * anchor is a cost with no visible return.\n */\n const anchorIndex = useRef<number | undefined>(undefined);\n\n /**\n * An entity that declares a parent renders as a tree: the page arranged\n * depth first, its first column indented, and a subtree hidden while its\n * record is collapsed. Collapsing is view state, so it lives here and is\n * forgotten with the table, like the shift-click anchor above.\n */\n const treeKeys = useMemo(\n () => hierarchyOf(entityConstructor),\n [entityConstructor],\n );\n const [collapsed, setCollapsed] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const rows = useMemo(() => treeRows(items, treeKeys), [items, treeKeys]);\n const shown = shownTreeRows(rows, collapsed);\n const shownItems = shown.map(row => row.item);\n const toggleCollapsed = (key: string) =>\n setCollapsed(current => {\n const next = new Set(current);\n if (next.has(key)) next.delete(key);\n else next.add(key);\n return next;\n });\n\n const setSelection = (next: EntitySelection<TEntity>) =>\n onSelectionChange?.(next);\n\n const toggleRow = (\n index: number,\n checked: boolean,\n shiftKey: boolean,\n current: EntitySelection<TEntity>,\n ) => {\n // A shift-click extends the last toggle across the rows *on this page*, in\n // the direction the user dragged, and applies the new state to all of them.\n const from = shiftKey ? (anchorIndex.current ?? index) : index;\n const [start, end] = from <= index ? [from, index] : [index, from];\n\n let next = current;\n // The rows as shown: in a tree the order on screen is not the load's.\n for (const row of shownItems.slice(start, end + 1)) {\n next = toggleSelected(next, row.id, checked);\n }\n\n anchorIndex.current = index;\n setSelection(next);\n };\n\n /**\n * The header box, and it has three states rather than two: none of this\n * page's rows selected, all of them, or some. Without the third the box\n * reads as \"nothing is selected\" while a bulk bar sits below it saying\n * otherwise.\n */\n const pageIds = items.map(item => item.id);\n const selectedOnPage = selectionState\n ? pageIds.filter(id => isSelected(selectionState, id)).length\n : 0;\n const allOnPageSelected =\n pageIds.length > 0 && selectedOnPage === pageIds.length;\n const someOnPageSelected = selectedOnPage > 0 && !allOnPageSelected;\n\n const toggleAllOnPage = (\n checked: boolean,\n current: EntitySelection<TEntity>,\n ) => {\n let next = current;\n for (const id of pageIds) next = toggleSelected(next, id, checked);\n anchorIndex.current = undefined;\n setSelection(next);\n };\n\n /**\n * How a row names itself to a screen reader.\n *\n * The first visible column's value, because that is what the user reads the\n * row *as* — \"Seleccionar Acme\" rather than \"Seleccionar fila 3\", which tells\n * them the position they can already see and not the thing they cannot.\n */\n const rowLabel = (item: TEntity): string => {\n // The first non-identifier column **that this row actually filled in**.\n //\n // An id names a row the way \"row 3\" does: it repeats what the user can\n // already see. But skipping only the id is not enough — measured against\n // the seeded catalog, where `code` is the first non-id column and is empty\n // on every brand, so the label fell straight through to the id and never\n // reached `name`. A column that is blank on this row names it no better\n // than its position does, so the search continues past it.\n //\n // The id remains the last resort, because a row with nothing rendered\n // still has to announce itself.\n const named = visibleColumns.find(column => {\n if (column.type === 'id') return false;\n const value = (item as Record<string, unknown>)[column.name];\n return value != null && String(value) !== '';\n });\n\n return named\n ? String((item as Record<string, unknown>)[named.name])\n : String(item.id);\n };\n\n const selectionCell = (\n item: TEntity,\n index: number,\n // Passed in rather than closed over: this is only ever called under\n // `selectable`, which already proves the selection exists, and reading the\n // optional prop here would add a fallback arm that nothing can reach.\n current: EntitySelection<TEntity>,\n ) => (\n <Checkbox\n // Deliberately no `id`: both pivots are always in the DOM, so an id here\n // would be emitted twice per row. `aria-label` names the control instead,\n // and it names the *record* rather than the position.\n aria-label={t('table.selectRow', { record: rowLabel(item) })}\n checked={isSelected(current, item.id)}\n disabled={isBulkRunning}\n onChange={event =>\n toggleRow(\n index,\n event.currentTarget.checked,\n (event.nativeEvent as MouseEvent).shiftKey === true,\n current,\n )\n }\n />\n );\n\n const runTarget = (key: string, target: PendingTarget<TEntity>) => {\n if (target.kind === 'row') onUseCase?.(key, target.item);\n else onBulkUseCase?.(key, target.selection);\n };\n\n const invoke = (\n useCase: UseCaseDescriptor,\n target: PendingTarget<TEntity>,\n ) => {\n const confirm = useCase.confirm;\n if (confirm) {\n setPending({\n key: useCase.key,\n labelKey: useCase.labelKey,\n confirm,\n target,\n });\n return;\n }\n runTarget(useCase.key, target);\n };\n\n const rowMenu = (item: TEntity) =>\n hasRowMenu ? (\n <Menu>\n <Menu.Trigger aria-label={t('table.actions')}>\n {OVERFLOW_GLYPH}\n </Menu.Trigger>\n <Menu.Items>\n {rowUseCases.map(useCase => (\n <Menu.Item\n key={useCase.key}\n tone={\n useCase.confirm?.tone === 'destructive'\n ? 'destructive'\n : 'neutral'\n }\n onClick={() => invoke(useCase, { kind: 'row', item })}\n >\n {translateKey(useCase.labelKey)}\n </Menu.Item>\n ))}\n </Menu.Items>\n </Menu>\n ) : undefined;\n\n /**\n * Selection wins over navigation: a picker's row must set the value, not send\n * the user away from the form they came from.\n *\n * The row menu sits *beside* whichever of the two applies rather than\n * replacing it — \"open this record\" and \"run a verb on it\" are both true at\n * once, and folding the first into the menu would bury the action every row\n * of a list exists for.\n */\n const recordAction = (item: TEntity) => {\n const primary = onSelect ? (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => onSelect(item)}\n >\n {t('table.select')}\n </Button>\n ) : hrefFor ? (\n renderLink({\n href: hrefFor(item.id),\n className: linkClassName,\n children: t('table.open'),\n intent: { kind: 'open', id: item.id },\n })\n ) : undefined;\n\n const menu = rowMenu(item);\n if (!menu) return primary;\n\n return (\n <span className=\"flex items-center gap-2xs\">\n {primary}\n {menu}\n </span>\n );\n };\n\n /**\n * \"No records\" is a statement about the data; after a failed load it would be\n * a lie about it. The distinction matters most to the user who is deciding\n * whether their catalog is empty or their backend is down.\n */\n const emptyMessage = error ? t('table.error') : t('table.empty');\n\n return (\n <div className=\"flex flex-col gap-s\">\n {/*\n One announcement for the whole table, not one per layout. Both the grid\n and the card list are always in the DOM — CSS picks which is visible —\n so a live region inside each meant assistive tech was told twice that\n the table was loading. The shimmer itself is aria-hidden, so this is the\n only thing that carries the news.\n */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {isFirstLoad ? t('table.loading') : ''}\n </span>\n\n {showControls && (\n <TableToolbar\n start={\n <>\n {newHref &&\n renderLink({\n href: newHref,\n className: linkClassName,\n children: t('table.new'),\n intent: { kind: 'new' },\n })}\n {/* Collection-bound verbs that need no selection. They sit with\n `New` rather than in the bulk bar, because the bulk bar only\n exists while rows are ticked and these are always available. */}\n <LoadingBoundary isLoading={isMetadataLoading} lines={0}>\n <>\n {toolbarUseCases.map(useCase => (\n <Button\n key={useCase.key}\n type=\"button\"\n variant={\n useCase.confirm?.tone === 'destructive'\n ? 'destructive'\n : 'secondary'\n }\n size=\"sm\"\n disabled={isBulkRunning}\n onClick={() =>\n onBulkUseCase?.(useCase.key, everythingMatching)\n }\n >\n {translateKey(useCase.labelKey)}\n </Button>\n ))}\n </>\n </LoadingBoundary>\n {slots.toolbar}\n </>\n }\n end={\n <>\n <Button\n type=\"button\"\n variant={panel === 'filters' ? 'primary' : 'secondary'}\n size=\"sm\"\n onClick={() =>\n setPanel(current =>\n current === 'filters' ? 'none' : 'filters',\n )\n }\n >\n {t('table.filters')}\n </Button>\n <Button\n type=\"button\"\n variant={panel === 'sorting' ? 'primary' : 'secondary'}\n size=\"sm\"\n onClick={() =>\n setPanel(current =>\n current === 'sorting' ? 'none' : 'sorting',\n )\n }\n >\n {t('table.sorting')}\n </Button>\n <ColumnSettings\n columns={columns}\n hidden={hidden}\n onChange={setPersonalization}\n />\n </>\n }\n panel={\n panel === 'filters' ? (\n <FilterBuilder<TEntity>\n descriptors={columns.filter(column => column.filterable)}\n value={filtering}\n onChange={(applied: FilterGroup<TEntity>) =>\n onFilteringChange?.(applied)\n }\n />\n ) : panel === 'sorting' ? (\n <SortBuilder<TEntity>\n descriptors={columns.filter(column => column.sortable)}\n value={sorting}\n onChange={(applied: EntitySorting<TEntity>) =>\n onSortingChange?.(applied)\n }\n />\n ) : undefined\n }\n />\n )}\n\n {/*\n The bar appears when there is something selected and something to do\n with it. It sits above the rows rather than floating over them: a fixed\n overlay covers the last row of a short page, which is the row the user\n most likely just ticked.\n */}\n {selectionState && selectionSize(selectionState) > 0 && (\n <BulkActionBar\n count={selectionSize(selectionState)}\n useCases={bulkUseCases}\n busy={isBulkRunning}\n // Offered only when this page is fully ticked and the store holds\n // more than the page — the escalation has somewhere to escalate to.\n matchingTotal={\n selectionState.mode === 'ids' &&\n allOnPageSelected &&\n totalItems > items.length\n ? totalItems\n : undefined\n }\n onSelectAllMatching={() => setSelection(everythingMatching)}\n onClear={() => setSelection(emptySelection<TEntity>())}\n onUseCase={useCase =>\n invoke(useCase, { kind: 'selection', selection: selectionState })\n }\n />\n )}\n\n {pending && (\n <ConfirmDialog\n open\n tone={pending.confirm.tone}\n title={translateKey(pending.labelKey)}\n message={translateKey(pending.confirm.messageKey)}\n busy={isBulkRunning}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n const { key, target } = pending;\n setPending(undefined);\n runTarget(key, target);\n }}\n />\n )}\n\n {bulkOutcomes && bulkOutcomes.length > 0 && onBulkDismiss && (\n <BulkResult\n outcomes={bulkOutcomes}\n busy={isBulkRunning}\n labelFor={id => {\n const row = items.find(item => item.id === id);\n return row ? rowLabel(row) : undefined;\n }}\n onRetryFailed={onBulkRetry}\n onDismiss={onBulkDismiss}\n />\n )}\n\n {/*\n Rendered once, outside both layouts, and independently of whether there\n are rows: a refetch that fails leaves the previous page on screen, and\n without this the user would read stale data as current.\n */}\n {error && (\n <p\n role=\"alert\"\n data-testid=\"entity-table-error\"\n className=\"rounded-sm border border-danger bg-danger-subtle px-s py-2xs text-step-sm text-danger\"\n >\n {errorMessage(error)}\n </p>\n )}\n\n {/* Wide viewports: a grid. */}\n <div className={`${pivot.grid} ${busyClass}`}>\n <Table>\n <TableHead>\n {slots.header ? (\n slots.header.render(visibleColumns)\n ) : (\n <tr>\n {selectable && (\n <TableHeaderCell>\n <Checkbox\n aria-label={t('table.selectAllOnPage')}\n checked={allOnPageSelected}\n indeterminate={someOnPageSelected}\n disabled={isBulkRunning || items.length === 0}\n onChange={event =>\n toggleAllOnPage(\n event.currentTarget.checked,\n selectionState,\n )\n }\n />\n </TableHeaderCell>\n )}\n {visibleColumns.map(column => (\n <TableHeaderCell key={column.name}>\n {column.header ?? column.label}\n </TableHeaderCell>\n ))}\n {hasRowAction && (\n <TableHeaderCell>{t('table.actions')}</TableHeaderCell>\n )}\n </tr>\n )}\n </TableHead>\n <TableBody aria-busy={isLoading || undefined}>\n {/* Skeleton rows rather than the word \"Loading\", and one per column\n rather than a single full-width blob: the placeholder occupies\n the real grid, so the swap to rows shifts nothing sideways.\n `TableMessageRow` is deliberately bypassed — it is one centred\n full-span cell and cannot express the geometry. */}\n {showSkeleton &&\n (customSkeleton ? (\n <TableMessageRow colSpan={columnCount}>\n {customSkeleton}\n </TableMessageRow>\n ) : (\n skeletonRows.map(row => (\n <TableRow key={row}>\n {selectable && (\n <TableCell>\n <Skeleton shape=\"line\" className=\"w-4\" />\n </TableCell>\n )}\n {visibleColumns.map(column => (\n <TableCell key={column.name}>\n <Skeleton shape=\"line\" className=\"w-full\" />\n </TableCell>\n ))}\n {hasRowAction && (\n <TableCell>\n <Skeleton shape=\"line\" className=\"w-16\" />\n </TableCell>\n )}\n </TableRow>\n ))\n ))}\n {!isLoading && items.length === 0 && (\n <TableMessageRow colSpan={columnCount}>\n {emptyMessage}\n </TableMessageRow>\n )}\n {shown.map(({ item, depth, hasChildren }, index) =>\n slots.row ? (\n // A Fragment carries the key without wrapping the custom row in\n // an element `<tbody>` would reject.\n <Fragment key={String(item.id ?? index)}>\n {slots.row.render(item, visibleColumns)}\n </Fragment>\n ) : (\n <TableRow key={String(item.id ?? index)}>\n {selectionState && (\n <TableCell>\n {selectionCell(item, index, selectionState)}\n </TableCell>\n )}\n {visibleColumns.map((column, columnIndex) => (\n <TableCell key={column.name}>\n {treeKeys !== undefined && columnIndex === 0 ? (\n <TreeCell\n depth={depth}\n hasChildren={hasChildren}\n expanded={!collapsed.has(treeKeys.idOf(item))}\n label={t(\n collapsed.has(treeKeys.idOf(item))\n ? 'table.expandRow'\n : 'table.collapseRow',\n { record: rowLabel(item) },\n )}\n onToggle={() => toggleCollapsed(treeKeys.idOf(item))}\n >\n {renderCell(column, item)}\n </TreeCell>\n ) : (\n renderCell(column, item)\n )}\n </TableCell>\n ))}\n {hasRowAction && <TableCell>{recordAction(item)}</TableCell>}\n </TableRow>\n ),\n )}\n </TableBody>\n </Table>\n </div>\n\n {/* Narrow viewports: the same columns pivoted into cards. */}\n <div\n className={`${pivot.cards} flex flex-col gap-2xs ${busyClass}`}\n aria-busy={isLoading || undefined}\n >\n {/* The card pivot gets card-shaped placeholders, not a bare shimmer\n block: the border, radius and padding are the geometry here. */}\n {showSkeleton &&\n (customSkeleton ??\n skeletonRows.map(row => (\n <EntityRecordCard\n key={row}\n columns={visibleColumns}\n renderCell={() => <Skeleton shape=\"line\" className=\"w-24\" />}\n />\n )))}\n {!isLoading && items.length === 0 && (\n <p className=\"text-step-sm text-content-muted\">{emptyMessage}</p>\n )}\n {shown.map(({ item }, index) => (\n <EntityRecordCard\n key={String(item.id ?? index)}\n columns={visibleColumns}\n // The card is handed the resolved columns themselves, so the\n // narrow layout goes through the exact same renderers as the grid.\n renderCell={column =>\n renderCell(column as EntityTableColumn<TEntity>, item)\n }\n leading={\n selectionState\n ? selectionCell(item, index, selectionState)\n : undefined\n }\n actions={recordAction(item)}\n />\n ))}\n </div>\n\n <Pagination\n currentPage={currentPage}\n pageSize={pageSize}\n totalItems={totalItems}\n onPageChange={onPageChange}\n onPageSizeChange={onPageSizeChange}\n />\n\n {slots.rest}\n </div>\n );\n}\n"],"names":["emptySelection","extractMetaEntity","hierarchyOf","isSelected","selectionSize","toggleSelected","Fragment","useMemo","useRef","useState","useErrorMessage","useT","useTranslateKey","OVERFLOW_GLYPH","renderAnchor","useCasesForSurface","Button","CellValue","Checkbox","Skeleton","Table","TableBody","TableCell","TableHead","TableHeaderCell","TableMessageRow","TableRow","linkClassName","BulkActionBar","BulkResult","ColumnSettings","ConfirmDialog","EntityRecordCard","FilterBuilder","LoadingBoundary","Menu","Pagination","SortBuilder","TableToolbar","readEntityTableSlots","shownTreeRows","TreeCell","treeRows","useEntityTableColumns","PIVOT_CLASS","sm","grid","cards","md","lg","SKELETON_ROW_CAP","EntityTable","entityConstructor","isLoading","skeleton","error","items","totalItems","currentPage","pageSize","onPageChange","onPageSizeChange","hrefFor","newHref","renderLink","onSelect","preferencesKey","showControls","pivotBreakpoint","filtering","sorting","onFilteringChange","onSortingChange","selection","onSelectionChange","metadata","isMetadataLoading","onBulkUseCase","onUseCase","bulkOutcomes","onBulkDismiss","onBulkRetry","isBulkRunning","children","t","translateKey","errorMessage","metaEntity","slots","panel","setPanel","pending","setPending","undefined","columns","visibleColumns","hidden","setPersonalization","key","name","pivot","Error","selectionState","selectable","rowUseCases","useCases","bulkUseCases","toolbarUseCases","hasRowMenu","length","everythingMatching","mode","total","excluded","Set","hasRowAction","columnCount","isFirstLoad","isRefetching","customSkeleton","showSkeleton","skeletonRows","Array","from","Math","min","_","index","busyClass","renderCell","column","item","render","value","descriptor","anchorIndex","treeKeys","collapsed","setCollapsed","rows","shown","shownItems","map","row","toggleCollapsed","current","next","has","delete","add","setSelection","toggleRow","checked","shiftKey","start","end","slice","id","pageIds","selectedOnPage","filter","allOnPageSelected","someOnPageSelected","toggleAllOnPage","rowLabel","named","find","type","String","selectionCell","aria-label","record","disabled","onChange","event","currentTarget","nativeEvent","runTarget","target","kind","invoke","useCase","confirm","labelKey","rowMenu","Trigger","Items","Item","tone","onClick","recordAction","primary","variant","size","href","className","intent","menu","span","emptyMessage","div","role","aria-live","lines","toolbar","descriptors","filterable","applied","sortable","count","busy","matchingTotal","onSelectAllMatching","onClear","open","title","message","messageKey","onCancel","onConfirm","outcomes","labelFor","onRetryFailed","onDismiss","p","data-testid","header","tr","indeterminate","label","aria-busy","colSpan","shape","depth","hasChildren","columnIndex","expanded","idOf","onToggle","leading","actions","rest"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { BulkOutcome, EntifixError, Entity, EntityConstructor, EntityFieldDescriptor, EntityId, EntityMetadataDocument, EntitySelection, EntitySorting, FilterGroup } from '@entifix/core';
1
+ import type { BulkOutcome, Entity, EntityConstructor, EntityFieldDescriptor, EntityId, EntityMetadataDocument, EntitySelection, EntitySorting, FilterGroup, TransitionalFailure } from '@entifix/core';
2
2
  import type { ReactNode } from 'react';
3
3
  import type { RenderLink } from '../../actions/index.js';
4
4
  /**
@@ -43,7 +43,7 @@ export interface EntityTableProps<TEntity extends Entity> {
43
43
  * It arrives for free from `useDataLoading`, whose whole state is spread into
44
44
  * this component.
45
45
  */
46
- error?: EntifixError;
46
+ error?: TransitionalFailure;
47
47
  items: ReadonlyArray<TEntity>;
48
48
  totalItems: number;
49
49
  currentPage: number;
@@ -1 +1 @@
1
- {"version":3,"file":"entity-table.types.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-table/entity-table.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"entity-table.types.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-table/entity-table.types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/organisms/entity-table/entity-table.types.ts"],"sourcesContent":["import type {\n BulkOutcome,\n EntifixError,\n Entity,\n EntityConstructor,\n EntityFieldDescriptor,\n EntityId,\n EntityMetadataDocument,\n EntitySelection,\n EntitySorting,\n FilterGroup,\n} from '@entifix/core';\nimport type { ReactNode } from 'react';\n\nimport type { RenderLink } from '../../actions/index.js';\n\n/**\n * A column as the table finally renders it: the metadata descriptor plus the\n * per-column overrides a caller supplied through an `<EntityColumn>` slot.\n */\nexport interface EntityTableColumn<\n TEntity extends Entity,\n> extends EntityFieldDescriptor {\n /** Replaces the default header text. */\n header?: ReactNode;\n /** Replaces the default `CellValue` rendering for this column. */\n render?: (item: TEntity) => ReactNode;\n /** True when the column has no backing entity member (slot-only column). */\n virtual?: boolean;\n}\n\n/** Breakpoint at and above which the table renders as a grid, not as cards. */\nexport type EntityTablePivotBreakpoint = 'sm' | 'md' | 'lg';\n\nexport interface EntityTableProps<TEntity extends Entity> {\n /** Metadata source. Columns, labels, types and the filter/sort controls all\n * derive from this constructor's accessors. */\n entityConstructor: EntityConstructor<TEntity>;\n\n isLoading: boolean;\n /**\n * What holds the table's shape during the first load.\n *\n * `true` (the default) renders the built-in placeholder, which is derived from\n * the table's own resolved geometry — one shimmer cell per visible column, so\n * the swap to real rows shifts nothing. A node replaces that default; `false`\n * renders no placeholder at all.\n *\n * Only the *first* load is held this way. A refetch that already has rows\n * keeps them and marks the region busy instead — replacing populated rows with\n * shimmer on every pagination click is a grey flash, not a loading state.\n */\n skeleton?: boolean | ReactNode;\n /**\n * The failure of the last load, if it failed.\n *\n * A listing that renders an empty table when the service is unreachable tells\n * the user their catalog is empty — the one thing they must not conclude. So\n * this is rendered as an alert, and it also changes the empty message.\n *\n * It arrives for free from `useDataLoading`, whose whole state is spread into\n * this component.\n */\n error?: EntifixError;\n items: ReadonlyArray<TEntity>;\n totalItems: number;\n currentPage: number;\n pageSize: number;\n onPageChange: (newPage: number) => void;\n onPageSizeChange?: (newPageSize: number) => void;\n\n /**\n * Builds the link to a row's record. Supplied as a function rather than the\n * table routing by itself: this package must not depend on a router, so the\n * caller (a shell page, which does know one) owns the URL shape.\n *\n * Omit it and the actions column is not rendered at all.\n */\n hrefFor?: (id: EntityId) => string;\n /** Link to the create form. Omit and no `New` action is rendered. */\n newHref?: string;\n /**\n * Renders the `Open` and `New` links. Defaults to a plain anchor, which a\n * client-routed host experiences as a full document load — pass one built on\n * the host's own link component. See {@link RenderLink}.\n */\n renderLink?: RenderLink;\n\n /**\n * Picks a row instead of navigating to it — how a link picker reuses this\n * table as its browse dialog.\n *\n * Takes precedence over {@link hrefFor}: inside a picker the user is choosing\n * a value, and offering to navigate away from the form they are editing is the\n * wrong affordance. Either one renders the actions column.\n */\n onSelect?: (item: TEntity) => void;\n\n /** Scope for persisted personalization. Defaults to the entity's key. */\n preferencesKey?: string;\n /** Hide the toolbar (columns, filters, sorting) — a bare listing. */\n showControls?: boolean;\n /** Viewport width at which rows stop pivoting into cards. Default `md`. */\n pivotBreakpoint?: EntityTablePivotBreakpoint;\n\n /** The filtering currently applied, seeding the filter panel so it shows\n * what is in effect rather than an empty form. */\n filtering?: FilterGroup<TEntity>;\n /** The sorting currently applied, seeding the sort panel. */\n sorting?: EntitySorting<TEntity>;\n\n /** Notified when the filter panel is **applied** — not while it is edited, so\n * the value can feed a load request directly. */\n onFilteringChange?: (filtering: FilterGroup<TEntity>) => void;\n /** Notified when the sort panel is applied. */\n onSortingChange?: (sorting: EntitySorting<TEntity>) => void;\n\n /**\n * The rows a collection-bound action would act on.\n *\n * **Controlled from above the table**, exactly as `filtering` and `sorting`\n * are, and for the same reason plus one more: a selection has to survive\n * pagination, and the page owns the pager. A selection held inside the table\n * would be reset by the very navigation the user performs to add to it.\n *\n * Omit it and no selection column is rendered — a listing that is only read\n * gains nothing from a column of empty boxes.\n */\n selection?: EntitySelection<TEntity>;\n onSelectionChange?: (selection: EntitySelection<TEntity>) => void;\n\n /**\n * What this caller may do with the entity, as the service decided it.\n *\n * The same document `EntityForm` takes. The table reads only the\n * `collection`-bound half of `useCases` — the entity-bound verbs belong to a\n * record, and a row's own overflow menu is fed from the\n * `context-dependent` ones.\n *\n * Absent keeps the pre-ADR-0026 behaviour: no bulk bar and no row menu, which\n * is what every un-migrated call site renders today.\n */\n metadata?: EntityMetadataDocument;\n isMetadataLoading?: boolean;\n\n /** Runs a `collection`-bound verb over {@link selection}. */\n onBulkUseCase?: (key: string, selection: EntitySelection<TEntity>) => void;\n /** Runs an `entity`-bound, `context-dependent` verb on one row. */\n onUseCase?: (key: string, item: TEntity) => void;\n\n /** The outcome of the last bulk run, rendered per row until dismissed. */\n bulkOutcomes?: readonly BulkOutcome[];\n onBulkDismiss?: () => void;\n onBulkRetry?: (ids: EntityId[]) => void;\n /** A bulk run is in flight — every verb is disabled while it is. */\n isBulkRunning?: boolean;\n\n /** Customization slots — see `entity-table-slots`. */\n children?: ReactNode;\n}\n"],"names":[],"mappings":"AAkCA,WA6HC"}
1
+ {"version":3,"sources":["../../../../src/ui/organisms/entity-table/entity-table.types.ts"],"sourcesContent":["import type {\n BulkOutcome,\n Entity,\n EntityConstructor,\n EntityFieldDescriptor,\n EntityId,\n EntityMetadataDocument,\n EntitySelection,\n EntitySorting,\n FilterGroup,\n TransitionalFailure,\n} from '@entifix/core';\nimport type { ReactNode } from 'react';\n\nimport type { RenderLink } from '../../actions/index.js';\n\n/**\n * A column as the table finally renders it: the metadata descriptor plus the\n * per-column overrides a caller supplied through an `<EntityColumn>` slot.\n */\nexport interface EntityTableColumn<\n TEntity extends Entity,\n> extends EntityFieldDescriptor {\n /** Replaces the default header text. */\n header?: ReactNode;\n /** Replaces the default `CellValue` rendering for this column. */\n render?: (item: TEntity) => ReactNode;\n /** True when the column has no backing entity member (slot-only column). */\n virtual?: boolean;\n}\n\n/** Breakpoint at and above which the table renders as a grid, not as cards. */\nexport type EntityTablePivotBreakpoint = 'sm' | 'md' | 'lg';\n\nexport interface EntityTableProps<TEntity extends Entity> {\n /** Metadata source. Columns, labels, types and the filter/sort controls all\n * derive from this constructor's accessors. */\n entityConstructor: EntityConstructor<TEntity>;\n\n isLoading: boolean;\n /**\n * What holds the table's shape during the first load.\n *\n * `true` (the default) renders the built-in placeholder, which is derived from\n * the table's own resolved geometry — one shimmer cell per visible column, so\n * the swap to real rows shifts nothing. A node replaces that default; `false`\n * renders no placeholder at all.\n *\n * Only the *first* load is held this way. A refetch that already has rows\n * keeps them and marks the region busy instead — replacing populated rows with\n * shimmer on every pagination click is a grey flash, not a loading state.\n */\n skeleton?: boolean | ReactNode;\n /**\n * The failure of the last load, if it failed.\n *\n * A listing that renders an empty table when the service is unreachable tells\n * the user their catalog is empty — the one thing they must not conclude. So\n * this is rendered as an alert, and it also changes the empty message.\n *\n * It arrives for free from `useDataLoading`, whose whole state is spread into\n * this component.\n */\n error?: TransitionalFailure;\n items: ReadonlyArray<TEntity>;\n totalItems: number;\n currentPage: number;\n pageSize: number;\n onPageChange: (newPage: number) => void;\n onPageSizeChange?: (newPageSize: number) => void;\n\n /**\n * Builds the link to a row's record. Supplied as a function rather than the\n * table routing by itself: this package must not depend on a router, so the\n * caller (a shell page, which does know one) owns the URL shape.\n *\n * Omit it and the actions column is not rendered at all.\n */\n hrefFor?: (id: EntityId) => string;\n /** Link to the create form. Omit and no `New` action is rendered. */\n newHref?: string;\n /**\n * Renders the `Open` and `New` links. Defaults to a plain anchor, which a\n * client-routed host experiences as a full document load — pass one built on\n * the host's own link component. See {@link RenderLink}.\n */\n renderLink?: RenderLink;\n\n /**\n * Picks a row instead of navigating to it — how a link picker reuses this\n * table as its browse dialog.\n *\n * Takes precedence over {@link hrefFor}: inside a picker the user is choosing\n * a value, and offering to navigate away from the form they are editing is the\n * wrong affordance. Either one renders the actions column.\n */\n onSelect?: (item: TEntity) => void;\n\n /** Scope for persisted personalization. Defaults to the entity's key. */\n preferencesKey?: string;\n /** Hide the toolbar (columns, filters, sorting) — a bare listing. */\n showControls?: boolean;\n /** Viewport width at which rows stop pivoting into cards. Default `md`. */\n pivotBreakpoint?: EntityTablePivotBreakpoint;\n\n /** The filtering currently applied, seeding the filter panel so it shows\n * what is in effect rather than an empty form. */\n filtering?: FilterGroup<TEntity>;\n /** The sorting currently applied, seeding the sort panel. */\n sorting?: EntitySorting<TEntity>;\n\n /** Notified when the filter panel is **applied** — not while it is edited, so\n * the value can feed a load request directly. */\n onFilteringChange?: (filtering: FilterGroup<TEntity>) => void;\n /** Notified when the sort panel is applied. */\n onSortingChange?: (sorting: EntitySorting<TEntity>) => void;\n\n /**\n * The rows a collection-bound action would act on.\n *\n * **Controlled from above the table**, exactly as `filtering` and `sorting`\n * are, and for the same reason plus one more: a selection has to survive\n * pagination, and the page owns the pager. A selection held inside the table\n * would be reset by the very navigation the user performs to add to it.\n *\n * Omit it and no selection column is rendered — a listing that is only read\n * gains nothing from a column of empty boxes.\n */\n selection?: EntitySelection<TEntity>;\n onSelectionChange?: (selection: EntitySelection<TEntity>) => void;\n\n /**\n * What this caller may do with the entity, as the service decided it.\n *\n * The same document `EntityForm` takes. The table reads only the\n * `collection`-bound half of `useCases` — the entity-bound verbs belong to a\n * record, and a row's own overflow menu is fed from the\n * `context-dependent` ones.\n *\n * Absent keeps the pre-ADR-0026 behaviour: no bulk bar and no row menu, which\n * is what every un-migrated call site renders today.\n */\n metadata?: EntityMetadataDocument;\n isMetadataLoading?: boolean;\n\n /** Runs a `collection`-bound verb over {@link selection}. */\n onBulkUseCase?: (key: string, selection: EntitySelection<TEntity>) => void;\n /** Runs an `entity`-bound, `context-dependent` verb on one row. */\n onUseCase?: (key: string, item: TEntity) => void;\n\n /** The outcome of the last bulk run, rendered per row until dismissed. */\n bulkOutcomes?: readonly BulkOutcome[];\n onBulkDismiss?: () => void;\n onBulkRetry?: (ids: EntityId[]) => void;\n /** A bulk run is in flight — every verb is disabled while it is. */\n isBulkRunning?: boolean;\n\n /** Customization slots — see `entity-table-slots`. */\n children?: ReactNode;\n}\n"],"names":[],"mappings":"AAkCA,WA6HC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/react-controls",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
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.8.0"
22
+ "@entifix/core": "0.10.0"
23
23
  },
24
24
  "exports": {
25
25
  "./package.json": "./package.json",
@@ -48,13 +48,13 @@
48
48
  "@storybook/addon-docs": "^10.6.2",
49
49
  "@storybook/addon-themes": "^10.6.2",
50
50
  "@storybook/react-vite": "^10.6.2",
51
- "effect": "4.0.5",
51
+ "effect": "4.0.6",
52
52
  "i18next": "26.5.3",
53
53
  "react": "19.3.1",
54
54
  "react-i18next": "17.1.0",
55
55
  "storybook": "^10.6.2",
56
- "@entifix/i18n": "0.8.0",
57
- "@entifix/style": "0.8.0"
56
+ "@entifix/i18n": "0.10.0",
57
+ "@entifix/style": "0.10.0"
58
58
  },
59
59
  "nx": {
60
60
  "tags": [
@@ -93,11 +93,11 @@
93
93
  }
94
94
  },
95
95
  "peerDependencies": {
96
- "effect": "^4.0.5",
96
+ "effect": "^4.0.6",
97
97
  "i18next": ">=26 <27",
98
98
  "react": ">=19 <20",
99
99
  "react-i18next": ">=17 <18",
100
- "@entifix/i18n": "0.8.0"
100
+ "@entifix/i18n": "0.10.0"
101
101
  },
102
102
  "peerDependenciesMeta": {
103
103
  "@entifix/i18n": {