@cosmicdrift/kumiko-renderer 0.386.0 → 0.388.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 (222) 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 +12 -4
  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/action-form-support.d.ts +1 -0
  7. package/dist/app/action-form-support.js +1 -1
  8. package/dist/app/app-features-context.js +4 -4
  9. package/dist/app/column-renderers.d.ts +7 -9
  10. package/dist/app/column-renderers.js +15 -18
  11. package/dist/app/config-edit-body.js +7 -3
  12. package/dist/app/config-edit-shim.d.ts +7 -8
  13. package/dist/app/config-edit-shim.js +18 -20
  14. package/dist/app/custom-screens.d.ts +3 -3
  15. package/dist/app/custom-screens.js +8 -8
  16. package/dist/app/dashboard-body.d.ts +2 -2
  17. package/dist/app/dashboard-body.js +2 -2
  18. package/dist/app/entity-edit-prefill.d.ts +11 -0
  19. package/dist/app/entity-edit-prefill.js +18 -13
  20. package/dist/app/entity-edit-screen.js +4 -4
  21. package/dist/app/entity-list-screen.js +3 -3
  22. package/dist/app/extension-form-submit.d.ts +1 -1
  23. package/dist/app/extension-form-submit.js +14 -15
  24. package/dist/app/extension-sections.d.ts +29 -28
  25. package/dist/app/extension-sections.js +10 -10
  26. package/dist/app/feature-schema.js +11 -11
  27. package/dist/app/form-schema.js +2 -2
  28. package/dist/app/header-actions-bar.js +2 -2
  29. package/dist/app/kumiko-screen.js +8 -10
  30. package/dist/app/layout-fields.js +1 -1
  31. package/dist/app/list-facets.js +3 -3
  32. package/dist/app/nav.d.ts +10 -10
  33. package/dist/app/nav.js +13 -13
  34. package/dist/app/projection-detail-body.js +4 -4
  35. package/dist/app/projection-detail-shim.d.ts +6 -5
  36. package/dist/app/projection-detail-shim.js +23 -22
  37. package/dist/app/projection-list-body.js +10 -10
  38. package/dist/app/projection-list-shim.d.ts +5 -5
  39. package/dist/app/projection-list-shim.js +17 -18
  40. package/dist/app/qn.js +10 -12
  41. package/dist/app/qualify-screen-id.js +1 -1
  42. package/dist/app/screen-access.js +2 -2
  43. package/dist/app/screen-shared.js +1 -1
  44. package/dist/app/secret-mint-body.js +4 -4
  45. package/dist/app/write-failed-error.js +8 -7
  46. package/dist/components/edit-mounts.d.ts +2 -2
  47. package/dist/components/embedded-list-field.js +5 -5
  48. package/dist/components/grid-cell-for-field.d.ts +1 -1
  49. package/dist/components/reference-create-dialog.js +3 -3
  50. package/dist/components/related-list-section.d.ts +1 -1
  51. package/dist/components/related-list-section.js +15 -2
  52. package/dist/components/render-edit-action-button.d.ts +1 -3
  53. package/dist/components/render-edit-action-button.js +2 -2
  54. package/dist/components/render-edit-logic.js +1 -1
  55. package/dist/components/render-edit-types.d.ts +6 -6
  56. package/dist/components/render-edit.js +74 -73
  57. package/dist/components/render-field.d.ts +7 -7
  58. package/dist/components/render-field.js +62 -61
  59. package/dist/components/render-list.d.ts +42 -43
  60. package/dist/components/render-list.js +93 -84
  61. package/dist/components/use-wizard-form-draft.js +10 -10
  62. package/dist/context/dispatcher-context.js +5 -5
  63. package/dist/context/user-roles-context.js +2 -2
  64. package/dist/flow-diagram/render.js +9 -1
  65. package/dist/headless/apex/index.d.ts +1 -2
  66. package/dist/headless/apex/index.js +5 -6
  67. package/dist/headless/form/form-controller.js +14 -6
  68. package/dist/headless/form/types.d.ts +1 -0
  69. package/dist/headless/form/zod-bridge.js +2 -2
  70. package/dist/headless/format/index.js +89 -64
  71. package/dist/headless/format/money.js +1 -1
  72. package/dist/headless/index.js +2 -2
  73. package/dist/headless/view-model/edit.js +4 -2
  74. package/dist/headless/view-model/list.js +184 -150
  75. package/dist/headless/view-model/types.d.ts +6 -3
  76. package/dist/hooks/live-refetch-scheduler.js +4 -0
  77. package/dist/hooks/reference-limits.js +11 -12
  78. package/dist/hooks/use-list-url-state.d.ts +13 -13
  79. package/dist/hooks/use-list-url-state.js +21 -22
  80. package/dist/hooks/use-query.js +3 -3
  81. package/dist/hooks/use-reference-lookup.d.ts +9 -1
  82. package/dist/hooks/use-reference-lookup.js +32 -19
  83. package/dist/hooks/use-store.js +18 -22
  84. package/dist/hooks/use-stream-handler.js +4 -2
  85. package/dist/i18n-defaults.js +6 -8
  86. package/dist/i18n.d.ts +10 -10
  87. package/dist/i18n.js +25 -25
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.js +6 -4
  90. package/dist/live/dispatcher-live.js +2 -2
  91. package/dist/primitives.d.ts +244 -221
  92. package/dist/primitives.js +1 -1
  93. package/dist/sse/live-events.d.ts +5 -5
  94. package/dist/sse/live-events.js +17 -17
  95. package/dist/tokens.d.ts +8 -8
  96. package/dist/tokens.js +16 -17
  97. package/dist/web/app/browser-locale.d.ts +9 -9
  98. package/dist/web/app/browser-locale.js +19 -19
  99. package/dist/web/app/client-plugin.d.ts +44 -52
  100. package/dist/web/app/client-plugin.js +4 -5
  101. package/dist/web/app/create-app.d.ts +28 -31
  102. package/dist/web/app/create-app.js +29 -25
  103. package/dist/web/app/dashboard-body.js +17 -1
  104. package/dist/web/app/dashboard-panels.js +12 -2
  105. package/dist/web/app/document-lang-sync.js +1 -1
  106. package/dist/web/app/draft-storage.js +1 -1
  107. package/dist/web/app/nav-providers-context.d.ts +3 -3
  108. package/dist/web/app/nav-providers-context.js +3 -3
  109. package/dist/web/app/nav.d.ts +18 -17
  110. package/dist/web/app/nav.js +66 -64
  111. package/dist/web/app/rich-content-editor.js +1 -1
  112. package/dist/web/components/config-cascade.js +17 -18
  113. package/dist/web/components/config-source-badge.js +4 -4
  114. package/dist/web/index.d.ts +1 -1
  115. package/dist/web/index.js +1 -1
  116. package/dist/web/layout/app-layout.d.ts +9 -9
  117. package/dist/web/layout/app-layout.js +2 -2
  118. package/dist/web/layout/avatar.d.ts +4 -3
  119. package/dist/web/layout/avatar.js +15 -17
  120. package/dist/web/layout/default-app-shell.d.ts +22 -23
  121. package/dist/web/layout/editor-panel.js +3 -3
  122. package/dist/web/layout/nav-tree.js +86 -84
  123. package/dist/web/layout/profile-menu.d.ts +1 -1
  124. package/dist/web/layout/shell-header.js +2 -1
  125. package/dist/web/layout/sidebar-brand.d.ts +7 -7
  126. package/dist/web/layout/sidebar-user.d.ts +1 -1
  127. package/dist/web/layout/sidebar-user.js +4 -4
  128. package/dist/web/layout/sidebar.d.ts +8 -8
  129. package/dist/web/layout/sidebar.js +7 -7
  130. package/dist/web/layout/target-resolver-stub.d.ts +5 -5
  131. package/dist/web/layout/target-resolver-stub.js +20 -20
  132. package/dist/web/layout/target-url.d.ts +7 -7
  133. package/dist/web/layout/target-url.js +22 -22
  134. package/dist/web/layout/theme-toggle.js +4 -5
  135. package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
  136. package/dist/web/layout/uniform-bar-controls.js +4 -0
  137. package/dist/web/layout/workspace-shell.d.ts +21 -21
  138. package/dist/web/layout/workspace-shell.js +29 -30
  139. package/dist/web/lib/cn.d.ts +4 -4
  140. package/dist/web/lib/cn.js +4 -4
  141. package/dist/web/lib/file-ref-download.js +4 -1
  142. package/dist/web/primitives/action-menu.d.ts +8 -8
  143. package/dist/web/primitives/action-menu.js +14 -13
  144. package/dist/web/primitives/calendar-popover.d.ts +1 -1
  145. package/dist/web/primitives/calendar-popover.js +10 -10
  146. package/dist/web/primitives/card-styles.js +1 -1
  147. package/dist/web/primitives/card.js +2 -2
  148. package/dist/web/primitives/code-input.d.ts +6 -0
  149. package/dist/web/primitives/code-input.js +31 -0
  150. package/dist/web/primitives/combobox.d.ts +12 -12
  151. package/dist/web/primitives/combobox.js +34 -35
  152. package/dist/web/primitives/data-table-cell.js +2 -2
  153. package/dist/web/primitives/data-table.js +6 -6
  154. package/dist/web/primitives/date-field.js +2 -2
  155. package/dist/web/primitives/date-input.js +4 -4
  156. package/dist/web/primitives/default-form.js +2 -2
  157. package/dist/web/primitives/dropdown-menu.js +14 -15
  158. package/dist/web/primitives/embedded-list-input.js +3 -3
  159. package/dist/web/primitives/facet-filter.js +2 -2
  160. package/dist/web/primitives/field.js +9 -8
  161. package/dist/web/primitives/file-thumbnail.d.ts +3 -0
  162. package/dist/web/primitives/file-thumbnail.js +5 -0
  163. package/dist/web/primitives/form.d.ts +1 -1
  164. package/dist/web/primitives/form.js +6 -6
  165. package/dist/web/primitives/input.js +8 -3
  166. package/dist/web/primitives/layout-elements.js +7 -5
  167. package/dist/web/primitives/layout.js +2 -2
  168. package/dist/web/primitives/located-timestamp-input.js +4 -4
  169. package/dist/web/primitives/money-input.js +13 -17
  170. package/dist/web/primitives/page-header.js +3 -1
  171. package/dist/web/primitives/pager.js +1 -1
  172. package/dist/web/primitives/row-actions.js +6 -7
  173. package/dist/web/primitives/secret-reveal.js +1 -1
  174. package/dist/web/primitives/timestamp-input.d.ts +9 -9
  175. package/dist/web/primitives/timestamp-input.js +21 -21
  176. package/dist/web/primitives/toast.js +12 -13
  177. package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
  178. package/dist/web/primitives/use-focus-on-mount.js +9 -0
  179. package/dist/web/sse/live-events.d.ts +6 -6
  180. package/dist/web/sse/live-events.js +3 -3
  181. package/dist/web/tokens.d.ts +11 -12
  182. package/dist/web/tokens.js +35 -37
  183. package/dist/web/version/update-checker.js +14 -13
  184. package/dist/web/widgets/ai-text-field.js +1 -1
  185. package/dist/web/widgets/bounded-load-cache.d.ts +1 -1
  186. package/dist/web/widgets/bounded-load-cache.js +10 -4
  187. package/dist/web/widgets/charts.d.ts +10 -10
  188. package/dist/web/widgets/charts.js +8 -8
  189. package/dist/web/widgets/choice-cards.d.ts +17 -0
  190. package/dist/web/widgets/choice-cards.js +12 -0
  191. package/dist/web/widgets/collapsible-section.d.ts +3 -3
  192. package/dist/web/widgets/collapsible-section.js +7 -7
  193. package/dist/web/widgets/detail-list.d.ts +5 -3
  194. package/dist/web/widgets/detail-list.js +5 -3
  195. package/dist/web/widgets/feed-list.d.ts +2 -2
  196. package/dist/web/widgets/feed-list.js +2 -2
  197. package/dist/web/widgets/form-fields.d.ts +14 -14
  198. package/dist/web/widgets/form-fields.js +14 -13
  199. package/dist/web/widgets/index.d.ts +1 -0
  200. package/dist/web/widgets/index.js +5 -4
  201. package/dist/web/widgets/infinity-list.js +3 -3
  202. package/dist/web/widgets/mode-switch.d.ts +2 -2
  203. package/dist/web/widgets/mode-switch.js +2 -2
  204. package/dist/web/widgets/pdf-preview.js +1 -1
  205. package/dist/web/widgets/plan-card.d.ts +2 -0
  206. package/dist/web/widgets/plan-card.js +1 -1
  207. package/dist/web/widgets/progress-list.d.ts +1 -2
  208. package/dist/web/widgets/progress-list.js +1 -2
  209. package/dist/web/widgets/query-table.d.ts +6 -6
  210. package/dist/web/widgets/result-panel.d.ts +16 -16
  211. package/dist/web/widgets/result-panel.js +10 -10
  212. package/dist/web/widgets/section-card.d.ts +3 -3
  213. package/dist/web/widgets/section-card.js +3 -3
  214. package/dist/web/widgets/stat.d.ts +4 -4
  215. package/dist/web/widgets/stat.js +6 -6
  216. package/dist/web/widgets/states.d.ts +4 -4
  217. package/dist/web/widgets/states.js +4 -4
  218. package/dist/web/widgets/upload-zone.js +13 -5
  219. package/dist/web/widgets/use-draft.d.ts +7 -7
  220. package/dist/web/widgets/use-draft.js +7 -7
  221. package/package.json +6 -6
  222. package/src/changes.json +49 -0
