@cosmicdrift/kumiko-renderer 0.385.0 → 0.387.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -57
- package/dist/app/action-form-body.d.ts +2 -2
- package/dist/app/action-form-body.js +2 -2
- package/dist/app/action-form-shim.d.ts +8 -8
- package/dist/app/action-form-shim.js +20 -22
- 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 +1 -1
- 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.js +9 -9
- 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 +24 -25
- package/dist/app/extension-sections.js +10 -10
- package/dist/app/feature-schema.js +11 -11
- package/dist/app/form-schema.js +1 -1
- package/dist/app/header-actions-bar.js +1 -1
- 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/embedded-list-field.js +4 -4
- 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 +28 -5
- 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 +63 -65
- package/dist/components/render-field.d.ts +7 -7
- package/dist/components/render-field.js +57 -59
- package/dist/components/render-list.d.ts +43 -44
- package/dist/components/render-list.js +63 -67
- 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/headless/apex/index.d.ts +1 -2
- package/dist/headless/apex/index.js +5 -6
- package/dist/headless/form/form-controller.js +6 -6
- package/dist/headless/form/zod-bridge.js +2 -2
- package/dist/headless/format/money.js +1 -1
- package/dist/headless/index.js +2 -2
- package/dist/headless/view-model/edit.js +2 -2
- package/dist/headless/view-model/list.js +3 -4
- package/dist/headless/view-model/types.d.ts +3 -3
- 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.js +16 -17
- package/dist/hooks/use-store.js +18 -22
- package/dist/hooks/use-stream-handler.js +4 -2
- package/dist/i18n-defaults.js +2 -8
- package/dist/i18n.d.ts +10 -10
- package/dist/i18n.js +25 -25
- package/dist/index.js +5 -4
- package/dist/live/dispatcher-live.js +2 -2
- package/dist/primitives.d.ts +225 -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 +21 -22
- 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/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/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/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/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/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 +2 -3
- 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 +1 -1
- package/dist/web/primitives/field.js +9 -8
- package/dist/web/primitives/form.d.ts +1 -1
- package/dist/web/primitives/form.js +6 -6
- package/dist/web/primitives/input.js +2 -2
- package/dist/web/primitives/layout-elements.js +1 -1
- 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/pager.js +1 -1
- package/dist/web/primitives/row-actions.js +3 -4
- 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/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/charts.d.ts +10 -10
- package/dist/web/widgets/charts.js +8 -8
- package/dist/web/widgets/collapsible-section.d.ts +3 -3
- package/dist/web/widgets/collapsible-section.js +6 -6
- package/dist/web/widgets/detail-list.d.ts +1 -3
- package/dist/web/widgets/detail-list.js +1 -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.js +4 -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/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/use-draft.d.ts +7 -7
- package/dist/web/widgets/use-draft.js +7 -7
- package/package.json +6 -6
|
@@ -26,20 +26,20 @@ import { ScreenErrorFallback } from "./screen-error-fallback.js";
|
|
|
26
26
|
import { ToastActionNotifier } from "./toast-action-notifier.js";
|
|
27
27
|
const EMPTY_FEATURES = [];
|
|
28
28
|
const EMPTY_COLLECTIONS = [];
|
|
29
|
-
//
|
|
30
|
-
// (
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
29
|
+
// Qualifies a navProvider key to its nav QN. Local IDs
|
|
30
|
+
// (e.g. "content") are qualified with the feature name as in r.nav;
|
|
31
|
+
// already qualified QNs (the app registers navigation for a bundled feature
|
|
32
|
+
// and passes the QN as navId, e.g. "publicstatus:nav:content") pass through
|
|
33
|
+
// unchanged. This MUST stay consistent with qualifyNavId, otherwise the
|
|
34
|
+
// schema-side NavTree node cannot find its provider.
|
|
35
35
|
export function qualifyNavProviderKey(feature, id) {
|
|
36
36
|
return id.includes(":nav:") ? id : `${feature}:nav:${id}`;
|
|
37
37
|
}
|
|
38
|
-
// Nav-
|
|
39
|
-
//
|
|
40
|
-
// in r.nav
|
|
41
|
-
//
|
|
42
|
-
//
|
|
38
|
+
// Nav-provider map: a navProvider attaches dynamic children to a specific
|
|
39
|
+
// r.nav({provider:true}) node by QN. Local nav IDs are qualified with the
|
|
40
|
+
// feature name as in r.nav; already qualified QNs (for cross-feature use,
|
|
41
|
+
// e.g. when the app registers navigation for a bundled feature) pass through
|
|
42
|
+
// unchanged.
|
|
43
43
|
//
|
|
44
44
|
// Schema-derived providers (r.contentCollection) run first and hold the weaker
|
|
45
45
|
// claim on a QN — an explicitly registered navProvider overrides them without
|
|
@@ -92,7 +92,7 @@ function readInjectedSchema() {
|
|
|
92
92
|
// screen a feature only registers for manual app-side placement (e.g.
|
|
93
93
|
// auth-mfa's enable screen) can win by declaration order alone, landing
|
|
94
94
|
// every app without an explicit `screenQn` on a screen nobody wired a
|
|
95
|
-
// component for
|
|
95
|
+
// component for.
|
|
96
96
|
function firstNavReachableScreenQn(features, acceptsScreen) {
|
|
97
97
|
// NavDefinition.screen carries two shapes in practice: most bundled
|
|
98
98
|
// features author it pre-qualified ("tenant:screen:members"), but the
|
|
@@ -117,7 +117,7 @@ function firstNavReachableScreenQn(features, acceptsScreen) {
|
|
|
117
117
|
}
|
|
118
118
|
// Only screens whose `access` excludes nobody: on an unprojected schema the
|
|
119
119
|
// landing is picked without knowing the user's roles, so a role-restricted
|
|
120
|
-
// screen would land every non-admin on access-denied
|
|
120
|
+
// screen would land every non-admin on access-denied.
|
|
121
121
|
export function firstOpenScreenQn(features) {
|
|
122
122
|
return firstNavReachableScreenQn(features, (s) => s.access === undefined || isOpenToAllGranted(s.access));
|
|
123
123
|
}
|
|
@@ -177,15 +177,15 @@ export function mergeContentEditors(clientFeatures) {
|
|
|
177
177
|
// declare a `type: "custom"` screen that's reachable directly by URL
|
|
178
178
|
// even without nav placement — without this check, a missing client
|
|
179
179
|
// plugin only surfaces as an error placeholder to whoever happens to
|
|
180
|
-
// open that URL
|
|
180
|
+
// open that URL. Screens flagged `dormant`
|
|
181
181
|
// (e.g. user-data-rights privacy-center, auth-mfa's enable screen) are
|
|
182
182
|
// registered without a self-owned r.nav on purpose — an app opts in by
|
|
183
183
|
// navving them explicitly, so a consumer that hasn't done that yet isn't
|
|
184
|
-
// missing anything
|
|
184
|
+
// missing anything. Still dev-only: several
|
|
185
185
|
// bundled screens that ARE self-navved (e.g. compliance-profiles'
|
|
186
186
|
// profile-picker) have real unmounted-client-plugin bugs in existing
|
|
187
|
-
// consumer apps today
|
|
188
|
-
// production before those are fixed
|
|
187
|
+
// consumer apps today — running this in
|
|
188
|
+
// production before those are fixed would just
|
|
189
189
|
// spam every affected app's console.
|
|
190
190
|
function warnMissingCustomScreens(app, customScreens) {
|
|
191
191
|
if (typeof process === "undefined" || process.env.NODE_ENV === "production")
|
|
@@ -209,7 +209,7 @@ function KumikoAppRoot(props) {
|
|
|
209
209
|
// 1. clientFeatures.translations — app overrides always win, even
|
|
210
210
|
// against the framework's own labels.
|
|
211
211
|
// 2. schemaTranslations — server-authored r.translations, projected
|
|
212
|
-
// verbatim by buildAppSchema
|
|
212
|
+
// verbatim by buildAppSchema. Without this bundle, nav/
|
|
213
213
|
// screen labels only resolve if an app ALSO duplicates them in its
|
|
214
214
|
// own web/i18n.ts — most bundled features never did, labels then
|
|
215
215
|
// render as raw i18n keys.
|
|
@@ -388,7 +388,7 @@ function findEditScreenForEntity(features, entityName) {
|
|
|
388
388
|
}
|
|
389
389
|
return undefined;
|
|
390
390
|
}
|
|
391
|
-
//
|
|
391
|
+
// A list screen with no reachable click target (no `detailFor`
|
|
392
392
|
// screen for its entity, no app-wide onRowClick, no entityEdit screen)
|
|
393
393
|
// must render without the row-click affordance instead of a silent no-op —
|
|
394
394
|
// mirrors the precedence effectiveOnRowClick itself resolves at click time.
|
|
@@ -426,7 +426,6 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
|
|
|
426
426
|
};
|
|
427
427
|
}
|
|
428
428
|
const shortId = nav.route.screenId;
|
|
429
|
-
// Suche das Feature dessen Screens den short id enthalten.
|
|
430
429
|
let matchedFeature;
|
|
431
430
|
for (const f of app.features) {
|
|
432
431
|
if (f.screens.some((s) => s.id === shortId)) {
|
|
@@ -452,7 +451,7 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
|
|
|
452
451
|
screenId: lastSegment(qn),
|
|
453
452
|
...(entityId !== undefined && { entityId }),
|
|
454
453
|
};
|
|
455
|
-
// Precedence
|
|
454
|
+
// Precedence: detailFor screen, then onRowClick, then the
|
|
456
455
|
// entityEdit-search fallback below. An explicit rowActions
|
|
457
456
|
// rowClick:true already wins over all of this — kumiko-screen.tsx
|
|
458
457
|
// handles it before onRowClick is ever called.
|
|
@@ -477,7 +476,7 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
|
|
|
477
476
|
}
|
|
478
477
|
};
|
|
479
478
|
}, [app, activeScreen, onRowClick, nav, qn, entityId]);
|
|
480
|
-
// Copy-link action
|
|
479
|
+
// Copy-link action for entityEdit update screens. Builds
|
|
481
480
|
// the absolute permalink URL from the current route + copies it —
|
|
482
481
|
// `navigator`/`window` are allowed here (renderer/web, not a
|
|
483
482
|
// platform-neutral package). No button without entityId (create-mode).
|
|
@@ -13,7 +13,7 @@ import { useEffect } from "react";
|
|
|
13
13
|
function syncDocumentLocale(locale) {
|
|
14
14
|
document.documentElement.lang = locale;
|
|
15
15
|
// Dedicated marker for dispatcher-live — static <html lang> must not
|
|
16
|
-
// look like an active UI locale
|
|
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"));
|
|
@@ -12,9 +12,9 @@ const SOURCE_I18N_KEY = {
|
|
|
12
12
|
default: "kumiko.config.source.default",
|
|
13
13
|
missing: "kumiko.config.source.missing",
|
|
14
14
|
};
|
|
15
|
-
// Fallback
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// Fallback order of the cascade, with the most specific source first.
|
|
16
|
+
// Comparing indices with the screen-scope source determines which
|
|
17
|
+
// levels a non-operator may see.
|
|
18
18
|
const SOURCE_ORDER = [
|
|
19
19
|
"user-row",
|
|
20
20
|
"tenant-row",
|
|
@@ -51,16 +51,16 @@ function scopeToSource(scope) {
|
|
|
51
51
|
return "system-row";
|
|
52
52
|
}
|
|
53
53
|
function toDisplayLevels(levels, screenScopeSource) {
|
|
54
|
-
// System
|
|
55
|
-
// app-override/computed/default
|
|
54
|
+
// System screens have operator visibility, so the full cascade, including
|
|
55
|
+
// app-override/computed/default, remains visible.
|
|
56
56
|
if (screenScopeSource === "system-row") {
|
|
57
57
|
return levels.map((level) => ({ level, badgeSource: level.source }));
|
|
58
58
|
}
|
|
59
59
|
const scopeIdx = SOURCE_ORDER.indexOf(screenScopeSource);
|
|
60
60
|
const own = levels.filter((l) => SOURCE_ORDER.indexOf(l.source) <= scopeIdx);
|
|
61
61
|
const higher = levels.filter((l) => SOURCE_ORDER.indexOf(l.source) > scopeIdx);
|
|
62
|
-
//
|
|
63
|
-
//
|
|
62
|
+
// Exactly one fallback row: the active higher level (whose value the user
|
|
63
|
+
// effectively receives), or the declared default/missing level.
|
|
64
64
|
const fallback = higher.find((l) => l.isActive) ??
|
|
65
65
|
higher.find((l) => l.source === "default" || l.source === "missing");
|
|
66
66
|
const ownRows = own.map((level) => ({ level, badgeSource: level.source }));
|
|
@@ -71,12 +71,11 @@ function toDisplayLevels(levels, screenScopeSource) {
|
|
|
71
71
|
{
|
|
72
72
|
level: fallback,
|
|
73
73
|
badgeSource: "default",
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// erscheinen sie hier neutral als "Standard".
|
|
74
|
+
// Use one consistent term, "Standard" (DE) / "Default" (EN)—the same
|
|
75
|
+
// key as the field-label badge (kumiko.config.source.default), so the
|
|
76
|
+
// badge and cascade disclosure do not show two different words. The
|
|
77
|
+
// screen scope cannot set or reset the operator levels (System/Override/
|
|
78
|
+
// Computed), so they appear here neutrally as "Standard".
|
|
80
79
|
badgeLabelKey: "kumiko.config.source.default",
|
|
81
80
|
},
|
|
82
81
|
];
|
|
@@ -106,11 +105,11 @@ export function ConfigCascadeView({ cascade, screenScope, onReset, qualifiedKey,
|
|
|
106
105
|
const displayLevels = toDisplayLevels(cascade.levels, screenScopeSource);
|
|
107
106
|
const activeDisplay = displayLevels.find((d) => d.level.isActive);
|
|
108
107
|
const hasOverride = activeDisplay?.level.source === screenScopeSource;
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
// (
|
|
112
|
-
//
|
|
113
|
-
//
|
|
108
|
+
// Expandable only when the panel provides meaningful additional information:
|
|
109
|
+
// an own override (→ Reset) or more than one level with a value. Empty levels
|
|
110
|
+
// (e.g. the unset tenant-row of a field using only its default) do not count;
|
|
111
|
+
// otherwise a field showing only its default would incorrectly be expandable,
|
|
112
|
+
// making the panel merely repeat the trigger.
|
|
114
113
|
const valuedLevels = displayLevels.filter((d) => d.level.hasValue);
|
|
115
114
|
const expandable = hasOverride || valuedLevels.length > 1;
|
|
116
115
|
const isUnset = activeDisplay === undefined || !activeDisplay.level.hasValue;
|
|
@@ -40,10 +40,10 @@ function scopeToSource(scope) {
|
|
|
40
40
|
}
|
|
41
41
|
export function ConfigSourceBadge({ source, screenScope, }) {
|
|
42
42
|
const t = useTranslation();
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
43
|
+
// Uses the same collapsing rule as toDisplayLevels (config-cascade.tsx):
|
|
44
|
+
// for non-operator screens, operator sources above the screen scope appear
|
|
45
|
+
// as a neutral "Vorgabe" badge so the badge does not reveal the system source
|
|
46
|
+
// intentionally hidden by the cascade view.
|
|
47
47
|
let effective = source;
|
|
48
48
|
if (screenScope !== undefined && screenScope !== "system") {
|
|
49
49
|
const scopeIdx = SOURCE_ORDER.indexOf(scopeToSource(screenScope));
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
export type AppLayoutProps = {
|
|
3
|
-
/** Optional.
|
|
3
|
+
/** Optional. Exists only above the main area, not above the sidebar. */
|
|
4
4
|
readonly topbar?: ReactNode;
|
|
5
5
|
readonly sidebar?: ReactNode;
|
|
6
6
|
readonly children: ReactNode;
|
|
7
7
|
readonly testId?: string;
|
|
8
|
-
/** Viewport-fit
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `min-h-screen
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
/** Viewport-fit shell. Default `true` → root = `h-screen` (fixed
|
|
9
|
+
* viewport height); sidebar/topbar remain fixed while the main area
|
|
10
|
+
* scrolls INSIDE (`min-h-0` + `overflow-auto`). `false` → classic
|
|
11
|
+
* `min-h-screen` flow where the whole page scrolls—an escape hatch for
|
|
12
|
+
* a public, long content page that needs real body scrolling. Never
|
|
13
|
+
* clips—content scrolls in `main` instead of the body. */
|
|
14
14
|
readonly fill?: boolean;
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
15
|
+
/** Optional classes appended to the root (for a custom background, etc.).
|
|
16
|
+
* Extends the defaults rather than replacing them (`cn` merge). */
|
|
17
17
|
readonly className?: string;
|
|
18
18
|
/** Optionaler Klassen-Append an `<main>`. */
|
|
19
19
|
readonly mainClassName?: string;
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
export function AppLayout({ topbar, sidebar, children, testId, fill = true, className, mainClassName, }) {
|
|
4
4
|
return (_jsxs("div", { "data-testid": testId, "data-kumiko-layout": "app", ...(fill === true && { "data-kumiko-fill": "true" }), className: cn(
|
|
5
|
-
// new-york inset
|
|
6
|
-
//
|
|
5
|
+
// new-york inset frame: muted rail background; the sidebar sits
|
|
6
|
+
// transparently on it, while the main area floats as its own panel.
|
|
7
7
|
"flex flex-row bg-muted/40 text-foreground", fill === true ? "h-screen" : "min-h-screen", className), children: [sidebar, _jsxs("div", { className: cn("flex flex-1 min-w-0 flex-col overflow-hidden rounded-xl border bg-background shadow-sm m-2 md:ml-0", fill === true && "min-h-0"), children: [topbar, _jsx("main", { className: cn("flex-1 overflow-auto", fill === true && "min-h-0", mainClassName), children: children })] })] }));
|
|
8
8
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
export type AvatarSize = "sm" | "md" | "lg";
|
|
3
3
|
export type AvatarProps = {
|
|
4
|
-
/** Stable
|
|
5
|
-
* user.id
|
|
4
|
+
/** Stable identifier—the source for the background-color hash. Usually
|
|
5
|
+
* `user.id`; for avatars based only on a label, the label string can also
|
|
6
|
+
* be used. */
|
|
6
7
|
readonly id: string;
|
|
7
|
-
/**
|
|
8
|
+
/** Full name or email—the initials are extracted from this value. */
|
|
8
9
|
readonly label: string;
|
|
9
10
|
readonly size?: AvatarSize;
|
|
10
11
|
readonly testId?: string;
|