@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.
Files changed (185) hide show
  1. package/LICENSE +21 -57
  2. package/dist/app/action-form-body.d.ts +2 -2
  3. package/dist/app/action-form-body.js +2 -2
  4. package/dist/app/action-form-shim.d.ts +8 -8
  5. package/dist/app/action-form-shim.js +20 -22
  6. package/dist/app/app-features-context.js +4 -4
  7. package/dist/app/column-renderers.d.ts +7 -9
  8. package/dist/app/column-renderers.js +15 -18
  9. package/dist/app/config-edit-body.js +1 -1
  10. package/dist/app/config-edit-shim.d.ts +7 -8
  11. package/dist/app/config-edit-shim.js +18 -20
  12. package/dist/app/custom-screens.d.ts +3 -3
  13. package/dist/app/custom-screens.js +8 -8
  14. package/dist/app/dashboard-body.d.ts +2 -2
  15. package/dist/app/dashboard-body.js +2 -2
  16. package/dist/app/entity-edit-prefill.js +9 -9
  17. package/dist/app/entity-edit-screen.js +4 -4
  18. package/dist/app/entity-list-screen.js +3 -3
  19. package/dist/app/extension-form-submit.d.ts +1 -1
  20. package/dist/app/extension-form-submit.js +14 -15
  21. package/dist/app/extension-sections.d.ts +24 -25
  22. package/dist/app/extension-sections.js +10 -10
  23. package/dist/app/feature-schema.js +11 -11
  24. package/dist/app/form-schema.js +1 -1
  25. package/dist/app/header-actions-bar.js +1 -1
  26. package/dist/app/kumiko-screen.js +8 -10
  27. package/dist/app/layout-fields.js +1 -1
  28. package/dist/app/list-facets.js +3 -3
  29. package/dist/app/nav.d.ts +10 -10
  30. package/dist/app/nav.js +13 -13
  31. package/dist/app/projection-detail-body.js +4 -4
  32. package/dist/app/projection-detail-shim.d.ts +6 -5
  33. package/dist/app/projection-detail-shim.js +23 -22
  34. package/dist/app/projection-list-body.js +10 -10
  35. package/dist/app/projection-list-shim.d.ts +5 -5
  36. package/dist/app/projection-list-shim.js +17 -18
  37. package/dist/app/qn.js +10 -12
  38. package/dist/app/qualify-screen-id.js +1 -1
  39. package/dist/app/screen-access.js +2 -2
  40. package/dist/app/screen-shared.js +1 -1
  41. package/dist/app/secret-mint-body.js +4 -4
  42. package/dist/app/write-failed-error.js +8 -7
  43. package/dist/components/embedded-list-field.js +4 -4
  44. package/dist/components/grid-cell-for-field.d.ts +1 -1
  45. package/dist/components/reference-create-dialog.js +3 -3
  46. package/dist/components/related-list-section.d.ts +1 -1
  47. package/dist/components/related-list-section.js +28 -5
  48. package/dist/components/render-edit-logic.js +1 -1
  49. package/dist/components/render-edit-types.d.ts +6 -6
  50. package/dist/components/render-edit.js +63 -65
  51. package/dist/components/render-field.d.ts +7 -7
  52. package/dist/components/render-field.js +57 -59
  53. package/dist/components/render-list.d.ts +43 -44
  54. package/dist/components/render-list.js +63 -67
  55. package/dist/components/use-wizard-form-draft.js +10 -10
  56. package/dist/context/dispatcher-context.js +5 -5
  57. package/dist/context/user-roles-context.js +2 -2
  58. package/dist/headless/apex/index.d.ts +1 -2
  59. package/dist/headless/apex/index.js +5 -6
  60. package/dist/headless/form/form-controller.js +6 -6
  61. package/dist/headless/form/zod-bridge.js +2 -2
  62. package/dist/headless/format/money.js +1 -1
  63. package/dist/headless/index.js +2 -2
  64. package/dist/headless/view-model/edit.js +2 -2
  65. package/dist/headless/view-model/list.js +3 -4
  66. package/dist/headless/view-model/types.d.ts +3 -3
  67. package/dist/hooks/reference-limits.js +11 -12
  68. package/dist/hooks/use-list-url-state.d.ts +13 -13
  69. package/dist/hooks/use-list-url-state.js +21 -22
  70. package/dist/hooks/use-query.js +3 -3
  71. package/dist/hooks/use-reference-lookup.js +16 -17
  72. package/dist/hooks/use-store.js +18 -22
  73. package/dist/hooks/use-stream-handler.js +4 -2
  74. package/dist/i18n-defaults.js +2 -8
  75. package/dist/i18n.d.ts +10 -10
  76. package/dist/i18n.js +25 -25
  77. package/dist/index.js +5 -4
  78. package/dist/live/dispatcher-live.js +2 -2
  79. package/dist/primitives.d.ts +225 -221
  80. package/dist/primitives.js +1 -1
  81. package/dist/sse/live-events.d.ts +5 -5
  82. package/dist/sse/live-events.js +17 -17
  83. package/dist/tokens.d.ts +8 -8
  84. package/dist/tokens.js +16 -17
  85. package/dist/web/app/browser-locale.d.ts +9 -9
  86. package/dist/web/app/browser-locale.js +19 -19
  87. package/dist/web/app/client-plugin.d.ts +44 -52
  88. package/dist/web/app/client-plugin.js +4 -5
  89. package/dist/web/app/create-app.d.ts +28 -31
  90. package/dist/web/app/create-app.js +21 -22
  91. package/dist/web/app/document-lang-sync.js +1 -1
  92. package/dist/web/app/draft-storage.js +1 -1
  93. package/dist/web/app/nav-providers-context.d.ts +3 -3
  94. package/dist/web/app/nav-providers-context.js +3 -3
  95. package/dist/web/app/nav.d.ts +18 -17
  96. package/dist/web/app/nav.js +66 -64
  97. package/dist/web/app/rich-content-editor.js +1 -1
  98. package/dist/web/components/config-cascade.js +17 -18
  99. package/dist/web/components/config-source-badge.js +4 -4
  100. package/dist/web/layout/app-layout.d.ts +9 -9
  101. package/dist/web/layout/app-layout.js +2 -2
  102. package/dist/web/layout/avatar.d.ts +4 -3
  103. package/dist/web/layout/avatar.js +15 -17
  104. package/dist/web/layout/default-app-shell.d.ts +22 -23
  105. package/dist/web/layout/editor-panel.js +3 -3
  106. package/dist/web/layout/nav-tree.js +86 -84
  107. package/dist/web/layout/profile-menu.d.ts +1 -1
  108. package/dist/web/layout/sidebar-brand.d.ts +7 -7
  109. package/dist/web/layout/sidebar-user.d.ts +1 -1
  110. package/dist/web/layout/sidebar-user.js +4 -4
  111. package/dist/web/layout/sidebar.d.ts +8 -8
  112. package/dist/web/layout/sidebar.js +7 -7
  113. package/dist/web/layout/target-resolver-stub.d.ts +5 -5
  114. package/dist/web/layout/target-resolver-stub.js +20 -20
  115. package/dist/web/layout/target-url.d.ts +7 -7
  116. package/dist/web/layout/target-url.js +22 -22
  117. package/dist/web/layout/theme-toggle.js +4 -5
  118. package/dist/web/layout/workspace-shell.d.ts +21 -21
  119. package/dist/web/layout/workspace-shell.js +29 -30
  120. package/dist/web/lib/cn.d.ts +4 -4
  121. package/dist/web/lib/cn.js +4 -4
  122. package/dist/web/primitives/action-menu.d.ts +8 -8
  123. package/dist/web/primitives/action-menu.js +14 -13
  124. package/dist/web/primitives/calendar-popover.d.ts +1 -1
  125. package/dist/web/primitives/calendar-popover.js +10 -10
  126. package/dist/web/primitives/card-styles.js +1 -1
  127. package/dist/web/primitives/card.js +2 -2
  128. package/dist/web/primitives/combobox.d.ts +12 -12
  129. package/dist/web/primitives/combobox.js +34 -35
  130. package/dist/web/primitives/data-table-cell.js +2 -2
  131. package/dist/web/primitives/data-table.js +2 -3
  132. package/dist/web/primitives/date-field.js +2 -2
  133. package/dist/web/primitives/date-input.js +4 -4
  134. package/dist/web/primitives/default-form.js +2 -2
  135. package/dist/web/primitives/dropdown-menu.js +14 -15
  136. package/dist/web/primitives/embedded-list-input.js +3 -3
  137. package/dist/web/primitives/facet-filter.js +1 -1
  138. package/dist/web/primitives/field.js +9 -8
  139. package/dist/web/primitives/form.d.ts +1 -1
  140. package/dist/web/primitives/form.js +6 -6
  141. package/dist/web/primitives/input.js +2 -2
  142. package/dist/web/primitives/layout-elements.js +1 -1
  143. package/dist/web/primitives/layout.js +2 -2
  144. package/dist/web/primitives/located-timestamp-input.js +4 -4
  145. package/dist/web/primitives/money-input.js +13 -17
  146. package/dist/web/primitives/pager.js +1 -1
  147. package/dist/web/primitives/row-actions.js +3 -4
  148. package/dist/web/primitives/secret-reveal.js +1 -1
  149. package/dist/web/primitives/timestamp-input.d.ts +9 -9
  150. package/dist/web/primitives/timestamp-input.js +21 -21
  151. package/dist/web/primitives/toast.js +12 -13
  152. package/dist/web/sse/live-events.d.ts +6 -6
  153. package/dist/web/sse/live-events.js +3 -3
  154. package/dist/web/tokens.d.ts +11 -12
  155. package/dist/web/tokens.js +35 -37
  156. package/dist/web/version/update-checker.js +14 -13
  157. package/dist/web/widgets/ai-text-field.js +1 -1
  158. package/dist/web/widgets/charts.d.ts +10 -10
  159. package/dist/web/widgets/charts.js +8 -8
  160. package/dist/web/widgets/collapsible-section.d.ts +3 -3
  161. package/dist/web/widgets/collapsible-section.js +6 -6
  162. package/dist/web/widgets/detail-list.d.ts +1 -3
  163. package/dist/web/widgets/detail-list.js +1 -3
  164. package/dist/web/widgets/feed-list.d.ts +2 -2
  165. package/dist/web/widgets/feed-list.js +2 -2
  166. package/dist/web/widgets/form-fields.d.ts +14 -14
  167. package/dist/web/widgets/form-fields.js +14 -13
  168. package/dist/web/widgets/index.js +4 -4
  169. package/dist/web/widgets/infinity-list.js +3 -3
  170. package/dist/web/widgets/mode-switch.d.ts +2 -2
  171. package/dist/web/widgets/mode-switch.js +2 -2
  172. package/dist/web/widgets/progress-list.d.ts +1 -2
  173. package/dist/web/widgets/progress-list.js +1 -2
  174. package/dist/web/widgets/query-table.d.ts +6 -6
  175. package/dist/web/widgets/result-panel.d.ts +16 -16
  176. package/dist/web/widgets/result-panel.js +10 -10
  177. package/dist/web/widgets/section-card.d.ts +3 -3
  178. package/dist/web/widgets/section-card.js +3 -3
  179. package/dist/web/widgets/stat.d.ts +4 -4
  180. package/dist/web/widgets/stat.js +6 -6
  181. package/dist/web/widgets/states.d.ts +4 -4
  182. package/dist/web/widgets/states.js +4 -4
  183. package/dist/web/widgets/use-draft.d.ts +7 -7
  184. package/dist/web/widgets/use-draft.js +7 -7
  185. package/package.json +6 -6