@@ -6,6 +6,7 @@ import { rendererError, rendererWarn } from "../../dev-console.js";
6
6
  import { AppFeaturesProvider, ClientActionsProvider, ColumnRenderersProvider, ContentEditorsProvider, CustomScreensProvider, createSessionEndedSignal, DashboardBodyProvider, DispatcherProvider, DraftStorageProvider, ErrorBoundary, ExtensionSectionsProvider, hasDetailScreen, KumikoScreen, kumikoDefaultTranslations, LeaveGuardDialog, LiveEventsProvider, LocaleProvider, mergeTranslations, NavProvider, navigateWithReturnTo, PrimitivesProvider, qualifyScreenId, ScreenMissingState, SessionEndedSignalProvider, TokensProvider, toAppSchema, translationsByLocaleFromKeys, useNav, usePrimitives, useTranslation, withSessionEndedDetection, } from "../../index.js";
7
7
  import { createLiveDispatcher } from "../../live/index.js";
8
8
  import { lastSegment } from "../layout/nav-tree.js";
9
+ import { clearFileMetaCache } from "../primitives/file-thumbnail.js";
9
10
  import { defaultPrimitives, ScreenWidthProvider } from "../primitives/index.js";
10
11
  import { ToastProvider } from "../primitives/toast.js";
