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.`); + } + })(); +}