@open-mercato/ui 0.4.11-develop.2631.481e9df5b0 → 0.5.0

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 (41) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/AGENTS.md +28 -4
  3. package/agentic/standalone-guide.md +97 -0
  4. package/build.mjs +10 -6
  5. package/dist/backend/AppShell.js +15 -2
  6. package/dist/backend/AppShell.js.map +2 -2
  7. package/dist/backend/DataTable.js +22 -1
  8. package/dist/backend/DataTable.js.map +2 -2
  9. package/dist/backend/detail/CustomDataSection.js +1 -5
  10. package/dist/backend/detail/CustomDataSection.js.map +2 -2
  11. package/dist/backend/detail/InlineEditors.js +2 -5
  12. package/dist/backend/detail/InlineEditors.js.map +2 -2
  13. package/dist/backend/detail/NotesSection.js +2 -6
  14. package/dist/backend/detail/NotesSection.js.map +2 -2
  15. package/dist/backend/icons/lucideRegistry.generated.js +93 -3
  16. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  17. package/dist/backend/markdown/MarkdownContent.js +47 -4
  18. package/dist/backend/markdown/MarkdownContent.js.map +2 -2
  19. package/dist/portal/PortalShell.js +41 -11
  20. package/dist/portal/PortalShell.js.map +2 -2
  21. package/dist/portal/hooks/usePortalDashboardWidgets.js +40 -1
  22. package/dist/portal/hooks/usePortalDashboardWidgets.js.map +2 -2
  23. package/dist/portal/utils/nav.js +84 -0
  24. package/dist/portal/utils/nav.js.map +7 -0
  25. package/package.json +13 -9
  26. package/src/backend/AppShell.tsx +22 -2
  27. package/src/backend/DataTable.tsx +28 -5
  28. package/src/backend/__tests__/AppShell.test.tsx +67 -0
  29. package/src/backend/__tests__/FormHeader.test.tsx +0 -1
  30. package/src/backend/detail/CustomDataSection.tsx +1 -10
  31. package/src/backend/detail/InlineEditors.tsx +3 -15
  32. package/src/backend/detail/NotesSection.tsx +5 -14
  33. package/src/backend/icons/lucideRegistry.generated.tsx +93 -3
  34. package/src/backend/injection/__tests__/resolveInjectedIcon.test.tsx +7 -0
  35. package/src/backend/markdown/MarkdownContent.tsx +76 -6
  36. package/src/backend/section-page/types.ts +1 -0
  37. package/src/portal/PortalShell.tsx +43 -11
  38. package/src/portal/hooks/__tests__/usePortalDashboardWidgets.test.tsx +117 -0
  39. package/src/portal/hooks/usePortalDashboardWidgets.ts +55 -1
  40. package/src/portal/utils/__tests__/nav.test.ts +199 -0
  41. package/src/portal/utils/nav.ts +150 -0
