@cosmicdrift/kumiko-renderer 0.386.0 → 0.387.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 (185) hide show
  1. package/LICENSE +21 -57
  2. package/dist/app/action-form-body.d.ts +2 -2
  3. package/dist/app/action-form-body.js +2 -2
  4. package/dist/app/action-form-shim.d.ts +8 -8
  5. package/dist/app/action-form-shim.js +20 -22
  6. package/dist/app/app-features-context.js +4 -4
  7. package/dist/app/column-renderers.d.ts +7 -9
  8. package/dist/app/column-renderers.js +15 -18
  9. package/dist/app/config-edit-body.js +1 -1
  10. package/dist/app/config-edit-shim.d.ts +7 -8
  11. package/dist/app/config-edit-shim.js +18 -20
  12. package/dist/app/custom-screens.d.ts +3 -3
  13. package/dist/app/custom-screens.js +8 -8
  14. package/dist/app/dashboard-body.d.ts +2 -2
  15. package/dist/app/dashboard-body.js +2 -2
  16. package/dist/app/entity-edit-prefill.js +9 -9
  17. package/dist/app/entity-edit-screen.js +4 -4
  18. package/dist/app/entity-list-screen.js +3 -3
  19. package/dist/app/extension-form-submit.d.ts +1 -1
  20. package/dist/app/extension-form-submit.js +14 -15
  21. package/dist/app/extension-sections.d.ts +24 -25
  22. package/dist/app/extension-sections.js +10 -10
  23. package/dist/app/feature-schema.js +11 -11
  24. package/dist/app/form-schema.js +1 -1
  25. package/dist/app/header-actions-bar.js +1 -1
  26. package/dist/app/kumiko-screen.js +8 -10
  27. package/dist/app/layout-fields.js +1 -1
  28. package/dist/app/list-facets.js +3 -3
  29. package/dist/app/nav.d.ts +10 -10
  30. package/dist/app/nav.js +13 -13
  31. package/dist/app/projection-detail-body.js +4 -4
  32. package/dist/app/projection-detail-shim.d.ts +6 -5
  33. package/dist/app/projection-detail-shim.js +23 -22
  34. package/dist/app/projection-list-body.js +10 -10
  35. package/dist/app/projection-list-shim.d.ts +5 -5
  36. package/dist/app/projection-list-shim.js +17 -18
  37. package/dist/app/qn.js +10 -12
  38. package/dist/app/qualify-screen-id.js +1 -1
  39. package/dist/app/screen-access.js +2 -2
  40. package/dist/app/screen-shared.js +1 -1
  41. package/dist/app/secret-mint-body.js +4 -4
  42. package/dist/app/write-failed-error.js +8 -7
  43. package/dist/components/embedded-list-field.js +4 -4
  44. package/dist/components/grid-cell-for-field.d.ts +1 -1
  45. package/dist/components/reference-create-dialog.js +3 -3
  46. package/dist/components/related-list-section.d.ts +1 -1
  47. package/dist/components/related-list-section.js +1 -1
  48. package/dist/components/render-edit-logic.js +1 -1
  49. package/dist/components/render-edit-types.d.ts +6 -6
  50. package/dist/components/render-edit.js +63 -65
  51. package/dist/components/render-field.d.ts +7 -7
  52. package/dist/components/render-field.js +57 -59
  53. package/dist/components/render-list.d.ts +42 -43
  54. package/dist/components/render-list.js +63 -67
  55. package/dist/components/use-wizard-form-draft.js +10 -10
  56. package/dist/context/dispatcher-context.js +5 -5
  57. package/dist/context/user-roles-context.js +2 -2
  58. package/dist/headless/apex/index.d.ts +1 -2
  59. package/dist/headless/apex/index.js +5 -6
  60. package/dist/headless/form/form-controller.js +6 -6
  61. package/dist/headless/form/zod-bridge.js +2 -2
  62. package/dist/headless/format/money.js +1 -1
  63. package/dist/headless/index.js +2 -2
  64. package/dist/headless/view-model/edit.js +2 -2
  65. package/dist/headless/view-model/list.js +3 -4
  66. package/dist/headless/view-model/types.d.ts +3 -3
  67. package/dist/hooks/reference-limits.js +11 -12
  68. package/dist/hooks/use-list-url-state.d.ts +13 -13
  69. package/dist/hooks/use-list-url-state.js +21 -22
  70. package/dist/hooks/use-query.js +3 -3
  71. package/dist/hooks/use-reference-lookup.js +16 -17
  72. package/dist/hooks/use-store.js +18 -22
  73. package/dist/hooks/use-stream-handler.js +4 -2
  74. package/dist/i18n-defaults.js +2 -8
  75. package/dist/i18n.d.ts +10 -10
  76. package/dist/i18n.js +25 -25
  77. package/dist/index.js +5 -4
  78. package/dist/live/dispatcher-live.js +2 -2
  79. package/dist/primitives.d.ts +225 -221
  80. package/dist/primitives.js +1 -1
  81. package/dist/sse/live-events.d.ts +5 -5
  82. package/dist/sse/live-events.js +17 -17
  83. package/dist/tokens.d.ts +8 -8
  84. package/dist/tokens.js +16 -17
  85. package/dist/web/app/browser-locale.d.ts +9 -9
  86. package/dist/web/app/browser-locale.js +19 -19
  87. package/dist/web/app/client-plugin.d.ts +44 -52
  88. package/dist/web/app/client-plugin.js +4 -5
  89. package/dist/web/app/create-app.d.ts +28 -31
  90. package/dist/web/app/create-app.js +21 -22
  91. package/dist/web/app/document-lang-sync.js +1 -1
  92. package/dist/web/app/draft-storage.js +1 -1
  93. package/dist/web/app/nav-providers-context.d.ts +3 -3
  94. package/dist/web/app/nav-providers-context.js +3 -3
  95. package/dist/web/app/nav.d.ts +18 -17
  96. package/dist/web/app/nav.js +66 -64
  97. package/dist/web/app/rich-content-editor.js +1 -1
  98. package/dist/web/components/config-cascade.js +17 -18
  99. package/dist/web/components/config-source-badge.js +4 -4
  100. package/dist/web/layout/app-layout.d.ts +9 -9
  101. package/dist/web/layout/app-layout.js +2 -2
  102. package/dist/web/layout/avatar.d.ts +4 -3
  103. package/dist/web/layout/avatar.js +15 -17
  104. package/dist/web/layout/default-app-shell.d.ts +22 -23
  105. package/dist/web/layout/editor-panel.js +3 -3
  106. package/dist/web/layout/nav-tree.js +86 -84
  107. package/dist/web/layout/profile-menu.d.ts +1 -1
  108. package/dist/web/layout/sidebar-brand.d.ts +7 -7
  109. package/dist/web/layout/sidebar-user.d.ts +1 -1
  110. package/dist/web/layout/sidebar-user.js +4 -4
  111. package/dist/web/layout/sidebar.d.ts +8 -8
  112. package/dist/web/layout/sidebar.js +7 -7
  113. package/dist/web/layout/target-resolver-stub.d.ts +5 -5
  114. package/dist/web/layout/target-resolver-stub.js +20 -20
  115. package/dist/web/layout/target-url.d.ts +7 -7
  116. package/dist/web/layout/target-url.js +22 -22
  117. package/dist/web/layout/theme-toggle.js +4 -5
  118. package/dist/web/layout/workspace-shell.d.ts +21 -21
  119. package/dist/web/layout/workspace-shell.js +29 -30
  120. package/dist/web/lib/cn.d.ts +4 -4
  121. package/dist/web/lib/cn.js +4 -4
  122. package/dist/web/primitives/action-menu.d.ts +8 -8
  123. package/dist/web/primitives/action-menu.js +14 -13
  124. package/dist/web/primitives/calendar-popover.d.ts +1 -1
  125. package/dist/web/primitives/calendar-popover.js +10 -10
  126. package/dist/web/primitives/card-styles.js +1 -1
  127. package/dist/web/primitives/card.js +2 -2
  128. package/dist/web/primitives/combobox.d.ts +12 -12
  129. package/dist/web/primitives/combobox.js +34 -35
  130. package/dist/web/primitives/data-table-cell.js +2 -2
  131. package/dist/web/primitives/data-table.js +2 -3
  132. package/dist/web/primitives/date-field.js +2 -2
  133. package/dist/web/primitives/date-input.js +4 -4
  134. package/dist/web/primitives/default-form.js +2 -2
  135. package/dist/web/primitives/dropdown-menu.js +14 -15
  136. package/dist/web/primitives/embedded-list-input.js +3 -3
  137. package/dist/web/primitives/facet-filter.js +1 -1
  138. package/dist/web/primitives/field.js +9 -8
  139. package/dist/web/primitives/form.d.ts +1 -1
  140. package/dist/web/primitives/form.js +6 -6
  141. package/dist/web/primitives/input.js +2 -2
  142. package/dist/web/primitives/layout-elements.js +1 -1
  143. package/dist/web/primitives/layout.js +2 -2
  144. package/dist/web/primitives/located-timestamp-input.js +4 -4
  145. package/dist/web/primitives/money-input.js +13 -17
  146. package/dist/web/primitives/pager.js +1 -1
  147. package/dist/web/primitives/row-actions.js +3 -4
  148. package/dist/web/primitives/secret-reveal.js +1 -1
  149. package/dist/web/primitives/timestamp-input.d.ts +9 -9
  150. package/dist/web/primitives/timestamp-input.js +21 -21
  151. package/dist/web/primitives/toast.js +12 -13
  152. package/dist/web/sse/live-events.d.ts +6 -6
  153. package/dist/web/sse/live-events.js +3 -3
  154. package/dist/web/tokens.d.ts +11 -12
  155. package/dist/web/tokens.js +35 -37
  156. package/dist/web/version/update-checker.js +14 -13
  157. package/dist/web/widgets/ai-text-field.js +1 -1
  158. package/dist/web/widgets/charts.d.ts +10 -10
  159. package/dist/web/widgets/charts.js +8 -8
  160. package/dist/web/widgets/collapsible-section.d.ts +3 -3
  161. package/dist/web/widgets/collapsible-section.js +6 -6
  162. package/dist/web/widgets/detail-list.d.ts +1 -3
  163. package/dist/web/widgets/detail-list.js +1 -3
  164. package/dist/web/widgets/feed-list.d.ts +2 -2
  165. package/dist/web/widgets/feed-list.js +2 -2
  166. package/dist/web/widgets/form-fields.d.ts +14 -14
  167. package/dist/web/widgets/form-fields.js +14 -13
  168. package/dist/web/widgets/index.js +4 -4
  169. package/dist/web/widgets/infinity-list.js +3 -3
  170. package/dist/web/widgets/mode-switch.d.ts +2 -2
  171. package/dist/web/widgets/mode-switch.js +2 -2
  172. package/dist/web/widgets/progress-list.d.ts +1 -2
  173. package/dist/web/widgets/progress-list.js +1 -2
  174. package/dist/web/widgets/query-table.d.ts +6 -6
  175. package/dist/web/widgets/result-panel.d.ts +16 -16
  176. package/dist/web/widgets/result-panel.js +10 -10
  177. package/dist/web/widgets/section-card.d.ts +3 -3
  178. package/dist/web/widgets/section-card.js +3 -3
  179. package/dist/web/widgets/stat.d.ts +4 -4
  180. package/dist/web/widgets/stat.js +6 -6
  181. package/dist/web/widgets/states.d.ts +4 -4
  182. package/dist/web/widgets/states.js +4 -4
  183. package/dist/web/widgets/use-draft.d.ts +7 -7
  184. package/dist/web/widgets/use-draft.js +7 -7
  185. package/package.json +6 -6
