diff --git a/.storybook/blocks/ComponentDetails.jsx b/.storybook/blocks/ComponentDetails.jsx
index 6418e4d8ce0..ece5910dd1d 100644
--- a/.storybook/blocks/ComponentDetails.jsx
+++ b/.storybook/blocks/ComponentDetails.jsx
@@ -5,13 +5,16 @@ import React, { useEffect, useState } from "react";
import { Body, Code, Heading } from "./Typography.jsx";
import { DDefinition, DList, DTerm } from "./Layouts.jsx";
-import { fetchToken } from "./utilities.js";
+import { fetchToken, getComponentsByStatusWrapper } from "./utilities.js";
import AdobeSVG from "../assets/images/adobe_logo.svg?raw";
import GitHubSVG from "../assets/images/github_logo.svg?raw";
import NpmSVG from "../assets/images/npm_logo.svg?raw";
import WCSVG from "../assets/images/wc_logo.svg?raw";
+// Import the pre-generated migrated components data
+import migratedComponentsData from '../data/migrated-components.json';
+
export const DSet = ({ term, children }) => {
return (
<>
@@ -443,4 +446,58 @@ export const TaggedReleases = () => {
);
};
+/**
+ * Displays a list of all components that have been migrated to Spectrum 2.
+ *
+ * Usage of this doc block within MDX template(s):
+ *
+ */
+export const MigratedComponentsList = () => {
+ const [components, setComponents] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ // Try to load components server-side first (Node.js environment)
+ try {
+ // Use the pre-generated data from our JSON file
+ if (migratedComponentsData && migratedComponentsData.components) {
+ setComponents(migratedComponentsData.components);
+ setIsLoading(false);
+ return;
+ }
+
+ // Dynamic loading as fallback
+ const migrated = getComponentsByStatusWrapper({ statusType: 'migrated' });
+
+ if (migrated && migrated.length > 0) {
+ setComponents(migrated);
+ }
+ } catch (error) {
+ console.warn('Failed to get migrated components:', error);
+ }
+
+ setIsLoading(false);
+ }, []);
+
+ return (
+
+ {!isLoading ? (
+ <>
+
+ >
+ ) : (
+ Loading migrated components...
+ )}
+
+ );
+};
+
export default ComponentDetails;
diff --git a/.storybook/blocks/utilities.js b/.storybook/blocks/utilities.js
index a9b733423d2..78ddc5c8a25 100644
--- a/.storybook/blocks/utilities.js
+++ b/.storybook/blocks/utilities.js
@@ -1,7 +1,51 @@
import spectrum from "@spectrum-css/tokens/dist/json/tokens.json";
-
import { useTheme } from "@storybook/theming";
+// Import fs and path conditionally to avoid browser issues
+let isNode = false;
+let componentUtils = null;
+
+// Check if we're running in Node.js environment
+try {
+ isNode = typeof process !== 'undefined' &&
+ typeof process.versions !== 'undefined' &&
+ typeof process.versions.node !== 'undefined';
+
+ if (isNode) {
+ // Import the component utility functions from migrated-component-scanner.js
+ try {
+ componentUtils = require('../../tasks/migrated-component-scanner');
+ } catch (err) {
+ console.warn('Failed to import component utilities:', err);
+ }
+ }
+} catch (e) {
+ // We're in a browser environment
+ console.log('Running in browser environment, file system functions will be limited');
+}
+
+/**
+ * Gets all component directories that have a specific status.
+ * This is a Storybook-facing "wrapper" that defers the actual work to the
+ * Node.js script in tasks/migrated-component-scanner.js. In this browser this
+ * wrapper returns an empty array and prevents bundlers from pulling in fs/path
+ * and avoiding runtime errors.
+ * @param {Object} options Options for filtering components
+ * @param {string} options.statusType Status type to filter by (e.g., 'migrated')
+ * @returns {string[]} Array of matching component directory names
+ */
+export function getComponentsByStatusWrapper(options = {}) {
+ // Check if we're in a Node.js environment
+ if (!isNode || !componentUtils) {
+ console.warn('getComponentsByStatus can only be used in a Node.js environment');
+ return [];
+ }
+
+ const { statusType } = options;
+
+ return componentUtils.getComponentsByStatus(statusType);
+}
+
/**
* A nestable function to search for a token value in the spectrum token data
* - If the key doesn't exist, it will log a warning
diff --git a/.storybook/data/migrated-components.json b/.storybook/data/migrated-components.json
new file mode 100644
index 00000000000..f9f65edd75b
--- /dev/null
+++ b/.storybook/data/migrated-components.json
@@ -0,0 +1,322 @@
+{
+ "total": 85,
+ "migrated": 63,
+ "components": [
+ {
+ "name": "accordion",
+ "title": "Accordion",
+ "url": "accordion"
+ },
+ {
+ "name": "actionbar",
+ "title": "Action bar",
+ "url": "action-bar"
+ },
+ {
+ "name": "actionbutton",
+ "title": "Action button",
+ "url": "action-button"
+ },
+ {
+ "name": "actiongroup",
+ "title": "Action group",
+ "url": "action-group"
+ },
+ {
+ "name": "alertbanner",
+ "title": "Alert banner",
+ "url": "alert-banner"
+ },
+ {
+ "name": "alertdialog",
+ "title": "Alert dialog",
+ "url": "alert-dialog"
+ },
+ {
+ "name": "avatar",
+ "title": "Avatar",
+ "url": "avatar"
+ },
+ {
+ "name": "badge",
+ "title": "Badge",
+ "url": "badge"
+ },
+ {
+ "name": "breadcrumb",
+ "title": "Breadcrumbs",
+ "url": "breadcrumbs"
+ },
+ {
+ "name": "button",
+ "title": "Button",
+ "url": "button"
+ },
+ {
+ "name": "buttongroup",
+ "title": "Button group",
+ "url": "button-group"
+ },
+ {
+ "name": "checkbox",
+ "title": "Checkbox",
+ "url": "checkbox"
+ },
+ {
+ "name": "clearbutton",
+ "title": "Clear button",
+ "url": "clear-button"
+ },
+ {
+ "name": "closebutton",
+ "title": "Close button",
+ "url": "close-button"
+ },
+ {
+ "name": "coachmark",
+ "title": "Coach mark",
+ "url": "coach-mark"
+ },
+ {
+ "name": "colorarea",
+ "title": "Color area",
+ "url": "color-area"
+ },
+ {
+ "name": "colorhandle",
+ "title": "Color handle",
+ "url": "color-handle"
+ },
+ {
+ "name": "colorloupe",
+ "title": "Color loupe",
+ "url": "color-loupe"
+ },
+ {
+ "name": "colorslider",
+ "title": "Color slider",
+ "url": "color-slider"
+ },
+ {
+ "name": "colorwheel",
+ "title": "Color wheel",
+ "url": "color-wheel"
+ },
+ {
+ "name": "combobox",
+ "title": "Combobox",
+ "url": "combobox"
+ },
+ {
+ "name": "contextualhelp",
+ "title": "Contextual help",
+ "url": "contextual-help"
+ },
+ {
+ "name": "dial",
+ "title": "Dial",
+ "url": "dial"
+ },
+ {
+ "name": "dialog",
+ "title": "Dialog",
+ "url": "dialog"
+ },
+ {
+ "name": "divider",
+ "title": "Divider",
+ "url": "divider"
+ },
+ {
+ "name": "dropzone",
+ "title": "Drop zone",
+ "url": "drop-zone"
+ },
+ {
+ "name": "fieldgroup",
+ "title": "Field group",
+ "url": "field-group"
+ },
+ {
+ "name": "fieldlabel",
+ "title": "Field label",
+ "url": "field-label"
+ },
+ {
+ "name": "form",
+ "title": "Form",
+ "url": "form"
+ },
+ {
+ "name": "helptext",
+ "title": "Help text",
+ "url": "help-text"
+ },
+ {
+ "name": "icon",
+ "title": "Icon",
+ "url": "icon"
+ },
+ {
+ "name": "illustratedmessage",
+ "title": "Illustrated message",
+ "url": "illustrated-message"
+ },
+ {
+ "name": "infieldbutton",
+ "title": "In-field button",
+ "url": "in-field-button"
+ },
+ {
+ "name": "infieldprogresscircle",
+ "title": "In-field progress circle",
+ "url": "in-field-progress-circle"
+ },
+ {
+ "name": "inlinealert",
+ "title": "In-line alert",
+ "url": "in-line-alert"
+ },
+ {
+ "name": "link",
+ "title": "Link",
+ "url": "link"
+ },
+ {
+ "name": "menu",
+ "title": "Menu",
+ "url": "menu"
+ },
+ {
+ "name": "meter",
+ "title": "Meter",
+ "url": "meter"
+ },
+ {
+ "name": "stepper",
+ "title": "Number field",
+ "url": "number-field"
+ },
+ {
+ "name": "opacitycheckerboard",
+ "title": "Opacity checkerboard",
+ "url": "opacity-checkerboard"
+ },
+ {
+ "name": "pagination",
+ "title": "Pagination",
+ "url": "pagination"
+ },
+ {
+ "name": "picker",
+ "title": "Picker",
+ "url": "picker"
+ },
+ {
+ "name": "pickerbutton",
+ "title": "Picker button",
+ "url": "picker-button"
+ },
+ {
+ "name": "popover",
+ "title": "Popover",
+ "url": "popover"
+ },
+ {
+ "name": "progressbar",
+ "title": "Progress bar",
+ "url": "progress-bar"
+ },
+ {
+ "name": "progresscircle",
+ "title": "Progress circle",
+ "url": "progress-circle"
+ },
+ {
+ "name": "radio",
+ "title": "Radio",
+ "url": "radio"
+ },
+ {
+ "name": "rating",
+ "title": "Rating",
+ "url": "rating"
+ },
+ {
+ "name": "search",
+ "title": "Search field",
+ "url": "search-field"
+ },
+ {
+ "name": "slider",
+ "title": "Slider",
+ "url": "slider"
+ },
+ {
+ "name": "statuslight",
+ "title": "Status light",
+ "url": "status-light"
+ },
+ {
+ "name": "swatch",
+ "title": "Swatch",
+ "url": "swatch"
+ },
+ {
+ "name": "swatchgroup",
+ "title": "Swatch group",
+ "url": "swatch-group"
+ },
+ {
+ "name": "switch",
+ "title": "Switch",
+ "url": "switch"
+ },
+ {
+ "name": "table",
+ "title": "Table",
+ "url": "table"
+ },
+ {
+ "name": "tabs",
+ "title": "Tabs",
+ "url": "tabs"
+ },
+ {
+ "name": "tag",
+ "title": "Tag",
+ "url": "tag"
+ },
+ {
+ "name": "taggroup",
+ "title": "Tag group",
+ "url": "tag-group"
+ },
+ {
+ "name": "textfield",
+ "title": "Text area",
+ "url": "text-area"
+ },
+ {
+ "name": "textfield",
+ "title": "Text field",
+ "url": "text-field"
+ },
+ {
+ "name": "thumbnail",
+ "title": "Thumbnail",
+ "url": "thumbnail"
+ },
+ {
+ "name": "toast",
+ "title": "Toast",
+ "url": "toast"
+ },
+ {
+ "name": "tooltip",
+ "title": "Tooltip",
+ "url": "tooltip"
+ }
+ ],
+ "generatedAt": "2025-08-15T21:14:18.477Z"
+}
\ No newline at end of file
diff --git a/.storybook/guides/s2_migration.mdx b/.storybook/guides/s2_migration.mdx
index c94a30bf8ad..2813efd4a82 100644
--- a/.storybook/guides/s2_migration.mdx
+++ b/.storybook/guides/s2_migration.mdx
@@ -1,5 +1,6 @@
import { Meta, Title } from "@storybook/blocks";
import { ThemeContainer, Heading } from "../blocks";
+import { MigratedComponentsList } from "../blocks/ComponentDetails.jsx";
import GrayMigrationGuide from "../assets/images/gray_migration-guide.png";
@@ -13,6 +14,12 @@ import GrayMigrationGuide from "../assets/images/gray_migration-guide.png";
- Search within: Use a search field with a separate control to filter the search instead.
- Split button: Use a button group to show any additional actions related to the most critical action. Reference [Spectrum documentation](https://spectrum.corp.adobe.com/page/button-group/#Use-a-button-group-to-show-additional-actions) for more information.
+## Migrated components
+
+This is a list of all components that have been fully migrated to Spectrum 2.
+
+
+
## Grays
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+/* eslint-disable no-console */
+
+/**
+ * this script scans the `components` directory and builds a list of storybook
+ * components that are marked as migrated. it can either print a human-friendly
+ * list to the console or write a structured json report to disk.
+ *
+ * important behavior:
+ * - a single component directory can contain multiple story files (for example,
+ * text field and text area). when multiple story files are marked as migrated,
+ * this script returns one entry per story so each item appears in the output.
+ * - console output is alphabetically sorted by title (case- and accent-insensitive)
+ * to make it easier to scan.
+ *
+ * usage:
+ * node tasks/migrated-component-scanner.js [--output=path/to/output.json]
+ */
+
+const fs = require("fs");
+const path = require("path");
+
+/**
+ * Gets all component directory names from the components folder
+ * @returns {string[]} Array of component directory names
+ */
+function getAllComponentDirectories() {
+ try {
+ // Get the absolute path to the components directory
+ const componentsDir = path.resolve(process.cwd(), "components");
+
+ // Read all directories in the components folder
+ const directories = fs.readdirSync(componentsDir, { withFileTypes: true })
+ .filter(dirent => dirent.isDirectory())
+ .map(dirent => dirent.name)
+ .sort();
+
+ return directories;
+ } catch (error) {
+ console.error("Error getting component directories:", error);
+ return [];
+ }
+}
+
+/**
+ * Gets all component directories that have a specific status
+ * @param {string} statusType Status type to filter by (e.g., 'migrated')
+ * @returns {string[]} Array of matching component directory names
+ */
+function getComponentsByStatus(statusType) {
+ try {
+ const componentsDir = path.resolve(process.cwd(), "components");
+ const directories = getAllComponentDirectories();
+
+ if (!statusType) return directories;
+
+ // Filter directories that have status type in their stories
+ const matchingComponents = directories.filter(dir => {
+ const storiesDir = path.join(componentsDir, dir, "stories");
+
+ // Check if stories directory exists
+ if (!fs.existsSync(storiesDir)) return false;
+
+ // Get all story files
+ const storyFiles = fs.readdirSync(storiesDir)
+ .filter(file => file.endsWith(".stories.js"));
+
+ // Check each story file for status type
+ return storyFiles.some(file => {
+ const storyContent = fs.readFileSync(path.join(storiesDir, file), "utf8");
+ return storyContent.includes(`type: "${statusType}"`);
+ });
+ });
+
+ return matchingComponents;
+ } catch (error) {
+ console.error(`Error getting components with status ${statusType}:`, error);
+ return [];
+ }
+}
+
+/**
+ * Extracts the title from a Storybook .stories.js file (to display the title in a list with proper capitalization)
+ * @param {string} storyFilePath - Absolute path to the .stories.js file
+ * @returns {string|null} The extracted title, or null if not found
+ */
+function extractTitleFromStoryFile(storyFilePath) {
+ try {
+ const content = fs.readFileSync(storyFilePath, "utf8");
+ const match = content.match(/title:\s*["'`](.+?)["'`]/);
+ if (match && match[1]) {
+ return match[1];
+ }
+ } catch (error) {
+ console.warn(`Could not extract title from ${storyFilePath}:`, error);
+ }
+ return null;
+}
+
+/**
+ * Generates the URL fragment from a component's title
+ * @param {string} title - The title to generate a URL fragment from
+ * @returns {string} The generated URL fragment
+ */
+function generateUrlFragmentFromTitle(title) {
+ return title
+ .split("/")
+ .map(segment =>
+ segment
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9 -]/g, "") // remove special chars except space and dash (i.e "in-line alert")
+ .replace(/\s+/g, "-") // replace spaces with dash
+ )
+ .join("-")
+ .replace(/-+/g, "-"); // collapse multiple dashes
+}
+
+/**
+ * generate a list of migrated components with titles and doc links.
+ *
+ * notes on shape of data:
+ * - the returned `components` array can include multiple entries for a single
+ * component directory when there are multiple migrated story files present.
+ * - each entry includes the human-friendly title and a url fragment derived
+ * from that title for linking.
+ *
+ * @returns {{ total: number, migrated: number, components: Array<{name: string, title: string, url: string}>, generatedAt: string }}
+ * information about migrated components and counts.
+ */
+function generateMigratedComponentsReport() {
+ const allComponents = getAllComponentDirectories();
+ const migratedComponents = getComponentsByStatus("migrated");
+
+ // build entries per component directory depending on how many story files
+ // inside the directory are marked as `type: "migrated"`.
+ const migratedComponentData = migratedComponents.flatMap((dir) => {
+ const storiesDir = path.resolve(process.cwd(), "components", dir, "stories");
+ if (!fs.existsSync(storiesDir)) {
+ // no stories directory; fall back to a single, generic entry so the component still shows up in the report.
+ return [{
+ name: dir,
+ title: dir,
+ url: dir,
+ }];
+ }
+
+ const storyFiles = fs.readdirSync(storiesDir).filter(file => file.endsWith(".stories.js"));
+
+ const entries = [];
+ for (const file of storyFiles) {
+ const filePath = path.join(storiesDir, file);
+ let content = "";
+ try {
+ content = fs.readFileSync(filePath, "utf8");
+ } catch (_) {
+ // if a file cannot be read, skip it and continue with others.
+ continue;
+ }
+
+ // only include story files that declare `type: "migrated"` in their metadata.
+ if (!content.includes("type: \"migrated\"")) continue;
+
+ const title = extractTitleFromStoryFile(filePath) || dir;
+ const urlFragment = generateUrlFragmentFromTitle(title);
+ entries.push({
+ name: dir,
+ title,
+ url: urlFragment,
+ });
+ }
+
+ // if there are stories but none are marked as migrated, include a single
+ // fallback entry for the directory to keep counts consistent.
+ if (entries.length === 0) {
+ const urlFragment = generateUrlFragmentFromTitle(dir);
+ return [{
+ name: dir,
+ title: dir,
+ url: urlFragment,
+ }];
+ }
+
+ return entries;
+ });
+
+ // sort the final array alphabetically by title (fallback to name),
+ // keeping the full objects intact for json output and rendering.
+ const componentsSorted = migratedComponentData
+ .slice()
+ .sort((a, b) => (a.title || a.name)
+ .localeCompare(b.title || b.name, undefined, { sensitivity: "base" }));
+
+ return {
+ total: allComponents.length,
+ migrated: migratedComponentData.length,
+ components: componentsSorted,
+ generatedAt: new Date().toISOString()
+ };
+}
+
+// Export the functions for use in other modules
+module.exports = {
+ getAllComponentDirectories,
+ getComponentsByStatus,
+ generateMigratedComponentsReport
+};
+
+// Main execution - only runs when script is executed directly in the terminal
+if (require.main === module) {
+ (async () => {
+ const args = process.argv.slice(2);
+ const outputArg = args.find(arg => arg.startsWith("--output="));
+ const outputPath = outputArg ? outputArg.split("=")[1] : null;
+
+ console.log("Scanning for migrated components...");
+ const report = generateMigratedComponentsReport();
+
+ if (outputPath) {
+ const outputDir = path.dirname(outputPath);
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
+ console.log(`Report saved to ${outputPath}`);
+ console.log(`Found ${report.migrated} migrated components out of ${report.total} total components.`);
+ } else {
+ console.log("Migrated Components:");
+ const componentNames = report.components
+ .map(component => component.title || component.name || component)
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
+ console.log(componentNames.join(", "));
+ console.log(`\nTotal: ${report.migrated} out of ${report.total} components are migrated.`);
+ }
+ })();
+}