diff --git a/Documentation/Toolbar/active-state.md b/Documentation/Toolbar/active-state.md new file mode 100644 index 0000000..cc73c4e --- /dev/null +++ b/Documentation/Toolbar/active-state.md @@ -0,0 +1,26 @@ +# Active State + +Use the `active` prop on `ToolbarButton` to highlight the selected tool: + +```tsx +function DrawingToolbar() { + const [activeTool, setActiveTool] = useState('select'); + + return ( + + setActiveTool('select')} + /> + setActiveTool('draw')} + /> + + ); +} +``` diff --git a/Documentation/Toolbar/basic-usage.md b/Documentation/Toolbar/basic-usage.md new file mode 100644 index 0000000..5e73370 --- /dev/null +++ b/Documentation/Toolbar/basic-usage.md @@ -0,0 +1,37 @@ +# Basic Usage + +Place `ToolbarButton` elements inside a `Toolbar`: + +```tsx +import { Toolbar, ToolbarButton } from '@cratis/components'; + +function MyToolbar() { + return ( + + + + + + ); +} +``` + +`ToolbarButton` supports either an icon, text, or both. For text-first controls such as zoom indicators, provide the `text` prop: + +```tsx +import { Toolbar, ToolbarButton, ToolbarSeparator } from '@cratis/components'; + +function ZoomToolbar() { + const [zoom, setZoom] = useState(120); + + return ( + + setZoom(value => value - 10)} /> + setZoom(100)} /> + setZoom(value => value + 10)} /> + + + + ); +} +``` diff --git a/Documentation/Toolbar/context-switching.md b/Documentation/Toolbar/context-switching.md new file mode 100644 index 0000000..5bea0a2 --- /dev/null +++ b/Documentation/Toolbar/context-switching.md @@ -0,0 +1,29 @@ +# Context Switching + +`ToolbarSection` and `ToolbarContext` enable smooth animated transitions between different sets of tools. When `activeContext` changes, the current buttons fade out, the section morphs to the new size, then the new buttons fade in. + +```tsx +function ContextualToolbar() { + const [mode, setMode] = useState('drawing'); + + return ( + + + + + + + + + + + + + + + + ); +} +``` + +Only the section transitions — buttons outside the section are unaffected. diff --git a/Documentation/Toolbar/fan-out.md b/Documentation/Toolbar/fan-out.md new file mode 100644 index 0000000..2673107 --- /dev/null +++ b/Documentation/Toolbar/fan-out.md @@ -0,0 +1,22 @@ +# Fan-Out Sub-Panel + +`ToolbarFanOutItem` replaces a regular button with one that slides out a horizontal panel of additional tools when clicked. The panel closes when clicking the button again or anywhere outside it. + +```tsx + + + + + + + + +``` + +By default the panel fans out to the right. Use `fanOutDirection='left'` when the toolbar is positioned on the right side of the screen: + +```tsx + + ... + +``` diff --git a/Documentation/Toolbar/index.md b/Documentation/Toolbar/index.md index e5b6005..068ead3 100644 --- a/Documentation/Toolbar/index.md +++ b/Documentation/Toolbar/index.md @@ -1,149 +1,15 @@ # Toolbar -The `Toolbar` component provides a canvas-style icon toolbar with support for orientations, active states, animated context switching, and fan-out sub-panels. +The `Toolbar` component provides a canvas-style icon toolbar with support for orientations, active states, animated context switching, separators, and fan-out sub-panels. ## Components | Component | Description | |---|---| | `Toolbar` | Container that groups toolbar buttons into a pill-shaped bar | -| `ToolbarButton` | Icon button with a hover tooltip | +| `ToolbarButton` | Button with optional icon and optional text, including hover tooltip | +| `ToolbarSeparator` | Visual divider that separates groups of buttons | | `ToolbarSection` | Section within a toolbar that animates between named contexts | | `ToolbarContext` | Named context (set of buttons) inside a `ToolbarSection` | | `ToolbarFanOutItem` | Button that slides out a horizontal sub-panel on click | -## Basic Usage - -Place `ToolbarButton` elements inside a `Toolbar`: - -```tsx -import { Toolbar, ToolbarButton } from '@cratis/components'; - -function MyToolbar() { - return ( - - - - - - ); -} -``` - -## Orientation - -The toolbar defaults to `vertical`. Pass `orientation='horizontal'` for a horizontal layout: - -```tsx - - - - -``` - -## Active State - -Use the `active` prop on `ToolbarButton` to highlight the selected tool: - -```tsx -function DrawingToolbar() { - const [activeTool, setActiveTool] = useState('select'); - - return ( - - setActiveTool('select')} - /> - setActiveTool('draw')} - /> - - ); -} -``` - -## Context Switching - -`ToolbarSection` and `ToolbarContext` enable smooth animated transitions between different sets of tools. When `activeContext` changes, the current buttons fade out, the section morphs to the new size, then the new buttons fade in. - -```tsx -function ContextualToolbar() { - const [mode, setMode] = useState('drawing'); - - return ( - - - - - - - - - - - - - - - - ); -} -``` - -Only the section transitions — buttons outside the section are unaffected. - -## Fan-Out Sub-Panel - -`ToolbarFanOutItem` replaces a regular button with one that slides out a horizontal panel of additional tools when clicked. The panel closes when clicking the button again or anywhere outside it. - -```tsx - - - - - - - - -``` - -By default the panel fans out to the right. Use `fanOutDirection='left'` when the toolbar is positioned on the right side of the screen: - -```tsx - - ... - -``` - -## Multiple Toolbar Groups - -Render multiple `Toolbar` instances to create separate groups, matching the style of canvas-based tools panels: - -```tsx -
- - - - - - - - -
-``` - -## Tooltip Position - -Both `ToolbarButton` and `ToolbarFanOutItem` default to showing tooltips on the `right`. Use `tooltipPosition` to override: - -```tsx - -``` - -Valid values are `'top'`, `'right'`, `'bottom'`, and `'left'`. diff --git a/Documentation/Toolbar/multiple-groups.md b/Documentation/Toolbar/multiple-groups.md new file mode 100644 index 0000000..2d91e7f --- /dev/null +++ b/Documentation/Toolbar/multiple-groups.md @@ -0,0 +1,16 @@ +# Multiple Toolbar Groups + +Render multiple `Toolbar` instances to create separate groups, matching the style of canvas-based tools panels: + +```tsx +
+ + + + + + + + +
+``` diff --git a/Documentation/Toolbar/orientation.md b/Documentation/Toolbar/orientation.md new file mode 100644 index 0000000..38dbc17 --- /dev/null +++ b/Documentation/Toolbar/orientation.md @@ -0,0 +1,10 @@ +# Orientation + +The toolbar defaults to `vertical`. Pass `orientation='horizontal'` for a horizontal layout: + +```tsx + + + + +``` diff --git a/Documentation/Toolbar/sections.md b/Documentation/Toolbar/sections.md new file mode 100644 index 0000000..1e47eb9 --- /dev/null +++ b/Documentation/Toolbar/sections.md @@ -0,0 +1,34 @@ +# Separators + +`ToolbarSeparator` renders a thin visual divider between groups of buttons. Unlike `ToolbarSection`, it has no behavioral logic — it simply draws a line perpendicular to the toolbar orientation. In a horizontal toolbar the separator is a vertical rule; in a vertical toolbar it is a horizontal rule. + +```tsx +import { Toolbar, ToolbarButton, ToolbarSeparator } from '@cratis/components'; + +function ZoomToolbar() { + return ( + + + + + + + + + ); +} +``` + +Pass the same `orientation` value to `ToolbarSeparator` as you pass to the enclosing `Toolbar` so the line is drawn perpendicular to the toolbar direction. + +## Vertical toolbar + +In a vertical toolbar (the default) the separator is a horizontal rule: + +```tsx + + + + + +``` diff --git a/Documentation/Toolbar/toc.yml b/Documentation/Toolbar/toc.yml index 1ba183c..09e367b 100644 --- a/Documentation/Toolbar/toc.yml +++ b/Documentation/Toolbar/toc.yml @@ -1,2 +1,18 @@ - name: Overview href: index.md +- name: Basic Usage + href: basic-usage.md +- name: Orientation + href: orientation.md +- name: Active State + href: active-state.md +- name: Separators + href: sections.md +- name: Context Switching + href: context-switching.md +- name: Fan-Out Sub-Panel + href: fan-out.md +- name: Multiple Groups + href: multiple-groups.md +- name: Tooltip Position + href: tooltip-position.md diff --git a/Documentation/Toolbar/tooltip-position.md b/Documentation/Toolbar/tooltip-position.md new file mode 100644 index 0000000..c18ff8e --- /dev/null +++ b/Documentation/Toolbar/tooltip-position.md @@ -0,0 +1,9 @@ +# Tooltip Position + +Both `ToolbarButton` and `ToolbarFanOutItem` default to showing tooltips on the `right`. Use `tooltipPosition` to override: + +```tsx + +``` + +Valid values are `'top'`, `'right'`, `'bottom'`, and `'left'`. diff --git a/Source/Toolbar/Toolbar.css b/Source/Toolbar/Toolbar.css index 499d154..13063c1 100644 --- a/Source/Toolbar/Toolbar.css +++ b/Source/Toolbar/Toolbar.css @@ -31,6 +31,13 @@ color: var(--primary-color-text); } +.toolbar-button__text { + font-size: 0.875rem; + font-weight: 600; + line-height: 1; + white-space: nowrap; +} + /* ── Toolbar project name label ──────────────────────────────────────────── */ .toolbar-project-name { padding: 0 8px; @@ -77,6 +84,26 @@ pointer-events: none; } +/* ── Toolbar separator ───────────────────────────────────────────────────── */ + +.toolbar-separator { + background: var(--surface-border); + flex-shrink: 0; + align-self: stretch; +} + +/* Separator inside a vertical toolbar — renders as a horizontal rule */ +.toolbar-separator--in-vertical { + height: 1px; + margin: 0.125rem 0.25rem; +} + +/* Separator inside a horizontal toolbar — renders as a vertical rule */ +.toolbar-separator--in-horizontal { + width: 1px; + margin: 0.25rem 0.125rem; +} + /* ── Toolbar fan-out item ────────────────────────────────────────────────── */ /* diff --git a/Source/Toolbar/Toolbar.stories.tsx b/Source/Toolbar/Toolbar.stories.tsx index 23005b7..5a7c5cc 100644 --- a/Source/Toolbar/Toolbar.stories.tsx +++ b/Source/Toolbar/Toolbar.stories.tsx @@ -8,6 +8,7 @@ import { ToolbarButton } from './ToolbarButton'; import { ToolbarContext } from './ToolbarContext'; import { ToolbarFanOutItem } from './ToolbarFanOutItem'; import { ToolbarSection } from './ToolbarSection'; +import { ToolbarSeparator } from './ToolbarSeparator'; const meta: Meta = { title: 'Components/Toolbar', @@ -154,6 +155,56 @@ export const WithContexts: Story = { }, }; +/** + * Demonstrates {@link ToolbarSeparator} in a horizontal toolbar. + * + * The separator renders as a thin vertical line between groups of buttons, + * matching the style seen in canvas-based tools (e.g. Miro, Figma). + * When the toolbar is vertical the line is horizontal. + */ +export const WithSeparators: Story = { + render: () => ( + + + + + + + + + ), +}; + +/** + * Demonstrates a zoom-style horizontal toolbar where the center text button + * resets the zoom level to 100% when clicked. + */ +export const ZoomBar: Story = { + render: () => { + const ZoomBarDemo = () => { + const [zoom, setZoom] = useState(120); + + const zoomOut = () => setZoom(current => Math.max(50, current - 10)); + const zoomIn = () => setZoom(current => Math.min(300, current + 10)); + const resetZoom = () => setZoom(100); + + return ( + + + + + + + + + + ); + }; + + return ; + }, +}; + /** * Demonstrates a {@link ToolbarFanOutItem} inside a vertical toolbar. * diff --git a/Source/Toolbar/ToolbarButton.tsx b/Source/Toolbar/ToolbarButton.tsx index 4d3ad71..5ee5bbf 100644 --- a/Source/Toolbar/ToolbarButton.tsx +++ b/Source/Toolbar/ToolbarButton.tsx @@ -7,7 +7,10 @@ import type { TooltipPosition } from '../Common/Tooltip'; /** Props for the {@link ToolbarButton} component. */ export interface ToolbarButtonProps { /** The PrimeIcons CSS class to use as the icon (e.g. 'pi pi-home'). */ - icon: string; + icon?: string; + + /** Optional text to render inside the button (e.g. '120%'). */ + text?: string; /** Tooltip text shown when the user hovers over the button. */ tooltip: string; @@ -26,8 +29,16 @@ export interface ToolbarButtonProps { * An icon button with a tooltip, intended to be placed inside a {@link Toolbar}. * Uses the shared {@link Tooltip} component for consistent hover labels. */ -export const ToolbarButton = ({ icon, tooltip, active = false, onClick, tooltipPosition = 'right' }: ToolbarButtonProps) => { +export const ToolbarButton = ({ icon, text, tooltip, active = false, onClick, tooltipPosition = 'right' }: ToolbarButtonProps) => { const activeClass = active ? 'toolbar-button--active' : ''; + const hasText = typeof text === 'string' && text.length > 0; + const hasIcon = typeof icon === 'string' && icon.length > 0; + const buttonContent = hasText + ? {text} + : hasIcon + ? + : null; + const buttonSizeClass = hasText ? 'h-10 px-3 min-w-[4rem]' : 'w-10 h-10'; return ( @@ -35,9 +46,9 @@ export const ToolbarButton = ({ icon, tooltip, active = false, onClick, tooltipP type='button' aria-label={tooltip} onClick={onClick} - className={`toolbar-button w-10 h-10 flex items-center justify-center rounded-lg cursor-pointer ${activeClass}`} + className={`toolbar-button ${buttonSizeClass} flex items-center justify-center rounded-lg cursor-pointer ${activeClass}`} > - + {buttonContent} ); diff --git a/Source/Toolbar/ToolbarSeparator.tsx b/Source/Toolbar/ToolbarSeparator.tsx new file mode 100644 index 0000000..6f78aa6 --- /dev/null +++ b/Source/Toolbar/ToolbarSeparator.tsx @@ -0,0 +1,22 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/** Props for the {@link ToolbarSeparator} component. */ +export interface ToolbarSeparatorProps { + /** Layout direction matching the parent {@link Toolbar} (default: 'vertical'). */ + orientation?: 'vertical' | 'horizontal'; +} + +/** + * A visual divider for use inside a {@link Toolbar}. + * + * In a vertical toolbar (default) the separator renders as a horizontal rule. + * In a horizontal toolbar it renders as a vertical rule. + */ +export const ToolbarSeparator = ({ orientation = 'vertical' }: ToolbarSeparatorProps) => ( +
+); diff --git a/Source/Toolbar/index.ts b/Source/Toolbar/index.ts index 9dee619..cdcece9 100644 --- a/Source/Toolbar/index.ts +++ b/Source/Toolbar/index.ts @@ -9,5 +9,7 @@ export { ToolbarContext } from './ToolbarContext'; export type { ToolbarContextProps } from './ToolbarContext'; export { ToolbarSection } from './ToolbarSection'; export type { ToolbarSectionProps } from './ToolbarSection'; +export { ToolbarSeparator } from './ToolbarSeparator'; +export type { ToolbarSeparatorProps } from './ToolbarSeparator'; export { ToolbarFanOutItem } from './ToolbarFanOutItem'; export type { ToolbarFanOutItemProps } from './ToolbarFanOutItem';