mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Implement M1 authoring loop: library, editor, live preview, persistence
M1 MVP from docs/IMPLEMENTATION-PLAN.md, with the /alignment pass applied. - core: Snippet model + factory; prepareSpecForRender (copy-not-mutate); per-theme Vega chart config - state/orchestration: SnippetStore with debounced auto-save; IndexedDB adapter + read-time migration; startup hydration + write-through persistence - ui: SnippetLibrary, SpecEditor (Monaco edcore.main — full editor features, JSON-only languages), LivePreview - build: Monaco/Vega manual chunks; raised PWA precache ceiling - alignment: flush a valid draft on snippet switch (+regression tests); TODO breadcrumbs for the preview render race and the window.confirm delete - housekeeping: gitignore .claude/projects/
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Snippet Library — the left pane (spec §02).
|
||||
*
|
||||
* M1 scope: the always-visible list with a pinned "Create New Snippet" item,
|
||||
* selection/highlight, and delete. Search, sort controls, the metadata panel,
|
||||
* status/dataset indicators, and the storage monitor arrive in later milestones.
|
||||
*/
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useSnippetStore } from '../stores/SnippetStore';
|
||||
import styles from './SnippetLibrary.module.css';
|
||||
|
||||
/** Compact relative date for the list (full date formatting lands in M5). */
|
||||
function relativeDate(iso: string): string {
|
||||
const then = new Date(iso);
|
||||
const now = new Date();
|
||||
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const dayMs = 24 * 60 * 60 * 1000;
|
||||
const days = Math.floor((startOfToday.getTime() - new Date(then.getFullYear(), then.getMonth(), then.getDate()).getTime()) / dayMs);
|
||||
if (days <= 0) return 'Today';
|
||||
if (days === 1) return 'Yesterday';
|
||||
if (days < 7) return `${days}d ago`;
|
||||
return then.toLocaleDateString();
|
||||
}
|
||||
|
||||
export function SnippetLibrary() {
|
||||
const snippets = useSnippetStore(useShallow((s) => s.snippets));
|
||||
const activeId = useSnippetStore((s) => s.activeSnippetId);
|
||||
const createSnippet = useSnippetStore((s) => s.createSnippet);
|
||||
const selectSnippet = useSnippetStore((s) => s.selectSnippet);
|
||||
const removeSnippet = useSnippetStore((s) => s.removeSnippet);
|
||||
|
||||
// Default ordering: newest-modified first (spec §02 → Sort).
|
||||
const ordered = [...snippets].sort((a, b) => b.modified.localeCompare(a.modified));
|
||||
|
||||
const handleDelete = (id: string, name: string) => {
|
||||
// TODO: M1 stopgap — route destructive confirms through the modal coordinator
|
||||
// (docs/architecture/03) when the modal system lands, and surface a deletion
|
||||
// toast (spec §02), instead of the native window.confirm.
|
||||
if (window.confirm(`Delete "${name}"? This cannot be undone.`)) removeSnippet(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.library}>
|
||||
<button className={styles.createNew} onClick={() => createSnippet()}>
|
||||
+ Create New Snippet
|
||||
</button>
|
||||
|
||||
<ul className={styles.list}>
|
||||
{ordered.length === 0 && <li className={styles.empty}>No snippets found</li>}
|
||||
{ordered.map((s) => (
|
||||
<li
|
||||
key={s.id}
|
||||
className={`${styles.item} ${s.id === activeId ? styles.active : ''}`}
|
||||
aria-current={s.id === activeId}
|
||||
onClick={() => selectSnippet(s.id)}
|
||||
>
|
||||
<div className={styles.itemMain}>
|
||||
<span className={styles.name}>{s.name}</span>
|
||||
<span className={styles.date}>{relativeDate(s.modified)}</span>
|
||||
</div>
|
||||
<button
|
||||
className={styles.delete}
|
||||
aria-label={`Delete ${s.name}`}
|
||||
title="Delete snippet"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(s.id, s.name);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user