11
12
  import { createEventSourceLiveEvents } from "../sse/live-events.js";
@@ -26,20 +27,20 @@ import { ScreenErrorFallback } from "./screen-error-fallback.js";
26
27
  import { ToastActionNotifier } from "./toast-action-notifier.js";
27
28
  const EMPTY_FEATURES = [];
28
29
  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.
30
+ // Qualifies a navProvider key to its nav QN. Local IDs
31
+ // (e.g. "content") are qualified with the feature name as in r.nav;
32
+ // already qualified QNs (the app registers navigation for a bundled feature
33
+ // and passes the QN as navId, e.g. "publicstatus:nav:content") pass through
34
+ // unchanged. This MUST stay consistent with qualifyNavId, otherwise the
35
+ // schema-side NavTree node cannot find its provider.
35
36
  export function qualifyNavProviderKey(feature, id) {
36
37
  return id.includes(":nav:") ? id : `${feature}:nav:${id}`;
37
38
  }
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.
39
+ // Nav-provider map: a navProvider attaches dynamic children to a specific
40
+ // r.nav({provider:true}) node by QN. Local nav IDs are qualified with the
41
+ // feature name as in r.nav; already qualified QNs (for cross-feature use,
42
+ // e.g. when the app registers navigation for a bundled feature) pass through
43
+ // unchanged.
43
44
  //
