├── .eslintrc.cjs ├── .gitignore ├── LICENSE ├── README.md ├── app ├── components │ ├── canvas.tsx │ ├── code-view.tsx │ ├── control-panel.tsx │ ├── header.tsx │ ├── hero.tsx │ ├── node │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── components-map.tsx │ │ ├── connector.tsx │ │ └── layout.tsx │ ├── react-iframe.tsx │ ├── render-node.tsx │ ├── settings-control.tsx │ ├── side-menu.tsx │ ├── ui │ │ ├── accordion.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── navigation-menu.tsx │ │ ├── tabs.tsx │ │ ├── tooltip.tsx │ │ └── vertical-navigation-menu.tsx │ └── viewport.tsx ├── entry.client.tsx ├── entry.server.tsx ├── lib │ ├── client-hints.tsx │ ├── code-gen.tsx │ ├── theme.server.ts │ ├── tw-classes.ts │ └── utils.ts ├── root.tsx ├── routes │ ├── _index.tsx │ └── resources.theme-toggle.tsx └── tailwind.css ├── components.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── favicon.ico ├── remix.config.js ├── remix.env.d.ts ├── tailwind.config.js └── tsconfig.json /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rajesh Babu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remix Craft.js Starter 2 | 3 | - Watch intro and architecure video on [YT channel](https://youtu.be/INNjkgE5p0o) 4 | 5 | - A simple application to generate code from a drag-drop website builder tool using shadcn-ui components. 6 | 7 | Screenshot 2024-02-17 at 17 57 20 8 | 9 | 10 | ## Development 11 | 12 | From your terminal: 13 | 14 | ```sh 15 | npm run dev 16 | ``` 17 | 18 | This starts your app in development mode, rebuilding assets on file changes. 19 | 20 | ## Deployment 21 | 22 | First, build your app for production: 23 | 24 | ```sh 25 | npm run build 26 | ``` 27 | 28 | Then run the app in production mode: 29 | 30 | ```sh 31 | npm start 32 | ``` 33 | 34 | Now you'll need to pick a host to deploy it to. 35 | 36 | ### DIY 37 | 38 | If you're familiar with deploying node applications, the built-in Remix app server is production-ready. 39 | 40 | Make sure to deploy the output of `remix build` 41 | 42 | - `build/` 43 | - `public/build/` 44 | -------------------------------------------------------------------------------- /app/components/canvas.tsx: -------------------------------------------------------------------------------- 1 | import { useEditor, useNode } from '@craftjs/core'; 2 | import { MonitorPlay, Smartphone, Code, Redo, Undo } from 'lucide-react'; 3 | import React, { useState } from 'react'; 4 | import { getOutputCode, getOutputHTMLFromId } from '~/lib/code-gen'; 5 | import { CodeView } from './code-view'; 6 | import { DrawerTrigger, DrawerContent, Drawer } from './ui/drawer'; 7 | 8 | type CanvasProps = { 9 | children: React.ReactNode; 10 | }; 11 | 12 | export const Canvas = ({ children }: CanvasProps) => { 13 | const { 14 | connectors: { connect, drag }, 15 | } = useNode(); 16 | const [canvasWidth, setCanvasWidth] = useState('w-[100%]'); 17 | const { canUndo, canRedo, actions, query } = useEditor((state, query) => ({ 18 | canUndo: query.history.canUndo(), 19 | canRedo: query.history.canRedo(), 20 | })); 21 | const [output, setOutput] = useState(); 22 | const [htmlOutput, setHtmlOutput] = useState(); 23 | 24 | const generateCode = () => { 25 | const { importString, output } = getOutputCode(query.getNodes()); 26 | 27 | console.log('printing ', importString, output); 28 | 29 | setOutput(`${importString}\n\n${output}`); 30 | }; 31 | 32 | const generateHTML = () => { 33 | const htmlOutput = getOutputHTMLFromId('canvas-iframe'); 34 | 35 | setHtmlOutput(htmlOutput); 36 | }; 37 | 38 | const [open, setOpen] = useState(false); 39 | const [htmlOpen, setHtmlOpen] = useState(false); 40 | 41 | const handleIconClick = (newWidth: any) => { 42 | setCanvasWidth(newWidth); 43 | }; 44 | 45 | return ( 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | { 58 | generateCode(); 59 | setOpen(value); 60 | }} 61 | > 62 | 63 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | {/* { 77 | generateHTML(); 78 | setHtmlOpen(value); 79 | }} 80 | > 81 | 82 | 87 | 88 | 89 | 90 | 91 | 92 | */} 93 |
94 | 95 |
96 |
97 |
98 | {canUndo && ( 99 | { 104 | actions.history.undo(); 105 | }} 106 | /> 107 | )} 108 |
109 |
110 | {canRedo && ( 111 | { 116 | actions.history.redo(); 117 | }} 118 | /> 119 | )} 120 |
121 |
122 |
123 |
124 | 125 |
{ 128 | if (ref) { 129 | connect(drag(ref)); 130 | } 131 | }} 132 | > 133 | {children} 134 |
135 |
136 |
137 | ); 138 | }; 139 | 140 | Canvas.craft = { 141 | displayName: 'div', 142 | props: { 143 | className: 'w-full h-full', 144 | }, 145 | }; 146 | -------------------------------------------------------------------------------- /app/components/code-view.tsx: -------------------------------------------------------------------------------- 1 | import { CopyBlock, dracula } from 'react-code-blocks'; 2 | 3 | export const CodeView = ({ codeString }: { codeString?: string }) => { 4 | const code = codeString 5 | ? codeString 6 | : // Initial code or a placeholder 7 | ` 8 | import React from "react";\n\nconst Card = () => {\n // Dummy data for the card\n const title = "Example Card";\n const description = "This is a simple card component in React with Tailwind CSS.";\n const imageUrl = "https://example.com/example-image.jpg";\n\n return (\n
\n {title}\n
\n
{title}
\n 9 |
{title}
\n 10 |
{title}
\n 11 |
{title}
\n 12 | 13 |

{description}

\n
\n
\n );\n};\n\nexport default Card 14 | `; 15 | 16 | return ( 17 |
18 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /app/components/control-panel.tsx: -------------------------------------------------------------------------------- 1 | import { useEditor } from "@craftjs/core"; 2 | import React from "react"; 3 | 4 | export const ControlPanel = () => { 5 | const { active, related } = useEditor((state, query) => { 6 | // TODO: handle multiple selected elements 7 | const currentlySelectedNodeId = query.getEvent("selected").first(); 8 | return { 9 | active: currentlySelectedNodeId, 10 | related: 11 | currentlySelectedNodeId && state.nodes[currentlySelectedNodeId].related, 12 | }; 13 | }); 14 | 15 | return ( 16 |
17 |

18 | Control Panel 19 |

20 | {active && related.toolbar && React.createElement(related.toolbar)} 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /app/components/header.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeToggle } from '~/routes/resources.theme-toggle'; 2 | import { Command } from 'lucide-react'; 3 | import { Link } from '@remix-run/react'; 4 | import { Drawer, DrawerContent, DrawerTrigger } from '~/components/ui/drawer'; 5 | import { Button } from './ui/button'; 6 | import { CodeView } from './code-view'; 7 | 8 | export const Header = () => { 9 | return ( 10 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /app/components/hero.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@remix-run/react"; 2 | import { Button } from "./ui/button"; 3 | import { Card, CardContent } from "./ui/card"; 4 | 5 | export const Hero = () => { 6 | return ( 7 |
8 |
9 |

10 | A{" "} 11 | 12 | Simple Starter 13 | {" "} 14 | For Remix and Shadcn-ui 15 |

16 | 17 |

18 | With optimistic dark-mode 19 |

20 | 21 | 22 | 23 | 28 | 29 | 30 |
31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /app/components/node/button.tsx: -------------------------------------------------------------------------------- 1 | import { withNode } from '~/components/node/connector'; 2 | import { Button } from '../ui/button'; 3 | import { SettingsControl } from '../settings-control'; 4 | 5 | const draggable = true; 6 | 7 | export const NodeButton = withNode(Button, { 8 | draggable, 9 | }); 10 | 11 | NodeButton.craft = { 12 | ...NodeButton.craft, 13 | related: { 14 | toolbar: SettingsControl, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /app/components/node/card.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardHeader, 4 | CardFooter, 5 | CardTitle, 6 | CardDescription, 7 | CardContent, 8 | } from '../ui/card'; 9 | import { Element } from '@craftjs/core'; 10 | import { SettingsControl } from '../settings-control'; 11 | import { withNode } from './connector'; 12 | import { NodeButton } from './button'; 13 | 14 | interface NodeCardProps extends React.HTMLAttributes {} 15 | 16 | const draggable = true; 17 | const droppable = true; // Can drop items into to this component 18 | 19 | export const NodeCardHeader = withNode(CardHeader, { 20 | droppable, 21 | }); 22 | 23 | export const NodeCardTitle = withNode(CardTitle, { 24 | draggable, 25 | droppable, 26 | }); 27 | 28 | NodeCardTitle.craft = { 29 | ...NodeCardTitle.craft, 30 | related: { 31 | toolbar: SettingsControl, 32 | }, 33 | }; 34 | 35 | export const NodeCardDescription = withNode(CardDescription, { 36 | draggable, 37 | droppable, 38 | }); 39 | 40 | NodeCardDescription.craft = { 41 | ...NodeCardDescription.craft, 42 | related: { 43 | toolbar: SettingsControl, 44 | }, 45 | }; 46 | 47 | export const NodeCardContent = withNode(CardContent, { 48 | droppable, 49 | }); 50 | 51 | export const NodeCardFooter = withNode(CardFooter, { 52 | droppable, 53 | }); 54 | 55 | export const NodeCardContainer = withNode(Card, { 56 | draggable, 57 | droppable, 58 | }); 59 | 60 | export const NodeCard = ({ ...props }: NodeCardProps) => { 61 | return ( 62 | 63 | 68 | Card Title 69 | Card Description 70 | 71 | 76 | 81 | Footer button 82 | 83 | 84 | ); 85 | }; 86 | 87 | NodeCard.craft = { 88 | ...NodeCard.craft, 89 | displayName: 'Card', 90 | props: { 91 | className: 'p-6 m-2', 92 | }, 93 | custom: { 94 | importPath: '@/components/card', 95 | }, 96 | related: { 97 | toolbar: SettingsControl, 98 | }, 99 | }; 100 | -------------------------------------------------------------------------------- /app/components/node/components-map.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, ReactNode } from 'react'; 2 | import { Button } from '../ui/button'; 3 | import { 4 | Card, 5 | CardHeader, 6 | CardTitle, 7 | CardDescription, 8 | CardContent, 9 | CardFooter, 10 | } from '../ui/card'; 11 | import { OneBlock, NodeOneBlock, NodeTwoBlocks } from './layout'; 12 | import { NodeButton } from './button'; 13 | import { NodeCard } from './card'; 14 | import { Element } from '@craftjs/core'; 15 | 16 | export type Components = { 17 | name: string; 18 | items: { 19 | name: string; 20 | props?: { 21 | variant?: 22 | | 'link' 23 | | 'default' 24 | | 'destructive' 25 | | 'outline' 26 | | 'secondary' 27 | | 'ghost' 28 | | null 29 | | undefined; 30 | className?: string; 31 | children?: ReactNode | string; 32 | }; 33 | node: ReactElement; 34 | demo?: ReactNode; 35 | }[]; 36 | }; 37 | 38 | export const componentsMap: Components[] = [ 39 | { 40 | name: 'Buttons', 41 | items: [ 42 | { 43 | name: 'Default', 44 | demo: , 45 | node: Default, 46 | }, 47 | { 48 | name: 'Outline', 49 | props: { variant: 'outline', children: 'Outline' }, 50 | demo: , 51 | node: Outline, 52 | }, 53 | { 54 | name: 'Destructive', 55 | props: { variant: 'destructive', children: 'Destructive' }, 56 | demo: , 57 | node: Destructive, 58 | }, 59 | ], 60 | }, 61 | { 62 | name: 'Cards', 63 | items: [ 64 | { 65 | name: 'Default', 66 | demo: ( 67 | 68 | 69 | Card Title 70 | Card Description 71 | 72 | Empty Container 73 | 74 | 75 | 76 | 77 | ), 78 | node: , 79 | }, 80 | ], 81 | }, 82 | { 83 | name: 'Layout', 84 | items: [ 85 | { 86 | name: 'One Block', 87 | demo: ( 88 | 89 | One Block 90 | 91 | ), 92 | node: ( 93 | 98 | ), 99 | }, 100 | { 101 | name: 'Two Blocks', 102 | demo: ( 103 | 104 | 105 | First Block 106 | 107 | 108 | Second Block 109 | 110 | 111 | ), 112 | node: , 113 | }, 114 | ], 115 | }, 116 | ]; 117 | -------------------------------------------------------------------------------- /app/components/node/connector.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react'; 2 | import React, { forwardRef } from 'react'; 3 | import { useEditor, useNode } from '@craftjs/core'; 4 | 5 | const BUTTON_PATH = '@/components/button'; 6 | const CARD_PATH = '@/components/card'; 7 | 8 | const importPathMap: { [key: string]: string } = { 9 | button: BUTTON_PATH, 10 | card: CARD_PATH, 11 | cardheader: CARD_PATH, 12 | cardcontent: CARD_PATH, 13 | cardfooter: CARD_PATH, 14 | cardtitle: CARD_PATH, 15 | carddescription: CARD_PATH, 16 | }; 17 | 18 | export const withNode = ( 19 | Component: React.ComponentType, 20 | { draggable = true, droppable = true } = {} 21 | ) => { 22 | // Wrap the returned component with forwardRef 23 | const WithNode = forwardRef>( 24 | (props, ref) => { 25 | const { 26 | id, 27 | connectors: { connect, drag }, 28 | } = useNode(); 29 | 30 | const { isActive } = useEditor((_, query) => ({ 31 | isActive: query.getEvent('selected').contains(id), 32 | })); 33 | 34 | const applyRef = (node: HTMLElement) => { 35 | if (node) { 36 | if (draggable && droppable) { 37 | connect(drag(node)); 38 | } else if (droppable) { 39 | connect(node); 40 | } else if (draggable) { 41 | drag(node); 42 | } 43 | // Forward the ref 44 | if (typeof ref === 'function') { 45 | ref(node); 46 | } else if (ref) { 47 | ref.current = node; 48 | } 49 | } 50 | }; 51 | 52 | return ( 53 | 60 | {typeof props.children === 'string' && 61 | props.children.trim() === '' ? ( 62 | <>Empty text 63 | ) : ( 64 | props.children || ( 65 |
66 | Empty container 67 |
68 | ) 69 | )} 70 |
71 | ); 72 | } 73 | ); 74 | 75 | console.log('Component.displayName ', Component.displayName); 76 | 77 | WithNode.displayName = `WithNode(${Component.displayName})`; 78 | 79 | const importPathMapKey = Component.displayName?.toLowerCase(); 80 | console.log( 81 | 'importPathMapKey ', 82 | importPathMapKey, 83 | importPathMapKey && importPathMap[importPathMapKey] 84 | ); 85 | 86 | WithNode.craft = { 87 | displayName: Component.displayName, 88 | custom: { 89 | importPath: importPathMapKey ? importPathMap[importPathMapKey] || '' : '', 90 | }, 91 | }; 92 | 93 | return WithNode; 94 | }; 95 | -------------------------------------------------------------------------------- /app/components/node/layout.tsx: -------------------------------------------------------------------------------- 1 | import { withNode } from '~/components/node/connector'; 2 | import { SettingsControl } from '../settings-control'; 3 | import React from 'react'; 4 | import { Element } from '@craftjs/core'; 5 | 6 | const draggable = true; 7 | const droppable = true; 8 | 9 | interface OneBlockProps extends React.HTMLAttributes {} 10 | 11 | export const OneBlock = React.forwardRef( 12 | ({ ...props }, ref) => { 13 | const Comp = 'div'; 14 | return ; 15 | } 16 | ); 17 | 18 | OneBlock.displayName = 'div'; 19 | 20 | export const NodeOneBlock = withNode(OneBlock, { 21 | draggable, 22 | droppable, 23 | }); 24 | 25 | interface NodeTwoBlocksProps extends React.HTMLAttributes {} 26 | 27 | export const NodeTwoBlocks = ({ ...props }: NodeTwoBlocksProps) => { 28 | return ( 29 | 30 | 35 | 40 | 41 | ); 42 | }; 43 | 44 | NodeTwoBlocks.craft = { 45 | displayName: 'div', 46 | props: { 47 | className: 'flex flex-row m-2 p-4', 48 | }, 49 | related: { 50 | toolbar: SettingsControl, 51 | }, 52 | }; 53 | 54 | NodeOneBlock.craft = { 55 | ...NodeOneBlock.craft, 56 | props: { 57 | className: 'w-full', 58 | }, 59 | related: { 60 | toolbar: SettingsControl, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /app/components/react-iframe.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode, IframeHTMLAttributes } from 'react'; 2 | import { useState, useCallback, useLayoutEffect } from 'react'; 3 | import { createPortal } from 'react-dom'; 4 | 5 | interface ReactIframeProps extends IframeHTMLAttributes { 6 | children: ReactNode; 7 | title: string; 8 | } 9 | 10 | export const ReactIframe = ({ 11 | children, 12 | title, 13 | ...props 14 | }: ReactIframeProps) => { 15 | const [contentRef, setContentRef] = useState(null); 16 | const mountNode = contentRef?.contentWindow?.document?.body; 17 | const iframeDoc = contentRef?.contentWindow?.document; 18 | 19 | useLayoutEffect(() => { 20 | if (iframeDoc) { 21 | // Clone and append all style elements from parent head to iframe head 22 | document.head.querySelectorAll('style').forEach((style) => { 23 | const frameStyles = style.cloneNode(true); 24 | iframeDoc.head.appendChild(frameStyles); 25 | }); 26 | 27 | // Clone and append all meta elements from parent head to iframe head 28 | document.head.querySelectorAll('meta').forEach((meta) => { 29 | const frameMeta = meta.cloneNode(true); 30 | iframeDoc.head.appendChild(frameMeta); 31 | }); 32 | 33 | document.head 34 | .querySelectorAll('link[rel="stylesheet"]') 35 | .forEach((stylesheet) => { 36 | const frameStylesheet = stylesheet.cloneNode(true); 37 | iframeDoc.head.appendChild(frameStylesheet); 38 | }); 39 | 40 | // Inject Tailwind CSS script into iframe head 41 | const tailwindScript = document.createElement('script'); 42 | tailwindScript.src = 'https://cdn.tailwindcss.com'; 43 | iframeDoc.head.appendChild(tailwindScript); 44 | 45 | // Add overflow hidden class to iframe body 46 | iframeDoc.body.classList.add('overflow-hidden'); 47 | } 48 | }, [iframeDoc]); 49 | 50 | const mountRef = useCallback((node: HTMLIFrameElement | null) => { 51 | setContentRef(node); 52 | }, []); 53 | 54 | return ( 55 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /app/components/render-node.tsx: -------------------------------------------------------------------------------- 1 | import { useNode, useEditor } from '@craftjs/core'; 2 | import React, { useEffect, useRef, useCallback } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { Move, ArrowUp, Trash2 } from 'lucide-react'; 5 | import { Button } from './ui/button'; 6 | 7 | export const RenderNode = ({ render }: { render: React.ReactNode }) => { 8 | const { id } = useNode(); 9 | const { actions, query, isActive } = useEditor((_, query) => ({ 10 | isActive: query.getEvent('selected').contains(id), 11 | })); 12 | 13 | const { 14 | isHover, 15 | isSelected, 16 | dom, 17 | moveable, 18 | connectors: { drag }, 19 | parent, 20 | deletable, 21 | props, 22 | } = useNode((node) => ({ 23 | isHover: node.events.hovered, 24 | isSelected: node.events.selected, 25 | dom: node.dom, 26 | name: node.data.custom.displayName || node.data.displayName, 27 | moveable: query.node(node.id).isDraggable(), 28 | deletable: query.node(node.id).isDeletable(), 29 | parent: node.data.parent, 30 | props: node.data.props, 31 | })); 32 | 33 | useEffect(() => { 34 | if (dom && id !== 'ROOT') { 35 | if (isHover) { 36 | // If either active or hover, add corresponding classes 37 | 38 | dom.classList.toggle('component-hover', isHover); 39 | } else { 40 | // If neither active nor hover, remove both classes 41 | dom.classList.remove('component-hover'); 42 | } 43 | } 44 | }, [dom, isHover]); 45 | 46 | return <>{render}; 47 | }; 48 | -------------------------------------------------------------------------------- /app/components/settings-control.tsx: -------------------------------------------------------------------------------- 1 | import { useEditor, useNode } from '@craftjs/core'; 2 | import { Component, ReactNode, useEffect, useState } from 'react'; 3 | import Select, { MultiValue, components, createFilter } from 'react-select'; 4 | import { suggestions } from '~/lib/tw-classes'; 5 | import { 6 | Option, 7 | SelectValue, 8 | } from 'react-tailwindcss-select/dist/components/type'; 9 | import { FixedSizeList as List } from 'react-window'; 10 | import { Button } from './ui/button'; 11 | import { Trash2 } from 'lucide-react'; 12 | import { Input } from './ui/input'; 13 | 14 | const selectOptions = suggestions.map((value) => ({ label: value, value })); 15 | 16 | export const SettingsControl = () => { 17 | const { query, actions } = useEditor(); 18 | const { 19 | id, 20 | classNames, 21 | deletable, 22 | text, 23 | actions: { setProp }, 24 | } = useNode((node) => ({ 25 | classNames: node.data.props['className'] as string, 26 | text: node.data.props['children'] as string, 27 | deletable: query.node(node.id).isDeletable(), 28 | })); 29 | 30 | const tailwindcssArr = classNames 31 | ? classNames.split(' ').filter(Boolean) 32 | : []; 33 | 34 | const initialOptions = tailwindcssArr.map((value) => ({ 35 | label: value, 36 | value, 37 | })); 38 | 39 | useEffect(() => { 40 | const tailwindcssArr = classNames 41 | ? classNames.split(' ').filter(Boolean) 42 | : []; 43 | 44 | const newOptions = tailwindcssArr.map((value) => ({ 45 | label: value, 46 | value, 47 | })); 48 | 49 | setValue(newOptions); 50 | }, [classNames]); 51 | 52 | const [value, setValue] = useState>(initialOptions); 53 | 54 | const height = 35; 55 | 56 | interface MenuListProps { 57 | options: any[]; 58 | children: any[]; 59 | maxHeight: number; 60 | getValue: () => any[]; 61 | } 62 | 63 | class MenuList extends Component { 64 | render() { 65 | const { options, children, maxHeight, getValue } = this.props; 66 | const [value] = getValue(); 67 | const initialOffset = options.indexOf(value) * height; 68 | 69 | return ( 70 | 77 | {({ index, style }) =>
{children[index]}
} 78 |
79 | ); 80 | } 81 | } 82 | 83 | const CustomOption = ({ 84 | children, 85 | ...props 86 | }: { 87 | children: React.ReactNode; 88 | innerProps: any; 89 | }) => { 90 | // Remove the niceties for mouseover and mousemove to optimize for large lists 91 | // eslint-disable-next-line no-unused-vars 92 | const { onMouseMove, onMouseOver, ...rest } = props.innerProps; 93 | const newProps = { ...props, innerProps: rest }; 94 | return ( 95 | 96 |
{children}
97 |
98 | ); 99 | }; 100 | 101 | return ( 102 |
103 | {deletable ? ( 104 | 117 | ) : null} 118 | {typeof text === 'string' ? ( 119 | 124 | setProp( 125 | (props: { children: ReactNode }) => 126 | (props.children = e.target.value.replace(/<\/?[^>]+(>|$)/g, '')) 127 | ) 128 | } 129 | /> 130 | ) : null} 131 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /app/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "~/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /app/components/ui/navigation-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ChevronDownIcon } from "@radix-ui/react-icons" 3 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" 4 | import { cva } from "class-variance-authority" 5 | 6 | import { cn } from "~/lib/utils" 7 | 8 | const NavigationMenu = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 20 | {children} 21 | 22 | 23 | )) 24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName 25 | 26 | const NavigationMenuList = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, ...props }, ref) => ( 30 | 38 | )) 39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName 40 | 41 | const NavigationMenuItem = NavigationMenuPrimitive.Item 42 | 43 | const navigationMenuTriggerStyle = cva( 44 | "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" 45 | ) 46 | 47 | const NavigationMenuTrigger = React.forwardRef< 48 | React.ElementRef, 49 | React.ComponentPropsWithoutRef 50 | >(({ className, children, ...props }, ref) => ( 51 | 56 | {children}{" "} 57 | 62 | )) 63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName 64 | 65 | const NavigationMenuContent = React.forwardRef< 66 | React.ElementRef, 67 | React.ComponentPropsWithoutRef 68 | >(({ className, ...props }, ref) => ( 69 | 77 | )) 78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName 79 | 80 | const NavigationMenuLink = NavigationMenuPrimitive.Link 81 | 82 | const NavigationMenuViewport = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 |
87 | 95 |
96 | )) 97 | NavigationMenuViewport.displayName = 98 | NavigationMenuPrimitive.Viewport.displayName 99 | 100 | const NavigationMenuIndicator = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, ...props }, ref) => ( 104 | 112 |
113 | 114 | )) 115 | NavigationMenuIndicator.displayName = 116 | NavigationMenuPrimitive.Indicator.displayName 117 | 118 | export { 119 | navigationMenuTriggerStyle, 120 | NavigationMenu, 121 | NavigationMenuList, 122 | NavigationMenuItem, 123 | NavigationMenuContent, 124 | NavigationMenuTrigger, 125 | NavigationMenuLink, 126 | NavigationMenuIndicator, 127 | NavigationMenuViewport, 128 | } 129 | -------------------------------------------------------------------------------- /app/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as TabsPrimitive from "@radix-ui/react-tabs" 3 | 4 | import { cn } from "~/lib/utils" 5 | 6 | const Tabs = TabsPrimitive.Root 7 | 8 | const TabsList = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | TabsList.displayName = TabsPrimitive.List.displayName 22 | 23 | const TabsTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 35 | )) 36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 37 | 38 | const TabsContent = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 | 50 | )) 51 | TabsContent.displayName = TabsPrimitive.Content.displayName 52 | 53 | export { Tabs, TabsList, TabsTrigger, TabsContent } 54 | -------------------------------------------------------------------------------- /app/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 3 | 4 | import { cn } from "~/lib/utils" 5 | 6 | const TooltipProvider = TooltipPrimitive.Provider 7 | 8 | const Tooltip = TooltipPrimitive.Root 9 | 10 | const TooltipTrigger = TooltipPrimitive.Trigger 11 | 12 | const TooltipContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, sideOffset = 4, ...props }, ref) => ( 16 | 25 | )) 26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 27 | 28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 29 | -------------------------------------------------------------------------------- /app/components/ui/vertical-navigation-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ChevronRightIcon } from "@radix-ui/react-icons"; 3 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"; 4 | import { cva } from "class-variance-authority"; 5 | 6 | import { cn } from "~/lib/utils"; 7 | 8 | const NavigationMenu = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 20 | {children} 21 | 22 | )); 23 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName; 24 | 25 | const NavigationMenuList = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 34 | )); 35 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; 36 | 37 | const NavigationMenuItem = NavigationMenuPrimitive.Item; 38 | 39 | const navigationMenuTriggerStyle = cva( 40 | "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background p-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" 41 | ); 42 | 43 | const NavigationMenuTrigger = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | <> 48 | 53 | {children} 54 | 59 | 60 | )); 61 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName; 62 | 63 | const NavigationMenuContent = React.forwardRef< 64 | React.ElementRef, 65 | React.ComponentPropsWithoutRef 66 | >(({ className, ...props }, ref) => ( 67 | 75 | )); 76 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName; 77 | 78 | const NavigationMenuLink = NavigationMenuPrimitive.Link; 79 | 80 | const NavigationMenuViewport = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 |
85 | 93 |
94 | )); 95 | NavigationMenuViewport.displayName = 96 | NavigationMenuPrimitive.Viewport.displayName; 97 | 98 | const NavigationMenuIndicator = React.forwardRef< 99 | React.ElementRef, 100 | React.ComponentPropsWithoutRef 101 | >(({ className, ...props }, ref) => ( 102 | 110 |
111 | 112 | )); 113 | NavigationMenuIndicator.displayName = 114 | NavigationMenuPrimitive.Indicator.displayName; 115 | 116 | export { 117 | navigationMenuTriggerStyle, 118 | NavigationMenu, 119 | NavigationMenuList, 120 | NavigationMenuItem, 121 | NavigationMenuContent, 122 | NavigationMenuTrigger, 123 | NavigationMenuLink, 124 | NavigationMenuIndicator, 125 | NavigationMenuViewport, 126 | }; 127 | -------------------------------------------------------------------------------- /app/components/viewport.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer, DrawerContent, DrawerTrigger } from "~/components/ui/drawer"; 2 | import { Button } from "./ui/button"; 3 | import { CodeView } from "./code-view"; 4 | import { useEditor } from "@craftjs/core"; 5 | import { useState } from "react"; 6 | import { getOutputCode } from "~/lib/code-gen"; 7 | 8 | export const Viewport = ({ children }: { children: React.ReactNode }) => { 9 | return ( 10 |
11 |
{children}
12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle hydrating your app on the client for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.client 5 | */ 6 | 7 | import { RemixBrowser } from "@remix-run/react"; 8 | import { startTransition, StrictMode } from "react"; 9 | import { hydrateRoot } from "react-dom/client"; 10 | 11 | startTransition(() => { 12 | hydrateRoot( 13 | document, 14 | 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle generating the HTTP Response for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.server 5 | */ 6 | 7 | import { PassThrough } from "node:stream"; 8 | 9 | import type { AppLoadContext, EntryContext } from "@remix-run/node"; 10 | import { createReadableStreamFromReadable } from "@remix-run/node"; 11 | import { RemixServer } from "@remix-run/react"; 12 | import isbot from "isbot"; 13 | import { renderToPipeableStream } from "react-dom/server"; 14 | 15 | const ABORT_DELAY = 5_000; 16 | 17 | export default function handleRequest( 18 | request: Request, 19 | responseStatusCode: number, 20 | responseHeaders: Headers, 21 | remixContext: EntryContext, 22 | // This is ignored so we can keep it in the template for visibility. Feel 23 | // free to delete this parameter in your app if you're not using it! 24 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 25 | loadContext: AppLoadContext 26 | ) { 27 | return isbot(request.headers.get("user-agent")) 28 | ? handleBotRequest( 29 | request, 30 | responseStatusCode, 31 | responseHeaders, 32 | remixContext 33 | ) 34 | : handleBrowserRequest( 35 | request, 36 | responseStatusCode, 37 | responseHeaders, 38 | remixContext 39 | ); 40 | } 41 | 42 | function handleBotRequest( 43 | request: Request, 44 | responseStatusCode: number, 45 | responseHeaders: Headers, 46 | remixContext: EntryContext 47 | ) { 48 | return new Promise((resolve, reject) => { 49 | let shellRendered = false; 50 | const { pipe, abort } = renderToPipeableStream( 51 | , 56 | { 57 | onAllReady() { 58 | shellRendered = true; 59 | const body = new PassThrough(); 60 | const stream = createReadableStreamFromReadable(body); 61 | 62 | responseHeaders.set("Content-Type", "text/html"); 63 | 64 | resolve( 65 | new Response(stream, { 66 | headers: responseHeaders, 67 | status: responseStatusCode, 68 | }) 69 | ); 70 | 71 | pipe(body); 72 | }, 73 | onShellError(error: unknown) { 74 | reject(error); 75 | }, 76 | onError(error: unknown) { 77 | responseStatusCode = 500; 78 | // Log streaming rendering errors from inside the shell. Don't log 79 | // errors encountered during initial shell rendering since they'll 80 | // reject and get logged in handleDocumentRequest. 81 | if (shellRendered) { 82 | console.error(error); 83 | } 84 | }, 85 | } 86 | ); 87 | 88 | setTimeout(abort, ABORT_DELAY); 89 | }); 90 | } 91 | 92 | function handleBrowserRequest( 93 | request: Request, 94 | responseStatusCode: number, 95 | responseHeaders: Headers, 96 | remixContext: EntryContext 97 | ) { 98 | return new Promise((resolve, reject) => { 99 | let shellRendered = false; 100 | const { pipe, abort } = renderToPipeableStream( 101 | , 106 | { 107 | onShellReady() { 108 | shellRendered = true; 109 | const body = new PassThrough(); 110 | const stream = createReadableStreamFromReadable(body); 111 | 112 | responseHeaders.set("Content-Type", "text/html"); 113 | 114 | resolve( 115 | new Response(stream, { 116 | headers: responseHeaders, 117 | status: responseStatusCode, 118 | }) 119 | ); 120 | 121 | pipe(body); 122 | }, 123 | onShellError(error: unknown) { 124 | reject(error); 125 | }, 126 | onError(error: unknown) { 127 | responseStatusCode = 500; 128 | // Log streaming rendering errors from inside the shell. Don't log 129 | // errors encountered during initial shell rendering since they'll 130 | // reject and get logged in handleDocumentRequest. 131 | if (shellRendered) { 132 | console.error(error); 133 | } 134 | }, 135 | } 136 | ); 137 | 138 | setTimeout(abort, ABORT_DELAY); 139 | }); 140 | } 141 | -------------------------------------------------------------------------------- /app/lib/client-hints.tsx: -------------------------------------------------------------------------------- 1 | import { getHintUtils } from "@epic-web/client-hints"; 2 | import { 3 | clientHint as colorSchemeHint, 4 | subscribeToSchemeChange, 5 | } from "@epic-web/client-hints/color-scheme"; 6 | import { useRevalidator, useRouteLoaderData } from "@remix-run/react"; 7 | import * as React from "react"; 8 | import type { loader as rootLoader } from "~/root"; 9 | import type { SerializeFrom } from "@remix-run/node"; 10 | import { useOptimisticTheme } from "~/routes/resources.theme-toggle"; 11 | 12 | const hintsUtils = getHintUtils({ 13 | theme: colorSchemeHint, 14 | }); 15 | 16 | export const { getHints } = hintsUtils; 17 | 18 | // Remix theme utils below 19 | export function useRequestInfo() { 20 | const data = useRouteLoaderData("root") as SerializeFrom; 21 | return data.requestInfo; 22 | } 23 | 24 | export function useHints() { 25 | const requestInfo = useRequestInfo(); 26 | return requestInfo.hints; 27 | } 28 | 29 | export function ClientHintCheck({ nonce }: { nonce: string }) { 30 | const { revalidate } = useRevalidator(); 31 | React.useEffect( 32 | () => subscribeToSchemeChange(() => revalidate()), 33 | [revalidate] 34 | ); 35 | 36 | return ( 37 |