Something went wrong. Try again.
[PRE-ALPHA] an open source web-based 2D/3D graphing calculator cartesium.org
Something went wrong. Try again.
Svelte
at mathquill
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212<script lang="ts"> import type { CartesiumDocumentV1, StoredDocument } from '#lib/document/cartesium-document.js';
let { documents, activeDocumentId, onclose, onopen, onrename, onduplicate, ondelete, onexport }: { documents: StoredDocument<CartesiumDocumentV1>[]; activeDocumentId: string | null; onclose: () => void; onopen: (id: string) => void; onrename: (id: string) => void; onduplicate: (id: string) => void; ondelete: (id: string) => void; onexport: (id: string) => void; } = $props();
function formatDate(value: string): string { const date = new Date(value);
return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date); }</script>
<svelte:window onkeydown={(event) => event.key === 'Escape' && onclose()} />
<section class="library" aria-label="My 3D graphs"> <header> <div> <p class="eyebrow">Cartesium</p> <h2>My 3D graphs</h2> </div> <button type="button" class="close" onclick={onclose} aria-label="Close graph library">×</button> </header>
<div class="content"> <div class="section-heading"> <div> <h3>Drafts in this browser</h3> <p>Private, device-local copies. They are not synced to an AT Protocol account.</p> </div> </div> {#if documents.length === 0} <div class="empty"> <h3>No saved 3D drafts yet</h3> <p>Use Save or Save as in the expression panel to keep a 3D graph in this browser.</p> </div> {:else} <div class="cards"> {#each documents as document (document.id)} <article class:current={document.id === activeDocumentId}> <div class="card-main"> <div> <h3>{document.document.snapshot.name || 'Untitled graph'}</h3> <p> Updated {formatDate(document.updatedAt)} {#if document.id === activeDocumentId}<strong>Open now</strong>{/if} </p> </div> <button type="button" class="primary" onclick={() => onopen(document.id)}>Open</button> </div> <div class="actions"> <button type="button" onclick={() => onrename(document.id)}>Rename</button> <button type="button" onclick={() => onduplicate(document.id)}>Duplicate</button> <button type="button" onclick={() => onexport(document.id)}>Export JSON</button> <button type="button" class="danger" onclick={() => ondelete(document.id)}>Delete</button> </div> </article> {/each} </div> {/if} </div></section>
<style> .library { position: absolute; inset: 0; z-index: 40; display: grid; grid-template-rows: auto minmax(0, 1fr); background: #f8fafc; color: #0f172a; }
header { display: flex; align-items: center; justify-content: space-between; padding: 18px clamp(18px, 4vw, 52px) 14px; border-bottom: 1px solid #cbd5e1; background: #0f172a; color: white; }
.eyebrow { margin: 0 0 2px; color: #93c5fd; font-size: 11px; font-weight: 750; letter-spacing: 0.12em; text-transform: uppercase; }
h2 { margin: 0; font-size: 24px; }
.close { display: grid; width: 40px; height: 40px; place-items: center; padding: 0; border: 1px solid #475569; border-radius: 8px; background: #1e293b; color: white; font-size: 27px; line-height: 1; cursor: pointer; }
.content { overflow-y: auto; padding: 24px clamp(18px, 4vw, 52px) 48px; }
.section-heading { display: flex; max-width: 1100px; justify-content: space-between; gap: 20px; margin: 0 auto 18px; }
.section-heading h3, .section-heading p { margin: 0; } .section-heading p { margin-top: 5px; color: #64748b; font-size: 13px; }
.cards { display: grid; max-width: 1100px; gap: 12px; margin: 0 auto; }
article { padding: 16px; border: 1px solid #cbd5e1; border-radius: 10px; background: white; box-shadow: 0 2px 7px rgb(15 23 42 / 6%); }
article.current { border-color: #60a5fa; box-shadow: 0 0 0 2px rgb(147 197 253 / 35%); }
.card-main { display: flex; align-items: center; justify-content: space-between; gap: 18px; } .card-main h3, .card-main p { margin: 0; } .card-main h3 { font-size: 16px; overflow-wrap: anywhere; } .card-main p { margin-top: 5px; color: #64748b; font-size: 12px; } .card-main strong { margin-left: 9px; color: #1d4ed8; }
.actions { display: flex; flex-wrap: wrap; gap: 7px; padding-top: 12px; margin-top: 12px; border-top: 1px solid #e2e8f0; }
button { padding: 7px 10px; border: 1px solid #94a3b8; border-radius: 6px; background: white; color: #334155; font-weight: 650; cursor: pointer; }
button:hover, button:focus-visible { border-color: #3b82f6; color: #1d4ed8; outline: 0; } button.primary { border-color: #2563eb; background: #2563eb; color: white; } button.danger { margin-left: auto; color: #be123c; }
.empty { max-width: 620px; padding: 45px 24px; border: 1px dashed #94a3b8; border-radius: 12px; margin: 38px auto; background: white; text-align: center; }
.empty h3, .empty p { margin: 0; } .empty p { margin-top: 7px; color: #64748b; }
@media (max-width: 600px) { .content { padding-top: 18px; } .card-main { align-items: stretch; flex-direction: column; } .card-main .primary { align-self: start; } button.danger { margin-left: 0; } }</style>