Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/nav-button-redesign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@workflowbuilder/ui': major
'@workflowbuilder/sdk': minor
---

`NavButton` takes `size`, `variant` (`square`, `round`, `plain`), `prefixIcon`, `suffixIcon` and `children` instead of inferring a subtype from the children structure, sizes are letter-based, and the selected state no longer shares a treatment with the pointer-down state. `MenuTriggerButton` is new, and `SegmentPicker` keeps its API while adopting the new slots; icons passed as `SegmentPicker.Item` children are no longer inferred as icons and must use an explicit icon slot.
2 changes: 1 addition & 1 deletion .changeset/ui-export-prop-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@workflowbuilder/ui': minor
---

Component prop types are now exported: `AvatarProps`, `CheckboxProps`, `RadioProps`, `StatusProps`, `TooltipProps`, `MenuProps`, `ModalProps`, `EdgeLabelProps`, `NodeIconProps`, `NodeDescriptionProps`, `NodeAsPortWrapperProps`, `SegmentPickerProps` (with its controlled/uncontrolled variants), the NavButton variant prop types, and `DatePickerProps` now covers the component's full runtime surface (`value`, `defaultValue`, `placeholder`, `valueFormat`, `type`, `error`). Supporting types used in those signatures (`Shape`, `IconNode`) are exported as well.
Component prop types are now exported: `AvatarProps`, `CheckboxProps`, `RadioProps`, `StatusProps`, `TooltipProps`, `MenuProps`, `ModalProps`, `EdgeLabelProps`, `NodeIconProps`, `NodeDescriptionProps`, `NodeAsPortWrapperProps`, `SegmentPickerProps` (with its controlled/uncontrolled variants), `NavButtonProps`, and `DatePickerProps` now covers the component's full runtime surface (`value`, `defaultValue`, `placeholder`, `valueFormat`, `type`, `error`). Supporting types used in those signatures (`Shape`, `IconNode`) are exported as well.
28 changes: 19 additions & 9 deletions apps/ai-studio/src/components/controls/ai-studio-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,28 @@ export function AiStudioControls() {
>
<div className={styles['panel']}>
{isRunning ? (
<NavButton onClick={cancel} tooltip="Cancel execution">
<Icon name="Stop" />
</NavButton>
<NavButton
aria-label="Cancel execution"
onClick={cancel}
tooltip="Cancel execution"
prefixIcon={<Icon name="Stop" />}
/>
) : (
<NavButton onClick={handleExecute} tooltip="Execute (backend)" disabled={isRunning}>
<Icon name="Play" />
</NavButton>
<NavButton
aria-label="Execute (backend)"
onClick={handleExecute}
tooltip="Execute (backend)"
disabled={isRunning}
prefixIcon={<Icon name="Play" />}
/>
)}
{isDone && (
<NavButton onClick={reset} tooltip="Reset">
<Icon name="ArrowCounterClockwise" />
</NavButton>
<NavButton
aria-label="Reset"
onClick={reset}
tooltip="Reset"
prefixIcon={<Icon name="ArrowCounterClockwise" />}
/>
)}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ export function ButtonsUndoRedo() {

return (
<>
<NavButton onClick={undo} disabled={!canUndo || isReadOnlyMode} tooltip={t('plugins.undoRedo.undo')}>
<Icon name="ArrowUUpLeft" />
</NavButton>
<NavButton onClick={redo} disabled={!canRedo || isReadOnlyMode} tooltip={t('plugins.undoRedo.redo')}>
<Icon name="ArrowUUpRight" />
</NavButton>
<NavButton
aria-label={t('plugins.undoRedo.undo')}
onClick={undo}
disabled={!canUndo || isReadOnlyMode}
tooltip={t('plugins.undoRedo.undo')}
prefixIcon={<Icon name="ArrowUUpLeft" />}
/>
<NavButton
aria-label={t('plugins.undoRedo.redo')}
onClick={redo}
disabled={!canRedo || isReadOnlyMode}
tooltip={t('plugins.undoRedo.redo')}
prefixIcon={<Icon name="ArrowUUpRight" />}
/>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ export function ButtonsUndoRedo() {

return (
<>
<NavButton onClick={undo} disabled={!canUndo || isReadOnlyMode} tooltip={t('plugins.undoRedo.undo')}>
<Icon name="ArrowUUpLeft" />
</NavButton>
<NavButton onClick={redo} disabled={!canRedo || isReadOnlyMode} tooltip={t('plugins.undoRedo.redo')}>
<Icon name="ArrowUUpRight" />
</NavButton>
<NavButton
aria-label={t('plugins.undoRedo.undo')}
onClick={undo}
disabled={!canUndo || isReadOnlyMode}
tooltip={t('plugins.undoRedo.undo')}
prefixIcon={<Icon name="ArrowUUpLeft" />}
/>
<NavButton
aria-label={t('plugins.undoRedo.redo')}
onClick={redo}
disabled={!canRedo || isReadOnlyMode}
tooltip={t('plugins.undoRedo.redo')}
prefixIcon={<Icon name="ArrowUUpRight" />}
/>
</>
);
}
8 changes: 7 additions & 1 deletion apps/docs/scripts/ui-components.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ export const COMPONENTS = [
],
},
{ slug: 'menu', name: 'Menu', propsType: 'MenuProps', dir: 'menu' },
{
slug: 'menu-trigger-button',
name: 'MenuTriggerButton',
propsType: 'MenuTriggerButtonProps',
dir: 'button/menu-trigger-button',
},
{ slug: 'modal', name: 'Modal', propsType: 'ModalProps', dir: 'modal' },
{
slug: 'nav-button',
name: 'NavButton',
propsType: ['NavLabelButtonProps', 'NavIconButtonProps', 'NavIconLabelButtonProps'],
propsType: ['NavLabelButtonProps', 'NavIconButtonProps'],
dir: 'button/nav-button',
},
{ slug: 'radio', name: 'Radio', propsType: 'RadioProps', dir: 'radio-button' },
Expand Down
27 changes: 27 additions & 0 deletions apps/docs/src/components/ui-examples/menu-trigger-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { DotsThreeVertical } from '@phosphor-icons/react';
import { Menu, MenuTriggerButton } from '@workflowbuilder/ui';
import { useState } from 'react';

import { ComponentPreview } from './component-preview';

export function MenuTriggerButtonExample() {
const [isOpen, setIsOpen] = useState(false);

return (
<ComponentPreview>
<Menu
open={isOpen}
onOpenChange={setIsOpen}
items={[
{ label: 'Edit', onClick: () => {} },
{ label: 'Duplicate', onClick: () => {} },
{ label: 'Delete', destructive: true, onClick: () => {} },
]}
>
<MenuTriggerButton isOpen={isOpen} aria-label="Open menu">
<DotsThreeVertical />
</MenuTriggerButton>
</Menu>
</ComponentPreview>
);
}
12 changes: 12 additions & 0 deletions apps/docs/src/components/ui-examples/nav-button.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.rows {
display: flex;
flex-direction: column;
gap: var(--wb-space-150);
}

.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--wb-space-100);
}
23 changes: 21 additions & 2 deletions apps/docs/src/components/ui-examples/nav-button.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import { NavButton } from '@workflowbuilder/ui';
import { ArrowRight, House, Plus } from '@phosphor-icons/react';
import { NAV_BUTTON_SIZES, NavButton } from '@workflowbuilder/ui';

