@opencosmos/ui 1.3.3 → 1.3.5
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/dist/index.d.mts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +39 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +39 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/layout/AppSidebar.tsx +55 -6
package/package.json
CHANGED
|
@@ -54,17 +54,19 @@ export interface AppSidebarProviderProps {
|
|
|
54
54
|
children: React.ReactNode;
|
|
55
55
|
/** Initial open state used on server and first render @default true */
|
|
56
56
|
defaultOpen?: boolean;
|
|
57
|
+
/** localStorage key for persisting open state. Use unique keys per page to avoid cross-page state bleed. @default 'appsidebar:state' */
|
|
58
|
+
storageKey?: string;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
export function AppSidebarProvider({ children, defaultOpen = true }: AppSidebarProviderProps) {
|
|
61
|
+
export function AppSidebarProvider({ children, defaultOpen = true, storageKey = STORAGE_KEY }: AppSidebarProviderProps) {
|
|
60
62
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
61
63
|
|
|
62
64
|
useEffect(() => {
|
|
63
|
-
const stored = localStorage.getItem(
|
|
65
|
+
const stored = localStorage.getItem(storageKey);
|
|
64
66
|
if (stored !== null) setIsOpen(stored === 'true');
|
|
65
|
-
}, []);
|
|
67
|
+
}, [storageKey]);
|
|
66
68
|
|
|
67
|
-
const persist = (value: boolean) => localStorage.setItem(
|
|
69
|
+
const persist = (value: boolean) => localStorage.setItem(storageKey, String(value));
|
|
68
70
|
|
|
69
71
|
const toggle = () => setIsOpen(prev => { const next = !prev; persist(next); return next; });
|
|
70
72
|
const open = () => { setIsOpen(true); persist(true); };
|
|
@@ -120,9 +122,11 @@ export interface AppSidebarProps {
|
|
|
120
122
|
logo?: React.ReactNode;
|
|
121
123
|
/** Wordmark shown next to the logo when expanded */
|
|
122
124
|
title?: string;
|
|
123
|
-
/** Navigation items */
|
|
125
|
+
/** Navigation items rendered at the top (below the header) */
|
|
124
126
|
items?: AppSidebarNavItem[];
|
|
125
|
-
/**
|
|
127
|
+
/** Navigation items rendered at the bottom (above the footer) */
|
|
128
|
+
bottomItems?: AppSidebarNavItem[];
|
|
129
|
+
/** Body slot — rendered in the flex-1 mid-section (e.g. conversation history). Only visible when expanded. */
|
|
126
130
|
children?: React.ReactNode;
|
|
127
131
|
/** Footer slot — auth section, user avatar, sign-in prompt, etc. */
|
|
128
132
|
footer?: React.ReactNode;
|
|
@@ -134,6 +138,7 @@ export function AppSidebar({
|
|
|
134
138
|
logo,
|
|
135
139
|
title,
|
|
136
140
|
items = [],
|
|
141
|
+
bottomItems = [],
|
|
137
142
|
children,
|
|
138
143
|
footer,
|
|
139
144
|
className,
|
|
@@ -261,6 +266,50 @@ export function AppSidebar({
|
|
|
261
266
|
{children}
|
|
262
267
|
</div>
|
|
263
268
|
|
|
269
|
+
{/* ── Bottom nav items ───────────────────────────────────────────── */}
|
|
270
|
+
{bottomItems.length > 0 && (
|
|
271
|
+
<nav className="px-2 py-2 space-y-1 shrink-0 border-t border-foreground/8" aria-label="Bottom navigation">
|
|
272
|
+
{bottomItems.map((item) => (
|
|
273
|
+
<a
|
|
274
|
+
key={item.label}
|
|
275
|
+
href={item.href}
|
|
276
|
+
target={item.external ? '_blank' : undefined}
|
|
277
|
+
rel={item.external ? 'noopener noreferrer' : undefined}
|
|
278
|
+
title={!isOpen ? item.label : undefined}
|
|
279
|
+
aria-label={!isOpen ? item.label : undefined}
|
|
280
|
+
className={cn(
|
|
281
|
+
'flex items-center rounded-lg transition-colors duration-150',
|
|
282
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus)]',
|
|
283
|
+
isOpen
|
|
284
|
+
? 'gap-3 px-3 py-3'
|
|
285
|
+
: 'justify-center w-9 h-9 mx-auto',
|
|
286
|
+
item.active
|
|
287
|
+
? 'bg-foreground/8 text-foreground font-medium'
|
|
288
|
+
: 'text-[var(--color-text-secondary)] hover:bg-foreground/5 hover:text-[var(--color-text-primary)]'
|
|
289
|
+
)}
|
|
290
|
+
>
|
|
291
|
+
<span className="shrink-0 flex items-center justify-center w-4 h-4">
|
|
292
|
+
{item.icon}
|
|
293
|
+
</span>
|
|
294
|
+
<span
|
|
295
|
+
className="text-sm whitespace-nowrap"
|
|
296
|
+
style={{
|
|
297
|
+
opacity: isOpen ? 1 : 0,
|
|
298
|
+
width: isOpen ? 'auto' : 0,
|
|
299
|
+
overflow: 'hidden',
|
|
300
|
+
pointerEvents: isOpen ? 'auto' : 'none',
|
|
301
|
+
transition: shouldAnimate
|
|
302
|
+
? `opacity ${Math.round(duration * 0.55)}ms ease-out`
|
|
303
|
+
: 'none',
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
{item.label}
|
|
307
|
+
</span>
|
|
308
|
+
</a>
|
|
309
|
+
))}
|
|
310
|
+
</nav>
|
|
311
|
+
)}
|
|
312
|
+
|
|
264
313
|
{/* ── Footer ─────────────────────────────────────────────────────── */}
|
|
265
314
|
{footer && (
|
|
266
315
|
<div
|