diff --git a/CLAUDE.md b/CLAUDE.md index fcff50d9..5137e7d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,7 @@ pnpm test # run Playwright E2E tests (requires dev serve pnpm lint # ESLint pnpm format # Prettier pnpm run generate:git-metadata # regenerate src/data/git-metadata.json from git history +pnpm run generate:dark-variants # bulk-generate draft -dark.html files for components missing one ``` ## Architecture @@ -31,7 +32,7 @@ The three categories map to Astro content collections defined in `src/content.co The actual component markup lives in `public/examples/{category}/{slug}/{n}.html` (light) and `public/examples/{category}/{slug}/{n}-dark.html` (dark). These are standalone HTML pages loaded in iframes by the `` custom element. -Dark variants can be generated using the browser-based dark mode generator tool at `/tools/dark-mode-generator`. +Dark variants can be generated with `pnpm run generate:dark-variants`, which bulk-generates draft `-dark.html` files (using default shade/color mappings) for every component missing one — review each result and add `dark: true` to its MDX entry by hand. Pass `--category=` and/or `--slug=` to scope a run to one collection instead of scanning every category (e.g. `pnpm run generate:dark-variants --category=application --slug=badges`). For a single component that needs hand-tuning, use the browser-based dark mode generator tool at `/tools/dark-mode-generator` instead. ### Preview System diff --git a/package.json b/package.json index 28905cb9..688136c6 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "format": "prettier --write . --ignore-path .prettierignore", "css:blog": "npx @tailwindcss/cli -i ./src/styles/blog.css -o ./public/blog.css -m", "css:component": "npx @tailwindcss/cli -i ./src/styles/component.css -o ./public/component.css -m", - "generate:git-metadata": "node scripts/generate-git-metadata.js" + "generate:git-metadata": "node scripts/generate-git-metadata.js", + "generate:dark-variants": "node scripts/generate-dark-variants.js" }, "dependencies": { "@astrojs/check": "^0.9.10", diff --git a/scripts/generate-dark-variants.js b/scripts/generate-dark-variants.js new file mode 100644 index 00000000..a6bf53bc --- /dev/null +++ b/scripts/generate-dark-variants.js @@ -0,0 +1,150 @@ +#!/usr/bin/env node + +import { fileURLToPath } from 'node:url' + +import fs from 'node:fs' +import path from 'node:path' + +import { transformHtmlString } from '../src/lib/dark-mode/transform-html.js' +import { DEFAULT_CONFIG } from '../src/lib/dark-mode/config.js' + +const scriptFilePath = fileURLToPath(import.meta.url) +const repositoryRootPath = path.resolve(path.dirname(scriptFilePath), '..') +const examplesRootPath = path.join(repositoryRootPath, 'public/examples') + +const componentCategories = ['application', 'marketing', 'neobrutalism', 'templates'] + +const cliCategoryFilter = process.argv + .find((argValue) => argValue.startsWith('--category=')) + ?.split('=')[1] +const cliSlugFilter = process.argv.find((argValue) => argValue.startsWith('--slug='))?.split('=')[1] + +function findComponentSlugFolders(categoryPath) { + return fs + .readdirSync(categoryPath) + .filter((entryName) => fs.statSync(path.join(categoryPath, entryName)).isDirectory()) +} + +function findMissingDarkVariantsInFolder(categoryName, componentSlug, componentFolderPath) { + const htmlFileNames = fs + .readdirSync(componentFolderPath) + .filter((fileName) => fileName.endsWith('.html')) + + const darkFileNameSet = new Set( + htmlFileNames.filter((fileName) => fileName.endsWith('-dark.html')), + ) + const lightFileNames = htmlFileNames.filter((fileName) => !fileName.endsWith('-dark.html')) + + return lightFileNames + .map((lightFileName) => lightFileName.replace(/\.html$/, '-dark.html')) + .filter((darkFileName) => !darkFileNameSet.has(darkFileName)) + .map((darkFileName) => { + const lightFileName = darkFileName.replace(/-dark\.html$/, '.html') + + return { + categoryName, + componentSlug, + lightFilePath: path.join(componentFolderPath, lightFileName), + darkFilePath: path.join(componentFolderPath, darkFileName), + } + }) +} + +function isDarkModeSupportedForCollection(categoryName, componentSlug) { + const mdxFilePath = path.join( + repositoryRootPath, + 'src/content/collection', + categoryName, + `${componentSlug}.mdx`, + ) + + if (!fs.existsSync(mdxFilePath)) { + return true + } + + const mdxFileContent = fs.readFileSync(mdxFilePath, 'utf8') + const frontmatterMatch = mdxFileContent.match(/^---\n([\s\S]*?)\n---/) + + if (!frontmatterMatch) { + return true + } + + return !/^dark:\s*false\s*$/m.test(frontmatterMatch[1]) +} + +function findMissingDarkVariants() { + const missingDarkVariants = [] + + for (const categoryName of componentCategories) { + if (cliCategoryFilter && categoryName !== cliCategoryFilter) { + continue + } + + const categoryPath = path.join(examplesRootPath, categoryName) + + if (!fs.existsSync(categoryPath)) { + continue + } + + for (const componentSlug of findComponentSlugFolders(categoryPath)) { + if (cliSlugFilter && componentSlug !== cliSlugFilter) { + continue + } + + if (!isDarkModeSupportedForCollection(categoryName, componentSlug)) { + continue + } + + const componentFolderPath = path.join(categoryPath, componentSlug) + + missingDarkVariants.push( + ...findMissingDarkVariantsInFolder(categoryName, componentSlug, componentFolderPath), + ) + } + } + + return missingDarkVariants +} + +function generateDarkVariants() { + if (process.env.NODE_ENV === 'production') { + console.error('āŒ Error: This script is blocked in production.') + + process.exit(1) + } + + const missingDarkVariants = findMissingDarkVariants() + + if (missingDarkVariants.length === 0) { + console.log('āœ… Every component already has a dark variant') + + return + } + + console.log(`šŸ” Found ${missingDarkVariants.length} file(s) without a dark variant\n`) + + const touchedComponentKeys = new Set() + + for (const missingVariant of missingDarkVariants) { + const lightHtmlContent = fs.readFileSync(missingVariant.lightFilePath, 'utf8') + const darkHtmlContent = transformHtmlString(lightHtmlContent, DEFAULT_CONFIG) + + fs.writeFileSync(missingVariant.darkFilePath, darkHtmlContent, 'utf8') + + const relativeDarkFilePath = path.relative(repositoryRootPath, missingVariant.darkFilePath) + + console.log(`✨ Created: ${relativeDarkFilePath}`) + + touchedComponentKeys.add(`${missingVariant.categoryName}/${missingVariant.componentSlug}`) + } + + console.log(`\nāœ… Generated ${missingDarkVariants.length} dark variant(s)`) + console.log('šŸ‘‹ These are drafts — review each rendering, then for every touched component') + console.log(' add `dark: true` to the matching entry in its .mdx `components` array:\n') + + for (const touchedComponentKey of [...touchedComponentKeys].sort()) { + console.log(` - src/content/collection/${touchedComponentKey}.mdx`) + } +} + +generateDarkVariants() diff --git a/src/content.config.ts b/src/content.config.ts index 180ed19c..076173c3 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -76,6 +76,7 @@ const collection = z.object({ title: z.string(), wrapper: z.string().default('h-[600px]'), pattern: z.url().optional(), + dark: z.boolean().default(true), components: z.array( z.object({ contributors: z.array(z.string()).default(['markmead']), diff --git a/src/content/collection/application/grids.mdx b/src/content/collection/application/grids.mdx index a8ce5231..4cb27ac5 100644 --- a/src/content/collection/application/grids.mdx +++ b/src/content/collection/application/grids.mdx @@ -4,6 +4,7 @@ description: Responsive grid layouts and grid systems for web applications with category: application slug: grids wrapper: h-[400px] +dark: false terms: - layout components: diff --git a/src/content/collection/application/media.mdx b/src/content/collection/application/media.mdx index 06aa146a..b35a34a9 100644 --- a/src/content/collection/application/media.mdx +++ b/src/content/collection/application/media.mdx @@ -4,6 +4,7 @@ description: Responsive media and avatar components for user profiles, image gal category: application slug: media wrapper: h-[400px] +dark: false terms: - avatar - image diff --git a/src/layouts/ComponentPost.astro b/src/layouts/ComponentPost.astro index 421599e6..b32dea78 100644 --- a/src/layouts/ComponentPost.astro +++ b/src/layouts/ComponentPost.astro @@ -24,6 +24,7 @@ const { wrapper, pattern, updated, + dark: darkModeSupported, } = Astro.props const darkCount = components.filter(({ dark }) => !!dark).length @@ -105,23 +106,27 @@ const componentPageSchema = {
- - {darkCount}/{totalCount} Dark Mode{' '} - { - darkCount !== totalCount && ( - - (Request) - - ) - } - + { + darkModeSupported && ( + <> + + {darkCount}/{totalCount} Dark Mode{' '} + {darkCount !== totalCount && ( + + (Request) + + )} + - +