44
45
  // Schema-derived providers (r.contentCollection) run first and hold the weaker
45
46
  // claim on a QN — an explicitly registered navProvider overrides them without
@@ -92,7 +93,7 @@ function readInjectedSchema() {
92
93
  // screen a feature only registers for manual app-side placement (e.g.
93
94
  // auth-mfa's enable screen) can win by declaration order alone, landing
94
95
  // every app without an explicit `screenQn` on a screen nobody wired a
95
- // component for (#1258).
96
+ // component for.
96
97
  function firstNavReachableScreenQn(features, acceptsScreen) {
97
98
  // NavDefinition.screen carries two shapes in practice: most bundled
98
99
  // features author it pre-qualified ("tenant:screen:members"), but the
@@ -117,7 +118,7 @@ function firstNavReachableScreenQn(features, acceptsScreen) {
117
118
  }
118
119
  // Only screens whose `access` excludes nobody: on an unprojected schema the
119
120
  // landing is picked without knowing the user's roles, so a role-restricted
120
- // screen would land every non-admin on access-denied (#1176).
121
+ // screen would land every non-admin on access-denied.
121
122
  export function firstOpenScreenQn(features) {
122
123
  return firstNavReachableScreenQn(features, (s) => s.access === undefined || isOpenToAllGranted(s.access));
123
124
  }
@@ -177,15 +178,15 @@ export function mergeContentEditors(clientFeatures) {
177
178
  // declare a `type: "custom"` screen that's reachable directly by URL
178
179
  // even without nav placement — without this check, a missing client
179
180
  // plugin only surfaces as an error placeholder to whoever happens to
180
- // open that URL (kumiko-framework#2025). Screens flagged `dormant`
181
+ // open that URL. Screens flagged `dormant`
181
182
  // (e.g. user-data-rights privacy-center, auth-mfa's enable screen) are
182
183
  // registered without a self-owned r.nav on purpose — an app opts in by
183
184
  // navving them explicitly, so a consumer that hasn't done that yet isn't
184
- // missing anything (kumiko-framework#2034). Still dev-only: several
185
+ // missing anything. Still dev-only: several
185
186
  // bundled screens that ARE self-navved (e.g. compliance-profiles'
186
187
  // 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
188
+ // consumer apps today — running this in
189
+ // production before those are fixed would just
189
190
  // spam every affected app's console.
190
191
  function warnMissingCustomScreens(app, customScreens) {
191
192
  if (typeof process === "undefined" || process.env.NODE_ENV === "production")
@@ -209,7 +210,7 @@ function KumikoAppRoot(props) {
209
210
  // 1. clientFeatures.translations — app overrides always win, even
210
211
  // against the framework's own labels.
211
212
  // 2. schemaTranslations — server-authored r.translations, projected
212
- // verbatim by buildAppSchema (#1059). Without this bundle, nav/
213
+ // verbatim by buildAppSchema. Without this bundle, nav/
213
214
  // screen labels only resolve if an app ALSO duplicates them in its
214
215
  // own web/i18n.ts — most bundled features never did, labels then
215
216
  // render as raw i18n keys.
@@ -255,9 +256,13 @@ function AppSchemaBoundary({ app, screenQn, schemaUrl, navAdapter, onAppLoaded,
255
256
  onUnmountResetSchemaRef.current = onUnmountResetSchema;
256
257
  // A real unmount (the gate withdrawing children, e.g. on logout) resets
257
258
  // the schema so the next mount re-fetches instead of reusing this
258
- // session's — fetch-mode only, see isFetchMode in KumikoAppRoot.
259
+ // session's — fetch-mode only, see isFetchMode in KumikoAppRoot. The file
260
+ // meta cache is dropped in every mode.
259
261
  useEffect(() => {
260
- return () => onUnmountResetSchemaRef.current?.();
262
+ return () => {
263
+ clearFileMetaCache();
264
+ onUnmountResetSchemaRef.current?.();
265
+ };
261
266
  }, []);
262
267
  if (app === undefined) {
263
268
  return (_jsx(AppSchemaFetchBoot, { onLoaded: onAppLoaded, ...(schemaUrl !== undefined && { schemaUrl }) }));
@@ -388,7 +393,7 @@ function findEditScreenForEntity(features, entityName) {
388
393
  }
389
394
  return undefined;
390
395
  }
391
- // fw#2640: a list screen with no reachable click target (no `detailFor`
396
+ // A list screen with no reachable click target (no `detailFor`
392
397
  // screen for its entity, no app-wide onRowClick, no entityEdit screen)
393
398
  // must render without the row-click affordance instead of a silent no-op —
394
399
  // mirrors the precedence effectiveOnRowClick itself resolves at click time.
@@ -426,7 +431,6 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
426
431
  };
427
432
  }
428
433
  const shortId = nav.route.screenId;
429
- // Suche das Feature dessen Screens den short id enthalten.
430
434
  let matchedFeature;
431
435
  for (const f of app.features) {
432
436
  if (f.screens.some((s) => s.id === shortId)) {
@@ -452,7 +456,7 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
452
456
  screenId: lastSegment(qn),
453
457
  ...(entityId !== undefined && { entityId }),
454
458
  };
455
- // Precedence (fw#2164): detailFor screen, then onRowClick, then the
459
+ // Precedence: detailFor screen, then onRowClick, then the
456
460
  // entityEdit-search fallback below. An explicit rowActions
457
461
  // rowClick:true already wins over all of this — kumiko-screen.tsx
458
462
  // handles it before onRowClick is ever called.
@@ -477,7 +481,7 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
477
481
  }
478
482
  };
479
483
  }, [app, activeScreen, onRowClick, nav, qn, entityId]);
480
- // Copy-link action (Issue #912) for entityEdit update screens. Builds
484
+ // Copy-link action for entityEdit update screens. Builds
481
485
  // the absolute permalink URL from the current route + copies it —
482
486
  // `navigator`/`window` are allowed here (renderer/web, not a
483
487
  // platform-neutral package). No button without entityId (create-mode).
@@ -499,7 +503,7 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
499
503
  }, [nav]);
