tembro 2.0.1 → 2.0.3

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 (46) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/components/calendar/calendar.cjs +1 -1
  3. package/dist/components/calendar/calendar.js +5 -5
  4. package/dist/components/inputs/clearable-input.cjs +1 -1
  5. package/dist/components/inputs/clearable-input.js +1 -1
  6. package/dist/components/inputs/input-chrome.cjs +1 -1
  7. package/dist/components/inputs/input-chrome.js +1 -1
  8. package/dist/components/layout/app-sidebar.cjs +1 -1
  9. package/dist/components/layout/app-sidebar.d.ts +19 -1
  10. package/dist/components/layout/app-sidebar.js +253 -126
  11. package/dist/components/ui/button/index.cjs +1 -1
  12. package/dist/components/ui/button/index.js +2 -2
  13. package/dist/components/ui/input/primitive.cjs +1 -1
  14. package/dist/components/ui/input/primitive.js +1 -1
  15. package/dist/components/ui/select/index.cjs +1 -1
  16. package/dist/components/ui/select/index.js +3 -3
  17. package/dist/hooks/index.cjs +1 -1
  18. package/dist/hooks/index.js +4 -4
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.js +134 -134
  21. package/dist/showcase/package-meta.cjs +1 -1
  22. package/dist/showcase/package-meta.d.ts +3 -3
  23. package/dist/showcase/package-meta.js +1 -1
  24. package/dist/showcase/premium/app-shell/showcase.cjs +1 -1
  25. package/dist/showcase/premium/app-shell/showcase.js +5 -1
  26. package/dist/showcase/premium/app-sidebar/showcase.cjs +1 -1
  27. package/dist/showcase/premium/app-sidebar/showcase.js +91 -70
  28. package/dist/showcase/site-data.cjs +1 -1
  29. package/dist/showcase/site-data.js +1 -1
  30. package/dist/showcase/tembro-registry.json.cjs +1 -1
  31. package/dist/showcase/tembro-registry.json.js +1 -1
  32. package/package.json +1 -1
  33. package/packages/cli/dist/index.cjs +6 -6
  34. package/packages/cli/vendor/src/components/calendar/calendar.tsx +5 -5
  35. package/packages/cli/vendor/src/components/inputs/clearable-input.tsx +1 -1
  36. package/packages/cli/vendor/src/components/inputs/input-chrome.tsx +1 -1
  37. package/packages/cli/vendor/src/components/layout/app-sidebar.tsx +259 -21
  38. package/packages/cli/vendor/src/components/ui/button/index.tsx +2 -2
  39. package/packages/cli/vendor/src/components/ui/input/primitive.tsx +1 -1
  40. package/packages/cli/vendor/src/components/ui/select/index.tsx +3 -3
  41. package/packages/cli/vendor/src/showcase/package-meta.ts +2 -2
  42. package/packages/cli/vendor/src/showcase/premium/app-shell/showcase.tsx +5 -1
  43. package/packages/cli/vendor/src/showcase/premium/app-sidebar/showcase.tsx +18 -4
  44. package/packages/cli/vendor/src/showcase/tembro-registry.json +1 -1
  45. package/packages/cli/vendor/templates/styles/globals.css +5 -5
  46. package/registry.json +1 -1
