mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Implement M1.5 visual design foundation: tokens, IBM Plex, theme toggle, chart themes
This commit is contained in:
@@ -46,7 +46,7 @@ doc before implementing.
|
||||
|---|-----------|---------|------|
|
||||
| **M0** | Skeleton ✅ | Repo builds, tests run, empty shell renders | — |
|
||||
| **M1** | **MVP core loop** | Author a Vega-Lite snippet, see it render live, it persists | §02, §03A–C, §04, §09A |
|
||||
| **M1.5** | Visual design foundation | Apply the design language: tokens, IBM Plex, restyled M1 surfaces, chart theme | [arch 09](architecture/09-visual-design.md) |
|
||||
| **M1.5** | Visual design foundation ✅ | Apply the design language: tokens, IBM Plex, restyled M1 surfaces, chart theme | [arch 09](architecture/09-visual-design.md) |
|
||||
| **M2** | Editor robustness | Draft/Published, validation, schema autocomplete, fit modes | §03D–E, §04, §07(editor) |
|
||||
| **M3** | Datasets | Named reusable data + reference resolution in preview | §05, §03F, §09B |
|
||||
| **M4** | Chart Builder | No-JSON chart composition from a dataset | §06 |
|
||||
@@ -113,7 +113,7 @@ preview, and have it survive reload. Single source kind: inline-data specs only
|
||||
|
||||
---
|
||||
|
||||
## M1.5 · Visual design foundation → *make the MVP look like itself*
|
||||
## M1.5 · Visual design foundation ✅ (done) → *make the MVP look like itself*
|
||||
|
||||
**Goal:** apply our design language so the running MVP looks deliberate, and every
|
||||
later milestone builds on settled tokens instead of placeholders. The expensive part
|
||||
@@ -134,6 +134,11 @@ and the companion `visual-specimen.html`.
|
||||
raw hexes, no hardcoded hues in components.
|
||||
- Establish the reusable component conventions (buttons, fields, list rows, status,
|
||||
focus ring) that M2–M6 reuse.
|
||||
- **Header theme toggle** (pulled forward from M5): a one-click light⇄dark control,
|
||||
persisted via the `ui.theme` settings key (a minimal forward-compatible
|
||||
`settings-store` adapter the full M5 UserSettings store will absorb). Hydrated
|
||||
before first paint (no FOUC). Justified: the theme system was already complete,
|
||||
so dogfooding dark mode through M2–M4 beat waiting for the Settings modal.
|
||||
|
||||
**Core**
|
||||
- Align `src/core/vega-themes.ts`: chart `Config` per theme + a categorical
|
||||
@@ -148,6 +153,15 @@ and the companion `visual-specimen.html`.
|
||||
- Keyboard focus ring visible; text/UI contrast passes AA in light and dark.
|
||||
- No placeholder styling remains on the M1 surfaces.
|
||||
|
||||
**Verified:** `typecheck` + `test` (61 passing, incl. `vega-themes.test.ts`) +
|
||||
`build` (Plex woff2, all script subsets, bundled & precached via the PWA
|
||||
`globPatterns`). Both themes screenshotted via the real
|
||||
app (chrome + Monaco + chart all repaint on theme flip); focus ring visible.
|
||||
Notes from the build-out: the placeholder `'experimental'` theme was renamed to
|
||||
`'dark'` (the settled name); the swappable `[data-accent]` layer landed with
|
||||
indigo as the robust default (no switcher UI until M5); Monaco's `fontFamily` is
|
||||
set to Plex Mono explicitly since it can't read the CSS token.
|
||||
|
||||
---
|
||||
|
||||
## M2 · Editor robustness
|
||||
@@ -260,12 +274,15 @@ the reference.
|
||||
- `export-envelope.ts` — build the `{version, exportedAt, exportedBy, snippets, datasets}` envelope.
|
||||
|
||||
**Infrastructure**
|
||||
- `settings-store.ts` (localStorage); `ux-prefs` for sort + panel layout (§09D).
|
||||
- `settings-store.ts` (localStorage): **extend** the minimal M1.5 adapter (which
|
||||
already persists `ui.theme`) to the full UserSettings record; `ux-prefs` for sort
|
||||
+ panel layout (§09D).
|
||||
|
||||
**App**
|
||||
- Settings **modal** (Appearance/Editor/Performance/Formatting), Apply/Cancel/Reset,
|
||||
dirty indicator; wire render-debounce + theme + date-format through to the app
|
||||
(the themes themselves already exist from M1.5 — this adds the switcher UI).
|
||||
dirty indicator; wire render-debounce + theme + date-format through to the app.
|
||||
Theme already switches (M1.5 header toggle + chart/editor themes) — M5 surfaces
|
||||
it inside the modal too (sharing the same `ui.theme`) and wires the rest.
|
||||
- Header **Import**/**Export** (direct file dialog / download, no modal).
|
||||
- Date formatting util (smart/iso/custom) used by the library list.
|
||||
|
||||
|
||||
@@ -373,8 +373,8 @@ export function ThemeToggle() {
|
||||
const theme = useAppStore((s) => s.uiTheme);
|
||||
const setTheme = useAppStore((s) => s.setTheme);
|
||||
return (
|
||||
<button onClick={() => setTheme(theme === 'experimental' ? 'light' : 'experimental')}>
|
||||
{theme === 'experimental' ? '🌙' : '☀️'}
|
||||
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
||||
{theme === 'dark' ? '🌙' : '☀️'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ export interface UserSettings {
|
||||
editor: { fontSize: number; theme: string; minimap: boolean; wordWrap: 'on' | 'off';
|
||||
lineNumbers: 'on' | 'off'; tabSize: number };
|
||||
performance: { renderDebounce: number };
|
||||
ui: { theme: 'light' | 'experimental'; previewFitMode: 'default' | 'width' | 'height' | 'full' };
|
||||
ui: { theme: 'light' | 'dark'; previewFitMode: 'default' | 'width' | 'height' | 'full' };
|
||||
formatting: { dateFormat: 'smart' | 'iso' | 'custom'; customDateFormat: string };
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ const DEFAULTS: UserSettings = {
|
||||
};
|
||||
|
||||
// NOTE — editor.theme default is 'auto': the editor theme follows the app UI
|
||||
// theme (light -> light editor theme, experimental -> dark) via custom Monaco
|
||||
// theme (light -> light editor theme, dark -> dark) via custom Monaco
|
||||
// themes that match the app chrome, unless the user picks an explicit override.
|
||||
// The explicit-override option set (custom themes; whether to include High
|
||||
// Contrast or the stock Monaco themes) is still TBD — see spec §07's provisional
|
||||
@@ -315,6 +315,8 @@ const SORT_DEFAULTS = { sortBy: 'modified' as const, sortOrder: 'desc' as const
|
||||
> **Do:** keep a single complete `DEFAULTS` object as the source of truth and merge over it.
|
||||
> **Don't:** read individual keys with bespoke `?? fallback` at each call site; one stale default and the shapes drift.
|
||||
|
||||
> **Testing:** exercise localStorage adapters against an **injected stub** (`vi.stubGlobal('localStorage', …)`), not the ambient global. Under Node + happy-dom a non-functional Node `localStorage` global shadows happy-dom's, so relying on the ambient one fails with `localStorage.clear is not a function`. Applies to every prefs/settings adapter test (settings-store today; dataset-payload/prefs stores later).
|
||||
|
||||
---
|
||||
|
||||
## 6. Storage Tiers, Budgets & Quota Monitoring
|
||||
|
||||
@@ -139,7 +139,7 @@ export const lightChartConfig: Config = {
|
||||
view: { stroke: 'transparent' },
|
||||
};
|
||||
|
||||
export const experimentalChartConfig: Config = {
|
||||
export const darkChartConfig: Config = {
|
||||
background: 'transparent',
|
||||
font: '"Inter", sans-serif',
|
||||
title: { fontSize: 15, fontWeight: 600, color: '#f4f4f5' },
|
||||
@@ -167,7 +167,7 @@ import type { UiTheme } from './theme'; // core-local — never import from src/
|
||||
|
||||
const CHART_CONFIG: Record<UiTheme, Config> = {
|
||||
light: lightChartConfig,
|
||||
experimental: experimentalChartConfig,
|
||||
dark: darkChartConfig,
|
||||
};
|
||||
|
||||
export function chartConfigFor(theme: UiTheme): Config {
|
||||
@@ -190,6 +190,26 @@ re-rendering picks up the new config and the chart restyles automatically.
|
||||
- **Don't** let the user's stored spec carry a `config`; the theme config is
|
||||
applied at embed time via the embed options, leaving the spec theme-agnostic.
|
||||
|
||||
### Theme flow (end to end)
|
||||
|
||||
Theme spans several layers; the path is:
|
||||
|
||||
`AppStore.uiTheme` (+ `toggleTheme`) → `orchestration/theme.ts` mirrors it onto
|
||||
`<html data-theme>` and writes through to `infrastructure/settings-store.ts`
|
||||
(localStorage `ui.theme`). On load, `initTheme()` — called from `main.tsx`
|
||||
**before** `createRoot().render` — hydrates the saved theme. Chart and editor
|
||||
follow by subscribing to `uiTheme`: `LivePreview` re-embeds with
|
||||
`chartConfigFor(theme)`, `SpecEditor` sets the Monaco theme. UI chrome repaints
|
||||
purely from the `[data-theme]` token swap in `styles/tokens.css`. The header
|
||||
`ThemeToggle` is the user control.
|
||||
|
||||
- **Do** hydrate the theme **synchronously before first paint** — an async
|
||||
hydrate (e.g. inside `initApp`) flashes the default theme on load.
|
||||
- **Do** keep the store browser-free: the `data-theme` write and the localStorage
|
||||
write-through live in `orchestration/theme.ts`, never in the store or a component.
|
||||
- The control currently lives in the header; spec §07 houses it in the Settings
|
||||
modal (M5), which will share the same `ui.theme` key.
|
||||
|
||||
---
|
||||
|
||||
## 4. Field-Name Escaping
|
||||
|
||||
@@ -60,8 +60,9 @@ were choices, not drift:
|
||||
## 3. Tokens
|
||||
|
||||
All tokens are CSS custom properties on `:root`, themed by overriding them on
|
||||
`[data-theme]` (and, for accent, `[data-accent]`). The specimen is the live source
|
||||
of truth for values until they're ported to `styles/tokens.css`.
|
||||
`[data-theme]` (and, for accent, `[data-accent]`). As of M1.5 the settled values
|
||||
live in `styles/tokens.css`; the specimen remains the sandbox for trying new
|
||||
tokens/themes before porting them across.
|
||||
|
||||
### 3.1 Typography — IBM Plex
|
||||
|
||||
@@ -172,7 +173,7 @@ The chart `Config` is themed to match the app, per theme:
|
||||
| Artifact | Role |
|
||||
|---|---|
|
||||
| [`visual-specimen.html`](./visual-specimen.html) | Living preview + token sandbox. Iterate here first |
|
||||
| `styles/tokens.css` | The settled tokens (currently placeholders) — port from the specimen |
|
||||
| `styles/tokens.css` | The settled tokens — ported from the specimen in M1.5 |
|
||||
| `styles/base.css` | Font wiring (`@fontsource`), reset, reduced-motion |
|
||||
| component `*.module.css` | Consume tokens only; no raw hexes, no hardcoded hue |
|
||||
| `src/core/vega-themes.ts` | Chart `Config` per theme; categorical palettes |
|
||||
@@ -196,7 +197,7 @@ Convention: clone under `/Users/oleh/code/reference/` with
|
||||
| **Principles / the "why"** (philosophy, 2x grid, color rationale, type, motion, icon geometry) | `design-language-website` | `src/pages/`: `philosophy/principles.mdx`, `2x-grid.mdx`, `color.mdx`, `typography/*.mdx`, `animation/overview.mdx`, `iconography/ui-icons/design.mdx` (~1.4 GB clone — image-heavy; the MDX is what we want) |
|
||||
| **Token values** (gray/blue ramps, type scale, font families, motion durations/easings, theme role→value maps) | `carbon` | `packages/colors/src/colors.ts`, `packages/type/src/{scale,fontFamily,fontWeight}.ts`, `packages/motion/src/index.ts`, `packages/themes/src/{white,g100}.ts` |
|
||||
| **Component-level usage guidance** | `carbon-website` | `src/pages/**/*.mdx` |
|
||||
| **Data-viz categorical chart palette** (for `vega-themes.ts` `range.category`) | `carbon-charts` | *not yet cloned* — clone when we do the chart-theming pass |
|
||||
| **Data-viz categorical chart palette** (for `vega-themes.ts` `range.category`) | `carbon-charts` | cloned in M1.5 → `packages/core/scss/_color-palette.scss` (the `'14'` pairing, white + g100); token→hex resolved against `carbon` `packages/colors/src/colors.ts` |
|
||||
|
||||
> The decisions we made *from* these sources are captured above (§1–6) and in the
|
||||
> specimen, so we don't need to re-derive them — only return to the repos to extend
|
||||
|
||||
@@ -13,11 +13,15 @@ Astrolabe provides a **Settings** modal where users tune appearance, the spec ed
|
||||
|
||||
### Appearance
|
||||
|
||||
Controls the overall UI theme. Choosing the experimental Dark theme switches the whole application chrome to a dark presentation.
|
||||
Controls the overall UI theme. Choosing the Dark theme switches the whole application chrome to a dark presentation.
|
||||
|
||||
| Setting | Options | Default |
|
||||
| -------- | ----------------------------- | ------- |
|
||||
| UI theme | Light, Experimental Dark | Light |
|
||||
| Setting | Options | Default |
|
||||
| -------- | ------------- | ------- |
|
||||
| UI theme | Light, Dark | Light |
|
||||
|
||||
The UI theme is also exposed as a **header toggle** for one-click switching; it
|
||||
reads and writes the same persisted `ui.theme` value as this Appearance control,
|
||||
so the two always agree. (The toggle shipped in M1.5, ahead of this modal.)
|
||||
|
||||
### Editor
|
||||
|
||||
@@ -33,7 +37,7 @@ These settings configure the spec editor used to edit Vega-Lite specs (see *Spec
|
||||
| Tab size | Integer number of spaces | 2 |
|
||||
|
||||
- Font size is chosen along a 10–18 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.
|
||||
- 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, dark → 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.
|
||||
|
||||
@@ -71,7 +71,7 @@ Both **Snippet** and **Dataset** records carry a numeric `version` recording the
|
||||
| `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.theme` | string | App theme: `light` or `dark`. |
|
||||
| `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`. |
|
||||
|
||||
Reference in New Issue
Block a user