@open-mercato/ui 0.6.8-develop.7072.1.19c2a8bbe0 → 0.6.8-develop.7077.1.94f1126c13

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.
@@ -411,6 +411,19 @@ function resolveItemKey(item: { id?: string; href: string }): string {
411
411
  return item.href
412
412
  }
413
413
 
414
+ // Horizontal padding lives on each sidebar block rather than on the <aside>, so the scroll
415
+ // container never needs a negative-margin bleed to reach the panel edges. The transition keeps the
416
+ // compact/expanded padding step in sync with the aside's width animation.
417
+ function sidebarBlockPadding(compact: boolean): string {
418
+ return `${compact ? 'px-2' : 'px-3'} transition-[padding] duration-200 ease-out`
419
+ }
420
+
421
+ // An InjectionSpot renders nothing when no widget is registered, so its wrapper must collapse
422
+ // instead of surviving as an empty flex child that still costs a full `gap-3`.
423
+ function sidebarInjectionWrapper(compact: boolean): string {
424
+ return `${sidebarBlockPadding(compact)} empty:hidden`
425
+ }
426
+
414
427
  function SerializedIcon({ markup }: { markup: string }) {
415
428
  return <span aria-hidden="true" dangerouslySetInnerHTML={{ __html: markup }} />
416
429
  }
@@ -804,11 +817,12 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
804
817
  ) {
805
818
  const sortedSections = [...sections].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
806
819
  const lastVisibleIndex = sortedSections.length - 1
820
+ const blockPadding = sidebarBlockPadding(compact)
807
821
 
808
822
  return (
809
823
  <div className="flex h-full flex-col gap-3">
810
824
  {!hideHeader && (
811
- <div className="mb-2">
825
+ <div className={`mb-2 ${blockPadding}`}>
812
826
  <Link
813
827
  href="/backend"
814
828
  className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}
@@ -820,16 +834,18 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
820
834
  </div>
821
835
  )}
822
836
  {!compact && !hideSearch && (
823
- <SearchInput
824
- value={navQuery}
825
- onChange={setNavQuery}
826
- placeholder={t('appShell.searchNavPlaceholder', 'Search...')}
827
- aria-label={t('appShell.searchNavAria', 'Search navigation')}
828
- clearLabel={t('appShell.searchNavClear', 'Clear search')}
829
- className="mb-2"
830
- />
837
+ <div className={blockPadding}>
838
+ <SearchInput
839
+ value={navQuery}
840
+ onChange={setNavQuery}
841
+ placeholder={t('appShell.searchNavPlaceholder', 'Search...')}
842
+ aria-label={t('appShell.searchNavAria', 'Search navigation')}
843
+ clearLabel={t('appShell.searchNavClear', 'Clear search')}
844
+ className="mb-2"
845
+ />
846
+ </div>
831
847
  )}
832
- <div data-sidebar-scroll="true" className={`flex flex-1 flex-col gap-3 overflow-y-auto scrollbar-hide pr-1 ${compact ? '-ml-2 pl-2' : '-ml-3 pl-3'}`}>
848
+ <div data-sidebar-scroll="true" className="flex flex-1 flex-col gap-3 overflow-x-hidden overflow-y-auto scrollbar-hide">
833
849
  <nav className="flex flex-col gap-2">
834
850
  {sortedSections.map((section, sectionIndex) => {
835
851
  const sectionNavQueryActive = hideSearch ? false : navQueryActive
@@ -909,7 +925,11 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
909
925
  }
910
926
 
911
927
  return (
912
- <div key={section.id}>
928
+ <div
929
+ key={section.id}
930
+ className={`${blockPadding} ${sectionIndex !== lastVisibleIndex ? 'border-b pb-2' : ''}`}
931
+ data-sidebar-group="true"
932
+ >
913
933
  {!compact && (
914
934
  <Button
915
935
  variant="muted"
@@ -926,7 +946,6 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
926
946
  {sortedItems.map((item) => renderSectionItem(item))}
927
947
  </div>
928
948
  )}
929
- {sectionIndex !== lastVisibleIndex && <div className={`my-2 border-t ${compact ? '-ml-2 -mr-3' : '-ml-3 -mr-4'}`} />}
930
949
  </div>
931
950
  )
932
951
  })}
