<>
-
-
-
-
-
-
+
+
+
@@ -85,7 +90,7 @@ export default function BorderImage() {
-
+
(initialStyles)
+
+ return (
+ <>
+
+
+
+
+ “The parameters comprise sequences which are theoretically infinite
+ but limits are, of course, set to them in practice. There is an
+ upward limit to size and certainly a downward one... Within these
+ sequences there are reasonable bounds; extremes set by technical and
+ functional experience”
+
+
+ In{' '}
+
+ Designing Programmes
+ {' '}
+ by Karl Gerstner
+
+
+
+
+
+
+ {codegen.css(styles)}
+
+
+ >
+ )
+}
diff --git a/apps/docs/pages/examples/borders.tsx b/apps/docs/pages/examples/borders.tsx
index 47198365..d3a67b51 100644
--- a/apps/docs/pages/examples/borders.tsx
+++ b/apps/docs/pages/examples/borders.tsx
@@ -6,7 +6,63 @@ import { Container } from '../../components/Container'
const initialStyles = {
padding: {
- value: 64,
+ top: {
+ value: 64,
+ unit: 'px',
+ },
+ },
+ margin: {
+ top: {
+ value: 0,
+ unit: 'px',
+ },
+ },
+ borderTopLeftRadius: [
+ {
+ value: 16,
+ unit: 'px',
+ },
+ ],
+ borderTopRightRadius: [
+ {
+ value: 16,
+ unit: 'px',
+ },
+ ],
+ borderBottomLeftRadius: [
+ {
+ value: 16,
+ unit: 'px',
+ },
+ ],
+ borderBottomRightRadius: [
+ {
+ value: 16,
+ unit: 'px',
+ },
+ ],
+ borderLeftColor: '#6465ff',
+ borderLeftStyle: 'double',
+ borderLeftWidth: {
+ value: 16,
+ unit: 'px',
+ },
+ borderRightColor: '#6465ff',
+ borderRightStyle: 'groove',
+ borderRightWidth: {
+ value: 16,
+ unit: 'px',
+ },
+ borderTopColor: '#6465ff',
+ borderTopStyle: 'dotted',
+ borderTopWidth: {
+ value: 16,
+ unit: 'px',
+ },
+ borderBottomColor: '#6465ff',
+ borderBottomStyle: 'dashed',
+ borderBottomWidth: {
+ value: 16,
unit: 'px',
},
}
@@ -17,43 +73,13 @@ export default function BorderImage() {
return (
<>
-
-
-
-
Color
-
-
-
-
- Style
-
-
-
-
- Width
-
-
-
-
- Radius
-
-
-
-
- Spacing
-
-
-
-
-
-
+
“The parameters comprise sequences which are theoretically infinite
but limits are, of course, set to them in practice. There is an
@@ -67,14 +93,53 @@ export default function BorderImage() {
href="https://www.lars-mueller-publishers.com/designing-programmes-0"
passHref={true}
>
- Designing Programmes
+ Designing Programmes
{' '}
by Karl Gerstner
+
+
+
Borders
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Radius
+
+
+
+
+
+
+ Spacing
+
+
+
+
+
-
+
(initialStyles)
-
- return (
-
- )
-}
diff --git a/apps/docs/pages/examples/filters.tsx b/apps/docs/pages/examples/filters.tsx
index 8a7124a4..68ca6a3a 100644
--- a/apps/docs/pages/examples/filters.tsx
+++ b/apps/docs/pages/examples/filters.tsx
@@ -1,47 +1,48 @@
-import { Editor, styled } from '@compai/css-gui'
+import { Editor, Inputs, styled, toCSSObject } from '@compai/css-gui'
import { useState } from 'react'
import { Container } from '../../components/Container'
-const initialStyles = {
- filter: [{ type: 'sepia', amount: { value: 50, unit: '%' } }],
-}
-
export default function Filters() {
- const [styles, setStyles] = useState
(initialStyles)
+ const [styles, setStyles] = useState({
+ filter: [
+ {
+ name: 'blur',
+ arguments: { value: 8, unit: 'px' },
+ },
+ ],
+ })
+
return (
-
-
-
-
+
+
+
+
- Fun with filters
-
-
+
+ Fun with filters
+
+
)
diff --git a/apps/docs/pages/examples/flex.tsx b/apps/docs/pages/examples/flex.tsx
index ff3a28e6..d22d99f8 100644
--- a/apps/docs/pages/examples/flex.tsx
+++ b/apps/docs/pages/examples/flex.tsx
@@ -1,8 +1,6 @@
import { useState } from 'react'
-import Link from 'next/link'
import { Editor, Inputs, styled, codegen } from '@compai/css-gui'
import { defaultTheme } from '../../data/default-theme'
-import { Container } from '../../components/Container'
const initialStyles = {
display: 'flex',
@@ -35,7 +33,7 @@ export default function TextDecoration() {
@@ -72,23 +70,94 @@ export default function TextDecoration() {
-
- One
- Two
- Three
- Four
- Five
- Six
- Seven
- Eight
- Nine
- Ten
- Eleven
- Twelve
+
+
+ One
+
+
+
+ Two
+
+
+
+ Three
+
+
+
+ Four
+
+
+
+ Five
+
+
+
+ Six
+
+
+
+ Seven
+
+
+
+ Eight
+
+
+
+ Nine
+
+
+
+ Ten
+
+
+
+ Eleven
+
+
+
+ Twelve
+
-
+
({
const initialStyles = {
transform: [
- { type: 'perspective', amount: { value: 512, unit: 'px' } },
- { type: 'rotateX', amount: { value: 15, unit: 'deg' } },
- { type: 'rotateY', amount: { value: 15, unit: 'deg' } },
+ { name: 'perspective', arguments: { value: 512, unit: 'px' } },
+ { name: 'rotateX', arguments: { value: 15, unit: 'deg' } },
+ { name: 'rotateY', arguments: { value: 15, unit: 'deg' } },
],
}
diff --git a/apps/docs/pages/examples/grid.tsx b/apps/docs/pages/examples/grid.tsx.tmp
similarity index 90%
rename from apps/docs/pages/examples/grid.tsx
rename to apps/docs/pages/examples/grid.tsx.tmp
index b4b0accb..8d360fcb 100644
--- a/apps/docs/pages/examples/grid.tsx
+++ b/apps/docs/pages/examples/grid.tsx.tmp
@@ -45,21 +45,21 @@ export default function GridExample() {
return (
- Container
+ Container
<>
>
- Child
+ Child
<>
@@ -72,7 +72,7 @@ export default function GridExample() {
{[...Array(100)].map((n, i) => {
return (
-
{i}
+
{i}
)
})}
diff --git a/apps/docs/pages/examples/masks.tsx b/apps/docs/pages/examples/masks.tsx
index e51cffcd..909918c7 100644
--- a/apps/docs/pages/examples/masks.tsx
+++ b/apps/docs/pages/examples/masks.tsx
@@ -8,23 +8,19 @@ const initialStyles = {
{
clip: 'border-box',
image: {
- type: 'url',
- arguments: ['https://source.unsplash.com/random'],
+ name: 'url',
+ arguments: 'https://dlu344star2bj.cloudfront.net/i/3090-0015.jpg',
},
origin: 'border-box',
position: {
- x: { value: 0, type: '%' },
- y: { value: 0, type: '%' },
- },
- repeat: {
- x: 'no-repeat',
- y: 'no-repeat',
- },
- size: {
- type: 'dimensions',
- x: { value: 100, unit: '%' },
- y: { value: 100, unit: '%' },
+ x: { value: 0, unit: '%' },
+ y: { value: 0, unit: '%' },
},
+ repeat: ['no-repeat', 'no-repeat'],
+ size: [
+ { value: 100, unit: '%' },
+ { value: 100, unit: '%' },
+ ],
composite: 'add',
mode: 'luminance',
},
@@ -35,13 +31,16 @@ export default function MaskExample() {
const [styles, setStyles] = useState
(initialStyles)
return (
-
- img': {maxWidth: '100%', display: 'block'} }}>
-
-
-
-
-
+
+ img': { maxWidth: '100%', display: 'block' } }}>
+
+
+
+
+
)
diff --git a/apps/docs/pages/examples/offset.tsx b/apps/docs/pages/examples/offset.tsx
index feb434ad..67c926f6 100644
--- a/apps/docs/pages/examples/offset.tsx
+++ b/apps/docs/pages/examples/offset.tsx
@@ -2,20 +2,25 @@ import { Editor, toCSSObject } from '@compai/css-gui'
import { useState } from 'react'
const initialStyles = {
- offsetPath: {
- shape: {
- type: 'path',
- path: 'M 10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80',
+ offset: {
+ path: {
+ type: 'shape',
+ shape: {
+ name: 'path',
+ arguments: 'M 10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80',
+ },
+ box: 'margin-box',
+ },
+ anchor: { x: 'center', y: 'center' },
+ distance: { value: 50, unit: '%' },
+ rotate: { value: 0, unit: 'deg' },
+ position: {
+ x: { value: 50, unit: '%' },
+ y: { value: 50, unit: '%' },
},
- box: 'margin-box',
- },
- offsetAnchor: {
- x: { value: 'center', unit: 'keyword' },
- y: { value: 'center', unit: 'keyword' },
},
- offsetDistance: { value: 50, unit: '%' },
- offsetRotate: { value: 0, unit: 'deg' },
}
+
export default function OffsetExample() {
const [styles, setStyles] = useState(initialStyles)
return (
diff --git a/apps/docs/pages/examples/shadows.tsx b/apps/docs/pages/examples/shadows.tsx
index 906f2811..da9860ea 100644
--- a/apps/docs/pages/examples/shadows.tsx
+++ b/apps/docs/pages/examples/shadows.tsx
@@ -1,4 +1,4 @@
-import { Editor, styled } from '@compai/css-gui'
+import { Editor, styled, toCSSObject } from '@compai/css-gui'
import { useState } from 'react'
import { Container } from '../../components/Container'
@@ -45,22 +45,22 @@ export default function Shadows() {
justifyContent: 'center',
}}
>
-
Fun with shadows
-
+
-
diff --git a/apps/docs/pages/examples/shape-outside.tsx b/apps/docs/pages/examples/shape-outside.tsx
index 0e758db3..4b4aea2e 100644
--- a/apps/docs/pages/examples/shape-outside.tsx
+++ b/apps/docs/pages/examples/shape-outside.tsx
@@ -5,12 +5,11 @@ const initialStyles = {
shapeOutside: {
type: 'shape',
shape: {
- type: 'inset',
- top: { value: 2, unit: 'px' },
- right: { value: 2, unit: 'px' },
- bottom: { value: 2, unit: 'px' },
- left: { value: 2, unit: 'px' },
- borderRadius: { value: 16, unit: 'px' },
+ name: 'inset',
+ arguments: {
+ offset: { top: { value: 2, unit: 'px' } },
+ borderRadius: { value: 16, unit: 'px' },
+ },
},
box: 'margin-box',
},
@@ -28,9 +27,8 @@ export default function ShapeOutsideExample() {
const [styles, setStyles] = useState
(initialStyles)
return (
-
-
-
+
)
}
@@ -52,7 +57,7 @@ function getDerivedStyles(shapeOutside: any) {
})
}
return toCSSObject({
- backgroundColor: 'tomato',
+ backgroundColor: '#6465ff',
clipPath: shapeOutside,
})
}
diff --git a/apps/docs/pages/examples/simple-button.tsx b/apps/docs/pages/examples/simple-button.tsx
new file mode 100644
index 00000000..097d01ec
--- /dev/null
+++ b/apps/docs/pages/examples/simple-button.tsx
@@ -0,0 +1,146 @@
+import { Editor, Fieldset, Inputs, codegen, toCSSObject } from '@compai/css-gui'
+import { useState } from 'react'
+import { defaultTheme } from '../../data/default-theme'
+
+const initialStyles = {
+ color: '#fff',
+ backgroundColor: '#6465ff',
+ borderRadius: {
+ value: 6,
+ unit: 'px',
+ },
+ appearance: 'none',
+ borderWidth: {
+ top: {
+ value: 0,
+ unit: 'px',
+ },
+ },
+ fontWeight: 600,
+ fontSize: { value: 16, unit: 'px' },
+ outlineWidth: {
+ value: 0,
+ unit: 'px',
+ },
+ outlineColor: 'rgba(255,255,255,0)',
+ outlineOffset: {
+ value: 0,
+ unit: 'px',
+ },
+ hover: {
+ color: '#ffffff',
+ backgroundColor: '#3e38b0',
+ outlineColor: '#3e38b0',
+ },
+ focus: {
+ color: '#ffffff',
+ backgroundColor: '#3e38b0',
+ outlineWidth: {
+ value: 4,
+ unit: 'px',
+ },
+ outlineColor: '#3e38b0',
+ outlineStyle: 'solid',
+ outlineOffset: {
+ value: 4,
+ unit: 'px',
+ },
+ },
+ active: {
+ color: '#ffffff',
+ backgroundColor: '#8170ff',
+ outlineColor: '#8170ff',
+ },
+}
+
+export default function SimpleButton() {
+ const [styles, setStyles] = useState
(initialStyles)
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hover
+
+
+
+
+
+
+
+
+
+ Focus
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Active
+
+
+
+
+
+
+
+
+
+
+
+ Click Here
+
+
+
+ {codegen.css(styles)}
+
+
+ )
+}
diff --git a/apps/docs/pages/examples/svg.tsx b/apps/docs/pages/examples/svg.tsx
index e5ffbd6c..69c141e7 100644
--- a/apps/docs/pages/examples/svg.tsx
+++ b/apps/docs/pages/examples/svg.tsx
@@ -1,46 +1,17 @@
-import { codegen, Editor, toCSSObject } from '@compai/css-gui'
+import { Editor, parseStyles, toCSSObject } from '@compai/css-gui'
import { useState } from 'react'
import { defaultTheme } from '../../data/default-theme'
-import { Container } from '../../components/Container'
-const initialStyles = {
+const initialStyles = parseStyles({
stroke: '#fff',
fill: 'none',
- strokeAlignment: {
- value: 'center',
- unit: 'keyword',
- },
- strokeWidth: {
- value: 8,
- unit: 'px',
- },
- strokeLinejoin: {
- value: 'round',
- unit: 'keyword',
- },
- strokeLinecap: {
- value: 'square',
- unit: 'keyword',
- },
- strokeDashadjust: {
- value: 'dashed',
- unit: 'keyword',
- },
- strokeDashcorner: {
- value: 0,
- unit: 'px',
- },
- strokeDashoffset: {
- value: 0,
- unit: 'px',
- },
- strokeDasharray: [
- { value: 0, unit: 'number' },
- { value: 8, unit: 'number' },
- { value: 0, unit: 'number' },
- { value: 24, unit: 'number' },
- ],
-}
+ strokeAlignment: 'center',
+ strokeWidth: '8px',
+ strokeLinejoin: 'round',
+ strokeLinecap: 'square',
+ strokeDashadjust: 'dashed',
+ strokeDasharray: '1, 20, 1, 15',
+})
export default function SvgExample() {
const [styles, setStyles] = useState(initialStyles)
diff --git a/apps/docs/pages/examples/text-decoration.tsx b/apps/docs/pages/examples/text-decoration.tsx
index 45fbcc58..b89b0ffb 100644
--- a/apps/docs/pages/examples/text-decoration.tsx
+++ b/apps/docs/pages/examples/text-decoration.tsx
@@ -1,32 +1,17 @@
import { useState } from 'react'
import Link from 'next/link'
-import { Editor, Inputs, styled, codegen } from '@compai/css-gui'
+import { Editor, Inputs, styled, codegen, parseStyles } from '@compai/css-gui'
import { defaultTheme } from '../../data/default-theme'
import { Container } from '../../components/Container'
-const initialStyles = {
- textDecorationColor: 'primary',
- textDecorationThickness: {
- value: 8,
- unit: 'px',
- },
- textDecorationLine: 'underline',
- textDecorationStyle: 'wavy',
+const initialStyles = parseStyles({
+ textDecoration: 'tomato 8px underline wavy',
// Font
- fontSize: {
- value: 3,
- unit: 'rem',
- },
- letterSpacing: {
- value: 'initial',
- unit: 'keyword',
- },
- lineHeight: {
- value: '1.5',
- unit: 'number',
- },
- fontFamily: 'Space Mono',
-}
+ fontSize: '3rem',
+ letterSpacing: 'initial',
+ lineHeight: '1.25',
+ // fontFamily: 'Space Mono',
+})
export default function TextDecoration() {
const [styles, setStyles] = useState(initialStyles)
@@ -34,7 +19,6 @@ export default function TextDecoration() {
return (
<>
<>
Text Decoration
-
-
+ {/*
+
+ */}
+
-
Font
@@ -65,25 +50,22 @@ export default function TextDecoration() {
“The parameters comprise sequences which are theoretically infinite
- but limits are, of course, set to them in practice. There is an
- upward limit to size and certainly a downward one... Within these
- sequences there are reasonable bounds; extremes set by technical and
- functional experience”
-
-
+ but limits are, of course, set to them in practice.“
+
+
In{' '}
- Designing Programmes
+ Designing Programmes
{' '}
by Karl Gerstner
-
+
-
Fun with transforms
-
+
(initialStyles)
return (
-
-
-
+
+
+
div': { display: 'grid', gap: '1em' } }}>
diff --git a/apps/docs/pages/examples/typography-group.tsx b/apps/docs/pages/examples/typography-group.tsx
index ebadb161..9686b269 100644
--- a/apps/docs/pages/examples/typography-group.tsx
+++ b/apps/docs/pages/examples/typography-group.tsx
@@ -2,7 +2,7 @@ import { TypographyGroupExample } from '../../components/examples/TypographyGrou
export default function TypographyGroup() {
return (
-
+
)
diff --git a/apps/docs/pages/examples/typography.tsx b/apps/docs/pages/examples/typography.tsx
index 59f7be1b..29bb9666 100644
--- a/apps/docs/pages/examples/typography.tsx
+++ b/apps/docs/pages/examples/typography.tsx
@@ -2,39 +2,35 @@ import { useState } from 'react'
import Link from 'next/link'
import { Editor, Inputs, styled, codegen } from '@compai/css-gui'
import { defaultTheme } from '../../data/default-theme'
-import { Container } from '../../components/Container'
const initialStyles = {
- color: 'text',
- backgroundColor: 'background',
+ color: { type: 'theme', path: 'text' },
+ backgroundColor: { type: 'theme', path: 'background' },
fontFamily: 'Recursive',
fontSize: {
value: 3,
unit: 'rem',
},
- letterSpacing: {
- value: 'initial',
- unit: 'keyword',
- },
+ letterSpacing: 'initial',
lineHeight: {
- value: '1.5',
+ value: 1.5,
unit: 'number',
},
- textDecorationColor: 'primary',
+ textDecorationColor: { type: 'theme', path: 'primary' },
textDecorationThickness: {
value: 0,
unit: 'px',
},
textDecorationLine: 'none',
- textDecorationStyle: 'none',
+ textDecorationStyle: 'solid',
width: {
value: 100,
- unit: '%'
+ unit: '%',
},
maxWidth: {
value: 42,
- unit: 'em'
- }
+ unit: 'em',
+ },
}
export default function Typography() {
@@ -47,8 +43,15 @@ export default function Typography() {
display: 'flex',
}}
>
-
-
+
“The parameters comprise sequences which are theoretically infinite
but limits are, of course, set to them in practice. There is an
@@ -69,67 +72,77 @@ export default function Typography() {
-
- <>
- Typography
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
-
-
+
+ <>
+ Typography
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
-
-
+
+
CSS GUI
v{pkg.version}
- A composable, extensible, and themeable controls for visually editing CSS.
+ A composable, extensible, and themeable controls for visually editing
+ CSS.
-
- Everyone should be able to explore the creative potential of CSS.
- This project is a growing set of parametric controls for rapidly editing CSS properties.
- Designed for composability, mix and match any combination of properties
- to create custom components and tap into the vast and beautiful world of CSS.
-
+
+ Everyone should be able to explore the creative potential of CSS. This
+ project is a growing set of parametric controls for rapidly editing
+ CSS properties. Designed for composability, mix and match any
+ combination of properties to create custom components and tap into the
+ vast and beautiful world of CSS.
+
Install
npm install --save @compai/css-gui
- Features
-
- Controls for 258 CSS properties
- +1000 Google Fonts
- Full variable fonts support
- Responsive value arrays
- Theme aware inputs
- Scrubbable number inputs
- Supports all CSS units
- Advanced layer based gradient editor
- Nested elements
- Cubic bezier editor for custom easings
- Style pseudo-elements and pseudo-classes
- Completely open source
-
+ Demo
+
+
+ Codesandbox
+
+ Features
+
+ Controls for 280 CSS properties
+ +1000 Google Fonts
+ Full variable fonts support
+ Responsive value arrays
+ Theme aware inputs
+ Scrubbable number inputs
+ Supports all CSS units
+ Advanced layer based gradient editor
+ Nested elements
+ Cubic bezier editor for custom easings
+ Style pseudo-elements and pseudo-classes
+ Completely open source
+
-
)
}
diff --git a/apps/docs/pages/html-editor.tsx b/apps/docs/pages/html-editor.tsx
new file mode 100644
index 00000000..79ffd9b4
--- /dev/null
+++ b/apps/docs/pages/html-editor.tsx
@@ -0,0 +1,42 @@
+import { HtmlEditor, HtmlRenderer, HtmlEditorProvider } from '@compai/css-gui'
+import { useState } from 'react'
+import { defaultTheme } from '../data/default-theme'
+import { initialValue } from '../data/initial-html-editor-data'
+
+export default function HtmlEditorExample() {
+ const [html, setHtml] = useState(initialValue)
+
+ return (
+
+
+
+ )
+}
diff --git a/apps/docs/pages/inputs/responsive.mdx b/apps/docs/pages/inputs/responsive.mdx
deleted file mode 100644
index 3f688426..00000000
--- a/apps/docs/pages/inputs/responsive.mdx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { FirstParagraph } from '../../components/FirstParagraph'
-import { ResponsiveExample } from '../../components/examples/Responsive'
-import { Container } from '../../components/Container'
-
-
-
-# Responsive Input
-
-
- A responsive utility for creating dynamic inputs based on breakpoints.
-
-
-Use this component to expose controls that target breakpoints or a single, global
-value. Responsive outputs contain an array of values based on the number of breakpoints
-in the theme.
-
-## Example
-
-
-
-## Usage
-
-```ts
-import { useState } from 'react'
-import {
- Length,
- ResponsiveLength,
- LengthInput,
- ResponsiveInput,
-} from '@compai/css-gui'
-
-const DEFAULT_VALUE = {
- value: 16,
- unit: 'px',
-}
-export const ResponsiveExample = () => {
- const [value, setValue] = useState(DEFAULT_VALUE)
-
- return (
- <>
-
- {JSON.stringify(value, null, 2)}
- >
- )
-}
-```
-
-
diff --git a/apps/docs/pages/introduction.mdx b/apps/docs/pages/introduction.mdx
index fe921eb0..9645e161 100644
--- a/apps/docs/pages/introduction.mdx
+++ b/apps/docs/pages/introduction.mdx
@@ -1,7 +1,6 @@
import { FirstParagraph } from '../components/FirstParagraph'
import { Container } from '../components/Container'
-
# Introduction
@@ -21,7 +20,10 @@ import { useState } from 'react'
import { Editor, styled } from '@compai/css-gui'
export const MyEditor = () => {
- const [styles, setStyles] = useState({})
+ const [styles, setStyles] = useState({
+ fontFamily: 'Recursive',
+ fontSize: { value: 16, unit: 'px' },
+ })
return (
<>
@@ -37,8 +39,8 @@ export const MyEditor = () => {
## Theme units
Many CSS properties accept a wide range of values. Designers and developers
-often develop scales for properties to help ensure that designs are visually harmonious,
-while also speeding up development by limiting the paradox of choice.
+often develop scales for properties to help ensure that designs are visually harmonious,
+while also speeding up development by limiting the paradox of choice.
Define a theme and use that to collapse n-dimensional space for what your custom component can output.
Controls still allow you to eject from constraints and enter any custom value, but will default
to the values in your theme for rapid design exploration within a visual brand.
diff --git a/apps/docs/pages/library/card.tsx b/apps/docs/pages/library/card.tsx
new file mode 100644
index 00000000..0b29287f
--- /dev/null
+++ b/apps/docs/pages/library/card.tsx
@@ -0,0 +1,174 @@
+import {
+ HtmlEditor,
+ HtmlRenderer,
+ HtmlEditorProvider,
+ htmlToEditorSchema,
+} from '@compai/css-gui'
+import { useState } from 'react'
+
+const initialValue: any = {
+ tagName: 'a',
+ href: '#',
+ style: {
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'stretch',
+ flexDirection: 'column',
+ // textDecoration: 'none',
+ overflow: ['hidden'],
+ height: 'auto',
+ },
+ children: [
+ {
+ tagName: 'section',
+ style: {
+ overflow: ['hidden'],
+ maxHeight: { value: 40, unit: 'vh' },
+ minHeight: {
+ type: 'responsive',
+ values: [
+ { value: 160, unit: 'px' },
+ { value: 256, unit: 'px' },
+ { value: 256, unit: 'px' },
+ ],
+ },
+ },
+ children: [
+ {
+ tagName: 'img',
+ style: {
+ width: { value: 100, unit: '%' },
+ display: 'block',
+ },
+ attributes: {
+ src: 'https://dlu344star2bj.cloudfront.net/i/3090-0015.jpg',
+ },
+ children: [],
+ },
+ ],
+ },
+ {
+ tagName: 'div',
+ style: {
+ height: { value: 100, unit: '%' },
+ display: 'flex',
+ flexDirection: 'column',
+ paddingTop: { value: 32, unit: 'px' },
+ paddingBottom: { value: 32, unit: 'px' },
+ },
+ children: [
+ {
+ tagName: 'h2',
+ style: {
+ marginTop: { value: 0, unit: 'px' },
+ marginBottom: { value: 0, unit: 'px' },
+ fontWeight: '900',
+ fontSize: {
+ type: 'responsive',
+ values: [
+ { value: 24, unit: 'px' },
+ { value: 32, unit: 'px' },
+ { value: 48, unit: 'px' },
+ ],
+ },
+ lineHeight: { value: 1.25, unit: 'number' },
+ },
+ children: ['Hello, world!'],
+ },
+ {
+ tagName: 'h3',
+ style: {
+ marginTop: { value: 8, unit: 'px' },
+ marginBottom: { value: 0, unit: 'px' },
+ fontWeight: '600',
+ opacity: { value: 0.7, unit: '%' },
+ fontSize: {
+ type: 'responsive',
+ values: [
+ { value: 14, unit: 'px' },
+ { value: 16, unit: 'px' },
+ { value: 20, unit: 'px' },
+ ],
+ },
+ lineHeight: { value: 1.25, unit: 'number' },
+ },
+ children: ['This is a subtitle'],
+ },
+ {
+ tagName: 'p',
+ style: {
+ fontSize: {
+ type: 'responsive',
+ values: [
+ { value: 14, unit: 'px' },
+ { value: 16, unit: 'px' },
+ { value: 16, unit: 'px' },
+ ],
+ },
+ lineHeight: { value: 1.5, unit: 'number' },
+ marginTop: { value: 16, unit: 'px' },
+ marginBottom: { value: 24, unit: 'px' },
+ },
+ children: [
+ `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Fusce pellentesque varius molestie. Integer viverra dui at
+ mauris tempus, a posuere turpis tincidunt. Etiam vitae aliquet
+ nunc.`,
+ ],
+ },
+ {
+ tagName: 'section',
+ style: {},
+ children: [
+ {
+ tagName: 'button',
+ style: {
+ all: 'unset',
+ backgroundColor: 'tomato',
+ color: '#fff',
+ display: 'inline-flex',
+ fontSize: {
+ type: 'responsive',
+ values: [
+ { value: 14, unit: 'px' },
+ { value: 16, unit: 'px' },
+ { value: 16, unit: 'px' },
+ ],
+ },
+ fontWeight: '800',
+ justifyContent: 'center',
+ maxWidth: {
+ value: 100,
+ unit: 'px',
+ },
+ textAlign: 'center',
+ // textDecoration: 'none',
+ whiteSpace: 'nowrap',
+ paddingLeft: { value: 16, unit: 'px' },
+ paddingRight: { value: 16, unit: 'px' },
+ paddingTop: { value: 4, unit: 'px' },
+ paddingBottom: { value: 4, unit: 'px' },
+ },
+ children: ['Click me!'],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+}
+
+export default function HtmlEditorExample() {
+ const [html, setHtml] = useState(initialValue)
+
+ return (
+
+ )
+}
diff --git a/apps/docs/pages/playground.tsx b/apps/docs/pages/playground.tsx
new file mode 100644
index 00000000..f297d216
--- /dev/null
+++ b/apps/docs/pages/playground.tsx
@@ -0,0 +1,24 @@
+import { useState } from 'react'
+import { Canvas } from '../components/playground/Canvas'
+import { Sidebar } from '../components/playground/Sidebar'
+import * as initialStyles from '../data/elements'
+
+export default function Playground() {
+ const [styles, setStyles] = useState(initialStyles.a)
+ const [element, setElement] = useState({
+ name: 'a',
+ children: 'Hello, world!',
+ })
+
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/apps/docs/pages/properties/index.tsx b/apps/docs/pages/properties/index.tsx
new file mode 100644
index 00000000..0d1b97e5
--- /dev/null
+++ b/apps/docs/pages/properties/index.tsx
@@ -0,0 +1,123 @@
+import { Type } from 'react-feather'
+import { FontFamilyPreview } from '../../components/examples/FontFamilyPreview'
+import { FontSizePreview } from '../../components/examples/FontSizePreview'
+import { FontStylePreview } from '../../components/examples/FontStylePreview'
+import { LetterSpacingPreview } from '../../components/examples/LetterSpacingPreview'
+import { TextTransformPreview } from '../../components/examples/TextTransformPreview'
+import { TextIndentPreview } from '../../components/examples/TextIndentPreview'
+import { AccentColorPreview } from '../../components/examples/AccentColorPreview'
+import { ColorPreview } from '../../components/examples/ColorPreview'
+import { BackgroundColorPreview } from '../../components/examples/BackgroundColorPreview'
+import { BorderColorPreview } from '../../components/examples/BorderColorPreview'
+import { BorderWidthPreview } from '../../components/examples/BorderWidthPreview'
+import { BorderStylePreview } from '../../components/examples/BorderStylePreview'
+
+import { AlignItemsPreview } from '../../components/examples/AlignItemsPreview'
+import { JustifyContentPreview } from '../../components/examples/JustifyContentPreview'
+import { FlexWrapPreview } from '../../components/examples/FlexWrapPreview'
+import { FlexGrowPreview } from '../../components/examples/FlexGrowPreview'
+import { FlexShrinkPreview } from '../../components/examples/FlexShrinkPreview'
+import { FlexDirectionPreview } from '../../components/examples/FlexDirectionPreview'
+
+import { OutlineColorPreview } from '../../components/examples/OutlineColorPreview'
+import { OutlineWidthPreview } from '../../components/examples/OutlineWidthPreview'
+import { OutlineStylePreview } from '../../components/examples/OutlineStylePreview'
+import { OutlineOffsetPreview } from '../../components/examples/OutlineOffsetPreview'
+
+import { ColorsPreview } from '../../components/examples/ColorsPreview'
+import { ColorPairPreview } from '../../components/examples/ColorPairPreview'
+import { TextDecorationColorPreview } from '../../components/examples/TextDecorationColorPreview'
+import { TextDecorationLinePreview } from '../../components/examples/TextDecorationLinePreview'
+import { TextDecorationStylePreview } from '../../components/examples/TextDecorationStylePreview'
+import { TextDecorationThicknessPreview } from '../../components/examples/TextDecorationThicknessPreview'
+import { WidthPreview } from '../../components/examples/WidthPreview'
+import { HeightPreview } from '../../components/examples/HeightPreview'
+import { OpacityPreview } from '../../components/examples/OpacityPreview'
+import { BackgroundImagePreview } from '../../components/examples/BackgroundImagePreview'
+import { BackgroundPreview } from '../../components/examples/BackgroundPreview'
+import { MixBlendModePreview } from '../../components/examples/MixBlendModePreview'
+import { BackgroundBlendModePreview } from '../../components/examples/BackgroundBlendModePreview'
+import { BorderRadiusPreview } from '../../components/examples/BorderRadiusPreview'
+import { BorderTopLeftRadiusPreview } from '../../components/examples/BorderTopLeftRadiusPreview'
+import { BoxShadowPreview } from '../../components/examples/BoxShadowPreview'
+import { TextShadowPreview } from '../../components/examples/TextShadowPreview'
+import { TextAlignPreview } from '../../components/examples/TextAlignPreview'
+import { FontWeightPreview } from '../../components/examples/FontWeightPreview'
+import { FilterPreview } from '../../components/examples/FilterPreview'
+import { CursorPreview } from '../../components/examples/CursorPreview'
+import pkg from '../../../../packages/gui/package.json'
+
+// display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(24rem, 1fr))', gap: '4rem',
+
+export default function Docs() {
+ return (
+
+
+
+ Properties
+
+
+ Preview available controls from the library
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/docs/pages/unsupported.tsx b/apps/docs/pages/unsupported.tsx
index 0e3b02e5..fa27f3c4 100644
--- a/apps/docs/pages/unsupported.tsx
+++ b/apps/docs/pages/unsupported.tsx
@@ -47,7 +47,14 @@ export default function UnsupportedProperties() {
width: '100%',
WebKitAppearance: 'none',
appearance: 'none',
+ border: 0,
height: '48px',
+ '&::-webkit-progress-value': {
+ background: '#6465ff',
+ },
+ '&::-moz-progress-bar': {
+ background: '#6465ff',
+ }
}}
>
{percentageComplete}%
diff --git a/package.json b/package.json
index 74aaa70e..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.6.2",
- "tsup": "^5.12.6",
- "turbo": "^1.2.6"
+ "prettier": "^2.7.1",
+ "tsup": "^6.2.3",
+ "turbo": "^1.3.1"
}
}
diff --git a/packages/config/package.json b/packages/config/package.json
index d599929c..f8db14b5 100644
--- a/packages/config/package.json
+++ b/packages/config/package.json
@@ -8,8 +8,8 @@
"eslint-preset.js"
],
"dependencies": {
- "eslint-config-next": "^12.1.5",
+ "eslint-config-next": "^13.2.3",
"eslint-config-prettier": "^8.5.0",
- "eslint-plugin-react": "7.29.4"
+ "eslint-plugin-react": "7.30.0"
}
}
diff --git a/packages/gui/CHANGELOG.md b/packages/gui/CHANGELOG.md
index 361b6106..ed89bf96 100644
--- a/packages/gui/CHANGELOG.md
+++ b/packages/gui/CHANGELOG.md
@@ -1,5 +1,1253 @@
# @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
+
+- 3930a3bc: Improve element selection
+
+## 0.0.171
+
+### Patch Changes
+
+- 56cacc60: Fix transforms
+
+## 0.0.170
+
+### Patch Changes
+
+- ff1d652e: Ensure that theme colors are generated
+
+## 0.0.169
+
+### Patch Changes
+
+- 122bd3ed: Enable selection for nested components
+
+## 0.0.168
+
+### Patch Changes
+
+- b6d1c2fe: Internal refactor of HTML renderers and editors
+
+## 0.0.167
+
+### Patch Changes
+
+- e2bd4fb8: Internal refactor for canvas prop instantiation
+
+## 0.0.166
+
+### Patch Changes
+
+- 084d6bb9: Add inline text editing to node tree
+
+## 0.0.165
+
+### Patch Changes
+
+- 1a9b3f54: Add more rect attributes
+
+## 0.0.164
+
+### Patch Changes
+
+- 27c01a47: Add support for nesting components as children
+
+## 0.0.163
+
+### Patch Changes
+
+- 7ee06fcd: Internal refactoring
+
+## 0.0.162
+
+### Patch Changes
+
+- 0a39cf5f: Don't allow for path selection inside component
+
+## 0.0.161
+
+### Patch Changes
+
+- 57dd87dd: Expose attributes for a component
+
+## 0.0.160
+
+### Patch Changes
+
+- b2c31b23: Make list schema use theme when relevant
+
+## 0.0.159
+
+### Patch Changes
+
+- 03505bbf: Add prop syntax to relevant exports
+
+## 0.0.158
+
+### Patch Changes
+
+- 993b2236: Add basic slot support for strings
+
+## 0.0.157
+
+### Patch Changes
+
+- 971e506a: Improve some default values
+
+## 0.0.156
+
+### Patch Changes
+
+- 2bd60942: Fix component selection
+
+## 0.0.155
+
+### Patch Changes
+
+- 184ff579: Fix responsive theme values
+
+## 0.0.154
+
+### Patch Changes
+
+- e4f12cad: Add initial component support
+
+## 0.0.153
+
+### Patch Changes
+
+- 7ba14dbb: Adds background color to fix bug on node editor
+
+## 0.0.152
+
+### Patch Changes
+
+- 0246cd41: Changes element editor to sticky position
+
+## 0.0.151
+
+### Patch Changes
+
+- 7683e6b8: Fix theme selection for box side
+
+## 0.0.150
+
+### Patch Changes
+
+- 000e4489: Add polyline element
+
+## 0.0.149
+
+### Patch Changes
+
+- 87d117be: Fix skew x/y
+
+## 0.0.148
+
+### Patch Changes
+
+- 5bd603aa: Allow cursor to be overridden in canvas
+- 97d48aee: Fix translate3d
+
+## 0.0.147
+
+### Patch Changes
+
+- 7e8991b8: Only set default canvas styles in canvas mode
+
+## 0.0.146
+
+### Patch Changes
+
+- 888a55fe: Set default to current color
+
+## 0.0.145
+
+### Patch Changes
+
+- 6f28e1db: Add theme support for box side properties
+- 141b70c7: Pass theme to shorthands
+
+## 0.0.144
+
+### Patch Changes
+
+- d2bf7579: Pass theme to codegen
+
+## 0.0.143
+
+### Patch Changes
+
+- 236266eb: Zero index theme values
+
+## 0.0.142
+
+### Patch Changes
+
+- 07cf4070: Fix system colors
+
+## 0.0.141
+
+### Patch Changes
+
+- 1fd871b3: Pass theme along with document for codegen
+
+## 0.0.140
+
+### Patch Changes
+
+- 09588f40: Improve theme integration
+
+## 0.0.139
+
+### Patch Changes
+
+- 20f68b77: Remove unneeded internal function
+- ffb1ef8e: Fix theme handling in HTML editor
+
+## 0.0.138
+
+### Patch Changes
+
+- 6ad62f66: Add some basic SVG functionality
+
+## 0.0.137
+
+### Patch Changes
+
+- 6df72919: Properly hide none schema type, tie in variable fonts
+
+## 0.0.136
+
+### Patch Changes
+
+- af3185f5: Default z index to 1
+
+## 0.0.135
+
+### Patch Changes
+
+- 5bfb117d: Change default length value for height
+
+## 0.0.134
+
+### Patch Changes
+
+- 4790114f: Don't error when the theme path doesn't exist
+- 50b691fd: Adjust height/width default values
+
+## 0.0.133
+
+### Patch Changes
+
+- cbac582a: Don't use old color shape
+
+## 0.0.132
+
+### Patch Changes
+
+- ea271f92: Stringify font family
+
+## 0.0.131
+
+### Patch Changes
+
+- d9d7d449: Add back node copying, make more robust
+
+## 0.0.130
+
+### Patch Changes
+
+- 5d9ad0c1: Misc bug fixes including tuple handling and box sides
+
+## 0.0.129
+
+### Patch Changes
+
+- 46040f61: Remove user select from labels
+
+## 0.0.128
+
+### Patch Changes
+
+- e4b01dea: Hide copy node temporarily
+
+## 0.0.127
+
+### Patch Changes
+
+- 3c1b1a68: Remove default button styles, add tokenizer
+
+## 0.0.126
+
+### Patch Changes
+
+- 3660e1ad: Transform legacy formats, ensure transform happens before CSS export
+
+## 0.0.125
+
+### Patch Changes
+
+- 89c8d7fa: Stringify keywords before validation
+
+## 0.0.124
+
+### Patch Changes
+
+- 69fc6fcb: Accept theme in html editor, pass to style controls
+
+## 0.0.123
+
+### Patch Changes
+
+- c269a1e3: More app like chrome
+
+## 0.0.122
+
+### Patch Changes
+
+- 94f1b85: Add copy/wrap node
+
+## 0.0.121
+
+### Patch Changes
+
+- 4632b1b: Make item display configurable
+
+## 0.0.120
+
+### Patch Changes
+
+- 1588bfd: Make default node an empty div
+- 0273dee: Fix background-size
+
+## 0.0.119
+
+### Patch Changes
+
+- c2a64fb: Fix selection bug on node adding
+- cc6d912: Improve comboboxes
+
+## 0.0.118
+
+### Patch Changes
+
+- 31bdf9b: Clear all styles dropdown
+
+## 0.0.117
+
+### Patch Changes
+
+- e432f72: Changes layers editor
+- 6c83225: Remove fieldset
+
+## 0.0.116
+
+### Patch Changes
+
+- 356d97e: Ensure "add child" is always shown
+
+## 0.0.115
+
+### Patch Changes
+
+- e0ef441: Make nodes in html editor unist friendly
+- 5fe33db: Remove props transform temporarily
+- ad2ed3e: Remove attr default temporarily
+- b894bd0: Improve unstyled html export
+
+## 0.0.114
+
+### Patch Changes
+
+- f7619d5: Fix schema transformer to not destructure raw strings
+- 0587bb0: Format exports
+- db91166: Add React(vanilla/Emotion/Theme UI/Styled JSX) and Vue exports
+
+## 0.0.113
+
+### Patch Changes
+
+- 2dbba5d: fixes lab and lch color picker
+
+## 0.0.112
+
+### Patch Changes
+
+- 67f54e3: fixes selecting values in picker
+
+## 0.0.111
+
+### Patch Changes
+
+- d2b4e14: Fixes color picker mode switching
+
+## 0.0.110
+
+### Patch Changes
+
+- 6df9f13: Make theme more of a first class citizen
+
+## 0.0.109
+
+### Patch Changes
+
+- cf5bfc0: Generative controls
+
+## 0.0.108
+
+### Patch Changes
+
+- 89a9014: Handle undefined values in dimension input
+
+## 0.0.107
+
+### Patch Changes
+
+- 0fa831b: Add tabs for node/tree/export, add copy to clipboarfor exported HTML + CSS
+- 00e15a3: Fix node editing in tabs
+- 1042ffc: Design tweaks to editor
+- 438ad58: Fix navigation alignment
+- 2474219: Force styles editor re-render when selection changes in html editor
+
+## 0.0.106
+
+### Patch Changes
+
+- f9d81c3: Changes label copy
+- 91a1e64: Design tweaks to editor
+
+## 0.0.105
+
+### Patch Changes
+
+- e1b17b3: Improve fuzzy sort by ignoring hypen in property names
+
+## 0.0.104
+
+### Patch Changes
+
+- 0b70d4a: Don't set outline or user select when not in canvas mode
+
+## 0.0.103
+
+### Patch Changes
+
+- 12d442b: Minor internal improvements to inputs
+
+## 0.0.102
+
+### Patch Changes
+
+- 760266c: Removes modes from color groups
+
+## 0.0.101
+
+### Patch Changes
+
+- eb84f84: Allow manually typed decimals in number input
+
+## 0.0.100
+
+### Patch Changes
+
+- 77a9d7c: Fixes colorpicker for safari
+
+## 0.0.99
+
+### Patch Changes
+
+- c96be76: Fix bug with default values
+
+## 0.0.98
+
+### Patch Changes
+
+- 21ec5af: Remove unneeded canvas wrap
+- b943e01: kebab property names
+- b0847c4: Fix href handling in canvas
+
+## 0.0.97
+
+### Patch Changes
+
+- c8f38a8: Tweak styles
+- 60189e3: Fix combobox, convert all inputs to new schema
+- ce61c0c: Clean up visuals
+
+## 0.0.96
+
+### Patch Changes
+
+- a679ce7: Implement dynamic fieldsets for pseudos
+
+## 0.0.95
+
+### Patch Changes
+
+- 28f0e9d: Fix combobox init
+
+## 0.0.94
+
+### Patch Changes
+
+- 0b91ab3: Begin generating controls from schema
+
+## 0.0.93
+
+### Patch Changes
+
+- 665fe5e: Fixes importing theme and theme value ranges
+
+## 0.0.92
+
+### Patch Changes
+
+- 159bfb1: Bump react and @types/react
+- 167af9a: Bump react-feather from 2.0.9 to 2.0.10
+
+## 0.0.91
+
+### Patch Changes
+
+- 1b7b5b4: Improve stringification to handle edge cases
+- 5fad1da: Improve dimensional controls for space and border
+- 76a9a8b: Fix palette swatches/gradient stops
+- 32b0449: Export a schema version
+
+## 0.0.90
+
+### Patch Changes
+
+- e552e6e: Adds html elements and attributes
+
+## 0.0.89
+
+### Patch Changes
+
+- a985b87: Fixes regen for color input
+
+## 0.0.88
+
+### Patch Changes
+
+- dc0f8a9: Improve selection after adding and removing nodes
+- f6630f1: Turn off user select in canvas
+
+## 0.0.87
+
+### Patch Changes
+
+- 4c12a85: Fix DraggableInput to be immediately reflected value when keyed down
+
+## 0.0.86
+
+### Patch Changes
+
+- db6f904: Add HTML codegen
+- 6fff7cd: Make text input on HTML editor a textarea
+
+## 0.0.85
+
+### Patch Changes
+
+- 95aec83: Only wrap elements in span when in canvas
+
+## 0.0.84
+
+### Patch Changes
+
+- 6e4b786: fixes negative value entry and NaN values
+
+## 0.0.83
+
+### Patch Changes
+
+- 2f746c0: New default values
+- f6b2ab6: Adds additional default values
+
+## 0.0.82
+
+### Patch Changes
+
+- 1b14e3b: Adds theme preview picker to color popover
+- dd9fff9: Fix tree view
+
+## 0.0.81
+
+### Patch Changes
+
+- 26e8157: Use text node rather than string
+
+## 0.0.80
+
+### Patch Changes
+
+- c8cfc1f: Delete href in canvas to avoid browser jump
+- 29cb0a3: Use default cursor in tree view
+- 607f95e: Make label spacing more consistent
+
+## 0.0.79
+
+### Patch Changes
+
+- ad49603: Sets value to theme unit on mount if init value matches a theme
+- d8d15c6: Set cursor to default in canvas
+- aae59ca: Improve selection styles
+
+## 0.0.78
+
+### Patch Changes
+
+- b0adbe2: Add selection indicator to tree view
+
+## 0.0.77
+
+### Patch Changes
+
+- d076466: Ensure class attrs don't drop styling
+
+## 0.0.76
+
+### Patch Changes
+
+- 6ec180e: Better handle void elements
+
+## 0.0.75
+
+### Patch Changes
+
+- a4373e2: Fix theme font families to the top of the input
+- b2e9ea5: Add ability to provide a theme as a provider
+
+## 0.0.74
+
+### Patch Changes
+
+- 300787d: Adds variable font tag support to html editor
+
+## 0.0.73
+
+### Patch Changes
+
+- 71c8bc4: Add selection state to HTML editor
+
+## 0.0.72
+
+### Patch Changes
+
+- f5460f6: Select outer root when html editor mounts
+
+## 0.0.71
+
+### Patch Changes
+
+- 2082101: Keep combobox value in sync
+
+## 0.0.70
+
+### Patch Changes
+
+- 02d3cac: Adds font tags to html editor
+
+## 0.0.69
+
+### Patch Changes
+
+- 60842c6: Adjust some types
+
+## 0.0.68
+
+### Patch Changes
+
+- d43581d: Fix duplicates bug
+
+## 0.0.67
+
+### Patch Changes
+
+- 5dc4b27: Fix sorting for properties
+
+## 0.0.66
+
+### Patch Changes
+
+- cec7fb8: Add default styles when a html tag is selected
+- 0ffe9ca: Adds more HTML elements to the list
+- fff36f0: Clears combobox when item selected
+
+## 0.0.65
+
+### Patch Changes
+
+- 26eeecb: Point to documentation if styles object is absent
+- e2cc0a2: Add property controls for fieldsets
+
+## 0.0.64
+
+### Patch Changes
+
+- dd21b63: Clears the combobox value after you add a property
+
+## 0.0.63
+
+### Patch Changes
+
+- a2dfb96: Combobox input and attribute editor
+
+## 0.0.62
+
+### Patch Changes
+
+- 72b59e4: Improve initialization of fieldsets that don't use composition API
+
+## 0.0.61
+
+### Patch Changes
+
+- dbb0ffd: Add basic HTML import to HTML editor
+
+## 0.0.60
+
+### Patch Changes
+
+- 135f774: remove dynamic properties
+
+## 0.0.59
+
+### Patch Changes
+
+- e451db4: Add ability to add new controls to composition based API
+
+## 0.0.58
+
+### Patch Changes
+
+- 8587cf3: Export some primitive inputs
+
+## 0.0.57
+
+### Patch Changes
+
+- 5005fb5: Allow properties to be added dynamically
+- 4fc1aae: Add fieldsets for classes
+
+## 0.0.56
+
+### Patch Changes
+
+- ea6b50e: Add more default values
+
+## 0.0.55
+
+### Patch Changes
+
+- 7b9efa3: allows optional pseudo syntax for initial styles. Change default color to inherit
+- 3c287db: Add handwriting category of fonts
+
+## 0.0.54
+
+### Patch Changes
+
+- 59fee5a: Add more responsive coverage
+
+## 0.0.53
+
+### Patch Changes
+
+- 5a59618: Add display fonts
+- 9ec945d: Adds calc operations
+
+## 0.0.52
+
+### Patch Changes
+
+- aa6dc32: Fix multidimensional init
+
+## 0.0.51
+
+### Patch Changes
+
+- 1abc092: Select theme values with a slider
+- a97f111: Add multidimensional length to border radii
+
+## 0.0.50
+
+### Patch Changes
+
+- 4bd301d: Miscellaneous input improvements
+
+## 0.0.49
+
+### Patch Changes
+
+- 74e5c1b: Don't fetch font family from API if it doesn't exist
+
+## 0.0.48
+
+### Patch Changes
+
+- 461ea3c: Add backdrop pseudo element
+
## 0.0.47
### Patch Changes
diff --git a/packages/gui/jest.config.cjs b/packages/gui/jest.config.cjs
new file mode 100644
index 00000000..670d7889
--- /dev/null
+++ b/packages/gui/jest.config.cjs
@@ -0,0 +1,13 @@
+/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
+module.exports = {
+ preset: 'ts-jest/presets/default-esm',
+ testEnvironment: 'node',
+ globals: {
+ 'ts-jest': {
+ useESM: true,
+ },
+ },
+ moduleNameMapper: {
+ '^(\\.{1,2}/.*)\\.js$': '$1',
+ },
+}
diff --git a/packages/gui/package.json b/packages/gui/package.json
index 2dce5c0c..97ae23a3 100644
--- a/packages/gui/package.json
+++ b/packages/gui/package.json
@@ -1,12 +1,14 @@
{
"name": "@compai/css-gui",
- "version": "0.0.47",
+ "version": "0.0.247",
"type": "module",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"license": "MIT",
"scripts": {
"build": "tsup",
+ "test": "jest",
+ "typecheck": "tsc --noEmit",
"dev": "tsup --watch",
"clean": "rm -rf .turbo node_modules dist"
},
@@ -25,13 +27,16 @@
"react-dom": ">= 18"
},
"devDependencies": {
+ "@types/jest": "^28.1.3",
"@types/lodash-es": "^4.17.6",
- "@types/react": "18.0.5",
+ "@types/react": "18.0.10",
"@types/react-dom": "^18.0.1",
"@types/uuid": "^8.3.4",
"config": "*",
- "react": "18.0.0",
+ "jest": "^28.1.1",
+ "react": "18.1.0",
"react-dom": "18.0.0",
+ "ts-jest": "^28.0.5",
"tsconfig": "*",
"typescript": "^4.6.4"
},
@@ -40,22 +45,41 @@
"@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": "^2.0.3",
"@radix-ui/react-label": "^0.1.5",
- "@radix-ui/react-popover": "^0.1.6",
- "@radix-ui/react-slider": "^0.1.4",
- "@radix-ui/react-switch": "^0.1.5",
+ "@radix-ui/react-popover": "^1.0.0",
+ "@radix-ui/react-select": "^0.1.1",
+ "@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",
+ "@use-gesture/react": "^10.2.17",
+ "copy-to-clipboard": "^3.3.2",
"csstype": "^3.0.11",
"culori": "^2.0.3",
"downshift": "^6.1.7",
+ "escape-html": "^1.0.3",
+ "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",
- "react-feather": "^2.0.9",
- "react-use-gesture": "^9.1.3",
- "theme-ui": "^0.14.5",
+ "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",
+ "rehype-parse": "^8.0.4",
+ "rehype-sanitize": "^5.0.1",
+ "rehype-stringify": "^9.0.3",
+ "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",
"uuid": "^8.3.2"
}
}
diff --git a/packages/gui/src/components/AddFieldset.tsx b/packages/gui/src/components/AddFieldset.tsx
new file mode 100644
index 00000000..97878923
--- /dev/null
+++ b/packages/gui/src/components/AddFieldset.tsx
@@ -0,0 +1,60 @@
+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'
+import { Combobox, Label } from './primitives'
+import { useEditor } from './providers/EditorContext'
+import { KeyArg } from './providers/types'
+import { joinPath } from './providers/util'
+
+interface Props {
+ field?: KeyArg
+ styles: Styles
+ label?: string
+}
+export const AddFieldsetControl = ({
+ field,
+ styles,
+ label = 'Add pseudo element or class',
+}: Props) => {
+ const { setField } = useEditor()
+ const allItems = [...pseudoClasses, ...pseudoElements, ...elements]
+
+ const handleFilterItems = (input: string) => {
+ if (input === '') {
+ return allItems
+ }
+
+ 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) => {
+ const fullField = field ? joinPath(field, propertyName) : propertyName
+ setField(fullField, {})
+ }
+
+ return (
+
+ {label}
+
+
+ )
+}
diff --git a/packages/gui/src/components/AddProperty.tsx b/packages/gui/src/components/AddProperty.tsx
new file mode 100644
index 00000000..5ee811b5
--- /dev/null
+++ b/packages/gui/src/components/AddProperty.tsx
@@ -0,0 +1,67 @@
+import { properties as propertyList } from '../data/properties'
+import { getDefaultValue } from './Editor/util'
+import { Styles } from '../types/css'
+import { Combobox, Label } from './primitives'
+import { useDynamicControls } from './providers/DynamicPropertiesContext'
+import { useEditor } from './providers/EditorContext'
+import { KeyArg } from './providers/types'
+import { joinPath } from './providers/util'
+import fuzzysort from 'fuzzysort'
+import { kebabCase } from 'lodash-es'
+
+interface Props {
+ field?: KeyArg
+ styles: Styles
+ label?: string
+}
+export const AddPropertyControl = ({
+ field,
+ styles,
+ label = 'Add property',
+}: Props) => {
+ const { setField } = useEditor()
+ const { addDynamicProperty } = useDynamicControls()
+
+ //@ts-ignore
+ const allProperties: string[] = Object.entries(propertyList)
+ .map(([name, data]) => {
+ return data.input ? name : null
+ })
+ .filter(Boolean)
+
+ const handleFilterItems = (input: string) => {
+ if (input === '') {
+ return allProperties
+ }
+
+ const styleItems = Object.keys(styles)
+ return fuzzysort
+ .go(input.replace(/-/g, ''), allProperties)
+ .map((res) => res.target)
+ .filter((item) => !styleItems.includes(item))
+ }
+
+ const handleAddProperty = (propertyName: string) => {
+ const fullField = field ? joinPath(field, propertyName) : propertyName
+
+ setField(fullField, getDefaultValue(propertyName))
+ if (addDynamicProperty && !field) {
+ addDynamicProperty(propertyName)
+ }
+ }
+
+ return (
+
+
+ {label}
+ kebabCase(str)}
+ clearOnSelect
+ />
+
+
+ )
+}
diff --git a/packages/gui/src/components/Editor/Controls.tsx b/packages/gui/src/components/Editor/Controls.tsx
index 09af873a..e23c66b1 100644
--- a/packages/gui/src/components/Editor/Controls.tsx
+++ b/packages/gui/src/components/Editor/Controls.tsx
@@ -1,77 +1,114 @@
import produce from 'immer'
-import { ComponentType, ReactChild, useId } from 'react'
-import { CSSUnitValue, Length, ResponsiveLength, Styles } from '../../types/css'
+import {
+ Children,
+ ComponentType,
+ Fragment,
+ isValidElement,
+ ReactNode,
+ useMemo,
+ useState,
+} from 'react'
+import { camelCase, isNil, mapValues, uniq } from 'lodash-es'
+import { RefreshCw } from 'react-feather'
+import { Styles } from '../../types/css'
import { Theme } from '../../types/theme'
import { EditorProvider, useEditor } from '../providers/EditorContext'
+import { useDynamicControls } from '../providers/DynamicPropertiesContext'
import { EditorData, KeyArg, Recipe } from '../providers/types'
-import { useFieldset } from './Fieldset'
+import { GenericFieldset, useFieldset } from './Fieldset'
import { joinPath } from '../providers/util'
import { properties } from '../../data/properties'
-import { ColorInput } from '../inputs/ColorInput'
-import { LengthInput } from '../inputs/LengthInput'
-import { ResponsiveInput } from '../Responsive'
import { sentenceCase } from '../../lib/util'
-import { EditorProps } from '../../types/editor'
-import { DimensionInput } from '../inputs/Dimension'
-import { SelectInput } from '../inputs/SelectInput'
-import { GLOBAL_KEYWORDS } from '../../data/global-keywords'
-import { Label } from '../primitives'
-import { kebabCase } from 'lodash-es'
import { useThemeProperty } from '../providers/ThemeContext'
-import { PositionInput } from '../inputs/PositionInput'
-import { TimeInput } from '../inputs/TimeInput'
import { UnitSteps } from '../../lib'
import { pascalCase } from '../../lib/util'
import { UnitRanges } from '../../data/ranges'
-import { StringInput } from '../inputs/StringInput'
-import { DEFAULT_LENGTH } from '../../lib/constants'
+import { AddPropertyControl } from '../AddProperty'
+import {
+ getDefaultValue,
+ isFieldsetGroup,
+ partitionProperties,
+ sortProperties,
+} from './util'
+import { stylesToEditorSchema } from '../../lib/transformers/styles-to-editor-schema'
+import { removeInternalCSSClassSyntax } from '../../lib/classes'
+import { AddFieldsetControl } from '../AddFieldset'
+import IconButton from '../ui/IconButton'
+import { SchemaInput } from '../inputs/SchemaInput'
+import { EditorDropdown } from '../ui/dropdowns/EditorDropdown'
+import { FieldsetDropdown } from '../ui/dropdowns/FieldsetDropdown'
+import { tokenize } from '../../lib/parse'
+import {
+ addPseudoSyntax,
+ getSelectorFunctionArgument,
+ getSelectorFunctionName,
+ isSelectorFunction,
+ removePseudoSyntax,
+ stringifySelectorFunction,
+} from '../../lib/pseudos'
+
+export const getPropertyFromField = (field: KeyArg) => {
+ if (Array.isArray(field)) {
+ return field[field.length - 1].toString()
+ }
+
+ return field.toString()
+}
interface ControlProps extends InputProps {
field: KeyArg
+ showRemove?: boolean
}
-const Control = ({ field, ...props }: ControlProps) => {
- const { getField, setField } = useEditor()
+const Control = ({ field, showRemove = false, ...props }: ControlProps) => {
+ const { getField, getParentField, setField, removeField } = useEditor()
+ const { removeDynamicProperty } = useDynamicControls()
const fieldset = useFieldset()
- const property = field.toString()
- const Component: ComponentType = getInputComponent(property)
+ const property = getPropertyFromField(field)
const themeValues = useThemeProperty(property)
- const keywords = [
- ...(properties[property].keywords ?? []),
- ...GLOBAL_KEYWORDS,
- ]
- const dependantProperties = properties[property].dependantProperties ?? []
-
- if (!Component) {
- console.error(`Unknown field: ${field}, ignoring`)
- return null
- }
+ const dependantProperties =
+ (properties[property] as any).dependantProperties ?? []
- const fullField = fieldset ? joinPath(fieldset.name, field) : field
- const componentProps = {
- label: sentenceCase(property),
- themeValues: themeValues,
- ...properties[property],
- ...props,
- keywords,
- }
+ const fieldsetName = fieldset?.name ?? null
+ const fullField = fieldsetName ? joinPath(fieldsetName, field) : field
+ const componentProps = { themeValues, ...props }
if (dependantProperties.length) {
return (
)
}
+ const handleRemoveProperty = () => {
+ if (removeDynamicProperty) {
+ removeDynamicProperty(property)
+ }
+ removeField(fullField)
+ }
+
+ const schema = properties[property]
+
+ if (schema.type === 'none') {
+ return null
+ }
+
return (
- {
setField(fullField, newValue)
}}
- {...componentProps}
+ onRemove={showRemove ? handleRemoveProperty : undefined}
+ ruleset={getParentField(fullField)}
+ property={property}
/>
)
}
@@ -79,19 +116,28 @@ const Control = ({ field, ...props }: ControlProps) => {
interface ComponentGroupProps {
dependantProperties: string[]
property: string
+ fullField: KeyArg
+ showRemove: boolean
}
const ComponentWithPropertyGroup = ({
dependantProperties,
property,
+ fullField,
+ showRemove = false,
...props
}: ComponentGroupProps) => {
- const Component: ComponentType = getInputComponent(property)
- const { getFields, setFields } = useEditor()
+ const Component: ComponentType | undefined = getInputComponent(property)
+ const { getFields, setFields, removeField } = useEditor()
+ if (!Component) {
+ console.error(`Unknown field: ${property}, ignoring`)
+ return null
+ }
return (
setFields(newValue, dependantProperties)}
+ onRemove={showRemove ? () => removeField(fullField) : null}
{...props}
/>
)
@@ -104,29 +150,31 @@ type InputProps = {
}
export const Inputs: Record = {}
Object.keys(properties).forEach((field: string) => {
- Inputs[pascalCase(field)] = (props: InputProps) => (
-
- )
+ const Component = (props: InputProps) =>
+ Component.displayName = pascalCase(field)
+ Inputs[pascalCase(field)] = Component
})
-type ControlsProps = {
+interface ControlsProps {
styles: Styles
theme?: Theme
onChange: (newStyles: any) => void
- children?: ReactChild
+ children?: ReactNode
hideResponsiveControls?: boolean
+ showRegenerate?: boolean
+ showAddProperties?: boolean
}
export const Editor = ({
theme,
styles,
onChange,
children,
+ showRegenerate,
hideResponsiveControls,
+ showAddProperties,
}: ControlsProps) => {
- const properties = Object.keys(styles)
-
const handleStylesChange = (recipe: Recipe>) => {
- const newData = produce(styles, (draft: any) => {
+ const newData = produce(stylesToEditorSchema(styles), (draft: any) => {
const valueData: EditorData = {
value: draft,
}
@@ -139,187 +187,281 @@ export const Editor = ({
onChange(newData)
}
- const controls = children ? (
- children
- ) : (
- <>
- {properties.map((property) => {
- return
- })}
- >
- )
+ const propertyList = useMemo(() => {
+ return getPropertiesFromChildren(children)
+ }, [children])
+
+ const defaultStyles = useMemo(() => {
+ return Object.fromEntries(
+ propertyList
+ .filter((property) => !(styles as any)[property])
+ .map((property) => {
+ return [property, getDefaultValue(property)]
+ })
+ )
+ }, [propertyList])
+
+ const allStyles = { ...defaultStyles, ...styles }
+
+ function regenerateAll(): any {
+ return mapValues(allStyles, (value, property) => {
+ return (
+ properties[property].regenerate?.({
+ theme,
+ previousValue: value,
+ ruleset: allStyles,
+ property,
+ }) ?? value
+ )
+ })
+ }
return (
- {controls}
+ {showRegenerate && (
+
+ onChange(regenerateAll())}
+ sx={{ ml: 'auto' }}
+ >
+
+
+
+ )}
+
+ {children}
+
)
}
-function getInputComponent(property: string) {
- const propertyData = properties[property]
- if (typeof propertyData.type === 'function') {
- return propertyData.type
- }
- return getPrimitiveInput(propertyData.type)
+interface EditorControlsProps {
+ children?: ReactNode
+ showAddProperties?: boolean
}
+export const EditorControls = ({
+ children,
+ showAddProperties,
+}: EditorControlsProps) => {
+ const { value: styles, clearAll } = useEditor()
+ const [fieldsets, properties] = partitionProperties(uniq(Object.keys(styles)))
+ const controls = children ? (
+ children
+ ) : (
+
+ )
-function getPrimitiveInput(type: string) {
- switch (type) {
- case 'keyword':
- return KeywordInput
- case 'number':
- return NumberInput
- case 'integer':
- return IntegerInput
- case 'percentage':
- return PercentageInput
- case 'length':
- return ResponsiveLengthInput
- case 'time':
- return TimeInput
- case 'string':
- return StringInput
- case 'color':
- return ColorInput
- case 'position':
- return PositionInput
- default:
- return TextInput
- }
-}
+ const fieldsetControls = children ? null : (
+
+ )
-type EditorPropsWithLabel = EditorProps & {
- label: string
- responsive: boolean
-}
-const NumberInput = ({
- value,
- onChange,
- label,
- ...props
-}: EditorPropsWithLabel) => {
return (
-
+
+ {showAddProperties ? (
+
+ ) : null}
+ {controls}
+ {showAddProperties ? (
+
+ ) : null}
+ {fieldsetControls}
+ {children ? : null}
+
)
}
-const IntegerInput = ({
- value,
- onChange,
- label,
- ...props
-}: EditorPropsWithLabel) => {
- return (
-
- )
+const DynamicControls = () => {
+ const { dynamicProperties } = useDynamicControls()
+
+ return dynamicProperties?.length ? (
+
+ ) : null
}
-const PercentageInput = ({
- value,
- onChange,
- label,
- ...props
-}: EditorPropsWithLabel) => {
+type ControlSetProps = {
+ field?: KeyArg
+ properties: string[]
+}
+const ControlSet = ({ field, properties }: ControlSetProps) => {
return (
-
+
+ {properties.map((property) => {
+ const fullField = field ? joinPath(field, property) : property
+
+ return isFieldsetGroup(property) ? (
+
+ ) : (
+
+ )
+ })}
+
)
}
-const ResponsiveLengthInput = ({
- value,
- onChange,
- label,
- ...props
-}: EditorPropsWithLabel & { property: string }) => {
+type FieldsetControlProps = {
+ field: string
+}
+const FieldsetControl = ({ field }: FieldsetControlProps) => {
+ const { getField, removeField, setFields } = useEditor()
+ const [argument, setArgument] = useState(getSelectorFunctionArgument(field))
+
+ const styles = getField(field)
+ const properties = Object.keys(styles)
+ const label = addPseudoSyntax(field)
+ const rawFieldsetName = getSelectorFunctionName(field)
+
return (
-
+ >
+
+
+ {rawFieldsetName}
+ {isSelectorFunction(rawFieldsetName) ? (
+ <>
+ {'('}
+ {
+ setArgument(e.target.value)
+ }}
+ onBlur={() => {
+ setFields(
+ {
+ [stringifySelectorFunction(rawFieldsetName, argument)]:
+ styles,
+ },
+ [field]
+ )
+ }}
+ />
+ {')'}
+ >
+ ) : null}
+
+ removeField(field)} />
+
+
+
+
+
+
)
}
-const DEFAULT_KEYWORD = 'inherit'
-const KeywordInput = ({
- value,
- onChange,
- label,
- keywords,
- responsive,
-}: EditorPropsWithLabel & { keywords: string[] }) => {
- if (responsive) {
- return (
- onChange(newValue)}
- defaultValue={DEFAULT_KEYWORD}
- Component={SelectInput}
- componentProps={{
- options: keywords,
- }}
- />
- )
- }
+function getInputComponent(property: string) {
+ const propertyData = properties[property]
+ return propertyData.input
+}
- return (
-
- )
+/**
+ * Extract the properties from the editor's children
+ */
+function getPropertiesFromChildren(children: ReactNode): string[] {
+ // Based on: https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx#L270
+ let properties: string[] = []
+ Children.forEach(children, (element) => {
+ if (!isValidElement(element)) {
+ return
+ }
+ if (element.type === Fragment) {
+ properties = [
+ ...properties,
+ ...getPropertiesFromChildren(element.props.children),
+ ]
+ }
+ // TODO defaults on nested fields
+ if (
+ typeof element.type === 'function' &&
+ (element.type as any).displayName
+ ) {
+ const property = camelCase((element.type as any).displayName)
+ properties = [...properties, property]
+ }
+ if (element.props.children) {
+ properties = [
+ ...properties,
+ ...getPropertiesFromChildren(element.props.children),
+ ]
+ }
+ })
+ return properties
}
-const TextInput = ({
- value,
- onChange,
- label,
-}: EditorPropsWithLabel) => {
- const id = `${useId()}-${kebabCase(label)}`
- return (
-
- {label}
- onChange(e.target.value)}
- />
-
- )
+export function parseStyles(styles: Record) {
+ return mapValues(styles, (value, property) => {
+ const schema = properties[property]
+ 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}`)
+ }
+ return parsed
+ })
}
diff --git a/packages/gui/src/components/Editor/Fieldset.tsx b/packages/gui/src/components/Editor/Fieldset.tsx
index 89e8ac78..1e5660c9 100644
--- a/packages/gui/src/components/Editor/Fieldset.tsx
+++ b/packages/gui/src/components/Editor/Fieldset.tsx
@@ -2,15 +2,21 @@ import * as React from 'react'
import { elements } from '../../data/elements'
import { pseudoClasses } from '../../data/pseudo-classes'
import { pseudoElements } from '../../data/pseudo-elements'
+import { getFieldsetPropsFromName } from './util'
type PseudoElementTypes = typeof pseudoElements[number]
type PseudoClassTypes = typeof pseudoClasses[number]
type ElementTypes = typeof elements[number]
+type ClassTypes = string
-type FieldsetNames = PseudoElementTypes | PseudoClassTypes | ElementTypes
+type FieldsetNames =
+ | PseudoElementTypes
+ | PseudoClassTypes
+ | ElementTypes
+ | ClassTypes
-type FieldsetContextProps = {
- type: 'pseudo-element' | 'pseudo-class' | 'element'
+export type FieldsetContextProps = {
+ type: 'pseudo-element' | 'pseudo-class' | 'element' | 'class'
name: FieldsetNames | FieldsetNames[]
}
@@ -26,6 +32,7 @@ export const Fieldset = ({ type, name, children }: FieldsetProps) => {
const fullName = outerNames.length
? [...outerNames, name]
: (name as FieldsetNames)
+
return (
// @ts-ignore
@@ -33,3 +40,11 @@ export const Fieldset = ({ type, name, children }: FieldsetProps) => {
)
}
+
+type GenericFieldsetProps = {
+ field: string
+ children?: React.ReactNode
+}
+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
new file mode 100644
index 00000000..af51b475
--- /dev/null
+++ b/packages/gui/src/components/Editor/util.ts
@@ -0,0 +1,66 @@
+import { partition, sortBy } from 'lodash-es'
+import { properties } from '../../data/properties'
+import {
+ addInternalCSSClassSyntax,
+ isInternalCSSClass,
+} from '../../lib/classes'
+import { isElement } from '../../lib/elements'
+import {
+ addPseudoSyntax,
+ isPseudo,
+ isPseudoElement,
+ removePseudoSyntax,
+} from '../../lib/pseudos'
+import { FieldsetContextProps } from './Fieldset'
+
+export const addFieldsetNameSyntax = (
+ fieldsetName: string,
+ fieldsetType: string
+): string => {
+ if (isPseudo(fieldsetName)) {
+ return addPseudoSyntax(fieldsetName)
+ } else if (fieldsetType === 'class' && !isInternalCSSClass(fieldsetName)) {
+ return addInternalCSSClassSyntax(fieldsetName)
+ }
+
+ return fieldsetName
+}
+
+export const isFieldsetGroup = (str: string) => {
+ return isPseudo(str) || isElement(str) || isInternalCSSClass(str)
+}
+
+export const getFieldsetPropsFromName = (str: string): FieldsetContextProps => {
+ if (isElement(str)) {
+ return {
+ type: 'element',
+ name: str,
+ }
+ } else if (isPseudo(str)) {
+ return {
+ type: isPseudoElement(str) ? 'pseudo-element' : 'pseudo-class',
+ name: str,
+ }
+ }
+
+ return {
+ type: 'class',
+ name: str,
+ }
+}
+
+export const partitionProperties = (properties: string[]) => {
+ return partition(properties, isFieldsetGroup)
+}
+
+export const sortProperties = (properties: string[]) => {
+ const [fieldsets, props] = partitionProperties(properties)
+ return [...sortBy(props), ...sortBy(fieldsets).reverse()]
+}
+
+export function getDefaultValue(property: string) {
+ const propertyDefinition = properties[property] ?? {}
+ // If a default value is defined, return it
+ // @ts-ignore
+ return propertyDefinition.defaultValue
+}
diff --git a/packages/gui/src/components/FieldArray.tsx b/packages/gui/src/components/FieldArray.tsx
index 0f3f8a11..14e3cbda 100644
--- a/packages/gui/src/components/FieldArray.tsx
+++ b/packages/gui/src/components/FieldArray.tsx
@@ -1,193 +1,143 @@
-import { Trash, ChevronUp, ChevronDown } from 'react-feather'
-import { useState, ComponentType, useId, ReactNode } from 'react'
-import * as Collapsible from '@radix-ui/react-collapsible'
-import IconButton from './ui/IconButton'
-import { kebabCase } from 'lodash-es'
-import { Label } from './primitives'
-import { ExpandMarker } from './ui/ExpandMarker'
+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 {
- label: string
- value: T[]
- onChange(newValue: T[]): void
+export interface FieldArrayProps extends EditorPropsWithLabel {
/**
* The component to render each of the individual input values.
* (See `LayerProps` for what props this takes)
*/
- content: ComponentType>
- /** The values that should be populated when a new item is added. */
- newItem(): T
- /** How to stringify the contents of the layer */
- stringify(value: T[]): string
-}
-
-export interface LayerProps {
- value: T
- onChange(newValue: T): void
- label: string
+ itemSchema: DataTypeSchema
+ addItem?(currentValue: T[]): T
}
/**
* An alternative field array that is collapsible.
*/
export default function FieldArray({
- label,
- value = [],
- onChange,
- content: Content,
- newItem,
- stringify,
+ addItem,
+ ...props
}: FieldArrayProps) {
- const id = `${useId()}-${kebabCase(label)}`
- const [open, setOpen] = useState(true)
+ const { label = '', value = [], onChange, itemSchema } = props
+ const [dragIndex, setDragIndex] = useState(-1)
+ const isDragging = dragIndex >= 0
- const handleReorder = (i1: number, i2: number) => {
- 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 (
-
-
-
- {label}
-
-
- {stringify(value)}
-
-
-
+
+ {value.map((item, i) => {
+ return (
- {value.map((item, i) => {
- return (
-
-
{
- onChange(replace(value, i, newValue))
- }}
- label={'' + i}
- />
-
-
{
- onChange(remove(value, i))
- }}
- >
-
-
-
- {
- if (i > 0) {
- handleReorder(i, i - 1)
- }
- }}
- >
-
-
- {
- if (i < value.length - 1) {
- handleReorder(i, i + 1)
- }
- }}
- >
-
-
-
-
-
- )
- })}
-
{
- onChange(value.concat([newItem()]))
- }}
+ {isDragging && (
+ {
+ handleDragDrop(dragIndex, i)
+ setDragIndex(-1)
+ }}
+ />
+ )}
+
- + Add {label.toLowerCase()}
-
+ {
+ onChange(replace(value, i, newValue))
+ }}
+ onRemove={() => onChange(remove(value, i))}
+ onDrag={() => {
+ setDragIndex(i)
+ }}
+ onDragEnd={() => {
+ setDragIndex(-1)
+ }}
+ />
+
-
-
+ )
+ })}
+ {isDragging && (
+
{
+ handleDragDrop(dragIndex, value.length)
+ setDragIndex(-1)
+ }}
+ />
+ )}
+ {
+ const newLayerValue = addItem?.(value) ?? itemSchema.defaultValue
+ onChange(value.concat([newLayerValue]))
+ }}
+ sx={{
+ width: '100%',
+ appearance: 'none',
+ px: 0,
+ py: 2,
+ m: 0,
+ border: '1px solid',
+ borderColor: 'border',
+ borderRadius: '6px',
+ background: 'none',
+ cursor: 'pointer',
+ color: 'text',
+ }}
+ >
+ + Add {label.toLowerCase()}
+
)
}
-// Return a new array with the given indices flipped
-function flip(array: T[], i1: number, i2: number) {
- const copy = [...array]
- copy[i1] = array[i2]
- copy[i2] = array[i1]
- return copy
+interface DropZoneProps {
+ onDrop(): void
}
-// Return a new array with the value at the index removed
-function remove(array: T[], index: number) {
- const copy = [...array]
- copy.splice(index, 1)
- return copy
-}
-
-function replace(array: T[], index: number, newValue: T) {
- const copy = [...array]
- copy.splice(index, 1, newValue)
- return copy
+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 573a97fb..28d6850d 100644
--- a/packages/gui/src/components/Layers.tsx
+++ b/packages/gui/src/components/Layers.tsx
@@ -1,16 +1,14 @@
import { Trash, ChevronUp, ChevronDown } from 'react-feather'
-import { useState, ComponentType, useId, ReactNode } from 'react'
+import { useState, ComponentType, useId } from 'react'
import * as Accordion from '@radix-ui/react-accordion'
-import * as Collapsible from '@radix-ui/react-collapsible'
import IconButton from './ui/IconButton'
import { kebabCase } from 'lodash-es'
-import { Label } from './primitives'
import LayerHeader from './LayerHeader'
+import { flip, replace, remove } from '../lib/array'
+import { InputHeader } from './ui/InputHeader'
+import { EditorPropsWithLabel } from '../types/editor'
-interface LayersProps {
- label: string
- value: T[]
- onChange(newValue: T[]): void
+interface LayersProps extends EditorPropsWithLabel {
/**
* The component to render each of the individual input values.
* (See `LayerProps` for what props this takes)
@@ -32,15 +30,16 @@ export interface LayerProps {
/**
* An alternative field array that is collapsible.
*/
-export default function Layers({
- label,
- value = [],
- onChange,
- content: Content,
- stringify,
- newItem,
- thumbnail,
-}: LayersProps) {
+export default function Layers(props: LayersProps) {
+ const {
+ label = '',
+ value = [],
+ onChange,
+ content: Content,
+ stringify,
+ newItem,
+ thumbnail,
+ } = props
const id = `${useId()}-${kebabCase(label)}`
const [expandedLayer, setExpandedLayer] = useState(-1)
@@ -50,190 +49,146 @@ export default function Layers({
return (
-
{label}
-
-
-
-
-
+
+
setExpandedLayer(i === '' ? -1 : +i)}
>
-
-
setExpandedLayer(i === '' ? -1 : +i)}
- >
- {value.map((item, i) => {
- return (
-
- {
+ return (
+
+
+
+
+
+
+
{
+ onChange(remove(value, i))
+ // If the deleted value was expanded, close it
+ if (i === expandedLayer) {
+ setExpandedLayer(-1)
+ } else if (i < expandedLayer) {
+ // If the deleted value was at a lower index, adjust expanded value accordingly
+ setExpandedLayer((v) => v - 1)
+ }
+ }}
>
-
-
-
-
-
{
- onChange(remove(value, i))
- // If the deleted value was expanded, close it
- if (i === expandedLayer) {
- setExpandedLayer(-1)
- } else if (i < expandedLayer) {
- // If the deleted value was at a lower index, adjust expanded value accordingly
- setExpandedLayer((v) => v - 1)
- }
- }}
- >
-
-
-
- {
- if (i > 0) {
- handleReorder(i, i - 1)
- if (i === expandedLayer) {
- setExpandedLayer(i - 1)
- } else if (i - 1 === expandedLayer) {
- setExpandedLayer(i)
- }
- }
- }}
- >
-
-
- {
- if (i < value.length - 1) {
- handleReorder(i, i + 1)
- }
- if (i === expandedLayer) {
- setExpandedLayer(i + 1)
- } else if (i + 1 === expandedLayer) {
- setExpandedLayer(i)
- }
- }}
- >
-
-
-
-
-
-
+
+
- {
- onChange(replace(value, i, newValue))
+ {
+ if (i > 0) {
+ handleReorder(i, i - 1)
+ if (i === expandedLayer) {
+ setExpandedLayer(i - 1)
+ } else if (i - 1 === expandedLayer) {
+ setExpandedLayer(i)
+ }
+ }
}}
- />
-
-
- )
- })}
-
- {
- onChange(value.concat([newItem()]))
- }}
- sx={{
- width: '100%',
- appearance: 'none',
- px: 0,
- py: 2,
- m: 0,
- border: 'none',
- background: 'none',
- cursor: 'pointer',
- color: 'text',
- }}
- >
- + Add {label.toLowerCase()}
-
-
-
-
+ >
+
+
+
{
+ if (i < value.length - 1) {
+ handleReorder(i, i + 1)
+ }
+ if (i === expandedLayer) {
+ setExpandedLayer(i + 1)
+ } else if (i + 1 === expandedLayer) {
+ setExpandedLayer(i)
+ }
+ }}
+ >
+
+
+
+
+
+
+ {
+ onChange(replace(value, i, newValue))
+ }}
+ />
+
+
+ )
+ })}
+
+
{
+ onChange(value.concat([newItem()]))
+ }}
+ sx={{
+ width: '100%',
+ appearance: 'none',
+ px: 0,
+ py: 2,
+ mt: 2,
+ border: 'none',
+ background: 'none',
+ cursor: 'pointer',
+ color: 'text',
+ }}
+ >
+ + Add {label.toLowerCase()}
+
+
)
}
-
-// Return a new array with the given indices flipped
-function flip(array: T[], i1: number, i2: number) {
- const copy = [...array]
- copy[i1] = array[i2]
- copy[i2] = array[i1]
- return copy
-}
-
-// Return a new array with the value at the index removed
-function remove(array: T[], index: number) {
- const copy = [...array]
- copy.splice(index, 1)
- return copy
-}
-
-function replace(array: T[], index: number, newValue: T) {
- const copy = [...array]
- copy.splice(index, 1, newValue)
- return copy
-}
diff --git a/packages/gui/src/components/RenderElement.tsx b/packages/gui/src/components/RenderElement.tsx
index 4f8db6bf..d9012ec6 100644
--- a/packages/gui/src/components/RenderElement.tsx
+++ b/packages/gui/src/components/RenderElement.tsx
@@ -2,6 +2,7 @@ import { HTMLAttributes } from 'react'
import { toCSSObject } from '../lib'
import { Styles } from '../types/css'
import { FontTags } from './inputs/FontFamily/FontTags'
+import { useTheme } from './providers/ThemeContext'
type RenderElementProps = HTMLAttributes & {
tagName: string
@@ -13,13 +14,14 @@ export const RenderElement = ({
styles,
...props
}: RenderElementProps) => {
+ const theme = useTheme()
const Component = tagName
- const styleObject = toCSSObject(styles)
+ const styleObject = toCSSObject(styles, theme)
return (
// @ts-ignore
<>
-
+ {styles.fontFamily ? : null}
{/* @ts-ignore */}
>
diff --git a/packages/gui/src/components/Responsive/Input.tsx b/packages/gui/src/components/Responsive/Input.tsx
index 36638547..afb74304 100644
--- a/packages/gui/src/components/Responsive/Input.tsx
+++ b/packages/gui/src/components/Responsive/Input.tsx
@@ -1,146 +1,25 @@
-import * as React from 'react'
-import { Monitor, Smartphone, X } from 'react-feather'
-import { Breakpoint } from '../../types/theme'
-import { useTheme } from '../providers/ThemeContext'
-import { Label } from '../primitives'
-import { useEditorConfig } from '../providers/EditorConfigContext'
+import { DataTypeSchema } from '../schemas/types'
+import FieldArray from '../FieldArray'
-const DEFAULT_BREAKPOINT_COUNT = 3
-
-export type Responsive = T | T[]
+export type Responsive = { type: 'responsive'; values: T[] }
type ResponsiveInputProps = {
- value?: Responsive
- defaultValue: Responsive
+ value: Responsive
onChange: (newValue: Responsive) => void
- label: string
- property?: string
- // TODO: Type this component
- Component: React.ComponentType
- componentProps?: any
+ itemSchema: DataTypeSchema
}
export function ResponsiveInput({
value,
onChange,
- label,
- Component,
- componentProps = {},
- property,
- defaultValue,
+ itemSchema,
}: ResponsiveInputProps) {
- const { breakpoints } = useTheme()
- const breakpointCount = breakpoints?.length || DEFAULT_BREAKPOINT_COUNT
-
- const handleResponsiveChange =
- (breakpointIndex: number) => (newItemValue: Responsive) => {
- const newValue: any[] = Array.isArray(value) ? [...value] : []
- newValue[breakpointIndex] = newItemValue
- onChange(newValue)
- }
-
- const handleChange = (newItemValue: Responsive) => {
- onChange(newItemValue)
- }
-
- const handleSwitchToResponsive = () => {
- const newValue: any[] = Array(breakpointCount).fill(value ?? null)
- onChange(newValue)
- }
-
- const handleSwitchFromResponsive = () => {
- const newValue: Responsive | undefined = Array.isArray(value)
- ? value[0]
- : value
- onChange(newValue ?? defaultValue)
- }
-
- const isResponsiveControls = Array.isArray(value)
-
- const editors = isResponsiveControls ? (
- Array(breakpointCount)
- .fill(null)
- .map((_breakpoint: Breakpoint, i: number) => {
- return (
-
-
-
- )
- })
- ) : (
-
- )
-
return (
-
-
-
- {label}
-
-
-
- {editors}
-
- )
-}
-
-type ResponsiveToggleProps = {
- isResponsive: boolean
- onSwitchFromResponsive: () => void
- onSwitchToResponsive: () => void
-}
-const ResponsiveToggle = ({
- isResponsive,
- onSwitchFromResponsive,
- onSwitchToResponsive,
-}: ResponsiveToggleProps) => {
- const { hideResponsiveControls } = useEditorConfig()
-
- if (hideResponsiveControls) {
- return null
- }
-
- return isResponsive ? (
-
-
-
- ) : (
-
-
-
-
+ {
+ onChange({ ...value, values: newValues })
+ }}
+ />
)
}
diff --git a/packages/gui/src/components/html/CanvasProvider.tsx b/packages/gui/src/components/html/CanvasProvider.tsx
new file mode 100644
index 00000000..c9790142
--- /dev/null
+++ b/packages/gui/src/components/html/CanvasProvider.tsx
@@ -0,0 +1,94 @@
+import { createContext, MouseEvent, ReactNode, useContext } from 'react'
+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 { ComponentData, ElementPath, HtmlNode } from './types'
+import { cleanAttributesForCanvas, isSamePath } from './util'
+
+const DEFAULT_CANVAS_VALUE = {}
+const DEFAULT_ELEMENT_STYLES_IN_CANVAS = {
+ cursor: 'default',
+}
+
+type CanvasProviderType = {
+ canvas?: boolean
+}
+
+export type CanvasElementProps = {
+ path: ElementPath
+ value: HtmlNode
+ component?: ComponentData
+ onClick?(e: MouseEvent): void
+}
+
+export function useCanvas() {
+ const context = useContext(CanvasContext)
+ return context
+}
+
+export function useCanvasProps({
+ path,
+ value,
+ component,
+ onClick,
+}: CanvasElementProps) {
+ const { canvas } = useContext(CanvasContext)
+ const { selected, setSelected } = useHtmlEditor()
+ const theme = useTheme()
+
+ const { attributes = {}, style = {} } = value
+
+ const sx = toCSSObject(
+ {
+ ...(canvas ? DEFAULT_ELEMENT_STYLES_IN_CANVAS : {}),
+ ...style,
+ },
+ theme
+ )
+
+ if (isSamePath(path, selected) && canvas) {
+ sx.outlineWidth = 'thin'
+ sx.outlineStyle = 'solid'
+ sx.outlineColor = 'primary'
+ sx.outlineOffset = '4px'
+ sx.userSelect = 'none'
+ }
+
+ const handleSelect = (e: MouseEvent) => {
+ if (!canvas) {
+ return
+ }
+
+ if (onClick) {
+ return onClick(e)
+ }
+
+ e.stopPropagation()
+ setSelected(path)
+ }
+
+ const props = toReactProps({
+ ...(canvas ? cleanAttributesForCanvas(attributes) : attributes),
+ ...(canvas ? { 'data-path': path.join('-') } : {}),
+ sx,
+ outerProps: component?.props,
+ onClick: handleSelect,
+ })
+
+ return props
+}
+
+const CanvasContext = createContext(DEFAULT_CANVAS_VALUE)
+
+type CanvasProviderProps = CanvasProviderType & {
+ children: ReactNode
+}
+
+export function CanvasProvider({ children, canvas }: CanvasProviderProps) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/packages/gui/src/components/html/Component/Editor.tsx b/packages/gui/src/components/html/Component/Editor.tsx
new file mode 100644
index 00000000..8cd2a72e
--- /dev/null
+++ b/packages/gui/src/components/html/Component/Editor.tsx
@@ -0,0 +1,171 @@
+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, Slot } from '../types'
+import { useHtmlEditor } from '../Provider'
+import { getSlots, isSlot } from '../../../lib/codegen/util'
+import { mergeComponentAttributes } from './util'
+import IconButton from '../../ui/IconButton'
+
+interface ComponentEditorProps {
+ value: ComponentData
+ onChange(value: ComponentData): void
+}
+
+export const ComponentEditor = ({ value, onChange }: ComponentEditorProps) => {
+ const { components = [] } = useHtmlEditor()
+
+ const componentIds = components.map((c) => c.id)
+ const componentNames = components.map((c) => c.tagName)
+ const componentProps = value.props || {}
+ const slots = getSlots(value.value)
+ const attributes = mergeComponentAttributes(value)
+ const attributeEntries = Object.entries(attributes)
+
+ const handleFilterComponents = (input: string) => {
+ if (!input) {
+ return componentIds
+ }
+
+ return fuzzysort
+ .go(input, componentNames)
+ .map((res) => res.target)
+ .map((name) => components.find((c) => c.tagName === name)?.id ?? name)
+ }
+
+ const handleComponentSelected = (selectedItem: string) => {
+ const component = components.find((c) => c.id === selectedItem)
+
+ if (component) {
+ onChange({
+ ...component,
+ props: value.props,
+ children: value.children,
+ })
+ }
+ }
+
+ const handlePropChange =
+ (name: string) => (e: ChangeEvent) => {
+ onChange({
+ ...value,
+ props: {
+ ...componentProps,
+ [name]: e.target.value,
+ },
+ })
+ }
+
+ const handleAttributeChange =
+ (name: string) => (e: ChangeEvent) => {
+ onChange({
+ ...value,
+ attributes: {
+ ...attributes,
+ [name]: e.target.value,
+ },
+ })
+ }
+
+ 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 (
+
+
+ Component
+
+ {
+ return components.find((c) => c.id === id)?.tagName ?? id
+ }}
+ items={componentIds}
+ />
+ {value.swappableComponentIds?.length ? (
+
+
+
+ ) : null}
+
+
+
+
Props
+ {slots.map((slot, index) => {
+ return (
+
+ {slot.name}
+
+
+ )
+ })}
+ {attributeEntries.length ? (
+ <>
+ {attributeEntries.map((entry) => {
+ const [key, rawValue] = entry
+
+ const val = isSlot(rawValue as Slot)
+ ? (rawValue as Slot).value
+ : rawValue
+
+ return (
+
+ )
+ })}
+ >
+ ) : null}
+
+
+ )
+}
diff --git a/packages/gui/src/components/html/Component/Provider.tsx b/packages/gui/src/components/html/Component/Provider.tsx
new file mode 100644
index 00000000..45d0df51
--- /dev/null
+++ b/packages/gui/src/components/html/Component/Provider.tsx
@@ -0,0 +1,92 @@
+import { createContext, ReactNode, useContext } from 'react'
+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() {
+ const context = useContext(ComponentContext)
+ return context
+}
+
+const ComponentContext = createContext(
+ DEFAULT_COMPONENT_VALUE
+)
+
+type ComponentProviderProps = {
+ value: ComponentData
+ path: ElementPath
+ children: ReactNode
+}
+
+export function ComponentProvider({
+ value,
+ 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/index.ts b/packages/gui/src/components/html/Component/index.ts
new file mode 100644
index 00000000..23518ca5
--- /dev/null
+++ b/packages/gui/src/components/html/Component/index.ts
@@ -0,0 +1,2 @@
+export * from './Provider'
+export * from './Editor'
diff --git a/packages/gui/src/components/html/Component/util.ts b/packages/gui/src/components/html/Component/util.ts
new file mode 100644
index 00000000..59f46b25
--- /dev/null
+++ b/packages/gui/src/components/html/Component/util.ts
@@ -0,0 +1,24 @@
+import { ComponentData, Slot } from '../types'
+
+export const mergeComponentAttributes = (value: ComponentData) => {
+ const attributes = {
+ ...(value.value.attributes || {}),
+ ...(value.attributes || {}),
+ }
+
+ 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
new file mode 100644
index 00000000..b78de0cb
--- /dev/null
+++ b/packages/gui/src/components/html/Editor.tsx
@@ -0,0 +1,184 @@
+import { Editor } from '../Editor'
+import * as Tabs from '@radix-ui/react-tabs'
+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'
+import { Import } from './Import'
+import { isText } from '../../lib/codegen/util'
+
+const TABS_TRIGGER_STYLES: any = {
+ all: 'unset',
+ cursor: 'pointer',
+ fontSize: 0,
+ fontWeight: 500,
+ 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 - 97px)',
+ maxHeight: '100%',
+ overflow: 'hidden',
+ resize: 'horizontal',
+ borderRightWidth: '1px',
+ borderRightStyle: 'solid',
+ borderColor: 'border',
+ '&::-webkit-scrollbar': { display: 'none' },
+ 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() {
+ 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 (
+
+
+
+
+ 🎨 Editor
+
+
+ Import
+
+
+ Export
+
+
+
+
+ {
+ const newItem = { ...nodeForStyleEditor, style: newStyles }
+ onChange(setChildAtPath(value, stylePath, newItem))
+ }}
+ showRegenerate
+ showAddProperties
+ />
+
+ Layers
+
+
+ onChange(setChildAtPath(value, selected, newItem))
+ }
+ onParentChange={(newItem) => {
+ const parentPath = [...(selected || [])]
+ parentPath.pop()
+ const newValue = setChildAtPath(value, parentPath, newItem)
+ onChange(newValue)
+ }}
+ onRemove={() => {
+ onChange(removeChildAtPath(value, selected))
+ const newPath = [...selected]
+ newPath.pop()
+ setSelected(newPath)
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/gui/src/components/html/Editors/AttributeEditor.tsx b/packages/gui/src/components/html/Editors/AttributeEditor.tsx
new file mode 100644
index 00000000..295254be
--- /dev/null
+++ b/packages/gui/src/components/html/Editors/AttributeEditor.tsx
@@ -0,0 +1,199 @@
+import { X } from 'react-feather'
+import { Label } from '../../primitives'
+import IconButton from '../../ui/IconButton'
+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
+ element: string
+}
+
+export const AttributeEditor = ({
+ value = {},
+ onChange,
+ element,
+}: AttributeEditorProps) => {
+ useEffect(() => {
+ handleElementChange()
+ }, [element])
+
+ const handleElementChange = () => {
+ const newAttributes = Object.entries(value).reduce(
+ (acc: any, [k, v]: any) => {
+ return ATTRIBUTE_MAP[element].includes(k) ? { ...acc, [k]: v } : acc
+ },
+ {}
+ )
+
+ onChange(newAttributes)
+ }
+
+ const handleFilterItems = (input: string) => {
+ return ATTRIBUTE_MAP[element].filter((item) => {
+ if (item.toLowerCase().startsWith(input.toLowerCase() || '')) {
+ return !Object.keys(value).includes(item)
+ }
+ })
+ }
+
+ const handleItemSelected = (selectedItem: string) => {
+ onChange({ ...value, [selectedItem]: '' })
+ }
+
+ const handleItemRemoved = (removedItem: string) => {
+ const newValue = { ...value }
+ delete newValue[removedItem]
+ 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 (
+
+
+ Add attribute
+
+
+ {/* @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 (
+
+
+ {name}
+
+
+
+
+
+ Make slot
+
+
+
+ )
+}
+
+interface SlotAttributeEditorProps {
+ name: string
+ value: Slot
+ onChange(newValue: Slot): void
+ onRemove(): void
+ onSlot(): void
+}
+const SlotAttributeEditor = ({
+ name,
+ value,
+ onChange,
+ onRemove,
+ onSlot,
+}: SlotAttributeEditorProps) => {
+ return (
+
+
{name}
+
+ Name
+
+
+ onChange({
+ ...value,
+ name: e.target.value,
+ })
+ }
+ />
+
+
+
+ Value
+
+
+ onChange({
+ ...value,
+ value: e.target.value,
+ })
+ }
+ />
+
+
+
+ Make string
+
+
+
+ )
+}
diff --git a/packages/gui/src/components/html/Editors/NodeEditor.tsx b/packages/gui/src/components/html/Editors/NodeEditor.tsx
new file mode 100644
index 00000000..10af647c
--- /dev/null
+++ b/packages/gui/src/components/html/Editors/NodeEditor.tsx
@@ -0,0 +1,230 @@
+import fuzzysort from 'fuzzysort'
+import { Layers } from 'react-feather'
+import { HtmlNode } from '../types'
+import { Label, Combobox } from '../../primitives'
+import { SelectInput } from '../../inputs/SelectInput'
+import { AttributeEditor } from './AttributeEditor'
+import { DEFAULT_ATTRIBUTES, DEFAULT_STYLES } from '../default-styles'
+import { useHtmlEditor } from '../Provider'
+import { addChildAtPath, getChildAtPath } from '../util'
+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
+ onChange(value: HtmlNode): void
+}
+
+interface TagEditorProps extends EditorProps {
+ onRemove(): void
+ onParentChange?(parentValue: HtmlNode): void
+}
+
+export function NodeEditor({
+ value,
+ onChange,
+ onRemove,
+ onParentChange,
+}: TagEditorProps) {
+ 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 nodeTypes = useNodeTypes()
+
+ return (
+
+
+
+ {
+ if (newType === 'text') {
+ onChange({ type: 'text', value: '' })
+ } else if (newType === 'component') {
+ const firstComponent = components?.[0]
+
+ if (firstComponent) {
+ onChange({
+ ...firstComponent,
+ props: value.props,
+ children: value.children,
+ })
+ }
+ } else if (newType === 'slot') {
+ onChange({
+ type: 'slot',
+ name: 'newSlot',
+ value: 'Hello, world!',
+ })
+ } else {
+ onChange({
+ type: 'element',
+ tagName: 'div',
+ props: value.props,
+ children: value.children ?? [],
+ })
+ }
+ }}
+ options={nodeTypes}
+ />
+
+
+ {
+ const parentPath = [...(selected || [])]
+ const childIndex = parentPath.pop() // Remove child from parent path
+
+ const parent = getChildAtPath(fullValue, parentPath)
+ const newParent = addChildAtPath(parent, [childIndex ?? 0], {
+ ...value,
+ })
+
+ const onChangeForParent = onParentChange
+ ? onParentChange
+ : onChange
+ onChangeForParent(newParent)
+ }}
+ onWrap={() => {
+ const wrappedNode: HtmlNode = {
+ type: 'element',
+ tagName: 'div',
+ children: [value],
+ }
+ return onChange(wrappedNode)
+ }}
+ />
+
+
+
+
+ )
+}
+
+function NodeSwitch({ value, onChange }: EditorProps) {
+ const { selected } = useHtmlEditor()
+
+ if (value.type === 'text') {
+ return (
+
+
+ Content
+
+
+ )
+ }
+
+ if (value.type === 'component') {
+ return
+ }
+
+ if (value.type === 'slot') {
+ return
+ }
+
+ const tagKey = [...(selected || []), value.tagName || ''].join('-')
+
+ return (
+
+
+ Element {' '}
+ {
+ if (!filterValue) {
+ return HTML_TAGS
+ }
+
+ return fuzzysort
+ .go(filterValue, HTML_TAGS)
+ .map((res) => res.target)
+ }}
+ onItemSelected={(selectedItem) => {
+ const defaultStyles = DEFAULT_STYLES[selectedItem] || {}
+ const mergedStyles = { ...defaultStyles, ...value.style }
+ const defaultAttributes = DEFAULT_ATTRIBUTES[selectedItem] || {}
+ const mergedAttributes = {
+ ...defaultAttributes,
+ ...(value.attributes || {}),
+ }
+ 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}
+ />
+
+
+
+ onChange({ ...value, attributes: newAttributes })
+ }
+ element={value.tagName as string}
+ />
+
+
+ )
+}
diff --git a/packages/gui/src/components/html/Editors/SlotEditor.tsx b/packages/gui/src/components/html/Editors/SlotEditor.tsx
new file mode 100644
index 00000000..7197bd21
--- /dev/null
+++ b/packages/gui/src/components/html/Editors/SlotEditor.tsx
@@ -0,0 +1,33 @@
+import { Label } from '../../primitives'
+import { Slot } from '../types'
+import { ChangeEvent } from 'react'
+
+interface SlotEditorProps {
+ value: Slot
+ onChange(value: Slot): void
+}
+
+export const SlotEditor = ({ value, onChange }: SlotEditorProps) => {
+ const handleNameChange = (e: ChangeEvent) => {
+ onChange({ ...value, name: e.target.value })
+ }
+
+ const handleDefaultValueChange = (e: ChangeEvent) => {
+ onChange({ ...value, value: e.target.value })
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/gui/src/components/html/Editors/index.ts b/packages/gui/src/components/html/Editors/index.ts
new file mode 100644
index 00000000..b5c6689a
--- /dev/null
+++ b/packages/gui/src/components/html/Editors/index.ts
@@ -0,0 +1,3 @@
+export * from './AttributeEditor'
+export * from './NodeEditor'
+export * from './SlotEditor'
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/Export.tsx b/packages/gui/src/components/html/Export.tsx
new file mode 100644
index 00000000..974859a8
--- /dev/null
+++ b/packages/gui/src/components/html/Export.tsx
@@ -0,0 +1,130 @@
+import { startCase } from 'lodash-es'
+import { useEffect, useState } from 'react'
+import { Copy } from 'react-feather'
+import { codegen } from '../../lib'
+import { extractStyles } from '../../lib/codegen/extract-styles'
+import { useCopyToClipboard } from '../../useCopyToClipboard'
+import { HtmlNode } from './types'
+
+const PRE_STYLES = {
+ overflow: 'auto',
+ height: '80vh',
+ border: 'thin solid',
+ borderColor: 'border',
+ backgroundColor: 'rgba(0, 0, 0, 0.02)',
+ p: 2,
+ m: 3,
+}
+
+const CODEGEN_DISPLAY_NAMES: Record = {
+ css: 'CSS',
+ html: 'HTML + CSS',
+ unstyledHtml: 'HTML',
+ themeUI: 'Theme UI',
+ styledJsx: 'Styled JSX',
+}
+
+type ExportProps = {
+ value: HtmlNode
+ theme?: any
+}
+export const Export = ({ value, theme }: ExportProps) => {
+ const [src, setSrc] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [copied, setCopied] = useState(true)
+ const [format, setFormat] = useState('html')
+ const copyToClipboard = useCopyToClipboard()
+
+ useEffect(() => {
+ setLoading(true)
+ setSrc('')
+
+ // @ts-ignore
+ let gen = codegen[format]
+ if (format === 'css') {
+ gen = codegen.html
+ }
+
+ gen(value, { theme }).then((v: string) => {
+ setLoading(false)
+
+ if (format === 'css') {
+ return extractStyles(v).then((res) => {
+ setSrc(res.styles)
+ })
+ }
+
+ setSrc(v)
+ })
+ }, [value, format])
+
+ useEffect(() => {
+ if (!copied) {
+ return
+ }
+
+ const clearCopiedTimer = setTimeout(() => setCopied(false), 1000)
+ return () => clearTimeout(clearCopiedTimer)
+ }, [copied])
+
+ const handleCopyToClipboard = () => {
+ copyToClipboard(src)
+ setCopied(true)
+ }
+
+ if (loading) {
+ return Exporting...
+ }
+
+ return (
+ <>
+ {src}
+
+ setFormat(e.target.value)}
+ sx={{
+ mr: 2,
+ px: 1,
+ py: 1,
+ }}
+ >
+ {Object.keys(codegen).map((f) => {
+ return (
+
+ {CODEGEN_DISPLAY_NAMES[f] ?? startCase(f)}
+
+ )
+ })}
+
+
+
+ {copied ? 'Copied to clipboard!' : 'Copy to clipboard'}
+
+
+ >
+ )
+}
diff --git a/packages/gui/src/components/html/FontTags.tsx b/packages/gui/src/components/html/FontTags.tsx
new file mode 100644
index 00000000..b2a7f0c7
--- /dev/null
+++ b/packages/gui/src/components/html/FontTags.tsx
@@ -0,0 +1,104 @@
+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, theme?: Theme): string[] {
+ if (!style) return []
+ let fonts: string[] = []
+
+ if (style.fontFamily) {
+ fonts.push(style.fontFamily)
+ }
+
+ for (const [_, v] of Object.entries(style)) {
+ if (typeof v === 'object') {
+ fonts = [...fonts, ...getStyleFonts(v)]
+ }
+ }
+
+ return uniq(fonts).map((font) => stringifyFontFamily(font, theme))
+}
+
+export function getHTMLTreeFonts(root: any, theme?: Theme): string[] {
+ if (!root) return []
+ let treeFonts: any[] = []
+
+ if (root.style) {
+ treeFonts = [...getStyleFonts(root.style)]
+ }
+
+ if (root.type === 'component' && root.value) {
+ return [...treeFonts, ...getHTMLTreeFonts(root.value, theme)]
+ }
+
+ if (!root.children) {
+ return treeFonts
+ }
+
+ for (const node of root.children) {
+ if (node.type !== 'text') {
+ treeFonts = [...treeFonts, ...getHTMLTreeFonts(node, theme)]
+ }
+ }
+
+ return uniq(treeFonts).map((font) => stringifyFontFamily(font, theme))
+}
+
+interface BuildHrefProps {
+ tree: any
+ style: any
+ setStaticHref: Function
+ setVariableHref: Function
+ theme?: Theme
+}
+async function buildHrefs({
+ tree,
+ style,
+ setStaticHref,
+ setVariableHref,
+ theme,
+}: BuildHrefProps) {
+ const fonts = style
+ ? getStyleFonts(style, theme)
+ : getHTMLTreeFonts(tree, theme)
+ const staticHref = await buildFontFamiliesHref(fonts)
+ const variableHref = await buildVariableFontFamiliesHref(fonts)
+
+ setStaticHref(staticHref)
+ setVariableHref(variableHref)
+}
+
+const debouncedBuildHref = debounce(buildHrefs, 1500)
+
+interface Props {
+ htmlTree: any
+ style?: any
+}
+export function HTMLFontTags({ htmlTree = {}, style }: Props) {
+ const theme = useTheme()
+ const [staticHref, setStaticHref] = useState('')
+ const [variableHref, setVariableHref] = useState('')
+
+ useEffect(() => {
+ debouncedBuildHref({
+ tree: htmlTree,
+ style,
+ setStaticHref,
+ setVariableHref,
+ theme,
+ })
+ }, [htmlTree, style])
+
+ return (
+ <>
+ {staticHref ? : null}
+ {variableHref ? : null}
+ >
+ )
+}
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
new file mode 100644
index 00000000..8db5b0e1
--- /dev/null
+++ b/packages/gui/src/components/html/Provider.tsx
@@ -0,0 +1,123 @@
+import { createContext, ReactNode, useContext, useState } from 'react'
+import { htmlToEditorSchema } from '../../lib'
+import { stylesToEditorSchema } from '../../lib/transformers/styles-to-editor-schema'
+import { ThemeProvider } from '../providers/ThemeContext'
+import { HtmlNode, ElementPath, ElementData, ComponentData } from './types'
+
+const DEFAULT_HTML_EDITOR_VALUE = {
+ selected: [],
+ setSelected: () => {},
+ value: htmlToEditorSchema(`
+
+ `),
+ 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
+ isEditing: boolean
+ setEditing(value: boolean): void
+ components?: ComponentData[]
+ updateComponent?(newComponent: ComponentData): void
+ hasComponents: boolean
+}
+
+export function useHtmlEditor() {
+ const context = useContext(HtmlEditorContext)
+ return context
+}
+
+const HtmlEditorContext = createContext(DEFAULT_HTML_EDITOR_VALUE)
+
+const coerceNodeIntoUnist = (node: any) => {
+ if (node.tagName) {
+ return { type: 'element', attributes: {}, ...node }
+ }
+
+ return node
+}
+
+export const transformValueToSchema = (value: any): ElementData => {
+ const fullValue = coerceNodeIntoUnist(value)
+
+ const transformed = Object.entries(fullValue).reduce((acc, [key, val]) => {
+ let updatedValue: any = val
+ if (key === 'children' && Array.isArray(val)) {
+ updatedValue = val.map((child) => transformValueToSchema(child))
+ } else if (key === 'style') {
+ updatedValue = stylesToEditorSchema(val)
+ } else if (value.type === 'component' && key === 'value') {
+ updatedValue = transformValueToSchema(val)
+ }
+
+ if (value.tagName && !value.type) {
+ return {
+ type: 'element',
+ [key]: updatedValue,
+ ...acc,
+ }
+ }
+
+ return {
+ [key]: updatedValue,
+ ...acc,
+ }
+ }, {})
+
+ return transformed as ElementData
+}
+
+type HtmlEditorProviderProps = {
+ value: HtmlNode
+ onChange(value: HtmlNode): void
+ children: ReactNode
+ theme?: any
+ components?: ComponentData[]
+ updateComponent?(newComponent: ComponentData): void
+}
+
+export function HtmlEditorProvider({
+ children,
+ value,
+ theme,
+ components = [],
+ updateComponent,
+ onChange,
+}: HtmlEditorProviderProps) {
+ const [selected, setSelected] = useState([])
+ const [isEditing, setEditing] = useState(false)
+ const transformedValue = transformValueToSchema(value)
+
+ const fullContext = {
+ value: transformedValue,
+ selected,
+ setSelected: (newSelection: ElementPath | null) =>
+ setSelected(newSelection),
+ components,
+ isEditing,
+ setEditing: (newValue: any) => setEditing(newValue),
+ updateComponent,
+ hasComponents: !!components.length,
+ update: onChange,
+ }
+
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/packages/gui/src/components/html/Renderer/Element.tsx b/packages/gui/src/components/html/Renderer/Element.tsx
new file mode 100644
index 00000000..8b8b3a82
--- /dev/null
+++ b/packages/gui/src/components/html/Renderer/Element.tsx
@@ -0,0 +1,140 @@
+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'
+import { TextRenderer } from './Text'
+
+export function ElementRenderer({
+ value,
+ path,
+ ...canvasElementProps
+}: CanvasElementProps) {
+ const { selectComponent, value: componentValue } = useComponent()
+ const { onClick, ...props } = useCanvasProps({
+ value,
+ path,
+ component: componentValue,
+ ...canvasElementProps,
+ })
+
+ if (value.type === 'slot') {
+ return
+ } else if (value.type === 'component') {
+ return
+ }
+
+ const Tag: any = value.tagName || 'div'
+
+ const handleClick = (e: MouseEvent) => {
+ if (selectComponent) {
+ return selectComponent(e)
+ }
+
+ onClick(e)
+ }
+
+ if (isVoidElement(Tag)) {
+ return
+ }
+
+ if (Tag === 'textarea' && value.children) {
+ return (
+
+ {value.children.map((child) => child.value).join(' ')}
+
+ )
+ }
+
+ return (
+
+
+
+ )
+}
+
+interface ChildrenRendererProps {
+ value?: HtmlNode[]
+ path: ElementPath
+}
+function ChildrenRenderer({ value = [], path }: ChildrenRendererProps) {
+ return (
+ <>
+ {value.map((child, i) => {
+ const childPath: ElementPath = [...path, i]
+ if (child.type === 'text') {
+ return
+ }
+
+ return
+ })}
+ >
+ )
+}
+
+interface ComponentRendererProps {
+ value: ComponentData
+ path: ElementPath
+}
+export function ComponentRenderer({ value, path }: ComponentRendererProps) {
+ const fullValue = {
+ ...value.value,
+ attributes: mergeComponentAttributes(value),
+ }
+
+ return (
+
+
+
+ )
+}
+
+interface SlotRendererProps {
+ value: Slot
+ path: ElementPath
+}
+function SlotRenderer({ value, path }: SlotRendererProps) {
+ const { value: outerValue } = useComponent()
+
+ const outerProps = outerValue?.props || {}
+ const hasSlottedChildren = value.name === 'children' && outerValue?.children
+
+ if (hasSlottedChildren) {
+ // Slots are essentially "fragments" when it comes to rendering. As such, we
+ // don't want their rendering to count as part of the constructed path. Since
+ // we're going to pass the children to the ChildrenRenderer which automatically
+ // increments the path for the component's direct children, we can remove the
+ // tail of the path which is currently pointing to the slot itself.
+ //
+ // The slot is being rendered from a ChildrenRenderer so the path construction
+ // by default will look something like:
+ // * component (path of [0]) -> renders children/slots with paths of [0, n]
+ // * slot receives path of [0, 0] -> renders children with paths of [0, 0, n]
+ // * children receive path of [0, 0, n]
+ //
+ // This results in [0, 0, n] for the component's children when we want [0, n].
+ //
+ // With this change we end up with the following:
+ // * component (path of [0]) -> renders children/slots with paths of [0, n]
+ // * slot receives path of [0, n] then passes [0] to render paths of [0, n]
+ // * children receive paths of [0, n]
+ //
+ // With the proper paths being passed to the component's child slots, the canvas
+ // selection and node editing target the proper elements. Yay.
+ const passThroughPath = removeTailFromPath(path)
+ return (
+
+ )
+ }
+
+ const slotValue = outerProps[value.name] || value.value
+ const textNode: HtmlNode = { type: 'text', value: slotValue as string }
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/gui/src/components/html/Renderer/Html.tsx b/packages/gui/src/components/html/Renderer/Html.tsx
new file mode 100644
index 00000000..b44f9681
--- /dev/null
+++ b/packages/gui/src/components/html/Renderer/Html.tsx
@@ -0,0 +1,25 @@
+import { ElementPath, HtmlNode } from '../types'
+import { HTMLFontTags } from '../FontTags'
+import { transformValueToSchema } from '../Provider'
+import { CanvasProvider } from '../CanvasProvider'
+import { ElementRenderer } from './Element'
+
+const DEFAULT_PATH: ElementPath = []
+
+interface HtmlRendererProps {
+ value: HtmlNode
+ path?: ElementPath
+ canvas?: boolean
+}
+export function HtmlRenderer({ value, canvas = true }: HtmlRendererProps) {
+ const transformedVal = transformValueToSchema(value)
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
diff --git a/packages/gui/src/components/html/Renderer/Text.tsx b/packages/gui/src/components/html/Renderer/Text.tsx
new file mode 100644
index 00000000..6ca70ac4
--- /dev/null
+++ b/packages/gui/src/components/html/Renderer/Text.tsx
@@ -0,0 +1,90 @@
+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: 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/Renderer/index.ts b/packages/gui/src/components/html/Renderer/index.ts
new file mode 100644
index 00000000..7c9a376d
--- /dev/null
+++ b/packages/gui/src/components/html/Renderer/index.ts
@@ -0,0 +1 @@
+export * from './Html'
diff --git a/packages/gui/src/components/html/TreeNode.tsx b/packages/gui/src/components/html/TreeNode.tsx
new file mode 100644
index 00000000..2903c679
--- /dev/null
+++ b/packages/gui/src/components/html/TreeNode.tsx
@@ -0,0 +1,421 @@
+import { HtmlNode, ElementPath } from './types'
+import * as Collapsible from '@radix-ui/react-collapsible'
+import { useState } from 'react'
+import { useHtmlEditor } from './Provider'
+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
+ onChange(value: HtmlNode): void
+}
+interface TreeNodeProps extends EditorProps {
+ path: ElementPath
+ onSelect(path: ElementPath | null): void
+}
+
+export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
+ const { selected, isEditing, setEditing, components } = useHtmlEditor()
+ const [open, setOpen] = useState(true)
+ const isSelected = isSamePath(path, selected)
+ const isEditingNode = isSelected && isEditing
+
+ 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') {
+ return (
+
+ {
+ handleSelect()
+ }}
+ >
+ {isEditingNode ? (
+
+
+ )
+ }
+
+ if (value.type === 'slot') {
+ return (
+
+ handleSelect()}
+ >
+ {value.name}: "{value.value}"
+
+
+ )
+ }
+
+ const tagEditor = isEditingNode ? (
+ {
+ return HTML_TAGS.filter((el) => el.startsWith(filterValue))
+ }}
+ onItemSelected={(selectedItem) => {
+ const defaultStyles = DEFAULT_STYLES[selectedItem] || {}
+ const mergedStyles = { ...defaultStyles, ...value.style }
+ const defaultAttributes = DEFAULT_ATTRIBUTES[selectedItem] || {}
+ const mergedAttributes = {
+ ...defaultAttributes,
+ ...(value.attributes || {}),
+ }
+ const fullValue = {
+ ...value,
+ attributes: mergedAttributes,
+ tagName: selectedItem,
+ style: mergedStyles,
+ }
+ if (isProseElement(selectedItem) && !fullValue.children?.length) {
+ fullValue.children = [{ type: 'text', value: '' }]
+ }
+ setEditing(false)
+ onChange(fullValue)
+ }}
+ items={HTML_TAGS}
+ value={value.tagName}
+ />
+ ) : (
+ {
+ if (isSelected) {
+ setEditing(true)
+ }
+ }}
+ >
+ {value.tagName}
+
+ )
+
+ const tagButton = (
+ {
+ handleSelect()
+ }}
+ >
+ <{tagEditor}
+ {!open || isSelfClosing(value) ? ' /' : null}>
+
+ )
+
+ if (isSelfClosing(value)) {
+ return tagButton
+ }
+
+ function handleAddChild(i: number, type: string) {
+ let child = DEFAULT_TEXT
+ if (type === 'tag') {
+ child = DEFAULT_TAG
+ } else if (type === 'component') {
+ child = components![0]
+ } else if (type === 'slot') {
+ child = DEFAULT_SLOT
+ }
+
+ onChange(addChildAtPath(value, [i], child))
+ }
+
+ return (
+
+
+
+ {tagButton}
+
+
+
+ {value.children?.map((child, i) => {
+ return (
+
+
{
+ handleAddChild(i, childType)
+ onSelect([...path, i])
+ setEditing(true)
+ }}
+ />
+
+ {
+ onChange({
+ ...value,
+ children: replaceAt(value.children ?? [], i, newChild),
+ })
+ }}
+ />
+
+
+ )
+ })}
+
{
+ const index = value.children?.length ?? 0
+ handleAddChild(index, childType)
+ onSelect([...path, index])
+ setEditing(true)
+ }}
+ />
+
+
+
+
+ </{value.tagName}>
+
+
+
+
+ )
+}
+
+const isSelfClosing = (node: HtmlNode) => {
+ if (node.type !== 'component') {
+ return isVoidElement(node.tagName as string)
+ }
+
+ return !hasChildrenSlot(node.value)
+}
+
+const DEFAULT_TAG: HtmlNode = {
+ type: 'element',
+ tagName: 'div',
+}
+
+const DEFAULT_TEXT: HtmlNode = {
+ type: 'text',
+ value: '',
+}
+
+const DEFAULT_SLOT: HtmlNode = {
+ type: 'slot',
+ name: 'newSlot',
+ value: '',
+}
+
+function AddChildButton({ onClick }: { onClick(type: string): void }) {
+ const [hovered, setHovered] = useState(false)
+ const [open, setOpen] = useState(false)
+ const nodeTypes = useNodeTypes()
+
+ return (
+
+
+ setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ >
+
+
+
+ {nodeTypes.map((childType) => {
+ return (
+ {
+ onClick(childType)
+ }}
+ sx={{
+ cursor: 'pointer',
+ px: 3,
+ ':hover': {
+ backgroundColor: 'backgroundOffset',
+ },
+ }}
+ >
+ Add {childType}
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/packages/gui/src/components/html/data.ts b/packages/gui/src/components/html/data.ts
new file mode 100644
index 00000000..99ef8d8a
--- /dev/null
+++ b/packages/gui/src/components/html/data.ts
@@ -0,0 +1,134 @@
+export const HTML_TAGS = [
+ 'a',
+ 'abbr',
+ 'address',
+ 'animate',
+ 'animateMotion',
+ 'animateTransform',
+ 'area',
+ 'article',
+ 'aside',
+ 'audio',
+ 'b',
+ 'base',
+ 'bdi',
+ 'bdo',
+ 'blockquote',
+ 'br',
+ 'button',
+ 'canvas',
+ 'caption',
+ 'clipPath',
+ 'circle',
+ 'cite',
+ 'code',
+ 'col',
+ 'colgroup',
+ 'data',
+ 'datalist',
+ 'dd',
+ 'del',
+ 'desc',
+ 'details',
+ 'dfn',
+ 'dialog',
+ 'div',
+ 'dl',
+ 'dt',
+ 'em',
+ 'feBlend',
+ 'feColorMatrix',
+ 'feComponentTransfer',
+ 'feComposite',
+ 'feConvolveMatrix',
+ 'feDiffuseLighting',
+ 'feDisplacementMap',
+ 'feDropShadow',
+ 'feFlood',
+ 'feGaussianBlur',
+ 'feImage',
+ 'feMerge',
+ 'feMorphology',
+ 'feOffset',
+ 'feSpecularLighting',
+ 'feTile',
+ 'feTurbulence',
+ 'fieldset',
+ 'figcaption',
+ 'figure',
+ 'filter',
+ 'footer',
+ 'form',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'header',
+ 'hr',
+ 'i',
+ 'img',
+ 'input',
+ 'ins',
+ 'kbd',
+ 'label',
+ 'legend',
+ 'li',
+ 'line',
+ 'main',
+ 'mark',
+ 'marker',
+ 'menu',
+ 'menuitem',
+ 'meter',
+ 'nav',
+ 'noscript',
+ 'ol',
+ 'optgroup',
+ 'option',
+ 'output',
+ 'p',
+ 'path',
+ 'picture',
+ 'polyline',
+ 'pre',
+ 'progress',
+ 'q',
+ 'rect',
+ 'rp',
+ 'rt',
+ 'rtc',
+ 'ruby',
+ 's',
+ 'samp',
+ 'section',
+ 'select',
+ 'set',
+ 'slot',
+ 'small',
+ 'source',
+ 'span',
+ 'stop',
+ 'sub',
+ 'summary',
+ 'sup',
+ 'svg',
+ 'table',
+ 'tbody',
+ 'td',
+ 'template',
+ 'text',
+ 'textarea',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'time',
+ 'tr',
+ 'track',
+ 'u',
+ 'ul',
+ 'var',
+ 'video',
+ 'wbr',
+]
diff --git a/packages/gui/src/components/html/default-styles.ts b/packages/gui/src/components/html/default-styles.ts
new file mode 100644
index 00000000..5304c0ac
--- /dev/null
+++ b/packages/gui/src/components/html/default-styles.ts
@@ -0,0 +1,22 @@
+export const DEFAULT_STYLES: Record = {
+ button: {},
+ a: {},
+ input: {},
+ h1: { textAlign: 'left' },
+ h2: { textAlign: 'left' },
+ h3: { textAlign: 'left' },
+ h4: { textAlign: 'left' },
+ h5: { textAlign: 'left' },
+ h6: { textAlign: 'left' },
+}
+
+export const DEFAULT_ATTRIBUTES: Record = {
+ svg: {
+ version: '1.1',
+ xmlns: 'http://www.w3.org/2000/svg',
+ },
+ img: {
+ src: 'https://dlu344star2bj.cloudfront.net/i/3090-0015.jpg',
+ alt: 'Image',
+ },
+}
diff --git a/packages/gui/src/components/html/types.ts b/packages/gui/src/components/html/types.ts
new file mode 100644
index 00000000..71d4a77f
--- /dev/null
+++ b/packages/gui/src/components/html/types.ts
@@ -0,0 +1,39 @@
+export interface ElementData {
+ type: 'element' | 'text'
+ tagName?: string
+ attributes?: Record
+ // `style` is an attribute, but we treat it specially for CSS.gui
+ style?: Record
+ value?: string
+ children?: HtmlNode[]
+ props?: Props
+}
+export type Props = {
+ [key: string]: string | number
+}
+
+export interface Slot {
+ type: 'slot'
+ name: string
+ value?: string
+ tagName?: string
+ attributes?: Record
+ style?: Record
+ children?: HtmlNode[]
+ props?: Props
+}
+export interface ComponentData {
+ type: 'component'
+ id: string
+ tagName: string
+ props?: Props
+ value: HtmlNode
+ attributes?: Record
+ style?: Record
+ swappableComponentIds?: string[]
+ children?: HtmlNode[]
+}
+
+export type HtmlBaseNode = ElementData | ComponentData
+export type HtmlNode = HtmlBaseNode | Slot
+export type ElementPath = number[]
diff --git a/packages/gui/src/components/html/util.ts b/packages/gui/src/components/html/util.ts
new file mode 100644
index 00000000..47d60a81
--- /dev/null
+++ b/packages/gui/src/components/html/util.ts
@@ -0,0 +1,165 @@
+import { HtmlNode, ElementPath, Slot } from './types'
+import { isNil } from 'lodash-es'
+
+export const isSamePath = (
+ path1: ElementPath | null,
+ path2: ElementPath | null
+) => {
+ if (!path1 || !path2) {
+ return false
+ }
+
+ return path1.join('-') === path2.join('-')
+}
+
+export const removeTailFromPath = (path: ElementPath) => {
+ const newPath = [...path]
+ newPath.pop()
+ return newPath
+}
+
+export const cleanAttributesForCanvas = (
+ attributes: Record
+) => {
+ const newAttributes = { ...attributes }
+
+ if (newAttributes.href) {
+ newAttributes.href = '#!'
+ }
+
+ return newAttributes
+}
+
+export function getChildAtPath(element: HtmlNode, path: ElementPath): HtmlNode {
+ if (path.length === 0) {
+ return element
+ }
+ if (typeof element === 'string') {
+ return element
+ }
+ const [head, ...rest] = path
+ const child = element.children?.[head]
+ if (isNil(child)) {
+ throw new Error('bad path')
+ }
+ return getChildAtPath(child, rest)
+}
+
+export function getParentAtPath(
+ element: HtmlNode,
+ path: ElementPath
+): HtmlNode {
+ const newPath = [...path]
+ newPath.pop()
+ return getChildAtPath(element, newPath)
+}
+
+export function addChildAtPath(
+ element: HtmlNode,
+ path: ElementPath,
+ item: HtmlNode
+): HtmlNode {
+ // if no path, replace the element
+ if (path.length === 0) {
+ throw new Error('Cannot add to root path')
+ }
+ if (typeof element === 'string') {
+ return element
+ }
+ if (path.length === 1) {
+ return {
+ ...element,
+ children: addAt(element.children ?? [], path[0], item),
+ }
+ }
+ const [head, ...rest] = path
+ const child = element.children?.[head]
+ if (isNil(child)) {
+ throw new Error('bad path')
+ }
+ return {
+ ...element,
+ children: replaceAt(
+ element.children ?? [],
+ head,
+ addChildAtPath(child, rest, item)
+ ),
+ }
+}
+
+export function setChildAtPath(
+ element: HtmlNode,
+ path: ElementPath,
+ newChild: HtmlNode
+): HtmlNode {
+ // if no path, replace the element
+ if (path.length === 0) {
+ return newChild
+ }
+ if (typeof element === 'string') {
+ return element
+ }
+ const [head, ...rest] = path
+ const child = element.children?.[head]
+ if (isNil(child)) {
+ throw new Error('bad path')
+ }
+ return {
+ ...element,
+ children: replaceAt(
+ element.children ?? [],
+ head,
+ setChildAtPath(child, rest, newChild)
+ ),
+ }
+}
+
+export function removeChildAtPath(
+ element: HtmlNode,
+ path: ElementPath
+): HtmlNode {
+ // if no path, invalid
+ if (path.length === 0) {
+ throw new Error('Cannot remove top-level')
+ }
+ if (typeof element === 'string') {
+ return element
+ }
+ if (path.length === 1) {
+ return {
+ ...element,
+ children: removeAt(element.children ?? [], path[0]),
+ }
+ }
+ const [head, ...rest] = path
+ const child = element.children?.[head]
+ if (isNil(child)) {
+ throw new Error('bad path')
+ }
+ return {
+ ...element,
+ children: replaceAt(
+ element.children ?? [],
+ head,
+ removeChildAtPath(child, rest)
+ ),
+ }
+}
+
+function addAt(items: T[], index: number, newItem: T) {
+ const spliced = [...items]
+ spliced.splice(index, 0, newItem)
+ return spliced
+}
+
+export function replaceAt(items: T[], index: number, newItem: T) {
+ const spliced = [...items]
+ spliced.splice(index, 1, newItem)
+ return spliced
+}
+
+function removeAt(items: T[], index: number) {
+ const spliced = [...items]
+ spliced.splice(index, 1)
+ return spliced
+}
diff --git a/packages/gui/src/components/inputs/AngleInput.tsx b/packages/gui/src/components/inputs/AngleInput.tsx
deleted file mode 100644
index 35ca237a..00000000
--- a/packages/gui/src/components/inputs/AngleInput.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { ANGLE_UNITS, CSSUnitValue } from '../../types/css'
-import { DimensionInput } from './Dimension'
-import { EditorProps } from '../../types/editor'
-
-// TODO allow optional percentage for conic gradients
-export function AngleInput({
- value,
- onChange,
- label,
-}: EditorProps & { label: string }) {
- return (
-
- )
-}
-
-const angleConversions = {
- deg: 360,
- turn: 1,
- rad: 2 * Math.PI,
- grad: 400,
-}
-
-const angleSteps = {
- deg: 1,
- turn: 0.01,
- rad: 0.01,
- grad: 1,
-}
diff --git a/packages/gui/src/components/inputs/Animation/field.tsx b/packages/gui/src/components/inputs/Animation/field.tsx
deleted file mode 100644
index 0b2524ae..00000000
--- a/packages/gui/src/components/inputs/Animation/field.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import Layers, { LayerProps } from '../../Layers'
-import {
- Animation,
- animationDirections,
- animationFillModes,
- animationPlayStates,
-} from './types'
-import { stringifyAnimationList } from './stringify'
-import { EditorPropsWithLabel, getInputProps } from '../../../lib/util'
-import { SelectInput } from '../SelectInput'
-import { TimeInput } from '../TimeInput'
-import { EasingFunctionEditor } from '../EasingFunction'
-import { DimensionInput } from '../Dimension'
-import { Label } from '../../primitives'
-import { useId } from 'react'
-
-export default function AnimationInput(
- props: EditorPropsWithLabel
-) {
- const newItem = () => {
- return {
- name: 'none',
- timingFunction: { type: 'cubic-bezier', p1: 0, p2: 0, p3: 0, p4: 0 },
- direction: 'normal',
- duration: { value: 350, unit: 'ms' },
- delay: { value: 0, unit: 'ms' },
- fillMode: 'none',
- iterationCount: { value: 1, unit: 'number' },
- playState: 'running',
- } as const
- }
- return (
-
- {...props}
- newItem={newItem}
- content={AnimationEntry}
- stringify={stringifyAnimationList}
- />
- )
-}
-
-export const AnimationEntry = (props: LayerProps) => {
- return (
-
- )
-}
-
-function NameInput({ label, value, onChange }: EditorPropsWithLabel) {
- const id = `${useId()}-${label}`
- return (
-
- {label}
- onChange(e.target.value)}
- />
-
- )
-}
diff --git a/packages/gui/src/components/inputs/Animation/stringify.ts b/packages/gui/src/components/inputs/Animation/stringify.ts
deleted file mode 100644
index 033d6832..00000000
--- a/packages/gui/src/components/inputs/Animation/stringify.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { stringifyValues } from '../../../lib/stringify'
-import { stringifyEasingFunction } from '../EasingFunction/stringify'
-import { Animation, animationDirections } from './types'
-
-export function stringifyAnimationList(animationList: Animation[]) {
- return animationList.map(stringifyAnimation).join(', ')
-}
-
-export function stringifyAnimation(animation: Animation) {
- const {
- duration,
- delay,
- fillMode,
- iterationCount,
- name,
- playState,
- direction,
- timingFunction,
- } = animation
-
- return stringifyValues([
- name,
- duration,
- stringifyEasingFunction(timingFunction),
- delay,
- iterationCount,
- direction,
- fillMode,
- playState,
- ])
-}
diff --git a/packages/gui/src/components/inputs/Animation/types.ts b/packages/gui/src/components/inputs/Animation/types.ts
deleted file mode 100644
index 460c33ca..00000000
--- a/packages/gui/src/components/inputs/Animation/types.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { CSSUnitValue, Time } from '../../../types/css'
-import { EasingFunction } from '../EasingFunction/types'
-
-export interface Animation {
- delay: Time
- direction: AnimationDirection
- duration: Time
- fillMode: AnimationFillMode
- iterationCount: CSSUnitValue
- name: string
- playState: AnimationPlayState
- timingFunction: EasingFunction
-}
-
-export const animationDirections = [
- 'normal',
- 'reverse',
- 'alternate',
- 'alternate-reverse',
-] as const
-export type AnimationDirection = typeof animationDirections[number]
-
-export const animationFillModes = [
- 'none',
- 'forwards',
- 'backwards',
- 'both',
-] as const
-export type AnimationFillMode = typeof animationFillModes[number]
-
-export const animationPlayStates = ['running', 'paused'] as const
-export type AnimationPlayState = typeof animationPlayStates[number]
diff --git a/packages/gui/src/components/inputs/Background/field.tsx b/packages/gui/src/components/inputs/Background/field.tsx
deleted file mode 100644
index 49bb898d..00000000
--- a/packages/gui/src/components/inputs/Background/field.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { EditorProps } from '../../../types/editor'
-import {
- attachmentKeywords,
- Background,
- repeatKeywords,
- RepeatStyle,
-} from './types'
-
-import { stringifyBackgroundList } from './stringify'
-
-import Layers from '../../Layers'
-import { EditorPropsWithLabel, getInputProps } from '../../../lib/util'
-import { ImageSourceEditor } from '../ImageSource/field'
-import { PositionInput } from '../PositionInput'
-import { Label } from '../../primitives'
-import { SelectInput } from '../SelectInput'
-import { BgSizeInput } from '../BgSizeInput'
-import { BOX_KEYWORDS } from '../../../types/css'
-
-export default function BackgroundInput(
- props: EditorPropsWithLabel
-) {
- const newItem = () => {
- // generate a new text shadow with the units of the previous box shadow
- return {
- attachment: 'scroll',
- clip: 'border-box',
- image: {
- type: 'gradient',
- gradient: {
- type: 'linear',
- degrees: 0,
- stops: [],
- },
- },
- origin: 'border-box',
- position: {
- x: { value: 0, unit: 'px' },
- y: { value: 0, unit: 'px' },
- },
- repeat: {
- x: 'no-repeat',
- y: 'no-repeat',
- },
- size: {
- x: { value: 100, unit: '%' },
- y: { value: 100, unit: '%' },
- },
- } as any
- }
- return (
-
- {...props}
- newItem={newItem}
- content={BackgroundLayer}
- stringify={stringifyBackgroundList}
- thumbnail={Thumbnail}
- />
- )
-}
-
-export const BackgroundLayer = (props: EditorProps) => {
- return (
-
- )
-}
-
-export function RepeatStyleInput(props: EditorPropsWithLabel) {
- return (
-
- )
-}
-
-function Thumbnail({ value }: { value: string }) {
- return (
-
- )
-}
diff --git a/packages/gui/src/components/inputs/Background/stringify.ts b/packages/gui/src/components/inputs/Background/stringify.ts
deleted file mode 100644
index 9fa9b580..00000000
--- a/packages/gui/src/components/inputs/Background/stringify.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { stringifyPosition, stringifyValues } from '../../../lib/stringify'
-import { stringifyBgSize } from '../BgSizeInput'
-import { stringifyImageSource } from '../ImageSource/stringify'
-import { Background, RepeatStyle } from './types'
-
-export function stringifyBackgroundList(backgrounds: Background[]) {
- return backgrounds.map(stringifyBackground).join(', ')
-}
-
-export function stringifyBackground(background: Background) {
- const { attachment, clip, image, origin, position, repeat, size } = background
- return stringifyValues([
- stringifyImageSource(image),
- stringifyPosition(position),
- `/ ${stringifyBgSize(size)}`,
- stringifyRepeatStyle(repeat),
- attachment,
- origin,
- clip,
- ])
-}
-
-export function stringifyRepeatStyle(repeat: RepeatStyle) {
- return `${repeat.x} ${repeat.y}`
-}
diff --git a/packages/gui/src/components/inputs/Background/types.ts b/packages/gui/src/components/inputs/Background/types.ts
deleted file mode 100644
index 0dee6cef..00000000
--- a/packages/gui/src/components/inputs/Background/types.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Box, Position } from '../../../types/css'
-import { BgSize } from '../BgSizeInput'
-import { ImageSource } from '../ImageSource/types'
-
-export interface Background {
- attachment: Attachment
- clip: Box | 'text'
- // TODO background color for final layer
- // color: Color
- image: ImageSource
- origin: Box
- position: Position
- repeat: RepeatStyle
- size: BgSize
-}
-
-export interface RepeatStyle {
- x: Repeat
- y: Repeat
-}
-
-export const repeatKeywords = ['repeat', 'space', 'round', 'no-repeat'] as const
-type Repeat = typeof repeatKeywords[number]
-
-export const attachmentKeywords = ['scroll', 'fixed', 'local'] as const
-type Attachment = typeof attachmentKeywords[number]
diff --git a/packages/gui/src/components/inputs/BasicShape/input.tsx b/packages/gui/src/components/inputs/BasicShape/input.tsx
deleted file mode 100644
index a5910577..00000000
--- a/packages/gui/src/components/inputs/BasicShape/input.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-import { EditorPropsWithLabel, getInputProps } from '../../../lib/util'
-import { EditorProps } from '../../../types/editor'
-import FieldArray from '../../FieldArray'
-import { Label } from '../../primitives'
-import { LengthInput } from '../LengthInput'
-import { PositionInput } from '../PositionInput'
-import { SelectInput } from '../SelectInput'
-import { stringifyBasicShape } from './stringify'
-import {
- BasicShape,
- BasicShapeType,
- Circle,
- Ellipse,
- Inset,
- Path,
- Point,
- Polygon,
-} from './types'
-
-export function BasicShapeInput(props: EditorPropsWithLabel) {
- return (
-
- {props.label}
- props.onChange(getDefaultBasicShape(type))}
- />
-
-
- )
-}
-
-function BasicShapeSwitch(props: EditorProps) {
- switch (props.value.type) {
- case 'inset': {
- const _props = props as EditorProps
- return (
-
-
-
-
-
-
-
- )
- }
- case 'circle': {
- const _props = props as EditorProps
- return (
-
- )
- }
- case 'ellipse': {
- const _props = props as EditorProps
- return (
-
- )
- }
- case 'polygon': {
- const _props = props as EditorProps
- return (
-
-
- ({
- x: { value: 0, unit: 'px' },
- y: { value: 0, unit: 'px' },
- })}
- />
-
- )
- }
- case 'path': {
- const _props = props as EditorProps
- return (
-
-
-
- path
-
- _props.onChange({ ..._props.value, path: e.target.value })
- }
- />
-
-
- )
- }
- }
-}
-
-function PointInput(props: EditorPropsWithLabel) {
- return (
-
-
-
-
- )
-}
-
-export function getDefaultBasicShape(type: BasicShapeType): BasicShape {
- switch (type) {
- case 'inset': {
- return {
- type,
- top: { value: 0, unit: 'px' },
- right: { value: 0, unit: 'px' },
- bottom: { value: 0, unit: 'px' },
- left: { value: 0, unit: 'px' },
- borderRadius: { value: 0, unit: 'px' },
- }
- }
- case 'circle': {
- return {
- type,
- radius: { value: 'closest-side', unit: 'keyword' },
- position: {
- x: { value: 'center', unit: 'keyword' },
- y: { value: 'center', unit: 'keyword' },
- },
- }
- }
- case 'ellipse': {
- return {
- type,
- rx: { value: 'closest-side', unit: 'keyword' },
- ry: { value: 'closest-side', unit: 'keyword' },
- position: {
- x: { value: 'center', unit: 'keyword' },
- y: { value: 'center', unit: 'keyword' },
- },
- }
- }
- case 'polygon': {
- return {
- type,
- fillRule: 'nonzero',
- points: [
- {
- x: { value: 0, unit: '%' },
- y: { value: 0, unit: '%' },
- },
- {
- x: { value: 100, unit: '%' },
- y: { value: 0, unit: '%' },
- },
- {
- x: { value: 100, unit: '%' },
- y: { value: 100, unit: '%' },
- },
- {
- x: { value: 0, unit: '%' },
- y: { value: 100, unit: '%' },
- },
- ],
- }
- }
- case 'path': {
- return {
- type,
- fillRule: 'nonzero',
- path: '',
- }
- }
- }
-}
diff --git a/packages/gui/src/components/inputs/BasicShape/stringify.ts b/packages/gui/src/components/inputs/BasicShape/stringify.ts
deleted file mode 100644
index 658fe904..00000000
--- a/packages/gui/src/components/inputs/BasicShape/stringify.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import {
- stringifyFunction,
- stringifyPosition,
- stringifyUnit,
-} from '../../../lib/stringify'
-import { BasicShape } from './types'
-
-export function stringifyBasicShape(value: BasicShape) {
- const { type } = value
- switch (type) {
- case 'inset': {
- const { top, right, bottom, left, borderRadius } = value
- return stringifyFunction(
- type,
- [top, right, bottom, left, 'round', borderRadius],
- ' '
- )
- }
- case 'circle': {
- const { radius, position } = value
- return stringifyFunction(
- type,
- [radius, 'at', stringifyPosition(position)],
- ' '
- )
- }
- case 'ellipse': {
- const { rx, ry, position } = value
- return stringifyFunction(
- type,
- [rx, ry, 'at', stringifyPosition(position)],
- ' '
- )
- }
- case 'polygon': {
- const { fillRule, points } = value
- console.log(points)
- return stringifyFunction(type, [
- fillRule,
- points
- .map(({ x, y }) => `${stringifyUnit(x)} ${stringifyUnit(y)}`)
- .join(', '),
- ])
- }
- case 'path': {
- const { fillRule, path } = value
- return stringifyFunction(type, [fillRule, `"${path}"`])
- }
- }
-}
diff --git a/packages/gui/src/components/inputs/BasicShape/types.ts b/packages/gui/src/components/inputs/BasicShape/types.ts
deleted file mode 100644
index 78938f5c..00000000
--- a/packages/gui/src/components/inputs/BasicShape/types.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Length, LengthPercentage, Position } from '../../../types/css'
-
-export type BasicShape = Inset | Circle | Ellipse | Polygon | Path
-export type BasicShapeType = BasicShape['type']
-
-export interface Inset {
- type: 'inset'
- top: LengthPercentage
- left: LengthPercentage
- bottom: LengthPercentage
- right: LengthPercentage
- // TODO full border radius syntax
- borderRadius: LengthPercentage
-}
-
-export interface Circle {
- type: 'circle'
- radius: LengthPercentage // keywords: closest-side, farthest-side
- position: Position
-}
-
-export interface Ellipse {
- type: 'ellipse'
- rx: LengthPercentage
- ry: LengthPercentage
- position: Position
-}
-
-export interface Polygon {
- type: 'polygon'
- fillRule: FillRule
- points: Point[]
-}
-
-export interface Path {
- type: 'path'
- fillRule: FillRule
- path: string // TODO svg path editor
-}
-
-type FillRule = 'nonzero' | 'evenodd'
-export type Point = { x: LengthPercentage; y: LengthPercentage }
diff --git a/packages/gui/src/components/inputs/BgSizeInput.tsx b/packages/gui/src/components/inputs/BgSizeInput.tsx
deleted file mode 100644
index 8dc836cc..00000000
--- a/packages/gui/src/components/inputs/BgSizeInput.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { stringifyUnit } from '../../lib/stringify'
-import { EditorPropsWithLabel, getInputProps } from '../../lib/util'
-import { LengthPercentage } from '../../types/css'
-import { Label } from '../primitives'
-import { LengthInput } from './LengthInput'
-
-export type BgSize = BgSizeKeyword | BgSizeDimensions
-
-interface BgSizeKeyword {
- type: 'keyword'
- value: 'contain' | 'cover'
-}
-
-interface BgSizeDimensions {
- type: 'dimensions'
- x: LengthPercentage
- y: LengthPercentage
-}
-
-/**
- * Input representing a background size (or anything in the shape of a background size, like clip-size).
- */
-export function BgSizeInput(props: EditorPropsWithLabel) {
- return (
-
-
- {props.label}
- {props.value.type === 'keyword' ? (
-
- props.onChange({ ...props.value, value: e.target.value } as any)
- }
- >
- {/* TODO enable global keywords */}
- cover
- contain
-
- ) : (
- {stringifyBgSize(props.value)}
- )}
-
- props.onChange(getDefaultBgSize(e.target.value as any))
- }
- >
- keyword
- dimensions
-
-
- {props.value.type === 'dimensions' && (
-
-
-
-
- )}
-
- )
-}
-
-export function stringifyBgSize(size: BgSize) {
- switch (size.type) {
- case 'keyword':
- return size.value
- case 'dimensions':
- return `${stringifyUnit(size.x)} ${stringifyUnit(size.y)}`
- }
-}
-
-function getDefaultBgSize(type: 'keyword' | 'dimensions'): BgSize {
- switch (type) {
- case 'keyword':
- return { type, value: 'cover' }
- case 'dimensions':
- return {
- type,
- x: { value: 100, unit: '%' },
- y: { value: 100, unit: '%' },
- }
- }
-}
diff --git a/packages/gui/src/components/inputs/BorderSpacing.tsx b/packages/gui/src/components/inputs/BorderSpacing.tsx
deleted file mode 100644
index 78889cc7..00000000
--- a/packages/gui/src/components/inputs/BorderSpacing.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { stringifyValues } from '../../lib/stringify'
-import { EditorPropsWithLabel, getInputProps } from '../../lib/util'
-import { Length } from '../../types/css'
-import { Label } from '../primitives'
-import { LengthInput } from './LengthInput'
-
-interface BorderSpacing {
- x: Length
- y: Length
-}
-
-export function BorderSpacingInput(props: EditorPropsWithLabel) {
- return (
-
-
{props.label}
-
-
-
-
-
- )
-}
-
-export function stringifyBorderSpacing(value: BorderSpacing) {
- return stringifyValues([value.x, value.y])
-}
diff --git a/packages/gui/src/components/inputs/BoxShadow/field.tsx b/packages/gui/src/components/inputs/BoxShadow/field.tsx
deleted file mode 100644
index 2128c32f..00000000
--- a/packages/gui/src/components/inputs/BoxShadow/field.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { ColorInput } from '../ColorInput'
-import { CheckboxInput } from '../CheckboxInput'
-import { LengthInput } from '../LengthInput'
-import Layers, { LayerProps } from '../../Layers'
-import { BoxShadow } from './types'
-import { stringifyBoxShadow } from './stringify'
-import { EditorPropsWithLabel, getInputProps } from '../../../lib/util'
-
-export default function BoxShadowInput(
- props: EditorPropsWithLabel
-) {
- const newItem = () => {
- return {
- spread: { value: 0, unit: 'px' },
- blur: { value: 0, unit: 'px' },
- offsetX: { value: 0, unit: 'px' },
- offsetY: { value: 0, unit: 'px' },
- color: '#000',
- } as const
- }
- return (
-
- {...props}
- newItem={newItem}
- content={BoxShadowEditor}
- stringify={stringifyBoxShadow}
- thumbnail={Thumbnail}
- />
- )
-}
-
-export const BoxShadowEditor = (props: LayerProps) => {
- return (
-
- )
-}
-
-function Thumbnail({ value }: { value: string }) {
- return (
-
- )
-}
diff --git a/packages/gui/src/components/inputs/BoxShadow/stringify.tsx b/packages/gui/src/components/inputs/BoxShadow/stringify.tsx
deleted file mode 100644
index 3bffbc62..00000000
--- a/packages/gui/src/components/inputs/BoxShadow/stringify.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { BoxShadow } from './types'
-import { stringifyUnit } from '../../../lib/stringify'
-
-export function stringifyBoxShadow(boxShadow: BoxShadow | BoxShadow[]): string {
- if (Array.isArray(boxShadow)) {
- return boxShadow.filter(Boolean).map(stringifyBoxShadow).join(', ')
- }
-
- return stringifyEntry(boxShadow)
-}
-
-const stringifyEntry = (boxShadow: BoxShadow) => {
- return [
- boxShadow.inset && 'inset',
- stringifyUnit(boxShadow.offsetX),
- stringifyUnit(boxShadow.offsetY),
- stringifyUnit(boxShadow.blur),
- stringifyUnit(boxShadow.spread),
- boxShadow.color,
- ]
- .filter(Boolean)
- .join(' ')
-}
diff --git a/packages/gui/src/components/inputs/BoxShadow/types.ts b/packages/gui/src/components/inputs/BoxShadow/types.ts
deleted file mode 100644
index 4ac090f6..00000000
--- a/packages/gui/src/components/inputs/BoxShadow/types.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Color, Length } from '../../../types/css'
-export interface BoxShadow {
- inset?: boolean
- offsetX: Length
- offsetY: Length
- spread: Length
- blur: Length
- color: Color
-}
diff --git a/packages/gui/src/components/inputs/CheckboxInput.tsx b/packages/gui/src/components/inputs/CheckboxInput.tsx
index 29b2b5b5..269de8d4 100644
--- a/packages/gui/src/components/inputs/CheckboxInput.tsx
+++ b/packages/gui/src/components/inputs/CheckboxInput.tsx
@@ -1,23 +1,14 @@
-import { kebabCase } from 'lodash-es'
-import { useId } from 'react'
import { EditorProps } from '../../types/editor'
-import { Label } from '../primitives'
export function CheckboxInput({
- label,
value = false,
onChange,
-}: EditorProps & { label: string }) {
- const id = `${useId()}-${kebabCase(label)}`
+}: EditorProps) {
return (
- <>
- {label}
- onChange(e.target.checked)}
- />
- >
+ onChange(e.target.checked)}
+ />
)
}
diff --git a/packages/gui/src/components/inputs/ClipPath.tsx b/packages/gui/src/components/inputs/ClipPath.tsx
deleted file mode 100644
index 7a82473b..00000000
--- a/packages/gui/src/components/inputs/ClipPath.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Label } from 'theme-ui'
-import { stringifyValues } from '../../lib/stringify'
-import { EditorPropsWithLabel, getInputProps } from '../../lib/util'
-import { GeometryBox, GEOMETRY_BOX_KEYWORDS } from '../../types/css'
-import { BasicShapeInput } from './BasicShape/input'
-import { stringifyBasicShape } from './BasicShape/stringify'
-import { BasicShape } from './BasicShape/types'
-import { SelectInput } from './SelectInput'
-
-// TODO `url()` option
-interface ClipPath {
- shape: BasicShape
- box: GeometryBox
-}
-
-export function ClipPathInput(props: EditorPropsWithLabel) {
- return (
-
- {props.label}
-
-
-
- )
-}
-
-export function stringifyClipPath(value: ClipPath) {
- return stringifyValues([stringifyBasicShape(value.shape), value.box])
-}
diff --git a/packages/gui/src/components/inputs/ColorInput.tsx b/packages/gui/src/components/inputs/ColorInput.tsx
index 972d167b..5a9e3977 100644
--- a/packages/gui/src/components/inputs/ColorInput.tsx
+++ b/packages/gui/src/components/inputs/ColorInput.tsx
@@ -1,32 +1,18 @@
-import { useId } from 'react'
import { Color } from '../../types/css'
-import { EditorProps } from '../../types/editor'
-import { ColorPopover, Label } from '../primitives'
+import { EditorPropsWithLabel } from '../../types/editor'
+import { ColorPopover } from '../primitives'
import { useTheme } from '../providers/ThemeContext'
+import { InputHeader } from '../ui/InputHeader'
-interface Props extends EditorProps {
- label: string
+interface Props extends EditorPropsWithLabel {
defaultValue?: Color
}
-export function ColorInput({
- label,
- value,
- onChange,
- defaultValue = '#000',
-}: Props) {
+export function ColorInput(props: Props) {
const theme = useTheme()
- const id = useId()
- const fullId = `${id}-${label}`
return (
-
-
{label}
-
+
+
)
}
diff --git a/packages/gui/src/components/inputs/Dimension/Input.tsx b/packages/gui/src/components/inputs/Dimension/Input.tsx
index d4940a53..ea79f07d 100644
--- a/packages/gui/src/components/inputs/Dimension/Input.tsx
+++ b/packages/gui/src/components/inputs/Dimension/Input.tsx
@@ -1,164 +1,95 @@
-import * as React from 'react'
-import {
- AbsoluteLengthUnits,
- CSSUnitValue,
- KeywordUnits,
- ThemeUnits,
-} from '../../../types/css'
-import { Label, Number, UnitSelect, ValueSelect } from '../../primitives'
-import { reducer } from './reducer'
-import { State } from './types'
-import { EditorProps } from '../../../types/editor'
+import { CSSUnitValue, Dimension } from '../../../types/css'
+import { Number, UnitSelect } from '../../primitives'
+import { EditorPropsWithLabel } from '../../../types/editor'
import { UnitConversions } from '../../../lib/convert'
-import { compact, kebabCase } from 'lodash-es'
+import { convertUnits } from '../../../lib/convert'
+import { X } from 'react-feather'
+import IconButton from '../../ui/IconButton'
// Mapping of units to [min, max] tuple
type UnitRanges = Record
+export type Range = UnitRanges | 'nonnegative'
// Mapping of units to steps
type UnitSteps = Record
-export interface DimensionInputProps extends EditorProps {
- label?: string
- range?: UnitRanges
+export interface DimensionInputProps extends EditorPropsWithLabel {
+ range?: Range
steps?: UnitSteps
units?: readonly string[]
- /** The available keyword values for the property. If provided, 'keyword' will be appended as a unit */
- keywords?: string[]
- /** The available theme values for the property. If provided, 'theme' will be appended as a unit */
- themeValues?: (CSSUnitValue & { id: string })[]
conversions?: UnitConversions
}
-export const DimensionInput = ({
- value,
- onChange,
- label,
- range,
- units = [],
- keywords = [],
- themeValues = [],
- steps,
- conversions = {},
-}: DimensionInputProps) => {
- const id = `${React.useId()}-${kebabCase(label)}`
- const [state, dispatch] = React.useReducer(reducer, {
- value: value?.value || 0,
- unit: value?.unit || AbsoluteLengthUnits.Px,
- themeId: value?.themeId,
- key: 0,
- } as State)
- React.useEffect(() => {
- if (
- // Only want to call on change when the value differs
- state.value !== value?.value ||
- state.unit !== value?.unit ||
- state.themeId !== value?.themeId
- ) {
- const newValue: CSSUnitValue = {
- value: state.value,
- unit: state.unit,
- }
- if (state.themeId) {
- newValue.themeId = state.themeId
- }
- onChange(newValue)
- }
- }, [state])
- const allUnits = compact([
- themeValues.length > 0 && 'theme',
- ...units,
- keywords.length > 0 && 'keyword',
- ])
+export function DimensionInput(props: DimensionInputProps) {
+ const {
+ value = {},
+ onChange,
+ range: providedRange,
+ units = [],
+ steps,
+ conversions = {},
+ } = props
- return (
-
- {label && (
-
- {label}
-
- )}
-
- {state.unit === KeywordUnits.Keyword ? (
-
{
- dispatch({
- type: 'CHANGED_INPUT_VALUE',
- value: e.target.value,
- })
- }}
- />
- ) : state.themeId ? (
- {
- const themeValue = themeValues?.find(
- (p) => p.id === e.target.value
- )
- dispatch({
- type: 'CHANGED_INPUT_TO_THEME_VALUE',
- value: themeValue?.value ?? 0,
- unit: (themeValue?.unit as any) ?? 'px',
- themeId: e.target.value,
- })
- }}
- values={themeValues ?? []}
- />
- ) : (
- {
- dispatch({
- type: 'CHANGED_INPUT_VALUE',
- value: newValue,
- })
- }}
- />
- )}
- ) => {
- const newUnit = e.target.value
+ const range =
+ providedRange === 'nonnegative' ? nonnegativeRange(units) : providedRange
- if (newUnit === ThemeUnits.Theme) {
- const themeValue = themeValues?.[0]
- return dispatch({
- type: 'CHANGED_INPUT_TO_THEME_VALUE',
- value: themeValue?.value ?? 0,
- unit: (themeValue?.unit as any) ?? 'px',
- themeId: themeValue?.id,
- })
- }
+ const normedValue = value as CSSUnitValue
- if (newUnit === KeywordUnits.Keyword) {
- dispatch({
- type: 'CHANGED_INPUT_VALUE',
- value: keywords[0],
- })
- }
+ const allUnits = units
- dispatch({
- type: 'CHANGED_UNIT_VALUE',
- unit: newUnit,
- steps: steps,
+ return (
+
+ {
+ onChange({
+ ...normedValue,
+ value: newValue,
+ })
+ }}
+ />
+ {
+ onChange({
+ unit: newUnit,
+ value: convertUnits(
+ newUnit,
+ normedValue,
conversions,
- })
- }}
- sx={{ marginLeft: 1, minHeight: '1.6em', width: 72 }}
- />
-
+ steps
+ ) as any,
+ })
+ }}
+ />
)
}
+
+export function nonnegativeRange(units: readonly string[]): UnitRanges {
+ return Object.fromEntries(units.map((unit) => [unit, [0, Infinity]]))
+}
+interface DeleteProps {
+ onRemove(): void
+}
+export const DeletePropButton = ({ onRemove }: DeleteProps) => {
+ return (
+
onRemove()}
+ >
+
+
+ )
+}
diff --git a/packages/gui/src/components/inputs/Dimension/reducer.ts b/packages/gui/src/components/inputs/Dimension/reducer.ts
deleted file mode 100644
index 20578e33..00000000
--- a/packages/gui/src/components/inputs/Dimension/reducer.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { convertUnits } from '../../../lib/convert'
-import { State, Action } from './types'
-
-export const reducer = (state: State, action: Action): State => {
- switch (action.type) {
- case 'CHANGED_INPUT_VALUE': {
- return {
- ...state,
- value: action.value,
- themeId: action.themeId,
- }
- }
- case 'CHANGED_UNIT_VALUE': {
- return {
- ...state,
- value: convertUnits(
- action.unit,
- state,
- action.conversions,
- action.steps
- ),
- unit: action.unit,
- key: state.key + 1, // Force number scrubber re-render
- themeId: undefined,
- }
- }
- case 'CHANGED_INPUT_TO_THEME_VALUE': {
- return {
- ...state,
- value: action.value,
- unit: action.unit,
- themeId: action.themeId,
- }
- }
- default: {
- break
- }
- }
-
- return state
-}
diff --git a/packages/gui/src/components/inputs/Dimension/types.ts b/packages/gui/src/components/inputs/Dimension/types.ts
deleted file mode 100644
index 033775aa..00000000
--- a/packages/gui/src/components/inputs/Dimension/types.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { UnitConversions, UnitSteps } from '../../../lib'
-
-export type State = {
- key: number
- value: number | string
- unit: string
- themeId?: string
- min?: number
- max?: number
-}
-
-export type Action =
- | {
- type: 'CHANGED_INPUT_VALUE'
- value: number | string
- themeId?: string
- }
- | {
- type: 'CHANGED_UNIT_VALUE'
- unit: string
- conversions: UnitConversions
- steps?: UnitSteps
- }
- | {
- type: 'CHANGED_INPUT_TO_THEME_VALUE'
- value: number | string
- unit: string
- themeId?: string
- }
diff --git a/packages/gui/src/components/inputs/EasingFunction/stringify.ts b/packages/gui/src/components/inputs/EasingFunction/stringify.ts
index 2300914a..452a3145 100644
--- a/packages/gui/src/components/inputs/EasingFunction/stringify.ts
+++ b/packages/gui/src/components/inputs/EasingFunction/stringify.ts
@@ -1,12 +1,7 @@
-import { getKeywordFromValue } from './keywords'
import { EasingFunction } from './types'
// Convert the transition function to a CSS Value
export function stringifyEasingFunction(value: EasingFunction) {
- const keyword = getKeywordFromValue(value)
- if (keyword) {
- return keyword
- }
switch (value.type) {
case 'cubic-bezier':
return `cubic-bezier(${value.p1}, ${value.p2}, ${value.p3}, ${value.p4})`
diff --git a/packages/gui/src/components/inputs/Filter/field.tsx b/packages/gui/src/components/inputs/Filter/field.tsx
deleted file mode 100644
index b1c99ff5..00000000
--- a/packages/gui/src/components/inputs/Filter/field.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-import { LengthInput } from '../LengthInput'
-import Layers, { LayerProps } from '../../Layers'
-
-import {
- Filter,
- FilterType,
- Blur,
- DropShadow,
- HueRotate,
- AmountFilter,
-} from './types'
-import { stringifyFilter } from './stringify'
-import { EditorProps } from '../../../types/editor'
-import { EditorPropsWithLabel, getInputProps } from '../../../lib/util'
-import { SelectInput } from '../SelectInput'
-import { NumberPercentageInput } from '../NumberPercentageInput'
-import { AngleInput } from '../AngleInput'
-import { ColorInput } from '../ColorInput'
-
-export default function FilterInput(props: EditorPropsWithLabel
) {
- const newItem = () => {
- return getDefault('blur')
- }
- return (
-
- {...props}
- newItem={newItem}
- stringify={stringifyFilter}
- content={FilterEditor}
- thumbnail={Thumbnail}
- />
- )
-}
-
-export const FilterEditor = (props: LayerProps) => {
- return (
-
- {
- props.onChange(convertFilterValue(props.value, newType))
- }}
- />
-
-
- )
-}
-
-const filterTypes = [
- 'blur',
- 'brightness',
- 'contrast',
- 'drop-shadow',
- 'grayscale',
- 'hue-rotate',
- 'invert',
- 'opacity',
- 'saturate',
- 'sepia',
-] as const
-
-function FilterSwitch(props: LayerProps) {
- switch (props.value.type) {
- case 'blur': {
- const _props = props as EditorProps
- return
- }
- case 'drop-shadow': {
- const _props = props as EditorProps
- return (
-
-
-
-
-
-
- )
- }
- case 'hue-rotate': {
- const _props = props as EditorProps
- return
- }
- default: {
- const _props = props as EditorProps
- // TODO some of the filters have different boundaries
- return
- }
- }
-}
-function Thumbnail({ value }: { value: string }) {
- return (
-
- )
-}
-
-function convertFilterValue(value: Filter, newType: FilterType): Filter {
- if (value.type === newType) {
- return value
- }
-
- // When converting between two values that take a number-percentage amount
- // keep the amount
- if (isAmountFilter(value.type) && isAmountFilter(newType)) {
- return { ...value, type: newType } as any
- }
-
- // Otherwise, reset to the default of that filter type
- return getDefault(newType)
-}
-
-function getDefault(type: FilterType): Filter {
- switch (type) {
- case 'hue-rotate':
- return { type, angle: { value: 0, unit: 'deg' } }
- case 'blur':
- return { type, radius: { value: 0, unit: 'px' } }
- case 'drop-shadow':
- return {
- type,
- offsetX: { value: 0, unit: 'px' },
- offsetY: { value: 0, unit: 'px' },
- blurRadius: { value: 0, unit: 'px' },
- color: '#000',
- }
- default:
- return { type, amount: { value: 0, unit: 'number' } }
- }
-}
-
-function isAmountFilter(type: FilterType) {
- return !['hue-rotate', 'blur', 'drop-shadow'].includes(type)
-}
diff --git a/packages/gui/src/components/inputs/Filter/stringify.ts b/packages/gui/src/components/inputs/Filter/stringify.ts
deleted file mode 100644
index d84bf22e..00000000
--- a/packages/gui/src/components/inputs/Filter/stringify.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { stringifyFunction, stringifyUnit } from '../../../lib/stringify'
-import { Filter } from './types'
-
-export function stringifyFilter(filter: Filter | Filter[]) {
- if (Array.isArray(filter)) {
- return filter.map(stringifyEntry).join(' ')
- }
- return stringifyEntry(filter)
-}
-
-function stringifyEntry(filter: Filter) {
- const { type } = filter
- switch (type) {
- case 'blur':
- return stringifyFunction(type, [filter.radius])
- case 'drop-shadow': {
- const { offsetX, offsetY, blurRadius, color } = filter
- return stringifyFunction(type, [offsetX, offsetY, blurRadius, color], ' ')
- }
- case 'hue-rotate':
- return stringifyFunction(type, [filter.angle])
- default:
- return stringifyFunction(type, [filter.amount])
- }
-}
diff --git a/packages/gui/src/components/inputs/Filter/types.ts b/packages/gui/src/components/inputs/Filter/types.ts
deleted file mode 100644
index a00d46ab..00000000
--- a/packages/gui/src/components/inputs/Filter/types.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Angle, Color, Length, NumberPercentage } from '../../../types/css'
-
-// TODO URLs
-export type Filter = Blur | DropShadow | HueRotate | AmountFilter
-export type FilterType = Filter['type']
-
-export interface Blur {
- type: 'blur'
- radius: Length
-}
-
-export interface DropShadow {
- type: 'drop-shadow'
- offsetX: Length
- offsetY: Length
- blurRadius: Length
- color: Color
-}
-
-export interface HueRotate {
- type: 'hue-rotate'
- angle: Angle
-}
-
-// A filter with a single number-percentage amount
-export interface AmountFilter {
- type:
- | 'brightness'
- | 'contrast'
- | 'grayscale'
- | 'invert'
- | 'opacity'
- | 'saturate'
- | 'sepia'
- amount: NumberPercentage
-}
diff --git a/packages/gui/src/components/inputs/FontFamily.tsx b/packages/gui/src/components/inputs/FontFamily.tsx
index 3e4e3a0d..a0178956 100644
--- a/packages/gui/src/components/inputs/FontFamily.tsx
+++ b/packages/gui/src/components/inputs/FontFamily.tsx
@@ -2,13 +2,14 @@ import { EditorProps } from '../../types/editor'
import { FontFamilyType } from '../../types/css'
import { FontFamilyInput } from './FontFamily/Input'
-export const FontFamily = ({value, onChange}: EditorProps) => {
+export const FontFamily = ({ value, onChange, onRemove }: EditorProps) => {
return (
// @ts-ignore
)
}
diff --git a/packages/gui/src/components/inputs/FontFamily/FontTags.tsx b/packages/gui/src/components/inputs/FontFamily/FontTags.tsx
index d815cffe..52847684 100644
--- a/packages/gui/src/components/inputs/FontFamily/FontTags.tsx
+++ b/packages/gui/src/components/inputs/FontFamily/FontTags.tsx
@@ -1,73 +1,105 @@
import * as React from 'react'
import { debounce } from 'lodash-es'
-import { toGoogleFontUrl, toGoogleVariableFontUrl } from '../../../lib/util'
+import {
+ toGoogleFontUrl,
+ toGoogleVariableFontUrl,
+ FontFamilyData,
+} from '../../../lib/util'
-export const getVariableFontFamilyHref = async (
- fontFamily: string
-) => {
- const formattedName = fontFamily?.replace(/['"]+/g, '')
- try {
- const res = await fetch(`https://components.ai/api/v1/typefaces/variable?name=${formattedName}`)
- const varFontData = await res.json()
- const fullData = {
- name: fontFamily,
- ...(varFontData ?? {})
+const getVariableFontFamiliesData = async (fonts: string[]) => {
+ const data = []
+ for (const font of fonts) {
+ const formattedName = font.replace(/['"]+/g, '')
+ try {
+ const res = await fetch(
+ `https://components.ai/api/v1/typefaces/variable?name=${formattedName}`
+ )
+ const varFontData = await res.json()
+ data.push({
+ name: font,
+ ...(varFontData ?? {}),
+ })
+ } catch (e) {
+ console.error(`Failed to fetch variable font ${font}`)
}
-
- return toGoogleVariableFontUrl([fullData])
- } catch {
- return null
}
+
+ return data
}
-const getFontFamilyHref = async (font: string) => {
- try {
- const res = await fetch(`https://components.ai/api/v1/typefaces/${font}`)
- const rawFontData = await res.json()
+const getFontFamiliesData = async (
+ fonts: string[]
+): Promise => {
+ const data: FontFamilyData[] = []
+ for (const font of fonts) {
+ try {
+ const res = await fetch(`https://components.ai/api/v1/typefaces/${font}`)
+ const rawFontData = await res.json()
- const styles = Object.keys(rawFontData?.variants)
- const weights = Object.keys(rawFontData?.variants[styles[0]])
- const fontData = {
- name: rawFontData?.name,
- weights,
- styles,
+ const styles = Object.keys(rawFontData?.variants)
+ const weights = Object.keys(rawFontData?.variants[styles[0]])
+ data.push({
+ name: rawFontData?.name,
+ weights,
+ styles,
+ })
+ } catch (e) {
+ console.error(`Failed to fetch ${font}`)
}
-
- return toGoogleFontUrl([fontData])
- } catch (e) {
- console.log(`failed to fetch ${font} font`)
- return null
}
+
+ return data
}
-const getVariableStyleSheet = async (fontFamily: string, setVariableStyleSheet: Function) => {
- const sheet = await getVariableFontFamilyHref(fontFamily)
+export const buildVariableFontFamiliesHref = async (
+ fonts: string[]
+): Promise => {
+ const fontData = await getVariableFontFamiliesData(fonts)
+ return toGoogleVariableFontUrl(fontData)
+}
+
+export const buildFontFamiliesHref = async (
+ fonts: string[]
+): Promise => {
+ const fontData = await getFontFamiliesData(fonts)
+ return toGoogleFontUrl(fontData)
+}
+
+const getVariableStyleSheet = async (
+ fontFamily: string,
+ setVariableStyleSheet: Function
+) => {
+ const sheet = await buildVariableFontFamiliesHref([fontFamily])
setVariableStyleSheet(sheet)
}
const debouncedVariableStyleSheet = debounce(getVariableStyleSheet, 1000)
const getStyleSheet = async (fontFamily: string, setStyleSheet: Function) => {
- const sheet = await getFontFamilyHref(fontFamily)
+ const sheet = await buildFontFamiliesHref([fontFamily])
setStyleSheet(sheet)
}
const debouncedGetStyleSheet = debounce(getStyleSheet, 1000)
export const FontTags = ({ fontFamily }: any) => {
const [styleSheet, setStyleSheet] = React.useState('')
- const [variableStyleSheet, setVariableStyleSheet] = React.useState('')
-
+ const [variableStyleSheet, setVariableStyleSheet] = React.useState<
+ string | null
+ >('')
+
React.useEffect(() => {
debouncedVariableStyleSheet(fontFamily, setVariableStyleSheet)
debouncedGetStyleSheet(fontFamily, setStyleSheet)
}, [fontFamily])
-
+
if (!fontFamily) {
return null
}
- return <>{styleSheet || variableStyleSheet
- ?
- : null
- }>
-
+ return (
+ <>
+ {styleSheet || variableStyleSheet ? (
+
+ ) : null}
+ >
+ )
}
diff --git a/packages/gui/src/components/inputs/FontFamily/Input.tsx b/packages/gui/src/components/inputs/FontFamily/Input.tsx
index 72dd0fc7..fc242163 100644
--- a/packages/gui/src/components/inputs/FontFamily/Input.tsx
+++ b/packages/gui/src/components/inputs/FontFamily/Input.tsx
@@ -5,6 +5,8 @@ import { FontFamilyType } from '../../../types/css'
import { EditorProps } from '../../../types/editor'
import { Label } from '../../primitives'
import { NumberInput } from '../NumberInput'
+import { DeletePropButton } from '../Dimension/Input'
+import { useEditor } from '../../providers/EditorContext'
type Font = {
name: string
@@ -21,6 +23,8 @@ const enum FontCategory {
Sans = 'sans-serif',
Mono = 'monospace',
Serif = 'serif',
+ Display = 'display',
+ Handwriting = 'handwriting',
}
const nameMap: any = {
opsz: 'Optical Size',
@@ -28,6 +32,8 @@ const nameMap: any = {
CRSV: 'Cursive',
MONO: 'Mono',
slnt: 'Slant',
+ wdth: 'Width',
+ wght: 'Weight',
}
interface Props extends EditorProps {
@@ -35,7 +41,8 @@ interface Props extends EditorProps {
defaultValue?: FontFamilyType
}
-export function FontFamilyInput({ label, value, onChange }: Props) {
+export function FontFamilyInput({ label, value, onChange, onRemove }: Props) {
+ const { theme } = useEditor()
const id = React.useId()
const fullId = `${id}-${label ? kebabCase(label) : 'font-family'}`
@@ -63,13 +70,22 @@ export function FontFamilyInput({ label, value, onChange }: Props) {
getFontData()
}, [])
+ const themeFonts = Object.keys(theme.fonts || {})
const [includeSans, setIncSans] = React.useState(true)
const [includeSerif, setIncSerif] = React.useState(true)
const [includeMono, setIncMono] = React.useState(true)
+ const [includeDisplay, setIncDisplay] = React.useState(true)
+ const [includeHandwriting, setIncHandwriting] = React.useState(true)
React.useEffect(() => {
handleFilterItems(value.fontFamily)
- }, [includeMono, includeSans, includeSerif])
+ }, [
+ includeMono,
+ includeSans,
+ includeSerif,
+ includeDisplay,
+ includeHandwriting,
+ ])
const {
isOpen,
@@ -97,13 +113,15 @@ export function FontFamilyInput({ label, value, onChange }: Props) {
return (
(includeSans && item.category === FontCategory.Sans) ||
(includeSerif && item.category === FontCategory.Serif) ||
- (includeMono && item.category === FontCategory.Mono)
+ (includeMono && item.category === FontCategory.Mono) ||
+ (includeDisplay && item.category === FontCategory.Display) ||
+ (includeHandwriting && item.category === FontCategory.Handwriting)
)
}
})
const items = filteredOptions.map((opt) => opt.name).sort()
- setInputItems(items)
+ setInputItems([...themeFonts, ...items])
}
const handleFontChange = (name: string) => {
@@ -115,7 +133,7 @@ export function FontFamilyInput({ label, value, onChange }: Props) {
setVariableFont(fontData)
}
- const handleCustomAxesChange = (axisKey: string, newValue: any) => {
+ const handleCustomAxisChange = (axisKey: string, newValue: any) => {
const axisDict: Record = {}
value.fontVariationSettings?.split(',').forEach((axis: string) => {
const axisSplit = axis.split(' ')
@@ -140,21 +158,24 @@ export function FontFamilyInput({ label, value, onChange }: Props) {
{label}
)}
- handleFontChange(e.target.value),
- })}
- onFocus={() => {
- if (!isOpen) {
- toggleMenu()
- handleFilterItems('')
- }
- }}
- sx={{ width: '100%' }}
- />
+
+ handleFontChange(e.target.value),
+ })}
+ onFocus={() => {
+ if (!isOpen) {
+ toggleMenu()
+ handleFilterItems('')
+ }
+ }}
+ sx={{ width: '100%' }}
+ />
+ {onRemove && }
+
{isOpen && inputItems.length > 0 && (
-
-
- setIncSans(!includeSans)}
- />
- Sans
-
-
- setIncSerif(!includeSerif)}
- />
- Serif
-
-
- setIncMono(!includeMono)}
- />
- Monospace
-
+
+ setIncSans(!includeSans)}
+ />
+ setIncSerif(!includeSerif)}
+ />
+ setIncMono(!includeMono)}
+ />
+ setIncDisplay(!includeDisplay)}
+ />
+ setIncHandwriting(!includeHandwriting)}
+ />
)}
{isOpen && inputItems.length === 0 && (
@@ -228,8 +252,8 @@ export function FontFamilyInput({ label, value, onChange }: Props) {
width: '100%',
alignItems: 'center',
justifyContent: 'space-between',
- paddingY: 2,
- paddingX: 3,
+ py: 2,
+ px: 3,
fontSize: 1,
}}
>
@@ -260,6 +284,7 @@ export function FontFamilyInput({ label, value, onChange }: Props) {
margin: 0,
pl: 3,
py: 1,
+ fontWeight: themeFonts.includes(item) ? 600 : 400,
backgroundColor:
highlightedIndex === index
? 'backgroundOffset'
@@ -283,64 +308,59 @@ export function FontFamilyInput({ label, value, onChange }: Props) {
})}
+
{variableFont &&
Object.entries(variableFont).map(([k, v]) => {
if (['name', 'ital'].includes(k)) return null
if (typeof v === 'string') return null
- if (k === 'wdth') {
- return (
- handleCustomAxesChange(k, e)}
- min={v.min}
- max={v.max}
- step={v.step}
- label="Width"
- sx={{ width: '100%' }}
- />
- )
- }
-
- if (k === 'wght') {
- return (
-
- onChange({ ...value, fontWeight: newVal })
- }
- min={v.min}
- max={v.max}
- step={v.step}
- label="Font Weight"
- sx={{ width: '100%' }}
- />
- )
- }
-
return (
handleCustomAxesChange(k, e)}
+ onChange={(e: any) => handleCustomAxisChange(k, e)}
axisKey={k}
min={v.min}
max={v.max}
step={v.step}
label={nameMap[k] ?? k}
- sx={{ width: '100%' }}
+ sx={{ width: '100%', }}
/>
)
})}
+
)
}
+type FontFamilyToggleProps = {
+ checked: boolean
+ onToggle: () => void
+ label: string
+}
+const FontFamilyToggle = ({
+ checked,
+ onToggle,
+ label,
+}: FontFamilyToggleProps) => {
+ return (
+
+
+ {label}
+
+ )
+}
+
const CustomAxis = ({
defaultValue,
axisKey,
@@ -371,14 +391,17 @@ type APIFontData = {
fontOptions: Font[]
variableFontsData: any
}
+const TYPEFACE_API_BASE_URL = 'https://components.ai/api'
const getFontsData = async (): Promise => {
const fontOptions: Font[] = []
- const rawGoogData = await fetch('https://components.ai/api/v1/typefaces/list')
+ const rawGoogData = await fetch(
+ `${TYPEFACE_API_BASE_URL}/v1/typefaces/list?v1=a`
+ )
const rawSystemData = await fetch(
- 'https://components.ai/api/v1/typefaces/system'
+ `${TYPEFACE_API_BASE_URL}/v1/typefaces/system`
)
const variableFontsData = await fetch(
- 'https://components.ai/api/v1/typefaces/variable'
+ `${TYPEFACE_API_BASE_URL}/v1/typefaces/variable`
)
const systemFonts = (await rawSystemData.json()) as any
@@ -401,7 +424,9 @@ const getFontsData = async (): Promise => {
if (
category === FontCategory.Sans ||
category === FontCategory.Serif ||
- category === FontCategory.Mono
+ category === FontCategory.Mono ||
+ category === FontCategory.Display ||
+ category === FontCategory.Handwriting
) {
fontOptions.push({ name, category })
}
diff --git a/packages/gui/src/components/inputs/Gradient/field.tsx b/packages/gui/src/components/inputs/Gradient/field.tsx
deleted file mode 100644
index 83162f3f..00000000
--- a/packages/gui/src/components/inputs/Gradient/field.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { SelectInput } from '../SelectInput'
-import { getInputProps } from '../../../lib/util'
-import { NumberInput } from '../NumberInput'
-import GradientStopsField from './stops'
-import {
- ConicGradient,
- Gradient,
- LinearGradient,
- RadialGradient,
-} from './types'
-
-const gradientTypeOptions = [
- 'linear',
- 'radial',
- 'conic',
- 'repeating-linear',
- 'repeating-radial',
- 'repeating-conic',
-] as const
-type GradientFieldProps = {
- value: Gradient
- onChange: (newValue: Gradient) => void
-}
-export const GradientField = (props: GradientFieldProps) => {
- return (
-
- )
-}
-
-export const GradientEditor = ({ value, ...props }: GradientFieldProps) => {
- switch (value.type) {
- case 'linear':
- case 'repeating-linear':
- return
- case 'radial':
- case 'repeating-radial':
- return
- case 'conic':
- case 'repeating-conic':
- return
- default:
- return null
- }
-}
-
-type RadialGradientEditorProps = {
- value: RadialGradient
- onChange: (newValue: RadialGradient) => void
-}
-export const RadialGradientEditor = (props: RadialGradientEditorProps) => {
- return (
-
- )
-}
-
-type ConicGradientEditorProps = {
- value: ConicGradient
- onChange: (newValue: ConicGradient) => void
-}
-export const ConicGradientEditor = (props: ConicGradientEditorProps) => {
- return (
-
- )
-}
-
-type LinearGradientEditorProps = {
- value: LinearGradient
- onChange: (newValue: LinearGradient) => void
-}
-export const LinearGradientEditor = (props: LinearGradientEditorProps) => {
- return
-}
diff --git a/packages/gui/src/components/inputs/Gradient/stops.tsx b/packages/gui/src/components/inputs/Gradient/stops.tsx
index 839f00fe..9f3795ac 100644
--- a/packages/gui/src/components/inputs/Gradient/stops.tsx
+++ b/packages/gui/src/components/inputs/Gradient/stops.tsx
@@ -8,18 +8,26 @@ import { NumberInput } from '../NumberInput'
import { getInputProps, randomInt } from '../../../lib/util'
import { GradientStop as GradientStopValue } from './types'
import { stringifyGradient } from './stringify'
+import { useTheme } from '../../providers/ThemeContext'
+import { DataTypeSchema } from '../../schemas/types'
+import { SchemaInput } from '../SchemaInput'
+import { color } from '../../schemas/color'
+import { Theme } from '../../../types/theme'
interface StopsProps {
value: GradientStopValue[]
onChange: any
repeating: boolean
+ itemSchema: DataTypeSchema
}
export default function GradientStopsField({
onChange,
value,
repeating,
+ itemSchema,
}: StopsProps) {
+ const theme = useTheme()
const track = useRef(null)
const [selected, setSelected] = useState(-1)
const [dragIndex, setDragIndex] = useState(-1)
@@ -65,14 +73,17 @@ export default function GradientStopsField({
return (
-
Stops
+
Stops
{
setSelected(value.length)
onChange([
...value,
{
- color: randomColor(),
+ color: {
+ type: 'theme',
+ path: randomColor({ theme, previousValue: '#000' }),
+ },
hinting: randomInt(0, 100),
},
])
@@ -97,10 +108,11 @@ export default function GradientStopsField({
height: '2rem',
}}
>
-
+
{value.map((stop, i) => {
return (
{selected !== -1 && value[selected] && (
- {
const newValue = produce(value, (draft: any) => {
@@ -182,8 +196,9 @@ function Toolbar({ onAdd, onDelete }: ToolbarProps) {
interface TrackProps {
repeating: boolean
value: GradientStopValue[]
+ theme: Theme
}
-function Track({ repeating, value }: TrackProps) {
+function Track({ repeating, value, theme }: TrackProps) {
return (
@@ -218,7 +236,7 @@ interface StopFieldsProps {
function StopFields(props: StopFieldsProps) {
return (
-
+
)
@@ -226,9 +244,10 @@ function StopFields(props: StopFieldsProps) {
interface MarkerProps extends HTMLAttributes {
value: GradientStopValue
+ theme: Theme
isSelected: boolean
}
-function Marker({ value, isSelected, ...props }: MarkerProps) {
+function Marker({ value, isSelected, theme, ...props }: MarkerProps) {
return (
diff --git a/packages/gui/src/components/inputs/Gradient/stringify.ts b/packages/gui/src/components/inputs/Gradient/stringify.ts
index b829c4ca..65631ae8 100644
--- a/packages/gui/src/components/inputs/Gradient/stringify.ts
+++ b/packages/gui/src/components/inputs/Gradient/stringify.ts
@@ -1,82 +1,49 @@
import { sortBy } from 'lodash-es'
-import { squeeze } from '../../../lib/util'
import {
- Gradient,
- LinearGradient,
- ConicGradient,
- RadialGradient,
- GradientList,
-} from './types'
-
-export const getLinearGradient = (gradient: LinearGradient) => {
- return `linear-gradient(${gradient.degrees}deg, ${getStops(gradient, '%')})`
-}
-
-export const getRepeatingLinearGradient = (gradient: LinearGradient) => {
- return `repeating-linear-gradient(${gradient.degrees}deg, ${getStops(
- gradient,
- '%'
- )})`
-}
-
-export const getConicGradient = (gradient: ConicGradient) => {
- return `conic-gradient(from ${gradient.degrees}deg at ${getLocation(
- gradient
- )}, ${getStops(gradient, '%')})`
-}
-
-export const getRepeatingConicGradient = (gradient: ConicGradient) => {
- return `repeating-conic-gradient(from ${gradient.degrees}deg at ${getLocation(
- gradient
- )}, ${getStops(gradient, '%')})`
-}
-
-export const getRadialGradient = (gradient: RadialGradient) => {
- return `radial-gradient(${gradient.shape ?? 'circle'} at ${getLocation(
- gradient
- )}, ${getStops(gradient, '%')})`
-}
-
-export const getRepeatingRadialGradient = (gradient: RadialGradient) => {
- return `repeating-radial-gradient(${
- gradient.shape ?? 'circle'
- } at ${getLocation(gradient)}, ${getStops(gradient, '%')})`
-}
-
-export function stringifyGradient(gradient: Gradient): string {
- if (gradient.type === 'linear') {
- return getLinearGradient(gradient)
- }
- if (gradient.type === 'radial') {
- return getRadialGradient(gradient)
- }
- if (gradient.type === 'conic') {
- return getConicGradient(gradient)
- }
- if (gradient.type === 'repeating-linear') {
- return getRepeatingLinearGradient(gradient)
+ stringifyFunction,
+ stringifyPosition,
+ stringifyUnit,
+} from '../../../lib/stringify'
+import { Theme } from '../../../types/theme'
+import { color } from '../../schemas/color'
+import { Gradient, GradientStop } from './types'
+
+export function stringifyGradient(gradient: Gradient, theme?: Theme): string {
+ switch (gradient.type) {
+ case 'linear':
+ case 'repeating-linear': {
+ return stringifyFunction(gradient.type + '-gradient', [
+ gradient.angle,
+ stringifyStops(gradient.stops, theme),
+ ])
+ }
+ case 'radial':
+ case 'repeating-radial': {
+ return stringifyFunction(gradient.type + '-gradient', [
+ `${gradient.shape ?? 'circle'} at ${stringifyPosition(
+ gradient.position
+ )}`,
+ stringifyStops(gradient.stops, theme),
+ ])
+ }
+ case 'conic':
+ case 'repeating-conic': {
+ return stringifyFunction(gradient.type + '-gradient', [
+ `from ${stringifyUnit(gradient.angle)} at ${stringifyPosition(
+ gradient.position
+ )}`,
+ stringifyStops(gradient.stops, theme),
+ ])
+ }
}
- if (gradient.type === 'repeating-radial') {
- return getRepeatingRadialGradient(gradient)
- }
- if (gradient.type === 'repeating-conic') {
- return getRepeatingConicGradient(gradient)
- }
- throw new Error('Unknown gradient type')
}
-// export const stringifyGradient = (gradient: GradientList): string => {
-// return squeeze(getDeclarationValue(gradient))
-// }
-
-const getStops = (gradient: Gradient, unit: string) => {
- return sortBy(gradient?.stops, (stop) => stop.hinting)
+export const stringifyStops = (stops: GradientStop[], theme?: Theme) => {
+ return sortBy(stops, (stop) => stop.hinting)
?.filter(Boolean)
- ?.map(({ color, hinting }) => `${color} ${hinting}${unit}`)
+ ?.map(
+ ({ color: stopColor, hinting }) =>
+ `${color().stringify(stopColor, theme)} ${hinting}%`
+ )
?.join(', ')
}
-
-const getLocation = (gradient: ConicGradient | RadialGradient) => {
- const { locationX = 50, locationY = 50 } = gradient
- return `${locationX}% ${locationY}%`
-}
diff --git a/packages/gui/src/components/inputs/Gradient/types.ts b/packages/gui/src/components/inputs/Gradient/types.ts
index f83bd43a..fe8f5ee0 100644
--- a/packages/gui/src/components/inputs/Gradient/types.ts
+++ b/packages/gui/src/components/inputs/Gradient/types.ts
@@ -1,32 +1,31 @@
+import { Angle, Color, Position } from '../../../types/css'
+import { ThemeColor } from '../../primitives/ColorPicker/PalettePicker'
+
interface BaseGradient {
type: string
stops: GradientStop[]
}
export interface GradientStop {
- color: string
- hinting: number
+ color: Color | ThemeColor
+ hinting: number // TODO units
}
export interface LinearGradient extends BaseGradient {
type: 'linear' | 'repeating-linear'
- degrees: number
+ angle: Angle
}
export interface RadialGradient extends BaseGradient {
type: 'radial' | 'repeating-radial'
- locationX: number
- locationY: number
+ position: Position
shape: 'circle' | 'ellipse'
}
export interface ConicGradient extends BaseGradient {
type: 'conic' | 'repeating-conic'
- locationX: number
- locationY: number
- degrees: number
+ position: Position
+ angle: Angle
}
export type Gradient = LinearGradient | RadialGradient | ConicGradient
-
-export type GradientList = Gradient[]
diff --git a/packages/gui/src/components/inputs/GridLine.tsx b/packages/gui/src/components/inputs/GridLine.tsx
deleted file mode 100644
index 9e067bd8..00000000
--- a/packages/gui/src/components/inputs/GridLine.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { EditorPropsWithLabel, getInputProps } from '../../lib/util'
-import { Label, Number } from '../primitives'
-import * as Toggle from '@radix-ui/react-toggle'
-import { stringifyValues } from '../../lib/stringify'
-
-interface GridLine {
- span?: boolean
- position: number
- ident: string
-}
-
-interface Props extends EditorPropsWithLabel {}
-
-/**
- * Input for grid-{row/column}-{start/end}
- */
-export function GridLineInput(props: Props) {
- const { label, value, onChange } = props
- return (
-
-
{label}
-
- onChange({ ...value, span: pressed })}
- sx={{
- border: '1px solid',
- borderColor: 'border',
- backgroundColor: 'background',
- color: 'muted',
- '&[data-state=on]': {
- backgroundColor: 'text',
- color: 'background',
- },
- }}
- >
- span
-
-
- onChange({ ...value, ident: e.target.value })}
- />
-
-
- )
-}
-
-export function stringifyGridLine(value: GridLine) {
- const { span, position, ident } = value
- return stringifyValues([span ? 'span' : null, position, ident ? ident : null])
-}
diff --git a/packages/gui/src/components/inputs/GridTrack/field.tsx b/packages/gui/src/components/inputs/GridTrack/field.tsx
deleted file mode 100644
index 7cb777c1..00000000
--- a/packages/gui/src/components/inputs/GridTrack/field.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { EditorPropsWithLabel, getInputProps } from '../../../lib/util'
-import { EditorProps } from '../../../types/editor'
-import Layers from '../../Layers'
-import { NumberInput } from '../NumberInput'
-import { SelectInput } from '../SelectInput'
-import TrackSizeListInput, {
- getDefaultTrackSizeValue,
- TrackSizeSwitch,
-} from '../TrackSize/field'
-import { TrackSize } from '../TrackSize/types'
-import { stringifyGridTrackList } from './stringify'
-import { GridTrack, TrackRepeat } from './types'
-
-export default function GridTrackListInput(
- props: EditorPropsWithLabel
-) {
- const addItem = () => {
- return {
- type: 'breadth',
- value: { value: 1, unit: 'fr' },
- } as const
- }
- return (
-
- )
-}
-
-function GridTrackInput(props: EditorProps) {
- return (
-
- props.onChange(getDefaultGridTrackValue(type))}
- />
-
-
- )
-}
-
-function GridTrackSwitch(props: EditorProps) {
- switch (props.value.type) {
- case 'repeat': {
- const _props = props as EditorProps
- return
- }
- default: {
- const _props = props as EditorProps
- return
- }
- }
-}
-
-function TrackRepeatInput(props: EditorProps) {
- return (
-
-
-
-
- )
-}
-
-function getDefaultGridTrackValue(type: GridTrack['type']): GridTrack {
- switch (type) {
- case 'repeat': {
- return {
- type,
- count: 1,
- trackList: [{ type: 'breadth', value: { value: 1, unit: 'fr' } }],
- }
- }
- default:
- return getDefaultTrackSizeValue(type)
- }
-}
diff --git a/packages/gui/src/components/inputs/GridTrack/stringify.ts b/packages/gui/src/components/inputs/GridTrack/stringify.ts
deleted file mode 100644
index c3ae2371..00000000
--- a/packages/gui/src/components/inputs/GridTrack/stringify.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { stringifyFunction } from '../../../lib/stringify'
-import {
- stringifyTrackSize,
- stringifyTrackSizeList,
-} from '../TrackSize/stringify'
-import { GridTrack, TrackRepeat } from './types'
-
-export function stringifyGridTrackList(trackList: GridTrack[]) {
- return trackList.map(stringifyGridTrack).join(' ')
-}
-
-export function stringifyGridTrack(track: GridTrack) {
- switch (track.type) {
- case 'repeat':
- return stringifyTrackRepeat(track)
- default:
- return stringifyTrackSize(track)
- }
-}
-
-function stringifyTrackRepeat(track: TrackRepeat) {
- const { type, count, trackList } = track
- return stringifyFunction(type, [count, stringifyTrackSizeList(trackList)])
-}
diff --git a/packages/gui/src/components/inputs/GridTrack/types.ts b/packages/gui/src/components/inputs/GridTrack/types.ts
deleted file mode 100644
index 393057ba..00000000
--- a/packages/gui/src/components/inputs/GridTrack/types.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { CSSUnitValue } from '../../../types/css'
-import { TrackSize } from '../TrackSize/types'
-
-export type GridTrack = TrackSize | TrackRepeat
-
-// TODO track names
-export interface TrackRepeat {
- type: 'repeat'
- count: number // TODO change to accept keywords
- trackList: TrackSize[]
-}
diff --git a/packages/gui/src/components/inputs/ImageSource/field.tsx b/packages/gui/src/components/inputs/ImageSource/field.tsx
deleted file mode 100644
index d6eeaa1b..00000000
--- a/packages/gui/src/components/inputs/ImageSource/field.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import Layers, { LayerProps } from '../../Layers'
-import { ImageSource, ImageSourceType } from './types'
-import { EditorPropsWithLabel, getInputProps } from '../../../lib/util'
-import { SelectInput } from '../SelectInput'
-import { stringifyImageSource } from './stringify'
-import { URLInput } from '../../primitives/URLInput'
-import produce from 'immer'
-import { GradientField } from '../Gradient/field'
-
-const DEFAULT_IMAGE_URL = ''
-export default function ImageSourceInput(
- props: EditorPropsWithLabel
-) {
- const newItem = () => {
- return getDefault('url')
- }
-
- return (
-
- {...props}
- newItem={newItem}
- content={ImageSourceEditor}
- stringify={stringifyImageSource}
- thumbnail={Thumbnail}
- />
- )
-}
-
-export const ImageSourceEditor = (props: LayerProps