diff options
| -rw-r--r-- | src/NodeEditor.css | 2 | ||||
| -rw-r--r-- | src/NodeEditor.tsx | 16 | ||||
| -rw-r--r-- | src/Toolbar.css | 78 | ||||
| -rw-r--r-- | src/Toolbar.tsx | 30 | ||||
| -rw-r--r-- | src/components/Button.css | 3 | ||||
| -rw-r--r-- | src/components/ButtonMenu.css | 33 | ||||
| -rw-r--r-- | src/components/ButtonMenu.tsx | 42 | ||||
| -rw-r--r-- | src/components/ContainedList.css | 1 | ||||
| -rw-r--r-- | src/components/Menu.css | 15 | ||||
| -rw-r--r-- | src/components/Menu.tsx | 20 | ||||
| -rw-r--r-- | src/components/MenuItem.css | 6 | ||||
| -rw-r--r-- | src/components/MenuItem.tsx | 17 | ||||
| -rw-r--r-- | src/components/TextInput.css | 3 | ||||
| -rw-r--r-- | src/components/Toolbar.css | 24 | ||||
| -rw-r--r-- | src/components/Toolbar.tsx | 23 | ||||
| -rw-r--r-- | src/components/index.ts | 14 | ||||
| -rw-r--r-- | src/node.css | 267 |
17 files changed, 339 insertions, 255 deletions
diff --git a/src/NodeEditor.css b/src/NodeEditor.css index 449286e..5a8a683 100644 --- a/src/NodeEditor.css +++ b/src/NodeEditor.css @@ -1,6 +1,6 @@ @scope (.__NodeEditor) { :scope { - --base: #122236; + --base: #121212; --surface: #242424; --overlay: #484848; --primary: #1c4c87; diff --git a/src/NodeEditor.tsx b/src/NodeEditor.tsx index a77f21b..b3ef838 100644 --- a/src/NodeEditor.tsx +++ b/src/NodeEditor.tsx @@ -4,7 +4,7 @@ import { Pb } from './context.ts'; import { nodeRegistry } from './nodes'; import { SocketHandlers, SocketHandler, NodeInfo } from './node.tsx'; import { InputSocket } from './dataflow.ts'; -import { Toolbar } from './Toolbar.tsx'; +import { Toolbar, ButtonMenu, MenuItem } from './components'; import './NodeEditor.css'; export const nodeFactory = () => { @@ -229,13 +229,19 @@ const NodeEditor = ({ user, project }) => { scale.value *= 1 + delta; }); - const onNodeAdded = (node: NodeInfo) => { + const addNode = (node: NodeInfo) => { nodes.value = nodes.value.concat(instantiateNode(100, 100, node)); }; return ( <div class="__NodeEditor"> - <Toolbar nodes={nodeRegistry} onNodeAdded={onNodeAdded} /> + <Toolbar title={project}> + <ButtonMenu label="Add"> + {Object.entries(nodeRegistry).map(([name, node]) => ( + <MenuItem label={name} onClick={() => addNode(node)} /> + ))} + </ButtonMenu> + </Toolbar> <svg width="100vw" height="100vh" ref={svgRef} onMouseDown={onBgMouseDown} onWheel={onBgWheel}> <pattern id="bg-grid-major" @@ -244,11 +250,11 @@ const NodeEditor = ({ user, project }) => { width={120 * scale.value} height={120 * scale.value} > <pattern id="bg-grid-minor" patternUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"> - <circle cx="2" cy="2" r="1" fill="#242424" /> + <circle cx="2" cy="2" r="1" fill="var(--surface)" /> </pattern> <g transform={`scale(${scale})`}> <rect fill="url(#bg-grid-minor)" x="0" y="0" width="100%" height="100%" /> - <circle cx="2" cy="2" r="2" fill="#242424" /> + <circle cx="2" cy="2" r="2" fill="var(--surface)" /> </g> </pattern> <rect fill="url(#bg-grid-major)" x="0" y="0" width="100%" height="100%" /> diff --git a/src/Toolbar.css b/src/Toolbar.css deleted file mode 100644 index b96e40c..0000000 --- a/src/Toolbar.css +++ /dev/null @@ -1,78 +0,0 @@ -@scope (.__Toolbar) { - .toolbar { - background-color: var(--surface); - border-bottom: 1px solid var(--overlay); - width: 100%; - margin: 0; - padding: 0; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - - > * { - flex-grow: 1; - display: flex; - justify-content: center; - } - :first-child, :last-child { flex-basis: 0 } - :first-child { justify-content: flex-start } - :last-child { justify-content: flex-end } - - > menu { - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: row; - } - - h1 { - font-size: 1.5rem; - font-weight: normal; - margin: 0; - } - - button { - background-color: transparent; - border: none; - outline: none; - padding: 1rem; - color: var(--text); - font-size: 1em; - :hover { background-color: var(--overlay) } - } - - path { - fill: var(--text); - } - } - - .addMenu:popover-open { - background-color: var(--surface); - list-style: none; - border: none; - position: absolute; - inset: unset; - width: auto; - height: auto; - left: 140px; - top: 52px; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - --shadow-color: 0deg 0% 0%; - box-shadow: - 0.1px 0.4px 0.6px hsl(var(--shadow-color) / 0.04), - 0.5px 3.3px 4.8px -0.2px hsl(var(--shadow-color) / 0.15), - 1.4px 10.2px 14.7px -0.5px hsl(var(--shadow-color) / 0.27); - - button { - padding: 4px; - width: 100%; - text-align: left; - &:hover { background-color: var(--overlay) } - } - } -}
\ No newline at end of file diff --git a/src/Toolbar.tsx b/src/Toolbar.tsx deleted file mode 100644 index 32f53ab..0000000 --- a/src/Toolbar.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useId } from 'preact/hooks'; -import { NodeInfo } from './node.tsx'; -import './Toolbar.css'; - -export interface ToolbarProps { - nodes: Record<string, NodeInfo>; - onNodeAdded?: (NodeInfo) => void; -} - -export const Toolbar = ({ nodes, onNodeAdded = _ => {} }: ToolbarProps) => { - const id = useId(); - return ( - <div class="__Toolbar"> - <menu> - <li><button>Edit</button></li> - <li><button>Select</button></li> - <li> - <menu id={id} class="addMenu" popover="auto"> - {Object.entries(nodes).map(([name, node]) => ( - <li><button onClick={() => onNodeAdded(node)}>{name}</button></li> - ))} - </menu> - <button popoverTarget={id}>Add</button> - </li> - </menu> - <h1>Sample Project</h1> - <div></div> - </div> - ); -}; diff --git a/src/components/Button.css b/src/components/Button.css index 210d131..595f053 100644 --- a/src/components/Button.css +++ b/src/components/Button.css @@ -2,7 +2,7 @@ :scope { width: 100%; min-width: fit-content; - height: 3rem; + height: var(--button-height, 2.5rem); padding: 0 1rem; /* Necessary for <a> */ @@ -26,6 +26,7 @@ &:focus { outline: 2px solid var(--primary); + outline-offset: -2px; border-color: var(--base); background-color: var(--button-focus, var(--button-base)); color: var(--button-focus-text, var(--button-text)); diff --git a/src/components/ButtonMenu.css b/src/components/ButtonMenu.css new file mode 100644 index 0000000..c454849 --- /dev/null +++ b/src/components/ButtonMenu.css @@ -0,0 +1,33 @@ +@scope (.__ButtonMenu) { + :scope { + > button { + position: relative; + padding-left: 2.5rem; + + &::before { + content: ''; + width: 1rem; + height: 1rem; + left: 0.75rem; + position: absolute; + background: url('../icons/chevron-right.svg'); + background-position: left 2px center; + background-repeat: no-repeat; + transition: transform 0.2s; + } + &:has(+ :popover-open)::before { + transform: rotate(90deg); + } + } + + [popover] { + inset: unset; + top: 0; + left: var(--anchor-x); + + &:popover-open { + top: var(--anchor-y); + } + } + } +}
\ No newline at end of file diff --git a/src/components/ButtonMenu.tsx b/src/components/ButtonMenu.tsx new file mode 100644 index 0000000..6a2b73e --- /dev/null +++ b/src/components/ButtonMenu.tsx @@ -0,0 +1,42 @@ +import type { ComponentChildren } from 'preact'; +import { useEffect, useRef, useId } from 'preact/hooks'; +import { useSignal, batch } from '@preact/signals'; +import Button from './Button.tsx'; +import Menu from './Menu.tsx'; +import './ButtonMenu.css'; + +export interface ButtonMenuProps { + children: ComponentChildren; + label: string; +} + +const ButtonMenu = ({ children, label }: ButtonMenuProps) => { + const id = useId(); + const ref = useRef(null); + const x = useSignal(0); + const y = useSignal(0); + + const updateRect = () => batch(() => { + const rect = ref.current?.getBoundingClientRect(); + if (!rect) return; + x.value = rect.x; + y.value = rect.y + rect.height + 1; + }); + useEffect(updateRect, [ref.current]); + + return ( + <div + class="__ButtonMenu" + ref={ref} + onScroll={updateRect} + style={`--anchor-x: ${x}px; --anchor-y: ${y}px;`} + > + <Button kind="ghost" popoverTarget={id}>{label}</Button> + <Menu id={id} popover="auto"> + {children} + </Menu> + </div> + ); +}; + +export default ButtonMenu; diff --git a/src/components/ContainedList.css b/src/components/ContainedList.css index 97c98cc..4cdf2f5 100644 --- a/src/components/ContainedList.css +++ b/src/components/ContainedList.css @@ -12,6 +12,7 @@ border-bottom: var(--border); } height: 3rem; + --button-height: 100%; display: flex; > * { flex-grow: 1; diff --git a/src/components/Menu.css b/src/components/Menu.css new file mode 100644 index 0000000..b999704 --- /dev/null +++ b/src/components/Menu.css @@ -0,0 +1,15 @@ +@scope (.__Menu) { + :scope { + padding: 0; + width: 15rem; + list-style: none; + background-color: var(--surface); + border: none; /* popover */ + + --shadow-color: 0deg 0% 0%; + box-shadow: + 0.1px 0.4px 0.6px hsl(var(--shadow-color) / 0.04), + 0.5px 3.3px 4.8px -0.2px hsl(var(--shadow-color) / 0.15), + 1.4px 10.2px 14.7px -0.5px hsl(var(--shadow-color) / 0.27); + } +}
\ No newline at end of file diff --git a/src/components/Menu.tsx b/src/components/Menu.tsx new file mode 100644 index 0000000..e8d37cc --- /dev/null +++ b/src/components/Menu.tsx @@ -0,0 +1,20 @@ +import type { ComponentChildren } from 'preact'; +import './Menu.css'; + +export interface MenuProps { + children: ComponentChildren; + id?: string; + popover?: string; +} + +const Menu = ({ id, popover, children }: MenuProps) => { + return ( + <> + <menu id={id} class="__Menu" popover={popover}> + {children} + </menu> + </> + ); +}; + +export default Menu; diff --git a/src/components/MenuItem.css b/src/components/MenuItem.css new file mode 100644 index 0000000..14a156e --- /dev/null +++ b/src/components/MenuItem.css @@ -0,0 +1,6 @@ +@scope (.__MenuItem) { + :scope { + --button-height: 2rem; + --surface: var(--overlay); + } +}
\ No newline at end of file diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx new file mode 100644 index 0000000..04c04c7 --- /dev/null +++ b/src/components/MenuItem.tsx @@ -0,0 +1,17 @@ +import Button from './Button.tsx'; +import './MenuItem.css'; + +export interface MenuItemProps { + label: string; + onClick?: (event: PointerEvent) => void; +} + +const MenuItem = ({ label, onClick }: MenuItemProps) => { + return ( + <li class="__MenuItem"> + <Button kind="ghost" onClick={onClick}>{label}</Button> + </li> + ); +}; + +export default MenuItem;
\ No newline at end of file diff --git a/src/components/TextInput.css b/src/components/TextInput.css index bb04415..08ab76b 100644 --- a/src/components/TextInput.css +++ b/src/components/TextInput.css @@ -10,8 +10,7 @@ &:focus { outline: 2px solid var(--primary); - border-bottom: none; - margin-bottom: 2px; + outline-offset: -2px; } } }
\ No newline at end of file diff --git a/src/components/Toolbar.css b/src/components/Toolbar.css new file mode 100644 index 0000000..02477ea --- /dev/null +++ b/src/components/Toolbar.css @@ -0,0 +1,24 @@ +@scope (.__Toolbar) { + :scope { + > * { + flex-grow: 1; + display: flex; + justify-content: center; + } + &:first-child, &:last-child { flex-basis: 0 } + &:first-child { justify-content: flex-start } + &:last-child { justify-content: flex-end } + + .actions { + margin: 0; + padding: 0; + list-style: none; + display: flex; + } + + .title { + font-size: 1.5rem; + width: fit-content; + } + } +}
\ No newline at end of file diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx new file mode 100644 index 0000000..1c985c1 --- /dev/null +++ b/src/components/Toolbar.tsx @@ -0,0 +1,23 @@ +import type { ComponentChildren } from 'preact'; +import Header from './Header.tsx'; +import Button from './Button.tsx'; +import './Toolbar.css'; + +export interface ToolbarProps { + children: ComponentChildren; + title: string; +} + +const Toolbar = ({ children, title }: ToolbarProps) => { + return ( + <Header class="__Toolbar"> + <menu class="actions"> + {children} + </menu> + <Button kind="ghost" class="title">{title}</Button> + <div></div> + </Header> + ); +}; + +export default Toolbar;
\ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts index 1beca4d..c9c3625 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,8 +1,12 @@ -export { default as Header } from './Header.tsx'; -export { default as TextInput } from './TextInput.tsx'; -export { default as Button } from './Button.tsx'; export { default as ArrowButton } from './ArrowButton.tsx'; +export { default as Button } from './Button.tsx'; +export { default as ButtonMenu } from './ButtonMenu.tsx'; export { default as ContainedList } from './ContainedList.tsx'; -export { default as FormLabel } from './FormLabel.tsx'; export { default as Content } from './Content.tsx'; -export { default as Form } from './Form.tsx';
\ No newline at end of file +export { default as Form } from './Form.tsx'; +export { default as FormLabel } from './FormLabel.tsx'; +export { default as Header } from './Header.tsx'; +export { default as Menu } from './Menu.tsx'; +export { default as MenuItem } from './MenuItem.tsx'; +export { default as TextInput } from './TextInput.tsx'; +export { default as Toolbar } from './Toolbar.tsx'; diff --git a/src/node.css b/src/node.css index 4ec7243..3b0ba16 100644 --- a/src/node.css +++ b/src/node.css @@ -1,149 +1,150 @@ - -.foreignObject { - width: 0; - height: 0; - overflow: visible; -} - -.node { - background-color: var(--surface); - min-width: 180px; - width: fit-content; - font-size: 0.9rem; - margin: 4px; - padding-bottom: 8px; - display: flex; - flex-direction: column; - border-radius: 6px; - user-select: none; - --shadow-color: 0deg 0% 0%; - box-shadow: - 0.1px 0.4px 0.6px hsl(var(--shadow-color) / 0.04), - 0.5px 3.3px 4.8px -0.2px hsl(var(--shadow-color) / 0.15), - 1.4px 10.2px 14.7px -0.5px hsl(var(--shadow-color) / 0.27); - - &:focus-within { - outline: 1px white solid; +@scope (.__NodeShell) { + :scope { + width: 0; + height: 0; + overflow: visible; } - - input[type="number"] { - background-color: var(--overlay); - border: none; - border-radius: 4px; - outline: none; - color: var(--text); - font-size: 1em; - text-align: right; - width: 100%; - max-width: 200px; - margin-right: 12px; - padding-top: 3px; - padding-bottom: 2px; - } - - summary { - background-color: var(--primary); - padding: 3px; - padding-left: 6px; - margin-bottom: 4px; - border-radius: 6px 6px 0 0; - list-style-image: url('icons/chevron-down.svg'); - } - &:not([open]) summary { - list-style-image: url('icons/chevron-right.svg'); - } - - ul { - list-style: none; - padding: 0; - margin: 0; - } - - ul li { + + .node { + background-color: var(--surface); + min-width: 180px; + width: fit-content; + font-size: 0.9rem; + margin: 4px; + padding-bottom: 8px; display: flex; - flex-direction: row; - align-items: center; - padding: 4px 0; - - circle { - fill: var(--data-float); - .number & { fill: var(--data-float) } - .vector & { fill: var(--data-vector) } - .select & { fill: var(--data-float) } + flex-direction: column; + border-radius: 6px; + user-select: none; + --shadow-color: 0deg 0% 0%; + box-shadow: + 0.1px 0.4px 0.6px hsl(var(--shadow-color) / 0.04), + 0.5px 3.3px 4.8px -0.2px hsl(var(--shadow-color) / 0.15), + 1.4px 10.2px 14.7px -0.5px hsl(var(--shadow-color) / 0.27); + + &:focus-within { + outline: 1px white solid; } - - &.out + &.in { - padding-top: 8px; + + input[type="number"] { + background-color: var(--overlay); + border: none; + border-radius: 4px; + outline: none; + color: var(--text); + font-size: 1em; + text-align: right; + width: 100%; + max-width: 200px; + margin-right: 12px; + padding-top: 3px; + padding-bottom: 2px; } - - &.out { - justify-content: flex-end; - - svg { - position: relative; - left: 4px; - } + + summary { + background-color: var(--primary); + padding: 3px; + padding-left: 6px; + margin-bottom: 4px; + border-radius: 6px 6px 0 0; + list-style-image: url('icons/chevron-down.svg'); + } + &:not([open]) summary { + list-style-image: url('icons/chevron-right.svg'); } - - &.in { - justify-content: flex-start; - - svg { - position: relative; - right: 4px; + + ul { + list-style: none; + padding: 0; + margin: 0; + } + + ul li { + display: flex; + flex-direction: row; + align-items: center; + padding: 4px 0; + + circle { + fill: var(--data-float); + .number & { fill: var(--data-float) } + .vector & { fill: var(--data-vector) } + .select & { fill: var(--data-float) } + } + + &.out + &.in { + padding-top: 8px; } - - &.number { - + .number { padding-top: 0 } - &.linked input { display: none } - - span { + + &.out { + justify-content: flex-end; + + svg { position: relative; - left: 8px; - width: 0; - white-space: nowrap; - padding-bottom: 2px; + left: 4px; } } - - &.vector { - flex-direction: column; - align-items: flex-start; - gap: 1px; - - & :first-child { - display: flex; - flex-direction: row; - align-items: center; - gap: 2px; - margin-bottom: 4px; - .linked & { margin-bottom: 0 } + + &.in { + justify-content: flex-start; + + svg { + position: relative; + right: 4px; } - - input { - width: 89%; - margin-left: 8px; - margin-right: 16px; - - &:nth-child(2) { border-radius: 4px 4px 0 0 } - &:nth-child(3) { border-radius: 0 } - &:nth-child(4) { border-radius: 0 0 4px 4px } - + + &.number { + + .number { padding-top: 0 } + &.linked input { display: none } + + span { + position: relative; + left: 8px; + width: 0; + white-space: nowrap; + padding-bottom: 2px; + } + } + + &.vector { + flex-direction: column; + align-items: flex-start; + gap: 1px; + + & :first-child { + display: flex; + flex-direction: row; + align-items: center; + gap: 2px; + margin-bottom: 4px; + .linked & { margin-bottom: 0 } + } + + input { + width: 89%; + margin-left: 8px; + margin-right: 16px; + + &:nth-child(2) { border-radius: 4px 4px 0 0 } + &:nth-child(3) { border-radius: 0 } + &:nth-child(4) { border-radius: 0 0 4px 4px } + + .linked & { display: none } + } + } + + &.select select { + background-color: color-mix(in srgb, var(--base) 50%, var(--surface)); + color: var(--text); + border: 1px solid color-mix(in srgb, var(--overlay) 50%, var(--surface)); + border-radius: 4px; + width: 100%; + margin-right: 12px; + padding: 3px 1px; + .linked & { display: none } } } - - &.select select { - background-color: color-mix(in srgb, var(--base) 50%, var(--surface)); - color: var(--text); - border: 1px solid color-mix(in srgb, var(--overlay) 50%, var(--surface)); - border-radius: 4px; - width: 100%; - margin-right: 12px; - padding: 3px 1px; - - .linked & { display: none } - } } } }
\ No newline at end of file |
