// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as i18n from '../../../core/i18n/i18n.js';
import * as Buttons from '../../../ui/components/buttons/buttons.js';
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
import * as IconButton from '../../../ui/components/icon_button/icon_button.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as LitHtml from '../../../ui/lit-html/lit-html.js';
import * as Menus from '../../../ui/components/menus/menus.js';

import type * as Converters from '../converters/converters.js';
import * as Models from '../models/models.js';

import stepViewStyles from './stepView.css.js';

import {type StepEditedEvent} from './StepEditor.js';

import {
  TimelineSection,
  type TimelineSectionData,
} from './TimelineSection.js';

const UIStrings = {
  /**
   *@description Title for the step type that configures the viewport
   */
  setViewportClickTitle: 'Set viewport',
  /**
   *@description Title for the customStep step type
   */
  customStepTitle: 'Custom step',
  /**
   *@description Title for the click step type
   */
  clickStepTitle: 'Click',
  /**
   *@description Title for the double click step type
   */
  doubleClickStepTitle: 'Double click',
  /**
   *@description Title for the hover step type
   */
  hoverStepTitle: 'Hover',
  /**
   *@description Title for the emulateNetworkConditions step type
   */
  emulateNetworkConditionsStepTitle: 'Emulate network conditions',
  /**
   *@description Title for the change step type
   */
  changeStepTitle: 'Change',
  /**
   *@description Title for the close step type
   */
  closeStepTitle: 'Close',
  /**
   *@description Title for the scroll step type
   */
  scrollStepTitle: 'Scroll',
  /**
   *@description Title for the key up step type. `up` refers to the state of the keyboard key: it's released, i.e., up. It does not refer to the down arrow key specifically.
   */
  keyUpStepTitle: 'Key up',
  /**
   *@description Title for the navigate step type
   */
  navigateStepTitle: 'Navigate',
  /**
   *@description Title for the key down step type. `down` refers to the state of the keyboard key: it's pressed, i.e., down. It does not refer to the down arrow key specifically.
   */
  keyDownStepTitle: 'Key down',
  /**
   *@description Title for the waitForElement step type
   */
  waitForElementStepTitle: 'Wait for element',
  /**
   *@description Title for the waitForExpression step type
   */
  waitForExpressionStepTitle: 'Wait for expression',
  /**
   *@description Title for elements with role button
   */
  elementRoleButton: 'Button',
  /**
   *@description Title for elements with role input
   */
  elementRoleInput: 'Input',
  /**
   *@description Default title for elements without a specific role
   */
  elementRoleFallback: 'Element',
  /**
   *@description The title of the button in the step's context menu that adds a new step before the current one.
   */
  addStepBefore: 'Add step before',
  /**
   *@description The title of the button in the step's context menu that adds a new step after the current one.
   */
  addStepAfter: 'Add step after',
  /**
   *@description The title of the button in the step's context menu that removes the step.
   */
  removeStep: 'Remove step',
  /**
   *@description The title of the button that open the step's context menu.
   */
  openStepActions: 'Open step actions',
  /**
   *@description The title of the button in the step's context menu that adds a breakpoint.
   */
  addBreakpoint: 'Add breakpoint',
  /**
   *@description The title of the button in the step's context menu that removes a breakpoint.
   */
  removeBreakpoint: 'Remove breakpoint',
  /**
   * @description A menu item item in the context menu that expands another menu which list all
   * the formats the user can copy the recording as.
   */
  copyAs: 'Copy as',
  /**
   * @description The title of the menu group that holds actions on recording steps.
   */
  stepManagement: 'Manage steps',
  /**
   * @description The title of the menu group that holds actions related to breakpoints.
   */
  breakpoints: 'Breakpoints',
};
const str_ = i18n.i18n.registerUIStrings(
    'panels/recorder/components/StepView.ts',
    UIStrings,
);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

declare global {
  interface HTMLElementTagNameMap {
    'devtools-step-view': StepView;
  }
}

export const enum State {
  Default = 'default',
  Success = 'success',
  Current = 'current',
  Outstanding = 'outstanding',
  Error = 'error',
  Stopped = 'stopped',
}

