@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
|
@@ -27,12 +27,12 @@ function treeContainsScreen(nodes, screenId) {
|
|
|
27
27
|
return nodes.some((n) => (n.screen !== undefined && lastSegment(n.screen) === screenId) ||
|
|
28
28
|
treeContainsScreen(n.children, screenId));
|
|
29
29
|
}
|
|
30
|
-
//
|
|
31
|
-
// Provider
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// (user
|
|
35
|
-
//
|
|
30
|
+
// A node survives filtering when it or a descendant matches.
|
|
31
|
+
// Provider children are already materialized during filtering (provider nodes
|
|
32
|
+
// are default-expanded → eagerly loaded), so static children recursion is
|
|
33
|
+
// sufficient. ponytail: a provider that is still NOT loaded during filtering
|
|
34
|
+
// (because the user collapsed it) is expanded via the force-expand in
|
|
35
|
+
// useNavNodeState, then loaded and filtered.
|
|
36
36
|
function subtreeMatches(node, matches) {
|
|
37
37
|
return matches(node.label) || node.children.some((c) => subtreeMatches(c, matches));
|
|
38
38
|
}
|
|
@@ -45,7 +45,7 @@ export function NavTree({ schema, user, testId, allowedNavQns, navBadges, }) {
|
|
|
45
45
|
const nav = useNav();
|
|
46
46
|
const activeScreenId = nav.route?.screenId;
|
|
47
47
|
const activeEntityId = nav.route?.entityId;
|
|
48
|
-
// Same "no nav entry of its own" gap the breadcrumb closes
|
|
48
|
+
// Same "no nav entry of its own" gap the breadcrumb closes:
|
|
49
49
|
// when the routed screen has no node in {tree}, fall back to its
|
|
50
50
|
// resolved parent so a detail/edit/form screen still orients the user.
|
|
51
51
|
const activeMarker = useMemo(() => {
|
|
@@ -64,9 +64,9 @@ export function NavTree({ schema, user, testId, allowedNavQns, navBadges, }) {
|
|
|
64
64
|
return { screenId: activeScreenId, exact: true };
|
|
65
65
|
return parentScreenId !== undefined ? { screenId: parentScreenId, exact: false } : undefined;
|
|
66
66
|
}, [tree, app.features, activeScreenId, activeEntityId]);
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
67
|
+
// The collapsed set contains only explicitly collapsed qualified names.
|
|
68
|
+
// Everything is therefore expanded by default, so new features appear open
|
|
69
|
+
// without requiring the user to click first.
|
|
70
70
|
const [collapsed, setCollapsed] = useState(() => new Set());
|
|
71
71
|
const onToggle = useCallback((qn) => {
|
|
72
72
|
setCollapsed((prev) => {
|
|
@@ -83,7 +83,7 @@ export function NavTree({ schema, user, testId, allowedNavQns, navBadges, }) {
|
|
|
83
83
|
const [filter, setFilter] = useState("");
|
|
84
84
|
// The search box is only CSS-hidden (`group-data-[collapsible=icon]:hidden`)
|
|
85
85
|
// when the sidebar collapses to icon rail, not unmounted — without this,
|
|
86
|
-
// a stale filter re-applies invisibly on the next expand
|
|
86
|
+
// a stale filter re-applies invisibly on the next expand.
|
|
87
87
|
useEffect(() => {
|
|
88
88
|
if (sidebarState === "collapsed")
|
|
89
89
|
setFilter("");
|
|
@@ -96,16 +96,16 @@ export function NavTree({ schema, user, testId, allowedNavQns, navBadges, }) {
|
|
|
96
96
|
const navFilter = useMemo(() => ({ q, matches }), [q, matches]);
|
|
97
97
|
return (_jsx(ActiveScreenMarkerContext.Provider, { value: activeMarker, children: _jsx(NavFilterContext.Provider, { value: navFilter, children: _jsx(NavBadgesContext.Provider, { value: navBadges ?? EMPTY_BADGES, children: _jsxs("div", { "data-testid": testId, "data-kumiko-layout": "nav-tree", className: "flex w-full flex-col gap-3", children: [_jsxs("div", { className: "relative mb-0 group-data-[collapsible=icon]:hidden", children: [_jsx(Search, { "aria-hidden": "true", className: "pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 text-sidebar-muted" }), _jsx(SidebarInput, { className: "pl-8", value: filter, onChange: (e) => setFilter(e.target.value), placeholder: t("kumiko.nav.search"), "aria-label": t("kumiko.nav.search") })] }), tree.map((node) => isPureSection(node) ? (_jsx(NavSection, { node: node, collapsed: collapsed, onToggle: onToggle }, node.qualifiedName)) : (_jsx(SidebarMenu, { children: _jsx(NavMenuNode, { node: node, collapsed: collapsed, onToggle: onToggle }) }, node.qualifiedName)))] }) }) }) }));
|
|
98
98
|
}
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// ponytail:
|
|
103
|
-
//
|
|
104
|
-
// Defense-in-depth
|
|
99
|
+
// The runtime badge slot is keyed by the bare nav ID. With no entry it
|
|
100
|
+
// renders nothing, avoiding layout shift. shrink-0 and ml-auto move it to the
|
|
101
|
+
// right edge while the truncated label gives up space.
|
|
102
|
+
// ponytail: badges are wired only to screen/target leaves, not container nodes
|
|
103
|
+
// (which use the ml-auto chevron). A tier badge belongs on a leaf.
|
|
104
|
+
// Defense-in-depth: current call sites already guard on
|
|
105
105
|
// node.screen/node.target !== undefined before rendering <NavBadge>, so this
|
|
106
|
-
// early
|
|
107
|
-
// site
|
|
108
|
-
//
|
|
106
|
+
// early return does not change current behavior. It prevents a future call
|
|
107
|
+
// site without that guard from showing a leaf badge on an unrelated container
|
|
108
|
+
// node that happens to share its last segment.
|
|
109
109
|
function NavBadge({ node }) {
|
|
110
110
|
const badges = useContext(NavBadgesContext);
|
|
111
111
|
if (node.screen === undefined && node.target === undefined)
|
|
@@ -115,10 +115,10 @@ function NavBadge({ node }) {
|
|
|
115
115
|
return null;
|
|
116
116
|
return _jsx("span", { className: "ml-auto shrink-0", children: badge });
|
|
117
117
|
}
|
|
118
|
-
//
|
|
119
|
-
// children
|
|
120
|
-
//
|
|
121
|
-
//
|
|
118
|
+
// A pure section is a group header without its own screen that only groups
|
|
119
|
+
// its children. At the top level it becomes a SidebarGroup with a toggleable
|
|
120
|
+
// GroupLabel; a node with a screen remains a clickable menu entry even when it
|
|
121
|
+
// also has children.
|
|
122
122
|
function isPureSection(node) {
|
|
123
123
|
return (node.screen === undefined &&
|
|
124
124
|
node.target === undefined &&
|
|
@@ -126,9 +126,9 @@ function isPureSection(node) {
|
|
|
126
126
|
node.createAction === undefined &&
|
|
127
127
|
node.children.length > 0);
|
|
128
128
|
}
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
129
|
+
// If the label contains a dot, pass it through t(): the bundle translates
|
|
130
|
+
// the key; otherwise the key remains visible as a reminder of a missing
|
|
131
|
+
// translation. Literal string labels are left unchanged.
|
|
132
132
|
function useLabel(node) {
|
|
133
133
|
const t = useTranslation();
|
|
134
134
|
return node.label.includes(".") ? t(node.label) : node.label;
|
|
@@ -152,11 +152,12 @@ function NavLeadingIcon({ node, active, expanded = false, label, }) {
|
|
|
152
152
|
return null;
|
|
153
153
|
return (_jsx("span", { "aria-hidden": "true", className: cn("hidden size-4 shrink-0 items-center justify-center font-medium text-xs group-data-[collapsible=icon]:flex", active ? "text-sidebar-accent-foreground" : "text-sidebar-foreground/70"), children: initial }));
|
|
154
154
|
}
|
|
155
|
-
//
|
|
156
|
-
//
|
|
155
|
+
// A stable empty-list reference prevents a new `[]` on every render from
|
|
156
|
+
// destabilizing the SSE subscription's useEffect dependencies.
|
|
157
157
|
const EMPTY_ENTITY_LIST = [];
|
|
158
|
-
// Active
|
|
159
|
-
// (
|
|
158
|
+
// Active targets require an exact featureId and action match, with shallow
|
|
159
|
+
// equality for args (currently primitives only). This mirrors the former
|
|
160
|
+
// TreeNodeRenderer.
|
|
160
161
|
function targetsEqual(a, b) {
|
|
161
162
|
if (b === undefined)
|
|
162
163
|
return false;
|
|
@@ -172,11 +173,12 @@ function targetsEqual(a, b) {
|
|
|
172
173
|
}
|
|
173
174
|
return true;
|
|
174
175
|
}
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
176
|
+
// Adapts provider-emitted TreeNodes to renderable NavNodes. Synthetic QNs use
|
|
177
|
+
// the parent QN and index label, remaining stable as long as emit order is
|
|
178
|
+
// stable. Static children are converted recursively. ponytail: a
|
|
179
|
+
// TreeChildrenSubscribe-Function supplied as children is treated as a leaf;
|
|
180
|
+
// nested dynamic providers have no consumer today. The upgrade path is to
|
|
181
|
+
// register synthetic QNs when such a provider emits them.
|
|
180
182
|
function treeNodeToNavNode(tn, parentQn, idx) {
|
|
181
183
|
const qualifiedName = `${parentQn}/${idx}-${tn.label}`;
|
|
182
184
|
const children = Array.isArray(tn.children)
|
|
@@ -193,10 +195,10 @@ function treeNodeToNavNode(tn, parentQn, idx) {
|
|
|
193
195
|
...(tn.createAction !== undefined && { createAction: tn.createAction }),
|
|
194
196
|
};
|
|
195
197
|
}
|
|
196
|
-
// Lazy provider
|
|
197
|
-
//
|
|
198
|
-
// treeEntities
|
|
199
|
-
//
|
|
198
|
+
// Lazy provider children with SSE live refresh for a `provider: true` node.
|
|
199
|
+
// Subscription starts only when the node is expanded (enabled). The
|
|
200
|
+
// treeEntities list, keyed by QN, re-fires the provider on entity events so
|
|
201
|
+
// newly created nodes appear live. The logic mirrors VisualTree.ProviderBranch.
|
|
200
202
|
function useNavProviderChildren(qn, enabled) {
|
|
201
203
|
const providers = useNavProviders();
|
|
202
204
|
const entitiesMap = useNavEntities();
|
|
@@ -222,8 +224,8 @@ function useNavProviderChildren(qn, enabled) {
|
|
|
222
224
|
scheduler.dispose();
|
|
223
225
|
};
|
|
224
226
|
}, [enabled, entities, subscribeLive]);
|
|
225
|
-
// `attempt`
|
|
226
|
-
//
|
|
227
|
+
// `attempt` triggers an SSE refresh and a new subscription, although Biome
|
|
228
|
+
// cannot see its use in the body; semantically, it is the refire trigger.
|
|
227
229
|
// biome-ignore lint/correctness/useExhaustiveDependencies: attempt = re-fire trigger
|
|
228
230
|
useEffect(() => {
|
|
229
231
|
if (!enabled) {
|
|
@@ -256,10 +258,9 @@ function useNavProviderChildren(qn, enabled) {
|
|
|
256
258
|
const nodes = useMemo(() => (raw === null ? null : raw.map((tn, i) => treeNodeToNavNode(tn, qn, i))), [raw, qn]);
|
|
257
259
|
return { nodes, error };
|
|
258
260
|
}
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// in den shadcn-Primitives, nicht in dieser Logik.
|
|
261
|
+
// Shared node logic for top-level (NavMenuNode) and nested (NavSubNode)
|
|
262
|
+
// nodes: labels, expansion, active state (screen or target), and lazy provider
|
|
263
|
+
// children. The two renderers differ only in their shadcn primitives.
|
|
263
264
|
function useNavNodeState(node, collapsed) {
|
|
264
265
|
const nav = useNav();
|
|
265
266
|
const { q: filterQ, matches } = useContext(NavFilterContext);
|
|
@@ -267,8 +268,8 @@ function useNavNodeState(node, collapsed) {
|
|
|
267
268
|
const displayLabel = useLabel(node);
|
|
268
269
|
const isCollapsed = collapsed.has(node.qualifiedName);
|
|
269
270
|
const expandable = node.children.length > 0 || node.provider === true;
|
|
270
|
-
//
|
|
271
|
-
//
|
|
271
|
+
// An active filter expands every container so matches inside collapsed
|
|
272
|
+
// folders become visible and provider children can load.
|
|
272
273
|
const isExpanded = expandable && (!isCollapsed || filterActive);
|
|
273
274
|
const { nodes: providerChildren, error: providerError } = useNavProviderChildren(node.qualifiedName, node.provider === true && isExpanded);
|
|
274
275
|
const activeTarget = useMemo(() => parseTargetFromSearchParams(nav.searchParams), [nav.searchParams]);
|
|
@@ -290,8 +291,8 @@ function useNavNodeState(node, collapsed) {
|
|
|
290
291
|
active: screenActive || targetActive,
|
|
291
292
|
ariaCurrent: screenIsExactMatch || targetActive,
|
|
292
293
|
childNodes: visibleChildNodes,
|
|
293
|
-
//
|
|
294
|
-
//
|
|
294
|
+
// A filtered node is hidden only when neither it nor a visible child matches;
|
|
295
|
+
// otherwise an empty folder header would remain.
|
|
295
296
|
hidden: filterActive && !matches(node.label) && visibleChildNodes.length === 0,
|
|
296
297
|
providerLoading: node.provider === true && isExpanded && providerChildren === null,
|
|
297
298
|
providerError,
|
|
@@ -360,7 +361,7 @@ function NodeActions({ node, expandable, workspaceId, }) {
|
|
|
360
361
|
const hover = "opacity-0 group-hover/menu-item:opacity-100 group-hover/menu-sub-item:opacity-100 group-focus-within/menu-item:opacity-100";
|
|
361
362
|
return (_jsxs("div", { className: cn("absolute top-1 flex items-center gap-0.5", expandable ? "right-7" : "right-1"), children: [create !== undefined && (_jsx(TreeActionControl, { action: create, ariaLabel: label(create.label), className: btn, dispatch: dispatch, workspaceId: workspaceId, children: _jsx(Plus, { className: "size-3.5" }) })), actions.map((a) => (_jsx(TreeActionControl, { action: a, ariaLabel: label(a.label), className: cn(btn, hover), dispatch: dispatch, workspaceId: workspaceId, children: _jsx(ActionGlyph, { icon: a.icon }) }, a.label)))] }));
|
|
362
363
|
}
|
|
363
|
-
// Status
|
|
364
|
+
// Status lines within a provider sub-list (lazy loading or error).
|
|
364
365
|
function ProviderStatus({ loading, error, }) {
|
|
365
366
|
if (error !== null) {
|
|
366
367
|
return _jsx("li", { className: "px-2 py-1 text-xs text-destructive", children: error });
|
|
@@ -370,14 +371,15 @@ function ProviderStatus({ loading, error, }) {
|
|
|
370
371
|
}
|
|
371
372
|
return null;
|
|
372
373
|
}
|
|
373
|
-
// Top-
|
|
374
|
-
// "Platform"/"Projects"
|
|
375
|
-
//
|
|
374
|
+
// Top-level section: a SidebarGroup with a static label
|
|
375
|
+
// ("Platform"/"Projects" are fixed headings, not toggles). Collapse belongs
|
|
376
|
+
// on items with children, not on the section itself.
|
|
376
377
|
function NavSection({ node, collapsed, onToggle }) {
|
|
377
378
|
const displayLabel = useLabel(node);
|
|
378
379
|
const { q, matches } = useContext(NavFilterContext);
|
|
379
|
-
// Provider
|
|
380
|
-
//
|
|
380
|
+
// Provider children are statically empty until loaded, so they keep the
|
|
381
|
+
// section visible; their matches cannot be checked statically, and the node
|
|
382
|
+
// hides itself.
|
|
381
383
|
if (q !== "" &&
|
|
382
384
|
!matches(node.label) &&
|
|
383
385
|
!node.children.some((c) => c.provider === true || subtreeMatches(c, matches))) {
|
|
@@ -385,13 +387,13 @@ function NavSection({ node, collapsed, onToggle }) {
|
|
|
385
387
|
}
|
|
386
388
|
return (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: displayLabel }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: node.children.map((child) => (_jsx(NavMenuNode, { node: child, collapsed: collapsed, onToggle: onToggle }, child.qualifiedName))) }) })] }));
|
|
387
389
|
}
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
// absolute NodeActions
|
|
390
|
+
// A menu entry (top-level item or section child) supports three click modes:
|
|
391
|
+
// `screen` is a route link (KumikoLink), `target` dispatches to EditorPanel
|
|
392
|
+
// (useDispatchTarget), and neither makes it a container/provider node that only
|
|
393
|
+
// expands or collapses. Provider nodes load children lazily with SSE refresh.
|
|
394
|
+
// The collapse chevron is a separate SidebarMenuAction because nesting a
|
|
395
|
+
// <button> inside an <a> would produce invalid HTML; createAction/actions are
|
|
396
|
+
// rendered as absolute NodeActions beside it.
|
|
395
397
|
function NavMenuNode({ node, collapsed, onToggle }) {
|
|
396
398
|
const t = useTranslation();
|
|
397
399
|
const dispatch = useDispatchTarget();
|
|
@@ -414,21 +416,22 @@ function NavMenuNode({ node, collapsed, onToggle }) {
|
|
|
414
416
|
}, ...(s.expandable && { "aria-expanded": s.isExpanded }), children: [_jsx(NavLeadingIcon, { node: node, active: false, expanded: s.isExpanded, label: s.displayLabel }), _jsx("span", { className: "truncate group-data-[collapsible=icon]:hidden", title: s.displayLabel, children: s.displayLabel }), s.expandable &&
|
|
415
417
|
(s.isExpanded ? (_jsx(ChevronDown, { className: "ml-auto" })) : (_jsx(ChevronRight, { className: "ml-auto" })))] }), _jsx(NodeActions, { node: node, expandable: s.expandable, workspaceId: s.workspaceId }), sub] }));
|
|
416
418
|
}
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
// screen
|
|
420
|
-
// Non-
|
|
421
|
-
// <a
|
|
419
|
+
// Nested entries inside SidebarMenuSub use the same three click modes as
|
|
420
|
+
// NavMenuNode, with the sub-primitives. Folder/container nodes without a
|
|
421
|
+
// screen or target expand; provider children and deeper static nodes recurse.
|
|
422
|
+
// Non-link targets require asChild with <button> because SidebarMenuSubButton
|
|
423
|
+
// is an <a>; the chevron is a separate absolute button to avoid a button inside
|
|
424
|
+
// an anchor.
|
|
422
425
|
function NavSubNode({ node, collapsed, onToggle }) {
|
|
423
426
|
const t = useTranslation();
|
|
424
427
|
const dispatch = useDispatchTarget();
|
|
425
428
|
const s = useNavNodeState(node, collapsed);
|
|
426
429
|
if (s.hidden)
|
|
427
430
|
return null;
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
431
|
+
// Children go in their own SidebarMenuSub (<ul>): a sub-item is an <li>, so
|
|
432
|
+
// nested <li> elements without an intervening <ul> would be invalid HTML
|
|
433
|
+
// (for example, nested content folders such as Marketing/ > Block). The
|
|
434
|
+
// <ul> also supplies the indentation and border level for each depth.
|
|
432
435
|
const deeper = s.isExpanded ? (_jsxs(SidebarMenuSub, { children: [_jsx(ProviderStatus, { loading: s.providerLoading, error: s.providerError }), s.childNodes.map((child) => (_jsx(NavSubNode, { node: child, collapsed: collapsed, onToggle: onToggle }, child.qualifiedName)))] })) : null;
|
|
433
436
|
const chevron = s.expandable ? (_jsx("button", { type: "button", "aria-label": t(s.isExpanded ? "kumiko.nav.collapse" : "kumiko.nav.expand"), "aria-expanded": s.isExpanded, onClick: (e) => {
|
|
434
437
|
e.stopPropagation();
|
|
@@ -447,19 +450,18 @@ function NavSubNode({ node, collapsed, onToggle }) {
|
|
|
447
450
|
onToggle(node.qualifiedName);
|
|
448
451
|
}, children: [_jsx(NavLeadingIcon, { node: node, active: false, expanded: s.isExpanded, label: s.displayLabel }), _jsx("span", { className: "truncate group-data-[collapsible=icon]:hidden", title: s.displayLabel, children: s.displayLabel })] }) }), _jsx(NodeActions, { node: node, expandable: s.expandable, workspaceId: s.workspaceId }), chevron, deeper] }));
|
|
449
452
|
}
|
|
450
|
-
//
|
|
451
|
-
// buildNavRegistrySliceForApp
|
|
452
|
-
// FeatureSchema
|
|
453
|
+
// Backward-compatible single-feature builder. This convenience wrapper around
|
|
454
|
+
// buildNavRegistrySliceForApp lets existing tests that call it with
|
|
455
|
+
// FeatureSchema remain unchanged.
|
|
453
456
|
export function buildNavRegistrySlice(schema, allowedNavQns) {
|
|
454
457
|
return buildNavRegistrySliceForApp(toAppSchema(schema), allowedNavQns);
|
|
455
458
|
}
|
|
456
|
-
// Multi-
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
+
// Multi-feature variant. Iterates over all features, qualifying each entry
|
|
460
|
+
// with its own featureName. Feature order follows AppSchema, and nav order
|
|
461
|
+
// follows each feature's declaration order.
|
|
459
462
|
//
|
|
460
|
-
// Cross-
|
|
461
|
-
//
|
|
462
|
-
// egal in welchem Feature sie deklariert sind.
|
|
463
|
+
// Cross-feature workspaces are supported natively: `navMembers` reference QNs,
|
|
464
|
+
// and the filter selects the correct entries regardless of their feature.
|
|
463
465
|
export function buildNavRegistrySliceForApp(app, allowedNavQns) {
|
|
464
466
|
const screenAccessByQn = buildScreenAccessMap(app);
|
|
465
467
|
const qualified = [];
|
|
@@ -468,7 +470,7 @@ export function buildNavRegistrySliceForApp(app, allowedNavQns) {
|
|
|
468
470
|
const screen = n.screen !== undefined ? qualifyScreenId(feature.featureName, n.screen) : undefined;
|
|
469
471
|
// Nav entries without their own `access` inherit the referenced
|
|
470
472
|
// screen's access rule — otherwise a broader workspace/parent access
|
|
471
|
-
// lets a role see a nav entry whose target screen 403s them
|
|
473
|
+
// lets a role see a nav entry whose target screen 403s them.
|
|
472
474
|
// An explicit `n.access` always wins; inheritance only fills the gap.
|
|
473
475
|
const inheritedAccess = n.access ?? (screen !== undefined ? screenAccessByQn.get(screen) : undefined);
|
|
474
476
|
qualified.push({
|
|
@@ -540,6 +542,6 @@ function buildScreenAccessMap(app) {
|
|
|
540
542
|
}
|
|
541
543
|
return map;
|
|
542
544
|
}
|
|
543
|
-
// `lastSegment`
|
|
544
|
-
//
|
|
545
|
+
// `lastSegment` now lives in @cosmicdrift/kumiko-renderer (./app/qn), giving
|
|
546
|
+
// both packages a shared implementation.
|
|
545
547
|
export { lastSegment };
|
|
@@ -4,7 +4,7 @@ export type ProfileMenuItem = MenuItemDef;
|
|
|
4
4
|
export type ProfileMenuProps = {
|
|
5
5
|
readonly user: {
|
|
6
6
|
readonly id: string;
|
|
7
|
-
/** Display
|
|
7
|
+
/** Display name or email — the source for initials. */
|
|
8
8
|
readonly label: string;
|
|
9
9
|
};
|
|
10
10
|
readonly items: readonly ProfileMenuItem[];
|
|
@@ -19,6 +19,7 @@ import { HeaderOverflowMenuContext, headerOverflowMenuItemClass } from "./header
|
|
|
19
19
|
import { buildNavRegistrySliceForApp, lastSegment } from "./nav-tree.js";
|
|
20
20
|
import { usePageHeaderSlot } from "./page-header-slot.js";
|
|
21
21
|
import { resolveDetailBreadcrumb, screenTitleKey, } from "./shell-breadcrumb.js";
|
|
22
|
+
import { UNIFORM_BAR_CONTROLS_CLASSES } from "./uniform-bar-controls.js";
|
|
22
23
|
export function ShellHeader({ schema, user, headerActions, }) {
|
|
23
24
|
const nav = useNav();
|
|
24
25
|
const t = useTranslation();
|
|
@@ -68,7 +69,7 @@ export function ShellHeader({ schema, user, headerActions, }) {
|
|
|
68
69
|
e.preventDefault();
|
|
69
70
|
nav.navigate({ screenId });
|
|
70
71
|
}, children: crumb.label })) : isLast ? (_jsx("h1", { "aria-current": "page", title: titleOverride ?? crumb.label, className: "truncate text-lg font-semibold text-foreground", children: titleOverride ?? crumb.label })) : (_jsx(BreadcrumbPage, { className: "whitespace-nowrap text-foreground-secondary", children: crumb.label })) })] }, screenId ?? crumb.label));
|
|
71
|
-
}) }) })), _jsx("div", { ref: setStatusElement, "data-kumiko-layout": "page-header-status", className: cn("flex items-center gap-2 empty:hidden", compact ? "min-w-0 overflow-hidden" : "shrink-0") })] }), _jsxs("div", { className: cn("ml-auto flex items-center gap-2", compact ? "min-w-0 max-w-[60%]" : "min-w-0 max-w-[50%] shrink-0 sm:max-w-none"), children: [_jsx("div", { ref: setActionsElement, "data-kumiko-layout": "page-header-actions", className: cn("flex min-w-0 items-center gap-2 empty:hidden [&_*]:min-w-0 [&_*]:max-w-full [&_button]:overflow-hidden [&_button]:whitespace-nowrap", compact && "overflow-hidden") }), !compact && headerActions !== undefined && (_jsx("div", { "data-kumiko-layout": "header-actions", className: "flex items-center gap-2", children: headerActions })), hasOverflow && _jsx(HeaderOverflow, { items: overflowItems, headerActions: headerActions })] })] }));
|
|
72
|
+
}) }) })), _jsx("div", { ref: setStatusElement, "data-kumiko-layout": "page-header-status", className: cn("flex items-center gap-2 empty:hidden", compact ? "min-w-0 overflow-hidden" : "shrink-0") })] }), _jsxs("div", { className: cn("ml-auto flex items-center gap-2", compact ? "min-w-0 max-w-[60%]" : "min-w-0 max-w-[50%] shrink-0 sm:max-w-none"), children: [_jsx("div", { ref: setActionsElement, "data-kumiko-layout": "page-header-actions", className: cn(UNIFORM_BAR_CONTROLS_CLASSES, "flex min-w-0 items-center gap-2 empty:hidden [&_*]:min-w-0 [&_*]:max-w-full [&_button]:overflow-hidden [&_button]:whitespace-nowrap", compact && "overflow-hidden") }), !compact && headerActions !== undefined && (_jsx("div", { "data-kumiko-layout": "header-actions", className: "flex items-center gap-2", children: headerActions })), hasOverflow && _jsx(HeaderOverflow, { items: overflowItems, headerActions: headerActions })] })] }));
|
|
72
73
|
}
|
|
73
74
|
// A hand-rolled menu instead of Radix DropdownMenu: app headerActions are opaque
|
|
74
75
|
// nodes with their own popovers, and the panel stays mounted while closed so their
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
export type SidebarBrandProps = {
|
|
3
|
-
/** Workspace
|
|
3
|
+
/** Workspace/app name (bold, first line). */
|
|
4
4
|
readonly name: string;
|
|
5
|
-
/**
|
|
5
|
+
/** Second line — plan, tagline, or tenant (small, dimmed). */
|
|
6
6
|
readonly plan?: string;
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
7
|
+
/** The tile typically contains a Lucide icon. If omitted, the first
|
|
8
|
+
* character of the name is shown. */
|
|
9
9
|
readonly logo?: ReactNode;
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
/** Shows the ChevronsUpDown icon only when the app wraps the brand in a
|
|
11
|
+
* dropdown. Default false: without a dropdown, the chevron misleadingly
|
|
12
|
+
* suggests a menu that does not open. */
|
|
13
13
|
readonly collapsible?: boolean;
|
|
14
14
|
};
|
|
15
15
|
export declare function SidebarBrand({ name, plan, logo, collapsible, }: SidebarBrandProps): ReactNode;
|
|
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
export type SidebarUserProps = {
|
|
3
3
|
readonly name: string;
|
|
4
4
|
readonly email?: string;
|
|
5
|
-
/** Avatar
|
|
5
|
+
/** Avatar URL. When omitted, initials derived from the name are shown. */
|
|
6
6
|
readonly avatar?: string;
|
|
7
7
|
};
|
|
8
8
|
export declare function SidebarUser({ name, email, avatar }: SidebarUserProps): ReactNode;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// SidebarUser
|
|
3
|
-
//
|
|
4
|
-
// DefaultAppShell.
|
|
5
|
-
//
|
|
2
|
+
// SidebarUser is the NavUser footer from shadcns sidebar-07: avatar + name +
|
|
3
|
+
// email + ChevronsUpDown. App authors pass it as `sidebarFooter` to
|
|
4
|
+
// DefaultAppShell. It is purely visual (no account dropdown); wrap it in a
|
|
5
|
+
// DropdownMenu to provide a user menu.
|
|
6
6
|
import { ChevronsUpDown } from "lucide-react";
|
|
7
7
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar.js";
|
|
8
8
|
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar.js";
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
export type SidebarProps = {
|
|
3
|
-
/** Header
|
|
4
|
-
*
|
|
3
|
+
/** Header area at the top — typically a workspace avatar, name, and badge.
|
|
4
|
+
* Sits ABOVE the scroll area and remains visible while the nav scrolls. */
|
|
5
5
|
readonly header?: ReactNode;
|
|
6
|
-
/** Action
|
|
7
|
-
*
|
|
6
|
+
/** Action cluster between the header and nav — typically a "+ Neu" button
|
|
7
|
+
* and search icon. Linear pattern: prominent and outside the scroll area. */
|
|
8
8
|
readonly actions?: ReactNode;
|
|
9
|
-
/** NavTree
|
|
10
|
-
*
|
|
9
|
+
/** NavTree and free-form sections (e.g. "Your teams" header and sub-items).
|
|
10
|
+
* Scrolls independently when the content becomes too long. */
|
|
11
11
|
readonly children: ReactNode;
|
|
12
|
-
/** Footer
|
|
13
|
-
*
|
|
12
|
+
/** Footer area at the bottom — typically an invite link, help, and plan banner.
|
|
13
|
+
* Sticks to the bottom via mt-auto. */
|
|
14
14
|
readonly footer?: ReactNode;
|
|
15
15
|
readonly testId?: string;
|
|
16
16
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
export function Sidebar({ header, actions, children, footer, testId }) {
|
|
3
|
-
// Linear
|
|
4
|
-
// 1. Header (
|
|
5
|
-
// 2. Actions (
|
|
6
|
-
// 3. Nav
|
|
7
|
-
// 4. Footer (
|
|
8
|
-
// bg-muted/40 + border-r
|
|
9
|
-
// tighter padding (p-3)
|
|
3
|
+
// Linear pattern: four vertical areas
|
|
4
|
+
// 1. Header (workspace identity)
|
|
5
|
+
// 2. Actions (non-scrolling quick buttons)
|
|
6
|
+
// 3. Nav scroll area (NavTree, scrolling when needed)
|
|
7
|
+
// 4. Footer (stays at the bottom via mt-auto)
|
|
8
|
+
// bg-muted/40 + border-r distinguish the sidebar visually from the main area;
|
|
9
|
+
// tighter padding (p-3) and a compact gap-0.5 between items.
|
|
10
10
|
return (_jsxs("aside", { "data-testid": testId, "data-kumiko-layout": "sidebar", className: "hidden md:flex w-60 flex-shrink-0 bg-transparent flex-col text-sm", children: [header !== undefined && (_jsx("div", { "data-kumiko-layout": "sidebar-header", className: "h-12 flex items-center px-3 border-b border-border/50", children: header })), actions !== undefined && (_jsx("div", { "data-kumiko-layout": "sidebar-actions", className: "px-3 py-2 border-b border-border/50 flex flex-row items-center gap-1", children: actions })), _jsx("div", { "data-kumiko-layout": "sidebar-nav", className: "px-3 py-2 flex flex-col gap-0.5 overflow-auto flex-1", children: children }), footer !== undefined && (_jsx("div", { "data-kumiko-layout": "sidebar-footer", className: "px-3 py-2 border-t mt-auto flex flex-col gap-1 text-xs text-muted-foreground", children: footer }))] }));
|
|
11
11
|
}
|
|
@@ -3,11 +3,11 @@ type DispatchListener = (target: TargetRef) => void;
|
|
|
3
3
|
export declare function dispatchTarget(target: TargetRef): void;
|
|
4
4
|
/** Test-Hook: Exklusiver Spy. Returnt cleanup. */
|
|
5
5
|
export declare function setDispatchListener(fn: DispatchListener): () => void;
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
6
|
+
/** Registers a production subscriber and returns an unsubscribe function.
|
|
7
|
+
* It runs only when no test listener is active (test isolation). */
|
|
8
8
|
export declare function subscribeTargetDispatches(fn: DispatchListener): () => void;
|
|
9
|
-
/**
|
|
10
|
-
* nav.searchParams (URL
|
|
11
|
-
*
|
|
9
|
+
/** Recommended production dispatch. Writes the target to
|
|
10
|
+
* nav.searchParams (URL state, F5-compatible) AND calls dispatchTarget for
|
|
11
|
+
* test listeners and legacy subscribers. */
|
|
12
12
|
export declare function useDispatchTarget(): (target: TargetRef) => void;
|
|
13
13
|
export {};
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
// TargetResolver
|
|
2
|
-
//
|
|
3
|
-
// target
|
|
4
|
-
//
|
|
1
|
+
// TargetResolver: multi-listener target dispatch with a test hook, plus a
|
|
2
|
+
// URL-state bridge via the useDispatchTarget hook. Production writes
|
|
3
|
+
// the target to nav.searchParams (F5 recovery); the subscription stream remains
|
|
4
|
+
// available for test hooks (setDispatchListener) and apps without a NavProvider.
|
|
5
5
|
//
|
|
6
|
-
// Dispatch
|
|
7
|
-
// 1. Test
|
|
8
|
-
//
|
|
9
|
-
// 2. Production
|
|
10
|
-
//
|
|
11
|
-
// 3.
|
|
12
|
-
//
|
|
6
|
+
// Dispatch priority:
|
|
7
|
+
// 1. Test listener (setDispatchListener) — exclusive; no production
|
|
8
|
+
// subscriber runs during tests (test isolation).
|
|
9
|
+
// 2. Production subscribers (subscribeTargetDispatches) — all registered
|
|
10
|
+
// production subscribers.
|
|
11
|
+
// 3. No test listener and no subscribers → console.debug fallback so unhandled
|
|
12
|
+
// clicks remain visible.
|
|
13
13
|
//
|
|
14
|
-
// **useDispatchTarget
|
|
15
|
-
// NavTree
|
|
16
|
-
//
|
|
17
|
-
//
|
|
14
|
+
// **useDispatchTarget** is the recommended production path.
|
|
15
|
+
// NavTree calls the hook from a target node's click handler. It writes the URL
|
|
16
|
+
// via nav.setSearchParams AND calls the global dispatchTarget for compatibility
|
|
17
|
+
// with test listeners.
|
|
18
18
|
import { useCallback } from "react";
|
|
19
19
|
import { rendererDebug } from "../../dev-console.js";
|
|
20
20
|
import { requestLeave, useNav } from "../../index.js";
|
|
@@ -41,17 +41,17 @@ export function setDispatchListener(fn) {
|
|
|
41
41
|
testListener = undefined;
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
44
|
+
/** Registers a production subscriber and returns an unsubscribe function.
|
|
45
|
+
* It runs only when no test listener is active (test isolation). */
|
|
46
46
|
export function subscribeTargetDispatches(fn) {
|
|
47
47
|
subscribers.add(fn);
|
|
48
48
|
return () => {
|
|
49
49
|
subscribers.delete(fn);
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
53
|
-
* nav.searchParams (URL
|
|
54
|
-
*
|
|
52
|
+
/** Recommended production dispatch. Writes the target to
|
|
53
|
+
* nav.searchParams (URL state, F5-compatible) AND calls dispatchTarget for
|
|
54
|
+
* test listeners and legacy subscribers. */
|
|
55
55
|
export function useDispatchTarget() {
|
|
56
56
|
const nav = useNav();
|
|
57
57
|
return useCallback((target) => {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { TargetRef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
/**
|
|
3
|
-
* existing `a_*` keys plus `t`
|
|
4
|
-
*
|
|
2
|
+
/** Builds a search-params update for `setSearchParams`. Clears all
|
|
3
|
+
* existing `a_*` keys plus `t` so switching targets cannot leave stale
|
|
4
|
+
* arguments behind. Returns null-valued entries to clear, plus the new
|
|
5
5
|
* target entries. */
|
|
6
6
|
export declare function serializeTarget(target: TargetRef, currentParams: Readonly<Record<string, string>>): Readonly<Record<string, string | null>>;
|
|
7
|
-
/**
|
|
8
|
-
* Returns undefined
|
|
7
|
+
/** Parses the current search parameters back into a TargetRef.
|
|
8
|
+
* Returns undefined when the `t` key is missing, indicating no active target. */
|
|
9
9
|
export declare function parseTargetFromSearchParams(params: Readonly<Record<string, string>>): TargetRef | undefined;
|
|
10
|
-
/** Returns null-valued updates that clear target
|
|
11
|
-
*
|
|
10
|
+
/** Returns null-valued updates that clear the target and all arguments. Used by
|
|
11
|
+
* the close button and on unmount. */
|
|
12
12
|
export declare function clearTargetSearchParams(currentParams: Readonly<Record<string, string>>): Readonly<Record<string, string | null>>;
|