diff --git a/docs/IMPLEMENTATION-PLAN.md b/docs/IMPLEMENTATION-PLAN.md
index fe80ce8..845d7f7 100644
--- a/docs/IMPLEMENTATION-PLAN.md
+++ b/docs/IMPLEMENTATION-PLAN.md
@@ -459,9 +459,10 @@ tablet tool; phones are out of scope. So the touch/installable surface we target
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** and **Donate** modals ~~(§01)~~ ✅. The project solicits nothing for
- itself — **Donate** redirects to Ukraine's defense (`savelife.in.ua`, the Come Back
- Alive foundation), where the author forwards any project donations anyway.
+- **About** and **Support** modals ~~(§01)~~ ✅. The project solicits nothing for
+ itself — **Support** offers feedback to the author and redirects donations to Ukraine's
+ defense (`savelife.in.ua`, the Come Back Alive foundation), where the author forwards any
+ project donations anyway.
- **Offline/installable:** the manifest ships a full SVG icon set (favicon / maskable /
monochrome) + a 180×180 `apple-touch-icon.png` (iPad add-to-home-screen) + `theme_color`,
and the SW precaches the shell — the app is **installable**. ✅ Manual verification in a
diff --git a/docs/architecture/09-visual-design.md b/docs/architecture/09-visual-design.md
index 48faa0a..78f20bf 100644
--- a/docs/architecture/09-visual-design.md
+++ b/docs/architecture/09-visual-design.md
@@ -124,7 +124,7 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
`--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).
+ with a primary action (the header's **Support** button).
- **Field-on-layer (Carbon layering).** A field's fill is **one step off the
surface it sits on**, alternating like Carbon's field set: `--field-01` on the
canvas (gray on white / near-black on black), `--field-02` on a `--layer-01`
@@ -188,7 +188,7 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
`--accent`), **secondary** (1px `--border-strong`, `--bg` fill —
so a bordered control on a gray panel goes white, never a darker gray), **ghost** (text-only, borderless; a transparent
border holds the box size), **soft-accent** (ghost on an `--accent-soft` wash —
- a low-emphasis solicitation, e.g. Donate), **danger** (filled
+ a low-emphasis solicitation, e.g. Support), **danger** (filled
`--support-error` — the confirm step), **danger-outline** (secondary geometry,
red label, filling solid red on hover/focus — a destructive action sitting
among peers, e.g. a detail view's Delete). 13px 600-weight label. Clear hover/active and a visible
@@ -218,7 +218,7 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
Import / Export / About are **icon-only** IconButtons per Carbon's UI-shell
header — global actions as a right-aligned icon row — with accessible names
that carry scope, e.g. "Export workspace" vs the preview's per-chart "Export";
- a divider then sets off the soft-accent Donate, which keeps its text label —
+ a divider then sets off the soft-accent Support, which keeps its text label —
a solicitation needs the word — 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
diff --git a/docs/architecture/visual-specimen.html b/docs/architecture/visual-specimen.html
index 20e0a50..47df8b4 100644
--- a/docs/architecture/visual-specimen.html
+++ b/docs/architecture/visual-specimen.html
@@ -466,7 +466,7 @@
.btn-ghost-util:hover {
background: var(--layer-02);
}
- /* Soft-accent: a low-emphasis accent wash (Donate) — noticed, not shouting. */
+ /* Soft-accent: a low-emphasis accent wash (Support) — noticed, not shouting. */
.btn-soft {
background: var(--accent-soft);
color: var(--accent-hover);
@@ -984,7 +984,7 @@
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
+ sets off Support on a soft-accent wash — one low-key
solicitation, no border. (Carbon: one high-emphasis action per region; utilities at low
emphasis.)
@@ -1024,7 +1024,7 @@
>About
-
+
diff --git a/docs/spec/01-application-shell.md b/docs/spec/01-application-shell.md
index bad067f..c30f7a8 100644
--- a/docs/spec/01-application-shell.md
+++ b/docs/spec/01-application-shell.md
@@ -25,21 +25,21 @@ Behavior:
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 **icon-only** utility entry points (each with a tooltip and an accessible name), then — set off by a divider — the text-labelled Donate button and the theme toggle. Each opens a destination:
+- **Right side**: a row of **icon-only** utility entry points (each with a tooltip and an accessible name), then — set off by a divider — the text-labelled Support button and the theme toggle. Each opens a destination:
-| Entry point | Opens |
-| ---------------- | --------------------------------------------------------------------------------------------------------------- |
-| Datasets | The Datasets manager modal (see _Datasets_). |
-| Import workspace | A file-picker dialog to choose a previously exported file; the chosen file is imported (see _Import & Export_). |
-| Export workspace | Immediately produces a downloaded file containing all snippets and datasets (see _Import & Export_). |
-| About | The About & Help modal (keyboard shortcuts, about, and privacy information); privacy lives inside. |
-| Donate | The Donate modal. The one text-labelled, soft-accent button — the solicitation keeps its word. |
+| Entry point | Opens |
+| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Datasets | The Datasets manager modal (see _Datasets_). |
+| Import workspace | A file-picker dialog to choose a previously exported file; the chosen file is imported (see _Import & Export_). |
+| Export workspace | Immediately produces a downloaded file containing all snippets and datasets (see _Import & Export_). |
+| About | The About & Help modal (keyboard shortcuts, about, and privacy information); privacy lives inside. |
+| Support | The Support modal — two ways to give back: feedback to the author, or a donation to Ukraine's defense. The one text-labelled, soft-accent button. |
Notes:
- The utilities are icon-only so the header reads as quiet chrome (Carbon UI-shell header: global actions are a right-aligned icon row). The accessible names scope the workspace-level Import/Export ("Export workspace") apart from the preview pane's per-chart "Export" — the two were previously both labelled "Export" at once.
- Import and Export act directly (file dialog / file download); they do not open in-app modals.
-- The Datasets, About, and Donate entry points each open a modal (see _Modal System_).
+- The Datasets, About, and Support 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
@@ -47,7 +47,7 @@ Notes:
## C. Modal System
-The app shows at most one modal at a time. The modal set is: Datasets, About & Help, Donate, Chart Builder, Extract-to-Dataset, and Theme Builder. (Settings are deliberately _not_ a modal — they are distributed to per-pane controls; see _Settings_.)
+The app shows at most one modal at a time. The modal set is: Datasets, About & Help, Support, Chart Builder, Extract-to-Dataset, and Theme Builder. (Settings are deliberately _not_ a modal — they are distributed to per-pane controls; see _Settings_.)
- 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. Exception: modals holding in-progress work (the Chart Builder and Theme Builder) ignore backdrop clicks; Escape and the close button still dismiss them.
diff --git a/src/app/App.module.css b/src/app/App.module.css
index ba26c0e..c645e7d 100644
--- a/src/app/App.module.css
+++ b/src/app/App.module.css
@@ -65,9 +65,9 @@
flex: 1;
}
-/* Vertical rule separating the utilities from Donate + the theme toggle.
+/* Vertical rule separating the utilities from Support + the theme toggle.
(The header buttons themselves are the shared Button primitive — ghost
- utilities, soft-accent Donate; arch 09 §4.) */
+ utilities, soft-accent Support; arch 09 §4.) */
/* TODO: --border-strong is the 3:1 component-boundary token (arch 09 §3.3), which
makes this passive divider fairly dark; --border would be the semantic choice but
is near-invisible on the --layer-01 header in light theme. A separator may want
diff --git a/src/app/App.tsx b/src/app/App.tsx
index 3fd3365..7ef4195 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -82,8 +82,9 @@ export function App() {
actions are a right-aligned row of icon-only buttons). The utilities
are IconButtons — tooltips carry the full action, accessible names
scope it ("Export workspace" vs the preview pane's per-chart
- "Export") — then a divider sets off Donate — the one solicitation,
- given a soft-accent wash — and the theme toggle. */}
+ "Export") — then a divider sets off Support (feedback + the one
+ donation solicitation), given a soft-accent wash — and the theme
+ toggle. */}
openModal('datasets')}
@@ -117,9 +118,9 @@ export function App() {
{/* Hidden picker for Import; restricted to JSON (spec §08). */}
diff --git a/src/app/components/AboutModal.module.css b/src/app/components/AboutModal.module.css
index d5c2185..7000e7b 100644
--- a/src/app/components/AboutModal.module.css
+++ b/src/app/components/AboutModal.module.css
@@ -36,6 +36,26 @@
color: var(--text-secondary);
}
+/* Inline accent link. Restated as DonateModal's `.email` — the two are the same
+ recipe; a shared link primitive would be the home if a third site appears. */
+/* TODO: if inline accent links recur, lift this recipe to a base.css element
+ baseline or a small primitive rather than copying it a third time. */
+.link {
+ color: var(--accent);
+ font-weight: 600;
+ text-decoration: none;
+}
+
+.link:hover {
+ text-decoration: underline;
+}
+
+.link:focus-visible {
+ outline: 2px solid var(--focus);
+ outline-offset: 2px;
+ border-radius: var(--radius);
+}
+
/* Shortcuts table */
.shortcuts {
border-collapse: collapse;
diff --git a/src/app/components/AboutModal.test.tsx b/src/app/components/AboutModal.test.tsx
index 34eaef4..1c68f8f 100644
--- a/src/app/components/AboutModal.test.tsx
+++ b/src/app/components/AboutModal.test.tsx
@@ -55,12 +55,21 @@ describe('AboutModal', () => {
expect(text).toMatch(/offline/i);
});
- test('has three labelled sections: identity, shortcuts, privacy', () => {
+ test('has labelled sections: identity, shortcuts, privacy, feedback', () => {
const headings = Array.from(container.querySelectorAll('h3')).map(
(h) => h.textContent?.toLowerCase() ?? '',
);
expect(headings.some((h) => h.includes('astrolabe'))).toBe(true);
expect(headings.some((h) => h.includes('keyboard'))).toBe(true);
expect(headings.some((h) => h.includes('privacy'))).toBe(true);
+ expect(headings.some((h) => h.includes('feedback'))).toBe(true);
+ });
+
+ test('offers the feedback address as a mailto link', () => {
+ const mailto = Array.from(container.querySelectorAll('a')).find((a) =>
+ a.getAttribute('href')?.startsWith('mailto:'),
+ );
+ expect(mailto).toBeTruthy();
+ expect(mailto!.getAttribute('href')).toContain('feedback@astrolabe-viz.com');
});
});
diff --git a/src/app/components/AboutModal.tsx b/src/app/components/AboutModal.tsx
index 792e565..d169784 100644
--- a/src/app/components/AboutModal.tsx
+++ b/src/app/components/AboutModal.tsx
@@ -7,6 +7,7 @@
* (SOUL.md — local-only, no accounts, no telemetry).
*/
+import { FEEDBACK_EMAIL, feedbackMailtoHref } from '../feedback';
import styles from './AboutModal.module.css';
/** True when the user agent is macOS / iOS — drives the Cmd vs. Ctrl label. */
@@ -64,7 +65,7 @@ export function AboutModal() {
No account, no sign-in, no server-side storage.
-
No telemetry, no analytics, no tracking of any kind.
+
The app runs no analytics or tracking — no cookies, no telemetry, no profiling.
The only outbound network requests are ones you create: URL-sourced datasets you add
yourself.
@@ -75,6 +76,18 @@ export function AboutModal() {
+ The most useful thing you can send back is feedback — what's broken, what's missing, what
+ you'd want next.
+
+
+
+ {FEEDBACK_EMAIL}
+
+
+ {/* A polite live region announces the copy to assistive tech, which the
+ button's label swap alone would not reliably do. */}
+
+ {copied ? 'Email address copied to clipboard' : ''}
+
+
+
+
+
+
+ And if you'd like to give something beyond words, please send it where it matters far more
+ than it would to me: Ukraine's defense against Russia's invasion. The Come Back Alive
+ foundation supports the people defending the country.
+
);
}
diff --git a/src/app/feedback.ts b/src/app/feedback.ts
new file mode 100644
index 0000000..6502469
--- /dev/null
+++ b/src/app/feedback.ts
@@ -0,0 +1,18 @@
+/**
+ * Project feedback channel.
+ *
+ * There is no server and no tracker (see the About modal — no telemetry of any
+ * kind); feedback is a plain email the user composes and sends from their own
+ * client. The address is a Cloudflare Email Routing alias that forwards to the
+ * author, so it can be retired without exposing or churning a personal inbox.
+ *
+ * Shared by the Support modal and the About modal — a contact address is worth
+ * a single source of truth so the two surfaces can't drift.
+ */
+export const FEEDBACK_EMAIL = 'feedback@astrolabe-viz.com';
+
+/** A `mailto:` href prefilled with a version-stamped subject, to ease triage. */
+export function feedbackMailtoHref(): string {
+ const subject = encodeURIComponent(`Astrolabe v${__APP_VERSION__} — feedback`);
+ return `mailto:${FEEDBACK_EMAIL}?subject=${subject}`;
+}
diff --git a/src/app/modals/modal-registry.ts b/src/app/modals/modal-registry.ts
index 98f4f59..66232d8 100644
--- a/src/app/modals/modal-registry.ts
+++ b/src/app/modals/modal-registry.ts
@@ -118,9 +118,11 @@ const MODAL_REGISTRY: Partial> = {
name: 'about',
title: 'About & Help',
},
+ // User-facing name is "Support" (feedback + the Ukraine-defense donation); the
+ // id stays `donate` — its origin and still its primary accent CTA.
donate: {
name: 'donate',
- title: 'Donate',
+ title: 'Support',
},
};
diff --git a/src/app/modals/types.ts b/src/app/modals/types.ts
index b735c69..e5329af 100644
--- a/src/app/modals/types.ts
+++ b/src/app/modals/types.ts
@@ -10,7 +10,7 @@
export type ModalName =
| 'datasets' // Datasets manager (list / detail / new-dataset form)
| 'about' // About & Help (M6)
- | 'donate' // Donate (M6)
+ | 'donate' // Support — feedback + Ukraine-defense donation (M6)
| 'chartBuilder' // Visual no-JSON chart composition for a dataset (M4)
| 'extract' // Extract inline spec data into a new dataset (M3)
| 'themeBuilder'; // Custom chart theme editor with a live preview gallery (spec §04)