export interface StepViewData {
  state: State;
  step?: Models.Schema.Step;
  section?: Models.Section.Section;
  error?: Error;
  hasBreakpoint: boolean;
  isEndOfGroup: boolean;
  isStartOfGroup: boolean;
  isFirstSection: boolean;
  isLastSection: boolean;
  stepIndex: number;
  sectionIndex: number;
  isRecording: boolean;
  isPlaying: boolean;
  removable: boolean;
  builtInConverters: Converters.Converter.Converter[];
  extensionConverters: Converters.Converter.Converter[];
  isSelected: boolean;
  recorderSettings: Models.RecorderSettings.RecorderSettings;
}

export class CaptureSelectorsEvent extends Event {
  static readonly eventName = 'captureselectors';
  data: Models.Schema.StepWithSelectors&Partial<Models.Schema.ClickAttributes>;

  constructor(
      step: Models.Schema.StepWithSelectors&Partial<Models.Schema.ClickAttributes>,
  ) {
    super(CaptureSelectorsEvent.eventName, {bubbles: true, composed: true});
    this.data = step;
  }
}

export class StopSelectorsCaptureEvent extends Event {
  static readonly eventName = 'stopselectorscapture';
  constructor() {
    super(StopSelectorsCaptureEvent.eventName, {
      bubbles: true,
      composed: true,
    });
  }
}

export class CopyStepEvent extends Event {
  static readonly eventName = 'copystep';
  step: Models.Schema.Step;
  constructor(step: Models.Schema.Step) {
    super(CopyStepEvent.eventName, {bubbles: true, composed: true});
    this.step = step;
  }
}

export class StepChanged extends Event {
  static readonly eventName = 'stepchanged';
  currentStep: Models.Schema.Step;
  newStep: Models.Schema.Step;

  constructor(currentStep: Models.Schema.Step, newStep: Models.Schema.Step) {
    super(StepChanged.eventName, {bubbles: true, composed: true});
    this.currentStep = currentStep;
    this.newStep = newStep;
  }
}

export const enum AddStepPosition {
  BEFORE = 'before',
  AFTER = 'after',
}

export class AddStep extends Event {
  static readonly eventName = 'addstep';
  position: AddStepPosition;
  stepOrSection: Models.Schema.Step|Models.Section.Section;

  constructor(
      stepOrSection: Models.Schema.Step|Models.Section.Section,
      position: AddStepPosition,
  ) {
    super(AddStep.eventName, {bubbles: true, composed: true});
    this.stepOrSection = stepOrSection;
    this.position = position;
  }
}

export class RemoveStep extends Event {
  static readonly eventName = 'removestep';
  step: Models.Schema.Step;

  constructor(step: Models.Schema.Step) {
    super(RemoveStep.eventName, {bubbles: true, composed: true});
    this.step = step;
  }
}

export class AddBreakpointEvent extends Event {
  static readonly eventName = 'addbreakpoint';
  index: number;

  constructor(index: number) {
    super(AddBreakpointEvent.eventName, {bubbles: true, composed: true});
    this.index = index;
  }
}

export class RemoveBreakpointEvent extends Event {
  static readonly eventName = 'removebreakpoint';
  index: number;

  constructor(index: number) {
    super(RemoveBreakpointEvent.eventName, {bubbles: true, composed: true});
    this.index = index;
  }
}

const COPY_ACTION_PREFIX = 'copy-step-as-';

type Action = {
  id: string,
  label: string,
  group: string,
  groupTitle: string,
};

export class StepView extends HTMLElement {
  static readonly litTagName = LitHtml.literal`devtools-step-view`;

  readonly #shadow = this.attachShadow({mode: 'open'});

