@emulsify/core 4.0.4 → 4.1.1

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,65 +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
- return staticDirs.filter((staticDir) => {
124
- const directory =
125
- typeof staticDir === 'string' ? staticDir : staticDir.from;
126
-
127
- return directory && fs.existsSync(directory);
128
- });
129
- }
130
-
131
- /**
132
- * Merge Storybook and project optimizeDeps excludes with Core Twig runtime IDs.
133
- *
134
- * Storybook's dependency optimizer runs before normal Vite virtual module
135
- * resolution. Core Twig runtime modules import virtual IDs that must stay in
136
- * the Vite module graph so Emulsify's virtual plugins can resolve them.
137
- *
138
- * @param {...string[]} excludeLists - Existing optimizeDeps exclude arrays.
139
- * @returns {string[]} Merged exclude list.
140
- */
141
- function mergeTwigRuntimeOptimizeDepsExcludes(...excludeLists) {
142
- return Array.from(
143
- new Set([
144
- ...excludeLists.flatMap((excludeList) =>
145
- Array.isArray(excludeList) ? excludeList : [],
146
- ),
147
- ...twigRuntimeOptimizeDepsExclude,
148
- ]),
149
- );
150
- }
151
-
152
- /**
153
- * Keep Emulsify Twig virtual imports out of Storybook dependency prebundles.
154
- *
155
- * @returns {import('esbuild').Plugin} Esbuild plugin for optimizeDeps.
156
- */
157
- function makeTwigVirtualModuleOptimizerPlugin() {
158
- return {
159
- name: 'emulsify-twig-virtual-modules',
160
- setup(build) {
161
- build.onResolve(
162
- { filter: /^virtual:emulsify-twig-(?:globs|asset-sources)$/ },
163
- (args) => ({
164
- path: args.path,
165
- external: true,
166
- }),
167
- );
168
- },
169
- };
170
- }
171
-
172
73
  /**
173
74
  * Reads optional project-level Storybook overrides.
174
75
  *
@@ -265,22 +166,7 @@ const baseConfig = {
265
166
  *
266
167
  * @type {Array<string|{from: string, to: string}>}
267
168
  */
268
- staticDirs: [
269
- ...existingStaticDirs([
270
- {
271
- from: path.resolve(projectRoot, 'assets'),
272
- to: '/assets',
273
- },
274
- {
275
- from: path.resolve(projectRoot, 'dist/assets'),
276
- to: '/assets',
277
- },
278
- {
279
- from: path.resolve(projectRoot, 'dist'),
280
- to: '/dist',
281
- },
282
- ]),
283
- ],
169
+ staticDirs: buildAssetStaticDirs(resolvedStorybookEnv),
284
170
 
285
171
  /**
286
172
  * Enable the default addon set used by Emulsify.
@@ -336,109 +222,12 @@ const baseConfig = {
336
222
  * @returns {string} Manager head markup with Emulsify additions appended.
337
223
  */
