From 68a044752fe3f448b38b81679395ac2988ee0c5d Mon Sep 17 00:00:00 2001 From: Oleh Omelchenko Date: Wed, 10 Jun 2026 14:14:02 +0300 Subject: [PATCH] Refine header emphasis, field-on-layer treatment, and narrow-pane control collapse --- docs/IMPLEMENTATION-PLAN.md | 2 +- docs/architecture/09-visual-design.md | 121 +++++++++++++----- .../10-interaction-and-feedback.md | 42 +++++- docs/architecture/visual-specimen.html | 97 +++++++++++++- docs/spec/01-application-shell.md | 16 +-- src/app/App.module.css | 46 ++++++- src/app/App.tsx | 15 ++- src/app/components/Icon.tsx | 30 +++++ src/app/components/LivePreview.module.css | 11 +- src/app/components/SnippetLibrary.module.css | 43 +++++-- src/app/components/SnippetLibrary.tsx | 13 +- src/app/components/SortControl.module.css | 10 +- src/app/components/SortControl.tsx | 18 ++- src/app/components/SpecEditor.module.css | 40 ++++++ src/app/components/SpecEditor.tsx | 19 ++- src/app/stores/PanesStore.test.ts | 19 +-- src/app/stores/PanesStore.ts | 13 +- styles/tokens.css | 11 ++ 18 files changed, 478 insertions(+), 88 deletions(-) diff --git a/docs/IMPLEMENTATION-PLAN.md b/docs/IMPLEMENTATION-PLAN.md index 702a8ab..25a4efc 100644 --- a/docs/IMPLEMENTATION-PLAN.md +++ b/docs/IMPLEMENTATION-PLAN.md @@ -450,7 +450,7 @@ through `services/transfer.ts` (→ `normalizeImport` / envelope build), no moda IDB-transaction atomicity would require exposing a raw transaction from `db.ts` — see [arch 02](architecture/02-persistence.md)). - **A11y:** modal focus trap + return, labelled icon buttons, contrast in both themes (§10) — ✅ in place. -- **About & Privacy** and **Donate** modals ~~(§01)~~ ✅ (Donate URL is a placeholder +- **About** and **Donate** modals ~~(§01)~~ ✅ (Donate URL is a placeholder `DONATE_URL` pending the real link). - **Offline/installable:** the manifest now ships a full SVG icon set (favicon / maskable / monochrome) + `theme_color`, and the SW precaches the shell — the app is **installable**. diff --git a/docs/architecture/09-visual-design.md b/docs/architecture/09-visual-design.md index 317798c..ab0826d 100644 --- a/docs/architecture/09-visual-design.md +++ b/docs/architecture/09-visual-design.md @@ -94,15 +94,16 @@ and size is a relationship of 8 (with 2/4 as fine sub-steps): Color is expressed as **roles**, never raw hexes, so themes can repaint the whole UI by swapping one set of values. Borrowed from Carbon's layering model: -| Role token | Meaning | -| ---------------------------------------------------- | ----------------------------------------------------------------------------------- | -| `--bg` | App canvas (lowest layer) | -| `--layer-01` / `--layer-02` | Raised surfaces (panels, cards, popovers) — elevation by lightness step, not shadow | -| `--border` / `--border-strong` | Subtle and prominent separators | -| `--text` / `--text-secondary` / `--text-placeholder` | Text hierarchy | -| `--accent` / `--accent-hover` / `--accent-contrast` | The expressive accent — **swappable**; UI must never hardcode a hue | -| `--focus` | Focus-ring color (defaults to `--accent`) | -| `--support-error / -success / -warning / -info` | Status only — color = meaning | +| Role token | Meaning | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------- | +| `--bg` | App canvas (lowest layer) | +| `--layer-01` / `--layer-02` | Raised surfaces (panels, cards, popovers) — elevation by lightness step, not shadow | +| `--border` / `--border-strong` | Subtle and prominent separators | +| `--text` / `--text-secondary` / `--text-placeholder` | Text hierarchy | +| `--accent` / `--accent-hover` / `--accent-contrast` | The expressive accent — **swappable**; UI must never hardcode a hue | +| `--accent-soft` / `--accent-soft-hover` | Low-emphasis accent **wash** (accent mixed into `--bg`) for a tinted-but-quiet surface | +| `--focus` | Focus-ring color (defaults to `--accent`) | +| `--support-error / -success / -warning / -info` | Status only — color = meaning | - **Neutrals** use the Carbon gray ramp (`#f4f4f4 … #161616`) — accessible and legible. **Accent and theming are open**: the default accent is **deep teal** @@ -113,6 +114,17 @@ UI by swapping one set of values. Borrowed from Carbon's layering model: warning `#f1c21b`, info `#0043ce` — tuned per theme for contrast. - **Contrast:** target WCAG AA (4.5:1 text, 3:1 large/UI). Accent-on-`--bg` and text-on-`--accent` must both pass for any shipped theme. +- **Soft accent is derived, not hardcoded.** `--accent-soft` / + `--accent-soft-hover` are `color-mix(in srgb, var(--accent) 12–20%, var(--bg))`, + so the wash follows whatever accent + theme is active rather than carrying a + per-accent value. Use it for a surface that should be _noticed_ without competing + with a primary action (the header's **Donate** button). +- **Field-on-layer (Carbon layering).** A field is a layer _on top of_ the surface + it sits on — so it steps **toward `--bg`**, never to a darker gray. On the white + canvas and on a `--layer-01` panel alike, an input reads as **`--bg` fill + + `--border-strong` outline**, not a `--layer-02` fill. Stacking a darker-gray field + on a gray panel (canvas → `--layer-01` panel → `--layer-02` field) muddies into + three indistinct grays — the bug this rule prevents (the snippet metadata panel). ### 3.4 Shape & elevation @@ -138,9 +150,19 @@ UI by swapping one set of values. Borrowed from Carbon's layering model: ## 4. Component conventions - **Buttons:** square, 32px (compact) / 40px (default) tall. Variants: **primary** - (filled `--accent`), **secondary** (bordered), **ghost** (text-only), - **danger** (filled `--support-error`). 600-weight label. Clear hover/active and - a visible focus ring. + (filled `--accent`), **secondary** (bordered), **ghost** (text-only, borderless), + **soft-accent** (ghost on an `--accent-soft` wash — a low-emphasis solicitation, + e.g. Donate), **danger** (filled `--support-error`). 600-weight label. Clear + hover/active and a visible focus ring. +- **Emphasis hierarchy (Carbon button/usage).** A region carries **one** + high-emphasis (primary) button at most; everything else is lower emphasis. In + toolbars/headers full of utilities, the utilities go **ghost** so they recede + behind the work area and read as a row of equals — only the genuine call to + action is filled. Worked examples: the **editor toolbar** (Publish is the lone + primary; Extract/Revert are secondary, and collapse to icons when narrow — see + [arch 10 §8](10-interaction-and-feedback.md)), and the **header** (Datasets / + Import / Export / About are ghost; a divider then sets off the soft-accent Donate + and the ghost theme toggle). - **Hover is variant-specific:** filled buttons (primary/danger) **darken** (`--accent-hover` / a slight brightness drop); outlined/ghost buttons **gain a fill one elevation step above their surface** — on `--bg` → `--layer-01`, on a @@ -150,8 +172,10 @@ UI by swapping one set of values. Borrowed from Carbon's layering model: must step its hover fill up to match. - **Focus ring:** a 2px `--focus` outline (offset 1–2px). Always visible on keyboard focus — accessibility is non-negotiable (principle 4). -- **Fields** (text, textarea, select, search): square, 1px `--border`, `--layer-01` - fill, accent border + focus ring on focus. Mono font for spec/JSON inputs. +- **Fields** (text, textarea, select, search): square, 1px `--border-strong`, + **`--bg` fill** (field-on-layer, §3.3 — white on the canvas _and_ on a + `--layer-01` panel; never a darker-gray fill), accent border + focus ring on + focus. Mono font for spec/JSON inputs. - **List rows** (snippet library): compact, full-row hover (`--layer-01`), active row marked by an accent left-border + `--layer-01` fill, secondary metadata in `--text-secondary`. Row-level actions reveal on hover. @@ -232,15 +256,32 @@ Carbon and drawn `fill: currentColor`. **Core set** — recurring, cross-surface: -| Meaning | Carbon glyph | Form | Surfaces | -| -------------------- | --------------- | --------------------- | ------------------------------------------------- | -| Close / dismiss | `Close` (✕) | icon-only ⭐ | `ModalShell`, `Toaster` | -| Theme → dark | `Asleep` (moon) | icon-only ⭐ | `ThemeToggle` (shown when light) | -| Theme → light | `Light` (sun) | icon-only ⭐ | `ThemeToggle` (shown when dark) | -| References a dataset | `DataTable` | icon + text | Library row marker, Linked-datasets list | -| Add / create-new | `Add` | icon + text | Library "Create New Snippet", Datasets "New …" | -| Delete | `TrashCan` | icon-only ⭐ (danger) | Library row delete¹ — text "Delete" in the panel² | -| Unpublished draft | (CSS dot) | status-glyph | Library row (paired with a hidden label) | +| Meaning | Carbon glyph | Form | Surfaces | +| -------------------- | --------------- | ------------------------ | -------------------------------------------------------------------------------------------- | +| Close / dismiss | `Close` (✕) | icon-only ⭐ | `ModalShell`, `Toaster` | +| Theme → dark | `Asleep` (moon) | icon-only ⭐ | `ThemeToggle` (shown when light) | +| Theme → light | `Light` (sun) | icon-only ⭐ | `ThemeToggle` (shown when dark) | +| References a dataset | `DataTable` | icon + text | Library row marker, Linked-datasets list, header **Datasets**, editor **Extract**⁴ | +| Add / create-new | `Add` | icon + text → icon-only⁴ | Library "Create New Snippet" (collapses to "+" when the pane is narrow), Datasets "New …" | +| Delete | `TrashCan` | icon-only ⭐ (danger) | Library row delete¹ — text "Delete" in the panel² | +| Import workspace | `Upload` | icon + text | Header **Import** (a file is brought into the app) | +| Export workspace | `Download` | icon + text | Header **Export** (the workspace is written out) | +| About / information | `Information` | icon + text | Header **About** | +| Revert draft | `Reset` | icon + text → icon-only⁴ | Editor toolbar **Revert** (restore last published) | +| Live search | `Search` | icon-in-field⁵ | Library search box (leading magnifier; the input's `aria-label`/placeholder names the field) | +| Settings (gear) | `Settings` | icon-only ⭐ | Per-pane settings disclosures (Editor, Preview, Library dates) | +| Unpublished draft | (CSS dot) | status-glyph | Library row (paired with a hidden label) | + +**Pane-toggle set** — the one **custom** sub-family (not single Carbon glyphs): a +panel frame with one of three regions filled, where the filled bar's _position_ +encodes which pane it toggles. Icon-only by design — position is the meaning — each +carrying an `aria-label`: + +| Meaning | Glyph | Form | Surface | +| ------------------- | ------------------------------ | ------------ | ----------------- | +| Toggle library pane | panel frame, **left** filled | icon-only ⭐ | `PaneToggleStrip` | +| Toggle editor pane | panel frame, **centre** filled | icon-only ⭐ | `PaneToggleStrip` | +| Toggle preview pane | panel frame, **right** filled | icon-only ⭐ | `PaneToggleStrip` | **Status set** — Carbon's **filled** notification glyphs, one per severity. Unlike the outline UI set, these are coloured **by status** (not by surrounding text) and @@ -257,23 +298,37 @@ notifications/validation as they arrive): | Success | `CheckmarkFilled` | `--support-success` | `Toaster` (success) | | Info | `InformationFilled` | `--support-info` | `Toaster` (info) | -**Scoped set** — registered (glyph reserved) but single-surface and **not yet -implemented**: +**Scoped set** — single-surface, glyph **reserved** in the ledger but **not yet in +the `Icon` registry**: -| Meaning | Carbon glyph | Form | Surface / note | -| ---------------- | ------------------ | ----------- | ----------------------------------------------------- | -| Swap / transpose | `ArrowsHorizontal` | icon + text | Chart Builder "Swap X/Y" — one button; icon is polish | +| Meaning | Carbon glyph | Form | Surface / note | +| ---------------- | ------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Swap / transpose | `ArrowsHorizontal` | icon + text | Chart Builder "Swap X/Y" — the button **ships** today with an interim Unicode `⇄`, not a registry `Icon`. `ArrowsHorizontal` stays reserved here so the meaning is claimed; promote the button to it (add the `IconName` + glyph) when polishing the swap. | -⭐ = **icon-only set**: close + theme (truly universal), plus **delete** as a -deliberate destructive-row exception — a dense, repeated list action where a label -would cost more than it gives. ✕ means **close only**; delete is `TrashCan`, never -✕ (that collision is exactly what one-glyph-one-meaning forbids). +⭐ = **icon-only set** (the glyph alone names the control, via `aria-label`): the +**universal** glyphs `close` + `theme`; the conventional disclosure/affordance +glyphs `settings` (gear) and the **pane-toggle** trio (position is the meaning); +and **delete** as a deliberate destructive-row exception — a dense, repeated list +action where a label would cost more than it gives. `search` is _not_ ⭐: its +magnifier is a decorative lead-in to a labelled input (footnote ⁵), not a control +named by the glyph. ✕ means **close only**; delete is `TrashCan`, never ✕ (that +collision is exactly what one-glyph-one-meaning forbids). Admitting a glyph to ⭐ is +a contract change (§5.1 rule 4), not a per-component call. ¹ Row delete is hover/focus-revealed and reddens on hover/focus (arch 10 — reveal & destructive-intent rules). ² "Duplicate" and "Delete" in the detail panel stay **text** (label-first; lower frequency, not a dense row). ³ The raw warning yellow fails contrast on light surfaces, so the warning glyph uses `--support-warning-fg` -(darkened amber; the yellow `--toast-accent` stays on the decorative border). +(darkened amber; the yellow `--toast-accent` stays on the decorative border). ⁴ +**Responsive collapse**, not membership in the icon-only set: these are icon+text +that _shed the label_ under width pressure (the editor toolbar's secondary actions, +and the library's standalone Create CTA, when the pane is narrow — [arch 10 +§8](10-interaction-and-feedback.md)), keeping the accessible name in +`aria-label`/`title`. A degradation that preserves the name is distinct from a +permanent icon-only control (§5.1 rule 4), so it isn't a closed-set change. ⁵ +**Icon-in-field**: a decorative leading glyph _inside_ a labelled control — the +search input's magnifier is `aria-hidden`, and the input itself carries the +accessible name. Not icon-only (the control is named by its label, not the glyph). ### 5.3 Size diff --git a/docs/architecture/10-interaction-and-feedback.md b/docs/architecture/10-interaction-and-feedback.md index 60bc928..0c3da18 100644 --- a/docs/architecture/10-interaction-and-feedback.md +++ b/docs/architecture/10-interaction-and-feedback.md @@ -303,8 +303,10 @@ Carbon search/active-search, NN/g #3. This bullet is the contract; cite it, not **Resolved — library sort (APG menu-button + NN/g recognition).** The Sort control (spec §02) reuses the **disclosure popover** primitive (the settings-popover model above), **not** -an ARIA menu — but its trigger shows the **current state for recognition** ("Sort: Modified -↓"), per NN/g #6 (recognition over recall), instead of a bare gear. Fields are **Modified / +an ARIA menu — but its trigger shows the **current state for recognition** ("Modified +↓"), per NN/g #6 (recognition over recall), instead of a bare gear. (The visible text drops +the "Sort:" verb prefix to stay compact in the narrow library rail — §8; the full name +"Sort by Modified, descending" rides in `aria-label`.) Fields are **Modified / Created / Name / Size**; the active field shows a **direction arrow** (↓ desc / ↑ asc) in both the trigger and the field row, and the arrow's meaning is mirrored into the field's `aria-label` ("Modified, descending") so it isn't carried by the glyph alone. **Selection @@ -454,6 +456,42 @@ iconography contract, [arch 09 §5](09-visual-design.md).) --- +## 8. Space-constrained controls (responsive collapse) + +The three work panes resize independently, so a toolbar's room is a function of its +**pane's** width, not the viewport's. Controls must stay usable at the pane minimum +without clipping, wrapping, or crowding. + +- **Query the pane, not the window.** Use a CSS **container query** + (`container-type: inline-size` on the row, `@container` on the controls), not a + media query — the pane width is what changed. Scope the container to the toolbar + row itself, away from heavy children (e.g. the Monaco editor) whose own layout + shouldn't inherit size containment. +- **Shed labels under pressure; keep a contested primary labelled.** When a toolbar + would wrap or crowd, **secondary** actions collapse to icon-only and the label + moves to `aria-label`/`title`. A **primary that shares the row with secondaries** + keeps its text — the label is what marks it as _the_ action to take (the editor + toolbar below ~480px: Publish stays "Publish"; Extract/Revert become glyphs). A + **standalone primary CTA**, whose prominence is carried by fill + size + position + rather than its words, _may_ collapse to a universal-set icon at the pane floor + (the library's "Create New Snippet" → "+" below ~250px). Either way it's a + degradation that preserves the accessible name — distinct from the closed + icon-only set (arch 09 §5.1 rule 4). +- **Trim a compact trigger's prose, not its state.** A disclosure trigger that names + its current state for recognition (NN/g #6) may drop the **verb prefix** to fit a + narrow rail — the library Sort trigger shows "Modified ↓", not "Sort: Modified ↓" + — but the full name (`aria-label="Sort by Modified, descending"`) is preserved for + assistive tech, so only redundant visible words are cut. +- **A flex control must be able to shrink.** A side control (a Sort button) carries + `flex: 0 0 auto` so the flexible field (search) absorbs the slack; the field's + `` needs `min-width: 0`, or its intrinsic ~20ch width overflows the slot + and overlaps its neighbour. Right-aligned toolbars (`justify-content: flex-end`) + clip their **leftmost** item on overflow — left-align so the trimmable end is a + settings affordance, not a primary control, and size the pane minimum so it + doesn't overflow at all. + +--- + ## Do / Don't **Do** diff --git a/docs/architecture/visual-specimen.html b/docs/architecture/visual-specimen.html index 083dcc1..095d8aa 100644 --- a/docs/architecture/visual-specimen.html +++ b/docs/architecture/visual-specimen.html @@ -87,6 +87,13 @@ --on-status: #161616; } + /* Soft accent — derived wash (mirrors styles/tokens.css). References --accent + + --bg, so it follows whatever accent/theme is active. */ + [data-theme] { + --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--bg)); + --accent-soft-hover: color-mix(in srgb, var(--accent) 20%, var(--bg)); + } + /* --- Accent: the FREE, swappable layer (theme-aware). Deep teal is the default; see styles/tokens.css for the shipped source of truth. --- */ [data-accent='teal'] { @@ -436,6 +443,29 @@ .btn-ghost:hover { background: var(--layer-01); } + /* Header utility ghost: text-coloured, recedes; hover steps up one layer. */ + .btn-ghost-util { + background: transparent; + color: var(--text); + gap: var(--space-2); + } + .btn-ghost-util:hover { + background: var(--layer-02); + } + /* Soft-accent: a low-emphasis accent wash (Donate) — noticed, not shouting. */ + .btn-soft { + background: var(--accent-soft); + color: var(--accent-hover); + } + .btn-soft:hover { + background: var(--accent-soft-hover); + } + .hdr-divider { + width: var(--border-width); + height: 20px; + background: var(--border-strong); + margin: 0 var(--space-2); + } .btn-danger { background: var(--support-error); color: var(--on-status); @@ -459,13 +489,16 @@ font-size: 12px; color: var(--text-secondary); } + /* Field-on-layer (arch 09 §3.3): a field is a layer above its surface, so it + steps toward --bg (white) with a --border-strong outline — on the canvas + and on a gray panel alike — never a darker-gray fill. */ .input, .select, .textarea { font: inherit; font-size: 14px; color: var(--text); - background: var(--layer-01); + background: var(--bg); border: var(--border-width) solid var(--border-strong); border-radius: var(--radius); padding: var(--space-3) var(--space-4); @@ -930,6 +963,26 @@

Tab through to see the focus ring (2px accent outline).

+ +

Emphasis hierarchy — a header cluster

+

+ Utilities go ghost (icon + text, borderless) so they recede; + a divider sets off Donate on a soft-accent wash + — one low-key solicitation, no border. (Carbon: one high-emphasis action per + region; utilities at low emphasis.) +

+
+ + + + + + +
@@ -1027,10 +1080,48 @@ delete + + + import + + + + export + + + + info + + + + revert + + + + search + + + + settings + + + + pane-library + + + + pane-editor + + + + pane-preview +
- In context: icon + text is the default; icon-only is the universal/destructive - exception. ✕ means close only — delete is the trash glyph. + The full Core + pane-toggle set (arch 09 §5.2); the four status glyphs appear, + coloured, in the Toasts section below. In context: icon + text is the default; + icon-only is the universal/affordance/destructive exception. ✕ means close only — + delete is the trash glyph.
diff --git a/docs/spec/01-application-shell.md b/docs/spec/01-application-shell.md index cd4ca26..f5b66e8 100644 --- a/docs/spec/01-application-shell.md +++ b/docs/spec/01-application-shell.md @@ -27,18 +27,18 @@ 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_). | -| About & Privacy | The About & Help modal (keyboard shortcuts, about, and privacy information). | -| Donate | The Donate modal. | +| 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_). | +| About | The About & Help modal (keyboard shortcuts, about, and privacy information). Labelled "About"; privacy lives inside. | +| Donate | The Donate modal. | Notes: - Import and Export act directly (file dialog / file download); they do not open in-app modals. -- The Datasets, About & Privacy, and Donate entry points each open a modal (see _Modal System_). +- The Datasets, About, and Donate entry points each open a modal (see _Modal System_). - **Settings are not a header entry point.** A design review (see _Settings_) distributed preferences to the panes they affect — the appearance theme is a header toggle, and the Editor / Performance / Formatting clusters open from a gear control in their own pane. There diff --git a/src/app/App.module.css b/src/app/App.module.css index 28e45b4..2dfe36b 100644 --- a/src/app/App.module.css +++ b/src/app/App.module.css @@ -61,11 +61,20 @@ flex: 1; } -/* Header text-button (Datasets, and future header actions). */ +/* + * Header utility buttons (Datasets / Import / Export / About) — low-emphasis + * ghost buttons: borderless, icon + label, so they recede behind the work area + * and read as a row of equals (Carbon: utilities in toolbars/dashboards belong at + * low emphasis). The transparent border holds the box size so hover doesn't shift + * neighbours. Matches the ThemeToggle's ghost icon button beside them. + */ .headerButton { + display: inline-flex; + align-items: center; + gap: var(--space-2); height: 32px; - padding: 0 var(--space-4); - border: var(--border-width) solid var(--border-strong); + padding: 0 var(--space-3); + border: var(--border-width) solid transparent; border-radius: var(--radius); background: transparent; color: var(--text); @@ -73,11 +82,13 @@ font-size: 13px; font-weight: 500; cursor: pointer; - transition: background var(--dur-fast) var(--ease); + transition: + background var(--dur-fast) var(--ease), + color var(--dur-fast) var(--ease); } .headerButton:hover { - background: var(--layer-01); + background: var(--layer-02); } .headerButton:focus-visible { @@ -85,6 +96,31 @@ outline-offset: 2px; } +/* Vertical rule separating the utilities from Donate + the theme toggle. */ +.headerDivider { + flex: 0 0 auto; + width: var(--border-width); + height: 20px; + margin: 0 var(--space-2); + background: var(--border-strong); +} + +/* + * Donate — the one solicitation in the header. A soft-accent wash lifts it just + * above the ghost utilities without competing with a primary action (it carries + * no border; the tint alone distinguishes it). Placed after .headerButton:hover + * so its own hover state wins the equal-specificity tie. + */ +.donate { + background: var(--accent-soft); + color: var(--accent-hover); +} + +.donate:hover { + background: var(--accent-soft-hover); + color: var(--accent-hover); +} + /* The Import file picker is driven programmatically by its header button. */ .hiddenInput { display: none; diff --git a/src/app/App.tsx b/src/app/App.tsx index 3258d03..9f0a658 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -4,6 +4,7 @@ import { LivePreview } from './components/LivePreview'; import { ModalShell } from './components/ModalShell'; import { Onboarding } from './components/Onboarding'; import { PaneSplitHandle } from './components/PaneSplitHandle'; +import { Icon } from './components/Icon'; import { PaneToggleStrip } from './components/PaneToggleStrip'; import { ResizeHandle } from './components/ResizeHandle'; import { SnippetLibrary } from './components/SnippetLibrary'; @@ -74,6 +75,11 @@ export function App() {

Astrolabe

v{__APP_VERSION__} + {/* Header actions establish a hierarchy (Carbon button/usage → one + high-emphasis action per region; utilities in dashboards/toolbars read + better at low emphasis). The utilities are ghost (borderless) buttons + with a leading icon for scanning; a divider then sets off Donate — the + one solicitation, given a soft-accent wash — and the theme toggle. */} +