From 6679179f5af35cf203dabc9302b84ac530f078ca Mon Sep 17 00:00:00 2001 From: zbeyens Date: Mon, 25 Nov 2024 19:01:25 +0100 Subject: [PATCH 1/5] sync --- packages/cli/package.json | 2 +- packages/cli/src/commands/info.ts | 21 ++ packages/cli/src/commands/init.ts | 1 + packages/cli/src/commands/migrate.ts | 89 ++++++++ packages/cli/src/index.ts | 14 +- packages/cli/src/migrations/migrate-icons.ts | 196 ++++++++++++++++++ .../cli/src/preflights/preflight-migrate.ts | 66 ++++++ packages/cli/src/utils/get-config.ts | 9 +- packages/cli/src/utils/get-project-info.ts | 50 +++-- packages/cli/src/utils/icon-libraries.ts | 12 ++ packages/cli/src/utils/registry/index.ts | 11 + packages/cli/src/utils/registry/schema.ts | 83 ++++---- packages/cli/src/utils/transformers/index.ts | 16 +- .../src/utils/transformers/transform-icons.ts | 84 ++++++++ .../utils/transformers/transform-import.ts | 4 +- .../src/utils/updaters/update-dependencies.ts | 2 +- .../cli/src/utils/updaters/update-files.ts | 170 ++++++++------- .../utils/updaters/update-tailwind-config.ts | 133 ++++++++++-- 18 files changed, 803 insertions(+), 160 deletions(-) create mode 100644 packages/cli/src/commands/info.ts create mode 100644 packages/cli/src/commands/migrate.ts create mode 100644 packages/cli/src/migrations/migrate-icons.ts create mode 100644 packages/cli/src/preflights/preflight-migrate.ts create mode 100644 packages/cli/src/utils/icon-libraries.ts create mode 100644 packages/cli/src/utils/transformers/transform-icons.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index d1889ba759..8520757c0f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "shadcx", - "version": "0.0.7", + "version": "0.1.0", "description": "Add Plate components to your apps.", "keywords": [ "components", diff --git a/packages/cli/src/commands/info.ts b/packages/cli/src/commands/info.ts new file mode 100644 index 0000000000..8284036ad5 --- /dev/null +++ b/packages/cli/src/commands/info.ts @@ -0,0 +1,21 @@ +import { Command } from 'commander'; + +import { getConfig } from '@/src/utils/get-config'; +import { getProjectInfo } from '@/src/utils/get-project-info'; +import { logger } from '@/src/utils/logger'; + +export const info = new Command() + .name('info') + .description('get information about your project') + .option( + '-c, --cwd ', + 'the working directory. defaults to the current directory.', + process.cwd() + ) + .action(async (opts) => { + logger.info('> project info'); + console.info(await getProjectInfo(opts.cwd)); + logger.break(); + logger.info('> components.json'); + console.info(await getConfig(opts.cwd)); + }); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 59193d1e41..952873f88a 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -458,6 +458,7 @@ async function promptForMinimalConfig( return rawConfigSchema.parse({ $schema: defaultConfig?.$schema, aliases: defaultConfig?.aliases, + iconLibrary: defaultConfig?.iconLibrary, registries: defaultConfig?.registries, rsc: defaultConfig?.rsc, style, diff --git a/packages/cli/src/commands/migrate.ts b/packages/cli/src/commands/migrate.ts new file mode 100644 index 0000000000..8bf01c1a58 --- /dev/null +++ b/packages/cli/src/commands/migrate.ts @@ -0,0 +1,89 @@ +import { Command } from 'commander'; +import path from 'path'; +import { z } from 'zod'; + +import { migrateIcons } from '@/src/migrations/migrate-icons'; +import { preFlightMigrate } from '@/src/preflights/preflight-migrate'; +import * as ERRORS from '@/src/utils/errors'; +import { handleError } from '@/src/utils/handle-error'; +import { logger } from '@/src/utils/logger'; + +export const migrations = [ + { + description: 'migrate your ui components to a different icon library.', + name: 'icons', + }, +] as const; + +export const migrateOptionsSchema = z.object({ + cwd: z.string(), + list: z.boolean(), + migration: z + .string() + .refine( + (value) => + value && migrations.some((migration) => migration.name === value), + { + message: + 'You must specify a valid migration. Run `shadcn migrate --list` to see available migrations.', + } + ) + .optional(), +}); + +export const migrate = new Command() + .name('migrate') + .description('run a migration.') + .argument('[migration]', 'the migration to run.') + .option( + '-c, --cwd ', + 'the working directory. defaults to the current directory.', + process.cwd() + ) + .option('-l, --list', 'list all migrations.', false) + .action(async (migration, opts) => { + try { + const options = migrateOptionsSchema.parse({ + cwd: path.resolve(opts.cwd), + list: opts.list, + migration, + }); + + if (options.list || !options.migration) { + logger.info('Available migrations:'); + + for (const migration of migrations) { + logger.info(`- ${migration.name}: ${migration.description}`); + } + + return; + } + if (!options.migration) { + throw new Error( + 'You must specify a migration. Run `shadcn migrate --list` to see available migrations.' + ); + } + + const { config, errors } = await preFlightMigrate(options); + + if ( + errors[ERRORS.MISSING_DIR_OR_EMPTY_PROJECT] || + errors[ERRORS.MISSING_CONFIG] + ) { + throw new Error( + 'No `components.json` file found. Ensure you are at the root of your project.' + ); + } + if (!config) { + throw new Error( + 'Something went wrong reading your `components.json` file. Please ensure you have a valid `components.json` file.' + ); + } + if (options.migration === 'icons') { + await migrateIcons(config); + } + } catch (error) { + logger.break(); + handleError(error); + } + }); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c324a64276..fb336b09e1 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,15 +1,18 @@ #!/usr/bin/env node +import { Command } from "commander" + import { add } from "@/src/commands/add" import { diff } from "@/src/commands/diff" +import { info } from "@/src/commands/info" import { init } from "@/src/commands/init" -import { Command } from "commander" +import { migrate } from "@/src/commands/migrate" import packageJson from "../package.json" process.on("SIGINT", () => process.exit(0)) process.on("SIGTERM", () => process.exit(0)) -async function main() { +function main() { const program = new Command() .name("shadcn") .description("add components and dependencies to your project") @@ -19,7 +22,12 @@ async function main() { "display the version number" ) - program.addCommand(init).addCommand(add).addCommand(diff) + program + .addCommand(init) + .addCommand(add) + .addCommand(diff) + .addCommand(migrate) + .addCommand(info) program.parse() } diff --git a/packages/cli/src/migrations/migrate-icons.ts b/packages/cli/src/migrations/migrate-icons.ts new file mode 100644 index 0000000000..b46cad162c --- /dev/null +++ b/packages/cli/src/migrations/migrate-icons.ts @@ -0,0 +1,196 @@ +import type { Config } from '@/src/utils/get-config'; +import type { iconsSchema } from '@/src/utils/registry/schema'; +import type { z } from 'zod'; + +import { randomBytes } from 'crypto'; +import fg from 'fast-glob'; +import { promises as fs } from 'fs'; +import { tmpdir } from 'os'; +import path from 'path'; +import prompts from 'prompts'; +import { Project, ScriptKind, SyntaxKind } from 'ts-morph'; + +import { highlighter } from '@/src/utils/highlighter'; +import { ICON_LIBRARIES } from '@/src/utils/icon-libraries'; +import { logger } from '@/src/utils/logger'; +import { getRegistryIcons } from '@/src/utils/registry'; +import { spinner } from '@/src/utils/spinner'; +import { updateDependencies } from '@/src/utils/updaters/update-dependencies'; + +export async function migrateIcons(config: Config) { + if (!config.resolvedPaths.ui) { + throw new Error( + 'We could not find a valid `ui` path in your `components.json` file. Please ensure you have a valid `ui` path in your `components.json` file.' + ); + } + + const uiPath = config.resolvedPaths.ui; + const [files, registryIcons] = await Promise.all([ + fg('**/*.{js,ts,jsx,tsx}', { + cwd: uiPath, + }), + getRegistryIcons(), + ]); + + if (Object.keys(registryIcons).length === 0) { + throw new Error('Something went wrong fetching the registry icons.'); + } + + const libraryChoices = Object.entries(ICON_LIBRARIES).map( + ([name, iconLibrary]) => ({ + title: iconLibrary.name, + value: name, + }) + ); + const migrateOptions = await prompts([ + { + choices: libraryChoices, + message: `Which icon library would you like to ${highlighter.info( + 'migrate from' + )}?`, + name: 'sourceLibrary', + type: 'select', + }, + { + choices: libraryChoices, + message: `Which icon library would you like to ${highlighter.info( + 'migrate to' + )}?`, + name: 'targetLibrary', + type: 'select', + }, + ]); + + if (migrateOptions.sourceLibrary === migrateOptions.targetLibrary) { + throw new Error( + 'You cannot migrate to the same icon library. Please choose a different icon library.' + ); + } + if ( + !( + migrateOptions.sourceLibrary in ICON_LIBRARIES && + migrateOptions.targetLibrary in ICON_LIBRARIES + ) + ) { + throw new Error( + 'Invalid icon library. Please choose a valid icon library.' + ); + } + + const sourceLibrary = + ICON_LIBRARIES[migrateOptions.sourceLibrary as keyof typeof ICON_LIBRARIES]; + const targetLibrary = + ICON_LIBRARIES[migrateOptions.targetLibrary as keyof typeof ICON_LIBRARIES]; + const { confirm } = await prompts({ + initial: true, + message: `We will migrate ${highlighter.info( + files.length + )} files in ${highlighter.info( + `./${path.relative(config.resolvedPaths.cwd, uiPath)}` + )} from ${highlighter.info(sourceLibrary.name)} to ${highlighter.info( + targetLibrary.name + )}. Continue?`, + name: 'confirm', + type: 'confirm', + }); + + if (!confirm) { + logger.info('Migration cancelled.'); + process.exit(0); + } + if (targetLibrary.package) { + await updateDependencies([targetLibrary.package], config, { + silent: false, + }); + } + + const migrationSpinner = spinner(`Migrating icons...`)?.start(); + await Promise.all( + files.map(async (file) => { + migrationSpinner.text = `Migrating ${file}...`; + const filePath = path.join(uiPath, file); + const fileContent = await fs.readFile(filePath, 'utf8'); + const content = await migrateIconsFile( + fileContent, + migrateOptions.sourceLibrary, + migrateOptions.targetLibrary, + registryIcons + ); + await fs.writeFile(filePath, content); + }) + ); + migrationSpinner.succeed('Migration complete.'); +} + +export async function migrateIconsFile( + content: string, + sourceLibrary: keyof typeof ICON_LIBRARIES, + targetLibrary: keyof typeof ICON_LIBRARIES, + iconsMapping: z.infer +) { + const sourceLibraryImport = ICON_LIBRARIES[sourceLibrary]?.import; + const targetLibraryImport = ICON_LIBRARIES[targetLibrary]?.import; + const dir = await fs.mkdtemp(path.join(tmpdir(), 'shadcn-')); + const project = new Project({ + compilerOptions: {}, + }); + const tempFile = path.join( + dir, + `shadcn-icons-${randomBytes(4).toString('hex')}.tsx` + ); + const sourceFile = project.createSourceFile(tempFile, content, { + scriptKind: ScriptKind.TSX, + }); + // Find all sourceLibrary imports. + const targetedIcons: string[] = []; + + for (const importDeclaration of sourceFile.getImportDeclarations() ?? []) { + if ( + importDeclaration.getModuleSpecifier()?.getText() !== + `"${sourceLibraryImport}"` + ) { + continue; + } + + for (const specifier of importDeclaration.getNamedImports() ?? []) { + const iconName = specifier.getName(); + // TODO: this is O(n^2) but okay for now. + const targetedIcon = ( + Object.values(iconsMapping).find( + (icon: any) => icon[sourceLibrary] === iconName + ) as any + )?.[targetLibrary]; + + if (!targetedIcon || targetedIcons.includes(targetedIcon)) { + continue; + } + + targetedIcons.push(targetedIcon); + // Remove the named import. + specifier.remove(); + // Replace with the targeted icon. + sourceFile + .getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement) + .filter((node) => node.getTagNameNode()?.getText() === iconName) + .forEach((node) => + node.getTagNameNode()?.replaceWithText(targetedIcon) + ); + } + + // If the named import is empty, remove the import declaration. + if (importDeclaration.getNamedImports()?.length === 0) { + importDeclaration.remove(); + } + } + + if (targetedIcons.length > 0) { + sourceFile.addImportDeclaration({ + moduleSpecifier: targetLibraryImport, + namedImports: targetedIcons.map((icon) => ({ + name: icon, + })), + }); + } + + return sourceFile.getText(); +} diff --git a/packages/cli/src/preflights/preflight-migrate.ts b/packages/cli/src/preflights/preflight-migrate.ts new file mode 100644 index 0000000000..84b9f20cfc --- /dev/null +++ b/packages/cli/src/preflights/preflight-migrate.ts @@ -0,0 +1,66 @@ +import type { migrateOptionsSchema } from '@/src/commands/migrate'; +import type { z } from 'zod'; + +import fs from 'fs-extra'; +import path from 'path'; + +import * as ERRORS from '@/src/utils/errors'; +import { getConfig } from '@/src/utils/get-config'; +import { highlighter } from '@/src/utils/highlighter'; +import { logger } from '@/src/utils/logger'; + +export async function preFlightMigrate( + options: z.infer +) { + const errors: Record = {}; + + // Ensure target directory exists. + // Check for empty project. We assume if no package.json exists, the project is empty. + if ( + !fs.existsSync(options.cwd) || + !fs.existsSync(path.resolve(options.cwd, 'package.json')) + ) { + errors[ERRORS.MISSING_DIR_OR_EMPTY_PROJECT] = true; + + return { + config: null, + errors, + }; + } + // Check for existing components.json file. + if (!fs.existsSync(path.resolve(options.cwd, 'components.json'))) { + errors[ERRORS.MISSING_CONFIG] = true; + + return { + config: null, + errors, + }; + } + + try { + const config = await getConfig(options.cwd); + + return { + config: config!, + errors, + }; + } catch (error) { + logger.break(); + logger.error( + `An invalid ${highlighter.info( + 'components.json' + )} file was found at ${highlighter.info( + options.cwd + )}.\nBefore you can run a migration, you must create a valid ${highlighter.info( + 'components.json' + )} file by running the ${highlighter.info('init')} command.` + ); + logger.error( + `Learn more at ${highlighter.info( + 'https://ui.shadcn.com/docs/components-json' + )}.` + ); + logger.break(); + process.exit(1); + } +} diff --git a/packages/cli/src/utils/get-config.ts b/packages/cli/src/utils/get-config.ts index 3a1e6ec3b5..582702d25c 100644 --- a/packages/cli/src/utils/get-config.ts +++ b/packages/cli/src/utils/get-config.ts @@ -59,6 +59,7 @@ export const rawConfigSchema = z ui: z.string().optional(), utils: z.string(), }), + iconLibrary: z.string().optional(), name: z.string().optional(), registries: z.record(z.string(), registrySchema).optional(), rsc: z.coerce.boolean().default(false), @@ -73,7 +74,7 @@ export const rawConfigSchema = z tsx: z.coerce.boolean().default(true), url: z.string().optional(), }) - .strict(); + .passthrough(); export type RawConfig = z.infer; @@ -98,13 +99,17 @@ export async function getConfig(cwd: string) { if (!config) { return null; } + // Set default icon library if not provided. + if (!config.iconLibrary) { + config.iconLibrary = config.style === 'new-york' ? 'radix' : 'lucide'; + } return await resolveConfigPaths(cwd, config); } export async function resolveConfigPaths(cwd: string, config: RawConfig) { // Read tsconfig.json. - const tsConfig = await loadConfig(cwd); + const tsConfig = loadConfig(cwd); if (tsConfig.resultType === 'failed') { throw new Error( diff --git a/packages/cli/src/utils/get-project-info.ts b/packages/cli/src/utils/get-project-info.ts index d89ec1128d..89d5c246d9 100644 --- a/packages/cli/src/utils/get-project-info.ts +++ b/packages/cli/src/utils/get-project-info.ts @@ -2,6 +2,7 @@ import fg from 'fast-glob'; import fs from 'fs-extra'; import path from 'path'; import { loadConfig } from 'tsconfig-paths'; +import { z } from 'zod'; import { type Framework, FRAMEWORKS } from '@/src/utils/frameworks'; import { @@ -31,6 +32,12 @@ const PROJECT_SHARED_IGNORE = [ 'build', ]; +const TS_CONFIG_SCHEMA = z.object({ + compilerOptions: z.object({ + paths: z.record(z.string().or(z.array(z.string()))), + }), +}); + export async function getProjectInfo(cwd: string): Promise { const [ configFiles, @@ -157,7 +164,10 @@ export async function getTailwindConfigFile(cwd: string) { export async function getTsConfigAliasPrefix(cwd: string) { const tsConfig = await loadConfig(cwd); - if (tsConfig?.resultType === 'failed' || !tsConfig?.paths) { + if ( + tsConfig?.resultType === 'failed' || + Object.entries(tsConfig?.paths).length === 0 + ) { return null; } @@ -169,11 +179,12 @@ export async function getTsConfigAliasPrefix(cwd: string) { paths.includes('./app/*') || paths.includes('./resources/js/*') // Laravel. ) { - return alias.at(0) ?? null; + return alias.replace(/\/\*$/, '') ?? null; } } - return null; + // Use the first alias as the prefix. + return Object.keys(tsConfig?.paths)?.[0].replace(/\/\*$/, '') ?? null; } export async function isTypeScriptProject(cwd: string) { @@ -186,19 +197,31 @@ export async function isTypeScriptProject(cwd: string) { return files.length > 0; } -export async function getTsConfig() { - try { - const tsconfigPath = path.join('tsconfig.json'); - const tsconfig = await fs.readJSON(tsconfigPath); +export async function getTsConfig(cwd: string) { + for (const fallback of [ + 'tsconfig.json', + 'tsconfig.web.json', + 'tsconfig.app.json', + ]) { + const filePath = path.resolve(cwd, fallback); - if (!tsconfig) { - throw new Error('tsconfig.json is missing'); + if (!(await fs.pathExists(filePath))) { + continue; } - return tsconfig; - } catch (error) { - return null; + // We can't use fs.readJSON because it doesn't support comments. + const contents = await fs.readFile(filePath, 'utf8'); + const cleanedContents = contents.replace(/\/\*\s*\*\//g, ''); + const result = TS_CONFIG_SCHEMA.safeParse(JSON.parse(cleanedContents)); + + if (result.error) { + continue; + } + + return result.data; } + + return null; } export async function getProjectConfig( @@ -229,8 +252,9 @@ export async function getProjectConfig( ui: `${projectInfo.aliasPrefix}/components/ui`, utils: `${projectInfo.aliasPrefix}/lib/utils`, }, + iconLibrary: 'lucide', rsc: projectInfo.isRSC, - style: 'new-york', + style: 'default', tailwind: { baseColor: 'zinc', config: projectInfo.tailwindConfigFile, diff --git a/packages/cli/src/utils/icon-libraries.ts b/packages/cli/src/utils/icon-libraries.ts new file mode 100644 index 0000000000..7e320521e8 --- /dev/null +++ b/packages/cli/src/utils/icon-libraries.ts @@ -0,0 +1,12 @@ +export const ICON_LIBRARIES = { + lucide: { + import: 'lucide-react', + name: 'lucide-react', + package: 'lucide-react', + }, + radix: { + import: '@radix-ui/react-icons', + name: '@radix-ui/react-icons', + package: '@radix-ui/react-icons', + }, +}; diff --git a/packages/cli/src/utils/registry/index.ts b/packages/cli/src/utils/registry/index.ts index 0c93f76f62..e6b1e8d199 100644 --- a/packages/cli/src/utils/registry/index.ts +++ b/packages/cli/src/utils/registry/index.ts @@ -11,6 +11,7 @@ import { highlighter } from "@/src/utils/highlighter" import { logger } from "@/src/utils/logger" import { type registryItemFileSchema, + iconsSchema, registryBaseColorSchema, registryIndexSchema, registryItemSchema, @@ -56,6 +57,16 @@ export async function getRegistryStyles(registryUrl?: string) { } } +export async function getRegistryIcons() { + try { + const [result] = await fetchRegistry(["icons/index.json"]) + return iconsSchema.parse(result) + } catch (error) { + handleError(error) + return {} + } +} + export async function getRegistryItem( name: string, style: string, diff --git a/packages/cli/src/utils/registry/schema.ts b/packages/cli/src/utils/registry/schema.ts index 8f76d324b2..1579b44a3a 100644 --- a/packages/cli/src/utils/registry/schema.ts +++ b/packages/cli/src/utils/registry/schema.ts @@ -1,87 +1,92 @@ -import { z } from "zod" +import { z } from 'zod'; // TODO: Extract this to a shared package. export const registryItemTypeSchema = z.enum([ - "registry:style", - "registry:lib", - "registry:example", - "registry:block", - "registry:component", - "registry:ui", - "registry:hook", - "registry:theme", - "registry:page", -]) + 'registry:style', + 'registry:lib', + 'registry:example', + 'registry:block', + 'registry:component', + 'registry:ui', + 'registry:hook', + 'registry:theme', + 'registry:page', +]); export const registryItemFileSchema = z.object({ - path: z.string(), content: z.string().optional(), - type: registryItemTypeSchema, + path: z.string(), target: z.string().optional(), -}) + type: registryItemTypeSchema, +}); export const registryItemTailwindSchema = z.object({ config: z .object({ content: z.array(z.string()).optional(), - theme: z.record(z.string(), z.any()).optional(), plugins: z.array(z.string()).optional(), + theme: z.record(z.string(), z.any()).optional(), }) .optional(), -}) +}); export const registryItemCssVarsSchema = z.object({ - light: z.record(z.string(), z.string()).optional(), dark: z.record(z.string(), z.string()).optional(), -}) + light: z.record(z.string(), z.string()).optional(), +}); export const registryItemSchema = z.object({ - name: z.string(), - type: registryItemTypeSchema, - description: z.string().optional(), + cssVars: registryItemCssVarsSchema.optional(), dependencies: z.array(z.string()).optional(), + description: z.string().optional(), devDependencies: z.array(z.string()).optional(), - registryDependencies: z.array(z.string()).optional(), + docs: z.string().optional(), files: z.array(registryItemFileSchema).optional(), - tailwind: registryItemTailwindSchema.optional(), - cssVars: registryItemCssVarsSchema.optional(), meta: z.record(z.string(), z.any()).optional(), - docs: z.string().optional(), -}) + name: z.string(), + registryDependencies: z.array(z.string()).optional(), + tailwind: registryItemTailwindSchema.optional(), + type: registryItemTypeSchema, +}); -export type RegistryItem = z.infer +export type RegistryItem = z.infer; export const registryIndexSchema = z.array( registryItemSchema.extend({ files: z.array(z.union([z.string(), registryItemFileSchema])).optional(), }) -) +); export const stylesSchema = z.array( z.object({ - name: z.string(), label: z.string(), + name: z.string(), }) -) +); + +export const iconsSchema = z.record( + z.string(), + z.record(z.string(), z.string()) +); export const registryBaseColorSchema = z.object({ - inlineColors: z.object({ - light: z.record(z.string(), z.string()), - dark: z.record(z.string(), z.string()), - }), cssVars: z.object({ + dark: z.record(z.string(), z.string()), light: z.record(z.string(), z.string()), + }), + cssVarsTemplate: z.string(), + inlineColors: z.object({ dark: z.record(z.string(), z.string()), + light: z.record(z.string(), z.string()), }), inlineColorsTemplate: z.string(), - cssVarsTemplate: z.string(), -}) +}); export const registryResolvedItemsTreeSchema = registryItemSchema.pick({ + cssVars: true, dependencies: true, devDependencies: true, + docs: true, files: true, tailwind: true, - cssVars: true, - docs: true, -}) +}); diff --git a/packages/cli/src/utils/transformers/index.ts b/packages/cli/src/utils/transformers/index.ts index d8b447997a..2892583d30 100644 --- a/packages/cli/src/utils/transformers/index.ts +++ b/packages/cli/src/utils/transformers/index.ts @@ -1,21 +1,24 @@ +import type { Config } from "@/src/utils/get-config" +import type { registryBaseColorSchema } from "@/src/utils/registry/schema" +import type { z } from "zod" + import { promises as fs } from "fs" import { tmpdir } from "os" import path from "path" -import { Config } from "@/src/utils/get-config" -import { registryBaseColorSchema } from "@/src/utils/registry/schema" +import { type SourceFile, Project, ScriptKind } from "ts-morph" + import { transformCssVars } from "@/src/utils/transformers/transform-css-vars" +import { transformIcons } from "@/src/utils/transformers/transform-icons" import { transformImport } from "@/src/utils/transformers/transform-import" import { transformJsx } from "@/src/utils/transformers/transform-jsx" import { transformRsc } from "@/src/utils/transformers/transform-rsc" -import { Project, ScriptKind, type SourceFile } from "ts-morph" -import { z } from "zod" import { transformTwPrefixes } from "./transform-tw-prefix" export type TransformOpts = { + config: Config filename: string raw: string - config: Config baseColor?: z.infer transformJsx?: boolean } @@ -42,6 +45,7 @@ export async function transform( transformRsc, transformCssVars, transformTwPrefixes, + transformIcons, ] ) { const tempFile = await createTempSourceFile(opts.filename) @@ -50,7 +54,7 @@ export async function transform( }) for (const transformer of transformers) { - transformer({ sourceFile, ...opts }) + await transformer({ sourceFile, ...opts }) } if (opts.transformJsx) { diff --git a/packages/cli/src/utils/transformers/transform-icons.ts b/packages/cli/src/utils/transformers/transform-icons.ts new file mode 100644 index 0000000000..806d4673ff --- /dev/null +++ b/packages/cli/src/utils/transformers/transform-icons.ts @@ -0,0 +1,84 @@ +import type { Transformer } from '@/src/utils/transformers'; + +import { type SourceFile, SyntaxKind } from 'ts-morph'; + +import { ICON_LIBRARIES } from '@/src/utils/icon-libraries'; +import { getRegistryIcons } from '@/src/utils/registry'; + +// Lucide is the default icon library in the registry. +const SOURCE_LIBRARY = 'lucide'; + +export const transformIcons: Transformer = async ({ config, sourceFile }) => { + // No transform if we cannot read the icon library. + if (!config.iconLibrary || !(config.iconLibrary in ICON_LIBRARIES)) { + return sourceFile; + } + + const registryIcons = await getRegistryIcons(); + const sourceLibrary = SOURCE_LIBRARY; + const targetLibrary = config.iconLibrary; + + if (sourceLibrary === targetLibrary) { + return sourceFile; + } + + const targetedIcons: string[] = []; + + for (const importDeclaration of sourceFile.getImportDeclarations() ?? []) { + if ( + importDeclaration.getModuleSpecifier()?.getText() !== + `"${ICON_LIBRARIES[SOURCE_LIBRARY].import}"` + ) { + continue; + } + + for (const specifier of importDeclaration.getNamedImports() ?? []) { + const iconName = specifier.getName(); + const targetedIcon = registryIcons[iconName]?.[targetLibrary]; + + if (!targetedIcon || targetedIcons.includes(targetedIcon)) { + continue; + } + + targetedIcons.push(targetedIcon); + // Remove the named import. + specifier.remove(); + // Replace with the targeted icon. + sourceFile + .getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement) + .filter((node) => node.getTagNameNode()?.getText() === iconName) + .forEach((node) => + node.getTagNameNode()?.replaceWithText(targetedIcon) + ); + } + + // If the named import is empty, remove the import declaration. + if (importDeclaration.getNamedImports()?.length === 0) { + importDeclaration.remove(); + } + } + + if (targetedIcons.length > 0) { + const iconImportDeclaration = sourceFile.addImportDeclaration({ + moduleSpecifier: + ICON_LIBRARIES[targetLibrary as keyof typeof ICON_LIBRARIES]?.import, + namedImports: targetedIcons.map((icon) => ({ + name: icon, + })), + }); + + if (!_useSemicolon(sourceFile)) { + iconImportDeclaration.replaceWithText( + iconImportDeclaration.getText().replace(';', '') + ); + } + } + + return sourceFile; +}; + +function _useSemicolon(sourceFile: SourceFile) { + return ( + sourceFile.getImportDeclarations()?.[0]?.getText().endsWith(';') ?? false + ); +} diff --git a/packages/cli/src/utils/transformers/transform-import.ts b/packages/cli/src/utils/transformers/transform-import.ts index 87220a9933..4b19c8e7b1 100644 --- a/packages/cli/src/utils/transformers/transform-import.ts +++ b/packages/cli/src/utils/transformers/transform-import.ts @@ -35,8 +35,8 @@ function updateImportAliases(moduleSpecifier: string, config: Config) { } // Not a registry import. if (!moduleSpecifier.startsWith('@/registry/')) { - // We fix the alias an return. - const alias = config.aliases.components.charAt(0); + // We fix the alias and return. + const alias = config.aliases.components.split('/')[0]; return moduleSpecifier.replace(/^@\//, `${alias}/`); } diff --git a/packages/cli/src/utils/updaters/update-dependencies.ts b/packages/cli/src/utils/updaters/update-dependencies.ts index f9d637129d..b91f0add87 100644 --- a/packages/cli/src/utils/updaters/update-dependencies.ts +++ b/packages/cli/src/utils/updaters/update-dependencies.ts @@ -38,7 +38,7 @@ export async function updateDependencies( if (isUsingReact19(config) && packageManager === 'npm') { dependenciesSpinner.stopAndPersist(); logger.warn( - '\nIt looks like you are using React 19. \nSome packages may fail to install due to peer dependency issues (see https://ui.shadcn.com/react-19).\n' + '\nIt looks like you are using React 19. \nSome packages may fail to install due to peer dependency issues in npm (see https://ui.shadcn.com/react-19).\n' ); const confirmation = await prompts([ { diff --git a/packages/cli/src/utils/updaters/update-files.ts b/packages/cli/src/utils/updaters/update-files.ts index 3a06110e56..813d0b72a5 100644 --- a/packages/cli/src/utils/updaters/update-files.ts +++ b/packages/cli/src/utils/updaters/update-files.ts @@ -1,181 +1,193 @@ -import { existsSync, promises as fs } from "fs" -import path, { basename } from "path" -import { Config } from "@/src/utils/get-config" -import { getProjectInfo } from "@/src/utils/get-project-info" -import { highlighter } from "@/src/utils/highlighter" -import { logger } from "@/src/utils/logger" +import type { Config } from '@/src/utils/get-config'; +import type { RegistryItem } from '@/src/utils/registry/schema'; + +import { existsSync, promises as fs } from 'fs'; +import path, { basename } from 'path'; +import prompts from 'prompts'; + +import { getProjectInfo } from '@/src/utils/get-project-info'; +import { highlighter } from '@/src/utils/highlighter'; +import { logger } from '@/src/utils/logger'; import { getRegistryBaseColor, getRegistryItemFileTargetPath, -} from "@/src/utils/registry" -import { RegistryItem } from "@/src/utils/registry/schema" -import { spinner } from "@/src/utils/spinner" -import { transform } from "@/src/utils/transformers" -import { transformCssVars } from "@/src/utils/transformers/transform-css-vars" -import { transformImport } from "@/src/utils/transformers/transform-import" -import { transformRsc } from "@/src/utils/transformers/transform-rsc" -import { transformTwPrefixes } from "@/src/utils/transformers/transform-tw-prefix" -import prompts from "prompts" +} from '@/src/utils/registry'; +import { spinner } from '@/src/utils/spinner'; +import { transform } from '@/src/utils/transformers'; +import { transformCssVars } from '@/src/utils/transformers/transform-css-vars'; +import { transformIcons } from '@/src/utils/transformers/transform-icons'; +import { transformImport } from '@/src/utils/transformers/transform-import'; +import { transformRsc } from '@/src/utils/transformers/transform-rsc'; +import { transformTwPrefixes } from '@/src/utils/transformers/transform-tw-prefix'; export function resolveTargetDir( projectInfo: Awaited>, config: Config, target: string ) { - if (target.startsWith("~/")) { - return path.join(config.resolvedPaths.cwd, target.replace("~/", "")) + if (target.startsWith('~/')) { + return path.join(config.resolvedPaths.cwd, target.replace('~/', '')); } + return projectInfo?.isSrcDir - ? path.join(config.resolvedPaths.cwd, "src", target) - : path.join(config.resolvedPaths.cwd, target) + ? path.join(config.resolvedPaths.cwd, 'src', target) + : path.join(config.resolvedPaths.cwd, target); } export async function updateFiles( - files: RegistryItem["files"], + files: RegistryItem['files'], config: Config, options: { - overwrite?: boolean - force?: boolean - silent?: boolean + force?: boolean; + overwrite?: boolean; + silent?: boolean; } ) { if (!files?.length) { - return + return; } + options = { - overwrite: false, force: false, + overwrite: false, silent: false, ...options, - } + }; const filesCreatedSpinner = spinner(`Updating files.`, { silent: options.silent, - })?.start() + })?.start(); const [projectInfo, baseColor] = await Promise.all([ getProjectInfo(config.resolvedPaths.cwd), getRegistryBaseColor(config.tailwind.baseColor), - ]) + ]); - const filesCreated = [] - const filesUpdated = [] - const filesSkipped = [] + const filesCreated = []; + const filesUpdated = []; + const filesSkipped = []; for (const file of files) { if (!file.content) { - continue + continue; } - let targetDir = getRegistryItemFileTargetPath(file, config) - const fileName = basename(file.path) - let filePath = path.join(targetDir, fileName) + let targetDir = getRegistryItemFileTargetPath(file, config); + const fileName = basename(file.path); + let filePath = path.join(targetDir, fileName); if (file.target) { - filePath = resolveTargetDir(projectInfo, config, file.target) - targetDir = path.dirname(filePath) + filePath = resolveTargetDir(projectInfo, config, file.target); + targetDir = path.dirname(filePath); } - if (!config.tsx) { filePath = filePath.replace(/\.tsx?$/, (match) => - match === ".tsx" ? ".jsx" : ".js" - ) + match === '.tsx' ? '.jsx' : '.js' + ); } - const existingFile = existsSync(filePath) + const existingFile = existsSync(filePath); + if (existingFile && !options.overwrite) { - filesCreatedSpinner.stop() + filesCreatedSpinner.stop(); const { overwrite } = await prompts({ - type: "confirm", - name: "overwrite", + initial: false, message: `The file ${highlighter.info( fileName )} already exists. Would you like to overwrite?`, - initial: false, - }) + name: 'overwrite', + type: 'confirm', + }); if (!overwrite) { - filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath)) - continue + filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath)); + + continue; } - filesCreatedSpinner?.start() - } + filesCreatedSpinner?.start(); + } // Create the target directory if it doesn't exist. if (!existsSync(targetDir)) { - await fs.mkdir(targetDir, { recursive: true }) + await fs.mkdir(targetDir, { recursive: true }); } // Run our transformers. const content = await transform( { + baseColor, + config, filename: file.path, raw: file.content, - config, - baseColor, transformJsx: !config.tsx, }, - [transformImport, transformRsc, transformCssVars, transformTwPrefixes] - ) - - await fs.writeFile(filePath, content, "utf-8") + [ + transformImport, + transformRsc, + transformCssVars, + transformTwPrefixes, + transformIcons, + ] + ); + + await fs.writeFile(filePath, content, 'utf-8'); existingFile ? filesUpdated.push(path.relative(config.resolvedPaths.cwd, filePath)) - : filesCreated.push(path.relative(config.resolvedPaths.cwd, filePath)) + : filesCreated.push(path.relative(config.resolvedPaths.cwd, filePath)); } - const hasUpdatedFiles = filesCreated.length || filesUpdated.length - if (!hasUpdatedFiles && !filesSkipped.length) { - filesCreatedSpinner?.info("No files updated.") - } + const hasUpdatedFiles = filesCreated.length || filesUpdated.length; - if (filesCreated.length) { + if (!hasUpdatedFiles && filesSkipped.length === 0) { + filesCreatedSpinner?.info('No files updated.'); + } + if (filesCreated.length > 0) { filesCreatedSpinner?.succeed( `Created ${filesCreated.length} ${ - filesCreated.length === 1 ? "file" : "files" + filesCreated.length === 1 ? 'file' : 'files' }:` - ) + ); + if (!options.silent) { for (const file of filesCreated) { - logger.log(` - ${file}`) + logger.log(` - ${file}`); } } } else { - filesCreatedSpinner?.stop() + filesCreatedSpinner?.stop(); } - - if (filesUpdated.length) { + if (filesUpdated.length > 0) { spinner( `Updated ${filesUpdated.length} ${ - filesUpdated.length === 1 ? "file" : "files" + filesUpdated.length === 1 ? 'file' : 'files' }:`, { silent: options.silent, } - )?.info() + )?.info(); + if (!options.silent) { for (const file of filesUpdated) { - logger.log(` - ${file}`) + logger.log(` - ${file}`); } } } - - if (filesSkipped.length) { + if (filesSkipped.length > 0) { spinner( `Skipped ${filesSkipped.length} ${ - filesUpdated.length === 1 ? "file" : "files" + filesUpdated.length === 1 ? 'file' : 'files' }:`, { silent: options.silent, } - )?.info() + )?.info(); + if (!options.silent) { for (const file of filesSkipped) { - logger.log(` - ${file}`) + logger.log(` - ${file}`); } } } - if (!options.silent) { - logger.break() + logger.break(); } } diff --git a/packages/cli/src/utils/updaters/update-tailwind-config.ts b/packages/cli/src/utils/updaters/update-tailwind-config.ts index 24b289745b..3b64d594ac 100644 --- a/packages/cli/src/utils/updaters/update-tailwind-config.ts +++ b/packages/cli/src/utils/updaters/update-tailwind-config.ts @@ -9,6 +9,7 @@ import { tmpdir } from 'os'; import path from 'path'; import objectToString from 'stringify-object'; import { + type ArrayLiteralExpression, type ObjectLiteralExpression, type PropertyAssignment, type VariableStatement, @@ -198,8 +199,11 @@ async function addTailwindConfigTheme( if (themeInitializer?.isKind(SyntaxKind.ObjectLiteralExpression)) { const themeObjectString = themeInitializer.getText(); const themeObject = await parseObjectLiteral(themeObjectString); - const result = deepmerge(themeObject, theme); + const result = deepmerge(themeObject, theme, { + arrayMerge: (dst, src) => src, + }); const resultString = objectToString(result) + .replace(/'\.{3}(.*)'/g, '...$1') // Remove quote around spread element .replace(/'"/g, "'") // Replace `\" with " .replace(/"'/g, "'") // Replace `\" with " .replace(/'\[/g, '[') // Replace `[ with [ @@ -293,7 +297,8 @@ export function nestSpreadProperties(obj: ObjectLiteralExpression) { // Replace spread with a property assignment obj.insertPropertyAssignment(i, { initializer: `"...${spreadText.replace(/^\.{3}/, '')}"`, - name: `___${spreadText.replace(/^\.{3}/, '')}`, + // Need to escape the name with " so that deepmerge doesn't mishandle the key + name: `"___${spreadText.replace(/^\.{3}/, '')}"`, }); // Remove the original spread assignment @@ -307,11 +312,40 @@ export function nestSpreadProperties(obj: ObjectLiteralExpression) { nestSpreadProperties( initializer.asKindOrThrow(SyntaxKind.ObjectLiteralExpression) ); + } else if (initializer?.isKind(SyntaxKind.ArrayLiteralExpression)) { + nestSpreadElements( + initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression) + ); } } } } +export function nestSpreadElements(arr: ArrayLiteralExpression) { + const elements = arr.getElements(); + + for (let j = 0; j < elements.length; j++) { + const element = elements[j]; + + if (element.isKind(SyntaxKind.ObjectLiteralExpression)) { + // Recursive check on objects within arrays + nestSpreadProperties( + element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression) + ); + } else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) { + // Recursive check on nested arrays + nestSpreadElements( + element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression) + ); + } else if (element.isKind(SyntaxKind.SpreadElement)) { + const spreadText = element.getText(); + // Spread element within an array + arr.removeElement(j); + arr.insertElement(j, `"${spreadText}"`); + } + } +} + export function unnestSpreadProperties(obj: ObjectLiteralExpression) { const properties = obj.getProperties(); @@ -323,7 +357,9 @@ export function unnestSpreadProperties(obj: ObjectLiteralExpression) { const initializer = propAssignment.getInitializer(); if (initializer?.isKind(SyntaxKind.StringLiteral)) { - const value = initializer.getLiteralValue(); + const value = initializer + .asKindOrThrow(SyntaxKind.StringLiteral) + .getLiteralValue(); if (value.startsWith('...')) { obj.insertSpreadAssignment(i, { expression: value.slice(3) }); @@ -331,6 +367,39 @@ export function unnestSpreadProperties(obj: ObjectLiteralExpression) { } } else if (initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) { unnestSpreadProperties(initializer as ObjectLiteralExpression); + } else if (initializer?.isKind(SyntaxKind.ArrayLiteralExpression)) { + unnsetSpreadElements( + initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression) + ); + } + } + } +} + +export function unnsetSpreadElements(arr: ArrayLiteralExpression) { + const elements = arr.getElements(); + + for (let j = 0; j < elements.length; j++) { + const element = elements[j]; + + if (element.isKind(SyntaxKind.ObjectLiteralExpression)) { + // Recursive check on objects within arrays + unnestSpreadProperties( + element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression) + ); + } else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) { + // Recursive check on nested arrays + unnsetSpreadElements( + element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression) + ); + } else if (element.isKind(SyntaxKind.StringLiteral)) { + const spreadText = element.getText(); + // check if spread element + const spreadTest = /^["'](\.{3}.*)["']$/g; + + if (spreadTest.test(spreadText)) { + arr.removeElement(j); + arr.insertElement(j, spreadText.replace(spreadTest, '$1')); } } } @@ -364,13 +433,46 @@ function parseObjectLiteralExpression(node: ObjectLiteralExpression): any { for (const property of node.getProperties()) { if (property.isKind(SyntaxKind.PropertyAssignment)) { const name = property.getName().replace(/'/g, ''); - result[name] = property - .getInitializer() - ?.isKind(SyntaxKind.ObjectLiteralExpression) - ? parseObjectLiteralExpression( - property.getInitializer() as ObjectLiteralExpression - ) - : parseValue(property.getInitializer()); + + if ( + property.getInitializer()?.isKind(SyntaxKind.ObjectLiteralExpression) + ) { + result[name] = parseObjectLiteralExpression( + property.getInitializer() as ObjectLiteralExpression + ); + } else if ( + property.getInitializer()?.isKind(SyntaxKind.ArrayLiteralExpression) + ) { + result[name] = parseArrayLiteralExpression( + property.getInitializer() as ArrayLiteralExpression + ); + } else { + result[name] = parseValue(property.getInitializer()); + } + } + } + + return result; +} + +function parseArrayLiteralExpression(node: ArrayLiteralExpression): any[] { + const result: any[] = []; + + for (const element of node.getElements()) { + if (element.isKind(SyntaxKind.ObjectLiteralExpression)) { + result.push( + parseObjectLiteralExpression( + element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression) + ) + ); + } else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) { + result.push( + parseArrayLiteralExpression( + element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression) + ) + ); + } else { + result.push(parseValue(element)); } } @@ -378,9 +480,9 @@ function parseObjectLiteralExpression(node: ObjectLiteralExpression): any { } function parseValue(node: any): any { - switch (node.kind) { + switch (node.getKind()) { case SyntaxKind.ArrayLiteralExpression: { - return node.elements.map(parseValue); + return node.getElements().map(parseValue); } case SyntaxKind.FalseKeyword: { return false; @@ -389,10 +491,13 @@ function parseValue(node: any): any { return null; } case SyntaxKind.NumericLiteral: { - return Number(node.text); + return Number(node.getText()); + } + case SyntaxKind.ObjectLiteralExpression: { + return parseObjectLiteralExpression(node); } case SyntaxKind.StringLiteral: { - return node.text; + return node.getText(); } case SyntaxKind.TrueKeyword: { return true; From af1ba1ad3cacf2e337a705499ee4dfd298dabc01 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Mon, 25 Nov 2024 19:05:03 +0100 Subject: [PATCH 2/5] eslint --- .eslintrc.cjs | 8 ++++++++ config/eslint/bases/unicorn.cjs | 1 + 2 files changed, 9 insertions(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e4b702d0ab..326e0906d6 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -36,6 +36,14 @@ module.exports = { '**/scripts/*.mts', ], overrides: [ + { + files: ['**/registry/default/**/*'], + rules: { + 'jsx-a11y/iframe-has-title': 'off', + 'jsx-a11y/media-has-caption': 'off', + 'react/jsx-no-comment-textnodes': 'off', + }, + }, { files: ['*.ts', '*.tsx', '*.mts'], parserOptions: { diff --git a/config/eslint/bases/unicorn.cjs b/config/eslint/bases/unicorn.cjs index 69b017c136..33b82173e1 100644 --- a/config/eslint/bases/unicorn.cjs +++ b/config/eslint/bases/unicorn.cjs @@ -27,6 +27,7 @@ module.exports = { 'unicorn/no-array-callback-reference': 'off', 'unicorn/no-array-for-each': 'off', 'unicorn/no-array-reduce': 'off', + 'unicorn/no-document-cookie': 'off', 'unicorn/no-for-loop': 'off', 'unicorn/no-null': 'off', 'unicorn/no-thenable': 'off', From dfd37ad8b86ea867e334c5dffee861574e5e6c03 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Mon, 25 Nov 2024 19:05:14 +0100 Subject: [PATCH 3/5] docs --- .../www/content/docs/components/changelog.mdx | 8 + apps/www/content/docs/components/cli.mdx | 16 +- .../docs/components/dark-mode/next.mdx | 6 +- .../docs/components/installation/manual.mdx | 19 +- apps/www/next.config.ts | 4 + apps/www/package.json | 4 +- apps/www/public/r/icons/index.json | 150 ++ .../r/styles/default/basic-elements-demo.json | 2 +- .../r/styles/default/basic-nodes-demo.json | 2 +- apps/www/public/r/styles/default/button.json | 2 +- apps/www/public/r/styles/default/editor.json | 2 +- .../public/r/styles/default/iframe-demo.json | 2 +- apps/www/public/r/styles/default/input.json | 2 +- .../r/styles/default/media-audio-element.json | 2 +- .../r/styles/default/reset-node-demo.json | 2 +- apps/www/public/r/styles/default/toolbar.json | 2 +- apps/www/scripts/build-registry.mts | 75 +- apps/www/src/__registry__/icons.tsx | 266 +++ apps/www/src/__registry__/index.tsx | 1884 +++++++++++++++-- .../(app)/_components/announcement-button.tsx | 9 +- .../src/app/(app)/_components/home-tabs.tsx | 2 +- .../(app)/docs/[[...slug]]/doc-content.tsx | 4 +- .../src/app/(app)/docs/[[...slug]]/page.tsx | 80 +- apps/www/src/app/(app)/docs/layout.tsx | 18 +- apps/www/src/app/(app)/editors/layout.tsx | 2 +- apps/www/src/app/(app)/editors/page.tsx | 38 +- apps/www/src/app/(app)/layout.tsx | 17 +- apps/www/src/app/(app)/page.tsx | 38 +- .../src/app/(blocks)/blocks/[name]/page.tsx | 66 +- apps/www/src/app/layout.tsx | 26 +- apps/www/src/components/block-actions.ts | 22 + apps/www/src/components/block-chunk.tsx | 58 - apps/www/src/components/block-display.tsx | 76 +- apps/www/src/components/block-preview.tsx | 75 - apps/www/src/components/block-toolbar.tsx | 134 -- apps/www/src/components/block-viewer.tsx | 606 ++++++ apps/www/src/components/block-wrapper.tsx | 71 - apps/www/src/components/command-menu.tsx | 2 +- .../src/components/component-installation.tsx | 129 +- .../src/components/component-preview-pro.tsx | 81 +- apps/www/src/components/component-preview.tsx | 146 +- apps/www/src/components/context/providers.tsx | 1 + .../src/components/context/theme-provider.tsx | 7 +- apps/www/src/components/copy-button.tsx | 47 +- apps/www/src/components/customizer-drawer.tsx | 2 +- apps/www/src/components/main-nav.tsx | 12 +- apps/www/src/components/mobile-nav.tsx | 70 +- apps/www/src/components/mode-switcher.tsx | 34 + apps/www/src/components/page-header.tsx | 5 +- apps/www/src/components/pager.tsx | 32 +- .../www/src/components/playground-preview.tsx | 2 +- .../src/components/plugins-tab-content.tsx | 2 +- apps/www/src/components/sidebar-nav.tsx | 13 +- apps/www/src/components/site-footer.tsx | 2 +- apps/www/src/components/site-header.tsx | 59 +- apps/www/src/components/theme-customizer.tsx | 97 +- apps/www/src/components/themes-button.tsx | 2 +- apps/www/src/components/ui/sidebar.tsx | 766 +++++++ apps/www/src/config/docs.ts | 4 + apps/www/src/config/site.ts | 5 + apps/www/src/hooks/use-meta-color.ts | 24 + apps/www/src/hooks/use-mobile.ts | 20 + apps/www/src/lib/blocks.ts | 171 +- apps/www/src/lib/fonts.ts | 4 - apps/www/src/lib/highlight-code.ts | 23 +- apps/www/src/lib/registry-cache.ts | 34 + apps/www/src/lib/registry.ts | 288 +++ apps/www/src/lib/rehype-component.ts | 479 +++-- apps/www/src/lib/rehype-utils.ts | 202 +- .../registry/default/example/iframe-demo.tsx | 1 - .../example/values/basic-elements-value.tsx | 1 - .../src/registry/default/plate-ui/button.tsx | 2 +- .../src/registry/default/plate-ui/editor.tsx | 4 +- .../src/registry/default/plate-ui/input.tsx | 2 +- .../default/plate-ui/media-audio-element.tsx | 1 - .../src/registry/default/plate-ui/toolbar.tsx | 1 - apps/www/src/registry/registry-components.ts | 204 +- apps/www/src/registry/registry-examples.ts | 805 +++++-- apps/www/src/registry/registry-icons.ts | 166 ++ apps/www/src/registry/registry-ui.ts | 280 +-- apps/www/src/registry/schema.ts | 30 +- apps/www/src/styles/globals.css | 44 +- 82 files changed, 6000 insertions(+), 2098 deletions(-) create mode 100644 apps/www/public/r/icons/index.json create mode 100644 apps/www/src/__registry__/icons.tsx create mode 100644 apps/www/src/components/block-actions.ts delete mode 100644 apps/www/src/components/block-chunk.tsx delete mode 100644 apps/www/src/components/block-preview.tsx delete mode 100644 apps/www/src/components/block-toolbar.tsx create mode 100644 apps/www/src/components/block-viewer.tsx delete mode 100644 apps/www/src/components/block-wrapper.tsx create mode 100644 apps/www/src/components/mode-switcher.tsx create mode 100644 apps/www/src/components/ui/sidebar.tsx create mode 100644 apps/www/src/hooks/use-meta-color.ts create mode 100644 apps/www/src/hooks/use-mobile.ts create mode 100644 apps/www/src/lib/registry-cache.ts create mode 100644 apps/www/src/lib/registry.ts create mode 100644 apps/www/src/registry/registry-icons.ts diff --git a/apps/www/content/docs/components/changelog.mdx b/apps/www/content/docs/components/changelog.mdx index 9c325803e5..5c4af4da9f 100644 --- a/apps/www/content/docs/components/changelog.mdx +++ b/apps/www/content/docs/components/changelog.mdx @@ -11,6 +11,14 @@ Use the [CLI](https://platejs.org/docs/components/cli) to install the latest ver ## November 2024 #16 +### November 21 #16.8 + +Shadcn sync: + +- `input`: add `text-base md:text-sm` +- `textarea`: add `text-base md:text-sm` +- `editor`(`ai`, `aiChat` variants): add `text-base md:text-sm` + ### November 14 #16.7 - `toolbar`: Add `ToolbarSplitButton`, `ToolbarSplitButtonPrimary`, `ToolbarSplitButtonSecondary` diff --git a/apps/www/content/docs/components/cli.mdx b/apps/www/content/docs/components/cli.mdx index 6488d65701..e925154f58 100644 --- a/apps/www/content/docs/components/cli.mdx +++ b/apps/www/content/docs/components/cli.mdx @@ -32,6 +32,7 @@ Options: -f, --force force overwrite of existing components.json. (default: false) -y, --yes skip confirmation prompt. (default: false) -c, --cwd the working directory. defaults to the current directory. + -a, --all add all available components. (default: false) -n, --name registry name. (default: plate) -s, --silent mute output (default: false) --src-dir use the src directory when creating a new project (default: false) @@ -118,13 +119,6 @@ Here's an example `components.json` file configured for [shadcn/ui](https://ui.s "style": "default", "rsc": true, "tsx": true, - "aliases": { - "components": "@/components", - "hooks": "@/hooks", - "lib": "@/lib", - "ui": "@/components/ui", - "utils": "@/lib/utils" - }, "tailwind": { "baseColor": "slate", "config": "tailwind.config.ts", @@ -132,6 +126,14 @@ Here's an example `components.json` file configured for [shadcn/ui](https://ui.s "cssVariables": true, "prefix": "" }, + "aliases": { + "components": "@/components", + "hooks": "@/hooks", + "lib": "@/lib", + "ui": "@/components/ui", + "utils": "@/lib/utils" + }, + "iconLibrary": "lucide", "registries": { "plate": { "aliases": { diff --git a/apps/www/content/docs/components/dark-mode/next.mdx b/apps/www/content/docs/components/dark-mode/next.mdx index 9bcb06461a..188e59c8c6 100644 --- a/apps/www/content/docs/components/dark-mode/next.mdx +++ b/apps/www/content/docs/components/dark-mode/next.mdx @@ -22,9 +22,11 @@ npm install next-themes import * as React from 'react'; import { ThemeProvider as NextThemesProvider } from 'next-themes'; -import { type ThemeProviderProps } from 'next-themes/dist/types'; -export function ThemeProvider({ children, ...props }: ThemeProviderProps) { +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { return {children}; } ``` diff --git a/apps/www/content/docs/components/installation/manual.mdx b/apps/www/content/docs/components/installation/manual.mdx index c921b1c584..70c3b4fc87 100644 --- a/apps/www/content/docs/components/installation/manual.mdx +++ b/apps/www/content/docs/components/installation/manual.mdx @@ -21,6 +21,8 @@ npm install slate slate-dom slate-react slate-history slate-hyperscript @udecode ### Configure path aliases +Configure the path aliases in your `tsconfig.json` file. + ```json {3-6} title="tsconfig.json" { "compilerOptions": { @@ -42,7 +44,7 @@ npm install slate slate-dom slate-react slate-history slate-hyperscript @udecode ### Configure components.json -Create [components.json](/docs/components/components-json) at the root of your project, then add the following: +Create a [components.json](/docs/components/components-json) in the root of your project. ```json { @@ -50,13 +52,6 @@ Create [components.json](/docs/components/components-json) at the root of your p "style": "default", "rsc": true, "tsx": true, - "aliases": { - "components": "@/components", - "hooks": "@/hooks", - "lib": "@/lib", - "ui": "@/components/ui", - "utils": "@/lib/utils" - }, "tailwind": { "baseColor": "slate", "config": "tailwind.config.ts", @@ -64,6 +59,14 @@ Create [components.json](/docs/components/components-json) at the root of your p "cssVariables": true, "prefix": "" }, + "aliases": { + "components": "@/components", + "hooks": "@/hooks", + "lib": "@/lib", + "ui": "@/components/ui", + "utils": "@/lib/utils" + }, + "iconLibrary": "lucide", "registries": { "plate": { "aliases": { diff --git a/apps/www/next.config.ts b/apps/www/next.config.ts index 5bf512c151..b4c16f3f40 100644 --- a/apps/www/next.config.ts +++ b/apps/www/next.config.ts @@ -26,6 +26,10 @@ const nextConfig = async (phase: string) => { ], }, + outputFileTracingIncludes: { + '/blocks/*': ['./registry/**/*'], + }, + // Configure domains to allow for optimized image loading. // https://nextjs.org/docs/api-reference/next.config.js/react-strict-mod reactStrictMode: true, diff --git a/apps/www/package.json b/apps/www/package.json index 5a8612e7ca..98fb2974f3 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -144,7 +144,7 @@ "match-sorter": "6.3.4", "next": "15.0.3", "next-contentlayer2": "^0.4.6", - "next-themes": "^0.3.0", + "next-themes": "^0.4.3", "nuqs": "^2.0.3", "prismjs": "^1.29.0", "react": "^18.3.1", @@ -175,7 +175,7 @@ "unist-util-visit": "^5.0.0", "uploadthing": "7.2.0", "use-file-picker": "2.1.2", - "vaul": "0.9.0" + "vaul": "1.1.1" }, "devDependencies": { "@shikijs/compat": "^1.17.5", diff --git a/apps/www/public/r/icons/index.json b/apps/www/public/r/icons/index.json new file mode 100644 index 0000000000..bf65dca463 --- /dev/null +++ b/apps/www/public/r/icons/index.json @@ -0,0 +1,150 @@ +{ + "AlertCircle": { + "lucide": "AlertCircle", + "radix": "ExclamationTriangleIcon" + }, + "ArrowLeft": { + "lucide": "ArrowLeft", + "radix": "ArrowLeftIcon" + }, + "ArrowRight": { + "lucide": "ArrowRight", + "radix": "ArrowRightIcon" + }, + "ArrowUpDown": { + "lucide": "ArrowUpDown", + "radix": "CaretSortIcon" + }, + "BellRing": { + "lucide": "BellRing", + "radix": "BellIcon" + }, + "Bold": { + "lucide": "Bold", + "radix": "FontBoldIcon" + }, + "Calculator": { + "lucide": "Calculator", + "radix": "ComponentPlaceholderIcon" + }, + "Calendar": { + "lucide": "Calendar", + "radix": "CalendarIcon" + }, + "Check": { + "lucide": "Check", + "radix": "CheckIcon" + }, + "ChevronDown": { + "lucide": "ChevronDown", + "radix": "ChevronDownIcon" + }, + "ChevronLeft": { + "lucide": "ChevronLeft", + "radix": "ChevronLeftIcon" + }, + "ChevronRight": { + "lucide": "ChevronRight", + "radix": "ChevronRightIcon" + }, + "ChevronUp": { + "lucide": "ChevronUp", + "radix": "ChevronUpIcon" + }, + "ChevronsUpDown": { + "lucide": "ChevronsUpDown", + "radix": "CaretSortIcon" + }, + "Circle": { + "lucide": "Circle", + "radix": "DotFilledIcon" + }, + "Copy": { + "lucide": "Copy", + "radix": "CopyIcon" + }, + "CreditCard": { + "lucide": "CreditCard", + "radix": "ComponentPlaceholderIcon" + }, + "GripVertical": { + "lucide": "GripVertical", + "radix": "DragHandleDots2Icon" + }, + "Italic": { + "lucide": "Italic", + "radix": "FontItalicIcon" + }, + "Loader2": { + "lucide": "Loader2", + "radix": "ReloadIcon" + }, + "Mail": { + "lucide": "Mail", + "radix": "EnvelopeClosedIcon" + }, + "MailOpen": { + "lucide": "MailOpen", + "radix": "EnvelopeOpenIcon" + }, + "Minus": { + "lucide": "Minus", + "radix": "MinusIcon" + }, + "Moon": { + "lucide": "Moon", + "radix": "MoonIcon" + }, + "MoreHorizontal": { + "lucide": "MoreHorizontal", + "radix": "DotsHorizontalIcon" + }, + "PanelLeft": { + "lucide": "PanelLeft", + "radix": "ViewVerticalIcon" + }, + "Plus": { + "lucide": "Plus", + "radix": "PlusIcon" + }, + "Search": { + "lucide": "Search", + "radix": "MagnifyingGlassIcon" + }, + "Send": { + "lucide": "Send", + "radix": "PaperPlaneIcon" + }, + "Settings": { + "lucide": "Settings", + "radix": "GearIcon" + }, + "Slash": { + "lucide": "Slash", + "radix": "SlashIcon" + }, + "Smile": { + "lucide": "Smile", + "radix": "FaceIcon" + }, + "Sun": { + "lucide": "Sun", + "radix": "SunIcon" + }, + "Terminal": { + "lucide": "Terminal", + "radix": "RocketIcon" + }, + "Underline": { + "lucide": "Underline", + "radix": "UnderlineIcon" + }, + "User": { + "lucide": "User", + "radix": "PersonIcon" + }, + "X": { + "lucide": "X", + "radix": "Cross2Icon" + } +} \ No newline at end of file diff --git a/apps/www/public/r/styles/default/basic-elements-demo.json b/apps/www/public/r/styles/default/basic-elements-demo.json index f4d49778a3..fdf9457d89 100644 --- a/apps/www/public/r/styles/default/basic-elements-demo.json +++ b/apps/www/public/r/styles/default/basic-elements-demo.json @@ -16,7 +16,7 @@ "type": "registry:example" }, { - "content": "import { jsx } from '@udecode/plate-test-utils';\n\njsx;\n\nexport const basicElementsValue: any = (\n \n Blocks\n \n Easily create headings of various levels, from H1 to H6, to structure your\n content and make it more organized.\n \n \n Create blockquotes to emphasize important information or highlight quotes\n from external sources.\n \n \n {/* eslint-disable-next-line react/jsx-no-comment-textnodes */}\n // Use code blocks to showcase code snippets\n {`function greet() {`}\n {` console.info('Hello World!');`}\n {`}`}\n \n \n);\n", + "content": "import { jsx } from '@udecode/plate-test-utils';\n\njsx;\n\nexport const basicElementsValue: any = (\n \n Blocks\n \n Easily create headings of various levels, from H1 to H6, to structure your\n content and make it more organized.\n \n \n Create blockquotes to emphasize important information or highlight quotes\n from external sources.\n \n \n // Use code blocks to showcase code snippets\n {`function greet() {`}\n {` console.info('Hello World!');`}\n {`}`}\n \n \n);\n", "path": "example/values/basic-elements-value.tsx", "target": "components/basic-elements-value.tsx", "type": "registry:example" diff --git a/apps/www/public/r/styles/default/basic-nodes-demo.json b/apps/www/public/r/styles/default/basic-nodes-demo.json index 0e91c0718f..e530abc640 100644 --- a/apps/www/public/r/styles/default/basic-nodes-demo.json +++ b/apps/www/public/r/styles/default/basic-nodes-demo.json @@ -26,7 +26,7 @@ "type": "registry:example" }, { - "content": "import { jsx } from '@udecode/plate-test-utils';\n\njsx;\n\nexport const basicElementsValue: any = (\n \n Blocks\n \n Easily create headings of various levels, from H1 to H6, to structure your\n content and make it more organized.\n \n \n Create blockquotes to emphasize important information or highlight quotes\n from external sources.\n \n \n {/* eslint-disable-next-line react/jsx-no-comment-textnodes */}\n // Use code blocks to showcase code snippets\n {`function greet() {`}\n {` console.info('Hello World!');`}\n {`}`}\n \n \n);\n", + "content": "import { jsx } from '@udecode/plate-test-utils';\n\njsx;\n\nexport const basicElementsValue: any = (\n \n Blocks\n \n Easily create headings of various levels, from H1 to H6, to structure your\n content and make it more organized.\n \n \n Create blockquotes to emphasize important information or highlight quotes\n from external sources.\n \n \n // Use code blocks to showcase code snippets\n {`function greet() {`}\n {` console.info('Hello World!');`}\n {`}`}\n \n \n);\n", "path": "example/values/basic-elements-value.tsx", "target": "components/basic-elements-value.tsx", "type": "registry:example" diff --git a/apps/www/public/r/styles/default/button.json b/apps/www/public/r/styles/default/button.json index 3c14623f30..658cebeca3 100644 --- a/apps/www/public/r/styles/default/button.json +++ b/apps/www/public/r/styles/default/button.json @@ -10,7 +10,7 @@ }, "files": [ { - "content": "import * as React from 'react';\n\nimport { Slot } from '@radix-ui/react-slot';\nimport { cn, withRef } from '@udecode/cn';\nimport { type VariantProps, cva } from 'class-variance-authority';\n\nexport const buttonVariants = cva(\n 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\n {\n defaultVariants: {\n size: 'sm',\n variant: 'default',\n },\n variants: {\n isMenu: {\n true: 'w-full cursor-pointer justify-start',\n },\n size: {\n icon: 'size-[28px] rounded-md px-1.5',\n lg: 'h-10 rounded-md px-4',\n md: 'h-8 px-3 text-sm',\n none: '',\n sm: 'h-[28px] rounded-md px-2.5',\n xs: 'h-8 rounded-md px-3 text-xs',\n },\n variant: {\n default: 'bg-primary text-primary-foreground hover:bg-primary/90',\n destructive:\n 'bg-destructive text-destructive-foreground hover:bg-destructive/90',\n ghost: 'hover:bg-accent hover:text-accent-foreground',\n inlineLink: 'text-base text-primary underline underline-offset-4',\n link: 'text-primary underline-offset-4 hover:underline',\n outline:\n 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',\n secondary:\n 'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n },\n },\n }\n);\n\nexport const Button = withRef<\n 'button',\n {\n asChild?: boolean;\n } & VariantProps\n>(({ asChild = false, className, isMenu, size, variant, ...props }, ref) => {\n const Comp = asChild ? Slot : 'button';\n\n return (\n \n );\n});\n", + "content": "import * as React from 'react';\n\nimport { Slot } from '@radix-ui/react-slot';\nimport { cn, withRef } from '@udecode/cn';\nimport { type VariantProps, cva } from 'class-variance-authority';\n\nexport const buttonVariants = cva(\n 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\n {\n defaultVariants: {\n size: 'sm',\n variant: 'default',\n },\n variants: {\n isMenu: {\n true: 'w-full cursor-pointer justify-start',\n },\n size: {\n icon: 'size-[28px] rounded-md px-1.5',\n lg: 'h-9 rounded-md px-4',\n md: 'h-8 px-3 text-sm',\n none: '',\n sm: 'h-[28px] rounded-md px-2.5',\n xs: 'h-8 rounded-md px-3 text-xs',\n },\n variant: {\n default: 'bg-primary text-primary-foreground hover:bg-primary/90',\n destructive:\n 'bg-destructive text-destructive-foreground hover:bg-destructive/90',\n ghost: 'hover:bg-accent hover:text-accent-foreground',\n inlineLink: 'text-base text-primary underline underline-offset-4',\n link: 'text-primary underline-offset-4 hover:underline',\n outline:\n 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',\n secondary:\n 'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n },\n },\n }\n);\n\nexport const Button = withRef<\n 'button',\n {\n asChild?: boolean;\n } & VariantProps\n>(({ asChild = false, className, isMenu, size, variant, ...props }, ref) => {\n const Comp = asChild ? Slot : 'button';\n\n return (\n \n );\n});\n", "path": "plate-ui/button.tsx", "target": "components/plate-ui/button.tsx", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/editor.json b/apps/www/public/r/styles/default/editor.json index df15e69c48..e70f1dc920 100644 --- a/apps/www/public/r/styles/default/editor.json +++ b/apps/www/public/r/styles/default/editor.json @@ -15,7 +15,7 @@ }, "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport type { PlateContentProps } from '@udecode/plate-common/react';\nimport type { VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@udecode/cn';\nimport {\n PlateContent,\n useEditorContainerRef,\n useEditorRef,\n} from '@udecode/plate-common/react';\nimport { cva } from 'class-variance-authority';\n\nconst editorContainerVariants = cva(\n 'relative w-full cursor-text overflow-y-auto caret-primary selection:bg-brand/25 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15',\n {\n defaultVariants: {\n variant: 'default',\n },\n variants: {\n variant: {\n default: 'h-full',\n demo: 'h-[650px]',\n },\n },\n }\n);\n\nexport const EditorContainer = ({\n className,\n variant,\n ...props\n}: React.HTMLAttributes &\n VariantProps) => {\n const editor = useEditorRef();\n const containerRef = useEditorContainerRef();\n\n return (\n \n );\n};\n\nEditorContainer.displayName = 'EditorContainer';\n\nconst editorVariants = cva(\n cn(\n 'group/editor',\n 'relative w-full overflow-x-hidden whitespace-pre-wrap break-words',\n 'rounded-md ring-offset-background placeholder:text-muted-foreground/80 focus-visible:outline-none',\n '[&_[data-slate-placeholder]]:text-muted-foreground/80 [&_[data-slate-placeholder]]:!opacity-100',\n '[&_[data-slate-placeholder]]:top-[auto_!important]',\n '[&_strong]:font-bold'\n ),\n {\n defaultVariants: {\n variant: 'default',\n },\n variants: {\n disabled: {\n true: 'cursor-not-allowed opacity-50',\n },\n focused: {\n true: 'ring-2 ring-ring ring-offset-2',\n },\n variant: {\n ai: 'w-full px-0 text-sm',\n aiChat:\n 'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-3 py-2 text-sm',\n default:\n 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',\n demo: 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',\n fullWidth: 'size-full px-16 pb-72 pt-4 text-base sm:px-24',\n none: '',\n },\n },\n }\n);\n\nexport type EditorProps = PlateContentProps &\n VariantProps;\n\nexport const Editor = React.forwardRef(\n ({ className, disabled, focused, variant, ...props }, ref) => {\n return (\n \n );\n }\n);\n\nEditor.displayName = 'Editor';\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport type { PlateContentProps } from '@udecode/plate-common/react';\nimport type { VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@udecode/cn';\nimport {\n PlateContent,\n useEditorContainerRef,\n useEditorRef,\n} from '@udecode/plate-common/react';\nimport { cva } from 'class-variance-authority';\n\nconst editorContainerVariants = cva(\n 'relative w-full cursor-text overflow-y-auto caret-primary selection:bg-brand/25 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15',\n {\n defaultVariants: {\n variant: 'default',\n },\n variants: {\n variant: {\n default: 'h-full',\n demo: 'h-[650px]',\n },\n },\n }\n);\n\nexport const EditorContainer = ({\n className,\n variant,\n ...props\n}: React.HTMLAttributes &\n VariantProps) => {\n const editor = useEditorRef();\n const containerRef = useEditorContainerRef();\n\n return (\n \n );\n};\n\nEditorContainer.displayName = 'EditorContainer';\n\nconst editorVariants = cva(\n cn(\n 'group/editor',\n 'relative w-full overflow-x-hidden whitespace-pre-wrap break-words',\n 'rounded-md ring-offset-background placeholder:text-muted-foreground/80 focus-visible:outline-none',\n '[&_[data-slate-placeholder]]:text-muted-foreground/80 [&_[data-slate-placeholder]]:!opacity-100',\n '[&_[data-slate-placeholder]]:top-[auto_!important]',\n '[&_strong]:font-bold'\n ),\n {\n defaultVariants: {\n variant: 'default',\n },\n variants: {\n disabled: {\n true: 'cursor-not-allowed opacity-50',\n },\n focused: {\n true: 'ring-2 ring-ring ring-offset-2',\n },\n variant: {\n ai: 'w-full px-0 text-base md:text-sm',\n aiChat:\n 'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-3 py-2 text-base md:text-sm',\n default:\n 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',\n demo: 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',\n fullWidth: 'size-full px-16 pb-72 pt-4 text-base sm:px-24',\n none: '',\n },\n },\n }\n);\n\nexport type EditorProps = PlateContentProps &\n VariantProps;\n\nexport const Editor = React.forwardRef(\n ({ className, disabled, focused, variant, ...props }, ref) => {\n return (\n \n );\n }\n);\n\nEditor.displayName = 'Editor';\n", "path": "plate-ui/editor.tsx", "target": "components/plate-ui/editor.tsx", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/iframe-demo.json b/apps/www/public/r/styles/default/iframe-demo.json index d282db54b4..9f2e1fec10 100644 --- a/apps/www/public/r/styles/default/iframe-demo.json +++ b/apps/www/public/r/styles/default/iframe-demo.json @@ -1,7 +1,7 @@ { "files": [ { - "content": "'use client';\n\nimport React, { useState } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { Plate } from '@udecode/plate-common/react';\n\nimport { editorPlugins } from '@/components/editor/plugins/editor-plugins';\nimport { useCreateEditor } from '@/components/editor/use-create-editor';\nimport { iframeValue } from '@/registry/default/example/values/iframe-value';\nimport { Editor, EditorContainer } from '@/components/plate-ui/editor';\n\nimport { EditableVoidPlugin } from './editable-voids-demo';\n\nexport function IFrame({ children, ...props }: any) {\n const [contentRef, setContentRef] = useState(null);\n const mountNode = contentRef?.contentWindow?.document.body;\n\n return (\n // eslint-disable-next-line jsx-a11y/iframe-has-title\n \n );\n}\n\nexport default function IframeDemo() {\n const editor = useCreateEditor({\n plugins: [...editorPlugins, EditableVoidPlugin],\n value: iframeValue,\n });\n\n return (\n \n );\n}\n", + "content": "'use client';\n\nimport React, { useState } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { Plate } from '@udecode/plate-common/react';\n\nimport { editorPlugins } from '@/components/editor/plugins/editor-plugins';\nimport { useCreateEditor } from '@/components/editor/use-create-editor';\nimport { iframeValue } from '@/registry/default/example/values/iframe-value';\nimport { Editor, EditorContainer } from '@/components/plate-ui/editor';\n\nimport { EditableVoidPlugin } from './editable-voids-demo';\n\nexport function IFrame({ children, ...props }: any) {\n const [contentRef, setContentRef] = useState(null);\n const mountNode = contentRef?.contentWindow?.document.body;\n\n return (\n \n );\n}\n\nexport default function IframeDemo() {\n const editor = useCreateEditor({\n plugins: [...editorPlugins, EditableVoidPlugin],\n value: iframeValue,\n });\n\n return (\n \n );\n}\n", "path": "example/iframe-demo.tsx", "target": "components/iframe-demo.tsx", "type": "registry:example" diff --git a/apps/www/public/r/styles/default/input.json b/apps/www/public/r/styles/default/input.json index 3f4858d2ac..12d8741a6a 100644 --- a/apps/www/public/r/styles/default/input.json +++ b/apps/www/public/r/styles/default/input.json @@ -5,7 +5,7 @@ }, "files": [ { - "content": "import React from 'react';\n\nimport { cn, withVariants } from '@udecode/cn';\nimport { type VariantProps, cva } from 'class-variance-authority';\n\nexport const inputVariants = cva(\n 'flex w-full rounded-md bg-transparent text-sm file:border-0 file:bg-background file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',\n {\n defaultVariants: {\n h: 'md',\n variant: 'default',\n },\n variants: {\n h: {\n md: 'h-10 px-3 py-2',\n sm: 'h-[28px] px-1.5 py-1',\n },\n variant: {\n default:\n 'border border-input ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n ghost: 'border-none focus-visible:ring-transparent',\n },\n },\n }\n);\n\nexport type InputProps = React.ComponentPropsWithoutRef<'input'> &\n VariantProps;\n\nexport const Input = withVariants('input', inputVariants, ['variant', 'h']);\n\nexport type FloatingInputProps = InputProps & {\n label: string;\n};\n\nexport function FloatingInput({\n id,\n className,\n label,\n ...props\n}: FloatingInputProps) {\n return (\n <>\n \n {label}\n \n \n \n );\n}\n", + "content": "import React from 'react';\n\nimport { cn, withVariants } from '@udecode/cn';\nimport { type VariantProps, cva } from 'class-variance-authority';\n\nexport const inputVariants = cva(\n 'flex w-full rounded-md bg-transparent text-base file:border-0 file:bg-background file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',\n {\n defaultVariants: {\n h: 'md',\n variant: 'default',\n },\n variants: {\n h: {\n md: 'h-10 px-3 py-2',\n sm: 'h-[28px] px-1.5 py-1',\n },\n variant: {\n default:\n 'border border-input ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n ghost: 'border-none focus-visible:ring-transparent',\n },\n },\n }\n);\n\nexport type InputProps = React.ComponentPropsWithoutRef<'input'> &\n VariantProps;\n\nexport const Input = withVariants('input', inputVariants, ['variant', 'h']);\n\nexport type FloatingInputProps = InputProps & {\n label: string;\n};\n\nexport function FloatingInput({\n id,\n className,\n label,\n ...props\n}: FloatingInputProps) {\n return (\n <>\n \n {label}\n \n \n \n );\n}\n", "path": "plate-ui/input.tsx", "target": "components/plate-ui/input.tsx", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/media-audio-element.json b/apps/www/public/r/styles/default/media-audio-element.json index ebd5c043ef..1a84c8f8f7 100644 --- a/apps/www/public/r/styles/default/media-audio-element.json +++ b/apps/www/public/r/styles/default/media-audio-element.json @@ -20,7 +20,7 @@ }, "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { withHOC } from '@udecode/plate-common/react';\nimport { useMediaState } from '@udecode/plate-media/react';\nimport { ResizableProvider } from '@udecode/plate-resizable';\n\nimport { Caption, CaptionTextarea } from './caption';\nimport { PlateElement } from './plate-element';\n\nexport const MediaAudioElement = withHOC(\n ResizableProvider,\n withRef(\n ({ children, className, nodeProps, ...props }, ref) => {\n const { align = 'center', readOnly, unsafeUrl } = useMediaState();\n\n return (\n \n
\n
\n {/* eslint-disable-next-line jsx-a11y/media-has-caption */}\n
\n\n \n \n \n
\n {children}\n \n );\n }\n )\n);\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { withHOC } from '@udecode/plate-common/react';\nimport { useMediaState } from '@udecode/plate-media/react';\nimport { ResizableProvider } from '@udecode/plate-resizable';\n\nimport { Caption, CaptionTextarea } from './caption';\nimport { PlateElement } from './plate-element';\n\nexport const MediaAudioElement = withHOC(\n ResizableProvider,\n withRef(\n ({ children, className, nodeProps, ...props }, ref) => {\n const { align = 'center', readOnly, unsafeUrl } = useMediaState();\n\n return (\n \n
\n
\n
\n\n \n \n \n
\n {children}\n \n );\n }\n )\n);\n", "path": "plate-ui/media-audio-element.tsx", "target": "components/plate-ui/media-audio-element.tsx", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/reset-node-demo.json b/apps/www/public/r/styles/default/reset-node-demo.json index c0682ccad6..62f19e6db0 100644 --- a/apps/www/public/r/styles/default/reset-node-demo.json +++ b/apps/www/public/r/styles/default/reset-node-demo.json @@ -7,7 +7,7 @@ "type": "registry:example" }, { - "content": "import { jsx } from '@udecode/plate-test-utils';\n\njsx;\n\nexport const basicElementsValue: any = (\n \n Blocks\n \n Easily create headings of various levels, from H1 to H6, to structure your\n content and make it more organized.\n \n \n Create blockquotes to emphasize important information or highlight quotes\n from external sources.\n \n \n {/* eslint-disable-next-line react/jsx-no-comment-textnodes */}\n // Use code blocks to showcase code snippets\n {`function greet() {`}\n {` console.info('Hello World!');`}\n {`}`}\n \n \n);\n", + "content": "import { jsx } from '@udecode/plate-test-utils';\n\njsx;\n\nexport const basicElementsValue: any = (\n \n Blocks\n \n Easily create headings of various levels, from H1 to H6, to structure your\n content and make it more organized.\n \n \n Create blockquotes to emphasize important information or highlight quotes\n from external sources.\n \n \n // Use code blocks to showcase code snippets\n {`function greet() {`}\n {` console.info('Hello World!');`}\n {`}`}\n \n \n);\n", "path": "example/values/basic-elements-value.tsx", "target": "components/basic-elements-value.tsx", "type": "registry:example" diff --git a/apps/www/public/r/styles/default/toolbar.json b/apps/www/public/r/styles/default/toolbar.json index a5ab23b6a5..60d117938c 100644 --- a/apps/www/public/r/styles/default/toolbar.json +++ b/apps/www/public/r/styles/default/toolbar.json @@ -7,7 +7,7 @@ }, "files": [ { - "content": "'use client';\n\nimport * as React from 'react';\n\nimport * as ToolbarPrimitive from '@radix-ui/react-toolbar';\nimport { cn, withCn, withRef, withVariants } from '@udecode/cn';\nimport { type VariantProps, cva } from 'class-variance-authority';\nimport { ChevronDown } from 'lucide-react';\n\nimport { Separator } from './separator';\nimport { withTooltip } from './tooltip';\n\nexport const Toolbar = withCn(\n ToolbarPrimitive.Root,\n 'relative flex select-none items-center'\n);\n\nexport const ToolbarToggleGroup = withCn(\n ToolbarPrimitive.ToolbarToggleGroup,\n 'flex items-center'\n);\n\nexport const ToolbarLink = withCn(\n ToolbarPrimitive.Link,\n 'font-medium underline underline-offset-4'\n);\n\nexport const ToolbarSeparator = withCn(\n ToolbarPrimitive.Separator,\n 'mx-2 my-1 w-px shrink-0 bg-border'\n);\n\nconst toolbarButtonVariants = cva(\n cn(\n 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium text-foreground ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg:not([data-icon])]:size-4'\n ),\n {\n defaultVariants: {\n size: 'sm',\n variant: 'default',\n },\n variants: {\n size: {\n default: 'h-10 px-3',\n lg: 'h-11 px-5',\n sm: 'h-7 px-2',\n },\n variant: {\n default:\n 'bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground',\n outline:\n 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',\n },\n },\n }\n);\n\nconst dropdownArrowVariants = cva(\n cn(\n 'inline-flex items-center justify-center rounded-r-md text-sm font-medium text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'\n ),\n {\n defaultVariants: {\n size: 'sm',\n variant: 'default',\n },\n variants: {\n size: {\n default: 'h-10 w-6',\n lg: 'h-11 w-8',\n sm: 'h-7 w-4',\n },\n variant: {\n default:\n 'bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground',\n outline:\n 'border border-l-0 border-input bg-transparent hover:bg-accent hover:text-accent-foreground',\n },\n },\n }\n);\n\nconst ToolbarButton = withTooltip(\n // eslint-disable-next-line react/display-name\n React.forwardRef<\n React.ElementRef,\n {\n isDropdown?: boolean;\n pressed?: boolean;\n } & Omit<\n React.ComponentPropsWithoutRef,\n 'asChild' | 'value'\n > &\n VariantProps\n >(\n (\n { children, className, isDropdown, pressed, size, variant, ...props },\n ref\n ) => {\n return typeof pressed === 'boolean' ? (\n \n \n {isDropdown ? (\n <>\n
\n {children}\n
\n
\n \n
\n \n ) : (\n children\n )}\n \n \n ) : (\n \n {children}\n \n );\n }\n )\n);\nToolbarButton.displayName = 'ToolbarButton';\n\nexport { ToolbarButton };\n\nexport const ToolbarSplitButton = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ children, className, ...props }, ref) => {\n return (\n \n {children}\n \n );\n});\n\nexport const ToolbarSplitButtonPrimary = React.forwardRef<\n React.ElementRef,\n Omit, 'value'>\n>(({ children, className, size, variant, ...props }, ref) => {\n return (\n \n {children}\n \n );\n});\n\nexport const ToolbarSplitButtonSecondary = React.forwardRef<\n HTMLButtonElement,\n React.ComponentPropsWithoutRef<'span'> &\n VariantProps\n>(({ className, size, variant, ...props }, ref) => {\n return (\n e.stopPropagation()}\n role=\"button\"\n {...props}\n >\n \n \n );\n});\n\nToolbarSplitButton.displayName = 'ToolbarButton';\n\nexport const ToolbarToggleItem = withVariants(\n ToolbarPrimitive.ToggleItem,\n toolbarButtonVariants,\n ['variant', 'size']\n);\n\nexport const ToolbarGroup = withRef<'div'>(({ children, className }, ref) => {\n return (\n