From c8236628ac8a8102d11a32dd3d32ac98e4432183 Mon Sep 17 00:00:00 2001 From: Einar Date: Fri, 27 Feb 2026 10:50:59 +0100 Subject: [PATCH 1/2] Introducing toolbar --- .github/copilot-instructions.md | 24 +++ Documentation/Toolbar/index.md | 149 +++++++++++++++++++ Documentation/Toolbar/toc.yml | 2 + Documentation/toc.yml | 2 + Source/Common/Tooltip.css | 10 ++ Source/Common/Tooltip.tsx | 44 ++++++ Source/Common/index.ts | 1 + Source/Toolbar/Toolbar.css | 131 +++++++++++++++++ Source/Toolbar/Toolbar.stories.tsx | 210 +++++++++++++++++++++++++++ Source/Toolbar/Toolbar.tsx | 28 ++++ Source/Toolbar/ToolbarButton.tsx | 44 ++++++ Source/Toolbar/ToolbarContext.tsx | 20 +++ Source/Toolbar/ToolbarFanOutItem.tsx | 88 +++++++++++ Source/Toolbar/ToolbarSection.tsx | 92 ++++++++++++ Source/Toolbar/index.ts | 13 ++ Source/index.ts | 2 + Source/package.json | 5 + 17 files changed, 865 insertions(+) create mode 100644 Documentation/Toolbar/index.md create mode 100644 Documentation/Toolbar/toc.yml create mode 100644 Source/Common/Tooltip.css create mode 100644 Source/Common/Tooltip.tsx create mode 100644 Source/Toolbar/Toolbar.css create mode 100644 Source/Toolbar/Toolbar.stories.tsx create mode 100644 Source/Toolbar/Toolbar.tsx create mode 100644 Source/Toolbar/ToolbarButton.tsx create mode 100644 Source/Toolbar/ToolbarContext.tsx create mode 100644 Source/Toolbar/ToolbarFanOutItem.tsx create mode 100644 Source/Toolbar/ToolbarSection.tsx create mode 100644 Source/Toolbar/index.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 598a325..06eb9b1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -44,6 +44,30 @@ - ESLint errors about unused variables or improper types are blocking issues - Fixing them takes seconds now, but accumulates into hours if deferred +## Adding a New Component Module + +When adding a new top-level component folder (e.g. `Source/MyComponent/`), ALL of the following steps are mandatory: + +1. **Create `Source/MyComponent/index.ts`** — export all public types and components. +2. **Add a namespace import and re-export in `Source/index.ts`**: + ```typescript + import * as MyComponent from './MyComponent'; + // ... add to the export block + export { ..., MyComponent }; + ``` +3. **Add a subpath export entry in `Source/package.json`** under `"exports"`: + ```json + "./MyComponent": { + "types": "./dist/esm/MyComponent/index.d.ts", + "require": "./dist/cjs/MyComponent/index.js", + "import": "./dist/esm/MyComponent/index.js" + } + ``` +4. **Create `Documentation/MyComponent/index.md`** and `Documentation/MyComponent/toc.yml`. +5. **Register in `Documentation/toc.yml`** under the appropriate category. + +> The `package.json` entry is required for consumers to import directly via `@cratis/components/MyComponent`. + ## Formatting - Honor the existing code style and conventions in the project. diff --git a/Documentation/Toolbar/index.md b/Documentation/Toolbar/index.md new file mode 100644 index 0000000..e5b6005 --- /dev/null +++ b/Documentation/Toolbar/index.md @@ -0,0 +1,149 @@ +# Toolbar + +The `Toolbar` component provides a canvas-style icon toolbar with support for orientations, active states, animated context switching, 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 | +| `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/toc.yml b/Documentation/Toolbar/toc.yml new file mode 100644 index 0000000..1ba183c --- /dev/null +++ b/Documentation/Toolbar/toc.yml @@ -0,0 +1,2 @@ +- name: Overview + href: index.md diff --git a/Documentation/toc.yml b/Documentation/toc.yml index 44eaac0..58b61e5 100644 --- a/Documentation/toc.yml +++ b/Documentation/toc.yml @@ -24,6 +24,8 @@ href: Dialogs/toc.yml - name: Dropdown href: Dropdown/index.md + - name: Toolbar + href: Toolbar/toc.yml - name: Specialized Components items: - name: PivotViewer diff --git a/Source/Common/Tooltip.css b/Source/Common/Tooltip.css new file mode 100644 index 0000000..bfc9388 --- /dev/null +++ b/Source/Common/Tooltip.css @@ -0,0 +1,10 @@ +/* Copyright (c) Cratis. All rights reserved. */ +/* Licensed under the MIT license. See LICENSE file in the project root for full license information. */ + +/* ── Tooltip bubble ──────────────────────────────────────────────────────── */ +.tooltip-bubble { + background: var(--surface-100); + color: var(--text-color); + border: 1px solid var(--surface-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} diff --git a/Source/Common/Tooltip.tsx b/Source/Common/Tooltip.tsx new file mode 100644 index 0000000..3bfa617 --- /dev/null +++ b/Source/Common/Tooltip.tsx @@ -0,0 +1,44 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import React from 'react'; +import './Tooltip.css'; + +/** Position of the tooltip relative to its trigger element. */ +export type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; + +/** Props for the {@link Tooltip} component. */ +export interface TooltipProps { + /** The text to display inside the tooltip. */ + content: string; + /** Where the tooltip appears relative to the trigger (default: 'top'). */ + position?: TooltipPosition; + /** The element that triggers the tooltip on hover. */ + children: React.ReactNode; +} + +const POSITION_CLASSES: Record = { + right: 'left-full ml-2 top-1/2 -translate-y-1/2', + left: 'right-full mr-2 top-1/2 -translate-y-1/2', + top: 'bottom-full mb-2 left-1/2 -translate-x-1/2', + bottom: 'top-full mt-2 left-1/2 -translate-x-1/2', +}; + +/** + * A CSS-only hover tooltip wrapper. Wraps any child element and displays + * a styled floating label on hover without relying on native browser tooltips. + */ +export const Tooltip: React.FC = ({ content, position = 'top', children }) => ( +
+ {children} +
+ {content} +
+
+); diff --git a/Source/Common/index.ts b/Source/Common/index.ts index 4e6c08a..2c2cb22 100644 --- a/Source/Common/index.ts +++ b/Source/Common/index.ts @@ -4,3 +4,4 @@ export * from './ErrorBoundary'; export * from './Page'; export * from './FormElement'; +export * from './Tooltip'; diff --git a/Source/Toolbar/Toolbar.css b/Source/Toolbar/Toolbar.css new file mode 100644 index 0000000..499d154 --- /dev/null +++ b/Source/Toolbar/Toolbar.css @@ -0,0 +1,131 @@ +/* Copyright (c) Cratis. All rights reserved. */ +/* Licensed under the MIT license. See LICENSE file in the project root for full license information. */ + +/* ── Toolbar container ───────────────────────────────────────────────────── */ +.toolbar { + background: var(--surface-card); + border: 1px solid var(--surface-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +/* ── Toolbar button ──────────────────────────────────────────────────────── */ +.toolbar-button { + color: var(--text-color-secondary); + background: transparent; + border: none; + transition: background 0.15s, color 0.15s; +} + +.toolbar-button:hover { + background: var(--surface-100); + color: var(--text-color); +} + +.toolbar-button--active { + background: var(--primary-color); + color: var(--primary-color-text); +} + +.toolbar-button--active:hover { + background: var(--primary-color); + color: var(--primary-color-text); +} + +/* ── Toolbar project name label ──────────────────────────────────────────── */ +.toolbar-project-name { + padding: 0 8px; + font-size: 0.875rem; + font-weight: 500; + color: var(--text-color); + white-space: nowrap; +} + +/* ── Toolbar section (context switching) ────────────────────────────────── */ + +/* + * The section is a fixed-size container that transitions smoothly between + * the dimensions required by each context. + * overflow is intentionally left as visible so that absolutely-positioned + * children (e.g. fan-out panels) can extend beyond the section's bounds. + * Inactive contexts are hidden via opacity/pointer-events, not overflow clipping. + */ +.toolbar-section { + position: relative; + transition: + width 0.35s cubic-bezier(0.4, 0, 0.2, 1), + height 0.35s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* + * Every context is absolutely stacked at the top-left of the section. + * Only the active one is visible; the others fade out and lose pointer events. + */ +.toolbar-context { + position: absolute; + top: 0; + left: 0; + transition: opacity 0.2s ease; +} + +.toolbar-context--active { + opacity: 1; + pointer-events: auto; +} + +.toolbar-context--inactive { + opacity: 0; + pointer-events: none; +} + +/* ── Toolbar fan-out item ────────────────────────────────────────────────── */ + +/* + * The wrapper provides the positioning context for the absolutely-placed panel. + */ +.toolbar-fanout-item { + position: relative; +} + +/* + * The fan-out panel is a floating pill that mirrors the Toolbar container style. + * When collapsed, it's hidden. When expanded, it appears elevated at the same + * position as the trigger button, with the trigger button as the first item. + * Vertically centered with where the collapsed button would be. + */ +.toolbar-fanout-panel { + position: absolute; + top: 50%; + left: calc(-0.5rem - 1px); + transform: translateY(-50%); + display: inline-flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; + padding: 0.5rem; + background: var(--surface-card); + border: 1px solid var(--surface-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + border-radius: 1rem; + white-space: nowrap; + z-index: 10; + pointer-events: none; + opacity: 0; + clip-path: inset(0 100% 0 0 round 1rem); + transition: + clip-path 0.35s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.2s ease; +} + +/* Panel fans out to the left — reveal animates right → left */ +.toolbar-fanout-panel--left { + left: auto; + right: calc(-0.5rem - 1px); + clip-path: inset(0 0 0 100% round 1rem); +} + +/* Expanded state — fully visible, pointer events restored */ +.toolbar-fanout-panel--visible { + clip-path: inset(0 0 0 0 round 1rem); + opacity: 1; + pointer-events: auto; +} diff --git a/Source/Toolbar/Toolbar.stories.tsx b/Source/Toolbar/Toolbar.stories.tsx new file mode 100644 index 0000000..23005b7 --- /dev/null +++ b/Source/Toolbar/Toolbar.stories.tsx @@ -0,0 +1,210 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import type { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; +import { Toolbar } from './Toolbar'; +import { ToolbarButton } from './ToolbarButton'; +import { ToolbarContext } from './ToolbarContext'; +import { ToolbarFanOutItem } from './ToolbarFanOutItem'; +import { ToolbarSection } from './ToolbarSection'; + +const meta: Meta = { + title: 'Components/Toolbar', + component: Toolbar, + parameters: { + layout: 'centered', + }, +}; + +export default meta; + +type Story = StoryObj; + +/** A single toolbar group with several drawing-tool buttons. */ +export const Default: Story = { + render: () => ( + + + + + + + + ), +}; + +/** Two separate toolbar groups displayed side-by-side, mirroring the Miro-style layout. */ +export const MultipleGroups: Story = { + render: () => ( +
+ + + + + + + + + + + +
+ ), +}; + +/** Demonstrates the active (selected) state of a toolbar button. */ +export const WithActiveButton: Story = { + render: () => { + const ActiveDemo = () => { + const [active, setActive] = useState('select'); + + return ( + + setActive('select')} + /> + setActive('layers')} + /> + setActive('rectangle')} + /> + setActive('sticky')} + /> + + ); + }; + + return ; + }, +}; + +/** + * Demonstrates animated context switching within a single toolbar section. + * + * Click the buttons below the toolbar to switch between the "Drawing" and "Text" + * contexts. The section fades its current items out, morphs to the new size, then + * fades the new items in — while other sections (if present) remain unchanged. + */ +export const WithContexts: Story = { + render: () => { + const WithContextsDemo = () => { + const [currentContext, setCurrentContext] = useState('drawing'); + + return ( +
+ + + + + + + + + + + + + + + + + +
+ + +
+
+ ); + }; + + return ; + }, +}; + +/** + * Demonstrates a {@link ToolbarFanOutItem} inside a vertical toolbar. + * + * Click the "Shapes" button to expand the fan-out panel to the right. + * Click the button again or anywhere outside the panel to collapse it. + */ +export const WithFanOut: Story = { + render: () => { + const WithFanOutDemo = () => { + const [activeTool, setActiveTool] = useState('select'); + + return ( +
+ + setActiveTool('select')} + /> + + setActiveTool('shapes')} /> + setActiveTool('info')} /> + setActiveTool('preview')} /> + setActiveTool('settings')} /> + setActiveTool('open')} /> + + setActiveTool('rectangle')} + /> + setActiveTool('sticky')} + /> + + + + + +
+ ); + }; + + return ; + }, +}; diff --git a/Source/Toolbar/Toolbar.tsx b/Source/Toolbar/Toolbar.tsx new file mode 100644 index 0000000..a273bcf --- /dev/null +++ b/Source/Toolbar/Toolbar.tsx @@ -0,0 +1,28 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { ReactNode } from 'react'; +import './Toolbar.css'; + +/** Props for the {@link Toolbar} component. */ +export interface ToolbarProps { + /** The {@link ToolbarButton} elements to render inside this toolbar group. */ + children: ReactNode; + /** Layout direction of the toolbar (default: 'vertical'). */ + orientation?: 'vertical' | 'horizontal'; +} + +/** + * A toolbar container that groups icon buttons with a rounded border, + * mimicking the style of tools panels found in canvas-based applications. + * Supports both vertical (default) and horizontal orientations. + */ +export const Toolbar = ({ children, orientation = 'vertical' }: ToolbarProps) => ( +
+ {children} +
+); diff --git a/Source/Toolbar/ToolbarButton.tsx b/Source/Toolbar/ToolbarButton.tsx new file mode 100644 index 0000000..4d3ad71 --- /dev/null +++ b/Source/Toolbar/ToolbarButton.tsx @@ -0,0 +1,44 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { Tooltip } from '../Common/Tooltip'; +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; + + /** Tooltip text shown when the user hovers over the button. */ + tooltip: string; + + /** Whether the button is currently in the active/selected state. */ + active?: boolean; + + /** Callback invoked when the button is clicked. */ + onClick?: () => void; + + /** Position of the tooltip relative to the button (default: 'right'). */ + tooltipPosition?: TooltipPosition; +} + +/** + * 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) => { + const activeClass = active ? 'toolbar-button--active' : ''; + + return ( + + + + ); +}; diff --git a/Source/Toolbar/ToolbarContext.tsx b/Source/Toolbar/ToolbarContext.tsx new file mode 100644 index 0000000..8054172 --- /dev/null +++ b/Source/Toolbar/ToolbarContext.tsx @@ -0,0 +1,20 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import type { FC, ReactNode } from 'react'; + +/** Props for the {@link ToolbarContext} component. */ +export interface ToolbarContextProps { + /** Unique name identifying this context within a {@link ToolbarSection}. */ + name: string; + /** The toolbar items to render when this context is active. */ + children: ReactNode; +} + +/** + * Defines a named context (a set of toolbar items) within a {@link ToolbarSection}. + * The section renders only the active context at a time and animates between them. + * + * This is a data-only component; its rendering is fully managed by {@link ToolbarSection}. + */ +export const ToolbarContext: FC = () => null; diff --git a/Source/Toolbar/ToolbarFanOutItem.tsx b/Source/Toolbar/ToolbarFanOutItem.tsx new file mode 100644 index 0000000..5216008 --- /dev/null +++ b/Source/Toolbar/ToolbarFanOutItem.tsx @@ -0,0 +1,88 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { ReactNode, useEffect, useRef, useState } from 'react'; +import { Tooltip } from '../Common/Tooltip'; +import type { TooltipPosition } from '../Common/Tooltip'; + +/** Props for the {@link ToolbarFanOutItem} component. */ +export interface ToolbarFanOutItemProps { + /** The PrimeIcons CSS class to use as the trigger icon (e.g. 'pi pi-home'). */ + icon: string; + + /** Tooltip text shown when hovering over the trigger button. */ + tooltip: string; + + /** Position of the tooltip relative to the trigger button (default: 'right'). */ + tooltipPosition?: TooltipPosition; + + /** Direction the panel fans out from the trigger button (default: 'right'). */ + fanOutDirection?: 'right' | 'left'; + + /** The toolbar items to render inside the fan-out panel. */ + children: ReactNode; +} + +/** + * A toolbar button that fans out a horizontal panel of sub-tool buttons when clicked. + * + * Place this inside a vertical {@link Toolbar}. When the button is clicked, a pill-shaped + * panel slides out to the side (right by default) containing the provided children. + * The panel animates in/out using a clip-path reveal transition. + * + * - Clicking the button again closes the panel + * - Clicking anywhere outside the panel also closes it + */ +export const ToolbarFanOutItem = ({ + icon, + tooltip, + tooltipPosition = 'right', + fanOutDirection = 'right', + children, +}: ToolbarFanOutItemProps) => { + const [isExpanded, setIsExpanded] = useState(false); + const containerRef = useRef(null); + + const handleToggle = () => { + setIsExpanded(!isExpanded); + }; + + // Close the fan-out when clicking outside + useEffect(() => { + if (!isExpanded) return; + + const handleClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setIsExpanded(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isExpanded]); + + const activeClass = isExpanded ? 'toolbar-button--active' : ''; + const panelVisibleClass = isExpanded ? 'toolbar-fanout-panel--visible' : ''; + const directionClass = `toolbar-fanout-panel--${fanOutDirection}`; + + return ( +
+ + + +
+ {children} +
+
+ ); +}; diff --git a/Source/Toolbar/ToolbarSection.tsx b/Source/Toolbar/ToolbarSection.tsx new file mode 100644 index 0000000..ac97e97 --- /dev/null +++ b/Source/Toolbar/ToolbarSection.tsx @@ -0,0 +1,92 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { Children, ReactElement, ReactNode, isValidElement, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; + +import type { ToolbarContextProps } from './ToolbarContext'; + +/** Props for the {@link ToolbarSection} component. */ +export interface ToolbarSectionProps { + /** + * The name of the currently active context. + * Change this to trigger the fade-out / morph / fade-in animation. + * Defaults to the first context if not specified. + */ + activeContext?: string; + + /** {@link ToolbarContext} elements that define each context's toolbar items. */ + children: ReactNode; + + /** Layout direction matching the parent {@link Toolbar} (default: 'vertical'). */ + orientation?: 'vertical' | 'horizontal'; +} + +/** + * A section within a {@link Toolbar} that supports multiple named contexts. + * + * When {@link activeContext} changes: + * - The current items fade out. + * - The section smoothly morphs to the dimensions required by the new context. + * - The new items fade in. + * + * Contexts are defined by placing {@link ToolbarContext} children inside this section. + * Switching contexts only affects this section; other sections are unaffected. + */ +export const ToolbarSection = ({ activeContext, children, orientation = 'vertical' }: ToolbarSectionProps) => { + const contextRefs = useRef>({}); + const [size, setSize] = useState<{ width: number; height: number } | null>(null); + + const contexts = Children.toArray(children).filter( + (child): child is ReactElement => + isValidElement(child) && typeof (child.props as ToolbarContextProps).name === 'string' + ); + + // Default to the first context if activeContext is not provided + const effectiveContext = activeContext ?? (contexts[0]?.props as ToolbarContextProps)?.name ?? ''; + + const flexClass = orientation === 'horizontal' ? 'flex-row' : 'flex-col'; + + /** Measure the given context's natural dimensions and update the section size. */ + const measureAndSetSize = useCallback((contextName: string) => { + const ref = contextRefs.current[contextName]; + if (ref) { + setSize({ width: ref.offsetWidth, height: ref.offsetHeight }); + } + }, []); + + // Set the initial size synchronously before the first browser paint so there is no layout flash. + // Empty dependency array is intentional: ongoing context changes are handled by the useEffect below. + useLayoutEffect(() => { + measureAndSetSize(effectiveContext); + }, []); // run only on mount + + // After a context switch, let the browser paint the opacity transition first, + // then update the explicit size so the CSS width/height transition kicks in. + useEffect(() => { + measureAndSetSize(effectiveContext); + }, [effectiveContext, measureAndSetSize]); + + return ( +
+ {contexts.map((child) => { + const { name, children: contextChildren } = child.props as ToolbarContextProps; + const isActive = name === effectiveContext; + + return ( +
{ contextRefs.current[name] = element; }} + className={`toolbar-context inline-flex ${flexClass} items-center gap-1 ${ + isActive ? 'toolbar-context--active' : 'toolbar-context--inactive' + }`} + > + {contextChildren} +
+ ); + })} +
+ ); +}; diff --git a/Source/Toolbar/index.ts b/Source/Toolbar/index.ts new file mode 100644 index 0000000..9dee619 --- /dev/null +++ b/Source/Toolbar/index.ts @@ -0,0 +1,13 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +export { Toolbar } from './Toolbar'; +export type { ToolbarProps } from './Toolbar'; +export { ToolbarButton } from './ToolbarButton'; +export type { ToolbarButtonProps } from './ToolbarButton'; +export { ToolbarContext } from './ToolbarContext'; +export type { ToolbarContextProps } from './ToolbarContext'; +export { ToolbarSection } from './ToolbarSection'; +export type { ToolbarSectionProps } from './ToolbarSection'; +export { ToolbarFanOutItem } from './ToolbarFanOutItem'; +export type { ToolbarFanOutItemProps } from './ToolbarFanOutItem'; diff --git a/Source/index.ts b/Source/index.ts index 4dca6a9..b682e5c 100644 --- a/Source/index.ts +++ b/Source/index.ts @@ -13,6 +13,7 @@ import * as ObjectNavigationalBar from './ObjectNavigationalBar'; import * as PivotViewer from './PivotViewer'; import * as SchemaEditor from './SchemaEditor'; import * as TimeMachine from './TimeMachine'; +import * as Toolbar from './Toolbar'; export { CommandDialog, @@ -27,5 +28,6 @@ export { PivotViewer, SchemaEditor, TimeMachine, + Toolbar, }; diff --git a/Source/package.json b/Source/package.json index af639ba..88648f9 100644 --- a/Source/package.json +++ b/Source/package.json @@ -78,6 +78,11 @@ "types": "./dist/esm/TimeMachine/index.d.ts", "require": "./dist/cjs/TimeMachine/index.js", "import": "./dist/esm/TimeMachine/index.js" + }, + "./Toolbar": { + "types": "./dist/esm/Toolbar/index.d.ts", + "require": "./dist/cjs/Toolbar/index.js", + "import": "./dist/esm/Toolbar/index.js" } }, "scripts": { From 4c945e47d403d53f367bd7f8a0f3b9fdf4df5b6f Mon Sep 17 00:00:00 2001 From: Einar Date: Fri, 27 Feb 2026 10:52:30 +0100 Subject: [PATCH 2/2] Adding dependabot and pull request template --- .github/dependabot.yml | 14 ++++++++++++++ .github/pull_request_template.md | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..55184fd --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: +- package-ecosystem: nuget + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 25 + labels: [] +- package-ecosystem: npm + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 25 + labels: [] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a048e9f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,33 @@ +## Summary + +Optional summary of the PR here. The GitHub release description is created from this comment so keep it nice and descriptive. +Remember to remove sections that you don't need or use. + +If it does not make sense to have a summary, you can take that out as well. +You would typically keep the summary only if there is a general theme change that can be summarized. If you find +yourself saying the same thing as any of the bullet points in a slightly different way; then this section is not +needed. + +### Added + +- Describe the added features + +### Changed + +- Describe the outwards facing code change + +### Fixed + +- Describe the fix and the bug + +### Removed + +- Describe what was removed and why + +### Security + +- Describe the security issue and the fix + +### Deprecated + +- Describe the part of the code being deprecated and why