diff --git a/packages/noya-compiler/src/index.ts b/packages/noya-compiler/src/index.ts index 2272bfc67..c3a981754 100644 --- a/packages/noya-compiler/src/index.ts +++ b/packages/noya-compiler/src/index.ts @@ -103,7 +103,6 @@ export interface CompilerConfiguration { name: string; ds: DS; designSystemDefinition: DesignSystemDefinition; - target: 'standalone' | 'codesandbox'; } function findElementNameAndSource( @@ -208,7 +207,7 @@ export function createSimpleElement( export function createReactComponentDeclaration( name: string, - resolvedElement: SimpleElement, + returnValue: ts.Expression, params: ts.ParameterDeclaration[] = [], ) { return ts.factory.createFunctionDeclaration( @@ -221,9 +220,7 @@ export function createReactComponentDeclaration( undefined, params, undefined, - ts.factory.createBlock([ - ts.factory.createReturnStatement(createElementCode(resolvedElement)), - ]), + ts.factory.createBlock([ts.factory.createReturnStatement(returnValue)]), ); } @@ -325,6 +322,80 @@ function extractImports( }); } +function createLayoutSource(DesignSystem: DesignSystemDefinition) { + const providerElement = DesignSystem.components[component.id.Provider] + ? DesignSystem.createElement( + DesignSystem.components[component.id.Provider], + { + theme: createPassthrough(ts.factory.createIdentifier('theme')), + }, + createPassthrough( + ts.factory.createJsxExpression( + undefined, + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('props'), + ts.factory.createIdentifier('children'), + ), + ), + ), + ) + : null; + + const nextProviderElement = DesignSystem.components[component.id.NextProvider] + ? DesignSystem.createElement( + DesignSystem.components[component.id.NextProvider], + {}, + providerElement, + ) + : providerElement; + + if (!nextProviderElement) return; + + const layoutElement = createSimpleElement(nextProviderElement, DesignSystem); + + if (!layoutElement) return; + + const layoutComponentFunc = createReactComponentDeclaration( + 'NextProvider', + createElementCode(layoutElement), + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier('props'), + undefined, + // Type React.PropsWithChildren<{}> + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier('React.PropsWithChildren'), + [ + // Empty object type + ts.factory.createTypeLiteralNode([]), + ], + ), + undefined, + ), + ], + ); + + const layoutImports = extractImports(layoutElement, DesignSystem); + + const layoutSource = [ + "'use client'", + [ + "import React from 'react'", + print(layoutImports), + 'import { theme } from "./theme"', + ].join('\n'), + print(layoutComponentFunc), + ] + .map(clean) + .join('\n'); + + return { + source: layoutSource, + }; +} + export function compile(configuration: CompilerConfiguration) { const DesignSystem = configuration.designSystemDefinition; @@ -368,7 +439,7 @@ export function compile(configuration: CompilerConfiguration) { const func = createReactComponentDeclaration( getComponentNameIdentifier(component.name), - simpleElement, + createElementCode(simpleElement), ); const dependencies = (DesignSystem.imports ?? []).reduce( @@ -412,63 +483,7 @@ export function compile(configuration: CompilerConfiguration) { {}, ); - const layoutElement = createSimpleElement( - DesignSystem.createElement( - DesignSystem.components[component.id.NextProvider], - {}, - DesignSystem.createElement( - DesignSystem.components[component.id.Provider], - { - theme: createPassthrough(ts.factory.createIdentifier('theme')), - }, - createPassthrough( - ts.factory.createJsxExpression( - undefined, - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('props'), - ts.factory.createIdentifier('children'), - ), - ), - ), - ), - ), - DesignSystem, - ); - const layoutComponentFunc = createReactComponentDeclaration( - 'NextProvider', - layoutElement!, - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier('props'), - undefined, - // Type React.PropsWithChildren<{}> - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('React.PropsWithChildren'), - [ - // Empty object type - ts.factory.createTypeLiteralNode([]), - ], - ), - undefined, - ), - ], - ); - - const layoutImports = extractImports(layoutElement!, DesignSystem); - - const layoutSource = [ - "'use client'", - [ - "import React from 'react'", - print(layoutImports), - 'import { theme } from "./theme"', - ].join('\n'), - print(layoutComponentFunc), - ] - .map(clean) - .join('\n'); + const layoutSource = createLayoutSource(DesignSystem); const theme: Theme = { colorMode: configuration.ds.config.colorMode ?? 'light', @@ -480,6 +495,14 @@ export function compile(configuration: CompilerConfiguration) { const themeFile = generateThemeFile(DesignSystem, { theme }); + const includeTailwindBase = configuration.ds.source.name === 'vanilla'; + + const tailwindLayers = [ + ...(includeTailwindBase ? ['base'] : []), + 'components', + 'utilities', + ]; + const files = { 'src/app/layout.tsx': `import './globals.css' @@ -495,10 +518,9 @@ export default function RootLayout({ ) } `, - 'src/app/globals.css': `@tailwind base; -@tailwind components; -@tailwind utilities; -`, + 'src/app/globals.css': tailwindLayers + .map((layer) => `@tailwind ${layer};`) + .join('\n'), ...Object.fromEntries( componentPageItems.map(({ name, source }) => [ `src/app/components/${getComponentNameIdentifier( @@ -509,7 +531,9 @@ export default function RootLayout({ ]), ), 'src/app/components/theme.ts': themeFile, - 'src/app/components/layout.tsx': layoutSource, + ...(layoutSource + ? { 'src/app/components/layout.tsx': layoutSource.source } + : {}), 'package.json': JSON.stringify( { name: sanitizePackageName(configuration.name), diff --git a/packages/noya-component/src/renderResolvedNode.tsx b/packages/noya-component/src/renderResolvedNode.tsx index 339b54f82..4205a0180 100644 --- a/packages/noya-component/src/renderResolvedNode.tsx +++ b/packages/noya-component/src/renderResolvedNode.tsx @@ -63,7 +63,7 @@ export function renderResolvedNode({ dsConfig, system, stylingMode = 'inline', - theme, + noya, }: { containerWidth?: number; contentEditable: boolean; @@ -73,7 +73,7 @@ export function renderResolvedNode({ dsConfig: DSConfig; system: DesignSystemDefinition; stylingMode?: StylingMode; - theme?: any; // Passed into components as _theme + noya?: any; // Passed into components as _noya }) { return ResolvedHierarchy.map( resolvedNode, @@ -266,7 +266,7 @@ export function renderResolvedNode({ element.componentID === component.id.Radio) && { label: transformedChildren, })} - {...(theme && { _theme: theme })} + {...(noya && { _noya: noya })} /> ); }, diff --git a/packages/noya-module-loader/src/ThumbnailDesignSystem.tsx b/packages/noya-module-loader/src/ThumbnailDesignSystem.tsx index d48185805..e54764342 100644 --- a/packages/noya-module-loader/src/ThumbnailDesignSystem.tsx +++ b/packages/noya-module-loader/src/ThumbnailDesignSystem.tsx @@ -19,7 +19,7 @@ const handler = { }; function getTheme(props: any) { - if (props._theme) return props._theme as Theme; + if (props._noya && props._noya.theme) return props._noya.theme as Theme; return undefined; } diff --git a/packages/noya-module-loader/src/VanillaDesignSystem.tsx b/packages/noya-module-loader/src/VanillaDesignSystem.tsx new file mode 100644 index 000000000..03fcef703 --- /dev/null +++ b/packages/noya-module-loader/src/VanillaDesignSystem.tsx @@ -0,0 +1,246 @@ +/* eslint-disable jsx-a11y/anchor-has-content */ +/* eslint-disable jsx-a11y/alt-text */ +import { + ButtonVariant, + DesignSystemDefinition, + Theme, + applyCommonProps, + component, + x, +} from '@noya-design-system/protocol'; +import { DSConfig } from 'noya-api'; +import { filterTailwindClassesByLastInGroup } from 'noya-tailwind'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +const handler = { + get(target: any, property: string | symbol): any { + if (property in target) return target[property]; + + return (props: any) => { + return
; + }; + }, +}; + +function getTheme(props: any) { + if (props._noya && props._noya.theme) return props._noya.theme as Theme; + return undefined; +} + +function getDSConfig(props: any): DSConfig { + if (props._noya && props._noya.dsConfig) return props._noya.dsConfig; + return { colorMode: 'light', colors: { primary: 'blue' } }; +} + +function parseClasses(classes: string | undefined) { + return classes?.split(/\s+/) ?? []; +} + +function mergeClasses( + classes: (string | false)[], + className: string | undefined, +) { + return filterTailwindClassesByLastInGroup( + [...classes, ...parseClasses(className)].filter( + (x): x is string => typeof x === 'string', + ), + ).join(' '); +} + +const proxyObject = new Proxy( + { + [component.id.Box]: (props: any) => { + return
; + }, + [component.id.Card]: (props: any) => { + const className = mergeClasses( + [`rounded-lg`, `bg-white`, `border`, `border-gray-200`, `p-4`], + props.className, + ); + + return
; + }, + [component.id.Button]: (props: any) => { + const config = getDSConfig(props); + const variant = (props.variant as ButtonVariant) ?? 'solid'; + + const className = mergeClasses( + [ + `appearance-none`, + ...(variant === 'solid' + ? [ + `bg-${config.colors.primary}-500`, + `hover:bg-${config.colors.primary}-400`, + `text-white`, + `shadow-sm`, + ] + : []), + ...(variant === 'outline' + ? [ + `border`, + `border-${config.colors.primary}-500`, + `text-${config.colors.primary}-500`, + `hover:bg-${config.colors.primary}-50`, + `hover:text-${config.colors.primary}-700`, + ] + : []), + ...(variant === 'text' + ? [ + `text-${config.colors.primary}-500`, + `hover:bg-${config.colors.primary}-50`, + `hover:text-${config.colors.primary}-700`, + ] + : []), + `rounded-md`, + `px-3.5`, + `py-2.5`, + `text-sm`, + `font-semibold`, + ], + props.className, + ); + + return