diff --git a/apps/docs/pages/examples/typography.tsx b/apps/docs/pages/examples/typography.tsx
index 5977840a..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',
}}
>
-
-
-
-
+
+
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
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
-
+ 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
+
-
+
@@ -119,9 +141,8 @@ export default function Docs() {
-
-
+
-
- View more properties
-
+ View more properties
-
+ {/*
Composable
-
- Mix and match controls to create your own custom component design interfaces.
+
+ Mix and match controls to create your own custom component design
+ interfaces.
-
)
}
diff --git a/apps/docs/pages/html-editor.tsx b/apps/docs/pages/html-editor.tsx
index a048cb36..79ffd9b4 100644
--- a/apps/docs/pages/html-editor.tsx
+++ b/apps/docs/pages/html-editor.tsx
@@ -1,243 +1,39 @@
import { HtmlEditor, HtmlRenderer, HtmlEditorProvider } from '@compai/css-gui'
import { useState } from 'react'
-
-const initialValue: any = {
- tagName: 'div',
- attributes: { className: 'section' },
- style: {
- paddingTop: {
- value: 128,
- unit: 'px',
- },
- paddingBottom: {
- value: 128,
- unit: 'px',
- },
- paddingLeft: {
- value: 64,
- unit: 'px',
- },
- paddingRight: {
- value: 64,
- unit: 'px',
- },
- },
- children: [
- {
- tagName: 'h1',
- attributes: {},
- style: {
- color: 'primary',
- fontSize: [
- {
- value: 4,
- unit: 'rem',
- },
- {
- value: 6,
- unit: 'rem',
- },
- {
- value: 10,
- unit: 'rem',
- },
- ],
- fontWeight: 900,
- fontFamily: 'Inter',
- letterSpacing: { value: -8, unit: 'px' },
- marginTop: {
- value: 0,
- unit: 'px',
- },
- marginBottom: {
- value: 0,
- unit: 'px',
- },
- lineHeight: {
- value: 1.25,
- unit: 'number',
- },
- },
- children: [{ type: 'text', value: 'CSS.GUI' }],
- },
- {
- tagName: 'h2',
- attributes: {},
- style: {
- marginBottom: {
- value: 0,
- unit: 'px',
- },
- fontSize: {
- value: 48,
- unit: 'px',
- },
- maxWidth: {
- value: 40,
- unit: 'em',
- },
- lineHeight: {
- value: 1.25,
- unit: 'number',
- },
- },
- children: [
- {
- type: 'text',
- value:
- 'Quickly build components with custom styling panels. No coding required.',
- },
- ],
- },
- {
- tagName: 'p',
- attributes: {},
- style: {
- marginBottom: {
- value: 96,
- unit: 'px',
- },
- fontSize: {
- value: 20,
- unit: 'px',
- },
- },
- children: [
- {
- type: 'text',
- value: 'Click anywhere on the canvas to start. Go ahead. Click away.',
- },
- ],
- },
- {
- attributes: {
- href: '#0',
- },
- style: {
- paddingTop: {
- value: 16,
- unit: 'px',
- },
- paddingBottom: {
- value: 16,
- unit: 'px',
- },
- paddingLeft: {
- value: 32,
- unit: 'px',
- },
- paddingRight: {
- value: 32,
- unit: 'px',
- },
- textDecorationColor: 'transparent',
- textDecorationThickness: { value: 0, unit: 'px' },
- textDecorationStyle: 'none',
- textDecorationLine: 'none',
- color: 'background',
- backgroundColor: 'text',
- borderWidth: {
- value: 2,
- unit: 'px',
- },
- borderStyle: 'solid',
- borderColor: 'text',
- marginRight: {
- value: 8,
- unit: 'px',
- },
- borderRadius: {
- value: 6,
- unit: 'px',
- },
- whiteSpace: 'nowrap',
- ':hover': {
- backgroundColor: 'primary',
- borderColor: 'primary',
- },
- },
- tagName: 'a',
- children: [
- {
- type: 'text',
- value: 'Primary CTA',
- },
- ],
- },
- {
- attributes: { href: 'https://components.ai' },
- style: {
- paddingTop: {
- value: 16,
- unit: 'px',
- },
- paddingBottom: {
- value: 16,
- unit: 'px',
- },
- paddingLeft: {
- value: 32,
- unit: 'px',
- },
- paddingRight: {
- value: 32,
- unit: 'px',
- },
- textDecorationColor: 'transparent',
- textDecorationThickness: { value: 0, unit: 'px' },
- textDecorationStyle: 'none',
- textDecorationLine: 'none',
- color: 'text',
- whiteSpace: 'nowrap',
- borderWidth: {
- value: 2,
- unit: 'px',
- },
- borderStyle: 'solid',
- borderColor: 'currentColor',
- borderRadius: {
- value: 6,
- unit: 'px',
- },
- ':hover': {
- color: 'primary',
- },
- },
- tagName: 'a',
- children: [
- {
- type: 'text',
- value: 'Secondary link',
- },
- ],
- },
- ],
-}
+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/library/card.tsx b/apps/docs/pages/library/card.tsx
index 6cf123df..0b29287f 100644
--- a/apps/docs/pages/library/card.tsx
+++ b/apps/docs/pages/library/card.tsx
@@ -14,21 +14,24 @@ const initialValue: any = {
display: 'flex',
alignItems: 'stretch',
flexDirection: 'column',
- textDecoration: 'none',
- overflow: 'hidden',
+ // textDecoration: 'none',
+ overflow: ['hidden'],
height: 'auto',
},
children: [
{
tagName: 'section',
style: {
- overflow: 'hidden',
+ overflow: ['hidden'],
maxHeight: { value: 40, unit: 'vh' },
- minHeight: [
- { value: 160, unit: 'px' },
- { value: 256, unit: 'px' },
- { value: 256, unit: 'px' },
- ],
+ minHeight: {
+ type: 'responsive',
+ values: [
+ { value: 160, unit: 'px' },
+ { value: 256, unit: 'px' },
+ { value: 256, unit: 'px' },
+ ],
+ },
},
children: [
{
@@ -38,7 +41,7 @@ const initialValue: any = {
display: 'block',
},
attributes: {
- src: 'https://source.unsplash.com/random',
+ src: 'https://dlu344star2bj.cloudfront.net/i/3090-0015.jpg',
},
children: [],
},
@@ -59,12 +62,15 @@ const initialValue: any = {
style: {
marginTop: { value: 0, unit: 'px' },
marginBottom: { value: 0, unit: 'px' },
- fontWeight: 900,
- fontSize: [
- { value: 24, unit: 'px' },
- { value: 32, unit: 'px' },
- { value: 48, 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!'],
@@ -74,13 +80,16 @@ const initialValue: any = {
style: {
marginTop: { value: 8, unit: 'px' },
marginBottom: { value: 0, unit: 'px' },
- fontWeight: 600,
- opacity: 0.7,
- fontSize: [
- { value: 14, unit: 'px' },
- { value: 16, unit: 'px' },
- { value: 20, 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'],
@@ -88,11 +97,14 @@ const initialValue: any = {
{
tagName: 'p',
style: {
- fontSize: [
- { value: 14, unit: 'px' },
- { value: 16, unit: 'px' },
- { value: 16, unit: 'px' },
- ],
+ 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' },
@@ -115,19 +127,22 @@ const initialValue: any = {
backgroundColor: 'tomato',
color: '#fff',
display: 'inline-flex',
- fontSize: [
- { value: 14, unit: 'px' },
- { value: 16, unit: 'px' },
- { value: 16, unit: 'px' },
- ],
- fontWeight: 800,
+ 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',
+ // textDecoration: 'none',
whiteSpace: 'nowrap',
paddingLeft: { value: 16, unit: 'px' },
paddingRight: { value: 16, unit: 'px' },
@@ -148,8 +163,8 @@ export default function HtmlEditorExample() {
return (
-
-
+
+
diff --git a/apps/docs/pages/properties/index.tsx b/apps/docs/pages/properties/index.tsx
index 9075bad0..0d1b97e5 100644
--- a/apps/docs/pages/properties/index.tsx
+++ b/apps/docs/pages/properties/index.tsx
@@ -71,7 +71,7 @@ export default function Docs() {
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 fb1f823b..ed89bf96 100644
--- a/packages/gui/CHANGELOG.md
+++ b/packages/gui/CHANGELOG.md
@@ -1,5 +1,979 @@
# @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
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 211033d8..97ae23a3 100644
--- a/packages/gui/package.json
+++ b/packages/gui/package.json
@@ -1,12 +1,14 @@
{
"name": "@compai/css-gui",
- "version": "0.0.90",
+ "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,25 +45,38 @@
"@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-popover": "^1.0.0",
"@radix-ui/react-select": "^0.1.1",
- "@radix-ui/react-slider": "^0.1.4",
- "@radix-ui/react-switch": "^0.1.5",
+ "@radix-ui/react-slider": "^1.0.0",
+ "@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-tabs": "^0.1.5",
- "@radix-ui/react-toggle": "^0.1.4",
+ "@radix-ui/react-toggle": "^1.0.1",
"@radix-ui/react-tooltip": "^0.1.7",
+ "@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",
+ "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",
- "theme-ui": "^0.14.5",
+ "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",
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 (
+
+
+
+
+ )
+}
diff --git a/packages/gui/src/components/AddProperty.tsx b/packages/gui/src/components/AddProperty.tsx
index 7285066b..5ee811b5 100644
--- a/packages/gui/src/components/AddProperty.tsx
+++ b/packages/gui/src/components/AddProperty.tsx
@@ -1,13 +1,13 @@
-import { useCombobox } from 'downshift'
-import { useEffect, useId, useRef, useState } from 'react'
import { properties as propertyList } from '../data/properties'
-import { getDefaultValue } from '../lib/defaults'
+import { getDefaultValue } from './Editor/util'
import { Styles } from '../types/css'
-import { Label } from './primitives'
+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
@@ -21,53 +21,24 @@ export const AddPropertyControl = ({
}: Props) => {
const { setField } = useEditor()
const { addDynamicProperty } = useDynamicControls()
- const id = useId()
- const inputRef = useRef(null)
- // @ts-ignore
+ //@ts-ignore
const allProperties: string[] = Object.entries(propertyList)
.map(([name, data]) => {
- return data.input !== 'none' ? name : null
+ return data.input ? name : null
})
.filter(Boolean)
- const [inputItems, setInputItems] = useState([])
- const [filterValue, setFilterValue] = useState('')
-
- useEffect(() => {
- handleFilterItems(filterValue)
- }, [])
-
- const {
- isOpen,
- toggleMenu,
- getMenuProps,
- getInputProps,
- getItemProps,
- getComboboxProps,
- highlightedIndex,
- } = useCombobox({
- id,
- items: inputItems,
- selectedItem: filterValue,
- onInputValueChange: ({ inputValue }) => {
- handleFilterItems(inputValue!)
- },
- onSelectedItemChange: ({ selectedItem }) => {
- handleAddProperty(selectedItem ?? '')
- },
- })
-
const handleFilterItems = (input: string) => {
+ if (input === '') {
+ return allProperties
+ }
+
const styleItems = Object.keys(styles)
- const filteredItems = allProperties
- .filter((item) => {
- if (item.toLowerCase().startsWith(input.toLowerCase() || '')) {
- return !styleItems.includes(item)
- }
- })
- .sort()
- setInputItems(filteredItems)
+ return fuzzysort
+ .go(input.replace(/-/g, ''), allProperties)
+ .map((res) => res.target)
+ .filter((item) => !styleItems.includes(item))
}
const handleAddProperty = (propertyName: string) => {
@@ -77,130 +48,20 @@ export const AddPropertyControl = ({
if (addDynamicProperty && !field) {
addDynamicProperty(propertyName)
}
-
- setFilterValue('')
}
return (
-
-
`),
+ isEditing: false,
+ setEditing: () => {},
+ hasComponents: false,
+ update: () => {},
}
export type HtmlEditor = {
value: HtmlNode
+ update(value: HtmlNode): void
+ theme?: any
selected: ElementPath | null
- setSelected: (newSelection: ElementPath | null) => void
+ setSelected(newSelection: ElementPath | null): void
+ isEditing: boolean
+ setEditing(value: boolean): void
+ components?: ComponentData[]
+ updateComponent?(newComponent: ComponentData): void
+ hasComponents: boolean
}
export function useHtmlEditor() {
@@ -28,26 +41,83 @@ export function useHtmlEditor() {
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,
+ value: transformedValue,
selected,
setSelected: (newSelection: ElementPath | null) =>
setSelected(newSelection),
+ components,
+ isEditing,
+ setEditing: (newValue: any) => setEditing(newValue),
+ updateComponent,
+ hasComponents: !!components.length,
+ update: onChange,
}
return (
-
- {children}
-
+
+
+ {children}
+
+
)
}
diff --git a/packages/gui/src/components/html/Renderer.tsx b/packages/gui/src/components/html/Renderer.tsx
deleted file mode 100644
index 266735a0..00000000
--- a/packages/gui/src/components/html/Renderer.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import { Fragment, HTMLAttributes } from 'react'
-import { toCSSObject } from '../../lib'
-import { ElementData, ElementPath } from './types'
-import { HTMLFontTags } from './FontTags'
-import { useHtmlEditor } from './Provider'
-import { isVoidElement } from '../../lib/elements'
-import { isSamePath } from './util'
-
-interface HtmlRendererProps {
- value: ElementData
- path?: ElementPath
- canvas?: boolean
-}
-export function HtmlRenderer({ value, canvas = true }: HtmlRendererProps) {
- return (
- <>
-
-
- >
- )
-}
-
-interface ElementRendererProps {
- value: ElementData
- path: ElementPath
- canvas: boolean
-}
-function ElementRenderer({ value, canvas, path }: ElementRendererProps) {
- const { selected, setSelected } = useHtmlEditor()
- const { attributes = {}, style = {}, children = [] } = value
- const Tag: any = value.tagName || 'div'
-
- const Wrap = canvas ? ElementWrap : Fragment
- const sx = toCSSObject(style)
-
- if (isSamePath(path, selected)) {
- sx.outlineWidth = 'thin'
- sx.outlineStyle = 'solid'
- sx.outlineColor = 'primary'
- sx.userSelect = 'none'
- }
-
- const props = {
- ...(canvas ? cleanAttributes(attributes) : attributes),
- sx,
- onClick: (e: MouseEvent) => {
- e.stopPropagation()
- setSelected(path)
- },
- }
-
- if (isVoidElement(Tag)) {
- return (
-
-
-
- )
- }
-
- return (
-
-
- {children.map((child, i) => {
- if (child.type === 'text') {
- return child.value
- }
- return (
-
- )
- })}
-
-
- )
-}
-
-function ElementWrap(props: HTMLAttributes) {
- return (
-
- )
-}
-
-const cleanAttributes = (attributes: Record) => {
- const newAttributes = { ...attributes }
-
- if (newAttributes.href) {
- delete newAttributes.href
- }
-
- if (newAttributes.class) {
- newAttributes.className = newAttributes.class
- delete newAttributes.class
- }
-
- return newAttributes
-}
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 (
+
+
+
+ )
+ }
+
+ if (value.type === 'slot') {
+ return (
+
+
+
+ )
+ }
+
+ 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}
+ />
+ ) : (
+
+ )
+
+ const tagButton = (
+
+ )
+
+ 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
index 832c7d78..5304c0ac 100644
--- a/packages/gui/src/components/html/default-styles.ts
+++ b/packages/gui/src/components/html/default-styles.ts
@@ -1,18 +1,22 @@
-import { HTMLTag } from './types'
-
export const DEFAULT_STYLES: Record = {
- [HTMLTag.Button]: {
- padding: { value: 16, unit: 'px' },
- borderRadius: { value: 5, unit: 'px' },
- display: 'block',
- borderStyle: 'none',
+ 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',
},
- [HTMLTag.A]: {},
- [HTMLTag.Input]: {},
- [HTMLTag.H1]: { textAlign: 'left' },
- [HTMLTag.H2]: { textAlign: 'left' },
- [HTMLTag.H3]: { textAlign: 'left' },
- [HTMLTag.H4]: { textAlign: 'left' },
- [HTMLTag.H5]: { textAlign: 'left' },
- [HTMLTag.H6]: { textAlign: 'left' },
}
diff --git a/packages/gui/src/components/html/editor.tsx b/packages/gui/src/components/html/editor.tsx
deleted file mode 100644
index 78e7541e..00000000
--- a/packages/gui/src/components/html/editor.tsx
+++ /dev/null
@@ -1,623 +0,0 @@
-import { Editor } from '../Editor'
-import { HtmlNode, HTMLTag, ElementPath } from './types'
-import * as Collapsible from '@radix-ui/react-collapsible'
-import { Fragment, useState } from 'react'
-import { isNil } from 'lodash-es'
-import IconButton from '../ui/IconButton'
-import { X } from 'react-feather'
-import { Label, Combobox } from '../primitives'
-import { SelectInput } from '../inputs/SelectInput'
-import { AttributeEditor } from './AttributeEditor'
-import { DEFAULT_STYLES } from './default-styles'
-import { useHtmlEditor } from './Provider'
-import { isVoidElement } from '../../lib/elements'
-import { isSamePath } from './util'
-
-const HTML_TAGS = [
- HTMLTag.A,
- HTMLTag.Abbr,
- HTMLTag.Address,
- HTMLTag.Article,
- HTMLTag.Aside,
- HTMLTag.Audio,
- HTMLTag.B,
- HTMLTag.Bdi,
- HTMLTag.Bdo,
- HTMLTag.Blockquote,
- HTMLTag.Br,
- HTMLTag.Button,
- HTMLTag.Caption,
- HTMLTag.Code,
- HTMLTag.Col,
- HTMLTag.Colgroup,
- HTMLTag.Data,
- HTMLTag.Datalist,
- HTMLTag.Dd,
- HTMLTag.Del,
- HTMLTag.Details,
- HTMLTag.Dfn,
- HTMLTag.Dialog,
- HTMLTag.Div,
- HTMLTag.Dl,
- HTMLTag.Dt,
- HTMLTag.Em,
- HTMLTag.Fieldset,
- HTMLTag.Figcaption,
- HTMLTag.Figure,
- HTMLTag.Footer,
- HTMLTag.Form,
- HTMLTag.H1,
- HTMLTag.H2,
- HTMLTag.H3,
- HTMLTag.H4,
- HTMLTag.H5,
- HTMLTag.H6,
- HTMLTag.Header,
- HTMLTag.Hr,
- HTMLTag.I,
- HTMLTag.Img,
- HTMLTag.Input,
- HTMLTag.Ins,
- HTMLTag.Kbd,
- HTMLTag.Label,
- HTMLTag.Legend,
- HTMLTag.Li,
- HTMLTag.Main,
- HTMLTag.Mark,
- HTMLTag.Menu,
- HTMLTag.Menuitem,
- HTMLTag.Meter,
- HTMLTag.Nav,
- HTMLTag.Noscript,
- HTMLTag.Ol,
- HTMLTag.Optgroup,
- HTMLTag.Option,
- HTMLTag.Output,
- HTMLTag.P,
- HTMLTag.Picture,
- HTMLTag.Pre,
- HTMLTag.Progress,
- HTMLTag.Q,
- HTMLTag.Rp,
- HTMLTag.Rt,
- HTMLTag.Rtc,
- HTMLTag.Ruby,
- HTMLTag.S,
- HTMLTag.Samp,
- HTMLTag.Span,
- HTMLTag.Section,
- HTMLTag.Select,
- HTMLTag.Source,
- HTMLTag.Slot,
- HTMLTag.Small,
- HTMLTag.Sub,
- HTMLTag.Summary,
- HTMLTag.Sup,
- HTMLTag.Table,
- HTMLTag.Tbody,
- HTMLTag.Td,
- HTMLTag.Template,
- HTMLTag.TextArea,
- HTMLTag.Tfoot,
- HTMLTag.Th,
- HTMLTag.Thead,
- HTMLTag.Time,
- HTMLTag.Tr,
- HTMLTag.Track,
- HTMLTag.U,
- HTMLTag.Ul,
- HTMLTag.Var,
- HTMLTag.Video,
- HTMLTag.Wbr,
-]
-
-interface HtmlEditorProps {
- onChange(value: HtmlNode): void
-}
-
-/**
- * An HTML tree-based editor that lets you add HTML nodes and mess around with their styles
- */
-export function HtmlEditor({ onChange }: HtmlEditorProps) {
- const { value, selected, setSelected } = useHtmlEditor()
-
- return (
-
-
-
-
- {selected && (
-
- onChange(setChildAtPath(value, selected, newItem))
- }
- onRemove={() => {
- onChange(removeChildAtPath(value, selected))
- const newPath = [...selected]
- newPath.pop()
- setSelected(newPath)
- }}
- />
- )}
-
- )
-}
-
-interface EditorProps {
- value: HtmlNode
- onChange(value: HtmlNode): void
-}
-interface TagEditorProps extends EditorProps {
- onRemove(): void
-}
-
-function NodeEditor({ value, onChange, onRemove }: TagEditorProps) {
- const nodeType = value.type === 'text' ? 'text' : 'tag'
- return (
-
-
-
{
- if (value === 'text') {
- onChange({ type: 'text', value: '' })
- } else {
- onChange({
- type: 'element',
- tagName: 'div',
- children: [],
- })
- }
- }}
- options={['text', 'tag']}
- />
-
-
-
-
-
-
-
-
- )
-}
-
-function NodeSwitch({ value, onChange }: EditorProps) {
- if (value.type === 'text') {
- return (
-
-
- Content
-
-
- )
- }
-
- return (
-
-
-
- Tag name{' '}
- {
- return HTML_TAGS.filter((el) => el.startsWith(filterValue))
- }}
- onItemSelected={(selectedItem) => {
- const defaultStyles = DEFAULT_STYLES[selectedItem] || {}
- const mergedStyles = { ...defaultStyles, ...value.style }
- onChange({
- ...value,
- tagName: selectedItem,
- style: mergedStyles,
- })
- }}
- items={HTML_TAGS}
- value={value.tagName}
- />
-
-
-
- onChange({ ...value, attributes: newAttributes })
- }
- element={value.tagName as string}
- />
-
-
-
-
🎨 Styles
-
- onChange({ ...value, style: newStyles })}
- showAddProperties
- />
-
-
-
- )
-}
-
-interface TreeNodeProps extends EditorProps {
- path: ElementPath
- onSelect(path: ElementPath | null): void
-}
-
-function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
- const { selected } = useHtmlEditor()
- const [open, setOpen] = useState(true)
- const isSelected = isSamePath(path, selected)
-
- if (value.type === 'text') {
- return (
-
-
-
- )
- }
-
- const tagButton = (
-
- )
-
- if (isVoidElement(value.tagName as string)) {
- return tagButton
- }
-
- return (
-
-
- {tagButton}
-
-
- {value.children?.map((child, i) => {
- return (
-
- {
- onChange(
- addChildAtPath(value, [i], {
- type: 'text',
- value: '',
- })
- )
- onSelect([i])
- }}
- />
- {
- onChange({
- ...value,
- children: replaceAt(value.children ?? [], i, newChild),
- })
- }}
- />
-
- )
- })}
-
{
- onChange(
- addChildAtPath(value, [value.children?.length ?? 0], {
- type: 'text',
- value: '',
- })
- )
- onSelect(null)
- }}
- />
-
-
-
-
-
</{value.tagName}>
-
-
-
- )
-}
-
-function AddChildButton({ onClick }: { onClick(): void }) {
- return (
-
- )
-}
-
-interface AttributeEditorProps {
- value: Record
- onChange(value: Record): void
-}
-
-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)
-}
-
-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)
- ),
- }
-}
-
-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)
- ),
- }
-}
-
-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
-}
-
-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/html/types.ts b/packages/gui/src/components/html/types.ts
index 941cc5f5..71d4a77f 100644
--- a/packages/gui/src/components/html/types.ts
+++ b/packages/gui/src/components/html/types.ts
@@ -1,110 +1,39 @@
export interface ElementData {
type: 'element' | 'text'
tagName?: string
- attributes?: Record
+ 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 type HtmlNode = ElementData
-export type ElementPath = number[]
-
-export const enum HTMLTag {
- A = 'a',
- Abbr = 'abbr',
- Address = 'address',
- Article = 'article',
- Aside = 'aside',
- Audio = 'audio',
- B = 'b',
- Bdi = 'bdi',
- Bdo = 'bdo',
- Blockquote = 'blockquote',
- Br = 'br',
- Button = 'button',
- Caption = 'caption',
- Code = 'code',
- Col = 'col',
- Colgroup = 'colgroup',
- Data = 'data',
- Datalist = 'datalist',
- Dd = 'dd',
- Del = 'del',
- Details = 'details',
- Dfn = 'dfn',
- Dialog = 'dialog',
- Div = 'div',
- Dl = 'dl',
- Dt = 'dt',
- Em = 'em',
- Fieldset = 'fieldset',
- Figcaption = 'figcaption',
- Figure = 'figure',
- Footer = 'footer',
- Form = 'form',
- H1 = 'h1',
- H2 = 'h2',
- H3 = 'h3',
- H4 = 'h4',
- H5 = 'h5',
- H6 = 'h6',
- Header = 'header',
- Hr = 'hr',
- I = 'i',
- Img = 'img',
- Input = 'input',
- Ins = 'ins',
- Kbd = 'kbd',
- Label = 'label',
- Legend = 'legend',
- Li = 'li',
- Main = 'main',
- Mark = 'mark',
- Menu = 'menu',
- Menuitem = 'menuitem',
- Meter = 'meter',
- Nav = 'nav',
- Noscript ='noscript',
- Ol = 'ol',
- Optgroup = 'optgroup',
- Option = 'option',
- Output = 'output',
- P = 'p',
- Picture = 'picture',
- Pre = 'pre',
- Progress = 'progress',
- Q = 'q',
- Rp = 'rp',
- Rt = 'rt',
- Rtc = 'rtc',
- Ruby = 'ruby',
- S = 's',
- Samp = 'samp',
- Span = 'span',
- Section = 'section',
- Select = 'select',
- Source = 'source',
- Slot = 'slot',
- Small = 'small',
- Sub = 'sub',
- Summary = 'summary',
- Sup = 'sup',
- Table = 'table',
- Tbody = 'tbody',
- Td = 'td',
- Template = 'template',
- TextArea = 'textarea',
- Tfoot = 'tfoot',
- Th = 'th',
- Thead = 'thead',
- Time = 'time',
- Tr = 'tr',
- Track = 'track',
- U = 'u',
- Ul = 'ul',
- Var = 'var',
- Video = 'video',
- Wbr = 'wbr',
+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
index a7bbd013..47d60a81 100644
--- a/packages/gui/src/components/html/util.ts
+++ b/packages/gui/src/components/html/util.ts
@@ -1,4 +1,5 @@
-import { ElementPath } from './types'
+import { HtmlNode, ElementPath, Slot } from './types'
+import { isNil } from 'lodash-es'
export const isSamePath = (
path1: ElementPath | null,
@@ -10,3 +11,155 @@ export const isSamePath = (
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 9e25530a..00000000
--- a/packages/gui/src/components/inputs/AngleInput.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Angle, ANGLE_UNITS } 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,
- keywords = [],
-}: EditorProps & { label: string; keywords?: 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 bbde946d..00000000
--- a/packages/gui/src/components/inputs/Animation/field.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import Layers, { LayerProps } from '../../Layers'
-import {
- Animation,
- animationDirections,
- animationFillModes,
- animationPlayStates,
-} from './types'
-import { stringifyAnimationList } from './stringify'
-import { 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'
-import { EditorPropsWithLabel } from '../../../types/editor'
-
-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 759faed0..00000000
--- a/packages/gui/src/components/inputs/Animation/types.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { CSSUnitValue, Time } from '../../../types/css'
-import { keywordValues } from '../EasingFunction/keywords'
-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]
-
-export const DEFAULT_ANIMATION: Animation = {
- delay: { value: 0, unit: 's' },
- duration: { value: 0, unit: 's' },
- direction: 'normal',
- fillMode: 'none',
- iterationCount: { value: 1, unit: 'number' },
- name: 'none',
- playState: 'running',
- timingFunction: keywordValues.ease,
-}
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 73850c58..00000000
--- a/packages/gui/src/components/inputs/Background/field.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { EditorProps, EditorPropsWithLabel } from '../../../types/editor'
-import {
- attachmentKeywords,
- Background,
- repeatKeywords,
- RepeatStyle,
-} from './types'
-
-import { stringifyBackgroundList } from './stringify'
-
-import Layers from '../../Layers'
-import { 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',
- angle: { value: 0, unit: 'deg' },
- 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 26fb4529..00000000
--- a/packages/gui/src/components/inputs/BasicShape/input.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-import { getInputProps } from '../../../lib/util'
-import { EditorProps, EditorPropsWithLabel } 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 b46ae086..00000000
--- a/packages/gui/src/components/inputs/BasicShape/stringify.ts
+++ /dev/null
@@ -1,49 +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
- 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 7f6c697e..00000000
--- a/packages/gui/src/components/inputs/BgSizeInput.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { stringifyUnit } from '../../lib/stringify'
-import { getInputProps } from '../../lib/util'
-import { LengthPercentage } from '../../types/css'
-import { EditorPropsWithLabel } from '../../types/editor'
-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.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 48c96a48..00000000
--- a/packages/gui/src/components/inputs/BorderSpacing.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { stringifyValues } from '../../lib/stringify'
-import { getInputProps } from '../../lib/util'
-import { Length } from '../../types/css'
-import { EditorPropsWithLabel } from '../../types/editor'
-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 788d3ee2..00000000
--- a/packages/gui/src/components/inputs/BoxShadow/field.tsx
+++ /dev/null
@@ -1,62 +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 { getInputProps } from '../../../lib/util'
-import { EditorPropsWithLabel } from '../../../types/editor'
-
-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 3f951eed..00000000
--- a/packages/gui/src/components/inputs/BoxShadow/types.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Color, Length } from '../../../types/css'
-export interface BoxShadow {
- inset?: boolean
- offsetX: Length
- offsetY: Length
- spread: Length
- blur: Length
- color: Color
-}
-
-export const DEFAULT_BOX_SHADOW: BoxShadow = {
- offsetX: { value: 0, unit: 'px' },
- offsetY: { value: 0, unit: 'px' },
- spread: { value: 0, unit: 'px' },
- blur: { value: 0, unit: 'px' },
- color: 'transparent',
-}
diff --git a/packages/gui/src/components/inputs/CheckboxInput.tsx b/packages/gui/src/components/inputs/CheckboxInput.tsx
index dfd5b43f..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 aebb5bf8..00000000
--- a/packages/gui/src/components/inputs/ClipPath.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Label } from 'theme-ui'
-import { stringifyValues } from '../../lib/stringify'
-import { getInputProps } from '../../lib/util'
-import { GeometryBox, GEOMETRY_BOX_KEYWORDS } from '../../types/css'
-import { EditorPropsWithLabel } from '../../types/editor'
-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 fb74b67d..5a9e3977 100644
--- a/packages/gui/src/components/inputs/ColorInput.tsx
+++ b/packages/gui/src/components/inputs/ColorInput.tsx
@@ -1,37 +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 { DeletePropButton } from './Dimension/Input'
+import { InputHeader } from '../ui/InputHeader'
-interface Props extends EditorProps {
- label: string
+interface Props extends EditorPropsWithLabel {
defaultValue?: Color
}
-export function ColorInput({
- label,
- value,
- onChange,
- onRemove,
- defaultValue = '#000',
-}: Props) {
+export function ColorInput(props: Props) {
const theme = useTheme()
- const id = useId()
- const fullId = `${id}-${label}`
return (
-
-
{label}
-
-
- {onRemove && }
-
+
+
)
}
diff --git a/packages/gui/src/components/inputs/Dimension/Input.tsx b/packages/gui/src/components/inputs/Dimension/Input.tsx
index 192db89a..ea79f07d 100644
--- a/packages/gui/src/components/inputs/Dimension/Input.tsx
+++ b/packages/gui/src/components/inputs/Dimension/Input.tsx
@@ -1,299 +1,95 @@
-import * as React from 'react'
-import {
- AbsoluteLengthUnits,
- CalcOperand,
- CSSFunctionCalc,
- CSSUnitValue,
- Dimension,
- KeywordUnits,
- ThemeUnits,
- UnitlessUnits,
-} from '../../../types/css'
-import { Label, Number, ThemeValue, UnitSelect } from '../../primitives'
-import { reducer } from './reducer'
-import { State } from './types'
+import { CSSUnitValue, Dimension } from '../../../types/css'
+import { Number, UnitSelect } from '../../primitives'
import { EditorPropsWithLabel } from '../../../types/editor'
import { UnitConversions } from '../../../lib/convert'
-import { compact, kebabCase, omit } from 'lodash-es'
-import { CalcInput } from '../../primitives/CalcInput'
+import { convertUnits } from '../../../lib/convert'
import { X } from 'react-feather'
-import { isCSSUnitValue } from '../../../lib/codegen/to-css-object'
-import { KeywordSelect } from '../../primitives/KeywordSelect'
-import { ResponsiveInput } from '../../Responsive'
+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
-const getInitialState = (
- value: Dimension,
- themeValues?: (CSSUnitValue & { id: string })[],
- units?: readonly string[]
-): State => {
- const defaultState = {
- value: (value as CSSUnitValue)?.value || 0,
- unit:
- (value as CSSUnitValue)?.unit ||
- (units && units[0]) ||
- AbsoluteLengthUnits.Px,
- themeId: (value as CSSUnitValue)?.themeId,
- key: 0,
- }
-
- for (const { unit, value: themeValue, id } of themeValues || []) {
- if (
- isCSSUnitValue(value) &&
- unit === value.unit &&
- themeValue === value.value
- ) {
- return {
- value: themeValue,
- unit,
- themeId: id,
- key: 0,
- }
- }
- }
-
- return defaultState
-}
-
export interface DimensionInputProps extends EditorPropsWithLabel {
- range?: UnitRanges
+ 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
- property?: string
}
export function DimensionInput(props: DimensionInputProps) {
- if (props.topLevel) {
- return (
-
- )
- }
+ const {
+ value = {},
+ onChange,
+ range: providedRange,
+ units = [],
+ steps,
+ conversions = {},
+ } = props
- return
-}
-
-const BaseDimensionInput = ({
- value,
- onChange,
- onRemove,
- label,
- range,
- units = [],
- keywords = [],
- themeValues = [],
- steps,
- conversions = {},
- topLevel,
-}: DimensionInputProps) => {
- const id = `${React.useId()}-${kebabCase(label)}`
+ const range =
+ providedRange === 'nonnegative' ? nonnegativeRange(units) : providedRange
- const [state, dispatch] = React.useReducer(
- reducer,
- getInitialState(value, themeValues, units)
- )
+ const normedValue = value as CSSUnitValue
- React.useEffect(() => {
- if ((value as CSSFunctionCalc)?.type === 'calc') return
- const unitValue = value as CSSUnitValue
- if (
- // Only want to call on change when the value differs
- state.value !== unitValue?.value ||
- state.unit !== unitValue?.unit ||
- state.themeId !== unitValue?.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',
- UnitlessUnits.Calc,
- ])
+ const allUnits = units
return (
- {label && (
-
- {label}
-
- )}
-
{
+ onChange({
+ ...normedValue,
+ value: newValue,
+ })
}}
- >
- {state.unit === KeywordUnits.Keyword ? (
- {
- dispatch({
- type: 'CHANGED_INPUT_VALUE',
- value,
- })
- }}
- />
- ) : state.themeId ? (
- tv.id === state.themeId) + 1}
- onChange={(newValue: number) => {
- const themeValue = themeValues[Math.max(0, newValue - 1)]
- dispatch({
- type: 'CHANGED_INPUT_TO_THEME_VALUE',
- value: themeValue?.value ?? 0,
- unit: (themeValue?.unit as any) ?? 'px',
- themeId: themeValue.id,
- })
- }}
- themeValues={themeValues}
- />
- ) : state.unit === UnitlessUnits.Calc ? (
-
- ) : (
- {
- dispatch({
- type: 'CHANGED_INPUT_VALUE',
- value: newValue,
- })
- }}
- />
- )}
- {
- if (newUnit === KeywordUnits.Keyword) {
- dispatch({
- type: 'CHANGED_INPUT_VALUE',
- value: keywords[0],
- })
- }
-
- if (newUnit === UnitlessUnits.Calc) {
- onChange({
- arguments: {
- valueX: value as CSSUnitValue,
- valueY: { value: 1, unit: 'px' },
- operand: CalcOperand.Plus,
- },
- type: 'calc',
- })
- }
- if (
- state.unit === UnitlessUnits.Calc &&
- newUnit !== UnitlessUnits.Calc
- ) {
- const unitValue = (value as CSSFunctionCalc).arguments.valueX
- .value
-
- onChange({ value: unitValue, unit: newUnit })
- dispatch({
- value: unitValue,
- type: 'CHANGED_INPUT_VALUE',
- })
- }
-
- 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,
- })
- }
-
- dispatch({
- type: 'CHANGED_UNIT_VALUE',
- unit: newUnit,
- steps: steps,
+ />
+ {
+ onChange({
+ unit: newUnit,
+ value: convertUnits(
+ newUnit,
+ normedValue,
conversions,
- })
- }}
- />
-
- {onRemove &&
}
+ 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 (
-
+
+
)
}
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 dd3bc0a4..00000000
--- a/packages/gui/src/components/inputs/Dimension/types.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { UnitConversions, UnitSteps } from '../../../lib'
-import { CSSFunctionCalc } from '../../../types/css'
-
-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/FontFamily/Input.tsx b/packages/gui/src/components/inputs/FontFamily/Input.tsx
index c54e09ae..fc242163 100644
--- a/packages/gui/src/components/inputs/FontFamily/Input.tsx
+++ b/packages/gui/src/components/inputs/FontFamily/Input.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { kebabCase, uniq } from 'lodash-es'
+import { kebabCase } from 'lodash-es'
import { useCombobox } from 'downshift'
import { FontFamilyType } from '../../../types/css'
import { EditorProps } from '../../../types/editor'
@@ -70,7 +70,7 @@ export function FontFamilyInput({ label, value, onChange, onRemove }: Props) {
getFontData()
}, [])
- const themeFonts = uniq(theme.fonts?.map((f) => f.stack) || [])
+ const themeFonts = Object.keys(theme.fonts || {})
const [includeSans, setIncSans] = React.useState(true)
const [includeSerif, setIncSerif] = React.useState(true)
const [includeMono, setIncMono] = React.useState(true)
@@ -158,7 +158,7 @@ export function FontFamilyInput({ label, value, onChange, onRemove }: Props) {
{label}
)}
-
+
@@ -308,6 +308,9 @@ export function FontFamilyInput({ label, value, onChange, onRemove }: Props) {
})}
+
{variableFont &&
Object.entries(variableFont).map(([k, v]) => {
if (['name', 'ital'].includes(k)) return null
@@ -324,10 +327,11 @@ export function FontFamilyInput({ label, value, onChange, onRemove }: Props) {
max={v.max}
step={v.step}
label={nameMap[k] ?? k}
- sx={{ width: '100%' }}
+ sx={{ width: '100%', }}
/>
)
})}
+
)
}
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 890eb73a..00000000
--- a/packages/gui/src/components/inputs/Gradient/field.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import { SelectInput } from '../SelectInput'
-import { getInputProps } from '../../../lib/util'
-import GradientStopsField from './stops'
-import {
- ConicGradient,
- Gradient,
- LinearGradient,
- RadialGradient,
-} from './types'
-import { PositionInput } from '../PositionInput'
-import { AngleInput } from '../AngleInput'
-import { EditorProps } from '../../../types/editor'
-
-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 (
-
-
- {
- props.onChange({
- ...getDefaultGradient(type),
- // keep the stops of the current gradient when overriding
- stops: props.value.stops,
- })
- }}
- />
-
-
-
-
-
-
- )
-}
-
-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
- }
-}
-
-export const LinearGradientEditor = (props: EditorProps) => {
- return (
-
- )
-}
-
-export const RadialGradientEditor = (props: EditorProps) => {
- return (
-
- )
-}
-
-export const ConicGradientEditor = (props: EditorProps) => {
- return (
-
- )
-}
-
-function getDefaultGradient(type: Gradient['type']): Gradient {
- switch (type) {
- case 'linear':
- case 'repeating-linear':
- return {
- type,
- angle: { value: 0, unit: 'deg' },
- stops: [],
- }
- case 'radial':
- case 'repeating-radial':
- return {
- type,
- shape: 'circle',
- position: {
- x: { value: 'center', unit: 'keyword' },
- y: { value: 'center', unit: 'keyword' },
- },
- stops: [],
- }
- case 'conic':
- case 'repeating-conic':
- return {
- type,
- angle: { value: 0, unit: 'deg' },
- position: {
- x: { value: 'center', unit: 'keyword' },
- y: { value: 'center', unit: 'keyword' },
- },
- stops: [],
- }
- }
-}
diff --git a/packages/gui/src/components/inputs/Gradient/stops.tsx b/packages/gui/src/components/inputs/Gradient/stops.tsx
index b3f4a8de..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 7b84a156..65631ae8 100644
--- a/packages/gui/src/components/inputs/Gradient/stringify.ts
+++ b/packages/gui/src/components/inputs/Gradient/stringify.ts
@@ -4,15 +4,17 @@ import {
stringifyPosition,
stringifyUnit,
} from '../../../lib/stringify'
-import { Gradient } from './types'
+import { Theme } from '../../../types/theme'
+import { color } from '../../schemas/color'
+import { Gradient, GradientStop } from './types'
-export function stringifyGradient(gradient: Gradient): string {
+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, '%'),
+ stringifyStops(gradient.stops, theme),
])
}
case 'radial':
@@ -21,7 +23,7 @@ export function stringifyGradient(gradient: Gradient): string {
`${gradient.shape ?? 'circle'} at ${stringifyPosition(
gradient.position
)}`,
- stringifyStops(gradient, '%'),
+ stringifyStops(gradient.stops, theme),
])
}
case 'conic':
@@ -30,15 +32,18 @@ export function stringifyGradient(gradient: Gradient): string {
`from ${stringifyUnit(gradient.angle)} at ${stringifyPosition(
gradient.position
)}`,
- stringifyStops(gradient, '%'),
+ stringifyStops(gradient.stops, theme),
])
}
}
}
-const stringifyStops = (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(', ')
}
diff --git a/packages/gui/src/components/inputs/Gradient/types.ts b/packages/gui/src/components/inputs/Gradient/types.ts
index 606c7129..fe8f5ee0 100644
--- a/packages/gui/src/components/inputs/Gradient/types.ts
+++ b/packages/gui/src/components/inputs/Gradient/types.ts
@@ -1,4 +1,5 @@
import { Angle, Color, Position } from '../../../types/css'
+import { ThemeColor } from '../../primitives/ColorPicker/PalettePicker'
interface BaseGradient {
type: string
@@ -6,7 +7,7 @@ interface BaseGradient {
}
export interface GradientStop {
- color: Color
+ color: Color | ThemeColor
hinting: number // TODO units
}
diff --git a/packages/gui/src/components/inputs/GridLine.tsx b/packages/gui/src/components/inputs/GridLine.tsx
deleted file mode 100644
index a87aa9e7..00000000
--- a/packages/gui/src/components/inputs/GridLine.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { getInputProps } from '../../lib/util'
-import { Label, Number } from '../primitives'
-import * as Toggle from '@radix-ui/react-toggle'
-import { stringifyValues } from '../../lib/stringify'
-import { EditorPropsWithLabel } from '../../types/editor'
-
-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: 'primary',
- 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 39108b28..00000000
--- a/packages/gui/src/components/inputs/GridTrack/field.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { getInputProps } from '../../../lib/util'
-import { EditorProps, EditorPropsWithLabel } 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 47f90a18..00000000
--- a/packages/gui/src/components/inputs/ImageSource/field.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import Layers, { LayerProps } from '../../Layers'
-import { ImageSource, ImageSourceType } from './types'
-import { 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'
-import { EditorPropsWithLabel } from '../../../types/editor'
-
-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) => {
- return (
-
- {
- props.onChange(convertBackgroundImageValue(props.value, newType))
- }}
- />
- {props.value.type === 'url' ? (
- {
- const newValue = produce(props.value, (draft: any) => {
- draft.arguments[0] = newUrl
- })
- props.onChange(newValue)
- }}
- />
- ) : (
- {
- const newValue = produce(props.value, (draft: any) => {
- draft.gradient = newGradient
- })
- props.onChange(newValue)
- }}
- />
- )}
-
- )
-}
-
-function Thumbnail({ value }: { value: string }) {
- return (
-
- )
-}
-
-function convertBackgroundImageValue(
- value: ImageSource,
- newType: ImageSourceType
-): ImageSource {
- if (value.type === newType) {
- return value
- }
-
- // Otherwise, reset to the default of that filter type
- return getDefault(newType)
-}
-
-function getDefault(type: ImageSourceType): ImageSource {
- switch (type) {
- case 'gradient':
- return {
- type,
- gradient: {
- type: 'linear',
- angle: { value: 0, unit: 'deg' },
- stops: [],
- },
- }
- case 'url':
- default:
- return { type: 'url', arguments: [DEFAULT_IMAGE_URL] }
- }
-}
diff --git a/packages/gui/src/components/inputs/ImageSource/stringify.ts b/packages/gui/src/components/inputs/ImageSource/stringify.ts
deleted file mode 100644
index cbf8d9f0..00000000
--- a/packages/gui/src/components/inputs/ImageSource/stringify.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { stringifyFunction } from '../../../lib/stringify'
-import { stringifyGradient } from '../Gradient/stringify'
-import { ImageSource } from './types'
-
-export function stringifyImageSource(imageSource: ImageSource | ImageSource[]) {
- if (Array.isArray(imageSource)) {
- return imageSource.map(stringifyEntry).join(', ')
- }
- return stringifyEntry(imageSource)
-}
-
-function stringifyEntry(imageSource: ImageSource) {
- const { type } = imageSource
- switch (type) {
- case 'gradient': {
- return stringifyGradient(imageSource.gradient)
- }
- case 'url':
- default:
- return stringifyFunction(type, imageSource.arguments)
- }
-}
diff --git a/packages/gui/src/components/inputs/ImageSource/types.ts b/packages/gui/src/components/inputs/ImageSource/types.ts
deleted file mode 100644
index 4c1ed88d..00000000
--- a/packages/gui/src/components/inputs/ImageSource/types.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { CSSFunctionURL } from '../../../types/css'
-import { Gradient } from '../Gradient/types'
-
-// TODO: The background-image grammar and functionality is actually much
-// more complex, but for now we'll just roll with url() and gradients.
-// For now, we'll hardcode the bg image as a single element stack which
-// we serialize, though in the future there's no reason why we can't expose
-// this as a proper stack/layer.
-export type ImageSourceType = 'url' | 'gradient'
-export type ImageSourceGradient = {
- type: 'gradient'
- gradient: Gradient
-}
-export type ImageSource = CSSFunctionURL | ImageSourceGradient
diff --git a/packages/gui/src/components/inputs/KeywordInput.tsx b/packages/gui/src/components/inputs/KeywordInput.tsx
index 33af3722..9e0cc236 100644
--- a/packages/gui/src/components/inputs/KeywordInput.tsx
+++ b/packages/gui/src/components/inputs/KeywordInput.tsx
@@ -1,41 +1,23 @@
-import { omit } from 'lodash-es'
import { EditorPropsWithLabel } from '../../types/editor'
-import { Label } from '../primitives'
import { KeywordSelect } from '../primitives/KeywordSelect'
-import { ResponsiveInput } from '../Responsive'
+import { InputHeader } from '../ui/InputHeader'
interface Props extends EditorPropsWithLabel {
options: T[]
}
-export function BaseKeywordInput(props: Props) {
+export function KeywordInput(props: Props) {
return (
-
-
{props.label}
-
-
-
+
+
)
}
-
-export function KeywordInput
({ ...props }: Props) {
- if (props.topLevel) {
- return (
-
- )
- }
-
- return
-}
diff --git a/packages/gui/src/components/inputs/LengthInput.tsx b/packages/gui/src/components/inputs/LengthInput.tsx
deleted file mode 100644
index ee8b6b48..00000000
--- a/packages/gui/src/components/inputs/LengthInput.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { compact } from 'lodash-es'
-import {
- CSSFunctionCalc,
- CSSUnitValue,
- Dimension,
- Length,
- LENGTH_UNITS,
- PercentageLengthUnits,
- UnitlessUnits,
-} from '../../types/css'
-import { DimensionInput } from './Dimension'
-
-interface LengthInputProps {
- value: Dimension
- onChange: (value: Length) => void
- label: string
- property?: string
- keywords?: string[]
- number?: boolean
- percentage?: boolean
- flex?: boolean
- themeValues?: (CSSUnitValue & { id: string })[]
-}
-
-export function LengthInput({
- property,
- number,
- percentage,
- flex,
- value: providedValue,
- ...props
-}: LengthInputProps) {
- const units = compact([
- number && UnitlessUnits.Number,
- ...LENGTH_UNITS,
- percentage && PercentageLengthUnits.Pct,
- flex && 'fr', // TODO flex should be non-negative
- ])
- const value =
- providedValue === '0' ? { value: 0, unit: 'number' } : providedValue
- return (
-
- )
-}
-
-const lengthConversions = {
- px: 16,
- rem: 1,
- em: 1,
-}
-
-const lengthSteps = {
- number: 0.1,
- theme: 1,
- px: 1,
- em: 0.125,
- rem: 0.125,
- '%': 0.1,
- fr: 0.1,
-}
diff --git a/packages/gui/src/components/inputs/Mask/field.tsx b/packages/gui/src/components/inputs/Mask/field.tsx
deleted file mode 100644
index d8295d9c..00000000
--- a/packages/gui/src/components/inputs/Mask/field.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { EditorProps, EditorPropsWithLabel } from '../../../types/editor'
-
-import { stringifyMaskList } from './stringify'
-
-import Layers from '../../Layers'
-import { getInputProps } from '../../../lib/util'
-import { ImageSourceEditor } from '../ImageSource/field'
-import { PositionInput } from '../PositionInput'
-import { SelectInput } from '../SelectInput'
-import { Mask, compositingOperators, maskingModes } from './types'
-import { RepeatStyleInput } from '../Background/field'
-import { BgSizeInput } from '../BgSizeInput'
-import { GEOMETRY_BOX_KEYWORDS } from '../../../types/css'
-
-export default function MaskInput(props: EditorPropsWithLabel) {
- const newItem = () => {
- // generate a new text shadow with the units of the previous box shadow
- return {
- clip: 'border-box',
- image: {
- type: 'gradient',
- gradient: {
- type: 'linear',
- angle: { value: 0, unit: 'deg' },
- 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: '%' },
- },
- composite: 'add',
- mode: 'alpha',
- } as any
- }
- return (
-
- {...props}
- newItem={newItem}
- content={MaskLayer}
- stringify={stringifyMaskList}
- thumbnail={Thumbnail}
- />
- )
-}
-
-export const MaskLayer = (props: EditorProps) => {
- return (
-
- )
-}
-
-function Thumbnail({ value }: { value: string }) {
- return (
-
- )
-}
diff --git a/packages/gui/src/components/inputs/Mask/stringify.ts b/packages/gui/src/components/inputs/Mask/stringify.ts
deleted file mode 100644
index 1a61f2e4..00000000
--- a/packages/gui/src/components/inputs/Mask/stringify.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { stringifyPosition, stringifyValues } from '../../../lib/stringify'
-import { stringifyRepeatStyle } from '../Background/stringify'
-import { stringifyBgSize } from '../BgSizeInput'
-import { stringifyImageSource } from '../ImageSource/stringify'
-import { Mask } from './types'
-
-export function stringifyMaskList(maskList: Mask[]) {
- return maskList.map(stringifyMask).join(', ')
-}
-
-function stringifyMask(mask: Mask) {
- const { clip, image, origin, position, repeat, size, composite, mode } = mask
- return stringifyValues([
- stringifyImageSource(image),
- stringifyPosition(position),
- `/ ${stringifyBgSize(size)}`,
- stringifyRepeatStyle(repeat),
- origin,
- clip,
- composite,
- mode,
- ])
-}
diff --git a/packages/gui/src/components/inputs/Mask/types.ts b/packages/gui/src/components/inputs/Mask/types.ts
deleted file mode 100644
index 55cf54d5..00000000
--- a/packages/gui/src/components/inputs/Mask/types.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { GeometryBox, Position } from '../../../types/css'
-import { RepeatStyle } from '../Background/types'
-import { BgSize } from '../BgSizeInput'
-import { ImageSource } from '../ImageSource/types'
-
-export interface Mask {
- clip: GeometryBox | 'no-clip'
- composite: CompositeOperator
- image: ImageSource
- mode: MaskingMode
- origin: GeometryBox
- position: Position
- repeat: RepeatStyle
- size: BgSize
-}
-
-export const maskingModes = ['alpha', 'luminance', 'match-source'] as const
-type MaskingMode = typeof maskingModes[number]
-
-export const compositingOperators = [
- 'add',
- 'subtract',
- 'intersect',
- 'exclude',
-] as const
-type CompositeOperator = typeof compositingOperators[number]
diff --git a/packages/gui/src/components/inputs/Multidimension/Input.tsx b/packages/gui/src/components/inputs/Multidimension/Input.tsx
deleted file mode 100644
index d45523ae..00000000
--- a/packages/gui/src/components/inputs/Multidimension/Input.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import * as React from 'react'
-import {
- CSSUnitValue,
- Length,
- MultidimensionalLength,
-} from '../../../types/css'
-import { reducer } from './reducer'
-import { State } from './types'
-import { EditorProps } from '../../../types/editor'
-import { DEFAULT_LENGTH } from '../../../lib/constants'
-import { LengthInput } from '../LengthInput'
-import { isMultidimensionalLength } from '../../../lib/util'
-import { convertToMultidimensional } from './util'
-
-// Mapping of units to [min, max] tuple
-type UnitRanges = Record
-// Mapping of units to steps
-type UnitSteps = Record
-
-export interface MultidimensionInputProps
- extends EditorProps {
- label?: string
- dimensions: number
-}
-export const MultidimensionInput = ({
- value,
- onChange,
- dimensions,
- label,
- ...props
-}: MultidimensionInputProps) => {
- const [state, dispatch] = React.useReducer(reducer, {
- value: convertToMultidimensional(value ?? DEFAULT_LENGTH),
- dimensions,
- isMultidimensional: isMultidimensionalLength(value),
- key: 0,
- } as State)
- React.useEffect(() => {
- onChange(state.value)
- }, [state])
-
- const handleDimensionChange = (dimension: number) => (value: Length) => {
- dispatch({
- type: 'CHANGED_VALUE',
- dimension,
- value,
- })
- }
-
- return (
-
- )
-}
diff --git a/packages/gui/src/components/inputs/Multidimension/index.ts b/packages/gui/src/components/inputs/Multidimension/index.ts
deleted file mode 100644
index ba0d3b5d..00000000
--- a/packages/gui/src/components/inputs/Multidimension/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { MultidimensionInput } from './Input'
diff --git a/packages/gui/src/components/inputs/Multidimension/reducer.ts b/packages/gui/src/components/inputs/Multidimension/reducer.ts
deleted file mode 100644
index d39b32a7..00000000
--- a/packages/gui/src/components/inputs/Multidimension/reducer.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import produce from 'immer'
-import { CSSUnitValue, MultidimensionalLengthUnit } from '../../../types/css'
-import { State, Action } from './types'
-import { convertToMultidimensional } from './util'
-
-export const reducer = (state: State, action: Action): State => {
- switch (action.type) {
- case 'CHANGED_VALUE': {
- const dimension = action.dimension as number
- const newValueItem = action.value as CSSUnitValue
-
- // @ts-ignore
- if (isNaN(newValueItem.value) && dimension > 0) {
- const firstValue = convertToMultidimensional(state.value).values[0]
-
- return {
- ...state,
- isMultidimensional: false,
- value: firstValue,
- }
- }
-
- const newValue = produce(
- convertToMultidimensional(state.value),
- (draft: MultidimensionalLengthUnit) => {
- draft.values[dimension] = newValueItem
- }
- )
-
- return {
- ...state,
- isMultidimensional: true,
- value: newValue,
- }
- }
- default: {
- break
- }
- }
-
- return state
-}
diff --git a/packages/gui/src/components/inputs/Multidimension/types.ts b/packages/gui/src/components/inputs/Multidimension/types.ts
deleted file mode 100644
index 87d87716..00000000
--- a/packages/gui/src/components/inputs/Multidimension/types.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Length, MultidimensionalLengthUnit } from '../../../types/css'
-
-export type State = {
- key: number
- dimensions: number
- value: MultidimensionalLengthUnit | Length
- isMultidimensional: boolean
-}
-
-export type Action =
- | {
- type: 'CHANGED_VALUE'
- dimension: number
- value: Length
- }
- | {
- type: 'TOGGLE_MULTIDIMENSIONAL'
- }
diff --git a/packages/gui/src/components/inputs/Multidimension/util.ts b/packages/gui/src/components/inputs/Multidimension/util.ts
deleted file mode 100644
index 3b747858..00000000
--- a/packages/gui/src/components/inputs/Multidimension/util.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { isMultidimensionalLength } from '../../../lib/util'
-import { Length, MultidimensionalLength } from '../../../types/css'
-
-export const convertToMultidimensional = (
- value: Length | MultidimensionalLength,
- dimensions: number = 2
-): MultidimensionalLength => {
- if (isMultidimensionalLength(value)) {
- return value as MultidimensionalLength
- }
-
- const values = Array(dimensions).fill(value)
-
- return {
- type: 'multidimensionalLength',
- values: values,
- }
-}
diff --git a/packages/gui/src/components/inputs/NumberInput.tsx b/packages/gui/src/components/inputs/NumberInput.tsx
index 0ceb9071..6368a55d 100644
--- a/packages/gui/src/components/inputs/NumberInput.tsx
+++ b/packages/gui/src/components/inputs/NumberInput.tsx
@@ -1,20 +1,16 @@
-import { kebabCase } from 'lodash-es'
-import { useId } from 'react'
-import { Number, Label } from '../primitives'
-import { EditorProps } from '../../types/editor'
+import { Number } from '../primitives'
+import { EditorPropsWithLabel } from '../../types/editor'
+import { InputHeader } from '../ui/InputHeader'
-interface Props extends EditorProps {
- label: string
+interface Props extends EditorPropsWithLabel {
min?: number
max?: number
step?: number
}
/** A labelled number field */
-export function NumberInput({ label, value, ...props }: Props) {
- const id = `${useId()}-${kebabCase(label)}`
+export function NumberInput(props: Props) {
return (
-
+
)
}
diff --git a/packages/gui/src/components/inputs/OffsetPath.tsx b/packages/gui/src/components/inputs/OffsetPath.tsx
deleted file mode 100644
index dfd53b55..00000000
--- a/packages/gui/src/components/inputs/OffsetPath.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Label } from 'theme-ui'
-import { stringifyValues } from '../../lib/stringify'
-import { getInputProps } from '../../lib/util'
-import { GeometryBox, GEOMETRY_BOX_KEYWORDS } from '../../types/css'
-import { EditorPropsWithLabel } from '../../types/editor'
-import { BasicShapeInput } from './BasicShape/input'
-import { stringifyBasicShape } from './BasicShape/stringify'
-import { BasicShape } from './BasicShape/types'
-import { SelectInput } from './SelectInput'
-
-// TODO `url()` and `ray()` options
-interface OffsetPath {
- shape: BasicShape
- box: GeometryBox
-}
-
-export function OffsetPathInput(props: EditorPropsWithLabel) {
- return (
-
- {props.label}
-
-
-
- )
-}
-
-export function stringifyOffsetPath(value: OffsetPath) {
- return stringifyValues([stringifyBasicShape(value.shape)])
-}
diff --git a/packages/gui/src/components/inputs/PositionInput.tsx b/packages/gui/src/components/inputs/PositionInput.tsx
deleted file mode 100644
index edae96ad..00000000
--- a/packages/gui/src/components/inputs/PositionInput.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { getInputProps } from '../../lib/util'
-import { Position } from '../../types/css'
-import { EditorPropsWithLabel } from '../../types/editor'
-import { Label } from '../primitives'
-import { DeletePropButton } from './Dimension/Input'
-import { LengthInput } from './LengthInput'
-
-export function PositionInput({
- onRemove,
- ...props
-}: EditorPropsWithLabel) {
- return (
-
-
{props.label}
-
-
-
- {onRemove && }
-
-
- )
-}
diff --git a/packages/gui/src/components/inputs/PrimitiveInput.tsx b/packages/gui/src/components/inputs/PrimitiveInput.tsx
index 726e2cfa..94640db8 100644
--- a/packages/gui/src/components/inputs/PrimitiveInput.tsx
+++ b/packages/gui/src/components/inputs/PrimitiveInput.tsx
@@ -1,90 +1,27 @@
-import { CSSUnitValue, Primitive } from '../../types/css'
+import { CSSUnitValue } from '../../types/css'
import { EditorPropsWithLabel } from '../../types/editor'
-import { ColorInput } from './ColorInput'
import { DimensionInput } from './Dimension'
-import { KeywordInput } from './KeywordInput'
-import { LengthInput } from './LengthInput'
-import { StringInput } from './StringInput'
-import { TimeInput } from './TimeInput'
+import { NumberInput } from './NumberInput'
-interface Props extends EditorPropsWithLabel {
- input: Primitive
- // TODO more robustly type the possible props of primitive inputs
- [prop: string]: any
-}
-
-export function PrimitiveInput({ input, ...props }: Props) {
- const Component = getPrimitiveInput(input)
- return
-}
-
-function getPrimitiveInput(type: Primitive) {
- switch (type) {
- case 'keyword':
- return KeywordInput2
- case 'number':
- return NumberInput
- case 'integer':
- return IntegerInput
- case 'percentage':
- return PercentageInput
- case 'length':
- return LengthInput
- case 'time':
- return TimeInput
- case 'string':
- return StringInput
- case 'color':
- return ColorInput
- }
-}
-
-// remap the prop names
-const KeywordInput2 = ({ keywords, ...props }: any) => {
- return
-}
-
-const NumberInput = ({
+export const IntegerInput = ({
value,
onChange,
onRemove,
label,
...props
-}: EditorPropsWithLabel) => {
+}: EditorPropsWithLabel) => {
return (
-
- )
-}
-
-const IntegerInput = ({
- value,
- onChange,
- onRemove,
- label,
- ...props
-}: EditorPropsWithLabel) => {
- return (
-
)
}
-const PercentageInput = ({
+export const PercentageInput = ({
value,
onChange,
onRemove,
diff --git a/packages/gui/src/components/inputs/SchemaInput.tsx b/packages/gui/src/components/inputs/SchemaInput.tsx
new file mode 100644
index 00000000..a966af70
--- /dev/null
+++ b/packages/gui/src/components/inputs/SchemaInput.tsx
@@ -0,0 +1,84 @@
+import { DataTypeSchema } from '../schemas/types'
+import * as Collapsible from '@radix-ui/react-collapsible'
+import { InputHeader } from '../ui/InputHeader'
+import IconButton from '../ui/IconButton'
+import { ChevronDown } from 'react-feather'
+
+interface Props {
+ schema: DataTypeSchema
+ label: string
+ value: T
+ ruleset?: any
+ property?: string
+ onChange(value: T): void
+ onRemove?(): void
+ onDrag?(): void
+ onDragEnd?(): void
+}
+
+/**
+ * Creates a labelled input from a schema
+ */
+export function SchemaInput({
+ schema,
+ label,
+ value,
+ onChange,
+ ruleset,
+ property,
+ ...props
+}: Props) {
+ const Input = schema.input
+ const InlineInput = schema.inlineInput
+
+ const content = Input && (
+
+ )
+ const { hasBlockInput = () => !!content } = schema
+ return (
+
+
+ {hasBlockInput(value) && (
+
+
+
+
+
+ )}
+ {InlineInput && (
+
+ )}
+
+ {content && {content}}
+
+ )
+}
diff --git a/packages/gui/src/components/inputs/ScrollSnapAlign.tsx b/packages/gui/src/components/inputs/ScrollSnapAlign.tsx
deleted file mode 100644
index 83c24217..00000000
--- a/packages/gui/src/components/inputs/ScrollSnapAlign.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { stringifyValues } from '../../lib/stringify'
-import { getInputProps } from '../../lib/util'
-import { EditorPropsWithLabel } from '../../types/editor'
-import { Label } from '../primitives'
-import { SelectInput } from './SelectInput'
-
-interface ScrollSnapAlign {
- x: SnapPosition
- y: SnapPosition
-}
-
-const snapPositions = ['none', 'start', 'center', 'end'] as const
-type SnapPosition = typeof snapPositions[number]
-
-export function ScrollSnapAlignInput(
- props: EditorPropsWithLabel
-) {
- return (
-
-
{props.label}
-
-
-
-
-
- )
-}
-
-export function stringifyScrollSnapAlign(value: ScrollSnapAlign) {
- const { x, y } = value
- return stringifyValues([x, y])
-}
diff --git a/packages/gui/src/components/inputs/SelectInput.tsx b/packages/gui/src/components/inputs/SelectInput.tsx
index b3af1018..d574d680 100644
--- a/packages/gui/src/components/inputs/SelectInput.tsx
+++ b/packages/gui/src/components/inputs/SelectInput.tsx
@@ -1,7 +1,5 @@
-import { kebabCase } from 'lodash-es'
-import { useId } from 'react'
-import { Label } from '../primitives'
-import { DeletePropButton } from './Dimension/Input'
+import * as Select from '../ui/Select'
+import { InputHeader } from '../ui/InputHeader'
interface Props {
label: string
@@ -9,39 +7,45 @@ interface Props {
onRemove?: () => void
value: T
options: readonly T[]
+ ruleset?: any
+ property?: string
+ decorateText?(text: T): string
}
// A select input with a label
export function SelectInput({
- label,
- value,
- onChange,
- onRemove,
- options,
+ decorateText,
+ ...props
}: Props) {
- const id = `${useId()}-${kebabCase(label)}`
+ const { value, onChange, options = [] } = props
return (
-
-
- {label}
-
-
- {onRemove && }
-
-
-
+
+
+
+
+
+
+
+ {options.map((v) => {
+ return (
+
+
+
+ {decorateText ? decorateText(v) : v}
+
+
+ )
+ })}
+
+
+
)
}
diff --git a/packages/gui/src/components/inputs/ShapeOutside.tsx b/packages/gui/src/components/inputs/ShapeOutside.tsx
deleted file mode 100644
index 3a1a5525..00000000
--- a/packages/gui/src/components/inputs/ShapeOutside.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { stringifyValues } from '../../lib/stringify'
-import { getInputProps } from '../../lib/util'
-import { ShapeBox, SHAPE_BOX_KEYWORDS } from '../../types/css'
-import { EditorProps, EditorPropsWithLabel } from '../../types/editor'
-import { Label } from '../primitives'
-import { BasicShapeInput, getDefaultBasicShape } from './BasicShape/input'
-import { stringifyBasicShape } from './BasicShape/stringify'
-import { BasicShape } from './BasicShape/types'
-import { ImageSourceEditor } from './ImageSource/field'
-import { stringifyImageSource } from './ImageSource/stringify'
-import { ImageSource } from './ImageSource/types'
-import { SelectInput } from './SelectInput'
-
-type ShapeOutside = Image | Shape
-
-interface Image {
- type: 'image'
- image: ImageSource
-}
-
-interface Shape {
- type: 'shape'
- shape: BasicShape
- box: ShapeBox
-}
-
-export function ShapeOutsideInput(props: EditorPropsWithLabel) {
- return (
-
- {props.label}
- props.onChange(getDefaultValue(type))}
- />
-
-
- )
-}
-
-function ShapeOutsideSwitch(props: EditorProps) {
- switch (props.value.type) {
- case 'image': {
- const _props = props as EditorProps
- return
- }
- case 'shape': {
- const _props = props as EditorProps
- return (
-
-
-
-
- )
- }
- }
-}
-
-export function stringifyShapeOutside(value: ShapeOutside) {
- switch (value.type) {
- case 'image': {
- return stringifyImageSource(value.image)
- }
- case 'shape': {
- return stringifyValues([stringifyBasicShape(value.shape), value.box])
- }
- }
-}
-
-function getDefaultValue(type: 'image' | 'shape'): ShapeOutside {
- switch (type) {
- case 'image': {
- return {
- type,
- image: {
- type: 'gradient',
- gradient: {
- type: 'linear',
- angle: { value: 0, unit: 'deg' },
- stops: [
- { hinting: 0, color: 'black' },
- { hinting: 100, color: 'transparent' },
- ],
- },
- },
- }
- }
- case 'shape': {
- return {
- type,
- shape: getDefaultBasicShape('inset'),
- box: 'content-box',
- }
- }
- }
-}
diff --git a/packages/gui/src/components/inputs/StrokeDasharray.tsx b/packages/gui/src/components/inputs/StrokeDasharray.tsx
deleted file mode 100644
index 62012061..00000000
--- a/packages/gui/src/components/inputs/StrokeDasharray.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { stringifyValues } from '../../lib/stringify'
-import { Length } from '../../types/css'
-import { EditorPropsWithLabel } from '../../types/editor'
-import FieldArray from '../FieldArray'
-import { LengthInput } from './LengthInput'
-
-export function StrokeDasharrayInput({
- label,
- value,
- onChange,
-}: EditorPropsWithLabel) {
- return (
- ({ value: 0, unit: 'number' })}
- />
- )
-}
-
-export function stringifyStrokeDasharray(items: Length[]) {
- return stringifyValues(items)
-}
diff --git a/packages/gui/src/components/inputs/TextInput.tsx b/packages/gui/src/components/inputs/TextInput.tsx
index 1680a0a5..2f1dcf79 100644
--- a/packages/gui/src/components/inputs/TextInput.tsx
+++ b/packages/gui/src/components/inputs/TextInput.tsx
@@ -1,37 +1,18 @@
-import { kebabCase } from 'lodash-es'
-import { useId } from 'react'
-import { Label } from '../primitives'
-import { DeletePropButton } from './Dimension/Input'
+import { EditorPropsWithLabel } from '../../types/editor'
+import { InputHeader } from '../ui/InputHeader'
-interface Props {
- label: string
- onChange: (newValue: T) => void
- onRemove?: () => void
- value: T
-}
-export function TextInput({
- label,
- value,
- onChange,
- onRemove,
-}: Props) {
- const id = `${useId()}-${kebabCase(label)}`
+type Props = EditorPropsWithLabel
+export function TextInput(props: Props) {
+ const { value, onChange } = props
return (
-
-
- {label}
-
- onChange(e.target.value as T)}
- sx={{ width: '100%', minHeight: '1.6em', mr: 1 }}
- />
- {onRemove && }
-
-
+
+ onChange(e.target.value as T)}
+ sx={{ WebkitAppearance: 'none', appearance: 'none', width: '100%', border: '1px solid', boxSizing: 'border-box', display: 'block', p:2, borderRadius: '6px' }}
+ />
)
}
diff --git a/packages/gui/src/components/inputs/TimeInput.tsx b/packages/gui/src/components/inputs/TimeInput.tsx
deleted file mode 100644
index b3dbd4d3..00000000
--- a/packages/gui/src/components/inputs/TimeInput.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Time, TIME_UNITS } from '../../types/css'
-import { EditorPropsWithLabel } from '../../types/editor'
-import { DimensionInput } from './Dimension'
-
-export const TimeInput = (props: EditorPropsWithLabel