| // sketch-diff-file-picker.ts |
| // Component for selecting files from a diff |
| |
| import { css, html, LitElement } from "lit"; |
| import { customElement, property, state } from "lit/decorators.js"; |
| import { GitDiffFile } from "./git-data-service"; |
| |
| /** |
| * Component for selecting files from a diff with next/previous navigation |
| */ |
| @customElement("sketch-diff-file-picker") |
| export class SketchDiffFilePicker extends LitElement { |
| @property({ type: Array }) |
| files: GitDiffFile[] = []; |
| |
| @property({ type: String }) |
| selectedPath: string = ""; |
| |
| @state() |
| private selectedIndex: number = -1; |
| |
| static styles = css` |
| :host { |
| display: block; |
| width: 100%; |
| font-family: var(--font-family, system-ui, sans-serif); |
| } |
| |
| .file-picker { |
| display: flex; |
| gap: 8px; |
| align-items: center; |
| background-color: var(--background-light, #f8f8f8); |
| border-radius: 4px; |
| border: 1px solid var(--border-color, #e0e0e0); |
| padding: 8px 12px; |
| width: 100%; |
| box-sizing: border-box; |
| } |
| |
| .file-select { |
| flex: 1; |
| min-width: 200px; |
| max-width: calc( |
| 100% - 230px |
| ); /* Leave space for the navigation buttons and file info */ |
| overflow: hidden; |
| } |
| |
| select { |
| width: 100%; |
| max-width: 100%; |
| padding: 8px 12px; |
| border-radius: 4px; |
| border: 1px solid var(--border-color, #e0e0e0); |
| background-color: white; |
| font-size: 14px; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| |
| .navigation-buttons { |
| display: flex; |
| gap: 8px; |
| } |
| |
| button { |
| padding: 8px 12px; |
| background-color: var(--button-bg, #4a7dfc); |
| color: var(--button-text, white); |
| border: none; |
| border-radius: 4px; |
| cursor: pointer; |
| font-size: 14px; |
| transition: background-color 0.2s; |
| } |
| |
| button:hover { |
| background-color: var(--button-hover, #3a6eee); |
| } |
| |
| button:disabled { |
| background-color: var(--button-disabled, #cccccc); |
| cursor: not-allowed; |
| } |
| |
| .file-info { |
| font-size: 14px; |
| color: var(--text-muted, #666); |
| margin-left: 8px; |
| white-space: nowrap; |
| } |
| |
| .no-files { |
| color: var(--text-muted, #666); |
| font-style: italic; |
| } |
| |
| @media (max-width: 768px) { |
| .file-picker { |
| flex-direction: column; |
| align-items: stretch; |
| } |
| |
| .file-select { |
| max-width: 100%; /* Full width on small screens */ |
| margin-bottom: 8px; |
| } |
| |
| .navigation-buttons { |
| width: 100%; |
| justify-content: space-between; |
| } |
| |
| .file-info { |
| margin-left: 0; |
| margin-top: 8px; |
| text-align: center; |
| } |
| } |
| `; |
| |
| updated(changedProperties: Map<string, any>) { |
| // If files changed, reset the selection |
| if (changedProperties.has("files")) { |
| this.updateSelectedIndex(); |
| } |
| |
| // If selectedPath changed externally, update the index |
| if (changedProperties.has("selectedPath")) { |
| this.updateSelectedIndex(); |
| } |
| } |
| |
| connectedCallback() { |
| super.connectedCallback(); |
| // Initialize the selection when the component is connected, but only if files exist |
| if (this.files && this.files.length > 0) { |
| this.updateSelectedIndex(); |
| |
| // Explicitly trigger file selection event for the first file when there's only one file |
| // This ensures the diff view is updated even when navigation buttons aren't clicked |
| if (this.files.length === 1) { |
| this.selectFileByIndex(0); |
| } |
| } |
| } |
| |
| render() { |
| if (!this.files || this.files.length === 0) { |
| return html`<div class="no-files">No files to display</div>`; |
| } |
| |
| return html` |
| <div class="file-picker"> |
| <div class="file-select"> |
| <select @change=${this.handleSelect}> |
| ${this.files.map( |
| (file, index) => html` |
| <option |
| value=${index} |
| ?selected=${index === this.selectedIndex} |
| > |
| ${this.formatFileOption(file)} |
| </option> |
| `, |
| )} |
| </select> |
| </div> |
| |
| <div class="navigation-buttons"> |
| <button |
| @click=${this.handlePrevious} |
| ?disabled=${this.selectedIndex <= 0} |
| > |
| Previous |
| </button> |
| <button |
| @click=${this.handleNext} |
| ?disabled=${this.selectedIndex >= this.files.length - 1} |
| > |
| Next |
| </button> |
| </div> |
| |
| ${this.selectedIndex >= 0 ? this.renderFileInfo() : ""} |
| </div> |
| `; |
| } |
| |
| renderFileInfo() { |
| const file = this.files[this.selectedIndex]; |
| return html` |
| <div class="file-info"> |
| ${this.getFileStatusName(file.status)} | ${this.selectedIndex + 1} of |
| ${this.files.length} |
| </div> |
| `; |
| } |
| |
| /** |
| * Format a file for display in the dropdown |
| */ |
| formatFileOption(file: GitDiffFile): string { |
| const statusSymbol = this.getFileStatusSymbol(file.status); |
| return `${statusSymbol} ${file.path}`; |
| } |
| |
| /** |
| * Get a short symbol for the file status |
| */ |
| getFileStatusSymbol(status: string): string { |
| switch (status.toUpperCase()) { |
| case "A": |
| return "+"; |
| case "M": |
| return "M"; |
| case "D": |
| return "-"; |
| case "R": |
| return "R"; |
| default: |
| return "?"; |
| } |
| } |
| |
| /** |
| * Get a descriptive name for the file status |
| */ |
| getFileStatusName(status: string): string { |
| switch (status.toUpperCase()) { |
| case "A": |
| return "Added"; |
| case "M": |
| return "Modified"; |
| case "D": |
| return "Deleted"; |
| case "R": |
| return "Renamed"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| /** |
| * Handle file selection from dropdown |
| */ |
| handleSelect(event: Event) { |
| const select = event.target as HTMLSelectElement; |
| const index = parseInt(select.value, 10); |
| this.selectFileByIndex(index); |
| } |
| |
| /** |
| * Handle previous button click |
| */ |
| handlePrevious() { |
| if (this.selectedIndex > 0) { |
| this.selectFileByIndex(this.selectedIndex - 1); |
| } |
| } |
| |
| /** |
| * Handle next button click |
| */ |
| handleNext() { |
| if (this.selectedIndex < this.files.length - 1) { |
| this.selectFileByIndex(this.selectedIndex + 1); |
| } |
| } |
| |
| /** |
| * Select a file by index and dispatch event |
| */ |
| selectFileByIndex(index: number) { |
| if (index >= 0 && index < this.files.length) { |
| this.selectedIndex = index; |
| this.selectedPath = this.files[index].path; |
| |
| const event = new CustomEvent("file-selected", { |
| detail: { file: this.files[index] }, |
| bubbles: true, |
| composed: true, |
| }); |
| |
| this.dispatchEvent(event); |
| } |
| } |
| |
| /** |
| * Update the selected index based on the selectedPath |
| */ |
| private updateSelectedIndex() { |
| // Add defensive check for files array |
| if (!this.files || this.files.length === 0) { |
| this.selectedIndex = -1; |
| return; |
| } |
| |
| if (this.selectedPath) { |
| // Find the file with the matching path |
| const index = this.files.findIndex( |
| (file) => file.path === this.selectedPath, |
| ); |
| if (index >= 0) { |
| this.selectedIndex = index; |
| return; |
| } |
| } |
| |
| // Default to first file if no match or no path |
| this.selectedIndex = 0; |
| const newSelectedPath = this.files[0].path; |
| |
| // Only dispatch event if the path has actually changed and files exist |
| if ( |
| this.selectedPath !== newSelectedPath && |
| this.files && |
| this.files.length > 0 |
| ) { |
| this.selectedPath = newSelectedPath; |
| |
| // Dispatch the event directly - we've already checked the files array |
| const event = new CustomEvent("file-selected", { |
| detail: { file: this.files[0] }, |
| bubbles: true, |
| composed: true, |
| }); |
| |
| this.dispatchEvent(event); |
| } |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| "sketch-diff-file-picker": SketchDiffFilePicker; |
| } |
| } |