# NodeToolbarComponent

Import it from `@grafloria/angular`.

NodeToolbar Component

A floating toolbar that attaches to nodes and provides contextual actions. The toolbar automatically positions itself relative to the node and updates
its position when the node moves, the canvas zooms/pans, or the window resizes.

Features:
- Smart positioning with boundary detection
- Keyboard navigation and accessibility (WCAG 2.1 Level AA)
- Custom styling with CSS variables
- Error boundaries for robust operation
- Configuration object pattern

```ts
@Component({
    selector: 'grafloria-node-toolbar',
    imports: [CommonModule],
    template: `
    <div
      #toolbar
      class="grafloria-node-toolbar"
      role="toolbar"
      [attr.aria-label]="effectiveConfig.ariaLabel || 'Node actions'"
      [attr.aria-hidden]="!isVisible"
      [class.visible]="isVisible"
      [class.animated]="effectiveConfig.animation?.enabled !== false"
      [attr.data-animation-preset]="effectiveConfig.animation?.preset || 'fade'"
      [style.transform]="transform"
      [style.opacity]="isVisible ? 1 : 0"
      [style.--toolbar-bg]="effectiveConfig.style?.backgroundColor"
      [style.--toolbar-border]="effectiveConfig.style?.borderColor"
      [style.--toolbar-radius]="effectiveConfig.style?.borderRadius"
      [style.--toolbar-shadow]="effectiveConfig.style?.boxShadow"
      [style.--toolbar-padding]="effectiveConfig.style?.padding"
      [style.--toolbar-z]="effectiveConfig.style?.zIndex"
      [style.--toolbar-transition]="effectiveConfig.animation?.duration || '0.2s'"
      [attr.data-position]="effectiveConfig.position"
      (keydown)="handleKeyDown($event)"
    >
      <!-- Default toolbar content (Phase 2: Supports groups) -->
      @if (!effectiveConfig.template) {
        <div class="toolbar-content" role="group">
          @for (group of effectiveGroups; track group.id; let groupIdx = $index) {
            <!-- Group label (optional) -->
            @if (group.label && useGroupedLayout) {
              <div class="toolbar-group-label">{{ group.label }}</div>
            }

            <!-- Before separator -->
            @if (groupIdx > 0 && (group.separator === 'before' || group.separator === 'both')) {
              <div class="toolbar-separator" role="separator"></div>
            }

            <!-- Group actions -->
            @for (action of group.actions; track action.id; let idx = $index) {
              <button
                #actionButton
                type="button"
                role="button"
                class="toolbar-button"
                [attr.aria-label]="action.tooltip || action.label"
                [attr.aria-disabled]="action.disabled"
                [attr.data-action-id]="action.id"
                [attr.data-group-id]="group.id"
                [attr.tabindex]="calculateTabIndex(groupIdx, idx)"
                [disabled]="action.disabled"
                [title]="action.tooltip || action.label"
                (click)="handleActionClick(action)"
                (focus)="onActionFocus(groupIdx, idx)"
              >
                @if (action.icon) {
                  <i [class]="action.icon" aria-hidden="true"></i>
                }
                <span>{{ action.label }}</span>
              </button>
            }

            <!-- After separator -->
            @if (groupIdx < effectiveGroups.length - 1 && (group.separator === 'after' || group.separator === 'both')) {
              <div class="toolbar-separator" role="separator"></div>
            }
          }
        </div>
      }

      <!-- Custom template content -->
      @if (effectiveConfig.template) {
        <ng-container *ngTemplateOutlet="effectiveConfig.template; context: { $implicit: node, actions: visibleActions, config: effectiveConfig }">
        </ng-container>
      }
    </div>
  `,
    styles: [`
    .grafloria-node-toolbar {
      position: absolute;
      background: var(--toolbar-bg, var(--grafloria-toolbar-bg, white));
      border: 1px solid var(--toolbar-border, var(--grafloria-toolbar-border, #e2e8f0));
      border-radius: var(--toolbar-radius, var(--grafloria-toolbar-radius, 8px));
      box-shadow: var(--toolbar-shadow, var(--grafloria-toolbar-shadow, 0 4px 6px rgba(0, 0, 0, 0.1)));
      padding: var(--toolbar-padding, var(--grafloria-toolbar-padding, 4px));
      display: flex;
      gap: 4px;
      z-index: var(--toolbar-z, var(--grafloria-toolbar-z, 1000));
      transition: opacity var(--toolbar-transition, 0.2s) ease;
      pointer-events: all;
  …
```

Use it as `<grafloria-node-toolbar>` in a template.

**Inputs**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `node?` | `NodeModel` |  |  |
| `engine?` | `DiagramEngine` |  |  |
| `canvasElement?` | `HTMLElement` |  |  |
| `viewport?` | `{ x: number; y: number; width: number; height: number }` |  |  |
| `zoom?` | `number` |  |  |
| `position?` | `ToolbarPosition` |  |  |
| `alignment?` | `ToolbarAlignment` |  |  |
| `offset?` | `number` |  |  |
| `actions?` | `ToolbarAction[]` |  |  |
| `customTemplate?` | `TemplateRef<any>` |  |  |
| `visible?` | `boolean` |  |  |
| `styleConfig?` | `ToolbarStyleConfig` |  |  |
| `enableAnimation?` | `boolean` |  |  |
| `autoHide?` | `boolean` |  |  |
| `config?` | `NodeToolbarConfig` |  |  |

**Outputs**

- `actionClicked`
- `visibilityChanged`
- `positionUpdated`

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `node` | `NodeModel` |  |  |
| `engine` | `DiagramEngine` |  |  |
| `canvasElement?` | `HTMLElement` |  |  |
| `viewport` | `{ x: number; y: number; width: number; height: number }` |  |  |
| `zoom` | `number` | `1.0` |  |
| `position` | `ToolbarPosition` | `'top'` |  |
| `alignment` | `ToolbarAlignment` | `'center'` |  |
| `offset` | `number` | `8` |  |
| `actions` | `ToolbarAction[]` | `[]` |  |
| `customTemplate?` | `TemplateRef<any>` |  |  |
| `visible` | `boolean` | `true` |  |
| `styleConfig?` | `ToolbarStyleConfig` |  |  |
| `enableAnimation` | `boolean` | `true` |  |
| `autoHide` | `boolean` | `false` |  |
| `config?` | `NodeToolbarConfig` |  |  |
| `actionClicked` |  |  |  |
| `visibilityChanged` |  |  |  |
| `positionUpdated` |  |  |  |
| `toolbarRef?` | `ElementRef<HTMLDivElement>` |  |  |
| `isVisible` |  | `false` |  |
| `transform` |  | `''` |  |
| `focusedActionIndex` |  | `0` |  |

**Methods**

- `constructor(private cdr: ChangeDetectorRef, private elementRef: ElementRef)`
- `get effectiveConfig(): EffectiveToolbarConfig` — Effective configuration merging individual inputs with config object
- `get visibleActions(): ToolbarAction[]` — Get visible actions based on visibility conditions
- `get effectiveGroups(): ToolbarActionGroup[]` — Get effective action groups
If actionGroups are provided, use them. Otherwise, convert flat actions into a single group.
- `get useGroupedLayout(): boolean` — Check if using grouped layout
- `ngOnInit()`
- `ngOnChanges(changes: SimpleChanges)`
- `ngOnDestroy()`
- `updatePosition()` — Update toolbar position with error handling
- `handleActionClick(action: ToolbarAction)` — Handle action click
- `handleKeyDown(event: KeyboardEvent)` — Handle keyboard navigation
- `onActionFocus(groupIdx: number, actionIdx: number)` — Handle action focus
- `calculateTabIndex(groupIdx: number, actionIdx: number): number` — Calculate tabindex for group-aware navigation
- `show()` — Show toolbar
- `hide()` — Hide toolbar
- `toggle()` — Toggle visibility

**Example**

```html
<grafloria-node-toolbar
  [node]="selectedNode"
  [engine]="diagramEngine"
  [config]="toolbarConfig">
</grafloria-node-toolbar>
```
