@emulsify/core 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,32 +14,13 @@
14
14
  import fs from 'fs';
15
15
  import path, { resolve } from 'path';
16
16
  import { fileURLToPath, pathToFileURL } from 'url';
17
- import viteConfig from '../config/vite/vite.config.js';
18
17
  import { resolveEnvironment } from '../config/vite/environment.js';
19
- import {
20
- mergeReactSingletonOptimizeDeps,
21
- mergeReactSingletonResolve,
22
- } from '../config/vite/utils/react-singleton.js';
23
- import { twigExtensionModuleSpecifiers } from '../config/vite/twig-extensions.js';
24
18
  import {
25
19
  applyStorybookConfigOverrides,
26
20
  normalizeStorybookConfigOverrideModule,
27
21
  } from '../src/storybook/main-config.js';
28
-
29
- // Twig glob maps are provided by config/vite/plugins/virtual-twig-globs.js.
30
-
31
- const twigVirtualModuleIds = [
32
- 'virtual:emulsify-twig-globs',
33
- 'virtual:emulsify-twig-asset-sources',
34
- 'virtual:emulsify-twig-extension-installers',
35
- ];
36
-
37
- const twigRuntimeOptimizeDepsExclude = [
38
- ...twigVirtualModuleIds,
39
- '@emulsify/core/storybook/twig/source-function',
40
- '@emulsify/core/storybook/twig/source',
41
- '@emulsify/core/storybook/twig/resolver',
42
- ];
22
+ import { buildAssetStaticDirs } from './main-static-assets.js';
23
+ import { createViteFinal } from './main-vite.js';
43
24
 
44
25
  /**
45
26
  * Minimal subset of the resolved Emulsify environment used by this file.
@@ -70,27 +51,6 @@ const _filename = fileURLToPath(import.meta.url);
70
51
  */
71
52
  const _dirname = path.dirname(_filename);
72
53
 
