@thorprovider/storefront 0.4.0-mvp.0 → 0.4.0-mvp.2
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/README.md +173 -153
- package/VENDOR.md +69 -0
- package/assets/openspec/config.yaml +38 -0
- package/assets/templates/AGENTS.md +76 -0
- package/assets/templates/app/app/[[...slug]]/page.tsx +42 -1
- package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
- package/assets/templates/app/app/globals.css +5 -8
- package/assets/templates/app/app/layout.tsx +1 -5
- package/assets/templates/app/app/robots.ts +9 -3
- package/assets/templates/app/app/sitemap.ts +64 -5
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +70 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +15 -5
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +71 -0
- package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +36 -4
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +82 -50
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -0
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +29 -14
- package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +31 -7
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +4 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +56 -26
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +165 -0
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +26 -7
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +2 -1
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
- package/assets/templates/features/checkout/handlers.ts +6 -0
- package/assets/templates/features/customer-auth/state.ts +10 -1
- package/assets/templates/views/account/index.tsx +1 -1
- package/assets/templates/views/account/registry.tsx +80 -19
- package/assets/templates/views/account/schema.ts +1 -0
- package/assets/templates/views/cart/content.ts +11 -0
- package/assets/templates/views/cart/data.ts +23 -0
- package/assets/templates/views/cart/index.tsx +53 -0
- package/assets/templates/views/cart/props.ts +12 -0
- package/assets/templates/views/cart/registry.tsx +81 -0
- package/assets/templates/views/cart/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +1 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +64 -6
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +2 -1
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
- package/assets/templates/views/category/index.tsx +76 -7
- package/assets/templates/views/category/props.ts +7 -0
- package/assets/templates/views/category/registry.tsx +19 -0
- package/assets/templates/views/category/schema.ts +13 -0
- package/assets/templates/views/home/assembly/assembly.json +9 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +22 -4
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +1 -1
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +2 -2
- package/assets/templates/views/home/data.ts +9 -0
- package/assets/templates/views/home/index.tsx +23 -5
- package/assets/templates/views/home/registry.tsx +2 -2
- package/assets/templates/views/product/data.ts +4 -2
- package/assets/templates/views/product/index.tsx +49 -5
- package/assets/templates/views/product/props.ts +7 -0
- package/assets/templates/views/product/registry.tsx +25 -0
- package/assets/templates/views/product/schema.ts +13 -0
- package/assets/templates/views/static/content.ts +4 -4
- package/assets/templates/views/static/index.tsx +1 -1
- package/bin/storefront.js +1231 -154
- package/examples/merchandising/category-led.json +37 -0
- package/examples/merchandising/collection-led.json +37 -0
- package/package.json +11 -7
- package/recipes/archetypes.json +9 -22
- package/recipes/catalog.json +40 -1
- package/recipes/features.json +25 -2
- package/recipes/recipe.schema.json +1 -1
- package/recipes/recipes.json +32 -30
- package/recipes/sections.json +10 -0
- package/recipes/validate.mjs +54 -0
- package/scripts/candidate.mjs +4 -4
- package/scripts/composers.mjs +25 -5
- package/scripts/env-file.mjs +86 -0
- package/scripts/file-classes.mjs +5 -0
- package/scripts/migrate-english-routes.mjs +276 -0
- package/scripts/pinning.mjs +74 -5
- package/scripts/plan-apply.mjs +149 -5
- package/scripts/reconcile.mjs +49 -11
- package/scripts/render-templates.mjs +97 -44
- package/scripts/section-config.mjs +253 -0
- package/scripts/starter.mjs +17 -0
- package/scripts/theme/color.mjs +185 -0
- package/scripts/theme/css-theme.mjs +233 -0
- package/scripts/theme/fonts.mjs +123 -0
- package/scripts/theme/import-export.mjs +125 -0
- package/scripts/theme/preset.mjs +93 -0
- package/scripts/theme/theme-plan.mjs +292 -0
- package/scripts/theme/tokens.mjs +137 -0
- package/scripts/view-assembly.mjs +47 -1
- package/skills/sf-feature/SKILL.md +32 -19
- package/skills/sf-setup/SKILL.md +79 -0
- package/skills/sf-theme-gen/SKILL.md +99 -36
- package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
- package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
- package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
- package/skills/sf-view-gen/SKILL.md +63 -40
- package/skills/sf-view-gen/assets/recipes/archetypes.json +9 -22
- package/skills/sf-view-gen/assets/recipes/catalog.json +40 -1
- package/skills/sf-view-gen/assets/recipes/features.json +25 -2
- package/skills/sf-view-gen/assets/recipes/recipes.json +32 -30
- package/skills/sf-view-gen/assets/recipes/sections.json +10 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
- package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
- package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
- package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
- package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
- package/skills/sf-view-gen/references/views.md +28 -37
- package/skills/sf-view-gen/scripts/validate.mjs +54 -0
- package/skills/storefront-workflow/SKILL.md +94 -31
- package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
- package/CHANGELOG.md +0 -84
- package/assets/templates/views/user-panel/index.tsx +0 -106
- package/assets/templates/views/user-panel/registry.tsx +0 -54
- package/assets/templates/views/user-panel/schema.ts +0 -79
- package/commands/sf-add-view.md +0 -57
- package/commands/sf-feature.md +0 -28
- package/commands/sf-theme.md +0 -26
- package/commands/sf-view.md +0 -67
- package/skills/building-storefronts/SKILL.md +0 -178
- package/skills/building-storefronts/references/frontend-integration.md +0 -229
- package/skills/json-render-remotion/SKILL.md +0 -111
- package/skills/json-render-solid/SKILL.md +0 -204
- package/skills/nextjs-shadcn/SKILL.md +0 -367
- package/skills/nextjs-shadcn/references/architecture.md +0 -499
- package/skills/nextjs-shadcn/references/project-setup.md +0 -162
- package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
- package/skills/nextjs-shadcn/references/sidebar.md +0 -274
- package/skills/nextjs-shadcn/references/styling.md +0 -555
- package/skills/shadcn-component-discovery/SKILL.md +0 -273
- package/skills/shadcn-component-discovery/references/registries.md +0 -226
- package/skills/shadcn-theming/SKILL.md +0 -104
- package/skills/storefront-best-practices/SKILL.md +0 -421
- package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
- package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
- package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
- package/skills/storefront-best-practices/reference/components/footer.md +0 -112
- package/skills/storefront-best-practices/reference/components/hero.md +0 -241
- package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
- package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
- package/skills/storefront-best-practices/reference/components/popups.md +0 -221
- package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
- package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
- package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
- package/skills/storefront-best-practices/reference/components/search.md +0 -101
- package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
- package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
- package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
- package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
- package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
- package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
- package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
- package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
- package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
- package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
- package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
- package/skills/storefront-best-practices/reference/medusa.md +0 -307
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Ecosystem preset resolution (task 4.1).
|
|
2
|
+
//
|
|
3
|
+
// A shadcn preset (name, code, or builder URL) is resolved by the *pinned*
|
|
4
|
+
// shadcn CLI inside an isolated staging directory seeded with the consumer's own
|
|
5
|
+
// configuration, using `apply <preset> --only theme,font`. Only the resulting
|
|
6
|
+
// theme tokens and font wiring are read back and merged; the consumer's
|
|
7
|
+
// component files, base/structural style, icon library and import aliases are
|
|
8
|
+
// never touched, and a full `apply` is never run against the live project.
|
|
9
|
+
|
|
10
|
+
import { cp, mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import { tmpdir } from 'node:os'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { spawnSync } from 'node:child_process'
|
|
15
|
+
import { readThemeCss } from './css-theme.mjs'
|
|
16
|
+
import { FONT_VARS, THEME_COLOR_TOKENS } from './tokens.mjs'
|
|
17
|
+
|
|
18
|
+
// Files copied into staging so shadcn sees the real consumer config. Never the
|
|
19
|
+
// component source — only what theme/font resolution needs.
|
|
20
|
+
const STAGING_FILES = ['package.json', 'components.json', 'tsconfig.json', 'app/globals.css', 'app/layout.tsx']
|
|
21
|
+
|
|
22
|
+
function slugify(value) {
|
|
23
|
+
return value
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
27
|
+
.replace(/^-+|-+$/g, '')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Default runner: stage the consumer config, run the pinned shadcn theme/font
|
|
31
|
+
// application, and let the caller read the staged stylesheet back.
|
|
32
|
+
export function makeShadcnRunner({ shadcnVersion, projectDir, run }) {
|
|
33
|
+
return async function runPreset(presetRef, stagingDir) {
|
|
34
|
+
for (const rel of STAGING_FILES) {
|
|
35
|
+
const from = join(projectDir, rel)
|
|
36
|
+
if (existsSync(from)) await cp(from, join(stagingDir, rel))
|
|
37
|
+
}
|
|
38
|
+
const args = ['dlx', `shadcn@${shadcnVersion}`, 'apply', presetRef, '--only', 'theme,font', '--yes']
|
|
39
|
+
return run('yarn', args, stagingDir)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function detectFonts(layout) {
|
|
44
|
+
const fonts = {}
|
|
45
|
+
const slots = { sans: 'sans', mono: 'mono', serif: 'serif' }
|
|
46
|
+
for (const [slot, word] of Object.entries(slots)) {
|
|
47
|
+
const m = layout.match(new RegExp(`variable:\\s*'--font-([a-z0-9-]+)-${word}'`))
|
|
48
|
+
if (m) fonts[slot] = { kind: 'google', value: m[1] }
|
|
49
|
+
}
|
|
50
|
+
return fonts
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a preset to normalized theme data. Returns
|
|
55
|
+
* `{ ok: true, tokens, fonts, raw }` or `{ ok: false, diagnostics }`. `runPreset`
|
|
56
|
+
* is injected so the resolution is testable without network access.
|
|
57
|
+
*/
|
|
58
|
+
export async function resolvePreset(presetRef, { projectDir, runPreset, keepStaging = false }) {
|
|
59
|
+
const stagingDir = await mkdtemp(join(tmpdir(), `cs-preset-${slugify(presetRef) || 'x'}-`))
|
|
60
|
+
try {
|
|
61
|
+
const result = await runPreset(presetRef, stagingDir)
|
|
62
|
+
if (result && result.status !== 0) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
diagnostics: [
|
|
66
|
+
{
|
|
67
|
+
code: 'E_PRESET',
|
|
68
|
+
severity: 'error',
|
|
69
|
+
message: `shadcn could not resolve preset "${presetRef}" (exit ${result.status ?? 'unknown'}). Check the name/code/URL and your network.`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const globalsPath = join(stagingDir, 'app', 'globals.css')
|
|
75
|
+
if (!existsSync(globalsPath)) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
diagnostics: [
|
|
79
|
+
{ code: 'E_PRESET', severity: 'error', message: `preset "${presetRef}" produced no app/globals.css in staging` },
|
|
80
|
+
],
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const css = await readFile(globalsPath, 'utf8')
|
|
84
|
+
const tokens = readThemeCss(css)
|
|
85
|
+
const layoutPath = join(stagingDir, 'app', 'layout.tsx')
|
|
86
|
+
const fonts = existsSync(layoutPath) ? detectFonts(await readFile(layoutPath, 'utf8')) : {}
|
|
87
|
+
return { ok: true, tokens, fonts, raw: css }
|
|
88
|
+
} finally {
|
|
89
|
+
if (!keepStaging) await rm(stagingDir, { recursive: true, force: true })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { spawnSync }
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Theme operation resolution: turn CLI inputs into a validated, deterministic
|
|
2
|
+
// change plan over app/globals.css (visual tokens) and app/layout.tsx (font
|
|
3
|
+
// wiring), then render the resulting files.
|
|
4
|
+
//
|
|
5
|
+
// One operation is resolved in a fixed precedence order (design decision 2):
|
|
6
|
+
// current state -> imported source -> brand -> primary/accent -> radius/font
|
|
7
|
+
// shortcuts -> explicit `--set` setters.
|
|
8
|
+
// Later steps win. The plan is computed entirely in memory so a dry-run and a
|
|
9
|
+
// real run share one code path, and a failure leaves the project untouched.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
bestForeground,
|
|
13
|
+
contrastRatio,
|
|
14
|
+
formatOklch,
|
|
15
|
+
isNamedColor,
|
|
16
|
+
parseColor,
|
|
17
|
+
shiftLightness,
|
|
18
|
+
NAMED_COLOR_NAMES,
|
|
19
|
+
} from './color.mjs'
|
|
20
|
+
import { applyDeclarations, readThemeCss, fillMissingTokens } from './css-theme.mjs'
|
|
21
|
+
import { BRAND_TOKENS, ACCENT_TOKENS, RUNTIME_ONLY_TOKENS, RUNTIME_TOKEN_DEFAULTS, THEME_COLOR_TOKENS } from './tokens.mjs'
|
|
22
|
+
|
|
23
|
+
const SCOPES = ['light', 'dark', 'shared']
|
|
24
|
+
|
|
25
|
+
function diagnostic(code, message, extra = {}) {
|
|
26
|
+
return { code, severity: extra.severity ?? 'error', message, ...extra }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A `--set` entry: `light.primary=value`, `dark.accent-foreground=value`, or
|
|
30
|
+
// `shared.radius=value`. The token name is written without the leading `--`.
|
|
31
|
+
export function parseSetter(entry) {
|
|
32
|
+
const eq = entry.indexOf('=')
|
|
33
|
+
if (eq === -1) return { error: `--set expects <scope>.<token>=<value> (got "${entry}")` }
|
|
34
|
+
const target = entry.slice(0, eq)
|
|
35
|
+
const value = entry.slice(eq + 1)
|
|
36
|
+
const dot = target.indexOf('.')
|
|
37
|
+
if (dot === -1) return { error: `--set target must be <scope>.<token> (got "${target}")` }
|
|
38
|
+
const scope = target.slice(0, dot)
|
|
39
|
+
const name = target.slice(dot + 1)
|
|
40
|
+
if (!SCOPES.includes(scope)) return { error: `--set scope must be one of ${SCOPES.join(', ')} (got "${scope}")` }
|
|
41
|
+
if (!name) return { error: `--set is missing a token name (got "${target}")` }
|
|
42
|
+
if (value === '') return { error: `--set is missing a value (got "${entry}")` }
|
|
43
|
+
return { scope, name, value }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Resolve and validate the whole operation before any write. Returns
|
|
47
|
+
// `{ operation, diagnostics }`; a non-empty error diagnostic list means the
|
|
48
|
+
// caller must abort without touching the project.
|
|
49
|
+
export function normalizeOperation(input = {}) {
|
|
50
|
+
const diagnostics = []
|
|
51
|
+
const operation = {
|
|
52
|
+
mode: input.mode ?? null,
|
|
53
|
+
brand: input.brand ?? null,
|
|
54
|
+
primary: input.primary ?? null,
|
|
55
|
+
accent: input.accent ?? null,
|
|
56
|
+
radius: input.radius ?? null,
|
|
57
|
+
fontSans: input.fontSans ?? null,
|
|
58
|
+
fontMono: input.fontMono ?? null,
|
|
59
|
+
fontSerif: input.fontSerif ?? null,
|
|
60
|
+
setters: [],
|
|
61
|
+
preset: input.preset ?? null,
|
|
62
|
+
presetOnly: input.presetOnly ?? null,
|
|
63
|
+
importSource: input.importSource ?? null,
|
|
64
|
+
exportPath: input.exportPath ?? null,
|
|
65
|
+
dryRun: Boolean(input.dryRun),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (operation.mode !== null && !['light', 'dark', 'system'].includes(operation.mode)) {
|
|
69
|
+
diagnostics.push(diagnostic('E_MODE', `--mode must be one of light, dark, system (got "${operation.mode}")`))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resolveColor = (label, value) => {
|
|
73
|
+
if (value === null) return null
|
|
74
|
+
const parsed = parseColor(value)
|
|
75
|
+
if (!parsed) {
|
|
76
|
+
diagnostics.push(
|
|
77
|
+
diagnostic(
|
|
78
|
+
'E_COLOR',
|
|
79
|
+
`--${label} expects a color name (${NAMED_COLOR_NAMES.join(', ')}), a #hex, or an oklch(...) value (got "${value}")`,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
return parsed
|
|
85
|
+
}
|
|
86
|
+
operation.brandColor = resolveColor('brand', operation.brand)
|
|
87
|
+
operation.primaryColor = resolveColor('primary', operation.primary)
|
|
88
|
+
operation.accentColor = resolveColor('accent', operation.accent)
|
|
89
|
+
|
|
90
|
+
if (operation.radius !== null && !/^(0|\d*\.?\d+)(rem|px|em)$/.test(String(operation.radius))) {
|
|
91
|
+
diagnostics.push(diagnostic('E_LENGTH', `--radius expects a CSS length such as 0.75rem or 8px (got "${operation.radius}")`))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const resolveFont = (label, value) => {
|
|
95
|
+
if (value === null) return null
|
|
96
|
+
if (typeof value === 'string' && (value.startsWith('google:') || value.startsWith('system:'))) {
|
|
97
|
+
const [kind, rest] = [value.slice(0, value.indexOf(':')), value.slice(value.indexOf(':') + 1)]
|
|
98
|
+
if (!rest.trim()) {
|
|
99
|
+
diagnostics.push(diagnostic('E_FONT', `--${label} "${kind}:" is missing a ${kind === 'google' ? 'family' : 'stack'}`))
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
return { kind, value: rest.trim() }
|
|
103
|
+
}
|
|
104
|
+
diagnostics.push(
|
|
105
|
+
diagnostic('E_FONT', `--${label} must be google:<Family> or system:<stack> (got "${value}")`),
|
|
106
|
+
)
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
operation.fonts = {
|
|
110
|
+
sans: resolveFont('font-sans', operation.fontSans),
|
|
111
|
+
mono: resolveFont('font-mono', operation.fontMono),
|
|
112
|
+
serif: resolveFont('font-serif', operation.fontSerif),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const seen = new Set()
|
|
116
|
+
for (const entry of input.sets ?? []) {
|
|
117
|
+
const parsed = parseSetter(entry)
|
|
118
|
+
if (parsed.error) {
|
|
119
|
+
diagnostics.push(diagnostic('E_SET', parsed.error))
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
const key = `${parsed.scope}.${parsed.name}`
|
|
123
|
+
if (seen.has(key)) {
|
|
124
|
+
diagnostics.push(diagnostic('E_SET_DUP', `--set assigns ${key} more than once`))
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
seen.add(key)
|
|
128
|
+
operation.setters.push(parsed)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (operation.presetOnly !== null && operation.preset === null) {
|
|
132
|
+
diagnostics.push(diagnostic('E_ONLY', '--only requires --preset'))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (operation.exportPath && hasMutation(operation)) {
|
|
136
|
+
diagnostics.push(diagnostic('E_EXPORT_MIX', '--export cannot be combined with theme mutations; export writes only the export file'))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { operation, diagnostics }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function hasMutation(operation) {
|
|
143
|
+
return Boolean(
|
|
144
|
+
operation.mode ||
|
|
145
|
+
operation.brand ||
|
|
146
|
+
operation.primary ||
|
|
147
|
+
operation.accent ||
|
|
148
|
+
operation.radius ||
|
|
149
|
+
operation.fontSans ||
|
|
150
|
+
operation.fontMono ||
|
|
151
|
+
operation.fontSerif ||
|
|
152
|
+
operation.setters.length ||
|
|
153
|
+
operation.preset ||
|
|
154
|
+
operation.importSource,
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The primary/brand family derived from one color, per mode.
|
|
159
|
+
export function brandFamily(color, mode) {
|
|
160
|
+
const fg = bestForeground(color).color
|
|
161
|
+
const hover = shiftLightness(color, mode === 'light' ? -0.08 : 0.1)
|
|
162
|
+
const soft = { l: mode === 'light' ? 0.93 : 0.28, c: Math.min(color.c, 0.06), h: color.h }
|
|
163
|
+
return {
|
|
164
|
+
primary: formatOklch(color),
|
|
165
|
+
'primary-foreground': formatOklch(fg),
|
|
166
|
+
'primary-hover': formatOklch(hover),
|
|
167
|
+
'primary-soft': formatOklch(soft),
|
|
168
|
+
ring: formatOklch(color),
|
|
169
|
+
nav: formatOklch(color),
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function accentFamily(color) {
|
|
174
|
+
return {
|
|
175
|
+
accent: formatOklch(color),
|
|
176
|
+
'accent-foreground': formatOklch(bestForeground(color).color),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Text/background pairs whose contrast the operation should report on.
|
|
181
|
+
const CONTRAST_PAIRS = [
|
|
182
|
+
['primary', 'primary-foreground'],
|
|
183
|
+
['accent', 'accent-foreground'],
|
|
184
|
+
['background', 'foreground'],
|
|
185
|
+
['card', 'card-foreground'],
|
|
186
|
+
['popover', 'popover-foreground'],
|
|
187
|
+
['secondary', 'secondary-foreground'],
|
|
188
|
+
['muted', 'muted-foreground'],
|
|
189
|
+
['destructive', 'destructive-foreground'],
|
|
190
|
+
['promo', 'promo-foreground'],
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
// Build the change plan for `css` (globals.css content). Returns
|
|
194
|
+
// `{ css, changed, diagnostics, values }`. `changed` lists `scope.token` keys
|
|
195
|
+
// whose value actually changed; `values` is the resolved palette.
|
|
196
|
+
export function planCss(css, operation) {
|
|
197
|
+
const diagnostics = []
|
|
198
|
+
const edits = []
|
|
199
|
+
const push = (scope, name, value) => edits.push({ scope, name, value })
|
|
200
|
+
|
|
201
|
+
const modes = ['light', 'dark']
|
|
202
|
+
|
|
203
|
+
// Imported source (local CSS export, registry theme or a prior storefront
|
|
204
|
+
// export) is applied first so brand/primary/accent and explicit setters can
|
|
205
|
+
// override it.
|
|
206
|
+
if (operation.importSource) {
|
|
207
|
+
let imported = null
|
|
208
|
+
if (operation.importSource.type === 'css') imported = readThemeCss(operation.importSource.css)
|
|
209
|
+
else if (operation.importSource.type === 'tokens') imported = operation.importSource.tokens
|
|
210
|
+
if (imported) {
|
|
211
|
+
for (const mode of modes) {
|
|
212
|
+
for (const [rawName, value] of Object.entries(imported[mode] ?? {})) {
|
|
213
|
+
const name = rawName.startsWith('--') ? rawName : `--${rawName}`
|
|
214
|
+
push(mode, name, value)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Brand / primary: the same color in both modes.
|
|
221
|
+
const primarySource = operation.brandColor ?? operation.primaryColor
|
|
222
|
+
if (primarySource) {
|
|
223
|
+
for (const mode of modes) {
|
|
224
|
+
const family = brandFamily(primarySource, mode)
|
|
225
|
+
for (const token of BRAND_TOKENS) push(mode, `--${token}`, family[token])
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (operation.accentColor) {
|
|
230
|
+
const family = accentFamily(operation.accentColor)
|
|
231
|
+
for (const mode of modes) for (const token of ACCENT_TOKENS) push(mode, `--${token}`, family[token])
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (operation.radius !== null) push('light', '--radius', String(operation.radius))
|
|
235
|
+
|
|
236
|
+
// Explicit setters last: they win over shortcuts and imports.
|
|
237
|
+
for (const setter of operation.setters) {
|
|
238
|
+
if (setter.scope === 'shared') push('light', `--${setter.name}`, setter.value)
|
|
239
|
+
else push(setter.scope, `--${setter.name}`, setter.value)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let next = applyDeclarations(css, edits)
|
|
243
|
+
|
|
244
|
+
// Completeness: the stylesheet owns every runtime token. Fill only what is
|
|
245
|
+
// absent so an existing (possibly branded) value is never replaced.
|
|
246
|
+
next = fillMissingTokens(next, [...THEME_COLOR_TOKENS, ...RUNTIME_ONLY_TOKENS], RUNTIME_TOKEN_DEFAULTS)
|
|
247
|
+
|
|
248
|
+
// Report measured contrast for the pairs that changed.
|
|
249
|
+
const before = readThemeCss(css)
|
|
250
|
+
const after = readThemeCss(next)
|
|
251
|
+
const changed = []
|
|
252
|
+
for (const edit of edits) {
|
|
253
|
+
const scope = edit.scope === 'shared' ? 'light' : edit.scope
|
|
254
|
+
if (before[scope][edit.name] !== edit.value) changed.push(`${scope}.${edit.name}`)
|
|
255
|
+
}
|
|
256
|
+
for (const mode of modes) {
|
|
257
|
+
for (const [bg, fg] of CONTRAST_PAIRS) {
|
|
258
|
+
const bgVal = after[mode][`--${bg}`]
|
|
259
|
+
const fgVal = after[mode][`--${fg}`]
|
|
260
|
+
if (!bgVal || !fgVal) continue
|
|
261
|
+
const touched = changed.includes(`${mode}.--${bg}`) || changed.includes(`${mode}.--${fg}`)
|
|
262
|
+
if (!touched) continue
|
|
263
|
+
const bgColor = parseColor(bgVal)
|
|
264
|
+
const fgColor = parseColor(fgVal)
|
|
265
|
+
if (!bgColor || !fgColor) continue
|
|
266
|
+
const ratio = contrastRatio(bgColor, fgColor)
|
|
267
|
+
if (ratio < 4.5) {
|
|
268
|
+
diagnostics.push(
|
|
269
|
+
diagnostic('W_CONTRAST', `${mode}: --${bg}/--${fg} contrast is ${ratio.toFixed(2)}:1 (below 4.5:1)`, {
|
|
270
|
+
severity: 'warning',
|
|
271
|
+
mode,
|
|
272
|
+
pair: [bg, fg],
|
|
273
|
+
ratio: Number(ratio.toFixed(2)),
|
|
274
|
+
}),
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { css: next, changed, diagnostics, values: after }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// High-level: compute the plan for a project's current file contents.
|
|
284
|
+
export function planTheme({ css, operation }) {
|
|
285
|
+
const { operation: op, diagnostics: opDiagnostics } = normalizeOperation(operation)
|
|
286
|
+
if (opDiagnostics.some((d) => d.severity === 'error')) {
|
|
287
|
+
return { ok: false, diagnostics: opDiagnostics }
|
|
288
|
+
}
|
|
289
|
+
const planned = planCss(css, op)
|
|
290
|
+
const diagnostics = [...opDiagnostics, ...planned.diagnostics]
|
|
291
|
+
return { ok: true, css: planned.css, changed: planned.changed, values: planned.values, diagnostics, operation: op }
|
|
292
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Token metadata for the theme controls: which tokens the CSS owns, which
|
|
2
|
+
// belong to the brand family, and the geometry/font variables the CLI can edit.
|
|
3
|
+
//
|
|
4
|
+
// RUNTIME_TOKEN_DEFAULTS mirrors @thorprovider/runtime/contracts
|
|
5
|
+
// (theme-tokens.ts). The CLI is a standalone Node program and cannot import the
|
|
6
|
+
// runtime TypeScript, so the values are embedded here exactly as the runtime
|
|
7
|
+
// declares them; `theme-tokens.parity.test.ts` parses the runtime source and
|
|
8
|
+
// fails if the two drift. These values are the *fallback* the scaffold writes
|
|
9
|
+
// when a generated stylesheet is missing a runtime-only token — never an
|
|
10
|
+
// override of a value a developer already set.
|
|
11
|
+
|
|
12
|
+
// The 29 tokens in the runtime theme contract, in contract order.
|
|
13
|
+
export const THEME_COLOR_TOKENS = [
|
|
14
|
+
'background', 'foreground', 'card', 'card-foreground', 'popover', 'popover-foreground',
|
|
15
|
+
'primary', 'primary-foreground', 'primary-hover', 'primary-soft', 'secondary',
|
|
16
|
+
'secondary-foreground', 'muted', 'muted-foreground', 'accent', 'accent-foreground',
|
|
17
|
+
'border', 'input', 'ring', 'destructive', 'destructive-foreground', 'nav', 'promo',
|
|
18
|
+
'promo-foreground', 'stock', 'urgent', 'success', 'warning', 'error',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
// Tokens written by a brand/primary/accent change as one coherent family.
|
|
22
|
+
export const BRAND_TOKENS = ['primary', 'primary-foreground', 'primary-hover', 'primary-soft', 'ring', 'nav']
|
|
23
|
+
export const ACCENT_TOKENS = ['accent', 'accent-foreground']
|
|
24
|
+
|
|
25
|
+
// Geometry + font variables editable through the bounded external surface. The
|
|
26
|
+
// radius scale is derived from a single `--radius` base (shadcn convention).
|
|
27
|
+
export const RADIUS_BASE_VAR = '--radius'
|
|
28
|
+
export const RADIUS_SCALE = { sm: 'calc(var(--radius) - 4px)', md: 'calc(var(--radius) - 2px)', lg: 'var(--radius)', xl: 'calc(var(--radius) + 4px)' }
|
|
29
|
+
export const FONT_VARS = ['--font-sans', '--font-mono', '--font-serif']
|
|
30
|
+
|
|
31
|
+
export const RUNTIME_TOKEN_DEFAULTS = {
|
|
32
|
+
light: {
|
|
33
|
+
background: 'oklch(0.98 0.002 90)',
|
|
34
|
+
foreground: 'oklch(0.15 0 0)',
|
|
35
|
+
card: 'oklch(1 0 0)',
|
|
36
|
+
'card-foreground': 'oklch(0.15 0 0)',
|
|
37
|
+
popover: 'oklch(1 0 0)',
|
|
38
|
+
'popover-foreground': 'oklch(0.15 0 0)',
|
|
39
|
+
primary: 'oklch(0.15 0 0)',
|
|
40
|
+
'primary-foreground': 'oklch(0.98 0.002 90)',
|
|
41
|
+
'primary-hover': 'oklch(0.3 0.01 0)',
|
|
42
|
+
'primary-soft': 'oklch(0.92 0.003 90)',
|
|
43
|
+
secondary: 'oklch(0.96 0.003 90)',
|
|
44
|
+
'secondary-foreground': 'oklch(0.15 0 0)',
|
|
45
|
+
muted: 'oklch(0.94 0.003 90)',
|
|
46
|
+
'muted-foreground': 'oklch(0.45 0 0)',
|
|
47
|
+
accent: 'oklch(0.65 0.15 25)',
|
|
48
|
+
'accent-foreground': 'oklch(1 0 0)',
|
|
49
|
+
border: 'oklch(0.88 0.003 90)',
|
|
50
|
+
input: 'oklch(0.92 0.003 90)',
|
|
51
|
+
ring: 'oklch(0.15 0 0)',
|
|
52
|
+
destructive: 'oklch(0.55 0.2 25)',
|
|
53
|
+
'destructive-foreground': 'oklch(1 0 0)',
|
|
54
|
+
nav: 'oklch(0.15 0 0)',
|
|
55
|
+
promo: 'oklch(0.8 0.15 45)',
|
|
56
|
+
'promo-foreground': 'oklch(1 0 0)',
|
|
57
|
+
stock: 'oklch(0.55 0.18 155)',
|
|
58
|
+
urgent: 'oklch(0.55 0.2 27)',
|
|
59
|
+
success: 'oklch(0.55 0.18 155)',
|
|
60
|
+
warning: 'oklch(0.65 0.16 75)',
|
|
61
|
+
error: 'oklch(0.55 0.2 27)',
|
|
62
|
+
},
|
|
63
|
+
dark: {
|
|
64
|
+
background: 'oklch(0.145 0 0)',
|
|
65
|
+
foreground: 'oklch(0.985 0 0)',
|
|
66
|
+
card: 'oklch(0.145 0 0)',
|
|
67
|
+
'card-foreground': 'oklch(0.985 0 0)',
|
|
68
|
+
popover: 'oklch(0.145 0 0)',
|
|
69
|
+
'popover-foreground': 'oklch(0.985 0 0)',
|
|
70
|
+
primary: 'oklch(0.985 0 0)',
|
|
71
|
+
'primary-foreground': 'oklch(0.205 0 0)',
|
|
72
|
+
'primary-hover': 'oklch(0.85 0.01 0)',
|
|
73
|
+
'primary-soft': 'oklch(0.269 0 0)',
|
|
74
|
+
secondary: 'oklch(0.269 0 0)',
|
|
75
|
+
'secondary-foreground': 'oklch(0.985 0 0)',
|
|
76
|
+
muted: 'oklch(0.269 0 0)',
|
|
77
|
+
'muted-foreground': 'oklch(0.708 0 0)',
|
|
78
|
+
accent: 'oklch(0.269 0 0)',
|
|
79
|
+
'accent-foreground': 'oklch(0.985 0 0)',
|
|
80
|
+
border: 'oklch(0.269 0 0)',
|
|
81
|
+
input: 'oklch(0.269 0 0)',
|
|
82
|
+
ring: 'oklch(0.439 0 0)',
|
|
83
|
+
destructive: 'oklch(0.396 0.141 25.723)',
|
|
84
|
+
'destructive-foreground': 'oklch(0.637 0.237 25.331)',
|
|
85
|
+
nav: 'oklch(0.985 0 0)',
|
|
86
|
+
promo: 'oklch(0.6 0.16 45)',
|
|
87
|
+
'promo-foreground': 'oklch(1 0 0)',
|
|
88
|
+
stock: 'oklch(0.6 0.14 155)',
|
|
89
|
+
urgent: 'oklch(0.6 0.18 27)',
|
|
90
|
+
success: 'oklch(0.6 0.14 155)',
|
|
91
|
+
warning: 'oklch(0.7 0.14 75)',
|
|
92
|
+
error: 'oklch(0.6 0.18 27)',
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The subset that historically lived only in the runtime injection (not in the
|
|
97
|
+
// shadcn-generated stylesheet). The scaffold fills exactly these when absent.
|
|
98
|
+
export const RUNTIME_ONLY_TOKENS = [
|
|
99
|
+
'primary-hover', 'primary-soft', 'nav', 'promo', 'promo-foreground', 'stock', 'urgent', 'error',
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
// Every CSS declaration the generated stylesheet must own so the runtime palette
|
|
103
|
+
// injection can be removed without losing a token: the 29 runtime tokens plus the
|
|
104
|
+
// block contract's foreground pairs.
|
|
105
|
+
export const REQUIRED_CSS_TOKENS = [
|
|
106
|
+
...THEME_COLOR_TOKENS,
|
|
107
|
+
'success-foreground',
|
|
108
|
+
'warning-foreground',
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
// Fallback values the scaffold fills for a missing required token — never an
|
|
112
|
+
// override of a value already present. Success/warning use the block contract's
|
|
113
|
+
// AA-conformant pair (mirrored by assets/templates/app/app/globals.css), the rest
|
|
114
|
+
// mirror the runtime contract.
|
|
115
|
+
export const DEFAULT_GLOBALS_TOKENS = {
|
|
116
|
+
light: {
|
|
117
|
+
...RUNTIME_TOKEN_DEFAULTS.light,
|
|
118
|
+
success: 'oklch(0.47 0.14 155)',
|
|
119
|
+
'success-foreground': 'oklch(0.98 0.01 155)',
|
|
120
|
+
warning: 'oklch(0.55 0.18 75)',
|
|
121
|
+
'warning-foreground': 'oklch(0.98 0.02 75)',
|
|
122
|
+
},
|
|
123
|
+
dark: {
|
|
124
|
+
...RUNTIME_TOKEN_DEFAULTS.dark,
|
|
125
|
+
success: 'oklch(0.6 0.13 155)',
|
|
126
|
+
'success-foreground': 'oklch(0.1 0.02 155)',
|
|
127
|
+
warning: 'oklch(0.7 0.15 75)',
|
|
128
|
+
'warning-foreground': 'oklch(0.1 0.02 75)',
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function isThemeColorToken(name) {
|
|
133
|
+
return THEME_COLOR_TOKENS.includes(name)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const THEME_MODES = ['light', 'dark', 'system']
|
|
137
|
+
export const DEFAULT_THEME_MODE = 'system'
|
|
@@ -153,6 +153,38 @@ function renderEditorialRegistry(entry, def) {
|
|
|
153
153
|
return ` ${def.elementType}: ${def.registryExport}.${def.elementType},`
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// One data-bound instance's element: like an editorial element, but its props
|
|
157
|
+
// merge the user-owned editorial content (`sectionContent[id]`) with the
|
|
158
|
+
// resolved per-instance selection delivered through the view state. The
|
|
159
|
+
// `variant` selects the verified presentation; `products`/`status` are dynamic
|
|
160
|
+
// state refs resolved by json-render, so the server module never fetches or
|
|
161
|
+
// imports the presentation components.
|
|
162
|
+
function renderDataBoundElement(entry, def) {
|
|
163
|
+
const variant =
|
|
164
|
+
typeof entry.variant === 'string' && entry.variant.length > 0
|
|
165
|
+
? entry.variant
|
|
166
|
+
: def.defaultVariant
|
|
167
|
+
if (typeof variant !== 'string' || variant.length === 0) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Data-bound section "${entry.type}" instance "${entry.id}" has no variant and no declared defaultVariant.`,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
return [
|
|
173
|
+
` '${entry.id}': {`,
|
|
174
|
+
` type: '${def.elementType}',`,
|
|
175
|
+
` props: {`,
|
|
176
|
+
` ...(${SECTION_CONTENT_EXPORT}['${entry.id}'] ?? {}),`,
|
|
177
|
+
` sectionId: '${entry.id}',`,
|
|
178
|
+
` sectionType: '${entry.type}',`,
|
|
179
|
+
` variant: '${variant}',`,
|
|
180
|
+
` products: { $state: '/sections/${entry.id}/products' },`,
|
|
181
|
+
` status: { $state: '/sections/${entry.id}/status' },`,
|
|
182
|
+
` },`,
|
|
183
|
+
` on: { addClickedToCart: { action: 'addClickedToCart' }, openProduct: { action: 'openProduct' } },`,
|
|
184
|
+
` },`,
|
|
185
|
+
].join('\n')
|
|
186
|
+
}
|
|
187
|
+
|
|
156
188
|
/**
|
|
157
189
|
* Assemble a view's derived `index.tsx` and `registry.tsx` from the view's
|
|
158
190
|
* single ordered composition (E3 task 5.1).
|
|
@@ -194,8 +226,10 @@ export function assembleView(templateDir, selection = {}, sections = null) {
|
|
|
194
226
|
const elements = []
|
|
195
227
|
const catalog = []
|
|
196
228
|
const registry = []
|
|
229
|
+
const registryTypes = new Set()
|
|
197
230
|
const bindings = []
|
|
198
231
|
const slotIds = []
|
|
232
|
+
const dataBoundIds = []
|
|
199
233
|
const sectionPropsModules = new Set()
|
|
200
234
|
const sectionRegistryImports = new Map()
|
|
201
235
|
for (const entry of ordered) {
|
|
@@ -231,11 +265,22 @@ export function assembleView(templateDir, selection = {}, sections = null) {
|
|
|
231
265
|
`View "${meta.view}" has no editorial fragment for section "${entry.type}".`,
|
|
232
266
|
)
|
|
233
267
|
}
|
|
234
|
-
elements.push(
|
|
268
|
+
elements.push(
|
|
269
|
+
def.dataBound === true
|
|
270
|
+
? renderDataBoundElement(entry, def)
|
|
271
|
+
: renderEditorialElement(entry, def),
|
|
272
|
+
)
|
|
235
273
|
registry.push(renderEditorialRegistry(entry, def))
|
|
274
|
+
// A registry binding is keyed by the element type, so several instances of
|
|
275
|
+
// the same section type (for example two `products` sections on one view)
|
|
276
|
+
// share a single registry entry. Emitting one per instance would produce
|
|
277
|
+
// duplicate object keys in the derived `registry.tsx`.
|
|
278
|
+
if (registryTypes.has(def.elementType)) registry.pop()
|
|
279
|
+
else registryTypes.add(def.elementType)
|
|
236
280
|
slotIds.push(`'${entry.id}'`)
|
|
237
281
|
sectionPropsModules.add(def.propsModule ?? './content')
|
|
238
282
|
sectionRegistryImports.set(def.registryModule, def.registryExport)
|
|
283
|
+
if (def.dataBound === true) dataBoundIds.push(`'${entry.id}'`)
|
|
239
284
|
}
|
|
240
285
|
|
|
241
286
|
const indexSkeleton = readFileSync(path.join(assemblyDir(templateDir), 'index.skeleton.tsx'), 'utf8')
|
|
@@ -246,6 +291,7 @@ export function assembleView(templateDir, selection = {}, sections = null) {
|
|
|
246
291
|
|
|
247
292
|
const index = indexSkeleton
|
|
248
293
|
.replace('@@SECTION_IMPORTS@@', () => renderSectionImports(sectionPropsModules))
|
|
294
|
+
.replace('@@SECTION_LOADERS@@', () => `[${dataBoundIds.join(', ')}]`)
|
|
249
295
|
.replace('@@ELEMENTS@@', () => elements.join('\n'))
|
|
250
296
|
.replace('@@CATALOG@@', () => catalog.join('\n'))
|
|
251
297
|
.replace('@@SLOT_IDS@@', () => slotIds.join(', '))
|