mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Fonts: carry uploaded faces through export/import + embed in SVG export
This commit is contained in:
@@ -6,13 +6,13 @@ Separately, a single chart can be exported on its own — its spec or its render
|
||||
|
||||
## Export
|
||||
|
||||
Export produces one downloadable JSON file containing every snippet (see _Snippet Library_), every dataset (see _Datasets_), and every custom chart theme (see _Live Preview → Chart theme_), wrapped in an envelope carrying format metadata.
|
||||
Export produces one downloadable JSON file containing every snippet (see _Snippet Library_), every dataset (see _Datasets_), every custom chart theme (see _Live Preview → Chart theme_), and every uploaded font face (see _Live Preview → Chart theme → fonts_), wrapped in an envelope carrying format metadata.
|
||||
|
||||
- **Trigger**: the **Export** header control runs the export immediately (no intermediate dialog).
|
||||
- **Contents**: all snippets, datasets, and custom chart themes 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 or themes exist.
|
||||
- **Contents**: all snippets, datasets, custom chart themes, and uploaded fonts 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, themes, or fonts 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, 2 datasets and 1 theme" (the dataset and theme clauses are omitted when their counts are zero; singular/plural wording adapts to the counts).
|
||||
- **Feedback**: on success a toast reports the counts, e.g. "Exported 4 snippets, 2 datasets and 1 theme" (the dataset, theme, and font clauses are omitted when their counts are zero; singular/plural wording adapts to the counts).
|
||||
|
||||
### Export envelope shape
|
||||
|
||||
@@ -31,6 +31,9 @@ The downloaded file is a single JSON object: an envelope with a format `version`
|
||||
],
|
||||
"themes": [
|
||||
/* full custom chart theme objects (see Data Model) */
|
||||
],
|
||||
"fonts": [
|
||||
/* uploaded font records, bytes base64-encoded (see Data Model) */
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -39,6 +42,7 @@ The downloaded file is a single JSON object: an envelope with a format `version`
|
||||
- `exportedAt` — ISO 8601 timestamp of the export.
|
||||
- `exportedBy` — fixed identifier `"Astrolabe"`.
|
||||
- `snippets` / `datasets` / `themes` — 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.) `themes` is additive: exports always write it, and importers treat it as optional, so pre-theme envelopes remain valid `"1.0"` files.
|
||||
- `fonts` — the uploaded font faces, each a complete record with its bytes **base64-encoded** (JSON cannot carry binary). Without this a theme or snippet referencing an uploaded font would import on another machine with only the family name, falling back to a system font. Like `themes`, `fonts` is additive — exports always write it, importers treat it as optional, so older envelopes remain valid `"1.0"` files.
|
||||
|
||||
## Per-chart export
|
||||
|
||||
@@ -53,7 +57,7 @@ A single chart can be exported on its own, separately from the whole-workspace E
|
||||
**Image** (available only when a chart is currently rendered — the actions are disabled, with an explanatory line, while the preview is empty or showing an error):
|
||||
|
||||
- **Download PNG** — a rasterized image of the chart as shown.
|
||||
- **Download SVG** — a vector image of the chart as shown.
|
||||
- **Download SVG** — a vector image of the chart as shown. When the chart uses an **uploaded** font (not a built-in roster or system family), that face is embedded into the SVG as a base64 `@font-face` rule, so the file renders the right type off-app instead of falling back to a system font; this happens automatically, with no option to configure.
|
||||
- **Resolution** (PNG) — `1×` / `2×` / `3×`, default `1×`. These are multipliers **of the display's pixel density**, so `1×` already matches on-screen crispness on a high-DPI (Retina) display; higher values produce larger images for print or zoom. (SVG is resolution-independent and ignores this.)
|
||||
- **Background** — `Theme` (default) / `White` / `None`. The chart itself renders on a transparent background (so on screen it shows the pane colour); export therefore fills it: _Theme_ matches the active theme's background, _White_ is always white, _None_ keeps it transparent. Applies to both PNG and SVG.
|
||||
|
||||
@@ -73,8 +77,8 @@ Import lets the user pick a JSON file from their device; its contents are normal
|
||||
|
||||
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; optional `datasets` and `themes` arrays are imported too.
|
||||
- **Bare array of snippets** — a top-level JSON array is treated as a list of snippets (no datasets or themes).
|
||||
- **Astrolabe export envelope** — an object with a `version` and a `snippets` array; optional `datasets`, `themes`, and `fonts` arrays are imported too.
|
||||
- **Bare array of snippets** — a top-level JSON array is treated as a list of snippets (no datasets, themes, or fonts).
|
||||
- **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.
|
||||
@@ -88,10 +92,11 @@ A snippet is treated as already in current Astrolabe format when it carries an I
|
||||
|
||||
- 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 and custom themes are imported **before** snippets so that snippet dataset references can resolve.
|
||||
- Imported custom themes always receive fresh ids from the theme library; an envelope's theme ids never displace existing records.
|
||||
- Datasets, custom themes, and uploaded fonts are imported **before** snippets so that snippet dataset references resolve and the whole import rolls back together if the snippet write fails.
|
||||
- Imported custom themes (and fonts) always receive fresh ids from their library; an envelope's ids never displace existing records.
|
||||
- An unusable font record (missing family or bytes, or bytes that aren't valid base64) is skipped; the rest of the import continues.
|
||||
|
||||
### Name conflicts (datasets and themes)
|
||||
### Name conflicts
|
||||
|
||||
When an imported dataset's or custom theme's name already exists in the library, it is auto-renamed to a unique name rather than overwriting the existing one (see _Datasets_).
|
||||
|
||||
@@ -100,6 +105,8 @@ When an imported dataset's or custom theme's name already exists in the library,
|
||||
- A dataset rename is propagated into the imported snippets that reference it; theme renames need no propagation (nothing references a theme by name).
|
||||
- If a single dataset fails to import, it is skipped and the rest of the import continues.
|
||||
|
||||
**Fonts conflict differently — skip, not rename.** A font is identified by its family, which is the key embedded directly in a config's font slots, so a same-named face already in the library satisfies any incoming reference. When an imported font's family already exists, the **incoming face is skipped** and the existing one is kept (the references resolve to it) — rather than renamed to a copy. This also means re-importing your own backup adds no duplicate "Font 2" copies. Skipped fonts are listed in the import's warning toast.
|
||||
|
||||
### Storage limit handling
|
||||
|
||||
Snippet storage has an approximate 5 MB budget (see _Snippet Library_ storage monitor).
|
||||
@@ -110,8 +117,8 @@ Snippet storage has an approximate 5 MB budget (see _Snippet Library_ storage mo
|
||||
|
||||
### Feedback
|
||||
|
||||
- **Success**: a toast reports how many snippets (and datasets and themes, when any) were imported, e.g. "Imported 4 snippets, 2 datasets and 1 theme".
|
||||
- **Renames**: when datasets or themes 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 — even if the file carries datasets or themes.
|
||||
- **Success**: a toast reports how many snippets (and datasets, themes, and fonts, when any) were imported, e.g. "Imported 4 snippets, 2 datasets and 1 theme".
|
||||
- **Renames / skips**: when datasets or themes were renamed, or fonts were skipped as already-present, the success message is shown as a warning toast that also lists the renames and skips.
|
||||
- **Empty file**: if no snippets are found in the file, the user is informed ("No snippets found in file") and nothing is imported — even if the file carries datasets, themes, or fonts.
|
||||
- **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.
|
||||
|
||||
@@ -100,6 +100,7 @@ Some preferences persist independently of _UserSettings_ so they can update freq
|
||||
| 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. |
|
||||
| Theme store | All _CustomTheme_ records (G) | Local, separate store; records are small (a config object plus metadata). |
|
||||
| Font store | All _FontAsset_ records (H) | Local, separate store; holds raw font-file bytes, so it is sized like the dataset tier (per-face cap ~10 MB). |
|
||||
| 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.
|
||||
@@ -127,3 +128,23 @@ A **CustomTheme** is a user-named Vega-Lite config saved in the library and offe
|
||||
| `modified` | ISO-timestamp string | When the theme was last changed. |
|
||||
|
||||
Selection is keyed by `id` (not name) so renaming a theme never invalidates the persisted `ui.chartTheme`. A persisted `custom:<id>` whose record no longer exists is not an error: charts render with the house style until the record appears (themes hydrate asynchronously), and deleting the actively-selected theme resets the selection to `astrolabe` explicitly. Custom themes travel in the _Import & Export_ envelope alongside snippets and datasets (spec §08).
|
||||
|
||||
## H. FontAsset
|
||||
|
||||
A **FontAsset** is a user-uploaded font face stored once and reused across themes and snippets. It is added and managed in the _Theme Builder → Type_ panel (see _Live Preview_), and referenced from a config's font slots by its `family` — exactly as a dataset is referenced by name. The raw bytes are registered as a live `FontFace` so a chart measures and renders the real face.
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| ---------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | number | Unique numeric identifier (store key). |
|
||||
| `version` | number | Schema version of this record, for read-time migration (see _Schema versioning_ above). |
|
||||
| `family` | string | Unique CSS family name — the key configs reference (case-insensitive uniqueness, like datasets/themes). |
|
||||
| `data` | bytes | The raw font-file bytes (registered as a `FontFace`). In an export envelope these are **base64-encoded** (spec §08). |
|
||||
| `format` | `woff2`/`woff`/`ttf`/`otf` | Container format, from the file extension. |
|
||||
| `fileName` | string | Original file name, kept for display and provenance. |
|
||||
| `source` | `file`/`google` | Provenance. Only `file` (an upload) ships today; `google` is reserved for a later keyless-catalog tier. |
|
||||
| `axes` | array, optional | Variation axes for a variable font (parsed from `fvar`); drives the `FontFace` weight/width ranges. Absent for a static face. |
|
||||
| `size` | number | Byte length of `data`. |
|
||||
| `created` | ISO-timestamp string | When the font was first added. |
|
||||
| `modified` | ISO-timestamp string | When the font was last changed (e.g. renamed). |
|
||||
|
||||
A font is identified by its `family`. No theme or snippet stores a font field: the faces a config uses are derived by scanning its font slots (the config is the source of truth), so a snippet can use a font with no theme to carry it. Fonts travel in the _Import & Export_ envelope alongside snippets, datasets, and themes; on import a family clash **skips** the incoming face rather than renaming it (spec §08 → Name conflicts).
|
||||
|
||||
Reference in New Issue
Block a user