@emulsify/core 2.7.1 → 3.0.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.
@@ -8,13 +8,13 @@ jobs:
8
8
  runs-on: ubuntu-latest
9
9
  steps:
10
10
  - name: Checkout
11
- uses: actions/checkout@v2
11
+ uses: actions/checkout@v4
12
12
  with:
13
13
  fetch-depth: 0
14
14
  - name: Install Node.js
15
- uses: actions/setup-node@v2
15
+ uses: actions/setup-node@v4
16
16
  with:
17
- node-version: 20
17
+ node-version: "24.x"
18
18
  - name: Install
19
19
  run: npm install
20
20
  - name: Release
package/.nvmrc CHANGED
@@ -1 +1 @@
1
- 20
1
+ 24
@@ -1,19 +1,41 @@
1
1
  // Simple Drupal.behaviors usage for Storybook
2
2
 
3
+ /**
4
+ * Global Drupal namespace stub for Storybook environment.
5
+ * @namespace Drupal
6
+ */
3
7
  window.Drupal = { behaviors: {} };
4
8
 
9
+ /**
10
+ * Immediately-Invoked Function Expression to scope Drupal behavior attachment logic.
11
+ * @param {Object} Drupal - The Drupal global namespace object.
12
+ * @param {Object} drupalSettings - Global Drupal settings object stub.
13
+ */
5
14
  (function (Drupal, drupalSettings) {
15
+ /**
16
+ * Throws an error asynchronously to avoid interrupting execution flow.
17
+ * @param {Error} error - The error object to throw.
18
+ * @returns {void}
19
+ */
6
20
  Drupal.throwError = function (error) {
7
21
  setTimeout(function () {
8
22
  throw error;
9
23
  }, 0);
10
24
  };
11
25
 
26
+ /**
27
+ * Attaches all registered Drupal behaviors.
28
+ * @param {HTMLElement|Document} [context=document] - DOM context to attach behaviors to.
29
+ * @param {Object} [settings=drupalSettings] - Drupal settings to pass to behaviors.
30
+ * @returns {void}
31
+ */
12
32
  Drupal.attachBehaviors = function (context, settings) {
13
33
  context = context || document;
14
34
  settings = settings || drupalSettings;
35
+ /** @type {Object.<string, {attach: Function}>} */
15
36
  const behaviors = Drupal.behaviors;
16
37
 
38
+ // Iterate through each behavior and invoke its attach method if defined.
17
39
  Object.keys(behaviors).forEach(function (i) {
18
40
  if (typeof behaviors[i].attach === 'function') {
19
41
  try {
@@ -1,5 +1,4 @@
1
1
  // Documentation on theming Storybook: https://storybook.js.org/docs/configurations/theming/
2
-
3
2
  import { create } from '@storybook/theming';
4
3
 
5
4
  export default create({
@@ -34,5 +33,5 @@ export default create({
34
33
  brandTitle: 'Emulsify',
35
34
  brandUrl: 'https://emulsify.info',
36
35
  brandImage:
37
- 'https://raw.githubusercontent.com/fourkitchens/emulsify-core/main/assets/images/emulsify-logo-sb.svg?token=GHSAT0AAAAAACIEXLVC5R3KBCX6HGKGTBBSZNYFWMA',
36
+ 'https://raw.githubusercontent.com/fourkitchens/emulsify-core/main/assets/images/emulsify-logo-sb.svg',
38
37
  });
@@ -1,36 +1,107 @@
1
- const { configOverrides } = require('../../../../config/emulsify-core/storybook/main');
2
-
1
+ // .storybook/main.js
2
+
3
+ /**
4
+ * Storybook main configuration file.
5
+ * This configures stories, static directories, addons, core builder,
6
+ * framework, documentation settings, manager head styles, and overrides.
7
+ * @module .storybook/main
8
+ */
9
+
10
+ import { resolve } from 'path';
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { fileURLToPath } from 'url';
14
+ import configOverrides from '../../../../config/emulsify-core/storybook/main.js';
15
+
16
+ /**
17
+ * The full path to the current file (ESM compatible).
18
+ * @type {string}
19
+ */
20
+ const __filename = fileURLToPath(import.meta.url);
21
+
22
+ /**
23
+ * The directory name of the current module file.
24
+ * @type {string}
25
+ */
26
+ const __dirname = path.dirname(__filename);
27
+
28
+ /**
29
+ * Safely apply any user-provided overrides or fall back to an empty object.
30
+ * @type {object}
31
+ */
3
32
  const safeConfigOverrides = configOverrides || {};
4
33
 
34
+ /**
35
+ * Primary Storybook configuration object.
36
+ * @type {import('@storybook/core-common').StorybookConfig}
37
+ */
5
38
  const config = {
39
+ /**
40
+ * Patterns for locating story files under src or components directories.
41
+ * @type {string[]}
42
+ */
6
43
  stories: [
7
44
  '../../../../(src|components)/**/*.stories.@(js|jsx|ts|tsx)',
8
45
  ],
46
+
47
+ /**
48
+ * Directories to serve as static assets in the Storybook build.
49
+ * @type {string[]}
50
+ */
9
51
  staticDirs: [
10
52
  '../../../../assets/images',
11
53
  '../../../../assets/icons',
12
54
  '../../../../dist',
13
55
  ],
56
+
57
+ /**
58
+ * List of Storybook addons to enable various features.
59
+ * @type {string[]}
60
+ */
14
61
  addons: [
15
62
  '../../../@storybook/addon-a11y',
16
63
  '../../../@storybook/addon-links',
17
64
  '../../../@storybook/addon-essentials',
18
65
  '../../../@storybook/addon-themes',
19
- '../../../@storybook/addon-styling-webpack'
66
+ '../../../@storybook/addon-styling-webpack',
20
67
  ],
68
+
69
+ /**
70
+ * Core builder configuration for Storybook.
71
+ * @type {{builder: string, disableTelemetry: boolean}}
72
+ */
21
73
  core: {
22
74
  builder: 'webpack5',
75
+ disableTelemetry: true,
23
76
  },
77
+
78
+ /**
79
+ * Framework specification for Storybook (HTML + Webpack5).
80
+ * @type {{name: string, options: object}}
81
+ */
24
82
  framework: {
25
83
  name: '@storybook/html-webpack5',
26
84
  options: {},
27
85
  },
86
+
87
+ /**
88
+ * Documentation settings for Storybook autodocs.
89
+ * @type {{autodocs: boolean}}
90
+ */
28
91
  docs: {
29
92
  autodocs: false,
30
93
  },
31
- managerHead: (head) => `
32
- ${head}
33
- <style>
94
+
95
+ /**
96
+ * Custom styles injected into the Storybook manager (sidebar) head,
97
+ * plus any external manager-head.html snippet.
98
+ * @param {string} head - Existing head HTML.
99
+ * @returns {string} Modified head HTML.
100
+ */
101
+ managerHead: (head) => {
102
+ // inline theme styles
103
+ const inlineStyles = `
104
+ <style>
34
105
  :root {
35
106
  --colors-emulsify-blue-100: #e6f5fc;
36
107
  --colors-emulsify-blue-200: #CCECFA;
@@ -44,41 +115,33 @@ const config = {
44
115
  --colors-emulsify-blue-1000: #00202e;
45
116
  --colors-purple: #8B1E7E;
46
117
  }
47
-
48
118
  .sidebar-container {
49
119
  background: url('https://raw.githubusercontent.com/fourkitchens/emulsify-core/main/assets/images/corner-bkg.png?token=GHSAT0AAAAAACIEXLVDMX56QK3ZIZWHWHTEZNYFYIA') no-repeat top left;
50
120
  }
51
-
52
121
  .sidebar-container .sidebar-subheading {
53
122
  color: var(--colors-emulsify-blue-200);
54
123
  font-size: 13px;
55
124
  letter-spacing: 0.15em;
56
125
  }
57
-
58
126
  .sidebar-container .sidebar-subheading button:focus {
59
127
  color: var(--colors-emulsify-blue-300);
60
128
  }
61
-
62
129
  /** Triangle icon **/
63
130
  .sidebar-container .sidebar-subheading button span {
64
131
  color: var(--colors-emulsify-blue-300);
65
132
  }
66
-
67
133
  .sidebar-container .search-field input {
68
134
  border-color: var(--colors-emulsify-blue-700);
69
135
  }
70
-
71
136
  .sidebar-container .search-field input:active {
72
137
  border-color: var(--colors-emulsify-blue-700);
73
138
  }
74
-
75
139
  .sidebar-container .search-result-recentlyOpened,
76
140
  .sidebar-container .search-result-back,
77
141
  .sidebar-container .search-result-clearHistory {
78
142
  color: var(--colors-emulsify-blue-300) !important;
79
143
  letter-spacing: 0.15em;
80
144
  }
81
-
82
145
  .sidebar-container .search-result-back span,
83
146
  .sidebar-container .search-result-back svg,
84
147
  .sidebar-container .search-result-clearHistory span,
@@ -86,74 +149,96 @@ const config = {
86
149
  letter-spacing: normal;
87
150
  color: white;
88
151
  }
89
-
90
152
  .sidebar-container .sidebar-item svg {
91
153
  margin-top: 1px;
92
154
  }
93
-
94
155
  .sidebar-container .sidebar-item span {
95
156
  margin-top: 4px;
96
157
  }
97
-
98
158
  .sidebar-container .sidebar-subheading-action svg {
99
159
  color: var(--colors-emulsify-blue-400);
100
160
  }
101
-
102
161
  .sidebar-container .sidebar-subheading-action:hover svg {
103
162
  color: var(--colors-emulsify-blue-300);
104
163
  }
105
-
106
164
  .sidebar-header button[title="Shortcuts"] {
107
165
  box-shadow: none;
108
166
  border: 1px solid var(--colors-emulsify-blue-700);
109
167
  }
110
-
111
168
  .sidebar-header button[title="Shortcuts"]:active {
112
169
  border: 1px solid var(--colors-emulsify-blue-500);
113
170
  }
114
-
115
171
  .sidebar-header button[title="Shortcuts"]:focus {
116
172
  background: transparent;
117
173
  }
118
-
119
174
  #shortcuts {
120
175
  border-bottom-color: var(--colors-emulsify-blue-900) !important;
121
176
  }
122
-
123
177
  [role="main"]:not(:nth-child(3)) {
124
178
  top: 1rem !important;
125
179
  height: calc(100vh - 2rem) !important;
126
180
  }
127
-
128
181
  [role="main"] .os-host .os-content button:hover {
129
182
  background: var(--colors-emulsify-blue-100);
130
183
  }
131
-
132
184
  [role="main"] .os-host .os-content button:hover svg {
133
185
  color: var(--colors-emulsify-blue-900);
134
186
  }
135
-
136
187
  #panel-tab-content,
137
188
  #panel-tab-content>* {
138
189
  color: var(--colors-emulsify-blue-100) !important;
139
190
  }
140
-
141
191
  #panel-tab-content a,
142
192
  #panel-tab-content a span,
143
193
  #panel-tab-content a span svg {
144
194
  color: var(--colors-emulsify-blue-800);
145
195
  }
146
-
147
196
  #panel-tab-content>div>div>div>div>div>div {
148
197
  background: transparent;
149
198
  }
150
-
151
199
  #panel-tab-content>div>div>div>div>div>div>div {
152
200
  color: var(--colors-emulsify-blue-1000) !important;
153
201
  }
154
202
  </style>
155
- `,
203
+ `;
204
+
205
+ // load external manager-head.html if present
206
+ const externalManagerHeadPath = resolve(
207
+ __dirname,
208
+ '../../../../config/emulsify-core/storybook/manager-head.html'
209
+ );
210
+ let externalManagerHtml = '';
211
+ if (fs.existsSync(externalManagerHeadPath)) {
212
+ externalManagerHtml = fs.readFileSync(externalManagerHeadPath, 'utf8');
213
+ }
214
+
215
+ return `${head}
216
+ ${inlineStyles}
217
+ ${externalManagerHtml}`;
218
+ },
219
+
220
+ /**
221
+ * Function to load and append an external preview-head.html into the preview iframe.
222
+ * @param {string} head - Existing preview head HTML.
223
+ * @returns {string} Combined head HTML including external snippet if present.
224
+ */
225
+ previewHead: (head) => {
226
+ const externalHeadPath = resolve(
227
+ __dirname,
228
+ '../../../../config/emulsify-core/storybook/preview-head.html'
229
+ );
230
+
231
+ let externalHtml = '';
232
+ if (fs.existsSync(externalHeadPath)) {
233
+ externalHtml = fs.readFileSync(externalHeadPath, 'utf8');
234
+ }
235
+
236
+ return `${head}
237
+ ${externalHtml}`;
238
+ },
239
+
240
+ // Merge in user overrides without modifying original logic
156
241
  ...safeConfigOverrides,
157
242
  };
158
243
 
159
- module.exports = config;
244
+ export default config;
@@ -1,14 +1,45 @@
1
+ // .storybook/manager.js
2
+
1
3
  import { addons } from '@storybook/manager-api';
2
4
  import emulsifyTheme from './emulsifyTheme';
3
5
 
6
+ /**
7
+ * Dynamically import the user-provided Storybook theme override.
8
+ * Falls back to the default Emulsify theme if the import fails or is empty.
9
+ */
4
10
  import('../../../../config/emulsify-core/storybook/theme')
5
- .then((customTheme) => {
6
- addons.setConfig({
7
- theme: customTheme.default,
8
- });
9
- })
10
- .catch(() => {
11
- addons.setConfig({
12
- theme: emulsifyTheme,
11
+ /**
12
+ * Handle successful dynamic import of the theme module.
13
+ * @param {{ default: object }} module - The imported theme module.
14
+ */
15
+ .then(({ default: customTheme }) => {
16
+ /**
17
+ * Determine if the imported theme object is empty or not.
18
+ * @type {boolean}
19
+ */
20
+ const isEmptyObject =
21
+ !customTheme ||
22
+ (typeof customTheme === 'object' && Object.keys(customTheme).length === 0);
23
+
24
+ /**
25
+ * Apply the chosen theme to Storybook’s manager UI configuration.
26
+ * @type {{ theme: object }}
27
+ */
28
+ addons.setConfig({
29
+ theme: isEmptyObject ? emulsifyTheme : customTheme,
30
+ });
31
+ })
32
+ /**
33
+ * Handle failure of the dynamic import (e.g., file not found).
34
+ * @returns {void}
35
+ */
36
+ .catch(() => {
37
+ addons.setConfig({
38
+ /**
39
+ * Fallback to the default Emulsify theme on import error.
40
+ * @type {{ theme: object }}
41
+ */
42
+ theme: emulsifyTheme,
43
+ });
13
44
  });
14
- });
45
+
@@ -1,32 +1,75 @@
1
+ // .storybook/preview.js
1
2
  import { useEffect } from '@storybook/preview-api';
2
3
  import Twig from 'twig';
3
4
  import { setupTwig, fetchCSSFiles } from './utils.js';
4
- import { getRules } from "axe-core";
5
+ import { getRules } from 'axe-core';
5
6
 
6
- // If in a Drupal project, it's recommended to import a symlinked version of drupal.js.
7
+ /**
8
+ * External override parameters loaded from project config file, if present.
9
+ * @type {object}
10
+ */
11
+ let externalOverrides = {};
12
+
13
+ // Load the preview.js from the project config overrides.
14
+ try {
15
+ /**
16
+ * Dynamically require external preview overrides.
17
+ * @module '../../../../config/emulsify-core/storybook/preview.js'
18
+ */
19
+ externalOverrides = require(
20
+ '../../../../config/emulsify-core/storybook/preview.js'
21
+ ).default;
22
+ } catch (err) {
23
+ // no override file? swallow the error and use {}
24
+ externalOverrides = {};
25
+ }
26
+
27
+ // Import Drupal behaviors for rich JavaScript integration.
7
28
  import './_drupal.js';
8
29
 
30
+ /**
31
+ * Filters accessibility rules by matching tags.
32
+ * @param {string[]} [tags=[]] List of WCAG rule tags to enable.
33
+ * @returns {{id: string, enabled: boolean}[]} Array of rule configurations.
34
+ */
9
35
  function enableRulesByTag(tags = []) {
10
36
  const allRules = getRules();
11
37
  return allRules.map(rule =>
12
- tags.some(t => rule.tags.includes(t)) ? { id: rule.ruleId, enabled: true } : { id: rule.ruleId, enabled: false }
38
+ tags.some(t => rule.tags.includes(t))
39
+ ? { id: rule.ruleId, enabled: true }
40
+ : { id: rule.ruleId, enabled: false }
13
41
  );
14
42
  }
15
43
 
44
+ /**
45
+ * Precomputed Axe accessibility rules enabled by default.
46
+ * @type {{id: string, enabled: boolean}[]}
47
+ */
16
48
  const AxeRules = enableRulesByTag([
17
- "wcag2a",
18
- "wcag2aa",
19
- "wcag21a",
20
- "wcag21aa",
21
- "wcag22aa",
22
- "best-practice",
49
+ 'wcag2a',
50
+ 'wcag2aa',
51
+ 'wcag21a',
52
+ 'wcag21aa',
53
+ 'wcag22aa',
54
+ 'best-practice',
23
55
  ]);
24
56
 
57
+ // Initialize Twig and load any CSS that your stories need.
58
+ setupTwig(Twig);
59
+ fetchCSSFiles();
60
+
61
+ /**
62
+ * Storybook decorators to apply Drupal behaviors before rendering each story.
63
+ * @type {Array<import('@storybook/react').Decorator>}
64
+ */
25
65
  export const decorators = [
66
+ /**
67
+ * Decorator that attaches Drupal behaviors on story mount.
68
+ * @param {Function} Story The story component to render.
69
+ * @param {object} context Story context including args.
70
+ * @returns {Function} Rendered story.
71
+ */
26
72
  (Story, { args }) => {
27
- const { renderAs } = args || {};
28
-
29
- // Usual emulsify hack to add Drupal behaviors.
30
73
  useEffect(() => {
31
74
  Drupal.attachBehaviors();
32
75
  }, [args]);
@@ -34,18 +77,27 @@ export const decorators = [
34
77
  },
35
78
  ];
36
79
 
37
- setupTwig(Twig);
38
- fetchCSSFiles();
39
-
40
- export const parameters = {
80
+ /**
81
+ * Default Storybook parameters before applying overrides.
82
+ * @type {object}
83
+ */
84
+ const defaultParams = {
41
85
  actions: { argTypesRegex: '^on[A-Z].*' },
42
86
  a11y: {
43
87
  config: {
44
88
  detailedReport: true,
45
- detailedReportOptions: {
46
- html: true,
47
- },
89
+ detailedReportOptions: { html: true },
48
90
  rules: AxeRules,
49
91
  },
50
92
  },
93
+ layout: 'fullscreen',
94
+ };
95
+
96
+ /**
97
+ * Merged Storybook parameters including external overrides.
98
+ * @type {object}
99
+ */
100
+ export const parameters = {
101
+ ...defaultParams,
102
+ ...externalOverrides,
51
103
  };
@@ -1,17 +1,28 @@
1
- const { resolve } = require('path');
2
- const twigDrupal = require('twig-drupal-filters');
3
- const twigBEM = require('bem-twig-extension');
4
- const twigAddAttributes = require('add-attributes-twig-extension');
1
+ import { resolve, dirname } from 'path';
2
+ import twigDrupal from 'twig-drupal-filters';
3
+ import twigBEM from 'bem-twig-extension';
4
+ import twigAddAttributes from 'add-attributes-twig-extension';
5
+ import emulsifyConfig from '../../../../project.emulsify.json' with { type: 'json' };
6
+
7
+ // Create __filename from import.meta.url without fileURLToPath
8
+ let _filename = decodeURIComponent(new URL(import.meta.url).pathname);
9
+
10
+ // On Windows, remove the leading slash (e.g. "/C:/path" -> "C:/path")
11
+ if (process.platform === 'win32' && _filename.startsWith('/')) {
12
+ _filename = _filename.slice(1);
13
+ }
14
+
15
+ const _dirname = dirname(_filename);
5
16
 
6
17
  /**
7
18
  * Fetches project-based variant configuration. If no such configuration
8
19
  * exists, returns default values as a flat component structure.
9
20
  *
10
- * @returns project-based variant configuration, or default config.
21
+ * @returns {Array} project-based variant configuration, or default config.
11
22
  */
12
23
  const fetchVariantConfig = () => {
13
24
  try {
14
- return require('../../../../project.emulsify.json').variant.structureImplementations;
25
+ return emulsifyConfig.variant.structureImplementations;
15
26
  } catch (e) {
16
27
  return [
17
28
  {
@@ -32,38 +43,37 @@ const fetchCSSFiles = () => {
32
43
  try {
33
44
  // Load all CSS files from 'dist'.
34
45
  const cssFiles = require.context('../../../../dist', true, /\.css$/);
35
- cssFiles.keys().forEach(file => cssFiles(file));
46
+ cssFiles.keys().forEach((file) => cssFiles(file));
36
47
 
37
48
  // Load all CSS files from 'components' for 'drupal' platform.
38
- const emulsifyConfig = require('../../../../project.emulsify.json');
39
49
  if (emulsifyConfig.project.platform === 'drupal') {
40
50
  const drupalCSSFiles = require.context('../../../../components', true, /\.css$/);
41
- drupalCSSFiles.keys().forEach(file => drupalCSSFiles(file));
51
+ drupalCSSFiles.keys().forEach((file) => drupalCSSFiles(file));
42
52
  }
43
53
  } catch (e) {
44
54
  return undefined;
45
55
  }
46
56
  };
47
57
 
48
- module.exports.namespaces = {};
58
+ // Build namespaces mapping.
59
+ export const namespaces = {};
49
60
  for (const { name, directory } of fetchVariantConfig()) {
50
- module.exports.namespaces[name] = resolve(__dirname, '../../../../', directory);
61
+ namespaces[name] = resolve(_dirname, '../../../../', directory);
51
62
  }
52
63
 
53
64
  /**
54
- * Configures and extends a standard twig object.
65
+ * Configures and extends a standard Twig object.
55
66
  *
56
- * @param {Twig} twig - twig object that should be configured and extended.
57
- *
58
- * @returns {Twig} configured twig object.
67
+ * @param {Object} twig - Twig object that should be configured and extended.
68
+ * @returns {Object} Configured Twig object.
59
69
  */
60
- module.exports.setupTwig = function setupTwig(twig) {
70
+ export function setupTwig(twig) {
61
71
  twig.cache();
62
72
  twigDrupal(twig);
63
73
  twigBEM(twig);
64
74
  twigAddAttributes(twig);
65
75
  return twig;
66
- };
76
+ }
67
77
 
68
- // Export the fetchCSSFiles function
69
- module.exports.fetchCSSFiles = fetchCSSFiles;
78
+ // Export the fetchCSSFiles function.
79
+ export { fetchCSSFiles };