73
- /**
74
- * The consuming project root for Storybook static mounts.
75
- *
76
- * Storybook loads this package config from different physical locations
77
- * depending on whether Core is linked locally or installed in node_modules, so
78
- * static paths must be rooted at the process cwd rather than this file.
79
- *
80
- * @type {string}
81
- */
82
- const projectRoot = process.cwd();
83
-
84
- /**
85
- * Vite-generated Storybook chunks should not share `/assets` with project
86
- * static files. Storybook copies staticDirs while the preview build runs, so
87
- * keeping generated chunks in a separate folder avoids concurrent writers in
88
- * `.out/assets`.
89
- *
90
- * @type {string}
91
- */
92
- const storybookViteAssetsDir = 'storybook-assets';
93
-
94
54
  /**
95
55
  * Reads an optional HTML fragment relative to this config file.
96
56
  *
@@ -110,111 +70,6 @@ function readOptionalHtmlFragment(relativePath) {
110
70
  return fs.readFileSync(fragmentPath, 'utf8');
111
71
  }
112
72
 
113
- /**
114
- * Keeps Storybook static directory config aligned to the consuming project.
115
- *
116
- * Storybook errors when a declared static directory is absent, so only expose
117
- * project asset directories that exist in the current workspace.
118
- *
119
- * @param {Array<string|{from: string, to: string}>} staticDirs - Static directory entries.
120
- * @returns {Array<string|{from: string, to: string}>} Existing static directory entries.
121
- */
122
- function existingStaticDirs(staticDirs) {
123
- const seen = new Set();
124
- const existing = [];
125
-
126
- for (const staticDir of staticDirs) {
127
- const directory =
128
- typeof staticDir === 'string' ? staticDir : staticDir.from;
129
-
130
- if (!directory || !fs.existsSync(directory)) continue;
131
-
132
- const key =
133
- typeof staticDir === 'string'
134
- ? staticDir
135
- : `${staticDir.from || ''}\0${staticDir.to || ''}`;
136
- if (seen.has(key)) continue;
137
-
138
- seen.add(key);
139
- existing.push(staticDir);
140
- }
141
-
142
- return existing;
143
- }
144
-
145
- /**
146
- * Build static directory mounts for normalized project asset roots.
147
- *
148
- * @param {StorybookEnvironment} env - Resolved project paths used by Storybook.
149
- * @returns {Array<string|{from: string, to: string}>} Static directory entries.
150
- */
151
- function buildAssetStaticDirs(env) {
152
- const configuredAssetRoots = Array.isArray(env.projectStructure?.assetRoots)
153
- ? env.projectStructure.assetRoots
154
- : [];
155
- const assetRoots = [
156
- ...configuredAssetRoots,
157
- path.resolve(projectRoot, 'assets'),
158
- path.resolve(projectRoot, 'src/assets'),
159
- ];
160
-
161
- return existingStaticDirs([
162
- ...assetRoots.map((root) => ({
163
- from: root,
164
- to: '/assets',
165
- })),
166
- {
167
- from: path.resolve(projectRoot, 'dist/assets'),
168
- to: '/assets',
169
- },
170
- {
171
- from: path.resolve(projectRoot, 'dist'),
172
- to: '/dist',
173
- },
174
- ]);
175
- }
176
-
177
- /**
178
- * Merge Storybook and project optimizeDeps excludes with Core Twig runtime IDs.
179
- *
180
- * Storybook's dependency optimizer runs before normal Vite virtual module
181
- * resolution. Core Twig runtime modules import virtual IDs that must stay in
182
- * the Vite module graph so Emulsify's virtual plugins can resolve them.
183
- *
184
- * @param {...string[]} excludeLists - Existing optimizeDeps exclude arrays.
185
- * @returns {string[]} Merged exclude list.
186
- */
187
- function mergeTwigRuntimeOptimizeDepsExcludes(...excludeLists) {
188
- return Array.from(
189
- new Set([
190
- ...excludeLists.flatMap((excludeList) =>
191
- Array.isArray(excludeList) ? excludeList : [],
192
- ),
193
- ...twigRuntimeOptimizeDepsExclude,
194
- ]),
195
- );
196
- }
197
-
198
- /**
199
- * Keep Emulsify Twig virtual imports out of Storybook dependency prebundles.
200
- *
201
- * @returns {import('esbuild').Plugin} Esbuild plugin for optimizeDeps.
202
- */
203
- function makeTwigVirtualModuleOptimizerPlugin() {
204
- return {
205
- name: 'emulsify-twig-virtual-modules',
206
- setup(build) {
207
- build.onResolve(
208
- { filter: /^virtual:emulsify-twig-(?:globs|asset-sources)$/ },
209
- (args) => ({
210
- path: args.path,
211
- external: true,
212
- }),
213
- );
214
- },
215
- };
216
- }
217
-
218
73
  /**
219
74
  * Reads optional project-level Storybook overrides.
220
75
  *
@@ -367,109 +222,12 @@ const baseConfig = {
367
222
  * @returns {string} Manager head markup with Emulsify additions appended.
368
223
  */