500
504
  if (feature === undefined || activeScreen === undefined) {
501
505
  const landingExists = findOwnerFeature(app.features, fallbackQn) !== undefined;
502
- return (_jsx(ScreenMissingState, { ...(landingExists && { homeScreenId: lastSegment(fallbackQn) }), screenQn: qn }));
506
+ return (_jsx(ScreenMissingState, { ...(landingExists && { homeScreenId: lastSegment(fallbackQn) }), screenQn: nav.route?.screenId ?? qn }));
503
507
  }
504
508
  // KumikoScreen takes a single-feature schema: the feature owning the active screen.
505
509
  const schemaForScreen = feature;
@@ -140,6 +140,20 @@ function useVisibilityGateResults() {
140
140
  }, []);
141
141
  return { results, report, forget };
142
142
  }
143
+ const NOOP_REFETCH = async () => { };
144
+ // A gate reports through an effect, so for its first commit (and after a payload change) no
145
+ // result exists yet; without a placeholder that reads as "hidden" and flashes the fallback.
146
+ function withPendingGates(results, gateKeys) {
147
+ if (gateKeys.every((key) => results.has(key)))
148
+ return results;
149
+ const merged = new Map(results);
150
+ for (const key of gateKeys) {
151
+ if (!merged.has(key)) {
152
+ merged.set(key, { data: null, error: null, loading: true, refetch: NOOP_REFETCH });
153
+ }
154
+ }
155
+ return merged;
156
+ }
143
157
  function distinctVisibilityGates(panels, screenParams) {
144
158
  const gates = new Map();
145
159
  for (const panel of panels) {
@@ -227,6 +241,8 @@ export function WebDashboardBody({ featureName, screen, translate, }) {
227
241
  const { loadedAtMs, reportLoaded } = useLoadedAt(screenParams);
228
242
  const visibilityGates = distinctVisibilityGates(screen.panels, screenParams);
229
243
  const gateResults = useVisibilityGateResults();
244
+ const gateKeysSignature = visibilityGates.map((gate) => gate.key).join("\n");
245
+ const gateResultsWithPending = useMemo(() => withPendingGates(gateResults.results, gateKeysSignature === "" ? [] : gateKeysSignature.split("\n")), [gateResults.results, gateKeysSignature]);
230
246
  // The record query is not a panel query: it needs the interval before the
231
247
  // registry context below exists, and `refresh.field` reads from its own row.
232
248
  const [refreshIntervalMs, setRefreshIntervalMs] = useState(() => resolveRefreshIntervalMs(screen.refresh, null));
@@ -271,7 +287,7 @@ export function WebDashboardBody({ featureName, screen, translate, }) {
271
287
  // The shell header owns the title; the badge and the actions sit in it there.
272
288
  const badgeInHeader = scopeBadge !== null && usesPageHeaderSlot;
273
289
  const showPageHeader = usesPageHeaderSlot && (badgeInHeader || recordTitle !== undefined || headerActionsBar !== null);
274
- return (_jsx(PanelLoadedContext.Provider, { value: reportLoaded, children: _jsx(DashboardQueryContext.Provider, { value: queryRuntime, children: _jsxs(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: [showPageHeader && (_jsx(PageHeader, { ...(recordTitle !== undefined && { title: recordTitle }), ...(badgeInHeader && { status: scopeBadge }), ...(headerActionsBar !== null && { actions: headerActionsBar }) })), _jsxs("div", { className: "flex flex-wrap items-end justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [!usesPageHeaderSlot && recordTitle !== undefined && (_jsx(Heading, { variant: "page", testId: `dashboard-${screen.id}-record-title`, children: recordTitle })), !badgeInHeader && scopeBadge, picker] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [!usesPageHeaderSlot && headerActionsBar, screen.showUpdatedAt !== false && (_jsx("span", { "data-testid": `dashboard-${screen.id}-updated-at`, className: "text-xs tabular-nums text-muted-foreground", children: t("kumiko.dashboard.updated-at", { time: formatDateTime(loadedAtMs) }) })), rangeControl] })] }), actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })), recordQuery.error !== null && recordRow === null && (_jsxs("div", { role: "alert", "data-testid": `dashboard-${screen.id}-record-error`, className: "flex flex-col items-start gap-2 text-sm", children: [_jsx("span", { className: "text-status-bad", children: dispatcherErrorText(recordQuery.error, t) }), _jsx(Button, { variant: "secondary", onClick: () => void refetchRecord(), children: t("kumiko.dashboard.panel.error.retry") })] })), description !== undefined && (_jsx(Text, { variant: "muted", testId: `dashboard-${screen.id}-description`, children: description })), scope?.notice !== undefined && (_jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-scope-notice`, children: effectiveTranslate(scope.notice) })), _jsxs(VisibilityQueryContext.Provider, { value: gateResults.results, children: [_jsx(VisibilityGateQueries, { gates: visibilityGates, onResult: gateResults.report, onForget: gateResults.forget }), _jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: screen.panels.map((panel) => {
290
+ return (_jsx(PanelLoadedContext.Provider, { value: reportLoaded, children: _jsx(DashboardQueryContext.Provider, { value: queryRuntime, children: _jsxs(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: [showPageHeader && (_jsx(PageHeader, { ...(recordTitle !== undefined && { title: recordTitle }), ...(badgeInHeader && { status: scopeBadge }), ...(headerActionsBar !== null && { actions: headerActionsBar }) })), _jsxs("div", { className: "flex flex-wrap items-end justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [!usesPageHeaderSlot && recordTitle !== undefined && (_jsx(Heading, { variant: "page", testId: `dashboard-${screen.id}-record-title`, children: recordTitle })), !badgeInHeader && scopeBadge, picker] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [!usesPageHeaderSlot && headerActionsBar, screen.showUpdatedAt !== false && (_jsx("span", { "data-testid": `dashboard-${screen.id}-updated-at`, className: "text-xs tabular-nums text-muted-foreground", children: t("kumiko.dashboard.updated-at", { time: formatDateTime(loadedAtMs) }) })), rangeControl] })] }), actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })), recordQuery.error !== null && recordRow === null && (_jsxs("div", { role: "alert", "data-testid": `dashboard-${screen.id}-record-error`, className: "flex flex-col items-start gap-2 text-sm", children: [_jsx("span", { className: "text-status-bad", children: dispatcherErrorText(recordQuery.error, t) }), _jsx(Button, { variant: "secondary", onClick: () => void refetchRecord(), children: t("kumiko.dashboard.panel.error.retry") })] })), description !== undefined && (_jsx(Text, { variant: "muted", testId: `dashboard-${screen.id}-description`, children: description })), scope?.notice !== undefined && (_jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-scope-notice`, children: effectiveTranslate(scope.notice) })), _jsxs(VisibilityQueryContext.Provider, { value: gateResultsWithPending, children: [_jsx(VisibilityGateQueries, { gates: visibilityGates, onResult: gateResults.report, onForget: gateResults.forget }), _jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: screen.panels.map((panel) => {
275
291
  if (panel.kind === "screen") {
276
292
  return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, translate: effectiveTranslate }, panel.id));
277
293
  }
@@ -311,7 +311,7 @@ function TextBlockView({ block, translate, }) {
311
311
  ? "text-sm font-medium text-foreground"
312
312
  : "text-sm text-muted-foreground", children: translate(block.text) }));
313
313
  case "list":
314
- return (_jsx("ul", { "data-block": "list", className: "list-disc space-y-1 pl-5 text-sm text-muted-foreground", children: block.items.map((item) => (_jsx("li", { children: translate(item) }, item))) }));
314
+ return (_jsx("ul", { "data-block": "list", className: "list-disc space-y-1 pl-5 text-sm text-muted-foreground", children: withOccurrenceKeys(block.items, (item) => item).map(({ entry, key }) => (_jsx("li", { children: translate(entry) }, key))) }));
315
315
  case "code":
316
316
  return (_jsx("pre", { "data-block": "code", className: "overflow-x-auto rounded-md bg-muted p-3 text-xs", children: _jsx("code", { children: block.code }) }));
317
317
  case "hint":
@@ -330,6 +330,16 @@ function TextBlockLink({ block, translate, }) {
330
330
  const target = { screenId: lastSegment(block.screen) };
331
331
  return (_jsx(Link, { href: nav.hrefFor(target), onPress: () => nav.navigate(target), testId: `dashboard-text-link-${target.screenId}`, children: translate(block.text) }));
332
332
  }
333
+ // Content-derived keys collide for identical entries; the occurrence count keeps them unique.
334
+ function withOccurrenceKeys(entries, contentKey) {
335
+ const seen = new Map();
336
+ return entries.map((entry) => {
337
+ const content = contentKey(entry);
338
+ const occurrence = seen.get(content) ?? 0;
339
+ seen.set(content, occurrence + 1);
340
+ return { entry, key: `${content}#${occurrence}` };
341
+ });
342
+ }
333
343
  function textBlockKey(block) {
334
344
  switch (block.kind) {
335
345
  case "list":
@@ -341,7 +351,7 @@ function textBlockKey(block) {
341
351
  }
342
352
  }
343
353
  export function TextPanelBody({ panel, label, translate, }) {
344
- const blocks = (_jsx("div", { className: "flex flex-col gap-3", children: panel.blocks.map((block) => (_jsx(TextBlockView, { block: block, translate: translate }, textBlockKey(block)))) }));
354
+ const blocks = (_jsx("div", { className: "flex flex-col gap-3", children: withOccurrenceKeys(panel.blocks, textBlockKey).map(({ entry, key }) => (_jsx(TextBlockView, { block: entry, translate: translate }, key))) }));
345
355
  const testId = `dashboard-panel-${panel.id}`;
346
356
  if (label !== undefined) {
347
357
  return (_jsx(PanelShell, { testId: testId, title: label, children: blocks }));
@@ -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"));