@@ -0,0 +1,150 @@
1
+ import type { FrontendRouteManifestEntry } from '@open-mercato/shared/modules/registry'
2
+ import { hasAllFeatures } from '@open-mercato/shared/security/features'
3
+
4
+ export type PortalNavGroupId = 'main' | 'account'
5
+
6
+ export type PortalNavItem = {
7
+ id: string
8
+ label: string
9
+ labelKey?: string
10
+ href: string
11
+ icon?: string
12
+ order: number
13
+ }
14
+
15
+ export type PortalNavGroup = {
16
+ id: PortalNavGroupId
17
+ items: PortalNavItem[]
18
+ }
19
+
20
+ export type BuildPortalNavOptions = {
21
+ /** Route manifest to inspect (typically `getFrontendRouteManifests()`). */
22
+ routes: readonly FrontendRouteManifestEntry[]
23
+ /** Current customer org slug — substituted into `[orgSlug]` patterns. */
24
+ orgSlug: string
25
+ /** Feature strings granted to the current customer (may include wildcards). */
26
+ grantedFeatures: readonly string[]
27
+ /** If true, bypass feature checks (portal admin). Defaults to false. */
28
+ isPortalAdmin?: boolean
29
+ }
30
+
31
+ function isPortalPattern(pattern: string | undefined): pattern is string {
32
+ if (!pattern) return false
33
+ return pattern.startsWith('/[orgSlug]/portal/') || pattern === '/[orgSlug]/portal'
34
+ }
35
+
36
+ function hasNoUnresolvedParams(href: string): boolean {
37
+ return !href.includes('[')
38
+ }
39
+
40
+ function resolveHref(pattern: string, orgSlug: string): string {
41
+ return pattern.replace('[orgSlug]', orgSlug)
42
+ }
43
+
44
+ function pickGroup(group: unknown): PortalNavGroupId {
45
+ if (group === 'main' || group === 'account') return group
46
+ return 'main'
47
+ }
48
+
49
+ /**
50
+ * Build the portal sidebar from the frontend route manifest.
51
+ *
52
+ * Mirrors `buildAdminNav()` for the portal surface: selects routes under
53
+ * `/[orgSlug]/portal/*` that declare a `nav` block, applies
54
+ * `requireCustomerFeatures` against the caller's grants (wildcards honored),
55
+ * and returns ordered sidebar groups.
56
+ *
57
+ * Absence of `nav` on a metadata file means the page is routable but not
58
+ * auto-listed — useful for detail/create pages.
59
+ */
60
+ export function buildPortalNav({
61
+ routes,
62
+ orgSlug,
63
+ grantedFeatures,
64
+ isPortalAdmin = false,
65
+ }: BuildPortalNavOptions): PortalNavGroup[] {
66
+ const mainItems: PortalNavItem[] = []
67
+ const accountItems: PortalNavItem[] = []
68
+
69
+ for (const route of routes) {
70
+ const pattern = route.pattern ?? route.path
71
+ if (!isPortalPattern(pattern)) continue
72
+ if (route.navHidden) continue
73
+ const nav = route.nav
74
+ if (!nav || typeof nav.label !== 'string' || nav.label.length === 0) continue
75
+
76
+ const requireFeatures = route.requireCustomerFeatures ?? []
77
+ if (!isPortalAdmin && requireFeatures.length) {
78
+ if (!hasAllFeatures(grantedFeatures as string[], requireFeatures as string[])) continue
79
+ }
80
+
81
+ const href = resolveHref(pattern as string, orgSlug)
82
+ if (!hasNoUnresolvedParams(href)) continue
83
+
84
+ const group = pickGroup(nav.group)
85
+ const item: PortalNavItem = {
86
+ id: `portal-nav:${pattern}`,
87
+ label: nav.label,
88
+ labelKey: nav.labelKey,
89
+ href,
90
+ icon: nav.icon,
91
+ order: typeof nav.order === 'number' ? nav.order : 100,
92
+ }
93
+ if (group === 'account') accountItems.push(item)
94
+ else mainItems.push(item)
95
+ }
96
+
97
+ const sortItems = (items: PortalNavItem[]) =>
98
+ items.sort((a, b) => {
99
+ if (a.order !== b.order) return a.order - b.order
100
+ return a.label.localeCompare(b.label)
101
+ })
102
+
103
+ sortItems(mainItems)
104
+ sortItems(accountItems)
105
+
106
+ const groups: PortalNavGroup[] = []
107
+ if (mainItems.length) groups.push({ id: 'main', items: mainItems })
108
+ if (accountItems.length) groups.push({ id: 'account', items: accountItems })
109
+ return groups
110
+ }
111
+
112
+ /**
113
+ * Merge sidebar groups from the portal nav endpoint with items contributed via
114
+ * `usePortalInjectedMenuItems`. Auto-discovered entries take precedence —
115
+ * injected items with matching `id` or `href` are dropped as duplicates.
116
+ */
117
+ export function mergePortalSidebarGroupsWithInjected<TInjected extends { id: string; href?: string }>(
118
+ discovered: readonly PortalNavGroup[],
119
+ injected: {
120
+ main: readonly TInjected[]
121
+ account: readonly TInjected[]
122
+ },
123
+ ): {
124
+ main: Array<PortalNavItem | TInjected>
125
+ account: Array<PortalNavItem | TInjected>
126
+ } {
127
+ const mergeGroup = <T extends PortalNavItem | TInjected>(
128
+ base: readonly PortalNavItem[],
129
+ extra: readonly TInjected[],
130
+ ): Array<PortalNavItem | TInjected> => {
131
+ const knownIds = new Set(base.map((item) => item.id))
132
+ const knownHrefs = new Set(base.map((item) => item.href).filter((href): href is string => Boolean(href)))
133
+ const merged: Array<PortalNavItem | TInjected> = [...base]
134
+ for (const item of extra) {
135
+ if (knownIds.has(item.id)) continue
136
+ if (item.href && knownHrefs.has(item.href)) continue
137
+ merged.push(item)
138
+ knownIds.add(item.id)
139
+ if (item.href) knownHrefs.add(item.href)
140
+ }
141
+ return merged
142
+ }
143
+
144
+ const mainBase = discovered.find((g) => g.id === 'main')?.items ?? []
145
+ const accountBase = discovered.find((g) => g.id === 'account')?.items ?? []
146
+ return {
147
+ main: mergeGroup(mainBase, injected.main),
148
+ account: mergeGroup(accountBase, injected.account),
149
+ }
150
+ }