@qld-gov-au/qgds-bootstrap5 2.0.4 → 2.0.5

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.
@@ -0,0 +1,40 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ /**
5
+ * Creates a temporary main.<themeVar>.scss file with the theme scss import injected after qld-variables import.
6
+ * Only creates/updates the file if content has changed or file doesn't exist.
7
+ * Returns the path to the temp file, or null if not created.
8
+ */
9
+ export function createOverrideThemeScssEntry({ cssDir, mainScss, themeVar }) {
10
+ const themeFile = path.join(cssDir, `themes/${themeVar}.scss`);
11
+ const tempEntry = path.join(cssDir, `main.${themeVar}.scss`);
12
+
13
+ // Copy main.scss and inject Theme after qld-variables import
14
+ let mainContent = fs.readFileSync(mainScss, "utf8");
15
+ if (fs.existsSync(themeFile)) {
16
+ mainContent = `@import './themes/${themeVar}.scss';` + mainContent;
17
+ } else {
18
+ console.warn(
19
+ `[SCSS Theme] Warning: Theme variables file not found at ${themeFile}`,
20
+ );
21
+ }
22
+
23
+ // Only write file if it doesn't exist or content has changed
24
+ let shouldWrite = true;
25
+ if (fs.existsSync(tempEntry)) {
26
+ const existingContent = fs.readFileSync(tempEntry, "utf8");
27
+ shouldWrite = existingContent !== mainContent;
28
+ }
29
+
30
+ if (shouldWrite) {
31
+ fs.writeFileSync(tempEntry, mainContent);
32
+ console.log(`[SCSS Theme] Created/updated temp entry: ${tempEntry}`);
33
+ } else {
34
+ console.log(
35
+ `[SCSS Theme] No changes detected, reusing existing: ${tempEntry}`,
36
+ );
37
+ }
38
+
39
+ return tempEntry;
40
+ }
@@ -35,6 +35,10 @@ export default function QGDSupdateHandlebarsPartialsPlugin() {
35
35
  build.onStart(async () => {
36
36
 
37
37
  const files = getAllFiles(COMPONENTS_DIR);
38
+ // Sort files alphabetically to ensure deterministic ordering
39
+ // This prevents TurboSnap cache invalidation in Chromatic builds
40
+ // caused by non-deterministic filesystem readdir ordering
41
+ files.sort();
38
42
  //console.log(files);
39
43
  const fileNames = new Map();
40
44
  let duplicateFound = false;
@@ -0,0 +1,155 @@
1
+ const loadedThemes = new Map();
2
+ const themeStyleElements = new Map();
3
+ let currentTheme = "masterbrand";
4
+
5
+ // Dynamic theme modules import for lazy loading
6
+ // Automatically generate theme modules based on available theme files
7
+ const themeModules = (() => {
8
+ const modules = {
9
+ customized: () => import("../src/css/main.scss"),
10
+ };
11
+
12
+ // Get all theme files in the themes directory
13
+ const themeContext = import.meta.glob("../src/css/main.*.scss", {
14
+ eager: false,
15
+ });
16
+
17
+ for (const [path, moduleImporter] of Object.entries(themeContext)) {
18
+ // Extract theme name from path (e.g., "../src/css/themes/main.test.scss" -> "test")
19
+ const match = path.match(/(?:main\.)?(\w+)\.scss$/);
20
+ if (match) {
21
+ const themeName = match[1];
22
+ // Only include main.*.scss files or individual theme files if no main.*.scss exists
23
+ if (path.includes(`main.${themeName}.scss`)) {
24
+ modules[themeName] = moduleImporter;
25
+ } else if (
26
+ !Object.keys(themeContext).some((p) =>
27
+ p.includes(`main.${themeName}.scss`),
28
+ )
29
+ ) {
30
+ // If no main.*.scss file exists for this theme, use the individual theme file
31
+ modules[themeName] = moduleImporter;
32
+ }
33
+ }
34
+ }
35
+
36
+ return modules;
37
+ })();
38
+
39
+ function mapStyleElementsByTheme(callback) {
40
+ // Handle both dev mode (style elements) and production mode (link elements)
41
+ const styleElements = document.querySelectorAll('style[type="text/css"]');
42
+ const linkElements = document.querySelectorAll('link[rel="stylesheet"]');
43
+
44
+ styleElements.forEach((element) => {
45
+ callback(element);
46
+ });
47
+
48
+ linkElements.forEach((element) => {
49
+ callback(element);
50
+ });
51
+ }
52
+
53
+ const unloadTheme = (themeName) => {
54
+ // Cache current theme's style elements before removing
55
+ // Remove existing style elements for current theme
56
+ const currentStyleElements = [];
57
+ const themeNotExist = themeName && !themeStyleElements.has(themeName);
58
+ mapStyleElementsByTheme((element) => {
59
+ if (themeNotExist) {
60
+ currentStyleElements.push(element.cloneNode(true));
61
+ }
62
+ element.remove();
63
+ });
64
+
65
+ if (currentStyleElements.length > 0) {
66
+ themeStyleElements.set(themeName, currentStyleElements);
67
+ }
68
+ };
69
+
70
+ const loadTheme = async (themeName) => {
71
+ // Unload current theme if it's different
72
+ if (currentTheme && currentTheme !== themeName) {
73
+ unloadTheme(currentTheme);
74
+ }
75
+
76
+ // If theme style elements are cached, restore them
77
+ if (themeStyleElements.has(themeName)) {
78
+ const cachedElements = themeStyleElements.get(themeName);
79
+ cachedElements.forEach((element) => {
80
+ document.head.appendChild(element.cloneNode(true));
81
+ });
82
+ currentTheme = themeName;
83
+ return;
84
+ }
85
+
86
+ // If theme is already loaded, no need to reload
87
+ if (loadedThemes.has(themeName)) {
88
+ currentTheme = themeName;
89
+ return;
90
+ }
91
+
92
+ const themeModuleImporter = themeModules[themeName];
93
+ if (themeModuleImporter) {
94
+ try {
95
+ // Import the theme module to trigger SCSS loading
96
+ await themeModuleImporter();
97
+
98
+ // Cache the newly created style elements
99
+ const newStyleElements = [];
100
+
101
+ mapStyleElementsByTheme((element) => {
102
+ newStyleElements.push(element.cloneNode(true));
103
+ });
104
+ if (newStyleElements.length > 0) {
105
+ themeStyleElements.set(themeName, newStyleElements);
106
+ }
107
+
108
+ // Store references
109
+ loadedThemes.set(themeName, true);
110
+ currentTheme = themeName;
111
+ } catch (error) {
112
+ console.warn(`Failed to load theme: ${themeName}`, error);
113
+ }
114
+ }
115
+ };
116
+
117
+ export const withDynamicTheme = (Story, context) => {
118
+ const { globals } = context;
119
+ const themeName = globals.themeName || "customized";
120
+
121
+ loadTheme(themeName);
122
+
123
+ return Story();
124
+ };
125
+
126
+ export const dynamicThemeGlobalTypes = {
127
+ themeName: {
128
+ name: "Theme Palette",
129
+ description: "Theme palette selector",
130
+ defaultValue: "masterbrand",
131
+ toolbar: {
132
+ icon: "switchalt",
133
+ items: (() => {
134
+ // Dynamically generate toolbar items from available themes
135
+ const items = [];
136
+
137
+ // Add items for all discovered themes
138
+ Object.keys(themeModules).forEach((themeName) => {
139
+ if (themeName !== "customized") {
140
+ const capitalizedName =
141
+ themeName.charAt(0).toUpperCase() + themeName.slice(1);
142
+ items.push({
143
+ value: themeName,
144
+ title: `${capitalizedName} theme`,
145
+ });
146
+ }
147
+ });
148
+ items.push({ value: "customized", title: "Customized theme" });
149
+ return items;
150
+ })(),
151
+ showName: true,
152
+ dynamicTitle: true,
153
+ },
154
+ },
155
+ };
@@ -46,6 +46,12 @@ const config = {
46
46
 
47
47
  viteFinal: async (config, { configType }) => {
48
48
  config.root = "./dist";
49
+
50
+ // Define environment variables for the browser
51
+ config.define = {
52
+ ...config.define,
53
+ 'import.meta.env.ENABLE_DYNAMIC_THEME': JSON.stringify(process.env.ENABLE_DYNAMIC_THEME === 'true')
54
+ };
49
55
  // config.plugins.push({
50
56
  // name: "html-transform",
51
57
  // transform(src, id) {
@@ -1,14 +1,32 @@
1
1
  import "../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js";
2
2
  import "../src/js/qld.bootstrap.js";
3
- import "../src/css/main.scss";
3
+ import "../src/css/main.masterbrand.scss";
4
4
  import { withThemeByClassName } from "@storybook/addon-themes";
5
+ import {
6
+ withDynamicTheme,
7
+ dynamicThemeGlobalTypes,
8
+ } from "./dynamicThemeDecorator.js";
9
+
10
+ // Check if dynamic theme should be enabled via environment variable
11
+ const ENABLE_DYNAMIC_THEME = import.meta.env.ENABLE_DYNAMIC_THEME;
5
12
  import { allBackgrounds } from "./modes.js";
6
13
  import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport";
7
14
  import init from "../src/js/handlebars.init.js";
8
15
  import Handlebars from "handlebars";
9
16
 
17
+ // NOTE: TurboSnap Performance Warning
18
+ // The handlebars.init.js import above loads handlebars.partials.js which is
19
+ // auto-generated during build. Changes to this file trigger TurboSnap to
20
+ // rebuild all stories. To prevent false positives:
21
+ // 1. The generator plugin ensures deterministic file ordering
22
+ // 2. The generated file is excluded from linting
23
+ // See: .esbuild/plugins/qgds-plugin-handlebar-partial-builder.js
24
+
10
25
  /** @type { import('@storybook/html-vite').Preview } */
11
26
  const preview = {
27
+ globalTypes: {
28
+ ...(ENABLE_DYNAMIC_THEME ? dynamicThemeGlobalTypes : {}),
29
+ },
12
30
  parameters: {
13
31
  //actions: { argTypesRegex: "^on[A-Z].*" },
14
32
  chromatic: {
@@ -95,6 +113,7 @@ const preview = {
95
113
  },
96
114
 
97
115
  decorators: [
116
+ ...(ENABLE_DYNAMIC_THEME ? [withDynamicTheme] : []),
98
117
  // data-bs-theme="dark" won't be used
99
118
  withThemeByClassName({
100
119
  themes: {
package/README.md CHANGED
@@ -87,25 +87,33 @@ mvn install
87
87
  ```
88
88
 
89
89
  4. Build the Design System CSS, Components and templates
90
+
91
+ ```bash
92
+ npm run build
93
+ ```
94
+
95
+ 5. Build the same command as above, but with `:theme` following by args `${ThemePaletteName}`
96
+ This will generate a separate altertive CSS export named as `qld.${ThemePaletteName}.bootstrap.css`
97
+
98
+ ```bash
99
+ npm run build:theme ${ThemePaletteName}
100
+
101
+ npm run build:theme -- --theme=${ThemePaletteA} --theme=${ThemePaletteB}
102
+ ```
103
+
104
+ 6. Start Watch and Storybook for component development
105
+
106
+ ```bash
107
+ npm run dev-storybook
108
+ ```
109
+ Alt:
110
+ ```bash
111
+ npm run watch
112
+ npm run storybook
113
+ ```
114
+
115
+ 7. Lint
90
116
 
91
- ```bash
92
- npm run build
93
- ```
94
-
95
- 5. Start Watch and Storybook for component development
96
-
97
- ```bash
98
- npm run dev-storybook
99
- ```
100
-
101
- Alt:
102
-
103
- ```bash
104
- npm run watch
105
- npm run storybook
106
- ```
107
-
108
- 6. Lint
109
117
  ```bash
110
118
  npm run lint
111
119
  ```
package/dist/README.md CHANGED
@@ -87,25 +87,33 @@ mvn install
87
87
  ```
88
88
 
89
89
  4. Build the Design System CSS, Components and templates
90
+
91
+ ```bash
92
+ npm run build
93
+ ```
94
+
95
+ 5. Build the same command as above, but with `:theme` following by args `${ThemePaletteName}`
96
+ This will generate a separate altertive CSS export named as `qld.${ThemePaletteName}.bootstrap.css`
97
+
98
+ ```bash
99
+ npm run build:theme ${ThemePaletteName}
100
+
101
+ npm run build:theme -- --theme=${ThemePaletteA} --theme=${ThemePaletteB}
102
+ ```
103
+
104
+ 6. Start Watch and Storybook for component development
105
+
106
+ ```bash
107
+ npm run dev-storybook
108
+ ```
109
+ Alt:
110
+ ```bash
111
+ npm run watch
112
+ npm run storybook
113
+ ```
114
+
115
+ 7. Lint
90
116
 
91
- ```bash
92
- npm run build
93
- ```
94
-
95
- 5. Start Watch and Storybook for component development
96
-
97
- ```bash
98
- npm run dev-storybook
99
- ```
100
-
101
- Alt:
102
-
103
- ```bash
104
- npm run watch
105
- npm run storybook
106
- ```
107
-
108
- 6. Lint
109
117
  ```bash
110
118
  npm run lint
111
119
  ```
@@ -1,5 +1,5 @@
1
1
 
2
- <!-- VERSION_DETAILS={"project_id":"@qld-gov-au/qgds-bootstrap5","version":"2.0.4","branch":"HEAD","tag":"v2.0.4","commit":"0b896ac48e7daadbef3fdbd2caccc437be120856","majorVersion":"v2"} -->
2
+ <!-- VERSION_DETAILS={"project_id":"@qld-gov-au/qgds-bootstrap5","version":"2.0.5","branch":"HEAD","tag":"v2.0.5","commit":"7972857bf46e8fa3db8ca361447711262fffc21e","majorVersion":"v2"} -->
3
3
 
4
4
  {{! Select environment, used verbatium if not using predefind key
5
5
  cdn := PROD|STAGING|BETA|TEST|DEV|???