@@ -1,20 +1,18 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- // Avatar-Pill mit Initials. Color-coded background basierend auf
3
- // string-hash damit der gleiche User immer dieselbe Farbe bekommt
4
- // (Linear-Pattern). 8 Hue-Targets für gute Distinktion ohne über-
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
- // Nutzung: `<Avatar id="user-123" label="Daniel Hennig" size="md" />`.
8
- // `id` ist die Hash-Quelle für die Farbe; `label` wird für Initials
9
- // + aria-label genutzt. Kein Image-Slot im MVP — kommt wenn Apps
10
- // echte Avatar-URLs haben.
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-Klassen pro Hue. Die Background-Lightness ist dezent (550-
14
- // 600 für saturation), die Foreground-Lightness ist hell (50-100) für
15
- // Kontrast. Passt zu beiden Modes weil die Tokens via CSS-vars greifen
16
- // — aber: feste hex-Farben hier sind stabiler als token-mapping
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-Variante — schnell, deterministic, gut genug verteilt für
35
- // 8 Buckets. Crypto-Hash wäre Overkill für color-bucketing.
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". Single-word
48
- // fällt auf erste 2 Buchstaben zurück ("Daniel" → "DA"). Alles
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-Slot oben in der Sidebar — Workspace-Identity (Logo, Name,
9
- * Plan-Tag). Caller liefert frei. */
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 oder legacy FeatureSchema) wird an NavTree
12
- * durchgereicht; Sidebar-Einträge bauen sich automatisch aus
13
- * schema.navs (per-Feature) auf. */
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 nav role-gating. Ohne user-prop werden
16
- * role-gated nav-einträge (`access: { roles: [...] }`) komplett
17
- * ausgeblendet (resolveNavigation behandelt undefined-user als
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-Row zwischen Brand und NavTree — typisch Search-Trigger,
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-Badges pro Nav-Item (bare nav-id → ReactNode), an NavTree
24
- * durchgereicht. Für Tier-/Status-Badges, die die App pro User liefert
25
- * (Wert + Farbe), statt sie ins statische Schema zu backen. */
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
- /** Rechtsbündiger Slot in der Topbar (neben dem Breadcrumb) — für
28
- * ThemeToggle, globale Actions o.ä. Spart die eigene sidebarActions-
29
- * Zeile in der Sidebar. */
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-Slot unten in der Sidebar — Profile-Row, Help-Link,
32
- * Plan-Banner. */
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 Shell. true → fixe Viewport-Höhe (`h-svh`), der Content
35
- * scrollt INNEN statt der ganzen Seite. Default true — `fill={false}`
36
- * schaltet zurück auf Seiten-Scroll. */
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-Content der im SidebarInset gerendert wird. */
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 → F5/Back stellen state wieder her.
19
- // useMemo stabilisiert reference solange searchParams shallow-gleich
20
- // sind (nav-Impl liefert plain-record für genau diesen check).
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
- // Ein Knoten überlebt den Filter, wenn er selbst oder ein Nachfahre matcht.
31
- // Provider-Kinder sind zur Filterzeit schon materialisiert (Provider-Knoten
32
- // sind default-expanded → eager geladen), darum reicht die statische
33
- // children-Rekursion. ponytail: ein zur Filterzeit noch NICHT geladener
34
- // (user-zugeklappter) Provider wird über den force-expand in useNavNodeState
35
- // aufgeklappt, lädt und filtert dann mit.
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 (fw#2724):
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
- // Collapsed-Set: nur die explizit zugeklappten qualified-names. Default
68
- // ist also "alles auf" — neue Features tauchen sofort offen auf, ohne
69
- // dass der User erst klicken muss.
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 (fw#1816).
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
- // Runtime-Badge-Slot rechts am Label, gekeyt auf die bare nav-id. Kein
100
- // Eintrag → nichts (silent, kein Layout-Shift). shrink-0 + die ml-auto
101
- // schiebt ihn ans rechte Ende; das truncate-Label gibt nach.
102
- // ponytail: nur an screen/target-Leaves verdrahtet, nicht an Container-
103
- // Knoten (die tragen den ml-auto-Chevron) — Tier-Badge ist ein Leaf.
104
- // Defense-in-depth (555/4): every current call site already guards on
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-return changes no current behavior — it's here so a future call
107
- // site added without that guard can't silently show a leaf's badge on an
108
- // unrelated container node that happens to share its last-segment.
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
- // Pure section = ein Gruppen-Header ohne eigenen Screen, der nur seine
119
- // children gruppiert. Auf Top-Level wird daraus eine SidebarGroup mit
120
- // togglebarem GroupLabel; ein Node mit Screen (auch wenn er children hat)
121
- // ist dagegen ein klickbarer Menu-Eintrag.
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
- // i18n-Key Konvention: enthält das label einen Punkt, durchs t() laufen
130
- // lassen (Bundle kennt den Key → übersetzt, sonst bleibt der Key sichtbar
131
- // = vergessene Übersetzung). Reine String-Labels bleiben unangetastet.
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
- // Stable-reference empty-list — sonst destabilisiert ein neues `[]` pro
156
- // Render die useEffect-deps der SSE-Subscription.
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-Target-Vergleich: featureId + action exakt, args shallow-equal
159
- // (heute nur primitives). Gespiegelt aus dem alten TreeNodeRenderer.
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
- // Adapter: provider-emittierte TreeNodes → renderbare NavNodes. Synthetische
176
- // QN aus parent-QN + idx-label (stabil über Emit-Order). Statische children
177
- // rekursiv. ponytail: eine TreeChildrenSubscribe-Function als children wird
178
- // als Leaf behandelt — nested-dynamic-Provider nutzt heute kein Consumer;
179
- // upgrade-path = synthetische QN registrieren wenn einer sie emittet.
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-children + SSE-Live-Refresh für einen `provider: true`-Knoten.
197
- // Subscribe läuft erst wenn der Knoten ausgeklappt ist (enabled); die
198
- // treeEntities-Liste (per QN) re-fired den Provider bei Entity-Events → neu
199
- // erstellte Knoten erscheinen live. Logik gespiegelt aus VisualTree.ProviderBranch.
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` triggert SSE-Refresh + erneutes Subscribe — Biome sieht es
226
- // nicht im body, semantisch ist es der re-fire-Trigger.
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
- // Geteilte Knoten-Logik für Top-Level- (NavMenuNode) und Sub-Knoten
260
- // (NavSubNode): Label, Expand/Collapse, active-State (screen ODER target),
261
- // und die lazy Provider-Children. Die zwei Renderer unterscheiden sich nur
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
- // Aktiver Filter klappt jeden Container auf, damit Treffer unter zugeklappten
271
- // Ordnern sichtbar werden (und Provider-Kinder nachladen).
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
- // Ein gefilterter Knoten verschwindet nur, wenn weder er selbst noch ein
294
- // sichtbar gebliebenes Kind matcht — sonst bliebe ein leerer Ordner-Header.
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-Zeilen innerhalb einer Provider-Sub-Liste (lazy-loading / error).
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-Level-Section: SidebarGroup mit STATISCHEM Label (sidebar-07-Muster —
374
- // "Platform"/"Projects" sind feste Überschriften, keine Toggles). Collapse
375
- // gehört auf Items MIT children, nicht auf die Section selbst.
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-Kinder (statisch leer bis geladen) halten die Section offen — ihr
380
- // Match lässt sich hier nicht statisch prüfen; der Knoten selbst self-hidet.
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
- // Ein Menu-Eintrag (Top-Level-Item oder Section-Child). Drei Klick-Modi am
389
- // EINEN Knoten: `screen` → Route-Link (KumikoLink), `target` → EditorPanel-
390
- // Dispatch (useDispatchTarget), keins → Container/Provider-Knoten der nur
391
- // auf-/zuklappt. Provider-Knoten laden ihre Children lazy + SSE-live. Der
392
- // Collapse-Chevron sitzt als SidebarMenuAction daneben (separater Button,
393
- // sonst <button> im <a> = invalides HTML); createAction/actions als
394
- // absolute NodeActions links davon.
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
- // Verschachtelte Einträge (innerhalb SidebarMenuSub). Gleiche drei Klick-Modi
418
- // wie NavMenuNode, nur mit den Sub-Primitives. Folder-/Container-Knoten (kein
419
- // screen/target) klappen auf — Provider-Children + tiefere Statics rekursiv.
420
- // Non-Link-Klickziele brauchen asChild+<button> (SidebarMenuSubButton ist ein
421
- // <a>); der Chevron sitzt als eigener absoluter Button (kein Button-im-<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
- // Kinder in ein eigenes SidebarMenuSub (<ul>) — ein Sub-Item ist ein <li>,
429
- // verschachtelte <li> ohne <ul> dazwischen sind invalides HTML (nested
430
- // Content-Ordner: Marketing/ > Block). Der <ul> trägt zugleich die
431
- // Einrück-/Border-Stufe pro Tiefe.
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
- // Backwards-kompatibler Single-Feature-Builder. Convenience-Wrapper um
451
- // buildNavRegistrySliceForApp — bestehende Tests die direkt mit
452
- // FeatureSchema rufen brauchen keine Änderung.
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-Feature-Variante. Iteriert alle Features, qualifiziert pro
457
- // Feature mit dem eigenen featureName. Reihenfolge: Features in der
458
- // AppSchema-Ordnung, navs in der vom Feature deklarierten Reihenfolge.
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-Feature-Workspaces sind hier nativ unterstützt — `navMembers`
461
- // referenzieren QNs, der Filter trifft die jeweils richtigen Einträge
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 (#1099).
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` lebt jetzt in @cosmicdrift/kumiko-renderer (./app/qn) — eine
544
- // Quelle, beide Pakete teilen sie.
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-Name oder Email — Quelle für Initials. */
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-/App-Name (fett, erste Zeile). */
3
+ /** Workspace/app name (bold, first line). */
4
4
  readonly name: string;
5
- /** Zweite Zeile — Plan, Tagline, Tenant (klein, gedimmt). */
5
+ /** Second line — plan, tagline, or tenant (small, dimmed). */
6
6
  readonly plan?: string;
7
- /** Logo in der Kachel — typisch ein Lucide-Icon. Fehlt es, steht der
8
- * erste Buchstabe des Namens. */
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
- /** Zeigt das ChevronsUpDown-Icon (Aufklapp-Affordance) nur wenn die App den
11
- * Brand tatsächlich in ein Dropdown wrappt. Default false: ohne Dropdown
12
- * ist das Chevron irreführend (suggeriert ein Menü, das nicht aufgeht). */
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-URL. Fehlt sie, stehen die Initialen aus dem Namen. */
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 — der NavUser-Footer aus shadcns sidebar-07: Avatar + Name +
3
- // Email + ChevronsUpDown. App-Author reicht ihn als `sidebarFooter` an
4
- // DefaultAppShell. Reiner Look (kein Account-Dropdown) — wer ein User-Menü
5
- // will, wrappt ihn in einen DropdownMenu.
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-Bereich oben — typisch Workspace-Avatar + Name + Badge.
4
- * Lebt VOR dem scroll-area, bleibt sichtbar wenn das nav scrollt. */
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-Cluster zwischen Header und Nav — typisch "+ Neu" Button
7
- * und Search-Icon. Linear-Pattern: prominent, nicht im scroll-area. */
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 + freie Sections (z.B. "Your teams" Header + Sub-Items).
10
- * Scrollt eigenständig wenn der Inhalt zu lang wird. */
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-Bereich unten — typisch Invite-Link, Help, Plan-Banner.
13
- * Klebt am unteren Rand via mt-auto. */
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-Pattern: 4 vertikale Bereiche
4
- // 1. Header (Workspace-Identity)
5
- // 2. Actions (Quick-Buttons, nicht-scrollend)
6
- // 3. Nav-Scroll-Area (NavTree, scrollt wenn nötig)
7
- // 4. Footer (klebt unten via mt-auto)
8
- // bg-muted/40 + border-r für visuelle Distinction zur Main-Area;
9
- // tighter padding (p-3) und kompakte gap-0.5 zwischen Items.
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
  }