@exegia/corpora-ui 0.20.0 → 0.22.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 (165) hide show
  1. package/dist-lib/components/blocks/auth/auth-flow-atom.d.ts +77 -0
  2. package/dist-lib/components/blocks/auth/auth-session-atom.d.ts +28 -0
  3. package/dist-lib/components/blocks/auth/auth-state-type.d.ts +71 -0
  4. package/dist-lib/components/blocks/auth/auth-state.d.ts +9 -0
  5. package/dist-lib/components/blocks/auth/use-auth-state.d.ts +52 -0
  6. package/dist-lib/components/blocks/layout.d.ts +4 -1
  7. package/dist-lib/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.d.ts +1 -0
  8. package/dist-lib/components/blocks/nav/sidebar/ai-sidebar-atom.d.ts +217 -0
  9. package/dist-lib/components/blocks/nav/sidebar/index.d.ts +3 -1
  10. package/dist-lib/components/blocks/nav/sidebar/sidebar-context.d.ts +3 -0
  11. package/dist-lib/components/blocks/nav/sidebar/sidebar-row.d.ts +3 -1
  12. package/dist-lib/components/blocks/nav/sidebar/type.d.ts +109 -29
  13. package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar-state.d.ts +29 -0
  14. package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +6 -1
  15. package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +1 -1
  16. package/dist-lib/components/blocks/profile/index.d.ts +7 -0
  17. package/dist-lib/components/blocks/profile/profile-card-atom.d.ts +68 -0
  18. package/dist-lib/components/blocks/profile/profile-card-block.d.ts +28 -39
  19. package/dist-lib/components/blocks/profile/type.d.ts +80 -0
  20. package/dist-lib/components/blocks/profile/use-profile-card-state.d.ts +24 -0
  21. package/dist-lib/components/blocks/profile/use-profile-card.d.ts +34 -0
  22. package/dist-lib/components/blocks/shell/__tests__/shell-fit-atom.test.d.ts +1 -0
  23. package/dist-lib/components/blocks/shell/__tests__/shell-metrics.test.d.ts +1 -0
  24. package/dist-lib/components/blocks/shell/animated-panel-provider.d.ts +1 -1
  25. package/dist-lib/components/blocks/shell/animated-panel.d.ts +1 -1
  26. package/dist-lib/components/blocks/shell/index.d.ts +4 -12
  27. package/dist-lib/components/blocks/shell/shell-fit-atom.d.ts +68 -0
  28. package/dist-lib/components/blocks/shell/shell-metrics.d.ts +54 -0
  29. package/dist-lib/components/blocks/shell/type.d.ts +115 -3
  30. package/dist-lib/components/blocks/shell/use-shell-fit-state.d.ts +25 -0
  31. package/dist-lib/components/blocks/shell/use-shell-fit.d.ts +14 -0
  32. package/dist-lib/components/blocks/shell/use-shell-panels.d.ts +12 -1
  33. package/dist-lib/components/blocks/shell/utils.d.ts +32 -0
  34. package/dist-lib/components/composed/__tests__/logo.test.d.ts +1 -0
  35. package/dist-lib/components/composed/logo.d.ts +34 -0
  36. package/dist-lib/components/composed/tree/__tests__/tree-atom.test.d.ts +1 -0
  37. package/dist-lib/components/composed/tree/constants.d.ts +4 -4
  38. package/dist-lib/components/composed/tree/index.d.ts +3 -1
  39. package/dist-lib/components/composed/tree/tree-atom.d.ts +160 -0
  40. package/dist-lib/components/composed/tree/tree-node.d.ts +6 -1
  41. package/dist-lib/components/composed/tree/type.d.ts +116 -18
  42. package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +18 -7
  43. package/dist-lib/components/composed/tree/use-tree-state.d.ts +25 -0
  44. package/dist-lib/components/composed/tree/use-tree.d.ts +4 -0
  45. package/dist-lib/components/composed/user-avatar.d.ts +5 -28
  46. package/dist-lib/components/user-avatar/__tests__/user-avatar.test.d.ts +1 -0
  47. package/dist-lib/components/user-avatar/component.d.ts +1 -8
  48. package/dist-lib/components/user-avatar/index.d.ts +5 -0
  49. package/dist-lib/components/user-avatar/presence-badge.d.ts +16 -0
  50. package/dist-lib/components/user-avatar/type.d.ts +53 -0
  51. package/dist-lib/components/user-avatar/use-user-avatar-state.d.ts +26 -0
  52. package/dist-lib/components/user-avatar/use-user-avatar.d.ts +37 -0
  53. package/dist-lib/components/user-avatar/user-avatar-atom.d.ts +64 -0
  54. package/dist-lib/components/user-avatar/utils.d.ts +22 -0
  55. package/dist-lib/index.d.ts +6 -3
  56. package/dist-lib/index.js +3732 -2670
  57. package/dist-lib/index.js.map +1 -1
  58. package/dist-lib/state/exegia-provider.d.ts +51 -0
  59. package/dist-lib/state/index.d.ts +4 -0
  60. package/dist-lib/state/store.d.ts +17 -0
  61. package/package.json +15 -12
  62. package/src/components/blocks/auth/__tests__/auth-state-atom.test.tsx +247 -0
  63. package/src/components/blocks/auth/auth-flow-atom.ts +238 -0
  64. package/src/components/blocks/auth/auth-session-atom.ts +82 -0
  65. package/src/components/blocks/auth/auth-state-type.ts +97 -0
  66. package/src/components/blocks/auth/auth-state.ts +52 -0
  67. package/src/components/blocks/auth/use-auth-state.ts +127 -0
  68. package/src/components/blocks/layout.ts +30 -2
  69. package/src/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.tsx +325 -0
  70. package/src/components/blocks/nav/sidebar/ai-sidebar-atom.ts +698 -0
  71. package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +48 -42
  72. package/src/components/blocks/nav/sidebar/index.ts +53 -1
  73. package/src/components/blocks/nav/sidebar/sidebar-context.ts +15 -0
  74. package/src/components/blocks/nav/sidebar/sidebar-row.tsx +119 -50
  75. package/src/components/blocks/nav/sidebar/type.ts +125 -30
  76. package/src/components/blocks/nav/sidebar/use-ai-sidebar-state.ts +123 -0
  77. package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +301 -248
  78. package/src/components/blocks/nav/sidebar/utils.ts +1 -1
  79. package/src/components/blocks/profile/__tests__/profile-card-block.test.tsx +194 -0
  80. package/src/components/blocks/profile/index.ts +28 -0
  81. package/src/components/blocks/profile/profile-card-atom.ts +247 -0
  82. package/src/components/blocks/profile/profile-card-block.tsx +153 -74
  83. package/src/components/blocks/profile/type.ts +95 -0
  84. package/src/components/blocks/profile/use-profile-card-state.ts +67 -0
  85. package/src/components/blocks/profile/use-profile-card.ts +126 -0
  86. package/src/components/blocks/shell/__tests__/shell-fit-atom.test.tsx +360 -0
  87. package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +192 -3
  88. package/src/components/blocks/shell/__tests__/shell-metrics.test.ts +108 -0
  89. package/src/components/blocks/shell/animated-panel-inset.tsx +5 -1
  90. package/src/components/blocks/shell/animated-panel-provider.tsx +77 -11
  91. package/src/components/blocks/shell/animated-panel-trigger.tsx +4 -0
  92. package/src/components/blocks/shell/animated-panel.tsx +126 -29
  93. package/src/components/blocks/shell/index.ts +18 -13
  94. package/src/components/blocks/shell/shell-fit-atom.ts +243 -0
  95. package/src/components/blocks/shell/shell-layout.tsx +55 -53
  96. package/src/components/blocks/shell/shell-metrics.ts +79 -0
  97. package/src/components/blocks/shell/type.ts +130 -3
  98. package/src/components/blocks/shell/use-shell-fit-state.ts +49 -0
  99. package/src/components/blocks/shell/use-shell-fit.ts +135 -0
  100. package/src/components/blocks/shell/use-shell-panels.ts +57 -4
  101. package/src/components/blocks/shell/utils.ts +44 -4
  102. package/src/components/composed/__tests__/logo.test.tsx +60 -0
  103. package/src/components/composed/logo.tsx +159 -0
  104. package/src/components/composed/tree/CLAUDE.md +132 -0
  105. package/src/components/composed/tree/__tests__/tree-atom.test.tsx +217 -0
  106. package/src/components/composed/tree/__tests__/tree.test.tsx +162 -37
  107. package/src/components/composed/tree/__tests__/use-tree.test.tsx +2 -1
  108. package/src/components/composed/tree/constants.ts +4 -4
  109. package/src/components/composed/tree/index.ts +38 -0
  110. package/src/components/composed/tree/tree-atom.ts +590 -0
  111. package/src/components/composed/tree/tree-node.tsx +117 -78
  112. package/src/components/composed/tree/tree.tsx +24 -16
  113. package/src/components/composed/tree/type.ts +125 -15
  114. package/src/components/composed/tree/use-tree-dnd.ts +82 -51
  115. package/src/components/composed/tree/use-tree-state.ts +105 -0
  116. package/src/components/composed/tree/use-tree.ts +185 -184
  117. package/src/components/composed/user-avatar.tsx +11 -99
  118. package/src/components/user-avatar/__tests__/user-avatar.test.tsx +286 -0
  119. package/src/components/user-avatar/component.tsx +151 -22
  120. package/src/components/user-avatar/fallback.tsx +3 -1
  121. package/src/components/user-avatar/index.ts +19 -0
  122. package/src/components/user-avatar/presence-badge.tsx +56 -0
  123. package/src/components/user-avatar/type.ts +58 -0
  124. package/src/components/user-avatar/use-user-avatar-state.ts +60 -0
  125. package/src/components/user-avatar/use-user-avatar.ts +180 -0
  126. package/src/components/user-avatar/user-avatar-atom.ts +218 -0
  127. package/src/components/user-avatar/utils.ts +94 -0
  128. package/src/index.ts +35 -3
  129. package/src/state/exegia-provider.tsx +79 -0
  130. package/src/state/index.ts +4 -0
  131. package/src/state/store.ts +19 -0
  132. package/dist-lib/components/blocks/nav/sidebar-block.d.ts +0 -14
  133. package/dist-lib/components/blocks/nav/sidebar-nav-row.d.ts +0 -3
  134. package/dist-lib/components/blocks/nav/types.d.ts +0 -60
  135. package/dist-lib/components/blocks/nav/utils.d.ts +0 -1
  136. package/dist-lib/components/blocks/shell/animated-sidebar-content.d.ts +0 -2
  137. package/dist-lib/components/blocks/shell/animated-sidebar-footer.d.ts +0 -2
  138. package/dist-lib/components/blocks/shell/animated-sidebar-group-content.d.ts +0 -2
  139. package/dist-lib/components/blocks/shell/animated-sidebar-group-label.d.ts +0 -2
  140. package/dist-lib/components/blocks/shell/animated-sidebar-group.d.ts +0 -2
  141. package/dist-lib/components/blocks/shell/animated-sidebar-header.d.ts +0 -2
  142. package/dist-lib/components/blocks/shell/animated-sidebar-menu-button.d.ts +0 -3
  143. package/dist-lib/components/blocks/shell/animated-sidebar-menu-item.d.ts +0 -2
  144. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-button.d.ts +0 -3
  145. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-item.d.ts +0 -2
  146. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub.d.ts +0 -2
  147. package/dist-lib/components/blocks/shell/animated-sidebar-menu.d.ts +0 -2
  148. package/src/components/blocks/nav/__tests__/sidebar-block.test.tsx +0 -125
  149. package/src/components/blocks/nav/sidebar-block.tsx +0 -142
  150. package/src/components/blocks/nav/sidebar-nav-row.tsx +0 -78
  151. package/src/components/blocks/nav/types.ts +0 -65
  152. package/src/components/blocks/nav/utils.ts +0 -4
  153. package/src/components/blocks/shell/animated-sidebar-content.tsx +0 -19
  154. package/src/components/blocks/shell/animated-sidebar-footer.tsx +0 -19
  155. package/src/components/blocks/shell/animated-sidebar-group-content.tsx +0 -16
  156. package/src/components/blocks/shell/animated-sidebar-group-label.tsx +0 -29
  157. package/src/components/blocks/shell/animated-sidebar-group.tsx +0 -16
  158. package/src/components/blocks/shell/animated-sidebar-header.tsx +0 -16
  159. package/src/components/blocks/shell/animated-sidebar-menu-button.tsx +0 -151
  160. package/src/components/blocks/shell/animated-sidebar-menu-item.tsx +0 -20
  161. package/src/components/blocks/shell/animated-sidebar-menu-sub-button.tsx +0 -85
  162. package/src/components/blocks/shell/animated-sidebar-menu-sub-item.tsx +0 -19
  163. package/src/components/blocks/shell/animated-sidebar-menu-sub.tsx +0 -44
  164. package/src/components/blocks/shell/animated-sidebar-menu.tsx +0 -26
  165. /package/dist-lib/components/blocks/{nav/__tests__/sidebar-block.test.d.ts → auth/__tests__/auth-state-atom.test.d.ts} +0 -0