338
224
  managerHead: (head) => {
339
- // Keep the manager styling inline so consumers inherit the branded UI
340
- // without having to maintain a separate manager-only stylesheet.
341
- const inlineStyles = `
342
- <style>
343
- :root {
344
- --colors-emulsify-blue-100: #e6f5fc;
345
- --colors-emulsify-blue-200: #CCECFA;
346
- --colors-emulsify-blue-300: #99D9F4;
347
- --colors-emulsify-blue-400: #66c5ef;
348
- --colors-emulsify-blue-500: #33b2e9;
349
- --colors-emulsify-blue-600: #009fe4;
350
- --colors-emulsify-blue-700: #007FB6;
351
- --colors-emulsify-blue-800: #005f89;
352
- --colors-emulsify-blue-900: #00405b;
353
- --colors-emulsify-blue-1000: #00202e;
354
- --colors-purple: #8B1E7E;
355
- }
356
- .sidebar-container {
357
- background-color: var(--colors-emulsify-blue-900);
358
- }
359
- .sidebar-container .sidebar-subheading {
360
- color: var(--colors-emulsify-blue-200);
361
- font-size: 13px;
362
- letter-spacing: 0.15em;
363
- }
364
- .sidebar-container .sidebar-subheading button:focus {
365
- color: var(--colors-emulsify-blue-300);
366
- }
367
- /* Triangle icon. */
368
- .sidebar-container .sidebar-subheading button span {
369
- color: var(--colors-emulsify-blue-300);
370
- }
371
- .sidebar-container .search-field input {
372
- border-color: var(--colors-emulsify-blue-700);
373
- }
374
- .sidebar-container .search-field input:active {
375
- border-color: var(--colors-emulsify-blue-700);
376
- }
377
- .sidebar-container .search-result-recentlyOpened,
378
- .sidebar-container .search-result-back,
379
- .sidebar-container .search-result-clearHistory {
380
- color: var(--colors-emulsify-blue-300) !important;
381
- letter-spacing: 0.15em;
382
- }
383
- .sidebar-container .search-result-back span,
384
- .sidebar-container .search-result-back svg,
385
- .sidebar-container .search-result-clearHistory span,
386
- .sidebar-container .search-result-clearHistory svg {
387
- letter-spacing: normal;
388
- color: white;
389
- }
390
- .sidebar-container .sidebar-item svg {
391
- margin-top: 1px;
392
- }
393
- .sidebar-container .sidebar-item span {
394
- margin-top: 4px;
395
- }
396
- .sidebar-container .sidebar-subheading-action svg {
397
- color: var(--colors-emulsify-blue-400);
398
- }
399
- .sidebar-container .sidebar-subheading-action:hover svg {
400
- color: var(--colors-emulsify-blue-300);
401
- }
402
- .sidebar-header button[title="Shortcuts"] {
403
- box-shadow: none;
404
- border: 1px solid var(--colors-emulsify-blue-700);
405
- }
406
- .sidebar-header button[title="Shortcuts"]:active {
407
- border: 1px solid var(--colors-emulsify-blue-500);
408
- }
409
- .sidebar-header button[title="Shortcuts"]:focus {
410
- background: transparent;
411
- }
412
- #shortcuts {
413
- border-bottom-color: var(--colors-emulsify-blue-900) !important;
414
- }
415
- [role="main"]:not(:nth-child(3)) {
416
- top: 1rem !important;
417
- height: calc(100vh - 2rem) !important;
418
- }
419
- [role="main"] .os-host .os-content button:hover {
420
- background: var(--colors-emulsify-blue-100);
421
- }
422
- [role="main"] .os-host .os-content button:hover svg {
423
- color: var(--colors-emulsify-blue-900);
424
- }
425
- #panel-tab-content,
426
- #panel-tab-content>* {
427
- color: var(--colors-emulsify-blue-100) !important;
428
- }
429
- #panel-tab-content a,
430
- #panel-tab-content a span,
431
- #panel-tab-content a span svg {
432
- color: var(--colors-emulsify-blue-800);
433
- }
434
- #panel-tab-content>div>div>div>div>div>div {
435
- background: transparent;
436
- }
437
- #panel-tab-content>div>div>div>div>div>div>div {
438
- color: var(--colors-emulsify-blue-1000) !important;
439
- }
440
- </style>
441
- `;
225
+ const managerStyles = readOptionalHtmlFragment('./manager-head.css');
226
+ const inlineStyles = managerStyles
227
+ ? `<style>
228
+ ${managerStyles}
229
+ </style>`
230
+ : '';
442
231
  const externalManagerHtml = readOptionalHtmlFragment(
443
232
  '../../../../config/emulsify-core/storybook/manager-head.html',
444
233
  );
@@ -466,156 +255,7 @@ const baseConfig = {
466
255
  ${externalHtml}`;
467
256
  },
468
257
 
469
- /**
470
- * Merges Storybook's generated Vite config with Emulsify's shared Vite config.
471
- *
472
- * Storybook supplies a baseline config, but Emulsify still needs to expose
473
- * the resolved environment, expand filesystem access, and expose the Twig
474
- * virtual glob module used by the runtime resolver.
475
- *
476
- * @param {import('vite').UserConfig} config - Storybook's generated Vite config.
477
- * @returns {Promise<import('vite').UserConfig>} Final Vite config used by Storybook.
478
- */
479
- async viteFinal(config) {
480
- const { mergeConfig } = await import('vite');
481
- /** @type {StorybookEnvironment} */
482
- const env = resolvedStorybookEnv;
483
- const storybookBuildConfig = config?.build || {};
484
-
485
- // Keep using the `serve` branch of the shared Vite config here. Storybook
486
- // has historically consumed that branch, while `mode` still reflects
487
- // whether Storybook is running in development or production.
488
- const mode = config?.mode || 'development';
489
- const baseViteConfig =
490
- typeof viteConfig === 'function'
491
- ? await viteConfig({ command: 'serve', mode })
492
- : viteConfig;
493
- const existingDefine = (config && config.define) || {};
494
- const viteDefine = (baseViteConfig && baseViteConfig.define) || {};
495
-
496
- // Allow Storybook's dev server to read component sources from the project
497
- // root and any structure override paths used by Emulsify consumers.
498
- const allowList = new Set([
499
- ...(config?.server?.fs?.allow || []),
500
- env.projectDir,
501
- path.resolve(env.projectDir, 'src'),
502
- path.resolve(env.projectDir, 'components'),
503
- path.resolve(env.projectDir, 'dist'),
504
- ...(Array.isArray(env.projectStructure?.sourceRoots)
505
- ? env.projectStructure.sourceRoots
506
- : []),
507
- ...(Array.isArray(env.componentRoots) ? env.componentRoots : []),
508
- ...(Array.isArray(env.structureRoots) ? env.structureRoots : []),
509
- ...(env.namespaceRoots && typeof env.namespaceRoots === 'object'
510
- ? Object.values(env.namespaceRoots)
511
- : []),
512
- ...(Array.isArray(env.projectStructure?.assetRoots)
513
- ? env.projectStructure.assetRoots
514
- : []),
515
- ]);
516
-
517
- // Twig files are loaded through custom resolvers/plugins, so they need to
518
- // be treated as importable assets by Storybook's Vite pipeline.
519
- const assetsInclude = Array.from(
520
- new Set([
521
- ...(config.assetsInclude || []),
522
- ...(baseViteConfig.assetsInclude || []),
523
- '**/*.twig',
524
- ]),
525
- );
526
- const optimizeDepsInclude = mergeReactSingletonOptimizeDeps(
527
- baseViteConfig?.optimizeDeps?.include,
528
- config?.optimizeDeps?.include,
529
- [
530
- 'twig',
531
- '@emulsify/core/extensions/twig',
532
- ...twigExtensionModuleSpecifiers(env),
533
- ],
534
- );
535
-
536
- const mergedConfig = mergeConfig(config, {
537
- ...baseViteConfig,
538
- resolve: mergeReactSingletonResolve(baseViteConfig, config),
539
- define: {
540
- // Preserve shared and Storybook-provided constants, then publish the
541
- // resolved Emulsify environment to client-side code.
542
- ...viteDefine,
543
- ...existingDefine,
544
- __EMULSIFY_ENV__: JSON.stringify(env),
545
- 'globalThis.__EMULSIFY_ENV__': JSON.stringify(env),
546
- },
547
- server: {
548
- ...(baseViteConfig?.server || {}),
549
- fs: {
550
- allow: Array.from(allowList),
551
- },
552
- },
553
- assetsInclude,
554
- plugins: [...(baseViteConfig?.plugins || [])],
555
- esbuild: {
556
- // Some downstream code is authored as `.js` files containing JSX, so
557
- // keep Storybook's esbuild settings aligned with the shared Vite config.
558
- jsx: 'automatic',
559
- loader: 'jsx',
560
- include: /.*\.jsx?$/,
561
- exclude: [],
562
- },
563
- optimizeDeps: {
564
- ...(baseViteConfig?.optimizeDeps || {}),
565
- ...(config?.optimizeDeps || {}),
566
- include: optimizeDepsInclude,
567
- exclude: mergeTwigRuntimeOptimizeDepsExcludes(
568
- baseViteConfig?.optimizeDeps?.exclude,
569
- config?.optimizeDeps?.exclude,
570
- ),
571
- esbuildOptions: {
572
- ...(baseViteConfig?.optimizeDeps?.esbuildOptions || {}),
573
- ...(config?.optimizeDeps?.esbuildOptions || {}),
574
- plugins: [
575
- ...(baseViteConfig?.optimizeDeps?.esbuildOptions?.plugins || []),
576
- ...(config?.optimizeDeps?.esbuildOptions?.plugins || []),
577
- makeTwigVirtualModuleOptimizerPlugin(),
578
- ],
579
- loader: {
580
- ...(baseViteConfig?.optimizeDeps?.esbuildOptions?.loader || {}),
581
- ...(config?.optimizeDeps?.esbuildOptions?.loader || {}),
582
- // Pre-bundle `.js` dependencies with the JSX loader for packages
583
- // that ship JSX without a `.jsx` extension.
584
- '.js': 'jsx',
585
- },
586
- },
587
- },
588
- });
589
-
590
- return {
591
- ...mergedConfig,
592
- build: {
593
- ...(mergedConfig.build || {}),
594
- ...(storybookBuildConfig.outDir
595
- ? { outDir: storybookBuildConfig.outDir }
596
- : {}),
597
- assetsDir: storybookViteAssetsDir,
598
- emptyOutDir: false,
599
- },
600
- resolve: mergeReactSingletonResolve(mergedConfig),
601
- optimizeDeps: {
602
- ...(mergedConfig.optimizeDeps || {}),
603
- include: mergeReactSingletonOptimizeDeps(
604
- mergedConfig.optimizeDeps?.include,
605
- ),
606
- exclude: mergeTwigRuntimeOptimizeDepsExcludes(
607
- mergedConfig.optimizeDeps?.exclude,
608
- ),
609
- esbuildOptions: {
610
- ...(mergedConfig.optimizeDeps?.esbuildOptions || {}),
611
- loader: {
612
- ...(mergedConfig.optimizeDeps?.esbuildOptions?.loader || {}),
613
- '.js': 'jsx',
614
- },
615
- },
616
- },
617
- };
618
- },
258
+ viteFinal: createViteFinal(resolvedStorybookEnv),
619
259
  };
620
260
 
621
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
+ }
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  import { getRules } from 'axe-core';
6
- import React from 'react';
7
6
  import { defaultDecorateStory, useEffect } from 'storybook/preview-api';
8
7
  import Twig from 'twig';
9
8
  import { twigExtensionInstallers } from 'virtual:emulsify-twig-extension-installers';
@@ -12,15 +11,12 @@ import {
12
11
  normalizePreviewOverrideModule,
13
12
  } from '../src/storybook/preview-parameters.js';
14
13
  import {
15
- renderHtmlStoryResult,
16
- withLegacyStoryToString,
17
- } from '../src/storybook/render-twig.js';
18
- import {
19
- attachStorybookBehaviors,
20
- fetchCSSFiles,
21
- getStorybookPlatformAdapter,
22
- setupTwig,
23
- } from './utils.js';
14
+ applyStoryDecorators,
15
+ renderPreviewStory,
16
+ } from '../src/storybook/preview-decorator.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';
24
20
 
25
21
  const previewOverrideModules = import.meta.glob(
26
22
  [
@@ -93,13 +89,7 @@ const AxeRules = enableRulesByTag([
93
89
  * @returns {Function} Decorated story function.
94
90
  */
95
91
  export const applyDecorators = (storyFn, decorators) =>
96
- defaultDecorateStory(
97
- (context) =>
98
- withLegacyStoryToString(React.createElement(storyFn, context), () =>
99
- storyFn(context),
100
- ),
101
- decorators,
102
- );
92
+ applyStoryDecorators(defaultDecorateStory, storyFn, decorators);
103
93
 
104
94
  /**
105
95
  * Storybook decorators to apply platform-specific behavior after each story render.
@@ -125,7 +115,7 @@ export const decorators = [
125
115
  });
126
116
  }, [args]);
127
117
 
128
- return renderHtmlStoryResult(Story(context), {
118
+ return renderPreviewStory(Story, context, {
129
119
  platformAdapter,
130
120
  });
131
121
  },
@@ -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; Craft CMS and WordPress + Timber can use the `none` adapter unless a project adds platform-specific behavior.
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.
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
 
@@ -56,10 +56,18 @@ Every project should provide a `project.emulsify.json` file at the project root:
56
56
  "platform": "none",
57
57
  "name": "example",
58
58
  "machineName": "example"
59
+ },
60
+ "assets": {
61
+ "roots": ["./design/assets"]
59
62
  }
60
63
  }
61
64
  ```
62
65
 
66
+ Asset files are discovered from the default asset roots and any additional
67
+ roots configured in `project.emulsify.json`. Use asset roots when a project
68
+ stores fonts, images, icons, or other static files outside the default
69
+ locations.
70
+
63
71
  Common project scripts call the shared Emulsify Core Vite and Storybook config:
64
72
 
65
73
  - `storybook`: starts Storybook development.
@@ -86,7 +94,7 @@ The documentation is split by task:
86
94
 
87
95
  ## Known Limitations
88
96
 
89
- - Implemented platform adapters are currently `none` and `drupal`. WordPress + Timber and Craft CMS are supported as Twig-oriented use cases through the `none` adapter today. Dedicated adapters are future opportunities. See [Platform Adapters](docs/platform-adapters.md).
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).
90
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).
91
99
  - Production sourcemaps are enabled by default unless a project overrides Vite config through `config/emulsify-core/vite/plugins.*`. See [Performance](docs/performance.md).
