@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.
Files changed (31) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/backend/AppShell.js +274 -697
  3. package/dist/backend/AppShell.js.map +3 -3
  4. package/dist/backend/CrudForm.js +1 -1
  5. package/dist/backend/CrudForm.js.map +2 -2
  6. package/dist/backend/crud/CollapsibleZoneLayout.js +23 -3
  7. package/dist/backend/crud/CollapsibleZoneLayout.js.map +2 -2
  8. package/dist/backend/section-page/SectionNav.js +10 -8
  9. package/dist/backend/section-page/SectionNav.js.map +2 -2
  10. package/dist/backend/section-page/SectionPage.js +2 -2
  11. package/dist/backend/section-page/SectionPage.js.map +2 -2
  12. package/dist/backend/sidebar/SidebarCustomizationEditor.js +1303 -0
  13. package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +7 -0
  14. package/dist/backend/sidebar/customization-helpers.js +150 -0
  15. package/dist/backend/sidebar/customization-helpers.js.map +7 -0
  16. package/dist/primitives/switch.js +1 -2
  17. package/dist/primitives/switch.js.map +2 -2
  18. package/jest.setup.ts +13 -0
  19. package/package.json +3 -3
  20. package/src/backend/AppShell.tsx +245 -732
  21. package/src/backend/CrudForm.tsx +1 -1
  22. package/src/backend/__tests__/AppShell.test.tsx +1 -1
  23. package/src/backend/__tests__/CollapsibleZoneLayout.test.tsx +101 -0
  24. package/src/backend/__tests__/CrudForm.navigation.test.tsx +42 -0
  25. package/src/backend/__tests__/SidebarCustomizationEditor.test.tsx +200 -0
  26. package/src/backend/crud/CollapsibleZoneLayout.tsx +28 -3
  27. package/src/backend/section-page/SectionNav.tsx +14 -10
  28. package/src/backend/section-page/SectionPage.tsx +15 -10
  29. package/src/backend/sidebar/SidebarCustomizationEditor.tsx +1562 -0
  30. package/src/backend/sidebar/customization-helpers.ts +203 -0
  31. 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
  />