@cosmicdrift/kumiko-renderer 0.385.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 +28 -5
  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 +43 -44
  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
@@ -26,20 +26,20 @@ import { ScreenErrorFallback } from "./screen-error-fallback.js";
26
26
  import { ToastActionNotifier } from "./toast-action-notifier.js";
27
27
  const EMPTY_FEATURES = [];
28
28
  const EMPTY_COLLECTIONS = [];
29
- // Qualifiziert den Key eines navProviders auf seine Nav-QN. Lokale ids
30
- // (z.B. "content") werden wie in r.nav mit dem Feature-Namen qualifiziert;
31
- // bereits qualifizierte QNs (App registriert die Nav für ein bundled-feature
32
- // und gibt die QN als navId rein, z.B. "publicstatus:nav:content") gehen
33
- // unverändert durch. MUSS konsistent zu qualifyNavId bleiben, sonst findet
34
- // der NavTree-Knoten (Schema-Seite) seinen Provider nicht.
29
+ // Qualifies a navProvider key to its nav QN. Local IDs
30
+ // (e.g. "content") are qualified with the feature name as in r.nav;
31
+ // already qualified QNs (the app registers navigation for a bundled feature
32
+ // and passes the QN as navId, e.g. "publicstatus:nav:content") pass through
33
+ // unchanged. This MUST stay consistent with qualifyNavId, otherwise the
34
+ // schema-side NavTree node cannot find its provider.
35
35
  export function qualifyNavProviderKey(feature, id) {
36
36
  return id.includes(":nav:") ? id : `${feature}:nav:${id}`;
37
37
  }
38
- // Nav-Provider-Map: ein navProvider hängt dynamische Children an einen
39
- // konkreten r.nav({provider:true})-Knoten (per QN). Lokale nav-ids werden wie
40
- // in r.nav mit dem Feature-Namen qualifiziert; bereits qualifizierte QNs
41
- // (cross-feature, z.B. App registriert Nav für ein bundled-feature) gehen
42
- // unverändert durch.
38
+ // Nav-provider map: a navProvider attaches dynamic children to a specific
39
+ // r.nav({provider:true}) node by QN. Local nav IDs are qualified with the
40
+ // feature name as in r.nav; already qualified QNs (for cross-feature use,
41
+ // e.g. when the app registers navigation for a bundled feature) pass through
42
+ // unchanged.
43
43
  //
44
44
  // Schema-derived providers (r.contentCollection) run first and hold the weaker
45
45
  // claim on a QN — an explicitly registered navProvider overrides them without