@@ -937,9 +956,12 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
937
956
  }
938
957
 
939
958
  function renderSidebar(compact: boolean, hideHeader?: boolean, forceMainOnly?: boolean) {
959
+ const blockPadding = sidebarBlockPadding(compact)
960
+ const injectionWrapper = sidebarInjectionWrapper(compact)
961
+
940
962
  if (!isChromeReady && isChromeLoading) {
941
963
  return (
942
- <div className="flex flex-col min-h-full gap-3" data-testid="backend-chrome-loading">
964
+ <div className={`flex min-h-full flex-col gap-3 ${blockPadding}`} data-testid="backend-chrome-loading">
943
965
  {!hideHeader ? (
944
966
  <div className="mb-2">
945
967
  <Link
@@ -952,7 +974,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
952
974
  </Link>
953
975
  </div>
954
976
  ) : null}
955
- <div className="flex flex-1 flex-col gap-3 pr-1">
977
+ <div className="flex flex-1 flex-col gap-3">
956
978
  <div className="space-y-3">
957
979
  <div className="h-8 rounded bg-muted/50" />
958
980
  <div className="space-y-2 pl-1">
@@ -1007,7 +1029,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1007
1029
  return (
1008
1030
  <div className="flex h-full flex-col gap-3">
1009
1031
  {!hideHeader && (
1010
- <div className="mb-2">
1032
+ <div className={`mb-2 ${blockPadding}`}>
1011
1033
  <Link
1012
1034
  href="/backend"
1013
1035
  className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}
@@ -1019,22 +1041,26 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1019
1041
  </div>
1020
1042
  )}
1021
1043
  {shouldRenderSidebarInjectionSpots ? (
1022
- <InjectionSpot
1023
- spotId={BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID}
1024
- context={injectionContext}
1025
- />
1044
+ <div className={injectionWrapper} data-sidebar-injection-wrapper="true">
1045
+ <InjectionSpot
1046
+ spotId={BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID}
1047
+ context={injectionContext}
1048
+ />
1049
+ </div>
1026
1050
  ) : null}
1027
1051
  {!compact && (
1028
- <SearchInput
1029
- value={navQuery}
1030
- onChange={setNavQuery}
1031
- placeholder={t('appShell.searchNavPlaceholder', 'Search...')}
1032
- aria-label={t('appShell.searchNavAria', 'Search navigation')}
1033
- clearLabel={t('appShell.searchNavClear', 'Clear search')}
1034
- className="mb-2"
1035
- />
1052
+ <div className={blockPadding}>
1053
+ <SearchInput
1054
+ value={navQuery}
1055
+ onChange={setNavQuery}
1056
+ placeholder={t('appShell.searchNavPlaceholder', 'Search...')}
1057
+ aria-label={t('appShell.searchNavAria', 'Search navigation')}
1058
+ clearLabel={t('appShell.searchNavClear', 'Clear search')}
1059
+ className="mb-2"
1060
+ />
1061
+ </div>
1036
1062
  )}
1037
- <div data-sidebar-scroll="true" className={`flex flex-1 flex-col gap-3 overflow-y-auto scrollbar-hide pr-1 ${compact ? '-ml-2 pl-2' : '-ml-3 pl-3'}`}>
1063
+ <div data-sidebar-scroll="true" className="flex flex-1 flex-col gap-3 overflow-x-hidden overflow-y-auto scrollbar-hide">
1038
1064
  {(() => {
1039
1065
  const isSettingsPath = (href: string) => {
1040
1066
  if (href === '/backend/settings') return true
@@ -1063,10 +1089,12 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1063
1089
  <>
1064
1090
  <nav className="flex flex-col gap-2" data-testid="sidebar">
1065
1091
  {shouldRenderSidebarInjectionSpots ? (
1066
- <InjectionSpot
1067
- spotId={BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID}
1068
- context={injectionContext}
1069
- />
1092
+ <div className={injectionWrapper} data-sidebar-injection-wrapper="true">
1093
+ <InjectionSpot
1094
+ spotId={BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID}
1095
+ context={injectionContext}
1096
+ />
1097
+ </div>
1070
1098
  ) : null}
1071
1099
  {mainGroups.map((g, gi) => {
1072
1100
  const groupId = resolveGroupKey(g)
@@ -1080,7 +1108,11 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1080
1108
  })
1081
1109
  if (visibleItems.length === 0) return null
1082
1110
  return (
1083
- <div key={groupId}>
1111
+ <div
1112
+ className={`${blockPadding} ${gi !== mainLastVisibleGroupIndex ? 'border-b pb-2' : ''}`}
1113
+ key={groupId}
1114
+ data-sidebar-group="true"
1115
+ >
1084
1116
  {!compact && (
1085
1117
  <Button
1086
1118
  variant="muted"
@@ -1173,7 +1205,6 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1173
1205
  })}
1174
1206
  </div>
1175
1207
  )}
1176
- {gi !== mainLastVisibleGroupIndex && <div className={`my-2 border-t ${compact ? '-ml-2 -mr-3' : '-ml-3 -mr-4'}`} />}
1177
1208
  </div>
1178
1209
  )
1179
1210
  })}
@@ -1182,7 +1213,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1182
1213
  )
1183
1214
  })()}
