chore: strip the comments this branch added

Removes every comment introduced on this branch: website/ in full, pages.yml
in full, and only the blocks added to ci.yml and release.yml — their
pre-existing comments are untouched.

Two directives survive because they are load-bearing rather than prose: the
triple-slash vite/client reference, and nothing else.

Two comments were carrying behaviour that the code did not state, so the
code now states it. useCopy's rejection handler clears the copied flag
instead of sitting empty, and writeTheme returns whether the write actually
landed instead of swallowing the failure in a bare catch. isExternal drops
its regex for two startsWith calls, which reads the same and no longer
hides a `//` inside a literal.
This commit is contained in:
Michel de Graaf
2026-07-25 01:29:55 +02:00
parent ba92e75fd3
commit c2dd227cee
45 changed files with 20 additions and 518 deletions
-19
View File
@@ -11,14 +11,9 @@ export default defineConfig(
js.configs.recommended,
// strictTypeChecked over recommended: it needs type information (below) but
// catches the whole class of bugs that only types can see — floating
// promises, unsafe `any` flowing through, misused nullish operands.
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
// `configs.flat.*`, not `configs['recommended-latest']` — the latter is
// still the legacy eslintrc shape (plugins as a string array).
reactHooks.configs.flat['recommended-latest'],
jsxA11y.flatConfigs.recommended,
@@ -41,8 +36,6 @@ export default defineConfig(
{ allowConstantExport: true },
],
// Unused args are usually a mistake, but an `_`-prefixed one is an
// explicit "I know, I need the positional slot".
'@typescript-eslint/no-unused-vars': [
'error',
{
@@ -52,14 +45,11 @@ export default defineConfig(
},
],
// Type-only imports must be erasable, so the bundler can drop them
// without consulting the type graph. Pairs with verbatimModuleSyntax.
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
],
// A dropped promise in an event handler fails silently in production.
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
@@ -71,11 +61,6 @@ export default defineConfig(
},
},
// The TUI demo is a composite widget: it declares role="application" and
// owns its keys (j/k, arrows, 1-3) the way the real terminal client does.
// These two rules police exactly the pattern ARIA prescribes for that, so
// they are off for this one file rather than worked around in the markup.
// Every control inside it is still a native button or input.
{
files: ['src/components/tui/hero-terminal.tsx'],
rules: {
@@ -84,10 +69,6 @@ export default defineConfig(
},
},
// Config and build scripts are Node, not browser. Build output is meant to
// be read on the terminal, so console is the interface, not a stray debug
// statement. entry-server is a build entry point that never enters the HMR
// graph, so the component-only export rule does not apply to it.
{
files: ['*.config.ts', 'scripts/**/*.ts'],
languageOptions: {
-15
View File
@@ -3,8 +3,6 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Root-relative on purpose: Vite rewrites this against `base`, so the
deploy path lives in vite.config.ts alone. -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>soulseek-rs, a Soulseek client for your terminal</title>
@@ -12,7 +10,6 @@
name="description"
content="soulseek-rs is a keyboard-driven Soulseek client and a Rust protocol library. Search, share, browse, and chat from your terminal."
/>
<!-- Browser chrome follows the theme; one value would be wrong half the time. -->
<meta name="theme-color" content="#14110F" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#F2EDE4" media="(prefers-color-scheme: light)" />
@@ -22,18 +19,10 @@
property="og:description"
content="A Soulseek client and protocol library for Rust. Search, share, browse, and chat from your terminal."
/>
<!-- og:url and canonical are rewritten per route by scripts/prerender.ts. -->
<meta property="og:url" content="https://re-invention.nl/soulseek-rs/" />
<link rel="canonical" href="https://re-invention.nl/soulseek-rs/" />
<meta name="twitter:card" content="summary" />
<!-- IBM Plex Mono is the brand typeface. CDN rather than self-hosted
binaries, matching the design system's tokens/fonts.css. OFL 1.1.
Loaded non-blocking: as a plain stylesheet it sat on the critical
path, so first paint waited on a third-party DNS + TLS + fetch. It
buys nothing there — `display=swap` plus the ui-monospace fallback
means the first frame renders in the fallback either way. -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
@@ -50,9 +39,6 @@
</noscript>
<script>
// Runs before first paint so the right theme is already on <html>.
// Without it the page flashes the wrong background on every load.
// Mirrors the resolution order in src/theme/theme-provider.tsx.
;(function () {
try {
var stored = localStorage.getItem('ssrs-theme')
@@ -64,7 +50,6 @@
document.documentElement.dataset.theme = theme
document.documentElement.style.colorScheme = theme
} catch (e) {
// localStorage blocked; the dark default in the markup stands.
}
})()
</script>
-25
View File
@@ -1,23 +1,8 @@
/*
* Turns the SPA build into real static files.
*
* Without this, GitHub Pages has no file at /install and answers a direct
* link with its 404 page. Writing one HTML file per route means every URL
* returns 200 with its own title, description, and fully rendered body; the
* client then hydrates it into the same interactive app.
*
* Run after both `vite build` and `vite build --ssr`.
*/
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { NOT_FOUND_META, ROUTES, type RouteMeta } from '../src/lib/routes.ts'
/*
* Held in a variable so TypeScript does not try to resolve it: the module is
* a build artefact, and `bun run typecheck` runs before `vite build --ssr`
* has produced it. The cast is the contract with src/entry-server.tsx.
*/
const SSR_ENTRY = '../dist-ssr/entry-server.js'
const { render } = (await import(SSR_ENTRY)) as {
render: (location: string) => string
@@ -30,20 +15,12 @@ const SITE_URL = 'https://re-invention.nl/soulseek-rs/'
const escapeAttr = (value: string): string =>
value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;')
/** Replaces the content of one `<meta>` matched by attribute and value. */
const setMeta = (html: string, attr: string, name: string, value: string): string =>
html.replace(
new RegExp(`(<meta\\s+${attr}="${name}"\\s+content=")[\\s\\S]*?(")`),
`$1${escapeAttr(value)}$2`,
)
/**
* Swaps every per-page tag in the template for the route's own.
*
* All five are listed here together on purpose: og:title and og:url used to be
* left alone, so each prerendered page advertised the site title and the
* homepage URL to anything reading Open Graph.
*/
const applyMeta = (html: string, meta: RouteMeta): string => {
const canonical = meta.path === '/' ? SITE_URL : `${SITE_URL}${meta.path.slice(1)}/`
let out = html.replace(
@@ -84,8 +61,6 @@ for (const route of ROUTES) {
await writePage(template, route, outPath, route.path)
}
// GitHub Pages serves this whenever no file matches, which after the loop
// above is only ever a genuinely unknown path.
await writePage(template, NOT_FOUND_META, '404.html', '/__not_found__')
console.log(`done — ${String(ROUTES.length + 1)} pages`)
-13
View File
@@ -9,14 +9,6 @@ import { Home } from '@/pages/home'
import { Install } from '@/pages/install'
import { NotFound } from '@/pages/not-found'
/*
* Keyed by RoutePath, so adding an entry to ROUTES without a page here — or a
* page here that ROUTES does not know about — fails `bun run typecheck`.
*
* That drift used to be silent and shipped broken both ways: a prerendered
* file containing NotFound markup under the new page's title, or a real page
* with no prerendered file and "not found" in its tab.
*/
const PAGES: Record<RoutePath, ReactElement> = {
'/': <Home />,
'/install': <Install />,
@@ -24,11 +16,6 @@ const PAGES: Record<RoutePath, ReactElement> = {
'/community': <Community />,
}
/*
* Everything is eagerly imported: the whole site is a couple of hundred kB,
* so route-splitting would trade a network round-trip on every navigation for
* a bundle saving nobody would notice.
*/
export const App = () => (
<Routes>
<Route element={<SiteLayout />}>
-11
View File
@@ -1,23 +1,12 @@
import { cn } from '@/lib/utils'
interface LogoProps {
/** Height in px. Width follows the artwork's aspect ratio. */
size?: number
className?: string
}
/** Native aspect ratio of the logo artwork (186.407 × 211). */
const ASPECT = 186.407 / 211
/*
* Two assets rather than one recoloured file — the ink and light cuts differ
* in more than colour — swapped by CSS.
*
* Painted as CSS background images rather than image elements: a hidden one
* is still downloaded, so the two-element version fetched both cuts on every
* visit (~12-23 kB gzip wasted above the fold). A background on a rule that
* never paints is never requested.
*/
export const Logo = ({ size = 180, className }: LogoProps) => (
<div
role="img"
@@ -1,17 +1,11 @@
import { cn } from '@/lib/utils'
interface WordmarkProps {
/** Font size in px. */
size?: number
blink?: boolean
className?: string
}
/**
* The primary soulseek-rs mark: lowercase mono wordmark plus a solid block
* cursor. Live text rather than an asset, so it blinks for free and stays
* crisp at any size.
*/
export const Wordmark = ({ size = 28, blink = false, className }: WordmarkProps) => (
<span
className={cn(
-6
View File
@@ -61,13 +61,7 @@ const FooterAnchor = ({ label, href }: FooterLink) =>
export const Footer = () => (
<footer className="border-t border-hairline bg-base pt-7 pb-6 sm:pt-11 sm:pb-10">
<Wrap>
{/* Gives the three link groups a heading to sit under, so the document
outline stays nested instead of putting "Packages" on the same
level as the page's own sections. */}
<h2 className="sr-only">Site footer</h2>
{/* Two columns even on the narrowest phone, with the brand spanning
both — one column per link group makes the footer taller than the
page it closes. */}
<div className="grid grid-cols-2 gap-x-4 gap-y-5 sm:gap-8 md:grid-cols-[1.6fr_1fr_1fr_1fr]">
<div className="col-span-2 flex max-w-[320px] flex-col gap-2 sm:gap-3.5 md:col-span-1">
<Wordmark size={22} />
-7
View File
@@ -12,16 +12,9 @@ const ITEMS = [
{ label: 'community', to: '/community' },
] as const
/*
* No hamburger. Below 820px the links become their own horizontally
* scrollable row, which keeps every destination one tap away instead of
* two — the site has four pages, a drawer would be ceremony.
*/
export const Nav = () => (
<header className="sticky top-0 z-50 border-b border-hairline bg-[color-mix(in_srgb,var(--bg-base)_88%,transparent)] backdrop-blur-[8px]">
<div className="mx-auto flex max-w-[1140px] flex-wrap items-center gap-x-4 gap-y-3 px-[18px] pt-2.5 sm:h-[60px] sm:flex-nowrap sm:gap-6 sm:px-7 sm:pt-0">
{/* min-h-11: the wordmark's own box is 19px tall, which left the most
reached-for control in the header under the 24px target minimum. */}
<NavLink
to="/"
aria-label="soulseek-rs home"
@@ -5,17 +5,6 @@ import { Footer } from '@/components/layout/footer'
import { Nav } from '@/components/layout/nav'
import { usePageMeta } from '@/hooks/use-page-meta'
/**
* On navigation: jump to the anchor if the URL carries one, otherwise back to
* the top, and move keyboard focus into the main landmark.
*
* That last part matters — without it focus stays on the link that was
* clicked, so a screen reader never announces that the page changed.
*
* replaces react-router's <ScrollRestoration>, which only works
* under a data router. Restoring exact scroll offsets on back/forward would
* mean adopting createBrowserRouter for four static pages.
*/
const useNavigationEffects = () => {
const { pathname, hash } = useLocation()
@@ -1,11 +1,6 @@
import { useTheme } from '@/theme/use-theme'
import type { Theme } from '@/theme/theme-context'
/*
* Cycles system → light → dark → system rather than a plain two-way flip, so
* "follow the OS" stays reachable after the first click. The glyph shows what
* is painted; the label says which mode is selected.
*/
const NEXT: Record<Theme, Theme> = {
system: 'light',
light: 'dark',
-6
View File
@@ -12,10 +12,6 @@ interface TuiPaneProps {
bodyClassName?: string
}
/**
* The TUI's bordered pane: a legend that breaks the top border, green when
* the pane has focus. Mirrors `pane_block()` in the Rust client.
*/
export const TuiPane = ({
title,
num,
@@ -44,7 +40,6 @@ export const TuiPane = ({
</div>
)
/** The macOS window bezel the whole demo sits in. */
export const MacWindow = ({ children }: { children: React.ReactNode }) => (
<div
className="w-[1360px] overflow-hidden rounded-xl border bg-base font-mono tabular-nums"
@@ -54,7 +49,6 @@ export const MacWindow = ({ children }: { children: React.ReactNode }) => (
</div>
)
export interface StatusCounts {
active: number
done: number
-11
View File
@@ -1,11 +1,3 @@
/*
* Sample data for the TUI recreation in the hero.
*
* Everything here is invented, public-domain, or Creative Commons. No real
* artist, album, or track names — see TONE-OF-VOICE.md §5 "Banned for legal
* reasons". Anything added here has to keep that true.
*/
export interface SearchRow {
id: string
query: string
@@ -20,9 +12,7 @@ export interface ResultRow {
bitrate: string
speed: string
slots: number
/** Marked for download with Space, not yet queued. */
marked?: boolean
/** Handed to the transfer list. */
queued?: boolean
}
@@ -35,7 +25,6 @@ export interface TransferRow {
user: string
progress: string
speed: string
/** Present only on transfers the demo drives; the static rows leave it out. */
pct?: number
sizeMb?: number
rateMbps?: number
@@ -14,23 +14,13 @@ import { InfoPane, ResultsPane, SearchesPane, TransfersPane } from './panes'
import { useFitScale } from './use-fit-scale'
const DESIGN_WIDTH = 1360
/*
* Natural height of the terminal at DESIGN_WIDTH, measured in the browser.
*
* The layout is fixed — no content-driven heights — so the wrapper can hold
* this ratio in CSS from the very first paint. Without it the prerendered
* hero occupied its full unscaled height, then collapsed to the scaled one
* once JS measured, shifting everything below it by ~145px.
*/
const DESIGN_HEIGHT = 863
const TICK_MS = 480
const MAX_CONCURRENT = 2
/** Where the scripted run leaves the cursor. */
const RESTING_ROW = 5
type Phase = 'searching' | 'results' | 'interactive'
/** Pages are prerendered in Node, where there is no window. */
const isBrowser = typeof window !== 'undefined'
const matches = (query: string): boolean =>
@@ -43,12 +33,6 @@ const sizeMb = (size: string): number => {
return Number.isNaN(parsed) || parsed === 0 ? 12 : parsed
}
/*
* Deterministic on purpose: the id is derived from the filename and the rate
* from its length, so queueing the same row twice is a no-op and nothing
* random runs inside a state updater. Both matter under StrictMode, which
* double-invokes updaters in development.
*/
const toTransfer = (row: ResultRow): TransferRow => ({
id: `t:${row.name}`,
status: 'queued',
@@ -61,16 +45,6 @@ const toTransfer = (row: ResultRow): TransferRow => ({
rateMbps: 1.4 + (row.name.length % 7) * 0.4,
})
/**
* The hero: a working recreation of the TUI.
*
* It plays one scripted run (search → results → queue two files) and then
* hands over to the keyboard. Only transfers carrying a `pct` are simulated;
* the seeded rows stay put so the pane looks lived-in from the first frame.
*
* Under `prefers-reduced-motion` it starts at the end state instead, so the
* page never animates at someone who asked it not to.
*/
export const HeroTerminal = () => {
const [searches, setSearches] = useState<readonly SearchRow[]>(SEARCHES)
const [results, setResults] =
@@ -79,23 +53,11 @@ export const HeroTerminal = () => {
const [transfers, setTransfers] = useState<readonly TransferRow[]>(TRANSFERS)
const [focus, setFocus] = useState(2)
/*
* Both start where the prerendered HTML starts, whatever the OS says.
*
* Reading prefers-reduced-motion in the initialiser made the first client
* render disagree with the server for anyone who has it on: the server
* always serialises the searching state, the client painted the resting
* one, and React bailed out with a hydration mismatch (#418). The
* preference is applied a tick later instead, by the opening-run effect.
*/
const [phase, setPhase] = useState<Phase>('searching')
const [selected, setSelected] = useState(0)
const shellRef = useRef<HTMLDivElement>(null)
// Mirrors `results` so queueFrom can read the latest rows without taking a
// dependency on them. Synced after commit, which is soon enough: every
// reader is a timer or an event handler, never a render.
const resultsRef = useRef(results)
useEffect(() => {
resultsRef.current = results
@@ -110,7 +72,6 @@ export const HeroTerminal = () => {
return tally
}, [transfers])
// Derived, not stored: the status bar is a pure function of what is moving.
const pct = useMemo(() => {
const live = transfers.filter(
(t) => t.pct !== undefined && (t.status === 'active' || t.status === 'done'),
@@ -119,15 +80,11 @@ export const HeroTerminal = () => {
return Math.round(live.reduce((sum, t) => sum + (t.pct ?? 0), 0) / live.length)
}, [transfers])
/** True while any simulated transfer still has somewhere to go. */
const hasWork = useMemo(
() => transfers.some((t) => t.pct !== undefined && t.status !== 'done'),
[transfers],
)
// Stops once everything has finished. Without the `hasWork` gate the timer
// would tick twice a second for as long as the tab is open, re-rendering
// the whole TUI to produce identical output.
useEffect(() => {
if (!hasWork || prefersReducedMotion()) return
@@ -151,8 +108,6 @@ export const HeroTerminal = () => {
}
if (t.status !== 'active') return t
// Step size varies per file but is a pure function of it, so the
// updater stays free of side effects under StrictMode's double call.
const step = 4 + ((t.name.length + Math.round(t.pct)) % 9)
const pct = Math.min(100, t.pct + step)
const finished = pct >= 100
@@ -167,9 +122,6 @@ export const HeroTerminal = () => {
}
})
// map() always allocates, so without this every tick would hand React
// a fresh array and force a re-render that produces identical output.
// Returning the original lets React bail out instead.
const moved = next.some((t, i) => t !== list[i])
return moved ? next : list
})
@@ -180,13 +132,6 @@ export const HeroTerminal = () => {
}
}, [hasWork])
/*
* Queue rows by index within a named search.
*
* Reads the current rows through a ref rather than closing over `results`,
* so the callback identity never changes — the opening script can then
* depend on it without restarting itself every time state moves.
*/
const queueFrom = useCallback((searchId: string, indexes: readonly number[]) => {
if (indexes.length === 0) return
@@ -291,17 +236,6 @@ export const HeroTerminal = () => {
}
}
/*
* The scripted opening run.
*
* No "has this already started" ref: the cleanup cancels every timer, so a
* StrictMode remount simply replays from the top. A ref guard would let the
* first cleanup cancel the run and the re-run skip scheduling it, leaving
* the demo frozen on its first frame in development.
*
* It targets 's1' directly rather than whatever is selected, because the
* script only ever describes the opening state.
*/
useEffect(() => {
const timers: ReturnType<typeof setTimeout>[] = []
const at = (fn: () => void, ms: number) => timers.push(setTimeout(fn, ms))
@@ -309,9 +243,6 @@ export const HeroTerminal = () => {
for (const timer of timers) clearTimeout(timer)
}
// Asked for no motion: cut to where the run would have ended. Deferred by
// a tick rather than decided in the state initialiser, so the first render
// still matches the prerendered HTML.
if (prefersReducedMotion()) {
at(() => {
setSelected(RESTING_ROW)
@@ -385,8 +316,6 @@ export const HeroTerminal = () => {
setPhase('interactive')
}
// No floor: the terminal shrinks to whatever width it is given rather than
// holding a minimum size and scrolling sideways.
const { wrapRef, innerRef, scale, height, ready } = useFitScale(DESIGN_WIDTH)
const shownSearches = searches.map((search, i) =>
@@ -398,8 +327,6 @@ export const HeroTerminal = () => {
<div
ref={wrapRef}
className="w-full overflow-hidden"
// aspect-ratio reserves the right box before JS runs; once measured the
// explicit height takes over and the two agree, so nothing moves.
style={ready ? { height } : { aspectRatio: `${String(DESIGN_WIDTH)} / ${String(DESIGN_HEIGHT)}` }}
>
<div style={ready ? { width: DESIGN_WIDTH * scale, height } : undefined}>
@@ -413,12 +340,6 @@ export const HeroTerminal = () => {
}}
>
<MacWindow>
{/*
role="application" is the ARIA escape hatch for a widget that
handles its own keys: it stops screen readers intercepting
j/k/arrows so the demo behaves like the real TUI. Every control
inside is still a native button or input.
*/}
<div
ref={shellRef}
tabIndex={0}
@@ -8,21 +8,12 @@ interface FitScale {
ready: boolean
}
/**
* Scales a fixed-width design down to whatever width it is given.
*
* The TUI is laid out on a 1360px character grid; reflowing it would stop it
* looking like a terminal. So it renders at native width and gets a CSS
* transform, and the wrapper is told the resulting height so the page below
* does not sit under it.
*/
export const useFitScale = (designWidth: number): FitScale => {
const wrapRef = useRef<HTMLDivElement>(null)
const innerRef = useRef<HTMLDivElement>(null)
const [{ scale, height }, setState] = useState({ scale: 0, height: 0 })
useEffect(() => {
// ResizeObserver is browser-only; prerendering never reaches this effect.
if (typeof ResizeObserver === 'undefined') return
const measure = () => {
@@ -46,8 +37,6 @@ export const useFitScale = (designWidth: number): FitScale => {
measure()
// Both boxes matter: the wrapper for available width, the inner for the
// height that width produces once the webfont has swapped in.
const observer = new ResizeObserver(measure)
if (wrapRef.current !== null) observer.observe(wrapRef.current)
if (innerRef.current !== null) observer.observe(innerRef.current)
-12
View File
@@ -25,7 +25,6 @@ const buttonVariants = cva(
type ButtonVariants = VariantProps<typeof buttonVariants>
interface ButtonProps extends ButtonVariants {
/** Internal route path, or an absolute URL for an off-site link. */
href: string
children: React.ReactNode
className?: string
@@ -33,17 +32,6 @@ interface ButtonProps extends ButtonVariants {
const isExternal = (href: string): boolean => /^https?:\/\//.test(href)
/**
* Every call to action on the site is a link, so this renders an anchor
* rather than a `<button>`. External links open in a new tab and carry
* `rel="noopener"`; internal ones route through React Router so navigation
* stays client-side.
*
* The leaving-the-site mark is drawn by the component, never typed into a
* label. Hand-written "GitHub ↗" put the same glyph in thirteen strings and
* made it copy — it belongs to the control, so the control renders it, once,
* and only when the link actually leaves.
*/
export const Button = ({ href, variant, size, children, className }: ButtonProps) => {
const classes = cn(buttonVariants({ variant, size }), className)
@@ -6,7 +6,6 @@ interface CommandPillProps {
className?: string
}
/** The one-line install command, click-to-copy. */
export const CommandPill = ({ text, className }: CommandPillProps) => {
const { copied, copy } = useCopy()
@@ -6,8 +6,6 @@ interface CopyButtonProps {
className?: string
}
/** The copy control in a terminal header. Styled against the terminal
* surface tokens, not the page ones, so it reads as part of the window. */
export const CopyButton = ({ text, className }: CopyButtonProps) => {
const { copied, copy } = useCopy()
@@ -37,9 +37,7 @@ const FeatureCommand = ({ text }: FeatureCommandProps) => {
}
interface FeatureCardProps {
/** A single glyph. Decorative — the title carries the meaning. */
glyph?: string
/** A CSS colour for the glyph, from the semantic token set. */
color?: string
title: string
children: React.ReactNode
@@ -61,9 +59,6 @@ export const FeatureCard = ({
className,
)}
>
{/* The glyph sits in the line like a prompt marker, not in a chip. A
tinted rounded square around an icon is the stock feature-card
treatment; here the terminal already gives us a better idiom. */}
<div className="flex items-center gap-2.5">
{glyph !== undefined && (
<span
@@ -5,7 +5,6 @@ interface CodeProps {
className?: string
}
/** Anything typeable: flags, commands, env vars, crate names, paths. */
export const Code = ({ children, className }: CodeProps) => (
<code
className={cn(
-7
View File
@@ -5,7 +5,6 @@ interface WrapProps {
className?: string
}
/** The site's single content column. One max-width, one gutter, everywhere. */
export const Wrap = ({ children, className }: WrapProps) => (
<div className={cn('mx-auto w-full max-w-[1140px] px-[18px] sm:px-7', className)}>
{children}
@@ -14,9 +13,7 @@ export const Wrap = ({ children, className }: WrapProps) => (
interface SectionProps {
children: React.ReactNode
/** Drops the top hairline — for the first section under the nav. */
flush?: boolean
/** Panel background plus rules top and bottom. */
band?: boolean
id?: string
className?: string
@@ -83,7 +80,6 @@ interface PageHeadProps {
children: React.ReactNode
}
/** The h1 block every subpage opens with. */
export const PageHead = ({ eyebrow, title, children }: PageHeadProps) => (
<Section flush className="pt-10">
<div className="flex max-w-[660px] flex-col gap-3.5">
@@ -98,13 +94,11 @@ export const PageHead = ({ eyebrow, title, children }: PageHeadProps) => (
interface ColsProps {
children: React.ReactNode
/** Aligns columns to their tops instead of stretching. */
start?: boolean
center?: boolean
className?: string
}
/** The site's only multi-column layout: two up on desktop, stacked below. */
export const Cols = ({ children, start, center, className }: ColsProps) => (
<div
className={cn(
@@ -123,7 +117,6 @@ interface ProseProps {
className?: string
}
/** Body copy: secondary colour, generous leading, `+p` spacing. */
export const Prose = ({ children, className }: ProseProps) => (
<div
className={cn(
-4
View File
@@ -17,10 +17,6 @@ interface OsIconProps {
className?: string
}
/*
* Labelled rather than decorative: below 520px the adjacent text is hidden,
* so the icon becomes the only thing naming the platform.
*/
export const OsIcon = ({ name, className }: OsIconProps) => (
<svg
viewBox="0 0 24 24"
-19
View File
@@ -1,18 +1,12 @@
import { cn } from '@/lib/utils'
interface PanelProps {
/** Pane number, rendered in signal blue like the TUI's `[2]` tags. */
num?: number | undefined
title: string
children: React.ReactNode
className?: string
}
/**
* The fieldset/legend motif lifted from the TUI: a bordered box whose title
* breaks the top border. `bg-base` on the legend is what punches the gap, so
* it has to match whatever surface the panel sits on.
*/
export const Panel = ({ num, title, children, className }: PanelProps) => (
<div
className={cn(
@@ -21,8 +15,6 @@ export const Panel = ({ num, title, children, className }: PanelProps) => (
)}
>
<span className="absolute -top-2.5 left-4 bg-base px-2 text-label uppercase tracking-[var(--tracking-label)] text-secondary">
{/* --color-info, not the raw --color-signal: this panel renders on the
page, outside .tui-content, where the raw palette never re-cuts. */}
{num !== undefined && <span className="text-info">[{num}] </span>}
{title}
</span>
@@ -37,17 +29,6 @@ interface CalloutProps {
className?: string
}
/**
* An aside. `warn` carries the tape amber.
*
* The tone lives in the label, not in a coloured left rule — one ribbon per
* card is decoration, and the label was already saying the same thing twice.
*
* But hue alone cannot carry it: in the light theme --status-warning and
* --accent-text are exactly isoluminant (5.14:1 each on --bg-panel, 1.00:1
* against each other), so a red-green deficient reader sees one colour. The
* "!" is the second channel the left rule used to provide by sheer area.
*/
export const Callout = ({ title, tone = 'accent', children, className }: CalloutProps) => (
<div
className={cn(
-8
View File
@@ -1,7 +1,6 @@
import { CopyButton } from '@/components/ui/copy-button'
import { cn } from '@/lib/utils'
/** `cmd` gets a prompt glyph; `cm` is a comment; `code` and `out` are plain. */
export type TermLineKind = 'cmd' | 'cm' | 'code' | 'out'
export interface TermLine {
@@ -19,16 +18,10 @@ const LINE_TONE: Record<TermLineKind, string> = {
interface TerminalProps {
label?: string
lines: readonly TermLine[]
/** Overrides what the copy button yields. Defaults to the commands only. */
copy?: string
className?: string
}
/**
* A terminal window. Paints from the `--tm-*` token set rather than the page
* tokens, so it stays a dark terminal in light mode instead of dissolving
* into the page.
*/
export const Terminal = ({ label, lines, copy, className }: TerminalProps) => {
const copyText =
copy ??
@@ -53,7 +46,6 @@ export const Terminal = ({ label, lines, copy, className }: TerminalProps) => {
<div className="overflow-x-auto px-4 py-4 text-[12.5px] leading-[1.75] text-[var(--tm-fg)] sm:text-[13.5px]">
{lines.map((line, i) => (
<span
// Lines are static content, never reordered — index is stable here.
key={i}
className={cn('block whitespace-pre', LINE_TONE[line.t])}
>
-6
View File
@@ -5,12 +5,6 @@ import { StaticRouter } from 'react-router'
import { App } from '@/App'
import { ThemeProvider } from '@/theme/theme-provider'
/**
* Renders one route to a static HTML string.
*
* `location` is the app-relative path (no base prefix) — the client mounts
* with `basename`, so the two see the same paths and hydration matches.
*/
export const render = (location: string): string =>
renderToString(
<StrictMode>
+1 -9
View File
@@ -2,14 +2,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'
const RESET_MS = 1400
/**
* Copy-to-clipboard with a self-clearing "copied" flag.
*
* no execCommand fallback. `navigator.clipboard` is available in
* every secure context, and the site only ever ships over HTTPS (GitHub
* Pages) or localhost. If a copy does fail, the flag stays off and the text
* is still selectable by hand.
*/
export const useCopy = (): { copied: boolean; copy: (text: string) => void } => {
const [copied, setCopied] = useState(false)
const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -31,7 +23,7 @@ export const useCopy = (): { copied: boolean; copy: (text: string) => void } =>
}, RESET_MS)
},
() => {
// Permission denied or no clipboard: leave the button untouched.
setCopied(false)
},
)
}, [])
-7
View File
@@ -3,13 +3,6 @@ import { useLocation } from 'react-router'
import { metaFor } from '@/lib/routes'
/**
* Keeps the title and meta description in step with the current route.
*
* Each route is also prerendered with its tags already in the HTML, so
* crawlers never depend on this running; it only covers client-side
* navigation after the first paint.
*/
export const usePageMeta = (): void => {
const { pathname } = useLocation()
+15 -78
View File
@@ -1,39 +1,25 @@
@import 'tailwindcss';
/*
* soulseek-rs — design tokens.
*
* Ported verbatim from the design system's tokens/{colors,typography,spacing}.css.
* The same values back the TUI in soulseek-rs/src/ui/styles.rs, so the terminal
* and the site stay one product. Dark is the native theme; light re-cuts the
* semantic hues ~10% darker because phosphor and tape read on vinyl but wash
* out on manila.
*
* Theme switching is driven by [data-theme] on <html>, matching the design's
* own selector, rather than Tailwind's default `.dark` class.
*/
@custom-variant light (&:where([data-theme='light'], [data-theme='light'] *));
@theme {
/* ---- Raw palette (dark-native names) ---- */
--color-sleeve: #14110f; /* warm off-black — cardboard shadow, never #000 */
--color-vinyl: #1e1a17; /* panels / surfaces, one step up from base */
--color-vinyl-2: #26211d; /* raised surface / hover on panels */
--color-dust: #8a8178; /* muted warm grey — secondary text, borders */
--color-paper: #e8e1d6; /* aged insert card — primary text, never #FFF */
--color-sleeve: #14110f;
--color-vinyl: #1e1a17;
--color-vinyl-2: #26211d;
--color-dust: #8a8178;
--color-paper: #e8e1d6;
--color-paper-dim: #c9c1b4;
--color-oxide: #c1502e; /* iron-oxide accent, deeper than Rust's own #CE422B */
--color-tape: #d8a657; /* cassette-shell amber — warnings, queued */
--color-phosphor: #7fb685; /* desaturated LCD green — success, connected */
--color-signal: #6e9dc9; /* softened windows-classic blue — links, info */
--color-alarm: #b33a3a; /* error red, distinguishable from oxide */
--color-oxide: #c1502e;
--color-tape: #d8a657;
--color-phosphor: #7fb685;
--color-signal: #6e9dc9;
--color-alarm: #b33a3a;
--color-manila: #f2ede4;
--color-manila-2: #e8e1d6;
--color-manila-3: #ded5c7;
/* ---- Semantic aliases, wired to the runtime vars below ---- */
--color-base: var(--bg-base);
--color-panel: var(--bg-panel);
--color-raised: var(--bg-raised);
@@ -46,13 +32,6 @@
--color-line: var(--border-default);
--color-line-strong: var(--border-strong);
/*
* --accent fills; --accent-text writes.
*
* The oxide accent is a mid-tone: it clears 4.5:1 as a background behind
* white, but not as 11px text on vinyl. Splitting the two uses lets the
* fill keep the brand hue while the text cut earns its contrast.
*/
--color-accent: var(--accent);
--color-accent-text: var(--accent-text);
--color-accent-hover: var(--accent-hover);
@@ -64,13 +43,11 @@
--color-link: var(--link);
--color-link-hover: var(--link-hover);
/* ---- Typography: monospace, non-negotiable. It's a terminal application. ---- */
--font-mono:
'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
/* the "earned wink" — the era's forum font. One surface only. */
--font-forum: Verdana, Tahoma, sans-serif;
/* Scale, from the brandbook specimen (48/56, 32/40, 20/28, 14/22, 12/18) */
--text-display: 48px;
--text-display--line-height: 56px;
--text-title: 32px;
@@ -100,14 +77,12 @@
}
}
/* ---- DARK is the default theme ---- */
:root {
--bg-base: var(--color-sleeve);
--bg-panel: var(--color-vinyl);
--bg-raised: var(--color-vinyl-2);
--bg-selection: rgb(110 157 201 / 0.22);
/* Brightened from the raw palette for web body contrast — still warm. */
--text-primary: #ffffff;
--text-secondary: #e4dfd8;
--text-muted: #bdb4a9;
@@ -117,7 +92,7 @@
--border-strong: var(--color-dust);
--accent: var(--color-oxide);
/* Raised off #c1502e, which reads 3.99:1 as small text on vinyl. */
--accent-text: #cd7155;
--accent-hover: #d05f3c;
@@ -128,7 +103,6 @@
--link: var(--color-signal);
--link-hover: #8bb4dc;
/* Terminal surface — a real terminal, darker than the page around it. */
--tm-bg: #1e1a17;
--tm-hd: #181513;
--tm-fg: #ffffff;
@@ -146,19 +120,12 @@
color-scheme: dark;
}
/* ---- LIGHT: base becomes manila, sleeve becomes ink, accents darken ~10% ---- */
[data-theme='light'] {
--bg-base: var(--color-manila);
--bg-panel: var(--color-manila-2);
--bg-raised: var(--color-manila-3);
--bg-selection: rgb(76 127 174 / 0.2);
/*
* Every value below clears 4.5:1 against --bg-raised, the darkest of the
* three light surfaces, so each one holds on base, panel and raised alike.
* The design's original cuts were tuned against the base only, which left
* links at 3.64:1 and the success green at 2.86:1.
*/
--text-primary: var(--color-sleeve);
--text-secondary: #574e45;
--text-muted: #645b52;
@@ -195,11 +162,6 @@
color-scheme: light;
}
/*
* Only the cut that is actually shown gets fetched; a hidden image element
* would still be downloaded. url() is relative to this stylesheet, which
* Vite rebases against `base`, so the deploy path stays in vite.config.ts.
*/
.logo-mark {
background-image: url('/logo-ink.svg');
}
@@ -208,27 +170,17 @@
background-image: url('/logo-light.svg');
}
/*
* The TUI panes paint from the raw palette (paper, dust, oxide…) because that
* is what the Rust client does. Those names do not flip with the theme, so
* each theme re-cuts them, scoped to the terminal's content.
*
* Dark brightens two of them the way a real emulator brightens its palette
* against black: #b33a3a reads 3.21:1 on vinyl and #c1502e 3.99:1 — fine for
* a glyph, not for the "Failed" and progress text beside it.
*/
.tui-content {
--color-alarm: #c26060;
--color-oxide: #c65f40;
}
/* Light would otherwise leave paper-on-paper: the whole set re-cuts dark. */
[data-theme='light'] .tui-content {
--color-paper: #14110f;
--color-paper-dim: #3a332c;
--color-dust: #645b52;
--color-oxide: #9e3f22;
--color-tape: #8d621f; /* 4.62:1 on manila; #8f6420 sat at 4.49 */
--color-tape: #8d621f;
--color-phosphor: #3f7147;
--color-signal: #35648f;
--color-alarm: #8f2626;
@@ -237,7 +189,7 @@
@layer base {
html {
scroll-behavior: smooth;
/* Anchored headings clear the sticky nav instead of hiding under it. */
scroll-padding-top: 5rem;
-webkit-text-size-adjust: 100%;
}
@@ -248,7 +200,7 @@
font-family: var(--font-mono);
font-size: var(--text-body);
line-height: var(--text-body--line-height);
/* Tabular figures EVERYWHERE — speeds, sizes, queue positions, ETAs. */
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
@@ -258,25 +210,11 @@
background: var(--bg-selection);
}
/* The design system's own focus treatment, applied once globally. */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/*
* Reduced motion, scoped rather than nuked.
*
* A blanket `*{animation-duration:0.01ms}` also kills the transitions that
* carry meaning — a button acknowledging a copy, a card answering a hover.
* Those are state changes a reduced-motion reader still wants; what they
* asked not to see is travel and looping.
*
* So: stop the looping cursor and smooth scrolling, cap the rest at a
* duration short enough to read as instant feedback rather than movement.
* The TUI goes further and skips its opening run entirely — see
* prefersReducedMotion() in components/tui/hero-terminal.tsx.
*/
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
@@ -289,7 +227,6 @@
transition-duration: 80ms !important;
}
/* Looping or travelling motion has no reduced-motion equivalent. */
[class*='animate-'] {
animation: none !important;
}
-3
View File
@@ -1,4 +1,3 @@
/** Every off-site destination, in one place. */
export const LINKS = {
gh: 'https://github.com/michel/soulseek-rs',
issues: 'https://github.com/michel/soulseek-rs/issues',
@@ -15,8 +14,6 @@ export const LINKS = {
slsknet: 'https://www.slsknet.org',
} as const
/** Injected from the workspace Cargo.toml at build time — see vite.config.ts. */
export const VERSION = __APP_VERSION__
/** Where the site is served from. Used for canonical and og:url. */
export const SITE_URL = 'https://re-invention.nl/soulseek-rs/'
-15
View File
@@ -1,23 +1,9 @@
export interface RouteMeta {
/** Path without the base prefix, always leading-slashed. */
path: string
title: string
description: string
}
/**
* The single source of truth for what pages exist.
*
* Read three times: React Router matches against it, the layout applies the
* meta on navigation, and scripts/prerender.ts walks it to decide which HTML
* files to write. Adding a page means adding one entry here plus its route.
*/
/*
* `as const satisfies` rather than a plain annotation: the annotation widened
* `path` to `string`, which is what let App.tsx's hand-written route list
* drift from this one without failing the build. Literal paths make the
* mapping in App.tsx exhaustive-checkable.
*/
export const ROUTES = [
{
path: '/',
@@ -45,7 +31,6 @@ export const ROUTES = [
},
] as const satisfies readonly RouteMeta[]
/** Every path the site serves, as literals. */
export type RoutePath = (typeof ROUTES)[number]['path']
export const NOT_FOUND_META: RouteMeta = {
-4
View File
@@ -13,8 +13,6 @@ if (container === null) throw new Error('Root element #root is missing')
const tree = (
<StrictMode>
<ThemeProvider>
{/* Must match `base` in vite.config.ts — the site is served from a
subpath, so the router has to strip it before matching. */}
<BrowserRouter basename={import.meta.env.BASE_URL}>
<App />
</BrowserRouter>
@@ -22,8 +20,6 @@ const tree = (
</StrictMode>
)
// Every route ships prerendered markup, so the normal path is to hydrate it.
// createRoot is the fallback for a container that somehow arrived empty.
if (container.hasChildNodes()) {
hydrateRoot(container, tree)
} else {
-3
View File
@@ -251,9 +251,6 @@ export const Community = () => {
</Button>
</div>
</div>
{/* Raised, not panel: this section is a band, which is itself
bg-panel — a panel-on-panel callout has only a 1.36:1 hairline
holding it together. */}
<Callout className="bg-raised" title="two things that stay true">
<p className="mb-1.5 !text-success">No telemetry. Ever.</p>
<p className="font-forum text-xs !text-muted">
-15
View File
@@ -59,14 +59,6 @@ const PANES: readonly { num?: number; title: string; body: string }[] = [
},
]
/*
* The keycap's heavier bottom edge is a shadow, not a thicker bottom border.
*
* A 2px border on one side of a rounded box thins out as it turns the corner,
* so the edge tapers exactly where a real keycap looks solid. An unblurred
* 1px shadow sits under the full curve instead, which is the look the design
* system's `.key` rule is after.
*/
const KeyRow = ({ combo, description }: { combo: string; description: string }) => (
<div className="grid grid-cols-[120px_1fr] items-baseline gap-3 border-b border-hairline py-[7px]">
<span>
@@ -204,13 +196,6 @@ export const Docs = () => {
</Section>
<Section band>
{/*
The only Panel on a band. Two consequences, both handled here:
its default bg-panel fill would match the band exactly, so it takes
bg-raised like the band callouts do; and the legend paints over the
top border to punch the gap, so it has to match the surface *behind*
the panel — the band — not the panel's own fill. See panel.tsx.
*/}
<Panel title="the rest" className="bg-raised [&>span]:bg-panel">
<Cols center className="gap-6">
<Prose>
-14
View File
@@ -104,9 +104,6 @@ export const Home = () => {
<section className="pt-5 pb-8 sm:pt-7 sm:pb-10">
<Wrap>
<div className="flex flex-col gap-[22px]">
{/* Eyebrow and sub are desktop-only: on a phone the headline and
the CTA should be all that stands between the top of the page
and the terminal. */}
<Eyebrow className="max-md:hidden">Soulseek client · terminal · Rust</Eyebrow>
<div className="flex items-start gap-3.5 sm:gap-6 md:gap-11">
<Logo size={172} className="mt-2 max-md:!h-28 max-sm:!h-16" />
@@ -139,11 +136,6 @@ export const Home = () => {
<section className="border-t border-hairline py-6">
<Wrap>
{/*
Three layouts, narrowest first: under 520px the labels drop and it
becomes a centred row of icons; up to 820px each platform is a
full-width stacked row; above that they sit inline, rule-separated.
*/}
<div className="flex flex-wrap items-center gap-3.5 sm:gap-8">
<Eyebrow className="max-[519px]:hidden">Runs on</Eyebrow>
<ul className="flex w-full flex-nowrap justify-center min-[520px]:flex-wrap min-[520px]:justify-start sm:w-auto">
@@ -230,12 +222,6 @@ export const Home = () => {
shell, so the same client works for scripting, cron jobs, and agents driving it
programmatically.
</SectionHead>
{/*
Three up, not four, with search spanning two of them: it's the thing
the client is for, and eight cards at identical weight said the
opposite. Eight items plus that one double = nine cells, so the grid
still comes out square.
*/}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{FEATURES.map((feature, i) => (
<FeatureCard
-10
View File
@@ -22,7 +22,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}`
// LIB_SRC carries no comment lines, so every line is plain code.
const LIB_LINES: TermLine[] = LIB_SRC.split('\n').map((text) => ({
t: 'code' as const,
text,
@@ -255,12 +254,6 @@ export const Install = () => {
<h3 className="mb-4 text-heading leading-[var(--text-heading--line-height)] font-medium">
{platform.name}
</h3>
{/*
Label beside value while the card is full width, label above it
once the cards go three up: at a third of the row the label
column ate enough of the line that every path broke mid-token
("config.to / ml"). Stacking gives the path the whole card.
*/}
<dl className="grid grid-cols-1 gap-x-3.5 gap-y-0.5 sm:grid-cols-[auto_1fr] sm:gap-y-2 lg:grid-cols-1 lg:gap-y-0.5">
{(
[
@@ -339,9 +332,6 @@ export const Install = () => {
/>
</Cols>
<div className="mt-5">
{/* Raised, not panel: this section is a band, which is itself
bg-panel — a panel-on-panel callout has only a 1.36:1 hairline
holding it together. */}
<Callout className="bg-raised" title="two things it will not touch">
<p>
Your downloads and your shared directories are yours: they stay exactly where
-7
View File
@@ -8,18 +8,11 @@ export const isTheme = (value: unknown): value is Theme =>
typeof value === 'string' && (THEMES as readonly string[]).includes(value)
export interface ThemeContextValue {
/** What the user picked — may be 'system'. */
theme: Theme
/** What is actually painted right now — never 'system'. */
resolvedTheme: ResolvedTheme
setTheme: (theme: Theme) => void
}
/*
* Split from the provider so this module exports only values, which keeps the
* provider file component-only for react-refresh.
*/
export const ThemeContext = createContext<ThemeContextValue | null>(null)
/** Matches the key the design's own inline boot script reads. */
export const THEME_STORAGE_KEY = 'ssrs-theme'
-2
View File
@@ -25,8 +25,6 @@ export const ThemeProvider = ({ children }: ThemeProviderProps) => {
useEffect(() => {
const root = document.documentElement
root.dataset['theme'] = resolvedTheme
// Paints native widgets (scrollbars, form controls) to match, which the
// attribute alone does not do.
root.style.colorScheme = resolvedTheme
}, [resolvedTheme])
+4 -18
View File
@@ -2,21 +2,8 @@ import { isTheme, THEME_STORAGE_KEY, type ResolvedTheme, type Theme } from './th
const DARK_QUERY = '(prefers-color-scheme: dark)'
/*
* The theme lives outside React: the boot script in index.html resolves and
* paints it before the bundle loads, and the OS can change it at any time.
* That makes it an external store, which is what useSyncExternalStore is for
* — and it is the piece that gives correct hydration for free.
*
* The server snapshots below must match what the prerendered HTML ships
* (`<html data-theme="dark">`). React renders the server snapshot for the
* hydration pass, then immediately re-renders with the real one, so a visitor
* whose actual theme is light never sees a mismatch, only a corrected render.
*/
const listeners = new Set<() => void>()
/** Notifies subscribers after a local write; OS changes come via matchMedia. */
const emit = () => {
for (const listener of listeners) listener()
}
@@ -25,7 +12,6 @@ export const subscribe = (onChange: () => void): (() => void) => {
listeners.add(onChange)
const query = window.matchMedia(DARK_QUERY)
query.addEventListener('change', onChange)
// Another tab changing the preference should move this one too.
window.addEventListener('storage', onChange)
return () => {
listeners.delete(onChange)
@@ -39,7 +25,6 @@ export const readTheme = (): Theme => {
const stored = localStorage.getItem(THEME_STORAGE_KEY)
return isTheme(stored) ? stored : 'system'
} catch {
// Safari in private mode throws on localStorage access.
return 'system'
}
}
@@ -50,15 +35,16 @@ export const readResolvedTheme = (): ResolvedTheme => {
return window.matchMedia(DARK_QUERY).matches ? 'dark' : 'light'
}
/** Both must agree with the `data-theme` baked into index.html. */
export const serverTheme = (): Theme => 'system'
export const serverResolvedTheme = (): ResolvedTheme => 'dark'
export const writeTheme = (next: Theme): void => {
export const writeTheme = (next: Theme): boolean => {
let persisted = true
try {
localStorage.setItem(THEME_STORAGE_KEY, next)
} catch {
// Not persisting is survivable; the theme still applies this session.
persisted = false
}
emit()
return persisted
}
-1
View File
@@ -1,4 +1,3 @@
/// <reference types="vite/client" />
/** Injected by `define` in vite.config.ts from the workspace Cargo.toml. */
declare const __APP_VERSION__: string
-2
View File
@@ -15,7 +15,6 @@
"resolveJsonModule": true,
"noEmit": true,
/* Strictest. Every flag below is on deliberately — see README. */
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
@@ -31,7 +30,6 @@
"skipLibCheck": true,
/* No baseUrl — deprecated in TS 6. `paths` resolves relative to this file. */
"paths": {
"@/*": ["./src/*"]
}
-1
View File
@@ -10,7 +10,6 @@
"isolatedModules": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
/* scripts/ is run straight from source by Bun, which needs the extension. */
"allowImportingTsExtensions": true,
"noEmit": true,
-6
View File
@@ -1,9 +1,3 @@
/*
* eslint-plugin-jsx-a11y ships no types, and TS's inference from its JS picks
* up the legacy eslintrc `configs` shape (plugins as string[]) instead of the
* flat-config one. Declaring the flat export is enough to type the one thing
* eslint.config.ts touches.
*/
declare module 'eslint-plugin-jsx-a11y' {
import type { Linter } from 'eslint'
-11
View File
@@ -5,11 +5,6 @@ import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
/*
* Read the version out of the workspace manifest rather than keeping a copy in
* TypeScript. release-plz bumps Cargo.toml; a hand-written constant here would
* silently advertise the previous release forever.
*/
const cargoToml = readFileSync(
fileURLToPath(new URL('../Cargo.toml', import.meta.url)),
'utf8',
@@ -19,10 +14,6 @@ if (versionMatch?.[1] === undefined)
throw new Error('could not read version from ../Cargo.toml')
const VERSION = `v${versionMatch[1]}`
// Project sites are served from https://<user>.github.io/<repo>/, so every
// asset URL needs that prefix baked in at build time.
// hardcoded. Point a custom domain at the site and this becomes '/'
// — one edit here plus the matching `basename` in src/main.tsx.
const BASE = '/soulseek-rs/'
export default defineConfig({
@@ -37,8 +28,6 @@ export default defineConfig({
},
},
build: {
// Small site; a warning at the default 500 kB would be noise. A jump past
// this means something heavy got imported by accident.
chunkSizeWarningLimit: 600,
},
})