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 (
-
- )
-}
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 (
-
-
+
+
-
+
)
@@ -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 (
-
- )
+export const GenericFieldset = ({ field, children }: GenericFieldsetProps) => {
+ return
}
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 (
{
- if (value === 'text') {
+ onChange={(newType) => {
+ if (newType === 'text') {
onChange({ type: 'text', value: '' })
- } else if (value === 'component') {
+ } else if (newType === 'component') {
const firstComponent = components?.[0]
if (firstComponent) {
- onChange(firstComponent)
+ onChange({
+ ...firstComponent,
+ props: value.props,
+ children: value.children,
+ })
}
- } else if (value === 'slot') {
+ } else if (newType === 'slot') {
onChange({
type: 'slot',
name: 'newSlot',
@@ -91,7 +97,8 @@ export function NodeEditor({
onChange({
type: 'element',
tagName: 'div',
- children: [],
+ props: value.props,
+ children: value.children ?? [],
})
}
}}
@@ -165,17 +172,18 @@ function NodeSwitch({ value, onChange }: EditorProps) {
return
}
+ const tagKey = [...(selected || []), value.tagName || ''].join('-')
+
return (
-
-
-
{' '}
+
+ {' '}
{
if (!filterValue) {
return HTML_TAGS
@@ -193,12 +201,16 @@ function NodeSwitch({ value, onChange }: EditorProps) {
...defaultAttributes,
...(value.attributes || {}),
}
- onChange({
+ const fullValue = {
...value,
attributes: mergedAttributes,
tagName: selectedItem,
style: mergedStyles,
- })
+ }
+ if (isProseElement(selectedItem) && !fullValue.children?.length) {
+ fullValue.children = [{ type: 'text', value: '' }]
+ }
+ onChange(fullValue)
}}
items={HTML_TAGS}
value={value.tagName}
@@ -214,6 +226,5 @@ function NodeSwitch({ value, onChange }: EditorProps) {
/>
-
)
}
diff --git a/packages/gui/src/components/html/Editors/util.ts b/packages/gui/src/components/html/Editors/util.ts
new file mode 100644
index 00000000..d5d83aa9
--- /dev/null
+++ b/packages/gui/src/components/html/Editors/util.ts
@@ -0,0 +1,8 @@
+import { useHtmlEditor } from '../Provider'
+
+export const useNodeTypes = () => {
+ const { hasComponents } = useHtmlEditor()
+
+ const baseNodeTypes = ['text', 'tag']
+ return hasComponents ? [...baseNodeTypes, 'component', 'slot'] : baseNodeTypes
+}
diff --git a/packages/gui/src/components/html/FontTags.tsx b/packages/gui/src/components/html/FontTags.tsx
index 31279c0a..b2a7f0c7 100644
--- a/packages/gui/src/components/html/FontTags.tsx
+++ b/packages/gui/src/components/html/FontTags.tsx
@@ -1,11 +1,14 @@
import { debounce, uniq } from 'lodash-es'
import { useEffect, useState } from 'react'
+import { stringifyFontFamily } from '../../lib/stringify'
+import { Theme } from '../../types/theme'
import {
buildFontFamiliesHref,
buildVariableFontFamiliesHref,
} from '../inputs/FontFamily/FontTags'
+import { useTheme } from '../providers/ThemeContext'
-export function getStyleFonts(style: any): string[] {
+export function getStyleFonts(style: any, theme?: Theme): string[] {
if (!style) return []
let fonts: string[] = []
@@ -19,10 +22,10 @@ export function getStyleFonts(style: any): string[] {
}
}
- return uniq(fonts)
+ return uniq(fonts).map((font) => stringifyFontFamily(font, theme))
}
-export function getHTMLTreeFonts(root: any): string[] {
+export function getHTMLTreeFonts(root: any, theme?: Theme): string[] {
if (!root) return []
let treeFonts: any[] = []
@@ -31,7 +34,7 @@ export function getHTMLTreeFonts(root: any): string[] {
}
if (root.type === 'component' && root.value) {
- return [...treeFonts, ...getHTMLTreeFonts(root.value)]
+ return [...treeFonts, ...getHTMLTreeFonts(root.value, theme)]
}
if (!root.children) {
@@ -40,11 +43,11 @@ export function getHTMLTreeFonts(root: any): string[] {
for (const node of root.children) {
if (node.type !== 'text') {
- treeFonts = [...treeFonts, ...getHTMLTreeFonts(node)]
+ treeFonts = [...treeFonts, ...getHTMLTreeFonts(node, theme)]
}
}
- return uniq(treeFonts)
+ return uniq(treeFonts).map((font) => stringifyFontFamily(font, theme))
}
interface BuildHrefProps {
@@ -52,14 +55,18 @@ interface BuildHrefProps {
style: any
setStaticHref: Function
setVariableHref: Function
+ theme?: Theme
}
async function buildHrefs({
tree,
style,
setStaticHref,
setVariableHref,
+ theme,
}: BuildHrefProps) {
- const fonts = style ? getStyleFonts(style) : getHTMLTreeFonts(tree)
+ const fonts = style
+ ? getStyleFonts(style, theme)
+ : getHTMLTreeFonts(tree, theme)
const staticHref = await buildFontFamiliesHref(fonts)
const variableHref = await buildVariableFontFamiliesHref(fonts)
@@ -74,6 +81,7 @@ interface Props {
style?: any
}
export function HTMLFontTags({ htmlTree = {}, style }: Props) {
+ const theme = useTheme()
const [staticHref, setStaticHref] = useState('')
const [variableHref, setVariableHref] = useState('')
@@ -83,6 +91,7 @@ export function HTMLFontTags({ htmlTree = {}, style }: Props) {
style,
setStaticHref,
setVariableHref,
+ theme,
})
}, [htmlTree, style])
diff --git a/packages/gui/src/components/html/Import.tsx b/packages/gui/src/components/html/Import.tsx
new file mode 100644
index 00000000..e5b79bd3
--- /dev/null
+++ b/packages/gui/src/components/html/Import.tsx
@@ -0,0 +1,109 @@
+import { startCase } from 'lodash-es'
+import { ChangeEvent, useState } from 'react'
+import { HtmlNode } from './types'
+import * as parsers from '../../lib/parsers'
+import { htmlToMd, mdToHtml } from '../../lib'
+
+const PRE_STYLES = {
+ overflow: 'auto',
+ height: '80vh',
+ border: 'thin solid',
+ borderColor: 'border',
+ backgroundColor: 'rgba(0, 0, 0, 0.02)',
+ display: 'block',
+ minWidth: '100%',
+ width: '-webkit-fill-available',
+ p: 2,
+ my: 3,
+}
+
+const FORMATS: string[] = ['html', 'md']
+
+const DISPLAY_NAMES: Record = {
+ html: 'HTML',
+ md: 'Markdown',
+}
+
+type ImportProps = {
+ onChange(newValue: HtmlNode): void
+}
+export const Import = ({ onChange }: ImportProps) => {
+ const [src, setSrc] = useState('')
+ const [format, setFormat] = useState('html')
+
+ const handleSetFormat = (e: ChangeEvent) => {
+ const newFormat = e.target.value
+
+ let newSrc = src
+ if (newFormat === 'md') {
+ newSrc = htmlToMd(src)
+ } else if (newFormat === 'html') {
+ newSrc = mdToHtml(src)
+ }
+
+ setFormat(newFormat)
+ setSrc(newSrc)
+ }
+
+ const handleImport = () => {
+ // @ts-ignore
+ const newValue = parsers[format](src)
+ onChange(newValue)
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/gui/src/components/html/Provider.tsx b/packages/gui/src/components/html/Provider.tsx
index 554d135d..8db5b0e1 100644
--- a/packages/gui/src/components/html/Provider.tsx
+++ b/packages/gui/src/components/html/Provider.tsx
@@ -15,15 +15,22 @@ const DEFAULT_HTML_EDITOR_VALUE = {
I'm a link!
`),
+ isEditing: false,
+ setEditing: () => {},
hasComponents: false,
+ update: () => {},
}
export type HtmlEditor = {
value: HtmlNode
+ update(value: HtmlNode): void
theme?: any
selected: ElementPath | null
- setSelected: (newSelection: ElementPath | null) => void
+ setSelected(newSelection: ElementPath | null): void
+ isEditing: boolean
+ setEditing(value: boolean): void
components?: ComponentData[]
+ updateComponent?(newComponent: ComponentData): void
hasComponents: boolean
}
@@ -46,7 +53,7 @@ export const transformValueToSchema = (value: any): ElementData => {
const fullValue = coerceNodeIntoUnist(value)
const transformed = Object.entries(fullValue).reduce((acc, [key, val]) => {
- let updatedValue = val
+ let updatedValue: any = val
if (key === 'children' && Array.isArray(val)) {
updatedValue = val.map((child) => transformValueToSchema(child))
} else if (key === 'style') {
@@ -74,9 +81,11 @@ export const transformValueToSchema = (value: any): ElementData => {
type HtmlEditorProviderProps = {
value: HtmlNode
+ onChange(value: HtmlNode): void
children: ReactNode
theme?: any
components?: ComponentData[]
+ updateComponent?(newComponent: ComponentData): void
}
export function HtmlEditorProvider({
@@ -84,8 +93,11 @@ export function HtmlEditorProvider({
value,
theme,
components = [],
+ updateComponent,
+ onChange,
}: HtmlEditorProviderProps) {
const [selected, setSelected] = useState([])
+ const [isEditing, setEditing] = useState(false)
const transformedValue = transformValueToSchema(value)
const fullContext = {
@@ -94,7 +106,11 @@ export function HtmlEditorProvider({
setSelected: (newSelection: ElementPath | null) =>
setSelected(newSelection),
components,
+ isEditing,
+ setEditing: (newValue: any) => setEditing(newValue),
+ updateComponent,
hasComponents: !!components.length,
+ update: onChange,
}
return (
diff --git a/packages/gui/src/components/html/Renderer/Element.tsx b/packages/gui/src/components/html/Renderer/Element.tsx
index ef70f4fe..8b8b3a82 100644
--- a/packages/gui/src/components/html/Renderer/Element.tsx
+++ b/packages/gui/src/components/html/Renderer/Element.tsx
@@ -1,6 +1,7 @@
import { isVoidElement } from '../../../lib/elements'
import { CanvasElementProps, useCanvasProps } from '../CanvasProvider'
import { ComponentProvider, useComponent } from '../Component'
+import { SlotProvider } from '../Component/SlotProvider'
import { mergeComponentAttributes } from '../Component/util'
import { ComponentData, ElementPath, HtmlNode, Slot } from '../types'
import { removeTailFromPath } from '../util'
@@ -11,7 +12,13 @@ export function ElementRenderer({
path,
...canvasElementProps
}: CanvasElementProps) {
- const props = useCanvasProps({ value, path, ...canvasElementProps })
+ const { selectComponent, value: componentValue } = useComponent()
+ const { onClick, ...props } = useCanvasProps({
+ value,
+ path,
+ component: componentValue,
+ ...canvasElementProps,
+ })
if (value.type === 'slot') {
return
@@ -21,12 +28,28 @@ export function ElementRenderer({
const Tag: any = value.tagName || 'div'
+ const handleClick = (e: MouseEvent) => {
+ if (selectComponent) {
+ return selectComponent(e)
+ }
+
+ onClick(e)
+ }
+
if (isVoidElement(Tag)) {
- return
+ return
+ }
+
+ if (Tag === 'textarea' && value.children) {
+ return (
+
+ {value.children.map((child) => child.value).join(' ')}
+
+ )
}
return (
-
+
)
@@ -42,7 +65,7 @@ function ChildrenRenderer({ value = [], path }: ChildrenRendererProps) {
{value.map((child, i) => {
const childPath: ElementPath = [...path, i]
if (child.type === 'text') {
- return
+ return
}
return
@@ -56,7 +79,6 @@ interface ComponentRendererProps {
path: ElementPath
}
export function ComponentRenderer({ value, path }: ComponentRendererProps) {
- const { onClick } = useCanvasProps({ value, path })
const fullValue = {
...value.value,
attributes: mergeComponentAttributes(value),
@@ -64,7 +86,7 @@ export function ComponentRenderer({ value, path }: ComponentRendererProps) {
return (
-
+
)
}
@@ -109,5 +131,10 @@ function SlotRenderer({ value, path }: SlotRendererProps) {
const slotValue = outerProps[value.name] || value.value
const textNode: HtmlNode = { type: 'text', value: slotValue as string }
- return
+
+ return (
+
+
+
+ )
}
diff --git a/packages/gui/src/components/html/Renderer/Text.tsx b/packages/gui/src/components/html/Renderer/Text.tsx
index 0a9bd36f..6ca70ac4 100644
--- a/packages/gui/src/components/html/Renderer/Text.tsx
+++ b/packages/gui/src/components/html/Renderer/Text.tsx
@@ -1,9 +1,90 @@
-import { ElementPath, HtmlNode } from '../types'
+import { FormEvent, useCallback, useState } from 'react'
+import { useCanvas } from '../CanvasProvider'
+import { useComponent } from '../Component'
+import { useSlot } from '../Component/SlotProvider'
+import { useHtmlEditor } from '../Provider'
+import { ElementPath, HtmlNode, Slot } from '../types'
+import { setChildAtPath } from '../util'
type TextRendererProps = {
value: HtmlNode
path: ElementPath
}
-export function TextRenderer({ value }: TextRendererProps) {
- return <>{value.value}>
+export function TextRenderer({
+ value: providedValue,
+ path,
+}: TextRendererProps) {
+ const { value: slot } = useSlot()
+ const {
+ value: component,
+ updateComponent,
+ updateComponentSlot,
+ } = useComponent()
+ const { value: fullValue, update } = useHtmlEditor()
+ const { canvas } = useCanvas()
+ const [text, setText] = useState(providedValue.value as string)
+ const [editing, setEditing] = useState(false)
+
+ const textRef = useCallback(() => {
+ if (!editing) {
+ return setText(providedValue.value as string)
+ }
+ }, [providedValue])
+
+ const handleInput = (e: FormEvent) => {
+ if (slot) {
+ return handleSlotInput(e)
+ } else if (component) {
+ return handleComponentInput(e)
+ }
+
+ // We still handle the text and component types a little funky so
+ // TS isn't happy here. Need to fix that...
+ // @ts-ignore
+ const newText: HtmlNode = {
+ ...providedValue,
+ value: e.currentTarget.textContent || '',
+ }
+ const newValue = setChildAtPath(fullValue, path, newText)
+ update(newValue)
+ }
+
+ const handleComponentInput = (e: FormEvent) => {
+ const newText = e.currentTarget.textContent || ''
+ // @ts-ignore
+ const newTextNode: HtmlNode = {
+ ...providedValue,
+ value: newText,
+ }
+
+ updateComponent!(path, newTextNode)
+ }
+
+ const handleSlotInput = (e: FormEvent) => {
+ const newSlot: Slot = {
+ ...(slot as Slot),
+ value: e.currentTarget.textContent ?? '',
+ }
+
+ updateComponentSlot!(newSlot)
+ }
+
+ if (!canvas) {
+ return <>{providedValue.value || null}>
+ }
+
+ return (
+ setEditing(false)}
+ onClick={() => setEditing(true)}
+ >
+ {text}
+
+ )
}
diff --git a/packages/gui/src/components/html/TreeNode.tsx b/packages/gui/src/components/html/TreeNode.tsx
index 0115abbc..2903c679 100644
--- a/packages/gui/src/components/html/TreeNode.tsx
+++ b/packages/gui/src/components/html/TreeNode.tsx
@@ -1,13 +1,16 @@
import { HtmlNode, ElementPath } from './types'
import * as Collapsible from '@radix-ui/react-collapsible'
-import { Fragment, useState } from 'react'
+import { useState } from 'react'
import { useHtmlEditor } from './Provider'
-import { isVoidElement } from '../../lib/elements'
+import { isProseElement, isVoidElement } from '../../lib/elements'
import { addChildAtPath, isSamePath, replaceAt } from './util'
import { hasChildrenSlot } from '../../lib/codegen/util'
import { Combobox } from '../primitives'
import { HTML_TAGS } from './data'
import { DEFAULT_ATTRIBUTES, DEFAULT_STYLES } from './default-styles'
+import { Plus } from 'react-feather'
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
+import { useNodeTypes } from './Editors/util'
interface EditorProps {
value: HtmlNode
@@ -19,13 +22,17 @@ interface TreeNodeProps extends EditorProps {
}
export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
- const { selected } = useHtmlEditor()
+ const { selected, isEditing, setEditing, components } = useHtmlEditor()
const [open, setOpen] = useState(true)
- const [editing, setEditing] = useState(false)
const isSelected = isSamePath(path, selected)
+ const isEditingNode = isSelected && isEditing
- if (editing && !isSelected) {
- setEditing(false)
+ function handleSelect() {
+ // If we are selecting a different node than the currently selected node, move out of editing mode
+ if (!isSelected) {
+ setEditing(false)
+ }
+ onSelect(path)
}
if (value.type === 'text') {
@@ -41,10 +48,14 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
textAlign: 'start',
fontSize: 0,
width: '100%',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ }}
+ onClick={() => {
+ handleSelect()
}}
- onClick={() => onSelect(path)}
>
- {editing ? (
+ {isEditingNode ? (