@hmlr/govuk-react-components-library 1.2.3 → 1.3.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.
Files changed (204) hide show
  1. package/README.md +6 -8
  2. package/dist/PDFViewerCanvas.cjs.js +3 -2
  3. package/dist/PDFViewerCanvas.cjs.js.map +1 -1
  4. package/dist/PDFViewerCanvas.esm.js +3 -2
  5. package/dist/PDFViewerCanvas.esm.js.map +1 -1
  6. package/dist/index.cjs.js.map +1 -1
  7. package/dist/index.d.ts +29 -60
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/types/PDFViewerCanvas.d.ts +1 -0
  10. package/dist/types/components/Accordion/Accordion.config.d.ts +5 -0
  11. package/dist/types/components/Accordion/Accordion.d.ts +4 -0
  12. package/dist/types/components/Accordion/Accordion.types.d.ts +20 -0
  13. package/dist/types/components/Accordion/index.d.ts +2 -0
  14. package/dist/types/components/ActionLink/ActionLink.d.ts +4 -0
  15. package/dist/types/components/ActionLink/ActionLink.types.d.ts +13 -0
  16. package/dist/types/components/ActionLink/index.d.ts +1 -0
  17. package/dist/types/components/BackLink/BackLink.d.ts +4 -0
  18. package/dist/types/components/BackLink/BackLink.types.d.ts +9 -0
  19. package/dist/types/components/BackLink/index.d.ts +1 -0
  20. package/dist/types/components/Boolean/Boolean.d.ts +4 -0
  21. package/dist/types/components/Boolean/Boolean.types.d.ts +37 -0
  22. package/dist/types/components/Boolean/OmitKey.d.ts +5 -0
  23. package/dist/types/components/Boolean/index.d.ts +1 -0
  24. package/dist/types/components/Breadcrumbs/Breadcrumbs.d.ts +4 -0
  25. package/dist/types/components/Breadcrumbs/Breadcrumbs.types.d.ts +13 -0
  26. package/dist/types/components/Breadcrumbs/index.d.ts +1 -0
  27. package/dist/types/components/Button/Button.config.d.ts +2 -0
  28. package/dist/types/components/Button/Button.d.ts +4 -0
  29. package/dist/types/components/Button/Button.types.d.ts +13 -0
  30. package/dist/types/components/Button/index.d.ts +2 -0
  31. package/dist/types/components/CardColumn/CardColumn.d.ts +5 -0
  32. package/dist/types/components/CardColumn/CardColumn.types.d.ts +25 -0
  33. package/dist/types/components/CardColumn/index.d.ts +1 -0
  34. package/dist/types/components/CardLayout/CardLayout.d.ts +4 -0
  35. package/dist/types/components/CardLayout/CardLayout.types.d.ts +5 -0
  36. package/dist/types/components/CardLayout/index.d.ts +1 -0
  37. package/dist/types/components/CharacterCount/CharacterCount.config.d.ts +2 -0
  38. package/dist/types/components/CharacterCount/CharacterCount.d.ts +4 -0
  39. package/dist/types/components/CharacterCount/CharacterCount.types.d.ts +12 -0
  40. package/dist/types/components/CharacterCount/index.d.ts +1 -0
  41. package/dist/types/components/Checkboxes/Checkboxes.config.d.ts +1 -0
  42. package/dist/types/components/Checkboxes/Checkboxes.d.ts +4 -0
  43. package/dist/types/components/Checkboxes/Checkboxes.types.d.ts +3 -0
  44. package/dist/types/components/Checkboxes/index.d.ts +2 -0
  45. package/dist/types/components/CookieBanner/CookieBanner.d.ts +4 -0
  46. package/dist/types/components/CookieBanner/CookieBanner.types.d.ts +21 -0
  47. package/dist/types/components/CookieBanner/index.d.ts +1 -0
  48. package/dist/types/components/DataNavigation/DataNavigation.d.ts +4 -0
  49. package/dist/types/components/DataNavigation/DataNavigation.types.d.ts +34 -0
  50. package/dist/types/components/DataNavigation/index.d.ts +1 -0
  51. package/dist/types/components/DateInput/DateInput.d.ts +4 -0
  52. package/dist/types/components/DateInput/DateInput.types.d.ts +27 -0
  53. package/dist/types/components/DateInput/index.d.ts +1 -0
  54. package/dist/types/components/Details/Details.d.ts +4 -0
  55. package/dist/types/components/Details/Details.types.d.ts +5 -0
  56. package/dist/types/components/Details/index.d.ts +1 -0
  57. package/dist/types/components/DifferenceNavigation/DifferenceNavigation.d.ts +4 -0
  58. package/dist/types/components/DifferenceNavigation/DifferenceNavigation.types.d.ts +32 -0
  59. package/dist/types/components/DifferenceNavigation/index.d.ts +1 -0
  60. package/dist/types/components/ErrorBoundary/ErrorBoundary.d.ts +9 -0
  61. package/dist/types/components/ErrorBoundary/ErrorBoundary.types.d.ts +11 -0
  62. package/dist/types/components/ErrorBoundary/index.d.ts +1 -0
  63. package/dist/types/components/ErrorMessage/ErrorMessage.d.ts +4 -0
  64. package/dist/types/components/ErrorMessage/ErrorMessage.types.d.ts +6 -0
  65. package/dist/types/components/ErrorMessage/index.d.ts +1 -0
  66. package/dist/types/components/ErrorSummary/ErrorSummary.config.d.ts +2 -0
  67. package/dist/types/components/ErrorSummary/ErrorSummary.d.ts +4 -0
  68. package/dist/types/components/ErrorSummary/ErrorSummary.types.d.ts +13 -0
  69. package/dist/types/components/ErrorSummary/index.d.ts +2 -0
  70. package/dist/types/components/ExitThisPage/ExitThisPage.config.d.ts +1 -0
  71. package/dist/types/components/ExitThisPage/ExitThisPage.d.ts +4 -0
  72. package/dist/types/components/ExitThisPage/ExitThisPage.types.d.ts +17 -0
  73. package/dist/types/components/ExitThisPage/index.d.ts +2 -0
  74. package/dist/types/components/Fieldset/Fieldset.d.ts +4 -0
  75. package/dist/types/components/Fieldset/Fieldset.types.d.ts +12 -0
  76. package/dist/types/components/Fieldset/index.d.ts +1 -0
  77. package/dist/types/components/FileUpload/FileUpload.d.ts +4 -0
  78. package/dist/types/components/FileUpload/FileUpload.types.d.ts +13 -0
  79. package/dist/types/components/FileUpload/index.d.ts +1 -0
  80. package/dist/types/components/Footer/Footer.d.ts +4 -0
  81. package/dist/types/components/Footer/Footer.types.d.ts +40 -0
  82. package/dist/types/components/Footer/index.d.ts +1 -0
  83. package/dist/types/components/Header/Header.config.d.ts +1 -0
  84. package/dist/types/components/Header/Header.d.ts +4 -0
  85. package/dist/types/components/Header/Header.types.d.ts +28 -0
  86. package/dist/types/components/Header/index.d.ts +2 -0
  87. package/dist/types/components/Hint/Hint.d.ts +4 -0
  88. package/dist/types/components/Hint/Hint.types.d.ts +4 -0
  89. package/dist/types/components/Hint/index.d.ts +1 -0
  90. package/dist/types/components/Input/Input.d.ts +4 -0
  91. package/dist/types/components/Input/Input.types.d.ts +17 -0
  92. package/dist/types/components/Input/index.d.ts +1 -0
  93. package/dist/types/components/InsetText/InsetText.d.ts +4 -0
  94. package/dist/types/components/InsetText/InsetText.types.d.ts +4 -0
  95. package/dist/types/components/InsetText/index.d.ts +1 -0
  96. package/dist/types/components/Label/Label.d.ts +4 -0
  97. package/dist/types/components/Label/Label.types.d.ts +6 -0
  98. package/dist/types/components/Label/index.d.ts +1 -0
  99. package/dist/types/components/Landing/Landing.d.ts +4 -0
  100. package/dist/types/components/Landing/Landing.types.d.ts +4 -0
  101. package/dist/types/components/Landing/index.d.ts +1 -0
  102. package/dist/types/components/LinkWithRef/LinkWithRef.d.ts +4 -0
  103. package/dist/types/components/LinkWithRef/LinkWithRef.types.d.ts +9 -0
  104. package/dist/types/components/LinkWithRef/index.d.ts +1 -0
  105. package/dist/types/components/Loading/Loading.d.ts +5 -0
  106. package/dist/types/components/Loading/Loading.types.d.ts +4 -0
  107. package/dist/types/components/Loading/index.d.ts +1 -0
  108. package/dist/types/components/Main/Main.d.ts +4 -0
  109. package/dist/types/components/Main/Main.types.d.ts +5 -0
  110. package/dist/types/components/Main/index.d.ts +1 -0
  111. package/dist/types/components/NotFoundPage/NotFoundPage.d.ts +3 -0
  112. package/dist/types/components/NotFoundPage/index.d.ts +1 -0
  113. package/dist/types/components/NotificationBanner/NotificationBanner.config.d.ts +2 -0
  114. package/dist/types/components/NotificationBanner/NotificationBanner.d.ts +4 -0
  115. package/dist/types/components/NotificationBanner/NotificationBanner.types.d.ts +12 -0
  116. package/dist/types/components/NotificationBanner/index.d.ts +1 -0
  117. package/dist/types/components/PDFViewer/ObjectToQueryString.d.ts +1 -0
  118. package/dist/types/components/PDFViewer/PDFViewer.d.ts +4 -0
  119. package/dist/types/components/PDFViewer/PDFViewer.types.d.ts +25 -0
  120. package/dist/types/components/PDFViewer/PDFViewerBackend.d.ts +2 -0
  121. package/dist/types/components/PDFViewer/ResolvePDFSource.d.ts +27 -0
  122. package/dist/types/components/PDFViewer/Slugify.d.ts +1 -0
  123. package/dist/types/components/PDFViewer/index.d.ts +2 -0
  124. package/dist/types/components/PDFViewer/testutilities/SampleBase64.d.ts +44 -0
  125. package/dist/types/components/PDFViewerCanvas/PDFViewerCanvas.d.ts +9 -0
  126. package/dist/types/components/PDFViewerCanvas/PDFViewerCanvas.types.d.ts +38 -0
  127. package/dist/types/components/PDFViewerCanvas/index.d.ts +1 -0
  128. package/dist/types/components/Pagination/NavigationButton.d.ts +3 -0
  129. package/dist/types/components/Pagination/Pagination.d.ts +4 -0
  130. package/dist/types/components/Pagination/Pagination.types.d.ts +32 -0
  131. package/dist/types/components/Pagination/SvgIcon.d.ts +3 -0
  132. package/dist/types/components/Pagination/UsePagination.d.ts +9 -0
  133. package/dist/types/components/Pagination/index.d.ts +3 -0
  134. package/dist/types/components/Panel/Panel.d.ts +4 -0
  135. package/dist/types/components/Panel/Panel.types.d.ts +7 -0
  136. package/dist/types/components/Panel/index.d.ts +1 -0
  137. package/dist/types/components/PasswordInput/PasswordInput.config.d.ts +1 -0
  138. package/dist/types/components/PasswordInput/PasswordInput.d.ts +4 -0
  139. package/dist/types/components/PasswordInput/PasswordInput.types.d.ts +35 -0
  140. package/dist/types/components/PasswordInput/index.d.ts +2 -0
  141. package/dist/types/components/PhaseBanner/PhaseBanner.d.ts +4 -0
  142. package/dist/types/components/PhaseBanner/PhaseBanner.types.d.ts +6 -0
  143. package/dist/types/components/PhaseBanner/index.d.ts +1 -0
  144. package/dist/types/components/ProblemWithService/ProblemWithService.d.ts +4 -0
  145. package/dist/types/components/ProblemWithService/ProblemWithService.types.d.ts +7 -0
  146. package/dist/types/components/ProblemWithService/index.d.ts +1 -0
  147. package/dist/types/components/Radios/Radios.config.d.ts +1 -0
  148. package/dist/types/components/Radios/Radios.d.ts +4 -0
  149. package/dist/types/components/Radios/Radios.types.d.ts +7 -0
  150. package/dist/types/components/Radios/index.d.ts +2 -0
  151. package/dist/types/components/Select/Select.d.ts +4 -0
  152. package/dist/types/components/Select/Select.types.d.ts +19 -0
  153. package/dist/types/components/Select/index.d.ts +1 -0
  154. package/dist/types/components/ServiceNavigation/ServiceNavigation.d.ts +4 -0
  155. package/dist/types/components/ServiceNavigation/ServiceNavigation.types.d.ts +21 -0
  156. package/dist/types/components/ServiceNavigation/index.d.ts +1 -0
  157. package/dist/types/components/SkipLink/SkipLink.config.d.ts +1 -0
  158. package/dist/types/components/SkipLink/SkipLink.d.ts +4 -0
  159. package/dist/types/components/SkipLink/SkipLink.types.d.ts +5 -0
  160. package/dist/types/components/SkipLink/index.d.ts +2 -0
  161. package/dist/types/components/SummaryList/SummaryList.d.ts +4 -0
  162. package/dist/types/components/SummaryList/SummaryList.types.d.ts +22 -0
  163. package/dist/types/components/SummaryList/index.d.ts +1 -0
  164. package/dist/types/components/Table/Table.d.ts +4 -0
  165. package/dist/types/components/Table/Table.types.d.ts +21 -0
  166. package/dist/types/components/Table/index.d.ts +1 -0
  167. package/dist/types/components/Tabs/Tabs.config.d.ts +1 -0
  168. package/dist/types/components/Tabs/Tabs.d.ts +4 -0
  169. package/dist/types/components/Tabs/Tabs.types.d.ts +14 -0
  170. package/dist/types/components/Tabs/index.d.ts +2 -0
  171. package/dist/types/components/Tag/Tag.d.ts +4 -0
  172. package/dist/types/components/Tag/Tag.types.d.ts +4 -0
  173. package/dist/types/components/Tag/index.d.ts +1 -0
  174. package/dist/types/components/TaskList/TaskList.d.ts +4 -0
  175. package/dist/types/components/TaskList/TaskList.types.d.ts +21 -0
  176. package/dist/types/components/TaskList/index.d.ts +1 -0
  177. package/dist/types/components/Textarea/Textarea.d.ts +4 -0
  178. package/dist/types/components/Textarea/Textarea.types.d.ts +14 -0
  179. package/dist/types/components/Textarea/index.d.ts +1 -0
  180. package/dist/types/components/WarningInfo/WarningInfo.d.ts +4 -0
  181. package/dist/types/components/WarningInfo/WarningInfo.types.d.ts +8 -0
  182. package/dist/types/components/WarningInfo/index.d.ts +1 -0
  183. package/dist/types/components/WarningText/WarningText.d.ts +4 -0
  184. package/dist/types/components/WarningText/WarningText.types.d.ts +4 -0
  185. package/dist/types/components/WarningText/index.d.ts +1 -0
  186. package/dist/types/components/common.types.d.ts +10 -0
  187. package/dist/types/components/index.d.ts +51 -0
  188. package/dist/types/dynamics/index.d.ts +3 -0
  189. package/dist/types/dynamics/types.d.ts +37 -0
  190. package/dist/types/index.d.ts +1 -0
  191. package/dist/types/utils/ProcessExampleData.d.ts +3 -0
  192. package/dist/types/utils/StoriesFromFixtures.d.ts +5 -0
  193. package/dist/types/utils/TitleCase.d.ts +2 -0
  194. package/dist/types/utils/WithReference.d.ts +5 -0
  195. package/dist/types/utils/WithReference.types.d.ts +15 -0
  196. package/package.json +47 -27
  197. package/.gitattributes +0 -5
  198. package/.nvmrc +0 -1
  199. package/.prettierignore +0 -2
  200. package/.stylelintrc.json +0 -8
  201. package/assets/Advance-submodule-behaviours.png +0 -0
  202. package/assets/logo.png +0 -0
  203. package/assets/uml-diagram.svg +0 -2745
  204. package/scripts/generateStories.mjs +0 -451
