From 5116e7e2ab805516058fb81c5d89162e93a06089 Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Sat, 3 Oct 2026 08:49:02 +0530 Subject: [PATCH 1/8] feat(website): add design tokens, a bundled typeface and interface primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The interface had no shared vocabulary: every stylesheet set its own colours, 6–9px monospace capitals and radii, and the typefaces came from Google Fonts at first paint, so offline visits fell back to a system face. This adds one token file (ink, surfaces, a type scale, spacing, radii, motion), ships Archivo as a bundled variable font and drops Google from the page and the content security policy, and adds the primitives the redesign is built from: Button, Icon, TabBar, Segmented, Kbd and Spinner, a chrome context, scene readout hooks, `starLight` for taking the accent from the local star's blackbody colour, and `readable` for bringing capitalised readings down to sentence case. Co-Authored-By: Claude Opus 5.5 (1M context) --- THIRD_PARTY_ASSETS.md | 10 + apps/website/index.html | 4 +- apps/website/package.json | 1 + apps/website/public/manifest.webmanifest | 4 +- apps/website/src/chrome-context.ts | 24 ++ apps/website/src/components/ui/Button.tsx | 48 +++ apps/website/src/components/ui/Icon.tsx | 173 +++++++++++ apps/website/src/components/ui/Kbd.tsx | 12 + apps/website/src/components/ui/Segmented.tsx | 31 ++ apps/website/src/components/ui/Spinner.tsx | 6 + apps/website/src/components/ui/TabBar.tsx | 33 +++ apps/website/src/components/ui/ui.module.css | 289 +++++++++++++++++++ apps/website/src/readable.test.ts | 42 +++ apps/website/src/readable.ts | 97 +++++++ apps/website/src/star-light.test.ts | 34 +++ apps/website/src/star-light.ts | 54 ++++ apps/website/src/styles/bind-styles.ts | 5 +- apps/website/src/styles/globals.css | 118 +++++--- apps/website/src/styles/tokens.css | 125 +++++++- apps/website/src/use-scene-readouts.ts | 20 ++ apps/website/src/web-fonts.ts | 38 +-- pnpm-lock.yaml | 129 +-------- pnpm-workspace.yaml | 1 + vercel.json | 2 +- 24 files changed, 1100 insertions(+), 200 deletions(-) create mode 100644 apps/website/src/chrome-context.ts create mode 100644 apps/website/src/components/ui/Button.tsx create mode 100644 apps/website/src/components/ui/Icon.tsx create mode 100644 apps/website/src/components/ui/Kbd.tsx create mode 100644 apps/website/src/components/ui/Segmented.tsx create mode 100644 apps/website/src/components/ui/Spinner.tsx create mode 100644 apps/website/src/components/ui/TabBar.tsx create mode 100644 apps/website/src/components/ui/ui.module.css create mode 100644 apps/website/src/readable.test.ts create mode 100644 apps/website/src/readable.ts create mode 100644 apps/website/src/star-light.test.ts create mode 100644 apps/website/src/star-light.ts create mode 100644 apps/website/src/use-scene-readouts.ts diff --git a/THIRD_PARTY_ASSETS.md b/THIRD_PARTY_ASSETS.md index 14d4d39..8e4ed72 100644 --- a/THIRD_PARTY_ASSETS.md +++ b/THIRD_PARTY_ASSETS.md @@ -310,6 +310,16 @@ curl -sL -o apps/website/public/ktx2/ https://cdn.babylonjs.com/ktx2Transc Keep the files byte-for-byte as published — do not run them through `vp fmt`. +## Interface typeface + +The interface is set in Archivo, a variable family with width and weight axes, by Omnibus-Type +(`github.com/Omnibus-Type/Archivo`), licensed under the SIL Open Font License 1.1. It arrives +through the npm package `@fontsource-variable/archivo`, whose `wdth.css` declares the upright +faces split by Unicode range, so the bundle emits hashed WOFF2 files that the service worker keeps +with every other asset. It used to be fetched from Google Fonts, which put a third-party origin on +the first paint and left the interface in a fallback face when offline; serving it from the page's +own origin also let the content security policy drop both Google hosts. + ## Star catalogue `apps/website/public/sky/hyg-v44-vmag65.bin` is the sky itself: the real stars, with real diff --git a/apps/website/index.html b/apps/website/index.html index 40f43e3..62826fe 100644 --- a/apps/website/index.html +++ b/apps/website/index.html @@ -3,8 +3,6 @@ - - - + diff --git a/apps/website/package.json b/apps/website/package.json index b1a68c1..d567e68 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -18,6 +18,7 @@ "@babylonjs/core": "catalog:", "@exora/contracts": "workspace:*", "@exora/worldgen": "workspace:*", + "@fontsource-variable/archivo": "catalog:", "@vercel/analytics": "catalog:", "@vercel/speed-insights": "catalog:", "react": "catalog:", diff --git a/apps/website/public/manifest.webmanifest b/apps/website/public/manifest.webmanifest index 3a7a162..df7974a 100644 --- a/apps/website/public/manifest.webmanifest +++ b/apps/website/public/manifest.webmanifest @@ -7,8 +7,8 @@ "scope": "/", "display": "standalone", "orientation": "any", - "background_color": "#020407", - "theme_color": "#020407", + "background_color": "#000000", + "theme_color": "#000000", "categories": ["education", "entertainment"], "icons": [ { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, diff --git a/apps/website/src/chrome-context.ts b/apps/website/src/chrome-context.ts new file mode 100644 index 0000000..1f7a9d0 --- /dev/null +++ b/apps/website/src/chrome-context.ts @@ -0,0 +1,24 @@ +import { createContext, useContext } from "react"; + +/* + * What every destination's chrome can ask the application to do, provided once by `App` rather + * than threaded through each experience as four more props. The defaults are inert so an + * experience still renders on its own — in a test, or on a server. + */ +export interface ChromeActions { + chromeHidden: boolean; + openDiscover: () => void; + openPalette: () => void; + toggleChrome: () => void; +} + +const inert = (): void => undefined; + +export const ChromeContext = createContext({ + chromeHidden: false, + openDiscover: inert, + openPalette: inert, + toggleChrome: inert, +}); + +export const useChrome = (): ChromeActions => useContext(ChromeContext); diff --git a/apps/website/src/components/ui/Button.tsx b/apps/website/src/components/ui/Button.tsx new file mode 100644 index 0000000..6c56dae --- /dev/null +++ b/apps/website/src/components/ui/Button.tsx @@ -0,0 +1,48 @@ +import type { ButtonHTMLAttributes, ReactNode, Ref } from "react"; +import { Icon, type IconName } from "./Icon.tsx"; +import styles from "./ui.module.css"; + +export type ButtonVariant = "ghost" | "primary" | "secondary" | "surface"; + +interface ButtonProps extends ButtonHTMLAttributes { + children?: ReactNode; + icon?: IconName; + iconEnd?: IconName; + ref?: Ref; + size?: "md" | "sm"; + variant?: ButtonVariant; +} + +/* + * The one button. A variant says how loud it is — `primary` is lit by the local star and there is + * at most one per view — and an icon with no children makes it square, in which case the caller + * owes it an `aria-label`. + */ +export const Button = ({ + children, + className, + icon, + iconEnd, + ref, + size = "md", + type = "button", + variant = "secondary", + ...rest +}: ButtonProps) => { + const iconOnly = icon !== undefined && (children === undefined || children === null); + return ( + + ); +}; diff --git a/apps/website/src/components/ui/Icon.tsx b/apps/website/src/components/ui/Icon.tsx new file mode 100644 index 0000000..e1afac1 --- /dev/null +++ b/apps/website/src/components/ui/Icon.tsx @@ -0,0 +1,173 @@ +import type { ReactNode } from "react"; +import styles from "./ui.module.css"; + +/* + * One stroke weight, one grid: every icon is drawn on 20×20 at a 1.6 stroke so they sit on a line + * of text at any size without one reading heavier than its neighbour. + */ +const PATHS = { + atlas: ( + <> + + + + + + + ), + "arrow-left": , + "arrow-right": , + "arrow-up-right": , + "black-hole": ( + <> + + + + ), + check: , + "chevron-down": , + "chevron-left": , + "chevron-right": , + "chevron-up": , + close: , + compass: ( + <> + + + + ), + dice: ( + <> + + + + + + ), + external: ( + + ), + eye: ( + <> + + + + ), + "eye-off": ( + <> + + + + ), + forge: ( + <> + + + + ), + grid: ( + <> + + + + + + ), + headset: ( + + ), + home: , + info: ( + <> + + + + ), + list: , + moon: , + music: ( + <> + + + + + ), + "music-off": ( + <> + + + + ), + orbit: ( + <> + + + + + ), + pause: , + planet: ( + <> + + + + ), + play: , + plus: , + refresh: , + reverse: , + route: ( + <> + + + + + ), + search: ( + <> + + + + ), + share: ( + <> + + + + ), + sliders: , + sparkle: ( + + ), + star: ( + <> + + + + ), + "wifi-off": ( + + ), +} satisfies Record; + +export type IconName = keyof typeof PATHS; + +interface IconProps { + className?: string; + name: IconName; + size?: number; +} + +export const Icon = ({ className, name, size = 18 }: IconProps) => ( + +); diff --git a/apps/website/src/components/ui/Kbd.tsx b/apps/website/src/components/ui/Kbd.tsx new file mode 100644 index 0000000..1725bd7 --- /dev/null +++ b/apps/website/src/components/ui/Kbd.tsx @@ -0,0 +1,12 @@ +import styles from "./ui.module.css"; + +/** A key as it is printed on the keyboard, set beside the control it reaches. */ +export const Kbd = ({ children, label }: { children: string; label?: string }) => ( + + {children} + +); + +/** ⌘ on Apple platforms, Ctrl everywhere else. */ +export const MODIFIER_KEY = + typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘" : "Ctrl"; diff --git a/apps/website/src/components/ui/Segmented.tsx b/apps/website/src/components/ui/Segmented.tsx new file mode 100644 index 0000000..e7dc596 --- /dev/null +++ b/apps/website/src/components/ui/Segmented.tsx @@ -0,0 +1,31 @@ +import { Icon, type IconName } from "./Icon.tsx"; +import styles from "./ui.module.css"; + +interface SegmentedProps { + label: string; + onChange: (value: Value) => void; + options: readonly { icon?: IconName; id: Value; label: string }[]; + value: Value; +} + +/** A small set of mutually exclusive choices that change how something is shown, not what. */ +export const Segmented = ({ + label, + onChange, + options, + value, +}: SegmentedProps) => ( +
+ {options.map((option) => ( + + ))} +
+); diff --git a/apps/website/src/components/ui/Spinner.tsx b/apps/website/src/components/ui/Spinner.tsx new file mode 100644 index 0000000..776922d --- /dev/null +++ b/apps/website/src/components/ui/Spinner.tsx @@ -0,0 +1,6 @@ +import styles from "./ui.module.css"; + +/** An orbit standing in for work in progress; decorative, so the caller says what is happening. */ +export const Spinner = ({ size = 18 }: { size?: number }) => ( +