diff --git a/packages/noya-compiler/src/astBuilders.ts b/packages/noya-compiler/src/astBuilders.ts index 1fc94ee2e..6c0a7d2ba 100644 --- a/packages/noya-compiler/src/astBuilders.ts +++ b/packages/noya-compiler/src/astBuilders.ts @@ -1,24 +1,16 @@ import { groupBy, unique } from '@noya-app/noya-utils'; -import { - DesignSystemDefinition, - Theme, - component, -} from '@noya-design-system/protocol'; -import { DSConfig } from 'noya-api'; -import React, { CSSProperties, ReactNode } from 'react'; +import { DesignSystemDefinition } from '@noya-design-system/protocol'; +import React, { ReactNode } from 'react'; import ts from 'typescript'; -import { clean } from './clean'; import { SimpleElement, SimpleElementTree, buildNamespaceMap, - createPassthrough, findElementNameAndSource, isPassthrough, isSimpleElement, simpleElement, } from './common'; -import { print } from './print'; import { isSafeForJsxText, isValidPropertyKey } from './validate'; export function createJsxElement( @@ -236,124 +228,7 @@ export function extractImports( ]; }); } -export function createLayoutSource({ - DesignSystem, - _noya, -}: { - DesignSystem: DesignSystemDefinition; - _noya: { theme: Theme; dsConfig: DSConfig }; -}): { - source: string; -} { - const cssImport = "import './globals.css'"; - - const defaultLayout = `${cssImport} - -export default function RootLayout({ children }: React.PropsWithChildren<{}>) { - return children; -} -`; - - const providerElement = DesignSystem.components[component.id.Provider] - ? DesignSystem.components[component.id.Provider]({ - theme: createPassthrough(ts.factory.createIdentifier('theme')), - children: createPassthrough( - ts.factory.createJsxExpression( - undefined, - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('props'), - ts.factory.createIdentifier('children'), - ), - ), - ), - ...(_noya && { _noya }), - }) - : null; - - const nextProviderElement = DesignSystem.components[component.id.NextProvider] - ? DesignSystem.components[component.id.NextProvider]({ - children: providerElement, - ...(_noya && { _noya }), - }) - : providerElement; - - if (!nextProviderElement) return { source: defaultLayout }; - - const layoutElement = createSimpleElement(nextProviderElement, DesignSystem); - - if (!layoutElement) return { source: defaultLayout }; - - const fonts = SimpleElementTree.reduce( - layoutElement, - (result, node) => { - if (!isSimpleElement(node)) return result; - - const style = node.props.style as CSSProperties | undefined; - const fontFamily = style?.fontFamily; - - if (!fontFamily) return result; - - delete style.fontFamily; - - node.props.className = createPassthrough( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('font' + fontFamily), - ts.factory.createIdentifier('className'), - ), - ); - return [...result, fontFamily]; - }, - [], - ); - - 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'", - [ - cssImport, - "import React from 'react'", - print(layoutImports), - ...(fonts.length > 0 - ? [`import { ${fonts.join(', ')} } from "next/font/google";`] - : []), - 'import { theme } from "./theme"', - ].join('\n'), - ...fonts.map( - (font) => `const font${font} = ${font}({ subsets: ["latin"] })`, - ), - print(layoutComponentFunc), - ] - .map(clean) - .join('\n'); - - return { - source: layoutSource, - }; -} export function createSimpleElement( originalElement: React.ReactNode, DesignSystem: DesignSystemDefinition, diff --git a/packages/noya-compiler/src/compileDesignSystem.ts b/packages/noya-compiler/src/compileDesignSystem.ts index b632ba49f..d3696ed82 100644 --- a/packages/noya-compiler/src/compileDesignSystem.ts +++ b/packages/noya-compiler/src/compileDesignSystem.ts @@ -1,5 +1,11 @@ import { tailwindColors } from '@noya-app/noya-tailwind'; -import { Theme } from '@noya-design-system/protocol'; +import { + DesignSystemDefinition, + Theme, + component, + transform, +} from '@noya-design-system/protocol'; +import { DSConfig } from 'noya-api'; import { FindComponent, NoyaComponent, @@ -8,12 +14,11 @@ import { createResolvedNode, renderResolvedNode, } from 'noya-component'; -import React, { ReactElement } from 'react'; +import React, { CSSProperties, ReactElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import ts from 'typescript'; import { createElementCode, - createLayoutSource, createReactComponentDeclaration, createSimpleElement, extractImports, @@ -25,6 +30,7 @@ import { createPassthrough, getComponentNameIdentifier, isPassthrough, + isSimpleElement, sortFiles, } from './common'; import { generateThemeFile } from './compileTheme'; @@ -44,6 +50,7 @@ export type ExportMap = Partial>>; export type CompileDesignSystemConfiguration = ResolvedCompilerConfiguration & { includeTailwindBase: boolean; + spreadTheme: boolean; exportTypes: ExportType[]; }; @@ -173,6 +180,7 @@ export function compileDesignSystem( const layoutSource = createLayoutSource({ DesignSystem, + spreadTheme: configuration.spreadTheme, _noya: createNoyaDSRenderingContext({ theme, dsConfig: configuration.ds.config, @@ -442,3 +450,128 @@ function createExport({ return exportMap; } + +export function createLayoutSource({ + DesignSystem, + _noya, + spreadTheme, +}: { + DesignSystem: DesignSystemDefinition; + _noya: { theme: Theme; dsConfig: DSConfig }; + spreadTheme: boolean; +}): { + source: string; +} { + const cssImport = "import './globals.css'"; + + const defaultLayout = `${cssImport} + +export default function RootLayout({ children }: React.PropsWithChildren<{}>) { + return children; +} +`; + + const providerElement = DesignSystem.components[component.id.Provider] + ? DesignSystem.components[component.id.Provider]({ + theme: createPassthrough( + spreadTheme + ? transform({ theme: _noya.theme }, DesignSystem.themeTransformer) + : ts.factory.createIdentifier('theme'), + ), + children: createPassthrough( + ts.factory.createJsxExpression( + undefined, + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('props'), + ts.factory.createIdentifier('children'), + ), + ), + ), + ...(_noya && { _noya }), + }) + : null; + + const nextProviderElement = DesignSystem.components[component.id.NextProvider] + ? DesignSystem.components[component.id.NextProvider]({ + children: providerElement, + ...(_noya && { _noya }), + }) + : providerElement; + + if (!nextProviderElement) return { source: defaultLayout }; + + const layoutElement = createSimpleElement(nextProviderElement, DesignSystem); + + if (!layoutElement) return { source: defaultLayout }; + + const fonts = SimpleElementTree.reduce( + layoutElement, + (result, node) => { + if (!isSimpleElement(node)) return result; + + const style = node.props.style as CSSProperties | undefined; + const fontFamily = style?.fontFamily; + + if (!fontFamily) return result; + + delete style.fontFamily; + + node.props.className = createPassthrough( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('font' + fontFamily), + ts.factory.createIdentifier('className'), + ), + ); + + return [...result, fontFamily]; + }, + [], + ); + + 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'", + [ + cssImport, + "import React from 'react'", + print(layoutImports), + ...(fonts.length > 0 + ? [`import { ${fonts.join(', ')} } from "next/font/google";`] + : []), + ...(spreadTheme ? [] : ['import { theme } from "./theme"']), + ].join('\n'), + ...fonts.map( + (font) => `const font${font} = ${font}({ subsets: ["latin"] })`, + ), + print(layoutComponentFunc), + ] + .map(clean) + .join('\n'); + + return { + source: layoutSource, + }; +} diff --git a/packages/noya-compiler/src/compileProject.ts b/packages/noya-compiler/src/compileProject.ts index dbc403821..b16e7ae78 100644 --- a/packages/noya-compiler/src/compileProject.ts +++ b/packages/noya-compiler/src/compileProject.ts @@ -64,6 +64,7 @@ export function compile(configuration: CompilerConfiguration) { designSystemDefinition: configuration.resolvedDefinitions[libraryName], includeTailwindBase: libraryName === 'vanilla', + spreadTheme: libraryName.endsWith('radix'), exportTypes: libraryName === 'vanilla' ? [ @@ -73,7 +74,7 @@ export function compile(configuration: CompilerConfiguration) { 'react-css-modules', 'react-tailwind', ] - : libraryName.endsWith('chakra') + : libraryName.endsWith('chakra') || libraryName.endsWith('radix') ? ['react'] : ['react-css', 'react-tailwind'], }); diff --git a/packages/noya-module-loader/src/ThumbnailDesignSystem.tsx b/packages/noya-module-loader/src/ThumbnailDesignSystem.tsx index bb5d9a586..89ad5e3d9 100644 --- a/packages/noya-module-loader/src/ThumbnailDesignSystem.tsx +++ b/packages/noya-module-loader/src/ThumbnailDesignSystem.tsx @@ -28,13 +28,12 @@ function getTheme(props: any) { * This returns the wrapper span around the string child */ function getStringChild(props: any): ReactElement | undefined { - if ( - props.children && - Array.isArray(props.children) && - props.children.length === 1 && - props.children[0].key === 'editable-span' - ) { - return props.children[0]; + if (props.children && React.Children.count(props.children) === 1) { + const child = React.Children.only(props.children); + + if (child.key === 'editable-span') { + return child; + } } return undefined; diff --git a/packages/site/src/ayon/components/inspector/DesignSystemPicker.tsx b/packages/site/src/ayon/components/inspector/DesignSystemPicker.tsx index aa4df8b96..4969caf88 100644 --- a/packages/site/src/ayon/components/inspector/DesignSystemPicker.tsx +++ b/packages/site/src/ayon/components/inspector/DesignSystemPicker.tsx @@ -16,6 +16,7 @@ const designSystems = { '@noya-design-system/mui': 'Material Design', '@noya-design-system/antd': 'Ant Design', '@noya-design-system/chakra': 'Chakra UI', + '@noya-design-system/radix': 'Radix Themes', }; export function DesignSystemPicker() { diff --git a/packages/site/src/dseditor/DSComponentInspector.tsx b/packages/site/src/dseditor/DSComponentInspector.tsx index 5118896b4..b2e243122 100644 --- a/packages/site/src/dseditor/DSComponentInspector.tsx +++ b/packages/site/src/dseditor/DSComponentInspector.tsx @@ -79,7 +79,11 @@ import { enforceSchema, enforceSchemaInDiff } from './layoutSchema'; type Props = Pick< ComponentProps, - 'highlightedPath' | 'setHighlightedPath' | 'selectedPath' | 'setSelectedPath' + | 'highlightedPath' + | 'setHighlightedPath' + | 'selectedPath' + | 'setSelectedPath' + | 'dsConfig' > & { selection: SelectedComponent; setSelection: (selection: SelectedComponent) => void; @@ -122,6 +126,7 @@ export function DSComponentInspector({ onPressMeasure, groups, uploadAsset, + dsConfig, }: Props) { const { query } = useRouter(); const openInputDialog = useOpenInputDialog(); @@ -755,6 +760,7 @@ export function DSComponentInspector({ } > diff --git a/packages/site/src/dseditor/DSLayoutTree.tsx b/packages/site/src/dseditor/DSLayoutTree.tsx index 5b1a7f2e6..3834a75b0 100644 --- a/packages/site/src/dseditor/DSLayoutTree.tsx +++ b/packages/site/src/dseditor/DSLayoutTree.tsx @@ -18,6 +18,7 @@ import { ClipboardCopyIcon, ClipboardIcon, CopyIcon, + DownloadIcon, DropdownMenuIcon, EnterIcon, ExitIcon, @@ -37,22 +38,25 @@ import { VercelLogoIcon, } from '@noya-app/noya-icons'; import { useKeyboardShortcuts } from '@noya-app/noya-keymap'; +import { tailwindColors } from '@noya-app/noya-tailwind'; import { isDeepEqual, uuid } from '@noya-app/noya-utils'; -import { component } from '@noya-design-system/protocol'; +import { Theme, component } from '@noya-design-system/protocol'; import { fileOpen } from 'browser-fs-access'; import cloneDeep from 'lodash/cloneDeep'; import { useRouter } from 'next/router'; -import { useNoyaClientOrFallback } from 'noya-api'; +import { DS, DSConfig, useNoyaClientOrFallback } from 'noya-api'; import { FindComponent, Model, NoyaComponent, + NoyaGeneratorProp, NoyaPrimitiveElement, NoyaProp, NoyaResolvedNode, NoyaResolvedPrimitiveElement, ResolvedHierarchy, createResolvedNode, + createSVG, getComponentName, getNodeName, randomSeed, @@ -73,6 +77,7 @@ import React, { import { IndexPath } from 'tree-visit'; import { z } from 'zod'; import { DraggableMenuButton } from '../ayon/components/inspector/DraggableMenuButton'; +import { downloadBlob } from '../utils/download'; import { StyleInputField } from './StyleInputField'; import { typeItems } from './completionItems'; import { exportLayout, parseLayoutWithOptions } from './componentLayout'; @@ -119,6 +124,7 @@ function flattenResolvedNode( } interface Props { + dsConfig?: DSConfig; onChange: (resolvedNode: NoyaResolvedNode) => void; findComponent: FindComponent; resolvedNode: NoyaResolvedNode; @@ -149,6 +155,7 @@ export const DSLayoutTree = memo(function DSLayoutTree({ components, onConfigureProp, uploadAsset, + dsConfig, }: Props) { const [expanded, setExpanded] = useState>({}); const handleSetExpanded = useCallback((id: string, expanded: boolean) => { @@ -363,6 +370,7 @@ export const DSLayoutTree = memo(function DSLayoutTree({ onPressDown={() => handlePressDirection(indexPath, 'down')} onPressUp={() => handlePressDirection(indexPath, 'up')} uploadAsset={uploadAsset} + dsConfig={dsConfig} /> )} /> @@ -400,6 +408,7 @@ export const DSLayoutRow = memo( onPressDown, focusPath, uploadAsset, + dsConfig, }: Pick< Props, | 'highlightedPath' @@ -426,7 +435,7 @@ export const DSLayoutRow = memo( onPressUp: () => void; onPressDown: () => void; focusPath: (path?: string[]) => void; - } & Pick, + } & Pick, forwardedRef: React.ForwardedRef, ) { const client = useNoyaClientOrFallback(); @@ -1350,6 +1359,16 @@ export const DSLayoutRow = memo( title: 'Configure', icon: , }, + prop.generator === 'geometric' && { + value: 'exportSVG', + title: 'Export SVG', + icon: , + }, + prop.generator === 'geometric' && { + value: 'exportPNG', + title: 'Export PNG', + icon: , + }, ], [ { @@ -1389,6 +1408,37 @@ export const DSLayoutRow = memo( prop: prop.name, }); break; + case 'exportSVG': { + if (!dsConfig || prop.generator !== 'geometric') + return; + + const svgBlob = createSVGBlob(prop, dsConfig); + downloadBlob(svgBlob, 'pattern.svg'); + break; + } + case 'exportPNG': { + if (!dsConfig || prop.generator !== 'geometric') + return; + + const svgBlob = createSVGBlob(prop, dsConfig); + const svgUrl = URL.createObjectURL(svgBlob); + + // Render svg on canvas + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const img = new Image(); + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + ctx?.drawImage(img, 0, 0); + canvas.toBlob((blob) => { + if (!blob) return; + downloadBlob(blob, 'pattern.png'); + }); + }; + img.src = svgUrl; + break; + } case 'fetch': { switch (prop.generator) { case 'random-image': @@ -1718,3 +1768,19 @@ function handleMoveItem( return inner(); } + +function createSVGBlob(prop: NoyaGeneratorProp, dsConfig: DS['config']) { + const theme: Theme = { + colorMode: dsConfig.colorMode ?? 'light', + colors: { + primary: (tailwindColors as any)[dsConfig.colors.primary], + neutral: tailwindColors.slate, + }, + }; + + const pattern = createSVG(prop.data, theme.colors); + const blob = new Blob([pattern], { + type: 'image/svg+xml', + }); + return blob; +} diff --git a/packages/site/src/dseditor/DSProjectInspector.tsx b/packages/site/src/dseditor/DSProjectInspector.tsx index 0ecb721c0..59d27552d 100644 --- a/packages/site/src/dseditor/DSProjectInspector.tsx +++ b/packages/site/src/dseditor/DSProjectInspector.tsx @@ -51,6 +51,7 @@ const designSystems = { '@noya-design-system/chakra': 'Chakra UI', '@noya-design-system/antd': 'Ant Design', '@noya-design-system/mui': 'Material Design', + '@noya-design-system/radix': 'Radix Themes', }; const noop = () => {};