# Node

Use `ui-node` when you need one reusable row primitive rather than a full tree or nav container. It works well for sidebars, result rows, explorer items, and any compact structure where icon, label, selection, and row actions need to stay aligned.

## Import
```ts
import { NodeComponent, type Node } from 'ui';
```

## Basic rows
```ts
import { Component } from '@angular/core';
import { NodeComponent, type Node } from 'ui';

@Component({
  selector: 'app-node-basic-demo',
  standalone: true,
  imports: [NodeComponent],
  template: `
    <div
      style="display:flex;flex-direction:column;gap:0.75rem;width:100%;max-width:34rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
    >
      <ui-node [node]="folderNode" appearance="subtle" />
      <ui-node [node]="fileNode" appearance="subtle" />
      <ui-node
        [node]="selectedNode"
        appearance="filled"
        variant="secondary"
        [showSelectionIndicator]="true"
      />
    </div>
  `,
})
export class NodeBasicDemoComponent {
  protected readonly folderNode: Node = {
    id: 'folder',
    label: 'Design system',
    icon: 'folder',
  };

  protected readonly fileNode: Node = {
    id: 'file',
    label: 'Release-notes.md',
    icon: 'document',
  };

  protected readonly selectedNode: Node = {
    id: 'selected',
    label: 'Current sprint board',
    icon: 'apps',
    selected: true,
  };
}
```

## Appearance and semantic variant
```ts
import { Component } from '@angular/core';
import { NodeComponent, type Node } from 'ui';

@Component({
  selector: 'app-node-appearance-variant-demo',
  standalone: true,
  imports: [NodeComponent],
  template: `
    <div
      style="display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:1rem;width:100%;max-width:48rem"
    >
      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.875rem;font-weight:600">Subtle navigation rows</div>
        <ui-node [node]="primaryNode" variant="primary" appearance="subtle" />
        <ui-node [node]="secondaryNode" variant="secondary" appearance="subtle" />
      </div>

      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.875rem;font-weight:600">Filled status rows</div>
        <ui-node [node]="successNode" variant="success" appearance="filled" />
        <ui-node [node]="warningNode" variant="warning" appearance="filled" />
        <ui-node [node]="dangerNode" variant="danger" appearance="filled" />
      </div>
    </div>
  `,
})
export class NodeAppearanceVariantDemoComponent {
  protected readonly primaryNode: Node = {
    id: 'recent',
    label: 'Recent files',
    icon: 'clock',
  };

  protected readonly secondaryNode: Node = {
    id: 'shared',
    label: 'Shared with team',
    icon: 'people',
  };

  protected readonly successNode: Node = {
    id: 'synced',
    label: 'Marketing assets synced',
    icon: 'checkmark_circle',
  };

  protected readonly warningNode: Node = {
    id: 'review',
    label: 'Copy review pending',
    icon: 'warning',
  };

  protected readonly dangerNode: Node = {
    id: 'failed',
    label: 'Build failed on production',
    icon: 'error_circle',
  };
}
```

## Density and shape
```ts
import { Component } from '@angular/core';
import { NodeComponent, type Node } from 'ui';

@Component({
  selector: 'app-node-size-shape-demo',
  standalone: true,
  imports: [NodeComponent],
  template: `
    <div
      style="display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:1rem;width:100%;max-width:48rem"
    >
      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.875rem;font-weight:600">Density</div>
        <ui-node [node]="smallNode" size="small" appearance="outline" />
        <ui-node [node]="mediumNode" size="medium" appearance="outline" />
        <ui-node [node]="largeNode" size="large" appearance="outline" />
      </div>

      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.875rem;font-weight:600">Shape</div>
        <ui-node [node]="roundedNode" shape="rounded" appearance="subtle" />
        <ui-node [node]="squareNode" shape="square" appearance="subtle" />
        <ui-node [node]="circularNode" shape="circular" appearance="subtle" />
      </div>
    </div>
  `,
})
export class NodeSizeShapeDemoComponent {
  protected readonly smallNode: Node = { id: 'small', label: 'Compact row', icon: 'document' };
  protected readonly mediumNode: Node = { id: 'medium', label: 'Default row', icon: 'document' };
  protected readonly largeNode: Node = { id: 'large', label: 'Comfortable row', icon: 'document' };

  protected readonly roundedNode: Node = { id: 'rounded', label: 'Rounded item', icon: 'folder' };
  protected readonly squareNode: Node = { id: 'square', label: 'Square item', icon: 'folder' };
  protected readonly circularNode: Node = {
    id: 'circular',
    label: 'Circular item',
    icon: 'folder',
  };
}
```

