summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/NodeEditor.css2
-rw-r--r--src/NodeEditor.tsx16
-rw-r--r--src/Toolbar.css78
-rw-r--r--src/Toolbar.tsx30
-rw-r--r--src/components/Button.css3
-rw-r--r--src/components/ButtonMenu.css33
-rw-r--r--src/components/ButtonMenu.tsx42
-rw-r--r--src/components/ContainedList.css1
-rw-r--r--src/components/Menu.css15
-rw-r--r--src/components/Menu.tsx20
-rw-r--r--src/components/MenuItem.css6
-rw-r--r--src/components/MenuItem.tsx17
-rw-r--r--src/components/TextInput.css3
-rw-r--r--src/components/Toolbar.css24
-rw-r--r--src/components/Toolbar.tsx23
-rw-r--r--src/components/index.ts14
-rw-r--r--src/node.css267
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