@@ -1,7 +1,7 @@
1
1
  // Currency decimal places — covers the world's zero- and three-decimal
2
2
  // outliers, defaults to 2 for everything else. Shared between renderer/web's
3
3
  // MoneyInput (minor-unit display factor) and RenderField's minor/major-unit
4
- // conversion (kumiko-framework#1923) — a single source keeps both sides
4
+ // conversion — a single source keeps both sides
5
5
  // scaling the same currency the same way.
6
6
  export function currencyDecimals(code) {
7
7
  if (code === "JPY" || code === "KRW" || code === "VND" || code === "ISK")
@@ -1,5 +1,5 @@
1
- // Re-Export aus framework/ui-types damit Renderer-Code denselben
2
- // Parser nutzt wie der Server-Boot-Validator (Cross-Feature-Refs).
1
+ // Re-exports from framework/ui-types so renderer code uses the same
2
+ // parser as the server boot validator (cross-feature references).
3
3
  export { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
4
4
  export { generateRequestId, StreamFrame } from "./dispatcher/index.js";
5
5
  export { createFormController, groupIssuesByPath, I18N_KEY_PARAM, relevantFieldIssues, zodErrorToFieldIssues, } from "./form/index.js";
@@ -52,7 +52,7 @@ function wasEmptiedByWriteExclusion(declaredFieldSpecs, remainingFields) {
52
52
  return declaredFieldSpecs.length > 0 && remainingFields.length === 0;
53
53
  }
54
54
  // relatedList runs its own query — nothing here to resolve against
55
- // entity/values, the spec passes through verbatim (fw#2166).
55
+ // entity/values, the spec passes through verbatim.
56
56
  export function computeRelatedListSectionViewModel(sectionSpec, translate) {
57
57
  return {
58
58
  kind: "relatedList",
@@ -461,7 +461,7 @@ export function computeEditViewModel(input) {
461
461
  const visible = hasVisibleContent(fields, sectionSpec.actions, values);
462
462
  // Tabs mode renders this section as its own panel, where two columns reads
463
463
  // better than the single-column default a stacked form keeps. Applies to
464
- // entityEdit tabs as well since fw#3134, not just projectionDetail.
464
+ // entityEdit tabs as well, not just projectionDetail.
465
465
  const defaultColumns = screen.layout.mode === "tabs" ? 2 : 1;
466
466
  const groups = ("groups" in sectionSpec ? sectionSpec.groups : undefined)
467
467
  ?.map((group) => ({ ...group, fields: group.fields.filter((f) => !isHiddenByExclusion(f)) }))
@@ -108,10 +108,9 @@ export function computeListViewModel(input) {
108
108
  continue;
109
109
  }
110
110
  const label = translate(normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field));
111
- // Tier 2.7e-3 + Cross-Feature: Reference-Field — entity-String
112
- // kann same-feature ("user") oder cross-feature ("users:user")
113
- // sein. parseRefTarget gibt (featureName, entityName), der
114
- // Renderer baut die Lookup-QN als
111
+ // Reference fields may target the same feature ("user") or another feature
112
+ // ("users:user"). parseRefTarget returns the feature and entity names, which
113
+ // the renderer uses to build the lookup query as
115
114
  // `<refFeature>:query:<refEntity>:list`.
116
115
  const refRaw = fieldDef.type === "reference" ? fieldDef.entity : undefined;
117
116
  const refTarget = refRaw !== undefined ? parseRefTarget(refRaw, featureName) : undefined;
@@ -191,7 +191,7 @@ export type EditFieldViewModel = {
191
191
  * entity appears as the display label in the dropdown. Default: "id". */
192
192
  readonly refLabelField?: string;
193
193
  /** Only for `type: "reference"` — query handler QN the picker sources its
194
- * options from (fw#2780), replacing the referenced entity's list handler.
194
+ * options from, replacing the referenced entity's list handler.
195
195
  * Rows carry `{ id, label }`; `refLabelField` is then unused for the
196
196
  * picker but still drives list cells. */
197
197
  readonly refOptionsQuery?: string;
@@ -268,7 +268,7 @@ export type EditFieldViewModel = {
268
268
  /** Only set for a plain (non-list, non-embedded) `type: "money"` field —
269
269
  * same source as embeddedListCurrency (EntityDefinition.defaultCurrency,
270
270
  * falls back to "EUR"). The renderer needs this to build the payload's
271
- * `{amount, currency}` shape (kumiko-framework#1923). */
271
+ * `{amount, currency}` shape. */
272
272
  readonly currency?: string;
273
273
  /** Only set for `type: "number"` when NumberFieldDef.unit is set — a static
274
274
  * suffix string, or a sibling-field reference the renderer resolves against
@@ -292,7 +292,7 @@ export type EditFieldsSectionViewModel = {
292
292
  * `excludeFields`. Unlike a declared-empty section (review step), the
293
293
  * renderer must not give it a tab or wizard step. */
294
294
  readonly emptiedByWriteExclusion?: boolean;
295
- /** Optional — eine titellose Section rendert nur ihre Felder (flache Form). */
295
+ /** Optional — a titleless section renders only its fields (flat form). */
296
296
  readonly title?: string;
297
297
  /** Translated help text under the block heading, from
298
298
  * `EditFieldsSection.description`. Renders even without `title`. */
@@ -1,17 +1,16 @@
1
- // Tier 2.7e Audit-Fix #7: Reference-Lookup-Limits zentral.
1
+ // Reference lookup limits are centralized here.
2
2
  //
3
- // Zwei verschiedene Use-Cases mit unterschiedlichen Default-Limits:
4
- // - Combobox-Edit (single User picks one): 50 reicht weil
5
- // typed-search-Fallback aktiv ist (REMOTE_SEARCH_DEBOUNCE_MS).
6
- // - List-Bulk-Display (Cell-Render für viele Rows): 200 weil wir
7
- // pro Reference-Spalte einmal pro Page laden, nicht pro Cell.
3
+ // Two use cases have different default limits:
4
+ // - Combobox editing (a user picks one): 50 is sufficient because the
5
+ // typed-search fallback is active (REMOTE_SEARCH_DEBOUNCE_MS).
6
+ // - Bulk list display (rendering cells for many rows): 200 because each
7
+ // reference column is loaded once per page, not once per cell.
8
8
  //
9
- // Apps können die Defaults überschreiben indem sie die Konstanten
10
- // hier neu setzen — pro feature/app gibt's heute keine Override-API
11
- // (das wäre eine Erweiterung in createKumikoApp, separater Sprint).
12
- // Die Konstanten leben in einem eigenen Modul damit Future-Author
13
- // sie an einer Stelle findet statt verstreut zwischen render-field
14
- // und use-reference-lookup.
9
+ // Apps can override the defaults by changing these constants, but there is
10
+ // currently no per-feature or per-app override API; adding one would require
11
+ // extending createKumikoApp.
12
+ // The constants live in their own module so future authors can find them in
13
+ // one place instead of searching through render-field and use-reference-lookup.
15
14
  export const REFERENCE_COMBOBOX_LIMIT = 50;
16
15
  export const REFERENCE_LIST_LOOKUP_LIMIT = 200;
17
16
  export const REFERENCE_SEARCH_DEBOUNCE_MS = 300;
@@ -3,33 +3,33 @@ export declare const NO_FILTER_URL_VALUE = "~";
3
3
  export type ListSortDir = DataTableSortDir;
4
4
  export type ListSort = DataTableSort;
5
5
  export type ListUrlState = {
6
- /** Aktive Sortierung (oder null = unsorted, Server liefert Default-Order). */
6
+ /** Active sort (or null for unsorted; the server supplies the default order). */
7
7
  readonly sort: ListSort | null;
8
- /** Search-Term (leer wenn nicht gesetzt). */
8
+ /** Search term (empty when unset). */
9
9
  readonly q: string;
10
- /** 1-basierte Page-Nummer. Bei pagination="infinite" oder false ist
11
- * der Wert ignoriert; Caller liest ihn nur wenn relevant. */
10
+ /** 1-based page number. For pagination="infinite" or false, this value is
11
+ * ignored; callers read it only when relevant. */
12
12
  readonly page: number;
13
13
  /** User-chosen page size from `PAGE_SIZE_OPTIONS`; undefined = screen default. */
14
14
  readonly pageSize: number | undefined;
15
- /** Aktive Faceted-Filter: field → ausgewählte Werte (Strings; Caller
16
- * coerced pro Field-Type). Leer wenn kein Filter gesetzt. */
15
+ /** Active faceted filters: field → selected values (strings; callers coerce
16
+ * them according to the field type). Empty when no filter is set. */
17
17
  readonly filters: Readonly<Record<string, readonly string[]>>;
18
18
  };
19
19
  export type ListUrlStateApi = ListUrlState & {
20
- /** Setzt sort + dir atomar. null = unsorted (löscht beide Keys). */
20
+ /** Atomically sets sort + dir. null means unsorted and clears both keys. */
21
21
  readonly setSort: (next: ListSort | null) => void;
22
- /** Setzt den Search-Term. Empty-String löscht den Key. Caller debounced
23
- * selber (z.B. useDebouncedCallback in der Search-Input-Komponente). */
22
+ /** Sets the search term. An empty string clears the key. Callers must debounce
23
+ * this themselves (e.g. with useDebouncedCallback in the search input component). */
24
24
  readonly setQ: (next: string) => void;
25
- /** Setzt die Page. 1 oder kleiner löscht den Key (Default-Page). */
25
+ /** Sets the page. 1 or less clears the key and restores the default page. */
26
26
  readonly setPage: (next: number) => void;
27
27
  /** Sets the page size and resets the page; undefined clears the key. */
28
28
  readonly setPageSize: (next: number | undefined) => void;
29
- /** Setzt die ausgewählten Werte eines Facet-Felds. Leeres Array löscht
30
- * den Key. Resettet die Page (wie sort/search). */
29
+ /** Sets the selected values for a facet field. An empty array clears the key
30
+ * and resets the page, like sort/search changes. */
31
31
  readonly setFilter: (field: string, values: readonly string[]) => void;
32
- /** Löscht alle aktiven Faceted-Filter (Reset-Button). */
32
+ /** Clears all active faceted filters (for a reset button). */
33
33
  readonly clearFilters: () => void;
34
34
  /** Sets both bounds of a dateRange facet in ONE params update — two
35
35
  * setFilter calls would overwrite each other, since the second still sees
@@ -1,28 +1,29 @@
1
- // useListUrlState — bündelt sort/dir/page/q einer entityList in einen
2
- // per-screen-id namespaced URL-State. Mit Screen-ID-Prefix damit zwei
3
- // Listen auf derselben Route (z.B. Dashboard mit Orders + Incidents)
4
- // nicht über dieselben Query-Keys streiten.
1
+ // useListUrlState — combines an entityList's sort/dir/page/q into a
2
+ // per-screen-id namespaced URL state. The screen-ID prefix prevents two
3
+ // lists on the same route (e.g. a dashboard with Orders + Incidents)
4
+ // from competing over the same query keys.
5
5
  //
6
- // Param-Schema pro Liste:
6
+ // Parameter schema per list:
7
7
  // <screenId>.sort — field name (string)
8
8
  // <screenId>.dir — "asc" | "desc"
9
9
  // <screenId>.q — search term (URL-encoded)
10
- // <screenId>.page — 1-based page number (nur bei pagination="pages")
11
- // <screenId>.f.<field> — Faceted-Filter: comma-joined selected values;
12
- // NO_FILTER_URL_VALUE = "no filter", chosen on purpose
10
+ // <screenId>.page — 1-based page number (only for pagination="pages")
11
+ // <screenId>.f.<field> — faceted filter: comma-joined selected values;
12
+ // NO_FILTER_URL_VALUE = "no filter", chosen deliberately
13
13
  // for a field that has a declared default
14
14
  //
15
- // Schreibt mit setSearchParams (replaceState — kein push), damit
16
- // Sort/Filter-Toggles nicht die Browser-History fluten.
15
+ // Uses setSearchParams (replaceState, not push) so sort/filter toggles do not
16
+ // flood the browser history.
17
17
  import { useCallback, useMemo } from "react";
18
18
  import { useNav } from "../app/nav.js";
19
19
  // A facet with a `defaultFilters` entry falls back to that default whenever
20
20
  // its URL key is absent. Clearing it therefore has to leave a marker, or the
21
21
  // default would come straight back.
22
22
  export const NO_FILTER_URL_VALUE = "~";
23
- // `.` als Trenner: lesbar (`?orders.sort=name`), kollidiert nicht mit
24
- // üblichen Field-Namen (kebab- oder camelCase ohne Punkt). Boot-Validator
25
- // pinnt screen.id ohne Punkt — siehe boot-validator entityList Section.
23
+ // `.` is used as a separator because it is readable (`?orders.sort=name`)
24
+ // and does not conflict with conventional field names (kebab- or camelCase
25
+ // without dots). The boot validator requires screen.id to contain no dots;
26
+ // see the boot-validator entityList section.
26
27
  export const PAGE_SIZE_OPTIONS = [25, 50, 100];
27
28
  function parsePageSize(value) {
28
29
  if (value === undefined)
@@ -68,8 +69,8 @@ export function useListUrlState(screenId, defaultFilters) {
68
69
  const field = k.slice(filterPrefix.length);
69
70
  if (field === "")
70
71
  continue;
71
- // ponytail: comma-join — Facet-Werte sind select-Options/booleans
72
- // ohne Komma; upgrade auf repeated-keys falls je Komma-Werte nötig.
72
+ // ponytail: comma-join — facet values are select options/booleans without
73
+ // commas; use repeated keys if comma-containing values are ever needed.
73
74
  out[field] = v === NO_FILTER_URL_VALUE ? [] : v.split(",").filter((s) => s !== "");
74
75
  }
75
76
  for (const [field, values] of Object.entries(defaultFilters ?? {})) {
@@ -82,10 +83,9 @@ export function useListUrlState(screenId, defaultFilters) {
82
83
  ? NO_FILTER_URL_VALUE
83
84
  : null, [defaultFilters]);
84
85
  const setSort = useCallback((next) => {
85
- // Atomares Update: bei jedem Sort-Wechsel resetten wir auch die
86
- // Page (sonst wäre der User auf "Seite 5 von alter Sortierung"
87
- // hängen, was visuell verwirrend ist). Page-Reset gilt auch bei
88
- // null — gleiche Logik.
86
+ // Atomically reset the page on every sort change; otherwise the user could
87
+ // land on page 5 of the old ordering, which is visually confusing. The
88
+ // page reset also applies when next is null for the same reason.
89
89
  nav.setSearchParams({
90
90
  [key(screenId, "sort")]: next === null ? null : next.field,
91
91
  [key(screenId, "dir")]: next === null ? null : next.dir,
@@ -93,9 +93,8 @@ export function useListUrlState(screenId, defaultFilters) {
93
93
  });
94
94
  }, [nav, screenId]);
95
95
  const setQ = useCallback((next) => {
96
- // Search-Change resettet Page (gleicher Grund wie Sort) UND
97
- // Sort? Nein — User kann mit aktivem Sort suchen wollen, das
98
- // soll die Sortierung nicht zerlegen. Nur Page wird gereset.
96
+ // A search change resets the page but preserves the sort, so users can search
97
+ // while keeping the active ordering.
99
98
  nav.setSearchParams({
100
99
  [key(screenId, "q")]: next === "" ? null : next,
101
100
  [key(screenId, "page")]: null,
@@ -114,9 +114,9 @@ export function useQuery(type, payload, options = {}) {
114
114
  }, refetchIntervalMs);
115
115
  return () => clearInterval(timer);
116
116
  }, [enabled, refetchIntervalMs, runFetch]);
117
- // Live-mode: auf SSE-Events für die Query-Entity hören und refetchen.
118
- // Separater Effect, damit eine Änderung an `live` oder `type` das
119
- // Subscription-Lifecycle genau einmal durchwalzt.
117
+ // Live mode: listen for SSE events for the query entity and refetch.
118
+ // A separate effect ensures changes to `live` or `type` update the
119
+ // subscription lifecycle exactly once.
120
120
  const subscribeLive = useLiveEvents();
121
121
  useEffect(() => {
122
122
  // skip: live mode or query disabled, no SSE subscription needed
@@ -1,21 +1,20 @@
1
- // Tier 2.7e-4: Renderer-Side Eagerload für Reference-Felder.
1
+ // Renderer-side eager load for reference fields.
2
2
  //
3
- // Pro Reference-Spalte einer Liste wird einmal `<feature>:query:
4
- // <refEntity>:list` (limit:200) gerufen, eine Map<uuid, displayValue>
5
- // gebaut, und der Renderer nutzt sie als Cell-Display-Renderer.
3
+ // Each reference column of a list calls `<feature>:query:
4
+ // <refEntity>:list` (limit:200) once, builds a Map<uuid, displayValue>,
5
+ // and the renderer uses it as the cell display renderer.
6
6
  //
7
- // Strategy-Trade-offs:
8
- // - Server-side Drizzle-Joins wären effizienter (eine Query statt
9
- // N+1), würden aber die executor-API um lookupTables erweitern
10
- // müssen. Für das MVP geht der Renderer-Side-Pfad vor.
11
- // - Limit:200 ist eine harte UX-Grenze: bei mehr Entries in der
12
- // referenced Entity zeigen die letzten Rows nur noch UUIDs.
13
- // Searchable-Combobox (Tier 2.1c) + Server-Eagerload mit
14
- // ID-Whitelist heben das später auf.
7
+ // Strategy trade-offs:
8
+ // - Server-side Drizzle joins would be more efficient (one query instead
9
+ // of N+1) but would have to extend the executor API with lookupTables.
10
+ // The renderer-side path wins for the MVP.
11
+ // - limit:200 is a hard UX limit: with more entries in the referenced
12
+ // entity, the last rows show only UUIDs. A searchable combobox plus a
13
+ // server-side eager load with an ID whitelist will lift that later.
15
14
  //
16
- // Dieser Hook ist call-stable: Lookup-Map wird durch useQuery's
17
- // internen Cache shared zwischen List + Edit-Form für die gleiche
18
- // Entity, Live-Updates kommen via SSE (use-query-live).
15
+ // The hook is call-stable: useQuery's internal cache shares the lookup map
16
+ // between list and edit form for the same entity, and live updates arrive
17
+ // via SSE (use-query-live).
19
18
  import { REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "@cosmicdrift/kumiko-framework/ui-types";
20
19
  import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-types/kms-adapter-types";
21
20
  import { useMemo } from "react";
@@ -37,8 +36,8 @@ export function referenceLookupSource(refFeature, refEntity, labelField) {
37
36
  }
38
37
  // Where a reference field's option rows come from, and which of their keys
39
38
  // holds the display text — the entity's own list handler plus `refLabelField`
40
- // by default, or an author-supplied handler whose rows carry `label`
41
- // (fw#2780), for targets with no readable column of their own.
39
+ // by default, or an author-supplied handler whose rows carry `label`,
40
+ // for targets with no readable column of their own.
42
41
  export function referenceOptionSource(field, refFeature, refEntity) {
43
42
  if (field.refOptionsQuery !== undefined) {
44
43
  return { queryQn: field.refOptionsQuery, labelKey: "label" };
@@ -1,37 +1,33 @@
1
1
  import { useRef, useSyncExternalStore } from "react";
2
2
  // React bindings for Kumiko's store primitive (`createStore` from
3
3
  // @cosmicdrift/kumiko-renderer/headless). Both hooks are thin wrappers around React's
4
- // useSyncExternalStore — the whole subscribe/notify mechanism lives in the
5
- // store itself, only React wiring happens here.
4
+ // useSyncExternalStore — the entire subscribe/notify mechanism lives in the
5
+ // store itself; only React wiring happens here.
6
6
  //
7
- // useStore: returns the whole snapshot. Re-renders when the store changes
8
- // its identity. The Object.is gate in the store itself prevents no-op
9
- // updates.
7
+ // useStore: returns the whole snapshot and re-renders when the store changes
8
+ // its identity. The Object.is gate in the store prevents no-op updates.
10
9
  //
11
- // useStoreSelector: gibt eine abgeleitete Sicht zurück. Selectors die
12
- // Object-/Array-Literale zurückgeben (z.B. `s => ({ a, b })`) erzeugen
13
- // pro Render eine neue Identität — useSyncExternalStore würde das als
14
- // "Change" lesen und in eine Re-Render-Schleife laufen. Der dritte
15
- // Arg `equals` dient genau dafür: gleiche Auswahl → cached
16
- // Identität zurück, kein Re-Render.
10
+ // useStoreSelector: returns a derived view. Selectors that return object or
11
+ // array literals (e.g. `s => ({ a, b })`) create a new identity on every
12
+ // render — useSyncExternalStore would interpret this as a change and enter
13
+ // a re-render loop. The `equals` argument handles this case: when the
14
+ // selection is equal, the cached identity is returned and no re-render occurs.
17
15
  // @wrapper-known semantic-alias
18
16
  export function useStore(store) {
19
17
  return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
20
18
  }
21
19
  export function useStoreSelector(store, select, equals = Object.is) {
22
- // Cache der letzten Auswahl. Beim nächsten getSnapshot-Call vergleicht
23
- // der Wrapper die neu berechnete Auswahl mit dem Cache und gibt — falls
24
- // gleich — die cached Identität zurück. Das ist der Trick, der
25
- // Selector-Returns wie `s => ({ a, b })` stabil hält.
26
- const lastSelected = useRef(undefined);
27
- const initialized = useRef(false);
20
+ // Cache of the last selection. On the next getSnapshot call, the wrapper
21
+ // compares the newly computed selection with the cache and returns the
22
+ // cached identity when they are equal. This keeps selector results such as
23
+ // `s => ({ a, b })` stable.
24
+ const lastSelected = useRef(null);
28
25
  const getSelectedSnapshot = () => {
29
26
  const next = select(store.getSnapshot());
30
- if (initialized.current && equals(lastSelected.current, next)) {
31
- return lastSelected.current;
32
- }
33
- lastSelected.current = next;
34
- initialized.current = true;
27
+ const cached = lastSelected.current;
28
+ if (cached !== null && equals(cached.value, next))
29
+ return cached.value;
30
+ lastSelected.current = { value: next };
35
31
  return next;
36
32
  };
37
33
  return useSyncExternalStore(store.subscribe, getSelectedSnapshot, getSelectedSnapshot);
@@ -80,10 +80,12 @@ export function useStreamHandler(type, payload = {}, options = {}) {
80
80
  }, []);
81
81
  return { chunks, status, error, start, abort, reset };
82
82
  }
83
+ function isDispatcherError(e) {
84
+ return !!e && typeof e === "object" && "code" in e && "message" in e && "i18nKey" in e;
85
+ }
83
86
  function asDispatcherError(e) {
84
- if (e && typeof e === "object" && "code" in e && "message" in e && "i18nKey" in e) {
87
+ if (isDispatcherError(e))
85
88
  return e;
86
- }
87
89
  return {
88
90
  code: "stream_error",
89
91
  httpStatus: 0,
@@ -1,12 +1,6 @@
1
- // Framework-Default-Bundle. Strings die die Renderer-Components hart
2
- // brauchen (Save/Cancel/Delete-Buttons, Empty-States, Search-Placeholder,
3
- // Nav-Toggle-aria-Labels, Validation-Reasons). createKumikoApp hängt das
4
- // als ALLERLETZTEN Fallback in den LocaleProvider — Apps können
5
- // einzelne Keys via clientFeatures.translations überschreiben.
1
+ // Framework default bundle. These strings are required by renderer components (Save/Cancel/Delete buttons, empty states, search placeholder, navigation-toggle ARIA labels, and validation reasons). createKumikoApp adds this as the last fallback in LocaleProvider, while apps can override individual keys via clientFeatures.translations.
6
2
  //
7
- // Convention: alle Keys mit `kumiko.`-Prefix damit sie nicht mit
8
- // App-Keys kollidieren. Sub-Pfade gruppieren nach Bereich (actions /
9
- // list / nav / form / validation).
3
+ // All keys use the `kumiko.` prefix to avoid collisions with app keys. Subpaths group keys by area (actions / list / nav / form / validation).
10
4
  export const kumikoDefaultTranslations = {
11
5
  en: {
12
6
  "kumiko.actions.save": "Save",
package/dist/i18n.d.ts CHANGED
@@ -5,7 +5,7 @@ import type { LocaleResolver } from "./headless/index.ts";
5
5
  * placeholders (same semantics as i18next t); plural entries resolve via
6
6
  * `resolveTranslationValue`. */
7
7
  export type TranslationBundle = Readonly<Record<string, TranslationValue>>;
8
- /** Map von Locale-Code (BCP-47, z.B. `"de"`, `"en-US"`) → Bundle. */
8
+ /** Map from locale code (BCP-47, e.g. `"de"`, `"en-US"`) → bundle. */
9
9
  export type TranslationsByLocale = Readonly<Record<string, TranslationBundle>>;
10
10
  /** Key-first shape for `r.translations({ keys })` — each key maps locale → TranslationValue. */
11
11
  export type TranslationsByKey = Readonly<Record<string, Readonly<Record<string, TranslationValue>>>>;
@@ -14,9 +14,9 @@ export declare function translationsByLocaleFromKeys(source: TranslationsByKey):
14
14
  /** Inverse of `translationsByLocaleFromKeys`: pivot locale-first bundles to the key-first
15
15
  * shape `r.translations({ keys })` expects, so one source feeds server and client. */
16
16
  export declare function translationsByKeyFromLocales(source: TranslationsByLocale): TranslationsByKey;
17
- /** Merged zwei TranslationsByLocale-Maps — der override gewinnt pro Key,
18
- * die Locales werden zusammengeführt. Standard-Baustein für Client-
19
- * Plugins, die App-Overrides über ihre Default-Bundles legen. */
17
+ /** Merges two TranslationsByLocale maps — the override wins per key,
18
+ * and the locales are combined. Standard building block for client
19
+ * plugins that layer app overrides over their default bundles. */
20
20
  export declare function mergeTranslations(base: TranslationsByLocale, override: TranslationsByLocale): TranslationsByLocale;
21
21
  export type LocaleProviderProps = {
22
22
  readonly resolver: LocaleResolver;
@@ -31,9 +31,9 @@ export type LocaleProviderProps = {
31
31
  readonly children: ReactNode;
32
32
  };
33
33
  export declare function LocaleProvider({ resolver, fallbackBundles, fallbackLocale, children, }: LocaleProviderProps): ReactNode;
34
- /** Liefert den aktuellen LocaleResolver und abonniert automatisch
35
- * Locale-Änderungen — der aufrufende Component re-rendert sobald die
36
- * Sprache gewechselt wird. Wirft wenn kein Provider im Baum ist. */
34
+ /** Provides the current LocaleResolver and automatically subscribes to
35
+ * locale changes — the calling component re-renders when the language
36
+ * changes. Throws when there is no provider in the tree. */
37
37
  export declare function useLocale(): LocaleResolver;
38
38
  /** `t("key", params)` resolves in this order:
39
39
  * 1. App resolver (e.g. i18next)
@@ -58,9 +58,9 @@ export declare function useOptionalLocale(): string | undefined;
58
58
  export declare function useOptionalTimeZone(): string;
59
59
  /** Non-throwing translate — undefined outside LocaleProvider. */
60
60
  export declare function useOptionalTranslation(): ((key: string, params?: Readonly<Record<string, unknown>>) => string) | undefined;
61
- /** Default-Resolver für Apps ohne eigene i18n-Schicht. Gibt jeden Key
62
- * unverändert zurück — die Plugin-Fallback-Bundles erledigen dann die
63
- * echte Übersetzung. Nützlich auch als Basis für Tests. */
61
+ /** Default resolver for apps without their own i18n layer. Returns every key
62
+ * unchanged — the plugin fallback bundles then provide the actual
63
+ * translation. Also useful as a basis for tests. */
64
64
  export declare function createStaticLocaleResolver(options?: {
65
65
  readonly locale?: string;
66
66
  readonly timeZone?: string;
package/dist/i18n.js CHANGED
@@ -39,9 +39,9 @@ export function translationsByKeyFromLocales(source) {
39
39
  }
40
40
  return out;
41
41
  }
42
- /** Merged zwei TranslationsByLocale-Maps — der override gewinnt pro Key,
43
- * die Locales werden zusammengeführt. Standard-Baustein für Client-
44
- * Plugins, die App-Overrides über ihre Default-Bundles legen. */
42
+ /** Merges two TranslationsByLocale maps — the override wins per key,
43
+ * and the locales are combined. Standard building block for client
44
+ * plugins that layer app overrides over their default bundles. */
45
45
  export function mergeTranslations(base, override) {
46
46
  const locales = new Set([...Object.keys(base), ...Object.keys(override)]);
47
47
  const merged = {};
@@ -52,10 +52,10 @@ export function mergeTranslations(base, override) {
52
52
  }
53
53
  const LocaleContext = createContext(undefined);
54
54
  const FormalityContext = createContext("informal");
55
- // Stabile Referenz statt `fallbackBundles = []` als Default-Parameter:
56
- // ein Literal-Default wird bei JEDEM Aufruf neu allokiert und würde die
57
- // useMemo-Referenzprüfung im Provider unten aushebeln, sobald der
58
- // Aufrufer fallbackBundles weglässt.
55
+ // Stable reference instead of `fallbackBundles = []` as a default parameter:
56
+ // a literal default is allocated on EVERY call and would invalidate the
57
+ // useMemo reference check in the provider below whenever the caller omits
58
+ // fallbackBundles.
59
59
  const EMPTY_FALLBACK_BUNDLES = [];
60
60
  export function LocaleProvider({ resolver, fallbackBundles = EMPTY_FALLBACK_BUNDLES, fallbackLocale = "en", children, }) {
61
61
  // Without memoization every re-render of the provider (e.g. because an
@@ -67,16 +67,16 @@ export function LocaleProvider({ resolver, fallbackBundles = EMPTY_FALLBACK_BUND
67
67
  const value = useMemo(() => ({ resolver, fallbackBundles, fallbackLocale }), [resolver, fallbackBundles, fallbackLocale]);
68
68
  return _jsx(LocaleContext.Provider, { value: value, children: children });
69
69
  }
70
- /** Liefert den aktuellen LocaleResolver und abonniert automatisch
71
- * Locale-Änderungen — der aufrufende Component re-rendert sobald die
72
- * Sprache gewechselt wird. Wirft wenn kein Provider im Baum ist. */
70
+ /** Provides the current LocaleResolver and automatically subscribes to
71
+ * locale changes — the calling component re-renders when the language
72
+ * changes. Throws when there is no provider in the tree. */
73
73
  export function useLocale() {
74
74
  const ctx = useContext(LocaleContext);
75
75
  if (ctx === undefined) {
76
76
  throw new Error("useLocale must be used inside <LocaleProvider>");
77
77
  }
78
- // Subscribe + current locale-snapshot. Wir brauchen den Rückgabewert
79
- // selbst nicht — wichtig ist nur der re-render-trigger.
78
+ // Subscribe + current locale snapshot. We do not need the return value
79
+ // itself — only the re-render trigger matters.
80
80
  const getLocaleSnapshot = () => ctx.resolver.locale();
81
81
  useSyncExternalStore(ctx.resolver.subscribe, getLocaleSnapshot, getLocaleSnapshot);
82
82
  return ctx.resolver;
@@ -93,16 +93,16 @@ export function useTranslation() {
93
93
  if (ctx === undefined) {
94
94
  throw new Error("useTranslation must be used inside <LocaleProvider>");
95
95
  }
96
- // Re-Render bei Sprach-Wechsel. `ctx.resolver.subscribe` ist bereits
97
- // eine stable-reference aus dem Resolver, daher hier keine eigene
98
- // Memoization der Subscribe-Callback nötig.
96
+ // Re-render on language changes. `ctx.resolver.subscribe` is already
97
+ // a stable reference from the resolver, so no separate subscription
98
+ // callback memoization is needed here.
99
99
  const getLocaleSnapshot = () => ctx.resolver.locale();
100
100
  const locale = useSyncExternalStore(ctx.resolver.subscribe, getLocaleSnapshot, getLocaleSnapshot);
101
- // `t` MUSS referenz-stabil sein solange sich Resolver/Bundles/Locale
102
- // nicht ändern — Consumer nutzen `t` regelmäßig in useEffect-Deps
103
- // (z.B. um Queries neu zu laden wenn sich die Sprache ändert). Ein neu
104
- // erzeugtes `t` pro Render führt sonst zu einem Render/Effect-Endlos-
105
- // Loop (siehe admin-shell Overview-Screens, Prod-Incident 2026-07-07).
101
+ // `t` MUST remain referentially stable while the resolver/bundles/locale
102
+ // do not change — consumers regularly use `t` in useEffect dependencies
103
+ // (e.g. to reload queries when the language changes). Creating a new `t`
104
+ // on every render would otherwise cause a render/effect infinite loop
105
+ // (see the admin-shell Overview screens, production incident).
106
106
  const formality = useContext(FormalityContext);
107
107
  return useCallback((key, params) => translateWithFallbacks(ctx, locale, formality, key, params), [ctx, locale, formality]);
108
108
  }
@@ -165,9 +165,9 @@ export function useOptionalTranslation() {
165
165
  const t = useCallback((key, params) => ctx === undefined ? key : translateWithFallbacks(ctx, locale, formality, key, params), [ctx, locale, formality]);
166
166
  return ctx === undefined ? undefined : t;
167
167
  }
168
- /** Default-Resolver für Apps ohne eigene i18n-Schicht. Gibt jeden Key
169
- * unverändert zurück — die Plugin-Fallback-Bundles erledigen dann die
170
- * echte Übersetzung. Nützlich auch als Basis für Tests. */
168
+ /** Default resolver for apps without their own i18n layer. Returns every key
169
+ * unchanged — the plugin fallback bundles then provide the actual
170
+ * translation. Also useful as a basis for tests. */
171
171
  export function createStaticLocaleResolver(options = {}) {
172
172
  const locale = options.locale ?? "en";
173
173
  const timeZone = options.timeZone ?? "UTC";
@@ -175,8 +175,8 @@ export function createStaticLocaleResolver(options = {}) {
175
175
  translate: (key) => key,
176
176
  locale: () => locale,
177
177
  timeZone: () => timeZone,
178
- // No-op subscribe: unsere Locale ist statisch, es gibt nie ein
179
- // Change-Event. Unsubscribe ist ebenfalls no-op.
178
+ // No-op subscribe: our locale is static, so no change event ever occurs.
179
+ // Unsubscribe is also a no-op.
180
180
  subscribe: () => () => { },
181
181
  };
182
182
  }
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
- // Platform-agnostic React renderer for Kumiko screens. Das ist der
2
- // Shared-Layer: Components, Hooks, Contexts, Types. Die Web-DOM-Impls
3
- // leben unter ./web und reichen ihre konkreten Primitives/Nav/SSE-Impls via
4
- // Provider in diesen Baum.
1
+ // Platform-agnostic React renderer for Kumiko screens. This is the
2
+ // shared layer for components, hooks, contexts, and types. Web DOM
3
+ // implementations live under ./web and provide their concrete
4
+ // primitives, navigation, and SSE implementations through providers
5
+ // in this tree.
5
6
  //
6
7
  // Direct consumers of this layer: platform bindings
7
8
  // (@cosmicdrift/kumiko-renderer/web) or an app writing its own bootstrap.
@@ -89,7 +89,7 @@ export function createLiveDispatcher(options = {}) {
89
89
  return {
90
90
  async write(type, payload, opts) {
91
91
  const body = { type, payload };
92
- // Idempotency by default (#761): without a requestId the server-side
92
+ // Idempotency by default: without a requestId the server-side
93
93
  // dedup never engages and a transport-level double-send duplicates
94
94
  // events. Callers with a logical-submit id (savable queue, form
95
95
  // controllers) pass their own and keep it across their retries.
@@ -106,7 +106,7 @@ export function createLiveDispatcher(options = {}) {
106
106
  const body = { commands };
107
107
  // One id for the whole batch — the server caches the BatchResult
108
108
  // under it, so a retried batch returns the cached outcome instead of
109
- // re-executing the commands (#761).
109
+ // re-executing the commands.
110
110
  body["requestId"] = opts?.requestId ?? generateRequestId();
111
111
  const call = await callJson(PATH_BATCH, body, opts?.signal);
112
112
  return normalizeBatchResponse(call);