diff options
| author | netop://ウィビ <paul@webb.page> | 2026-04-24 11:33:25 -0700 |
|---|---|---|
| committer | netop://ウィビ <paul@webb.page> | 2026-04-24 11:33:25 -0700 |
| commit | 8a59f92d031963e23ecc84b75feecf43eb4dd146 (patch) | |
| tree | 75de5768885583897061a3b1795e4c987ce90039 /source/library/components/HistoryPanel.svelte | |
| download | graphiql-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.svelte | 187 |
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> |