From db370aaf430d01bfe7402e2edfe83d0397da375f Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:43:34 +0200 Subject: [PATCH 01/21] refactor: replace menu types --- src/components/menu/types.ts | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/components/menu/types.ts diff --git a/src/components/menu/types.ts b/src/components/menu/types.ts deleted file mode 100644 index cd8fd51..0000000 --- a/src/components/menu/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type React from 'react'; - -export interface MenuItem { - id: string; - label: React.ReactNode; - disabled?: boolean; - onPress?: (() => void) | undefined; -} - -export interface MenuProps { - trigger?: React.ReactNode; - items: MenuItem[]; - onDismiss?: (() => void) | undefined; - closeOnSelect?: boolean; - testID?: string; -} From c9e1353bdb30a39ebc4b3dce9350c4ab22392a91 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:44:13 +0200 Subject: [PATCH 02/21] restore menu types placeholder --- src/components/menu/types.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/components/menu/types.ts diff --git a/src/components/menu/types.ts b/src/components/menu/types.ts new file mode 100644 index 0000000..c430d29 --- /dev/null +++ b/src/components/menu/types.ts @@ -0,0 +1 @@ +export interface MenuProps {} From 467e9805bf1495f978fbfcf85c24a39d4137ba1e Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:44:35 +0200 Subject: [PATCH 03/21] feat: define structured menu props --- src/components/menu/types.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/menu/types.ts b/src/components/menu/types.ts index c430d29..5e79fef 100644 --- a/src/components/menu/types.ts +++ b/src/components/menu/types.ts @@ -1 +1,15 @@ -export interface MenuProps {} +import type React from 'react'; + +export interface MenuAction { + id: string; + title: React.ReactNode; + disabled?: boolean; + selected?: boolean; +} + +export interface MenuProps { + trigger?: React.ReactNode; + actions: readonly MenuAction[]; + closeOnSelect?: boolean; + testID?: string; +} From 4b90903f25efac683d0176af27d07e2aba1f16a2 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:45:06 +0200 Subject: [PATCH 04/21] feat: add menu action content fields --- src/components/menu/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/menu/types.ts b/src/components/menu/types.ts index 5e79fef..9927da7 100644 --- a/src/components/menu/types.ts +++ b/src/components/menu/types.ts @@ -3,6 +3,9 @@ import type React from 'react'; export interface MenuAction { id: string; title: React.ReactNode; + description?: React.ReactNode; + leading?: React.ReactNode; + trailing?: React.ReactNode; disabled?: boolean; selected?: boolean; } From a440ff59a758e2eb5d5251468b78c8bd4d0ac4f8 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:46:19 +0200 Subject: [PATCH 05/21] feat: add menu action callbacks --- src/components/menu/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/menu/types.ts b/src/components/menu/types.ts index 9927da7..c7807ee 100644 --- a/src/components/menu/types.ts +++ b/src/components/menu/types.ts @@ -8,11 +8,13 @@ export interface MenuAction { trailing?: React.ReactNode; disabled?: boolean; selected?: boolean; + activate?: VoidFunction; } export interface MenuProps { trigger?: React.ReactNode; actions: readonly MenuAction[]; + dismiss?: VoidFunction; closeOnSelect?: boolean; testID?: string; } From f70c5d6b9a43a85315f800e91864fead52151a2c Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:46:57 +0200 Subject: [PATCH 06/21] feat: render structured menu actions --- src/components/menu/Menu.tsx | 97 +++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/src/components/menu/Menu.tsx b/src/components/menu/Menu.tsx index 8a4bb25..b246598 100644 --- a/src/components/menu/Menu.tsx +++ b/src/components/menu/Menu.tsx @@ -5,12 +5,12 @@ import { FocusScope } from '../../internal/focus/FocusScope'; import { useFocusManager } from '../../internal/focus/useFocusManager'; import { Portal } from '../../internal/overlay/Portal'; import { resolveOverlayAnimation } from '../../internal/resolvers'; -import { Box, Surface } from '../../layout'; +import { Box, Inline, Stack, Surface } from '../../layout'; import { ButtonBase } from '../../primitives/button-base'; import { Text } from '../../primitives/text'; import { useTheme } from '../../theme/ThemeContext'; import { resolveNextMenuIndex } from './navigation'; -import type { MenuProps } from './types'; +import type { MenuAction, MenuProps } from './types'; interface MeasurableNode { measureInWindow?: ( @@ -25,7 +25,30 @@ function measureNode(node: unknown, callback: (layout: LayoutRectangle) => void) }); } -export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID }: MenuProps) { +function renderActionContent(action: MenuAction, active: boolean) { + const titleColor = active || action.selected ? 'neutral' : undefined; + + return ( + + {action.leading ? {action.leading} : null} + + + + {action.title} + + {action.description ? ( + + {action.description} + + ) : null} + + + {action.trailing ? {action.trailing} : null} + + ); +} + +export function Menu({ trigger, actions, dismiss, closeOnSelect = true, testID }: MenuProps) { const { theme } = useTheme(); const { bindKeydown } = useFocusManager(); const animation = resolveOverlayAnimation('menu'); @@ -36,15 +59,29 @@ export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID } const closeMenu = React.useCallback(() => { setOpen(false); - onDismiss?.(); - }, [onDismiss]); + dismiss?.(); + }, [dismiss]); + + const activateAction = React.useCallback( + (action: MenuAction) => { + if (action.disabled) { + return; + } + + action.activate?.(); + if (closeOnSelect) { + closeMenu(); + } + }, + [closeMenu, closeOnSelect], + ); const openMenu = React.useCallback(() => { measureNode(anchorRef.current, setLayout); - const firstEnabledIndex = items.findIndex((item) => !item.disabled); + const firstEnabledIndex = actions.findIndex((action) => !action.disabled); setActiveIndex(firstEnabledIndex === -1 ? 0 : firstEnabledIndex); setOpen(true); - }, [items]); + }, [actions]); React.useEffect(() => { if (!open) { @@ -55,17 +92,14 @@ export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID } const { key } = event; if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Home' || key === 'End') { event.preventDefault(); - setActiveIndex((current) => resolveNextMenuIndex(items, current, key)); + setActiveIndex((current) => resolveNextMenuIndex(actions, current, key)); } if (event.key === 'Enter') { event.preventDefault(); - const activeItem = items[activeIndex]; - if (activeItem && !activeItem.disabled) { - activeItem.onPress?.(); - if (closeOnSelect) { - closeMenu(); - } + const activeAction = actions[activeIndex]; + if (activeAction) { + activateAction(activeAction); } } @@ -74,7 +108,7 @@ export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID } closeMenu(); } }); - }, [activeIndex, bindKeydown, closeMenu, closeOnSelect, items, open]); + }, [activateAction, actions, activeIndex, bindKeydown, closeMenu, open]); return ( @@ -117,7 +151,7 @@ export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID } accessibilityRole="menu" p="xs" style={{ - minWidth: Math.max(layout?.width ?? 0, 180), + minWidth: Math.max(layout?.width ?? 0, 220), shadowOpacity: 0.12, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, @@ -125,40 +159,31 @@ export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID } testID={testID} variant="raised" > - {items.map((item, index) => { - const selected = index === activeIndex; + {actions.map((action, index) => { + const active = index === activeIndex; + const selected = action.selected || active; return ( { - if (item.disabled) { - return; - } - - item.onPress?.(); - if (closeOnSelect) { - closeMenu(); - } - }} + accessibilityState={{ disabled: action.disabled, selected }} + disabled={action.disabled} + key={action.id} + onPress={() => activateAction(action)} > - - {item.label} - + {renderActionContent(action, active)} ); From 7898506fb769cc83a97f588297f9b4a5edbd5303 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:47:56 +0200 Subject: [PATCH 07/21] feat: update menu barrel type export --- src/components/menu/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/menu/index.ts b/src/components/menu/index.ts index f45dfe6..c837993 100644 --- a/src/components/menu/index.ts +++ b/src/components/menu/index.ts @@ -1,2 +1,2 @@ export { Menu } from './Menu'; -export type { MenuItem, MenuProps } from './types'; +export type { MenuAction as MenuItem, MenuProps } from './types'; From e0ada25bdc2b460b8c8406548fa47bc795c88b03 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:49:22 +0200 Subject: [PATCH 08/21] feat: export menu action type --- src/components/menu/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/menu/index.ts b/src/components/menu/index.ts index c837993..72253b5 100644 --- a/src/components/menu/index.ts +++ b/src/components/menu/index.ts @@ -1,2 +1,2 @@ export { Menu } from './Menu'; -export type { MenuAction as MenuItem, MenuProps } from './types'; +export type { MenuAction, MenuAction as MenuItem, MenuProps } from './types'; From 2c8093284ce3c7dcadd480c9ee40b52c9cc4dfc7 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:49:53 +0200 Subject: [PATCH 09/21] feat: expose menu action type --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 93f1490..ab708a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ export type { LabelProps } from './components/label'; export { Label } from './components/label'; export type { ListItemProps } from './components/list-item'; export { ListItem } from './components/list-item'; -export type { MenuItem, MenuProps } from './components/menu'; +export type { MenuAction, MenuItem, MenuProps } from './components/menu'; export { Menu } from './components/menu'; export type { ModalProps } from './components/modal'; export { Modal } from './components/modal'; @@ -86,4 +86,4 @@ export { } from './surfaceColor'; export * from './theme'; export { isDeepEqual } from './utils/deepEqual'; -export { deepMerge } from './utils/deepMerge'; +export { deepMerge } from './utils/deepMerge'; \ No newline at end of file From 1f1f02f56ec95968ed69c52e4da5148f61f87b53 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:50:23 +0200 Subject: [PATCH 10/21] chore: add changeset --- .changeset/menu.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/menu.md diff --git a/.changeset/menu.md b/.changeset/menu.md new file mode 100644 index 0000000..a9047ac --- /dev/null +++ b/.changeset/menu.md @@ -0,0 +1,5 @@ +--- +'@ankhorage/surface': patch +--- + +Update Menu. From b363931929ab139bdabff1972c04d954afd54280 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:51:01 +0200 Subject: [PATCH 11/21] feat: add menu action intent --- src/components/menu/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/menu/types.ts b/src/components/menu/types.ts index c7807ee..f099523 100644 --- a/src/components/menu/types.ts +++ b/src/components/menu/types.ts @@ -1,11 +1,14 @@ import type React from 'react'; +export type MenuActionIntent = 'default' | 'danger'; + export interface MenuAction { id: string; title: React.ReactNode; description?: React.ReactNode; leading?: React.ReactNode; trailing?: React.ReactNode; + intent?: MenuActionIntent; disabled?: boolean; selected?: boolean; activate?: VoidFunction; From f2519c3b92efa79cd7da88e61597378e4db8efa2 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:51:47 +0200 Subject: [PATCH 12/21] feat: render dangerous menu actions --- src/components/menu/Menu.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/menu/Menu.tsx b/src/components/menu/Menu.tsx index b246598..add981a 100644 --- a/src/components/menu/Menu.tsx +++ b/src/components/menu/Menu.tsx @@ -26,7 +26,8 @@ function measureNode(node: unknown, callback: (layout: LayoutRectangle) => void) } function renderActionContent(action: MenuAction, active: boolean) { - const titleColor = active || action.selected ? 'neutral' : undefined; + const titleColor = + action.intent === 'danger' ? 'danger' : active || action.selected ? 'neutral' : undefined; return ( From 4073c9755d85aa0945dcabfd20f73d51ddbb343f Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:52:19 +0200 Subject: [PATCH 13/21] feat: export menu action intent type --- src/components/menu/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/menu/index.ts b/src/components/menu/index.ts index 72253b5..73f4fad 100644 --- a/src/components/menu/index.ts +++ b/src/components/menu/index.ts @@ -1,2 +1,2 @@ export { Menu } from './Menu'; -export type { MenuAction, MenuAction as MenuItem, MenuProps } from './types'; +export type { MenuAction, MenuAction as MenuItem, MenuActionIntent, MenuProps } from './types'; From 4a4090dcb6d8f0a17cda8e3903b8ceddfcd7003d Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:52:47 +0200 Subject: [PATCH 14/21] feat: expose menu action intent type --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index ab708a4..6ab0350 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ export type { LabelProps } from './components/label'; export { Label } from './components/label'; export type { ListItemProps } from './components/list-item'; export { ListItem } from './components/list-item'; -export type { MenuAction, MenuItem, MenuProps } from './components/menu'; +export type { MenuAction, MenuActionIntent, MenuItem, MenuProps } from './components/menu'; export { Menu } from './components/menu'; export type { ModalProps } from './components/modal'; export { Modal } from './components/modal'; From 4dee3ce89baddec08a7e861a50a30531e3671080 Mon Sep 17 00:00:00 2001 From: artiphishle Date: Sun, 17 May 2026 00:55:30 +0200 Subject: [PATCH 15/21] chore(lint): fix --- src/components/menu/Menu.tsx | 6 +++++- src/components/menu/index.ts | 2 +- src/index.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/menu/Menu.tsx b/src/components/menu/Menu.tsx index add981a..2cb493c 100644 --- a/src/components/menu/Menu.tsx +++ b/src/components/menu/Menu.tsx @@ -34,7 +34,11 @@ function renderActionContent(action: MenuAction, active: boolean) { {action.leading ? {action.leading} : null} - + {action.title} {action.description ? ( diff --git a/src/components/menu/index.ts b/src/components/menu/index.ts index 73f4fad..7f2b9f8 100644 --- a/src/components/menu/index.ts +++ b/src/components/menu/index.ts @@ -1,2 +1,2 @@ export { Menu } from './Menu'; -export type { MenuAction, MenuAction as MenuItem, MenuActionIntent, MenuProps } from './types'; +export type { MenuAction, MenuActionIntent, MenuAction as MenuItem, MenuProps } from './types'; diff --git a/src/index.ts b/src/index.ts index 6ab0350..d7e0fe0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,4 +86,4 @@ export { } from './surfaceColor'; export * from './theme'; export { isDeepEqual } from './utils/deepEqual'; -export { deepMerge } from './utils/deepMerge'; \ No newline at end of file +export { deepMerge } from './utils/deepMerge'; From 004d7b75c2f6e4b9622fc45edf86e9dad8d78e3d Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:56:00 +0200 Subject: [PATCH 16/21] fix: accept readonly menu actions in navigation --- src/components/menu/navigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/menu/navigation.ts b/src/components/menu/navigation.ts index 5fa31d5..681c150 100644 --- a/src/components/menu/navigation.ts +++ b/src/components/menu/navigation.ts @@ -1,5 +1,5 @@ export function resolveNextMenuIndex( - items: { disabled?: boolean }[], + items: readonly { disabled?: boolean }[], currentIndex: number, key: 'ArrowDown' | 'ArrowUp' | 'Home' | 'End', ): number { From 49f5998d82a39ac0899cb5882caa417df524a396 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 00:59:18 +0200 Subject: [PATCH 17/21] fix: use explicit menu callback types --- src/components/menu/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/menu/types.ts b/src/components/menu/types.ts index f099523..3874105 100644 --- a/src/components/menu/types.ts +++ b/src/components/menu/types.ts @@ -11,13 +11,13 @@ export interface MenuAction { intent?: MenuActionIntent; disabled?: boolean; selected?: boolean; - activate?: VoidFunction; + activate?: () => void; } export interface MenuProps { trigger?: React.ReactNode; actions: readonly MenuAction[]; - dismiss?: VoidFunction; + dismiss?: () => void; closeOnSelect?: boolean; testID?: string; } From d826924e05fd58fdfc6fe95fc8ce11796a5ca8c1 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 17 May 2026 01:00:28 +0200 Subject: [PATCH 18/21] fix: satisfy menu lint rules --- src/components/menu/Menu.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/menu/Menu.tsx b/src/components/menu/Menu.tsx index 2cb493c..e0800f6 100644 --- a/src/components/menu/Menu.tsx +++ b/src/components/menu/Menu.tsx @@ -64,7 +64,9 @@ export function Menu({ trigger, actions, dismiss, closeOnSelect = true, testID } const closeMenu = React.useCallback(() => { setOpen(false); - dismiss?.(); + if (dismiss) { + dismiss(); + } }, [dismiss]); const activateAction = React.useCallback( @@ -73,7 +75,10 @@ export function Menu({ trigger, actions, dismiss, closeOnSelect = true, testID } return; } - action.activate?.(); + if (action.activate) { + action.activate(); + } + if (closeOnSelect) { closeMenu(); } @@ -166,7 +171,7 @@ export function Menu({ trigger, actions, dismiss, closeOnSelect = true, testID } > {actions.map((action, index) => { const active = index === activeIndex; - const selected = action.selected || active; + const selected = action.selected ?? active; return ( Date: Sun, 17 May 2026 01:00:54 +0200 Subject: [PATCH 19/21] fix: update menu docs example --- src/examples/DocsExamples.tsx | 127 ++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 60 deletions(-) diff --git a/src/examples/DocsExamples.tsx b/src/examples/DocsExamples.tsx index 7a5f888..ea43f17 100644 --- a/src/examples/DocsExamples.tsx +++ b/src/examples/DocsExamples.tsx @@ -1,77 +1,84 @@ import React from 'react'; -import { - Badge, - Box, - Button, - Checkbox, - Drawer, - Field, - HelperText, - Label, - Menu, - Modal, - Radio, - Stack, - Switch, - Tab, - TabList, - TabPanel, - Tabs, - Text, - Textarea, - TextInput, - ThemeProvider, -} from '../index'; +import { ThemeProvider } from '../theme'; +import { Box, Stack } from '../layout'; +import { Button } from '../components/button'; +import { Card } from '../components/card'; +import { Checkbox } from '../components/checkbox'; +import { Drawer } from '../components/drawer'; +import { Field } from '../components/field'; +import { HelperText } from '../components/helper-text'; +import { Menu } from '../components/menu'; +import { Modal } from '../components/modal'; +import { Tab, TabList, TabPanel, Tabs } from '../components/tabs'; +import { TextInput } from '../components/text-input'; +import { ToastProvider, useToast } from '../components/toast'; +import { Tooltip } from '../components/tooltip'; +import { Text } from '../primitives/text'; +import type { ThemeConfig } from '../theme/types'; -const docsThemeConfig = { - id: 'docs-example', - name: 'Docs Example', - light: { - harmony: 'monochromatic' as const, - primaryColor: '#2563eb', - }, - dark: { - harmony: 'monochromatic' as const, - primaryColor: '#2563eb', - }, +const docsThemeConfig: ThemeConfig = { + id: 'docs', + name: 'Docs', + light: { primaryColor: '#2563EB', harmony: 'analogous' }, + dark: { primaryColor: '#60A5FA', harmony: 'analogous' }, }; -export function ProviderExample() { +export function FormExample() { return ( - Surface starter - - + + + + + Looks good. ); } -export function FormAndOverlayExample() { +function ToastButton() { + const { showToast } = useToast(); + return ( + + ); +} + +export function FeedbackExample() { return ( - - - - - -