Initial scaffold: spec, architecture playbook, and M0 skeleton

This commit is contained in:
2026-06-04 22:14:33 +03:00
commit 056644450c
51 changed files with 13754 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
# 00 · Product Overview
This document set is a UX/behavioral specification for **Astrolabe**, a browser-based snippet manager for [Vega-Lite](https://vega.github.io/vega-lite/) visualizations. It describes *what the app does* from the user's perspective — its capabilities, workflows, and structural layout — so the app can be recreated on any web/HTML/TS stack. It deliberately avoids prescribing *how* anything is built: no frameworks, libraries, storage technologies, code structure, or concrete visual styling are mandated. Implementers are free to choose those.
## What Astrolabe Is
Astrolabe is a local-first tool for authoring, organizing, and previewing Vega-Lite charts. A user keeps a personal library of **snippets** (saved chart specifications), edits each one as JSON with live validation, and sees the result render in real time beside the editor. Reusable **datasets** can be stored once and referenced by many snippets. Everything lives in the user's browser — there is no account, no server, and no network dependency after first load.
## Who It Is For
People who work with Vega-Lite directly and want a fast, private workspace to draft, iterate on, and keep many visualizations: data practitioners, analysts, educators, and chart authors. Familiarity with Vega-Lite's JSON spec format is assumed; the app does not abstract Vega-Lite away (though the *Chart Builder* offers a no-JSON starting point).
## Core Value
- **Iterate quickly** — edit JSON and watch the chart update live, with schema-aware assistance and instant error feedback.
- **Stay organized** — a searchable, sortable library of named, annotated snippets.
- **Experiment safely** — a draft/published model lets users tinker without losing a known-good version.
- **Reuse data** — datasets stored once, referenced anywhere, in multiple formats and from inline data or remote URLs.
- **Own your data** — fully local, private, and offline-capable, with import/export for backup and transfer.
## Scope & Principles
- **Local-first** — all data is stored in the browser and survives reload; the app works fully offline and is installable as a standalone app.
- **Single-screen workspace** — a three-pane layout (library · editor · preview) plus modals for cross-cutting tools (datasets, chart builder, settings, help).
- **Vega-Lite native** — snippets *are* Vega-Lite specs; the app validates, renders, and reasons about them as such.
- **Keyboard-friendly and shareable** — common actions have shortcuts, and the current location is reflected in a shareable URL.
## Non-Goals
- No user accounts, authentication, or cross-device sync (use *Import & Export* to move data).
- No server-side storage, rendering, or processing.
- No collaboration or multi-user features.
- No general BI/dashboarding — a snippet is a single Vega-Lite visualization, not a composed report.
## Key Concepts (Glossary)
- **Snippet** — a saved Vega-Lite specification plus metadata (name, comment, timestamps, tags, dataset references). The primary user-authored entity. See *Snippet Library* and *Data Model & Persistence*.
- **Spec** — the Vega-Lite JSON specification that defines one visualization.
- **Draft vs Published** — each snippet holds a stable **published** spec and an editable **draft**; edits affect only the draft until the user publishes. See *Spec Editor & Draft/Published Workflow*.
- **Dataset** — a named, reusable data source (JSON/CSV/TSV/TopoJSON; inline or URL) that snippets reference by name. See *Datasets*.
- **Dataset reference** — a Vega-Lite named-data reference, e.g. `{ "data": { "name": "MyDataset" } }`, linking a spec to a stored dataset.
- **Live preview** — the rendered chart, updated automatically as the spec changes. See *Live Preview*.
## How This Specification Is Organized
| # | Section | Covers |
|---|---------|--------|
| 00 | Product Overview | This document — purpose, scope, glossary. |
| 01 | Application Shell & Navigation | Layout, panes, header, modals, keyboard shortcuts, URL state, toasts, offline/installable. |
| 02 | Snippet Library | Browsing, search, sort, metadata, create/duplicate/delete, storage monitor. |
| 03 | Spec Editor & Draft/Published Workflow | Editing, auto-save, auto-render, draft/publish/revert, extract-to-dataset. |
| 04 | Live Preview | Rendering, reference resolution, fit modes, error display. |
| 05 | Datasets | Dataset manager, formats, sources, profiling, references, linking. |
| 06 | Chart Builder | Visual no-JSON chart composition from a dataset. |
| 07 | Settings | Appearance, editor, performance, and formatting preferences. |
| 08 | Import & Export | Backup/transfer file format, import normalization and merging. |
| 09 | Data Model & Persistence | Entity field definitions, storage tiers, relationships. |
| 10 | Non-Functional Requirements | Platform, performance, accessibility, reliability, privacy. |
Read 00 first for orientation, then any section independently. Sections cross-reference one another by title where behavior spans more than one area.
+115
View File
@@ -0,0 +1,115 @@
# 01 · Application Shell & Navigation
This section describes the overall workspace structure, the header toolbar, the modal system, keyboard shortcuts, URL-based navigation, transient notifications, and the offline/installable nature of the app. Feature-specific behavior lives in the sections referenced inline.
## A. Workspace Layout
Astrolabe is a single-screen workspace. Below a fixed top header sits a three-pane working area, each pane dedicated to one part of the snippet-editing workflow:
- **Snippet library** (left) — browse, search, select, and manage saved snippets (see *Snippet Library*).
- **Spec editor** (center) — edit the Vega-Lite spec of the selected snippet (see *Spec Editor & Draft/Published Workflow*).
- **Live preview** (right) — render the current spec (see *Live Preview*).
Behavior:
- All three panes are visible by default, laid out side by side in the order library, editor, preview.
- Adjacent panes are separated by a vertical drag handle. The user can drag a handle left/right to resize the two panes it sits between; the rest of the layout is unaffected.
- Each pane enforces a minimum width while resizing, so a pane cannot be dragged to nothing.
- Each pane can be individually shown or hidden via a persistent **toggle strip** (a narrow vertical strip of toggle buttons, one per pane, each indicating whether its pane is currently shown). The toggle strip also contains a shortcut button that opens the Datasets manager.
- When a pane is hidden, the remaining visible panes expand to fill the freed space, redistributing proportionally to their remembered widths. When a previously hidden pane is shown again, it returns at its remembered width.
- Hiding all panes is permitted; the toggle strip remains available to bring panes back.
- Pane widths and per-pane visibility persist locally across sessions and are restored on next load. The app remembers a pane's preferred width even while it is hidden, so re-showing it restores that width rather than an arbitrary one.
## B. Header / Toolbar
A fixed header spans the top of the app.
- **Left side**: the app icon, the app title ("Astrolabe"), and a version badge showing the current app version.
- **Right side**: a row of text entry points. Each opens a destination:
| Entry point | Opens |
|---|---|
| Import | A file-picker dialog to choose a previously exported file; the chosen file is imported (see *Import & Export*). |
| Export | Immediately produces a downloaded file containing all snippets and datasets (see *Import & Export*). |
| Datasets | The Datasets manager modal (see *Datasets*). |
| Settings | The Settings modal (see *Settings*). |
| About & Privacy | The About & Help modal (keyboard shortcuts, about, and privacy information). |
| Donate | The Donate modal. |
Notes:
- Import and Export act directly (file dialog / file download); they do not open in-app modals.
- The Datasets, Settings, About & Privacy, and Donate entry points each open a modal (see *Modal System*).
## C. Modal System
The app shows at most one modal at a time. The modal set is: Datasets, Settings, About & Help, Donate, Chart Builder, and Extract-to-Dataset.
- Opening any modal closes whichever modal was previously open; the two never overlap.
- Every modal can be dismissed by: clicking its close button, pressing **Escape**, or clicking the backdrop outside the modal body.
- Clicking inside the modal body does not dismiss it.
- The Chart Builder and Extract-to-Dataset modals are opened from within the Datasets / snippet workflows (see *Chart Builder* and *Datasets*), not from the header.
- Dismissing a modal returns the user to the underlying workspace unchanged.
## D. Keyboard Shortcuts
Shortcuts are platform-aware: the modifier is **Cmd** on Mac and **Ctrl** on other platforms (shown below as Cmd/Ctrl).
| Shortcut | Action |
|---|---|
| Cmd/Ctrl + Shift + N | Create a new snippet (see *Snippet Library*) |
| Cmd/Ctrl + K | Toggle the Datasets manager open/closed |
| Cmd/Ctrl + S | Publish the current snippet's draft (see *Spec Editor & Draft/Published Workflow*) |
| Cmd/Ctrl + , | Open the Settings modal |
| Escape | Close the active modal |
Notes:
- Cmd/Ctrl + K is a toggle: if the Datasets manager is already open it closes it; otherwise it opens it.
- Escape only acts when a modal is open; with no modal open it does nothing.
- The shortcut actions override the browser's default behavior for those key combinations.
## E. Navigation & Shareable URL State
The app reflects its current location in the URL hash so that reloading restores the same view and the browser's Back/Forward buttons move between prior states. The user can copy the URL to share or bookmark a specific location.
States and their hash forms:
| State | Hash |
|---|---|
| A selected snippet | `#snippet-<id>` |
| Datasets manager (list) | `#datasets` |
| A specific dataset | `#datasets/dataset-<id>` |
| New-dataset form | `#datasets/new` |
| Chart Builder for a dataset | `#datasets/dataset-<id>/build` |
Behavior:
- Selecting a snippet updates the URL to that snippet; reloading reopens that snippet.
- Opening the Datasets manager updates the URL to `#datasets`; opening a specific dataset, the new-dataset form, or the Chart Builder for a dataset updates the URL to the corresponding form above.
- Browser Back/Forward navigate between these states (e.g. closing a modal via Back returns to the previously selected snippet).
- On load, the app reads the hash and restores the corresponding state (selected snippet, Datasets list, a specific dataset, the new-dataset form, or the Chart Builder).
- An empty/absent hash opens the default snippets view with no modal.
## F. Toast Notifications
Transient toast messages appear in a corner of the screen to confirm actions or report problems, without interrupting the workflow.
- Four kinds, each visually distinct: **success**, **error**, **warning**, **info**.
- Each toast auto-dismisses after a few seconds, and can also be dismissed manually via its close control.
- Multiple toasts stack rather than replacing one another, and appear/disappear with a brief fade.
Events that raise toasts include:
- Snippet actions: creating, duplicating, deleting, publishing a draft, and reverting/discarding a draft.
- Dataset actions: creating, deleting, and extracting inline data into a dataset.
- Import/Export: results of an import (success/partial/failure) and confirmation of an export.
- Errors: spec/data validation failures and local-storage capacity warnings.
## G. Offline & Installable
Astrolabe is local-first and usable without a network connection.
- After the first successful load, the app works fully offline; the interface and previously loaded content remain available with no connection.
- All snippets, datasets, and settings are stored locally and remain accessible offline (see *Data Model*).
- The app is installable as a standalone application from a supporting browser and, once installed, launches in its own window.
+82
View File
@@ -0,0 +1,82 @@
# 02 · Snippet Library
The Snippet Library is the left pane and the primary entry point to the app. A **snippet** is a saved Vega-Lite specification together with metadata (name, comment, timestamps, tags, references to external datasets). The library lets the user browse, search, sort, select, and manage their snippets. Editing the specification, the draft-vs-published workflow, the live preview, and dataset management are covered elsewhere (see *Spec Editor & Draft/Published Workflow*, *Live Preview*, *Datasets*); this section covers only the library and management surface.
## The List
The list shows every saved snippet and is always visible. A persistent "Create New Snippet" affordance sits at the top of the list, above all snippets, so the user can always start a new snippet regardless of scroll position.
- The list shows all snippets, ordered newest-modified first by default (see *Sort*).
- A "Create New Snippet" item is pinned at the top of the list; activating it creates and selects a new snippet (see *Snippet Operations*).
- Selecting a snippet makes it the **active snippet**: it loads into the editor and preview, becomes highlighted in the list, and the URL updates to reflect the selected snippet so the state is shareable and survives a page reload (see *Application Shell & Navigation*).
- Exactly one snippet is active at a time.
- When no snippets match the current search, the list shows an empty-state message ("No snippets match your search"); when there are genuinely no snippets, it shows "No snippets found".
- On first run, when no snippets exist, the app seeds one sample bar-chart snippet so the user starts with a working example.
## List Item
Each list item is a compact row summarizing one snippet, designed for fast scanning.
- Shows the snippet **name**.
- Shows a **last-modified date**, rendered relatively for recent items ("Today", "Yesterday", "Nd ago" within the past week) and as a full date beyond that, formatted per the user's date-format setting (see *Settings*). When sorting by Created, the item shows the created date instead of the modified date.
- Shows the snippet **size** (in KB), but only once the snippet reaches at least about 1 KB; smaller snippets omit the size to reduce clutter.
- Shows a **status indicator** distinguishing a snippet that has unpublished draft changes from one that is fully published (the indicator communicates "draft" vs "published"). The publish and revert actions themselves live in *Spec Editor & Draft/Published Workflow*.
- Shows a small **dataset icon** when the snippet references one or more external datasets (see *Datasets*); the icon is omitted otherwise.
- The active snippet is visually highlighted.
## Search
A live search box lets the user narrow the list as they type. It exists so users with many snippets can find one by name, by note, or by something inside the specification itself.
- The search box filters the list immediately on each keystroke.
- Matching is case-insensitive and spans the snippet **name**, the snippet **comment**, and the **specification content** (the current working/draft spec text), so a search for a field name, mark type, or dataset name in the spec will surface matching snippets.
- The search has a clear control that empties the box and returns focus to it, restoring the full list.
- Search affects only which snippets are shown; it does not change the active snippet or any data.
## Sort
The user chooses how the list is ordered. The choice persists across sessions so the library always opens the way the user left it.
- Sort fields: **Modified**, **Created**, **Name**, **Size**.
- An ascending/descending toggle controls direction; the current field and direction are indicated (e.g. a directional arrow on the active field).
- Selecting the already-active sort field flips the direction; selecting a different field switches to it and resets to descending.
- Default ordering is **Modified, descending** (newest changes first).
- Name sorts alphabetically; Size sorts by stored snippet size; Created and Modified sort chronologically.
- The **Modified** time advances on every save — including silent draft auto-saves (see *Spec Editor & Draft/Published Workflow*) and inline name/comment edits — so under the default Modified-descending sort the active snippet continually rises to the top while it is being edited.
- The selected sort field and direction persist across sessions.
## Selected-Snippet Metadata Panel
When a snippet is active, a metadata panel (within the left pane) exposes its editable properties and key facts. It exists so the user can rename, annotate, and inspect a snippet without leaving the library.
- Shows and lets the user edit the **Name** inline; edits save automatically.
- Shows and lets the user edit a multiline **Comment** (free-form notes); edits save automatically.
- Shows read-only **Created** and **Modified** timestamps, formatted per the user's date-format setting (see *Settings*).
- When the snippet references external datasets, shows a **Linked Datasets** list of the referenced dataset names, each with a dataset icon (see *Datasets*). The list is omitted when there are no references.
- The panel also exposes the Duplicate and Delete operations for the active snippet (see *Snippet Operations*).
## Snippet Operations
The library provides the lifecycle operations for snippets. Each operation gives clear feedback via a toast notification (see *Application Shell & Navigation*).
- **Create New**: starts a new snippet from a small sample Vega-Lite bar-chart template (a few inline category/value rows), assigns it an auto-generated default name (see *Naming & Tags*), saves it, and makes it the active snippet.
- **Duplicate**: creates an independent copy of the active snippet with a name suffixed "(copy)". The copy carries over the specification, comment, tags, and dataset references, gets fresh created/modified timestamps and a new identity, and becomes the active snippet. A success toast confirms the duplication.
- **Delete**: permanently removes the active snippet after the user confirms a warning that the action cannot be undone. After deletion no snippet is active. A toast confirms the deletion.
- These operations never affect other snippets.
## Naming & Tags
New snippets get a sensible default name, and a tag field exists on each snippet for categorization, though tags are not a primary user surface.
- A new snippet receives an auto-generated default name based on the current date and time, so it is uniquely identifiable until the user renames it (renaming happens in the metadata panel).
- Each snippet stores a list of **tags**. Tags are persisted and carried through duplication; for example, snippets brought in via import are tagged "imported" (see *Import & Export*).
- There is no dedicated tag-management UI; tags are stored on the data model but are not surfaced as a primary browsing or editing control.
## Storage Monitor
A small indicator at the bottom of the library shows how much of the snippet storage budget is in use, warning the user before they run out of room. This concerns snippet storage specifically; datasets are stored separately with far greater capacity (see *Datasets* / *Data Model*).
- Displays current usage against the total budget (used vs. total), where the practical snippet budget is about 5 MB.
- A fill indicator reflects the percentage used.
- The indicator enters escalating warning states as usage climbs (a cautionary state past roughly 70% and a critical state past roughly 90%).
- When storage is full, a save may fail; the system warns the user that the snippet could not be saved rather than silently losing data, so the user can delete snippets to free space.
+78
View File
@@ -0,0 +1,78 @@
# 03 · Spec Editor & Draft/Published Workflow
The center pane is where the user reads and edits the active snippet's Vega-Lite specification. It is a code editor for JSON paired with a Draft/Published workflow: edits are made against a working draft that auto-saves silently, drive the *Live Preview* automatically, and become the snippet's stable version only when explicitly published. The pane is empty when no snippet is selected; it loads the active snippet's spec when one is chosen in the *Snippet Library*.
## A. The Spec Editor
The editor presents the active snippet's spec as formatted JSON with full code-editing affordances tuned for Vega-Lite.
- The editor displays the spec as indented, readable JSON with syntax highlighting.
- As the user types, the spec is validated against the Vega-Lite schema; problems are surfaced as inline indicators at the offending locations (e.g. squiggles/markers), without blocking continued editing.
- The editor offers schema-driven autocomplete/suggestions while typing (property names and allowed values from the Vega-Lite schema).
- Pasting content and typing trigger automatic reformatting so the JSON stays consistently indented.
- The editor's appearance and behavior — font size, editor theme, minimap visibility, word wrap, line numbers, and tab size — are configurable and read from *Settings*; this section does not redefine their defaults.
- The editor always edits a single active snippet. Selecting a different snippet in the *Snippet Library*, or toggling the Draft/Published view, replaces the editor content with the corresponding spec.
## B. Auto-Save of the Draft
Edits persist automatically so the user never loses work and never needs an explicit "save" action for ordinary editing.
- A short moment after the user stops typing, the current editor content is parsed and stored as the snippet's working draft, silently and with no notification.
- Auto-save only commits when the editor content is valid JSON; if the content is momentarily unparseable, the save is skipped and retried after the next pause in typing, so a half-typed spec never overwrites the stored draft.
- Auto-save writes the **draft** only. It never alters the published version (see *D. Draft vs Published*).
- Auto-save is distinct from Publish: auto-save preserves in-progress work; Publish promotes that work to stable.
## C. Auto-Render to Preview
Edits flow to the *Live Preview* automatically, so the user sees results without invoking a render.
- A brief moment after the user stops typing, the current spec is sent to the *Live Preview* for rendering.
- The delay before rendering is a configurable debounce (see *Settings* / *Live Preview*), letting the user trade responsiveness against churn while typing heavy specs.
- Rendering also occurs immediately when a snippet is first loaded into the editor or the Draft/Published view is switched.
- Rendering specifics (dataset reference resolution, fit modes) belong to *Live Preview*; the editor's role is to supply the current spec text on each settle.
## D. Draft vs Published Workflow
Every snippet carries two versions of its spec: a **published** (stable) version and a **working draft**. This separation is the central editing model. A view toggle in the pane header switches which version the editor shows.
- The header offers a Draft/Published toggle; the currently active view is visually indicated.
- **Draft view** shows the working draft and is the editable surface — all typing, auto-save, and auto-render act on the draft.
- **Published view** shows the last published version, for reference; the published version is never modified by ordinary editing.
- Editing the draft never touches the published version until the user publishes.
- The *Snippet Library* status indicator reflects whether a snippet currently has unpublished draft changes (draft differs from published); this section only produces that difference, it does not render the indicator.
### Publish
- A **Publish** action promotes the current draft to become the published version (the two are made identical).
- Publish is also triggered by the keyboard shortcut Cmd/Ctrl+S.
- On publish, the snippet's dataset references are recomputed from the now-published spec (see *Datasets* for reference linking).
- A success toast confirms the snippet was published.
- Publish is unavailable when no snippet is active.
### Revert
- A **Revert** action discards all draft changes and restores the draft to match the last published version.
- Revert requires explicit confirmation before discarding, warning that the action cannot be undone.
- On confirmation, the editor reloads with the published spec and a toast confirms the draft was reverted.
- Revert is unavailable when no snippet is active.
## E. Inline Error Surface
When the spec cannot be parsed or cannot be rendered, the editor pane shows the problem clearly while keeping the user in place to fix it.
- When the spec is invalid JSON, or is valid JSON but fails to render as Vega-Lite (including an unresolved dataset reference), a clear, readable error message appears in the editor pane, near the editor area.
- The error message is plainly legible (monospaced, distinct from normal content) and conveys what went wrong.
- The editor remains fully usable while an error is shown, so the user can edit to fix it; the error clears automatically once a subsequent edit renders successfully.
- This is the editor-side error affordance only; how a valid spec is drawn lives in *Live Preview*.
## F. Extract Inline Data to a Dataset
When a snippet's spec embeds its data inline, the user can lift that data out into a reusable, named dataset and have the spec reference it instead. This keeps specs lean and lets the same data serve multiple snippets (see *Datasets*).
- When the active snippet's draft spec contains inline data, an **Extract to Dataset** action is available in the pane header; it is hidden when the spec has no inline data.
- Choosing it opens a modal that shows a read-only preview of the inline data and asks the user for a dataset name (required).
- The user enters a name and confirms creation. Names must be non-empty and unique; if the name is blank or already in use, the modal shows an inline error and the action does not proceed.
- On success, the system: saves the inline data as a new dataset (preserving its detected format), rewrites the snippet's draft spec so the inline data is replaced by a reference to the dataset by name, links the dataset to the snippet, and reloads the editor to show the rewritten spec.
- A toast confirms the dataset was created, and the modal closes.
- The user can cancel the modal at any time, leaving the spec unchanged.
- Dataset-side specifics (formats, storage, the bidirectional snippet↔dataset link) are described in *Datasets*.
+81
View File
@@ -0,0 +1,81 @@
# 04 · Live Preview
The right pane renders the active snippet's current specification as a live Vega-Lite visualization. It mirrors whatever the editor currently shows and updates on its own as the user types, giving immediate visual feedback without any explicit "run" action.
## Purpose & Live Updating
- Renders the active snippet's current spec as a Vega-Lite visualization.
- Always reflects the version currently shown in the editor: while the user edits the draft, the preview renders the draft; once published/viewing the published version, it renders that (see *Spec Editor & Draft/Published Workflow*).
- Updates automatically as the user edits, after a brief render debounce so rapid keystrokes do not trigger constant re-rendering. The debounce delay is user-configurable (see *Settings*).
- A subtle busy indication may appear over the preview while a render is in progress; it clears when rendering completes.
- When no snippet is active, or the editor content is empty/blank, the preview renders nothing (a clean, empty pane) rather than showing an error.
## Dataset Reference Resolution
When a spec uses inline data, the preview renders it directly. When a spec instead references a named dataset from the library, the preview resolves that reference and renders using the stored dataset's contents (see *Datasets*).
- A spec may point at a dataset from the library by name instead of embedding the data inline.
- Before rendering, the preview substitutes the referenced dataset's stored contents into the spec.
- URL-sourced datasets are fetched as needed at render time.
- If a referenced dataset cannot be found or fetched, the preview shows a readable error (see *Error Display*) rather than a broken chart.
## Fit / Sizing Modes
The preview pane header has a "Fit" control offering exactly four modes that determine how the chart is sized within the pane. The chosen mode applies immediately and re-renders the current chart.
- **Original** — renders the chart at its natural size as defined by the spec. If the chart is larger than the pane, it overflows and the pane provides scrolling to reach the rest.
- **Width** — fits the chart's width to the pane (the width becomes responsive to the pane); the height is left to the chart's own natural sizing.
- **Height** — fits the chart's height to the pane; the width is left to the chart's own natural sizing.
- **Full** — fits the chart to the pane in both dimensions, so it occupies the full available width and height.
The exact spec transform each mode performs is defined in *Rendering Contract* below.
Behavior of the selected mode:
- The control shows the four modes with the active one visibly indicated.
- The selected mode persists across sessions, stored in *Settings* as `previewFitMode`.
- The default is the natural Original mode.
## Rendering Contract
Before the chart is drawn, the spec shown in the editor is transformed into the spec actually rendered. Two deterministic transforms are applied in order. They are specified here because reproducing them faithfully is what makes references and fit modes behave correctly; the result is observable as the rendered chart.
**1. Dataset reference resolution.** Any named-data reference (`data` with a `name`) is replaced in-place with the referenced dataset's actual contents, shaped by the dataset's source and format (see *Datasets*):
| Dataset source / format | The reference's `data` becomes |
|---|---|
| URL (any format) | a URL reference to the dataset's address, tagged with its format |
| Inline JSON | the parsed values, inlined |
| Inline CSV / TSV | the raw text, inlined, tagged with its format (CSV or TSV) |
| Inline TopoJSON | the value, inlined, tagged as TopoJSON |
- Resolution recurses into nested sub-specs (layered and concatenated specs, and a parent spec's child `spec`), so references anywhere in the spec are resolved.
- If a referenced dataset does not exist, rendering fails with a "dataset not found" error (see *Error Display*).
**2. Fit-mode sizing.** The selected fit mode rewrites the spec's sizing using Vega-Lite's responsive `"container"` sizing keyword, recursing into the same nested sub-specs:
| Mode | Transform |
|---|---|
| Original | spec sizing left untouched (the spec's own `width`/`height`, or Vega-Lite defaults, apply) |
| Width | set `width` to `"container"`; remove any explicit `height` |
| Height | set `height` to `"container"`; remove any explicit `width` |
| Full | set both `width` and `height` to `"container"` |
- For the responsive (non-Original) modes the chart's container-relative dimension follows the pane size, while the unconstrained dimension is recomputed naturally — this is why Width/Height do not preserve the original aspect ratio.
- The transform operates on a copy; the user's stored spec is never modified by rendering.
The preview renders the resulting spec without the charting library's built-in action/export menu, so the output is a clean chart with no overlaid controls.
## Error Display
When a spec cannot be rendered, the preview replaces the chart area with a clear, readable error message rather than a broken or partial visualization, and recovers on its own once the spec becomes valid again.
- Invalid JSON, incomplete specs, Vega-Lite errors, and data problems (e.g. a missing or unfetchable dataset) all surface as a legible error message.
- The message identifies it as a rendering error and includes the underlying reason, with a hint to check the JSON syntax and the Vega-Lite specification.
- As soon as the spec becomes valid again, the error clears automatically and the chart renders without any manual retry.
- Empty/blank specs are not treated as errors — they simply render nothing.
## Responsiveness
- The preview re-fits when the pane is resized, re-applying the current fit mode so the chart continues to honor the chosen sizing (see panes in *Application Shell & Navigation*).
- Resizing does not require a manual refresh; the displayed chart adapts to the new pane dimensions.
+107
View File
@@ -0,0 +1,107 @@
# 05 · Datasets
The **Dataset Manager** is a modal for creating and managing named, reusable datasets that snippets can reference by name. It is the home of the dataset library: a place separate from snippets where data lives once and is shared across many visualizations.
## Purpose & Model
Datasets are named blobs of data stored in the user's local library, independent of any single snippet. A snippet references a dataset by name rather than embedding the data inline, so the same data can power many snippets and be edited in one place.
- Datasets persist locally across sessions in a high-capacity local store, far larger than the budget available to snippets — large datasets belong here, not inline in specs.
- A snippet references a dataset using a Vega-Lite named-data reference, e.g. `{ "data": { "name": "MyDataset" } }`. When the *Live Preview* renders a spec, it resolves any such named reference against the dataset library (see *Live Preview*).
- See *Data Model* for the stored shape of a dataset.
## Opening & Navigation
- Opened from a header control or via the keyboard shortcut Cmd/Ctrl+K.
- The current view and the selected dataset are reflected in the URL, so a selected dataset produces a shareable/back-navigable location (see *Application Shell & Navigation*).
- Closing the modal clears the current selection and any open create form.
## Layout
A two-pane modal:
- **List pane** (left): a "New Dataset" action plus the list of all datasets, sorted most-recently-modified first.
- **Detail pane** (right): shows the selected dataset's details, the create form when creating, or an empty prompt ("Select a dataset or create a new one") when nothing is selected.
### List item
Each list item shows:
- The dataset **name**.
- A **meta line** combining: source ("URL" prefix for URL datasets), row count when known, the **format label** (JSON / CSV / TSV / TOPOJSON), and **size** (human-readable, e.g. B / KB / MB). For URL datasets where counts are not yet known, only the source and format label are shown.
- A **usage badge** when one or more snippets reference the dataset, indicating how many.
Clicking an item selects it and shows its detail. Per-item actions (delete, plus copy-reference and build-chart) live in the detail pane for the selected dataset.
## Source Types
A dataset has one of two source types, chosen when creating it:
- **Inline** — the data itself is pasted in and stored directly in the library.
- **URL** — the dataset stores a remote URL (http/https). The data is not copied locally; it is fetched on demand when a referencing spec is rendered (see *Live Preview*).
For inline datasets the library holds the full data and can profile it. For URL datasets the library holds only the link, so row/column/size figures are typically not computed up front and show as "N/A".
## Supported Formats
Four data formats are supported, named in the UI and stored on the dataset:
- **JSON** — an array of objects (most common, profilable) or a single object.
- **CSV** — comma-separated with a header row.
- **TSV** — tab-separated with a header row.
- **TopoJSON** — topology/map data (a JSON object whose type marks it as a topology).
### Auto-detection
When the user pastes inline data, the app auto-detects the format and reports a **confidence** level (high / medium / low):
- Valid JSON parses to JSON, or to TopoJSON when it is a topology object — high confidence.
- Otherwise, multi-line text with a header row is detected as TSV (when tab-separated) or CSV (when comma-separated) — medium confidence.
- Unrecognized input yields no format (low confidence); saving is blocked with a message asking the user to check the input.
The detected format and source are shown as badges in the create form so the user can confirm or override the source (Inline/URL) before saving. For URL datasets the format is inferred from the URL's file extension (`.csv`, `.tsv`, `.json`, `.topojson`) and shown as a hint.
## Profiling
For tabular inline data (JSON array-of-objects, CSV, TSV) the app computes and stores a profile:
- **Row count** and **column count**.
- The list of **column names**.
- An **inferred type per column**: number, text/string, date, or boolean. Type inference looks at the column's values: all-numeric becomes number, all `true`/`false` becomes boolean, otherwise string; empty cells are ignored.
- **Size** in bytes of the stored data.
A **truncated data preview** of the raw data is also retained for display. URL datasets and non-tabular data are not profiled (counts show "N/A").
## Detail Panel
The detail pane for a selected dataset shows:
- **Name**.
- **Comment** (optional free-text notes), when present.
- **Overview**: statistics (rows, columns, size), the **column list** with each column's name and inferred type shown with a simple type indicator, and created/modified timestamps.
- **Preview**: a truncated rendering of the data (raw text for CSV/TSV/URL, pretty-printed for JSON/TopoJSON).
- **Linked Snippets**: the list of snippets that reference this dataset by name. This is the dataset side of bidirectional dataset↔snippet linking (see *Snippet Library*).
## Actions
Each action raises a confirming toast (or an error toast on failure).
- **Copy Reference** — copies the by-name reference object to the clipboard, ready to paste into a spec:
`{ "data": { "name": "MyDataset" } }`
- **New / Create New** — opens the create form in the detail pane with fields: **name** (required, unique), **source** toggle (Inline / URL), the **data** (a paste area for inline, a URL field for URL source), and an optional **comment**. Save is disabled until a name and valid data/URL are present. On success the new dataset is selected.
- **Edit** — rename, edit the comment, and update the data (re-paste inline data or refresh the URL). Updating inline data re-profiles it; the modified timestamp advances.
- **Delete** — asks for confirmation ("Delete \"Name\"? This cannot be undone."), then removes the dataset and clears the selection.
## Build Chart From Dataset
From a selected dataset the user can launch the visual *Chart Builder* (see *Chart Builder*) pre-targeted at that dataset, producing a new snippet whose spec references the dataset by name.
## Extract Inline Data → Dataset
The reverse flow starts in the editor: a user can extract inline `data.values` out of a spec into a new named dataset (see *Spec Editor & Draft/Published Workflow*). The result appears here as a new dataset, and the originating snippet's spec is rewritten to reference it by name.
## Naming & Uniqueness
- Dataset names must be **unique**. Attempting to create a dataset with a name already in use is rejected with an error toast.
- During bulk operations such as import, conflicting names are automatically suffixed to remain unique rather than overwriting existing datasets (see *Import & Export*).
- Renaming a dataset that is referenced by snippets keeps references consistent by updating the matching named-data references in affected specs.
+70
View File
@@ -0,0 +1,70 @@
# 06 · Chart Builder
The Chart Builder is a visual, no-JSON way to compose a Vega-Lite chart from a selected dataset. The user picks a mark type and maps the dataset's columns to encoding channels; the builder produces a complete Vega-Lite spec and saves it as a new snippet that references the dataset. It is intended for users who want to start a chart quickly without hand-writing JSON in the *Spec Editor & Draft/Published Workflow*.
## Opening
- Launched from a selected dataset in the *Datasets* manager via that dataset's "build chart" action.
- Opens as a modal dialog over the application; the URL reflects the dataset's "build" action so the open builder is shareable/restorable (see *Application Shell & Navigation*).
- On open, the builder loads the selected dataset, displays its name, and pre-populates sensible defaults (see below). If no dataset is available, it shows a "No dataset loaded" message and offers no controls.
## Layout
A two-pane modal:
- **Left — configuration:** dataset name, mark type selector, one row per encoding channel, optional width/height inputs, and a "Create Snippet" action.
- **Right — live preview:** a rendered chart that updates as the configuration changes, with a placeholder/error area.
## Inputs and Controls
### Mark type
- Single selection from an exact set of five mark types: **Bar, Line, Point, Area, Circle**.
- Defaults to **Bar**.
- Exactly one mark type is active at any time; selecting one updates the preview.
### Encoding channels
- Exactly four channels are offered, in this order: **X, Y, Color, Size**.
- For each channel the user:
- Picks a dataset column from a dropdown of the dataset's detected columns (see *Datasets* for column detection). A "None" option leaves the channel unmapped. Each column option shows a small type indicator alongside the column name.
- Optionally overrides the channel's **field type**, chosen from an exact set: **Quantitative, Nominal, Ordinal, Temporal**. The type override only appears once a column is selected for that channel.
- When a column is chosen, its field type defaults from the dataset's inferred column type (numeric → Quantitative, date → Temporal, otherwise Nominal); the user may change it afterward.
- Clearing a channel back to "None" leaves it out of the produced spec.
### Default pre-population
- On open, the first detected column is assigned to **X** and the second (if any) to **Y**, each with its derived field type. Remaining channels start unmapped. Mark type starts at Bar.
### Dimensions (optional)
- Optional numeric **Width** and **Height** inputs in pixels.
- When left empty, the chart uses default/responsive sizing (consistent with *Live Preview*); when provided, the values are written into the spec.
## Live Preview
- The right pane renders the chart described by the current mark, encodings, and dimensions, resolving the dataset reference to its actual data (same rendering behavior as *Live Preview*).
- Updates are debounced: changes to mark, encodings, or dimensions trigger a re-render after a short pause rather than on every keystroke.
- While no encoding is mapped, the pane shows a placeholder instructing the user to configure at least one encoding.
- If the spec fails to render, the pane shows an inline error message describing the problem instead of a chart.
## Validation
- A chart requires **at least one** channel mapped to a column.
- While no channel is mapped, the "Create Snippet" action is disabled and the preview shows the configuration prompt.
## Output / Create
Selecting "Create Snippet" produces the final artifact:
- Builds a complete Vega-Lite spec containing: the schema reference, a named data reference to the dataset, the chosen mark (with tooltips enabled), the mapped encodings (each with its field and field type), and any explicit width/height.
- Channels left unmapped are omitted; if no encodings exist the spec omits the encoding block entirely (prevented by validation here).
- Creates a new snippet from that spec with an auto-generated descriptive name, adds it to the snippet library, and records that it was built from the dataset.
- Links the snippet to the dataset by recording the dataset reference, so the bidirectional snippet↔dataset relationship is established (see *Datasets*).
- Raises a success toast naming the created snippet.
- Closes the builder; the newly created snippet becomes the active snippet in the library/editor.
## Closing
- The builder can be dismissed without creating anything (close control / modal dismissal).
- Closing resets all builder state (dataset, mark type, encodings, dimensions, preview) so a later open starts fresh, and any pending preview render is cancelled.
+79
View File
@@ -0,0 +1,79 @@
# 07 · Settings
Astrolabe provides a **Settings** modal where users tune appearance, the spec editor, preview performance, and date formatting. All settings persist locally and apply across sessions on the same device. Settings load at startup; any unknown or missing value falls back to its factory default, so older or partial saved settings never break the app.
## Opening the modal
- An entry in the application header opens the Settings modal.
- The keyboard shortcut **Cmd/Ctrl+,** also opens it.
- The modal is grouped into clearly titled sections: Appearance, Editor, Performance, and Formatting.
- The modal can be dismissed with a Cancel action or the standard modal-close affordance; dismissing without applying discards any pending edits and restores the last saved values.
## Settings
### Appearance
Controls the overall UI theme. Choosing the experimental Dark theme switches the whole application chrome to a dark presentation.
| Setting | Options | Default |
| -------- | ----------------------------- | ------- |
| UI theme | Light, Experimental Dark | Light |
### Editor
These settings configure the spec editor used to edit Vega-Lite specs (see *Spec Editor & Draft/Published Workflow*). They take effect in the editing surface for the snippet spec.
| Setting | Options / Range | Default |
| ------------- | ------------------------------------- | -------- |
| Font size | 1018 px (integer) | 12 px |
| Editor theme | Auto + explicit overrides (provisional — see note) | Auto |
| Minimap | On / Off | Off |
| Word wrap | On / Off | On |
| Line numbers | On / Off | On |
| Tab size | Integer number of spaces | 2 |
- Font size is chosen along a 1018 range; the current value is shown alongside the control.
- Editor theme controls the syntax/color presentation inside the editor. **Provisional (to be finalized as we implement the editor):** the default is **Auto**, which derives the editor theme from the app UI theme (light app theme → light editor theme, experimental → dark), using custom Monaco themes that match the app chrome. The user may override Auto with an explicit editor theme; the exact override list (custom themes, and whether to include High Contrast or the stock Monaco themes) is deferred. Stored as `editor.theme` with an `'auto'` sentinel for the follow-the-app default.
- Minimap toggles the condensed overview strip beside the editor.
- Word wrap toggles soft wrapping of long lines.
- Line numbers toggles the line-number gutter.
- Tab size sets the indentation width applied while editing.
### Performance
| Setting | Range | Default |
| --------------- | -------------------- | -------- |
| Render debounce | 5005000 ms | 1500 ms |
- Render debounce is the delay after the user stops typing before the preview re-renders (see *Live Preview*).
- Tradeoff: a lower value makes the preview feel snappier and more immediate but re-renders more often and uses more CPU; a higher value keeps the app calmer and lighter but makes the preview feel laggier behind the spec.
- The current value is shown alongside the control.
### Formatting
Governs how dates are rendered throughout the app, for example the timestamps shown in the *Snippet Library* list.
| Setting | Options | Default |
| ------------------- | ----------------------------------------- | ------- |
| Date format | Smart, ISO 8601, Custom | Smart |
| Custom date format | Free-text format string | (empty) |
- **Smart**: relative, human-friendly rendering (e.g. "Today", "Yesterday", "3d ago", falling back to a full date for older items).
- **ISO 8601**: a full ISO 8601 timestamp.
- **Custom**: dates render using the user-supplied format string.
- The custom format string field is only relevant when Date format is set to Custom; it is shown only in that case (placeholder guidance such as `yyyy-MM-dd HH:mm`).
## Related persisted preferences (documented elsewhere)
The following preferences also persist locally across sessions but are managed outside this modal and are documented in their own sections:
- **Preview fit mode** — how the preview is sized/fit; see *Live Preview*.
- **Snippet sort preference** — the snippet list's sort field and direction; see *Snippet Library*.
## Behaviors
- **Apply / save**: An explicit Apply action writes all changes; they take effect immediately (e.g. the UI theme switches at once).
- **Dirty indication**: While the form differs from the last saved state, the modal shows an "Unsaved changes" indicator.
- **Cancel / dismiss**: Closing without applying reverts the form to the last saved values and leaves stored settings untouched.
- **Reset to defaults**: A Reset action restores every setting to its factory default. It requires explicit confirmation before applying, then saves the defaults.
- **Startup load**: Settings are read on startup and applied to the UI and editor; missing or unrecognized values silently use their defaults.
+83
View File
@@ -0,0 +1,83 @@
# 08 · Import & Export
Astrolabe lets a user back up or transfer their entire workspace as a single JSON file, and bring data back in by importing such a file. Both actions are triggered from header controls labelled **Import** and **Export**. Import always merges with existing data; it never replaces what is already stored.
## Export
Export produces one downloadable JSON file containing every snippet (see *Snippet Library*) and every dataset (see *Datasets*), wrapped in an envelope carrying format metadata.
- **Trigger**: the **Export** header control runs the export immediately (no intermediate dialog).
- **Contents**: all snippets and all datasets currently stored, plus envelope metadata.
- **Empty workspace**: if there are no snippets, the user is informed ("No snippets to export") and no file is downloaded — even if datasets exist.
- **Filename**: `astrolabe-project-YYYY-MM-DD.json`, where the date is today's date (export day).
- **Feedback**: on success a toast reports the counts, e.g. "Exported 4 snippets and 2 datasets" (the dataset clause is omitted when there are no datasets; singular/plural wording adapts to the counts).
### Export envelope shape
The downloaded file is a single JSON object: an envelope with a format `version`, an export timestamp, an exporter tag, and the two data arrays.
```json
{
"version": "1.0",
"exportedAt": "2026-06-03T12:00:00.000Z",
"exportedBy": "Astrolabe",
"snippets": [ /* full snippet objects (see Data Model) */ ],
"datasets": [ /* full dataset objects (see Data Model) */ ]
}
```
- `version` — export format version (currently `"1.0"`).
- `exportedAt` — ISO 8601 timestamp of the export.
- `exportedBy` — fixed identifier `"Astrolabe"`.
- `snippets` / `datasets` — arrays of complete records as defined in *Data Model*, each including its record `version` field. (This is the per-record schema version, not the envelope `version` above.)
## Import
Import lets the user pick a JSON file from their device; its contents are normalized, merged into the current workspace, and saved.
- **Trigger**: the **Import** header control opens a file picker restricted to JSON files. After a file is chosen (or the picker cancelled) the control is ready to be used again immediately.
### Accepted inputs
The importer recognizes several shapes so that both Astrolabe exports and looser snippet files work:
- **Astrolabe export envelope** — an object with a `version` and a `snippets` array; an optional `datasets` array is imported too.
- **Bare array of snippets** — a top-level JSON array is treated as a list of snippets (no datasets).
- **Single snippet object** — any other object is treated as one snippet.
- **Older / foreign snippet shapes** — snippets that do not match the current model are normalized onto it:
- Alternative field names are mapped: `content` → spec, `draft` → draft spec, `createdAt` → creation timestamp.
- Missing timestamps are generated at import time (creation and modification set to now, or derived from the source timestamp when present).
- Missing identifiers, names, comments, tags, dataset references, metadata, and record `version` are filled with defaults (a missing `version` is treated as the earliest shape and migrated up on read — see *Data Model*).
- Such normalized imports are tagged `"imported"` so the user can find them.
A snippet is treated as already in current Astrolabe format when it carries an ISO-style creation timestamp; in that case its existing fields (id, name, timestamps, spec, draft spec, comment, tags, dataset references, metadata) are preserved as-is, with sensible fallbacks for any missing field.
### Merge behavior
- Imported snippets are **appended** to the existing library; nothing is overwritten or removed.
- **ID collisions** (an incoming snippet whose id already exists) are resolved by assigning the incoming snippet a fresh unique id; the original snippet keeps its id.
- Datasets are imported **before** snippets so that snippet dataset references can resolve.
### Dataset conflicts
When an imported dataset's name already exists in the library, it is auto-renamed to a unique name rather than overwriting the existing one (see *Datasets*).
- A numeric suffix is appended to the original name; further suffixes are added until the name is unique.
- The renamed datasets are reported to the user via a warning toast listing each `original -> new` rename.
- If a single dataset fails to import, it is skipped and the rest of the import continues.
### Storage limit handling
Snippet storage has an approximate 5 MB budget (see *Snippet Library* storage monitor).
- If the incoming snippets would push total snippet storage over the budget, the user is warned about the overage amount, but the app still attempts to save the import.
- If the save ultimately fails because the storage quota is exceeded, the user is told to delete some snippets and try again, and no partial snippet import is committed.
- The storage check applies to snippets; datasets are stored separately and saved during the dataset phase above.
### Feedback
- **Success**: a toast reports how many snippets (and datasets, when any) were imported, e.g. "Imported 4 snippets and 2 datasets".
- **Renames**: when datasets were renamed, the success message is shown as a warning toast that also lists the renames.
- **Empty file**: if no snippets are found in the file, the user is informed ("No snippets found in file") and nothing is imported.
- **Quota failure**: a clear error advising the user to delete snippets and retry.
- **Invalid file**: a non-JSON or unparseable file produces a clear error ("Failed to import. Please check that the file is valid JSON."); an unreadable file produces a read error. In all error cases the existing workspace is left unchanged.
+107
View File
@@ -0,0 +1,107 @@
# 09 · Data Model & Persistence
This section defines the persistent entities of Astrolabe and how they relate. It is the authoritative data contract: an implementer recreating the app should store equivalent records with these fields and meanings. Types are given abstractly (string, number, boolean, ISO-timestamp string, string[], object, "JSON value") so they map onto any stack. "JSON value" means any valid JSON shape — object, array, string, number, boolean, or null.
All data lives entirely in the browser. There is no server, account, or sync. Records survive page reload and remain available offline (see *Application Shell & Navigation*). To move data between browsers or devices, use *Import & Export*.
## A. Snippet
A **Snippet** is a saved Vega-Lite specification together with its metadata. Snippets are the primary user-authored entity, listed and managed in the *Snippet Library*.
| Field | Type | Meaning |
|-------|------|---------|
| `id` | string | Unique, stable identifier for the snippet. |
| `version` | number | Schema version of this record, used for read-time migration (see *Schema versioning* below). |
| `name` | string | Human-readable title shown in the library. |
| `created` | ISO-timestamp string | When the snippet was first created. |
| `modified` | ISO-timestamp string | When the snippet was last saved. |
| `spec` | JSON value | The **published** Vega-Lite spec. May be an object or a string. This is the version rendered and shared by default. |
| `draftSpec` | JSON value | The **working draft** Vega-Lite spec being edited. May be an object or a string. |
| `comment` | string | Free-form user note about the snippet. |
| `tags` | string[] | User-assigned labels for filtering and organization. |
| `datasetRefs` | string[] | Names of *Datasets* referenced by this spec (see relationships below). |
| `meta` | object | Free-form, extensible metadata bag for app- or feature-specific data. |
### Dual spec / draftSpec model
A snippet carries two specs at once. `draftSpec` is the editable working copy; `spec` is the last published copy. Editing affects only `draftSpec` until the user publishes, at which point `draftSpec` is promoted to `spec`. This separation backs the draft/published workflow described in *Spec Editor & Draft/Published Workflow* — it lets users experiment freely while keeping a known-good published version, and drives indicators for unpublished changes.
### datasetRefs
`datasetRefs` records the **names** of datasets the spec depends on. It is the link used to display a snippet's linked datasets and, conversely, to find which snippets use a given dataset (see *Cross-entity relationships*). It is maintained to mirror the dataset names actually referenced in the spec.
## B. Dataset
A **Dataset** is a named, reusable data source that snippets can reference by name instead of inlining data. Datasets are managed in the *Datasets* manager and support multiple formats and two source kinds.
| Field | Type | Meaning |
|-------|------|---------|
| `id` | number | Unique numeric identifier. |
| `version` | number | Schema version of this record, used for read-time migration (see *Schema versioning* below). |
| `name` | string | Unique, human-readable name; the key snippets reference via `datasetRefs`. |
| `data` | JSON value | The payload. For `source = url`: the URL string. For `source = inline`: the raw CSV/TSV text, or the parsed JSON/TopoJSON value. |
| `format` | string | One of `json`, `csv`, `tsv`, `topojson`. |
| `source` | string | One of `inline` (data embedded in the record) or `url` (data fetched from a remote address). |
| `comment` | string | Free-form user note about the dataset. |
| `rowCount` | number or null | Number of data rows, or null when unknown/not applicable. |
| `columnCount` | number or null | Number of columns, or null when unknown/not applicable. |
| `columns` | string[] | Column names, in order. |
| `columnTypes` | array of `{ name, type }` | Per-column inferred type. `name` is the column; `type` is one of `number`, `string`, `date`, `boolean`. |
| `size` | number | Approximate payload size in bytes. |
| `created` | ISO-timestamp string | When the dataset was first added. |
| `modified` | ISO-timestamp string | When the dataset was last changed. |
The `rowCount`, `columnCount`, `columns`, `columnTypes`, and `size` fields are derived summaries computed when data is added or updated; they support previews and type display without re-parsing the full payload.
### Schema versioning
Both **Snippet** and **Dataset** records carry a numeric `version` recording the shape of that individual record. When a record is read from storage it is migrated up to the current shape before the app uses it; new writes always store the current version. A record written before versioning existed (no `version` field) is treated as version `1`. This is distinct from the storage container's own layout version, and from the *Import & Export* envelope `version` (which describes the file format, not a record). Records exported via *Import & Export* include their `version`.
## C. UserSettings
**UserSettings** holds persisted user preferences as a single structured record. The semantics and UX of each option are covered in *Settings*; the shape below is the storage contract.
| Field | Type | Meaning |
|-------|------|---------|
| `version` | number | Schema version of the settings record, used for migration. |
| `editor.fontSize` | number | Editor font size. |
| `editor.theme` | string | Editor color theme identifier. |
| `editor.minimap` | boolean | Whether the editor minimap is shown. |
| `editor.wordWrap` | string | `on` or `off`. |
| `editor.lineNumbers` | string | `on` or `off`. |
| `editor.tabSize` | number | Spaces per indentation level. |
| `performance.renderDebounce` | number | Delay (ms) before re-rendering the preview after edits. |
| `ui.theme` | string | App theme: `light` or `experimental`. |
| `ui.previewFitMode` | string | Preview sizing: `default`, `width`, `height`, or `full`. |
| `formatting.dateFormat` | string | Date display mode: `smart`, `iso`, or `custom`. |
| `formatting.customDateFormat` | string | Pattern used when `dateFormat = custom`. |
A reference shape:
UserSettings = { version, editor: { fontSize, theme, minimap, wordWrap, lineNumbers, tabSize }, performance: { renderDebounce }, ui: { theme, previewFitMode }, formatting: { dateFormat, customDateFormat } }
## D. App / UI preferences (persisted separately)
Some preferences persist independently of *UserSettings* so they can update frequently without rewriting the settings record. They are stored locally and restored on load.
- **Snippet sort preference** — how the *Snippet Library* list is ordered. `sortBy` is one of `name`, `modified`, `created`; `sortOrder` is `asc` or `desc`. Default is `modified` / `desc` (most recently changed first).
- **Panel layout** — the resizable three-panel arrangement: per-pane widths and per-pane visibility (which panels are shown or hidden). Restored so the workspace reopens as the user left it.
## E. Persistence & limits
| Tier | What it holds | Capacity & behavior |
|------|---------------|---------------------|
| Snippet store | All *Snippet* records | Local, with a practical budget of about 5 MB. A storage monitor tracks usage and surfaces warnings as the budget fills (see *Snippet Library*). |
| Dataset store | All *Dataset* records | Local, in a separate, much higher-capacity store, suited to larger payloads. |
| Settings & preferences | *UserSettings* plus the app/UI preferences in (D) | Local, small. |
Everything stays in the browser — no server or account is involved. All tiers survive reload and function offline. Because capacity is finite and per-browser, *Import & Export* is the supported path for backup and for moving data between browsers or devices.
## F. Cross-entity relationships
Snippets and datasets are linked **bidirectionally by dataset name**: `snippet.datasetRefs` holds dataset names, and each such name matches a `dataset.name`.
- From a snippet, `datasetRefs` yields its linked datasets.
- From a dataset, scanning snippets for its `name` in `datasetRefs` yields the snippets that reference it.
This name-based link is what the *Snippet Library* and *Datasets* surfaces use to show linkage in both directions. The actual resolution of a referenced dataset into spec data at render time is covered in *Live Preview*.
+54
View File
@@ -0,0 +1,54 @@
# 10 · Non-Functional Requirements
This section defines quality attributes the rebuild must satisfy — performance, accessibility, reliability, privacy, and platform posture — independent of any single feature. Feature behavior lives in the other sections; this one constrains *how well* that behavior must work.
## Platform & Form Factor
- **Target**: modern evergreen desktop browsers. The app is a single-page application that loads once and then runs locally.
- **Desktop-first**: the primary experience is the three-pane workspace (see *Application Shell & Navigation*), designed for wide viewports. Each pane has a minimum usable width and stops shrinking below it.
- **Small screens**: the three-pane layout is not expected to reach full parity on narrow/mobile viewports. A graceful fallback (e.g. collapsing to fewer visible panes via the toggle strip, or a single-column arrangement) is acceptable; an unusable or broken layout is not.
- **Offline & installable**: after first load the app must function fully offline, and must be installable as a standalone application that launches in its own window (see *Application Shell & Navigation*).
## Embedding & Environment Assumptions
Astrolabe is specified as a standalone single-page app that owns its whole viewport. A team integrating these capabilities into a larger product should know which shared environment surfaces the app currently reserves, so they can decide how to reconcile each with the host. (Surfacing the assumption is the spec's job; choosing the reconciliation is the integrator's.)
- **Global keyboard shortcuts** — the shortcuts in *Application Shell & Navigation* are bound document-wide and override the browser default, regardless of which element has focus or which modal is open. In a host app they may collide with the host's own bindings.
- **URL hash as view state** — the app stores its current view (selected snippet, open dataset, chart-builder target) in the URL hash and reads it on load (see *Navigation & Shareable URL State*). A host that owns routing will need to share or namespace the hash.
- **Local browser storage** — all state persists to local browser storage across the tiers in *Data Model & Persistence*; storage keys are not namespaced against a co-resident host app.
- **Full-window workspace** — the layout assumes a wide, app-owned viewport (header, three panes, and modals). Hosting it within a smaller region falls under the small-screen fallback above.
## Performance & Responsiveness
- **Live editing stays fluid**: typing in the editor must remain smooth regardless of spec size; rendering must never block input.
- **Debounced rendering**: preview rendering is deferred until the user pauses typing, by a user-configurable delay (see *Settings* / *Live Preview*), so rapid keystrokes do not cause continuous re-rendering.
- **Non-blocking renders**: while a render is in progress the UI stays interactive; a busy indication may overlay the preview but must not freeze editing or navigation.
- **Auto-save is cheap and silent**: persisting the working draft must not interrupt typing or cause visible stalls (see *Spec Editor & Draft/Published Workflow*).
- **Scales with the library**: search, sort, and list rendering must stay responsive with a large number of snippets, and large datasets must be handled by the high-capacity dataset store rather than inflating snippet storage (see *Data Model & Persistence*).
## Accessibility
- **Keyboard operable**: all primary actions are reachable from the keyboard — the global shortcuts (see *Application Shell & Navigation*) plus standard tab/focus traversal of controls, lists, and forms.
- **Modal focus management**: opening a modal moves focus into it and returns focus sensibly on close; **Escape** closes the active modal; focus is contained within an open modal.
- **Labelled controls**: form fields, toggles, and icon-only buttons carry accessible names so assistive technology can announce them.
- **Reduced motion**: animations and transitions (toast fades, etc.) are suppressed when the user's system requests reduced motion.
- **Contrast**: text and interactive elements meet legible contrast in every offered UI theme; a theme that cannot meet contrast in part of the UI is not considered complete (see *Settings*).
## Reliability & Data Safety
- **No silent data loss**: edits are auto-saved as drafts; a known-good published version is always preserved separately (see *Spec Editor & Draft/Published Workflow*).
- **Confirm destructive actions**: deleting snippets or datasets, reverting a draft, and resetting settings require explicit confirmation.
- **Warn before storage failure**: snippet storage usage is surfaced with escalating warnings as it fills, and the user is told when a save fails rather than losing data silently (see *Snippet Library*).
- **Non-destructive import**: importing always merges with existing data and never overwrites or removes it; on failure the existing workspace is left unchanged (see *Import & Export*).
- **Resilient rendering**: an invalid or unrenderable spec produces a readable error and recovers automatically when fixed; it never leaves the app in a broken state (see *Live Preview*).
- **State survives reload**: the current selection/view is restored from the URL, and all data persists across reloads and sessions (see *Application Shell & Navigation*, *Data Model & Persistence*).
## Privacy & Security
- **Local-only data**: all snippets, datasets, and settings stay in the browser. No user content is transmitted to any server, and the app requires no account or login.
- **User-initiated network only**: the only outbound requests for user content are fetches of URL-sourced datasets or remote data referenced by a spec, which the user explicitly created (see *Datasets*). The app performs no background upload of user content.
- **Client-side rendering of untrusted input**: specs and data are user-authored and rendered locally; rendering must fail safely on malformed input rather than crashing the app.
## Internationalization
- **Locale-aware formatting where it exists**: date rendering follows the user's chosen format mode (see *Settings*). Full UI translation is out of scope unless explicitly added later; the spec does not require multiple UI languages.
+31
View File
@@ -0,0 +1,31 @@
# Astrolabe — Product Specification
A UX/behavioral specification of **Astrolabe**, a browser-based snippet manager for [Vega-Lite](https://vega.github.io/vega-lite/) visualizations. It describes *what the app does* from the user's perspective so it can be recreated on any web/HTML/TS stack.
## How to read this spec
- Start with [00 · Product Overview](00-product-overview.md) for orientation and the glossary.
- Each subsequent file is one feature area and can be read on its own; they cross-reference each other by title.
- Every section describes intended behavior plus testable acceptance points ("The user can…", "When X, the system…").
## What this spec deliberately omits
- **Implementation.** No frameworks, libraries, languages, storage technologies, or code architecture are prescribed. Storage is described behaviorally (e.g. "persists locally across sessions", capacity tiers), not by naming a technology.
- **Visual design.** Structural layout (panes, regions, modal vs inline, where controls live) is specified; concrete styling, colors, and the app's visual aesthetic are left to the implementer.
- **Domain exception.** Vega-Lite and its vocabulary (specs, marks, encoding channels, field types) and data-format names (JSON, CSV, TSV, TopoJSON) *are* named — they are the product domain, not implementation choices.
## Contents
| # | Section |
|---|---------|
| 00 | [Product Overview](00-product-overview.md) |
| 01 | [Application Shell & Navigation](01-application-shell.md) |
| 02 | [Snippet Library](02-snippet-library.md) |
| 03 | [Spec Editor & Draft/Published Workflow](03-editor-and-drafts.md) |
| 04 | [Live Preview](04-live-preview.md) |
| 05 | [Datasets](05-datasets.md) |
| 06 | [Chart Builder](06-chart-builder.md) |
| 07 | [Settings](07-settings.md) |
| 08 | [Import & Export](08-import-export.md) |
| 09 | [Data Model & Persistence](09-data-model.md) |
| 10 | [Non-Functional Requirements](10-non-functional.md) |