@@ -0,0 +1,79 @@
1
+ "use client"
2
+
3
+ /* eslint-disable react-refresh/only-export-components */
4
+ /**
5
+ * The library's one and only state provider.
6
+ *
7
+ * Mount it once at the app root. Every library state hook then works
8
+ * anywhere below it — there are no per-component providers to nest.
9
+ *
10
+ * One provider suffices because library state lives in module-level Jotai
11
+ * atom families keyed by an instance id, not in a context created per
12
+ * component. A new stateful component adds atoms to that module scope; it
13
+ * never adds a provider. Consumer code therefore stays exactly as it is as
14
+ * the library grows.
15
+ *
16
+ * Without this provider the hooks still run, which is the trap: Jotai falls
17
+ * back to its implicit default store, and that store is NOT `exegiaStore`,
18
+ * so imperative access would read a different store than the one the
19
+ * components render from. Mount the provider.
20
+ *
21
+ * ```tsx
22
+ * export function App() {
23
+ * return (
24
+ * <ExegiaProvider sound>
25
+ * <Routes />
26
+ * </ExegiaProvider>
27
+ * )
28
+ * }
29
+ * ```
30
+ */
31
+ import * as React from "react"
32
+ import { Provider, useStore } from "jotai"
33
+
34
+ import { ThemeProvider } from "@/components/theme-provider"
35
+ import { bindSounds } from "@/lib/sound"
36
+ import { exegiaStore } from "./store"
37
+ import type { ExegiaStore } from "./store"
38
+
39
+ /** Options forwarded to the library's `ThemeProvider`. */
40
+ export interface ExegiaThemeOptions {
41
+ defaultTheme?: "dark" | "light" | "system"
42
+ storageKey?: string
43
+ disableTransitionOnChange?: boolean
44
+ }
45
+
46
+ export interface ExegiaProviderProps {
47
+ children: React.ReactNode
48
+ /** Store to mount. Defaults to the library's `exegiaStore`. */
49
+ store?: ExegiaStore
50
+ /** Opt into interaction sound. Off unless the app asks for it. */
51
+ sound?: boolean
52
+ /** Mount `ThemeProvider` with these options. Omit it and none is mounted —
53
+ * it reads `localStorage` in its state initializer, so an unconditional
54
+ * mount would break SSR consumers, and `useTheme` already throws without
55
+ * one. */
56
+ theme?: ExegiaThemeOptions
57
+ }
58
+
59
+ export function ExegiaProvider({
60
+ children,
61
+ store,
62
+ sound = false,
63
+ theme,
64
+ }: ExegiaProviderProps): React.ReactElement {
65
+ React.useEffect(() => {
66
+ // Idempotent, and deliberately not at module scope: opting into sound is
67
+ // the consuming app's decision, not an import side effect.
68
+ if (sound) bindSounds()
69
+ }, [sound])
70
+
71
+ return (
72
+ <Provider store={store ?? exegiaStore}>
73
+ {theme ? <ThemeProvider {...theme}>{children}</ThemeProvider> : children}
74
+ </Provider>
75
+ )
76
+ }
77
+
78
+ /** The store mounted by the nearest `ExegiaProvider` above the caller. */
79
+ export const useExegiaStore = useStore
@@ -0,0 +1,4 @@
1
+ export { ExegiaProvider, useExegiaStore } from "./exegia-provider"
2
+ export type { ExegiaProviderProps, ExegiaThemeOptions } from "./exegia-provider"
3
+ export { exegiaStore } from "./store"
4
+ export type { ExegiaStore } from "./store"
@@ -0,0 +1,19 @@
1
+ import { createStore } from "jotai"
2
+
3
+ /** A Jotai store as created by `createStore()`. */
4
+ export type ExegiaStore = ReturnType<typeof createStore>
5
+
6
+ /**
7
+ * The library's default store — the one `ExegiaProvider` mounts unless it is
8
+ * handed another. Exported so non-React code (a router guard, a command
9
+ * palette, a test) can read and write library state imperatively:
10
+ *
11
+ * ```ts
12
+ * exegiaStore.get(treeExpandedIdsAtom("app-nav"))
13
+ * ```
14
+ *
15
+ * A single module-level store is wrong for SSR — state would leak between
16
+ * requests — so render-per-request apps pass a fresh `createStore()` to
17
+ * `ExegiaProvider` instead.
18
+ */
19
+ export const exegiaStore: ExegiaStore = createStore()
@@ -1,14 +0,0 @@
1
- import { ISidebarBlockProps } from './types';
2
- import * as React from "react";
3
- export type { ISidebarBlockProps as SidebarBlockProps, ISidebarNavItem as SidebarNavItem, ISidebarNavSection as SidebarNavSection, ISidebarNavSubItem as SidebarNavSubItem, } from './types';
4
- /**
5
- * Application sidebar navigation, driven by data rather than composition.
6
- *
7
- * This is the navigation panel only — it claims no page layout, so place it
8
- * beside your own content. It owns its own sidebar context (⌘B toggles it);
9
- * pass `open`/`onOpenChange` to drive the rail, and `openMobile`/
10
- * `onOpenMobileChange` to open the drawer from a header button of your own.
11
- * Compose the underlying `AnimatedPanel*` parts directly when a screen
12
- * needs something this API does not cover.
13
- */
14
- export declare function SidebarBlock({ sections, activeId, onNavigate, header, footer, showTrigger, side, variant, collapsible, open, defaultOpen, onOpenChange, openMobile, defaultOpenMobile, onOpenMobileChange, width, iconWidth, mobileWidth, ariaLabel, className, }: ISidebarBlockProps): React.ReactElement;
@@ -1,3 +0,0 @@
1
- import { IRowProps } from './types';
2
- import * as React from "react";
3
- export declare function SidebarNavRow({ item, activeId, onNavigate }: IRowProps): React.JSX.Element;
@@ -1,60 +0,0 @@
1
- /** A nested link, one level under a top-level entry. */
2
- export interface ISidebarNavSubItem {
3
- id: string;
4
- label: string;
5
- icon?: React.ReactNode;
6
- /** Renders an anchor. Without one the row is a button. */
7
- href?: string;
8
- disabled?: boolean;
9
- target?: "_blank" | "_self" | "_parent" | "_top";
10
- onSelect?: () => void;
11
- }
12
- /** A top-level entry. With `items` it expands instead of navigating. */
13
- export interface ISidebarNavItem extends ISidebarNavSubItem {
14
- /** Trailing hint — a count, a "New" pill. Hidden while collapsed. */
15
- badge?: React.ReactNode;
16
- items?: ISidebarNavSubItem[];
17
- /** Start expanded. Defaults to expanded when one of its children is active. */
18
- defaultOpen?: boolean;
19
- }
20
- /** A titled run of entries. The title hides while the rail is collapsed. */
21
- export interface ISidebarNavSection {
22
- id: string;
23
- label?: string;
24
- items: ISidebarNavItem[];
25
- }
26
- export interface ISidebarBlockProps {
27
- sections: ISidebarNavSection[];
28
- /** `id` of the current entry — matches top-level and nested ids alike. */
29
- activeId?: string;
30
- /** Fires for every selection, after the entry's own `onSelect`. */
31
- onNavigate?: (item: ISidebarNavItem | ISidebarNavSubItem) => void;
32
- /** Brand row above the navigation. */
33
- header?: React.ReactNode;
34
- /** Pinned below the navigation — an account card, a version note. */
35
- footer?: React.ReactNode;
36
- /** Collapse toggle in the header row. */
37
- showTrigger?: boolean;
38
- side?: "left" | "right";
39
- variant?: "sidebar" | "floating" | "inset";
40
- /** How the rail collapses: to icons, off-canvas, or not at all. */
41
- collapsible?: "offcanvas" | "icon" | "none";
42
- open?: boolean;
43
- defaultOpen?: boolean;
44
- onOpenChange?: (open: boolean) => void;
45
- /** Under 768px the panel is a drawer — drive it from your own header. */
46
- openMobile?: boolean;
47
- defaultOpenMobile?: boolean;
48
- onOpenMobileChange?: (open: boolean) => void;
49
- /** Rail widths. CSS lengths, e.g. "16rem". */
50
- width?: string;
51
- iconWidth?: string;
52
- mobileWidth?: string;
53
- ariaLabel?: string;
54
- className?: string;
55
- }
56
- export interface IRowProps {
57
- item: ISidebarNavItem;
58
- activeId?: string;
59
- onNavigate?: (item: ISidebarNavItem | ISidebarNavSubItem) => void;
60
- }
@@ -1 +0,0 @@
1
- export declare function isActive(id: string, activeId?: string): boolean;
@@ -1,2 +0,0 @@
1
- import { HTMLAttributes } from 'react';
2
- export declare const AnimatedSidebarContent: import('react').ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import('react').RefAttributes<HTMLDivElement>>;
@@ -1,2 +0,0 @@
1
- import { HTMLAttributes } from 'react';
2
- export declare const AnimatedSidebarFooter: import('react').ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import('react').RefAttributes<HTMLDivElement>>;
@@ -1,2 +0,0 @@
1
- import { HTMLAttributes } from 'react';
2
- export declare const AnimatedSidebarGroupContent: import('react').ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import('react').RefAttributes<HTMLDivElement>>;
@@ -1,2 +0,0 @@
1
- import { HTMLAttributes } from 'react';
2
- export declare const AnimatedSidebarGroupLabel: import('react').ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import('react').RefAttributes<HTMLDivElement>>;
@@ -1,2 +0,0 @@
1
- import { HTMLAttributes } from 'react';
2
- export declare const AnimatedSidebarGroup: import('react').ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import('react').RefAttributes<HTMLDivElement>>;
@@ -1,2 +0,0 @@
1
- import { HTMLAttributes } from 'react';
2
- export declare const AnimatedSidebarHeader: import('react').ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import('react').RefAttributes<HTMLDivElement>>;
@@ -1,3 +0,0 @@
1
- import { AnimatedSidebarMenuButtonProps } from './type';
2
- import type * as React from "react";
3
- export declare function AnimatedSidebarMenuButton({ children, icon, badge, href, isActive, ariaExpanded, disabled, closeOnSelect, target, rel, onSelect, className, }: AnimatedSidebarMenuButtonProps): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import { HTMLMotionProps } from 'motion/react';
2
- export declare const AnimatedSidebarMenuItem: import('react').ForwardRefExoticComponent<Omit<HTMLMotionProps<"li">, "ref"> & import('react').RefAttributes<HTMLLIElement>>;
@@ -1,3 +0,0 @@
1
- import { AnimatedSidebarMenuSubButtonProps } from './type';
2
- import type * as React from "react";
3
- export declare function AnimatedSidebarMenuSubButton({ children, icon, href, isActive, disabled, closeOnSelect, target, rel, onSelect, className, }: AnimatedSidebarMenuSubButtonProps): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import { HTMLMotionProps } from 'motion/react';
2
- export declare const AnimatedSidebarMenuSubItem: import('react').ForwardRefExoticComponent<Omit<HTMLMotionProps<"li">, "ref"> & import('react').RefAttributes<HTMLLIElement>>;
@@ -1,2 +0,0 @@
1
- import { AnimatedSidebarMenuSubProps } from './type';
2
- export declare const AnimatedSidebarMenuSub: import('react').ForwardRefExoticComponent<Omit<AnimatedSidebarMenuSubProps, "ref"> & import('react').RefAttributes<HTMLUListElement>>;
@@ -1,2 +0,0 @@
1
- import { HTMLAttributes } from 'react';
2
- export declare const AnimatedSidebarMenu: import('react').ForwardRefExoticComponent<HTMLAttributes<HTMLUListElement> & import('react').RefAttributes<HTMLUListElement>>;
@@ -1,125 +0,0 @@
1
- import { describe, expect, mock, test } from "bun:test"
2
- import { render, screen } from "@testing-library/react"
3
- import userEvent from "@testing-library/user-event"
4
-
5
- import { SidebarBlock, type SidebarNavSection } from "../sidebar-block"
6
-
7
- const SECTIONS: SidebarNavSection[] = [
8
- {
9
- id: "work",
10
- label: "Research",
11
- items: [
12
- { id: "search", label: "Search" },
13
- {
14
- id: "library",
15
- label: "Library",
16
- items: [
17
- { id: "manuscripts", label: "Manuscripts" },
18
- { id: "codices", label: "Codices" },
19
- ],
20
- },
21
- ],
22
- },
23
- { id: "workspace", items: [{ id: "settings", label: "Settings" }] },
24
- ]
25
-
26
- describe("SidebarBlock", () => {
27
- test("renders the sections and their entries", () => {
28
- render(<SidebarBlock ariaLabel="Main" sections={SECTIONS} />)
29
-
30
- expect(screen.getByRole("navigation", { name: "Main" })).toBeDefined()
31
- expect(screen.getByText("Research")).toBeDefined()
32
- expect(screen.getByRole("button", { name: "Search" })).toBeDefined()
33
- expect(screen.getByRole("button", { name: "Settings" })).toBeDefined()
34
- })
35
-
36
- test("marks the active entry", () => {
37
- render(<SidebarBlock activeId="search" sections={SECTIONS} />)
38
-
39
- expect(
40
- screen
41
- .getByRole("button", { name: "Search" })
42
- .getAttribute("aria-current")
43
- ).toBe("page")
44
- expect(
45
- screen
46
- .getByRole("button", { name: "Settings" })
47
- .getAttribute("aria-current")
48
- ).toBeNull()
49
- })
50
-
51
- test("reports selections through onNavigate, after the entry's onSelect", async () => {
52
- const user = userEvent.setup()
53
- const calls: string[] = []
54
- render(
55
- <SidebarBlock
56
- onNavigate={(item) => calls.push(`navigate:${item.id}`)}
57
- sections={[
58
- {
59
- id: "work",
60
- items: [
61
- {
62
- id: "search",
63
- label: "Search",
64
- onSelect: () => calls.push("select:search"),
65
- },
66
- ],
67
- },
68
- ]}
69
- />
70
- )
71
-
72
- await user.click(screen.getByRole("button", { name: "Search" }))
73
-
74
- expect(calls).toEqual(["select:search", "navigate:search"])
75
- })
76
-
77
- test("an entry with children expands instead of navigating", async () => {
78
- const user = userEvent.setup()
79
- const onNavigate = mock(() => {})
80
- render(<SidebarBlock onNavigate={onNavigate} sections={SECTIONS} />)
81
-
82
- const parent = screen.getByRole("button", { name: "Library" })
83
- expect(parent.getAttribute("aria-expanded")).toBe("false")
84
- expect(screen.queryByRole("button", { name: "Manuscripts" })).toBeNull()
85
-
86
- await user.click(parent)
87
-
88
- expect(
89
- await screen.findByRole("button", { name: "Manuscripts" })
90
- ).toBeDefined()
91
- expect(parent.getAttribute("aria-expanded")).toBe("true")
92
- expect(onNavigate).not.toHaveBeenCalled()
93
- })
94
-
95
- test("starts expanded when one of its children is active", async () => {
96
- render(<SidebarBlock activeId="codices" sections={SECTIONS} />)
97
-
98
- const child = await screen.findByRole("button", { name: "Codices" })
99
- expect(child.getAttribute("aria-current")).toBe("page")
100
- expect(
101
- screen
102
- .getByRole("button", { name: "Library" })
103
- .getAttribute("aria-expanded")
104
- ).toBe("true")
105
- })
106
-
107
- test("renders href entries as links", () => {
108
- render(
109
- <SidebarBlock
110
- sections={[
111
- {
112
- id: "work",
113
- items: [
114
- { id: "docs", label: "Docs", href: "/docs", target: "_blank" },
115
- ],
116
- },
117
- ]}
118
- />
119
- )
120
-
121
- const link = screen.getByRole("link", { name: "Docs" })
122
- expect(link.getAttribute("href")).toBe("/docs")
123
- expect(link.getAttribute("rel")).toBe("noreferrer noopener")
124
- })
125
- })
@@ -1,142 +0,0 @@
1
- "use client"
2
-
3
- import { PanelLeftIcon } from "lucide-react"
4
- import * as React from "react"
5
-
6
- import {
7
- AnimatedPanel as AnimatedSidebar,
8
- AnimatedPanelProvider as AnimatedSidebarProvider,
9
- AnimatedPanelTrigger as AnimatedSidebarTrigger,
10
- AnimatedSidebarContent,
11
- AnimatedSidebarFooter,
12
- AnimatedSidebarGroup,
13
- AnimatedSidebarGroupContent,
14
- AnimatedSidebarGroupLabel,
15
- AnimatedSidebarHeader,
16
- AnimatedSidebarMenu,
17
- } from "@/components/blocks/shell"
18
- import { cn } from "@/lib/utils"
19
- import { SidebarNavRow } from "@/components/blocks/nav/sidebar-nav-row"
20
-
21
- import type { ISidebarBlockProps } from "@/components/blocks/nav/types"
22
-
23
- export type {
24
- ISidebarBlockProps as SidebarBlockProps,
25
- ISidebarNavItem as SidebarNavItem,
26
- ISidebarNavSection as SidebarNavSection,
27
- ISidebarNavSubItem as SidebarNavSubItem,
28
- } from "@/components/blocks/nav/types"
29
-
30
- /**
31
- * Application sidebar navigation, driven by data rather than composition.
32
- *
33
- * This is the navigation panel only — it claims no page layout, so place it
34
- * beside your own content. It owns its own sidebar context (⌘B toggles it);
35
- * pass `open`/`onOpenChange` to drive the rail, and `openMobile`/
36
- * `onOpenMobileChange` to open the drawer from a header button of your own.
37
- * Compose the underlying `AnimatedPanel*` parts directly when a screen
38
- * needs something this API does not cover.
39
- */
40
- export function SidebarBlock({
41
- sections,
42
- activeId,
43
- onNavigate,
44
- header,
45
- footer,
46
- showTrigger = true,
47
- side = "left",
48
- variant = "sidebar",
49
- collapsible = "icon",
50
- open,
51
- defaultOpen,
52
- onOpenChange,
53
- openMobile,
54
- defaultOpenMobile,
55
- onOpenMobileChange,
56
- width,
57
- iconWidth,
58
- mobileWidth,
59
- ariaLabel = "Main",
60
- className,
61
- }: ISidebarBlockProps): React.ReactElement {
62
- return (
63
- <AnimatedSidebarProvider
64
- // The provider ships a full-page flex shell; this block is just the
65
- // panel, so the shell is flattened back to the panel's own box. The
66
- // block is a single panel, so its scalar open props map onto the
67
- // provider's side-keyed records under the block's own `side`.
68
- className="h-full min-h-0 w-auto"
69
- defaultOpen={{ [side]: defaultOpen ?? true }}
70
- defaultOpenMobile={
71
- defaultOpenMobile === undefined ? undefined : { [side]: defaultOpenMobile }
72
- }
73
- onOpenChange={(nextOpen, changedSide) => {
74
- if (changedSide === side) onOpenChange?.(nextOpen)
75
- }}
76
- onOpenMobileChange={(nextOpen, changedSide) => {
77
- if (changedSide === side) onOpenMobileChange?.(nextOpen)
78
- }}
79
- open={open === undefined ? undefined : { [side]: open }}
80
- openMobile={openMobile === undefined ? undefined : { [side]: openMobile }}
81
- style={{
82
- ...(width ? { "--sidebar-width": width } : null),
83
- ...(iconWidth ? { "--sidebar-width-icon": iconWidth } : null),
84
- ...(mobileWidth ? { "--sidebar-width-mobile": mobileWidth } : null),
85
- }}
86
- >
87
- <AnimatedSidebar
88
- ariaLabel={ariaLabel}
89
- // The panel is built for a full-page shell (`h-svh`, so its footer
90
- // hangs below any shorter box). Fill the parent instead — in a
91
- // full-height layout that comes to the same thing.
92
- className={cn("h-full", className)}
93
- collapsible={collapsible}
94
- panelClassName="h-full"
95
- // The panel is navigation and nothing else, so it names a navigation
96
- // landmark rather than the complementary one <aside> implies. (The
97
- // mobile drawer is a dialog and keeps that role.)
98
- role="navigation"
99
- side={side}
100
- variant={variant}
101
- >
102
- {header ||
103
- (showTrigger && (
104
- <AnimatedSidebarHeader>
105
- <div className="flex min-w-0 items-center justify-between gap-2">
106
- {header && <div className="min-w-0 flex-1">{header}</div>}
107
- {showTrigger && (
108
- <AnimatedSidebarTrigger className="text-muted-foreground hover:text-foreground">
109
- <PanelLeftIcon className="size-4.5" />
110
- </AnimatedSidebarTrigger>
111
- )}
112
- </div>
113
- </AnimatedSidebarHeader>
114
- ))}
115
- <AnimatedSidebarContent>
116
- {sections.map((section) => (
117
- <AnimatedSidebarGroup key={section.id}>
118
- {section.label && (
119
- <AnimatedSidebarGroupLabel>
120
- {section.label}
121
- </AnimatedSidebarGroupLabel>
122
- )}
123
- <AnimatedSidebarGroupContent>
124
- <AnimatedSidebarMenu>
125
- {section.items.map((item) => (
126
- <SidebarNavRow
127
- activeId={activeId}
128
- item={item}
129
- key={item.id}
130
- onNavigate={onNavigate}
131
- />
132
- ))}
133
- </AnimatedSidebarMenu>
134
- </AnimatedSidebarGroupContent>
135
- </AnimatedSidebarGroup>
136
- ))}
137
- </AnimatedSidebarContent>
138
- {footer && <AnimatedSidebarFooter>{footer}</AnimatedSidebarFooter>}
139
- </AnimatedSidebar>
140
- </AnimatedSidebarProvider>
141
- )
142
- }
@@ -1,78 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
-
5
- import type { IRowProps, ISidebarNavItem, ISidebarNavSubItem } from "@/components/blocks/nav/types"
6
- import { isActive } from "@/components/blocks/nav/utils"
7
- import {
8
- AnimatedSidebarMenuButton,
9
- AnimatedSidebarMenuItem,
10
- AnimatedSidebarMenuSub,
11
- AnimatedSidebarMenuSubButton,
12
- AnimatedSidebarMenuSubItem,
13
- } from "@/components/blocks/shell"
14
-
15
- export function SidebarNavRow({ item, activeId, onNavigate }: IRowProps) {
16
- const children = item.items ?? []
17
- const hasChildren = children.length > 0
18
- const childActive = children.some((child) => isActive(child.id, activeId))
19
- // null = follow the active child; a boolean = the reader has said otherwise.
20
- const [toggled, setToggled] = React.useState<boolean | null>(null)
21
- const [wasChildActive, setWasChildActive] = React.useState(childActive)
22
-
23
- // Navigating into a nested route from elsewhere reveals its parent, even if
24
- // the reader had collapsed it. Adjusted during render rather than in an
25
- // effect, so the group never paints closed for a frame first.
26
- if (childActive !== wasChildActive) {
27
- setWasChildActive(childActive)
28
- if (childActive) setToggled(null)
29
- }
30
-
31
- const open = toggled ?? item.defaultOpen ?? childActive
32
-
33
- function select(entry: ISidebarNavItem | ISidebarNavSubItem) {
34
- entry.onSelect?.()
35
- onNavigate?.(entry)
36
- }
37
-
38
- return (
39
- <AnimatedSidebarMenuItem>
40
- <AnimatedSidebarMenuButton
41
- ariaExpanded={hasChildren ? open : undefined}
42
- badge={item.badge}
43
- disabled={item.disabled}
44
- href={hasChildren ? undefined : item.href}
45
- icon={item.icon}
46
- isActive={isActive(item.id, activeId)}
47
- onSelect={() => {
48
- if (hasChildren) {
49
- setToggled(!open)
50
- return
51
- }
52
- select(item)
53
- }}
54
- target={item.target}
55
- >
56
- {item.label}
57
- </AnimatedSidebarMenuButton>
58
- {hasChildren ? (
59
- <AnimatedSidebarMenuSub open={open}>
60
- {children.map((child) => (
61
- <AnimatedSidebarMenuSubItem key={child.id}>
62
- <AnimatedSidebarMenuSubButton
63
- disabled={child.disabled}
64
- href={child.href}
65
- icon={child.icon}
66
- isActive={isActive(child.id, activeId)}
67
- onSelect={() => select(child)}
68
- target={child.target}
69
- >
70
- {child.label}
71
- </AnimatedSidebarMenuSubButton>
72
- </AnimatedSidebarMenuSubItem>
73
- ))}
74
- </AnimatedSidebarMenuSub>
75
- ) : null}
76
- </AnimatedSidebarMenuItem>
77
- )
78
- }
@@ -1,65 +0,0 @@
1
-
2
- /** A nested link, one level under a top-level entry. */
3
- export interface ISidebarNavSubItem {
4
- id: string
5
- label: string
6
- icon?: React.ReactNode
7
- /** Renders an anchor. Without one the row is a button. */
8
- href?: string
9
- disabled?: boolean
10
- target?: "_blank" | "_self" | "_parent" | "_top"
11
- onSelect?: () => void
12
- }
13
-
14
- /** A top-level entry. With `items` it expands instead of navigating. */
15
- export interface ISidebarNavItem extends ISidebarNavSubItem {
16
- /** Trailing hint — a count, a "New" pill. Hidden while collapsed. */
17
- badge?: React.ReactNode
18
- items?: ISidebarNavSubItem[]
19
- /** Start expanded. Defaults to expanded when one of its children is active. */
20
- defaultOpen?: boolean
21
- }
22
-
23
- /** A titled run of entries. The title hides while the rail is collapsed. */
24
- export interface ISidebarNavSection {
25
- id: string
26
- label?: string
27
- items: ISidebarNavItem[]
28
- }
29
-
30
- export interface ISidebarBlockProps {
31
- sections: ISidebarNavSection[]
32
- /** `id` of the current entry — matches top-level and nested ids alike. */
33
- activeId?: string
34
- /** Fires for every selection, after the entry's own `onSelect`. */
35
- onNavigate?: (item: ISidebarNavItem | ISidebarNavSubItem) => void
36
- /** Brand row above the navigation. */
37
- header?: React.ReactNode
38
- /** Pinned below the navigation — an account card, a version note. */
39
- footer?: React.ReactNode
40
- /** Collapse toggle in the header row. */
41
- showTrigger?: boolean
42
- side?: "left" | "right"
43
- variant?: "sidebar" | "floating" | "inset"
44
- /** How the rail collapses: to icons, off-canvas, or not at all. */
45
- collapsible?: "offcanvas" | "icon" | "none"
46
- open?: boolean
47
- defaultOpen?: boolean
48
- onOpenChange?: (open: boolean) => void
49
- /** Under 768px the panel is a drawer — drive it from your own header. */
50
- openMobile?: boolean
51
- defaultOpenMobile?: boolean
52
- onOpenMobileChange?: (open: boolean) => void
53
- /** Rail widths. CSS lengths, e.g. "16rem". */
54
- width?: string
55
- iconWidth?: string
56
- mobileWidth?: string
57
- ariaLabel?: string
58
- className?: string
59
- }
60
-
61
- export interface IRowProps {
62
- item: ISidebarNavItem
63
- activeId?: string
64
- onNavigate?: (item: ISidebarNavItem | ISidebarNavSubItem) => void
65
- }
@@ -1,4 +0,0 @@
1
-
2
- export function isActive(id: string, activeId?: string): boolean {
3
- return activeId !== undefined && id === activeId
4
- }