@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
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
// Avatar
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// bunte Sidebar.
|
|
2
|
+
// Avatar pill with initials. The color is based on a string hash so the same
|
|
3
|
+
// user always gets the same color (linear pattern). Eight hue targets provide
|
|
4
|
+
// good distinction without making the sidebar overly colorful.
|
|
6
5
|
//
|
|
7
|
-
//
|
|
8
|
-
// `id`
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// Usage: `<Avatar id="user-123" label="Daniel Hennig" size="md" />`.
|
|
7
|
+
// `id` is the color's hash source; `label` is used for initials and the
|
|
8
|
+
// aria-label. There is no image slot yet; one follows once apps provide real
|
|
9
|
+
// avatar URLs.
|
|
11
10
|
import { useMemo } from "react";
|
|
12
11
|
import { cn } from "../lib/cn.js";
|
|
13
|
-
// Tailwind
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// (Avatar-Farbe sollte deterministic sein, nicht theme-abhängig).
|
|
12
|
+
// Tailwind classes for each hue. Background lightness is moderate (550–600)
|
|
13
|
+
// for saturation, while foreground lightness is high (50–100) for contrast.
|
|
14
|
+
// Fixed colors keep avatar colors deterministic and independent of the theme,
|
|
15
|
+
// even though the tokens work in both modes through CSS variables.
|
|
18
16
|
const COLOR_CLASSES = [
|
|
19
17
|
"bg-rose-600 text-rose-50",
|
|
20
18
|
"bg-orange-600 text-orange-50",
|
|
@@ -31,8 +29,8 @@ const SIZE_CLASSES = {
|
|
|
31
29
|
lg: "size-8 text-xs",
|
|
32
30
|
};
|
|
33
31
|
function hashCode(str) {
|
|
34
|
-
// djb2
|
|
35
|
-
//
|
|
32
|
+
// A djb2 variant: fast and sufficiently well distributed for 8 buckets.
|
|
33
|
+
// A cryptographic hash would be overkill for color bucketing.
|
|
36
34
|
let hash = 5381;
|
|
37
35
|
for (let i = 0; i < str.length; i++) {
|
|
38
36
|
hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
|
|
@@ -44,8 +42,8 @@ function pickColor(id) {
|
|
|
44
42
|
return COLOR_CLASSES[idx] ?? COLOR_CLASSES[0] ?? "bg-muted";
|
|
45
43
|
}
|
|
46
44
|
function extractInitials(label) {
|
|
47
|
-
// "Daniel Hennig" → "DH". "alice@example.com" → "AL".
|
|
48
|
-
//
|
|
45
|
+
// "Daniel Hennig" → "DH". "alice@example.com" → "AL". For a single word,
|
|
46
|
+
// it falls back to the first 2 letters ("Daniel" → "DA"). Everything is
|
|
49
47
|
// upper-case.
|
|
50
48
|
const trimmed = label.trim();
|
|
51
49
|
if (trimmed.length === 0)
|
|
@@ -5,37 +5,36 @@ export type DefaultAppShellUser = {
|
|
|
5
5
|
readonly roles: readonly string[];
|
|
6
6
|
};
|
|
7
7
|
export type DefaultAppShellProps = {
|
|
8
|
-
/** Header
|
|
9
|
-
*
|
|
8
|
+
/** Header slot at the top of the sidebar — workspace identity (logo, name,
|
|
9
|
+
* plan tag). The caller provides the content. */
|
|
10
10
|
readonly brand: ReactNode;
|
|
11
|
-
/** Schema (AppSchema
|
|
12
|
-
*
|
|
13
|
-
* schema.navs (per-
|
|
11
|
+
/** Schema (AppSchema or legacy FeatureSchema) passed through to NavTree;
|
|
12
|
+
* sidebar entries are generated automatically from
|
|
13
|
+
* schema.navs (per-feature). */
|
|
14
14
|
readonly schema: AppSchema | FeatureSchema;
|
|
15
|
-
/** Current user — drives
|
|
16
|
-
* role-gated
|
|
17
|
-
*
|
|
18
|
-
* anonymous). */
|
|
15
|
+
/** Current user — drives navigation role gating. Without a user prop,
|
|
16
|
+
* role-gated navigation entries (`access: { roles: [...] }`) are hidden
|
|
17
|
+
* completely (resolveNavigation treats an undefined user as anonymous). */
|
|
19
18
|
readonly user?: DefaultAppShellUser;
|
|
20
|
-
/** Icon
|
|
21
|
-
* ThemeToggle, TenantSwitcher. */
|
|
19
|
+
/** Icon row between the brand and NavTree — typically a Search trigger,
|
|
20
|
+
* ThemeToggle, or TenantSwitcher. */
|
|
22
21
|
readonly sidebarActions?: ReactNode;
|
|
23
|
-
/** Runtime
|
|
24
|
-
*
|
|
25
|
-
* (
|
|
22
|
+
/** Runtime badges per navigation item (bare nav-id → ReactNode), passed
|
|
23
|
+
* through to NavTree. For tier/status badges supplied by the app per user
|
|
24
|
+
* (value + color), rather than embedded in the static schema. */
|
|
26
25
|
readonly navBadges?: ReadonlyMap<string, ReactNode>;
|
|
27
|
-
/**
|
|
28
|
-
* ThemeToggle,
|
|
29
|
-
*
|
|
26
|
+
/** Right-aligned slot in the topbar (next to the breadcrumb) — for
|
|
27
|
+
* ThemeToggle, global actions, and similar controls. Avoids a separate
|
|
28
|
+
* sidebarActions row in the sidebar. */
|
|
30
29
|
readonly headerActions?: ReactNode;
|
|
31
|
-
/** Footer
|
|
32
|
-
*
|
|
30
|
+
/** Footer slot at the bottom of the sidebar — profile row, help link,
|
|
31
|
+
* or plan banner. */
|
|
33
32
|
readonly sidebarFooter?: ReactNode;
|
|
34
|
-
/** Viewport-fit
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
/** Viewport-fit shell. true → fixed viewport height (`h-svh`); content
|
|
34
|
+
* scrolls inside instead of the whole page. Defaults to true — `fill={false}`
|
|
35
|
+
* switches back to page scrolling. */
|
|
37
36
|
readonly fill?: boolean;
|
|
38
|
-
/** Screen
|
|
37
|
+
/** Screen content rendered inside SidebarInset. */
|
|
39
38
|
readonly children: ReactNode;
|
|
40
39
|
};
|
|
41
40
|
export declare function DefaultAppShell({ brand, schema, user, sidebarActions, headerActions, navBadges, sidebarFooter, fill, children, }: DefaultAppShellProps): ReactNode;
|
|
@@ -15,9 +15,9 @@ function EditorPanelInner({ target, resolvers, onClose, }) {
|
|
|
15
15
|
}
|
|
16
16
|
export function EditorPanel({ resolvers }) {
|
|
17
17
|
const nav = useNav();
|
|
18
|
-
// target derived from URL
|
|
19
|
-
// useMemo
|
|
20
|
-
//
|
|
18
|
+
// target is derived from the URL, so F5 and Back restore the state.
|
|
19
|
+
// useMemo keeps the reference stable while searchParams are shallow-equal
|
|
20
|
+
// (the nav implementation provides a plain record for this check).
|
|
21
21
|
const target = useMemo(() => parseTargetFromSearchParams(nav.searchParams), [nav.searchParams]);
|
|
22
22
|
const handleClose = useCallback(() => {
|
|
23
23
|
requestLeave(() => nav.setSearchParams(clearTargetSearchParams(nav.searchParams)));
|
|
@@ -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[];
|
|
@@ -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
|
}
|