@@ -269,7 +269,7 @@ function Calendar({
269
269
  data-slot="calendar"
270
270
  data-months={resolvedNumberOfMonths}
271
271
  className={cn(
272
- "w-fit rounded-[var(--aui-card-radius,var(--radius-lg))] border border-[color:var(--aui-card-border,var(--border))] bg-popover p-3 text-popover-foreground shadow-[var(--aui-control-panel-shadow,0_18px_40px_rgba(15,23,42,0.14))] backdrop-blur",
272
+ "w-fit rounded-[var(--aui-card-radius,var(--radius-lg))] border border-[color:var(--aui-card-border,var(--border))] bg-popover p-3 text-popover-foreground shadow-[var(--aui-control-panel-shadow,0_18px_40px_rgba(15,23,42,0.14))]",
273
273
  className
274
274
  )}
275
275
  {...props}
@@ -279,7 +279,7 @@ function Calendar({
279
279
  type="button"
280
280
  variant="outline"
281
281
  size="icon-sm"
282
- className="rounded-full border-border/70 bg-background text-foreground shadow-none hover:border-border hover:bg-accent hover:text-accent-foreground"
282
+ className="rounded-[var(--radius-md)] border-border/70 bg-background text-foreground shadow-none hover:border-border hover:bg-accent hover:text-accent-foreground"
283
283
  aria-label={labels?.previousMonth ?? "Previous month"}
284
284
  onClick={() => setMonth(addMonths(currentMonth, -navigationStep))}
285
285
  >
@@ -293,7 +293,7 @@ function Calendar({
293
293
  type="button"
294
294
  variant="outline"
295
295
  size="icon-sm"
296
- className="rounded-full border-border/70 bg-background text-foreground shadow-none hover:border-border hover:bg-accent hover:text-accent-foreground"
296
+ className="rounded-[var(--radius-md)] border-border/70 bg-background text-foreground shadow-none hover:border-border hover:bg-accent hover:text-accent-foreground"
297
297
  aria-label={labels?.nextMonth ?? "Next month"}
298
298
  onClick={() => setMonth(addMonths(currentMonth, navigationStep))}
299
299
  >
@@ -310,7 +310,7 @@ function Calendar({
310
310
  }
311
311
  >
312
312
  {monthDaysByMonth.map(({ month: visibleMonth, days }) => (
313
- <div key={toDateKey(visibleMonth)} className="min-w-[16.5rem] rounded-[var(--radius-lg)] bg-transparent p-1">
313
+ <div key={toDateKey(visibleMonth)} className="min-w-[16.5rem] rounded-[var(--radius-md)] bg-transparent p-1">
314
314
  {shouldShowMonthHeaders && (
315
315
  <div className="mb-2 text-center text-sm font-semibold capitalize tracking-tight text-foreground">
316
316
  {getMonthLabel(visibleMonth, locale)}
@@ -354,7 +354,7 @@ function Calendar({
354
354
  data-in-range={inRange || undefined}
355
355
  data-disabled-reason={disabledReason}
356
356
  className={cn(
357
- "flex h-9 items-center justify-center rounded-[var(--radius-md)] border border-transparent text-sm font-medium outline-none transition-[background-color,color,border-color,box-shadow,transform] hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-30",
357
+ "flex h-9 items-center justify-center rounded-[var(--radius-sm)] border border-transparent text-sm font-medium outline-none transition-[background-color,color,border-color,box-shadow,transform] hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-30",
358
358
  outside && "text-muted-foreground/34",
359
359
  dateKey === todayKey && "border-border/70 bg-muted/60 text-foreground",
360
360
  inRange && "bg-primary/10 text-foreground",
@@ -96,7 +96,7 @@ const ClearableInput = React.forwardRef<HTMLInputElement, ClearableInputProps>(
96
96
  type="button"
97
97
  data-slot="clearable-input-clear"
98
98
  aria-label={clearLabel}
99
- className="inline-flex size-7 items-center justify-center rounded-full border border-transparent bg-transparent text-muted-foreground/74 transition hover:border-border/60 hover:bg-muted/58 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
99
+ className="inline-flex size-7 items-center justify-center rounded-[var(--radius-sm)] border border-transparent bg-transparent text-muted-foreground/74 transition hover:border-border/60 hover:bg-muted/58 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
100
100
  onClick={handleClearClick}
101
101
  onMouseDown={handleClearMouseDown}
102
102
  onDoubleClick={handleClearMouseDown}
@@ -22,7 +22,7 @@ function InputChrome({
22
22
  <div
23
23
  data-slot="input-chrome"
24
24
  className={cn(
25
- "flex h-10 w-full min-w-0 items-center rounded-[var(--aui-control-radius,var(--radius-lg))] border border-[color:var(--aui-control-border-strong,var(--input))] bg-[color:var(--aui-control-surface,var(--background))] shadow-[var(--aui-control-shadow,none)] transition-[background-color,border-color,box-shadow] hover:border-[color:var(--aui-control-hover-border,var(--ring))] hover:bg-[color:var(--aui-control-surface-hover,var(--background))] focus-within:border-[color:var(--ring)] focus-within:shadow-[var(--aui-control-shadow,none),0_0_0_1px_var(--aui-focus-ring,var(--ring)),0_0_0_5px_var(--aui-focus-ring-soft,transparent)]",
25
+ "flex h-11 w-full min-w-0 items-center rounded-[var(--aui-control-radius,var(--radius-md))] border border-[color:var(--aui-control-border-strong,var(--input))] bg-[color:var(--aui-control-surface,var(--background))] shadow-[var(--aui-control-shadow,none)] transition-[background-color,border-color,box-shadow] hover:border-[color:var(--aui-control-hover-border,var(--ring))] hover:bg-[color:var(--aui-control-surface-hover,var(--background))] focus-within:border-[color:var(--ring)] focus-within:shadow-[var(--aui-control-shadow,none),0_0_0_1px_var(--aui-focus-ring,var(--ring)),0_0_0_4px_var(--aui-focus-ring-soft,transparent)]",
26
26
  className
27
27
  )}
28
28
  {...props}
@@ -1,5 +1,7 @@
1
1
  import * as React from "react"
2
+ import { MenuIcon, XIcon } from "lucide-react"
2
3
 
4
+ import { useIsMobile } from "@/hooks/use-is-mobile"
3
5
  import { Tooltip } from "@/components/ui/tooltip"
4
6
  import { cn } from "@/lib/utils"
5
7
 
@@ -40,11 +42,28 @@ export type AppSidebarProps = React.ComponentProps<"aside"> & {
40
42
  secondaryActions?: AppSidebarNavItem[]
41
43
  footerSecondary?: React.ReactNode
42
44
  tooltipOnCollapsed?: boolean
45
+ responsive?: boolean
46
+ mobileBreakpoint?: number
47
+ mobileOpen?: boolean
48
+ defaultMobileOpen?: boolean
49
+ onMobileOpenChange?: (open: boolean) => void
50
+ mobileTitle?: React.ReactNode
51
+ mobileDescription?: React.ReactNode
52
+ mobileToggleLabel?: string
53
+ mobileToggleIcon?: React.ReactNode
54
+ showMobileToggle?: boolean
55
+ closeOnSelect?: boolean
56
+ mobileToggleClassName?: string
57
+ mobilePanelClassName?: string
58
+ mobileOverlayClassName?: string
59
+ renderMobileToggle?: (state: { open: boolean; setOpen: (open: boolean) => void }) => React.ReactNode
43
60
  onItemSelect?: (item: AppSidebarNavItem) => void
44
61
  renderItem?: (item: AppSidebarNavItem, state: { collapsed: boolean }) => React.ReactNode
45
62
  renderLink?: (props: React.ComponentProps<"a"> & { item: AppSidebarNavItem; [key: `data-${string}`]: string | boolean | undefined }) => React.ReactNode
46
63
  }
47
64
 
65
+ const DEFAULT_APP_SIDEBAR_BREAKPOINT = 1024
66
+
48
67
  function hasVisibleSidebarChildren(item: AppSidebarNavItem) {
49
68
  return item.items?.some((child) => !child.hidden) ?? false
50
69
  }
@@ -54,6 +73,11 @@ function isSidebarItemActive(item: AppSidebarNavItem): boolean {
54
73
  return item.items?.some((child) => isSidebarItemActive(child)) ?? false
55
74
  }
56
75
 
76
+ function triggerSidebarItem(item: AppSidebarNavItem, onItemSelect?: (item: AppSidebarNavItem) => void) {
77
+ item.onSelect?.()
78
+ onItemSelect?.(item)
79
+ }
80
+
57
81
  function SidebarLeafItem({
58
82
  item,
59
83
  collapsed,
@@ -111,8 +135,7 @@ function SidebarLeafItem({
111
135
  event.preventDefault()
112
136
  return
113
137
  }
114
- item.onSelect?.()
115
- onItemSelect?.(item)
138
+ triggerSidebarItem(item, onItemSelect)
116
139
  },
117
140
  children: content,
118
141
  })}
@@ -129,8 +152,7 @@ function SidebarLeafItem({
129
152
  event.preventDefault()
130
153
  return
131
154
  }
132
- item.onSelect?.()
133
- onItemSelect?.(item)
155
+ triggerSidebarItem(item, onItemSelect)
134
156
  }}
135
157
  >
136
158
  {content}
@@ -148,8 +170,7 @@ function SidebarLeafItem({
148
170
  if (item.disabled) return
149
171
  const href = item.href
150
172
  if (!href) return
151
- item.onSelect?.()
152
- onItemSelect?.(item)
173
+ triggerSidebarItem(item, onItemSelect)
153
174
  if (href.startsWith("http")) {
154
175
  window.open(href, "_blank", "noopener,noreferrer")
155
176
  return
@@ -168,8 +189,8 @@ function SidebarLeafItem({
168
189
  disabled={item.disabled}
169
190
  {...commonProps}
170
191
  onClick={() => {
171
- item.onSelect?.()
172
- onItemSelect?.(item)
192
+ if (item.disabled) return
193
+ triggerSidebarItem(item, onItemSelect)
173
194
  }}
174
195
  >
175
196
  {content}
@@ -250,7 +271,14 @@ function SidebarTree({
250
271
  ) : null}
251
272
  {!collapsed && <span className="min-w-0 flex-1 truncate">{item.label}</span>}
252
273
  {!collapsed && item.badge && <span className="shrink-0">{item.badge}</span>}
253
- {!collapsed && <span data-slot="app-sidebar-group-chevron" className="ml-auto text-xs text-muted-foreground transition-transform group-open/app-sidebar-details:rotate-90">›</span>}
274
+ {!collapsed && (
275
+ <span
276
+ data-slot="app-sidebar-group-chevron"
277
+ className="ml-auto text-xs text-muted-foreground transition-transform group-open/app-sidebar-details:rotate-90"
278
+ >
279
+
280
+ </span>
281
+ )}
254
282
  </summary>
255
283
  <div data-slot="app-sidebar-group-content" className={cn("mt-1 space-y-1", !collapsed && "pl-3")}>
256
284
  <SidebarTree
@@ -289,8 +317,7 @@ function SidebarActionButton({
289
317
  disabled={item.disabled}
290
318
  onClick={() => {
291
319
  if (item.disabled) return
292
- item.onSelect?.()
293
- onItemSelect?.(item)
320
+ triggerSidebarItem(item, onItemSelect)
294
321
  }}
295
322
  >
296
323
  {item.icon ? <span className="shrink-0">{item.icon}</span> : null}
@@ -311,9 +338,11 @@ function SidebarActionButton({
311
338
  function SidebarFooterAccount({
312
339
  account,
313
340
  collapsed,
341
+ onAfterSelect,
314
342
  }: {
315
343
  account: AppSidebarFooterAccount
316
344
  collapsed: boolean
345
+ onAfterSelect?: () => void
317
346
  }) {
318
347
  const body = (
319
348
  <button
@@ -325,6 +354,7 @@ function SidebarFooterAccount({
325
354
  )}
326
355
  onClick={() => {
327
356
  account.onSelect?.()
357
+ onAfterSelect?.()
328
358
  if (!account.href) return
329
359
  if (account.href.startsWith("http")) {
330
360
  window.open(account.href, "_blank", "noopener,noreferrer")
@@ -365,7 +395,7 @@ function SidebarFooterAccount({
365
395
  )
366
396
  }
367
397
 
368
- function AppSidebar({
398
+ function SidebarSurface({
369
399
  className,
370
400
  header,
371
401
  footer,
@@ -376,25 +406,70 @@ function AppSidebar({
376
406
  footerAccount,
377
407
  secondaryActions = [],
378
408
  footerSecondary,
379
- tooltipOnCollapsed = true,
409
+ tooltipOnCollapsed,
380
410
  onItemSelect,
381
411
  renderItem,
382
412
  renderLink,
383
413
  children,
414
+ mobile,
415
+ mobileTitle,
416
+ mobileDescription,
417
+ onRequestClose,
418
+ closeOnSelect = true,
384
419
  ...props
385
- }: AppSidebarProps) {
420
+ }: Omit<AppSidebarProps, "responsive" | "mobileBreakpoint" | "mobileOpen" | "defaultMobileOpen" | "onMobileOpenChange" | "mobileToggleLabel" | "mobileToggleIcon" | "showMobileToggle" | "mobileToggleClassName" | "mobilePanelClassName" | "mobileOverlayClassName" | "renderMobileToggle"> & {
421
+ mobile?: boolean
422
+ onRequestClose?: () => void
423
+ }) {
386
424
  const visibleItems = items.filter((item) => !item.hidden)
387
425
  const visibleRailItems = railItems.filter((item) => !item.hidden)
388
426
  const visibleSecondaryActions = secondaryActions.filter((item) => !item.hidden)
389
427
 
428
+ const handleSelect = React.useCallback((item: AppSidebarNavItem) => {
429
+ onItemSelect?.(item)
430
+ if (mobile && closeOnSelect) onRequestClose?.()
431
+ }, [closeOnSelect, mobile, onItemSelect, onRequestClose])
432
+
433
+ const showMobileHeader = mobile && (mobileTitle || mobileDescription || onRequestClose)
434
+
390
435
  return (
391
436
  <aside
392
437
  data-slot="app-sidebar"
393
438
  data-collapsed={collapsed || undefined}
439
+ data-mobile={mobile || undefined}
394
440
  className={cn("flex h-full min-h-0 flex-col overflow-hidden", className)}
395
441
  {...props}
396
442
  >
397
- {header && <div data-slot="app-sidebar-header" className="shrink-0 border-b p-3">{header}</div>}
443
+ {(header || showMobileHeader) && (
444
+ <div data-slot="app-sidebar-header" className="shrink-0 border-b p-3">
445
+ <div className="flex items-start gap-3">
446
+ <div className="min-w-0 flex-1">
447
+ {header ?? null}
448
+ {showMobileHeader ? (
449
+ <div className={cn(header && "mt-3")}>
450
+ {mobileTitle ? (
451
+ <p className="text-sm font-semibold text-foreground">{mobileTitle}</p>
452
+ ) : null}
453
+ {mobileDescription ? (
454
+ <p className="mt-1 text-xs leading-5 text-muted-foreground">{mobileDescription}</p>
455
+ ) : null}
456
+ </div>
457
+ ) : null}
458
+ </div>
459
+ {mobile && onRequestClose ? (
460
+ <button
461
+ type="button"
462
+ aria-label="Close navigation"
463
+ data-slot="app-sidebar-mobile-close"
464
+ className="inline-flex size-9 shrink-0 items-center justify-center rounded-xl border border-[color:var(--aui-divider)] bg-[color:var(--aui-page-bg-alt)] text-[color:var(--aui-page-foreground)] transition hover:bg-[color:var(--aui-control-bg)]"
465
+ onClick={onRequestClose}
466
+ >
467
+ <XIcon className="size-4" />
468
+ </button>
469
+ ) : null}
470
+ </div>
471
+ </div>
472
+ )}
398
473
 
399
474
  <nav data-slot="app-sidebar-nav" className="min-h-0 flex-1 space-y-1 overflow-y-auto overscroll-contain p-2">
400
475
  {children ??
@@ -417,7 +492,7 @@ function AppSidebar({
417
492
  items={visibleItems}
418
493
  collapsed={collapsed}
419
494
  depth={0}
420
- onItemSelect={onItemSelect}
495
+ onItemSelect={handleSelect}
421
496
  renderLink={renderLink}
422
497
  />
423
498
  )}
@@ -434,7 +509,7 @@ function AppSidebar({
434
509
  key={item.key}
435
510
  item={item}
436
511
  collapsed
437
- onItemSelect={onItemSelect}
512
+ onItemSelect={handleSelect}
438
513
  />
439
514
  ))}
440
515
  </div>
@@ -444,7 +519,9 @@ function AppSidebar({
444
519
  ) : null}
445
520
  {!collapsed && footerAccount ? (
446
521
  <div data-slot="app-sidebar-account-wrap" className="mb-3">
447
- <SidebarFooterAccount account={footerAccount} collapsed={false} />
522
+ <SidebarFooterAccount account={footerAccount} collapsed={false} onAfterSelect={() => {
523
+ if (mobile && closeOnSelect) onRequestClose?.()
524
+ }} />
448
525
  </div>
449
526
  ) : null}
450
527
  {!collapsed && visibleSecondaryActions.length > 0 ? (
@@ -454,7 +531,7 @@ function AppSidebar({
454
531
  key={item.key}
455
532
  item={item}
456
533
  collapsed={false}
457
- onItemSelect={onItemSelect}
534
+ onItemSelect={handleSelect}
458
535
  />
459
536
  ))}
460
537
  </div>
@@ -466,7 +543,13 @@ function AppSidebar({
466
543
  ) : null}
467
544
  {collapsed && footerAccount ? (
468
545
  <div data-slot="app-sidebar-account-wrap">
469
- <SidebarFooterAccount account={footerAccount} collapsed />
546
+ <SidebarFooterAccount
547
+ account={footerAccount}
548
+ collapsed
549
+ onAfterSelect={() => {
550
+ if (mobile && closeOnSelect) onRequestClose?.()
551
+ }}
552
+ />
470
553
  </div>
471
554
  ) : null}
472
555
  {!collapsed && footer}
@@ -475,7 +558,162 @@ function AppSidebar({
475
558
  </aside>
476
559
  )
477
560
  }
478
-
561
+
562
+ function AppSidebar({
563
+ className,
564
+ header,
565
+ footer,
566
+ items = [],
567
+ collapsed = false,
568
+ collapsedRail,
569
+ railItems = [],
570
+ footerAccount,
571
+ secondaryActions = [],
572
+ footerSecondary,
573
+ tooltipOnCollapsed = true,
574
+ responsive = true,
575
+ mobileBreakpoint = DEFAULT_APP_SIDEBAR_BREAKPOINT,
576
+ mobileOpen: mobileOpenProp,
577
+ defaultMobileOpen = false,
578
+ onMobileOpenChange,
579
+ mobileTitle,
580
+ mobileDescription,
581
+ mobileToggleLabel = "Open navigation",
582
+ mobileToggleIcon,
583
+ showMobileToggle = true,
584
+ closeOnSelect = true,
585
+ mobileToggleClassName,
586
+ mobilePanelClassName,
587
+ mobileOverlayClassName,
588
+ renderMobileToggle,
589
+ onItemSelect,
590
+ renderItem,
591
+ renderLink,
592
+ children,
593
+ ...props
594
+ }: AppSidebarProps) {
595
+ const isMobile = responsive ? useIsMobile(mobileBreakpoint) : false
596
+ const [uncontrolledMobileOpen, setUncontrolledMobileOpen] = React.useState(defaultMobileOpen)
597
+ const mobileOpen = mobileOpenProp ?? uncontrolledMobileOpen
598
+
599
+ const setMobileOpen = React.useCallback((nextOpen: boolean) => {
600
+ if (mobileOpenProp == null) {
601
+ setUncontrolledMobileOpen(nextOpen)
602
+ }
603
+ onMobileOpenChange?.(nextOpen)
604
+ }, [mobileOpenProp, onMobileOpenChange])
605
+
606
+ React.useEffect(() => {
607
+ if (!isMobile && mobileOpen) {
608
+ setMobileOpen(false)
609
+ }
610
+ }, [isMobile, mobileOpen, setMobileOpen])
611
+
612
+ React.useEffect(() => {
613
+ if (!isMobile || !mobileOpen) return
614
+
615
+ const previousOverflow = document.body.style.overflow
616
+ document.body.style.overflow = "hidden"
617
+
618
+ const handleKeyDown = (event: KeyboardEvent) => {
619
+ if (event.key === "Escape") {
620
+ setMobileOpen(false)
621
+ }
622
+ }
623
+
624
+ window.addEventListener("keydown", handleKeyDown)
625
+ return () => {
626
+ document.body.style.overflow = previousOverflow
627
+ window.removeEventListener("keydown", handleKeyDown)
628
+ }
629
+ }, [isMobile, mobileOpen, setMobileOpen])
630
+
631
+ const baseProps = {
632
+ className,
633
+ header,
634
+ footer,
635
+ items,
636
+ collapsed,
637
+ collapsedRail,
638
+ railItems,
639
+ footerAccount,
640
+ secondaryActions,
641
+ footerSecondary,
642
+ tooltipOnCollapsed,
643
+ closeOnSelect,
644
+ onItemSelect,
645
+ renderItem,
646
+ renderLink,
647
+ children,
648
+ ...props,
649
+ }
650
+
651
+ if (!responsive || !isMobile) {
652
+ return <SidebarSurface {...baseProps} />
653
+ }
654
+
655
+ const defaultTrigger = (
656
+ <button
657
+ type="button"
658
+ aria-label={mobileOpen ? "Close navigation" : mobileToggleLabel}
659
+ data-slot="app-sidebar-mobile-trigger"
660
+ data-state={mobileOpen ? "open" : "closed"}
661
+ className={cn(
662
+ "inline-flex min-h-10 items-center gap-2 rounded-xl border border-[color:var(--aui-divider)] bg-[color:var(--aui-page-bg)] px-3 text-sm font-medium text-[color:var(--aui-page-foreground)] shadow-sm transition hover:bg-[color:var(--aui-page-bg-alt)]",
663
+ mobileToggleClassName
664
+ )}
665
+ onClick={() => setMobileOpen(!mobileOpen)}
666
+ >
667
+ <span className="inline-flex size-8 items-center justify-center rounded-lg bg-[color:var(--aui-page-bg-alt)]">
668
+ {mobileToggleIcon ?? <MenuIcon className="size-4" />}
669
+ </span>
670
+ <span>{mobileOpen ? "Close navigation" : mobileToggleLabel}</span>
671
+ </button>
672
+ )
673
+
674
+ return (
675
+ <>
676
+ {showMobileToggle
677
+ ? renderMobileToggle
678
+ ? renderMobileToggle({ open: mobileOpen, setOpen: setMobileOpen })
679
+ : defaultTrigger
680
+ : null}
681
+ <div data-slot="app-sidebar-mobile-root" className="relative z-40">
682
+ <button
683
+ type="button"
684
+ aria-label="Dismiss navigation"
685
+ data-slot="app-sidebar-mobile-overlay"
686
+ data-state={mobileOpen ? "open" : "closed"}
687
+ className={cn(
688
+ "fixed inset-0 z-40 bg-black/45 transition-opacity duration-200",
689
+ mobileOpen ? "opacity-100" : "pointer-events-none opacity-0",
690
+ mobileOverlayClassName
691
+ )}
692
+ onClick={() => setMobileOpen(false)}
693
+ />
694
+ <SidebarSurface
695
+ {...baseProps}
696
+ mobile
697
+ collapsed={false}
698
+ mobileTitle={mobileTitle}
699
+ mobileDescription={mobileDescription}
700
+ onRequestClose={() => setMobileOpen(false)}
701
+ data-state={mobileOpen ? "open" : "closed"}
702
+ role="dialog"
703
+ aria-modal="true"
704
+ aria-label={typeof mobileTitle === "string" ? mobileTitle : "Navigation"}
705
+ className={cn(
706
+ "fixed inset-y-0 left-0 z-50 w-[min(88vw,22rem)] max-w-[22rem] border-r border-[color:var(--aui-divider)] bg-[color:var(--aui-page-bg)] shadow-2xl transition-transform duration-200 ease-out",
707
+ mobileOpen ? "translate-x-0" : "-translate-x-full",
708
+ mobilePanelClassName,
709
+ className
710
+ )}
711
+ />
712
+ </div>
713
+ </>
714
+ )
715
+ }
716
+
479
717
  /**
480
718
  * Canonical sidebar surface for new usage.
481
719
  */
@@ -5,11 +5,11 @@ import { cva, type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "@/lib/utils"
6
6
 
7
7
  const buttonVariants = cva(
8
- "group/button inline-flex shrink-0 items-center justify-center rounded-[var(--aui-control-radius,var(--radius-lg))] border border-[color:var(--aui-control-border-strong,var(--border))] bg-[color:var(--aui-control-surface,var(--background))] bg-clip-padding text-sm font-semibold text-foreground whitespace-nowrap shadow-[var(--aui-control-shadow,0_1px_2px_rgba(15,23,42,0.04))] transition-[transform,background-color,border-color,color,box-shadow,opacity] outline-none select-none hover:border-[color:var(--aui-control-hover-border,var(--ring))] hover:bg-[color:var(--aui-control-surface-hover,var(--muted))] focus-visible:ring-0 focus-visible:shadow-[var(--aui-control-shadow,0_1px_2px_rgba(15,23,42,0.04)),0_0_0_1px_var(--aui-focus-ring,var(--ring)),0_0_0_5px_var(--aui-focus-ring-soft,transparent)] active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:cursor-not-allowed disabled:translate-y-0 disabled:border-[color:color-mix(in_oklch,var(--border),transparent_24%)] disabled:bg-[color:var(--aui-control-surface-disabled,var(--muted))] disabled:text-muted-foreground disabled:shadow-none disabled:opacity-100 aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:translate-y-0 aria-disabled:opacity-100 aria-invalid:border-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
+ "group/button inline-flex shrink-0 items-center justify-center rounded-[var(--aui-control-radius,var(--radius-md))] border border-[color:var(--aui-control-border-strong,var(--border))] bg-[color:var(--aui-control-surface,var(--background))] bg-clip-padding text-sm font-semibold text-foreground whitespace-nowrap shadow-[var(--aui-control-shadow,0_1px_2px_rgba(15,23,42,0.04))] transition-[transform,background-color,border-color,color,box-shadow,opacity] outline-none select-none hover:border-[color:var(--aui-control-hover-border,var(--ring))] hover:bg-[color:var(--aui-control-surface-hover,var(--muted))] focus-visible:ring-0 focus-visible:shadow-[var(--aui-control-shadow,0_1px_2px_rgba(15,23,42,0.04)),0_0_0_1px_var(--aui-focus-ring,var(--ring)),0_0_0_4px_var(--aui-focus-ring-soft,transparent)] active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:cursor-not-allowed disabled:translate-y-0 disabled:border-[color:color-mix(in_oklch,var(--border),transparent_24%)] disabled:bg-[color:var(--aui-control-surface-disabled,var(--muted))] disabled:text-muted-foreground disabled:shadow-none disabled:opacity-100 aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:translate-y-0 aria-disabled:opacity-100 aria-invalid:border-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
9
  {
10
10
  variants: {
11
11
  variant: {
12
- default: "border-[color:color-mix(in_oklch,var(--primary),black_10%)] bg-primary text-primary-foreground shadow-[var(--aui-button-primary-shadow,0_10px_24px_rgba(16,185,129,0.18))] hover:border-[color:color-mix(in_oklch,var(--primary),black_18%)] hover:bg-[color:color-mix(in_oklch,var(--primary),white_8%)]",
12
+ default: "border-[color:color-mix(in_oklch,var(--primary),black_10%)] bg-primary text-primary-foreground shadow-[var(--aui-button-primary-shadow,0_10px_24px_rgba(16,185,129,0.18))] hover:border-[color:color-mix(in_oklch,var(--primary),black_18%)] hover:bg-[color:color-mix(in_oklch,var(--primary),white_8%)] hover:text-primary-foreground",
13
13
  outline: "bg-[color:var(--aui-control-surface,var(--background))] text-foreground shadow-none",
14
14
  secondary: "border-[color:color-mix(in_oklch,var(--border),var(--foreground)_6%)] bg-secondary text-secondary-foreground shadow-none",
15
15
  ghost: "border-transparent bg-transparent shadow-none",
@@ -13,7 +13,7 @@ function InputPrimitive({ className, type, ...props }: InputPrimitiveProps) {
13
13
  type={type}
14
14
  data-slot="input"
15
15
  className={cn(
16
- "h-11 w-full min-w-0 rounded-[var(--aui-control-radius,var(--radius-lg))] border border-[color:color-mix(in_oklch,var(--aui-control-border-strong,var(--input)),var(--foreground)_6%)] bg-[color:var(--aui-control-surface,var(--background))] px-4 py-2.5 text-sm font-medium text-foreground shadow-[var(--aui-shadow-xs,0_1px_2px_rgba(15,23,42,0.04))] transition-[background-color,border-color,box-shadow,color] outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground/72 hover:border-[color:color-mix(in_oklch,var(--aui-control-hover-border,var(--ring)),var(--foreground)_10%)] hover:bg-[color:var(--aui-control-surface-hover,var(--background))] focus-visible:border-[color:var(--ring)] focus-visible:ring-0 focus-visible:shadow-[var(--aui-shadow-xs,0_1px_2px_rgba(15,23,42,0.04)),0_0_0_1px_var(--aui-focus-ring,var(--ring)),0_0_0_5px_var(--aui-focus-ring-soft,transparent)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:border-[color:color-mix(in_oklch,var(--border),transparent_18%)] disabled:bg-[color:var(--aui-control-surface-disabled,var(--muted))] disabled:text-muted-foreground disabled:shadow-none disabled:opacity-100 read-only:bg-[color:var(--aui-control-surface-readonly,var(--muted))]",
16
+ "h-11 w-full min-w-0 rounded-[var(--aui-control-radius,var(--radius-md))] border border-[color:color-mix(in_oklch,var(--aui-control-border-strong,var(--input)),var(--foreground)_6%)] bg-[color:var(--aui-control-surface,var(--background))] px-3.5 py-2.5 text-sm font-normal text-foreground shadow-[var(--aui-shadow-xs,0_1px_2px_rgba(15,23,42,0.04))] transition-[background-color,border-color,box-shadow,color] outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground/74 hover:border-[color:color-mix(in_oklch,var(--aui-control-hover-border,var(--ring)),var(--foreground)_10%)] hover:bg-[color:var(--aui-control-surface-hover,var(--background))] focus-visible:border-[color:var(--ring)] focus-visible:ring-0 focus-visible:shadow-[var(--aui-shadow-xs,0_1px_2px_rgba(15,23,42,0.04)),0_0_0_1px_var(--aui-focus-ring,var(--ring)),0_0_0_4px_var(--aui-focus-ring-soft,transparent)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:border-[color:color-mix(in_oklch,var(--border),transparent_18%)] disabled:bg-[color:var(--aui-control-surface-disabled,var(--muted))] disabled:text-muted-foreground disabled:shadow-none disabled:opacity-100 read-only:bg-[color:var(--aui-control-surface-readonly,var(--muted))]",
17
17
  className
18
18
  )}
19
19
  {...props}
@@ -54,7 +54,7 @@ function SelectTrigger({
54
54
  data-slot="select-trigger"
55
55
  data-size={size}
56
56
  className={cn(
57
- "flex w-full min-w-0 items-center justify-between gap-2 rounded-[var(--aui-control-radius,var(--radius-md))] border border-[color:var(--aui-control-border-strong,var(--input))] bg-[color:var(--aui-control-surface,var(--background))] pr-3 pl-3 text-sm whitespace-nowrap text-foreground shadow-[var(--aui-control-shadow,0_1px_2px_rgba(15,23,42,0.04))] transition-[background-color,border-color,box-shadow,color] outline-none select-none hover:border-[color:var(--aui-control-hover-border,var(--ring))] hover:bg-[color:var(--aui-control-surface-hover,var(--background))] hover:shadow-[var(--aui-control-shadow-hover,0_2px_6px_rgba(15,23,42,0.06))] focus-visible:border-[color:var(--ring)] focus-visible:ring-0 focus-visible:shadow-[var(--aui-control-shadow,0_1px_2px_rgba(15,23,42,0.04)),0_0_0_1px_var(--aui-focus-ring,var(--ring)),0_0_0_4px_var(--aui-focus-ring-soft,transparent)] disabled:cursor-not-allowed disabled:border-[color:color-mix(in_oklch,var(--border),transparent_18%)] disabled:bg-[color:var(--aui-control-surface-disabled,var(--muted))] disabled:opacity-100 aria-invalid:border-destructive aria-invalid:shadow-[var(--aui-control-shadow,0_1px_2px_rgba(15,23,42,0.04)),0_0_0_1px_var(--aui-danger-ring,var(--destructive)),0_0_0_4px_var(--aui-danger-ring-soft,transparent)] data-placeholder:text-muted-foreground/74 data-[size=default]:h-10 data-[size=lg]:h-11 data-[size=sm]:h-9 data-[size=sm]:rounded-[var(--radius-sm)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:min-w-0 *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
57
+ "flex w-full min-w-0 items-center justify-between gap-2 rounded-[var(--aui-control-radius,var(--radius-md))] border border-[color:var(--aui-control-border-strong,var(--input))] bg-[color:var(--aui-control-surface,var(--background))] pr-3 pl-3 text-sm whitespace-nowrap text-foreground shadow-[var(--aui-control-shadow,0_1px_2px_rgba(15,23,42,0.04))] transition-[background-color,border-color,box-shadow,color] outline-none select-none hover:border-[color:var(--aui-control-hover-border,var(--ring))] hover:bg-[color:var(--aui-control-surface-hover,var(--background))] hover:shadow-[var(--aui-control-shadow-hover,0_2px_6px_rgba(15,23,42,0.06))] focus-visible:border-[color:var(--ring)] focus-visible:ring-0 focus-visible:shadow-[var(--aui-control-shadow,0_1px_2px_rgba(15,23,42,0.04)),0_0_0_1px_var(--aui-focus-ring,var(--ring)),0_0_0_4px_var(--aui-focus-ring-soft,transparent)] disabled:cursor-not-allowed disabled:border-[color:color-mix(in_oklch,var(--border),transparent_18%)] disabled:bg-[color:var(--aui-control-surface-disabled,var(--muted))] disabled:text-muted-foreground disabled:opacity-100 aria-invalid:border-destructive aria-invalid:shadow-[var(--aui-control-shadow,0_1px_2px_rgba(15,23,42,0.04)),0_0_0_1px_var(--aui-danger-ring,var(--destructive)),0_0_0_4px_var(--aui-danger-ring-soft,transparent)] data-placeholder:text-muted-foreground/74 data-[size=default]:h-11 data-[size=lg]:h-12 data-[size=sm]:h-9 data-[size=sm]:rounded-[var(--radius-sm)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:min-w-0 *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
58
58
  className
59
59
  )}
60
60
  {...props}
@@ -166,7 +166,7 @@ function SelectScrollUpButton({
166
166
  <SelectPrimitive.ScrollUpArrow
167
167
  data-slot="select-scroll-up-button"
168
168
  className={cn(
169
- "top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1.5 [&_svg:not([class*='size-'])]:size-4",
169
+ "top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 text-muted-foreground [&_svg:not([class*='size-'])]:size-4",
170
170
  className
171
171
  )}
172
172
  {...props}
@@ -184,7 +184,7 @@ function SelectScrollDownButton({
184
184
  <SelectPrimitive.ScrollDownArrow
185
185
  data-slot="select-scroll-down-button"
186
186
  className={cn(
187
- "bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1.5 [&_svg:not([class*='size-'])]:size-4",
187
+ "bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 text-muted-foreground [&_svg:not([class*='size-'])]:size-4",
188
188
  className
189
189
  )}
190
190
  {...props}
@@ -1,4 +1,4 @@
1
- export const PACKAGE_LATEST_VERSION = "2.0.1"
2
- export const PACKAGE_LATEST_RELEASE_DATE = "July 7, 2026"
1
+ export const PACKAGE_LATEST_VERSION = "2.0.3"
2
+ export const PACKAGE_LATEST_RELEASE_DATE = "July 8, 2026"
3
3
  export const PACKAGE_DEPENDENCY_RANGE = `^${PACKAGE_LATEST_VERSION}`
4
4
 
@@ -69,9 +69,13 @@ export function AppShellShowcase({ mode }: ComponentDemoProps) {
69
69
 
70
70
  <div className="grid min-h-[420px] lg:grid-cols-[250px_minmax(0,1fr)_260px]">
71
71
  <AppSidebar
72
- className={collapsed ? "hidden lg:block" : ""}
72
+ responsive
73
+ mobileTitle="Workspace navigation"
74
+ mobileDescription="Open the shell menu first, then move through app routes without squeezing the main content."
73
75
  header={<div className="px-3 py-2 text-sm font-semibold">AZA Studio</div>}
74
76
  footer={<div className="aui-text-muted px-3 py-2 text-xs">Shell footer</div>}
77
+ className={collapsed ? "lg:w-[92px]" : ""}
78
+ collapsed={collapsed}
75
79
  >
76
80
  <SidebarNav items={navItems} />
77
81
  </AppSidebar>
@@ -2,6 +2,7 @@ import * as React from "react"
2
2
  import { BarChart3Icon, CreditCardIcon, FolderIcon, HomeIcon, SettingsIcon } from "lucide-react"
3
3
 
4
4
  import { AppSidebar, Badge, Button } from "@/index"
5
+ import { cn } from "@/lib/utils"
5
6
 
6
7
  import type { ComponentDemoProps } from "../types"
7
8
 
@@ -18,6 +19,7 @@ const panelClass = "rounded-[22px] border border-[color:var(--aui-divider)] bg-[
18
19
  export function AppSidebarShowcase({ mode }: ComponentDemoProps) {
19
20
  const [activeKey, setActiveKey] = React.useState("overview")
20
21
  const [collapsed, setCollapsed] = React.useState(mode === "playground")
22
+ const [previewSurface, setPreviewSurface] = React.useState<"desktop" | "mobile">("desktop")
21
23
 
22
24
  const items = React.useMemo(
23
25
  () =>
@@ -30,6 +32,7 @@ export function AppSidebarShowcase({ mode }: ComponentDemoProps) {
30
32
  )
31
33
 
32
34
  const activeItem = navItems.find((item) => item.key === activeKey)
35
+ const mobilePreview = previewSurface === "mobile"
33
36
 
34
37
  return (
35
38
  <div className="space-y-5">
@@ -45,7 +48,7 @@ export function AppSidebarShowcase({ mode }: ComponentDemoProps) {
45
48
  <div className="flex flex-wrap gap-2">
46
49
  <Badge variant="outline" className="rounded-full">Header slot</Badge>
47
50
  <Badge variant="outline" className="rounded-full">Badges</Badge>
48
- <Badge variant="outline" className="rounded-full">Collapsed rail</Badge>
51
+ <Badge variant="outline" className="rounded-full">Mobile drawer</Badge>
49
52
  </div>
50
53
  </div>
51
54
  </section>
@@ -58,14 +61,25 @@ export function AppSidebarShowcase({ mode }: ComponentDemoProps) {
58
61
  <Button size="sm" variant="outline" onClick={() => setActiveKey("reports")}>
59
62
  Jump to reports
60
63
  </Button>
64
+ <Button size="sm" variant={mobilePreview ? "default" : "outline"} onClick={() => setPreviewSurface("mobile")}>
65
+ Mobile drawer
66
+ </Button>
67
+ <Button size="sm" variant={!mobilePreview ? "default" : "outline"} onClick={() => setPreviewSurface("desktop")}>
68
+ Desktop rail
69
+ </Button>
61
70
  </div>
62
71
  ) : null}
63
72
 
64
73
  <div className="grid gap-4 xl:grid-cols-[320px_minmax(0,1fr)]">
65
- <section className={panelClass}>
74
+ <section className={cn(panelClass, mobilePreview && "mx-auto w-full max-w-[420px]")}>
66
75
  <AppSidebar
67
76
  collapsed={collapsed}
68
77
  items={items}
78
+ responsive
79
+ mobileBreakpoint={mobilePreview ? 10000 : 0}
80
+ mobileTitle="Workspace navigation"
81
+ mobileDescription="Open routes, check badges, and move between sections from one compact drawer."
82
+ mobileToggleLabel="Open workspace menu"
69
83
  header={<div className="px-3 py-2 text-sm font-semibold">Azamat Workspace</div>}
70
84
  footer={<div className="aui-text-muted px-3 py-2 text-xs">Starter plan • 3 editors</div>}
71
85
  className="min-h-[440px] rounded-[22px] border border-[color:var(--aui-surface-border)] bg-[color:var(--aui-surface)]"
@@ -81,8 +95,8 @@ export function AppSidebarShowcase({ mode }: ComponentDemoProps) {
81
95
 
82
96
  <div className="mt-6 grid gap-3 sm:grid-cols-3">
83
97
  <div className="rounded-[20px] border border-[color:var(--aui-divider)] bg-[color:var(--aui-page-bg-alt)] px-4 py-3">
84
- <p className="text-[11px] font-semibold uppercase tracking-[0.24em] aui-text-muted">Mode</p>
85
- <p className="mt-2 text-lg font-semibold aui-text-strong">{collapsed ? "Collapsed" : "Expanded"}</p>
98
+ <p className="text-[11px] font-semibold uppercase tracking-[0.24em] aui-text-muted">Shell</p>
99
+ <p className="mt-2 text-lg font-semibold aui-text-strong">{mobilePreview ? "Mobile drawer" : (collapsed ? "Collapsed" : "Expanded")}</p>
86
100
  </div>
87
101
  <div className="rounded-[20px] border border-[color:var(--aui-divider)] bg-[color:var(--aui-page-bg-alt)] px-4 py-3">
88
102
  <p className="text-[11px] font-semibold uppercase tracking-[0.24em] aui-text-muted">Visible items</p>