├── .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 | 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 \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 | 11 | 12 | 13 | Drag-cn 14 | 15 | {/* */} 16 | 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 | 24 | 25 | Star on Github 26 | 27 | 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: Default, 45 | node: Default, 46 | }, 47 | { 48 | name: 'Outline', 49 | props: { variant: 'outline', children: 'Outline' }, 50 | demo: Outline, 51 | node: Outline, 52 | }, 53 | { 54 | name: 'Destructive', 55 | props: { variant: 'destructive', children: 'Destructive' }, 56 | demo: Destructive, 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 | Footer button 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 | 56 | {mountNode && createPortal(children, mountNode)} 57 | 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 | { 108 | event.stopPropagation(); 109 | if (parent) { 110 | actions.delete(id); 111 | } 112 | }} 113 | > 114 | 115 | Delete 116 | 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 | { 141 | if (option && Array.isArray(option)) { 142 | const classNames = option.map((item) => item.value).join(' '); 143 | setProp((props: { className: string }) => { 144 | console.log('Setting props ', props.className); 145 | props.className = classNames; 146 | }); 147 | } 148 | 149 | if (!option) { 150 | setProp((props: { className: string }) => (props.className = '')); 151 | } 152 | 153 | setValue(option); 154 | }} 155 | /> 156 | 157 | ); 158 | }; 159 | -------------------------------------------------------------------------------- /app/components/side-menu.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, ReactNode } from 'react'; 2 | 3 | import { 4 | NavigationMenuLink, 5 | NavigationMenu, 6 | NavigationMenuList, 7 | NavigationMenuItem, 8 | NavigationMenuTrigger, 9 | NavigationMenuContent, 10 | NavigationMenuViewport, 11 | } from './ui/vertical-navigation-menu'; 12 | import { cn } from '~/lib/utils'; 13 | import { useEditor } from '@craftjs/core'; 14 | import { Components } from './node/components-map'; 15 | 16 | export interface SideMenuProps { 17 | componentsMap: Components[]; 18 | } 19 | 20 | export const SideMenu = ({ componentsMap }: SideMenuProps) => { 21 | const { connectors } = useEditor(); 22 | 23 | return ( 24 | 28 | 29 | {componentsMap.map((menuItem, index) => ( 30 | 31 | 32 | {menuItem.name} 33 | 34 | 35 | 36 | {menuItem.items.map((component, index) => ( 37 | { 40 | if (ref) { 41 | connectors.create(ref, component.node); 42 | } 43 | }} 44 | > 45 | {component.demo ? component.demo : component.name} 46 | 47 | ))} 48 | 49 | 50 | 51 | ))} 52 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | const ListItem = React.forwardRef< 59 | React.ElementRef<'a'>, 60 | React.ComponentPropsWithoutRef<'a'> 61 | >(({ className, children, ...props }, ref) => { 62 | return ( 63 | 64 | 65 | 73 | 74 | {children} 75 | 76 | 77 | 78 | 79 | ); 80 | }); 81 | ListItem.displayName = 'ListItem'; 82 | -------------------------------------------------------------------------------- /app/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 3 | import { ChevronDownIcon } from "@radix-ui/react-icons" 4 | 5 | import { cn } from "~/lib/utils" 6 | 7 | const Accordion = AccordionPrimitive.Root 8 | 9 | const AccordionItem = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 18 | )) 19 | AccordionItem.displayName = "AccordionItem" 20 | 21 | const AccordionTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, children, ...props }, ref) => ( 25 | 26 | svg]:rotate-180", 30 | className 31 | )} 32 | {...props} 33 | > 34 | {children} 35 | 36 | 37 | 38 | )) 39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 40 | 41 | const AccordionContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, children, ...props }, ref) => ( 45 | 50 | {children} 51 | 52 | )) 53 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 54 | 55 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 56 | -------------------------------------------------------------------------------- /app/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "~/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 | 33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /app/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "~/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /app/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "~/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 | 17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 | 29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 | 41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 | 53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 | 61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 | 73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /app/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Drawer as DrawerPrimitive } from "vaul" 3 | 4 | import { cn } from "~/lib/utils" 5 | 6 | const Drawer = ({ 7 | shouldScaleBackground = true, 8 | ...props 9 | }: React.ComponentProps) => ( 10 | 14 | ) 15 | Drawer.displayName = "Drawer" 16 | 17 | const DrawerTrigger = DrawerPrimitive.Trigger 18 | 19 | const DrawerPortal = DrawerPrimitive.Portal 20 | 21 | const DrawerClose = DrawerPrimitive.Close 22 | 23 | const DrawerOverlay = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName 34 | 35 | const DrawerContent = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, children, ...props }, ref) => ( 39 | 40 | 41 | 49 | 50 | {children} 51 | 52 | 53 | )) 54 | DrawerContent.displayName = "DrawerContent" 55 | 56 | const DrawerHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 | 64 | ) 65 | DrawerHeader.displayName = "DrawerHeader" 66 | 67 | const DrawerFooter = ({ 68 | className, 69 | ...props 70 | }: React.HTMLAttributes) => ( 71 | 75 | ) 76 | DrawerFooter.displayName = "DrawerFooter" 77 | 78 | const DrawerTitle = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef 81 | >(({ className, ...props }, ref) => ( 82 | 90 | )) 91 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName 92 | 93 | const DrawerDescription = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, ...props }, ref) => ( 97 | 102 | )) 103 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName 104 | 105 | export { 106 | Drawer, 107 | DrawerPortal, 108 | DrawerOverlay, 109 | DrawerTrigger, 110 | DrawerClose, 111 | DrawerContent, 112 | DrawerHeader, 113 | DrawerFooter, 114 | DrawerTitle, 115 | DrawerDescription, 116 | } 117 | -------------------------------------------------------------------------------- /app/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 3 | import { 4 | CheckIcon, 5 | ChevronRightIcon, 6 | DotFilledIcon, 7 | } from "@radix-ui/react-icons"; 8 | 9 | import { cn } from "~/lib/utils"; 10 | 11 | const DropdownMenu = DropdownMenuPrimitive.Root; 12 | 13 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 14 | 15 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 16 | 17 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 18 | 19 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 20 | 21 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 22 | 23 | const DropdownMenuSubTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef & { 26 | inset?: boolean; 27 | } 28 | >(({ className, inset, children, ...props }, ref) => ( 29 | 38 | {children} 39 | 40 | 41 | )); 42 | DropdownMenuSubTrigger.displayName = 43 | DropdownMenuPrimitive.SubTrigger.displayName; 44 | 45 | const DropdownMenuSubContent = React.forwardRef< 46 | React.ElementRef, 47 | React.ComponentPropsWithoutRef 48 | >(({ className, ...props }, ref) => ( 49 | 57 | )); 58 | DropdownMenuSubContent.displayName = 59 | DropdownMenuPrimitive.SubContent.displayName; 60 | 61 | const DropdownMenuContent = React.forwardRef< 62 | React.ElementRef, 63 | React.ComponentPropsWithoutRef 64 | >(({ className, sideOffset = 4, ...props }, ref) => ( 65 | 66 | 76 | 77 | )); 78 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 79 | 80 | const DropdownMenuItem = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef & { 83 | inset?: boolean; 84 | } 85 | >(({ className, inset, ...props }, ref) => ( 86 | 95 | )); 96 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 97 | 98 | const DropdownMenuCheckboxItem = React.forwardRef< 99 | React.ElementRef, 100 | React.ComponentPropsWithoutRef 101 | >(({ className, children, checked, ...props }, ref) => ( 102 | 111 | 112 | 113 | 114 | 115 | 116 | {children} 117 | 118 | )); 119 | DropdownMenuCheckboxItem.displayName = 120 | DropdownMenuPrimitive.CheckboxItem.displayName; 121 | 122 | const DropdownMenuRadioItem = React.forwardRef< 123 | React.ElementRef, 124 | React.ComponentPropsWithoutRef 125 | >(({ className, children, ...props }, ref) => ( 126 | 134 | 135 | 136 | 137 | 138 | 139 | {children} 140 | 141 | )); 142 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 143 | 144 | const DropdownMenuLabel = React.forwardRef< 145 | React.ElementRef, 146 | React.ComponentPropsWithoutRef & { 147 | inset?: boolean; 148 | } 149 | >(({ className, inset, ...props }, ref) => ( 150 | 159 | )); 160 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 161 | 162 | const DropdownMenuSeparator = React.forwardRef< 163 | React.ElementRef, 164 | React.ComponentPropsWithoutRef 165 | >(({ className, ...props }, ref) => ( 166 | 171 | )); 172 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 173 | 174 | const DropdownMenuShortcut = ({ 175 | className, 176 | ...props 177 | }: React.HTMLAttributes) => { 178 | return ( 179 | 183 | ); 184 | }; 185 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 186 | 187 | export { 188 | DropdownMenu, 189 | DropdownMenuTrigger, 190 | DropdownMenuContent, 191 | DropdownMenuItem, 192 | DropdownMenuCheckboxItem, 193 | DropdownMenuRadioItem, 194 | DropdownMenuLabel, 195 | DropdownMenuSeparator, 196 | DropdownMenuShortcut, 197 | DropdownMenuGroup, 198 | DropdownMenuPortal, 199 | DropdownMenuSub, 200 | DropdownMenuSubContent, 201 | DropdownMenuSubTrigger, 202 | DropdownMenuRadioGroup, 203 | }; 204 | -------------------------------------------------------------------------------- /app/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "~/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 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 | 61 | 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 | 58 | 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 | 43 | ); 44 | } 45 | 46 | /** 47 | * @returns the user's theme preference, or the client hint theme if the user 48 | * has not set a preference. 49 | */ 50 | export function useTheme() { 51 | const hints = useHints(); 52 | const requestInfo = useRequestInfo(); 53 | const optimisticTheme = useOptimisticTheme(); 54 | if (optimisticTheme) { 55 | return optimisticTheme === "system" ? hints.theme : optimisticTheme; 56 | } 57 | return requestInfo.userPrefs.theme ?? hints.theme; 58 | } 59 | 60 | // Use nonce for the script tag 61 | const NonceContext = React.createContext(""); 62 | export const useNonce = () => React.useContext(NonceContext); 63 | -------------------------------------------------------------------------------- /app/lib/code-gen.tsx: -------------------------------------------------------------------------------- 1 | import type { Node, Nodes } from '@craftjs/core'; 2 | 3 | let imports: { displayName: string; importPath: string }[] = []; 4 | 5 | const generateComponentCode = ( 6 | nodesMap: Nodes, 7 | nodeId: string, 8 | level: number 9 | ): string => { 10 | const node = nodesMap[nodeId]; 11 | const { displayName, props, nodes, linkedNodes, custom } = node.data; 12 | 13 | const indendation = getIndentation(level); 14 | const openingTag = `<${displayName}${generatePropsString(props)}>`; 15 | const closingTag = `${displayName}>`; 16 | 17 | console.log(' custom ', displayName, custom); 18 | 19 | if (!imports.find((item) => item.displayName === displayName)) { 20 | imports.push({ 21 | displayName, 22 | importPath: custom.importPath, 23 | }); 24 | } 25 | 26 | if (nodes.length === 0 && Object.keys(linkedNodes).length === 0) { 27 | // No child nodes, return the self-closing tag 28 | return `${indendation}${openingTag}${generateChildString( 29 | props.children, 30 | level + 1 31 | )}${closingTag}`; 32 | } else { 33 | // Has child nodes, recursively generate code for children 34 | const childComponents = nodes.map((childId) => 35 | generateComponentCode(nodesMap, childId, level + 1) 36 | ); 37 | 38 | const childComponentsString = childComponents.length 39 | ? `\n${childComponents.join(`\n`)}` 40 | : ''; 41 | 42 | const linkedChildComponents = Object.entries(linkedNodes).map( 43 | ([key, value]) => generateComponentCode(nodesMap, value, level + 1) 44 | ); 45 | 46 | const linkedChildComponentsString = linkedChildComponents.length 47 | ? `\n${linkedChildComponents.join(`\n`)}` 48 | : ''; 49 | 50 | return `${indendation}${openingTag}${childComponentsString}${linkedChildComponentsString}\n${indendation}${closingTag}`; 51 | } 52 | }; 53 | 54 | interface ComponentInfo { 55 | displayName: string; 56 | importPath: string; 57 | } 58 | 59 | const generateImportStatements = (components: ComponentInfo[]): string => { 60 | const filteredComponents = components.filter( 61 | (comp) => comp.displayName !== 'div' 62 | ); 63 | 64 | const groupedComponents: { [key: string]: ComponentInfo[] } = {}; 65 | 66 | // Group components by import path 67 | filteredComponents.forEach((comp) => { 68 | const key = comp.importPath || ''; // Use an empty string for components without a path 69 | if (!groupedComponents[key]) { 70 | groupedComponents[key] = []; 71 | } 72 | groupedComponents[key].push(comp); 73 | }); 74 | 75 | // Generate import statements 76 | const importStatements = Object.values(groupedComponents).map((group) => { 77 | const displayNameList = group.map((comp) => comp.displayName).join(', '); 78 | const importPath = group[0].importPath 79 | ? ` from "${group[0].importPath}"` 80 | : ''; 81 | return `import { ${displayNameList} }${importPath};`; 82 | }); 83 | 84 | return importStatements.join('\n'); 85 | }; 86 | 87 | function wrapInsideComponent(input: string): string { 88 | return ` 89 | export function Component() { 90 | return ( 91 | ${input.trim().replace(/^/gm, ' ')} 92 | ); 93 | } 94 | `.trim(); 95 | } 96 | 97 | const generatePropsString = (props: { 98 | [key: string]: string | undefined; 99 | }): string => { 100 | const propsArray = Object.entries(props) 101 | .filter(([key]) => key !== 'children') // Exclude children from props 102 | .map(([key, value]) => `${key}="${value}"`); 103 | return propsArray.length > 0 ? ` ${propsArray.join(' ')}` : ''; 104 | }; 105 | 106 | const getIndentation = (level: number): string => { 107 | if (!level) { 108 | return ''; 109 | } 110 | return ' '.repeat(level * 2); // Adjust the number of spaces per level as needed 111 | }; 112 | 113 | const generateChildString = ( 114 | children: string | Node[] | undefined, 115 | level: number 116 | ): string => { 117 | if (typeof children === 'string') { 118 | // If children is a string, return it directly 119 | return children; 120 | } else if (Array.isArray(children) && children.length > 0) { 121 | return children 122 | .map((child) => generateComponentCode({ TEMP: child }, 'TEMP', level)) 123 | .join(''); 124 | } else { 125 | return ''; 126 | } 127 | }; 128 | 129 | export const getOutputCode = (nodes: Nodes) => { 130 | imports = []; 131 | 132 | const componentString = generateComponentCode(nodes, 'ROOT', 2); 133 | const importString = generateImportStatements(imports); 134 | const output = wrapInsideComponent(componentString); 135 | console.log(generateImportStatements(imports)); 136 | console.log('imports ', imports); 137 | 138 | return { importString, output }; 139 | }; 140 | 141 | export const getOutputHTMLFromId = (iframeId: string): string => { 142 | const iframe = document.getElementById(iframeId) as HTMLIFrameElement | null; 143 | const iframeDocument = iframe?.contentWindow?.document || null; 144 | 145 | if (iframeDocument) { 146 | const indentation = ' '; // Adjust the indentation as needed 147 | const iframeHtml = iframeDocument.documentElement.outerHTML; 148 | const indentedHtml = iframeHtml.replace(/^(.*)$/gm, indentation + '$1'); 149 | 150 | return indentedHtml; 151 | } else { 152 | alert('Failed to access iframe content.'); 153 | return ''; 154 | } 155 | }; 156 | -------------------------------------------------------------------------------- /app/lib/theme.server.ts: -------------------------------------------------------------------------------- 1 | import * as cookie from "cookie"; 2 | 3 | const cookieName = "en_theme"; 4 | type Theme = "light" | "dark"; 5 | 6 | export function getTheme(request: Request): Theme | null { 7 | const cookieHeader = request.headers.get("cookie"); 8 | const parsed = cookieHeader 9 | ? cookie.parse(cookieHeader)[cookieName] 10 | : "light"; 11 | if (parsed === "light" || parsed === "dark") return parsed; 12 | return null; 13 | } 14 | 15 | export function setTheme(theme: Theme | "system") { 16 | if (theme === "system") { 17 | return cookie.serialize(cookieName, "", { path: "/", maxAge: -1 }); 18 | } else { 19 | return cookie.serialize(cookieName, theme, { path: "/", maxAge: 31536000 }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import { cssBundleHref } from "@remix-run/css-bundle"; 2 | import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node"; 3 | import { 4 | Links, 5 | LiveReload, 6 | Meta, 7 | Outlet, 8 | Scripts, 9 | ScrollRestoration, 10 | json, 11 | } from "@remix-run/react"; 12 | import styles from "./tailwind.css"; 13 | import { getTheme } from "./lib/theme.server"; 14 | import { 15 | ClientHintCheck, 16 | getHints, 17 | useNonce, 18 | useTheme, 19 | } from "./lib/client-hints"; 20 | import clsx from "clsx"; 21 | 22 | export const links: LinksFunction = () => [ 23 | { rel: "stylesheet", href: styles }, 24 | ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), 25 | ]; 26 | 27 | export const loader = async ({ request }: LoaderFunctionArgs) => { 28 | return json({ 29 | requestInfo: { 30 | hints: getHints(request), 31 | userPrefs: { 32 | theme: getTheme(request), 33 | }, 34 | }, 35 | }); 36 | }; 37 | 38 | export default function App() { 39 | const theme = useTheme(); 40 | const nonce = useNonce(); 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import { Editor, Frame, Element } from '@craftjs/core'; 2 | import { SideMenu } from '~/components/side-menu'; 3 | import { Header } from '~/components/header'; 4 | import { Canvas } from '~/components/canvas'; 5 | import { NodeButton } from '~/components/node/button'; 6 | import { 7 | NodeCardHeader, 8 | NodeCard, 9 | NodeCardContent, 10 | NodeCardDescription, 11 | NodeCardTitle, 12 | NodeCardFooter, 13 | } from '~/components/node/card'; 14 | import { ReactIframe } from '~/components/react-iframe'; 15 | import { ControlPanel } from '~/components/control-panel'; 16 | import { Viewport } from '~/components/viewport'; 17 | import { RenderNode } from '~/components/render-node'; 18 | import { componentsMap } from '~/components/node/components-map'; 19 | import { NodeOneBlock, NodeTwoBlocks } from '~/components/node/layout'; 20 | 21 | export default function Index() { 22 | return ( 23 | 24 | 25 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | Button 1 50 | Button 2 51 | Button 3 52 | Button 4 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /app/routes/resources.theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react"; 2 | 3 | import { Button } from "~/components/ui/button"; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuTrigger, 9 | } from "~/components/ui/dropdown-menu"; 10 | import { useFetcher, useFetchers } from "@remix-run/react"; 11 | import type { ActionFunction } from "@remix-run/node"; 12 | import { json } from "@remix-run/node"; 13 | import { setTheme } from "~/lib/theme.server"; 14 | 15 | enum Theme { 16 | DARK = "dark", 17 | LIGHT = "light", 18 | SYSTEM = "system", 19 | } 20 | 21 | export const themes: Array = Object.values(Theme); 22 | 23 | function isTheme(value: unknown): value is Theme { 24 | return typeof value === "string" && themes.includes(value as Theme); 25 | } 26 | 27 | export const action: ActionFunction = async ({ request }) => { 28 | const requestText = await request.text(); 29 | const form = new URLSearchParams(requestText); 30 | const theme = form.get("theme"); 31 | 32 | if (!isTheme(theme)) { 33 | return json({ 34 | success: false, 35 | message: `theme value of ${theme} is not a valid theme`, 36 | }); 37 | } 38 | 39 | return json( 40 | { success: true }, 41 | { 42 | headers: { 43 | "Set-Cookie": setTheme(theme), 44 | }, 45 | } 46 | ); 47 | }; 48 | 49 | export function useOptimisticTheme(): Theme | null { 50 | const fetchers = useFetchers(); 51 | const themeFetcher = fetchers.find( 52 | (f) => f.formAction === "/resources/theme-toggle" 53 | ); 54 | 55 | const optimisticTheme = themeFetcher?.formData?.get("theme"); 56 | 57 | if (optimisticTheme && isTheme(optimisticTheme)) { 58 | return optimisticTheme; 59 | } 60 | 61 | return null; 62 | } 63 | 64 | export function ThemeToggle() { 65 | const fetcher = useFetcher(); 66 | 67 | const handleThemeChange = (theme: Theme) => { 68 | fetcher.submit( 69 | { theme }, 70 | { method: "post", action: "/resources/theme-toggle" } 71 | ); 72 | }; 73 | 74 | return ( 75 | 76 | 77 | 78 | 79 | 80 | Toggle theme 81 | 82 | 83 | 84 | handleThemeChange(Theme.LIGHT)}> 85 | Light 86 | 87 | handleThemeChange(Theme.DARK)}> 88 | Dark 89 | 90 | handleThemeChange(Theme.SYSTEM)}> 91 | System 92 | 93 | 94 | 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /app/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | @layer base { 7 | :root { 8 | --background: 0 0% 100%; 9 | --foreground: 240 10% 3.9%; 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | --popover: 0 0% 100%; 13 | --popover-foreground: 240 10% 3.9%; 14 | --primary: 240 5.9% 10%; 15 | --primary-foreground: 0 0% 98%; 16 | --secondary: 240 4.8% 95.9%; 17 | --secondary-foreground: 240 5.9% 10%; 18 | --muted: 240 4.8% 95.9%; 19 | --muted-foreground: 240 3.8% 46.1%; 20 | --accent: 240 4.8% 95.9%; 21 | --accent-foreground: 240 5.9% 10%; 22 | --destructive: 0 84.2% 60.2%; 23 | --destructive-foreground: 0 0% 98%; 24 | --border: 240 5.9% 90%; 25 | --input: 240 5.9% 90%; 26 | --ring: 240 5.9% 10%; 27 | --radius: 0.5rem; 28 | } 29 | 30 | .dark { 31 | --background: 240 10% 3.9%; 32 | --foreground: 0 0% 98%; 33 | --card: 240 10% 3.9%; 34 | --card-foreground: 0 0% 98%; 35 | --popover: 240 10% 3.9%; 36 | --popover-foreground: 0 0% 98%; 37 | --primary: 0 0% 98%; 38 | --primary-foreground: 240 5.9% 10%; 39 | --secondary: 240 3.7% 15.9%; 40 | --secondary-foreground: 0 0% 98%; 41 | --muted: 240 3.7% 15.9%; 42 | --muted-foreground: 240 5% 64.9%; 43 | --accent: 240 3.7% 15.9%; 44 | --accent-foreground: 0 0% 98%; 45 | --destructive: 0 62.8% 30.6%; 46 | --destructive-foreground: 0 0% 98%; 47 | --border: 240 3.7% 15.9%; 48 | --input: 240 3.7% 15.9%; 49 | --ring: 240 4.9% 83.9%; 50 | } 51 | } 52 | 53 | @layer base { 54 | * { 55 | @apply border-border; 56 | } 57 | body { 58 | @apply bg-background text-foreground; 59 | } 60 | } 61 | 62 | .component-selected, 63 | .component-hover { 64 | @apply relative cursor-move; 65 | } 66 | .component-selected::after { 67 | content: " "; 68 | @apply border-blue-500 border-2 border-dashed w-full h-full absolute left-0 top-0 pointer-events-none block; 69 | } 70 | .component-hover::after { 71 | content: " "; 72 | @apply border-green-500 border-2 border-dashed w-full h-full absolute left-0 top-0 pointer-events-none block; 73 | } 74 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/tailwind.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "~/components", 15 | "utils": "~/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix-shadcn-darkmode", 3 | "private": true, 4 | "sideEffects": false, 5 | "type": "module", 6 | "scripts": { 7 | "build": "remix build", 8 | "dev": "PORT=10000 remix dev --manual", 9 | "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", 10 | "start": "remix-serve ./build/index.js", 11 | "typecheck": "tsc" 12 | }, 13 | "dependencies": { 14 | "@craftjs/core": "^0.2.4", 15 | "@epic-web/client-hints": "^1.2.2", 16 | "@radix-ui/react-accordion": "^1.1.2", 17 | "@radix-ui/react-dialog": "^1.0.5", 18 | "@radix-ui/react-dropdown-menu": "^2.0.6", 19 | "@radix-ui/react-icons": "^1.3.0", 20 | "@radix-ui/react-label": "^2.0.2", 21 | "@radix-ui/react-navigation-menu": "^1.1.4", 22 | "@radix-ui/react-slot": "^1.0.2", 23 | "@radix-ui/react-tabs": "^1.0.4", 24 | "@remix-run/css-bundle": "^2.4.1", 25 | "@remix-run/node": "^2.4.1", 26 | "@remix-run/react": "^2.4.1", 27 | "@remix-run/serve": "^2.4.1", 28 | "class-variance-authority": "^0.7.0", 29 | "clsx": "^2.1.0", 30 | "cookie": "^0.6.0", 31 | "isbot": "^3.6.8", 32 | "lucide-react": "^0.308.0", 33 | "react": "^18.2.0", 34 | "react-code-blocks": "^0.1.6", 35 | "react-contenteditable": "^3.3.7", 36 | "react-dom": "^18.2.0", 37 | "react-frame-component": "^5.2.6", 38 | "react-markdown": "^9.0.1", 39 | "react-select": "^5.8.0", 40 | "react-syntax-highlighter": "^15.5.0", 41 | "react-tailwindcss-select": "^1.8.5", 42 | "react-window": "^1.8.10", 43 | "tailwind-merge": "^2.2.0", 44 | "tailwindcss-animate": "^1.0.7", 45 | "vaul": "^0.8.9" 46 | }, 47 | "devDependencies": { 48 | "@remix-run/dev": "^2.4.1", 49 | "@remix-run/eslint-config": "^2.4.1", 50 | "@tailwindcss/typography": "^0.5.10", 51 | "@types/react": "^18.2.47", 52 | "@types/react-dom": "^18.2.7", 53 | "@types/react-syntax-highlighter": "^15.5.11", 54 | "@types/react-window": "^1.8.8", 55 | "@typescript-eslint/eslint-plugin": "^6.7.4", 56 | "autoprefixer": "^10.4.16", 57 | "eslint": "^8.38.0", 58 | "eslint-config-prettier": "^9.0.0", 59 | "eslint-import-resolver-typescript": "^3.6.1", 60 | "eslint-plugin-import": "^2.28.1", 61 | "eslint-plugin-jsx-a11y": "^6.7.1", 62 | "eslint-plugin-react": "^7.33.2", 63 | "eslint-plugin-react-hooks": "^4.6.0", 64 | "tailwindcss": "^3.4.1", 65 | "typescript": "^5.1.6" 66 | }, 67 | "engines": { 68 | "node": ">=18.0.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajeshdavidbabu/remix-craftjs-starter/61daefd69b20c9cac10a6a9990ead3dc02d4dc96/public/favicon.ico -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | export default { 3 | ignoredRouteFiles: ["**/.*"], 4 | // appDirectory: "app", 5 | // assetsBuildDirectory: "public/build", 6 | // publicPath: "/build/", 7 | // serverBuildPath: "build/index.js", 8 | tailwind: true, 9 | postcss: true, 10 | }; 11 | -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | "./pages/**/*.{ts,tsx}", 6 | "./components/**/*.{ts,tsx}", 7 | "./app/**/*.{ts,tsx}", 8 | "./src/**/*.{ts,tsx}", 9 | ], 10 | prefix: "", 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | colors: { 21 | border: "hsl(var(--border))", 22 | input: "hsl(var(--input))", 23 | ring: "hsl(var(--ring))", 24 | background: "hsl(var(--background))", 25 | foreground: "hsl(var(--foreground))", 26 | primary: { 27 | DEFAULT: "hsl(var(--primary))", 28 | foreground: "hsl(var(--primary-foreground))", 29 | }, 30 | secondary: { 31 | DEFAULT: "hsl(var(--secondary))", 32 | foreground: "hsl(var(--secondary-foreground))", 33 | }, 34 | destructive: { 35 | DEFAULT: "hsl(var(--destructive))", 36 | foreground: "hsl(var(--destructive-foreground))", 37 | }, 38 | muted: { 39 | DEFAULT: "hsl(var(--muted))", 40 | foreground: "hsl(var(--muted-foreground))", 41 | }, 42 | accent: { 43 | DEFAULT: "hsl(var(--accent))", 44 | foreground: "hsl(var(--accent-foreground))", 45 | }, 46 | popover: { 47 | DEFAULT: "hsl(var(--popover))", 48 | foreground: "hsl(var(--popover-foreground))", 49 | }, 50 | card: { 51 | DEFAULT: "hsl(var(--card))", 52 | foreground: "hsl(var(--card-foreground))", 53 | }, 54 | }, 55 | borderRadius: { 56 | lg: "var(--radius)", 57 | md: "calc(var(--radius) - 2px)", 58 | sm: "calc(var(--radius) - 4px)", 59 | }, 60 | keyframes: { 61 | "accordion-down": { 62 | from: { height: "0" }, 63 | to: { height: "var(--radix-accordion-content-height)" }, 64 | }, 65 | "accordion-up": { 66 | from: { height: "var(--radix-accordion-content-height)" }, 67 | to: { height: "0" }, 68 | }, 69 | animatedgradient: { 70 | "0%": { backgroundPosition: "0% 50%" }, 71 | "50%": { backgroundPosition: "100% 50%" }, 72 | "100%": { backgroundPosition: "0% 50%" }, 73 | }, 74 | }, 75 | animation: { 76 | "accordion-down": "accordion-down 0.2s ease-out", 77 | "accordion-up": "accordion-up 0.2s ease-out", 78 | gradient: "animatedgradient 6s ease infinite alternate", 79 | }, 80 | backgroundSize: { 81 | "300%": "300%", 82 | }, 83 | }, 84 | }, 85 | plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], 86 | }; 87 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "Bundler", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | 19 | // Remix takes care of building everything in `remix build`. 20 | "noEmit": true 21 | } 22 | } 23 | --------------------------------------------------------------------------------
{description}
18 | With optimistic dark-mode 19 |