@@ -1,451 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * scripts/generateStories.mjs
4
- *
5
- * Generates *.stories.tsx files from govuk-frontend fixtures.json files.
6
- * Each non-hidden fixture becomes its own top-level named export — the only
7
- * format Storybook's CSF parser supports for individual sidebar entries.
8
- *
9
- * Also reports any govuk-frontend components that have no matching folder in
10
- * src/components/, so new upstream components are never silently missed.
11
- *
12
- * Usage:
13
- * node scripts/generateStories.mjs
14
- * node scripts/generateStories.mjs --component=Accordion
15
- * node scripts/generateStories.mjs --framework=react-webpack5
16
- * node scripts/generateStories.mjs --framework=react-vite
17
- *
18
- * The --framework flag overrides auto-detection from package.json.
19
- * Auto-detection checks devDependencies for @storybook/react-webpack5
20
- * or @storybook/react-vite and picks whichever is present.
21
- *
22
- * Add to package.json:
23
- * "generate-stories": "node scripts/generateStories.mjs",
24
- * "prestorybook": "npm run generate-stories",
25
- * "prebuild-storybook": "npm run generate-stories"
26
- */
27
-
28
- import fs from "fs";
29
- import path from "path";
30
- import { fileURLToPath } from "url";
31
- import * as prettier from "prettier";
32
-
33
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
34
- const ROOT = path.resolve(__dirname, "..");
35
- const COMPONENTS_DIR = path.join(ROOT, "src", "components");
36
- const GOVUK_FIXTURES_BASE = path.join(
37
- ROOT,
38
- "node_modules",
39
- "govuk-frontend",
40
- "dist",
41
- "govuk",
42
- "components",
43
- );
44
-
45
- // ---------------------------------------------------------------------------
46
- // Storybook framework detection
47
- //
48
- // Determines which @storybook package to import Meta/StoryObj from.
49
- // Override by passing --framework=react-webpack5 or --framework=react-vite.
50
- // If not specified, auto-detects from the installed packages.
51
- // ---------------------------------------------------------------------------
52
- function detectStorybookFramework() {
53
- const frameworkArg = process.argv.find((a) => a.startsWith("--framework="));
54
- if (frameworkArg) return frameworkArg.split("=")[1];
55
-
56
- const pkgPath = path.join(ROOT, "package.json");
57
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
58
- const allDeps = {
59
- ...pkg.dependencies,
60
- ...pkg.devDependencies,
61
- ...pkg.peerDependencies,
62
- };
63
-
64
- if (allDeps["@storybook/react-webpack5"]) return "react-webpack5";
65
- if (allDeps["@storybook/react-vite"]) return "react-vite";
66
-
67
- // fallback
68
- return "react-webpack5";
69
- }
70
-
71
- const STORYBOOK_FRAMEWORK = detectStorybookFramework();
72
-
73
- // ---------------------------------------------------------------------------
74
- // Fixture name overrides
75
- //
76
- // By default the script converts a component folder name to kebab-case and
77
- // looks for a matching govuk-frontend fixtures.json:
78
- // "CookieBanner" → "cookie-banner" → .../cookie-banner/fixtures.json
79
- //
80
- // If a component folder name does NOT map cleanly to its govuk-frontend name,
81
- // add an entry here:
82
- // "MyFolderName": "govuk-fixture-name"
83
- // ---------------------------------------------------------------------------
84
- const FIXTURE_NAME_OVERRIDES = {
85
- // Example:
86
- // "ErrorSummary": "error-summary",
87
- };
88
-
89
- // ---------------------------------------------------------------------------
90
- // Per-component customisation
91
- //
92
- // Components whose folder contains a {Component}.config.ts[x] file receive a
93
- // standard configuration decorator automatically — no entry needed here.
94
- //
95
- // Only add an entry here if a component needs something beyond the standard
96
- // config-file decorator (e.g. completely different decorator logic, additional
97
- // imports on top of the config import, etc.). Entries here are deep-merged
98
- // with — and take precedence over — the auto-generated config.
99
- // ---------------------------------------------------------------------------
100
- const CUSTOM_CONFIG = {};
101
-
102
- // ---------------------------------------------------------------------------
103
- // Generation skip list
104
- //
105
- // Add a component folder name here when its stories are hand-authored and
106
- // must never be overwritten by this script.
107
- //
108
- // Why a component ends up here:
109
- // The govuk-frontend fixture props are designed for the server-side Nunjucks
110
- // template and don't map to the React component's API. Feeding them through
111
- // extractShownFixtures produces undefined/NaN values at runtime.
112
- // Hand-authored stories use the real React props directly instead.
113
- // ---------------------------------------------------------------------------
114
- const GENERATION_SKIP = new Set([
115
- // Add component folder names here if their stories must be hand-authored.
116
- // See comment block above for guidance on when this is needed.
117
- ]);
118
-
119
- // ---------------------------------------------------------------------------
120
- // Config-file auto-detection
121
- //
122
- // If a component folder contains {Component}.config.ts or .config.tsx the
123
- // generator injects:
124
- // • an import for the Configure function
125
- // • a `let configured = false` guard (prevents double-init in docs mode)
126
- // • a decorator that calls the function on every story render, but only
127
- // once per page-load when Storybook is showing the auto-docs page.
128
- //
129
- // The docs-mode URL pattern changed in Storybook 8:
130
- // old: ?viewMode=docs
131
- // new: ?path=/docs/govuk-design-system-{kebab}--docs
132
- // ---------------------------------------------------------------------------
133
-
134
- /** Returns true when the component folder has a .config.ts or .config.tsx file. */
135
- function hasConfigFile(componentName) {
136
- const dir = path.join(COMPONENTS_DIR, componentName);
137
- return (
138
- fs.existsSync(path.join(dir, `${componentName}.config.ts`)) ||
139
- fs.existsSync(path.join(dir, `${componentName}.config.tsx`))
140
- );
141
- }
142
-
143
- /**
144
- * Builds the standard CUSTOM_CONFIG-shaped object for any component that has
145
- * a config file. The caller can then merge this with an explicit CUSTOM_CONFIG
146
- * entry when one exists.
147
- */
148
- function buildConfigFileCustomisation(componentName) {
149
- const kebab = resolveFixtureName(componentName);
150
- const fnName = `ConfigureOverall${componentName}`;
151
- const docsPath = `path=/docs/govuk-design-system-${kebab}--docs`;
152
-
153
- return {
154
- extraImports: `import { ${fnName} } from "./${componentName}.config";`,
155
- extraSetup: `\nlet configured = false;`,
156
- decorator: `(Story, { parameters }) => {
157
- React.useEffect(() => {
158
- const isDocsMode = window.location.search.includes("${docsPath}");
159
- if (isDocsMode && !configured && parameters.initializeConfigurations) {
160
- void ${fnName}();
161
- configured = true;
162
- } else if (!isDocsMode) {
163
- void ${fnName}();
164
- }
165
- }, []);
166
- return <Story />;
167
- }`,
168
- };
169
- }
170
-
171
- /** Returns the resolved customisation for a component (auto + manual merged). */
172
- function resolveCustomConfig(componentName) {
173
- const auto = hasConfigFile(componentName)
174
- ? buildConfigFileCustomisation(componentName)
175
- : null;
176
- const manual = CUSTOM_CONFIG[componentName] ?? null;
177
-
178
- if (!auto && !manual) return null;
179
- // Manual entries win — they can override individual keys (e.g. decorator)
180
- // while still inheriting auto-generated imports/setup when not specified.
181
- return { ...auto, ...manual };
182
- }
183
-
184
- // ---------------------------------------------------------------------------
185
- // Helpers
186
- // ---------------------------------------------------------------------------
187
-
188
- /** "with all sections already open" → "WithAllSectionsAlreadyOpen" */
189
- function toExportName(name) {
190
- return name
191
- .replace(/[^a-zA-Z0-9 ]/g, " ")
192
- .split(" ")
193
- .filter(Boolean)
194
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
195
- .join("");
196
- }
197
-
198
- /** "CookieBanner" → "cookie-banner" */
199
- function toKebabCase(name) {
200
- return name
201
- .replace(/([A-Z])/g, (m, l, i) => (i === 0 ? l : `-${l}`).toLowerCase())
202
- .toLowerCase();
203
- }
204
-
205
- /** "cookie-banner" → "CookieBanner" */
206
- function toPascalCase(kebab) {
207
- return kebab
208
- .split("-")
209
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
210
- .join("");
211
- }
212
-
213
- /** "file-upload" → "File upload" | "cookie-banner" → "Cookie banner" */
214
- function toTitleLabel(kebab) {
215
- const words = kebab.replace(/-/g, " ");
216
- return words.charAt(0).toUpperCase() + words.slice(1);
217
- }
218
-
219
- function resolveFixtureName(componentName) {
220
- return FIXTURE_NAME_OVERRIDES[componentName] ?? toKebabCase(componentName);
221
- }
222
-
223
- function getVisibleFixtures(componentName) {
224
- const fixtureName = resolveFixtureName(componentName);
225
- const fixturePath = path.join(
226
- GOVUK_FIXTURES_BASE,
227
- fixtureName,
228
- "fixtures.json",
229
- );
230
- if (!fs.existsSync(fixturePath)) return null;
231
- const { fixtures } = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
232
- return fixtures.filter((f) => f.hidden === false);
233
- }
234
-
235
- function getAllGovukComponents() {
236
- if (!fs.existsSync(GOVUK_FIXTURES_BASE)) return [];
237
- return fs
238
- .readdirSync(GOVUK_FIXTURES_BASE, { withFileTypes: true })
239
- .filter(
240
- (d) =>
241
- d.isDirectory() &&
242
- fs.existsSync(path.join(GOVUK_FIXTURES_BASE, d.name, "fixtures.json")),
243
- )
244
- .map((d) => d.name);
245
- }
246
-
247
- // ---------------------------------------------------------------------------
248
- // File generator
249
- //
250
- // The generated file imports fixtures and calls extractShownFixtures at
251
- // runtime — this is intentional. extractShownFixtures transforms the raw
252
- // govuk-frontend fixture data (e.g. `text` → `children`, `classes` →
253
- // `className`) into props the React components actually accept. Inlining the
254
- // raw JSON as args would cause TypeScript errors like:
255
- // "text does not exist in type { children: ReactNode }"
256
- // because the component types expect the already-transformed prop names.
257
- // ---------------------------------------------------------------------------
258
- function generateFile(componentName, visibleFixtures) {
259
- const fixtureName = resolveFixtureName(componentName);
260
- const custom = resolveCustomConfig(componentName);
261
- const hasScss = fs.existsSync(
262
- path.join(COMPONENTS_DIR, componentName, `${componentName}.scss`),
263
- );
264
-
265
- const decorator =
266
- custom?.decorator ??
267
- `(Story) => {
268
- return <Story />;
269
- }`;
270
-
271
- // Each story references its fixture by name at runtime so extractShownFixtures
272
- // can transform it. The index lookup is intentionally avoided — name-based
273
- // lookup means stories stay correct even if govuk-frontend reorders fixtures.
274
- const storyExports = visibleFixtures
275
- .map((fixture) => {
276
- const exportName = toExportName(fixture.name);
277
- return [
278
- `export const ${exportName}: Story = {`,
279
- ` name: "${fixture.name}",`,
280
- ` args: { ...examplesFromFixtures.find((f) => f.name === "${fixture.name}")?.options },`,
281
- `};`,
282
- ].join("\n");
283
- })
284
- .join("\n\n");
285
-
286
- return [
287
- `// THIS FILE IS AUTO-GENERATED — do not edit manually.`,
288
- `// Source: node_modules/govuk-frontend/dist/govuk/components/${fixtureName}/fixtures.json`,
289
- `// Regenerate: npm run generate-stories`,
290
- ``,
291
- `import React from "react";`,
292
- hasScss ? `import "./${componentName}.scss";` : null,
293
- `import ${componentName} from "./${componentName}";`,
294
- `import { Meta, StoryObj } from "@storybook/react";`,
295
- `import fixtures from "govuk-frontend/dist/govuk/components/${fixtureName}/fixtures.json";`,
296
- `import { extractShownFixtures } from "../../utils/ProcessExampleData";`,
297
- `import { ComponentFixture } from "../../dynamics";`,
298
- custom?.extraImports ?? null,
299
- custom?.extraSetup ?? null,
300
- `const meta: Meta<typeof ${componentName}> = {`,
301
- ` title: "GOVUK Design System/${toTitleLabel(fixtureName)}",`,
302
- ` component: ${componentName},`,
303
- ` decorators: [${decorator}],`,
304
- ` tags: ["autodocs"],`,
305
- `};`,
306
- ``,
307
- `export default meta;`,
308
- `type Story = StoryObj<typeof ${componentName}>;`,
309
- ``,
310
- `// extractShownFixtures transforms raw govuk-frontend fixture data into`,
311
- `// React-compatible props (e.g. text → children, classes → className).`,
312
- `const examplesFromFixtures: Array<ComponentFixture> = extractShownFixtures(fixtures);`,
313
- ``,
314
- storyExports,
315
- ]
316
- .filter((line) => line !== null)
317
- .join("\n")
318
- .replace(/\n{3,}/g, "\n\n");
319
- }
320
-
321
- // ---------------------------------------------------------------------------
322
- // Main
323
- // ---------------------------------------------------------------------------
324
- const targetArg = process.argv.find((a) => a.startsWith("--component="));
325
- const targetComponent = targetArg?.split("=")[1];
326
-
327
- const srcComponentNames = fs
328
- .readdirSync(COMPONENTS_DIR, { withFileTypes: true })
329
- .filter((d) => d.isDirectory())
330
- .map((d) => d.name)
331
- .filter((name) => !targetComponent || name === targetComponent);
332
-
333
- const govukComponentKebabs = getAllGovukComponents();
334
-
335
- const srcKebabToFolder = Object.fromEntries(
336
- srcComponentNames.map((name) => [resolveFixtureName(name), name]),
337
- );
338
-
339
- const notImplemented = govukComponentKebabs.filter(
340
- (kebab) => !srcKebabToFolder[kebab],
341
- );
342
-
343
- const results = { generated: [], skipped: [], noFixtures: [] };
344
-
345
- for (const componentName of srcComponentNames) {
346
- if (GENERATION_SKIP.has(componentName)) {
347
- results.skipped.push({
348
- componentName,
349
- reason: "hand-authored stories (GENERATION_SKIP)",
350
- });
351
- continue;
352
- }
353
-
354
- const fixtures = getVisibleFixtures(componentName);
355
- const fixtureName = resolveFixtureName(componentName);
356
-
357
- if (!fixtures) {
358
- results.noFixtures.push({ componentName, fixtureName });
359
- continue;
360
- }
361
-
362
- if (fixtures.length === 0) {
363
- results.skipped.push({ componentName, reason: "all fixtures are hidden" });
364
- continue;
365
- }
366
-
367
- const outputPath = path.join(
368
- COMPONENTS_DIR,
369
- componentName,
370
- `${componentName}.stories.tsx`,
371
- );
372
-
373
- // Formatted here (rather than left for a separate `npm run format` pass)
374
- // so the output always matches what `npm run lint` expects - this script
375
- // runs on every `npm run storybook` via the `prestorybook` hook, and
376
- // unformatted output would fail lint the moment someone commits it.
377
- const formatted = await prettier.format(
378
- generateFile(componentName, fixtures),
379
- { parser: "typescript", filepath: outputPath },
380
- );
381
-
382
- // Skip the write when content is unchanged, so a plain `npm run storybook`
383
- // restart doesn't touch every story file's mtime (noisy for file watchers
384
- // and git status) when nothing upstream has actually changed.
385
- const existing = fs.existsSync(outputPath)
386
- ? fs.readFileSync(outputPath, "utf8")
387
- : null;
388
- if (existing !== formatted) {
389
- fs.writeFileSync(outputPath, formatted, "utf8");
390
- }
391
- results.generated.push({ componentName, count: fixtures.length });
392
- }
393
-
394
- // ---------------------------------------------------------------------------
395
- // Report
396
- // ---------------------------------------------------------------------------
397
- console.log("\n📖 Story generation report\n");
398
- console.log(` Storybook framework: @storybook/${STORYBOOK_FRAMEWORK}`);
399
-
400
- if (results.generated.length) {
401
- console.log("✅ Generated:");
402
- results.generated.forEach(({ componentName, count }) =>
403
- console.log(
404
- ` ${componentName}.stories.tsx (${count} ${count === 1 ? "story" : "stories"})`,
405
- ),
406
- );
407
- }
408
-
409
- if (results.skipped.length) {
410
- console.log("\n⚠️ Skipped (fixtures exist but all are hidden):");
411
- results.skipped.forEach(({ componentName, reason }) =>
412
- console.log(` ${componentName} — ${reason}`),
413
- );
414
- }
415
-
416
- if (results.noFixtures.length) {
417
- console.log(
418
- "\n⬜ No govuk-frontend fixtures (custom components — expected):",
419
- );
420
- results.noFixtures.forEach(({ componentName, fixtureName }) =>
421
- console.log(
422
- ` ${componentName} → looked for: ${fixtureName}/fixtures.json`,
423
- ),
424
- );
425
- console.log(
426
- "\n 💡 If any of the above should have fixtures, add an entry to",
427
- "\n FIXTURE_NAME_OVERRIDES in scripts/generateStories.mjs",
428
- );
429
- }
430
-
431
- if (notImplemented.length && !targetComponent) {
432
- console.log(
433
- "\n🆕 govuk-frontend components not yet in src/components/ (consider implementing):",
434
- );
435
- notImplemented.forEach((kebab) =>
436
- console.log(
437
- ` ${kebab} → suggested folder name: ${toPascalCase(kebab)}`,
438
- ),
439
- );
440
- console.log(
441
- "\n 💡 These are govuk-frontend components that exist upstream but have",
442
- "\n no matching folder in src/components/. Create the component folder",
443
- "\n and re-run this script to generate its stories automatically.",
444
- );
445
- }
446
-
447
- console.log(
448
- `\nDone — ${results.generated.length} generated, ${results.skipped.length} skipped, ` +
449
- `${results.noFixtures.length} have no govuk-frontend fixtures, ` +
450
- `${notImplemented.length} govuk-frontend component(s) not yet implemented.\n`,
451
- );