aboutsummaryrefslogtreecommitdiff
path: root/source/library/components/HistoryPanel.svelte
diff options
context:
space:
mode:
authornetop://ウィビ <paul@webb.page>2026-04-24 11:33:25 -0700
committernetop://ウィビ <paul@webb.page>2026-04-24 11:33:25 -0700
commit8a59f92d031963e23ecc84b75feecf43eb4dd146 (patch)
tree75de5768885583897061a3b1795e4c987ce90039 /source/library/components/HistoryPanel.svelte
downloadgraphiql-8a59f92d031963e23ecc84b75feecf43eb4dd146.tar.gz
graphiql-8a59f92d031963e23ecc84b75feecf43eb4dd146.zip
Initial commit: @eol/graphiql v0.3
Svelte 5 GraphiQL alternative for JSR. Covers: - HTTP fetcher with injectable fetch; SSE/WS stubs - Session store with tabs, auto-titling, persistence, rename - Operation detection via graphql parse(); Toolbar picker - CodeMirror 6 editor via cm6-graphql with theme prop - Light theme preset (hand-rolled EditorView.theme) - Doc explorer with breadcrumb nav and type guards - History panel with 100-entry cap, favorite pinning - Deno tests for operations, storage, and history eviction
Diffstat (limited to 'source/library/components/HistoryPanel.svelte')
-rw-r--r--source/library/components/HistoryPanel.svelte187
1 files changed, 187 insertions, 0 deletions
diff --git a/source/library/components/HistoryPanel.svelte b/source/library/components/HistoryPanel.svelte
new file mode 100644
index 0000000..b7f5c4c
--- /dev/null
+++ b/source/library/components/HistoryPanel.svelte
@@ -0,0 +1,187 @@
+<script lang="ts">
+ import type { HistoryEntry } from "../state/history.svelte.ts";
+
+ type Props = {
+ entries: HistoryEntry[];
+ onClear: () => void;
+ onFavorite: (id: string) => void;
+ onLoad: (id: string, inNewTab: boolean) => void;
+ onRemove: (id: string) => void;
+ };
+
+ let { entries, onClear, onFavorite, onLoad, onRemove }: Props = $props();
+
+ const sorted = $derived([...entries].sort((a, b) => {
+ if (a.favorite !== b.favorite)
+ return a.favorite ? -1 : 1;
+
+ return b.timestamp - a.timestamp;
+ }));
+
+ function formatTimestamp(ms: number): string {
+ const d = new Date(ms);
+ return d.toLocaleString();
+ }
+
+ function onEntryClick(event: MouseEvent, id: string) {
+ onLoad(id, event.shiftKey);
+ }
+
+ function onEntryKey(event: KeyboardEvent, id: string) {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ onLoad(id, event.shiftKey);
+ }
+ }
+</script>
+
+<style lang="scss">
+ .panel {
+ background: var(--graphiql-panel, #252526);
+ border-right: 1px solid var(--graphiql-border, #333);
+ display: grid;
+ grid-template-rows: auto 1fr;
+ height: 100%;
+ min-height: 0;
+ overflow: hidden;
+ }
+
+ .header {
+ align-items: center;
+ border-bottom: 1px solid var(--graphiql-border, #333);
+ display: flex;
+ justify-content: space-between;
+ padding: 0.5rem 0.75rem;
+ }
+
+ .title {
+ font-size: 0.8125rem;
+ font-weight: 600;
+ }
+
+ .clear {
+ background: none;
+ border: none;
+ color: var(--graphiql-muted, #858585);
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 0.75rem;
+ padding: 0;
+
+ &:hover {
+ color: var(--graphiql-fg, #d4d4d4);
+ }
+ }
+
+ .list {
+ display: grid;
+ gap: 0.125rem;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 0.375rem 0;
+ }
+
+ .entry {
+ align-items: flex-start;
+ cursor: pointer;
+ display: grid;
+ gap: 0.125rem;
+ grid-template-columns: auto 1fr auto;
+ padding: 0.375rem 0.75rem;
+
+ &:hover {
+ background: var(--graphiql-bg, #1e1e1e);
+ }
+ }
+
+ .star {
+ background: none;
+ border: none;
+ color: var(--graphiql-muted, #858585);
+ cursor: pointer;
+ font-size: 0.875rem;
+ padding: 0 0.375rem 0 0;
+
+ &.active {
+ color: var(--graphiql-accent, #e3b341);
+ }
+ }
+
+ .meta {
+ align-self: center;
+ min-width: 0;
+ }
+
+ .entry-title {
+ font-size: 0.8125rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .entry-time {
+ color: var(--graphiql-muted, #858585);
+ font-size: 0.7rem;
+ }
+
+ .remove {
+ align-self: center;
+ background: none;
+ border: none;
+ color: var(--graphiql-muted, #858585);
+ cursor: pointer;
+ font-size: 1rem;
+ line-height: 1;
+ padding: 0 0.25rem;
+
+ &:hover {
+ color: var(--graphiql-fg, #d4d4d4);
+ }
+ }
+
+ .empty {
+ color: var(--graphiql-muted, #858585);
+ font-size: 0.8125rem;
+ padding: 0.75rem;
+ }
+</style>
+
+<div class="panel">
+ <div class="header">
+ <span class="title">History</span>
+ {#if entries.length > 0}
+ <button class="clear" onclick={onClear} type="button">Clear</button>
+ {/if}
+ </div>
+ <div class="list">
+ {#if sorted.length === 0}
+ <div class="empty">No history yet.</div>
+ {:else}
+ {#each sorted as entry (entry.id)}
+ <div
+ aria-label="Load history entry"
+ class="entry"
+ onclick={(e) => onEntryClick(e, entry.id)}
+ onkeydown={(e) => onEntryKey(e, entry.id)}
+ role="button"
+ tabindex="0">
+ <button
+ aria-label={entry.favorite ? "Unfavorite" : "Favorite"}
+ class="star"
+ class:active={entry.favorite}
+ onclick={(e) => { e.stopPropagation(); onFavorite(entry.id); }}
+ type="button">{entry.favorite ? "★" : "☆"}</button>
+ <div class="meta">
+ <div class="entry-title">{entry.title}</div>
+ <div class="entry-time">{formatTimestamp(entry.timestamp)}</div>
+ </div>
+ <button
+ aria-label="Remove entry"
+ class="remove"
+ onclick={(e) => { e.stopPropagation(); onRemove(entry.id); }}
+ type="button">×</button>
+ </div>
+ {/each}
+ {/if}
+ </div>
+</div>