import styles from './nav-button.module.css';

import { ComponentPreview } from './component-preview';

export function NavButtonExample() {
return (
<ComponentPreview>
<NavButton>Nav button</NavButton>
<div className={styles['rows']}>
<div className={styles['row']}>
<NavButton prefixIcon={<House />}>Square</NavButton>
<NavButton variant="round" prefixIcon={<House />}>
Round
</NavButton>
<NavButton aria-label="Plain" variant="plain" prefixIcon={<House />} />
<NavButton isSelected prefixIcon={<House />} suffixIcon={<ArrowRight />}>
Selected
</NavButton>
</div>
<div className={styles['row']}>
{NAV_BUTTON_SIZES.map((size) => (
<NavButton key={size} aria-label={`Add (${size})`} size={size} prefixIcon={<Plus />} />
))}
</div>
</div>
</ComponentPreview>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ live, interactive example plus the component's props and CSS variables.
- [DatePicker](/ui-library/ui-components/date-picker/) - Date selection with a calendar popover.
- [Input](/ui-library/ui-components/input/) - Text input with icons and explicit field states.
- [Menu](/ui-library/ui-components/menu/) - Popup menu for dropdowns.
- [MenuTriggerButton](/ui-library/ui-components/menu-trigger-button/) - Icon button for opening a menu.
- [Modal](/ui-library/ui-components/modal/) - Dialog overlay with a backdrop.
- [NavButton](/ui-library/ui-components/nav-button/) - Compact icon / label navigation button.
- [Radio](/ui-library/ui-components/radio/) - Radio button for a single choice.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: Menu Trigger Button
description: A compact icon button that reflects whether its menu is open.
---

import CssVariablesTable from '../../../../components/api/css-variables-table.astro';
import PropsTable from '../../../../components/api/props-table.astro';
import { MenuTriggerButtonExample } from '../../../../components/ui-examples/menu-trigger-button';

`MenuTriggerButton` is an icon-only NavButton composition for menu triggers.
Set `isOpen` from the menu state to render the persistent Pressed state.

<MenuTriggerButtonExample client:visible />

## Usage

```tsx
import { DotsThreeVertical } from '@phosphor-icons/react';
import { Menu, MenuTriggerButton } from '@workflowbuilder/ui';
import { useState } from 'react';

function Example() {
const [isOpen, setIsOpen] = useState(false);

return (
<Menu items={[{ label: 'Edit', onClick: () => {} }]} open={isOpen} onOpenChange={setIsOpen}>
<MenuTriggerButton isOpen={isOpen} aria-label="Open menu">
<DotsThreeVertical />
</MenuTriggerButton>
</Menu>
);
}
```

## Props

<PropsTable slug="menu-trigger-button" />

## CSS variables

It renders a `NavButton`, so its appearance is customized through the
[NavButton variables](/ui-library/ui-components/nav-button/#css-variables).

<CssVariablesTable slug="menu-trigger-button" />
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
---
title: Nav Button
description: A compact, type-safe navigation button with a selected state.
description: A compact navigation button with explicit content slots and a selected state.
---

import CssVariablesTable from '../../../../components/api/css-variables-table.astro';
import PropsTable from '../../../../components/api/props-table.astro';
import { NavButtonExample } from '../../../../components/ui-examples/nav-button';

`NavButton` is a compact, type-safe navigation button. Like `Button`, it
automatically selects the correct type (label, icon, or icon + label) based on
the structure of its `children`, and it adds an `isSelected` state for marking
the active item.
`NavButton` renders its label from `children` and accepts optional `prefixIcon`
and `suffixIcon` props. Omit `children` and provide `prefixIcon` for an icon-only
button. Use `variant` to select square or round styling; icon-only buttons also
support the no-background `plain` variant.

The `isSelected` prop renders the persistent Pressed state, including while the
button is hovered. The mouse-down `:active` state uses a separate Active token.

<NavButtonExample client:visible />

## Usage

```tsx
import { House } from '@phosphor-icons/react';
import { NavButton } from '@workflowbuilder/ui';

function Example() {
return <NavButton isSelected>Overview</NavButton>;
return (
<NavButton isSelected prefixIcon={<House />}>
Overview
</NavButton>
);
}
```

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DotsThreeVertical } from '@phosphor-icons/react';
import { Menu, type MenuItemProps, NavButton } from '@workflowbuilder/ui';
import { useMemo } from 'react';
import { Menu, type MenuItemProps, MenuTriggerButton } from '@workflowbuilder/ui';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';

import styles from '../../app-bar.module.css';
Expand All @@ -12,6 +12,7 @@ import { ToggleReadyOnlyMode } from '../toggle-read-only-mode/toggle-read-only-m

export function Controls() {
const { t } = useTranslation();
const [isMenuOpen, setIsMenuOpen] = useState(false);

// eslint-disable-next-line react-hooks/exhaustive-deps
const items: MenuItemProps[] = useMemo(() => getControlsDotsItems(), [t]);
Expand All @@ -24,10 +25,10 @@ export function Controls() {
</OptionalAppBarControls>
{items.length > 0 && (
<div className={styles['menu-container']}>
<Menu items={items}>
<NavButton tooltip={t('tooltips.menu')}>
<Menu items={items} open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<MenuTriggerButton aria-label={t('tooltips.menu')} isOpen={isMenuOpen} tooltip={t('tooltips.menu')}>
<DotsThreeVertical />
</NavButton>
</MenuTriggerButton>
</Menu>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { MenuItemProps } from '@workflowbuilder/ui';
import { describe, expect, it, vi } from 'vitest';

// Render the menu's `items` inline so we can assert on them without driving the
// real menu popover. The component also imports Input/NavButton, so the
// real menu popover. The component also imports Input/MenuTriggerButton, so the
// mock must expose them too.
vi.mock('@workflowbuilder/ui', () => ({
Menu: ({ items }: { items: MenuItemProps[] }) => (
Expand All @@ -15,7 +15,7 @@ vi.mock('@workflowbuilder/ui', () => ({
))}
</ul>
),
NavButton: () => null,
MenuTriggerButton: () => null,
Input: () => null,
}));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CaretDown } from '@phosphor-icons/react';
import { Input, Menu, NavButton } from '@workflowbuilder/ui';
import { Input, Menu, MenuTriggerButton } from '@workflowbuilder/ui';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';

Expand Down Expand Up @@ -39,6 +39,7 @@ function ProjectSelectionComponent({ onDuplicateClick }: ProjectSelectionProps)
const isReadOnlyMode = useStore((store) => store.isReadOnlyMode);
const setDocumentName = useStore((state) => state.setDocumentName);
const [editName, setEditName] = useState<boolean>(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);

const { t } = useTranslation();

Expand Down Expand Up @@ -88,10 +89,14 @@ function ProjectSelectionComponent({ onDuplicateClick }: ProjectSelectionProps)
</span>
)}
<div className={styles['menu-container']}>
<Menu items={items}>
<NavButton tooltip={t('tooltips.pickTheProject')}>
<Menu items={items} open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<MenuTriggerButton
aria-label={t('tooltips.pickTheProject')}
isOpen={isMenuOpen}
tooltip={t('tooltips.pickTheProject')}
>
<CaretDown />
</NavButton>
</MenuTriggerButton>
</Menu>
</div>
</div>
Expand Down

This file was deleted.

Loading