@@ -92,7 +92,7 @@ function readInjectedSchema() {
92
92
  // screen a feature only registers for manual app-side placement (e.g.
93
93
  // auth-mfa's enable screen) can win by declaration order alone, landing
94
94
  // every app without an explicit `screenQn` on a screen nobody wired a
95
- // component for (#1258).
95
+ // component for.
96
96
  function firstNavReachableScreenQn(features, acceptsScreen) {
97
97
  // NavDefinition.screen carries two shapes in practice: most bundled
98
98
  // features author it pre-qualified ("tenant:screen:members"), but the
@@ -117,7 +117,7 @@ function firstNavReachableScreenQn(features, acceptsScreen) {
117
117
  }
118
118
  // Only screens whose `access` excludes nobody: on an unprojected schema the
119
119
  // landing is picked without knowing the user's roles, so a role-restricted
120
- // screen would land every non-admin on access-denied (#1176).
120
+ // screen would land every non-admin on access-denied.
121
121
  export function firstOpenScreenQn(features) {
122
122
  return firstNavReachableScreenQn(features, (s) => s.access === undefined || isOpenToAllGranted(s.access));
123
123
  }
@@ -177,15 +177,15 @@ export function mergeContentEditors(clientFeatures) {
177
177
  // declare a `type: "custom"` screen that's reachable directly by URL
178
178
  // even without nav placement — without this check, a missing client
179
179
  // plugin only surfaces as an error placeholder to whoever happens to
180
- // open that URL (kumiko-framework#2025). Screens flagged `dormant`
180
+ // open that URL. Screens flagged `dormant`
181
181
  // (e.g. user-data-rights privacy-center, auth-mfa's enable screen) are
182
182
  // registered without a self-owned r.nav on purpose — an app opts in by
183
183
  // navving them explicitly, so a consumer that hasn't done that yet isn't
184
- // missing anything (kumiko-framework#2034). Still dev-only: several
184
+ // missing anything. Still dev-only: several
185
185
  // bundled screens that ARE self-navved (e.g. compliance-profiles'
186
186
  // profile-picker) have real unmounted-client-plugin bugs in existing
187
- // consumer apps today (kumiko-framework#2025) — running this in
188
- // production before those are fixed (tracked via infra#503) would just
187
+ // consumer apps today — running this in
188
+ // production before those are fixed would just
189
189
  // spam every affected app's console.
190
190
  function warnMissingCustomScreens(app, customScreens) {
191
191
  if (typeof process === "undefined" || process.env.NODE_ENV === "production")
@@ -209,7 +209,7 @@ function KumikoAppRoot(props) {
209
209
  // 1. clientFeatures.translations — app overrides always win, even
210
210
  // against the framework's own labels.
211
211
  // 2. schemaTranslations — server-authored r.translations, projected
212
- // verbatim by buildAppSchema (#1059). Without this bundle, nav/
212
+ // verbatim by buildAppSchema. Without this bundle, nav/
213
213
  // screen labels only resolve if an app ALSO duplicates them in its
214
214
  // own web/i18n.ts — most bundled features never did, labels then
215
215
  // render as raw i18n keys.
@@ -388,7 +388,7 @@ function findEditScreenForEntity(features, entityName) {
388
388
  }
389
389
  return undefined;
390
390
  }
391
- // fw#2640: a list screen with no reachable click target (no `detailFor`
391
+ // A list screen with no reachable click target (no `detailFor`
392
392
  // screen for its entity, no app-wide onRowClick, no entityEdit screen)
393
393
  // must render without the row-click affordance instead of a silent no-op —
394
394
  // mirrors the precedence effectiveOnRowClick itself resolves at click time.
@@ -426,7 +426,6 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
426
426
  };
427
427
  }
428
428
  const shortId = nav.route.screenId;
429
- // Suche das Feature dessen Screens den short id enthalten.
430
429
  let matchedFeature;
431
430
  for (const f of app.features) {
432
431
  if (f.screens.some((s) => s.id === shortId)) {
@@ -452,7 +451,7 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
452
451
  screenId: lastSegment(qn),
453
452
  ...(entityId !== undefined && { entityId }),
454
453
  };
455
- // Precedence (fw#2164): detailFor screen, then onRowClick, then the
454
+ // Precedence: detailFor screen, then onRowClick, then the
456
455
  // entityEdit-search fallback below. An explicit rowActions
457
456
  // rowClick:true already wins over all of this — kumiko-screen.tsx
458
457
  // handles it before onRowClick is ever called.
@@ -477,7 +476,7 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
477
476
  }
478
477
  };
479
478
  }, [app, activeScreen, onRowClick, nav, qn, entityId]);
480
- // Copy-link action (Issue #912) for entityEdit update screens. Builds
479
+ // Copy-link action for entityEdit update screens. Builds
481
480
  // the absolute permalink URL from the current route + copies it —
482
481
  // `navigator`/`window` are allowed here (renderer/web, not a
483
482
  // platform-neutral package). No button without entityId (create-mode).
@@ -13,7 +13,7 @@ import { useEffect } from "react";
13
13
  function syncDocumentLocale(locale) {
14
14
  document.documentElement.lang = locale;
15
15
  // Dedicated marker for dispatcher-live — static <html lang> must not
16
- // look like an active UI locale (kumiko-framework#2334).
16
+ // look like an active UI locale.
17
17
  document.documentElement.dataset["kumikoLocale"] = locale;
18
18
  }
