From 9dad26c37607488c20826b7006828bd87e6edcbe Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Thu, 2 Jul 2026 10:31:09 +0200 Subject: [PATCH 01/15] refactor: move dev page url parser to utils --- .../__tests__/app-modes.test.ts | 75 ++++++++++++++ lib/dev-pages-utils/app-modes.ts | 98 +++++++++++++++++++ lib/dev-pages-utils/index.ts | 9 ++ package-lock.json | 7 ++ package.json | 1 + 5 files changed, 190 insertions(+) create mode 100644 lib/dev-pages-utils/__tests__/app-modes.test.ts create mode 100644 lib/dev-pages-utils/app-modes.ts diff --git a/lib/dev-pages-utils/__tests__/app-modes.test.ts b/lib/dev-pages-utils/__tests__/app-modes.test.ts new file mode 100644 index 0000000..80df76c --- /dev/null +++ b/lib/dev-pages-utils/__tests__/app-modes.test.ts @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { describe, expect, test, vi } from "vitest"; + +// global-styles is a workspace sibling that is not installed in this package's node_modules. +// app-modes.ts imports it at module load, so we stub it with the real enum string values. +vi.mock("@cloudscape-design/global-styles", () => ({ + Mode: { Light: "light", Dark: "dark" }, + Density: { Comfortable: "comfortable", Compact: "compact" }, + Theme: { Default: "default", VisualRefresh: "visual-refresh", OneTheme: "one-theme" }, + applyMode: () => {}, + applyDensity: () => {}, + applyTheme: () => {}, + disableMotion: () => {}, +})); + +const { appModesDefaults, formatAppModes, updateAppModes } = await import("../app-modes"); + +describe("formatAppModes", () => { + test("serializes every param to a string (full URL, no omission)", () => { + expect(formatAppModes(appModesDefaults)).toEqual({ + mode: "light", + density: "comfortable", + direction: "ltr", + motionDisabled: "false", + theme: "default", + i18n: "true", + screenshotMode: "false", + }); + }); + + test("skips undefined values", () => { + expect(formatAppModes({ mode: "dark", appLayoutToolbar: undefined })).toEqual({ mode: "dark" }); + }); +}); + +describe("updateAppModes", () => { + test("writes the full serialized query through setQuery", () => { + const setQuery = vi.fn(); + updateAppModes(appModesDefaults, { mode: "dark" as any }, setQuery); + expect(setQuery).toHaveBeenCalledWith({ + mode: "dark", + density: "comfortable", + direction: "ltr", + motionDisabled: "false", + theme: "default", + i18n: "true", + screenshotMode: "false", + }); + }); + + test("does not reload when direction is unchanged", () => { + const reload = vi.fn(); + vi.stubGlobal("window", { location: { reload } }); + updateAppModes(appModesDefaults, { mode: "dark" as any }, vi.fn()); + expect(reload).not.toHaveBeenCalled(); + vi.unstubAllGlobals(); + }); + + test("reloads when direction changes via next", () => { + const reload = vi.fn(); + vi.stubGlobal("window", { location: { reload } }); + updateAppModes(appModesDefaults, { direction: "rtl" }, vi.fn()); + expect(reload).toHaveBeenCalledOnce(); + vi.unstubAllGlobals(); + }); + + test("does not reload when next omits direction even if current is non-default", () => { + const reload = vi.fn(); + vi.stubGlobal("window", { location: { reload } }); + updateAppModes({ ...appModesDefaults, direction: "rtl" }, { mode: "dark" as any }, vi.fn()); + expect(reload).not.toHaveBeenCalled(); + vi.unstubAllGlobals(); + }); +}); diff --git a/lib/dev-pages-utils/app-modes.ts b/lib/dev-pages-utils/app-modes.ts new file mode 100644 index 0000000..1711126 --- /dev/null +++ b/lib/dev-pages-utils/app-modes.ts @@ -0,0 +1,98 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + applyDensity, + applyMode, + applyTheme, + disableMotion, + Density, + Mode, + Theme, +} from "@cloudscape-design/global-styles"; + +export interface AppUrlParams { + mode: Mode; + density: Density; + direction: "ltr" | "rtl"; + motionDisabled: boolean; + theme: Theme; + i18n?: boolean; + screenshotMode?: boolean; +} + +/** + * Shared shape for a dev-pages app context. Extend the params with package-specific fields via + * the generic `T` (e.g. useAppContext<{ visualRefresh: boolean }>()). + */ +export interface AppContextType { + pageId?: string; + urlParams: AppUrlParams & T; + setUrlParams: (newParams: Partial) => void; +} + +export const appModesDefaults: AppUrlParams = { + mode: Mode.Light, + density: Density.Comfortable, + direction: "ltr", + motionDisabled: false, + theme: Theme.Default, + i18n: true, + screenshotMode: false, +}; + +/** + * Parses dev-pages URL params into a typed object. Every known param lives on AppUrlParams; + * params absent from the URL fall back to appModesDefaults. String values of "true"/"false" are + * cast to booleans, and an unrecognized `theme` falls back to Theme.Default. + */ +export function parseAppModes(searchParams: URLSearchParams): AppUrlParams { + const queryParams: Record = { ...appModesDefaults }; + searchParams.forEach((value, key) => { + queryParams[key] = value === "true" || value === "false" ? value === "true" : value; + }); + + const themeValues = Object.values(Theme) as string[]; + if (!themeValues.includes(queryParams.theme)) { + queryParams.theme = Theme.Default; + } + + return queryParams as AppUrlParams; +} + +/** Serializes params to a query record. Emits every param (skipping only `undefined` values). */ +export function formatAppModes(params: T): Record { + const query: Record = {}; + for (const [key, value] of Object.entries(params)) { + if (value === undefined) { + continue; + } + query[key] = String(value); + } + return query; +} + +/** + * Router-agnostic setter for app-modes URL state: serializes `current` merged with `next`, writes + * it through the caller-provided `setQuery` (e.g. react-router's `setSearchParams`), and reloads + * the page when `direction` changed. + */ +export function updateAppModes( + current: AppUrlParams, + next: Partial, + setQuery: (query: Record) => void, +): void { + const merged = { ...current, ...next }; + setQuery(formatAppModes(merged)); + if ((next.direction ?? current.direction) !== current.direction) { + window.location.reload(); + } +} + +export function applyAppModes(params: AppUrlParams, target: Element = document.body): void { + applyMode(params.mode, target); + applyDensity(params.density, target); + disableMotion(params.motionDisabled, target); + applyTheme(params.theme, target); + document.documentElement.setAttribute("dir", params.direction); +} diff --git a/lib/dev-pages-utils/index.ts b/lib/dev-pages-utils/index.ts index 95eb94d..1148e4e 100644 --- a/lib/dev-pages-utils/index.ts +++ b/lib/dev-pages-utils/index.ts @@ -4,4 +4,13 @@ import { PermutationsView, PermutationsViewProps } from "./permutations-view.js"; import { createPermutations, ComponentPermutations } from "./permutations.js"; +export { + type AppUrlParams, + type AppContextType, + appModesDefaults, + applyAppModes, + formatAppModes, + parseAppModes, + updateAppModes, +} from "./app-modes.js"; export { createPermutations, PermutationsView, type ComponentPermutations, type PermutationsViewProps }; diff --git a/package-lock.json b/package-lock.json index dd3a9b7..36489f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "3.0.0", "license": "Apache-2.0", "dependencies": { + "@cloudscape-design/global-styles": "^1.0.62", "lodash": "^4.18.1", "minimatch": "^10.2.4" }, @@ -192,6 +193,12 @@ "@keyv/serialize": "^1.1.1" } }, + "node_modules/@cloudscape-design/global-styles": { + "version": "1.0.62", + "resolved": "https://registry.npmjs.org/@cloudscape-design/global-styles/-/global-styles-1.0.62.tgz", + "integrity": "sha512-8ll8m/Z2/nwHNBhGtEj/pqkBb08OkboyXoKef+D73HmUFgAOhhXCKUaqtF2SVOsvjHm5IffEQRvKK3/CQUeR1g==", + "license": "Apache-2.0" + }, "node_modules/@csstools/css-parser-algorithms": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", diff --git a/package.json b/package.json index fb06a0e..5a11188 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "test": "vitest run" }, "dependencies": { + "@cloudscape-design/global-styles": "^1.0.62", "lodash": "^4.18.1", "minimatch": "^10.2.4" }, From b983789661702d96223467f92aa770b4724b3705 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Thu, 2 Jul 2026 11:00:45 +0200 Subject: [PATCH 02/15] chore: rewrite docstrings --- lib/dev-pages-utils/app-modes.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/dev-pages-utils/app-modes.ts b/lib/dev-pages-utils/app-modes.ts index 1711126..e422943 100644 --- a/lib/dev-pages-utils/app-modes.ts +++ b/lib/dev-pages-utils/app-modes.ts @@ -22,8 +22,7 @@ export interface AppUrlParams { } /** - * Shared shape for a dev-pages app context. Extend the params with package-specific fields via - * the generic `T` (e.g. useAppContext<{ visualRefresh: boolean }>()). + * Shared shape for a dev-pages app context. */ export interface AppContextType { pageId?: string; @@ -42,9 +41,7 @@ export const appModesDefaults: AppUrlParams = { }; /** - * Parses dev-pages URL params into a typed object. Every known param lives on AppUrlParams; - * params absent from the URL fall back to appModesDefaults. String values of "true"/"false" are - * cast to booleans, and an unrecognized `theme` falls back to Theme.Default. + * Parses dev-pages URL params into a typed object. */ export function parseAppModes(searchParams: URLSearchParams): AppUrlParams { const queryParams: Record = { ...appModesDefaults }; @@ -60,13 +57,13 @@ export function parseAppModes(searchParams: URLSearchParams): AppUrlParams { return queryParams as AppUrlParams; } -/** Serializes params to a query record. Emits every param (skipping only `undefined` values). */ +/** + * Serializes params to a query record. Skips undefined values. + */ export function formatAppModes(params: T): Record { const query: Record = {}; for (const [key, value] of Object.entries(params)) { - if (value === undefined) { - continue; - } + if (value === undefined) continue; query[key] = String(value); } return query; From 427c992e38b97a69230c47664de5021b5b8ecbc7 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Thu, 2 Jul 2026 11:02:22 +0200 Subject: [PATCH 03/15] chore: run npm audit fix --- package-lock.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 36489f6..f523ece 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4414,9 +4414,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" From 5eaa9d1b3679698919a56b30148b0f58d8ca1a9e Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Thu, 2 Jul 2026 11:10:48 +0200 Subject: [PATCH 04/15] refactor: replace stubs --- .../__tests__/app-modes.test.ts | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/dev-pages-utils/__tests__/app-modes.test.ts b/lib/dev-pages-utils/__tests__/app-modes.test.ts index 80df76c..704e4f6 100644 --- a/lib/dev-pages-utils/__tests__/app-modes.test.ts +++ b/lib/dev-pages-utils/__tests__/app-modes.test.ts @@ -2,19 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, test, vi } from "vitest"; -// global-styles is a workspace sibling that is not installed in this package's node_modules. -// app-modes.ts imports it at module load, so we stub it with the real enum string values. -vi.mock("@cloudscape-design/global-styles", () => ({ - Mode: { Light: "light", Dark: "dark" }, - Density: { Comfortable: "comfortable", Compact: "compact" }, - Theme: { Default: "default", VisualRefresh: "visual-refresh", OneTheme: "one-theme" }, - applyMode: () => {}, - applyDensity: () => {}, - applyTheme: () => {}, - disableMotion: () => {}, -})); +import { Density, Mode } from "@cloudscape-design/global-styles"; -const { appModesDefaults, formatAppModes, updateAppModes } = await import("../app-modes"); +import { appModesDefaults, formatAppModes, updateAppModes } from "../app-modes"; describe("formatAppModes", () => { test("serializes every param to a string (full URL, no omission)", () => { @@ -30,14 +20,14 @@ describe("formatAppModes", () => { }); test("skips undefined values", () => { - expect(formatAppModes({ mode: "dark", appLayoutToolbar: undefined })).toEqual({ mode: "dark" }); + expect(formatAppModes({ mode: Mode.Dark, appLayoutToolbar: undefined })).toEqual({ mode: "dark" }); }); }); describe("updateAppModes", () => { test("writes the full serialized query through setQuery", () => { const setQuery = vi.fn(); - updateAppModes(appModesDefaults, { mode: "dark" as any }, setQuery); + updateAppModes(appModesDefaults, { mode: Mode.Dark }, setQuery); expect(setQuery).toHaveBeenCalledWith({ mode: "dark", density: "comfortable", @@ -52,7 +42,7 @@ describe("updateAppModes", () => { test("does not reload when direction is unchanged", () => { const reload = vi.fn(); vi.stubGlobal("window", { location: { reload } }); - updateAppModes(appModesDefaults, { mode: "dark" as any }, vi.fn()); + updateAppModes(appModesDefaults, { mode: Mode.Dark }, vi.fn()); expect(reload).not.toHaveBeenCalled(); vi.unstubAllGlobals(); }); @@ -68,7 +58,7 @@ describe("updateAppModes", () => { test("does not reload when next omits direction even if current is non-default", () => { const reload = vi.fn(); vi.stubGlobal("window", { location: { reload } }); - updateAppModes({ ...appModesDefaults, direction: "rtl" }, { mode: "dark" as any }, vi.fn()); + updateAppModes({ ...appModesDefaults, direction: "rtl" }, { density: Density.Compact }, vi.fn()); expect(reload).not.toHaveBeenCalled(); vi.unstubAllGlobals(); }); From 2d4c14b3d3b417b99946466eb99d412b24945f86 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Thu, 2 Jul 2026 11:17:21 +0200 Subject: [PATCH 05/15] build: move global-styles to peer dep --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a11188..cc8924c 100644 --- a/package.json +++ b/package.json @@ -26,11 +26,11 @@ "test": "vitest run" }, "dependencies": { - "@cloudscape-design/global-styles": "^1.0.62", "lodash": "^4.18.1", "minimatch": "^10.2.4" }, "peerDependencies": { + "@cloudscape-design/global-styles": "^1.0.62", "stylelint": "^16.8.1" }, "peerDependenciesMeta": { @@ -42,6 +42,7 @@ } }, "devDependencies": { + "@cloudscape-design/global-styles": "^1.0.62", "@tony.ganchev/eslint-plugin-header": "^3.3.1", "@types/lodash": "^4.17.21", "@types/node": "^20.19.24", From 3bd83ce709e19abbf415fe517d25fa768fcbde0e Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Thu, 2 Jul 2026 13:17:47 +0200 Subject: [PATCH 06/15] Revert "build: move global-styles to peer dep" This reverts commit a592db0c722b781c94df91d6a11a495e14b6b592. --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index cc8924c..5a11188 100644 --- a/package.json +++ b/package.json @@ -26,11 +26,11 @@ "test": "vitest run" }, "dependencies": { + "@cloudscape-design/global-styles": "^1.0.62", "lodash": "^4.18.1", "minimatch": "^10.2.4" }, "peerDependencies": { - "@cloudscape-design/global-styles": "^1.0.62", "stylelint": "^16.8.1" }, "peerDependenciesMeta": { @@ -42,7 +42,6 @@ } }, "devDependencies": { - "@cloudscape-design/global-styles": "^1.0.62", "@tony.ganchev/eslint-plugin-header": "^3.3.1", "@types/lodash": "^4.17.21", "@types/node": "^20.19.24", From a1945df1ddd601c60b15dd3a95e407fc3722e7d1 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Thu, 2 Jul 2026 13:24:16 +0200 Subject: [PATCH 07/15] chore: add prepare-lock.json --- package-lock.json | 6 ------ package.json | 3 +++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index f523ece..89ea252 100644 --- a/package-lock.json +++ b/package-lock.json @@ -193,12 +193,6 @@ "@keyv/serialize": "^1.1.1" } }, - "node_modules/@cloudscape-design/global-styles": { - "version": "1.0.62", - "resolved": "https://registry.npmjs.org/@cloudscape-design/global-styles/-/global-styles-1.0.62.tgz", - "integrity": "sha512-8ll8m/Z2/nwHNBhGtEj/pqkBb08OkboyXoKef+D73HmUFgAOhhXCKUaqtF2SVOsvjHm5IffEQRvKK3/CQUeR1g==", - "license": "Apache-2.0" - }, "node_modules/@csstools/css-parser-algorithms": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", diff --git a/package.json b/package.json index 5a11188..5e5ba46 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,9 @@ ], "*.{scss,css}": [ "stylelint --fix" + ], + "package-lock.json": [ + "node ./lib/scripts/prepare-package-lock.js" ] } } From ab450022342784a8e1fed42847ba4e35481ab3fc Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Fri, 3 Jul 2026 10:28:04 +0200 Subject: [PATCH 08/15] chore: remove default theme param --- lib/dev-pages-utils/__tests__/app-modes.test.ts | 2 -- lib/dev-pages-utils/app-modes.ts | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/dev-pages-utils/__tests__/app-modes.test.ts b/lib/dev-pages-utils/__tests__/app-modes.test.ts index 704e4f6..70b3034 100644 --- a/lib/dev-pages-utils/__tests__/app-modes.test.ts +++ b/lib/dev-pages-utils/__tests__/app-modes.test.ts @@ -13,7 +13,6 @@ describe("formatAppModes", () => { density: "comfortable", direction: "ltr", motionDisabled: "false", - theme: "default", i18n: "true", screenshotMode: "false", }); @@ -33,7 +32,6 @@ describe("updateAppModes", () => { density: "comfortable", direction: "ltr", motionDisabled: "false", - theme: "default", i18n: "true", screenshotMode: "false", }); diff --git a/lib/dev-pages-utils/app-modes.ts b/lib/dev-pages-utils/app-modes.ts index e422943..d5cf7f0 100644 --- a/lib/dev-pages-utils/app-modes.ts +++ b/lib/dev-pages-utils/app-modes.ts @@ -16,7 +16,7 @@ export interface AppUrlParams { density: Density; direction: "ltr" | "rtl"; motionDisabled: boolean; - theme: Theme; + theme?: Theme; i18n?: boolean; screenshotMode?: boolean; } @@ -35,7 +35,6 @@ export const appModesDefaults: AppUrlParams = { density: Density.Comfortable, direction: "ltr", motionDisabled: false, - theme: Theme.Default, i18n: true, screenshotMode: false, }; @@ -90,6 +89,6 @@ export function applyAppModes(params: AppUrlParams, target: Element = document.b applyMode(params.mode, target); applyDensity(params.density, target); disableMotion(params.motionDisabled, target); - applyTheme(params.theme, target); + applyTheme(params.theme ?? null, target); document.documentElement.setAttribute("dir", params.direction); } From 265bac447baee12d2754a7a030e82111e80f389c Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Fri, 3 Jul 2026 10:32:15 +0200 Subject: [PATCH 09/15] chore: refactor url parsing --- .../__tests__/app-modes.test.ts | 20 ++++++++++++++++++- lib/dev-pages-utils/app-modes.ts | 19 ++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/lib/dev-pages-utils/__tests__/app-modes.test.ts b/lib/dev-pages-utils/__tests__/app-modes.test.ts index 70b3034..581187a 100644 --- a/lib/dev-pages-utils/__tests__/app-modes.test.ts +++ b/lib/dev-pages-utils/__tests__/app-modes.test.ts @@ -4,7 +4,25 @@ import { describe, expect, test, vi } from "vitest"; import { Density, Mode } from "@cloudscape-design/global-styles"; -import { appModesDefaults, formatAppModes, updateAppModes } from "../app-modes"; +import { appModesDefaults, formatAppModes, parseAppModes, updateAppModes } from "../app-modes"; + +describe("parseAppModes", () => { + test("casts boolean strings and applies defaults for absent params", () => { + const params = parseAppModes(new URLSearchParams("mode=dark&motionDisabled=true")); + expect(params.mode).toBe("dark"); + expect(params.motionDisabled).toBe(true); + expect(params.density).toBe("comfortable"); // default + expect(params.i18n).toBe(true); // default + }); + + test("keeps a valid theme", () => { + expect(parseAppModes(new URLSearchParams("theme=one-theme")).theme).toBe("one-theme"); + }); + + test("leaves theme undefined when absent", () => { + expect(parseAppModes(new URLSearchParams("")).theme).toBeUndefined(); + }); +}); describe("formatAppModes", () => { test("serializes every param to a string (full URL, no omission)", () => { diff --git a/lib/dev-pages-utils/app-modes.ts b/lib/dev-pages-utils/app-modes.ts index d5cf7f0..81f430c 100644 --- a/lib/dev-pages-utils/app-modes.ts +++ b/lib/dev-pages-utils/app-modes.ts @@ -10,6 +10,7 @@ import { Mode, Theme, } from "@cloudscape-design/global-styles"; +import mapValues from "lodash/mapValues"; export interface AppUrlParams { mode: Mode; @@ -44,16 +45,18 @@ export const appModesDefaults: AppUrlParams = { */ export function parseAppModes(searchParams: URLSearchParams): AppUrlParams { const queryParams: Record = { ...appModesDefaults }; - searchParams.forEach((value, key) => { - queryParams[key] = value === "true" || value === "false" ? value === "true" : value; - }); + searchParams.forEach((value, key) => (queryParams[key] = value)); const themeValues = Object.values(Theme) as string[]; - if (!themeValues.includes(queryParams.theme)) { - queryParams.theme = Theme.Default; - } - - return queryParams as AppUrlParams; + return mapValues(queryParams, (value, key) => { + if (key === "theme") { + return themeValues.includes(value) ? value : Theme.Default; + } + if (value === "true" || value === "false") { + return value === "true"; + } + return value; + }) as AppUrlParams; } /** From 1e3a6340f5362bea6ba030f7ac2c79226cf3612e Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Fri, 3 Jul 2026 10:37:03 +0200 Subject: [PATCH 10/15] chore: refactor url parsing --- lib/dev-pages-utils/app-modes.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/dev-pages-utils/app-modes.ts b/lib/dev-pages-utils/app-modes.ts index 81f430c..0dff208 100644 --- a/lib/dev-pages-utils/app-modes.ts +++ b/lib/dev-pages-utils/app-modes.ts @@ -47,11 +47,7 @@ export function parseAppModes(searchParams: URLSearchParams): AppUrlParams { const queryParams: Record = { ...appModesDefaults }; searchParams.forEach((value, key) => (queryParams[key] = value)); - const themeValues = Object.values(Theme) as string[]; - return mapValues(queryParams, (value, key) => { - if (key === "theme") { - return themeValues.includes(value) ? value : Theme.Default; - } + return mapValues(queryParams, value => { if (value === "true" || value === "false") { return value === "true"; } @@ -59,7 +55,7 @@ export function parseAppModes(searchParams: URLSearchParams): AppUrlParams { }) as AppUrlParams; } -/** +/** * Serializes params to a query record. Skips undefined values. */ export function formatAppModes(params: T): Record { From 1c0388142deb408f710ea9ec62618735c281c448 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Mon, 6 Jul 2026 10:30:39 +0200 Subject: [PATCH 11/15] feat: add AppModesProvider --- lib/dev-pages-utils/app-modes-provider.tsx | 37 +++++++++++ lib/dev-pages-utils/app-modes.ts | 15 +---- lib/dev-pages-utils/index.ts | 11 +--- package-lock.json | 75 ++++++++++++++++++++++ package.json | 4 ++ 5 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 lib/dev-pages-utils/app-modes-provider.tsx diff --git a/lib/dev-pages-utils/app-modes-provider.tsx b/lib/dev-pages-utils/app-modes-provider.tsx new file mode 100644 index 0000000..f5020af --- /dev/null +++ b/lib/dev-pages-utils/app-modes-provider.tsx @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { createContext, ReactNode, useCallback, useContext, useEffect, useMemo } from "react"; +import { useSearchParams } from "react-router-dom"; + +import { AppContextType, appModesDefaults, applyAppModes, AppUrlParams, parseAppModes, updateAppModes } from "./app-modes.js"; + +const AppModesContext = createContext({ + urlParams: appModesDefaults, + setUrlParams: () => {}, +}); + +export function AppModesProvider({ children }: { children: ReactNode }) { + const [searchParams, setSearchParams] = useSearchParams(); + + // Depend on the whole object (memoized on the query) rather than enumerating params, so params + // added to parseAppModes/applyAppModes re-apply without touching this file or its consumers. + const urlParams = useMemo(() => parseAppModes(searchParams), [searchParams]); + + const setUrlParams = useCallback( + (newParams: Partial) => updateAppModes(urlParams, newParams, setSearchParams), + [urlParams, setSearchParams] + ); + + useEffect(() => { + applyAppModes(urlParams); + }, [urlParams]); + + const value = useMemo(() => ({ urlParams, setUrlParams }), [urlParams, setUrlParams]); + + return {children}; +} + +// Pass a type argument for package-specific params, e.g. useAppModes<{ myFlag: boolean }>(). +export function useAppModes(): AppContextType { + return useContext(AppModesContext) as AppContextType; +} diff --git a/lib/dev-pages-utils/app-modes.ts b/lib/dev-pages-utils/app-modes.ts index 0dff208..72a0186 100644 --- a/lib/dev-pages-utils/app-modes.ts +++ b/lib/dev-pages-utils/app-modes.ts @@ -22,9 +22,6 @@ export interface AppUrlParams { screenshotMode?: boolean; } -/** - * Shared shape for a dev-pages app context. - */ export interface AppContextType { pageId?: string; urlParams: AppUrlParams & T; @@ -40,9 +37,6 @@ export const appModesDefaults: AppUrlParams = { screenshotMode: false, }; -/** - * Parses dev-pages URL params into a typed object. - */ export function parseAppModes(searchParams: URLSearchParams): AppUrlParams { const queryParams: Record = { ...appModesDefaults }; searchParams.forEach((value, key) => (queryParams[key] = value)); @@ -55,9 +49,6 @@ export function parseAppModes(searchParams: URLSearchParams): AppUrlParams { }) as AppUrlParams; } -/** - * Serializes params to a query record. Skips undefined values. - */ export function formatAppModes(params: T): Record { const query: Record = {}; for (const [key, value] of Object.entries(params)) { @@ -67,11 +58,7 @@ export function formatAppModes(params: T): Record, diff --git a/lib/dev-pages-utils/index.ts b/lib/dev-pages-utils/index.ts index 1148e4e..c661b9c 100644 --- a/lib/dev-pages-utils/index.ts +++ b/lib/dev-pages-utils/index.ts @@ -4,13 +4,6 @@ import { PermutationsView, PermutationsViewProps } from "./permutations-view.js"; import { createPermutations, ComponentPermutations } from "./permutations.js"; -export { - type AppUrlParams, - type AppContextType, - appModesDefaults, - applyAppModes, - formatAppModes, - parseAppModes, - updateAppModes, -} from "./app-modes.js"; +export { type AppUrlParams } from "./app-modes.js"; +export { AppModesProvider, useAppModes } from "./app-modes-provider.js"; export { createPermutations, PermutationsView, type ComponentPermutations, type PermutationsViewProps }; diff --git a/package-lock.json b/package-lock.json index 89ea252..c529d88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,10 +38,14 @@ "lodash": "^4.18.1", "prettier": "^3.3.3", "react": "16.14.0", + "react-dom": "16.14.0", + "react-router-dom": "^6.30.4", "typescript": "^5.0.0", "vitest": "^4.1.8" }, "peerDependencies": { + "react": ">=16.14.0", + "react-router-dom": ">=6.0.0", "stylelint": "^16.8.1" } }, @@ -577,6 +581,16 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", @@ -5885,6 +5899,22 @@ "node": ">=0.10.0" } }, + "node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5892,6 +5922,40 @@ "dev": true, "license": "MIT" }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -6297,6 +6361,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", diff --git a/package.json b/package.json index 5e5ba46..ce2d4d9 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "minimatch": "^10.2.4" }, "peerDependencies": { + "react": ">=16.14.0", + "react-router-dom": ">=6.0.0", "stylelint": "^16.8.1" }, "peerDependenciesMeta": { @@ -63,6 +65,8 @@ "lodash": "^4.18.1", "prettier": "^3.3.3", "react": "16.14.0", + "react-dom": "16.14.0", + "react-router-dom": "^6.30.4", "typescript": "^5.0.0", "vitest": "^4.1.8" }, From 37b2ec663b41b0b18803e15ff6aee6e939ae5510 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Mon, 6 Jul 2026 13:05:26 +0200 Subject: [PATCH 12/15] move global styles to peer dep and mart as optional --- package.json | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index ce2d4d9..1d2c77d 100644 --- a/package.json +++ b/package.json @@ -26,24 +26,31 @@ "test": "vitest run" }, "dependencies": { - "@cloudscape-design/global-styles": "^1.0.62", "lodash": "^4.18.1", "minimatch": "^10.2.4" }, "peerDependencies": { + "@cloudscape-design/global-styles": "^1.0.62", "react": ">=16.14.0", "react-router-dom": ">=6.0.0", "stylelint": "^16.8.1" }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-router-dom": { - "optional": true - } + "peerDependenciesMeta": { + "@cloudscape-design/global-styles": { + "optional": true + }, + "react": { + "optional": true + }, + "react-router-dom": { + "optional": true + }, + "stylelint": { + "optional": true + } }, "devDependencies": { + "@cloudscape-design/global-styles": "^1.0.62", "@tony.ganchev/eslint-plugin-header": "^3.3.1", "@types/lodash": "^4.17.21", "@types/node": "^20.19.24", From e5526d32901713891e85fd89f5f3b6c62c4556dd Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Tue, 7 Jul 2026 13:57:57 +0200 Subject: [PATCH 13/15] chore: downgrade react-router version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1d2c77d..07cb5d8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "peerDependencies": { "@cloudscape-design/global-styles": "^1.0.62", "react": ">=16.14.0", - "react-router-dom": ">=6.0.0", + "react-router-dom": ">=5.0.0", "stylelint": "^16.8.1" }, "peerDependenciesMeta": { From 512aedffb128074f5de79698ead794b174d20169 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Tue, 7 Jul 2026 14:22:40 +0200 Subject: [PATCH 14/15] chore: make react-router deps more specific --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07cb5d8..1d2c77d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "peerDependencies": { "@cloudscape-design/global-styles": "^1.0.62", "react": ">=16.14.0", - "react-router-dom": ">=5.0.0", + "react-router-dom": ">=6.0.0", "stylelint": "^16.8.1" }, "peerDependenciesMeta": { From 38982142e228c7345fc3d09377f24a9487f037e1 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Wed, 8 Jul 2026 09:15:53 +0200 Subject: [PATCH 15/15] chore: allow 5 & 6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1d2c77d..d24b8bd 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "peerDependencies": { "@cloudscape-design/global-styles": "^1.0.62", "react": ">=16.14.0", - "react-router-dom": ">=6.0.0", + "react-router-dom": "^5.3.0 || ^6.0.0", "stylelint": "^16.8.1" }, "peerDependenciesMeta": {