add new style and global search
This commit is contained in:
@@ -74,15 +74,15 @@
|
||||
</script>
|
||||
|
||||
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Selected file history">
|
||||
<div class="section-head">
|
||||
<div class="min-w-0 flex-1 overflow-hidden">
|
||||
<div class="section-head file-history-head">
|
||||
<div class="file-history-heading">
|
||||
<span class="eyebrow">{selectedExplorerLabel}</span>
|
||||
<h2
|
||||
class="mt-0.5 text-ink text-base font-bold leading-tight truncate"
|
||||
class="file-history-name"
|
||||
use:pathTooltip={selectedExplorerPath}
|
||||
>{selectedExplorerPath ? fileName(selectedExplorerPath) : "No file"}</h2>
|
||||
</div>
|
||||
<span class="pill pill-count flex-shrink-0">{fileHistory.length}</span>
|
||||
<span class="pill pill-count file-history-count">{fileHistory.length}</span>
|
||||
</div>
|
||||
|
||||
{#if !hasRepository}
|
||||
@@ -94,16 +94,16 @@
|
||||
{:else}
|
||||
<div class="history-list overflow-auto p-2">
|
||||
{#each fileHistory as item (item.hash)}
|
||||
<article class="commit-row compact">
|
||||
<article class="commit-row compact file-history-row">
|
||||
<div class="commit-line">
|
||||
<History size={16} aria-hidden="true" />
|
||||
<div>
|
||||
<div class="commit-line-text">
|
||||
<strong title={item.summary}>{item.summary}</strong>
|
||||
<span>{item.short_hash} - {item.author_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="commit-actions">
|
||||
<div class="commit-actions file-history-actions">
|
||||
<time datetime={item.date}>{formatCommitDate(item.date)}</time>
|
||||
<div class="commit-action-buttons">
|
||||
<button class="btn-sm" type="button" onclick={() => onDiff(item)} disabled={isBusy} title="Show changes vs working tree">
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<script lang="ts">
|
||||
import { CalendarDays, FileCode, LoaderCircle, Search, User, X } from "@lucide/svelte";
|
||||
import type { GitSearchHit } from "../types";
|
||||
|
||||
interface Props {
|
||||
hasRepository: boolean;
|
||||
isBusy: boolean;
|
||||
isSearching: boolean;
|
||||
error: string;
|
||||
results: GitSearchHit[];
|
||||
onClose: () => void;
|
||||
onSearch: (query: string, caseSensitive: boolean, limit: number) => void | Promise<void>;
|
||||
onCancel: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
let {
|
||||
hasRepository = false,
|
||||
isBusy = false,
|
||||
isSearching = false,
|
||||
error = "",
|
||||
results = [],
|
||||
onClose = () => {},
|
||||
onSearch = () => {},
|
||||
onCancel = () => {},
|
||||
}: Props = $props();
|
||||
|
||||
let query = $state("");
|
||||
let caseSensitive = $state(false);
|
||||
let limit = $state(250);
|
||||
let searchedQuery = $state("");
|
||||
let searched = $state(false);
|
||||
|
||||
function submit(event?: SubmitEvent) {
|
||||
event?.preventDefault();
|
||||
const value = query.trim();
|
||||
if (!value || !hasRepository || isBusy || isSearching) return;
|
||||
searched = true;
|
||||
searchedQuery = value;
|
||||
void onSearch(value, caseSensitive, limit);
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
||||
submit();
|
||||
}
|
||||
}
|
||||
|
||||
function formatCommitDate(value: string): string {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date);
|
||||
}
|
||||
|
||||
function displayPath(hit: GitSearchHit): string {
|
||||
return hit.old_file ? `${hit.old_file} -> ${hit.file}` : hit.file;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="dialog-backdrop"
|
||||
role="presentation"
|
||||
onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||
>
|
||||
<div class="dialog global-search-dialog" role="dialog" aria-modal="true" aria-label="Global code search" tabindex="-1">
|
||||
<header class="dialog-header">
|
||||
<div>
|
||||
<span class="eyebrow">Global search</span>
|
||||
<h2 class="dialog-title">Find where code was introduced</h2>
|
||||
</div>
|
||||
<button class="dialog-close" type="button" onclick={onClose} title="Close">
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="global-search-body">
|
||||
<form class="global-search-form" onsubmit={submit}>
|
||||
<label class="global-search-query">
|
||||
<span>String or function</span>
|
||||
<textarea
|
||||
bind:value={query}
|
||||
onkeydown={handleKeydown}
|
||||
disabled={!hasRepository || isBusy || isSearching}
|
||||
spellcheck="false"
|
||||
placeholder={"Paste a string, symbol, or full function body..."}
|
||||
></textarea>
|
||||
</label>
|
||||
|
||||
<div class="global-search-options">
|
||||
<label class="check-row">
|
||||
<input type="checkbox" bind:checked={caseSensitive} disabled={!hasRepository || isBusy || isSearching} />
|
||||
<span>Exact case</span>
|
||||
</label>
|
||||
|
||||
<label class="search-limit">
|
||||
<span>Results</span>
|
||||
<select bind:value={limit} disabled={!hasRepository || isBusy || isSearching}>
|
||||
<option value={100}>100</option>
|
||||
<option value={250}>250</option>
|
||||
<option value={500}>500</option>
|
||||
<option value={1000}>1000</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button class="btn-primary" type="submit" disabled={!hasRepository || isBusy || isSearching || query.trim().length === 0}>
|
||||
{#if isSearching}
|
||||
<LoaderCircle class="spin" size={16} aria-hidden="true" />
|
||||
{:else}
|
||||
<Search size={16} aria-hidden="true" />
|
||||
{/if}
|
||||
Search
|
||||
</button>
|
||||
|
||||
{#if isSearching}
|
||||
<button class="btn-secondary search-cancel" type="button" onclick={onCancel}>
|
||||
<X size={16} aria-hidden="true" />
|
||||
Cancel
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section class="global-search-results" aria-live="polite">
|
||||
{#if !hasRepository}
|
||||
<div class="blank-state">Open a repository first.</div>
|
||||
{:else if isSearching}
|
||||
<div class="blank-state">
|
||||
<LoaderCircle class="spin" size={20} aria-hidden="true" />
|
||||
Searching all branches...
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="blank-state search-error">{error}</div>
|
||||
{:else if !searched}
|
||||
<div class="blank-state">Search a string or paste a complete function to find where it was added.</div>
|
||||
{:else if results.length === 0}
|
||||
<div class="blank-state">No introduction found for "{searchedQuery}".</div>
|
||||
{:else}
|
||||
<div class="search-result-head">
|
||||
<strong>{results.length}</strong>
|
||||
<span>{results.length === 1 ? "introduction" : "introductions"} found for "{searchedQuery}"</span>
|
||||
</div>
|
||||
|
||||
<div class="search-hit-list">
|
||||
{#each results as hit (`${hit.commit_hash}:${hit.file}:${hit.line_number ?? 0}`)}
|
||||
<article class="search-hit">
|
||||
<header class="search-hit-top">
|
||||
<span class="hash">{hit.short_hash}</span>
|
||||
<strong title={hit.summary}>{hit.summary || "No commit message"}</strong>
|
||||
{#if hit.matches_added > 1}
|
||||
<span class="pill pill-active">+{hit.matches_added} matches</span>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="search-hit-meta">
|
||||
<span><User size={12} aria-hidden="true" />{hit.author_name || "Unknown author"}</span>
|
||||
<span><CalendarDays size={12} aria-hidden="true" />{formatCommitDate(hit.date)}</span>
|
||||
</div>
|
||||
|
||||
<div class="search-hit-file" title={displayPath(hit)}>
|
||||
<FileCode size={14} aria-hidden="true" />
|
||||
<span>{displayPath(hit)}</span>
|
||||
{#if hit.line_number}
|
||||
<strong>:{hit.line_number}</strong>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<pre class="search-hit-line">{hit.line || searchedQuery}</pre>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user