1184
1215
  </div>
1185
- <div className="sticky bottom-0 bg-background pb-1">
1216
+ <div className={`sticky bottom-0 bg-background pb-1 ${injectionWrapper}`} data-sidebar-injection-wrapper="true">
1186
1217
  {shouldRenderSidebarInjectionSpots ? (
1187
1218
  <InjectionSpot
1188
1219
  spotId={BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID}
@@ -1227,15 +1258,17 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1227
1258
  if (!sections) return null
1228
1259
  return (
1229
1260
  <div className="flex h-full flex-col gap-2">
1230
- <Link
1231
- href="/backend"
1232
- className="inline-flex items-center gap-2 rounded-lg px-2 py-2 text-sm font-semibold text-foreground transition-colors hover:bg-muted"
1233
- data-testid="appshell-section-back-to-main"
1234
- aria-label={t('backend.nav.backToMain', 'Back to Main')}
1235
- >
1236
- <ChevronLeft className="size-4 shrink-0" aria-hidden />
1237
- <span className="truncate">{title}</span>
1238
- </Link>
1261
+ <div className={sidebarBlockPadding(false)}>
1262
+ <Link
1263
+ href="/backend"
1264
+ className="inline-flex items-center gap-2 rounded-lg px-2 py-2 text-sm font-semibold text-foreground transition-colors hover:bg-muted"
1265
+ data-testid="appshell-section-back-to-main"
1266
+ aria-label={t('backend.nav.backToMain', 'Back to Main')}
1267
+ >
1268
+ <ChevronLeft className="size-4 shrink-0" aria-hidden />
1269
+ <span className="truncate">{title}</span>
1270
+ </Link>
1271
+ </div>
1239
1272
  <div className="min-h-0 flex-1">
1240
1273
  {renderSectionSidebar(sections, title, false, true, true)}
1241
1274
  </div>
@@ -1304,7 +1337,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1304
1337
  {effectiveCollapsed ? <PanelLeftOpen className="size-4" /> : <PanelLeftClose className="size-4" />}
1305
1338
  </button>
1306
1339
  {/* Desktop main sidebar */}
1307
- <aside ref={sidebarAsideRef} className={`${asideClassesBase} ${effectiveCollapsed ? 'px-2' : 'px-3'} hidden lg:block lg:sticky lg:top-0 lg:h-svh lg:self-start lg:overflow-hidden lg:relative transition-[width,padding] duration-200 ease-out`} style={{ width: asideWidth }}>
1340
+ <aside ref={sidebarAsideRef} className={`${asideClassesBase} px-0 hidden lg:block lg:sticky lg:top-0 lg:h-svh lg:self-start lg:overflow-hidden lg:relative transition-[width] duration-200 ease-out`} style={{ width: asideWidth }}>
1308
1341
  {renderSidebar(effectiveCollapsed, false, isSectionView)}
1309
1342
  {/* Scroll affordance — gradient fade + clickable chevron that flips up when
1310
1343
  the user reaches the bottom and disappears when nothing is scrollable
@@ -1349,7 +1382,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1349
1382
  original swap behavior to fit the narrow width. */}
1350
1383
  {isSectionView ? (
1351
1384
  <aside
1352
- className={`${asideClassesBase} px-3 hidden lg:block lg:sticky lg:top-0 lg:h-svh lg:self-start lg:overflow-hidden lg:relative`}
1385
+ className={`${asideClassesBase} px-0 hidden lg:block lg:sticky lg:top-0 lg:h-svh lg:self-start lg:overflow-hidden lg:relative`}
1353
1386
  style={{ width: '240px' }}
1354
1387
  data-testid="appshell-section-sidebar"
1355
1388
  >
@@ -1571,7 +1604,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1571
1604
  ? `mobile-drawer-tab-${mobileDrawerView === 'main' ? 'main' : 'section'}`
1572
1605
  : undefined
1573
1606
  }
1574
- className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden p-3"
1607
+ className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden py-3"
1575
1608
  >
1576
1609
  {/* Force expanded sidebar in mobile drawer, hide its header and collapse toggle */}
1577
1610
  {renderSidebar(false, true, mobileDrawerView === 'main')}
@@ -9,6 +9,7 @@ import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWith
9
9
 
10
10
  const mockInjectionSpot = jest.fn()
11
11
  let mockPathname = '/backend/users'
12
+ let mockInjectionSpotRendersNothing = false
12
13
 
13
14
  jest.mock('next/link', () => {
14
15
  const React = require('react')
@@ -39,6 +40,7 @@ jest.mock('next/navigation', () => ({
39
40
  jest.mock('../injection/InjectionSpot', () => ({
40
41
  InjectionSpot: (props: { spotId: string; context?: Record<string, unknown> }) => {
41
42
  mockInjectionSpot(props)
43
+ if (mockInjectionSpotRendersNothing) return null
42
44
  return <div data-testid={`injection-spot:${props.spotId}`} />
43
45
  },
44
46
  }))
@@ -55,7 +57,9 @@ jest.mock('../injection/eventBridge', () => ({
55
57
  }))
56
58
 
57
59
  jest.mock('../injection/StatusBadgeInjectionSpot', () => ({
58
- StatusBadgeInjectionSpot: () => <div data-testid="status-badge-injection-spot" />,
60
+ StatusBadgeInjectionSpot: () => (
61
+ mockInjectionSpotRendersNothing ? null : <div data-testid="status-badge-injection-spot" />
62
+ ),
59
63
  }))
60
64
 
61
65
  jest.mock('../operations/LastOperationBanner', () => ({
@@ -117,6 +121,7 @@ describe('AppShell', () => {
117
121
  beforeEach(() => {
118
122
  mockInjectionSpot.mockClear()
119
123
  mockPathname = '/backend/users'
124
+ mockInjectionSpotRendersNothing = false
120
125
  })
121
126
 
122
127
  beforeAll(() => {
@@ -623,6 +628,116 @@ describe('AppShell', () => {
623
628
  }
624
629
  })
625
630
 
631
+ describe('sidebar layout', () => {
632
+ const multiGroups = [
633
+ groups[0],
634
+ {
635
+ id: 'commerce',
636
+ name: 'Commerce',
637
+ items: [{ href: '/backend/orders', title: 'Orders' }],
638
+ },
639
+ {
640
+ id: 'ops',
641
+ name: 'Ops',
642
+ items: [{ href: '/backend/jobs', title: 'Jobs' }],
643
+ },
644
+ ]
645
+
646
+ it('collapses sidebar injection-spot wrappers when their spots render nothing', async () => {
647
+ mockInjectionSpotRendersNothing = true
648
+
649
+ const { container } = renderWithProviders(
650
+ <AppShell email="demo@example.com" groups={groups}>
651
+ <div>Child content</div>
652
+ </AppShell>,
653
+ { dict },
654
+ )
655
+
656
+ await waitFor(() => {
657
+ expect(screen.getByText('Users List')).toBeInTheDocument()
658
+ })
659
+
660
+ const wrappers = Array.from(container.querySelectorAll('[data-sidebar-injection-wrapper="true"]'))
661
+ expect(wrappers.length).toBeGreaterThan(0)
662
+ for (const wrapper of wrappers) {
663
+ // The spot rendered nothing, so the wrapper is an empty flex child of the sidebar's
664
+ // `gap-3` column and would still cost a full gap unless it collapses itself.
665
+ expect(wrapper.children).toHaveLength(0)
666
+ expect(wrapper).toHaveClass('empty:hidden')
667
+ }
668
+ })
669
+
670
+ it('keeps every sidebar injection spot inside a self-collapsing wrapper', async () => {
671
+ const { container } = renderWithProviders(
672
+ <AppShell email="demo@example.com" groups={groups}>
673
+ <div>Child content</div>
674
+ </AppShell>,
675
+ { dict },
676
+ )
677
+
678
+ await waitFor(() => {
679
+ expect(screen.getByText('Users List')).toBeInTheDocument()
680
+ })
681
+
682
+ const sidebarSpotIds = ['backend:sidebar:top', 'backend:sidebar:nav', 'backend:sidebar:nav:footer', 'backend:sidebar:footer']
683
+ for (const spotId of sidebarSpotIds) {
684
+ const spot = container.querySelector(`[data-testid="injection-spot:${spotId}"]`)
685
+ expect(spot).not.toBeNull()
686
+ expect(spot!.closest('[data-sidebar-injection-wrapper="true"]')).not.toBeNull()
687
+ }
688
+ })
689
+
690
+ it('renders group separators on the group wrapper itself, skipping the last visible group', async () => {
691
+ const { container } = renderWithProviders(
692
+ <AppShell email="demo@example.com" groups={multiGroups}>
693
+ <div>Child content</div>
694
+ </AppShell>,
695
+ { dict },
696
+ )
697
+
698
+ await waitFor(() => {
699
+ expect(screen.getByText('Orders')).toBeInTheDocument()
700
+ })
701
+
702
+ const sidebar = screen.getByTestId('sidebar')
703
+ const groupWrappers = Array.from(sidebar.querySelectorAll('[data-sidebar-group="true"]'))
704
+ expect(groupWrappers).toHaveLength(multiGroups.length)
705
+ groupWrappers.forEach((wrapper, index) => {
706
+ const isLast = index === groupWrappers.length - 1
707
+ expect(wrapper).toHaveClass(isLast ? 'px-3' : 'border-b')
708
+ expect(wrapper.classList.contains('border-b')).toBe(!isLast)
709
+ expect(wrapper.classList.contains('pb-2')).toBe(!isLast)
710
+ })
711
+
712
+ // The old standalone bleeding separator elements must not come back — they were what
713
+ // forced the scroll container's negative-margin bleed in the first place.
714
+ expect(container.querySelectorAll('[data-sidebar-scroll="true"] div.border-t')).toHaveLength(0)
715
+ })
716
+
717
+ it('pads sidebar content blocks instead of bleeding the scroll container out of the aside', async () => {
718
+ const { container } = renderWithProviders(
719
+ <AppShell email="demo@example.com" groups={groups}>
720
+ <div>Child content</div>
721
+ </AppShell>,
722
+ { dict },
723
+ )
724
+
725
+ await waitFor(() => {
726
+ expect(screen.getByText('Users List')).toBeInTheDocument()
727
+ })
728
+
729
+ const scrollContainers = Array.from(container.querySelectorAll('[data-sidebar-scroll="true"]'))
730
+ expect(scrollContainers.length).toBeGreaterThan(0)
731
+ for (const scroller of scrollContainers) {
732
+ expect(scroller).toHaveClass('overflow-x-hidden')
733
+ for (const className of Array.from(scroller.classList)) {
734
+ expect(className.startsWith('-ml-')).toBe(false)
735
+ expect(className.startsWith('-mr-')).toBe(false)
736
+ }
737
+ }
738
+ })
739
+ })
740
+
626
741
  describe('two-level sidebar (settings/profile mode)', () => {
627
742
  it('renders main + section sidebars side-by-side when on a settings path', async () => {
628
743
  mockPathname = '/backend/entities/user'
@@ -1579,11 +1579,10 @@ function SidebarPreview({
1579
1579
 
1580
1580
  return (
1581
1581
  <div className="relative w-[240px] overflow-hidden rounded-xl border bg-background shadow-sm">
1582
- {/* Match AppShell's outer aside: border-r, py-4, px-3 — minus border-r since the
1583
- card border already serves that purpose, plus rounded so it reads as a preview tile. */}
1584
- <div className="flex flex-col gap-3 px-3 py-4">
1582
+ {/* Match AppShell's full-width group boxes with padding applied by each inner region. */}
1583
+ <div className="flex flex-col gap-3 py-4">
1585
1584
  {/* Brand block — same classes as AppShell brand tile */}
1586
- <div className="mb-2">
1585
+ <div className="mb-2 px-3">
1587
1586
  <div className="flex items-center gap-3 rounded-xl p-3">
1588
1587
  <Image
1589
1588
  src="/open-mercato.svg"
@@ -1596,11 +1595,13 @@ function SidebarPreview({
1596
1595
  </div>
1597
1596
  </div>
1598
1597
  {/* Search input mock — same container styling as the real sidebar */}
1599
- <div className="mb-2 flex items-center gap-2 rounded-lg border border-border bg-background pl-2.5 pr-2 py-2 shadow-sm">
1600
- <Search className="size-4 shrink-0 text-muted-foreground" aria-hidden />
1601
- <span className="min-w-0 flex-1 text-sm text-muted-foreground/70 truncate">
1602
- {t('appShell.sidebarCustomizationPreviewSearchPlaceholder', 'Search...')}
1603
- </span>
1598
+ <div className="px-3">
1599
+ <div className="mb-2 flex items-center gap-2 rounded-lg border border-border bg-background pl-2.5 pr-2 py-2 shadow-sm">
1600
+ <Search className="size-4 shrink-0 text-muted-foreground" aria-hidden />
1601
+ <span className="min-w-0 flex-1 text-sm text-muted-foreground/70 truncate">
1602
+ {t('appShell.sidebarCustomizationPreviewSearchPlaceholder', 'Search...')}
1603
+ </span>
1604
+ </div>
1604
1605
  </div>
1605
1606
  {groups.length === 0 ? (
1606
1607
  <p className="px-2 text-sm text-muted-foreground">
@@ -1612,7 +1613,10 @@ function SidebarPreview({
1612
1613
  const visibleItems = group.items.filter((item) => item.hidden !== true)
1613
1614
  if (visibleItems.length === 0) return null
1614
1615
  return (
1615
- <div key={resolveGroupKey(group)}>
1616
+ <div
1617
+ key={resolveGroupKey(group)}
1618
+ className={`px-3 ${gi < groups.length - 1 ? 'border-b pb-2' : ''}`}
1619
+ >
1616
1620
  <div className="w-full px-1 justify-between flex text-xs font-medium uppercase tracking-wider text-muted-foreground/70 py-1">
1617
1621
  <span>{group.name}</span>
1618
1622
  </div>
@@ -1641,7 +1645,6 @@ function SidebarPreview({
1641
1645
  )
1642
1646
  })}
1643
1647
  </div>
1644
- {gi < groups.length - 1 ? <div className="my-2 border-t -ml-3 -mr-4" /> : null}
1645
1648
  </div>
1646
1649
  )
1647
1650
  })}