Files
astrolabe/src/app/components/AboutModal.tsx
T

187 lines
7.2 KiB
TypeScript

/**
* About & Help modal body (spec §01B/§01C).
*
* Rendered inside ModalShell — no backdrop, close button, or focus trap here;
* the shell owns all of that (docs/architecture/03 → Layer 3). This component
* is pure content: app identity, keyboard shortcuts (§01D), privacy posture
* (SOUL.md — local-only, no accounts, no telemetry), and acknowledgements of
* the projects Astrolabe is built on and shaped by.
*/
import type { ReactNode } from 'react';
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. */
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform);
const mod = isMac ? '⌘' : 'Ctrl';
/** Keyboard shortcut rows sourced from spec §01D. */
const SHORTCUTS: readonly { keys: string; action: string }[] = [
{ keys: `${mod}+Shift+N`, action: 'Create a new snippet' },
{ keys: `${mod}+K`, action: 'Toggle the Datasets manager' },
{ keys: `${mod}+S`, action: 'Publish the current snippet draft' },
{ keys: `${mod}+,`, action: 'Open editor settings' },
{ keys: 'Esc', action: 'Close the active modal' },
];
/**
* External acknowledgement link. Opens in a new tab so following a credit never
* unloads the workspace mid-edit; `rel` isolates the opened context.
*/
function Ack({ href, children }: { href: string; children: ReactNode }) {
return (
<a className={styles.link} href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
}
export function AboutModal() {
return (
<div className={styles.about}>
{/* Identity */}
<section className={styles.section}>
<h3 className={styles.heading}>Astrolabe</h3>
<p className={styles.body}>
v<span className={styles.version}>{__APP_VERSION__}</span>
</p>
<p className={styles.body}>
A local-first workspace for authoring, organizing, and previewing Vega-Lite charts. Edit
JSON, see the chart update live, and keep a personal library of snippets with no
account, no server, and full offline support.
</p>
</section>
{/* Keyboard shortcuts */}
<section className={styles.section}>
<h3 className={styles.heading}>Keyboard shortcuts</h3>
<table className={styles.shortcuts} aria-label="Keyboard shortcuts">
<tbody>
{SHORTCUTS.map(({ keys, action }) => (
<tr key={keys} className={styles.row}>
<td className={styles.keys}>
<kbd className={styles.kbd}>{keys}</kbd>
</td>
<td className={styles.action}>{action}</td>
</tr>
))}
</tbody>
</table>
</section>
{/* Privacy */}
<section className={styles.section}>
<h3 className={styles.heading}>Privacy</h3>
<p className={styles.body}>
Astrolabe runs entirely in your browser. Your snippets, datasets, and settings are stored
locally and never leave your machine.
</p>
<ul className={styles.list}>
<li>No account, no sign-in, no server-side storage.</li>
<li>The app runs no analytics or tracking no cookies, no telemetry, no profiling.</li>
<li>
The only outbound network requests are ones you create: URL-sourced datasets you add
yourself.
</li>
<li>After the first load, the app works fully offline.</li>
<li>
Use the header&rsquo;s import / export buttons to move your library between devices.
</li>
</ul>
</section>
{/* Feedback */}
<section className={styles.section}>
<h3 className={styles.heading}>Feedback</h3>
<p className={styles.body}>
Found a bug or have an idea? Email{' '}
<a className={styles.link} href={feedbackMailtoHref()}>
{FEEDBACK_EMAIL}
</a>
.
</p>
</section>
{/* Acknowledgements */}
<section className={styles.section}>
<h3 className={styles.heading}>Acknowledgements</h3>
<p className={styles.body}>Astrolabe stands on the work of many open projects.</p>
<div className={styles.ackGroup}>
<h4 className={styles.ackLabel}>Built on</h4>
<ul className={styles.list}>
<li>
<Ack href="https://vega.github.io/vega-lite/">Vega-Lite</Ack>,{' '}
<Ack href="https://vega.github.io/vega/">Vega</Ack>,{' '}
<Ack href="https://github.com/vega/vega-embed">vega-embed</Ack>
<span className={styles.ackNote}> render every chart</span>
</li>
<li>
<Ack href="https://microsoft.github.io/monaco-editor/">Monaco</Ack>
<span className={styles.ackNote}> the JSON editor</span>
</li>
<li>
<Ack href="https://react.dev/">React</Ack>,{' '}
<Ack href="https://github.com/pmndrs/zustand">Zustand</Ack>,{' '}
<Ack href="https://vite.dev/">Vite</Ack>
</li>
</ul>
</div>
<div className={styles.ackGroup}>
<h4 className={styles.ackLabel}>Chart guidance shaped by</h4>
<ul className={styles.list}>
<li>
<Ack href="https://github.com/uwdata/draco">Draco</Ack>,{' '}
<Ack href="https://github.com/vega/voyager">Voyager</Ack>
<span className={styles.ackNote}> encoding rules</span>
</li>
<li>
<Ack href="https://github.com/Financial-Times/chart-doctor">FT Visual Vocabulary</Ack>
, <Ack href="https://www.datawrapper.de/">Datawrapper</Ack>
</li>
<li>
<Ack href="https://github.com/vega/lyra">Lyra</Ack>
<span className={styles.ackNote}> authoring interactions</span>
</li>
</ul>
</div>
<div className={styles.ackGroup}>
<h4 className={styles.ackLabel}>Design informed by</h4>
<ul className={styles.list}>
<li>
<Ack href="https://carbondesignsystem.com/">IBM Carbon</Ack>,{' '}
<Ack href="https://design-system.service.gov.uk/">GOV.UK</Ack>
</li>
<li>
<Ack href="https://www.w3.org/WAI/ARIA/apg/">WAI-ARIA APG</Ack>,{' '}
<Ack href="https://www.nngroup.com/">Nielsen Norman</Ack>
</li>
</ul>
</div>
<div className={styles.ackGroup}>
<h4 className={styles.ackLabel}>Typography</h4>
<ul className={styles.list}>
<li>
<Ack href="https://github.com/IBM/plex">IBM Plex</Ack> and other open typefaces,
self-hosted via <Ack href="https://fontsource.org/">Fontsource</Ack>
</li>
</ul>
</div>
</section>
{/* Sibling project */}
<section className={styles.section}>
<h3 className={styles.heading}>Also from the maker</h3>
<p className={styles.body}>
Astrolabe has a sibling: <Ack href="https://syto.app">Syto</Ack>, a workspace for cleaning
and reshaping data before you chart it. Go take a look.
</p>
</section>
</div>
);
}