Skip to content

Commit 5d78f1d

Browse files
authored
Merge pull request #512 from components-ai/revamp-adding
Improve visual clutter of "add child" button
2 parents 2759ea5 + b9c9c6f commit 5d78f1d

File tree

2 files changed

+129
-73
lines changed

2 files changed

+129
-73
lines changed

packages/gui/src/components/html/Provider.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const DEFAULT_HTML_EDITOR_VALUE = {
1515
<a href="https://components.ai">I'm a link!</a>
1616
</div>
1717
`),
18+
isEditing: false,
19+
setEditing: () => {},
1820
hasComponents: false,
1921
}
2022

@@ -23,6 +25,8 @@ export type HtmlEditor = {
2325
theme?: any
2426
selected: ElementPath | null
2527
setSelected: (newSelection: ElementPath | null) => void
28+
isEditing: boolean
29+
setEditing(value: boolean): void
2630
components?: ComponentData[]
2731
hasComponents: boolean
2832
}
@@ -86,6 +90,7 @@ export function HtmlEditorProvider({
8690
components = [],
8791
}: HtmlEditorProviderProps) {
8892
const [selected, setSelected] = useState<ElementPath | null>([])
93+
const [isEditing, setEditing] = useState(false)
8994
const transformedValue = transformValueToSchema(value)
9095

9196
const fullContext = {
@@ -94,6 +99,8 @@ export function HtmlEditorProvider({
9499
setSelected: (newSelection: ElementPath | null) =>
95100
setSelected(newSelection),
96101
components,
102+
isEditing,
103+
setEditing: (newValue: any) => setEditing(newValue),
97104
hasComponents: !!components.length,
98105
}
99106

packages/gui/src/components/html/TreeNode.tsx

Lines changed: 122 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { HtmlNode, ElementPath } from './types'
22
import * as Collapsible from '@radix-ui/react-collapsible'
3-
import { Fragment, useState } from 'react'
3+
import { useState } from 'react'
44
import { useHtmlEditor } from './Provider'
55
import { isVoidElement } from '../../lib/elements'
66
import { addChildAtPath, isSamePath, replaceAt } from './util'
77
import { hasChildrenSlot } from '../../lib/codegen/util'
88
import { Combobox } from '../primitives'
99
import { HTML_TAGS } from './data'
1010
import { DEFAULT_ATTRIBUTES, DEFAULT_STYLES } from './default-styles'
11+
import { Plus } from 'react-feather'
12+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
1113

1214
interface EditorProps {
1315
value: HtmlNode
@@ -19,13 +21,17 @@ interface TreeNodeProps extends EditorProps {
1921
}
2022

2123
export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
22-
const { selected } = useHtmlEditor()
24+
const { selected, isEditing, setEditing } = useHtmlEditor()
2325
const [open, setOpen] = useState(true)
24-
const [editing, setEditing] = useState(false)
2526
const isSelected = isSamePath(path, selected)
27+
const isEditingNode = isSelected && isEditing
2628

27-
if (editing && !isSelected) {
28-
setEditing(false)
29+
function handleSelect() {
30+
// If we are selecting a different node than the currently selected node, move out of editing mode
31+
if (!isSelected) {
32+
setEditing(false)
33+
}
34+
onSelect(path)
2935
}
3036

3137
if (value.type === 'text') {
@@ -42,9 +48,11 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
4248
fontSize: 0,
4349
width: '100%',
4450
}}
45-
onClick={() => onSelect(path)}
51+
onClick={() => {
52+
handleSelect()
53+
}}
4654
>
47-
{editing ? (
55+
{isEditingNode ? (
4856
<textarea
4957
sx={{
5058
display: 'block',
@@ -97,15 +105,15 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
97105
textAlign: 'start',
98106
fontSize: 0,
99107
}}
100-
onClick={() => onSelect(path)}
108+
onClick={() => handleSelect()}
101109
>
102110
{value.name}: "{value.value}"
103111
</button>
104112
</div>
105113
)
106114
}
107115

108-
const tagEditor = editing ? (
116+
const tagEditor = isEditingNode ? (
109117
<Combobox
110118
key={selected?.join('-')}
111119
onFilterItems={(filterValue) => {
@@ -166,7 +174,9 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
166174
textAlign: 'start',
167175
display: 'inline-flex',
168176
}}
169-
onClick={() => onSelect(path)}
177+
onClick={() => {
178+
handleSelect()
179+
}}
170180
>
171181
&lt;{tagEditor}
172182
{!open || isSelfClosing(value) ? ' /' : null}&gt;
@@ -177,6 +187,11 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
177187
return tagButton
178188
}
179189

190+
function handleAddChild(i: number, type: string) {
191+
const child = type === 'tag' ? DEFAULT_TAG : DEFAULT_TEXT
192+
onChange(addChildAtPath(value, [i], child))
193+
}
194+
180195
return (
181196
<Collapsible.Root open={open} onOpenChange={setOpen}>
182197
<Collapsible.Trigger
@@ -203,17 +218,18 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
203218
/>
204219
<span sx={{ lineHeight: 1, fontFamily: 'monospace' }}>{tagButton}</span>
205220
<Collapsible.Content>
206-
<div sx={{ ml: 4 }}>
207-
{value.children?.length ? (
208-
value.children?.map((child, i) => {
209-
return (
210-
<Fragment key={i}>
211-
<AddChildButton
212-
onClick={() => {
213-
onChange(addChildAtPath(value, [i], DEFAULT_CHILD_NODE))
214-
onSelect([...path, i])
215-
}}
216-
/>
221+
<div sx={{ ml: 4, py: '0.0625rem' }}>
222+
{value.children?.map((child, i) => {
223+
return (
224+
<div key={i}>
225+
<AddChildButton
226+
onClick={(childType) => {
227+
handleAddChild(i, childType)
228+
onSelect([...path, i])
229+
setEditing(true)
230+
}}
231+
/>
232+
<div sx={{ py: '0.0625rem' }}>
217233
<TreeNode
218234
value={child}
219235
onSelect={onSelect}
@@ -225,29 +241,18 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
225241
})
226242
}}
227243
/>
228-
<AddChildButton
229-
onClick={() => {
230-
onChange(
231-
addChildAtPath(
232-
value,
233-
[value.children?.length ?? 0],
234-
DEFAULT_CHILD_NODE
235-
)
236-
)
237-
onSelect(null)
238-
}}
239-
/>
240-
</Fragment>
241-
)
242-
})
243-
) : (
244-
<AddChildButton
245-
onClick={() => {
246-
onChange(addChildAtPath(value, [0], DEFAULT_CHILD_NODE))
247-
onSelect([0])
248-
}}
249-
/>
250-
)}
244+
</div>
245+
</div>
246+
)
247+
})}
248+
<AddChildButton
249+
onClick={(childType) => {
250+
const index = value.children?.length ?? 0
251+
handleAddChild(index, childType)
252+
onSelect([...path, index])
253+
setEditing(true)
254+
}}
255+
/>
251256
</div>
252257
<div sx={{ display: 'flex', alignItems: 'center' }}>
253258
<div
@@ -299,39 +304,83 @@ const isSelfClosing = (node: HtmlNode) => {
299304
return !hasChildrenSlot(node.value)
300305
}
301306

302-
const DEFAULT_CHILD_NODE: HtmlNode = {
307+
const DEFAULT_TAG: HtmlNode = {
303308
type: 'element',
304309
tagName: 'div',
305-
children: [
306-
{
307-
type: 'text',
308-
value: '',
309-
},
310-
],
311310
}
312311

313-
function AddChildButton({ onClick }: { onClick(): void }) {
312+
const DEFAULT_TEXT: HtmlNode = {
313+
type: 'text',
314+
value: '',
315+
}
316+
317+
function AddChildButton({ onClick }: { onClick(type: string): void }) {
318+
const [hovered, setHovered] = useState(false)
319+
const [open, setOpen] = useState(false)
314320
return (
315-
<button
316-
onClick={onClick}
317-
sx={{
318-
cursor: 'pointer',
319-
display: 'block',
320-
background: 'none',
321-
border: 'none',
322-
textAlign: 'left',
323-
fontSize: 0,
324-
pt: 2,
325-
whiteSpace: 'nowrap',
326-
color: 'muted',
327-
zIndex: '99',
328-
transition: 'color .2s ease-in-out',
329-
':hover': {
330-
color: 'text',
331-
},
332-
}}
333-
>
334-
+ Add child
335-
</button>
321+
<div sx={{ position: 'relative' }}>
322+
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
323+
<DropdownMenu.Trigger
324+
sx={{
325+
'--height': '0.5rem',
326+
display: 'flex',
327+
alignItems: 'center',
328+
position: 'absolute',
329+
height: 'var(--height)',
330+
top: 'calc(var(--height) / -2 )',
331+
width: '100%',
332+
cursor: 'pointer',
333+
':hover': {
334+
color: 'muted',
335+
},
336+
337+
background: 'transparent',
338+
border: 'none',
339+
340+
'::before': {
341+
content: '""',
342+
backgroundColor: hovered || open ? 'muted' : 'transparent',
343+
display: 'block',
344+
height: '2px',
345+
width: '100%',
346+
},
347+
}}
348+
onMouseEnter={() => setHovered(true)}
349+
onMouseLeave={() => setHovered(false)}
350+
>
351+
<Plus size={16} />
352+
</DropdownMenu.Trigger>
353+
<DropdownMenu.Content
354+
sx={{
355+
minWidth: '12rem',
356+
border: '1px solid',
357+
borderColor: 'border',
358+
borderRadius: '0.25rem',
359+
backgroundColor: 'background',
360+
py: 2,
361+
}}
362+
>
363+
{['tag', 'text'].map((childType) => {
364+
return (
365+
<DropdownMenu.Item
366+
key={childType}
367+
onClick={() => {
368+
onClick(childType)
369+
}}
370+
sx={{
371+
cursor: 'pointer',
372+
px: 3,
373+
':hover': {
374+
backgroundColor: 'backgroundOffset',
375+
},
376+
}}
377+
>
378+
Add {childType}
379+
</DropdownMenu.Item>
380+
)
381+
})}
382+
</DropdownMenu.Content>
383+
</DropdownMenu.Root>
384+
</div>
336385
)
337386
}

0 commit comments

Comments
 (0)