@open-mercato/core 0.6.7-develop.6788.1.c2a1520a30 → 0.6.7-develop.6795.1.8a3f27921c

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 (190) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities.ids.generated.js +1 -0
  3. package/dist/generated/entities.ids.generated.js.map +2 -2
  4. package/dist/modules/customers/cli.js +17 -7
  5. package/dist/modules/customers/cli.js.map +2 -2
  6. package/dist/modules/customers/search.js +50 -29
  7. package/dist/modules/customers/search.js.map +2 -2
  8. package/dist/modules/data_sync/api/mappings/[id]/route.js +22 -9
  9. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
  10. package/dist/modules/data_sync/api/mappings/route.js +13 -4
  11. package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
  12. package/dist/modules/data_sync/api/options.js +7 -2
  13. package/dist/modules/data_sync/api/options.js.map +2 -2
  14. package/dist/modules/data_sync/api/run.js +7 -2
  15. package/dist/modules/data_sync/api/run.js.map +2 -2
  16. package/dist/modules/data_sync/api/runs/[id]/cancel.js +8 -3
  17. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  18. package/dist/modules/data_sync/api/runs/[id]/retry.js +7 -2
  19. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  20. package/dist/modules/data_sync/api/runs/[id]/route.js +8 -3
  21. package/dist/modules/data_sync/api/runs/[id]/route.js.map +2 -2
  22. package/dist/modules/data_sync/api/runs.js +7 -2
  23. package/dist/modules/data_sync/api/runs.js.map +2 -2
  24. package/dist/modules/data_sync/api/schedules/[id]/route.js +19 -6
  25. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  26. package/dist/modules/data_sync/api/schedules/route.js +13 -4
  27. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  28. package/dist/modules/data_sync/api/validate.js +9 -4
  29. package/dist/modules/data_sync/api/validate.js.map +2 -2
  30. package/dist/modules/design_system/acl.js +9 -0
  31. package/dist/modules/design_system/acl.js.map +7 -0
  32. package/dist/modules/design_system/backend/design-system/page.js +11 -0
  33. package/dist/modules/design_system/backend/design-system/page.js.map +7 -0
  34. package/dist/modules/design_system/backend/design-system/page.meta.js +15 -0
  35. package/dist/modules/design_system/backend/design-system/page.meta.js.map +7 -0
  36. package/dist/modules/design_system/gallery/components/CodeSnippet.js +52 -0
  37. package/dist/modules/design_system/gallery/components/CodeSnippet.js.map +7 -0
  38. package/dist/modules/design_system/gallery/components/EntryCard.js +63 -0
  39. package/dist/modules/design_system/gallery/components/EntryCard.js.map +7 -0
  40. package/dist/modules/design_system/gallery/components/GalleryShell.js +163 -0
  41. package/dist/modules/design_system/gallery/components/GalleryShell.js.map +7 -0
  42. package/dist/modules/design_system/gallery/components/VariantPreview.js +9 -0
  43. package/dist/modules/design_system/gallery/components/VariantPreview.js.map +7 -0
  44. package/dist/modules/design_system/gallery/entries/banners.js +186 -0
  45. package/dist/modules/design_system/gallery/entries/banners.js.map +7 -0
  46. package/dist/modules/design_system/gallery/entries/buttons.js +361 -0
  47. package/dist/modules/design_system/gallery/entries/buttons.js.map +7 -0
  48. package/dist/modules/design_system/gallery/entries/charts.js +457 -0
  49. package/dist/modules/design_system/gallery/entries/charts.js.map +7 -0
  50. package/dist/modules/design_system/gallery/entries/dates.js +306 -0
  51. package/dist/modules/design_system/gallery/entries/dates.js.map +7 -0
  52. package/dist/modules/design_system/gallery/entries/detail.js +358 -0
  53. package/dist/modules/design_system/gallery/entries/detail.js.map +7 -0
  54. package/dist/modules/design_system/gallery/entries/display.js +785 -0
  55. package/dist/modules/design_system/gallery/entries/display.js.map +7 -0
  56. package/dist/modules/design_system/gallery/entries/feedback.js +658 -0
  57. package/dist/modules/design_system/gallery/entries/feedback.js.map +7 -0
  58. package/dist/modules/design_system/gallery/entries/filters.js +505 -0
  59. package/dist/modules/design_system/gallery/entries/filters.js.map +7 -0
  60. package/dist/modules/design_system/gallery/entries/foundations.js +453 -0
  61. package/dist/modules/design_system/gallery/entries/foundations.js.map +7 -0
  62. package/dist/modules/design_system/gallery/entries/icons.js +140 -0
  63. package/dist/modules/design_system/gallery/entries/icons.js.map +7 -0
  64. package/dist/modules/design_system/gallery/entries/inputs.js +1217 -0
  65. package/dist/modules/design_system/gallery/entries/inputs.js.map +7 -0
  66. package/dist/modules/design_system/gallery/entries/messages.js +334 -0
  67. package/dist/modules/design_system/gallery/entries/messages.js.map +7 -0
  68. package/dist/modules/design_system/gallery/entries/navigation.js +475 -0
  69. package/dist/modules/design_system/gallery/entries/navigation.js.map +7 -0
  70. package/dist/modules/design_system/gallery/entries/notifications.js +257 -0
  71. package/dist/modules/design_system/gallery/entries/notifications.js.map +7 -0
  72. package/dist/modules/design_system/gallery/entries/overlays.js +576 -0
  73. package/dist/modules/design_system/gallery/entries/overlays.js.map +7 -0
  74. package/dist/modules/design_system/gallery/entries/scaffolding.js +378 -0
  75. package/dist/modules/design_system/gallery/entries/scaffolding.js.map +7 -0
  76. package/dist/modules/design_system/gallery/entries/schedule.js +297 -0
  77. package/dist/modules/design_system/gallery/entries/schedule.js.map +7 -0
  78. package/dist/modules/design_system/gallery/registry.js +139 -0
  79. package/dist/modules/design_system/gallery/registry.js.map +7 -0
  80. package/dist/modules/design_system/gallery/types.js +1 -0
  81. package/dist/modules/design_system/gallery/types.js.map +7 -0
  82. package/dist/modules/design_system/index.js +9 -0
  83. package/dist/modules/design_system/index.js.map +7 -0
  84. package/dist/modules/design_system/setup.js +14 -0
  85. package/dist/modules/design_system/setup.js.map +7 -0
  86. package/dist/modules/inbox_ops/search.js +15 -4
  87. package/dist/modules/inbox_ops/search.js.map +2 -2
  88. package/dist/modules/integrations/api/[id]/credentials/route.js +11 -5
  89. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  90. package/dist/modules/integrations/api/[id]/health/route.js +6 -3
  91. package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
  92. package/dist/modules/integrations/api/[id]/route.js +6 -3
  93. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  94. package/dist/modules/integrations/api/[id]/state/route.js +6 -3
  95. package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
  96. package/dist/modules/integrations/api/[id]/version/route.js +6 -3
  97. package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
  98. package/dist/modules/integrations/api/logs/route.js +6 -3
  99. package/dist/modules/integrations/api/logs/route.js.map +2 -2
  100. package/dist/modules/integrations/api/route.js +6 -3
  101. package/dist/modules/integrations/api/route.js.map +2 -2
  102. package/dist/modules/integrations/api/umes-read.js +2 -2
  103. package/dist/modules/integrations/api/umes-read.js.map +2 -2
  104. package/dist/modules/integrations/lib/organization-scope.js +2 -9
  105. package/dist/modules/integrations/lib/organization-scope.js.map +2 -2
  106. package/dist/modules/messages/search.js +10 -5
  107. package/dist/modules/messages/search.js.map +2 -2
  108. package/dist/modules/query_index/data/entities.js +2 -1
  109. package/dist/modules/query_index/data/entities.js.map +2 -2
  110. package/dist/modules/query_index/lib/engine.js +32 -50
  111. package/dist/modules/query_index/lib/engine.js.map +2 -2
  112. package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js +17 -0
  113. package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js.map +7 -0
  114. package/generated/entities.ids.generated.ts +1 -0
  115. package/package.json +16 -16
  116. package/src/modules/customers/cli.ts +17 -7
  117. package/src/modules/customers/i18n/de.json +17 -0
  118. package/src/modules/customers/i18n/en.json +17 -0
  119. package/src/modules/customers/i18n/es.json +17 -0
  120. package/src/modules/customers/i18n/pl.json +17 -0
  121. package/src/modules/customers/search.ts +59 -27
  122. package/src/modules/data_sync/AGENTS.md +1 -0
  123. package/src/modules/data_sync/api/mappings/[id]/route.ts +22 -9
  124. package/src/modules/data_sync/api/mappings/route.ts +13 -4
  125. package/src/modules/data_sync/api/options.ts +7 -2
  126. package/src/modules/data_sync/api/run.ts +7 -2
  127. package/src/modules/data_sync/api/runs/[id]/cancel.ts +8 -3
  128. package/src/modules/data_sync/api/runs/[id]/retry.ts +7 -2
  129. package/src/modules/data_sync/api/runs/[id]/route.ts +8 -3
  130. package/src/modules/data_sync/api/runs.ts +7 -2
  131. package/src/modules/data_sync/api/schedules/[id]/route.ts +19 -6
  132. package/src/modules/data_sync/api/schedules/route.ts +13 -4
  133. package/src/modules/data_sync/api/validate.ts +9 -4
  134. package/src/modules/design_system/AGENTS.md +34 -0
  135. package/src/modules/design_system/acl.ts +5 -0
  136. package/src/modules/design_system/backend/design-system/page.meta.ts +11 -0
  137. package/src/modules/design_system/backend/design-system/page.tsx +14 -0
  138. package/src/modules/design_system/gallery/components/CodeSnippet.tsx +55 -0
  139. package/src/modules/design_system/gallery/components/EntryCard.tsx +95 -0
  140. package/src/modules/design_system/gallery/components/GalleryShell.tsx +206 -0
  141. package/src/modules/design_system/gallery/components/VariantPreview.tsx +15 -0
  142. package/src/modules/design_system/gallery/entries/banners.tsx +226 -0
  143. package/src/modules/design_system/gallery/entries/buttons.tsx +422 -0
  144. package/src/modules/design_system/gallery/entries/charts.tsx +518 -0
  145. package/src/modules/design_system/gallery/entries/dates.tsx +325 -0
  146. package/src/modules/design_system/gallery/entries/detail.tsx +403 -0
  147. package/src/modules/design_system/gallery/entries/display.tsx +893 -0
  148. package/src/modules/design_system/gallery/entries/feedback.tsx +746 -0
  149. package/src/modules/design_system/gallery/entries/filters.tsx +570 -0
  150. package/src/modules/design_system/gallery/entries/foundations.tsx +529 -0
  151. package/src/modules/design_system/gallery/entries/icons.tsx +168 -0
  152. package/src/modules/design_system/gallery/entries/inputs.tsx +1516 -0
  153. package/src/modules/design_system/gallery/entries/messages.tsx +339 -0
  154. package/src/modules/design_system/gallery/entries/navigation.tsx +573 -0
  155. package/src/modules/design_system/gallery/entries/notifications.tsx +285 -0
  156. package/src/modules/design_system/gallery/entries/overlays.tsx +677 -0
  157. package/src/modules/design_system/gallery/entries/scaffolding.tsx +430 -0
  158. package/src/modules/design_system/gallery/entries/schedule.tsx +307 -0
  159. package/src/modules/design_system/gallery/registry.ts +152 -0
  160. package/src/modules/design_system/gallery/types.ts +32 -0
  161. package/src/modules/design_system/i18n/.hardcoded-allowlist.json +71 -0
  162. package/src/modules/design_system/i18n/de.json +46 -0
  163. package/src/modules/design_system/i18n/en.json +46 -0
  164. package/src/modules/design_system/i18n/es.json +46 -0
  165. package/src/modules/design_system/i18n/pl.json +46 -0
  166. package/src/modules/design_system/index.ts +7 -0
  167. package/src/modules/design_system/setup.ts +12 -0
  168. package/src/modules/inbox_ops/i18n/de.json +3 -0
  169. package/src/modules/inbox_ops/i18n/en.json +3 -0
  170. package/src/modules/inbox_ops/i18n/es.json +3 -0
  171. package/src/modules/inbox_ops/i18n/pl.json +3 -0
  172. package/src/modules/inbox_ops/search.ts +20 -4
  173. package/src/modules/integrations/api/[id]/credentials/route.ts +11 -5
  174. package/src/modules/integrations/api/[id]/health/route.ts +6 -3
  175. package/src/modules/integrations/api/[id]/route.ts +6 -3
  176. package/src/modules/integrations/api/[id]/state/route.ts +6 -3
  177. package/src/modules/integrations/api/[id]/version/route.ts +6 -3
  178. package/src/modules/integrations/api/logs/route.ts +6 -3
  179. package/src/modules/integrations/api/route.ts +6 -3
  180. package/src/modules/integrations/api/umes-read.ts +2 -2
  181. package/src/modules/integrations/lib/organization-scope.ts +5 -25
  182. package/src/modules/messages/i18n/de.json +2 -0
  183. package/src/modules/messages/i18n/en.json +2 -0
  184. package/src/modules/messages/i18n/es.json +2 -0
  185. package/src/modules/messages/i18n/pl.json +2 -0
  186. package/src/modules/messages/search.ts +11 -5
  187. package/src/modules/query_index/data/entities.ts +7 -0
  188. package/src/modules/query_index/lib/engine.ts +40 -63
  189. package/src/modules/query_index/migrations/.snapshot-open-mercato.json +12 -0
  190. package/src/modules/query_index/migrations/Migration20260731105052_query_index.ts +35 -0
