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:
@@ -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(
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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])}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user