From 9eb1625ec5de3c221ed0445dde874fcb1dc3ff48 Mon Sep 17 00:00:00 2001 From: Sam Nystrom Date: Wed, 13 Mar 2024 18:01:48 +0000 Subject: feat: add menu components --- src/components/Button.css | 3 ++- src/components/ButtonMenu.css | 33 +++++++++++++++++++++++++++++++ src/components/ButtonMenu.tsx | 42 ++++++++++++++++++++++++++++++++++++++++ src/components/ContainedList.css | 1 + src/components/Menu.css | 15 ++++++++++++++ src/components/Menu.tsx | 20 +++++++++++++++++++ src/components/MenuItem.css | 6 ++++++ src/components/MenuItem.tsx | 17 ++++++++++++++++ src/components/TextInput.css | 3 +-- src/components/Toolbar.css | 24 +++++++++++++++++++++++ src/components/Toolbar.tsx | 23 ++++++++++++++++++++++ src/components/index.ts | 14 +++++++++----- 12 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 src/components/ButtonMenu.css create mode 100644 src/components/ButtonMenu.tsx create mode 100644 src/components/Menu.css create mode 100644 src/components/Menu.tsx create mode 100644 src/components/MenuItem.css create mode 100644 src/components/MenuItem.tsx create mode 100644 src/components/Toolbar.css create mode 100644 src/components/Toolbar.tsx (limited to 'src/components') 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 */ @@ -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 ( +
+ + + {children} + +
+ ); +}; + +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 ( + <> + + {children} + + + ); +}; + +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 ( +
  • + +
  • + ); +}; + +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 ( +
    + + {children} + + +
    +
    + ); +}; + +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'; -- cgit v1.2.3