Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/many-foxes-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@compai/css-gui': patch
---

Add selection state to HTML editor
3 changes: 1 addition & 2 deletions apps/docs/components/playground/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export const Layout = (props: Props) => {
<div
sx={{
fontFamily: 'body',
display: 'grid',
gridTemplateColumns: ['1fr', 'auto 320px', 'auto 320px'],
marginRight: '1px',
}}
{...props}
/>
Expand Down
16 changes: 13 additions & 3 deletions apps/docs/pages/html-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { HtmlEditor, HtmlRenderer, htmlToEditorSchema } from '@compai/css-gui'
import {
HtmlEditor,
HtmlRenderer,
HtmlEditorProvider,
htmlToEditorSchema,
} from '@compai/css-gui'
import { useState } from 'react'

// TODO: Handle style attrs
Expand All @@ -13,10 +18,15 @@ const initialValue = htmlToEditorSchema(`

export default function HtmlEditorExample() {
const [html, setHtml] = useState(initialValue)

return (
<div sx={{ display: 'flex' }}>
<HtmlEditor value={html} onChange={setHtml} />
<HtmlRenderer value={html} />
<HtmlEditorProvider value={html}>
<HtmlEditor onChange={setHtml} />
<div sx={{ width: '100%' }}>
<HtmlRenderer value={html} />
</div>
</HtmlEditorProvider>
</div>
)
}
53 changes: 53 additions & 0 deletions packages/gui/src/components/html/Provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createContext, ReactNode, useContext, useState } from 'react'
import { htmlToEditorSchema } from '../../lib'
import { HtmlNode, ElementPath } from './types'

const DEFAULT_HTML_EDITOR_VALUE = {
selected: [],
setSelected: () => {},
value: htmlToEditorSchema(`
<div class="section">
<h1>Hello, world!</h1>
<h2>Weeee!</h2>
<button>I'm a CTA</button>
<a href="https://components.ai">I'm a link!</a>
</div>
`),
}

export type HtmlEditor = {
value: HtmlNode
selected: ElementPath | null
setSelected: (newSelection: ElementPath | null) => void
}

export function useHtmlEditor() {
const context = useContext(HtmlEditorContext)
return context
}

const HtmlEditorContext = createContext<HtmlEditor>(DEFAULT_HTML_EDITOR_VALUE)

type HtmlEditorProviderProps = {
value: HtmlNode
children: ReactNode
}
export function HtmlEditorProvider({
children,
value,
}: HtmlEditorProviderProps) {
const [selected, setSelected] = useState<ElementPath | null>([])

const fullContext = {
value,
selected,
setSelected: (newSelection: ElementPath | null) =>
setSelected(newSelection),
}

return (
<HtmlEditorContext.Provider value={fullContext}>
{children}
</HtmlEditorContext.Provider>
)
}
51 changes: 42 additions & 9 deletions packages/gui/src/components/html/Renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,67 @@
import { toCSSObject } from '../../lib'
import { ElementData } from './types'
import { ElementData, ElementPath } from './types'
import { HTMLFontTags } from './FontTags'
import { useHtmlEditor } from './Provider'

interface Props {
interface HtmlRendererProps {
value: ElementData
path?: ElementPath
}

export function HtmlRenderer({ value }: Props) {
export function HtmlRenderer({ value }: HtmlRendererProps) {
return (
<>
<HTMLFontTags htmlTree={value} />
<ElementRenderer value={value} />
<ElementRenderer value={value} path={[] as ElementPath} />
</>
)
}

function ElementRenderer({ value }: Props) {
const { tagName, attributes = {}, style = {}, children = [] } = value
interface ElementRendererProps {
value: ElementData
path: ElementPath
}
function ElementRenderer({ value, path }: ElementRendererProps) {
const { selected, setSelected } = useHtmlEditor()
const { attributes = {}, style = {}, children = [] } = value
const Tag: any = value.tagName || 'div'

const sx = toCSSObject(style)

if (selected && isSamePath(path, selected)) {
sx.outline = 'thin solid tomato'
}

return (
<>
<Tag {...attributes} sx={{ ...toCSSObject(style) }}>
<Tag
{...cleanAttributes(attributes)}
sx={sx}
onClick={(e: MouseEvent) => {
e.stopPropagation()
setSelected(path)
}}
>
{children.map((child, i) => {
if (typeof child === 'string') {
return child
}
return <ElementRenderer key={i} value={child} />
return <ElementRenderer key={i} value={child} path={[...path, i]} />
})}
</Tag>
</>
)
}

const cleanAttributes = (attributes: Record<string, string>) => {
const newAttributes = { ...attributes }

if (newAttributes.href) {
newAttributes.href = '#'
}

return newAttributes
}

const isSamePath = (path1: ElementPath, path2: ElementPath) => {
return path1.join('-') === path2.join('-')
}
19 changes: 10 additions & 9 deletions packages/gui/src/components/html/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Editor } from '../Editor'
import { HtmlNode, HTMLTag } from './types'
import { HtmlNode, HTMLTag, ElementPath } from './types'
import * as Collapsible from '@radix-ui/react-collapsible'
import { Fragment, useState } from 'react'
import { isNil } from 'lodash-es'
Expand All @@ -9,6 +9,7 @@ import { Label, Combobox } from '../primitives'
import { SelectInput } from '../inputs/SelectInput'
import { AttributeEditor } from './AttributeEditor'
import { DEFAULT_STYLES } from './default-styles'
import { useHtmlEditor } from './Provider'

const HTML_TAGS = [
HTMLTag.P,
Expand All @@ -26,20 +27,16 @@ const HTML_TAGS = [
HTMLTag.Div,
]

interface EditorProps {
value: HtmlNode
interface HtmlEditorProps {
onChange(value: HtmlNode): void
}

type ElementPath = number[]

/**
* An HTML tree-based editor that lets you add HTML nodes and mess around with their styles
*/
export function HtmlEditor({ value, onChange }: EditorProps) {
const [selected, setSelected] = useState<ElementPath | null>(
value ? [0] : null
)
export function HtmlEditor({ onChange }: HtmlEditorProps) {
const { value, selected, setSelected } = useHtmlEditor()

return (
<div
sx={{
Expand Down Expand Up @@ -83,6 +80,10 @@ export function HtmlEditor({ value, onChange }: EditorProps) {
)
}

interface EditorProps {
value: HtmlNode
onChange(value: HtmlNode): void
}
interface TagEditorProps extends EditorProps {
onRemove(): void
}
Expand Down
3 changes: 2 additions & 1 deletion packages/gui/src/components/html/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface ElementData {
}

export type HtmlNode = ElementData | string
export type ElementPath = number[]

export const enum HTMLTag {
// Text
Expand Down Expand Up @@ -69,7 +70,7 @@ export const enum HTMLTag {
Th = 'th',
Thead = 'thead',
Tr = 'tr',
//
//
Details = 'details',
Dialog = 'dialog',
Summary = 'summary',
Expand Down
1 change: 1 addition & 0 deletions packages/gui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export { EditorProvider } from './components/providers/EditorContext'
export { Layout } from './components/ui/Layout'
export { HtmlEditor } from './components/html/editor'
export { HtmlRenderer } from './components/html/Renderer'
export { HtmlEditorProvider } from './components/html/Provider'

export { theme } from './components/ui/theme'
export { supportedProperties, unsupportedProperties } from './data/properties'
Expand Down