create-mercato-app 0.5.1-develop.2949.009dcdd2d5 → 0.5.1-develop.2954.610bab2d08
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/package.json +1 -1
- package/template/package.json.template +4 -4
- package/template/scripts/dev-runtime.mjs +14 -42
- package/template/scripts/dev-splash-coding-flow.mjs +26 -9
- package/template/scripts/dev.mjs +22 -67
- package/template/src/app/globals.css +64 -32
- package/template/src/components/DemoFeedbackWidget.tsx +16 -9
- package/template/src/components/GlobalNoticeBars.tsx +7 -7
- package/template/src/components/StartPageContent.tsx +6 -6
- package/template/src/components/ui/button.tsx +1 -59
- package/template/src/modules/example/__integration__/TC-UMES-005.spec.ts +32 -21
- package/template/src/modules/example/api/blog/[id]/route.ts +1 -1
- package/template/src/modules/example/backend/umes-integrations/page.tsx +31 -18
- package/template/src/modules/example/data/entities.ts +1 -1
- package/template/src/modules/example/widgets/components.ts +2 -2
- package/template/src/modules/example/widgets/dashboard/todos/widget.client.tsx +5 -4
- package/template/src/modules/example/widgets/injection/catalog-seo-report/widget.client.tsx +4 -4
- package/template/src/modules/example/widgets/injection/customer-priority-detail/widget.client.tsx +19 -8
- package/template/src/modules/example/widgets/injection/portal-recent-activity/widget.client.tsx +4 -4
- package/template/src/modules/example/widgets/injection/sales-todos/widget.client.tsx +3 -3
- package/template/src/modules/example_customers_sync/api/example-customers-sync/mappings/route.ts +1 -1
- package/template/src/modules/ratelimit_probe/api/ping/route.ts +35 -0
- package/template/src/modules.ts +1 -0
package/package.json
CHANGED
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"@uiw/react-markdown-preview": "^5.1.5",
|
|
78
78
|
"@uiw/react-md-editor": "^4.0.11",
|
|
79
79
|
"@xyflow/react": "^12.6.0",
|
|
80
|
-
"ai": "^6.0.
|
|
80
|
+
"ai": "^6.0.168",
|
|
81
81
|
"awilix": "^12.0.5",
|
|
82
82
|
"bcryptjs": "^3.0.3",
|
|
83
83
|
"class-variance-authority": "^0.7.1",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"lucide-react": "^0.556.0",
|
|
87
87
|
"mammoth": "^1.9.0",
|
|
88
88
|
"newrelic": "^13.7.0",
|
|
89
|
-
"next": "16.2.
|
|
89
|
+
"next": "16.2.4",
|
|
90
90
|
"pg": "8.20.0",
|
|
91
91
|
"pdfjs-dist": "^5.4.149",
|
|
92
92
|
"react": "19.2.1",
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
"semver": "^7.7.3",
|
|
101
101
|
"tailwind-merge": "^3.4.0",
|
|
102
102
|
"zod": "^4.1.13",
|
|
103
|
-
"@stripe/react-stripe-js": "^
|
|
104
|
-
"@stripe/stripe-js": "^
|
|
103
|
+
"@stripe/react-stripe-js": "^6.2.0",
|
|
104
|
+
"@stripe/stripe-js": "^9.2.0",
|
|
105
105
|
"@open-mercato/gateway-stripe": "{{PACKAGE_VERSION}}",
|
|
106
106
|
"@open-mercato/sync-akeneo": "{{PACKAGE_VERSION}}"
|
|
107
107
|
},
|
|
@@ -55,9 +55,9 @@ const {
|
|
|
55
55
|
stripAnsi,
|
|
56
56
|
wrapListLines,
|
|
57
57
|
} = await import(resolveSplashHelpersImport())
|
|
58
|
-
const {
|
|
58
|
+
const { resolveSpawnCommand } = await import(resolveSpawnUtilsImport())
|
|
59
59
|
|
|
60
|
-
const command =
|
|
60
|
+
const command = process.platform === 'win32' ? 'mercato.cmd' : 'mercato'
|
|
61
61
|
const classic = process.argv.includes('--classic') || isEnabledEnvFlag(process.env.OM_DEV_CLASSIC)
|
|
62
62
|
const verbose = !classic && (process.argv.includes('--verbose') || process.env.MERCATO_DEV_OUTPUT === 'verbose')
|
|
63
63
|
const rawPassthrough = classic || verbose
|
|
@@ -412,10 +412,10 @@ function spawnMercato(args) {
|
|
|
412
412
|
return child
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
function waitForExit(child
|
|
415
|
+
function waitForExit(child) {
|
|
416
416
|
return new Promise((resolve) => {
|
|
417
417
|
child.on('exit', (code, signal) => {
|
|
418
|
-
resolve({
|
|
418
|
+
resolve({ code, signal })
|
|
419
419
|
})
|
|
420
420
|
})
|
|
421
421
|
}
|
|
@@ -441,32 +441,6 @@ function resolveChildExitCode(result, fallback = 1) {
|
|
|
441
441
|
return fallback
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
function formatChildExitStatus(result) {
|
|
445
|
-
if (typeof result?.code === 'number') {
|
|
446
|
-
return `exit code ${result.code}`
|
|
447
|
-
}
|
|
448
|
-
if (result?.signal) {
|
|
449
|
-
return `signal ${result.signal}`
|
|
450
|
-
}
|
|
451
|
-
return 'an unknown status'
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function resolveUnexpectedExitCode(result) {
|
|
455
|
-
const exitCode = resolveChildExitCode(result, 1)
|
|
456
|
-
return exitCode === 0 ? 1 : exitCode
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function reportUnexpectedChildExit(result) {
|
|
460
|
-
const message = `❌ ${result?.label ?? 'Child process'} exited unexpectedly with ${formatChildExitStatus(result)}`
|
|
461
|
-
console.error(message)
|
|
462
|
-
rememberRawLog(message)
|
|
463
|
-
publishRuntimeFailure(message, {
|
|
464
|
-
progressCurrent: splashState.progressCurrent >= runtimeProgressCurrent ? splashState.progressCurrent : runtimeProgressCurrent,
|
|
465
|
-
progressLabel: splashState.progressLabel || startupProgress.label,
|
|
466
|
-
failureLines: [...collectRuntimeFailureLines(), message].slice(-10),
|
|
467
|
-
})
|
|
468
|
-
}
|
|
469
|
-
|
|
470
444
|
function joinBaseUrl(baseUrl, pathname) {
|
|
471
445
|
return `${String(baseUrl ?? '').replace(/\/$/, '')}${pathname}`
|
|
472
446
|
}
|
|
@@ -1547,16 +1521,15 @@ async function runClassicRuntime() {
|
|
|
1547
1521
|
|
|
1548
1522
|
const watch = spawnMercato(['generate', 'watch', '--skip-initial'])
|
|
1549
1523
|
const server = spawnMercato(['server', 'dev'])
|
|
1550
|
-
const result = await Promise.race([
|
|
1551
|
-
waitForExit(watch, 'Generator watch'),
|
|
1552
|
-
waitForExit(server, 'App runtime'),
|
|
1553
|
-
])
|
|
1524
|
+
const result = await Promise.race([waitForExit(watch), waitForExit(server)])
|
|
1554
1525
|
if (isGracefulShutdownResult(result)) {
|
|
1555
1526
|
return
|
|
1556
1527
|
}
|
|
1557
1528
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1529
|
+
// Unexpected child exit MUST surface as non-zero even if the child reported
|
|
1530
|
+
// code 0 — hiding a broken runtime as success masks failures from scripts/CI.
|
|
1531
|
+
const childCode = resolveChildExitCode(result, 1)
|
|
1532
|
+
shutdown(childCode === 0 ? 1 : childCode)
|
|
1560
1533
|
}
|
|
1561
1534
|
|
|
1562
1535
|
if (classic) {
|
|
@@ -1571,11 +1544,10 @@ printRuntimePackagesSummary()
|
|
|
1571
1544
|
const watch = startFilteredChild(['generate', 'watch', '--skip-initial'], 'Generator watch', classifyWatchLine)
|
|
1572
1545
|
const server = startFilteredChild(['server', 'dev'], 'App runtime', classifyServerLine)
|
|
1573
1546
|
|
|
1574
|
-
const result = await Promise.race([
|
|
1575
|
-
waitForExit(watch, 'Generator watch'),
|
|
1576
|
-
waitForExit(server, 'App runtime'),
|
|
1577
|
-
])
|
|
1547
|
+
const result = await Promise.race([waitForExit(watch), waitForExit(server)])
|
|
1578
1548
|
if (!isGracefulShutdownResult(result)) {
|
|
1579
|
-
|
|
1580
|
-
|
|
1549
|
+
// Unexpected child exit MUST surface as non-zero even if the child reported
|
|
1550
|
+
// code 0 — hiding a broken runtime as success masks failures from scripts/CI.
|
|
1551
|
+
const childCode = resolveChildExitCode(result, 1)
|
|
1552
|
+
shutdown(childCode === 0 ? 1 : childCode)
|
|
1581
1553
|
}
|
|
@@ -303,11 +303,10 @@ function writeJson(res, statusCode, payload) {
|
|
|
303
303
|
res.end(JSON.stringify(payload))
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
// Reject
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
const SHELL_UNSAFE_CHAR_PATTERN = /[\u0000-\u001f\u007f`$"';&|<>()[\]!^]/
|
|
306
|
+
// Reject control characters and shell metacharacters that could alter command
|
|
307
|
+
// semantics. Quotes are allowed here because these values are passed through
|
|
308
|
+
// `spawn` argument arrays instead of shell interpolation.
|
|
309
|
+
const SHELL_UNSAFE_CHAR_PATTERN = /[\u0000-\u001f\u007f`$&|;<>()[\]{}*!?~]/
|
|
311
310
|
|
|
312
311
|
export function isShellSafePathString(value) {
|
|
313
312
|
return typeof value === 'string'
|
|
@@ -324,11 +323,29 @@ export function assertShellSafePath(value, label) {
|
|
|
324
323
|
|
|
325
324
|
export { sanitizeLaunchDirectory }
|
|
326
325
|
|
|
327
|
-
function
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
|
|
326
|
+
function resolveSafeLaunchFallbackDirectory() {
|
|
327
|
+
const candidates = [process.cwd(), os.homedir(), path.parse(process.cwd()).root]
|
|
328
|
+
for (const candidate of candidates) {
|
|
329
|
+
const resolvedCandidate = path.resolve(candidate)
|
|
330
|
+
if (!isShellSafePathString(resolvedCandidate)) {
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
331
333
|
|
|
334
|
+
try {
|
|
335
|
+
const stat = fs.statSync(resolvedCandidate)
|
|
336
|
+
if (stat.isDirectory()) {
|
|
337
|
+
return resolvedCandidate
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
// Try next fallback candidate
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return path.parse(process.cwd()).root
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function sanitizeLaunchDirectory(value) {
|
|
348
|
+
const fallback = resolveSafeLaunchFallbackDirectory()
|
|
332
349
|
if (!isShellSafePathString(value) || value.trim().length === 0) {
|
|
333
350
|
return fallback
|
|
334
351
|
}
|
package/template/scripts/dev.mjs
CHANGED
|
@@ -174,6 +174,14 @@ function isEnabledEnvFlag(value) {
|
|
|
174
174
|
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase())
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// OM_DEV_AUTO_MIGRATE defaults to ON: yarn dev applies pending migrations once
|
|
178
|
+
// at startup unless the user explicitly opts out. Documented in template AGENTS.md.
|
|
179
|
+
function shouldAutoMigrateOnDev() {
|
|
180
|
+
const raw = process.env.OM_DEV_AUTO_MIGRATE
|
|
181
|
+
if (typeof raw !== 'string') return true
|
|
182
|
+
return !['0', 'false', 'no', 'off'].includes(raw.trim().toLowerCase())
|
|
183
|
+
}
|
|
184
|
+
|
|
177
185
|
const splashPortConfig = (() => {
|
|
178
186
|
try {
|
|
179
187
|
return resolveSplashPortConfig()
|
|
@@ -1440,70 +1448,6 @@ async function runPassthroughStage(label, commandArgs, options = {}) {
|
|
|
1440
1448
|
console.log(`✅ ${formatProgressLine(label, stageCurrent, stageTotal, resolveProgressPercent(stageCurrent, stageTotal))} in ${formatDuration(Date.now() - startedAt)}`)
|
|
1441
1449
|
}
|
|
1442
1450
|
|
|
1443
|
-
function isAutoMigrateEnabled() {
|
|
1444
|
-
const raw = process.env.OM_DEV_AUTO_MIGRATE
|
|
1445
|
-
if (typeof raw !== 'string') return true
|
|
1446
|
-
const normalized = raw.trim().toLowerCase()
|
|
1447
|
-
if (normalized === '') return true
|
|
1448
|
-
return !['0', 'false', 'no', 'off'].includes(normalized)
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
async function runAutoMigrateIfEnabled() {
|
|
1452
|
-
if (!isAutoMigrateEnabled()) {
|
|
1453
|
-
console.log('ℹ️ Skipping auto-migrate: OM_DEV_AUTO_MIGRATE is disabled.')
|
|
1454
|
-
return
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
const label = '🗄️ Auto-applying database migrations'
|
|
1458
|
-
const startedAt = Date.now()
|
|
1459
|
-
console.log(`${label}...`)
|
|
1460
|
-
updateSplashState({
|
|
1461
|
-
phase: label,
|
|
1462
|
-
detail: 'Running yarn db:migrate before dev launch',
|
|
1463
|
-
activity: 'Auto-migrate running',
|
|
1464
|
-
})
|
|
1465
|
-
|
|
1466
|
-
const child = spawnCommand(yarnCommand, ['db:migrate'], {
|
|
1467
|
-
label: 'db:migrate (auto)',
|
|
1468
|
-
logFile: getDevRunnerLog(),
|
|
1469
|
-
})
|
|
1470
|
-
const capturedLines = []
|
|
1471
|
-
const capture = (line) => {
|
|
1472
|
-
capturedLines.push(line)
|
|
1473
|
-
if (capturedLines.length > 200) capturedLines.shift()
|
|
1474
|
-
}
|
|
1475
|
-
connectLineStream(child.stdout, capture)
|
|
1476
|
-
connectLineStream(child.stderr, capture)
|
|
1477
|
-
|
|
1478
|
-
const result = await waitForClose(child)
|
|
1479
|
-
if (isGracefulShutdownResult(result)) return
|
|
1480
|
-
|
|
1481
|
-
const exitCode = resolveChildExitCode(result)
|
|
1482
|
-
if (exitCode === 0) {
|
|
1483
|
-
console.log(`✅ ${label} in ${formatDuration(Date.now() - startedAt)}`)
|
|
1484
|
-
return
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
console.warn('')
|
|
1488
|
-
console.warn('⚠️ Auto-migrate reported a non-zero exit.')
|
|
1489
|
-
console.warn(' This usually means a migration conflict — for example, a parallel')
|
|
1490
|
-
console.warn(' `yarn db:migrate` from another shell already applied the pending change.')
|
|
1491
|
-
console.warn(' Last captured output (tail):')
|
|
1492
|
-
for (const line of capturedLines.slice(-20)) {
|
|
1493
|
-
console.warn(` | ${line}`)
|
|
1494
|
-
}
|
|
1495
|
-
console.warn('')
|
|
1496
|
-
console.warn(' Suggested next steps:')
|
|
1497
|
-
console.warn(' • Re-run `yarn db:migrate` manually to inspect the exact error.')
|
|
1498
|
-
console.warn(' • If a migration was only partially applied, roll back with')
|
|
1499
|
-
console.warn(' `yarn db:migrate --down` and re-apply.')
|
|
1500
|
-
console.warn(' • To disable auto-migrate entirely for this project, set')
|
|
1501
|
-
console.warn(' `OM_DEV_AUTO_MIGRATE=0` in `.env.local`.')
|
|
1502
|
-
console.warn(' Continuing to launch dev — the app may still work if the schema is')
|
|
1503
|
-
console.warn(' actually up to date. If it does not, fix the migration before reloading.')
|
|
1504
|
-
console.warn('')
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
1451
|
function startPackageWatch() {
|
|
1508
1452
|
if (classic) {
|
|
1509
1453
|
const child = spawnCommand(yarnCommand, ['watch:packages'], {
|
|
@@ -1629,7 +1573,10 @@ function launchMonorepoAppDev() {
|
|
|
1629
1573
|
|
|
1630
1574
|
app.on('close', (code, signal) => {
|
|
1631
1575
|
if (!shuttingDown) {
|
|
1632
|
-
|
|
1576
|
+
// Unexpected child exit MUST surface as non-zero even if the child reported
|
|
1577
|
+
// code 0 — hiding a broken runtime as success masks failures from scripts/CI.
|
|
1578
|
+
const childCode = resolveChildExitCode({ code, signal }, 1)
|
|
1579
|
+
shutdown(childCode === 0 ? 1 : childCode)
|
|
1633
1580
|
}
|
|
1634
1581
|
})
|
|
1635
1582
|
}
|
|
@@ -1720,7 +1667,10 @@ async function runClassicStandaloneDev() {
|
|
|
1720
1667
|
await runRawYarnCommand(['install'])
|
|
1721
1668
|
}
|
|
1722
1669
|
|
|
1723
|
-
|
|
1670
|
+
if (shouldAutoMigrateOnDev()) {
|
|
1671
|
+
await runRawYarnCommand(['db:migrate'])
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1724
1674
|
launchStandaloneDev()
|
|
1725
1675
|
}
|
|
1726
1676
|
|
|
@@ -1751,7 +1701,12 @@ async function main() {
|
|
|
1751
1701
|
stageTotal: standaloneStageTotal,
|
|
1752
1702
|
})
|
|
1753
1703
|
}
|
|
1754
|
-
|
|
1704
|
+
if (shouldAutoMigrateOnDev()) {
|
|
1705
|
+
await runPassthroughStage('🗄️ Applying database migrations', ['db:migrate'], {
|
|
1706
|
+
stageCurrent: 2,
|
|
1707
|
+
stageTotal: standaloneStageTotal,
|
|
1708
|
+
})
|
|
1709
|
+
}
|
|
1755
1710
|
launchStandaloneDev()
|
|
1756
1711
|
return
|
|
1757
1712
|
}
|
|
@@ -51,27 +51,9 @@ TODO: Fix that latter to have reference by the package names
|
|
|
51
51
|
--color-primary: var(--primary);
|
|
52
52
|
--color-brand-violet: var(--brand-violet);
|
|
53
53
|
--color-brand-violet-foreground: var(--brand-violet-foreground);
|
|
54
|
-
|
|
55
|
-
/* Accent indigo — used by selection controls (checkbox/radio/switch) */
|
|
56
|
-
--color-accent-indigo: var(--accent-indigo);
|
|
57
|
-
--color-accent-indigo-foreground: var(--accent-indigo-foreground);
|
|
58
|
-
|
|
59
|
-
/* Disabled control tokens */
|
|
60
54
|
--color-bg-disabled: var(--bg-disabled);
|
|
61
55
|
--color-text-disabled: var(--text-disabled);
|
|
62
56
|
--color-border-disabled: var(--border-disabled);
|
|
63
|
-
|
|
64
|
-
/* Figma focus ring shadow */
|
|
65
|
-
--shadow-focus: 0 0 0 2px var(--focus-ring-inner), 0 0 0 4px var(--focus-ring-outer);
|
|
66
|
-
|
|
67
|
-
/* Social brand colors (theme-invariant) */
|
|
68
|
-
--color-brand-apple: var(--brand-apple);
|
|
69
|
-
--color-brand-github: var(--brand-github);
|
|
70
|
-
--color-brand-x: var(--brand-x);
|
|
71
|
-
--color-brand-google-stroke: var(--brand-google-stroke);
|
|
72
|
-
--color-brand-facebook: var(--brand-facebook);
|
|
73
|
-
--color-brand-dropbox: var(--brand-dropbox);
|
|
74
|
-
--color-brand-linkedin: var(--brand-linkedin);
|
|
75
57
|
--color-popover-foreground: var(--popover-foreground);
|
|
76
58
|
--color-popover: var(--popover);
|
|
77
59
|
--color-card-foreground: var(--card-foreground);
|
|
@@ -79,7 +61,7 @@ TODO: Fix that latter to have reference by the package names
|
|
|
79
61
|
--radius-sm: calc(var(--radius) - 4px);
|
|
80
62
|
--radius-md: calc(var(--radius) - 2px);
|
|
81
63
|
--radius-lg: var(--radius);
|
|
82
|
-
--radius-xl: calc(var(--radius) +
|
|
64
|
+
--radius-xl: calc(var(--radius) + 6px);
|
|
83
65
|
|
|
84
66
|
/* ═══ Design System: Semantic Status Colors ═══ */
|
|
85
67
|
--color-status-error-bg: var(--status-error-bg);
|
|
@@ -107,11 +89,28 @@ TODO: Fix that latter to have reference by the package names
|
|
|
107
89
|
--color-status-neutral-border: var(--status-neutral-border);
|
|
108
90
|
--color-status-neutral-icon: var(--status-neutral-icon);
|
|
109
91
|
|
|
92
|
+
/* ═══ Design System: Accent Colors ═══ */
|
|
93
|
+
--color-accent-indigo: var(--accent-indigo);
|
|
94
|
+
--color-accent-indigo-foreground: var(--accent-indigo-foreground);
|
|
95
|
+
|
|
96
|
+
/* ═══ Design System: Brand Colors ═══ */
|
|
97
|
+
--color-brand-violet: var(--brand-violet);
|
|
98
|
+
--color-brand-lime: var(--brand-lime);
|
|
99
|
+
|
|
100
|
+
/* ═══ Design System: Social Brand Colors (theme-invariant) ═══ */
|
|
101
|
+
--color-brand-apple: var(--brand-apple);
|
|
102
|
+
--color-brand-github: var(--brand-github);
|
|
103
|
+
--color-brand-x: var(--brand-x);
|
|
104
|
+
--color-brand-google-stroke: var(--brand-google-stroke);
|
|
105
|
+
--color-brand-facebook: var(--brand-facebook);
|
|
106
|
+
--color-brand-dropbox: var(--brand-dropbox);
|
|
107
|
+
--color-brand-linkedin: var(--brand-linkedin);
|
|
108
|
+
|
|
110
109
|
/* ═══ Design System: Typography ═══ */
|
|
111
110
|
--font-size-overline: 0.6875rem;
|
|
112
111
|
--font-size-overline--line-height: 1rem;
|
|
113
112
|
|
|
114
|
-
/* ═══ Design System: Z-Index Scale
|
|
113
|
+
/* ═══ Design System: Z-Index Scale ═══ */
|
|
115
114
|
--z-index-base: 0;
|
|
116
115
|
--z-index-sticky: 10;
|
|
117
116
|
--z-index-dropdown: 20;
|
|
@@ -121,6 +120,25 @@ TODO: Fix that latter to have reference by the package names
|
|
|
121
120
|
--z-index-tooltip: 60;
|
|
122
121
|
--z-index-banner: 70;
|
|
123
122
|
--z-index-top: 100;
|
|
123
|
+
|
|
124
|
+
/* ═══ Design System: Shadow Scale ═══
|
|
125
|
+
Tuned to ink #101828. Dark mode overrides in .dark below.
|
|
126
|
+
xs → flat controls (button, input, icon-button)
|
|
127
|
+
sm → cards, panels, sections
|
|
128
|
+
md → hover elevation, slightly raised cards
|
|
129
|
+
lg → dialogs, overlays, popovers
|
|
130
|
+
xl → floating panels (drawers, side sheets)
|
|
131
|
+
2xl → top-level modals, command palette
|
|
132
|
+
*/
|
|
133
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
134
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.06);
|
|
135
|
+
--shadow-md: 0 4px 12px rgb(16 24 40 / 0.08);
|
|
136
|
+
--shadow-lg: 0 12px 32px rgb(16 24 40 / 0.10);
|
|
137
|
+
--shadow-xl: 0 20px 40px rgb(16 24 40 / 0.12);
|
|
138
|
+
--shadow-2xl: 0 25px 50px rgb(16 24 40 / 0.20);
|
|
139
|
+
|
|
140
|
+
/* Figma button focus: 2px white inner + 4px slate-alpha-16 outer */
|
|
141
|
+
--shadow-focus: 0 0 0 2px var(--focus-ring-inner), 0 0 0 4px var(--focus-ring-outer);
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
:root {
|
|
@@ -128,7 +146,9 @@ TODO: Fix that latter to have reference by the package names
|
|
|
128
146
|
--font-geist-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
129
147
|
--font-geist-mono: ui-monospace, "SFMono-Regular", "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
|
|
130
148
|
--radius: 0.625rem;
|
|
131
|
-
/*
|
|
149
|
+
/* Brand lime — theme-invariant (no .dark override). Brand violet is themed (see below + .dark). */
|
|
150
|
+
--brand-lime: #D4F372;
|
|
151
|
+
/* Focus ring tokens (Figma dual-shadow spec — opt-in via .focus-ring-fancy) */
|
|
132
152
|
--focus-ring-inner: rgba(255, 255, 255, 1);
|
|
133
153
|
--focus-ring-outer: rgba(153, 160, 174, 0.16);
|
|
134
154
|
/* Social brand colors — theme-invariant */
|
|
@@ -150,8 +170,10 @@ TODO: Fix that latter to have reference by the package names
|
|
|
150
170
|
--brand-violet: oklch(0.55 0.2 293);
|
|
151
171
|
--brand-violet-foreground: oklch(0.985 0 0);
|
|
152
172
|
--primary-hover: oklch(0.145 0 0);
|
|
173
|
+
/* Accent indigo — used for selection controls (checkbox, radio, switch) */
|
|
153
174
|
--accent-indigo: #6366f1;
|
|
154
175
|
--accent-indigo-foreground: #ffffff;
|
|
176
|
+
/* Disabled control tokens (Figma: bg/weak-25, text/disabled-300, stroke-soft-200) */
|
|
155
177
|
--bg-disabled: #f7f7f7;
|
|
156
178
|
--text-disabled: #d1d1d1;
|
|
157
179
|
--border-disabled: #ebebeb;
|
|
@@ -209,11 +231,11 @@ TODO: Fix that latter to have reference by the package names
|
|
|
209
231
|
--status-warning-border: oklch(0.830 0.070 80);
|
|
210
232
|
--status-warning-icon: oklch(0.700 0.160 70);
|
|
211
233
|
|
|
212
|
-
/* Info (hue ~
|
|
213
|
-
--status-info-bg: oklch(0.
|
|
214
|
-
--status-info-text: oklch(0.
|
|
215
|
-
--status-info-border: oklch(0.
|
|
216
|
-
--status-info-icon: oklch(0.
|
|
234
|
+
/* Info (hue ~277° — indigo, bridges to brand violet) */
|
|
235
|
+
--status-info-bg: oklch(0.962 0.018 272.314); /* indigo-50 #EEF2FF */
|
|
236
|
+
--status-info-text: oklch(0.359 0.144 278.697); /* indigo-800 #3730A3 */
|
|
237
|
+
--status-info-border: oklch(0.870 0.065 274.039);/* indigo-200 #C7D2FE */
|
|
238
|
+
--status-info-icon: oklch(0.511 0.262 276.966); /* indigo-600 #4F46E5 = --chart-indigo */
|
|
217
239
|
|
|
218
240
|
/* Neutral (achromatic — aligned with --muted) */
|
|
219
241
|
--status-neutral-bg: oklch(0.965 0 0);
|
|
@@ -232,8 +254,16 @@ TODO: Fix that latter to have reference by the package names
|
|
|
232
254
|
|
|
233
255
|
.dark {
|
|
234
256
|
color-scheme: dark;
|
|
257
|
+
/* Focus ring tokens — dark variant */
|
|
235
258
|
--focus-ring-inner: var(--background);
|
|
236
259
|
--focus-ring-outer: rgba(255, 255, 255, 0.18);
|
|
260
|
+
/* Shadow scale — dark mode: use pure black, higher opacity for visibility */
|
|
261
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.30);
|
|
262
|
+
--shadow-sm: 0 1px 3px rgb(0 0 0 / 0.40), 0 1px 2px rgb(0 0 0 / 0.30);
|
|
263
|
+
--shadow-md: 0 4px 12px rgb(0 0 0 / 0.50);
|
|
264
|
+
--shadow-lg: 0 12px 32px rgb(0 0 0 / 0.60);
|
|
265
|
+
--shadow-xl: 0 20px 40px rgb(0 0 0 / 0.70);
|
|
266
|
+
--shadow-2xl: 0 25px 50px rgb(0 0 0 / 0.80);
|
|
237
267
|
--background: oklch(0.145 0 0);
|
|
238
268
|
--foreground: oklch(0.985 0 0);
|
|
239
269
|
--card: oklch(0.205 0 0);
|
|
@@ -245,8 +275,10 @@ TODO: Fix that latter to have reference by the package names
|
|
|
245
275
|
--brand-violet: oklch(0.65 0.2 293);
|
|
246
276
|
--brand-violet-foreground: oklch(0.985 0 0);
|
|
247
277
|
--primary-hover: oklch(0.85 0 0);
|
|
278
|
+
/* Accent indigo — slightly lighter in dark mode for contrast */
|
|
248
279
|
--accent-indigo: #818cf8;
|
|
249
280
|
--accent-indigo-foreground: #ffffff;
|
|
281
|
+
/* Disabled control tokens (dark) */
|
|
250
282
|
--bg-disabled: oklch(0.25 0 0);
|
|
251
283
|
--text-disabled: oklch(0.45 0 0);
|
|
252
284
|
--border-disabled: oklch(0.30 0 0);
|
|
@@ -304,11 +336,11 @@ TODO: Fix that latter to have reference by the package names
|
|
|
304
336
|
--status-warning-border: oklch(0.420 0.060 80);
|
|
305
337
|
--status-warning-icon: oklch(0.820 0.160 84.429); /* = dark --chart-amber */
|
|
306
338
|
|
|
307
|
-
/* Info */
|
|
308
|
-
--status-info-bg: oklch(0.220 0.
|
|
309
|
-
--status-info-text: oklch(0.
|
|
310
|
-
--status-info-border: oklch(0.
|
|
311
|
-
--status-info-icon: oklch(0.
|
|
339
|
+
/* Info (indigo — bridges to brand violet in dark mode) */
|
|
340
|
+
--status-info-bg: oklch(0.220 0.040 276);
|
|
341
|
+
--status-info-text: oklch(0.785 0.115 274.713); /* indigo-300 #A5B4FC */
|
|
342
|
+
--status-info-border: oklch(0.359 0.144 278.697);/* indigo-800 #3730A3 */
|
|
343
|
+
--status-info-icon: oklch(0.585 0.233 277.117); /* indigo-500 #6366F1 */
|
|
312
344
|
|
|
313
345
|
/* Neutral */
|
|
314
346
|
--status-neutral-bg: oklch(0.230 0 0);
|
|
@@ -324,7 +356,7 @@ TODO: Fix that latter to have reference by the package names
|
|
|
324
356
|
body {
|
|
325
357
|
@apply bg-background text-foreground;
|
|
326
358
|
}
|
|
327
|
-
/* Native form controls — use the DS indigo accent */
|
|
359
|
+
/* Native form controls — use the DS indigo accent instead of browser default */
|
|
328
360
|
input[type="checkbox"],
|
|
329
361
|
input[type="radio"] {
|
|
330
362
|
accent-color: var(--accent-indigo);
|
|
@@ -7,6 +7,7 @@ import { MessageCircle, Send } from 'lucide-react'
|
|
|
7
7
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@open-mercato/ui/primitives/dialog'
|
|
8
8
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
9
9
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
10
|
+
import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
10
11
|
import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
|
|
11
12
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
12
13
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
@@ -200,19 +201,24 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
|
|
|
200
201
|
}, [submitState, resetForm])
|
|
201
202
|
|
|
202
203
|
if (!mounted) return null
|
|
203
|
-
if (otherModalOpen && !open) return null
|
|
204
204
|
|
|
205
205
|
const caption = CAPTIONS[captionIndex]
|
|
206
206
|
const currentCaption = t(caption.key, caption.fallback)
|
|
207
207
|
|
|
208
|
+
if (otherModalOpen && !open) return null
|
|
209
|
+
|
|
210
|
+
// Brand-gradient floating CTA. Uses brand CSS vars (no hardcoded hex) +
|
|
211
|
+
// z-banner token (no z-[60]) so it stays DS-compliant while keeping the
|
|
212
|
+
// bespoke 135deg / 0-50-100 gradient that the marketing visual depends on
|
|
213
|
+
// (FancyButton's primary variant uses 161.7deg / 0-35.36-70.72 which
|
|
214
|
+
// truncates the violet-to-end transition and looks banded).
|
|
208
215
|
const floatingButton = (
|
|
209
216
|
<button
|
|
210
217
|
type="button"
|
|
211
218
|
onClick={() => { setOpen(true); if (submitState === 'sent') resetForm() }}
|
|
212
|
-
className="fixed bottom-6 right-6 z-banner flex items-center gap-2 rounded-full px-5 py-3 text-sm font-semibold text-
|
|
219
|
+
className="fixed bottom-6 right-6 z-banner flex items-center gap-2 rounded-full px-5 py-3 text-sm font-semibold text-foreground shadow-xl transition-all hover:scale-105 hover:shadow-2xl active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 animate-[subtle-bounce_2s_ease-in-out_infinite]"
|
|
213
220
|
style={{
|
|
214
|
-
|
|
215
|
-
color: '#1B1B1B',
|
|
221
|
+
backgroundImage: 'linear-gradient(135deg, var(--brand-lime, #B4F372) 0%, #EEFB63 50%, var(--brand-violet, #BC9AFF) 100%)',
|
|
216
222
|
}}
|
|
217
223
|
aria-label={t('demoFeedback.button.ariaLabel', 'Open feedback form')}
|
|
218
224
|
>
|
|
@@ -272,14 +278,14 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
|
|
|
272
278
|
{fieldErrors.email && <p className="text-xs text-status-error-text">{fieldErrors.email}</p>}
|
|
273
279
|
</div>
|
|
274
280
|
|
|
275
|
-
<
|
|
281
|
+
<Textarea
|
|
276
282
|
id="feedback-message"
|
|
277
283
|
rows={3}
|
|
278
284
|
placeholder={t('demoFeedback.form.message', 'Your message (optional)')}
|
|
279
285
|
value={message}
|
|
280
286
|
onChange={(e) => setMessage(e.target.value)}
|
|
281
287
|
disabled={submitState === 'sending'}
|
|
282
|
-
className="
|
|
288
|
+
className="resize-none"
|
|
283
289
|
/>
|
|
284
290
|
|
|
285
291
|
<label className="flex items-start gap-2.5 text-xs text-muted-foreground leading-relaxed">
|
|
@@ -359,12 +365,13 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
|
|
|
359
365
|
|
|
360
366
|
<Button
|
|
361
367
|
type="button"
|
|
362
|
-
className="mt-1 w-full gap-2"
|
|
368
|
+
className="mt-1 w-full gap-2 text-foreground"
|
|
363
369
|
disabled={submitState === 'sending'}
|
|
364
370
|
onClick={handleSubmit}
|
|
365
371
|
style={{
|
|
366
|
-
|
|
367
|
-
|
|
372
|
+
// Same brand-gradient as the floating CTA (135deg / 0-50-100,
|
|
373
|
+
// brand vars instead of hex literals to satisfy DS rules).
|
|
374
|
+
backgroundImage: 'linear-gradient(135deg, var(--brand-lime, #B4F372) 0%, #EEFB63 50%, var(--brand-violet, #BC9AFF) 100%)',
|
|
368
375
|
}}
|
|
369
376
|
>
|
|
370
377
|
{submitState === 'sending' ? (
|
|
@@ -54,11 +54,11 @@ export function GlobalNoticeBars({ demoModeEnabled }: { demoModeEnabled: boolean
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
|
-
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-
|
|
57
|
+
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-[70] flex flex-col items-center gap-3 px-4">
|
|
58
58
|
{showDemoNotice ? (
|
|
59
|
-
<div className="pointer-events-auto w-full max-w-4xl rounded-lg border border-
|
|
59
|
+
<div className="pointer-events-auto w-full max-w-4xl rounded-lg border border-amber-200 bg-amber-50/90 p-4 shadow-lg backdrop-blur supports-[backdrop-filter]:bg-amber-50/70 dark:border-amber-900/70 dark:bg-amber-950/40">
|
|
60
60
|
<div className="flex items-start gap-3">
|
|
61
|
-
<div className="flex-1 text-sm text-
|
|
61
|
+
<div className="flex-1 text-sm text-amber-900 dark:text-amber-50 space-y-1">
|
|
62
62
|
<p className="font-medium">{t('notices.demo.title', 'Demo Environment')}</p>
|
|
63
63
|
<p>
|
|
64
64
|
{t('notices.demo.description', 'This instance is provided for demo purposes only. Data may be reset at any time and is not retained for any guaranteed period.')}
|
|
@@ -69,22 +69,22 @@ export function GlobalNoticeBars({ demoModeEnabled }: { demoModeEnabled: boolean
|
|
|
69
69
|
href="https://github.com/open-mercato"
|
|
70
70
|
target="_blank"
|
|
71
71
|
rel="noreferrer"
|
|
72
|
-
className="underline font-medium hover:text-
|
|
72
|
+
className="underline font-medium hover:text-amber-800 dark:hover:text-amber-200"
|
|
73
73
|
>
|
|
74
74
|
{t('notices.demo.installLink', 'Install Open Mercato locally')}
|
|
75
75
|
</a>
|
|
76
76
|
. {t('notices.demo.reviewLinks', 'Review our')}{' '}
|
|
77
|
-
<Link className="underline font-medium hover:text-
|
|
77
|
+
<Link className="underline font-medium hover:text-amber-800 dark:hover:text-amber-200" href="/terms">
|
|
78
78
|
{t('common.terms')}
|
|
79
79
|
</Link>{' '}
|
|
80
80
|
{t('notices.demo.and', 'and')}{' '}
|
|
81
|
-
<Link className="underline font-medium hover:text-
|
|
81
|
+
<Link className="underline font-medium hover:text-amber-800 dark:hover:text-amber-200" href="/privacy">
|
|
82
82
|
{t('common.privacy')}
|
|
83
83
|
</Link>
|
|
84
84
|
.
|
|
85
85
|
</p>
|
|
86
86
|
</div>
|
|
87
|
-
<Button variant="ghost" size="icon" onClick={handleDismissDemo} className="shrink-0 text-
|
|
87
|
+
<Button variant="ghost" size="icon" onClick={handleDismissDemo} className="shrink-0 text-amber-900 dark:text-amber-100">
|
|
88
88
|
<X className="size-4" />
|
|
89
89
|
</Button>
|
|
90
90
|
</div>
|
|
@@ -135,16 +135,16 @@ export function StartPageContent({ showStartPage: initialShowStartPage, showOnbo
|
|
|
135
135
|
</section>
|
|
136
136
|
) : null}
|
|
137
137
|
|
|
138
|
-
<section className="rounded-lg border bg-
|
|
138
|
+
<section className="rounded-lg border bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-900 p-4">
|
|
139
139
|
<div className="flex items-start gap-3">
|
|
140
|
-
<Info className="size-5 text-
|
|
140
|
+
<Info className="size-5 text-blue-600 dark:text-blue-400 shrink-0 mt-0.5" />
|
|
141
141
|
<div className="flex-1">
|
|
142
|
-
<h3 className="text-sm font-semibold text-
|
|
143
|
-
<p className="text-sm text-
|
|
142
|
+
<h3 className="text-sm font-semibold text-blue-900 dark:text-blue-100 mb-1">{t('startPage.defaultPassword.title', 'Default Password')}</h3>
|
|
143
|
+
<p className="text-sm text-blue-800 dark:text-blue-200">
|
|
144
144
|
{t('startPage.defaultPassword.description1', 'The default password for all demo accounts is')}{' '}
|
|
145
|
-
<code className="px-1.5 py-0.5 rounded bg-
|
|
145
|
+
<code className="px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-900 font-mono text-xs">secret</code>.
|
|
146
146
|
{' '}{t('startPage.defaultPassword.description2', 'To change passwords, use the CLI command:')}{' '}
|
|
147
|
-
<code className="px-1.5 py-0.5 rounded bg-
|
|
147
|
+
<code className="px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-900 font-mono text-xs">yarn mercato auth set-password --email <email> --password <newPassword></code>
|
|
148
148
|
<span className="mt-2 block">{t('startPage.defaultPassword.description3', 'Demo account emails are printed in the terminal output during yarn initialize.')}</span>
|
|
149
149
|
</p>
|
|
150
150
|
</div>
|
|
@@ -1,59 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
-
|
|
5
|
-
import { cn } from "@open-mercato/shared/lib/utils"
|
|
6
|
-
|
|
7
|
-
const buttonVariants = cva(
|
|
8
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
-
{
|
|
10
|
-
variants: {
|
|
11
|
-
variant: {
|
|
12
|
-
default:
|
|
13
|
-
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
14
|
-
destructive:
|
|
15
|
-
"bg-destructive text-white shadow-xs hover:bg-destructive/90 aria-invalid:ring-destructive dark:aria-invalid:ring-destructive dark:bg-destructive/10",
|
|
16
|
-
outline:
|
|
17
|
-
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
18
|
-
secondary:
|
|
19
|
-
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
20
|
-
ghost:
|
|
21
|
-
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
22
|
-
link: "text-primary underline-offset-4 hover:underline",
|
|
23
|
-
},
|
|
24
|
-
size: {
|
|
25
|
-
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
26
|
-
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
27
|
-
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
28
|
-
icon: "size-9",
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
defaultVariants: {
|
|
32
|
-
variant: "default",
|
|
33
|
-
size: "default",
|
|
34
|
-
},
|
|
35
|
-
}
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
function Button({
|
|
39
|
-
className,
|
|
40
|
-
variant,
|
|
41
|
-
size,
|
|
42
|
-
asChild = false,
|
|
43
|
-
...props
|
|
44
|
-
}: React.ComponentProps<"button"> &
|
|
45
|
-
VariantProps<typeof buttonVariants> & {
|
|
46
|
-
asChild?: boolean
|
|
47
|
-
}) {
|
|
48
|
-
const Comp = asChild ? Slot : "button"
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<Comp
|
|
52
|
-
data-slot="button"
|
|
53
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
54
|
-
{...props}
|
|
55
|
-
/>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export { Button, buttonVariants }
|
|
1
|
+
export { Button, buttonVariants } from '@open-mercato/ui/primitives/button'
|
|
@@ -46,26 +46,34 @@ test.describe('TC-UMES-005: Phase L — Integration Extensions', () => {
|
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
test('TC-UMES-L03: wizard completes all 3 steps and outputs result', async ({ page }) => {
|
|
49
|
-
|
|
49
|
+
test.setTimeout(60_000)
|
|
50
|
+
// Step 1 — fill credentials (controlled inputs)
|
|
50
51
|
const apiKeyInput = page.locator('[data-crud-field-id="apiKey"] input')
|
|
51
52
|
const apiSecretInput = page.locator('[data-crud-field-id="apiSecret"] input')
|
|
52
53
|
await apiKeyInput.click()
|
|
53
|
-
await apiKeyInput.
|
|
54
|
-
await
|
|
55
|
-
await apiSecretInput.
|
|
56
|
-
|
|
54
|
+
await apiKeyInput.pressSequentially('test-key-123', { delay: 10 })
|
|
55
|
+
await apiSecretInput.click()
|
|
56
|
+
await apiSecretInput.pressSequentially('test-secret-456', { delay: 10 })
|
|
57
|
+
// Confirm React state caught up before validation-gated Next click
|
|
58
|
+
await expect(apiKeyInput).toHaveValue('test-key-123', { timeout: 5_000 })
|
|
59
|
+
await expect(apiSecretInput).toHaveValue('test-secret-456', { timeout: 5_000 })
|
|
57
60
|
await page.getByRole('button', { name: 'Next', exact: true }).click()
|
|
58
61
|
|
|
59
|
-
// Step 2 —
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// Step 2 — wait for transition (apiKey input gone, syncDirection rendered)
|
|
63
|
+
await expect(apiKeyInput).toHaveCount(0, { timeout: 10_000 })
|
|
64
|
+
// Wizard uses Radix Select in apps/mercato — interact via combobox + portal option
|
|
65
|
+
const syncTrigger = page.locator('[data-crud-field-id="syncDirection"] [role="combobox"]')
|
|
66
|
+
await expect(syncTrigger).toBeVisible({ timeout: 10_000 })
|
|
67
|
+
await syncTrigger.click()
|
|
68
|
+
await page.getByRole('option', { name: 'Bidirectional', exact: true }).click()
|
|
63
69
|
await page.getByRole('button', { name: 'Next', exact: true }).click()
|
|
64
70
|
|
|
65
|
-
// Step 3 —
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await
|
|
71
|
+
// Step 3 — wait for syncDirection gone, frequency rendered (Radix Select)
|
|
72
|
+
await expect(page.locator('[data-crud-field-id="syncDirection"]')).toHaveCount(0, { timeout: 10_000 })
|
|
73
|
+
const freqTrigger = page.locator('[data-crud-field-id="frequency"] [role="combobox"]')
|
|
74
|
+
await expect(freqTrigger).toBeVisible({ timeout: 10_000 })
|
|
75
|
+
await freqTrigger.click()
|
|
76
|
+
await page.getByRole('option', { name: 'Daily', exact: true }).click()
|
|
69
77
|
await page.getByRole('button', { name: 'Complete', exact: true }).click()
|
|
70
78
|
|
|
71
79
|
// Verify wizard result output
|
|
@@ -80,19 +88,22 @@ test.describe('TC-UMES-005: Phase L — Integration Extensions', () => {
|
|
|
80
88
|
})
|
|
81
89
|
|
|
82
90
|
test('TC-UMES-L04: wizard back button navigates to previous step', async ({ page }) => {
|
|
83
|
-
|
|
91
|
+
test.setTimeout(60_000)
|
|
92
|
+
// Fill step 1 (controlled inputs — flush state then advance)
|
|
84
93
|
const apiKeyInput = page.locator('[data-crud-field-id="apiKey"] input')
|
|
85
94
|
const apiSecretInput = page.locator('[data-crud-field-id="apiSecret"] input')
|
|
86
95
|
await apiKeyInput.click()
|
|
87
|
-
await apiKeyInput.
|
|
88
|
-
await
|
|
89
|
-
await apiSecretInput.
|
|
90
|
-
await
|
|
96
|
+
await apiKeyInput.pressSequentially('key', { delay: 10 })
|
|
97
|
+
await apiSecretInput.click()
|
|
98
|
+
await apiSecretInput.pressSequentially('secret', { delay: 10 })
|
|
99
|
+
await expect(apiKeyInput).toHaveValue('key', { timeout: 5_000 })
|
|
100
|
+
await expect(apiSecretInput).toHaveValue('secret', { timeout: 5_000 })
|
|
91
101
|
await page.getByRole('button', { name: 'Next', exact: true }).click()
|
|
92
102
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
// Step 2 — wait for transition (apiKey input gone, syncDirection rendered as Radix combobox)
|
|
104
|
+
await expect(apiKeyInput).toHaveCount(0, { timeout: 10_000 })
|
|
105
|
+
const syncTrigger = page.locator('[data-crud-field-id="syncDirection"] [role="combobox"]')
|
|
106
|
+
await expect(syncTrigger).toBeVisible({ timeout: 10_000 })
|
|
96
107
|
|
|
97
108
|
// Click back
|
|
98
109
|
await page.getByRole('button', { name: 'Back', exact: true }).click()
|
|
@@ -4,7 +4,7 @@ import { exampleTag, exampleErrorSchema } from '../../openapi'
|
|
|
4
4
|
|
|
5
5
|
export const metadata = {
|
|
6
6
|
GET: { requireAuth: true, requireFeatures: ['example.todos.view'] },
|
|
7
|
-
POST: { requireAuth: true, requireFeatures: ['example.todos.
|
|
7
|
+
POST: { requireAuth: true, requireFeatures: ['example.todos.view'] },
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export async function GET(_req: Request, ctx: { params: { id: string } }) {
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
5
5
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
} from '@open-mercato/ui/primitives/select'
|
|
6
13
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
14
|
import { registerIntegration, getAllIntegrations, getIntegrationTitle } from '@open-mercato/shared/modules/integrations/types'
|
|
8
15
|
|
|
@@ -244,32 +251,38 @@ export default function UmesIntegrationsPage() {
|
|
|
244
251
|
{currentStepId === 'scope' && (
|
|
245
252
|
<div data-crud-field-id="syncDirection" className="space-y-1">
|
|
246
253
|
<label className="text-sm font-medium">Sync Direction</label>
|
|
247
|
-
<
|
|
248
|
-
value={wizardData.syncDirection
|
|
249
|
-
|
|
250
|
-
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
254
|
+
<Select
|
|
255
|
+
value={wizardData.syncDirection || undefined}
|
|
256
|
+
onValueChange={(value) => handleWizardChange('syncDirection', value ?? '')}
|
|
251
257
|
>
|
|
252
|
-
<
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
<
|
|
256
|
-
|
|
258
|
+
<SelectTrigger>
|
|
259
|
+
<SelectValue placeholder="Select direction" />
|
|
260
|
+
</SelectTrigger>
|
|
261
|
+
<SelectContent>
|
|
262
|
+
<SelectItem value="push">Push</SelectItem>
|
|
263
|
+
<SelectItem value="pull">Pull</SelectItem>
|
|
264
|
+
<SelectItem value="bidirectional">Bidirectional</SelectItem>
|
|
265
|
+
</SelectContent>
|
|
266
|
+
</Select>
|
|
257
267
|
</div>
|
|
258
268
|
)}
|
|
259
269
|
|
|
260
270
|
{currentStepId === 'schedule' && (
|
|
261
271
|
<div data-crud-field-id="frequency" className="space-y-1">
|
|
262
272
|
<label className="text-sm font-medium">Frequency</label>
|
|
263
|
-
<
|
|
264
|
-
value={wizardData.frequency
|
|
265
|
-
|
|
266
|
-
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
273
|
+
<Select
|
|
274
|
+
value={wizardData.frequency || undefined}
|
|
275
|
+
onValueChange={(value) => handleWizardChange('frequency', value ?? '')}
|
|
267
276
|
>
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
<
|
|
272
|
-
|
|
277
|
+
<SelectTrigger>
|
|
278
|
+
<SelectValue placeholder="Select frequency" />
|
|
279
|
+
</SelectTrigger>
|
|
280
|
+
<SelectContent>
|
|
281
|
+
<SelectItem value="hourly">Hourly</SelectItem>
|
|
282
|
+
<SelectItem value="daily">Daily</SelectItem>
|
|
283
|
+
<SelectItem value="weekly">Weekly</SelectItem>
|
|
284
|
+
</SelectContent>
|
|
285
|
+
</Select>
|
|
273
286
|
</div>
|
|
274
287
|
)}
|
|
275
288
|
|
|
@@ -39,7 +39,7 @@ const checkoutTestComponentOverrides: ComponentOverride[] = [
|
|
|
39
39
|
React.createElement(
|
|
40
40
|
'div',
|
|
41
41
|
{
|
|
42
|
-
className: 'rounded-
|
|
42
|
+
className: 'rounded-2xl border border-dashed border-blue-300 bg-blue-50/40 p-3',
|
|
43
43
|
'data-testid': 'example-checkout-summary-wrapper',
|
|
44
44
|
},
|
|
45
45
|
React.createElement(Original, props as object)
|
|
@@ -57,7 +57,7 @@ const checkoutTestComponentOverrides: ComponentOverride[] = [
|
|
|
57
57
|
React.createElement(
|
|
58
58
|
'div',
|
|
59
59
|
{
|
|
60
|
-
className: 'rounded-
|
|
60
|
+
className: 'rounded-2xl border border-dashed border-amber-300 bg-amber-50/40 p-3',
|
|
61
61
|
'data-testid': 'example-checkout-help-wrapper',
|
|
62
62
|
},
|
|
63
63
|
React.createElement(Original, props as object)
|
|
@@ -4,6 +4,7 @@ import * as React from 'react'
|
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
7
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
7
8
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
8
9
|
import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
10
|
import { hydrateTodoSettings, type TodoSettings } from './config'
|
|
@@ -138,12 +139,12 @@ const TodoWidgetClient: React.FC<DashboardWidgetComponentProps<TodoSettings>> =
|
|
|
138
139
|
<label htmlFor="todo-page-size" className="text-xs font-medium uppercase text-muted-foreground">
|
|
139
140
|
{t('example.widgets.todo.settings.itemsLabel')}
|
|
140
141
|
</label>
|
|
141
|
-
<
|
|
142
|
+
<Input
|
|
142
143
|
id="todo-page-size"
|
|
143
144
|
type="number"
|
|
144
145
|
min={1}
|
|
145
146
|
max={20}
|
|
146
|
-
className="w-24
|
|
147
|
+
className="w-24"
|
|
147
148
|
value={value.pageSize}
|
|
148
149
|
onChange={(event) => onSettingsChange({ ...value, pageSize: Number(event.target.value) })}
|
|
149
150
|
/>
|
|
@@ -166,9 +167,9 @@ const TodoWidgetClient: React.FC<DashboardWidgetComponentProps<TodoSettings>> =
|
|
|
166
167
|
return (
|
|
167
168
|
<div className="space-y-4">
|
|
168
169
|
<div className="flex gap-2">
|
|
169
|
-
<
|
|
170
|
+
<Input
|
|
170
171
|
type="text"
|
|
171
|
-
className="flex-1
|
|
172
|
+
className="flex-1"
|
|
172
173
|
placeholder={t('example.widgets.todo.input.placeholder')}
|
|
173
174
|
value={draft}
|
|
174
175
|
onChange={(event) => setDraft(event.target.value)}
|
|
@@ -96,15 +96,15 @@ export default function CatalogSeoReportWidget(_props: InjectionWidgetComponentP
|
|
|
96
96
|
) : loading ? (
|
|
97
97
|
<p className="mt-2 text-xs text-muted-foreground">{t('common.loading', 'Loading…')}</p>
|
|
98
98
|
) : issues.length === 0 ? (
|
|
99
|
-
<p className="mt-2 text-xs text-
|
|
99
|
+
<p className="mt-2 text-xs text-emerald-700">{t('example.widgets.catalogSeoReport.healthy', 'All reviewed items look good!')}</p>
|
|
100
100
|
) : (
|
|
101
101
|
<ul className="mt-3 space-y-2">
|
|
102
102
|
{issues.map((issue) => (
|
|
103
|
-
<li key={issue.id} className="rounded border border-
|
|
103
|
+
<li key={issue.id} className="rounded border border-amber-200 dark:border-amber-900/70 bg-amber-50 dark:bg-amber-950/40 px-3 py-2">
|
|
104
104
|
<div className="flex items-center justify-between gap-3">
|
|
105
105
|
<div>
|
|
106
|
-
<div className="text-sm font-medium text-foreground">{issue.title}</div>
|
|
107
|
-
<div className="text-overline text-
|
|
106
|
+
<div className="text-sm font-medium text-foreground dark:text-amber-50">{issue.title}</div>
|
|
107
|
+
<div className="text-overline text-amber-800 dark:text-amber-300">{issue.issue}</div>
|
|
108
108
|
</div>
|
|
109
109
|
<Button asChild size="sm" variant="outline">
|
|
110
110
|
<a href={`/backend/catalog/products/${issue.id}`} className="text-xs">
|
package/template/src/modules/example/widgets/injection/customer-priority-detail/widget.client.tsx
CHANGED
|
@@ -5,6 +5,13 @@ import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules
|
|
|
5
5
|
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
6
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
7
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from '@open-mercato/ui/primitives/select'
|
|
8
15
|
|
|
9
16
|
type PriorityValue = 'low' | 'normal' | 'high' | 'critical'
|
|
10
17
|
type PriorityItem = { id?: string; priority?: string }
|
|
@@ -117,17 +124,21 @@ export default function CustomerPriorityDetailWidget({ context, data, disabled }
|
|
|
117
124
|
<div className="rounded-md border border-border p-3">
|
|
118
125
|
<div className="mb-1 text-sm font-medium text-foreground">{t('example.priority.detail.label')}</div>
|
|
119
126
|
<div className="text-xs text-muted-foreground mb-2">{t('example.priority.detail.description')}</div>
|
|
120
|
-
<
|
|
121
|
-
className="h-9 w-full rounded border border-input bg-background px-3 text-sm"
|
|
127
|
+
<Select
|
|
122
128
|
value={value}
|
|
123
|
-
|
|
129
|
+
onValueChange={(next) => { void handleChange({ target: { value: next } } as React.ChangeEvent<HTMLSelectElement>) }}
|
|
124
130
|
disabled={disabled || loading || saving}
|
|
125
131
|
>
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<
|
|
130
|
-
|
|
132
|
+
<SelectTrigger>
|
|
133
|
+
<SelectValue />
|
|
134
|
+
</SelectTrigger>
|
|
135
|
+
<SelectContent>
|
|
136
|
+
<SelectItem value="low">{t('example.priority.low')}</SelectItem>
|
|
137
|
+
<SelectItem value="normal">{t('example.priority.normal')}</SelectItem>
|
|
138
|
+
<SelectItem value="high">{t('example.priority.high')}</SelectItem>
|
|
139
|
+
<SelectItem value="critical">{t('example.priority.critical')}</SelectItem>
|
|
140
|
+
</SelectContent>
|
|
141
|
+
</Select>
|
|
131
142
|
{loading ? <div className="mt-2 text-xs text-muted-foreground">{t('example.priority.detail.loading')}</div> : null}
|
|
132
143
|
{saving ? <div className="mt-2 text-xs text-muted-foreground">{t('example.priority.detail.saving')}</div> : null}
|
|
133
144
|
{error ? <div className="mt-2 text-xs text-destructive">{error}</div> : null}
|
package/template/src/modules/example/widgets/injection/portal-recent-activity/widget.client.tsx
CHANGED
|
@@ -10,11 +10,11 @@ const MOCK_ACTIVITY = [
|
|
|
10
10
|
|
|
11
11
|
function ActivityIcon({ type }: { type: string }) {
|
|
12
12
|
const colors: Record<string, string> = {
|
|
13
|
-
login: 'bg-
|
|
14
|
-
profile: 'bg-
|
|
15
|
-
order: 'bg-
|
|
13
|
+
login: 'bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400',
|
|
14
|
+
profile: 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400',
|
|
15
|
+
order: 'bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400',
|
|
16
16
|
download: 'bg-violet-100 text-violet-600 dark:bg-violet-900/30 dark:text-violet-400',
|
|
17
|
-
security: 'bg-
|
|
17
|
+
security: 'bg-rose-100 text-rose-600 dark:bg-rose-900/30 dark:text-rose-400',
|
|
18
18
|
}
|
|
19
19
|
return (
|
|
20
20
|
<div className={`flex size-8 shrink-0 items-center justify-center rounded-lg text-xs font-bold ${colors[type] ?? 'bg-muted text-muted-foreground'}`}>
|
|
@@ -148,10 +148,10 @@ export default function SalesTodosWidget({ context }: InjectionWidgetComponentPr
|
|
|
148
148
|
</Button>
|
|
149
149
|
</form>
|
|
150
150
|
{lastEvent ? (
|
|
151
|
-
<div className="flex items-center gap-2 rounded bg-
|
|
152
|
-
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-
|
|
151
|
+
<div className="flex items-center gap-2 rounded bg-blue-50 px-3 py-1.5 text-xs text-blue-700 dark:bg-blue-950 dark:text-blue-300">
|
|
152
|
+
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-blue-500" />
|
|
153
153
|
SSE Event received: <code className="font-mono">{lastEvent.id}</code>
|
|
154
|
-
<span className="text-
|
|
154
|
+
<span className="text-blue-500/70">
|
|
155
155
|
{new Date(lastEvent.timestamp).toLocaleTimeString()}
|
|
156
156
|
</span>
|
|
157
157
|
</div>
|
package/template/src/modules/example_customers_sync/api/example-customers-sync/mappings/route.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
-
import { type Kysely } from 'kysely'
|
|
4
|
+
import { type Kysely, sql } from 'kysely'
|
|
5
5
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
6
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
7
7
|
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
POST: {
|
|
6
|
+
requireAuth: false,
|
|
7
|
+
rateLimit: { points: 3, duration: 60, keyPrefix: 'ratelimit_probe' },
|
|
8
|
+
},
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function POST() {
|
|
12
|
+
return Response.json({ ok: true })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const openApi: OpenApiRouteDoc = {
|
|
16
|
+
tag: 'RateLimitProbe',
|
|
17
|
+
methods: {
|
|
18
|
+
POST: {
|
|
19
|
+
summary: 'Test-only endpoint with per-route metadata.rateLimit — used to prove rate-limit leakage under OM_INTEGRATION_TEST',
|
|
20
|
+
tags: ['RateLimitProbe'],
|
|
21
|
+
responses: [
|
|
22
|
+
{
|
|
23
|
+
status: 200,
|
|
24
|
+
description: 'Always OK when under the points budget',
|
|
25
|
+
schema: z.object({ ok: z.literal(true) }),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
status: 429,
|
|
29
|
+
description: 'Rate limit exceeded (3 points / 60 s per client IP)',
|
|
30
|
+
schema: z.object({ error: z.string() }),
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
package/template/src/modules.ts
CHANGED
|
@@ -50,6 +50,7 @@ export const enabledModules: ModuleEntry[] = [
|
|
|
50
50
|
{ id: 'customer_accounts', from: '@open-mercato/core' },
|
|
51
51
|
{ id: 'portal', from: '@open-mercato/core' },
|
|
52
52
|
{ id: 'example', from: '@app' },
|
|
53
|
+
{ id: 'ratelimit_probe', from: '@app' },
|
|
53
54
|
]
|
|
54
55
|
|
|
55
56
|
if (enabledModules.some((entry) => entry.id === 'example')) {
|