summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorSam Nystrom <sam@samnystrom.dev>2024-03-13 18:01:48 +0000
committerSam Nystrom <sam@samnystrom.dev>2024-03-13 20:17:07 -0400
commit9eb1625ec5de3c221ed0445dde874fcb1dc3ff48 (patch)
treefe2d2cb383813ca3511af68065257b6ea561afe8 /src/components
parent13451b7588aa5800f0c1a87e7c3b49830d9e4087 (diff)
feat: add menu components
Diffstat (limited to 'src/components')
-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
12 files changed, 193 insertions, 8 deletions
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';