19
19
  export function DocumentLangSync({ resolver }) {
@@ -1,4 +1,4 @@
1
- // Browser-backed DraftStorage default for createKumikoApp (issue #1913).
1
+ // Browser-backed DraftStorage default for createKumikoApp.
2
2
  //
3
3
  // Persists a create-mode draftId per screen in `sessionStorage` — same-tab
4
4
  // scope only (deliberately, unlike browser-locale.ts's localStorage): the
@@ -6,8 +6,8 @@ export type NavProvidersProviderProps = {
6
6
  readonly children: ReactNode;
7
7
  };
8
8
  export declare function NavProvidersProvider({ value, entities, children, }: NavProvidersProviderProps): ReactNode;
9
- /** Provider-Map keyed auf Nav-QN. Empty-Map-Default → kein Crash ohne
10
- * registrierte Provider. */
9
+ /** Provider map keyed by Nav-QN. The empty-map default prevents crashes when
10
+ * no providers are registered. */
11
11
  export declare function useNavProviders(): ReadonlyMap<string, TreeChildrenSubscribe>;
12
- /** SSE-Entity-Liste pro Provider-QN für Live-Refresh. Empty-Map-Default. */
12
+ /** SSE entity list for each provider QN, used for live refresh. Empty-map default. */
13
13
  export declare function useNavEntities(): ReadonlyMap<string, readonly string[]>;
@@ -7,12 +7,12 @@ const NavEntitiesContext = createContext(EMPTY_ENTITIES);
7
7
  export function NavProvidersProvider({ value, entities, children, }) {
8
8
  return (_jsx(NavProvidersContext.Provider, { value: value, children: _jsx(NavEntitiesContext.Provider, { value: entities ?? EMPTY_ENTITIES, children: children }) }));
9
9
  }
10
- /** Provider-Map keyed auf Nav-QN. Empty-Map-Default → kein Crash ohne
11
- * registrierte Provider. */
10
+ /** Provider map keyed by Nav-QN. The empty-map default prevents crashes when
11
+ * no providers are registered. */
12
12
  export function useNavProviders() {
13
13
  return useContext(NavProvidersContext);
14
14
  }
15
- /** SSE-Entity-Liste pro Provider-QN für Live-Refresh. Empty-Map-Default. */
15
+ /** SSE entity list for each provider QN, used for live refresh. Empty-map default. */
16
16
  export function useNavEntities() {
17
17
  return useContext(NavEntitiesContext);
18
18
  }
@@ -1,31 +1,32 @@
1
1
  import { type AnchorHTMLAttributes, type ReactNode } from "react";
2
2
  import { type FeatureSchema, type NavApi, type NavTarget } from "../../index.ts";
3
- /** React-Hook der eine NavApi aus der Browser-History baut. Sollte
4
- * einmal im App-Root aufgerufen und als value an den shared
5
- * `<NavProvider>` durchgereicht werden — createKumikoApp tut das.
3
+ /** React hook that builds a NavApi from browser history. It should be
4
+ * called once at the app root and passed as the value to the shared
5
+ * `<NavProvider>`—createKumikoApp does this.
6
6
  *
7
- * hasWorkspaces aus dem Schema (schema.workspaces non-empty) entscheidet,
8
- * ob das erste URL-Segment als Workspace-id parsed wird. Pure Pass-
9
- * through an parsePath; formatPath checkt selbst auf target.workspaceId.
7
+ * Whether the schema has workspaces (schema.workspaces is non-empty) determines
8
+ * whether the first URL segment is parsed as a workspace ID. This is a pure
9
+ * pass-through to parsePath; formatPath checks target.workspaceId itself.
10
10
  *
11
- * basePath mounted die App unter einem URL-Prefix (z.B. "/admin"). Read-
12
- * Pfad strippt vor parsePath, Write-Pfad prepend'd vor pushState/href.
13
- * URLs außerhalb des basePath liefern route=undefined, ohne Auto-Redirect.
11
+ * basePath mounts the app under a URL prefix (for example, "/admin"). The read
12
+ * path strips it before parsePath, and the write path prepends it before
13
+ * pushState/href. URLs outside basePath produce route=undefined without an
14
+ * automatic redirect.
14
15
  *
15
- * Achtung — Ambiguität bei route=undefined mit basePath:
16
- * Sowohl die App-Root (URL === basePath, z.B. "/admin") als auch URLs
17
- * außerhalb des basePath (z.B. "/marketing") liefern route=undefined. Eine
18
- * App die zwischen "render Default-Screen" und "render Not-Found"
19
- * unterscheiden muss, muss zusätzlich `window.location.pathname` prüfen:
16
+ * Note the ambiguity of route=undefined with basePath:
17
+ * Both the app root (URL === basePath, for example, "/admin") and URLs
18
+ * outside basePath (for example, "/marketing") produce route=undefined. An
19
+ * app that must distinguish between "render Default-Screen" and "render
20
+ * Not-Found" must also inspect `window.location.pathname`:
20
21
  *
21
22
  * if (window.location.pathname.startsWith("/admin")) {
22
- * // in-app, aber an der Root → Default-Screen
23
+ * // in-app, but at the root → Default-Screen
23
24
  * } else {
24
25
  * // out-of-app → Not-Found / Marketing-Layer
25
26
  * }
26
27
  *
27
- * Im non-basePath-Modus ergibt sich diese Ambiguität nicht — out-of-app
28
- * ist dort kein Konzept. */
28
+ * In non-basePath mode, this ambiguity does not arise—out-of-app is not a
29
+ * concept there. */
29
30
  export declare function useBrowserNavApi(options?: {
30
31
  readonly hasWorkspaces?: boolean;
31
32
  readonly basePath?: string;
@@ -1,36 +1,37 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- // Browser-Impl der Navigation: HTML5-History + popstate subscribe.
3
- // Types + Context + useNav leben im shared `@cosmicdrift/kumiko-renderer`; dieser
4
- // File liefert nur die Web-spezifische NavApi-Instanz und die
5
- // `<KumikoLink>` Anchor-Komponente.
2
+ // Browser navigation implementation: HTML5 History + popstate subscription.
3
+ // Types, Context, and useNav live in the shared `@cosmicdrift/kumiko-renderer`; this
4
+ // file provides only the web-specific NavApi instance and the
5
+ // `<KumikoLink>` anchor component.
6
6
  //
7
- // Bewusst KEIN createStore: Source-of-truth ist `window.location.pathname`
8
- // (extern, kann via replaceState außerhalb unserer Kontrolle wechseln).
9
- // Ein Store wäre entweder eine zweite Wahrheit (Drift-Risiko) oder ein
10
- // reiner Tick-Counter (Anti-Pattern — createStore ist State-Holder, kein
11
- // Event-Bus). Hand-rolled Listener-Set ist hier idiomatisch: zwei
12
- // Notify-Trigger (eigenes pushPath, popstate-Event), nicht generalisierbar.
7
+ // Deliberately no createStore: the source of truth is `window.location.pathname`
8
+ // (external and able to change through replaceState outside our control).
9
+ // A store would either be a second source of truth (risking drift) or a
10
+ // tick counter (an anti-pattern—createStore is a state holder, not an
11
+ // event bus). A hand-rolled listener set is appropriate here: there are two
12
+ // notification triggers (our pushPath and the popstate event), so this is not
13
+ // generalizable.
13
14
  import { useCallback, useMemo, useSyncExternalStore, } from "react";
14
15
  import { formatPath, hasUnsavedInput, parsePath, requestLeave, resolveTarget, useNav, } from "../../index.js";
15
- // basePath erlaubt es, die App unter einem URL-Prefix zu mounten
16
- // (z.B. `/admin`). Read-Pfad strippt den Prefix vor dem parsePath,
17
- // Write-Pfad prepend'd ihn vor dem pushState/replaceState/href.
16
+ // basePath allows the app to be mounted under a URL prefix
17
+ // (for example, `/admin`). The read path strips the prefix before parsePath;
18
+ // the write path prepends it before pushState/replaceState/href.
18
19
  //
19
- // Wenn die URL nicht im basePath liegt (z.B. /marketing/foo bei
20
- // basePath="/admin"), liefert stripBasePath `undefined` — useBrowserNavApi
21
- // gibt dann route=undefined zurück, die App rendert ihren "outside"-State
22
- // (Not-Found, Marketing-Layer, Server-Routing-Pickup, …). Es gibt KEIN
23
- // Auto-Redirect zur App-Root — die Host-App entscheidet selbst.
20
+ // If the URL is not under basePath (for example, /marketing/foo with
21
+ // basePath="/admin"), stripBasePath returns `undefined`—useBrowserNavApi then
22
+ // returns route=undefined, and the app renders its "outside" state
23
+ // (Not-Found, marketing layer, server-routing pickup, …). There is no
24
+ // automatic redirect to the app root—the host app decides.
24
25
  function normalizeBasePath(raw) {
25
26
  if (raw === undefined || raw === "" || raw === "/")
26
27
  return "";
27
28
  const withLeading = raw.startsWith("/") ? raw : `/${raw}`;
28
29
  return withLeading.endsWith("/") ? withLeading.slice(0, -1) : withLeading;
29
30
  }
30
- // Returns the in-app path (basePath-relativ) wenn `path` im basePath liegt,
31
- // sonst `undefined`. Strict-segment-Boundary: "/administrator" matcht
32
- // nicht "/admin" — sonst würde der Prefix-Check auf String-Ebene falsche
33
- // Treffer liefern.
31
+ // Returns the in-app (basePath-relative) path when `path` is under basePath;
32
+ // otherwise returns `undefined`. The segment boundary is strict: "/administrator"
33
+ // does not match "/admin"—otherwise a string-level prefix check would produce
34
+ // false matches.
34
35
  function stripBasePath(path, basePath) {
35
36
  if (basePath === "")
36
37
  return path;
@@ -43,15 +44,15 @@ function stripBasePath(path, basePath) {
43
44
  function prependBasePath(path, basePath) {
44
45
  if (basePath === "")
45
46
  return path;
46
- // formatPath liefert immer absoluten in-app-Pfad: "/" oder "/screen-id"
47
- // oder "/screen-id/entity-id". "/" → einfach basePath, sonst concat.
47
+ // formatPath always returns an absolute in-app path: "/" or "/screen-id"
48
+ // or "/screen-id/entity-id". "/" becomes basePath; otherwise, concatenate.
48
49
  if (path === "/" || path === "")
49
50
  return basePath;
50
51
  return `${basePath}${path}`;
51
52
  }
52
- // pushState feuert keinen popstate — wir halten einen eigenen
53
- // Listener-Set, den navigate() notifiziert. popstate (Back/Forward)
54
- // läuft über einen window-Listener, den wir einmalig verdrahten.
53
+ // pushState does not fire popstate, so we maintain a listener set that
54
+ // navigate() notifies. Back/Forward navigation goes through a window listener
55
+ // wired exactly once.
55
56
  const listeners = new Set();
56
57
  let popstateWired = false;
57
58
  // URL the app currently shows; popstate has already moved the browser to the
@@ -107,11 +108,11 @@ function subscribe(listener) {
107
108
  function readPath() {
108
109
  return typeof window !== "undefined" ? window.location.pathname : "/";
109
110
  }
110
- // Read der aktuellen ?key=value-Pairs als Plain-Record. URLSearchParams-
111
- // Iterator gibt String/String — wir kollabieren auf "letzter Wert
112
- // gewinnt" wenn ein Key mehrfach im Query auftaucht (`?a=1&a=2` → "2").
113
- // Multi-Value-Lists sind kein Use-Case in Kumiko-Filter-State; explizit
114
- // dokumentieren statt unspezifizierten Behavior zu liefern.
111
+ // Reads the current ?key=value pairs as a plain record. URLSearchParams-
112
+ // iterator yields String/String; when a key occurs more than once, the last
113
+ // value wins (`?a=1&a=2` → "2").
114
+ // Multi-value lists are not supported in Kumiko filter state; documenting this
115
+ // explicitly is preferable to providing unspecified behavior.
115
116
  function readSearch() {
116
117
  return typeof window !== "undefined" ? window.location.search : "";
117
118
  }
@@ -124,10 +125,10 @@ function parseSearchParams(search) {
124
125
  out[k] = v;
125
126
  return out;
126
127
  }
127
- // Mergt updates in den aktuellen ?-String. `null` löscht, sonst
128
- // überschreibt. Verwendet replaceState (KEIN push) — Sort/Filter-Toggles
129
- // flutten sonst die Back-Navigation und User clicked sich durch
130
- // dutzende Zwischen-States um zur vorherigen Seite zu kommen.
128
+ // Merges updates into the current query string. `null` deletes a key; other
129
+ // values overwrite it. Uses replaceState rather than pushState—otherwise
130
+ // sort/filter toggles would flood Back navigation, forcing users through
131
+ // dozens of intermediate states to reach the previous page.
131
132
  function applySearchParamUpdates(updates) {
132
133
  if (typeof window === "undefined")
133
134
  return;
@@ -194,38 +195,39 @@ function replacePath(path, search) {
194
195
  else
195
196
  apply();
196
197
  }
197
- /** React-Hook der eine NavApi aus der Browser-History baut. Sollte
198
- * einmal im App-Root aufgerufen und als value an den shared
199
- * `<NavProvider>` durchgereicht werden — createKumikoApp tut das.
198
+ /** React hook that builds a NavApi from browser history. It should be
199
+ * called once at the app root and passed as the value to the shared
200
+ * `<NavProvider>`—createKumikoApp does this.
200
201
  *
201
- * hasWorkspaces aus dem Schema (schema.workspaces non-empty) entscheidet,
202
- * ob das erste URL-Segment als Workspace-id parsed wird. Pure Pass-
203
- * through an parsePath; formatPath checkt selbst auf target.workspaceId.
202
+ * Whether the schema has workspaces (schema.workspaces is non-empty) determines
203
+ * whether the first URL segment is parsed as a workspace ID. This is a pure
204
+ * pass-through to parsePath; formatPath checks target.workspaceId itself.
204
205
  *
205
- * basePath mounted die App unter einem URL-Prefix (z.B. "/admin"). Read-
206
- * Pfad strippt vor parsePath, Write-Pfad prepend'd vor pushState/href.
207
- * URLs außerhalb des basePath liefern route=undefined, ohne Auto-Redirect.
206
+ * basePath mounts the app under a URL prefix (for example, "/admin"). The read
207
+ * path strips it before parsePath, and the write path prepends it before
208
+ * pushState/href. URLs outside basePath produce route=undefined without an
209
+ * automatic redirect.
208
210
  *
209
- * Achtung — Ambiguität bei route=undefined mit basePath:
210
- * Sowohl die App-Root (URL === basePath, z.B. "/admin") als auch URLs
211
- * außerhalb des basePath (z.B. "/marketing") liefern route=undefined. Eine
212
- * App die zwischen "render Default-Screen" und "render Not-Found"
213
- * unterscheiden muss, muss zusätzlich `window.location.pathname` prüfen:
211
+ * Note the ambiguity of route=undefined with basePath:
212
+ * Both the app root (URL === basePath, for example, "/admin") and URLs
213
+ * outside basePath (for example, "/marketing") produce route=undefined. An
214
+ * app that must distinguish between "render Default-Screen" and "render
215
+ * Not-Found" must also inspect `window.location.pathname`:
214
216
  *
215
217
  * if (window.location.pathname.startsWith("/admin")) {
216
- * // in-app, aber an der Root → Default-Screen
218
+ * // in-app, but at the root → Default-Screen
217
219
  * } else {
218
220
  * // out-of-app → Not-Found / Marketing-Layer
219
221
  * }
220
222
  *
221
- * Im non-basePath-Modus ergibt sich diese Ambiguität nicht — out-of-app
222
- * ist dort kein Konzept. */
223
+ * In non-basePath mode, this ambiguity does not arise—out-of-app is not a
224
+ * concept there. */
223
225
  export function useBrowserNavApi(options) {
224
226
  const path = useSyncExternalStore(subscribe, readPath, () => "/");
225
- // Search wird über denselben Listener-Set notifiziert (popstate +
226
- // unsere replaceState-Calls), also reichen wir denselben subscribe
227
- // durch. Beide Snapshots werden zusammen recomputed — kein Drift
228
- // zwischen Pfad und Query.
227
+ // Search is notified through the same listener set (popstate +
228
+ // our replaceState calls), so we reuse the same subscribe function.
229
+ // Both snapshots are recomputed together, preventing drift
230
+ // between the path and query.
229
231
  const search = useSyncExternalStore(subscribe, readSearch, () => "");
230
232
  const hasWorkspaces = options?.hasWorkspaces === true;
231
233
  const basePath = useMemo(() => normalizeBasePath(options?.basePath), [options?.basePath]);
@@ -234,11 +236,11 @@ export function useBrowserNavApi(options) {
234
236
  const features = options?.features ?? NO_FEATURES;
235
237
  return useMemo(() => {
236
238
  const route = inAppPath === undefined ? undefined : parsePath(inAppPath, hasWorkspaces);
237
- // NavTarget-Contract: workspaceId weglassen = aktueller Workspace
238
- // bleibt. formatPath kennt die aktuelle Route nicht — ohne Injection
239
- // landet `/screen-id` in parsePath(hasWorkspaces) als workspaceId,
240
- // WorkspaceShell sieht einen unbekannten Workspace und revertet
241
- // sofort auf den Default-Screen ("Klick tut nichts"-Prod-Bug).
239
+ // NavTarget contract: omitting workspaceId preserves the current workspace.
240
+ // formatPath does not know the current route, so without this injection
241
+ // `/screen-id` is parsed as a workspaceId by parsePath(hasWorkspaces),
242
+ // causing WorkspaceShell to see an unknown workspace and immediately
243
+ // revert to the default screen (a production bug where "click does nothing").
242
244
  const inCurrentWorkspace = (target) => hasWorkspaces && target.workspaceId === undefined && route?.workspaceId !== undefined
243
245
  ? { ...target, workspaceId: route.workspaceId }
244
246
  : target;
@@ -262,8 +264,8 @@ export function KumikoLink({ to, onClick, children, ...rest }) {
262
264
  onClick?.(e);
263
265
  if (e.defaultPrevented)
264
266
  return;
265
- // Standard-Browser-Verhalten für Cmd/Ctrl/Shift/Alt + Middle-Click
266
- // erhalten — nur der plain-left-click landet bei navigate().
267
+ // Preserve standard browser behavior for Cmd/Ctrl/Shift/Alt + middle-click;
268
+ // only a plain left click is handled by navigate().
267
269
  if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
268
270
  return;
269
271
  e.preventDefault();
@@ -6,7 +6,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
6
6
  // crosses this boundary. tiptap itself is dynamic-imported (TiptapEditor
7
7
  // lives in ./tiptap-editor) so an app that never mounts a "rich" collection
8
8
  // never pays for it; the fallback while the chunk loads is the same plain
9
- // textarea #1794 uses for a missing editor, never a blank panel.
9
+ // textarea uses for a missing editor, never a blank panel.
10
10
  import { lazy, Suspense } from "react";
11
11
  import { TextareaContentEditor } from "../../index.js";
12
12
  const TiptapEditor = lazy(() => import("./tiptap-editor.js"));
@@ -12,9 +12,9 @@ const SOURCE_I18N_KEY = {
12
12
  default: "kumiko.config.source.default",
13
13
  missing: "kumiko.config.source.missing",
14
14
  };
15
- // Fallback-Reihenfolge der Cascade, spezifischste Quelle zuerst.
16
- // Index-Vergleich gegen die Screen-Scope-Quelle entscheidet, welche
17
- // Ebenen ein Nicht-Operator sehen darf.
15
+ // Fallback order of the cascade, with the most specific source first.
16
+ // Comparing indices with the screen-scope source determines which
17
+ // levels a non-operator may see.
18
18
  const SOURCE_ORDER = [
19
19
  "user-row",
20
20
  "tenant-row",
@@ -51,16 +51,16 @@ function scopeToSource(scope) {
51
51
  return "system-row";
52
52
  }
53
53
  function toDisplayLevels(levels, screenScopeSource) {
54
- // System-Screens sind Operator-Sicht — volle Cascade inkl.
55
- // app-override/computed/default bleibt sichtbar.
54
+ // System screens have operator visibility, so the full cascade, including
55
+ // app-override/computed/default, remains visible.
56
56
  if (screenScopeSource === "system-row") {
57
57
  return levels.map((level) => ({ level, badgeSource: level.source }));
58
58
  }
59
59
  const scopeIdx = SOURCE_ORDER.indexOf(screenScopeSource);
60
60
  const own = levels.filter((l) => SOURCE_ORDER.indexOf(l.source) <= scopeIdx);
61
61
  const higher = levels.filter((l) => SOURCE_ORDER.indexOf(l.source) > scopeIdx);
62
- // Genau eine Fallback-Zeile: die aktive höhere Ebene (deren Wert der
63
- // User effektiv bekommt), sonst der deklarierte Default/Missing.
62
+ // Exactly one fallback row: the active higher level (whose value the user
63
+ // effectively receives), or the declared default/missing level.
64
64
  const fallback = higher.find((l) => l.isActive) ??
65
65
  higher.find((l) => l.source === "default" || l.source === "missing");
66
66
  const ownRows = own.map((level) => ({ level, badgeSource: level.source }));
@@ -71,12 +71,11 @@ function toDisplayLevels(levels, screenScopeSource) {
71
71
  {
72
72
  level: fallback,
73
73
  badgeSource: "default",
74
- // Ein durchgängiger Begriff "Standard" (DE) / "Default" (EN) — derselbe
75
- // Key wie das Feld-Label-Badge (kumiko.config.source.default), damit
76
- // Badge + Cascade-Disclosure NICHT zwei verschiedene Wörter zeigen
77
- // (Bug-Bash 3 #11). Der Screen-Scope kann die Operator-Ebenen
78
- // (System/Override/Computed) weder setzen noch zurücksetzen, deshalb
79
- // erscheinen sie hier neutral als "Standard".
74
+ // Use one consistent term, "Standard" (DE) / "Default" (EN)—the same
75
+ // key as the field-label badge (kumiko.config.source.default), so the
76
+ // badge and cascade disclosure do not show two different words. The
77
+ // screen scope cannot set or reset the operator levels (System/Override/
78
+ // Computed), so they appear here neutrally as "Standard".
80
79
  badgeLabelKey: "kumiko.config.source.default",
81
80
  },
82
81
  ];
@@ -106,11 +105,11 @@ export function ConfigCascadeView({ cascade, screenScope, onReset, qualifiedKey,
106
105
  const displayLevels = toDisplayLevels(cascade.levels, screenScopeSource);
107
106
  const activeDisplay = displayLevels.find((d) => d.level.isActive);
108
107
  const hasOverride = activeDisplay?.level.source === screenScopeSource;
109
- // Aufklappbar nur wenn das Panel echten Mehrwert bringt: ein eigener
110
- // Override (→ Reset) oder mehr als eine Ebene MIT Wert. Leere Ebenen
111
- // (z.B. die ungesetzte tenant-row eines reinen Default-Felds) zählen
112
- // nicht — sonst wäre ein Feld, das nur seinen Standard zeigt, fälschlich
113
- // aufklappbar und das Panel nur eine Wiederholung des Triggers.
108
+ // Expandable only when the panel provides meaningful additional information:
109
+ // an own override (→ Reset) or more than one level with a value. Empty levels
110
+ // (e.g. the unset tenant-row of a field using only its default) do not count;
111
+ // otherwise a field showing only its default would incorrectly be expandable,
112
+ // making the panel merely repeat the trigger.
114
113
  const valuedLevels = displayLevels.filter((d) => d.level.hasValue);
115
114
  const expandable = hasOverride || valuedLevels.length > 1;
116
115
  const isUnset = activeDisplay === undefined || !activeDisplay.level.hasValue;
@@ -40,10 +40,10 @@ function scopeToSource(scope) {
40
40
  }
41
41
  export function ConfigSourceBadge({ source, screenScope, }) {
42
42
  const t = useTranslation();
43
- // Gleiche Kollaps-Regel wie toDisplayLevels (config-cascade.tsx):
44
- // Operator-Quellen oberhalb des Screen-Scopes erscheinen für Nicht-
45
- // Operator-Screens als neutrales "Vorgabe"-Badge — sonst leakte das
46
- // Badge die System-Quelle, die die Cascade-View bewusst versteckt.
43
+ // Uses the same collapsing rule as toDisplayLevels (config-cascade.tsx):
44
+ // for non-operator screens, operator sources above the screen scope appear
45
+ // as a neutral "Vorgabe" badge so the badge does not reveal the system source
46
+ // intentionally hidden by the cascade view.
47
47
  let effective = source;
48
48
  if (screenScope !== undefined && screenScope !== "system") {
49
49
  const scopeIdx = SOURCE_ORDER.indexOf(scopeToSource(screenScope));
@@ -1,19 +1,19 @@
1
1
  import type { ReactNode } from "react";
2
2
  export type AppLayoutProps = {
3
- /** Optional. Lebt nur über dem Main-Bereich, nicht über der Sidebar. */
3
+ /** Optional. Exists only above the main area, not above the sidebar. */
4
4
  readonly topbar?: ReactNode;
5
5
  readonly sidebar?: ReactNode;
6
6
  readonly children: ReactNode;
7
7
  readonly testId?: string;
8
- /** Viewport-fit Shell. Default `true` → Wurzel = `h-screen` (fixe
9
- * Viewport-Höhe), Sidebar/Topbar bleiben stehen, der Main-Bereich
10
- * scrollt INNEN (`min-h-0` + `overflow-auto`). `false` → klassischer
11
- * `min-h-screen`-Flow, die ganze Seite scrollt — Escape-Hatch für
12
- * eine öffentliche, lange Content-Seite die echten Body-Scroll
13
- * braucht. Clippt nie — der Content scrollt in `main` statt im Body. */
8
+ /** Viewport-fit shell. Default `true` → root = `h-screen` (fixed
9
+ * viewport height); sidebar/topbar remain fixed while the main area
10
+ * scrolls INSIDE (`min-h-0` + `overflow-auto`). `false` → classic
11
+ * `min-h-screen` flow where the whole page scrolls—an escape hatch for
12
+ * a public, long content page that needs real body scrolling. Never
13
+ * clips—content scrolls in `main` instead of the body. */
14
14
  readonly fill?: boolean;
15
- /** Optionaler Klassen-Append an die Wurzel (eigener Hintergrund etc.).
16
- * Erweitert die Defaults, ersetzt sie nicht (cn-merge). */
15
+ /** Optional classes appended to the root (for a custom background, etc.).
16
+ * Extends the defaults rather than replacing them (`cn` merge). */
17
17
  readonly className?: string;
18
18
  /** Optionaler Klassen-Append an `<main>`. */
19
19
  readonly mainClassName?: string;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "../lib/cn.js";
3
3
  export function AppLayout({ topbar, sidebar, children, testId, fill = true, className, mainClassName, }) {
4
4
  return (_jsxs("div", { "data-testid": testId, "data-kumiko-layout": "app", ...(fill === true && { "data-kumiko-fill": "true" }), className: cn(
5
- // new-york inset-Frame: gedämpfter Rail-Background, die Sidebar sitzt
6
- // transparent darauf, der Main-Bereich schwebt als eigenes Panel.
5
+ // new-york inset frame: muted rail background; the sidebar sits
6
+ // transparently on it, while the main area floats as its own panel.
7
7
  "flex flex-row bg-muted/40 text-foreground", fill === true ? "h-screen" : "min-h-screen", className), children: [sidebar, _jsxs("div", { className: cn("flex flex-1 min-w-0 flex-col overflow-hidden rounded-xl border bg-background shadow-sm m-2 md:ml-0", fill === true && "min-h-0"), children: [topbar, _jsx("main", { className: cn("flex-1 overflow-auto", fill === true && "min-h-0", mainClassName), children: children })] })] }));
8
8
  }
@@ -1,10 +1,11 @@
1
1
  import { type ReactNode } from "react";
2
2
  export type AvatarSize = "sm" | "md" | "lg";
3
3
  export type AvatarProps = {
4
- /** Stable Identifier — Hash-Quelle für die Background-Farbe. Üblich
5
- * user.id; bei rein labelbasierten Avataren auch der label-String. */
4
+ /** Stable identifier—the source for the background-color hash. Usually
5
+ * `user.id`; for avatars based only on a label, the label string can also
6
+ * be used. */
6
7
  readonly id: string;
7
- /** Voller Name oder Email — Initials werden daraus extrahiert. */
8
+ /** Full name or email—the initials are extracted from this value. */
8
9
  readonly label: string;
9
10
  readonly size?: AvatarSize;
10
11
  readonly testId?: string;