diff --git a/apps/docs/components/Header.tsx b/apps/docs/components/Header.tsx index d6083a78..3824e190 100644 --- a/apps/docs/components/Header.tsx +++ b/apps/docs/components/Header.tsx @@ -103,47 +103,6 @@ export const Header = () => { > GitHub - - Log in - - - Sign up - ) } diff --git a/apps/docs/components/Nav.tsx b/apps/docs/components/Nav.tsx index 1017a4ba..f7f3d832 100644 --- a/apps/docs/components/Nav.tsx +++ b/apps/docs/components/Nav.tsx @@ -1,13 +1,12 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes, ReactNode } from 'react' import Link, { LinkProps } from 'next/link' import { useRouter } from 'next/router' -interface NavItemProps extends HTMLAttributes {} export const NavItem = ({ children, href, ...props -}: NavItemProps & LinkProps) => { +}: LinkProps & { children: ReactNode }) => { const router = useRouter() const isActive = router.asPath === href return ( @@ -26,8 +25,8 @@ export const NavItem = ({ py: 2, transition: 'color .2s ease-in-out', ':hover': { - color: isActive? 'background' : 'primary' - } + color: isActive ? 'background' : 'primary', + }, }} > {children} @@ -36,8 +35,7 @@ export const NavItem = ({ ) } -interface NavSectionTitleProps extends HTMLAttributes {} -export const NavSectionTitle = (props: NavSectionTitleProps) => { +export const NavSectionTitle = (props: HTMLAttributes) => { return (

{ <>
- +

Heading

Body text

diff --git a/apps/docs/components/examples/FilterPreview.tsx b/apps/docs/components/examples/FilterPreview.tsx index 5171b903..889cb090 100644 --- a/apps/docs/components/examples/FilterPreview.tsx +++ b/apps/docs/components/examples/FilterPreview.tsx @@ -37,7 +37,7 @@ export function FilterPreview() { mb: 3, }}> - + diff --git a/apps/docs/components/examples/MixBlendModePreview.tsx b/apps/docs/components/examples/MixBlendModePreview.tsx index 6096e437..1fc83291 100644 --- a/apps/docs/components/examples/MixBlendModePreview.tsx +++ b/apps/docs/components/examples/MixBlendModePreview.tsx @@ -36,7 +36,7 @@ export function MixBlendModePreview() { }}>
', }, style: { @@ -164,7 +164,7 @@ export const initialValue: any = { tagName: 'video', attributes: { title: 'Video -
(initialStyles) - - return ( -
- -
-
- fun with ✂️ -
-
-
- ) -} diff --git a/apps/docs/pages/examples/filters.tsx b/apps/docs/pages/examples/filters.tsx index 5838cab6..68ca6a3a 100644 --- a/apps/docs/pages/examples/filters.tsx +++ b/apps/docs/pages/examples/filters.tsx @@ -29,7 +29,7 @@ export default function Filters() { sx={{ width: '100%', aspectRatio: '4 / 3', - backgroundImage: 'url("https://source.unsplash.com/random")', + backgroundImage: 'url("https://dlu344star2bj.cloudfront.net/i/3090-0015.jpg")', backgroundSize: 'cover', backgroundPosition: 'center', display: 'flex', diff --git a/apps/docs/pages/examples/flex.tsx b/apps/docs/pages/examples/flex.tsx index 39f04de8..d22d99f8 100644 --- a/apps/docs/pages/examples/flex.tsx +++ b/apps/docs/pages/examples/flex.tsx @@ -73,84 +73,84 @@ export default function TextDecoration() {
One
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
Eleven
Twelve
diff --git a/apps/docs/pages/examples/masks.tsx b/apps/docs/pages/examples/masks.tsx index a47cd7f9..909918c7 100644 --- a/apps/docs/pages/examples/masks.tsx +++ b/apps/docs/pages/examples/masks.tsx @@ -9,7 +9,7 @@ const initialStyles = { clip: 'border-box', image: { name: 'url', - arguments: 'https://source.unsplash.com/random', + arguments: 'https://dlu344star2bj.cloudfront.net/i/3090-0015.jpg', }, origin: 'border-box', position: { @@ -34,7 +34,7 @@ export default function MaskExample() {
img': { maxWidth: '100%', display: 'block' } }}>
diff --git a/apps/docs/pages/html-editor.tsx b/apps/docs/pages/html-editor.tsx index c928057d..79ffd9b4 100644 --- a/apps/docs/pages/html-editor.tsx +++ b/apps/docs/pages/html-editor.tsx @@ -7,7 +7,7 @@ export default function HtmlEditorExample() { const [html, setHtml] = useState(initialValue) return ( - +
- +
- - + +
diff --git a/package.json b/package.json index 541b89be..7a7c1bd9 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "engines": { "npm": ">=7.0.0", - "node": ">=14.0.0" + "node": ">=24.0.0" }, "prettier": { "singleQuote": true, @@ -31,10 +31,10 @@ }, "packageManager": "yarn@1.22.18", "dependencies": { - "@changesets/cli": "^2.22.0", + "@changesets/cli": "^2.26.0", "@manypkg/cli": "^0.19.1", "prettier": "^2.7.1", - "tsup": "^5.12.6", - "turbo": "^1.2.6" + "tsup": "^6.2.3", + "turbo": "^1.3.1" } } diff --git a/packages/config/package.json b/packages/config/package.json index 13e5e6d4..f8db14b5 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -8,7 +8,7 @@ "eslint-preset.js" ], "dependencies": { - "eslint-config-next": "^12.1.6", + "eslint-config-next": "^13.2.3", "eslint-config-prettier": "^8.5.0", "eslint-plugin-react": "7.30.0" } diff --git a/packages/gui/CHANGELOG.md b/packages/gui/CHANGELOG.md index ca2ecc08..ed89bf96 100644 --- a/packages/gui/CHANGELOG.md +++ b/packages/gui/CHANGELOG.md @@ -1,5 +1,462 @@ # @compai/css-gui +## 0.0.247 + +### Patch Changes + +- 2c1bbde9: Bump @radix-ui/react-dropdown-menu from 1.0.0 to 2.0.3 + +## 0.0.246 + +### Patch Changes + +- 95540fca: Adds Astro support for export + +## 0.0.245 + +### Patch Changes + +- debd3b89: Properly handle pseudos when stringifying CSS object + +## 0.0.244 + +### Patch Changes + +- d543091c: Changes color to be more accessible in editor +- fd7865c8: Fixes typo + +## 0.0.243 + +### Patch Changes + +- bb2079e3: New design styles +- e116b855: Ensure theme is passed to style element stringification + +## 0.0.242 + +### Patch Changes + +- 1f758bed: Fixes to two small design bugs + +## 0.0.241 + +### Patch Changes + +- 4be2253e: Copy over emotion's hash code, faux ESM makes it tricky to use as dep + +## 0.0.240 + +### Patch Changes + +- 3d312157: Add style element to enhance export + +## 0.0.239 + +### Patch Changes + +- 8db98565: Use style string and don't destructure slots as attrs + +## 0.0.238 + +### Patch Changes + +- 700259d1: Make sure all code generators are passed theme in export + +## 0.0.237 + +### Patch Changes + +- 263b9b23: Move getAttrSyntax to util + +## 0.0.236 + +### Patch Changes + +- 38996b16: Include attributes in enhance export + +## 0.0.235 + +### Patch Changes + +- b0c58514: Implement first pass of enhance output + +## 0.0.234 + +### Patch Changes + +- 63cc6ff4: Add initial support for selector functions + +## 0.0.233 + +### Patch Changes + +- adea2803: Add filter element attributes + +## 0.0.232 + +### Patch Changes + +- 08e07226: Bump @radix-ui/react-slider from 0.1.4 to 1.0.0 +- d48ae3bf: Bump copy-to-clipboard from 3.3.1 to 3.3.2 + +## 0.0.231 + +### Patch Changes + +- 294beec5: Adds new svg elements and attributes + +## 0.0.230 + +### Patch Changes + +- 0846769d: Fix color label overflow + +## 0.0.229 + +### Patch Changes + +- 5770da69: Improve tap targets and spacing + +## 0.0.228 + +### Patch Changes + +- 312acb38: Apply styles to text parent + +## 0.0.227 + +### Patch Changes + +- 8d695237: Ignore colors with alpha in regen + +## 0.0.226 + +### Patch Changes + +- 98b7d345: Fix regen for theme values + +## 0.0.225 + +### Patch Changes + +- 603fbfed: Add an empty text node to prose elements + +## 0.0.224 + +### Patch Changes + +- 859ef100: Fix inconsistent typefaces +- d6ddbe3b: Send along options to stringify in tuple schema + +## 0.0.223 + +### Patch Changes + +- 049630e1: Add repeat keywords to count + +## 0.0.222 + +### Patch Changes + +- 7f4f6573: Add component swapping + +## 0.0.221 + +### Patch Changes + +- d1039b08: Move event propagation call + +## 0.0.220 + +### Patch Changes + +- c9b6365d: Disable browser label/input selection handling in the canvas + +## 0.0.219 + +### Patch Changes + +- ff2f1dcd: A small patch to remove autocorrect from property input control + +## 0.0.218 + +### Patch Changes + +- 794eab84: Make font size consistent in DOM tree + +## 0.0.217 + +### Patch Changes + +- eb553037: Improve node type swapping + +## 0.0.216 + +### Patch Changes + +- 354f899f: Implement inline insert for components/slots + +## 0.0.215 + +### Patch Changes + +- 565f3ed3: Maintain component children + +## 0.0.214 + +### Patch Changes + +- 74ac4a3e: Improve adding box shadow layers + +## 0.0.213 + +### Patch Changes + +- 8114b4c4: Improve color display + +## 0.0.212 + +### Patch Changes + +- 3f04e7ac: Only wrap imports when there's more than one top level element + +## 0.0.211 + +### Patch Changes + +- 790afdba: Keep component name in combobox when selected + +## 0.0.210 + +### Patch Changes + +- 9628acc5: Keep props around when swapping between components + +## 0.0.209 + +### Patch Changes + +- 50d060da: Adds new properties and svg elements + +## 0.0.208 + +### Patch Changes + +- b5dc91a7: Make sure tag name combobox syncs + +## 0.0.207 + +### Patch Changes + +- c53ca4bb: Improve enter handling for combobox + +## 0.0.206 + +### Patch Changes + +- 2908cba5: Make sure default value is passed to base schema + +## 0.0.205 + +### Patch Changes + +- 635d59de: Fix palette picker z index + +## 0.0.204 + +### Patch Changes + +- f0114ae9: Fix color picker style bug + +## 0.0.203 + +### Patch Changes + +- 08f99dbb: Fixes typo +- a90031f8: Visual changes to editor controls + +## 0.0.202 + +### Patch Changes + +- f7c41754: Bump @radix-ui/react-switch from 0.1.5 to 1.0.0 +- 5aa3cbec: Bump @radix-ui/react-dropdown-menu from 0.1.6 to 1.0.0 +- 473ba671: Bump @radix-ui/react-popover from 0.1.6 to 1.0.0 + +## 0.0.201 + +### Patch Changes + +- afcce3ed: Improve control styles + +## 0.0.200 + +### Patch Changes + +- 58c2c3fa: Display theme value for scales + +## 0.0.199 + +### Patch Changes + +- af5a6cf9: Add component name to dropdown in editor + +## 0.0.198 + +### Patch Changes + +- f464b9d0: Bump theme-ui from 0.14.6 to 0.14.7 + +## 0.0.197 + +### Patch Changes + +- 5cb0fcf5: Show function names in input + +## 0.0.196 + +### Patch Changes + +- a3654f79: Add theme support in gradient + +## 0.0.195 + +### Patch Changes + +- 5a373ac3: Add support for box shadows in theme + +## 0.0.194 + +### Patch Changes + +- 9784d96d: Add changeset + +## 0.0.193 + +### Patch Changes + +- b5c54ad8: Make border radius theme aware + +## 0.0.192 + +### Patch Changes + +- 82aafd34: Add random image to image when added to canvas + +## 0.0.191 + +### Patch Changes + +- 1fd09f49: Improve font family theme integration + +## 0.0.190 + +### Patch Changes + +- 684e2f65: Fix gradient stops adding + +## 0.0.189 + +### Patch Changes + +- 05f3157a: Add component/slot to layers panel + +## 0.0.188 + +### Patch Changes + +- 130847c4: Fix bug with inline text editing + +## 0.0.187 + +### Patch Changes + +- db4fa469: Implement inline text editing + +## 0.0.186 + +### Patch Changes + +- 6e4d520b: Theme handling improvements, remove duplicate UI + +## 0.0.185 + +### Patch Changes + +- d9ff3da0: Properly handle contrast for all properties + +## 0.0.184 + +### Patch Changes + +- 304e6671: Improve node adding and editing + +## 0.0.183 + +### Patch Changes + +- 49aae809: Constrain to accessible contrasts when generating colors + +## 0.0.182 + +### Patch Changes + +- 2351ca88: Handle textarea children + +## 0.0.181 + +### Patch Changes + +- 1a4562c9: Refactor fieldset passing + +## 0.0.180 + +### Patch Changes + +- d7124e11: Add draggable reordering to field array inputs + +## 0.0.179 + +### Patch Changes + +- 5820a1bb: Fix node type select + +## 0.0.178 + +### Patch Changes + +- bb72987d: Ignore style attribute on import for now + +## 0.0.177 + +### Patch Changes + +- 474882f6: Improve nested component selection + +## 0.0.176 + +### Patch Changes + +- ebde90d6: Improve HTML node imports + +## 0.0.175 + +### Patch Changes + +- cc3bed1f: Improve fieldset fuzzy search + +## 0.0.174 + +### Patch Changes + +- 92807f4a: Add support for nested elements + +## 0.0.173 + +### Patch Changes + +- 9b88bf0d: Implement HTML and Markdown imports for DOM structure + ## 0.0.172 ### Patch Changes diff --git a/packages/gui/package.json b/packages/gui/package.json index c9759cf7..97ae23a3 100644 --- a/packages/gui/package.json +++ b/packages/gui/package.json @@ -1,6 +1,6 @@ { "name": "@compai/css-gui", - "version": "0.0.172", + "version": "0.0.247", "type": "module", "module": "./dist/index.js", "types": "./dist/index.d.ts", @@ -45,16 +45,17 @@ "@mdx-js/react": "^1.6.22", "@radix-ui/react-accordion": "^0.1.6", "@radix-ui/react-collapsible": "^0.1.6", - "@radix-ui/react-dropdown-menu": "^0.1.6", + "@radix-ui/react-dropdown-menu": "^2.0.3", "@radix-ui/react-label": "^0.1.5", - "@radix-ui/react-popover": "^0.1.6", + "@radix-ui/react-popover": "^1.0.0", "@radix-ui/react-select": "^0.1.1", - "@radix-ui/react-slider": "^0.1.4", - "@radix-ui/react-switch": "^0.1.5", + "@radix-ui/react-slider": "^1.0.0", + "@radix-ui/react-switch": "^1.0.0", "@radix-ui/react-tabs": "^0.1.5", - "@radix-ui/react-toggle": "^0.1.4", + "@radix-ui/react-toggle": "^1.0.1", "@radix-ui/react-tooltip": "^0.1.7", - "copy-to-clipboard": "^3.3.1", + "@use-gesture/react": "^10.2.17", + "copy-to-clipboard": "^3.3.2", "csstype": "^3.0.11", "culori": "^2.0.3", "downshift": "^6.1.7", @@ -62,15 +63,20 @@ "fuzzysort": "^2.0.1", "get-contrast": "^3.0.0", "hast-to-hyperscript": "^10.0.1", + "hast-util-to-html": "^8.0.3", + "hast-util-to-mdast": "^8.3.1", "immer": "^9.0.12", "lodash-es": "^4.17.21", + "mdast-util-from-markdown": "^1.2.0", + "mdast-util-to-hast": "^12.1.2", + "mdast-util-to-markdown": "^1.3.0", "property-information": "^6.1.1", "react-feather": "^2.0.10", - "react-use-gesture": "^9.1.3", "rehype-parse": "^8.0.4", "rehype-sanitize": "^5.0.1", "rehype-stringify": "^9.0.3", - "theme-ui": "^0.14.6", + "remark-parse": "^10.0.1", + "theme-ui": "^0.14.7", "unified": "^10.1.2", "unist-util-remove": "^3.1.0", "unist-util-visit": "^4.1.0", diff --git a/packages/gui/src/components/AddFieldset.tsx b/packages/gui/src/components/AddFieldset.tsx index b3430cd8..97878923 100644 --- a/packages/gui/src/components/AddFieldset.tsx +++ b/packages/gui/src/components/AddFieldset.tsx @@ -1,5 +1,5 @@ -import { useCombobox } from 'downshift' -import { useEffect, useId, useRef, useState } from 'react' +import fuzzysort from 'fuzzysort' +import { elements } from '../data/elements' import { pseudoClasses } from '../data/pseudo-classes' import { pseudoElements } from '../data/pseudo-elements' import { Styles } from '../types/css' @@ -19,19 +19,18 @@ export const AddFieldsetControl = ({ label = 'Add pseudo element or class', }: Props) => { const { setField } = useEditor() - const allItems = [...pseudoClasses, ...pseudoElements] + const allItems = [...pseudoClasses, ...pseudoElements, ...elements] const handleFilterItems = (input: string) => { - const styleItems = Object.keys(styles) - const filteredItems = allItems - .filter((item) => { - if (item.toLowerCase().startsWith(input.toLowerCase() || '')) { - return !styleItems.includes(item) - } - }) - .sort() + if (input === '') { + return allItems + } - return filteredItems + const styleItems = Object.keys(styles) + return fuzzysort + .go(input.replace(/-/g, ''), allItems) + .map((res) => res.target) + .filter((item) => !styleItems.includes(item)) } const handleAddFieldset = (propertyName: string) => { @@ -40,13 +39,15 @@ export const AddFieldsetControl = ({ } return ( -
+
- - kebabCase(str)} - clearOnSelect - /> +
) } diff --git a/packages/gui/src/components/Editor/Controls.tsx b/packages/gui/src/components/Editor/Controls.tsx index 997b67c6..e23c66b1 100644 --- a/packages/gui/src/components/Editor/Controls.tsx +++ b/packages/gui/src/components/Editor/Controls.tsx @@ -6,6 +6,7 @@ import { isValidElement, ReactNode, useMemo, + useState, } from 'react' import { camelCase, isNil, mapValues, uniq } from 'lodash-es' import { RefreshCw } from 'react-feather' @@ -37,8 +38,14 @@ import { SchemaInput } from '../inputs/SchemaInput' import { EditorDropdown } from '../ui/dropdowns/EditorDropdown' import { FieldsetDropdown } from '../ui/dropdowns/FieldsetDropdown' import { tokenize } from '../../lib/parse' -import { pseudoClasses } from '../../data/pseudo-classes' -import { pseudoElements } from '../../data/pseudo-elements' +import { + addPseudoSyntax, + getSelectorFunctionArgument, + getSelectorFunctionName, + isSelectorFunction, + removePseudoSyntax, + stringifySelectorFunction, +} from '../../lib/pseudos' export const getPropertyFromField = (field: KeyArg) => { if (Array.isArray(field)) { @@ -53,7 +60,7 @@ interface ControlProps extends InputProps { showRemove?: boolean } const Control = ({ field, showRemove = false, ...props }: ControlProps) => { - const { getField, setField, removeField } = useEditor() + const { getField, getParentField, setField, removeField } = useEditor() const { removeDynamicProperty } = useDynamicControls() const fieldset = useFieldset() const property = getPropertyFromField(field) @@ -100,6 +107,8 @@ const Control = ({ field, showRemove = false, ...props }: ControlProps) => { setField(fullField, newValue) }} onRemove={showRemove ? handleRemoveProperty : undefined} + ruleset={getParentField(fullField)} + property={property} /> ) } @@ -197,8 +206,12 @@ export const Editor = ({ function regenerateAll(): any { return mapValues(allStyles, (value, property) => { return ( - properties[property].regenerate?.({ theme, previousValue: value }) ?? - value + properties[property].regenerate?.({ + theme, + previousValue: value, + ruleset: allStyles, + property, + }) ?? value ) }) } @@ -211,12 +224,12 @@ export const Editor = ({ hideResponsiveControls={hideResponsiveControls} > {showRegenerate && ( -
+
onChange(regenerateAll())} - sx={{ ml: 'auto', display: 'flex', gap: 2 }} + sx={{ ml: 'auto' }} > - Regenerate +
)} @@ -242,6 +255,7 @@ export const EditorControls = ({ ) : ( ) + const fieldsetControls = children ? null : ( ) @@ -301,7 +315,7 @@ const ControlSet = ({ field, properties }: ControlSetProps) => { const fullField = field ? joinPath(field, property) : property return isFieldsetGroup(property) ? ( - + ) : ( ) @@ -311,19 +325,16 @@ const ControlSet = ({ field, properties }: ControlSetProps) => { } type FieldsetControlProps = { - field?: KeyArg - property: string + field: string } -const FieldsetControl = ({ field, property }: FieldsetControlProps) => { - const { getField, removeField } = useEditor() - const styles = getField(field || property) - const properties = Object.keys(styles) +const FieldsetControl = ({ field }: FieldsetControlProps) => { + const { getField, removeField, setFields } = useEditor() + const [argument, setArgument] = useState(getSelectorFunctionArgument(field)) - const propertyLabel = pseudoClasses.includes(property as any) - ? `:${property}` - : pseudoElements.includes(property as any) - ? `::${property}` - : property + const styles = getField(field) + const properties = Object.keys(styles) + const label = addPseudoSyntax(field) + const rawFieldsetName = getSelectorFunctionName(field) return (
{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', + mt: 3, mb: 2, }} >

- {removeInternalCSSClassSyntax(propertyLabel)} + {rawFieldsetName} + {isSelectorFunction(rawFieldsetName) ? ( + <> + {'('} + { + setArgument(e.target.value) + }} + onBlur={() => { + setFields( + { + [stringifySelectorFunction(rawFieldsetName, argument)]: + styles, + }, + [field] + ) + }} + /> + {')'} + + ) : null}

- removeField(field || property)} /> + removeField(field)} />
- -
+ +
- +
) @@ -410,6 +457,7 @@ export function parseStyles(styles: Record) { if (!schema) { throw new Error(`Parsing unknown property: ${property}`) } + const [parsed, rest] = schema.parse!(tokenize(value)) if (isNil(parsed) || rest.length > 0) { throw new Error(`Error parsing given value ${value} into ${property}`) diff --git a/packages/gui/src/components/Editor/Fieldset.tsx b/packages/gui/src/components/Editor/Fieldset.tsx index 8c449a17..1e5660c9 100644 --- a/packages/gui/src/components/Editor/Fieldset.tsx +++ b/packages/gui/src/components/Editor/Fieldset.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { elements } from '../../data/elements' import { pseudoClasses } from '../../data/pseudo-classes' import { pseudoElements } from '../../data/pseudo-elements' -import { getFieldsetPropsFromProperty } from './util' +import { getFieldsetPropsFromName } from './util' type PseudoElementTypes = typeof pseudoElements[number] type PseudoClassTypes = typeof pseudoClasses[number] @@ -42,14 +42,9 @@ export const Fieldset = ({ type, name, children }: FieldsetProps) => { } type GenericFieldsetProps = { - property: string + field: string children?: React.ReactNode } -export const GenericFieldset = ({ - property, - children, -}: GenericFieldsetProps) => { - return ( -
{children}
- ) +export const GenericFieldset = ({ field, children }: GenericFieldsetProps) => { + return
{children}
} diff --git a/packages/gui/src/components/Editor/util.ts b/packages/gui/src/components/Editor/util.ts index e02b849a..af51b475 100644 --- a/packages/gui/src/components/Editor/util.ts +++ b/packages/gui/src/components/Editor/util.ts @@ -30,9 +30,7 @@ export const isFieldsetGroup = (str: string) => { return isPseudo(str) || isElement(str) || isInternalCSSClass(str) } -export const getFieldsetPropsFromProperty = ( - str: string -): FieldsetContextProps => { +export const getFieldsetPropsFromName = (str: string): FieldsetContextProps => { if (isElement(str)) { return { type: 'element', diff --git a/packages/gui/src/components/FieldArray.tsx b/packages/gui/src/components/FieldArray.tsx index d899761d..14e3cbda 100644 --- a/packages/gui/src/components/FieldArray.tsx +++ b/packages/gui/src/components/FieldArray.tsx @@ -1,70 +1,89 @@ -import { flip, replace, remove } from '../lib/array' +import { useState } from 'react' +import { replace, remove, insert } from '../lib/array' import { EditorPropsWithLabel } from '../types/editor' import { SchemaInput } from './inputs/SchemaInput' import { DataTypeSchema } from './schemas/types' -interface FieldArrayProps extends EditorPropsWithLabel { +export interface FieldArrayProps extends EditorPropsWithLabel { /** * The component to render each of the individual input values. * (See `LayerProps` for what props this takes) */ itemSchema: DataTypeSchema + addItem?(currentValue: T[]): T } /** * An alternative field array that is collapsible. */ -export default function FieldArray(props: FieldArrayProps) { +export default function FieldArray({ + addItem, + ...props +}: FieldArrayProps) { const { label = '', value = [], onChange, itemSchema } = props + const [dragIndex, setDragIndex] = useState(-1) + const isDragging = dragIndex >= 0 - const handleReorder = (i1: number, i2: number) => { - if (typeof value === 'string') { - return - } - onChange(flip(value, i1, i2)) + const handleDragDrop = (i1: number, i2: number) => { + const item = value[i1] + const removed = remove(value, i1) + if (i2 > i1) i2-- + const final = insert(removed, i2, item) + onChange(final) } return ( -
+
{value.map((item, i) => { return ( -
- { - onChange(replace(value, i, newValue)) +
+ {isDragging && ( + { + handleDragDrop(dragIndex, i) + setDragIndex(-1) + }} + /> + )} +
onChange(remove(value, i))} - reorder={{ - onMoveUp: - i === 0 - ? undefined - : () => { - handleReorder(i, i - 1) - }, - onMoveDown: - i === value.length - 1 - ? undefined - : () => { - handleReorder(i, i + 1) - }, - }} - /> + > + { + onChange(replace(value, i, newValue)) + }} + onRemove={() => onChange(remove(value, i))} + onDrag={() => { + setDragIndex(i) + }} + onDragEnd={() => { + setDragIndex(-1) + }} + /> +
) })} + {isDragging && ( + { + handleDragDrop(dragIndex, value.length) + setDragIndex(-1) + }} + /> + )}
) } + +interface DropZoneProps { + onDrop(): void +} + +function DropZone({ onDrop }: DropZoneProps) { + const [hovered, setHovered] = useState(false) + return ( +
+
setHovered(true)} + onDragLeave={() => setHovered(false)} + onDragOver={(e) => { + e.preventDefault() + }} + onDrop={onDrop} + >
+
+ ) +} diff --git a/packages/gui/src/components/Layers.tsx b/packages/gui/src/components/Layers.tsx index 672c34d2..28d6850d 100644 --- a/packages/gui/src/components/Layers.tsx +++ b/packages/gui/src/components/Layers.tsx @@ -54,7 +54,7 @@ export default function Layers(props: LayersProps) { sx={{ border: '1px solid', borderColor: 'border', - borderRadius: 8, + borderRadius: '6px', }} > (props: LayersProps) { appearance: 'none', px: 0, py: 2, - m: 0, + mt: 2, border: 'none', background: 'none', cursor: 'pointer', diff --git a/packages/gui/src/components/html/CanvasProvider.tsx b/packages/gui/src/components/html/CanvasProvider.tsx index c0bc5a28..c9790142 100644 --- a/packages/gui/src/components/html/CanvasProvider.tsx +++ b/packages/gui/src/components/html/CanvasProvider.tsx @@ -3,7 +3,7 @@ import { toCSSObject } from '../../lib/codegen/to-css-object' import { toReactProps } from '../../lib/codegen/to-react-props' import { useTheme } from '../providers/ThemeContext' import { useHtmlEditor } from './Provider' -import { ElementPath, HtmlNode } from './types' +import { ComponentData, ElementPath, HtmlNode } from './types' import { cleanAttributesForCanvas, isSamePath } from './util' const DEFAULT_CANVAS_VALUE = {} @@ -18,6 +18,7 @@ type CanvasProviderType = { export type CanvasElementProps = { path: ElementPath value: HtmlNode + component?: ComponentData onClick?(e: MouseEvent): void } @@ -26,7 +27,12 @@ export function useCanvas() { return context } -export function useCanvasProps({ path, value, onClick }: CanvasElementProps) { +export function useCanvasProps({ + path, + value, + component, + onClick, +}: CanvasElementProps) { const { canvas } = useContext(CanvasContext) const { selected, setSelected } = useHtmlEditor() const theme = useTheme() @@ -66,6 +72,7 @@ export function useCanvasProps({ path, value, onClick }: CanvasElementProps) { ...(canvas ? cleanAttributesForCanvas(attributes) : attributes), ...(canvas ? { 'data-path': path.join('-') } : {}), sx, + outerProps: component?.props, onClick: handleSelect, }) diff --git a/packages/gui/src/components/html/Component/Editor.tsx b/packages/gui/src/components/html/Component/Editor.tsx index c3ed7533..8cd2a72e 100644 --- a/packages/gui/src/components/html/Component/Editor.tsx +++ b/packages/gui/src/components/html/Component/Editor.tsx @@ -1,10 +1,13 @@ import { ChangeEvent } from 'react' import fuzzysort from 'fuzzysort' +import { sample } from 'lodash-es' +import { RefreshCw } from 'react-feather' import { Label, Combobox } from '../../primitives' -import { ComponentData } from '../types' +import { ComponentData, Slot } from '../types' import { useHtmlEditor } from '../Provider' -import { getSlots } from '../../../lib/codegen/util' +import { getSlots, isSlot } from '../../../lib/codegen/util' import { mergeComponentAttributes } from './util' +import IconButton from '../../ui/IconButton' interface ComponentEditorProps { value: ComponentData @@ -34,8 +37,13 @@ export const ComponentEditor = ({ value, onChange }: ComponentEditorProps) => { const handleComponentSelected = (selectedItem: string) => { const component = components.find((c) => c.id === selectedItem) + if (component) { - onChange(component) + onChange({ + ...component, + props: value.props, + children: value.children, + }) } } @@ -61,6 +69,23 @@ export const ComponentEditor = ({ value, onChange }: ComponentEditorProps) => { }) } + const handleSwap = () => { + const newComponentId = sample(value.swappableComponentIds || []) + const newComponent = components.find((c) => c.id === newComponentId) + + if (!newComponentId || !newComponent) { + return + } + + onChange({ + ...value, + id: newComponentId, + tagName: newComponent.tagName, + value: newComponent.value, + swappableComponentIds: newComponent.swappableComponentIds, + }) + } + return (
{ }} > - { - return components.find((c) => c.id === id)?.tagName ?? id - }} - items={componentIds} - clearOnSelect - /> + + { + return components.find((c) => c.id === id)?.tagName ?? id + }} + items={componentIds} + /> + {value.swappableComponentIds?.length ? ( + + + + ) : null} +

Props

@@ -100,7 +137,12 @@ export const ComponentEditor = ({ value, onChange }: ComponentEditorProps) => { {attributeEntries.length ? ( <> {attributeEntries.map((entry) => { - const [key, val] = entry + const [key, rawValue] = entry + + const val = isSlot(rawValue as Slot) + ? (rawValue as Slot).value + : rawValue + return (
diff --git a/packages/gui/src/components/html/Component/Provider.tsx b/packages/gui/src/components/html/Component/Provider.tsx index 6e49b9fa..45d0df51 100644 --- a/packages/gui/src/components/html/Component/Provider.tsx +++ b/packages/gui/src/components/html/Component/Provider.tsx @@ -1,11 +1,17 @@ import { createContext, ReactNode, useContext } from 'react' -import { ComponentData, ElementPath } from '../types' +import { useHtmlEditor } from '../Provider' +import { ComponentData, ElementPath, HtmlNode, Slot } from '../types' +import { setChildAtPath } from '../util' +import { updateSlotForComponentInstance } from './util' const DEFAULT_COMPONENT_VALUE = {} type ComponentProviderType = { value?: ComponentData path?: ElementPath + selectComponent?(e: MouseEvent): void + updateComponent?(path: ElementPath, newItem: HtmlNode): void + updateComponentSlot?(newSlotValue: HtmlNode): void } export function useComponent() { @@ -28,8 +34,58 @@ export function ComponentProvider({ path, children, }: ComponentProviderProps) { + const { + setSelected, + value: fullValue, + update, + components, + updateComponent: emitUpdatedComponent, + } = useHtmlEditor() + const selectComponent = (e: MouseEvent) => { + e.stopImmediatePropagation() + setSelected(path) + } + + const updateComponent = (fullEditPath: ElementPath, newValue: HtmlNode) => { + const component = components!.find((c) => c.id === value.id)! + const editPath = fullEditPath.slice(path.length) + + const newComponentValue = setChildAtPath(value.value, editPath, newValue) + const newComponent = { + ...component, + value: newComponentValue, + } + + const newFullValue = setChildAtPath(fullValue, path, { + ...value, + value: newComponentValue, + }) + + update(newFullValue) + emitUpdatedComponent?.(newComponent) + } + + const updateComponentSlot = (newValue: HtmlNode) => { + const fullComponent = updateSlotForComponentInstance( + value, + newValue as Slot + ) + + // @ts-ignore + const newFullValue = setChildAtPath(fullValue, path, fullComponent) + update(newFullValue) + } + return ( - + {children} ) diff --git a/packages/gui/src/components/html/Component/SlotProvider.tsx b/packages/gui/src/components/html/Component/SlotProvider.tsx new file mode 100644 index 00000000..f5de0e66 --- /dev/null +++ b/packages/gui/src/components/html/Component/SlotProvider.tsx @@ -0,0 +1,30 @@ +import { createContext, ReactNode, useContext } from 'react' +import { ElementPath, Slot } from '../types' + +const DEFAULT_SLOT_VALUE = {} + +type SlotProviderType = { + value?: Slot + path?: ElementPath +} + +export function useSlot() { + const context = useContext(SlotContext) + return context +} + +const SlotContext = createContext(DEFAULT_SLOT_VALUE) + +type SlotProviderProps = { + value: Slot + path: ElementPath + children: ReactNode +} + +export function SlotProvider({ value, path, children }: SlotProviderProps) { + return ( + + {children} + + ) +} diff --git a/packages/gui/src/components/html/Component/util.ts b/packages/gui/src/components/html/Component/util.ts index d0ecb776..59f46b25 100644 --- a/packages/gui/src/components/html/Component/util.ts +++ b/packages/gui/src/components/html/Component/util.ts @@ -1,4 +1,4 @@ -import { ComponentData } from '../types' +import { ComponentData, Slot } from '../types' export const mergeComponentAttributes = (value: ComponentData) => { const attributes = { @@ -8,3 +8,17 @@ export const mergeComponentAttributes = (value: ComponentData) => { return attributes } + +export const updateSlotForComponentInstance = ( + value: ComponentData, + slotValue: Slot +) => { + const props = value.props || {} + return { + ...value, + props: { + ...props, + [slotValue.name]: slotValue.value, + }, + } +} diff --git a/packages/gui/src/components/html/Editor.tsx b/packages/gui/src/components/html/Editor.tsx index 92129f53..b78de0cb 100644 --- a/packages/gui/src/components/html/Editor.tsx +++ b/packages/gui/src/components/html/Editor.tsx @@ -1,37 +1,45 @@ import { Editor } from '../Editor' -import { HtmlNode } from './types' import * as Tabs from '@radix-ui/react-tabs' -import { Code, Layers } from 'react-feather' +import { Code, Layers, LogIn } from 'react-feather' import { useHtmlEditor } from './Provider' import { getChildAtPath, removeChildAtPath, setChildAtPath } from './util' import { Export } from './Export' import { useTheme } from '../providers/ThemeContext' import { NodeEditor } from './Editors/NodeEditor' import { TreeNode } from './TreeNode' - -interface HtmlEditorProps { - onChange(value: HtmlNode): void -} +import { Import } from './Import' +import { isText } from '../../lib/codegen/util' const TABS_TRIGGER_STYLES: any = { all: 'unset', cursor: 'pointer', fontSize: 0, fontWeight: 500, - px: 2, + px: 3, py: 1, + my: 2, + borderRadius: '6px', color: 'muted', display: 'inline-flex', gap: '.5em', alignItems: 'center', + filter: 'grayscale(100%)', + transition: 'all .2s ease-in-out', '&[data-state="active"]': { color: 'text', + filter: 'grayscale(0%)', + bg: 'backgroundOffset', + }, + ':hover': { + color: 'text', + filter: 'grayscale(0%)', }, } const TABS_CONTENT_STYLES: any = { width: 400, - height: 'calc(100vh - 81px)', - overflow: 'auto', + height: 'calc(100vh - 97px)', + maxHeight: '100%', + overflow: 'hidden', resize: 'horizontal', borderRightWidth: '1px', borderRightStyle: 'solid', @@ -40,16 +48,41 @@ const TABS_CONTENT_STYLES: any = { scrollbarWidth: 0, } +const TABS_EDITOR_STYLES: any = { + width: '400px', + height: 'calc(100vh - 97px)', + maxHeight: '100%', + overflow: 'hidden', + resize: 'horizontal', + borderRightWidth: '1px', + borderRightStyle: 'solid', + borderRightColor: 'border', + '&::-webkit-scrollbar': { display: 'none' }, + scrollbarWidth: 0, +} + /** * An HTML tree-based editor that lets you add HTML nodes and mess around with their styles */ -export function HtmlEditor({ onChange }: HtmlEditorProps) { - const { value, selected: providedSelected, setSelected } = useHtmlEditor() +export function HtmlEditor() { + const { + value, + update: onChange, + selected: providedSelected, + setSelected, + } = useHtmlEditor() const theme = useTheme() const selected = providedSelected || [] const nodeValue = getChildAtPath(value, selected) + let nodeForStyleEditor = nodeValue + const stylePath = [...selected] + if (isText(nodeValue)) { + stylePath.pop() + nodeForStyleEditor = getChildAtPath(value, stylePath) + } + return (
- 🎨 Styles + 🎨 Editor - - Layers + + Import Export - -
+ +
{ - const newItem = { ...nodeValue, style: newStyles } - onChange(setChildAtPath(value, selected, newItem)) + const newItem = { ...nodeForStyleEditor, style: newStyles } + onChange(setChildAtPath(value, stylePath, newItem)) }} showRegenerate showAddProperties />
-
- -
+

Layers

+
@@ -127,13 +160,20 @@ export function HtmlEditor({ onChange }: HtmlEditorProps) { setSelected(newPath) }} /> - -
+
+
+ +
+
+
+
+ + diff --git a/packages/gui/src/components/html/Editors/AttributeEditor.tsx b/packages/gui/src/components/html/Editors/AttributeEditor.tsx index ef7beefd..295254be 100644 --- a/packages/gui/src/components/html/Editors/AttributeEditor.tsx +++ b/packages/gui/src/components/html/Editors/AttributeEditor.tsx @@ -1,13 +1,15 @@ import { X } from 'react-feather' import { Label } from '../../primitives' import IconButton from '../../ui/IconButton' -import { useEffect } from 'react' +import { ChangeEvent, useEffect } from 'react' import { Combobox } from '../../primitives' import { ATTRIBUTE_MAP } from '../../../data/attributes' +import { isSlot } from '../../../lib/codegen/util' +import { Slot } from '../types' interface AttributeEditorProps { - value: Record - onChange(value: Record): void + value: Record + onChange(value: Record): void element: string } @@ -49,6 +51,27 @@ export const AttributeEditor = ({ onChange(newValue) } + const handleSlotToggle = (key: string) => { + const val = value[key] + const slotValue = val as unknown as Slot + + if (isSlot(slotValue)) { + onChange({ + ...value, + [key]: slotValue.value as string, + }) + } else { + onChange({ + ...value, + [key]: { + type: 'slot', + name: key, + value: val as string, + }, + }) + } + } + return (
@@ -62,25 +85,115 @@ export const AttributeEditor = ({
{/* @ts-ignore */} {Object.entries(value).map(([key, attrValue]) => { + if (isSlot(attrValue as unknown as Slot)) { + return ( + + onChange({ ...value, [key]: newValue }) + } + onRemove={() => handleItemRemoved(key)} + onSlot={() => handleSlotToggle(key)} + /> + ) + } + return ( -
- -
+ onChange({ ...value, [key]: e.target.value })} + onRemove={() => handleItemRemoved(key)} + onSlot={() => handleSlotToggle(key)} + /> ) })}
) } + +interface StringAttributeEditorProps { + name: string + value: string + onChange(e: ChangeEvent): void + onRemove(): void + onSlot(): void +} +const StringAttributeEditor = ({ + name, + value, + onChange, + onRemove, + onSlot, +}: StringAttributeEditorProps) => { + return ( +
+ +
+ ) +} + +interface SlotAttributeEditorProps { + name: string + value: Slot + onChange(newValue: Slot): void + onRemove(): void + onSlot(): void +} +const SlotAttributeEditor = ({ + name, + value, + onChange, + onRemove, + onSlot, +}: SlotAttributeEditorProps) => { + return ( +
+ {name} + + +
+ ) +} diff --git a/packages/gui/src/components/html/Editors/NodeEditor.tsx b/packages/gui/src/components/html/Editors/NodeEditor.tsx index 7a29a941..10af647c 100644 --- a/packages/gui/src/components/html/Editors/NodeEditor.tsx +++ b/packages/gui/src/components/html/Editors/NodeEditor.tsx @@ -1,4 +1,5 @@ import fuzzysort from 'fuzzysort' +import { Layers } from 'react-feather' import { HtmlNode } from '../types' import { Label, Combobox } from '../../primitives' import { SelectInput } from '../../inputs/SelectInput' @@ -10,6 +11,8 @@ import { NodeEditorDropdown } from '../../ui/dropdowns/NodeEditorDropdown' import { ComponentEditor } from '../Component' import { SlotEditor } from './SlotEditor' import { HTML_TAGS } from '../data' +import { useNodeTypes } from './util' +import { isProseElement } from '../../../lib/elements' interface EditorProps { value: HtmlNode @@ -27,35 +30,34 @@ export function NodeEditor({ onRemove, onParentChange, }: TagEditorProps) { - const { - value: fullValue, - selected, - hasComponents, - components, - } = useHtmlEditor() + const { value: fullValue, selected, components } = useHtmlEditor() let nodeType = value.type === 'text' ? 'text' : 'tag' if (value.type === 'component') { nodeType = 'component' + } else if (value.type === 'slot') { + nodeType = 'slot' } - const baseNodeTypes = ['text', 'tag'] - const nodeTypes = hasComponents - ? [...baseNodeTypes, 'component', 'slot'] - : baseNodeTypes + const nodeTypes = useNodeTypes() return (