@open-mercato/ui 0.5.1-develop.2975.ccbadc8198 → 0.5.1-develop.2996.ce62fd491c
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/.turbo/turbo-build.log +1 -1
- package/dist/backend/AppShell.js +274 -697
- package/dist/backend/AppShell.js.map +3 -3
- package/dist/backend/CrudForm.js +1 -1
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/crud/CollapsibleZoneLayout.js +23 -3
- package/dist/backend/crud/CollapsibleZoneLayout.js.map +2 -2
- package/dist/backend/section-page/SectionNav.js +10 -8
- package/dist/backend/section-page/SectionNav.js.map +2 -2
- package/dist/backend/section-page/SectionPage.js +2 -2
- package/dist/backend/section-page/SectionPage.js.map +2 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js +1303 -0
- package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +7 -0
- package/dist/backend/sidebar/customization-helpers.js +150 -0
- package/dist/backend/sidebar/customization-helpers.js.map +7 -0
- package/dist/primitives/switch.js +1 -2
- package/dist/primitives/switch.js.map +2 -2
- package/jest.setup.ts +13 -0
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +245 -732
- package/src/backend/CrudForm.tsx +1 -1
- package/src/backend/__tests__/AppShell.test.tsx +1 -1
- package/src/backend/__tests__/CollapsibleZoneLayout.test.tsx +101 -0
- package/src/backend/__tests__/CrudForm.navigation.test.tsx +42 -0
- package/src/backend/__tests__/SidebarCustomizationEditor.test.tsx +200 -0
- package/src/backend/crud/CollapsibleZoneLayout.tsx +28 -3
- package/src/backend/section-page/SectionNav.tsx +14 -10
- package/src/backend/section-page/SectionPage.tsx +15 -10
- package/src/backend/sidebar/SidebarCustomizationEditor.tsx +1562 -0
- package/src/backend/sidebar/customization-helpers.ts +203 -0
- package/src/primitives/switch.tsx +1 -2
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { slugifySidebarId } from '@open-mercato/shared/modules/navigation/sidebarPreferences'
|
|
2
|
+
|
|
3
|
+
export type SidebarItem = {
|
|
4
|
+
id?: string
|
|
5
|
+
href: string
|
|
6
|
+
title: string
|
|
7
|
+
defaultTitle?: string
|
|
8
|
+
icon?: React.ReactNode
|
|
9
|
+
iconName?: string
|
|
10
|
+
iconMarkup?: string
|
|
11
|
+
enabled?: boolean
|
|
12
|
+
hidden?: boolean
|
|
13
|
+
pageContext?: 'main' | 'admin' | 'settings' | 'profile'
|
|
14
|
+
children?: SidebarItem[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SidebarGroup = {
|
|
18
|
+
id?: string
|
|
19
|
+
name: string
|
|
20
|
+
defaultName?: string
|
|
21
|
+
items: SidebarItem[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SidebarCustomizationDraft = {
|
|
25
|
+
order: string[]
|
|
26
|
+
groupLabels: Record<string, string>
|
|
27
|
+
itemLabels: Record<string, string>
|
|
28
|
+
hiddenItemIds: Record<string, boolean>
|
|
29
|
+
/** Per-group ordered item keys. Missing items keep their natural position at the tail. */
|
|
30
|
+
itemOrder: Record<string, string[]>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type SidebarRoleTarget = {
|
|
34
|
+
id: string
|
|
35
|
+
name: string
|
|
36
|
+
hasPreference: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveGroupKey(group: SidebarGroup): string {
|
|
40
|
+
if (group.id && group.id.length) return group.id
|
|
41
|
+
if (group.defaultName && group.defaultName.length) return slugifySidebarId(group.defaultName)
|
|
42
|
+
return slugifySidebarId(group.name)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveItemKey(item: { id?: string; href: string }): string {
|
|
46
|
+
const candidate = item.id?.trim()
|
|
47
|
+
if (candidate && candidate.length > 0) return candidate
|
|
48
|
+
return item.href
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function cloneSidebarGroups(groups: SidebarGroup[]): SidebarGroup[] {
|
|
52
|
+
const cloneItem = (item: SidebarItem): SidebarItem => ({
|
|
53
|
+
id: item.id,
|
|
54
|
+
href: item.href,
|
|
55
|
+
title: item.title,
|
|
56
|
+
defaultTitle: item.defaultTitle,
|
|
57
|
+
icon: item.icon,
|
|
58
|
+
iconName: item.iconName,
|
|
59
|
+
iconMarkup: item.iconMarkup,
|
|
60
|
+
enabled: item.enabled,
|
|
61
|
+
hidden: item.hidden,
|
|
62
|
+
pageContext: item.pageContext,
|
|
63
|
+
children: item.children ? item.children.map((child) => cloneItem(child)) : undefined,
|
|
64
|
+
})
|
|
65
|
+
return groups.map((group) => ({
|
|
66
|
+
id: group.id,
|
|
67
|
+
name: group.name,
|
|
68
|
+
defaultName: group.defaultName,
|
|
69
|
+
items: group.items.map((item) => cloneItem(item)),
|
|
70
|
+
}))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function mergeGroupOrder(preferred: string[], current: string[]): string[] {
|
|
74
|
+
const seen = new Set<string>()
|
|
75
|
+
const merged: string[] = []
|
|
76
|
+
for (const id of preferred) {
|
|
77
|
+
const trimmed = id.trim()
|
|
78
|
+
if (!trimmed || seen.has(trimmed) || !current.includes(trimmed)) continue
|
|
79
|
+
seen.add(trimmed)
|
|
80
|
+
merged.push(trimmed)
|
|
81
|
+
}
|
|
82
|
+
for (const id of current) {
|
|
83
|
+
if (seen.has(id)) continue
|
|
84
|
+
seen.add(id)
|
|
85
|
+
merged.push(id)
|
|
86
|
+
}
|
|
87
|
+
return merged
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Reorders items by preferred keys; items missing from `preferred` keep their original
|
|
91
|
+
* relative order at the tail. Drops keys that no longer exist. */
|
|
92
|
+
export function applyItemOrder<T>(items: T[], keyOf: (item: T) => string, preferred: string[] | undefined): T[] {
|
|
93
|
+
if (!preferred || preferred.length === 0) return items
|
|
94
|
+
const byKey = new Map<string, T>()
|
|
95
|
+
for (const item of items) byKey.set(keyOf(item), item)
|
|
96
|
+
const seen = new Set<string>()
|
|
97
|
+
const ordered: T[] = []
|
|
98
|
+
for (const key of preferred) {
|
|
99
|
+
if (seen.has(key)) continue
|
|
100
|
+
const match = byKey.get(key)
|
|
101
|
+
if (!match) continue
|
|
102
|
+
ordered.push(match)
|
|
103
|
+
seen.add(key)
|
|
104
|
+
}
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
const key = keyOf(item)
|
|
107
|
+
if (seen.has(key)) continue
|
|
108
|
+
ordered.push(item)
|
|
109
|
+
seen.add(key)
|
|
110
|
+
}
|
|
111
|
+
return ordered
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function applyItemDraft(item: SidebarItem, draft: SidebarCustomizationDraft): SidebarItem {
|
|
115
|
+
const itemKey = resolveItemKey(item)
|
|
116
|
+
const baseTitle = item.defaultTitle ?? item.title
|
|
117
|
+
const override = draft.itemLabels[itemKey]?.trim()
|
|
118
|
+
const children = item.children
|
|
119
|
+
? item.children.map((child) => applyItemDraft(child, draft))
|
|
120
|
+
: undefined
|
|
121
|
+
const hidden = draft.hiddenItemIds[itemKey] === true
|
|
122
|
+
return {
|
|
123
|
+
...item,
|
|
124
|
+
title: override && override.length > 0 ? override : baseTitle,
|
|
125
|
+
hidden,
|
|
126
|
+
children,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function applyCustomizationDraft(
|
|
131
|
+
baseGroups: SidebarGroup[],
|
|
132
|
+
draft: SidebarCustomizationDraft,
|
|
133
|
+
): SidebarGroup[] {
|
|
134
|
+
const clones = cloneSidebarGroups(baseGroups)
|
|
135
|
+
const byId = new Map<string, SidebarGroup>()
|
|
136
|
+
for (const group of clones) {
|
|
137
|
+
byId.set(resolveGroupKey(group), group)
|
|
138
|
+
}
|
|
139
|
+
const orderedIds = mergeGroupOrder(draft.order, Array.from(byId.keys()))
|
|
140
|
+
const seen = new Set<string>()
|
|
141
|
+
const result: SidebarGroup[] = []
|
|
142
|
+
for (const id of orderedIds) {
|
|
143
|
+
if (seen.has(id)) continue
|
|
144
|
+
const group = byId.get(id)
|
|
145
|
+
if (!group) continue
|
|
146
|
+
seen.add(id)
|
|
147
|
+
const baseName = group.defaultName ?? group.name
|
|
148
|
+
const override = draft.groupLabels[id]?.trim()
|
|
149
|
+
const orderedItems = applyItemOrder(group.items, resolveItemKey, draft.itemOrder?.[id])
|
|
150
|
+
result.push({
|
|
151
|
+
...group,
|
|
152
|
+
name: override && override.length > 0 ? override : baseName,
|
|
153
|
+
items: orderedItems.map((item) => applyItemDraft(item, draft)),
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Filters groups to include only main sidebar items.
|
|
161
|
+
* Excludes items with pageContext 'settings' or 'profile' from customization.
|
|
162
|
+
* Per SPEC-007: Sidebar customization applies only to the main sidebar.
|
|
163
|
+
*/
|
|
164
|
+
export function filterMainSidebarGroups(groups: SidebarGroup[]): SidebarGroup[] {
|
|
165
|
+
const isMainItem = (item: SidebarItem): boolean => {
|
|
166
|
+
if (item.pageContext && item.pageContext !== 'main') return false
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return groups
|
|
171
|
+
.map((group) => ({
|
|
172
|
+
...group,
|
|
173
|
+
items: group.items.filter(isMainItem).map((item) => ({
|
|
174
|
+
...item,
|
|
175
|
+
children: item.children?.filter(isMainItem),
|
|
176
|
+
})),
|
|
177
|
+
}))
|
|
178
|
+
.filter((group) => group.items.length > 0)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function collectSidebarDefaults(groups: SidebarGroup[]) {
|
|
182
|
+
const groupDefaults = new Map<string, string>()
|
|
183
|
+
const itemDefaults = new Map<string, string>()
|
|
184
|
+
|
|
185
|
+
const visitItems = (items: SidebarItem[]) => {
|
|
186
|
+
for (const item of items) {
|
|
187
|
+
const key = resolveItemKey(item)
|
|
188
|
+
const baseTitle = item.defaultTitle ?? item.title
|
|
189
|
+
itemDefaults.set(key, baseTitle)
|
|
190
|
+
// Backward-compatible alias for legacy stored href-based preferences.
|
|
191
|
+
itemDefaults.set(item.href, baseTitle)
|
|
192
|
+
if (item.children && item.children.length > 0) visitItems(item.children)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const group of groups) {
|
|
197
|
+
const key = resolveGroupKey(group)
|
|
198
|
+
groupDefaults.set(key, group.defaultName ?? group.name)
|
|
199
|
+
visitItems(group.items)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { groupDefaults, itemDefaults }
|
|
203
|
+
}
|
|
@@ -85,8 +85,7 @@ export const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
|
|
85
85
|
>
|
|
86
86
|
<span
|
|
87
87
|
className={cn(
|
|
88
|
-
'block size-3 rounded-full bg-white transition-transform duration-200',
|
|
89
|
-
'shadow-[0_1px_2px_rgba(10,13,20,0.10),0_0_0_0.5px_rgba(10,13,20,0.04)]',
|
|
88
|
+
'block size-3 rounded-full bg-white transition-transform duration-200 shadow-switch-thumb',
|
|
90
89
|
currentChecked ? 'translate-x-3' : 'translate-x-0'
|
|
91
90
|
)}
|
|
92
91
|
/>
|