## Selection and interaction behavior
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, NodeComponent, type Node } from 'ui';

@Component({
  selector: 'app-node-selection-behavior-demo',
  standalone: true,
  imports: [ButtonComponent, NodeComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:40rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Selected: <strong>{{ selectedLabel() || 'none' }}</strong>
        </span>
      </div>

      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-node
          [node]="inboxNode()"
          variant="primary"
          appearance="subtle"
          [showSelectionIndicator]="true"
          indicatorPosition="horizontal"
          [selectOnClick]="true"
          (nodeSelect)="select($event)"
        />
        <ui-node
          [node]="mentionsNode()"
          variant="secondary"
          appearance="filled"
          [showSelectionIndicator]="true"
          indicatorPosition="vertical"
          [asButton]="true"
          [selectOnClick]="true"
          (nodeSelect)="select($event)"
        />
        <ui-node
          [node]="archiveNode()"
          variant="secondary"
          appearance="outline"
          [selectOnClick]="false"
          [asButton]="true"
          (nodeClick)="clickOnly($event)"
        />
      </div>
    </div>
  `,
})
export class NodeSelectionBehaviorDemoComponent {
  protected readonly selectedId = signal<string | null>(null);
  protected readonly selectedLabel = signal('');

  protected readonly inboxNode = signal<Node>(this.buildNode('inbox', 'Inbox', 'mail'));
  protected readonly mentionsNode = signal<Node>(
    this.buildNode('mentions', 'Mentions', 'person_accounts'),
  );
  protected readonly archiveNode = signal<Node>(
    this.buildNode('archive', 'Archive only click action', 'archive'),
  );

  protected select(node: Node): void {
    this.selectedId.set(String(node.id));
    this.selectedLabel.set(node.label);
    this.syncNodes();
  }

  protected clickOnly(node: Node): void {
    this.selectedLabel.set(`${node.label} clicked`);
  }

  protected reset(): void {
    this.selectedId.set(null);
    this.selectedLabel.set('');
    this.syncNodes();
  }

  private syncNodes(): void {
    this.inboxNode.set(this.buildNode('inbox', 'Inbox', 'mail', this.selectedId() === 'inbox'));
    this.mentionsNode.set(
      this.buildNode('mentions', 'Mentions', 'person_accounts', this.selectedId() === 'mentions'),
    );
    this.archiveNode.set(
      this.buildNode(
        'archive',
        'Archive only click action',
        'archive',
        this.selectedId() === 'archive',
      ),
    );
  }

  private buildNode(id: string, label: string, icon: Node['icon'], selected = false): Node {
    return { id, label, icon, selected };
  }
}
```

## Custom content templates
```ts
import { Component } from '@angular/core';
import { BadgeComponent, NodeComponent, type Node } from 'ui';

@Component({
  selector: 'app-node-custom-content-demo',
  standalone: true,
  imports: [BadgeComponent, NodeComponent],
  template: `
    <div
      style="display:flex;flex-direction:column;gap:0.75rem;width:100%;max-width:42rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
    >
      <ui-node [node]="syncNode" appearance="subtle">
        <ng-template #content let-node>
          <div
            style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;width:100%;min-width:0"
          >
            <div style="display:flex;flex-direction:column;gap:0.1875rem;min-width:0">
              <div
                style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"
              >
                {{ node.label }}
              </div>
              <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
                Updated 2 minutes ago
              </div>
            </div>
            <ui-badge text="Synced" variant="success" appearance="subtle" size="small" />
          </div>
        </ng-template>
      </ui-node>

      <ui-node [node]="reviewNode" appearance="filled" variant="warning">
        <ng-template #content let-node>
          <div
            style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;width:100%;min-width:0"
          >
            <div style="display:flex;flex-direction:column;gap:0.1875rem;min-width:0">
              <div
                style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"
              >
                {{ node.label }}
              </div>
              <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
                3 unresolved comments
              </div>
            </div>
            <ui-badge text="Needs review" variant="warning" appearance="filled" size="small" />
          </div>
        </ng-template>
      </ui-node>
    </div>
  `,
})
export class NodeCustomContentDemoComponent {
  protected readonly syncNode: Node = {
    id: 'sync',
    label: 'Homepage.fig',
    icon: 'document',
  };

  protected readonly reviewNode: Node = {
    id: 'review',
    label: 'Q2 launch copy',
    icon: 'document_text',
  };
}
```

## Quick actions
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, MenuComponent, NodeComponent, type MenuItem, type Node } from 'ui';

@Component({
  selector: 'app-node-quick-actions-demo',
  standalone: true,
  imports: [ButtonComponent, MenuComponent, NodeComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:42rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Last action: <strong>{{ lastAction() || 'none' }}</strong>
        </span>
      </div>

      <div
        style="display:flex;flex-direction:column;gap:0.5rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        @for (node of nodes; track node.id) {
          <ui-node
            [node]="node"
            appearance="subtle"
            variant="secondary"
            [showQuickActions]="true"
            [quickActionsTemplate]="actionsMenu"
            (nodeClick)="lastAction.set(node.label)"
          />
        }
      </div>

      <ng-template #actionsMenu let-node>
        <ui-menu
          triggerVariant="dropdown"
          appearance="tint"
          [menuItems]="menuItems"
          [ariaLabel]="'Node actions'"
          (menuItemClick)="onMenuAction($event, node)"
        />
      </ng-template>
    </div>
  `,
})
export class NodeQuickActionsDemoComponent {
  protected readonly lastAction = signal('');

  protected readonly nodes: Node[] = [
    { id: 'brief', label: 'Creative brief', icon: 'document_text' },
    { id: 'assets', label: 'Campaign assets', icon: 'folder' },
    { id: 'launch', label: 'Launch checklist', icon: 'clipboard_task' },
  ];

  protected readonly menuItems: MenuItem[] = [
    { id: 'open', label: 'Open', icon: 'open' },
    { id: 'rename', label: 'Rename', icon: 'rename' },
    { id: 'archive', label: 'Archive', icon: 'archive' },
    { id: 'delete', label: 'Delete', icon: 'delete', variant: 'danger' },
  ];

  protected onMenuAction(item: MenuItem, node: Node): void {
    this.lastAction.set(`${item.label} on ${node.label}`);
  }

  protected reset(): void {
    this.lastAction.set('');
  }
}
```

## Drag source and drop target
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, NodeComponent, type Node } from 'ui';

@Component({
  selector: 'app-node-drag-drop-demo',
  standalone: true,
  imports: [ButtonComponent, NodeComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:40rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Status: <strong>{{ status() }}</strong>
        </span>
      </div>

      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-node
          [node]="sourceNode"
          appearance="outline"
          [draggable]="true"
          (dragStart)="status.set('Dragging ' + $event.node.label)"
          (dragEnd)="status.set('Drag ended')"
        />

        <ui-node
          [node]="targetNode"
          appearance="subtle"
          variant="secondary"
          [dropZone]="true"
          (dragOver)="status.set('Drop inside ' + $event.node.label)"
          (drop)="status.set('Dropped inside ' + $event.node.label)"
        />
      </div>
    </div>
  `,
})
export class NodeDragDropDemoComponent {
  protected readonly status = signal('Drag the source row into the target row');

  protected readonly sourceNode: Node = {
    id: 'source',
    label: 'Release tasks',
    icon: 're_order_dots_vertical',
  };

  protected readonly targetNode: Node = {
    id: 'target',
    label: 'Sprint planning board',
    icon: 'board',
  };

  protected reset(): void {
    this.status.set('Drag the source row into the target row');
  }
}
```

## Accessibility

### Role and row semantics
`ui-node` exposes `role="treeitem"` by default and switches to `role="button"` when `asButton` is enabled. Selected and disabled state are reflected through `aria-selected` and `aria-disabled` where applicable.

### Keyboard behavior
Keyboard support follows the rendered role.

| Key | Action |
| --- | --- |
| Tab / Shift+Tab | Moves focus to or from the row. |
| Enter / Space | Activates the row when focus is on the main node content. |
| Delete or Backspace | Not handled automatically for closable rows; wire your own removal shortcut if needed. |

### Custom content and row actions
If you replace the default content template, preserve a clear primary label. Quick-action menus and close buttons need their own accessible names and should not make the main row purpose ambiguous.
