@open-mercato/core 0.6.7-develop.6785.1.1dd7cfac55 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/modules/auth/backend/settings/page.js +14 -9
- package/dist/modules/auth/backend/settings/page.js.map +2 -2
- package/dist/modules/customers/cli.js +17 -7
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +229 -216
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/search.js +50 -29
- package/dist/modules/customers/search.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/[id]/route.js +22 -9
- package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/route.js +13 -4
- package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
- package/dist/modules/data_sync/api/options.js +7 -2
- package/dist/modules/data_sync/api/options.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +7 -2
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/cancel.js +8 -3
- package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js +7 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/route.js +8 -3
- package/dist/modules/data_sync/api/runs/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/runs.js +7 -2
- package/dist/modules/data_sync/api/runs.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +19 -6
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +13 -4
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/data_sync/api/validate.js +9 -4
- package/dist/modules/data_sync/api/validate.js.map +2 -2
- package/dist/modules/design_system/acl.js +9 -0
- package/dist/modules/design_system/acl.js.map +7 -0
- package/dist/modules/design_system/backend/design-system/page.js +11 -0
- package/dist/modules/design_system/backend/design-system/page.js.map +7 -0
- package/dist/modules/design_system/backend/design-system/page.meta.js +15 -0
- package/dist/modules/design_system/backend/design-system/page.meta.js.map +7 -0
- package/dist/modules/design_system/gallery/components/CodeSnippet.js +52 -0
- package/dist/modules/design_system/gallery/components/CodeSnippet.js.map +7 -0
- package/dist/modules/design_system/gallery/components/EntryCard.js +63 -0
- package/dist/modules/design_system/gallery/components/EntryCard.js.map +7 -0
- package/dist/modules/design_system/gallery/components/GalleryShell.js +163 -0
- package/dist/modules/design_system/gallery/components/GalleryShell.js.map +7 -0
- package/dist/modules/design_system/gallery/components/VariantPreview.js +9 -0
- package/dist/modules/design_system/gallery/components/VariantPreview.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/banners.js +186 -0
- package/dist/modules/design_system/gallery/entries/banners.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/buttons.js +361 -0
- package/dist/modules/design_system/gallery/entries/buttons.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/charts.js +457 -0
- package/dist/modules/design_system/gallery/entries/charts.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/dates.js +306 -0
- package/dist/modules/design_system/gallery/entries/dates.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/detail.js +358 -0
- package/dist/modules/design_system/gallery/entries/detail.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/display.js +785 -0
- package/dist/modules/design_system/gallery/entries/display.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/feedback.js +658 -0
- package/dist/modules/design_system/gallery/entries/feedback.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/filters.js +505 -0
- package/dist/modules/design_system/gallery/entries/filters.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/foundations.js +453 -0
- package/dist/modules/design_system/gallery/entries/foundations.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/icons.js +140 -0
- package/dist/modules/design_system/gallery/entries/icons.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/inputs.js +1217 -0
- package/dist/modules/design_system/gallery/entries/inputs.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/messages.js +334 -0
- package/dist/modules/design_system/gallery/entries/messages.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/navigation.js +475 -0
- package/dist/modules/design_system/gallery/entries/navigation.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/notifications.js +257 -0
- package/dist/modules/design_system/gallery/entries/notifications.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/overlays.js +576 -0
- package/dist/modules/design_system/gallery/entries/overlays.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/scaffolding.js +378 -0
- package/dist/modules/design_system/gallery/entries/scaffolding.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/schedule.js +297 -0
- package/dist/modules/design_system/gallery/entries/schedule.js.map +7 -0
- package/dist/modules/design_system/gallery/registry.js +139 -0
- package/dist/modules/design_system/gallery/registry.js.map +7 -0
- package/dist/modules/design_system/gallery/types.js +1 -0
- package/dist/modules/design_system/gallery/types.js.map +7 -0
- package/dist/modules/design_system/index.js +9 -0
- package/dist/modules/design_system/index.js.map +7 -0
- package/dist/modules/design_system/setup.js +14 -0
- package/dist/modules/design_system/setup.js.map +7 -0
- package/dist/modules/inbox_ops/search.js +15 -4
- package/dist/modules/inbox_ops/search.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +11 -5
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/health/route.js +6 -3
- package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +6 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +6 -3
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +6 -3
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/logs/route.js +6 -3
- package/dist/modules/integrations/api/logs/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +6 -3
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/api/umes-read.js +2 -2
- package/dist/modules/integrations/api/umes-read.js.map +2 -2
- package/dist/modules/integrations/lib/organization-scope.js +2 -9
- package/dist/modules/integrations/lib/organization-scope.js.map +2 -2
- package/dist/modules/messages/search.js +10 -5
- package/dist/modules/messages/search.js.map +2 -2
- package/dist/modules/query_index/data/entities.js +2 -1
- package/dist/modules/query_index/data/entities.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +32 -50
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js +17 -0
- package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js.map +7 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/package.json +16 -16
- package/src/modules/auth/backend/settings/page.tsx +17 -8
- package/src/modules/customers/cli.ts +17 -7
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +24 -13
- package/src/modules/customers/i18n/de.json +17 -0
- package/src/modules/customers/i18n/en.json +17 -0
- package/src/modules/customers/i18n/es.json +17 -0
- package/src/modules/customers/i18n/pl.json +17 -0
- package/src/modules/customers/search.ts +59 -27
- package/src/modules/data_sync/AGENTS.md +1 -0
- package/src/modules/data_sync/api/mappings/[id]/route.ts +22 -9
- package/src/modules/data_sync/api/mappings/route.ts +13 -4
- package/src/modules/data_sync/api/options.ts +7 -2
- package/src/modules/data_sync/api/run.ts +7 -2
- package/src/modules/data_sync/api/runs/[id]/cancel.ts +8 -3
- package/src/modules/data_sync/api/runs/[id]/retry.ts +7 -2
- package/src/modules/data_sync/api/runs/[id]/route.ts +8 -3
- package/src/modules/data_sync/api/runs.ts +7 -2
- package/src/modules/data_sync/api/schedules/[id]/route.ts +19 -6
- package/src/modules/data_sync/api/schedules/route.ts +13 -4
- package/src/modules/data_sync/api/validate.ts +9 -4
- package/src/modules/design_system/AGENTS.md +34 -0
- package/src/modules/design_system/acl.ts +5 -0
- package/src/modules/design_system/backend/design-system/page.meta.ts +11 -0
- package/src/modules/design_system/backend/design-system/page.tsx +14 -0
- package/src/modules/design_system/gallery/components/CodeSnippet.tsx +55 -0
- package/src/modules/design_system/gallery/components/EntryCard.tsx +95 -0
- package/src/modules/design_system/gallery/components/GalleryShell.tsx +206 -0
- package/src/modules/design_system/gallery/components/VariantPreview.tsx +15 -0
- package/src/modules/design_system/gallery/entries/banners.tsx +226 -0
- package/src/modules/design_system/gallery/entries/buttons.tsx +422 -0
- package/src/modules/design_system/gallery/entries/charts.tsx +518 -0
- package/src/modules/design_system/gallery/entries/dates.tsx +325 -0
- package/src/modules/design_system/gallery/entries/detail.tsx +403 -0
- package/src/modules/design_system/gallery/entries/display.tsx +893 -0
- package/src/modules/design_system/gallery/entries/feedback.tsx +746 -0
- package/src/modules/design_system/gallery/entries/filters.tsx +570 -0
- package/src/modules/design_system/gallery/entries/foundations.tsx +529 -0
- package/src/modules/design_system/gallery/entries/icons.tsx +168 -0
- package/src/modules/design_system/gallery/entries/inputs.tsx +1516 -0
- package/src/modules/design_system/gallery/entries/messages.tsx +339 -0
- package/src/modules/design_system/gallery/entries/navigation.tsx +573 -0
- package/src/modules/design_system/gallery/entries/notifications.tsx +285 -0
- package/src/modules/design_system/gallery/entries/overlays.tsx +677 -0
- package/src/modules/design_system/gallery/entries/scaffolding.tsx +430 -0
- package/src/modules/design_system/gallery/entries/schedule.tsx +307 -0
- package/src/modules/design_system/gallery/registry.ts +152 -0
- package/src/modules/design_system/gallery/types.ts +32 -0
- package/src/modules/design_system/i18n/.hardcoded-allowlist.json +71 -0
- package/src/modules/design_system/i18n/de.json +46 -0
- package/src/modules/design_system/i18n/en.json +46 -0
- package/src/modules/design_system/i18n/es.json +46 -0
- package/src/modules/design_system/i18n/pl.json +46 -0
- package/src/modules/design_system/index.ts +7 -0
- package/src/modules/design_system/setup.ts +12 -0
- package/src/modules/inbox_ops/i18n/de.json +3 -0
- package/src/modules/inbox_ops/i18n/en.json +3 -0
- package/src/modules/inbox_ops/i18n/es.json +3 -0
- package/src/modules/inbox_ops/i18n/pl.json +3 -0
- package/src/modules/inbox_ops/search.ts +20 -4
- package/src/modules/integrations/api/[id]/credentials/route.ts +11 -5
- package/src/modules/integrations/api/[id]/health/route.ts +6 -3
- package/src/modules/integrations/api/[id]/route.ts +6 -3
- package/src/modules/integrations/api/[id]/state/route.ts +6 -3
- package/src/modules/integrations/api/[id]/version/route.ts +6 -3
- package/src/modules/integrations/api/logs/route.ts +6 -3
- package/src/modules/integrations/api/route.ts +6 -3
- package/src/modules/integrations/api/umes-read.ts +2 -2
- package/src/modules/integrations/lib/organization-scope.ts +5 -25
- package/src/modules/messages/i18n/de.json +2 -0
- package/src/modules/messages/i18n/en.json +2 -0
- package/src/modules/messages/i18n/es.json +2 -0
- package/src/modules/messages/i18n/pl.json +2 -0
- package/src/modules/messages/search.ts +11 -5
- package/src/modules/query_index/data/entities.ts +7 -0
- package/src/modules/query_index/lib/engine.ts +40 -63
- package/src/modules/query_index/migrations/.snapshot-open-mercato.json +12 -0
- package/src/modules/query_index/migrations/Migration20260731105052_query_index.ts +35 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
3
|
+
import { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'
|
|
3
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
5
|
import { listSyncRunsQuerySchema } from '../data/validators'
|
|
5
6
|
import type { SyncRunService } from '../lib/sync-run-service'
|
|
@@ -15,9 +16,13 @@ export const openApi = {
|
|
|
15
16
|
|
|
16
17
|
export async function GET(req: Request) {
|
|
17
18
|
const auth = await getAuthFromRequest(req)
|
|
18
|
-
if (!auth?.tenantId
|
|
19
|
+
if (!auth?.tenantId) {
|
|
19
20
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
20
21
|
}
|
|
22
|
+
const organizationId = resolveActiveOrganizationId(auth)
|
|
23
|
+
if (!organizationId) {
|
|
24
|
+
return organizationScopeRequiredResponse()
|
|
25
|
+
}
|
|
21
26
|
|
|
22
27
|
const url = new URL(req.url)
|
|
23
28
|
const parsed = listSyncRunsQuerySchema.safeParse({
|
|
@@ -38,7 +43,7 @@ export async function GET(req: Request) {
|
|
|
38
43
|
const syncRunService = container.resolve('dataSyncRunService') as SyncRunService
|
|
39
44
|
|
|
40
45
|
const { items, total } = await syncRunService.listRuns(parsed.data, {
|
|
41
|
-
organizationId
|
|
46
|
+
organizationId,
|
|
42
47
|
tenantId: auth.tenantId,
|
|
43
48
|
})
|
|
44
49
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
4
|
+
import { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'
|
|
4
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
6
|
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
6
7
|
import { readOptimisticLockExpected } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
@@ -30,9 +31,13 @@ export const openApi = {
|
|
|
30
31
|
|
|
31
32
|
export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
|
|
32
33
|
const auth = await getAuthFromRequest(req)
|
|
33
|
-
if (!auth?.tenantId
|
|
34
|
+
if (!auth?.tenantId) {
|
|
34
35
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
35
36
|
}
|
|
37
|
+
const organizationId = resolveActiveOrganizationId(auth)
|
|
38
|
+
if (!organizationId) {
|
|
39
|
+
return organizationScopeRequiredResponse()
|
|
40
|
+
}
|
|
36
41
|
|
|
37
42
|
const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')
|
|
38
43
|
? await (ctx.params as Promise<{ id?: string }>)
|
|
@@ -46,7 +51,7 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
46
51
|
const container = await createRequestContainer()
|
|
47
52
|
const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
|
|
48
53
|
const schedule = await scheduleService.getById(parsedParams.data.id, {
|
|
49
|
-
organizationId
|
|
54
|
+
organizationId,
|
|
50
55
|
tenantId: auth.tenantId,
|
|
51
56
|
})
|
|
52
57
|
|
|
@@ -59,9 +64,13 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
59
64
|
|
|
60
65
|
export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
|
|
61
66
|
const auth = await getAuthFromRequest(req)
|
|
62
|
-
if (!auth?.tenantId
|
|
67
|
+
if (!auth?.tenantId) {
|
|
63
68
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
64
69
|
}
|
|
70
|
+
const organizationId = resolveActiveOrganizationId(auth)
|
|
71
|
+
if (!organizationId) {
|
|
72
|
+
return organizationScopeRequiredResponse()
|
|
73
|
+
}
|
|
65
74
|
|
|
66
75
|
const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')
|
|
67
76
|
? await (ctx.params as Promise<{ id?: string }>)
|
|
@@ -80,7 +89,7 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
80
89
|
|
|
81
90
|
const container = await createRequestContainer()
|
|
82
91
|
const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
|
|
83
|
-
const scope = { organizationId
|
|
92
|
+
const scope = { organizationId, tenantId: auth.tenantId }
|
|
84
93
|
const current = await scheduleService.getById(parsedParams.data.id, scope)
|
|
85
94
|
|
|
86
95
|
if (!current) {
|
|
@@ -141,9 +150,13 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
141
150
|
|
|
142
151
|
export async function DELETE(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
|
|
143
152
|
const auth = await getAuthFromRequest(req)
|
|
144
|
-
if (!auth?.tenantId
|
|
153
|
+
if (!auth?.tenantId) {
|
|
145
154
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
146
155
|
}
|
|
156
|
+
const organizationId = resolveActiveOrganizationId(auth)
|
|
157
|
+
if (!organizationId) {
|
|
158
|
+
return organizationScopeRequiredResponse()
|
|
159
|
+
}
|
|
147
160
|
|
|
148
161
|
const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')
|
|
149
162
|
? await (ctx.params as Promise<{ id?: string }>)
|
|
@@ -156,7 +169,7 @@ export async function DELETE(req: Request, ctx: { params?: Promise<{ id?: string
|
|
|
156
169
|
|
|
157
170
|
const container = await createRequestContainer()
|
|
158
171
|
const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
|
|
159
|
-
const scope = { organizationId
|
|
172
|
+
const scope = { organizationId, tenantId: auth.tenantId }
|
|
160
173
|
|
|
161
174
|
const guardResult = await validateCrudMutationGuard(container, {
|
|
162
175
|
tenantId: auth.tenantId,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
3
|
+
import { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'
|
|
3
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
5
|
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
5
6
|
import { createSyncScheduleSchema, listSyncSchedulesQuerySchema } from '../../data/validators'
|
|
@@ -24,9 +25,13 @@ export const openApi = {
|
|
|
24
25
|
|
|
25
26
|
export async function GET(req: Request) {
|
|
26
27
|
const auth = await getAuthFromRequest(req)
|
|
27
|
-
if (!auth?.tenantId
|
|
28
|
+
if (!auth?.tenantId) {
|
|
28
29
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
29
30
|
}
|
|
31
|
+
const organizationId = resolveActiveOrganizationId(auth)
|
|
32
|
+
if (!organizationId) {
|
|
33
|
+
return organizationScopeRequiredResponse()
|
|
34
|
+
}
|
|
30
35
|
|
|
31
36
|
const url = new URL(req.url)
|
|
32
37
|
const parsed = listSyncSchedulesQuerySchema.safeParse({
|
|
@@ -43,7 +48,7 @@ export async function GET(req: Request) {
|
|
|
43
48
|
|
|
44
49
|
const container = await createRequestContainer()
|
|
45
50
|
const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
|
|
46
|
-
const scope = { organizationId
|
|
51
|
+
const scope = { organizationId, tenantId: auth.tenantId }
|
|
47
52
|
const { items, total } = await scheduleService.listSchedules(parsed.data, scope)
|
|
48
53
|
|
|
49
54
|
return NextResponse.json({
|
|
@@ -57,9 +62,13 @@ export async function GET(req: Request) {
|
|
|
57
62
|
|
|
58
63
|
export async function POST(req: Request) {
|
|
59
64
|
const auth = await getAuthFromRequest(req)
|
|
60
|
-
if (!auth?.tenantId
|
|
65
|
+
if (!auth?.tenantId) {
|
|
61
66
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
62
67
|
}
|
|
68
|
+
const organizationId = resolveActiveOrganizationId(auth)
|
|
69
|
+
if (!organizationId) {
|
|
70
|
+
return organizationScopeRequiredResponse()
|
|
71
|
+
}
|
|
63
72
|
|
|
64
73
|
const payload = await readJsonSafe(req)
|
|
65
74
|
const parsed = createSyncScheduleSchema.safeParse(payload)
|
|
@@ -69,7 +78,7 @@ export async function POST(req: Request) {
|
|
|
69
78
|
|
|
70
79
|
const container = await createRequestContainer()
|
|
71
80
|
const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
|
|
72
|
-
const scope = { organizationId
|
|
81
|
+
const scope = { organizationId, tenantId: auth.tenantId }
|
|
73
82
|
|
|
74
83
|
const guardResult = await validateCrudMutationGuard(container, {
|
|
75
84
|
tenantId: auth.tenantId,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
3
|
+
import { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'
|
|
3
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
5
|
import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
|
|
5
6
|
import type { CredentialsService } from '../../integrations/lib/credentials-service'
|
|
@@ -17,9 +18,13 @@ export const openApi = {
|
|
|
17
18
|
|
|
18
19
|
export async function POST(req: Request) {
|
|
19
20
|
const auth = await getAuthFromRequest(req)
|
|
20
|
-
if (!auth?.tenantId
|
|
21
|
+
if (!auth?.tenantId) {
|
|
21
22
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
22
23
|
}
|
|
24
|
+
const organizationId = resolveActiveOrganizationId(auth)
|
|
25
|
+
if (!organizationId) {
|
|
26
|
+
return organizationScopeRequiredResponse()
|
|
27
|
+
}
|
|
23
28
|
|
|
24
29
|
const parsed = validateConnectionSchema.safeParse(await req.json())
|
|
25
30
|
if (!parsed.success) {
|
|
@@ -39,7 +44,7 @@ export async function POST(req: Request) {
|
|
|
39
44
|
const container = await createRequestContainer()
|
|
40
45
|
const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
|
|
41
46
|
const credentials = await credentialsService.resolve(integration.id, {
|
|
42
|
-
organizationId
|
|
47
|
+
organizationId,
|
|
43
48
|
tenantId: auth.tenantId,
|
|
44
49
|
})
|
|
45
50
|
|
|
@@ -50,7 +55,7 @@ export async function POST(req: Request) {
|
|
|
50
55
|
const mapping = await adapter.getMapping({
|
|
51
56
|
entityType: parsed.data.entityType,
|
|
52
57
|
scope: {
|
|
53
|
-
organizationId
|
|
58
|
+
organizationId,
|
|
54
59
|
tenantId: auth.tenantId,
|
|
55
60
|
},
|
|
56
61
|
})
|
|
@@ -64,7 +69,7 @@ export async function POST(req: Request) {
|
|
|
64
69
|
credentials,
|
|
65
70
|
mapping,
|
|
66
71
|
scope: {
|
|
67
|
-
organizationId
|
|
72
|
+
organizationId,
|
|
68
73
|
tenantId: auth.tenantId,
|
|
69
74
|
},
|
|
70
75
|
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Design System Module — Agent Guidelines
|
|
2
|
+
|
|
3
|
+
Live component gallery at `/backend/design-system`, gated by `design_system.view`. Entries are plain data (`gallery/entries/<family>.tsx`) listed in the family manifest (`gallery/registry.ts`) and lazy-loaded per family. The gallery is a DS consumer: it must showcase the DS without ever violating it.
|
|
4
|
+
|
|
5
|
+
## Registry Contract
|
|
6
|
+
|
|
7
|
+
- `gallery/types.ts` defines `GalleryVariant` / `GalleryEntry` / `GalleryFamily` — do not extend them casually; the shapes are the spec contract (`.ai/specs/2026-07-05-ds-live-component-gallery.md`).
|
|
8
|
+
- A family = one manifest row in `galleryFamilies` + one `entries/<id>.tsx` exporting `entries: GalleryEntry[]`.
|
|
9
|
+
- Every variant keeps `render` and `code` side by side in one object literal; review them together — colocation is the drift defense.
|
|
10
|
+
|
|
11
|
+
## Always
|
|
12
|
+
|
|
13
|
+
1. **MUST keep every variant `code` snippet containing the entry's `importPath`** — enforced by `gallery/__tests__/registry-integrity.test.ts`.
|
|
14
|
+
2. **MUST update the coverage allowlist honestly** — when adding a primitive entry, delete its row from `PENDING_FAMILIES` in `gallery/__tests__/gallery-coverage.test.ts`; when a new primitive lands without an entry, add it there with a one-line reason. `PENDING_FAMILIES` must only shrink as families land.
|
|
15
|
+
3. **MUST use semantic tokens only** in gallery chrome (`border-border`, `bg-background`, `bg-muted`, `text-muted-foreground`) and lucide icons only.
|
|
16
|
+
4. **MUST route all gallery chrome strings through `i18n/` in all four locales** (en, pl, es, de) via `useT()`. Component titles and variant names (`Button`, `destructive-soft`) are proper nouns and stay untranslated.
|
|
17
|
+
5. **MUST render entries with inline mock props only** — the gallery never calls tenant APIs and never renders tenant data.
|
|
18
|
+
6. **MUST keep `figmaNodeId` in `<page>:<node>` format** pointing into `DS_FIGMA_FILE`; omit it when unknown — never guess node ids.
|
|
19
|
+
|
|
20
|
+
## Never
|
|
21
|
+
|
|
22
|
+
- Never add gallery-specific props or code to `packages/ui` — the gallery consumes primitives exactly as any other module does.
|
|
23
|
+
- Never hardcode colors, arbitrary sizes, or full-pill chips in gallery chrome.
|
|
24
|
+
- Never add API routes, entities, DI registrations, or tenant-scoped anything to this module.
|
|
25
|
+
- Never remove a file from the primitives coverage enumeration by loosening the test — only entries or allowlist rows with reasons.
|
|
26
|
+
|
|
27
|
+
## Validation Commands
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
yarn generate
|
|
31
|
+
yarn workspace @open-mercato/core test --testPathPattern design_system
|
|
32
|
+
yarn workspace @open-mercato/core typecheck
|
|
33
|
+
yarn i18n:check-hardcoded
|
|
34
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const metadata = {
|
|
2
|
+
requireAuth: true,
|
|
3
|
+
requireFeatures: ['design_system.view'],
|
|
4
|
+
pageTitle: 'Design system',
|
|
5
|
+
pageTitleKey: 'design_system.nav.title',
|
|
6
|
+
pageGroup: 'Developer',
|
|
7
|
+
pageGroupKey: 'design_system.nav.group',
|
|
8
|
+
pageOrder: 900,
|
|
9
|
+
icon: 'shapes',
|
|
10
|
+
breadcrumb: [{ label: 'Design system', labelKey: 'design_system.nav.title' }],
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
4
|
+
import { GalleryShell } from '../../gallery/components/GalleryShell'
|
|
5
|
+
|
|
6
|
+
export default function DesignSystemGalleryPage() {
|
|
7
|
+
return (
|
|
8
|
+
<Page>
|
|
9
|
+
<PageBody>
|
|
10
|
+
<GalleryShell />
|
|
11
|
+
</PageBody>
|
|
12
|
+
</Page>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -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
|
+
}
|