@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencosmos/ui",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "OpenCosmos UI — Make it Lovable. 100 accessible React components, three themes, user-controlled motion.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -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(STORAGE_KEY);
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(STORAGE_KEY, String(value));
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
- /** Body slot rendered in the scrollable mid-section (e.g. conversation history). Only visible when expanded. */
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