369
224
  managerHead: (head) => {
370
- // Keep the manager styling inline so consumers inherit the branded UI
371
- // without having to maintain a separate manager-only stylesheet.
372
- const inlineStyles = `
373
- <style>
374
- :root {
375
- --colors-emulsify-blue-100: #e6f5fc;
376
- --colors-emulsify-blue-200: #CCECFA;
377
- --colors-emulsify-blue-300: #99D9F4;
378
- --colors-emulsify-blue-400: #66c5ef;
379
- --colors-emulsify-blue-500: #33b2e9;
380
- --colors-emulsify-blue-600: #009fe4;
381
- --colors-emulsify-blue-700: #007FB6;
382
- --colors-emulsify-blue-800: #005f89;
383
- --colors-emulsify-blue-900: #00405b;
384
- --colors-emulsify-blue-1000: #00202e;
385
- --colors-purple: #8B1E7E;
386
- }
387
- .sidebar-container {
388
- background-color: var(--colors-emulsify-blue-900);
389
- }
390
- .sidebar-container .sidebar-subheading {
391
- color: var(--colors-emulsify-blue-200);
392
- font-size: 13px;
393
- letter-spacing: 0.15em;
394
- }
395
- .sidebar-container .sidebar-subheading button:focus {
396
- color: var(--colors-emulsify-blue-300);
397
- }
398
- /* Triangle icon. */
399
- .sidebar-container .sidebar-subheading button span {
400
- color: var(--colors-emulsify-blue-300);
401
- }
402
- .sidebar-container .search-field input {
403
- border-color: var(--colors-emulsify-blue-700);
404
- }
405
- .sidebar-container .search-field input:active {
406
- border-color: var(--colors-emulsify-blue-700);
407
- }
408
- .sidebar-container .search-result-recentlyOpened,
409
- .sidebar-container .search-result-back,
410
- .sidebar-container .search-result-clearHistory {
411
- color: var(--colors-emulsify-blue-300) !important;
412
- letter-spacing: 0.15em;
413
- }
414
- .sidebar-container .search-result-back span,
415
- .sidebar-container .search-result-back svg,
416
- .sidebar-container .search-result-clearHistory span,
417
- .sidebar-container .search-result-clearHistory svg {
418
- letter-spacing: normal;
419
- color: white;
420
- }
421
- .sidebar-container .sidebar-item svg {
422
- margin-top: 1px;
423
- }
424
- .sidebar-container .sidebar-item span {
425
- margin-top: 4px;
426
- }
427
- .sidebar-container .sidebar-subheading-action svg {
428
- color: var(--colors-emulsify-blue-400);
429
- }
430
- .sidebar-container .sidebar-subheading-action:hover svg {
431
- color: var(--colors-emulsify-blue-300);
432
- }
433
- .sidebar-header button[title="Shortcuts"] {
434
- box-shadow: none;
435
- border: 1px solid var(--colors-emulsify-blue-700);
436
- }
437
- .sidebar-header button[title="Shortcuts"]:active {
438
- border: 1px solid var(--colors-emulsify-blue-500);
439
- }
440
- .sidebar-header button[title="Shortcuts"]:focus {
441
- background: transparent;
442
- }
443
- #shortcuts {
444
- border-bottom-color: var(--colors-emulsify-blue-900) !important;
445
- }
446
- [role="main"]:not(:nth-child(3)) {
447
- top: 1rem !important;
448
- height: calc(100vh - 2rem) !important;
449
- }
450
- [role="main"] .os-host .os-content button:hover {
451
- background: var(--colors-emulsify-blue-100);
452
- }
453
- [role="main"] .os-host .os-content button:hover svg {
454
- color: var(--colors-emulsify-blue-900);
455
- }
456
- #panel-tab-content,
457
- #panel-tab-content>* {
458
- color: var(--colors-emulsify-blue-100) !important;
459
- }
460
- #panel-tab-content a,
461
- #panel-tab-content a span,
462
- #panel-tab-content a span svg {
463
- color: var(--colors-emulsify-blue-800);
464
- }
465
- #panel-tab-content>div>div>div>div>div>div {
466
- background: transparent;
467
- }
468
- #panel-tab-content>div>div>div>div>div>div>div {
469
- color: var(--colors-emulsify-blue-1000) !important;
470
- }
471
- </style>
472
- `;
225
+ const managerStyles = readOptionalHtmlFragment('./manager-head.css');
226
+ const inlineStyles = managerStyles
227
+ ? `<style>
228
+ ${managerStyles}
229
+ </style>`
230
+ : '';
473
231
  const externalManagerHtml = readOptionalHtmlFragment(
474
232
  '../../../../config/emulsify-core/storybook/manager-head.html',
475
233
  );
@@ -497,156 +255,7 @@ const baseConfig = {
497
255
  ${externalHtml}`;
498
256
  },
499
257
 
500
- /**
501
- * Merges Storybook's generated Vite config with Emulsify's shared Vite config.
502
- *
503
- * Storybook supplies a baseline config, but Emulsify still needs to expose
504
- * the resolved environment, expand filesystem access, and expose the Twig
505
- * virtual glob module used by the runtime resolver.
506
- *
507
- * @param {import('vite').UserConfig} config - Storybook's generated Vite config.
508
- * @returns {Promise<import('vite').UserConfig>} Final Vite config used by Storybook.
509
- */
510
- async viteFinal(config) {
511
- const { mergeConfig } = await import('vite');
512
- /** @type {StorybookEnvironment} */
513
- const env = resolvedStorybookEnv;
514
- const storybookBuildConfig = config?.build || {};
515
-
516
- // Keep using the `serve` branch of the shared Vite config here. Storybook
517
- // has historically consumed that branch, while `mode` still reflects
518
- // whether Storybook is running in development or production.
519
- const mode = config?.mode || 'development';
520
- const baseViteConfig =
521
- typeof viteConfig === 'function'
522
- ? await viteConfig({ command: 'serve', mode })
523
- : viteConfig;
524
- const existingDefine = (config && config.define) || {};
525
- const viteDefine = (baseViteConfig && baseViteConfig.define) || {};
526
-
527
- // Allow Storybook's dev server to read component sources from the project
528
- // root and any structure override paths used by Emulsify consumers.
529
- const allowList = new Set([
530
- ...(config?.server?.fs?.allow || []),
531
- env.projectDir,
532
- path.resolve(env.projectDir, 'src'),
533
- path.resolve(env.projectDir, 'components'),
534
- path.resolve(env.projectDir, 'dist'),
535
- ...(Array.isArray(env.projectStructure?.sourceRoots)
536
- ? env.projectStructure.sourceRoots
537
- : []),
538
- ...(Array.isArray(env.componentRoots) ? env.componentRoots : []),
539
- ...(Array.isArray(env.structureRoots) ? env.structureRoots : []),
540
- ...(env.namespaceRoots && typeof env.namespaceRoots === 'object'
541
- ? Object.values(env.namespaceRoots)
542
- : []),
543
- ...(Array.isArray(env.projectStructure?.assetRoots)
544
- ? env.projectStructure.assetRoots
545
- : []),
546
- ]);
547
-
548
- // Twig files are loaded through custom resolvers/plugins, so they need to
549
- // be treated as importable assets by Storybook's Vite pipeline.
550
- const assetsInclude = Array.from(
551
- new Set([
552
- ...(config.assetsInclude || []),
553
- ...(baseViteConfig.assetsInclude || []),
554
- '**/*.twig',
555
- ]),
556
- );
557
- const optimizeDepsInclude = mergeReactSingletonOptimizeDeps(
558
- baseViteConfig?.optimizeDeps?.include,
559
- config?.optimizeDeps?.include,
560
- [
561
- 'twig',
562
- '@emulsify/core/extensions/twig',
563
- ...twigExtensionModuleSpecifiers(env),
564
- ],
565
- );
566
-
567
- const mergedConfig = mergeConfig(config, {
568
- ...baseViteConfig,
569
- resolve: mergeReactSingletonResolve(baseViteConfig, config),
570
- define: {
571
- // Preserve shared and Storybook-provided constants, then publish the
572
- // resolved Emulsify environment to client-side code.
573
- ...viteDefine,
574
- ...existingDefine,
575
- __EMULSIFY_ENV__: JSON.stringify(env),
576
- 'globalThis.__EMULSIFY_ENV__': JSON.stringify(env),
577
- },
578
- server: {
579
- ...(baseViteConfig?.server || {}),
580
- fs: {
581
- allow: Array.from(allowList),
582
- },
583
- },
584
- assetsInclude,
585
- plugins: [...(baseViteConfig?.plugins || [])],
586
- esbuild: {
587
- // Some downstream code is authored as `.js` files containing JSX, so
588
- // keep Storybook's esbuild settings aligned with the shared Vite config.
589
- jsx: 'automatic',
590
- loader: 'jsx',
591
- include: /.*\.jsx?$/,
592
- exclude: [],
593
- },
594
- optimizeDeps: {
595
- ...(baseViteConfig?.optimizeDeps || {}),
596
- ...(config?.optimizeDeps || {}),
597
- include: optimizeDepsInclude,
598
- exclude: mergeTwigRuntimeOptimizeDepsExcludes(
599
- baseViteConfig?.optimizeDeps?.exclude,
600
- config?.optimizeDeps?.exclude,
601
- ),
602
- esbuildOptions: {
603
- ...(baseViteConfig?.optimizeDeps?.esbuildOptions || {}),
604
- ...(config?.optimizeDeps?.esbuildOptions || {}),
605
- plugins: [
606
- ...(baseViteConfig?.optimizeDeps?.esbuildOptions?.plugins || []),
607
- ...(config?.optimizeDeps?.esbuildOptions?.plugins || []),
608
- makeTwigVirtualModuleOptimizerPlugin(),
609
- ],
610
- loader: {
611
- ...(baseViteConfig?.optimizeDeps?.esbuildOptions?.loader || {}),
612
- ...(config?.optimizeDeps?.esbuildOptions?.loader || {}),
613
- // Pre-bundle `.js` dependencies with the JSX loader for packages
614
- // that ship JSX without a `.jsx` extension.
615
- '.js': 'jsx',
616
- },
617
- },
618
- },
619
- });
620
-
621
- return {
622
- ...mergedConfig,
623
- build: {
624
- ...(mergedConfig.build || {}),
625
- ...(storybookBuildConfig.outDir
626
- ? { outDir: storybookBuildConfig.outDir }
627
- : {}),
628
- assetsDir: storybookViteAssetsDir,
629
- emptyOutDir: false,
630
- },
631
- resolve: mergeReactSingletonResolve(mergedConfig),
632
- optimizeDeps: {
633
- ...(mergedConfig.optimizeDeps || {}),
634
- include: mergeReactSingletonOptimizeDeps(
635
- mergedConfig.optimizeDeps?.include,
636
- ),
637
- exclude: mergeTwigRuntimeOptimizeDepsExcludes(
638
- mergedConfig.optimizeDeps?.exclude,
639
- ),
640
- esbuildOptions: {
641
- ...(mergedConfig.optimizeDeps?.esbuildOptions || {}),
642
- loader: {
643
- ...(mergedConfig.optimizeDeps?.esbuildOptions?.loader || {}),
644
- '.js': 'jsx',
645
- },
646
- },
647
- },
648
- };
649
- },
258
+ viteFinal: createViteFinal(resolvedStorybookEnv),
650
259
  };
651
260
 
652
261
  /**
@@ -0,0 +1,120 @@
1
+ :root {
2
+ --colors-emulsify-blue-100: #e6f5fc;
3
+ --colors-emulsify-blue-200: #ccecfa;
4
+ --colors-emulsify-blue-300: #99d9f4;
5
+ --colors-emulsify-blue-400: #66c5ef;
6
+ --colors-emulsify-blue-500: #33b2e9;
7
+ --colors-emulsify-blue-600: #009fe4;
8
+ --colors-emulsify-blue-700: #007fb6;
9
+ --colors-emulsify-blue-800: #005f89;
10
+ --colors-emulsify-blue-900: #00405b;
11
+ --colors-emulsify-blue-1000: #00202e;
12
+ --colors-purple: #8b1e7e;
13
+ }
14
+
15
+ .sidebar-container {
16
+ background-color: var(--colors-emulsify-blue-900);
17
+ }
18
+
19
+ .sidebar-container .sidebar-subheading {
20
+ color: var(--colors-emulsify-blue-200);
21
+ font-size: 13px;
22
+ letter-spacing: 0.15em;
23
+ }
24
+
25
+ .sidebar-container .sidebar-subheading button:focus {
26
+ color: var(--colors-emulsify-blue-300);
27
+ }
28
+
29
+ /* Triangle icon. */
30
+ .sidebar-container .sidebar-subheading button span {
31
+ color: var(--colors-emulsify-blue-300);
32
+ }
33
+
34
+ .sidebar-container .search-field input {
35
+ border-color: var(--colors-emulsify-blue-700);
36
+ }
37
+
38
+ .sidebar-container .search-field input:active {
39
+ border-color: var(--colors-emulsify-blue-700);
40
+ }
41
+
42
+ .sidebar-container .search-result-recentlyOpened,
43
+ .sidebar-container .search-result-back,
44
+ .sidebar-container .search-result-clearHistory {
45
+ color: var(--colors-emulsify-blue-300) !important;
46
+ letter-spacing: 0.15em;
47
+ }
48
+
49
+ .sidebar-container .search-result-back span,
50
+ .sidebar-container .search-result-back svg,
51
+ .sidebar-container .search-result-clearHistory span,
52
+ .sidebar-container .search-result-clearHistory svg {
53
+ letter-spacing: normal;
54
+ color: white;
55
+ }
56
+
57
+ .sidebar-container .sidebar-item svg {
58
+ margin-top: 1px;
59
+ }
60
+
61
+ .sidebar-container .sidebar-item span {
62
+ margin-top: 4px;
63
+ }
64
+
65
+ .sidebar-container .sidebar-subheading-action svg {
66
+ color: var(--colors-emulsify-blue-400);
67
+ }
68
+
69
+ .sidebar-container .sidebar-subheading-action:hover svg {
70
+ color: var(--colors-emulsify-blue-300);
71
+ }
72
+
73
+ .sidebar-header button[title='Shortcuts'] {
74
+ box-shadow: none;
75
+ border: 1px solid var(--colors-emulsify-blue-700);
76
+ }
77
+
78
+ .sidebar-header button[title='Shortcuts']:active {
79
+ border: 1px solid var(--colors-emulsify-blue-500);
80
+ }
81
+
82
+ .sidebar-header button[title='Shortcuts']:focus {
83
+ background: transparent;
84
+ }
85
+
86
+ #shortcuts {
87
+ border-bottom-color: var(--colors-emulsify-blue-900) !important;
88
+ }
89
+
90
+ [role='main']:not(:nth-child(3)) {
91
+ top: 1rem !important;
92
+ height: calc(100vh - 2rem) !important;
93
+ }
94
+
95
+ [role='main'] .os-host .os-content button:hover {
96
+ background: var(--colors-emulsify-blue-100);
97
+ }
98
+
99
+ [role='main'] .os-host .os-content button:hover svg {
100
+ color: var(--colors-emulsify-blue-900);
101
+ }
102
+
103
+ #panel-tab-content,
104
+ #panel-tab-content > * {
105
+ color: var(--colors-emulsify-blue-100) !important;
106
+ }
107
+
108
+ #panel-tab-content a,
109
+ #panel-tab-content a span,
110
+ #panel-tab-content a span svg {
111
+ color: var(--colors-emulsify-blue-800);
112
+ }
113
+
114
+ #panel-tab-content > div > div > div > div > div > div {
115
+ background: transparent;
116
+ }
117
+
118
+ #panel-tab-content > div > div > div > div > div > div > div {
119
+ color: var(--colors-emulsify-blue-1000) !important;
120
+ }
@@ -14,12 +14,9 @@ import {
14
14
  applyStoryDecorators,
15
15
  renderPreviewStory,
16
16
  } from '../src/storybook/preview-decorator.js';
17
- import {
18
- attachStorybookBehaviors,
19
- fetchCSSFiles,
20
- getStorybookPlatformAdapter,
21
- setupTwig,
22
- } from './utils.js';
17
+ import { attachStorybookBehaviors } from '../src/storybook/platform-behaviors.js';
18
+ import { setupTwig } from '../src/storybook/twig/setup.js';
19
+ import { fetchCSSFiles, getStorybookPlatformAdapter } from './utils.js';
23
20
 
24
21
  const previewOverrideModules = import.meta.glob(
25
22
  [
@@ -89,7 +89,7 @@ export function getProjectMachineName() {
89
89
  : undefined;
90
90
  }
91
91
 
92
- // Keep these named exports stable for preview.js and downstream overrides.
92
+ // Keep these compatibility exports for downstream preview overrides.
93
93
  export {
94
94
  attachStorybookBehaviors,
95
95
  fetchCSSFiles,
package/README.md CHANGED
@@ -34,7 +34,7 @@ See [Version Evolution](docs/version-evolution.md) for more release history.
34
34
 
35
35
  Twig and React are equally valid ways to build component libraries with Emulsify Core. The right authoring model depends on the consuming project:
36
36
 
37
- - Use Twig for CMS themes and server-rendered template systems. Drupal has a dedicated adapter today. WordPress and Timber projects should currently use `platform: "none"` unless a project adds its own platform-specific behavior.
37
+ - Use Twig for CMS themes and server-rendered template systems. Drupal has a Drupal-specific adapter, and WordPress/Timber projects can use the intentionally neutral `wordpress` adapter. WordPress runtime integration belongs in `emulsify-wordpress-theme`.
38
38
  - Use React for standalone UI libraries, application components, or projects that already use React.
39
39
  - Use mixed Twig and React when a design system needs to document both CMS-rendered and JavaScript-rendered components in the same Storybook instance.
40
40
 
@@ -85,7 +85,7 @@ The documentation is split by task:
85
85
  | [Component Authoring](docs/component-authoring.md) | Choosing Twig, React, or mixed Storybook authoring and comparing component examples. |
86
86
  | [Storybook](docs/storybook.md) | Rendering Twig stories, using `renderTwig()`, understanding Twig runtime helpers, and mixing Twig with React stories. |
87
87
  | [Project Structure And Output](docs/project-structure.md) | Configuring `src/components`, root `./components`, `variant.structureImplementations`, and expected output paths. |
88
- | [Platform Adapters](docs/platform-adapters.md) | Understanding `none`, `drupal`, platform resolution order, and Drupal SDC behavior. |
88
+ | [Platform Adapters](docs/platform-adapters.md) | Understanding `none`, `wordpress`, `drupal`, platform resolution order, and Drupal SDC behavior. |
89
89
  | [Extension Points](docs/extension-points.md) | Adding Vite plugins, Tailwind CSS, Storybook preview overrides, and other framework tooling. |
90
90
  | [Performance](docs/performance.md) | Understanding sourcemaps, eager Twig imports, Tailwind scanning, copied files, and fixture validation. |
91
91
  | [Native Twig Extensions](docs/native-twig-extensions.md) | Using `bem()`, `add_attributes()`, and `switch/case/default/endswitch` in Twig.js. |
@@ -94,24 +94,25 @@ The documentation is split by task:
94
94
 
95
95
  ## Known Limitations
96
96
 
97
- - Implemented platform adapters are currently `none` and `drupal`. WordPress and Timber projects should currently use `platform: "none"`. This keeps Emulsify Core in platform-neutral mode while still supporting Twig-oriented component development. A dedicated WordPress adapter may be added later when WordPress-specific behavior is introduced. See [Platform Adapters](docs/platform-adapters.md).
97
+ - Implemented platform adapters are `none`, `wordpress`, and `drupal`. The `wordpress` adapter is intentionally neutral: it supports Core Twig authoring, Storybook, Vite, `bem()`, `add_attributes()`, `include()`, and `source()`, but it does not emulate WordPress or Timber PHP runtime behavior. Runtime integration belongs in `emulsify-wordpress-theme`. See [Platform Adapters](docs/platform-adapters.md).
98
98
  - Storybook's Twig resolver eagerly imports Twig modules and raw Twig source. This is reliable for `include()` and `source()`, but large Twig libraries should keep Storybook source roots intentional. See [Performance](docs/performance.md).
99
99
  - Production sourcemaps are enabled by default unless a project overrides Vite config through `config/emulsify-core/vite/plugins.*`. See [Performance](docs/performance.md).
100
100
  - Project extensions use the public `config/emulsify-core` directory: `config/emulsify-core/vite/plugins.*` for Vite, `config/emulsify-core/storybook/...` for Storybook, and `config/emulsify-core/a11y.config.js` for a11y. See [Extension Points](docs/extension-points.md).
101
101
  - Webpack-specific customizations must be migrated manually to Vite plugins or `extendConfig()`. See [Migration](docs/migration-4x.md).
102
- - Drupal SDC mirroring only applies when the Drupal adapter and SDC settings are enabled. `none` projects should expect output to remain in `dist/`. See [Platform Adapters](docs/platform-adapters.md).
102
+ - Drupal SDC mirroring only applies when the Drupal adapter and SDC settings are enabled. `none` and `wordpress` projects should expect output to remain in `dist/`. See [Platform Adapters](docs/platform-adapters.md).
103
103
 
104
104
  ## Supported Project Shapes
105
105
 
106
- Release-readiness coverage validates:
106
+ Core supports these project shapes:
107
107
 
108
108
  - Drupal SDC projects using `src/components`.
109
109
  - `none` platform Twig projects using `src/components`.
110
+ - `wordpress` platform Twig projects using `src/components`.
110
111
  - Root `./components` projects.
111
112
  - Projects using multiple `variant.structureImplementations`.
112
113
  - Mixed Twig + React Storybook projects.
113
114
 
114
- WordPress and Timber projects should currently use `platform: "none"`. This keeps Emulsify Core in platform-neutral mode while still supporting Twig-oriented component development. A dedicated WordPress adapter may be added later when WordPress-specific behavior is introduced. The implemented adapters in this package are currently `none` and `drupal`.
115
+ WordPress and Timber projects should use `platform: "wordpress"` when they want Core's neutral WordPress adapter. The adapter keeps output in `dist/`, loads Storybook CSS from `dist/**/*.css`, and leaves WordPress runtime behavior to `emulsify-wordpress-theme`.
115
116
 
116
117
  ## Public Imports
117
118
 
@@ -125,7 +126,7 @@ import { defineReactExtension } from '@emulsify/core/extensions/react';
125
126
 
126
127
  `defineReactExtension` is reserved for future React extension support. It currently returns the input unchanged. Adopting the import path is safe; the runtime is intentionally a no-op until the registry lands. See [Extension Points](docs/extension-points.md#public-imports).
127
128
 
128
- Vite consumers can import the shared config from `@emulsify/core/vite` and public Vite plugin helpers from `@emulsify/core/vite/plugins`.
129
+ Vite consumers can import the shared config from `@emulsify/core/vite`, public Vite plugin helpers from `@emulsify/core/vite/plugins`, and platform adapter helpers from `@emulsify/core/vite/platforms`.
129
130
 
130
131
  ## Contributing
131
132