@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.
- package/dist/backend/AppShell.js +125 -102
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js +36 -30
- package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +80 -47
- package/src/backend/__tests__/AppShell.test.tsx +116 -1
- package/src/backend/sidebar/SidebarCustomizationEditor.tsx +14 -11
package/src/backend/AppShell.tsx
CHANGED
|
@@ -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=
|
|
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
|
-
<
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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=
|
|
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
|
|
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=
|
|
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
|
|
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=
|
|
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
|
-
<
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
-
<
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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=
|
|
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
|
-
<
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
|
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=
|
|
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
|
-
<
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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}
|
|
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-
|
|
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
|
|
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: () =>
|
|
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
|
|
1583
|
-
|
|
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="
|
|
1600
|
-
<
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
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
|
|
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
|
})}
|