  #step?: Models.Schema.Step;
  #section?: Models.Section.Section;
  #state = State.Default;
  #error?: Error;
  #showDetails: boolean = false;
  #isEndOfGroup: boolean = false;
  #isStartOfGroup: boolean = false;
  #stepIndex = 0;
  #sectionIndex = 0;
  #isFirstSection: boolean = false;
  #isLastSection: boolean = false;
  #isRecording = false;
  #isPlaying = false;
  #actionsMenuButton?: Buttons.Button.Button;
  #actionsMenuExpanded = false;
  #isVisible = false;
  #observer: IntersectionObserver = new IntersectionObserver(result => {
    this.#isVisible = result[0].isIntersecting;
  });
  #hasBreakpoint = false;
  #removable = true;
  #builtInConverters?: Converters.Converter.Converter[];
  #extensionConverters?: Converters.Converter.Converter[];
  #isSelected = false;
  #recorderSettings?: Models.RecorderSettings.RecorderSettings;

  set data(data: StepViewData) {
    const prevState = this.#state;
    this.#step = data.step;
    this.#section = data.section;
    this.#state = data.state;
    this.#error = data.error;
    this.#isEndOfGroup = data.isEndOfGroup;
    this.#isStartOfGroup = data.isStartOfGroup;
    this.#stepIndex = data.stepIndex;
    this.#sectionIndex = data.sectionIndex;
    this.#isFirstSection = data.isFirstSection;
    this.#isLastSection = data.isLastSection;
    this.#isRecording = data.isRecording;
    this.#isPlaying = data.isPlaying;
    this.#hasBreakpoint = data.hasBreakpoint;
    this.#removable = data.removable;
    this.#builtInConverters = data.builtInConverters;
    this.#extensionConverters = data.extensionConverters;
    this.#isSelected = data.isSelected;
    this.#recorderSettings = data.recorderSettings;

    this.#render();

    if (this.#state !== prevState && this.#state === 'current' && !this.#isVisible) {
      this.scrollIntoView();
    }
  }

  get step(): Models.Schema.Step|undefined {
    return this.#step;
  }

  get section(): Models.Section.Section|undefined {
    return this.#section;
  }

  connectedCallback(): void {
    this.#shadow.adoptedStyleSheets = [stepViewStyles];
    this.#observer.observe(this);
    this.#render();
  }

  disconnectedCallback(): void {
    this.#observer.unobserve(this);
  }

  #toggleShowDetails(): void {
    this.#showDetails = !this.#showDetails;
    this.#render();
  }

  #onToggleShowDetailsKeydown(event: Event): void {
    const keyboardEvent = event as KeyboardEvent;
    if (keyboardEvent.key === 'Enter' || keyboardEvent.key === ' ') {
      this.#toggleShowDetails();
      event.stopPropagation();
      event.preventDefault();
    }
  }

  #getStepTypeTitle(): string|LitHtml.TemplateResult {
    if (this.#section) {
      return this.#section.title ? this.#section.title : LitHtml.html`<span class="fallback">(No Title)</span>`;
    }
    if (!this.#step) {
      throw new Error('Missing both step and section');
    }
    switch (this.#step.type) {
      case Models.Schema.StepType.CustomStep:
        return i18nString(UIStrings.customStepTitle);
      case Models.Schema.StepType.SetViewport:
        return i18nString(UIStrings.setViewportClickTitle);
      case Models.Schema.StepType.Click:
        return i18nString(UIStrings.clickStepTitle);
      case Models.Schema.StepType.DoubleClick:
        return i18nString(UIStrings.doubleClickStepTitle);
      case Models.Schema.StepType.Hover:
        return i18nString(UIStrings.hoverStepTitle);
      case Models.Schema.StepType.EmulateNetworkConditions:
        return i18nString(UIStrings.emulateNetworkConditionsStepTitle);
      case Models.Schema.StepType.Change:
        return i18nString(UIStrings.changeStepTitle);
      case Models.Schema.StepType.Close:
        return i18nString(UIStrings.closeStepTitle);
      case Models.Schema.StepType.Scroll:
        return i18nString(UIStrings.scrollStepTitle);
      case Models.Schema.StepType.KeyUp:
        return i18nString(UIStrings.keyUpStepTitle);
      case Models.Schema.StepType.KeyDown:
        return i18nString(UIStrings.keyDownStepTitle);
      case Models.Schema.StepType.WaitForElement:
        return i18nString(UIStrings.waitForElementStepTitle);
      case Models.Schema.StepType.WaitForExpression:
        return i18nString(UIStrings.waitForExpressionStepTitle);
      case Models.Schema.StepType.Navigate:
        return i18nString(UIStrings.navigateStepTitle);
    }
  }

  #getElementRoleTitle(role: string): string {
    switch (role) {
      case 'button':
        return i18nString(UIStrings.elementRoleButton);
      case 'input':
        return i18nString(UIStrings.elementRoleInput);
      default:
        return i18nString(UIStrings.elementRoleFallback);
    }
  }

  #getSelectorPreview(): string {
    if (!this.#step || !('selectors' in this.#step)) {
      return '';
    }

    const ariaSelector = this.#step.selectors.flat().find(selector => selector.startsWith('aria/'));

    if (!ariaSelector) {
      return '';
    }

    const m = ariaSelector.match(/^aria\/(.+?)(\[role="(.+)"\])?$/);
    if (!m) {
      return '';
    }

    return `${this.#getElementRoleTitle(m[3])} "${m[1]}"`;
  }

  #getSectionPreview(): string {
    if (!this.#section) {
      return '';
    }
    return this.#section.url;
  }

  #stepEdited(event: StepEditedEvent): void {
    const step = this.#step || this.#section?.causingStep;
    if (!step) {
      throw new Error('Expected step.');
    }
    this.dispatchEvent(new StepChanged(step, event.data));
  }

  #handleStepAction(event: Menus.Menu.MenuItemSelectedEvent): void {
    switch (event.itemValue) {
      case 'add-step-before': {
        const stepOrSection = this.#step || this.#section;
        if (!stepOrSection) {
          throw new Error('Expected step or section.');
        }
        this.dispatchEvent(new AddStep(stepOrSection, AddStepPosition.BEFORE));
        break;
      }
      case 'add-step-after': {
        const stepOrSection = this.#step || this.#section;
        if (!stepOrSection) {
          throw new Error('Expected step or section.');
        }
        this.dispatchEvent(new AddStep(stepOrSection, AddStepPosition.AFTER));
        break;
      }
      case 'remove-step': {
        const causingStep = this.#section?.causingStep;
        if (!this.#step && !causingStep) {
          throw new Error('Expected step.');
        }
        this.dispatchEvent(
            new RemoveStep(this.#step || (causingStep as Models.Schema.Step)),
        );
        break;
      }
      case 'add-breakpoint': {
        if (!this.#step) {
          throw new Error('Expected step');
        }
        this.dispatchEvent(new AddBreakpointEvent(this.#stepIndex));
        break;
      }
      case 'remove-breakpoint': {
        if (!this.#step) {
          throw new Error('Expected step');
        }
        this.dispatchEvent(new RemoveBreakpointEvent(this.#stepIndex));
        break;
      }
      default: {
        const actionId = event.itemValue as string;
        if (!actionId.startsWith(COPY_ACTION_PREFIX)) {
          throw new Error('Unknown step action.');
        }

        const copyStep = this.#step || this.#section?.causingStep;
        if (!copyStep) {
          throw new Error('Step not found.');
        }

        const converterId = actionId.substring(COPY_ACTION_PREFIX.length);
        if (this.#recorderSettings) {
          this.#recorderSettings.preferredCopyFormat = converterId;
        }

        this.dispatchEvent(new CopyStepEvent(structuredClone(copyStep)));
      }
    }
  }

  #onToggleActionsMenu(event: Event): void {
    event.stopPropagation();
    event.preventDefault();
    this.#actionsMenuExpanded = !this.#actionsMenuExpanded;
    this.#render();
  }

  #onCloseActionsMenu(): void {
    this.#actionsMenuExpanded = false;
    this.#render();
  }

  #onBreakpointClick(): void {
    if (this.#hasBreakpoint) {
      this.dispatchEvent(new RemoveBreakpointEvent(this.#stepIndex));
    } else {
      this.dispatchEvent(new AddBreakpointEvent(this.#stepIndex));
    }
    this.#render();
  }

  #getActionsMenuButton(): Buttons.Button.Button {
    if (!this.#actionsMenuButton) {
      throw new Error('Missing actionsMenuButton');
    }
    return this.#actionsMenuButton;
  }

  #getActions = (): Array<Action> => {
    const actions = [];

    if (!this.#isPlaying) {
      if (this.#step) {
        actions.push({
          id: 'add-step-before',
          label: i18nString(UIStrings.addStepBefore),
          group: 'stepManagement',
          groupTitle: i18nString(UIStrings.stepManagement),
        });
      }

      actions.push({
        id: 'add-step-after',
        label: i18nString(UIStrings.addStepAfter),
        group: 'stepManagement',
        groupTitle: i18nString(UIStrings.stepManagement),
      });

      if (this.#removable) {
        actions.push({
          id: 'remove-step',
          group: 'stepManagement',
          groupTitle: i18nString(UIStrings.stepManagement),
          label: i18nString(UIStrings.removeStep),
        });
      }
    }

    if (this.#step && !this.#isRecording) {
      if (this.#hasBreakpoint) {
        actions.push({
          id: 'remove-breakpoint',
          label: i18nString(UIStrings.removeBreakpoint),
          group: 'breakPointManagement',
          groupTitle: i18nString(UIStrings.breakpoints),
        });
      } else {
        actions.push({
          id: 'add-breakpoint',
          label: i18nString(UIStrings.addBreakpoint),
          group: 'breakPointManagement',
          groupTitle: i18nString(UIStrings.breakpoints),
        });
      }
    }

    if (this.#step) {
      for (const converter of this.#builtInConverters || []) {
        actions.push({
          id: COPY_ACTION_PREFIX + converter.getId(),
          label: converter.getFormatName(),
          group: 'copy',
          groupTitle: i18nString(UIStrings.copyAs),
        });
      }
      for (const converter of this.#extensionConverters || []) {
        actions.push({
          id: COPY_ACTION_PREFIX + converter.getId(),
          label: converter.getFormatName(),
          group: 'copy',
          groupTitle: i18nString(UIStrings.copyAs),
        });
      }
    }

    return actions;
  };

  #renderStepActions(): LitHtml.TemplateResult|null {
    const actions = this.#getActions();
    const groupsById = new Map<string, Action[]>();
    for (const action of actions) {
      const group = groupsById.get(action.group);
      if (!group) {
        groupsById.set(action.group, [action]);
      } else {
        group.push(action);
      }
    }
    const groups = [];
    for (const [group, actions] of groupsById) {
      groups.push({
        group,
        groupTitle: actions[0].groupTitle,
        actions,
      });
    }
    // clang-format off
    return LitHtml.html`
      <${Buttons.Button.Button.litTagName}
        class="step-actions"
        title=${i18nString(UIStrings.openStepActions)}
        aria-label=${i18nString(UIStrings.openStepActions)}
        @click=${this.#onToggleActionsMenu}
        @keydown=${(event: Event): void => {
          event.stopPropagation();
        }}
        on-render=${ComponentHelpers.Directives.nodeRenderedCallback(node => {
          this.#actionsMenuButton = node as Buttons.Button.Button;
        })}
        .data=${
          {
            variant: Buttons.Button.Variant.TOOLBAR,
            iconName: 'dots-vertical',
            title: i18nString(UIStrings.openStepActions),
          } as Buttons.Button.ButtonData
        }
      ></${Buttons.Button.Button.litTagName}>
      <${Menus.Menu.Menu.litTagName}
        @menucloserequest=${this.#onCloseActionsMenu}
        @menuitemselected=${this.#handleStepAction}
        .origin=${this.#getActionsMenuButton.bind(this)}
        .showSelectedItem=${false}
        .showConnector=${false}
        .open=${this.#actionsMenuExpanded}
      >
        ${LitHtml.Directives.repeat(
          groups,
          item => item.group,
          item => {
            return LitHtml.html`
            <${Menus.Menu.MenuGroup.litTagName}
              .name=${item.groupTitle}
            >
              ${LitHtml.Directives.repeat(
                item.actions,
                item => item.id,
                item => {
                  return LitHtml.html`<${Menus.Menu.MenuItem.litTagName}
                      .value=${item.id}
                    >
                      ${item.label}
                    </${Menus.Menu.MenuItem.litTagName}>
                  `;
                },
              )}
            </${Menus.Menu.MenuGroup.litTagName}>
          `;
          },
        )}
      </${Menus.Menu.Menu.litTagName}>
    `;
    // clang-format on
  }

  #onStepContextMenu = (event: MouseEvent): void => {
    if (event.button !== 2) {
      // 2 = secondary button = right click. We only show context menus if the
      // user has right clicked.
      return;
    }

    const menu = new UI.ContextMenu.ContextMenu(event);

    const actions = this.#getActions();
    const copyActions = actions.filter(
        item => item.id.startsWith(COPY_ACTION_PREFIX),
    );
    const otherActions = actions.filter(
        item => !item.id.startsWith(COPY_ACTION_PREFIX),
    );
    for (const item of otherActions) {
      const section = menu.section(item.group);
      section.appendItem(item.label, () => {
        this.#handleStepAction(
            new Menus.Menu.MenuItemSelectedEvent(item.id),
        );
      });
    }

    const preferredCopyAction = copyActions.find(
        item => item.id === COPY_ACTION_PREFIX + this.#recorderSettings?.preferredCopyFormat,
    );

    if (preferredCopyAction) {
      menu.section('copy').appendItem(preferredCopyAction.label, () => {
        this.#handleStepAction(
            new Menus.Menu.MenuItemSelectedEvent(preferredCopyAction.id),
        );
      });
    }

    if (copyActions.length) {
      const copyAs = menu.section('copy').appendSubMenuItem(i18nString(UIStrings.copyAs));
      for (const item of copyActions) {
        if (item === preferredCopyAction) {
          continue;
        }
        copyAs.section(item.group).appendItem(item.label, () => {
          this.#handleStepAction(
              new Menus.Menu.MenuItemSelectedEvent(item.id),
          );
        });
      }
    }

    void menu.show();
  };

  #render(): void {
    if (!this.#step && !this.#section) {
      return;
    }

    const stepClasses = {
      step: true,
      expanded: this.#showDetails,
      'is-success': this.#state === State.Success,
      'is-current': this.#state === State.Current,
      'is-outstanding': this.#state === State.Outstanding,
      'is-error': this.#state === State.Error,
      'is-stopped': this.#state === State.Stopped,
      'is-start-of-group': this.#isStartOfGroup,
      'is-first-section': this.#isFirstSection,
      'has-breakpoint': this.#hasBreakpoint,
    };
    const isExpandable = Boolean(this.#step);
    const mainTitle = this.#getStepTypeTitle();
    const subtitle = this.#step ? this.#getSelectorPreview() : this.#getSectionPreview();

    // clang-format off
    LitHtml.render(
      LitHtml.html`
      <${TimelineSection.litTagName} .data=${
        {
          isFirstSection: this.#isFirstSection,
          isLastSection: this.#isLastSection,
          isStartOfGroup: this.#isStartOfGroup,
          isEndOfGroup: this.#isEndOfGroup,
          isSelected: this.#isSelected,
        } as TimelineSectionData
      } @contextmenu=${this.#onStepContextMenu} data-step-index=${
        this.#stepIndex
      } data-section-index=${
        this.#sectionIndex
      } class=${LitHtml.Directives.classMap(stepClasses)}>
        <svg slot="icon" width="24" height="24" height="100%" class="icon">
          <circle class="circle-icon"/>
          <g class="error-icon">
            <path d="M1.5 1.5L6.5 6.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
            <path d="M1.5 6.5L6.5 1.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
          </g>
          <path @click=${this.#onBreakpointClick.bind(
            this,
          )} class="breakpoint-icon" d="M2.5 5.5H17.7098L21.4241 12L17.7098 18.5H2.5V5.5Z"/>
        </svg>
        <div class="summary">
          <div class="title-container ${isExpandable ? 'action' : ''}"
            @click=${isExpandable && this.#toggleShowDetails.bind(this)}
            @keydown=${
              isExpandable && this.#onToggleShowDetailsKeydown.bind(this)
            }
            tabindex="0"
            aria-role=${isExpandable ? 'button' : ''}
            aria-label=${isExpandable ? 'Show details for step' : ''}
          >
            ${
              isExpandable
                ? LitHtml.html`<${IconButton.Icon.Icon.litTagName}
                    class="chevron"
                    .data=${
                      {
                        iconName: 'triangle-down',
                        color: 'var(--color-text-primary)',
                      } as IconButton.Icon.IconData
                    }>
                  </${IconButton.Icon.Icon.litTagName}>`
                : ''
            }
            <div class="title">
              <div class="main-title" title=${mainTitle}>${mainTitle}</div>
              <div class="subtitle" title=${subtitle}>${subtitle}</div>
            </div>
          </div>
          <div class="filler"></div>
          ${this.#renderStepActions()}
        </div>
        <div class="details">
          ${
            this.#step &&
            LitHtml.html`<devtools-recorder-step-editor
            .step=${this.#step}
            .disabled=${this.#isPlaying}
            @stepedited=${this.#stepEdited}>
          </devtools-recorder-step-editor>`
          }
          ${
            this.#section?.causingStep &&
            LitHtml.html`<devtools-recorder-step-editor
            .step=${this.#section.causingStep}
            .isTypeEditable=${false}
            .disabled=${this.#isPlaying}
            @stepedited=${this.#stepEdited}>
          </devtools-recorder-step-editor>`
          }
        </div>
        ${
          this.#error &&
          LitHtml.html`
          <div class="error" role="alert">
            ${this.#error.message}
          </div>
        `
        }
      </${TimelineSection.litTagName}>
    `,
      this.#shadow,
      { host: this },
    );
    // clang-format on
  }
}

ComponentHelpers.CustomElements.defineComponent('devtools-step-view', StepView);
