diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f09a83f..9607733 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,4 +1,4 @@
-# Contributing to gitlawb node explorer
+# Contributing to Twigpine Explorer
Thanks for your interest in contributing! This document covers how to get set up,
the conventions the codebase follows, and what to expect from the review process.
diff --git a/PRODUCT.md b/PRODUCT.md
index f5b28f8..70956fb 100644
--- a/PRODUCT.md
+++ b/PRODUCT.md
@@ -8,7 +8,7 @@ web
## Users
-**Primary: developers evaluating the gitlawb network.** Someone has heard about gitlawb — from a shared repo link, the docs, a mention of "signed git pushes without a blockchain" — and arrives wanting to judge whether it is real and worth their time. They are skeptical by default. They are looking for proof of life: real repositories with real names, pushes that happened minutes ago, signatures they could verify themselves. The job is *forming a judgment*, not completing a task. Credibility and comprehension at first contact outrank throughput.
+**Primary: developers evaluating the Twigpine network.** Someone has heard about Twigpine — from a shared repo link, the docs, a mention of "signed git pushes without a blockchain" — and arrives wanting to judge whether it is real and worth their time. They are skeptical by default. They are looking for proof of life: real repositories with real names, pushes that happened minutes ago, signatures they could verify themselves. The job is *forming a judgment*, not completing a task. Credibility and comprehension at first contact outrank throughput.
Secondary audiences exist and are served, but do not steer design decisions:
@@ -17,21 +17,21 @@ Secondary audiences exist and are served, but do not steer design decisions:
## Product Purpose
-A live, read-only web explorer for a single gitlawb node. It renders the node's REST API — repositories, code, commits, pulls, issues, push events, signed certificates, registered agents, peers, and delegated tasks — as a browsable surface.
+A live, read-only web explorer for a single Twigpine node. It renders the node's REST API — repositories, code, commits, pulls, issues, push events, signed certificates, registered agents, peers, and delegated tasks — as a browsable surface.
Its real function is evidentiary. Anyone can claim to run a decentralized git network; the explorer is the artifact that demonstrates one is actually running, populated, and signing. Success is a skeptical developer leaving convinced the network is real and understanding what makes it different.
## Positioning
-gitlawb's mechanism is verifiable provenance without accounts or a blockchain: identity is an Ed25519 DID keypair generated on the user's machine, every push is signed under RFC 9421 HTTP signatures, and every push produces a ref-update certificate anyone can verify independently. Repositories are cloned over a `gitlawb://` scheme via the `git-remote-gitlawb` helper. Public nodes gate registration behind iCaptcha, a proof-of-intelligence challenge, rather than proof-of-work or human-only CAPTCHAs — agents are first-class participants, not adversaries to be filtered out.
+Twigpine's mechanism is verifiable provenance without accounts or a blockchain: identity is an Ed25519 DID keypair generated on the user's machine, every push is signed under RFC 9421 HTTP signatures, and every push produces a ref-update certificate anyone can verify independently. Repositories are cloned over a `gitlawb://` scheme via the `git-remote-gitlawb` helper. Public nodes gate registration behind iCaptcha, a proof-of-intelligence challenge, rather than proof-of-work or human-only CAPTCHAs — agents are first-class participants, not adversaries to be filtered out.
-A conventional forge cannot truthfully copy this: its authority comes from an account system and a server that vouches for history. gitlawb's comes from signatures anyone can check without trusting the node that served them.
+A conventional forge cannot truthfully copy this: its authority comes from an account system and a server that vouches for history. Twigpine's comes from signatures anyone can check without trusting the node that served them.
The explorer's job is to make that mechanism *visible* rather than merely stated. Certificates, pushers, DIDs, and cert IDs are not incidental metadata on this surface — they are the evidence.
## Operating Context
-- The explorer is a pure client of one node's REST API (`node.gitlawb.com`). It holds no database and no server state of its own.
+- The explorer is a pure client of one node's REST API (`node.twigpine.com`). It holds no database and no server state of its own.
- Nodes serve no CORS headers, so every request goes through a same-origin proxy: Vite's dev proxy locally, Vercel rewrites in production. Peer nodes are reached through per-node path prefixes. This list is duplicated in three files and must be kept in sync.
- Data arrives from a live public network. Repository names, descriptions, and owner DIDs are attacker-influenceable strings and must be treated as untrusted in both rendering and routing.
- Much of the live corpus is machine-generated: at time of writing, roughly 5,900 repositories, many named `pixel-655812`, `astro-505073`, described as "automated repo" or "mirrored from peer", all on branch `main`, all public. The interface's real content is repetitive by nature. Designs validated against curated sample data will not survive contact with it.
@@ -57,17 +57,25 @@ The explorer's job is to make that mechanism *visible* rather than merely stated
## Brand Commitments
-- **Name:** gitlawb. The site is "gitlawb node explorer" at `explorer.gitlawb.com`.
-- **Mark:** the official gitlawb glyph, already extracted from the platform brand asset and shipped at [Logo.tsx](src/components/ui/Logo.tsx) and [favicon.svg](public/favicon.svg). Binding — carry it forward.
-- **Visual register: modern product showcase.** The explorer is a shop window for the network as much as a tool — it has to look impressive to a developer meeting gitlawb for the first time. Motion, depth, and visual ambition are wanted, delivered through the Magic UI and Aceternity component libraries the user chose. Data density and readability still bind on list-heavy surfaces (repos index, repo detail), but the overview is allowed to sell.
+- **Name:** Twigpine Explorer, "Explorer by Twigpine" in prose. The site is at `explorer.twigpine.com`. The lockup is the Pine mark, lowercase `twigpine`, then `/ explorer` dimmer.
+- **Mark:** the Pine mark (stem, three tiers, a bud), shipped at [Mark.tsx](src/components/brand/Mark.tsx) and [favicon.svg](public/favicon.svg) with the same geometry as twigpine.com. Binding.
+- **Visual system: Canopy, the Twigpine theme.** A white ledger with one ink. Values and rules are the ones twigpine.com ships; the tokens are in [index.css](src/index.css).
+ - **One ink, one accent.** Every colour is the ink at a few opacities over paper, plus the needle. The needle means one thing only: newest, live now. It marks the newest row of a ledger and the Live dot. It is never a link, a button, a hover, a success state or a chart bar.
+ - **Status in words.** States are told apart by the word and by ink weight. Only a failure takes a colour, because a failure has to be findable in a long column.
+ - **Type.** Geist for the interface and headings, sentence case, no caps labels and no letterspacing. Geist Mono only for what a machine emits or a person types: hashes, DIDs, refs, paths, commands.
+ - **Shape.** Radius 0, 2px on buttons and inputs, circles for dots. No shadows, blur, gradients or glass. Regions are ruled, not boxed.
+ - **Every image is data.** The page's picture is the live ledger. Instrument chrome (corner brackets, a tick ruler) frames the places where the page reads an instrument: the ledger and a certificate.
+ - **Motion.** One: the bud arriving on a new newest row. Numbers are stated, never counted up.
+ - **Theme.** Follows the OS until the reader picks one; both themes are first-class.
+- **Names that do not change.** The `gitlawb://` scheme, the `gl` and `git-remote-gitlawb` binaries, the `gitlawb/ref-updates/v1` topic, `did:gitlawb`, the `@gitlawb/gl` package and the `GITLAWB_*` settings are protocol and client identifiers. Renaming them breaks clients, so copy uses them as they are.
- **Superseded (do not revive):**
- - *"The Register"* — light safety-paper ground, engraved guilloche from hashes, wax-seal marks, Bodoni display. Rejected as unprofessional for this audience.
- - *Plain forge convention* — a straight GitHub/Sourcegraph rendition. Correct but judged too plain: "we need to make modern which need to showcase". The conventional structure survives on dense list surfaces; the flat, unaccented treatment does not.
-- **Libraries are load-bearing.** Aceternity, Magic UI, and GSAP were chosen by the user and are to be used as real components, not reimplemented by hand.
+ - *"The Register"*: light safety-paper ground, engraved guilloche from hashes, wax-seal marks, Bodoni display.
+ - *Plain forge convention*: a straight GitHub/Sourcegraph rendition.
+ - *Modern product showcase* (August 2026): the black gitlawb identity in Geist Mono with Magic UI, Aceternity and GSAP effects (globe, orbit, beams, aurora text, confetti). Replaced by Canopy in November 2026; the libraries were removed.
## Evidence on Hand
-- A live, populated node at `node.gitlawb.com` — real repositories, real signed pushes, real agents and certificates. This is the product's strongest asset and the interface's actual content.
+- A live, populated node at `node.twigpine.com` — real repositories, real signed pushes, real agents and certificates. This is the product's strongest asset and the interface's actual content.
- Written documentation in `public/docs/` (quickstart, agents, protocol, node), plus `llms.txt` and a packaged agent skill at `public/skill.md`. Factual and specific; usable as copy source.
- Screenshots of the v0.3.8 interface in `reference/`, predating the current code.
- **Absent — must not be fabricated:** testimonials, named users or customers, adoption or traffic numbers, benchmarks, funding, pricing, uptime or SLA claims, security audits, and any endorsement. The network is described in its own docs as alpha. No claim of scale or production-readiness beyond what the live API returns is available.
diff --git a/README.md b/README.md
index e9d68c9..8dbab21 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
-# gitlawb node explorer
+# Twigpine Explorer
[](https://github.com/Twigpine/node-explorer/actions/workflows/ci.yml)
[](LICENSE)
-A terminal-styled web explorer for a [gitlawb](https://twigpine.com) node — browse live
+A web explorer for a [Twigpine](https://twigpine.com) node: browse live
repositories and registered agents, inspect code, commits, pulls, issues, certs, and
push events, straight from the node's REST API.
@@ -19,7 +19,7 @@ push events, straight from the node's REST API.
- **Agents view** — registered agents with capabilities, trust tiers, and last-seen.
- **Keyboard-first** — command palette, fuzzy file finder (background tree indexing),
and a shortcuts cheatsheet.
-- **Terminal aesthetic** — dark-first JetBrains Mono design with a light theme toggle.
+- **Canopy, the Twigpine theme** — one ink on paper, Geist and Geist Mono, light and dark.
## Quick start
@@ -50,7 +50,7 @@ through a same-origin proxy. To point at a different node, change the proxy
## Data source
-The explorer is a read-only client of a gitlawb node's REST API:
+The explorer is a read-only client of a Twigpine node's REST API:
- `GET /api/v1/repos?limit=&offset=&owner=` — server-side pagination via the
`X-Total-Count` response header (limit is clamped to 200 by the node).
@@ -114,9 +114,10 @@ marked + Shiki (lazy-loaded) · Radix UI primitives · Vitest
## Design
-Reference mockups live in `reference/`. The look is dark-first terminal monospace
-(JetBrains Mono): grid-line hero panels, uppercase micro-labels, square pills, and a
-footer carrying the node DID and version. Light theme is available via the toggle.
+The look is Canopy, the Twigpine theme: one ink on paper, square corners, ruled
+tables, and a single accent (the needle) that marks only what is newest and live.
+The rules are written down in [PRODUCT.md](PRODUCT.md) under Brand Commitments, and the
+tokens are in `src/index.css`. The screenshots in `reference/` predate this design.
## Contributing
diff --git a/api/_lib/repoSocial.test.ts b/api/_lib/repoSocial.test.ts
index 5491000..794ff01 100644
--- a/api/_lib/repoSocial.test.ts
+++ b/api/_lib/repoSocial.test.ts
@@ -63,7 +63,7 @@ describe('cleanSocialText', () => {
it('returns an explicit fallback description for empty text', () => {
expect(buildRepoDescription({ ...REPO, description: ' \n\t ' })).toBe(
- 'Explore z6MkqRzA/digital-garden on gitlawb explorer.',
+ 'Explore z6MkqRzA/digital-garden on Twigpine Explorer.',
);
});
@@ -95,7 +95,7 @@ describe('escapeHtml', () => {
});
expect(escapeHtml(model.title)).toBe(
- 'z6MkqRzA/<script>alert("owned")</script> · gitlawb explorer',
+ 'z6MkqRzA/<script>alert("owned")</script> · Twigpine Explorer',
);
expect(escapeHtml(model.description)).toBe(
'Ship & 'share' <b>bold</b>',
@@ -125,7 +125,7 @@ describe('buildRepoSocialModel', () => {
expect(buildRepoSocialModel(REPO)).toMatchObject({
name: 'digital-garden',
repositoryLabel: 'z6MkqRzA/digital-garden',
- title: 'z6MkqRzA/digital-garden · gitlawb explorer',
+ title: 'z6MkqRzA/digital-garden · Twigpine Explorer',
description: REPO.description,
branch: 'main',
visibility: 'public',
diff --git a/api/_lib/repoSocial.ts b/api/_lib/repoSocial.ts
index 1b8fbe7..0b26de9 100644
--- a/api/_lib/repoSocial.ts
+++ b/api/_lib/repoSocial.ts
@@ -48,7 +48,7 @@ export interface RepoSocialModel {
canonicalPath: string;
}
-const SITE_NAME = 'gitlawb explorer';
+const SITE_NAME = 'Twigpine Explorer';
const DEFAULT_TEXT_LIMIT = 240;
const OWNER_LIMIT = 256;
const NAME_LIMIT = 96;
diff --git a/api/_tests/docs-page.test.ts b/api/_tests/docs-page.test.ts
index 51a6e08..7f636a9 100644
--- a/api/_tests/docs-page.test.ts
+++ b/api/_tests/docs-page.test.ts
@@ -87,7 +87,7 @@ describe('docs page HTML shell', () => {
);
expect(response.headers.get('vary')).toBe('Accept, User-Agent');
expect(staticFetch).toHaveBeenCalledTimes(1);
- expect(html).toContain('
for AI agents · docs · gitlawb explorer');
+ expect(html).toContain('for AI agents · docs · Twigpine Explorer');
expect(html).toContain('content="https://preview.example/og-docs.png"');
expect(html).toContain('href="https://preview.example/docs/agents"');
expect(html).not.toContain('old metadata');
@@ -102,7 +102,7 @@ describe('docs page HTML shell', () => {
const html = await response.text();
expect(response.status).toBe(200);
- expect(html).toContain('docs · gitlawb explorer');
+ expect(html).toContain('docs · Twigpine Explorer');
expect(html).toContain('href="https://preview.example/docs"');
expect(html).toContain('content="https://preview.example/og-docs.png"');
});
@@ -114,7 +114,7 @@ describe('docs page HTML shell', () => {
const html = await response.text();
expect(response.status).toBe(200);
- expect(html).toContain('docs · gitlawb explorer');
+ expect(html).toContain('docs · Twigpine Explorer');
expect(html).toContain('href="https://preview.example/docs"');
expect(html).not.toContain('');
});
@@ -141,7 +141,7 @@ describe('docs page HTML shell', () => {
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toContain('text/html');
- expect(html).toContain('for AI agents · docs · gitlawb explorer');
+ expect(html).toContain('for AI agents · docs · Twigpine Explorer');
expect(html).toContain('');
});
@@ -181,7 +181,7 @@ describe('docs page markdown negotiation', () => {
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('text/markdown; charset=utf-8');
- expect(body).toContain('# gitlawb docs');
+ expect(body).toContain('# Twigpine docs');
expect(body).toContain('https://preview.example/docs/quickstart.md');
expect(body).toContain('https://preview.example/docs/agents.md');
expect(body).toContain('https://preview.example/skill.md');
@@ -196,7 +196,7 @@ describe('docs page markdown negotiation', () => {
const body = await response.text();
expect(response.status).toBe(200);
- expect(body).toContain('# gitlawb docs');
+ expect(body).toContain('# Twigpine docs');
expect(body).not.toContain('nope');
});
@@ -221,7 +221,7 @@ describe('docs page markdown negotiation', () => {
const response = await GET(docsRequest('protocol', { accept: '*/*' }));
const body = await response.text();
- expect(body).not.toContain('# gitlawb docs');
+ expect(body).not.toContain('# Twigpine docs');
expect(body).not.toContain('All guides are served as raw markdown');
expect(response.status).toBe(404);
});
@@ -255,7 +255,7 @@ describe('docs page markdown negotiation', () => {
);
expect(response.status).toBe(404);
- expect(await response.text()).not.toContain('# gitlawb docs');
+ expect(await response.text()).not.toContain('# Twigpine docs');
});
it('rejects an HTML shell served in place of a document', async () => {
diff --git a/api/_tests/repo-page.test.ts b/api/_tests/repo-page.test.ts
index 5245735..8a593aa 100644
--- a/api/_tests/repo-page.test.ts
+++ b/api/_tests/repo-page.test.ts
@@ -106,7 +106,7 @@ describe('repository page HTML shell', () => {
expect(indexFetch).toHaveBeenCalledTimes(1);
expect(fetchRepositoryMock).toHaveBeenCalledTimes(1);
expect(fetchRepositoryMock).toHaveBeenCalledWith('z6Owner', 'demo<&', expect.any(AbortSignal));
- expect(html).toContain('z6Owner/demo<& · gitlawb explorer');
+ expect(html).toContain('z6Owner/demo<& · Twigpine Explorer');
expect(html).toContain('content="A "quoted" <repository> & more"');
expect(html).toContain(
'href="https://preview.example/repos/z6Owner/demo%3C%26"',
@@ -153,7 +153,7 @@ describe('repository page HTML shell', () => {
expect(response.status).toBe(200);
expect(response.headers.get('cache-control')).toBe('private, no-store, max-age=0');
expect(response.headers.get('x-robots-tag')).toBe('noindex, noarchive');
- expect(html).toContain('gitlawb explorer');
+ expect(html).toContain('Twigpine Explorer');
expect(html).toContain('');
expect(html).not.toContain('secret-name');
expect(html).not.toContain('extremely secret description');
diff --git a/api/docs-page.ts b/api/docs-page.ts
index 93bfdef..c4b646f 100644
--- a/api/docs-page.ts
+++ b/api/docs-page.ts
@@ -17,8 +17,8 @@ import { markdownResponse, prefersHtml, NEGOTIATION_VARY } from './_lib/negotiat
// served before rewrites, so only extensionless page routes reach this
// function.
-const SITE_NAME = 'gitlawb explorer';
-const IMAGE_ALT = 'gitlawb docs — guides for agents, humans, and node operators';
+const SITE_NAME = 'Twigpine Explorer';
+const IMAGE_ALT = 'Twigpine docs: guides for agents, humans, and node operators';
const SOCIAL_META_PATTERN =
/[\s\S]*?/;
@@ -38,21 +38,21 @@ interface DocMeta {
const DOCS: Record = {
quickstart: {
title: `quickstart · docs · ${SITE_NAME}`,
- description: 'Install gl and make your first signed push on the gitlawb network in about two minutes.',
+ description: 'Install gl and make your first signed push on the Twigpine network in about two minutes.',
},
agents: {
title: `for AI agents · docs · ${SITE_NAME}`,
description:
- 'End-to-end instructions for agents operating on gitlawb — install, identity, signed pushes, PRs, MCP server, and failure modes.',
+ 'End-to-end instructions for agents operating on Twigpine: install, identity, signed pushes, PRs, MCP server, and failure modes.',
},
protocol: {
title: `protocol · docs · ${SITE_NAME}`,
description:
- 'How gitlawb works: DIDs, RFC 9421 signed pushes, UCAN delegation, iCaptcha, three-tier storage, libp2p networking, and ref consensus without a blockchain.',
+ 'How Twigpine works: DIDs, RFC 9421 signed pushes, UCAN delegation, iCaptcha, three-tier storage, libp2p networking, and ref consensus without a blockchain.',
},
node: {
title: `run a node · docs · ${SITE_NAME}`,
- description: 'Register and operate a gitlawb node — deployment, environment reference, and health checks.',
+ description: 'Register and operate a Twigpine node: deployment, environment reference, and health checks.',
},
};
@@ -170,9 +170,9 @@ const HUB_MARKER = 'All guides are served as raw markdown at stable URLs';
/** Markdown index served to text clients at the bare /docs route. */
function hubMarkdown(origin: string): string {
const lines = [
- '# gitlawb docs',
+ '# Twigpine docs',
'',
- '> gitlawb is a decentralized git network where AI agents and humans collaborate as equals. Identity is an Ed25519 DID keypair; every push is signed and produces a verifiable ref-update certificate.',
+ '> Twigpine is a decentralized git network where AI agents and humans collaborate as equals. Identity is an Ed25519 DID keypair; every push is signed and produces a verifiable ref-update certificate.',
'',
`${HUB_MARKER} — fetch these directly:`,
'',
diff --git a/api/og.tsx b/api/og.tsx
index 29c5280..8ba4713 100644
--- a/api/og.tsx
+++ b/api/og.tsx
@@ -19,19 +19,29 @@ const REPOSITORY_CACHE_CONTROL =
const FALLBACK_CACHE_CONTROL =
'public, max-age=60, s-maxage=300, stale-while-revalidate=600, no-transform';
-const regularFontUrl = new URL(
- '../node_modules/@expo-google-fonts/jetbrains-mono/400Regular/JetBrainsMono_400Regular.ttf',
- import.meta.url,
-);
-const boldFontUrl = new URL(
- '../node_modules/@expo-google-fonts/jetbrains-mono/700Bold/JetBrainsMono_700Bold.ttf',
- import.meta.url,
-);
+const FONT_FACES = [
+ ['Geist', 400],
+ ['Geist', 500],
+ ['Geist Mono', 400],
+ ['Geist Mono', 500],
+] as const;
+
+// One per face above, in the same order. Literal `new URL(..., import.meta.url)`
+// calls: Vercel's
+// function tracer follows these to bundle the font files, and it cannot follow
+// a path built at runtime.
+const fontUrls = [
+ new URL('../node_modules/@expo-google-fonts/geist/400Regular/Geist_400Regular.ttf', import.meta.url),
+ new URL('../node_modules/@expo-google-fonts/geist/500Medium/Geist_500Medium.ttf', import.meta.url),
+ new URL('../node_modules/@expo-google-fonts/geist-mono/400Regular/GeistMono_400Regular.ttf', import.meta.url),
+ new URL('../node_modules/@expo-google-fonts/geist-mono/500Medium/GeistMono_500Medium.ttf', import.meta.url),
+];
type OgFonts = NonNullable;
interface FontAssets {
fonts: OgFonts;
+ /** Geist Mono regular: the face the repository name and branch are set in. */
regular: OpenTypeFont;
}
@@ -41,30 +51,20 @@ function exactArrayBuffer(bytes: Uint8Array): ArrayBuffer {
return copy.buffer;
}
-// Start both reads at module initialization so concurrent requests share the
+// Start the reads at module initialization so concurrent requests share the
// same work. TTF is intentional: Satori cannot render WOFF2 font data.
-const fontAssetsPromise: Promise = Promise.all([
- readFile(regularFontUrl),
- readFile(boldFontUrl),
-]).then(([regular, bold]) => {
- const regularData = exactArrayBuffer(regular);
- const boldData = exactArrayBuffer(bold);
+const fontAssetsPromise: Promise = Promise.all(
+ fontUrls.map(url => readFile(url)),
+).then(files => {
+ const data = files.map(exactArrayBuffer);
return {
- fonts: [
- {
- name: 'JetBrains Mono',
- data: regularData,
- style: 'normal' as const,
- weight: 400 as const,
- },
- {
- name: 'JetBrains Mono',
- data: boldData,
- style: 'normal' as const,
- weight: 700 as const,
- },
- ],
- regular: parse(regularData),
+ fonts: FONT_FACES.map(([name, weight], index) => ({
+ name,
+ data: data[index],
+ style: 'normal' as const,
+ weight,
+ })),
+ regular: parse(data[2]),
};
});
@@ -197,17 +197,23 @@ import type { CSSProperties, ReactElement, ReactNode } from 'react';
export const OG_WIDTH = 1200;
export const OG_HEIGHT = 630;
+// Canopy, the Twigpine theme: paper, one ink at a few opacities, and the
+// needle, which appears once, as the bud on the mark.
const colors = {
- background: '#0a0a0a',
- foreground: '#f2f2f2',
- surface: '#0d0d0d',
- border: '#1c1c1c',
- grid: '#131313',
- muted: '#8a8a8a',
- dim: '#555555',
- warm: '#c9a96e',
+ paper: '#ffffff',
+ ink: '#0c1210',
+ ink2: 'rgba(12, 18, 16, 0.72)',
+ ink3: 'rgba(12, 18, 16, 0.62)',
+ ink4: 'rgba(12, 18, 16, 0.48)',
+ rule: 'rgba(12, 18, 16, 0.12)',
+ rule2: 'rgba(12, 18, 16, 0.22)',
+ grid: 'rgba(12, 18, 16, 0.045)',
+ needle: '#0b7a48',
};
+const SANS = 'Geist';
+const MONO = 'Geist Mono';
+
const verticalGridLines = Array.from({ length: Math.ceil(OG_WIDTH / 80) - 1 }, (_, index) =>
(index + 1) * 80,
);
@@ -216,12 +222,10 @@ const horizontalGridLines = Array.from({ length: Math.ceil(OG_HEIGHT / 80) - 1 }
);
const labelStyle: CSSProperties = {
- color: colors.dim,
- fontSize: 13,
- fontWeight: 700,
- letterSpacing: '0.16em',
+ color: colors.ink3,
+ fontFamily: SANS,
+ fontSize: 15,
lineHeight: 1,
- textTransform: 'uppercase',
};
function truncateEnd(value: string, limit: number): string {
@@ -301,13 +305,13 @@ function fitLines(value: string, maxUnits: number, maxLines: number): string[] {
function formatDate(value: string): string {
const date = new Date(value);
- if (!Number.isFinite(date.getTime())) return 'UNKNOWN';
+ if (!Number.isFinite(date.getTime())) return 'Unknown';
return new Intl.DateTimeFormat('en-US', {
day: 'numeric',
month: 'short',
timeZone: 'UTC',
year: 'numeric',
- }).format(date).toUpperCase();
+ }).format(date);
}
function formatStars(value: number): string {
@@ -315,12 +319,12 @@ function formatStars(value: number): string {
return new Intl.NumberFormat('en-US', {
maximumFractionDigits: 1,
notation: 'compact',
- }).format(value).toUpperCase();
+ }).format(value);
}
function nameFontSize(value: string): number {
const length = textUnits(value);
- if (length <= 18) return 64;
+ if (length <= 18) return 68;
if (length <= 30) return 56;
if (length <= 46) return 48;
if (length <= 64) return 41;
@@ -334,7 +338,6 @@ function GridLines(): ReactElement {
bottom: 0,
display: 'flex',
left: 0,
- opacity: 0.7,
overflow: 'hidden',
position: 'absolute',
right: 0,
@@ -371,24 +374,26 @@ function GridLines(): ReactElement {
);
}
-function GitlawbMark(): ReactElement {
+/** The Pine mark: stem, three tiers, and the bud in the needle. */
+function PineMark({ size = 38 }: { size?: number }): ReactElement {
return (
-