@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.
- package/LICENSE +21 -57
- package/dist/app/action-form-body.d.ts +2 -2
- package/dist/app/action-form-body.js +12 -4
- package/dist/app/action-form-shim.d.ts +8 -8
- package/dist/app/action-form-shim.js +20 -22
- package/dist/app/action-form-support.d.ts +1 -0
- package/dist/app/action-form-support.js +1 -1
- package/dist/app/app-features-context.js +4 -4
- package/dist/app/column-renderers.d.ts +7 -9
- package/dist/app/column-renderers.js +15 -18
- package/dist/app/config-edit-body.js +7 -3
- package/dist/app/config-edit-shim.d.ts +7 -8
- package/dist/app/config-edit-shim.js +18 -20
- package/dist/app/custom-screens.d.ts +3 -3
- package/dist/app/custom-screens.js +8 -8
- package/dist/app/dashboard-body.d.ts +2 -2
- package/dist/app/dashboard-body.js +2 -2
- package/dist/app/entity-edit-prefill.d.ts +11 -0
- package/dist/app/entity-edit-prefill.js +18 -13
- package/dist/app/entity-edit-screen.js +4 -4
- package/dist/app/entity-list-screen.js +3 -3
- package/dist/app/extension-form-submit.d.ts +1 -1
- package/dist/app/extension-form-submit.js +14 -15
- package/dist/app/extension-sections.d.ts +29 -28
- package/dist/app/extension-sections.js +10 -10
- package/dist/app/feature-schema.js +11 -11
- package/dist/app/form-schema.js +2 -2
- package/dist/app/header-actions-bar.js +2 -2
- package/dist/app/kumiko-screen.js +8 -10
- package/dist/app/layout-fields.js +1 -1
- package/dist/app/list-facets.js +3 -3
- package/dist/app/nav.d.ts +10 -10
- package/dist/app/nav.js +13 -13
- package/dist/app/projection-detail-body.js +4 -4
- package/dist/app/projection-detail-shim.d.ts +6 -5
- package/dist/app/projection-detail-shim.js +23 -22
- package/dist/app/projection-list-body.js +10 -10
- package/dist/app/projection-list-shim.d.ts +5 -5
- package/dist/app/projection-list-shim.js +17 -18
- package/dist/app/qn.js +10 -12
- package/dist/app/qualify-screen-id.js +1 -1
- package/dist/app/screen-access.js +2 -2
- package/dist/app/screen-shared.js +1 -1
- package/dist/app/secret-mint-body.js +4 -4
- package/dist/app/write-failed-error.js +8 -7
- package/dist/components/edit-mounts.d.ts +2 -2
- package/dist/components/embedded-list-field.js +5 -5
- package/dist/components/grid-cell-for-field.d.ts +1 -1
- package/dist/components/reference-create-dialog.js +3 -3
- package/dist/components/related-list-section.d.ts +1 -1
- package/dist/components/related-list-section.js +15 -2
- package/dist/components/render-edit-action-button.d.ts +1 -3
- package/dist/components/render-edit-action-button.js +2 -2
- package/dist/components/render-edit-logic.js +1 -1
- package/dist/components/render-edit-types.d.ts +6 -6
- package/dist/components/render-edit.js +74 -73
- package/dist/components/render-field.d.ts +7 -7
- package/dist/components/render-field.js +62 -61
- package/dist/components/render-list.d.ts +42 -43
- package/dist/components/render-list.js +93 -84
- package/dist/components/use-wizard-form-draft.js +10 -10
- package/dist/context/dispatcher-context.js +5 -5
- package/dist/context/user-roles-context.js +2 -2
- package/dist/flow-diagram/render.js +9 -1
- package/dist/headless/apex/index.d.ts +1 -2
- package/dist/headless/apex/index.js +5 -6
- package/dist/headless/form/form-controller.js +14 -6
- package/dist/headless/form/types.d.ts +1 -0
- package/dist/headless/form/zod-bridge.js +2 -2
- package/dist/headless/format/index.js +89 -64
- package/dist/headless/format/money.js +1 -1
- package/dist/headless/index.js +2 -2
- package/dist/headless/view-model/edit.js +4 -2
- package/dist/headless/view-model/list.js +184 -150
- package/dist/headless/view-model/types.d.ts +6 -3
- package/dist/hooks/live-refetch-scheduler.js +4 -0
- package/dist/hooks/reference-limits.js +11 -12
- package/dist/hooks/use-list-url-state.d.ts +13 -13
- package/dist/hooks/use-list-url-state.js +21 -22
- package/dist/hooks/use-query.js +3 -3
- package/dist/hooks/use-reference-lookup.d.ts +9 -1
- package/dist/hooks/use-reference-lookup.js +32 -19
- package/dist/hooks/use-store.js +18 -22
- package/dist/hooks/use-stream-handler.js +4 -2
- package/dist/i18n-defaults.js +6 -8
- package/dist/i18n.d.ts +10 -10
- package/dist/i18n.js +25 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -4
- package/dist/live/dispatcher-live.js +2 -2
- package/dist/primitives.d.ts +244 -221
- package/dist/primitives.js +1 -1
- package/dist/sse/live-events.d.ts +5 -5
- package/dist/sse/live-events.js +17 -17
- package/dist/tokens.d.ts +8 -8
- package/dist/tokens.js +16 -17
- package/dist/web/app/browser-locale.d.ts +9 -9
- package/dist/web/app/browser-locale.js +19 -19
- package/dist/web/app/client-plugin.d.ts +44 -52
- package/dist/web/app/client-plugin.js +4 -5
- package/dist/web/app/create-app.d.ts +28 -31
- package/dist/web/app/create-app.js +29 -25
- package/dist/web/app/dashboard-body.js +17 -1
- package/dist/web/app/dashboard-panels.js +12 -2
- package/dist/web/app/document-lang-sync.js +1 -1
- package/dist/web/app/draft-storage.js +1 -1
- package/dist/web/app/nav-providers-context.d.ts +3 -3
- package/dist/web/app/nav-providers-context.js +3 -3
- package/dist/web/app/nav.d.ts +18 -17
- package/dist/web/app/nav.js +66 -64
- package/dist/web/app/rich-content-editor.js +1 -1
- package/dist/web/components/config-cascade.js +17 -18
- package/dist/web/components/config-source-badge.js +4 -4
- package/dist/web/index.d.ts +1 -1
- package/dist/web/index.js +1 -1
- package/dist/web/layout/app-layout.d.ts +9 -9
- package/dist/web/layout/app-layout.js +2 -2
- package/dist/web/layout/avatar.d.ts +4 -3
- package/dist/web/layout/avatar.js +15 -17
- package/dist/web/layout/default-app-shell.d.ts +22 -23
- package/dist/web/layout/editor-panel.js +3 -3
- package/dist/web/layout/nav-tree.js +86 -84
- package/dist/web/layout/profile-menu.d.ts +1 -1
- package/dist/web/layout/shell-header.js +2 -1
- package/dist/web/layout/sidebar-brand.d.ts +7 -7
- package/dist/web/layout/sidebar-user.d.ts +1 -1
- package/dist/web/layout/sidebar-user.js +4 -4
- package/dist/web/layout/sidebar.d.ts +8 -8
- package/dist/web/layout/sidebar.js +7 -7
- package/dist/web/layout/target-resolver-stub.d.ts +5 -5
- package/dist/web/layout/target-resolver-stub.js +20 -20
- package/dist/web/layout/target-url.d.ts +7 -7
- package/dist/web/layout/target-url.js +22 -22
- package/dist/web/layout/theme-toggle.js +4 -5
- package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
- package/dist/web/layout/uniform-bar-controls.js +4 -0
- package/dist/web/layout/workspace-shell.d.ts +21 -21
- package/dist/web/layout/workspace-shell.js +29 -30
- package/dist/web/lib/cn.d.ts +4 -4
- package/dist/web/lib/cn.js +4 -4
- package/dist/web/lib/file-ref-download.js +4 -1
- package/dist/web/primitives/action-menu.d.ts +8 -8
- package/dist/web/primitives/action-menu.js +14 -13
- package/dist/web/primitives/calendar-popover.d.ts +1 -1
- package/dist/web/primitives/calendar-popover.js +10 -10
- package/dist/web/primitives/card-styles.js +1 -1
- package/dist/web/primitives/card.js +2 -2
- package/dist/web/primitives/code-input.d.ts +6 -0
- package/dist/web/primitives/code-input.js +31 -0
- package/dist/web/primitives/combobox.d.ts +12 -12
- package/dist/web/primitives/combobox.js +34 -35
- package/dist/web/primitives/data-table-cell.js +2 -2
- package/dist/web/primitives/data-table.js +6 -6
- package/dist/web/primitives/date-field.js +2 -2
- package/dist/web/primitives/date-input.js +4 -4
- package/dist/web/primitives/default-form.js +2 -2
- package/dist/web/primitives/dropdown-menu.js +14 -15
- package/dist/web/primitives/embedded-list-input.js +3 -3
- package/dist/web/primitives/facet-filter.js +2 -2
- package/dist/web/primitives/field.js +9 -8
- package/dist/web/primitives/file-thumbnail.d.ts +3 -0
- package/dist/web/primitives/file-thumbnail.js +5 -0
- package/dist/web/primitives/form.d.ts +1 -1
- package/dist/web/primitives/form.js +6 -6
- package/dist/web/primitives/input.js +8 -3
- package/dist/web/primitives/layout-elements.js +7 -5
- package/dist/web/primitives/layout.js +2 -2
- package/dist/web/primitives/located-timestamp-input.js +4 -4
- package/dist/web/primitives/money-input.js +13 -17
- package/dist/web/primitives/page-header.js +3 -1
- package/dist/web/primitives/pager.js +1 -1
- package/dist/web/primitives/row-actions.js +6 -7
- package/dist/web/primitives/secret-reveal.js +1 -1
- package/dist/web/primitives/timestamp-input.d.ts +9 -9
- package/dist/web/primitives/timestamp-input.js +21 -21
- package/dist/web/primitives/toast.js +12 -13
- package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
- package/dist/web/primitives/use-focus-on-mount.js +9 -0
- package/dist/web/sse/live-events.d.ts +6 -6
- package/dist/web/sse/live-events.js +3 -3
- package/dist/web/tokens.d.ts +11 -12
- package/dist/web/tokens.js +35 -37
- package/dist/web/version/update-checker.js +14 -13
- package/dist/web/widgets/ai-text-field.js +1 -1
- package/dist/web/widgets/bounded-load-cache.d.ts +1 -1
- package/dist/web/widgets/bounded-load-cache.js +10 -4
- package/dist/web/widgets/charts.d.ts +10 -10
- package/dist/web/widgets/charts.js +8 -8
- package/dist/web/widgets/choice-cards.d.ts +17 -0
- package/dist/web/widgets/choice-cards.js +12 -0
- package/dist/web/widgets/collapsible-section.d.ts +3 -3
- package/dist/web/widgets/collapsible-section.js +7 -7
- package/dist/web/widgets/detail-list.d.ts +5 -3
- package/dist/web/widgets/detail-list.js +5 -3
- package/dist/web/widgets/feed-list.d.ts +2 -2
- package/dist/web/widgets/feed-list.js +2 -2
- package/dist/web/widgets/form-fields.d.ts +14 -14
- package/dist/web/widgets/form-fields.js +14 -13
- package/dist/web/widgets/index.d.ts +1 -0
- package/dist/web/widgets/index.js +5 -4
- package/dist/web/widgets/infinity-list.js +3 -3
- package/dist/web/widgets/mode-switch.d.ts +2 -2
- package/dist/web/widgets/mode-switch.js +2 -2
- package/dist/web/widgets/pdf-preview.js +1 -1
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +1 -1
- package/dist/web/widgets/progress-list.d.ts +1 -2
- package/dist/web/widgets/progress-list.js +1 -2
- package/dist/web/widgets/query-table.d.ts +6 -6
- package/dist/web/widgets/result-panel.d.ts +16 -16
- package/dist/web/widgets/result-panel.js +10 -10
- package/dist/web/widgets/section-card.d.ts +3 -3
- package/dist/web/widgets/section-card.js +3 -3
- package/dist/web/widgets/stat.d.ts +4 -4
- package/dist/web/widgets/stat.js +6 -6
- package/dist/web/widgets/states.d.ts +4 -4
- package/dist/web/widgets/states.js +4 -4
- package/dist/web/widgets/upload-zone.js +13 -5
- package/dist/web/widgets/use-draft.d.ts +7 -7
- package/dist/web/widgets/use-draft.js +7 -7
- package/package.json +6 -6
- 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
|
-
//
|
|
30
|
-
// (
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
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-
|
|
39
|
-
//
|
|
40
|
-
// in r.nav
|
|
41
|
-
//
|
|
42
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
188
|
-
// production before those are fixed
|
|
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
|
|
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 () =>
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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:
|
|
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((
|
|
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((
|
|
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
|
|
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
|
|
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
|
|
10
|
-
*
|
|
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
|
|
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
|
|
11
|
-
*
|
|
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
|
|
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
|
}
|
package/dist/web/app/nav.d.ts
CHANGED
|
@@ -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
|
|
4
|
-
*
|
|
5
|
-
* `<NavProvider
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* through
|
|
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
|
|
12
|
-
*
|
|
13
|
-
* URLs
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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;
|
package/dist/web/app/nav.js
CHANGED
|
@@ -1,36 +1,37 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
// Browser
|
|
3
|
-
// Types
|
|
4
|
-
//
|
|
5
|
-
// `<KumikoLink>`
|
|
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
|
-
//
|
|
8
|
-
// (
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
|
16
|
-
// (
|
|
17
|
-
//
|
|
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
|
-
//
|
|
20
|
-
// basePath="/admin"),
|
|
21
|
-
//
|
|
22
|
-
// (Not-Found,
|
|
23
|
-
//
|
|
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
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
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
|
|
47
|
-
//
|
|
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
|
|
53
|
-
//
|
|
54
|
-
//
|
|
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
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// Multi-
|
|
114
|
-
//
|
|
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
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
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
|
|
198
|
-
*
|
|
199
|
-
* `<NavProvider
|
|
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
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* through
|
|
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
|
|
206
|
-
*
|
|
207
|
-
* URLs
|
|
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
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
222
|
-
*
|
|
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
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
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
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
// WorkspaceShell
|
|
241
|
-
//
|
|
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
|
-
//
|
|
266
|
-
//
|
|
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
|
|
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"));
|