@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.
@@ -16,6 +16,7 @@ export const VIRTUAL_TWIG_ASSET_SOURCES_ID =
16
16
  'virtual:emulsify-twig-asset-sources';
17
17
  const RESOLVED_VIRTUAL_TWIG_ASSET_SOURCES_ID = `\0${VIRTUAL_TWIG_ASSET_SOURCES_ID}`;
18
18
  const GENERATED_ASSET_ALIASES = new Set(['icons.svg']);
19
+ const GENERATED_ASSET_ROOTS = ['/dist/assets'];
19
20
  const PUBLIC_ASSET_ROOTS = new Map([
20
21
  ['/assets', '/assets'],
21
22
  ['/dist/assets', '/assets'],
@@ -116,14 +117,21 @@ export function assetSourceRoots(env) {
116
117
  */
117
118
  export function generatedAssetSourceRoots(env) {
118
119
  return unique(
119
- ['/dist/assets']
120
- .map((root) => toAbsoluteAssetRoot(env?.projectDir, root))
120
+ GENERATED_ASSET_ROOTS.map((root) =>
121
+ toAbsoluteAssetRoot(env?.projectDir, root),
122
+ )
121
123
  .filter((root) => root && safeExists(root))
122
124
  .map((root) => toRootRelativePath(env?.projectDir, root))
123
125
  .filter(Boolean),
124
126
  );
125
127
  }
126
128
 
129
+ function generatedAssetRootPrefixes() {
130
+ return GENERATED_ASSET_ROOTS.map((root) =>
131
+ `${root}/`.replace(/\/{2,}/g, '/'),
132
+ );
133
+ }
134
+
127
135
  /**
128
136
  * Build Vite glob patterns from text asset roots.
129
137
  *
@@ -216,6 +224,20 @@ export function publicAssetSourceEntries(env) {
216
224
  }
217
225
  }
218
226
 
227
+ for (const root of generatedAssetRootPrefixes()) {
228
+ const publicBase = publicAssetBaseForRoot(root);
229
+ if (!publicBase) continue;
230
+
231
+ for (const alias of GENERATED_ASSET_ALIASES) {
232
+ const key = `${root.replace(/\/+$/, '')}/${alias}`.replace(
233
+ /\/{2,}/g,
234
+ '/',
235
+ );
236
+ const url = `${publicBase}${alias}`.replace(/\/{2,}/g, '/');
237
+ entries.set(key, { key, url });
238
+ }
239
+ }
240
+
219
241
  return Array.from(entries.values());
220
242
  }
221
243
 
@@ -232,6 +254,10 @@ export function generateVirtualTwigAssetSourcesModule(env) {
232
254
  const generatedRootPrefixes = generatedAssetSourceRoots(env).map((root) =>
233
255
  `${root === '/' ? '' : root}/`.replace(/\/{2,}/g, '/'),
234
256
  );
257
+ const allGeneratedRootPrefixes = unique([
258
+ ...generatedRootPrefixes,
259
+ ...generatedAssetRootPrefixes(),
260
+ ]);
235
261
  const patterns = assetSourceGlobPatterns(env);
236
262
  const globEntries = patterns.length
237
263
  ? patterns
@@ -255,7 +281,7 @@ export function generateVirtualTwigAssetSourcesModule(env) {
255
281
  */
256
282
 
257
283
  export const assetRootPrefixes = ${JSON.stringify(rootPrefixes)};
258
- export const generatedAssetRootPrefixes = ${JSON.stringify(generatedRootPrefixes)};
284
+ export const generatedAssetRootPrefixes = ${JSON.stringify(allGeneratedRootPrefixes)};
259
285
  export const generatedAssetAliases = ${JSON.stringify(
260
286
  Array.from(GENERATED_ASSET_ALIASES),
261
287
  )};
@@ -11,6 +11,7 @@ import { normalize, resolve, sep } from 'path';
11
11
  import { getPlatformAdapter, normalizePlatformName } from './platforms.js';
12
12
  import { resolveProjectStructure } from './project-structure.js';
13
13
  import { safeExists, safeReadJson } from './utils/fs-safe.js';
14
+ import { unique } from './utils/unique.js';
14
15
 
15
16
  /**
16
17
  * Cache normalized project config by project root and relevant env signature.
@@ -94,6 +95,65 @@ function normalizeStructureImplementations(projectDir, implementations = []) {
94
95
  .filter(Boolean);
95
96
  }
96
97
 
98
+ /**
99
+ * Read public asset root configuration from supported project config paths.
100
+ *
101
+ * `assets.roots` is the documented public path.
102
+ * `projectStructure.assetRoots` and `project.assetRoots` remain supported for
103
+ * compatibility with earlier follow-up work and local project experiments.
104
+ *
105
+ * @param {object} rawConfig - Parsed project.emulsify.json contents.
106
+ * @returns {Array} Raw configured asset roots.
107
+ */
108
+ function rawAssetRoots(rawConfig = {}) {
109
+ return [
110
+ ...(Array.isArray(rawConfig?.assets?.roots) ? rawConfig.assets.roots : []),
111
+ ...(Array.isArray(rawConfig?.projectStructure?.assetRoots)
112
+ ? rawConfig.projectStructure.assetRoots
113
+ : []),
114
+ ...(Array.isArray(rawConfig?.project?.assetRoots)
115
+ ? rawConfig.project.assetRoots
116
+ : []),
117
+ ];
118
+ }
119
+
120
+ /**
121
+ * Normalize public asset root declarations.
122
+ *
123
+ * Paths must resolve inside the project root. Invalid entries are retained as
124
+ * diagnostics so the audit command can report them without making Storybook or
125
+ * Vite builds fail for existing projects.
126
+ *
127
+ * @param {string} projectDir - Absolute project root.
128
+ * @param {Array} roots - Raw configured asset root entries.
129
+ * @returns {{roots: string[], ignored: string[]}} Normalized asset root state.
130
+ */
131
+ function normalizeAssetRoots(projectDir, roots = []) {
132
+ if (!Array.isArray(roots)) return { roots: [], ignored: [] };
133
+
134
+ const normalizedRoots = [];
135
+ const ignored = [];
136
+
137
+ for (const item of roots) {
138
+ if (typeof item !== 'string' || !item.trim()) {
139
+ continue;
140
+ }
141
+
142
+ const directory = coerceToProjectPath(projectDir, item);
143
+ if (!directory) {
144
+ ignored.push(item);
145
+ continue;
146
+ }
147
+
148
+ normalizedRoots.push(normalize(directory));
149
+ }
150
+
151
+ return {
152
+ roots: unique(normalizedRoots),
153
+ ignored: unique(ignored),
154
+ };
155
+ }
156
+
97
157
  /**
98
158
  * Normalize project config for current tooling consumers.
99
159
  *
@@ -140,6 +200,7 @@ export function resolveProjectConfig(
140
200
  root,
141
201
  rawStructureImplementations,
142
202
  );
203
+ const assetRoots = normalizeAssetRoots(root, rawAssetRoots(rawConfig));
143
204
  const structureRoots = structureImplementations.map(
144
205
  (implementation) => implementation.directory,
145
206
  );
@@ -149,6 +210,8 @@ export function resolveProjectConfig(
149
210
  srcExists,
150
211
  SDC: singleDirectoryComponents,
151
212
  structureImplementations,
213
+ assetRoots: assetRoots.roots,
214
+ ignoredAssetRoots: assetRoots.ignored,
152
215
  platformAdapter,
153
216
  });
154
217
 
@@ -166,6 +229,8 @@ export function resolveProjectConfig(
166
229
  structureOverrides: projectStructure.structureOverrides,
167
230
  structureImplementations,
168
231
  structureRoots,
232
+ assetRoots: projectStructure.assetRoots,
233
+ ignoredAssetRoots: projectStructure.ignoredAssetRoots,
169
234
  componentRoots: projectStructure.componentRoots,
170
235
  globalRoots: projectStructure.globalRoots,
171
236
  namespaceRoots: projectStructure.namespaceRoots,
@@ -7,7 +7,7 @@
7
7
  * process.
8
8
  */
9
9
 
10
- import { basename, relative, resolve, sep } from 'path';
10
+ import { basename, normalize, relative, resolve, sep } from 'path';
11
11
  import { safeExists } from './utils/fs-safe.js';
12
12
  import { replaceLastSlash, toPosixPath } from './utils/paths.js';
13
13
  import { unique } from './utils/unique.js';
@@ -181,6 +181,33 @@ function fallbackNamespaceRoots({
181
181
  return namespaceRoots;
182
182
  }
183
183
 
184
+ /**
185
+ * Normalize project-scoped asset roots.
186
+ *
187
+ * @param {string} projectDir - Absolute project root.
188
+ * @param {string[]} assetRoots - Raw asset root paths.
189
+ * @returns {string[]} Safe absolute asset root paths.
190
+ */
191
+ function normalizeAssetRoots(projectDir, assetRoots = []) {
192
+ if (!Array.isArray(assetRoots)) return [];
193
+
194
+ const projectRoot = resolve(projectDir);
195
+
196
+ return unique(
197
+ assetRoots
198
+ .map((root) =>
199
+ typeof root === 'string' && root.trim()
200
+ ? normalize(resolve(projectRoot, root))
201
+ : '',
202
+ )
203
+ .filter(
204
+ (root) =>
205
+ root &&
206
+ (root === projectRoot || root.startsWith(`${projectRoot}${sep}`)),
207
+ ),
208
+ );
209
+ }
210
+
184
211
  /**
185
212
  * Resolve the serializable project structure model.
186
213
  *
@@ -190,6 +217,8 @@ function fallbackNamespaceRoots({
190
217
  * srcExists?: boolean,
191
218
  * SDC?: boolean,
192
219
  * structureImplementations?: {name: string, directory: string}[],
220
+ * assetRoots?: string[],
221
+ * ignoredAssetRoots?: string[],
193
222
  * platformAdapter?: object
194
223
  * }} [env] - Normalized project environment.
195
224
  * @returns {object} Project structure model.
@@ -214,6 +243,8 @@ export function resolveProjectStructure(env) {
214
243
  srcDir = defaultSrcDir,
215
244
  srcExists = safeExists(defaultSrcDir),
216
245
  SDC = false,
246
+ assetRoots: rawAssetRoots = [],
247
+ ignoredAssetRoots = [],
217
248
  platformAdapter = {},
218
249
  } = resolvedEnv;
219
250
  const structureImplementations =
@@ -244,6 +275,7 @@ export function resolveProjectStructure(env) {
244
275
  });
245
276
  const componentRoots = componentRootRecords.map((root) => root.directory);
246
277
  const globalRoots = globalRootRecords.map((root) => root.directory);
278
+ const assetRoots = normalizeAssetRoots(projectDir, rawAssetRoots);
247
279
  const namespaceRootValues = Object.values(namespaceRoots);
248
280
  const sourceRoots = unique(
249
281
  [...componentRoots, ...globalRoots].filter(Boolean),
@@ -278,7 +310,9 @@ export function resolveProjectStructure(env) {
278
310
  globalRootRecords,
279
311
  componentRoots,
280
312
  globalRoots,
313
+ assetRoots,
281
314
  sourceRoots,
315
+ ignoredAssetRoots: unique(ignoredAssetRoots),
282
316
  sourceRootRecords,
283
317
  storyRoots,
284
318
  twigRoots,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emulsify/core",
3
- "version": "4.0.4",
3
+ "version": "4.1.1",
4
4
  "description": "Bundled tooling for Storybook development + Vite Build",
5
5
  "keywords": [
6
6
  "component library",
@@ -38,7 +38,10 @@
38
38
  ".storybook/css-components.js",
39
39
  ".storybook/css-dist.js",
40
40
  ".storybook/emulsifyTheme.js",
41
+ ".storybook/main-static-assets.js",
42
+ ".storybook/main-vite.js",
41
43
  ".storybook/main.js",
44
+ ".storybook/manager-head.css",
42
45
  ".storybook/manager.js",
43
46
  ".storybook/preview.js",
44
47
  ".storybook/utils.js",
@@ -98,6 +101,7 @@
98
101
  "src/storybook/index.js",
99
102
  "src/storybook/main-config.js",
100
103
  "src/storybook/platform-behaviors.js",
104
+ "src/storybook/preview-decorator.js",
101
105
  "src/storybook/preview-parameters.js",
102
106
  "src/storybook/render-twig.js",
103
107
  "src/storybook/twig/include-function.js",
package/scripts/a11y.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * and reports issues.
6
6
  */
7
7
 
8
- import { existsSync } from 'fs';
8
+ import { existsSync, readFileSync } from 'fs';
9
9
  import path from 'path';
10
10
  import { fileURLToPath, pathToFileURL } from 'url';
11
11
  import * as R from 'ramda';
@@ -16,15 +16,15 @@ import a11yConfig from '../config/a11y.config.js';
16
16
  const __filename = fileURLToPath(import.meta.url);
17
17
  const __dirname = path.dirname(__filename);
18
18
 
19
- const { storybookBuildDir, pa11y: pa11yConfig } = a11yConfig;
20
-
21
19
  // Project-specific configuration.
22
- let { ignore = {}, components = [] } = a11yConfig;
20
+ let {
21
+ ignore = {},
22
+ components = [],
23
+ discoverStories = true,
24
+ storybookBuildDir,
25
+ pa11y: pa11yConfig = {},
26
+ } = a11yConfig;
23
27
 
24
- /** Absolute path to Storybook build directory. */
25
- const STORYBOOK_BUILD_DIR = path.resolve(__dirname, '../', storybookBuildDir);
26
- /** Absolute path to Storybook iframe file used for per-story rendering. */
27
- const STORYBOOK_IFRAME = path.join(STORYBOOK_BUILD_DIR, 'iframe.html');
28
28
  /** Project-specific accessibility config path used by generated themes. */
29
29
  const PROJECT_A11Y_CONFIG = path.resolve(
30
30
  __dirname,
@@ -48,12 +48,26 @@ const loadProjectA11yConfig = async () => {
48
48
  /**
49
49
  * Apply project-specific a11y config values over shared defaults.
50
50
  *
51
- * @param {{ignore?: object, components?: string[]}} config - Project config.
51
+ * @param {{ignore?: object, components?: string[], discoverStories?: boolean, storybookBuildDir?: string, pa11y?: object}} config - Project config.
52
52
  * @returns {void}
53
53
  */
54
54
  const applyProjectA11yConfig = (config = {}) => {
55
55
  ignore = config.ignore || ignore;
56
- components = config.components || components;
56
+ components = Array.isArray(config.components)
57
+ ? config.components
58
+ : components;
59
+ discoverStories =
60
+ typeof config.discoverStories === 'boolean'
61
+ ? config.discoverStories
62
+ : discoverStories;
63
+ storybookBuildDir =
64
+ typeof config.storybookBuildDir === 'string' && config.storybookBuildDir
65
+ ? config.storybookBuildDir
66
+ : storybookBuildDir;
67
+ pa11yConfig =
68
+ config.pa11y && typeof config.pa11y === 'object'
69
+ ? { ...pa11yConfig, ...config.pa11y }
70
+ : pa11yConfig;
57
71
  };
58
72
 
59
73
  /**
@@ -67,12 +81,128 @@ const printHelp = () => {
67
81
  'Usage: node scripts/a11y.js [options]',
68
82
  '',
69
83
  'Options:',
70
- ' -r Run pa11y against configured Storybook component IDs.',
84
+ ' -r Run pa11y against discovered and configured Storybook story IDs.',
71
85
  ' -h, --help Print this help text.',
72
86
  ].join('\n'),
73
87
  );
74
88
  };
75
89
 
90
+ /**
91
+ * Resolve the configured Storybook build directory.
92
+ *
93
+ * @param {string} [buildDir=storybookBuildDir] - Configured build directory.
94
+ * @returns {string} Absolute Storybook build directory.
95
+ */
96
+ const resolveStorybookBuildDir = (buildDir = storybookBuildDir) =>
97
+ path.resolve(__dirname, '../', buildDir);
98
+
99
+ /**
100
+ * Resolve Storybook's iframe file used for per-story rendering.
101
+ *
102
+ * @param {string} [buildDir=storybookBuildDir] - Configured build directory.
103
+ * @returns {string} Absolute iframe.html path.
104
+ */
105
+ const resolveStorybookIframe = (buildDir = storybookBuildDir) =>
106
+ path.join(resolveStorybookBuildDir(buildDir), 'iframe.html');
107
+
108
+ /**
109
+ * Return unique non-empty Storybook IDs in first-seen order.
110
+ *
111
+ * @param {Array} values - Candidate story IDs.
112
+ * @returns {string[]} Unique story IDs.
113
+ */
114
+ const normalizeStoryIds = (values = []) =>
115
+ Array.from(
116
+ new Set(
117
+ values
118
+ .filter((value) => typeof value === 'string')
119
+ .map((value) => value.trim())
120
+ .filter(Boolean),
121
+ ),
122
+ );
123
+
124
+ /**
125
+ * Extract runnable story IDs from a Storybook index object.
126
+ *
127
+ * @param {object} index - Parsed Storybook index.json or stories.json.
128
+ * @returns {string[]} Story IDs.
129
+ */
130
+ const storyIdsFromStorybookIndex = (index = {}) => {
131
+ const entries =
132
+ index?.entries && typeof index.entries === 'object'
133
+ ? index.entries
134
+ : index?.stories && typeof index.stories === 'object'
135
+ ? index.stories
136
+ : {};
137
+
138
+ return normalizeStoryIds(
139
+ Object.entries(entries)
140
+ .filter(([, entry]) => !entry?.type || entry.type === 'story')
141
+ .map(([id, entry]) =>
142
+ typeof entry?.id === 'string' && entry.id ? entry.id : id,
143
+ ),
144
+ );
145
+ };
146
+
147
+ /**
148
+ * Discover runnable Storybook story IDs from built Storybook output.
149
+ *
150
+ * @param {string} [buildDir=storybookBuildDir] - Configured Storybook build directory.
151
+ * @param {{warn?: Function}} [options] - Reporting options.
152
+ * @returns {string[]} Story IDs discovered from Storybook's generated index.
153
+ */
154
+ const discoverStoryIds = (
155
+ buildDir = storybookBuildDir,
156
+ { warn = console.warn } = {},
157
+ ) => {
158
+ const indexPath = path.join(resolveStorybookBuildDir(buildDir), 'index.json');
159
+
160
+ if (!existsSync(indexPath)) {
161
+ warn(
162
+ `Storybook index not found at ${indexPath}; falling back to configured Pa11y story IDs.`,
163
+ );
164
+ return [];
165
+ }
166
+
167
+ try {
168
+ return storyIdsFromStorybookIndex(
169
+ JSON.parse(readFileSync(indexPath, 'utf8')),
170
+ );
171
+ } catch (error) {
172
+ warn(
173
+ `Unable to read Storybook index at ${indexPath}: ${
174
+ error.message || error
175
+ }; falling back to configured Pa11y story IDs.`,
176
+ );
177
+ return [];
178
+ }
179
+ };
180
+
181
+ /**
182
+ * Resolve the final Pa11y story ID list.
183
+ *
184
+ * @param {object} [options={}] - Resolution options.
185
+ * @param {string[]} [options.manualIds=components] - Manually configured IDs.
186
+ * @param {boolean} [options.discover=discoverStories] - Whether discovery is enabled.
187
+ * @param {string} [options.buildDir=storybookBuildDir] - Storybook build directory.
188
+ * @param {Function} [options.warn=console.warn] - Warning sink.
189
+ * @returns {string[]} Story IDs to lint.
190
+ */
191
+ const resolvePa11yStoryIds = ({
192
+ manualIds = components,
193
+ discover = discoverStories,
194
+ buildDir = storybookBuildDir,
195
+ warn = console.warn,
196
+ } = {}) => {
197
+ const manual = normalizeStoryIds(manualIds);
198
+ if (discover === false) return manual;
199
+
200
+ return normalizeStoryIds([
201
+ ...manual,
202
+ ...discoverStoryIds(buildDir, { warn }),
203
+ ]);
204
+ };
205
+
76
206
  /**
77
207
  * Map pa11y/axe severity to a label (historically a color name).
78
208
  * Retained for backward compatibility, but not used for styling anymore.
@@ -155,7 +285,7 @@ const logReport = ({ issues, pageUrl }) => {
155
285
  * @returns {Promise<{ issues: Pa11yIssue[], pageUrl: string }>} Pa11y result.
156
286
  */
157
287
  const lintComponent = async (name) =>
158
- pa11y(`${STORYBOOK_IFRAME}?id=${name}`, {
288
+ pa11y(`${resolveStorybookIframe()}?id=${name}`, {
159
289
  includeNotices: true,
160
290
  includeWarnings: true,
161
291
  runners: ['axe'],
@@ -188,15 +318,22 @@ if (R.includes(process.argv[2], ['-h', '--help'])) {
188
318
  } else if (R.pathEq(['argv', 2], '-r')(process)) {
189
319
  loadProjectA11yConfig().then((projectConfig) => {
190
320
  applyProjectA11yConfig(projectConfig);
191
- return lintReportAndExit(components);
321
+ return lintReportAndExit(resolvePa11yStoryIds());
192
322
  });
193
323
  }
194
324
 
195
325
  export {
196
326
  severityToColor,
327
+ applyProjectA11yConfig,
328
+ discoverStoryIds,
197
329
  issueIsValid,
198
330
  logIssue,
199
331
  logReport,
200
332
  lintComponent,
201
333
  lintReportAndExit,
334
+ normalizeStoryIds,
335
+ resolvePa11yStoryIds,
336
+ resolveStorybookBuildDir,
337
+ resolveStorybookIframe,
338
+ storyIdsFromStorybookIndex,
202
339
  };
package/scripts/audit.js CHANGED
@@ -396,6 +396,18 @@ function auditProjectConfig(context) {
396
396
  }
397
397
  }
398
398
 
399
+ for (const root of env.ignoredAssetRoots || []) {
400
+ findings.push(
401
+ makeFinding({
402
+ id: 'invalid-asset-root',
403
+ severity: 'warn',
404
+ filePath: resolve(projectDir, 'project.emulsify.json'),
405
+ message: `Configured asset root "${root}" was ignored because it resolves outside the project root.`,
406
+ docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/project-structure.md#asset-roots',
407
+ }),
408
+ );
409
+ }
410
+
399
411
  return findings;
400
412
  }
401
413
 
@@ -1046,7 +1058,7 @@ function styleRuntimeDirectories(filePath, env, projectDir) {
1046
1058
  function auditCssAssetReferences(context) {
1047
1059
  const { env, projectDir, styleFiles } = context;
1048
1060
  const findings = [];
1049
- const projectAssetsDir = resolve(projectDir, 'assets');
1061
+ const projectAssetRoots = auditAssetRoots(env).filter(safeIsDirectory);
1050
1062
  const styleSourceRoots = env.projectStructure?.sourceRoots || [];
1051
1063
 
1052
1064
  for (const filePath of styleFiles) {
@@ -1092,7 +1104,7 @@ function auditCssAssetReferences(context) {
1092
1104
  }
1093
1105
 
1094
1106
  if (
1095
- isSameOrInside(resolvedAsset, projectAssetsDir) &&
1107
+ projectAssetRoots.some((root) => isSameOrInside(resolvedAsset, root)) &&
1096
1108
  (!sourceAsset || runtimeAsset || assetPath.startsWith('..'))
1097
1109
  ) {
1098
1110
  findings.push(
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @file Shared helpers for Storybook preview decorator wiring.
3
+ */
4
+
5
+ import React from 'react';
6
+ import {
7
+ renderHtmlStoryResult,
8
+ withLegacyStoryToString,
9
+ } from './render-twig.js';
10
+
11
+ /**
12
+ * Render a Storybook story function as a React element with full context props.
13
+ *
14
+ * @param {Function} storyFn - Storybook story function.
15
+ * @param {object} context - Full Storybook story context.
16
+ * @returns {React.ReactElement} React story element.
17
+ */
18
+ export function createStoryElement(storyFn, context) {
19
+ return withLegacyStoryToString(React.createElement(storyFn, context), () =>
20
+ storyFn(context),
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Apply Storybook decorators while preserving full story context.
26
+ *
27
+ * @param {Function} decorateStory - Storybook decorator applier.
28
+ * @param {Function} storyFn - Storybook story function.
29
+ * @param {Function[]} decorators - Storybook decorators.
30
+ * @returns {Function} Decorated story function.
31
+ */
32
+ export const applyStoryDecorators = (decorateStory, storyFn, decorators) =>
33
+ decorateStory((context) => createStoryElement(storyFn, context), decorators);
34
+
35
+ /**
36
+ * Render a preview Story callback through Emulsify's HTML compatibility layer.
37
+ *
38
+ * @param {Function} Story - Decorated Storybook story callback.
39
+ * @param {object} context - Full Storybook story context.
40
+ * @param {object} [options={}] - Render options.
41
+ * @returns {*} Rendered Storybook story result.
42
+ */
43
+ export function renderPreviewStory(Story, context, options = {}) {
44
+ return renderHtmlStoryResult(Story(context), options);
45
+ }