@@ -0,0 +1,55 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Copy } from 'lucide-react'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
7
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
8
+
9
+ export function CodeSnippet({ code }: { code: string }) {
10
+ const t = useT()
11
+ const copyLabel = t('design_system.gallery.copy', 'Copy code')
12
+
13
+ const onCopy = React.useCallback(async () => {
14
+ try {
15
+ if (navigator.clipboard) {
16
+ await navigator.clipboard.writeText(code)
17
+ } else {
18
+ // Non-secure contexts (plain-HTTP self-hosted installs) have no
19
+ // navigator.clipboard — fall back to the legacy selection API.
20
+ const holder = document.createElement('textarea')
21
+ holder.value = code
22
+ holder.setAttribute('readonly', '')
23
+ holder.style.position = 'fixed'
24
+ holder.style.opacity = '0'
25
+ document.body.appendChild(holder)
26
+ holder.select()
27
+ const copied = document.execCommand('copy')
28
+ holder.remove()
29
+ if (!copied) throw new Error('[internal] execCommand copy failed')
30
+ }
31
+ flash(t('design_system.gallery.copied', 'Snippet copied to clipboard'), 'success')
32
+ } catch {
33
+ flash(t('design_system.gallery.copyFailed', 'Could not copy the snippet'), 'error')
34
+ }
35
+ }, [code, t])
36
+
37
+ return (
38
+ <div className="relative rounded-md border border-border bg-muted/50">
39
+ <pre className="overflow-x-auto p-3 pr-12 text-xs leading-relaxed">
40
+ <code>{code}</code>
41
+ </pre>
42
+ <IconButton
43
+ type="button"
44
+ variant="ghost"
45
+ size="sm"
46
+ className="absolute right-1.5 top-1.5"
47
+ aria-label={copyLabel}
48
+ title={copyLabel}
49
+ onClick={onCopy}
50
+ >
51
+ <Copy />
52
+ </IconButton>
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Check, ExternalLink, X } from 'lucide-react'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { LinkButton } from '@open-mercato/ui/primitives/link-button'
7
+ import type { GalleryEntry } from '../types'
8
+ import { DS_DOCS_URL, figmaNodeUrl } from '../registry'
9
+ import { VariantPreview } from './VariantPreview'
10
+ import { CodeSnippet } from './CodeSnippet'
11
+
12
+ export function EntryCard({ entry }: { entry: GalleryEntry }) {
13
+ const t = useT()
14
+
15
+ return (
16
+ <section
17
+ id={`gallery-entry-${entry.id}`}
18
+ aria-labelledby={`gallery-entry-${entry.id}-title`}
19
+ className="scroll-mt-4 space-y-4 rounded-lg border border-border bg-card p-4"
20
+ >
21
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
22
+ <div className="min-w-0 space-y-1">
23
+ <h3 id={`gallery-entry-${entry.id}-title`} className="text-base font-semibold leading-tight">
24
+ {entry.title}
25
+ </h3>
26
+ {entry.descriptionKey ? (
27
+ <p className="text-sm text-muted-foreground">{t(entry.descriptionKey, entry.title)}</p>
28
+ ) : null}
29
+ <code className="block truncate text-xs text-muted-foreground">{entry.importPath}</code>
30
+ </div>
31
+ <div className="flex shrink-0 items-center gap-3">
32
+ {entry.figmaNodeId ? (
33
+ <LinkButton asChild size="sm">
34
+ <a href={figmaNodeUrl(entry.figmaNodeId)} target="_blank" rel="noreferrer">
35
+ {t('design_system.gallery.openInFigma', 'Open in Figma')}
36
+ <ExternalLink />
37
+ </a>
38
+ </LinkButton>
39
+ ) : null}
40
+ {entry.docsAnchor ? (
41
+ <LinkButton asChild size="sm" variant="gray">
42
+ <a href={`${DS_DOCS_URL}${entry.docsAnchor}`} target="_blank" rel="noreferrer">
43
+ {t('design_system.gallery.viewDocs', 'View docs')}
44
+ <ExternalLink />
45
+ </a>
46
+ </LinkButton>
47
+ ) : null}
48
+ </div>
49
+ </div>
50
+ {entry.usage && (entry.usage.do?.length || entry.usage.dont?.length) ? (
51
+ <div className="grid gap-4 rounded-md border border-border bg-muted/30 p-4 sm:grid-cols-2">
52
+ {entry.usage.do?.length ? (
53
+ <div className="space-y-1.5">
54
+ <div className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
55
+ {t('design_system.gallery.usageDo', 'Do')}
56
+ </div>
57
+ <ul className="space-y-1.5">
58
+ {entry.usage.do.map((rule) => (
59
+ <li key={rule} className="flex items-start gap-2 text-sm text-foreground">
60
+ <Check aria-hidden className="mt-0.5 size-3.5 shrink-0 text-status-success-icon" />
61
+ <span>{rule}</span>
62
+ </li>
63
+ ))}
64
+ </ul>
65
+ </div>
66
+ ) : null}
67
+ {entry.usage.dont?.length ? (
68
+ <div className="space-y-1.5">
69
+ <div className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
70
+ {t('design_system.gallery.usageDont', "Don't")}
71
+ </div>
72
+ <ul className="space-y-1.5">
73
+ {entry.usage.dont.map((rule) => (
74
+ <li key={rule} className="flex items-start gap-2 text-sm text-foreground">
75
+ <X aria-hidden className="mt-0.5 size-3.5 shrink-0 text-status-error-icon" />
76
+ <span>{rule}</span>
77
+ </li>
78
+ ))}
79
+ </ul>
80
+ </div>
81
+ ) : null}
82
+ </div>
83
+ ) : null}
84
+ <div className="space-y-4">
85
+ {entry.variants.map((variant) => (
86
+ <div key={variant.id} className="space-y-2">
87
+ <div className="text-sm font-medium text-foreground">{variant.title}</div>
88
+ <VariantPreview>{variant.render()}</VariantPreview>
89
+ <CodeSnippet code={variant.code} />
90
+ </div>
91
+ ))}
92
+ </div>
93
+ </section>
94
+ )
95
+ }
@@ -0,0 +1,206 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { SectionPage } from '@open-mercato/ui/backend/section-page'
7
+ import type { SectionNavGroup } from '@open-mercato/ui/backend/section-page'
8
+ import { SearchInput } from '@open-mercato/ui/primitives/search-input'
9
+ import { Skeleton } from '@open-mercato/ui/primitives/skeleton'
10
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
11
+ import { Button } from '@open-mercato/ui/primitives/button'
12
+ import { ErrorMessage } from '@open-mercato/ui/backend/detail'
13
+ import type { GalleryEntry, GalleryFamily } from '../types'
14
+ import { GALLERY_BASE_PATH, galleryFamilies } from '../registry'
15
+ import { EntryCard } from './EntryCard'
16
+
17
+ /** 'buttons' → 'Buttons' — untranslated fallback when a family labelKey has no message. */
18
+ function familyLabelFallback(id: string): string {
19
+ return id.charAt(0).toUpperCase() + id.slice(1).replace(/[-_]/g, ' ')
20
+ }
21
+
22
+ function matchesQuery(entry: GalleryEntry, query: string): boolean {
23
+ const needle = query.trim().toLowerCase()
24
+ if (!needle) return true
25
+ if (entry.id.toLowerCase().includes(needle) || entry.title.toLowerCase().includes(needle)) return true
26
+ if (entry.keywords?.some((keyword) => keyword.toLowerCase().includes(needle))) return true
27
+ // 'radiobutton' should still find 'radio' — match when the query CONTAINS the id/title too.
28
+ return needle.includes(entry.id.toLowerCase()) || needle.includes(entry.title.toLowerCase())
29
+ }
30
+
31
+ type LoadedEntries = Record<string, GalleryEntry[]>
32
+
33
+ export function GalleryShell() {
34
+ const t = useT()
35
+ const searchParams = useSearchParams()
36
+ const [loaded, setLoaded] = React.useState<LoadedEntries>({})
37
+ const [failed, setFailed] = React.useState<Record<string, boolean>>({})
38
+ const [search, setSearch] = React.useState('')
39
+ const pendingRef = React.useRef(new Set<string>())
40
+
41
+ const familyParam = searchParams?.get('family') ?? null
42
+ const entryParam = searchParams?.get('entry') ?? null
43
+
44
+ // Clicking a family in the section nav exits search mode — otherwise the
45
+ // query keeps precedence and family navigation appears dead.
46
+ React.useEffect(() => {
47
+ setSearch('')
48
+ }, [familyParam])
49
+ const activeFamily: GalleryFamily | undefined =
50
+ galleryFamilies.find((family) => family.id === familyParam) ?? galleryFamilies[0]
51
+ const activeFamilyId = activeFamily?.id ?? null
52
+
53
+ const loadFamily = React.useCallback((family: GalleryFamily) => {
54
+ if (pendingRef.current.has(family.id)) return
55
+ pendingRef.current.add(family.id)
56
+ setFailed((prev) => (prev[family.id] ? { ...prev, [family.id]: false } : prev))
57
+ family
58
+ .load()
59
+ .then((mod) => {
60
+ setLoaded((prev) => (prev[family.id] ? prev : { ...prev, [family.id]: mod.entries }))
61
+ })
62
+ .catch(() => {
63
+ pendingRef.current.delete(family.id)
64
+ setFailed((prev) => ({ ...prev, [family.id]: true }))
65
+ })
66
+ }, [])
67
+
68
+ // Lazy-load the active family; when searching, load every family so results
69
+ // span the whole gallery.
70
+ const searching = search.trim().length > 0
71
+ React.useEffect(() => {
72
+ if (searching) {
73
+ for (const family of galleryFamilies) loadFamily(family)
74
+ } else if (activeFamily) {
75
+ loadFamily(activeFamily)
76
+ }
77
+ }, [searching, activeFamily, loadFamily])
78
+
79
+ // Deep link: ?entry=<id> scrolls the entry into view once its family loaded.
80
+ const activeEntries = activeFamilyId ? loaded[activeFamilyId] : undefined
81
+ React.useEffect(() => {
82
+ if (!entryParam || searching || !activeEntries) return
83
+ const node = document.getElementById(`gallery-entry-${entryParam}`)
84
+ node?.scrollIntoView({ block: 'start' })
85
+ }, [entryParam, searching, activeEntries])
86
+
87
+ const sections: SectionNavGroup[] = React.useMemo(
88
+ () => [
89
+ {
90
+ id: 'families',
91
+ label: 'Families',
92
+ labelKey: 'design_system.gallery.familiesGroup',
93
+ items: galleryFamilies.map((family) => ({
94
+ id: family.id,
95
+ label: familyLabelFallback(family.id),
96
+ labelKey: family.labelKey,
97
+ icon: family.icon,
98
+ href: `${GALLERY_BASE_PATH}?family=${family.id}`,
99
+ })),
100
+ },
101
+ ],
102
+ [],
103
+ )
104
+
105
+ const renderFamilySkeleton = () => (
106
+ <div className="space-y-4" data-testid="gallery-family-skeleton">
107
+ <Skeleton className="h-40 w-full" />
108
+ <Skeleton className="h-40 w-full" />
109
+ </div>
110
+ )
111
+
112
+ const renderSearchResults = () => {
113
+ const groups = galleryFamilies
114
+ .map((family) => ({
115
+ family,
116
+ entries: (loaded[family.id] ?? []).filter((entry) => matchesQuery(entry, search)),
117
+ }))
118
+ .filter((group) => group.entries.length > 0)
119
+ const stillLoading = galleryFamilies.some((family) => !loaded[family.id] && !failed[family.id])
120
+ if (groups.length === 0 && stillLoading) return renderFamilySkeleton()
121
+ if (groups.length === 0) {
122
+ return (
123
+ <EmptyState
124
+ title={t('design_system.gallery.noResults', 'No components match your search')}
125
+ />
126
+ )
127
+ }
128
+ return (
129
+ <div className="space-y-8">
130
+ {groups.map(({ family, entries }) => (
131
+ <div key={family.id} className="space-y-4">
132
+ <h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
133
+ {t(family.labelKey, familyLabelFallback(family.id))}
134
+ </h2>
135
+ {entries.map((entry) => (
136
+ <EntryCard key={entry.id} entry={entry} />
137
+ ))}
138
+ </div>
139
+ ))}
140
+ </div>
141
+ )
142
+ }
143
+
144
+ const renderActiveFamily = () => {
145
+ if (!activeFamily) return null
146
+ const entries = loaded[activeFamily.id]
147
+ if (!entries && failed[activeFamily.id]) {
148
+ return (
149
+ <ErrorMessage
150
+ label={t('design_system.gallery.loadFailed', 'Could not load this family')}
151
+ action={
152
+ <Button type="button" variant="outline" size="sm" onClick={() => loadFamily(activeFamily)}>
153
+ {t('design_system.gallery.retry', 'Retry')}
154
+ </Button>
155
+ }
156
+ />
157
+ )
158
+ }
159
+ if (!entries) return renderFamilySkeleton()
160
+ return (
161
+ <div className="space-y-4">
162
+ <h2 className="text-lg font-semibold">
163
+ {t(activeFamily.labelKey, familyLabelFallback(activeFamily.id))}
164
+ </h2>
165
+ {entries.length > 1 ? (
166
+ <nav
167
+ aria-label={t('design_system.gallery.familyContents', 'Components in this family')}
168
+ className="flex flex-wrap items-center gap-1.5 rounded-md border border-border bg-muted/30 p-2"
169
+ >
170
+ {entries.map((entry) => (
171
+ <a
172
+ key={entry.id}
173
+ href={`#gallery-entry-${entry.id}`}
174
+ className="rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:shadow-focus"
175
+ >
176
+ {entry.title}
177
+ </a>
178
+ ))}
179
+ </nav>
180
+ ) : null}
181
+ {entries.map((entry) => (
182
+ <EntryCard key={entry.id} entry={entry} />
183
+ ))}
184
+ </div>
185
+ )
186
+ }
187
+
188
+ return (
189
+ <SectionPage
190
+ title="Design system"
191
+ titleKey="design_system.nav.title"
192
+ sections={sections}
193
+ activePath={activeFamilyId ? `${GALLERY_BASE_PATH}?family=${activeFamilyId}` : GALLERY_BASE_PATH}
194
+ >
195
+ <div className="mx-auto w-full max-w-5xl space-y-6">
196
+ <SearchInput
197
+ value={search}
198
+ onChange={setSearch}
199
+ placeholder={t('design_system.gallery.searchPlaceholder', 'Search components…')}
200
+ aria-label={t('design_system.gallery.searchPlaceholder', 'Search components…')}
201
+ />
202
+ {searching ? renderSearchResults() : renderActiveFamily()}
203
+ </div>
204
+ </SectionPage>
205
+ )
206
+ }
@@ -0,0 +1,15 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ /**
6
+ * Bordered stage a variant renders inside. Chrome uses semantic tokens only —
7
+ * the stage must never impose colors of its own on the previewed component.
8
+ */
9
+ export function VariantPreview({ children }: { children: React.ReactNode }) {
10
+ return (
11
+ <div className="flex flex-wrap items-center gap-3 rounded-md border border-border bg-background p-4">
12
+ {children}
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,226 @@
1
+ import * as React from 'react'
2
+ import { CircleCheck, Rocket, Sparkles } from 'lucide-react'
3
+ import { Button } from '@open-mercato/ui/primitives/button'
4
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
5
+ import { NextStepCallout } from '@open-mercato/ui/backend/NextStepCallout'
6
+ import { ContextHelp } from '@open-mercato/ui/backend/ContextHelp'
7
+ import type { GalleryEntry } from '../types'
8
+
9
+ // Component titles and variant names are proper nouns from the codebase and
10
+ // are deliberately not translated. `code` MUST contain the entry's importPath
11
+ // (enforced by the registry-integrity test) and is always reviewed alongside
12
+ // its sibling `render`.
13
+
14
+ // `FlashMessages` itself is a singleton mounted by AppShell — the gallery only
15
+ // demonstrates the imperative `flash()` API; the triggered toast renders
16
+ // through the already-mounted singleton.
17
+ const flashMessagesEntry: GalleryEntry = {
18
+ id: 'flash-messages',
19
+ title: 'FlashMessages',
20
+ importPath: '@open-mercato/ui/backend/FlashMessages',
21
+ variants: [
22
+ {
23
+ id: 'kinds',
24
+ title: 'flash() kinds (toast via the AppShell singleton)',
25
+ render: () => (
26
+ <>
27
+ <Button variant="outline" onClick={() => flash('Changes saved', 'success')}>
28
+ Fire success
29
+ </Button>
30
+ <Button variant="outline" onClick={() => flash('Sync scheduled', 'info')}>
31
+ Fire info
32
+ </Button>
33
+ <Button variant="outline" onClick={() => flash('Quota almost reached', 'warning')}>
34
+ Fire warning
35
+ </Button>
36
+ <Button variant="outline" onClick={() => flash('Save failed', 'error')}>
37
+ Fire error
38
+ </Button>
39
+ </>
40
+ ),
41
+ code: `import { flash } from '@open-mercato/ui/backend/FlashMessages'
42
+
43
+ // FlashMessages is mounted once by AppShell — never mount your own.
44
+ flash('Changes saved', 'success')
45
+ flash('Sync scheduled', 'info')
46
+ flash('Quota almost reached', 'warning')
47
+ flash('Save failed', 'error')`,
48
+ },
49
+ ],
50
+ }
51
+
52
+ const nextStepCalloutEntry: GalleryEntry = {
53
+ id: 'next-step-callout',
54
+ title: 'NextStepCallout',
55
+ importPath: '@open-mercato/ui/backend/NextStepCallout',
56
+ variants: [
57
+ {
58
+ id: 'basic',
59
+ title: 'Basic',
60
+ render: () => (
61
+ <div className="w-full">
62
+ <NextStepCallout
63
+ icon={<Rocket className="size-6" />}
64
+ title="Publish your catalog"
65
+ description="Products are imported and priced. Publishing makes them visible on every sales channel."
66
+ actionLabel="Publish catalog"
67
+ actionIcon={<Sparkles className="mr-2 size-4" />}
68
+ onAction={() => {}}
69
+ />
70
+ </div>
71
+ ),
72
+ code: `import { Rocket, Sparkles } from 'lucide-react'
73
+ import { NextStepCallout } from '@open-mercato/ui/backend/NextStepCallout'
74
+
75
+ <NextStepCallout
76
+ icon={<Rocket className="size-6" />}
77
+ title="Publish your catalog"
78
+ description="Products are imported and priced. Publishing makes them visible on every sales channel."
79
+ actionLabel="Publish catalog"
80
+ actionIcon={<Sparkles className="mr-2 size-4" />}
81
+ onAction={handlePublish}
82
+ />`,
83
+ },
84
+ {
85
+ id: 'with-steps',
86
+ title: 'With steps',
87
+ render: () => (
88
+ <div className="w-full">
89
+ <NextStepCallout
90
+ title="Finish store setup"
91
+ description="Two steps are done — connect a payment provider to start selling."
92
+ steps={[
93
+ { id: 'import', label: 'Import products', state: 'completed' },
94
+ { id: 'pricing', label: 'Set pricing', state: 'completed' },
95
+ { id: 'payments', label: 'Connect payments', state: 'active' },
96
+ { id: 'launch', label: 'Launch', state: 'pending' },
97
+ ]}
98
+ actionLabel="Connect payments"
99
+ onAction={() => {}}
100
+ />
101
+ </div>
102
+ ),
103
+ code: `import { NextStepCallout } from '@open-mercato/ui/backend/NextStepCallout'
104
+
105
+ <NextStepCallout
106
+ title="Finish store setup"
107
+ description="Two steps are done — connect a payment provider to start selling."
108
+ steps={[
109
+ { id: 'import', label: 'Import products', state: 'completed' },
110
+ { id: 'pricing', label: 'Set pricing', state: 'completed' },
111
+ { id: 'payments', label: 'Connect payments', state: 'active' },
112
+ { id: 'launch', label: 'Launch', state: 'pending' },
113
+ ]}
114
+ actionLabel="Connect payments"
115
+ onAction={handleConnect}
116
+ />`,
117
+ },
118
+ {
119
+ id: 'with-status',
120
+ title: 'With status progress',
121
+ render: () => (
122
+ <div className="w-full">
123
+ <NextStepCallout
124
+ title="Index your products"
125
+ description="Search stays disabled until the first indexing run completes."
126
+ actionLabel="Re-run indexing"
127
+ onAction={() => {}}
128
+ busy
129
+ status={{
130
+ tone: 'info',
131
+ icon: <CircleCheck className="size-4" />,
132
+ label: 'Indexing in progress',
133
+ progressValue: 64,
134
+ progressDescription: '1,280 of 2,000 products indexed',
135
+ }}
136
+ />
137
+ </div>
138
+ ),
139
+ code: `import { CircleCheck } from 'lucide-react'
140
+ import { NextStepCallout } from '@open-mercato/ui/backend/NextStepCallout'
141
+
142
+ <NextStepCallout
143
+ title="Index your products"
144
+ description="Search stays disabled until the first indexing run completes."
145
+ actionLabel="Re-run indexing"
146
+ onAction={handleReindex}
147
+ busy
148
+ status={{
149
+ tone: 'info',
150
+ icon: <CircleCheck className="size-4" />,
151
+ label: 'Indexing in progress',
152
+ progressValue: 64,
153
+ progressDescription: '1,280 of 2,000 products indexed',
154
+ }}
155
+ />`,
156
+ },
157
+ ],
158
+ }
159
+
160
+ const contextHelpEntry: GalleryEntry = {
161
+ id: 'context-help',
162
+ title: 'ContextHelp',
163
+ importPath: '@open-mercato/ui/backend/ContextHelp',
164
+ variants: [
165
+ {
166
+ id: 'default',
167
+ title: 'default (collapsed)',
168
+ render: () => (
169
+ <div className="w-full max-w-md">
170
+ <ContextHelp title="How do exchange rates work?">
171
+ Rates refresh nightly from the configured provider. Manual overrides
172
+ stay pinned until you clear them.
173
+ </ContextHelp>
174
+ </div>
175
+ ),
176
+ code: `import { ContextHelp } from '@open-mercato/ui/backend/ContextHelp'
177
+
178
+ <ContextHelp title="How do exchange rates work?">
179
+ Rates refresh nightly from the configured provider. Manual overrides
180
+ stay pinned until you clear them.
181
+ </ContextHelp>`,
182
+ },
183
+ {
184
+ id: 'default-open',
185
+ title: 'defaultOpen',
186
+ render: () => (
187
+ <div className="w-full max-w-md">
188
+ <ContextHelp title="Why is this field required?" defaultOpen>
189
+ The tax office identifier is mandatory for invoices issued to
190
+ companies registered in the EU.
191
+ </ContextHelp>
192
+ </div>
193
+ ),
194
+ code: `import { ContextHelp } from '@open-mercato/ui/backend/ContextHelp'
195
+
196
+ <ContextHelp title="Why is this field required?" defaultOpen>
197
+ The tax office identifier is mandatory for invoices issued to
198
+ companies registered in the EU.
199
+ </ContextHelp>`,
200
+ },
201
+ {
202
+ id: 'info-icon',
203
+ title: 'Info icon (bulb={false})',
204
+ render: () => (
205
+ <div className="w-full max-w-md">
206
+ <ContextHelp title="About draft orders" bulb={false} defaultOpen>
207
+ Draft orders are invisible to customers and skip stock reservation
208
+ until they are confirmed.
209
+ </ContextHelp>
210
+ </div>
211
+ ),
212
+ code: `import { ContextHelp } from '@open-mercato/ui/backend/ContextHelp'
213
+
214
+ <ContextHelp title="About draft orders" bulb={false} defaultOpen>
215
+ Draft orders are invisible to customers and skip stock reservation
216
+ until they are confirmed.
217
+ </ContextHelp>`,
218
+ },
219
+ ],
220
+ }
221
+
222
+ export const entries: GalleryEntry[] = [
223
+ flashMessagesEntry,
224
+ nextStepCalloutEntry,
225
+ contextHelpEntry,
226
+ ]