92
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).
@@ -103,7 +111,7 @@ Release-readiness coverage validates:
103
111
  - Projects using multiple `variant.structureImplementations`.
104
112
  - Mixed Twig + React Storybook projects.
105
113
 
106
- WordPress + Timber and Craft CMS are Twig-based project use cases that can use the `none` adapter today. Dedicated adapters for those platforms are future opportunities. The implemented adapters in this package are currently `none` and `drupal`.
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`.
107
115
 
108
116
  ## Public Imports
109
117
 
@@ -7,6 +7,7 @@
7
7
 
8
8
  export default {
9
9
  storybookBuildDir: '../../../../.out',
10
+ discoverStories: true,
10
11
  pa11y: {
11
12
  includeNotices: false,
12
13
  includeWarnings: false,
@@ -17,7 +18,7 @@ export default {
17
18
  codes: ['landmark-one-main', 'page-has-heading-one'],
18
19
  descriptions: ['Ensures all page content is contained by landmarks'],
19
20
  },
20
- // List of storybook component IDs defined and used in this project.
21
+ // Manual Storybook IDs merged with IDs discovered from built Storybook output.
21
22
  components: [
22
23
  'base-colors--palettes',
23
24
  'base-motion--usage',
@@ -9,6 +9,7 @@
9
9
  * - `SDC`: boolean from project.emulsify.json `project.singleDirectoryComponents`.
10
10
  * - `structureOverrides`: true when safe `variant.structureImplementations` exist.
11
11
  * - `structureRoots`: array of directories from `variant.structureImplementations`.
12
+ * - `assetRoots`: array of directories from safe `assets.roots` config.
12
13
  * - `platformAdapter`: active adapter for platform-specific behavior.
13
14
  */
14
15
 
@@ -26,6 +27,7 @@ import { resolveProjectConfig } from './project-config.js';
26
27
  * structureOverrides: boolean,
27
28
  * structureRoots: string[],
28
29
  * structureImplementations: Array<{name: string, directory: string}>,
30
+ * assetRoots: string[],
29
31
  * componentRoots: string[],
30
32
  * globalRoots: string[],
31
33
  * namespaceRoots: Record<string, string>,