feat(web): rebuild the site on the whale-road — blue ombre, one horizon, the v2 whale
Replaces the graphite foundation left by the stopped build (daf842343) with
the direction in codewhale-design/DIRECTION.md: navy grounds rising into the
logo gradient, one calm horizon per page with the sea below it, and the v2
whale as the only character.
Shared layer
- Tokens 1.1.1 vendored via codewhale-design/scripts/sync-to.sh; the
exporter now also emits live, danger and border_strong, so no state hue is
hand-kept in the site.
- tokens-roles.css: light keeps the artifact's paper; dark and the stage
(footer, home sea) use measured navy grounds (--ocean-*). Every text ink
clears 4.5:1 on every ground in every scheme; white on the primary
gradient clears 4.5:1 (#1877c9 -> #0b48bb). Legacy names alias the roles
so the docs keep painting.
- primitives.css: buttons, page header, sections, tiles, def rows, dir rows,
status (mark + word), segmented, tabs, code with icon copy, plate, figure,
callout, disclosure rows, empty/loading/error states, data table, field,
prose, steps, search field, horizon and sea texture.
- The 17 v2 poses exported from the kit's posters.json to public/whale
(scripts/export-whale-poses.py) with a light belly underlay; <WhalePose>
puts the pose that matches the page's job beside its title (search on
404, hmm on error, busy while loading, listen on sign-in).
- Nav: whale + mask-drawn wordmark on the sky. Footer: the sea under the
page's one horizon; the home sea continues into it with no second edge.
- Favicon, apple icon, manifest icons and OG image use the brand mark, not
the navy tile.
Pages: home (whale resting on the horizon, install plate in the sky, real
terminal capture under the surface), install, product, computer-use, models,
plugins, changelog (releases as receipts), community, contribute, roadmap,
faq, constitution, runtime (crate list and source revision behind Details),
feed, digest, admin, legal (Terms | Privacy tabs), sign-in/sign-up, 404.
Pricing still redirects to install; no desktop download beyond the existing
ready-release gate on /computer-use.
Truth fixes found on the way: the FAQ's `brew tap … && brew install
codewhale` fails on Homebrew 7 (docs/INSTALL.md) — now the full-name tap
command; the zh roadmap said usage counting was off until accepted — it is
on by default, as en says; the install page no longer tells English readers
about the Chinese translation; the 404 parody poster and its copy are gone.
Removed: strata, seal, install-binary, not-found poster, portal.css.
Design-contract tests that pinned the graphite/Strata look were rewritten to
real invariants (role coverage, AA on every ground, radius grammar, one focus
ring, gradient primary, keyframes behind a motion preference, one horizon,
the 17 poses); blue-stage-contract became ocean-contract.
Verification (local, no dev server or generators run):
- vitest lib + components: 472 passed, 1 failed — gt-site: gt-catalog/en.json
needs `npm run i18n:gt -- export` for the new 404 copy (left for Integrate).
- tsc --noEmit: clean. eslint on touched files: clean.
- Static renders of 19 pages screenshotted at 1280 dark and 390 light: no
horizontal overflow.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01R9rJEoMuUSRWznU6QjkE7h
No files matched your search
@@ -102,7 +102,8 @@ def gpui_tokens(data):
|
||||
"BG": "background", "TEXT": "foreground", "PANEL": "surface",
|
||||
"TEXT_MUTED": "muted_foreground", "BORDER": "border", "SIDEBAR": "sidebar",
|
||||
"PRIMARY": "primary", "ON_PRIMARY": "primary_foreground", "ACCENT": "hover",
|
||||
"LIST_ACTIVE": "selected", "ATTENTION": "attention",
|
||||
"LIST_ACTIVE": "selected", "ATTENTION": "attention", "LIVE": "live",
|
||||
"DANGER": "danger", "BORDER_STRONG": "border_strong",
|
||||
}
|
||||
return [
|
||||
(f"{prefix}_{name}", tuple(bytes.fromhex(palette[key])))
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
# Codewhale GPUI design
|
||||
# codewhale-design
|
||||
|
||||
Keep every Codewhale surface on one set of design tokens.
|
||||
|
||||
This repository is the source of truth for Codewhale's colours, type, spacing,
|
||||
radii and motion constants. The desktop app, the website, the terminal UI
|
||||
(`codewhale-ratatui`) and the mobile app each vendor a copy and verify it at
|
||||
build time, so no consumer needs the network or a sibling checkout.
|
||||
|
||||
Update tokens: edit `tokens.json`, bump its version, run `python3 generate.py`,
|
||||
then run `scripts/sync-to.sh <consumer-repo>` for each consumer.
|
||||
|
||||
## Details
|
||||
|
||||
`tokens.json` is the versioned semantic design authority. Desktop consumes
|
||||
`tokens.rs`; GPUI mobile vendors this folder and consumes the same Rust data;
|
||||
web-next vendors this folder and imports `tokens.css`. No network access or
|
||||
sibling checkout is required to build a consumer.
|
||||
|
||||
Change the JSON here, increment its version, run `python3 design/generate.py`,
|
||||
Change the JSON here, increment its version, run `python3 generate.py`,
|
||||
and copy this entire folder into each consumer's `vendor/codewhale-design`.
|
||||
Run the generator with `--check` in each repository. Generated files include the
|
||||
source digest; consumer tests reject drift. Layout remains native to each
|
||||
@@ -18,4 +30,8 @@ cadence are shared without introducing decorative animation on web or phones.
|
||||
|
||||
CSS consumers use `font-family: var(--font-family), var(--font-fallbacks), sans-serif`
|
||||
to keep the shared family and CJK fallbacks together. The generator rejects
|
||||
selection and primary-hover opacity values outside the inclusive 0–1 range.
|
||||
selection and primary-hover opacity values outside the inclusive 0–1 range,
|
||||
and any palette that misses WCAG AA contrast in either mode: every text role
|
||||
at 4.5:1 and `border_strong` at 3:1 on `background`, `surface`, `sidebar`,
|
||||
`hover` and `selected`, and `primary_foreground` on `primary` at 4.5:1. A new
|
||||
colour, such as a named blue, enters the tokens only once it passes.
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the portable GPUI design artifact. No sibling checkout is needed.
|
||||
Edit tokens.json only in codewhale-app/design. Consumers vendor the whole folder.
|
||||
Edit tokens.json only in the codewhale-design repository. Consumers vendor the whole folder.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
@@ -9,6 +9,35 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
# WCAG 2.x contrast every palette must hold, in both modes, before anything is
|
||||
# generated: text roles at 4.5:1 and control edges at 3:1 on each ground a
|
||||
# role can sit on, including a hovered or selected row.
|
||||
GROUNDS = ["background", "surface", "sidebar", "hover", "selected"]
|
||||
TEXT = ["foreground", "muted_foreground", "primary", "live", "attention", "danger"]
|
||||
EDGES = ["border_strong"]
|
||||
FILLED = [("primary_foreground", "primary")]
|
||||
|
||||
def luminance(hex6):
|
||||
channels = [int(hex6[i:i + 2], 16) / 255 for i in (0, 2, 4)]
|
||||
channels = [c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 for c in channels]
|
||||
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]
|
||||
|
||||
def contrast(a, b):
|
||||
hi, lo = sorted([luminance(a), luminance(b)], reverse=True)
|
||||
return (hi + 0.05) / (lo + 0.05)
|
||||
|
||||
def contrast_violations(colors):
|
||||
pairs = [(fg, bg, 4.5) for fg in TEXT for bg in GROUNDS]
|
||||
pairs += [(edge, bg, 3.0) for edge in EDGES for bg in GROUNDS]
|
||||
pairs += [(fg, bg, 4.5) for fg, bg in FILLED]
|
||||
found = []
|
||||
for mode, palette in colors.items():
|
||||
for fg, bg, floor in pairs:
|
||||
ratio = contrast(palette[fg], palette[bg])
|
||||
if ratio < floor:
|
||||
found.append(f"{mode} {fg} on {bg} is {ratio:.2f}:1, needs {floor}:1")
|
||||
return found
|
||||
|
||||
def render():
|
||||
source = (ROOT / "tokens.json").read_bytes()
|
||||
data = json.loads(source)
|
||||
@@ -22,6 +51,9 @@ def render():
|
||||
assert set(keys) == set(data["colors"]["light"])
|
||||
for palette in data["colors"].values():
|
||||
assert all(len(v) == 6 and all(c in "0123456789abcdef" for c in v) for v in palette.values())
|
||||
violations = contrast_violations(data["colors"])
|
||||
if violations:
|
||||
raise ValueError("Contrast below WCAG AA:\n " + "\n ".join(violations))
|
||||
rust = [f"// {header}", "#![allow(dead_code)]", "", f'pub const VERSION: &str = "{data["version"]}";',
|
||||
"#[derive(Clone, Copy, Debug)]", "pub struct Colors {"]
|
||||
rust += [f" pub {k}: u32," for k in keys]
|
||||
@@ -41,10 +73,14 @@ def render():
|
||||
for name in ["selection_opacity", "primary_hover_opacity"]:
|
||||
rust += [f"pub const {name.upper()}: f32 = {float(data[name])};"]
|
||||
rust += [f'pub const MONO_PX: f32 = {float(data["typography"]["mono_px"])};']
|
||||
for name in ["body_px", "caption_px", "prose_px", "heading_px", "title_px"]:
|
||||
if name in data["typography"]:
|
||||
rust += [f'pub const TYPE_{name.upper()}: f32 = {float(data["typography"][name])};']
|
||||
# Existing web names remain aliases, so every current component migrates together.
|
||||
aliases = {"outside":"sidebar","surface":"background","panel":"surface","text":"foreground",
|
||||
"muted":"muted_foreground","line":"border","accent":"primary","button":"primary",
|
||||
"button-text":"primary_foreground","attention":"attention","hover":"hover","selected":"selected"}
|
||||
"button-text":"primary_foreground","attention":"attention","hover":"hover","selected":"selected",
|
||||
"live":"live","danger":"danger","border-strong":"border_strong"}
|
||||
css = [f"/* {header} */"]
|
||||
for mode, palette in data["colors"].items():
|
||||
css += [":root {" if mode == "dark" else '[data-theme="light"] {']
|
||||
@@ -58,7 +94,11 @@ def render():
|
||||
f' --font-family: {json.dumps(data["typography"]["family"], ensure_ascii=False)};',
|
||||
f' --font-fallbacks: {", ".join(json.dumps(v, ensure_ascii=False) for v in data["typography"]["fallbacks"])};',
|
||||
f' --font-body-size: {data["typography"]["body_px"]}px;',
|
||||
f' --font-code-size: {data["typography"]["mono_px"]}px;', "}"]
|
||||
f' --font-code-size: {data["typography"]["mono_px"]}px;']
|
||||
for name in ["caption_px", "prose_px", "heading_px", "title_px"]:
|
||||
if name in data["typography"]:
|
||||
css += [f' --font-{name[:-3]}-size: {data["typography"][name]}px;']
|
||||
css += ["}"]
|
||||
return {"tokens.rs":"\n".join(rust) + "\n", "tokens.css":"\n".join(css) + "\n"}
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Generated from Codewhale GPUI design 1.0.0; sha256 a0c23f8a235bbc61e42fb8a60795ae7d9825dbbd1730142acc9ffec9c0abb3af. Do not edit. */
|
||||
/* Generated from Codewhale GPUI design 1.1.1; sha256 c08d7d4b253afe340207658b1f233dd1a9dbe94fa57e925bde025a3db70543ee. Do not edit. */
|
||||
:root {
|
||||
--outside: #191a1c;
|
||||
--surface: #202123;
|
||||
@@ -12,6 +12,9 @@
|
||||
--attention: #e8b077;
|
||||
--hover: #303134;
|
||||
--selected: #37393d;
|
||||
--live: #9ec7b2;
|
||||
--danger: #e39a90;
|
||||
--border-strong: #828386;
|
||||
--selection: #90b9ff47;
|
||||
color-scheme: dark;
|
||||
}
|
||||
@@ -28,6 +31,9 @@
|
||||
--attention: #86520d;
|
||||
--hover: #e8e5e0;
|
||||
--selected: #dfdcd6;
|
||||
--live: #3a6352;
|
||||
--danger: #9e3f36;
|
||||
--border-strong: #807c76;
|
||||
--selection: #245bc747;
|
||||
color-scheme: light;
|
||||
}
|
||||
@@ -40,6 +46,7 @@
|
||||
--spacing-page: 32px;
|
||||
--radius-control: 6px;
|
||||
--radius-panel: 10px;
|
||||
--radius-sheet: 14px;
|
||||
--focus-width: 2px;
|
||||
--focus-offset: 3px;
|
||||
--icon-stroke: 1.7;
|
||||
@@ -47,4 +54,8 @@
|
||||
--font-fallbacks: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC";
|
||||
--font-body-size: 14px;
|
||||
--font-code-size: 13px;
|
||||
--font-caption-size: 12px;
|
||||
--font-prose-size: 17px;
|
||||
--font-heading-size: 17px;
|
||||
--font-title-size: 22px;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.1",
|
||||
"colors": {
|
||||
"dark": {
|
||||
"background": "202123",
|
||||
@@ -12,7 +12,10 @@
|
||||
"primary_foreground": "15243e",
|
||||
"hover": "303134",
|
||||
"selected": "37393d",
|
||||
"attention": "e8b077"
|
||||
"attention": "e8b077",
|
||||
"live": "9ec7b2",
|
||||
"danger": "e39a90",
|
||||
"border_strong": "828386"
|
||||
},
|
||||
"light": {
|
||||
"background": "faf8f5",
|
||||
@@ -25,7 +28,10 @@
|
||||
"primary_foreground": "fbf5ee",
|
||||
"hover": "e8e5e0",
|
||||
"selected": "dfdcd6",
|
||||
"attention": "86520d"
|
||||
"attention": "86520d",
|
||||
"live": "3a6352",
|
||||
"danger": "9e3f36",
|
||||
"border_strong": "807c76"
|
||||
}
|
||||
},
|
||||
"typography": {
|
||||
@@ -40,7 +46,11 @@
|
||||
"Source Han Sans SC"
|
||||
],
|
||||
"body_px": 14,
|
||||
"mono_px": 13
|
||||
"mono_px": 13,
|
||||
"caption_px": 12,
|
||||
"prose_px": 17,
|
||||
"heading_px": 17,
|
||||
"title_px": 22
|
||||
},
|
||||
"spacing": {
|
||||
"compact": 4,
|
||||
@@ -52,7 +62,8 @@
|
||||
},
|
||||
"radius": {
|
||||
"control": 6,
|
||||
"panel": 10
|
||||
"panel": 10,
|
||||
"sheet": 14
|
||||
},
|
||||
"focus": {
|
||||
"width": 2,
|
||||
@@ -67,7 +78,10 @@
|
||||
"spring_damping": 42,
|
||||
"spring_mass": 1,
|
||||
"pet_poll_ms": 30,
|
||||
"reduced_poll_ms": 500
|
||||
"reduced_poll_ms": 500,
|
||||
"duration_state_ms": 120,
|
||||
"duration_arrive_ms": 180,
|
||||
"duration_panel_ms": 340
|
||||
},
|
||||
"selection_opacity": 0.28,
|
||||
"primary_hover_opacity": 0.9
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Generated from Codewhale GPUI design 1.0.0; sha256 a0c23f8a235bbc61e42fb8a60795ae7d9825dbbd1730142acc9ffec9c0abb3af. Do not edit.
|
||||
// Generated from Codewhale GPUI design 1.1.1; sha256 c08d7d4b253afe340207658b1f233dd1a9dbe94fa57e925bde025a3db70543ee. Do not edit.
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub const VERSION: &str = "1.0.0";
|
||||
pub const VERSION: &str = "1.1.1";
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Colors {
|
||||
pub background: u32,
|
||||
@@ -15,6 +15,9 @@ pub struct Colors {
|
||||
pub hover: u32,
|
||||
pub selected: u32,
|
||||
pub attention: u32,
|
||||
pub live: u32,
|
||||
pub danger: u32,
|
||||
pub border_strong: u32,
|
||||
}
|
||||
pub const DARK: Colors = Colors {
|
||||
background: 0x202123,
|
||||
@@ -28,6 +31,9 @@ pub const DARK: Colors = Colors {
|
||||
hover: 0x303134,
|
||||
selected: 0x37393d,
|
||||
attention: 0xe8b077,
|
||||
live: 0x9ec7b2,
|
||||
danger: 0xe39a90,
|
||||
border_strong: 0x828386,
|
||||
};
|
||||
pub const LIGHT: Colors = Colors {
|
||||
background: 0xfaf8f5,
|
||||
@@ -41,6 +47,9 @@ pub const LIGHT: Colors = Colors {
|
||||
hover: 0xe8e5e0,
|
||||
selected: 0xdfdcd6,
|
||||
attention: 0x86520d,
|
||||
live: 0x3a6352,
|
||||
danger: 0x9e3f36,
|
||||
border_strong: 0x807c76,
|
||||
};
|
||||
pub fn colors(dark: bool) -> Colors {
|
||||
if dark { DARK } else { LIGHT }
|
||||
@@ -63,6 +72,7 @@ pub const SPACING_LARGE: f32 = 24.0;
|
||||
pub const SPACING_PAGE: f32 = 32.0;
|
||||
pub const RADIUS_CONTROL: f32 = 6.0;
|
||||
pub const RADIUS_PANEL: f32 = 10.0;
|
||||
pub const RADIUS_SHEET: f32 = 14.0;
|
||||
pub const FOCUS_WIDTH: f32 = 2.0;
|
||||
pub const FOCUS_OFFSET: f32 = 3.0;
|
||||
pub const ICONS_GRID: f32 = 24.0;
|
||||
@@ -72,6 +82,14 @@ pub const MOTION_SPRING_DAMPING: f32 = 42.0;
|
||||
pub const MOTION_SPRING_MASS: f32 = 1.0;
|
||||
pub const MOTION_PET_POLL_MS: f32 = 30.0;
|
||||
pub const MOTION_REDUCED_POLL_MS: f32 = 500.0;
|
||||
pub const MOTION_DURATION_STATE_MS: f32 = 120.0;
|
||||
pub const MOTION_DURATION_ARRIVE_MS: f32 = 180.0;
|
||||
pub const MOTION_DURATION_PANEL_MS: f32 = 340.0;
|
||||
pub const SELECTION_OPACITY: f32 = 0.28;
|
||||
pub const PRIMARY_HOVER_OPACITY: f32 = 0.9;
|
||||
pub const MONO_PX: f32 = 13.0;
|
||||
pub const TYPE_BODY_PX: f32 = 14.0;
|
||||
pub const TYPE_CAPTION_PX: f32 = 12.0;
|
||||
pub const TYPE_PROSE_PX: f32 = 17.0;
|
||||
pub const TYPE_HEADING_PX: f32 = 17.0;
|
||||
pub const TYPE_TITLE_PX: f32 = 22.0;
|
||||
@@ -65,7 +65,7 @@ export function AdminClient({ drafts, posted, isZh, typeLabels }: Props) {
|
||||
const key = draftStorageKey(draft);
|
||||
const label = typeLabels[draft.type] ?? { en: draft.type, zh: draft.type };
|
||||
return (
|
||||
<div key={key} className="hairline-t hairline-b hairline-l hairline-r bg-paper">
|
||||
<div key={key} className="group-card">
|
||||
<div className="bg-paper-deep text-ink px-4 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-indigo">
|
||||
@@ -86,19 +86,19 @@ export function AdminClient({ drafts, posted, isZh, typeLabels }: Props) {
|
||||
<textarea
|
||||
value={editBody}
|
||||
onChange={(e) => setEditBody(e.target.value)}
|
||||
className="w-full h-48 p-3 bg-paper-deep hairline-t hairline-b hairline-l hairline-r font-mono text-sm resize-y"
|
||||
className="field-input h-48 py-3 font-mono text-sm resize-y"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleAction(key, "post", editBody)}
|
||||
disabled={loading === key}
|
||||
className="portal-button portal-button-primary disabled:opacity-50"
|
||||
className="btn btn-primary disabled:opacity-50"
|
||||
>
|
||||
{isZh ? "确认发布" : "Post edited"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditing(null)}
|
||||
className="portal-button portal-button-secondary"
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
{isZh ? "取消" : "Cancel"}
|
||||
</button>
|
||||
@@ -113,20 +113,20 @@ export function AdminClient({ drafts, posted, isZh, typeLabels }: Props) {
|
||||
<button
|
||||
onClick={() => handleAction(key, "post")}
|
||||
disabled={loading === key}
|
||||
className="portal-button portal-button-primary disabled:opacity-50"
|
||||
className="btn btn-primary disabled:opacity-50"
|
||||
>
|
||||
{isZh ? "发布评论" : "Post as comment"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => startEdit(draft)}
|
||||
className="portal-button portal-button-secondary"
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
{isZh ? "编辑后发布" : "Edit & post"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAction(key, "discard")}
|
||||
disabled={loading === key}
|
||||
className="px-4 py-2 text-sm text-ink-mute hover:text-indigo transition-colors disabled:opacity-50"
|
||||
className="btn btn-danger"
|
||||
>
|
||||
{isZh ? "丢弃" : "Discard"}
|
||||
</button>
|
||||
@@ -158,7 +158,7 @@ export function AdminClient({ drafts, posted, isZh, typeLabels }: Props) {
|
||||
{draft.targetNumber && (
|
||||
<span className="font-mono text-xs text-ink-mute tabular">#{draft.targetNumber}</span>
|
||||
)}
|
||||
<span className="ml-auto pill pill-jade text-[0.6rem]">{isZh ? "已发布" : "posted"}</span>
|
||||
<span className="ml-auto status status-ready"><span className="status-dot" aria-hidden="true" />{isZh ? "已发布" : "posted"}</span>
|
||||
</div>
|
||||
<p className="text-xs text-ink-mute line-clamp-2">{(isZh ? draft.bodyZh : draft.bodyEn).slice(0, 120)}…</p>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { ConnectionBanner } from "@/components/connection-banner";
|
||||
import { EmptyState, ErrorState } from "@/components/surface-state";
|
||||
import { WhalePose } from "@/components/whale-pose";
|
||||
import { getAgentEnv, listDrafts, validateSession, type AgentDraft } from "@/lib/community-agent";
|
||||
import { AdminClient } from "./admin-client";
|
||||
|
||||
@@ -26,15 +27,16 @@ const TYPE_LABELS: Record<string, { en: string; zh: string }> = {
|
||||
function LoginForm({ locale, error }: { locale: string; error: boolean }) {
|
||||
const isZh = locale === "zh";
|
||||
return (
|
||||
<div className="mx-auto max-w-md px-6 py-20">
|
||||
<div className="account-entry">
|
||||
<ConnectionBanner locale={locale} />
|
||||
<h1 className="font-display text-3xl mb-6 mt-6">
|
||||
<WhalePose pose="listen" className="account-entry-pose" />
|
||||
<h1 className="page-title">
|
||||
{isZh ? "维护者登录" : "Maintainer login"}
|
||||
</h1>
|
||||
<form method="POST" action={`/api/admin/login?locale=${locale}`} autoComplete="off" className="space-y-4">
|
||||
<form method="POST" action={`/api/admin/login?locale=${locale}`} autoComplete="off" className="account-form">
|
||||
<input type="hidden" name="locale" value={locale} />
|
||||
<label className="block">
|
||||
<span className="eyebrow block mb-2">{isZh ? "令牌" : "Token"}</span>
|
||||
<label className="field">
|
||||
<span className="field-label">{isZh ? "令牌" : "Token"}</span>
|
||||
<input
|
||||
type="password"
|
||||
name="token"
|
||||
@@ -42,17 +44,19 @@ function LoginForm({ locale, error }: { locale: string; error: boolean }) {
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
className="w-full px-3 py-2 hairline-t hairline-b hairline-l hairline-r bg-paper font-mono text-sm focus:border-indigo"
|
||||
aria-invalid={error || undefined}
|
||||
aria-describedby={error ? "admin-token-error" : undefined}
|
||||
className="field-input font-mono"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full portal-button portal-button-primary"
|
||||
className="btn btn-primary btn-lg"
|
||||
>
|
||||
{isZh ? "登录 →" : "Sign in →"}
|
||||
{isZh ? "登录" : "Sign in"}
|
||||
</button>
|
||||
{error && (
|
||||
<p className="text-sm text-indigo font-mono">
|
||||
<p id="admin-token-error" className="field-hint field-hint-error" role="alert">
|
||||
{isZh ? "令牌错误。" : "Invalid token."}
|
||||
</p>
|
||||
)}
|
||||
@@ -112,25 +116,25 @@ export default async function AdminPage({
|
||||
const posted = drafts.filter((d) => d.posted);
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-4 pb-20">
|
||||
<section className="page-body admin-page">
|
||||
{/* The signed-in shell: typed offline / reconnect state with a real
|
||||
server probe, so a paused action is never mistaken for a posted one. */}
|
||||
<ConnectionBanner locale={locale} />
|
||||
<div className="flex items-baseline justify-between mb-8 mt-8 hairline-b pb-4">
|
||||
<div className="admin-head">
|
||||
<div>
|
||||
<h1 className="font-display tracking-crisp text-3xl">
|
||||
<h1 className="page-title">
|
||||
{isZh ? "社区助理草稿" : "Community Assistant Drafts"}
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-ink-mute font-mono">
|
||||
<p className="page-meta tabular">
|
||||
{pending.length} pending · {posted.length} posted
|
||||
</p>
|
||||
</div>
|
||||
<form method="POST" action={`/api/admin/logout?locale=${locale}`}>
|
||||
<button
|
||||
type="submit"
|
||||
className="text-xs text-ink-mute hover:text-indigo transition-colors"
|
||||
className="btn btn-ghost btn-sm"
|
||||
>
|
||||
{isZh ? "退出 →" : "Sign out →"}
|
||||
{isZh ? "退出" : "Sign out"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { Status } from "@/components/status-badge";
|
||||
import { EmptyState } from "@/components/surface-state";
|
||||
import { CHANGELOG, type ChangelogRelease } from "@/lib/changelog.generated";
|
||||
import { changelogAnchor } from "@/scripts/changelog-lib.mjs";
|
||||
@@ -22,6 +25,16 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
||||
});
|
||||
}
|
||||
|
||||
/** Link labels carry a trailing ↗ in the dictionaries; the icon draws it. */
|
||||
function ext(label: string) {
|
||||
return (
|
||||
<>
|
||||
{label.replace(/\s*↗\s*$/u, "")}
|
||||
<Icon name="external" className="icon" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDate(iso: string, dateLocale: string): string {
|
||||
return new Date(iso).toLocaleDateString(dateLocale, {
|
||||
year: "numeric",
|
||||
@@ -43,62 +56,59 @@ export default async function ChangelogPage({ params }: { params: Promise<{ loca
|
||||
const candidateIsPublished = published !== null && candidate !== null && published.version === candidate;
|
||||
|
||||
return (
|
||||
<div className="portal-home">
|
||||
<section className="hero">
|
||||
<div className="portal-current" aria-hidden="true" />
|
||||
<div className="portal-container community-welcome-inner">
|
||||
<div className="eyebrow">{t.kicker}</div>
|
||||
<h1>{t.title}</h1>
|
||||
<p>{t.lead}</p>
|
||||
|
||||
<div className="changelog-facts">
|
||||
<div data-published={published ? "true" : "false"}>
|
||||
<span>{t.publishedLabel}</span>
|
||||
<>
|
||||
<PageHeader kicker={t.kicker} title={t.title} lede={t.lead} pose="write">
|
||||
<dl className="def-rows changelog-facts">
|
||||
<div className="def-row" data-published={published ? "true" : "false"}>
|
||||
<dt>{t.publishedLabel}</dt>
|
||||
<dd>
|
||||
{published ? (
|
||||
<>
|
||||
<strong>
|
||||
<Status tone="ready">
|
||||
{fill(t.publishedValue, {
|
||||
tag: published.tag,
|
||||
date: formatDate(published.publishedAt, chrome.dateLocale),
|
||||
})}
|
||||
</strong>
|
||||
<a href={published.url} target="_blank" rel="noreferrer">
|
||||
{t.releasePageLink}
|
||||
</Status>
|
||||
<a href={published.url} target="_blank" rel="noreferrer" className="link">
|
||||
{ext(t.releasePageLink)}
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<a href={REPO_RELEASES_URL} target="_blank" rel="noreferrer">
|
||||
{t.releasesLink}
|
||||
<a href={REPO_RELEASES_URL} target="_blank" rel="noreferrer" className="link">
|
||||
{ext(t.releasesLink)}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div data-candidate={candidateIsPublished ? "published" : "unreleased"}>
|
||||
<span>{t.candidateLabel}</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="def-row" data-candidate={candidateIsPublished ? "published" : "unreleased"}>
|
||||
<dt>{t.candidateLabel}</dt>
|
||||
<dd>
|
||||
{candidate && (
|
||||
<strong>
|
||||
<Status tone={candidateIsPublished ? "ready" : "idle"}>
|
||||
{candidateIsPublished
|
||||
? fill(t.candidateMatches, { version: `v${candidate}` })
|
||||
: fill(t.candidateValue, { version: `v${candidate}` })}
|
||||
</strong>
|
||||
</Status>
|
||||
)}
|
||||
<a href={`${REPO_URL}/blob/main/CHANGELOG.md`} target="_blank" rel="noreferrer">
|
||||
{t.fullNotes}
|
||||
<a href={`${REPO_URL}/blob/main/CHANGELOG.md`} target="_blank" rel="noreferrer" className="link">
|
||||
{ext(t.fullNotes)}
|
||||
</a>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</dl>
|
||||
</PageHeader>
|
||||
|
||||
<section className="portal-section">
|
||||
<div className="portal-container" style={{ maxWidth: "56rem" }}>
|
||||
<div className="page-body">
|
||||
<div className="page-body-narrow changelog-list">
|
||||
{CHANGELOG.length === 0 ? (
|
||||
<EmptyState
|
||||
locale={locale}
|
||||
title={t.emptyTitle}
|
||||
body={t.emptyBody}
|
||||
action={
|
||||
<a className="portal-button portal-button-secondary" href={REPO_RELEASES_URL}>
|
||||
{t.releasesLink}
|
||||
<a className="btn btn-secondary" href={REPO_RELEASES_URL}>
|
||||
{ext(t.releasesLink)}
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
@@ -114,18 +124,17 @@ export default async function ChangelogPage({ params }: { params: Promise<{ loca
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<p className="mt-8 text-sm text-ink-mute">
|
||||
<Link href={`/${locale}/docs`} className="body-link">
|
||||
<p className="status-line">
|
||||
<Link href={`/${locale}/docs`} className="link">
|
||||
{chrome.footerDocs}
|
||||
</Link>
|
||||
{" · "}
|
||||
<a href={REPO_RELEASES_URL} target="_blank" rel="noreferrer" className="body-link">
|
||||
{t.releasesLink}
|
||||
<a href={REPO_RELEASES_URL} target="_blank" rel="noreferrer" className="link">
|
||||
{ext(t.releasesLink)}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,21 +158,25 @@ function Release({
|
||||
return (
|
||||
<article id={anchor} className="changelog-release scroll-mt-32">
|
||||
<div className="changelog-release-head">
|
||||
<h2>{heading}</h2>
|
||||
<div className="dotline">
|
||||
{/* Each release is a receipt: the shell marks it. */}
|
||||
<h2>
|
||||
<Icon name="shell" />
|
||||
{heading}
|
||||
</h2>
|
||||
<div className="changelog-links">
|
||||
{release.date && <span className="tabular">{formatDate(release.date, chrome.dateLocale)}</span>}
|
||||
{publishedUrl && (
|
||||
<a href={publishedUrl} target="_blank" rel="noreferrer">
|
||||
{t.releasePageLink}
|
||||
{ext(t.releasePageLink)}
|
||||
</a>
|
||||
)}
|
||||
{release.compareUrl && (
|
||||
<a href={release.compareUrl} target="_blank" rel="noreferrer">
|
||||
{t.compareLink}
|
||||
{ext(t.compareLink)}
|
||||
</a>
|
||||
)}
|
||||
<a href={notesUrl} target="_blank" rel="noreferrer">
|
||||
{fill(t.releaseNotesLink, { version: heading })}
|
||||
{ext(fill(t.releaseNotesLink, { version: heading }))}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { Icon, type IconName } from "@/components/icon";
|
||||
import { PageHeader, Section } from "@/components/page-header";
|
||||
import { getFacts } from "@/lib/facts";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
import { RELEASE_CONTRIBUTORS, RELEASE_HELPERS } from "@/lib/release-credits";
|
||||
@@ -30,25 +32,25 @@ export default async function CommunityPage({ params }: { params: Promise<{ loca
|
||||
{
|
||||
title: "报告问题",
|
||||
description: "报告 bug、兼容性问题或不清楚的行为,并附上系统信息、复现步骤和可以安全分享的日志。",
|
||||
cta: "提交 issue →",
|
||||
cta: "提交 issue",
|
||||
href: "https://github.com/Hmbown/CodeWhale/issues/new/choose",
|
||||
},
|
||||
{
|
||||
title: "改进代码或测试",
|
||||
description: "挑一个范围清楚的问题,写最小的补丁,加一个覆盖改动的回归测试。",
|
||||
cta: "查看开放 issues →",
|
||||
cta: "查看开放 issues",
|
||||
href: "https://github.com/Hmbown/CodeWhale/issues",
|
||||
},
|
||||
{
|
||||
title: "改进文档或翻译",
|
||||
description: "改正说错的地方,补一个示例,或者帮忙完成一个语言包。",
|
||||
cta: "查看本地化指南 ↗",
|
||||
cta: "查看本地化指南",
|
||||
href: "https://github.com/Hmbown/CodeWhale/blob/main/docs/LOCALIZATION.md",
|
||||
},
|
||||
{
|
||||
title: "复现并审查现有工作",
|
||||
description: "在你的平台和提供商上验证 issue 或 pull request,然后分享你运行的命令、结果和剩余问题。",
|
||||
cta: "查看 pull requests →",
|
||||
cta: "查看 pull requests",
|
||||
href: "https://github.com/Hmbown/CodeWhale/pulls",
|
||||
},
|
||||
]
|
||||
@@ -56,25 +58,25 @@ export default async function CommunityPage({ params }: { params: Promise<{ loca
|
||||
{
|
||||
title: "Report a problem",
|
||||
description: "File a bug, compatibility problem, or unclear behavior with system details, reproduction steps, and any logs you can share safely.",
|
||||
cta: "File an issue →",
|
||||
cta: "File an issue",
|
||||
href: "https://github.com/Hmbown/CodeWhale/issues/new/choose",
|
||||
},
|
||||
{
|
||||
title: "Improve code or tests",
|
||||
description: "Pick one problem with clear edges, write the smallest patch that fixes it, and add a regression test that covers it.",
|
||||
cta: "Browse open issues →",
|
||||
cta: "Browse open issues",
|
||||
href: "https://github.com/Hmbown/CodeWhale/issues",
|
||||
},
|
||||
{
|
||||
title: "Improve documentation or translations",
|
||||
description: "Fix a wrong sentence, add an example, or help finish a language pack.",
|
||||
cta: "Open the localization guide ↗",
|
||||
cta: "Open the localization guide",
|
||||
href: "https://github.com/Hmbown/CodeWhale/blob/main/docs/LOCALIZATION.md",
|
||||
},
|
||||
{
|
||||
title: "Reproduce and review existing work",
|
||||
description: "Try an issue or pull request on your platform and provider. Post the commands you ran, what happened, and what is still wrong.",
|
||||
cta: "Browse pull requests →",
|
||||
cta: "Browse pull requests",
|
||||
href: "https://github.com/Hmbown/CodeWhale/pulls",
|
||||
},
|
||||
];
|
||||
@@ -91,125 +93,134 @@ export default async function CommunityPage({ params }: { params: Promise<{ loca
|
||||
{ title: "Public roadmap", description: "Shipped, underway, considered, and ruled-out work.", href: p("/roadmap") },
|
||||
];
|
||||
|
||||
const activityIcons: IconName[] = ["repeat", "book", "tide"];
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="hero">
|
||||
<div className="portal-current" aria-hidden="true" />
|
||||
<div className="portal-container community-welcome-inner">
|
||||
<div className="eyebrow">{isZh ? "国际开源社区" : "International open-source community"}</div>
|
||||
<h1>{isZh ? "与世界各地的贡献者一起构建 Codewhale。" : "Build Codewhale with contributors around the world."}</h1>
|
||||
<p>
|
||||
{isZh
|
||||
? "运行时、文档、测试和翻译,来自不同国家、语言和平台的贡献者。第一次参与不必是大功能。一份清楚的 bug 报告、一处文档修正、或一个带测试的小补丁,都算。"
|
||||
: "The runtime, docs, tests, and translations come from contributors across countries, languages, and platforms. A first contribution does not have to be a feature. A clear bug report, a documentation fix, or a small tested patch counts."}
|
||||
</p>
|
||||
<div className="portal-actions">
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/issues/new/choose" className="portal-button portal-button-primary">
|
||||
<PageHeader
|
||||
kicker={isZh ? "国际开源社区" : "International open-source community"}
|
||||
title={isZh ? "与世界各地的贡献者一起构建 Codewhale。" : "Build Codewhale with contributors around the world."}
|
||||
lede={
|
||||
isZh
|
||||
? "运行时、文档、测试和翻译,来自不同国家、语言和平台的贡献者。第一次参与不必是大功能。一份清楚的 bug 报告、一处文档修正、或一个带测试的小补丁,都算。"
|
||||
: "The runtime, docs, tests, and translations come from contributors across countries, languages, and platforms. A first contribution does not have to be a feature. A clear bug report, a documentation fix, or a small tested patch counts."
|
||||
}
|
||||
pose="talk"
|
||||
actions={
|
||||
<>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/issues/new/choose" className="btn btn-primary btn-lg">
|
||||
{isZh ? "提交 issue" : "File an issue"}
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/pulls" className="portal-button portal-button-secondary">
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/pulls" className="btn btn-secondary btn-lg">
|
||||
{isZh ? "查看 pull requests" : "Browse pull requests"}
|
||||
</Link>
|
||||
<Link href={p("/contribute")} className="portal-button portal-button-secondary">
|
||||
<Link href={p("/contribute")} className="btn btn-ghost btn-lg">
|
||||
{isZh ? "阅读贡献指南" : "Read the contribution guide"}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="portal-section">
|
||||
<div className="portal-container portal-section-grid">
|
||||
<div className="portal-section-copy">
|
||||
<span>{isZh ? "参与方式" : "Ways to contribute"}</span>
|
||||
<h2>{isZh ? "从一件小事开始。" : "Start with one small thing."}</h2>
|
||||
<p>
|
||||
{isZh
|
||||
? "报 bug、写代码、写测试、改文档、翻译、审查,都有用。选一样适合你时间的。"
|
||||
: "Bug reports, code, tests, docs, translations, and review all help. Pick the one that fits the time you have."}
|
||||
</p>
|
||||
</div>
|
||||
<div className="contribute-path-grid">
|
||||
<div className="page-body">
|
||||
<Section
|
||||
id="community-paths"
|
||||
title={isZh ? "从一件小事开始。" : "Start with one small thing."}
|
||||
scope={
|
||||
isZh
|
||||
? "报 bug、写代码、写测试、改文档、翻译、审查,都有用。选一样适合你时间的。"
|
||||
: "Bug reports, code, tests, docs, translations, and review all help. Pick the one that fits the time you have."
|
||||
}
|
||||
>
|
||||
<div className="grid-2">
|
||||
{contributionPaths.map((path) => (
|
||||
<article key={path.title}>
|
||||
<Link key={path.title} href={path.href} className="tile">
|
||||
<h3>{path.title}</h3>
|
||||
<p>{path.description}</p>
|
||||
<Link href={path.href}>{path.cta}</Link>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="portal-section portal-section-muted">
|
||||
<div className="portal-container portal-section-grid">
|
||||
<div className="portal-section-copy">
|
||||
<span>{isZh ? "公开项目记录" : "Public project record"}</span>
|
||||
<h2>{isZh ? "从提案到发布,都是公开的。" : "From proposal to release, in the open."}</h2>
|
||||
<p>
|
||||
{isZh
|
||||
? "动态页显示最近的仓库活动。社区摘要保留每周存档。路线图区分已发布的和还在讨论的。"
|
||||
: "The activity feed shows recent repository work. The community digest keeps the weekly archive of repository activity. The roadmap separates what shipped from what is still being discussed."}
|
||||
</p>
|
||||
</div>
|
||||
<div className="portal-topic-list">
|
||||
{activityLinks.map((item) => (
|
||||
<Link key={item.href} href={item.href}>
|
||||
<strong>{item.title}</strong>
|
||||
<span>{item.description}</span>
|
||||
<span aria-hidden="true">→</span>
|
||||
<span className="section-link">
|
||||
{path.cta}
|
||||
<Icon name={path.href.includes("LOCALIZATION") ? "external" : "arrow-right"} className="icon icon-flip" />
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
<section className="portal-section community-credit-section">
|
||||
<div className="portal-container portal-section-grid">
|
||||
<div className="portal-section-copy">
|
||||
<span>
|
||||
{sourceIsPublished
|
||||
? isZh
|
||||
? `v${facts.version} 版本致谢`
|
||||
: `v${facts.version} release credit`
|
||||
: isZh
|
||||
? `v${facts.version} 致谢(未发布)`
|
||||
: `v${facts.version} credit (unreleased)`}
|
||||
</span>
|
||||
<h2>{isZh ? "贡献者署名是版本记录的一部分。" : "Contributor credit is part of the release record."}</h2>
|
||||
<p>
|
||||
{isZh
|
||||
? `${sourceIsPublished ? "这一版本" : "这一版"}包含社区提交的代码、测试、复现和验证。即使维护者改过补丁再合入,原作者的署名也保留在提交、更新日志和贡献者名单中。`
|
||||
: `This ${sourceIsPublished ? "release" : "version"} includes code, tests, reproductions, and verification from the community. If a maintainer reworks a patch before it lands, the original author stays credited in the commit, the changelog, and the contributor record.`}
|
||||
</p>
|
||||
<div className="community-record-links">
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/blob/main/docs/CONTRIBUTORS.md">
|
||||
{isZh ? "完整贡献者名单 ↗" : "Full contributor record ↗"}
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/blob/main/CHANGELOG.md">CHANGELOG ↗</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="community-credit-groups">
|
||||
<section>
|
||||
<Section
|
||||
id="community-record"
|
||||
title={isZh ? "从提案到发布,都是公开的。" : "From proposal to release, in the open."}
|
||||
scope={
|
||||
isZh
|
||||
? "动态页显示最近的仓库活动。社区摘要保留每周存档。路线图区分已发布的和还在讨论的。"
|
||||
: "The activity feed shows recent repository work. The community digest keeps the weekly archive of repository activity. The roadmap separates what shipped from what is still being discussed."
|
||||
}
|
||||
>
|
||||
<ul className="dir-list dir-list-card" role="list">
|
||||
{activityLinks.map((item, index) => (
|
||||
<li key={item.href}>
|
||||
<Link href={item.href} className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name={activityIcons[index] ?? "book"} /></span>
|
||||
<span className="dir-text">
|
||||
<span className="dir-title">{item.title}</span>
|
||||
<span className="dir-purpose">{item.description}</span>
|
||||
</span>
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="chevron-right" className="icon icon-flip" /></span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
id="community-credit"
|
||||
title={isZh ? "贡献者署名是版本记录的一部分。" : "Contributor credit is part of the release record."}
|
||||
scope={
|
||||
isZh
|
||||
? `${sourceIsPublished ? "这一版本" : "这一版"}包含社区提交的代码、测试、复现和验证。即使维护者改过补丁再合入,原作者的署名也保留在提交、更新日志和贡献者名单中。`
|
||||
: `This ${sourceIsPublished ? "release" : "version"} includes code, tests, reproductions, and verification from the community. If a maintainer reworks a patch before it lands, the original author stays credited in the commit, the changelog, and the contributor record.`
|
||||
}
|
||||
>
|
||||
<p className="page-kicker mb-4">
|
||||
{sourceIsPublished
|
||||
? isZh
|
||||
? `v${facts.version} 版本致谢`
|
||||
: `v${facts.version} release credit`
|
||||
: isZh
|
||||
? `v${facts.version} 致谢(未发布)`
|
||||
: `v${facts.version} credit (unreleased)`}
|
||||
</p>
|
||||
<div className="grid-2">
|
||||
<div className="tile">
|
||||
<h3>{isZh ? "已合并或吸收的贡献" : "Merged or adapted contributions"}</h3>
|
||||
<div className="community-credit-list">
|
||||
<ul className="credit-list" role="list">
|
||||
{RELEASE_CONTRIBUTORS.map((handle) => (
|
||||
<Link key={handle} href={`https://github.com/${handle.slice(1)}`}>{handle}</Link>
|
||||
<li key={handle}>
|
||||
<Link href={`https://github.com/${handle.slice(1)}`}>{handle}</Link>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</ul>
|
||||
</div>
|
||||
{RELEASE_HELPERS.length > 0 ? (
|
||||
<section>
|
||||
<div className="tile">
|
||||
<h3>{isZh ? "报告、复现与验证" : "Reports, reproductions, and verification"}</h3>
|
||||
<div className="community-credit-list">
|
||||
<ul className="credit-list" role="list">
|
||||
{RELEASE_HELPERS.map((handle) => (
|
||||
<Link key={handle} href={`https://github.com/${handle.slice(1)}`}>{handle}</Link>
|
||||
<li key={handle}>
|
||||
<Link href={`https://github.com/${handle.slice(1)}`}>{handle}</Link>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<p className="status-line mt-4">
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/blob/main/docs/CONTRIBUTORS.md" className="link">
|
||||
{isZh ? "完整贡献者名单" : "Full contributor record"}
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/blob/main/CHANGELOG.md" className="link">CHANGELOG</Link>
|
||||
</p>
|
||||
</Section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import Image from "next/image";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { PageHeader, Section } from "@/components/page-header";
|
||||
import { Status } from "@/components/status-badge";
|
||||
import { getComputerUse } from "@/lib/i18n/dictionaries";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
import { COMPUTER_USE_REPO, getComputerUseRelease } from "@/lib/computer-use-release";
|
||||
@@ -17,74 +19,115 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
||||
return buildPageMetadata({ path: "/computer-use", locale, title: copy.metaTitle, description: copy.metaDescription });
|
||||
}
|
||||
|
||||
/**
|
||||
* /computer-use — the Mac helper. The download button exists only while the
|
||||
* release check reports a ready, verified release; otherwise the page says
|
||||
* why there is no download and links the published releases.
|
||||
*/
|
||||
export default async function ComputerUsePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const t = getComputerUse(locale);
|
||||
const release = await getComputerUseRelease((await getEnv()).GITHUB_TOKEN);
|
||||
// The disk image is the human download; the archive stays for the updater.
|
||||
const primary = release.status === "ready" ? (release.dmg ?? { downloadUrl: release.downloadUrl, size: release.size }) : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section className="hero">
|
||||
<div className="portal-container community-welcome-inner">
|
||||
<div className="flex items-center gap-5 mb-5">
|
||||
<Image src="/brand/computer-use.png" width={80} height={80} alt="" priority className="shrink-0" />
|
||||
<h1>{t.title}</h1>
|
||||
<>
|
||||
<PageHeader
|
||||
kicker={t.publisher}
|
||||
title={t.title}
|
||||
lede={t.lead}
|
||||
pose="computer"
|
||||
meta={
|
||||
<>
|
||||
{t.requirements}
|
||||
<br />
|
||||
{t.included}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{release.status === "ready" && primary ? (
|
||||
<div className="cu-download">
|
||||
<div className="actions">
|
||||
<a className="btn btn-primary btn-lg" href={primary.downloadUrl}>
|
||||
{t.download}
|
||||
</a>
|
||||
<span className="page-meta tabular">
|
||||
v{release.version} · {Math.ceil(primary.size / 1024 / 1024)} MB
|
||||
</span>
|
||||
</div>
|
||||
<p className="status-line">
|
||||
<a href={release.receiptUrl} className="link">{t.receipt}</a>
|
||||
{release.dmg ? <a href={release.downloadUrl} className="link">{t.downloadZip}</a> : null}
|
||||
</p>
|
||||
</div>
|
||||
<p className="max-w-3xl">{t.lead}</p>
|
||||
<p className="text-sm mt-4">{t.publisher}</p>
|
||||
<div className="mt-8 max-w-3xl">
|
||||
{release.status === "ready" && primary ? <>
|
||||
<a className="portal-button portal-button-primary" href={primary.downloadUrl}>{t.download}</a>
|
||||
<p className="text-sm mt-3">v{release.version} · {Math.ceil(primary.size / 1024 / 1024)} MB</p>
|
||||
<p className="text-sm mt-1 flex flex-wrap gap-x-4">
|
||||
<a href={release.receiptUrl} className="body-link">{t.receipt}</a>
|
||||
{release.dmg ? <a href={release.downloadUrl} className="body-link">{t.downloadZip}</a> : null}
|
||||
</p>
|
||||
</> : <>
|
||||
<h2 className="text-xl">{release.status === "pending" ? t.pendingTitle : t.unavailableTitle}</h2>
|
||||
<p className="mt-2">{release.status === "pending" ? t.pendingBody : t.unavailableBody}</p>
|
||||
<a href={`${COMPUTER_USE_REPO}/releases`} className="body-link mt-3 inline-block">{t.releases}</a>
|
||||
</>}
|
||||
) : (
|
||||
<div className="callout" role="status">
|
||||
<p className="callout-title">
|
||||
<Status tone={release.status === "pending" ? "attention" : "idle"}>
|
||||
{release.status === "pending" ? t.pendingTitle : t.unavailableTitle}
|
||||
</Status>
|
||||
</p>
|
||||
<p>{release.status === "pending" ? t.pendingBody : t.unavailableBody}</p>
|
||||
<p>
|
||||
<a href={`${COMPUTER_USE_REPO}/releases`} className="link">{t.releases}</a>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm mt-6">{t.requirements}<br />{t.included}</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</PageHeader>
|
||||
|
||||
<section className="portal-section">
|
||||
<div className="portal-container">
|
||||
<h2 className="mb-8">{t.setup}</h2>
|
||||
<ol className="grid md:grid-cols-2 gap-x-12 gap-y-8">
|
||||
{t.steps.map((step, index) => <li key={index}>
|
||||
<h3 className="mb-3">{index + 1}. {step.title}</h3>
|
||||
<p className="text-ink-soft leading-relaxed max-w-2xl">{step.body}</p>
|
||||
</li>)}
|
||||
<div className="page-body">
|
||||
<Section id="cu-setup" title={t.setup}>
|
||||
<ol className="steps">
|
||||
{t.steps.map((step, index) => (
|
||||
<li key={index}>
|
||||
<span className="gs-step-index" aria-hidden="true">{index + 1}</span>
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
<section className="portal-section portal-section-muted">
|
||||
<div className="portal-container grid md:grid-cols-2 gap-12">
|
||||
<div><h2 className="mb-4">{t.controlsTitle}</h2>
|
||||
<p className="text-ink-soft leading-relaxed">{t.controlsBody}</p>
|
||||
<a href={`${COMPUTER_USE_REPO}/blob/main/docs/DEMO.md`} className="body-link mt-4 inline-block">{t.demo}</a>
|
||||
<section className="page-section" aria-label={t.controlsTitle}>
|
||||
<div className="grid-2">
|
||||
<div className="tile">
|
||||
<span className="tile-icon" aria-hidden="true"><Icon name="shield" /></span>
|
||||
<h2 className="section-title">{t.controlsTitle}</h2>
|
||||
<p>{t.controlsBody}</p>
|
||||
<a href={`${COMPUTER_USE_REPO}/blob/main/docs/DEMO.md`} className="section-link">
|
||||
{t.demo}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="tile">
|
||||
<span className="tile-icon" aria-hidden="true"><Icon name="repeat" /></span>
|
||||
<h2 className="section-title">{t.updateTitle}</h2>
|
||||
<p>{t.updateBody}</p>
|
||||
<a href={`${COMPUTER_USE_REPO}/blob/main/CHANGELOG.md`} className="section-link">
|
||||
{t.notes}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div><h2 className="mb-4">{t.updateTitle}</h2>
|
||||
<p className="text-ink-soft leading-relaxed">{t.updateBody}</p>
|
||||
<a href={`${COMPUTER_USE_REPO}/blob/main/CHANGELOG.md`} className="body-link mt-4 inline-block">{t.notes}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section className="portal-section">
|
||||
<div className="portal-container">
|
||||
<p className="text-ink-soft max-w-3xl">{t.platforms}</p>
|
||||
<div className="portal-actions">
|
||||
<a href={`${COMPUTER_USE_REPO}/blob/main/docs/TROUBLESHOOTING.md`} className="body-link">{t.help}</a>
|
||||
<a href={COMPUTER_USE_REPO} className="body-link">{t.source}</a>
|
||||
<section className="page-section">
|
||||
<div className="callout">
|
||||
<p className="callout-title">
|
||||
<Icon name="info" className="icon" />
|
||||
{t.platforms}
|
||||
</p>
|
||||
<div className="actions">
|
||||
<a href={`${COMPUTER_USE_REPO}/blob/main/docs/TROUBLESHOOTING.md`} className="btn btn-secondary">{t.help}</a>
|
||||
<a href={COMPUTER_USE_REPO} className="btn btn-ghost">
|
||||
{t.source}
|
||||
<Icon name="external" className="icon" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { Seal } from "@/components/seal";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { PageHeader, Section } from "@/components/page-header";
|
||||
import { Status } from "@/components/status-badge";
|
||||
import { ThinkingTrace } from "@/components/thinking-trace";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
|
||||
@@ -51,105 +53,88 @@ export default async function ConstitutionPage({ params }: { params: Promise<{ l
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* THE THESIS */}
|
||||
<section className="site-container section">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="法" />
|
||||
<div className="eyebrow">{isZh ? "立论" : "The thesis"}</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp mb-6">
|
||||
{isZh ? "三层法" : "Three layers of law"}
|
||||
<span className="font-cjk text-indigo text-3xl sm:text-5xl ml-3">
|
||||
{isZh ? "Three layers of law" : "三层法"}
|
||||
</span>
|
||||
</h1>
|
||||
<p className={`max-w-2xl text-ink-soft ${isZh ? "leading-[1.9] tracking-wide" : "leading-relaxed"}`}>
|
||||
{isZh
|
||||
<PageHeader
|
||||
kicker={isZh ? "立论" : "The thesis"}
|
||||
title={isZh ? "三层法" : "Three layers of law"}
|
||||
lede={
|
||||
isZh
|
||||
? "项目一变老,指令就开始堆积、彼此冲突:最初的规格、后来推翻它的重构、陈旧的记忆、上一个智能体的交接、你此刻的要求、刚跑出的与交接说法不符的测试结果。扁平的系统提示词让模型靠猜来化解;Codewhale 用一部嵌套的宪章给出明确的位阶。顺序由执行框架强制生效——有测试断言它不会漂移——换掉模型,结构依然完好。"
|
||||
: "As a project ages, instructions pile up and conflict: the original spec, a refactor that contradicts it, stale memory, a previous agent's handoff, your current request, fresh test output that doesn't match what the handoff claimed. A flat system prompt makes the model resolve that by guess. Codewhale uses a nested constitution so there is a defined rank instead of vibes — the order is enforced in the harness, with tests asserting it can't drift, and it stays intact when you swap models."}
|
||||
</p>
|
||||
|
||||
{/* v0.9.0 flagship framing */}
|
||||
<div className="mt-7 max-w-2xl px-4 py-3 hairline-t hairline-b hairline-l hairline-r">
|
||||
<span className="pill pill-new mr-2">{isZh ? "v0.9.0 新增" : "New in v0.9.0"}</span>
|
||||
<span className={`text-sm text-ink-soft ${isZh ? "leading-[1.9] tracking-wide" : "leading-relaxed"}`}>
|
||||
: "As a project ages, instructions pile up and conflict: the original spec, a refactor that contradicts it, stale memory, a previous agent's handoff, your current request, fresh test output that doesn't match what the handoff claimed. A flat system prompt makes the model resolve that by guess. Codewhale uses a nested constitution with a defined rank. The harness enforces the order, tests assert it can't drift, and it stays intact when you swap models."
|
||||
}
|
||||
pose="read"
|
||||
>
|
||||
<div className="callout">
|
||||
<p className="callout-title">
|
||||
<Status tone="accent">{isZh ? "自 v0.9.0 起" : "Since v0.9.0"}</Status>
|
||||
</p>
|
||||
<p>
|
||||
{isZh
|
||||
? "宪章优先的初始设置——首次启动依次引导语言、模型、安全姿态和你的宪章;之后随时 /setup。模型可以起草,由你批准。"
|
||||
: "Constitution-first setup — first launch walks language, model, posture, and your constitution; /setup any time. The model can draft it. You ratify it."}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* THE THREE LAYERS */}
|
||||
<section className="site-container py-10 hairline-t">
|
||||
<div className="flex items-baseline gap-4 mb-6">
|
||||
<Seal char="序" />
|
||||
<div className="eyebrow">{isZh ? "位阶,从最稳到最活" : "The rank, most-static first"}</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-0 col-rule hairline-t hairline-b">
|
||||
{LAYERS.map((layer) => (
|
||||
<div key={layer.n} className="p-6">
|
||||
<div className="mb-2 text-xs text-indigo">{layer.n}</div>
|
||||
<h2 className="font-display text-xl mb-1">{isZh ? layer.name.zh : layer.name.en}</h2>
|
||||
<div className="font-mono text-[0.68rem] text-ink-mute mb-3 break-all">
|
||||
{isZh ? layer.pathZh : layer.path}
|
||||
</div>
|
||||
<p className={`text-sm text-ink-soft ${isZh ? "leading-[1.9] tracking-wide" : "leading-relaxed"}`}>
|
||||
{isZh ? layer.zh : layer.en}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className={`mt-5 max-w-2xl text-sm text-ink-soft ${isZh ? "leading-[1.9] tracking-wide" : "leading-relaxed"}`}>
|
||||
{isZh
|
||||
? "三层之下依次是项目说明(AGENTS.md)、记忆与交接。你此刻的要求和实时工具证据仍然主宰当前回合——模型可以被给到很多层,但它绝不能报告一个工具没有返回的事实。"
|
||||
: "Below the three layers rank project instructions (AGENTS.md), then memory and handoffs. Your current request and live tool evidence still control the active turn — the model may be given many layers, but it may never report a fact the tools did not return."}
|
||||
</p>
|
||||
|
||||
{/* The honest boundary */}
|
||||
<div className="mt-6 max-w-2xl px-4 py-3 hairline-t hairline-b hairline-l hairline-r text-sm">
|
||||
<div className="eyebrow mb-1.5">{isZh ? "诚实的边界" : "The honest boundary"}</div>
|
||||
<p className={`text-ink-soft ${isZh ? "leading-[1.9] tracking-wide" : "leading-relaxed"}`}>
|
||||
{isZh
|
||||
? "审批、沙箱、网络与信任控制由代码强制执行——宪章文本永远越不过它们。"
|
||||
: "Approval, sandbox, network, and trust controls are enforced in code — constitution text never overrides them."}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</PageHeader>
|
||||
|
||||
{/* OBSERVABLE IN THE REASONING */}
|
||||
<section className="site-container py-10 hairline-t">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="证" />
|
||||
<div className="eyebrow">{isZh ? "在推理里可以被看到" : "Observable in the reasoning"}</div>
|
||||
</div>
|
||||
<p className={`mb-6 max-w-2xl text-sm text-ink-soft ${isZh ? "leading-[1.9] tracking-wide" : "leading-relaxed"}`}>
|
||||
{isZh
|
||||
? "位阶不是落地页上的一句宣称。下面是真实会话的忠实片段——模型在裁决时直接援引条款。"
|
||||
: "The rank is not a claim on a landing page. These are faithful excerpts from a real session — the model cites the articles as it decides."}
|
||||
</p>
|
||||
<ThinkingTrace locale={locale} />
|
||||
</section>
|
||||
<div className="page-body">
|
||||
<Section
|
||||
id="constitution-rank"
|
||||
title={isZh ? "位阶,从最稳到最活" : "The rank, most-static first"}
|
||||
scope={
|
||||
isZh
|
||||
? "三层之下依次是项目说明(AGENTS.md)、记忆与交接。你此刻的要求和实时工具证据仍然主宰当前回合——模型可以被给到很多层,但它绝不能报告一个工具没有返回的事实。"
|
||||
: "Below the three layers rank project instructions (AGENTS.md), then memory and handoffs. Your current request and live tool evidence still control the active turn — the model may be given many layers, but it may never report a fact the tools did not return."
|
||||
}
|
||||
>
|
||||
<ol className="steps">
|
||||
{LAYERS.map((layer, index) => (
|
||||
<li key={layer.n}>
|
||||
<span className="gs-step-index" aria-hidden="true">{index + 1}</span>
|
||||
<h3>{isZh ? layer.name.zh : layer.name.en}</h3>
|
||||
<p className="dir-meta">{isZh ? layer.pathZh : layer.path}</p>
|
||||
<p>{isZh ? layer.zh : layer.en}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<div className="callout mt-5">
|
||||
<p className="callout-title">
|
||||
<Icon name="shield" className="icon" />
|
||||
{isZh ? "诚实的边界" : "The honest boundary"}
|
||||
</p>
|
||||
<p>
|
||||
{isZh
|
||||
? "审批、沙箱、网络与信任控制由代码强制执行——宪章文本永远越不过它们。"
|
||||
: "Approval, sandbox, network, and trust controls are enforced in code — constitution text never overrides them."}
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* WHERE TO GO NEXT */}
|
||||
<section className="site-container py-8 hairline-t">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Link
|
||||
href={p("/install")}
|
||||
className="portal-button portal-button-primary"
|
||||
>
|
||||
{isZh ? "安装 →" : "Install →"}
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/Hmbown/CodeWhale/blob/main/docs/CONFIGURATION.md#constitution-project-instructions-and-repo-authority"
|
||||
className="px-5 py-3 text-sm text-ink-mute hover:text-indigo transition-colors"
|
||||
>
|
||||
{isZh ? "配置文档 ↗" : "Configuration ↗"}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
<Section
|
||||
id="constitution-trace"
|
||||
title={isZh ? "在推理里可以被看到" : "Observable in the reasoning"}
|
||||
scope={
|
||||
isZh
|
||||
? "位阶不是落地页上的一句宣称。下面是真实会话的忠实片段——模型在裁决时直接援引条款。"
|
||||
: "The rank is not a claim on a landing page. These are faithful excerpts from a real session — the model cites the articles as it decides."
|
||||
}
|
||||
>
|
||||
<ThinkingTrace locale={locale} />
|
||||
</Section>
|
||||
|
||||
<section className="page-section">
|
||||
<div className="actions">
|
||||
<Link href={p("/install")} className="btn btn-primary btn-lg">
|
||||
{isZh ? "安装" : "Install"}
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/Hmbown/CodeWhale/blob/main/docs/CONFIGURATION.md#constitution-project-instructions-and-repo-authority"
|
||||
className="btn btn-ghost btn-lg"
|
||||
>
|
||||
{isZh ? "配置文档" : "Configuration"}
|
||||
<Icon name="external" className="icon" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { PageHeader, Section } from "@/components/page-header";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
@@ -142,102 +144,92 @@ export default async function ContributePage({ params }: { params: Promise<{ loc
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="contribute-page">
|
||||
<section className="hero">
|
||||
<div className="portal-current" aria-hidden="true" />
|
||||
<div className="portal-container community-welcome-inner">
|
||||
<div className="eyebrow">{isZh ? "参与国际开源社区" : "Contribute to an international open-source project"}</div>
|
||||
<h1>{isZh ? "从一个具体的改进开始。" : "Start with one concrete improvement."}</h1>
|
||||
<p>
|
||||
{isZh
|
||||
? "Codewhale 欢迎来自不同国家、语言、平台和经验水平的贡献者。清楚的 bug 报告、复现结果、文档修正、翻译和小而完整的代码补丁都会直接帮助项目。"
|
||||
: "Codewhale welcomes contributors across countries, languages, platforms, and experience levels. Clear bug reports, reproduction results, documentation corrections, translations, and small complete patches all move the project forward."}
|
||||
</p>
|
||||
<div className="portal-actions">
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/issues/new/choose" className="portal-button portal-button-primary">
|
||||
<>
|
||||
<PageHeader
|
||||
kicker={isZh ? "参与国际开源社区" : "Contribute to an international open-source project"}
|
||||
title={isZh ? "从一个具体的改进开始。" : "Start with one concrete improvement."}
|
||||
lede={
|
||||
isZh
|
||||
? "Codewhale 欢迎来自不同国家、语言、平台和经验水平的贡献者。清楚的 bug 报告、复现结果、文档修正、翻译和小而完整的代码补丁都会直接帮助项目。"
|
||||
: "Codewhale welcomes contributors across countries, languages, platforms, and experience levels. Clear bug reports, reproduction results, documentation corrections, translations, and small complete patches all move the project forward."
|
||||
}
|
||||
pose="write"
|
||||
actions={
|
||||
<>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/issues/new/choose" className="btn btn-primary btn-lg">
|
||||
{isZh ? "提交 issue" : "File an issue"}
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/pulls" className="portal-button portal-button-secondary">
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/pulls" className="btn btn-secondary btn-lg">
|
||||
{isZh ? "查看 pull requests" : "Browse pull requests"}
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/blob/main/CONTRIBUTING.md" className="portal-button portal-button-secondary">
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/blob/main/CONTRIBUTING.md" className="btn btn-ghost btn-lg">
|
||||
{isZh ? "打开完整贡献指南" : "Open the full contributor guide"}
|
||||
<Icon name="external" className="icon" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="portal-section">
|
||||
<div className="portal-container">
|
||||
<div className="portal-docs-heading">
|
||||
<div>
|
||||
<span>{isZh ? "现在就可以参与" : "Ways to help now"}</span>
|
||||
<h2>{isZh ? "选择适合你的贡献方式。" : "Choose the contribution that fits."}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="contribute-path-grid">
|
||||
<div className="page-body">
|
||||
<Section id="contribute-paths" title={isZh ? "选择适合你的贡献方式。" : "Choose the contribution that fits."}>
|
||||
<div className="grid-2">
|
||||
{paths.map((path) => (
|
||||
<article key={path.title}>
|
||||
<Link key={path.title} href={path.href} className="tile">
|
||||
<h3>{path.title}</h3>
|
||||
<p>{path.body}</p>
|
||||
<Link href={path.href}>{path.label} →</Link>
|
||||
</article>
|
||||
<span className="section-link">
|
||||
{path.label}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
<section className="portal-section portal-section-muted">
|
||||
<div className="portal-container">
|
||||
<div className="portal-docs-heading">
|
||||
<div>
|
||||
<span>{isZh ? "Pull request 流程" : "Pull request workflow"}</span>
|
||||
<h2>{isZh ? "从问题到可审查的补丁。" : "From a problem to a reviewable patch."}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<ol className="contribute-steps">
|
||||
{steps.map((step) => (
|
||||
<Section id="contribute-workflow" title={isZh ? "从问题到可审查的补丁。" : "From a problem to a reviewable patch."}>
|
||||
<ol className="steps">
|
||||
{steps.map((step, index) => (
|
||||
<li key={step.n}>
|
||||
<span>{step.n}</span>
|
||||
<div>
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.body}</p>
|
||||
<Link href={step.cta.href}>{step.cta.label} →</Link>
|
||||
</div>
|
||||
<span className="gs-step-index" aria-hidden="true">{index + 1}</span>
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.body}</p>
|
||||
<Link href={step.cta.href} className="section-link">
|
||||
{step.cta.label}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
<section className="portal-section">
|
||||
<div className="portal-container portal-section-grid">
|
||||
<div className="portal-section-copy">
|
||||
<span>{isZh ? "审查准备" : "Prepare for review"}</span>
|
||||
<h2>{isZh ? "让修改容易验证。" : "Make the change easy to verify."}</h2>
|
||||
<p>
|
||||
{isZh
|
||||
? "审查者需要看到问题、修改理由、测试证据和剩余风险。范围越清楚,反馈通常越快。"
|
||||
: "A reviewer needs the problem, the reason for the change, test evidence, and remaining risk. Clear scope usually leads to clearer feedback."}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="contribute-review-list">
|
||||
{reviewNotes.map((note) => <li key={note}>{note}</li>)}
|
||||
<Section
|
||||
id="contribute-review"
|
||||
title={isZh ? "让修改容易验证。" : "Make the change easy to verify."}
|
||||
scope={
|
||||
isZh
|
||||
? "审查者需要看到问题、修改理由、测试证据和剩余风险。范围越清楚,反馈通常越快。"
|
||||
: "A reviewer needs the problem, the reason for the change, test evidence, and remaining risk. Clear scope usually leads to clearer feedback."
|
||||
}
|
||||
>
|
||||
<ul className="group-card" role="list">
|
||||
{reviewNotes.map((note) => (
|
||||
<li key={note} className="group-row check-row">
|
||||
<Icon name="check" className="icon" />
|
||||
<span>{note}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
<section className="contribute-dev-loop">
|
||||
<div className="portal-container portal-section-grid min-w-0">
|
||||
<div className="portal-section-copy">
|
||||
<span>{isZh ? "本地开发" : "Local development"}</span>
|
||||
<h2>{isZh ? "构建并运行相关检查。" : "Build and run the relevant checks."}</h2>
|
||||
<p>
|
||||
{isZh
|
||||
? "仓库使用 stable Rust。先运行你所修改部分的测试,再运行格式检查、Clippy 和完整工作区测试。"
|
||||
: "The repository uses stable Rust. Run the focused test for your change first, followed by formatting, Clippy, and the workspace suite."}
|
||||
</p>
|
||||
</div>
|
||||
<Section
|
||||
id="contribute-dev"
|
||||
title={isZh ? "构建并运行相关检查。" : "Build and run the relevant checks."}
|
||||
scope={
|
||||
isZh
|
||||
? "仓库使用 stable Rust。先运行你所修改部分的测试,再运行格式检查、Clippy 和完整工作区测试。"
|
||||
: "The repository uses stable Rust. Run the focused test for your change first, followed by formatting, Clippy, and the workspace suite."
|
||||
}
|
||||
>
|
||||
<pre className="code-block">
|
||||
{`git clone https://github.com/YOUR_USERNAME/CodeWhale.git
|
||||
cd CodeWhale
|
||||
@@ -249,8 +241,8 @@ cargo fmt --all -- --check
|
||||
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||
cargo test --workspace --all-features --locked`}
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
// Bypassing the '@' alias to force TypeScript to find the file
|
||||
import { EmptyState } from "../../../components/surface-state";
|
||||
import { getDigest } from "../../../lib/i18n/dictionaries";
|
||||
import { getEnv } from "../../../lib/kv";
|
||||
import { buildPageMetadata } from "../../../lib/page-meta";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { EmptyState } from "@/components/surface-state";
|
||||
import { getDigest, pickTextLocale } from "@/lib/i18n/dictionaries";
|
||||
import { getEnv } from "@/lib/kv";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
|
||||
// Define the exact structure of the Digest data to fix all the 'any' type errors
|
||||
interface DigestSection {
|
||||
@@ -78,58 +78,49 @@ export default async function DigestArchivePage({ params }: { params: Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
// Handle empty state gracefully
|
||||
// The records are bilingual; show the reader's language (English for
|
||||
// every locale without a Chinese record).
|
||||
const zh = pickTextLocale(locale) === "zh";
|
||||
|
||||
if (digests.length === 0) {
|
||||
return (
|
||||
<div className="route-state">
|
||||
<h1 className="font-display text-3xl mb-6 tracking-crisp text-ink">
|
||||
{t.emptyTitle}
|
||||
</h1>
|
||||
<EmptyState locale={locale} body={t.emptyBody} />
|
||||
</div>
|
||||
<>
|
||||
<PageHeader title={t.title} lede={t.lead} pose="read" />
|
||||
<div className="page-body">
|
||||
<EmptyState locale={locale} title={t.emptyTitle} body={t.emptyBody} pose="sleep" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-12 px-6">
|
||||
<h1 className="font-display text-4xl mb-2 tracking-crisp text-ink">
|
||||
{t.title}
|
||||
</h1>
|
||||
<p className="text-ink-mute mb-8">
|
||||
{t.lead}
|
||||
</p>
|
||||
|
||||
<div className="space-y-12">
|
||||
{digests.map((digest: WeeklyDigest) => (
|
||||
<article key={digest.weekId} className="border hairline rounded-lg p-6 bg-paper-card">
|
||||
<header className="mb-6 hairline-b pb-4">
|
||||
<span className="pill">
|
||||
{digest.weekId}
|
||||
</span>
|
||||
<h2 className="font-display text-2xl mt-3 text-ink">{digest.titleEn}</h2>
|
||||
<h3 className="font-cjk text-lg text-ink-soft mt-1">{digest.titleZh}</h3>
|
||||
</header>
|
||||
|
||||
<div className="mb-6 space-y-4">
|
||||
<p className="text-ink-soft leading-relaxed">{digest.summaryEn}</p>
|
||||
<p className="text-ink-mute leading-relaxed italic">{digest.summaryZh}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{digest.sections.map((section: DigestSection, idx: number) => (
|
||||
<section key={idx} className="border-l-2 border-paper-edge pl-4">
|
||||
<h4 className="font-display text-lg mb-2 text-ink">{section.heading}</h4>
|
||||
<ul className="list-disc list-inside space-y-1 text-ink-soft font-mono text-sm">
|
||||
{section.items.map((item: string, itemIdx: number) => (
|
||||
<li key={itemIdx}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
<>
|
||||
<PageHeader title={t.title} lede={t.lead} pose="read" />
|
||||
<div className="page-body">
|
||||
<div className="page-body-narrow digest-list">
|
||||
{digests.map((digest: WeeklyDigest) => (
|
||||
<article key={digest.weekId} className="changelog-release" lang={zh ? "zh" : "en"}>
|
||||
<header className="digest-head">
|
||||
<span className="pill tabular">{digest.weekId}</span>
|
||||
<h2 className="section-title">{zh ? digest.titleZh : digest.titleEn}</h2>
|
||||
<p className="section-scope">{zh ? digest.summaryZh : digest.summaryEn}</p>
|
||||
</header>
|
||||
<div className="changelog-sections">
|
||||
{digest.sections.map((section: DigestSection, idx: number) => (
|
||||
<section key={idx}>
|
||||
<h3>{section.heading}</h3>
|
||||
<ul>
|
||||
{section.items.map((item: string, itemIdx: number) => (
|
||||
<li key={itemIdx}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { GETTING_STARTED_STEPS } from "@/lib/content/getting-started";
|
||||
import { Seal } from "@/components/seal";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { FaqSearch } from "@/components/faq-search";
|
||||
import { buildFaqPageJsonLd } from "@/lib/faq-schema";
|
||||
import { FACTS } from "@/lib/facts.generated";
|
||||
@@ -53,8 +53,8 @@ npm install -g codewhale
|
||||
# Cargo (needs Rust 1.88+; installs the codewhale command)
|
||||
cargo install codewhale-cli --locked
|
||||
|
||||
# Homebrew (macOS)
|
||||
brew tap Hmbown/deepseek-tui && brew install codewhale
|
||||
# Homebrew (tap; the full name taps and trusts in one step)
|
||||
brew install Hmbown/deepseek-tui/codewhale
|
||||
|
||||
# Direct download
|
||||
# https://github.com/Hmbown/CodeWhale/releases`}
|
||||
@@ -426,8 +426,8 @@ npm install -g codewhale
|
||||
# Cargo(需要 Rust 1.88+;安装 codewhale 命令)
|
||||
cargo install codewhale-cli --locked
|
||||
|
||||
# Homebrew(macOS)
|
||||
brew tap Hmbown/deepseek-tui && brew install codewhale
|
||||
# Homebrew(tap;完整名称会一步完成 tap 与信任)
|
||||
brew install Hmbown/deepseek-tui/codewhale
|
||||
|
||||
# 直接下载
|
||||
# https://github.com/Hmbown/CodeWhale/releases`}
|
||||
@@ -773,42 +773,31 @@ export default async function FaqPage({ params }: { params: Promise<{ locale: st
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLd) }}
|
||||
/>
|
||||
<section className="site-container section">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="问" />
|
||||
<div className="eyebrow">{isZh ? "常见问题" : "FAQ"}</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
{isZh ? (
|
||||
<>常见问题 <span className="font-cjk text-indigo text-5xl ml-2">FAQ</span></>
|
||||
) : (
|
||||
<>FAQ <span className="font-cjk text-indigo text-5xl ml-2">常见问题</span></>
|
||||
)}
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-[1.9] tracking-wide">
|
||||
{isZh
|
||||
<PageHeader
|
||||
kicker={isZh ? "常见问题" : "FAQ"}
|
||||
title={isZh ? "常见问题" : "Frequently asked questions"}
|
||||
lede={
|
||||
isZh
|
||||
? "答案来自实际代码、文档、发布说明和 GitHub 议题。每个回答下方标注了信息来源。如有未覆盖的问题,请在 GitHub 上提交 Issue。"
|
||||
: "Answers sourced from real code, docs, release notes, and GitHub issues. Sources are cited below each answer. If your question isn't covered, open an issue on GitHub."}
|
||||
</p>
|
||||
</section>
|
||||
: "Answers sourced from real code, docs, release notes, and GitHub issues. Sources are cited below each answer. If your question isn't covered, open an issue on GitHub."
|
||||
}
|
||||
pose="talk"
|
||||
/>
|
||||
|
||||
<section className="site-container pb-20">
|
||||
<FaqSearch items={items} locale={locale} />
|
||||
<div className="page-body">
|
||||
<div className="page-body-narrow">
|
||||
<FaqSearch items={items} locale={locale} />
|
||||
|
||||
<div className="mt-12 text-center">
|
||||
<p className="text-ink-soft text-sm mb-4">
|
||||
{isZh
|
||||
? "没找到你的问题?"
|
||||
: "Didn't find your question?"}
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/Hmbown/CodeWhale/issues/new/choose"
|
||||
className="portal-button portal-button-primary gap-2"
|
||||
>
|
||||
{isZh ? "提交 Issue →" : "Open an issue →"}
|
||||
</a>
|
||||
<div className="empty-state empty-state-compact faq-more">
|
||||
<p className="empty-state-title">{isZh ? "没找到你的问题?" : "Didn't find your question?"}</p>
|
||||
<div className="empty-state-actions">
|
||||
<a href="https://github.com/Hmbown/CodeWhale/issues/new/choose" className="btn btn-secondary">
|
||||
{isZh ? "提交 Issue" : "Open an issue"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { Seal } from "@/components/seal";
|
||||
import { Icon, type IconName } from "@/components/icon";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { FeedCard } from "@/components/feed-card";
|
||||
import { FeedRetry } from "@/components/feed-retry";
|
||||
import { EmptyState, ErrorState, UnavailableState } from "@/components/surface-state";
|
||||
@@ -50,7 +51,6 @@ export default async function FeedPage({ params }: { params: Promise<{ locale: s
|
||||
|
||||
const issues = feed.filter((f) => f.kind === "issue");
|
||||
const pulls = feed.filter((f) => f.kind === "pull");
|
||||
const eyebrow = isZh ? "动态" : "Activity";
|
||||
const states = getStates(locale);
|
||||
// FeedRetry first busts this route's ISR entry, because a bare server
|
||||
// re-render would serve the same cached `skipped`/`unavailable` record for
|
||||
@@ -65,145 +65,83 @@ export default async function FeedPage({ params }: { params: Promise<{ locale: s
|
||||
<UnavailableState locale={locale} compact action={retry} />
|
||||
);
|
||||
|
||||
const copy = isZh
|
||||
? {
|
||||
title: "动态",
|
||||
lede: (
|
||||
<>
|
||||
来自{" "}
|
||||
<Link href="https://github.com/Hmbown/CodeWhale" className="link">Hmbown/CodeWhale</Link>
|
||||
{" "}的议题与合并请求镜像,每十分钟刷新一次。点击任意条目跳转至 GitHub。
|
||||
</>
|
||||
),
|
||||
pulls: "合并请求",
|
||||
issues: "议题",
|
||||
shown: (n: number) => `${n} 条`,
|
||||
actions: ["提交议题", "提交合并请求", "发起讨论"],
|
||||
}
|
||||
: {
|
||||
title: "Activity",
|
||||
lede: (
|
||||
<>
|
||||
Follow issues and pull requests from{" "}
|
||||
<Link href="https://github.com/Hmbown/CodeWhale" className="link">Hmbown/CodeWhale</Link>,
|
||||
mirrored here and refreshed every ten minutes. Select any item to open it on GitHub.
|
||||
</>
|
||||
),
|
||||
pulls: "Pull requests",
|
||||
issues: "Issues",
|
||||
shown: (n: number) => `${n} shown`,
|
||||
actions: ["Open an issue", "Open a pull request", "Start a discussion"],
|
||||
};
|
||||
const actionLinks: { href: string; icon: IconName }[] = [
|
||||
{ href: "https://github.com/Hmbown/CodeWhale/issues/new/choose", icon: "alert" },
|
||||
{ href: "https://github.com/Hmbown/CodeWhale/compare", icon: "git-pull-request" },
|
||||
{ href: "https://github.com/Hmbown/CodeWhale/discussions/new", icon: "message" },
|
||||
];
|
||||
const columns = [
|
||||
{ id: "feed-pulls", title: copy.pulls, items: pulls, status: pullsStatus },
|
||||
{ id: "feed-issues", title: copy.issues, items: issues, status: issuesStatus },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{isZh ? (
|
||||
<>
|
||||
<section className="site-container section">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="动" />
|
||||
<div className="eyebrow">{eyebrow}</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
动态 <span className="font-cjk text-indigo text-5xl ml-2">Activity</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-[1.9] tracking-wide">
|
||||
来自{" "}
|
||||
<Link href="https://github.com/Hmbown/CodeWhale" className="body-link">Hmbown/CodeWhale</Link>
|
||||
{" "}的议题与合并请求实时镜像。每十分钟刷新一次。点击任意条目跳转至 GitHub。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="site-container pb-16 grid lg:grid-cols-12 gap-10">
|
||||
<div className="lg:col-span-6">
|
||||
<div className="hairline-t hairline-b hairline-l hairline-r bg-paper">
|
||||
<div className="bg-indigo text-paper px-4 py-3 flex items-baseline justify-between">
|
||||
<h2 className="font-cjk text-base">合并请求 · Pull Requests</h2>
|
||||
<span className="text-xs tabular">{pulls.length} 条</span>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
{pulls.length > 0 ? (
|
||||
pulls.map((p) => <FeedCard key={p.url} item={p} />)
|
||||
) : (
|
||||
<div className="py-4">{columnState(pullsStatus)}</div>
|
||||
)}
|
||||
</div>
|
||||
<PageHeader title={copy.title} lede={copy.lede} pose="browse" />
|
||||
<div className="page-body">
|
||||
<div className="grid-2 feed-columns">
|
||||
{columns.map((column) => (
|
||||
<section key={column.id} className="feed-column" aria-labelledby={column.id}>
|
||||
<div className="feed-column-head">
|
||||
<h2 id={column.id}>{column.title}</h2>
|
||||
<span className="page-meta tabular">{copy.shown(column.items.length)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{column.items.length > 0 ? (
|
||||
<ul className="feed-items" role="list">
|
||||
{column.items.map((item) => (
|
||||
<li key={item.url}><FeedCard item={item} /></li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="feed-empty">{columnState(column.status)}</div>
|
||||
)}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-6">
|
||||
<div className="hairline-t hairline-b hairline-l hairline-r bg-paper">
|
||||
<div className="bg-paper-deep text-ink px-4 py-3 flex items-baseline justify-between">
|
||||
<h2 className="font-cjk text-base">议题 · Issues</h2>
|
||||
<span className="text-xs tabular">{issues.length} 条</span>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
{issues.length > 0 ? (
|
||||
issues.map((i) => <FeedCard key={i.url} item={i} />)
|
||||
) : (
|
||||
<div className="py-4">{columnState(issuesStatus)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-paper-deep hairline-t hairline-b">
|
||||
<div className="site-container py-10 grid md:grid-cols-3 gap-6 text-center">
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/issues/new/choose" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">提交议题</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">Open an issue</div>
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/compare" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">提交合并请求</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">Open a PR</div>
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/discussions/new" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">发起讨论</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">Start a discussion</div>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<section className="site-container section">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="动" />
|
||||
<div className="eyebrow">{eyebrow}</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
Activity <span className="font-cjk text-indigo text-5xl ml-2">动态</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-relaxed">
|
||||
A live mirror of issues and pull requests from{" "}
|
||||
<Link href="https://github.com/Hmbown/CodeWhale" className="body-link">Hmbown/CodeWhale</Link>.
|
||||
Refreshed every ten minutes. Click any item to jump to GitHub.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="site-container pb-16 grid lg:grid-cols-12 gap-10">
|
||||
<div className="lg:col-span-6">
|
||||
<div className="hairline-t hairline-b hairline-l hairline-r bg-paper">
|
||||
<div className="bg-indigo text-paper px-4 py-3 flex items-baseline justify-between">
|
||||
<h2 className="font-cjk text-base">合并 · Pull Requests</h2>
|
||||
<span className="text-xs tabular">{pulls.length} shown</span>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
{pulls.length > 0 ? (
|
||||
pulls.map((p) => <FeedCard key={p.url} item={p} />)
|
||||
) : (
|
||||
<div className="py-4">{columnState(pullsStatus)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-6">
|
||||
<div className="hairline-t hairline-b hairline-l hairline-r bg-paper">
|
||||
<div className="bg-paper-deep text-ink px-4 py-3 flex items-baseline justify-between">
|
||||
<h2 className="font-cjk text-base">议题 · Issues</h2>
|
||||
<span className="text-xs tabular">{issues.length} shown</span>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
{issues.length > 0 ? (
|
||||
issues.map((i) => <FeedCard key={i.url} item={i} />)
|
||||
) : (
|
||||
<div className="py-4">{columnState(issuesStatus)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-paper-deep hairline-t hairline-b">
|
||||
<div className="site-container py-10 grid md:grid-cols-3 gap-6 text-center">
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/issues/new/choose" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">Open an issue</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">提交议题</div>
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/compare" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">Open a PR</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">提交合并</div>
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/discussions/new" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">Start a discussion</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">发起讨论</div>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
<section className="page-section">
|
||||
<ul className="grid-3" role="list">
|
||||
{actionLinks.map((link, index) => (
|
||||
<li key={link.href}>
|
||||
<Link href={link.href} className="dir-row feed-action">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name={link.icon} /></span>
|
||||
<span className="dir-text"><span className="dir-title">{copy.actions[index]}</span></span>
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="external" /></span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import Link from "next/link";
|
||||
import { HeroInstall } from "@/components/hero-install";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { InstallCodeBlock } from "@/components/install-code-block";
|
||||
import { WhalePose } from "@/components/whale-pose";
|
||||
import { INSTALL_COPY } from "@/lib/content/install";
|
||||
import { INSTALL_GUIDE } from "@/lib/install-guide.generated";
|
||||
import { getHome, pickText, pickTextLocale } from "@/lib/i18n/dictionaries";
|
||||
@@ -15,26 +18,51 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* /install — the quick install plate first (the two one-line installs the
|
||||
* home page offers), then the verified guide from docs/INSTALL.md, which owns
|
||||
* the page's heading and every other channel. The guide stays in English
|
||||
* until its translation is verified; the notice says so.
|
||||
*/
|
||||
export default async function InstallPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const home = getHome(locale);
|
||||
const copyLocale = pickTextLocale(locale);
|
||||
return (
|
||||
<article className="install-guide" lang="en">
|
||||
<div className="install-guide-source" lang={pickTextLocale(locale)}>
|
||||
<a href="https://github.com/Hmbown/CodeWhale/blob/main/docs/INSTALL.md">
|
||||
{pickText(INSTALL_COPY.source, locale)}
|
||||
</a>
|
||||
<p>{pickText(INSTALL_COPY.translationNotice, locale)}</p>
|
||||
<>
|
||||
<div className="install-head" lang={copyLocale}>
|
||||
<div className="install-head-inner">
|
||||
<WhalePose pose="run" className="install-head-pose" priority />
|
||||
<div className="install-head-text">
|
||||
<HeroInstall ariaLabel={home.heroInstallAria} copyLabel={home.copy} copiedLabel={home.copied} />
|
||||
<p className="install-head-source">
|
||||
<Icon name="check" className="icon" />
|
||||
<a href="https://github.com/Hmbown/CodeWhale/blob/main/docs/INSTALL.md" className="link">
|
||||
{pickText(INSTALL_COPY.source, locale)}
|
||||
</a>
|
||||
</p>
|
||||
{/* The notice speaks about the Chinese translation, so only the
|
||||
Chinese page shows it. */}
|
||||
{copyLocale === "zh" ? (
|
||||
<p className="install-head-note">{pickText(INSTALL_COPY.translationNotice, locale)}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{INSTALL_GUIDE.chunks.map((chunk, index) => chunk.kind === "code" ? (
|
||||
<InstallCodeBlock key={index} cmd={chunk.text} copyLabel={home.copy} copiedLabel={home.copied} copyLocale={locale} />
|
||||
) : (
|
||||
// HTML is generated at build time with strict raw-HTML and URL guards.
|
||||
<div key={index} dangerouslySetInnerHTML={{ __html: chunk.text }} />
|
||||
))}
|
||||
<p lang={pickTextLocale(locale)}>
|
||||
<Link href={`/${locale}/docs/configuration`}>{pickText(INSTALL_COPY.configuration, locale)}</Link>
|
||||
</p>
|
||||
</article>
|
||||
<article className="install-guide" lang="en">
|
||||
{INSTALL_GUIDE.chunks.map((chunk, index) => chunk.kind === "code" ? (
|
||||
<InstallCodeBlock key={index} cmd={chunk.text} copyLabel={home.copy} copiedLabel={home.copied} copyLocale={locale} />
|
||||
) : (
|
||||
// HTML is generated at build time with strict raw-HTML and URL guards.
|
||||
<div key={index} dangerouslySetInnerHTML={{ __html: chunk.text }} />
|
||||
))}
|
||||
<p lang={copyLocale}>
|
||||
<Link href={`/${locale}/docs/configuration`} className="section-link">
|
||||
{pickText(INSTALL_COPY.configuration, locale)}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
</p>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { LegalTabs } from "@/components/legal-tabs";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { UsagePreferenceControl } from "@/components/usage-counting";
|
||||
import { USAGE_COUNTING_COPY } from "@/lib/content/usage-counting";
|
||||
import { BUILD_FACTS } from "@/lib/facts";
|
||||
@@ -21,27 +23,34 @@ export default async function PrivacyPage({ params }: { params: Promise<{ locale
|
||||
const { locale } = await params;
|
||||
const t = getLegalPrivacy(locale);
|
||||
return (
|
||||
<div className="portal-home">
|
||||
<article className="legal-doc">
|
||||
<p className="legal-doc-kicker">{t.kicker}</p>
|
||||
<h1>{t.title}</h1>
|
||||
<p className="legal-doc-updated">{fill(t.updated, { date: LEGAL_UPDATED })}</p>
|
||||
<p>This policy explains how Shannon Labs handles information when you use Codewhale.</p>
|
||||
{PRIVACY_SECTIONS.map((section) => (
|
||||
<section key={section.title}>
|
||||
<h2>{section.title}</h2>
|
||||
<p>{section.body}</p>
|
||||
</section>
|
||||
))}
|
||||
<section id="usage-counting" className="scroll-mt-32">
|
||||
<h2>{pickText(USAGE_COUNTING_COPY.heading, locale)}</h2>
|
||||
<UsagePreferenceControl locale={locale} appVersion={BUILD_FACTS.version ?? "0.0.0"} />
|
||||
</section>
|
||||
<p className="legal-doc-nav">
|
||||
<Link href={`/${locale}/legal/terms`}>{t.termsLink}</Link>
|
||||
<Link href={`/${locale}`}>{t.homeLink}</Link>
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
<>
|
||||
<PageHeader
|
||||
kicker={t.kicker}
|
||||
title={t.title}
|
||||
meta={fill(t.updated, { date: LEGAL_UPDATED })}
|
||||
/>
|
||||
<div className="page-body">
|
||||
<div className="page-body-narrow">
|
||||
<LegalTabs locale={locale} current="privacy" />
|
||||
<article className="prose legal-doc">
|
||||
<p>This policy explains how Shannon Labs handles information when you use Codewhale.</p>
|
||||
{PRIVACY_SECTIONS.map((section) => (
|
||||
<section key={section.title}>
|
||||
<h2>{section.title}</h2>
|
||||
<p>{section.body}</p>
|
||||
</section>
|
||||
))}
|
||||
<section id="usage-counting" className="scroll-mt-32">
|
||||
<h2>{pickText(USAGE_COUNTING_COPY.heading, locale)}</h2>
|
||||
<UsagePreferenceControl locale={locale} appVersion={BUILD_FACTS.version ?? "0.0.0"} />
|
||||
</section>
|
||||
<p className="status-line">
|
||||
<Link href={`/${locale}/legal/terms`} className="link">{t.termsLink}</Link>
|
||||
<Link href={`/${locale}`} className="link">{t.homeLink}</Link>
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { LegalTabs } from "@/components/legal-tabs";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { fill, getLegalTerms } from "@/lib/i18n/dictionaries";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
import { LEGAL_UPDATED, TERMS_SECTIONS } from "@/lib/legal-copy";
|
||||
@@ -18,26 +20,33 @@ export default async function TermsPage({ params }: { params: Promise<{ locale:
|
||||
const { locale } = await params;
|
||||
const t = getLegalTerms(locale);
|
||||
return (
|
||||
<div className="portal-home">
|
||||
<article className="legal-doc">
|
||||
<p className="legal-doc-kicker">{t.kicker}</p>
|
||||
<h1>{t.title}</h1>
|
||||
<p className="legal-doc-updated">{fill(t.updated, { date: LEGAL_UPDATED })}</p>
|
||||
<p>
|
||||
These terms govern your use of Codewhale, a Shannon Labs product. By creating an
|
||||
account or using the service, you agree to them.
|
||||
</p>
|
||||
{TERMS_SECTIONS.map((section) => (
|
||||
<section key={section.title}>
|
||||
<h2>{section.title}</h2>
|
||||
<p>{section.body}</p>
|
||||
</section>
|
||||
))}
|
||||
<p className="legal-doc-nav">
|
||||
<Link href={`/${locale}/legal/privacy`}>{t.privacyLink}</Link>
|
||||
<Link href={`/${locale}`}>{t.homeLink}</Link>
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
<>
|
||||
<PageHeader
|
||||
kicker={t.kicker}
|
||||
title={t.title}
|
||||
meta={fill(t.updated, { date: LEGAL_UPDATED })}
|
||||
/>
|
||||
<div className="page-body">
|
||||
<div className="page-body-narrow">
|
||||
<LegalTabs locale={locale} current="terms" />
|
||||
<article className="prose legal-doc">
|
||||
<p>
|
||||
These terms govern your use of Codewhale, a Shannon Labs product. By creating an
|
||||
account or using the service, you agree to them.
|
||||
</p>
|
||||
{TERMS_SECTIONS.map((section) => (
|
||||
<section key={section.title}>
|
||||
<h2>{section.title}</h2>
|
||||
<p>{section.body}</p>
|
||||
</section>
|
||||
))}
|
||||
<p className="status-line">
|
||||
<Link href={`/${locale}/legal/privacy`} className="link">{t.privacyLink}</Link>
|
||||
<Link href={`/${locale}`} className="link">{t.homeLink}</Link>
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import Link from "next/link";
|
||||
import { getFacts } from "@/lib/facts";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
import { MODELS_COPY } from "@/lib/content/models";
|
||||
import { Icon, type IconName } from "@/components/icon";
|
||||
import { ModelsTable } from "@/components/models-table";
|
||||
import { PageHeader, Section } from "@/components/page-header";
|
||||
import type { LocalizedText } from "@/lib/content/vocabulary";
|
||||
import { fill, pickText } from "@/lib/i18n/dictionaries";
|
||||
|
||||
@@ -15,6 +17,12 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
||||
description: pickText(MODELS_COPY.metaDescription, locale) });
|
||||
}
|
||||
|
||||
const PATTERN_ICONS: IconName[] = ["key", "cpu", "layers"];
|
||||
|
||||
/**
|
||||
* /models — connect a provider, then the source's own model and provider
|
||||
* lists (facts layer), never a hand-typed catalogue.
|
||||
*/
|
||||
export default async function ModelsPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const facts = await getFacts();
|
||||
@@ -22,86 +30,79 @@ export default async function ModelsPage({ params }: { params: Promise<{ locale:
|
||||
version: facts.version ?? "—", model: facts.defaultModel ?? "—",
|
||||
});
|
||||
const providerDocs = "https://github.com/Hmbown/CodeWhale/blob/main/docs/PROVIDERS.md";
|
||||
const docsLink = (
|
||||
<Link href={providerDocs} className="section-link">
|
||||
{t(MODELS_COPY.providerDocs)}
|
||||
<Icon name="external" className="icon" />
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="models-page">
|
||||
<section className="hero">
|
||||
<div className="portal-current" aria-hidden="true" />
|
||||
<div className="portal-container community-welcome-inner">
|
||||
<div className="eyebrow">{t(MODELS_COPY.kicker)}</div>
|
||||
<h1>{t(MODELS_COPY.title)}</h1>
|
||||
<p>{t(MODELS_COPY.lead)}</p>
|
||||
<div className="portal-actions">
|
||||
<Link href={providerDocs} className="portal-button portal-button-primary">{t(MODELS_COPY.providerDocs)}</Link>
|
||||
<Link href={`/${locale}/install`} className="portal-button portal-button-secondary">{t(MODELS_COPY.install)}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<>
|
||||
<PageHeader
|
||||
kicker={t(MODELS_COPY.kicker)}
|
||||
title={t(MODELS_COPY.title)}
|
||||
lede={t(MODELS_COPY.lead)}
|
||||
pose="think"
|
||||
actions={
|
||||
<>
|
||||
<Link href={providerDocs} className="btn btn-primary btn-lg">{t(MODELS_COPY.providerDocs)}</Link>
|
||||
<Link href={`/${locale}/install`} className="btn btn-secondary btn-lg">{t(MODELS_COPY.install)}</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="portal-section">
|
||||
<div className="portal-container portal-section-grid">
|
||||
<div className="portal-section-copy">
|
||||
<span>{t(MODELS_COPY.setupLabel)}</span>
|
||||
<h2>{t(MODELS_COPY.setupTitle)}</h2>
|
||||
<p>{t(MODELS_COPY.setupLead)}</p>
|
||||
</div>
|
||||
<div className="portal-topic-list">
|
||||
{MODELS_COPY.patterns.map((pattern) => (
|
||||
<Link key={pattern.reference} href={providerDocs}>
|
||||
<strong>{t(pattern.title)}</strong>
|
||||
<span>{t(pattern.detail)}</span>
|
||||
<span className="font-mono break-all">{pattern.reference}</span>
|
||||
</Link>
|
||||
<div className="page-body">
|
||||
<Section id="models-setup" title={t(MODELS_COPY.setupTitle)} scope={t(MODELS_COPY.setupLead)}>
|
||||
<ul className="dir-list dir-list-card" role="list">
|
||||
{MODELS_COPY.patterns.map((pattern, index) => (
|
||||
<li key={pattern.reference}>
|
||||
<Link href={providerDocs} className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name={PATTERN_ICONS[index] ?? "key"} /></span>
|
||||
<span className="dir-text">
|
||||
<span className="dir-title">{t(pattern.title)}</span>
|
||||
<span className="dir-purpose">{t(pattern.detail)}</span>
|
||||
<span className="dir-meta">{pattern.reference}</span>
|
||||
</span>
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="external" /></span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</ul>
|
||||
</Section>
|
||||
|
||||
<section className="portal-section portal-section-muted" aria-labelledby="models-title">
|
||||
<div className="portal-container">
|
||||
<div className="portal-docs-heading">
|
||||
<h2 id="models-title">{t(MODELS_COPY.modelsTitle)}</h2>
|
||||
<Link href={providerDocs}>{t(MODELS_COPY.providerDocs)}</Link>
|
||||
</div>
|
||||
<p className="mb-6 max-w-3xl text-ink-soft leading-relaxed">{t(MODELS_COPY.modelsLead)}</p>
|
||||
<Section id="models-title" title={t(MODELS_COPY.modelsTitle)} scope={t(MODELS_COPY.modelsLead)} link={docsLink}>
|
||||
<ModelsTable models={facts.models} locale={locale} />
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
<section className="portal-section" aria-labelledby="providers-title">
|
||||
<div className="portal-container">
|
||||
<div className="portal-docs-heading">
|
||||
<h2 id="providers-title">{t(MODELS_COPY.listTitle)}</h2>
|
||||
<Link href={providerDocs}>{t(MODELS_COPY.providerDocs)}</Link>
|
||||
</div>
|
||||
<p className="mb-6 max-w-3xl text-ink-soft leading-relaxed">{t(MODELS_COPY.listLead)}</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<Section id="providers-title" title={t(MODELS_COPY.listTitle)} scope={t(MODELS_COPY.listLead)} link={docsLink}>
|
||||
<div className="data-table-wrap">
|
||||
<table className="data-table">
|
||||
<caption className="sr-only">{t(MODELS_COPY.listTitle)}</caption>
|
||||
<thead>
|
||||
<tr className="hairline-b">
|
||||
<th scope="col" className="py-3 pr-4">{t(MODELS_COPY.provider)}</th>
|
||||
<th scope="col" className="py-3 pr-4">{t(MODELS_COPY.id)}</th>
|
||||
<th scope="col" className="py-3">{t(MODELS_COPY.credential)}</th>
|
||||
<tr>
|
||||
<th scope="col">{t(MODELS_COPY.provider)}</th>
|
||||
<th scope="col">{t(MODELS_COPY.id)}</th>
|
||||
<th scope="col">{t(MODELS_COPY.credential)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{facts.providers.map((provider) => (
|
||||
<tr key={provider.id} className="hairline-b">
|
||||
<th scope="row" className="py-3 pr-4 font-medium">{provider.label}</th>
|
||||
<td className="py-3 pr-4"><code className="break-all">{provider.id}</code></td>
|
||||
<td className="py-3"><code className="break-all">{provider.env}</code></td>
|
||||
<tr key={provider.id}>
|
||||
<th scope="row">{provider.label}</th>
|
||||
<td><code>{provider.id}</code></td>
|
||||
<td><code>{provider.env}</code></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="mt-6 text-ink-soft">
|
||||
<p className="section-scope mt-5">
|
||||
{t(MODELS_COPY.missing)}{" "}
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/issues/new/choose" className="body-link">{t(MODELS_COPY.request)}</Link>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/issues/new/choose" className="link">{t(MODELS_COPY.request)}</Link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -18,5 +18,9 @@ export const metadata: Metadata = {
|
||||
* instead of the framework's bare page.
|
||||
*/
|
||||
export default function NotFound() {
|
||||
return <NotFoundRoute />;
|
||||
return (
|
||||
<div className="route-state">
|
||||
<NotFoundRoute />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,10 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { GettingStartedSteps } from "@/components/getting-started-steps";
|
||||
import { HeroInstall } from "@/components/hero-install";
|
||||
import { Icon, type IconName } from "@/components/icon";
|
||||
import { InstallCodeBlock } from "@/components/install-code-block";
|
||||
import { Strata } from "@/components/strata";
|
||||
import { Status, type StatusTone } from "@/components/status-badge";
|
||||
import { WhalePose } from "@/components/whale-pose";
|
||||
import { getFacts } from "@/lib/facts";
|
||||
import { GETTING_STARTED_STEPS } from "@/lib/content/getting-started";
|
||||
import { fill, getHome, splitToken } from "@/lib/i18n/dictionaries";
|
||||
@@ -23,16 +25,22 @@ import { TERMINAL_SCREENSHOT } from "@/lib/media-manifest";
|
||||
// caching. `getFacts()` rejects legacy or older KV snapshots.
|
||||
export const revalidate = 300;
|
||||
|
||||
// Row order is shared by every locale's `gain`, `surfaces` and
|
||||
// `availability` lists, so the marks and states follow the row, not a word.
|
||||
const GAIN_ICONS: IconName[] = ["terminal", "repeat", "shield"];
|
||||
const SURFACE_ICONS: IconName[] = ["terminal", "plug", "monitor", "folder", "users"];
|
||||
// Released · development preview · development build · in development.
|
||||
const AVAILABILITY_TONES: StatusTone[] = ["ready", "attention", "idle", "idle"];
|
||||
|
||||
/**
|
||||
* The Tidal Folio homepage: a sheet read under the sea.
|
||||
* The whale-road homepage: the promise and the install plate in the sky,
|
||||
* the whale resting on one calm horizon, and everything else in the sea
|
||||
* below it, which continues into the footer.
|
||||
*
|
||||
* Every visible string resolves through `getHome(locale)` — English,
|
||||
* Chinese, and every other routed locale take the identical path, with the
|
||||
* English dictionary as the build-time-guaranteed fallback. The only literals
|
||||
* left in this file are code-owned per docs/VOICE.md: the product control
|
||||
* vocabulary (`Plan · Work · Operate`, `Ask · Auto-Review · Full Access`),
|
||||
* the install command, package-manager and mirror proper nouns, the chapter
|
||||
* numerals, and the screenshot path.
|
||||
* Every visible string resolves through `getHome(locale)`. The only literals
|
||||
* left here are code-owned per docs/VOICE.md: the product control vocabulary
|
||||
* (`Plan · Work · Operate`, `Ask · Auto-Review · Full Access`), package
|
||||
* channel proper nouns, and the screenshot path.
|
||||
*/
|
||||
export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
@@ -53,244 +61,296 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
const ledeParts = splitToken(d.heroIntro, "brand");
|
||||
|
||||
return (
|
||||
<div className="product-home">
|
||||
<div className="home">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLd) }}
|
||||
/>
|
||||
|
||||
{/* THE PLATE — paper at the top left, the water rising from the bottom
|
||||
right, the real terminal floating at the waterline. */}
|
||||
<section className="folio-hero">
|
||||
<Strata variant="hero" />
|
||||
<div className="product-container folio-hero-grid">
|
||||
<div className="folio-hero-copy">
|
||||
<h1>{d.heroTitle}</h1>
|
||||
<p className="folio-lede">
|
||||
{ledeParts.map((part, index) => (
|
||||
<Fragment key={index}>
|
||||
{index > 0 && <span className="font-semibold text-ink">Codewhale</span>}
|
||||
{part}
|
||||
</Fragment>
|
||||
{/* THE SKY — the promise, the one primary action, the install plate,
|
||||
and the whale resting on the horizon. */}
|
||||
<section className="home-hero" aria-labelledby="home-title">
|
||||
<div className="home-sky">
|
||||
<div className="home-hero-inner">
|
||||
<div className="home-hero-copy">
|
||||
<h1 id="home-title" className="home-title">{d.heroTitle}</h1>
|
||||
<p className="home-lede">
|
||||
{ledeParts.map((part, index) => (
|
||||
<Fragment key={index}>
|
||||
{index > 0 && <strong>Codewhale</strong>}
|
||||
{part}
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
<div className="actions">
|
||||
<Link href={`/${locale}/install`} className="btn btn-primary btn-lg">
|
||||
{d.getCodewhale}
|
||||
</Link>
|
||||
<Link href={`/${locale}/product`} className="btn btn-ghost btn-lg">
|
||||
{d.exploreProduct}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
</div>
|
||||
<HeroInstall ariaLabel={d.heroInstallAria} copyLabel={d.copy} copiedLabel={d.copied} />
|
||||
</div>
|
||||
<div className="home-whale">
|
||||
<WhalePose pose="rest" priority />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* THE HORIZON — one straight line; nothing stands on it but the whale. */}
|
||||
<div className="horizon" aria-hidden="true" />
|
||||
</section>
|
||||
|
||||
{/* THE SEA — the stage in both appearances, down into the footer. */}
|
||||
<div className="home-sea stage sea-continues">
|
||||
<div className="sea-texture" aria-hidden="true" />
|
||||
<div className="home-reflection" aria-hidden="true">
|
||||
<WhalePose pose="rest" />
|
||||
</div>
|
||||
|
||||
<div className="home-sea-body">
|
||||
{/* The real terminal, just under the surface. Exact-build PTY
|
||||
capture of an empty session: no fabricated conversation,
|
||||
connected tools or completion metrics. */}
|
||||
<section className="home-section" aria-label={d.shotPreview}>
|
||||
<figure className="figure home-shot">
|
||||
<div className="figure-frame">
|
||||
<Image
|
||||
src={TERMINAL_SCREENSHOT.src}
|
||||
alt={fill(d.screenshotAlt, { version: TERMINAL_SCREENSHOT.version })}
|
||||
width={TERMINAL_SCREENSHOT.width}
|
||||
height={TERMINAL_SCREENSHOT.height}
|
||||
sizes="(max-width: 62rem) calc(100vw - 2rem), 60rem"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<figcaption className="figure-caption">
|
||||
<span>
|
||||
{d.shotPreview} · {fill(d.shotBuild, { version: TERMINAL_SCREENSHOT.version })}
|
||||
</span>
|
||||
{/* Each fact is its own translated unit; nothing is
|
||||
concatenated around a token. */}
|
||||
<span
|
||||
className="status-line"
|
||||
data-source-state={sourceIsPublished ? "published release" : "source candidate"}
|
||||
data-source-state-label={sourceIsPublished ? d.publishedRelease : d.figcaptionSourceCandidate}
|
||||
>
|
||||
<Status tone={publishedRelease ? "ready" : "idle"}>
|
||||
{publishedRelease
|
||||
? fill(d.latestRelease, { tag: publishedRelease.tag })
|
||||
: d.releaseUnavailable}
|
||||
</Status>
|
||||
<span>{`${sourceIsPublished ? d.currentSource : d.sourceCandidate} v${sourceVersion}`}</span>
|
||||
<span>{facts.license ?? "MIT"}</span>
|
||||
</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
{/* WHAT YOU CAN DO */}
|
||||
<section className="home-section" aria-labelledby="home-gain">
|
||||
<div className="section-head">
|
||||
<div className="section-head-text">
|
||||
<h2 className="section-title" id="home-gain">{d.gainHeading}</h2>
|
||||
<p className="section-scope">{d.gainLede}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid-3">
|
||||
{d.gain.map(([title, body], index) => (
|
||||
<div key={title} className="tile">
|
||||
<span className="tile-icon" aria-hidden="true">
|
||||
<Icon name={GAIN_ICONS[index] ?? "terminal"} />
|
||||
</span>
|
||||
<h3>{title}</h3>
|
||||
<p>{body}</p>
|
||||
</div>
|
||||
))}
|
||||
</p>
|
||||
<div className="folio-actions">
|
||||
<Link href={`/${locale}/install`} className="folio-button folio-button-primary">
|
||||
{d.getCodewhale}
|
||||
</Link>
|
||||
<Link href={`/${locale}/product`} className="folio-button">
|
||||
{d.exploreProduct}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* TOOLS, CONNECTED APPS, SAVED WORK */}
|
||||
<section className="home-section" aria-labelledby="home-surfaces">
|
||||
<div className="section-head">
|
||||
<div className="section-head-text">
|
||||
<h2 className="section-title" id="home-surfaces">{d.surfacesHeading}</h2>
|
||||
</div>
|
||||
<Link href={`/${locale}/runtime`} className="section-link">
|
||||
{d.runtimeLink}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
</div>
|
||||
<HeroInstall ariaLabel={d.heroInstallAria} copyLabel={d.copy} copiedLabel={d.copied} />
|
||||
</div>
|
||||
<ul className="dir-list dir-list-card" role="list">
|
||||
{d.surfaces.map(([name, description], index) => (
|
||||
<li key={name}>
|
||||
<div className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true">
|
||||
<Icon name={SURFACE_ICONS[index] ?? "terminal"} />
|
||||
</span>
|
||||
<span className="dir-text">
|
||||
<span className="dir-title">{name}</span>
|
||||
<span className="dir-purpose">{description}</span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* Exact-build PTY capture of an empty session. No fabricated
|
||||
conversation, connected tools or completion metrics. */}
|
||||
<figure className="folio-shot">
|
||||
<Image
|
||||
src={TERMINAL_SCREENSHOT.src}
|
||||
alt={fill(d.screenshotAlt, { version: TERMINAL_SCREENSHOT.version })}
|
||||
width={TERMINAL_SCREENSHOT.width}
|
||||
height={TERMINAL_SCREENSHOT.height}
|
||||
sizes="(max-width: 58rem) calc(100vw - 2rem), 56rem"
|
||||
unoptimized
|
||||
priority
|
||||
/>
|
||||
<figcaption>
|
||||
<p className="dotline">
|
||||
<span>{d.shotPreview}</span>
|
||||
<span>{fill(d.shotBuild, { version: TERMINAL_SCREENSHOT.version })}</span>
|
||||
</p>
|
||||
{/*
|
||||
The TUI header grammar: a `cw` chip and a dot chain. Each fact is
|
||||
its own translated unit — the separators are CSS punctuation, so
|
||||
nothing is concatenated around a token and no locale inherits an
|
||||
English joining word.
|
||||
*/}
|
||||
<p
|
||||
className="paper-facts dotline"
|
||||
data-source-state={sourceIsPublished ? "published release" : "source candidate"}
|
||||
data-source-state-label={sourceIsPublished ? d.publishedRelease : d.figcaptionSourceCandidate}
|
||||
>
|
||||
<span className="dotline-chip">cw</span>
|
||||
<span>
|
||||
{publishedRelease
|
||||
? fill(d.latestRelease, { tag: publishedRelease.tag })
|
||||
: d.releaseUnavailable}
|
||||
</span>
|
||||
<span>
|
||||
{`${sourceIsPublished ? d.currentSource : d.sourceCandidate} v${sourceVersion}`}
|
||||
</span>
|
||||
<span>{facts.license ?? "MIT"}</span>
|
||||
</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<aside className="folio-chapter">
|
||||
<span className="folio-chapter-num">01 / {d.chapterTerminal}</span>
|
||||
<p className="folio-chapter-title">{d.chapterTerminalTitle}</p>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* WHAT YOU GAIN — three ruled columns on paper. */}
|
||||
<section className="folio-section">
|
||||
<div className="product-container">
|
||||
<h2>{d.gainHeading}</h2>
|
||||
<p className="folio-section-lede">{d.gainLede}</p>
|
||||
<div className="folio-gain-grid">
|
||||
{d.gain.map(([title, body]) => (
|
||||
<div key={title}>
|
||||
<h3>{title}</h3>
|
||||
<p>{body}</p>
|
||||
{/* YOUR MODELS */}
|
||||
<section className="home-section" aria-labelledby="home-models">
|
||||
<div className="split">
|
||||
<div className="section-head-text">
|
||||
<h2 className="section-title" id="home-models">{d.modelsHeading}</h2>
|
||||
<p className="section-scope">{d.modelsBody}</p>
|
||||
<Link href={`/${locale}/models`} className="section-link">
|
||||
{d.modelsLink}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Tools and saved work, before setup details. */}
|
||||
<section className="folio-section">
|
||||
<div className="product-container">
|
||||
<h2>{d.surfacesHeading}</h2>
|
||||
<dl className="folio-fact-list mt-8">
|
||||
{d.surfaces.map(([name, description]) => (
|
||||
<div key={name}>
|
||||
<dt>{name}</dt>
|
||||
<dd>{description}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<Link href={`/${locale}/runtime`} className="folio-link">
|
||||
{d.runtimeLink}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 02 — YOUR MODELS */}
|
||||
<section className="folio-section">
|
||||
<div className="product-container folio-chapter-grid">
|
||||
<div>
|
||||
<span className="folio-running-head">02 / {d.chapterModels}</span>
|
||||
<h2>{d.modelsHeading}</h2>
|
||||
<p className="folio-section-lede">{d.modelsBody}</p>
|
||||
<Link href={`/${locale}/models`} className="folio-link">
|
||||
{d.modelsLink}
|
||||
</Link>
|
||||
</div>
|
||||
<dl className="folio-fact-list">
|
||||
{d.modelsFacts.map(([kind, description]) => (
|
||||
<div key={kind}>
|
||||
<dt>{kind}</dt>
|
||||
<dd>{description}</dd>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<dt>Plan · Work · Operate</dt>
|
||||
<dd>Ask · Auto-Review · Full Access</dd>
|
||||
<dl className="def-rows">
|
||||
{d.modelsFacts.map(([kind, description]) => (
|
||||
<div key={kind} className="def-row">
|
||||
<dt>{kind}</dt>
|
||||
<dd>{description}</dd>
|
||||
</div>
|
||||
))}
|
||||
<div className="def-row home-modes">
|
||||
<dt>Plan · Work · Operate</dt>
|
||||
<dd>Ask · Auto-Review · Full Access</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{/* 03 — START */}
|
||||
<section className="product-start">
|
||||
<div className="product-container">
|
||||
<span className="folio-running-head">03 / {d.startHeading}</span>
|
||||
<h2>{d.startHeading}</h2>
|
||||
<p className="product-start-lede">{d.startLede}</p>
|
||||
<GettingStartedSteps locale={locale} />
|
||||
<div className="product-start-links">
|
||||
<Link href={`/${locale}/docs/guide`}>{d.startGuideLink}</Link>
|
||||
<Link href={`/${locale}/docs/vocabulary`}>{d.startVocabularyLink}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/* START */}
|
||||
<section className="home-section product-start" aria-labelledby="home-start">
|
||||
<div className="section-head">
|
||||
<div className="section-head-text">
|
||||
<h2 className="section-title" id="home-start">{d.startHeading}</h2>
|
||||
<p className="section-scope">{d.startLede}</p>
|
||||
</div>
|
||||
</div>
|
||||
<GettingStartedSteps locale={locale} />
|
||||
<div className="product-start-links">
|
||||
<Link href={`/${locale}/docs/guide`} className="section-link">
|
||||
{d.startGuideLink}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
<Link href={`/${locale}/docs/vocabulary`} className="section-link">
|
||||
{d.startVocabularyLink}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* THE WATERLINE. Everything below is one water column: the same
|
||||
component grammar, re-inked with the dark whale tokens by the
|
||||
shared below-the-waterline rule in globals.css. */}
|
||||
<div className="folio-waterline" aria-hidden="true">
|
||||
<Strata variant="band" />
|
||||
</div>
|
||||
<div className="ocean-column">
|
||||
{/* 04 — WHERE IT RUNS TODAY */}
|
||||
<section className="folio-availability">
|
||||
<div className="product-container">
|
||||
<span className="folio-running-head">04 / {d.chapterAccount}</span>
|
||||
<h2>{d.availabilityHeading}</h2>
|
||||
<p className="folio-section-lede">{d.availabilityLede}</p>
|
||||
<dl className="folio-availability-list">
|
||||
{d.availability.map(([surface, status, detail]) => (
|
||||
<div key={surface}>
|
||||
{/* WHERE IT RUNS TODAY — each surface with a mark and a word. */}
|
||||
<section className="home-section" aria-labelledby="home-availability">
|
||||
<div className="section-head">
|
||||
<div className="section-head-text">
|
||||
<h2 className="section-title" id="home-availability">{d.availabilityHeading}</h2>
|
||||
<p className="section-scope">{d.availabilityLede}</p>
|
||||
</div>
|
||||
</div>
|
||||
<dl className="def-rows">
|
||||
{d.availability.map(([surface, status, detail], index) => (
|
||||
<div key={surface} className="def-row">
|
||||
<dt>{surface}</dt>
|
||||
<dd>
|
||||
<strong>{status}</strong>
|
||||
<Status tone={AVAILABILITY_TONES[index] ?? "idle"}>{status}</Status>
|
||||
{detail}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<p className="folio-availability-note">{d.availabilityNote}</p>
|
||||
<a href={APP_SIGNUP_URL} className="folio-link" data-usage="signup">
|
||||
{d.accountLink}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<p className="section-scope mt-4">{d.availabilityNote}</p>
|
||||
<div className="actions mt-3">
|
||||
<a href={APP_SIGNUP_URL} className="section-link" data-usage="signup">
|
||||
{d.accountLink}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Install band — the composer plate. */}
|
||||
<section className="product-install-band">
|
||||
<div className="product-container product-install-grid">
|
||||
<h2>{d.installBandHeading}</h2>
|
||||
<div>
|
||||
{/* `❯` is a code-owned literal, like the install command it
|
||||
prompts for — it is the product's glyph, not a sentence. */}
|
||||
<div className="product-composer">
|
||||
<span className="product-composer-prompt" aria-hidden>
|
||||
❯
|
||||
</span>
|
||||
<InstallCodeBlock
|
||||
cmd={GETTING_STARTED_STEPS[0].commands[0]}
|
||||
copyLabel={d.copy}
|
||||
copiedLabel={d.copied}
|
||||
/>
|
||||
{/* INSTALL — the command again, where the page ends. */}
|
||||
<section className="home-section" aria-labelledby="home-install">
|
||||
<div className="home-install">
|
||||
<div className="section-head-text">
|
||||
<h2 className="section-title" id="home-install">{d.installBandHeading}</h2>
|
||||
<p className="home-install-channels">
|
||||
GitHub Releases ({d.binaries}) · npm · Cargo · Docker · Nix · Windows · Android / Termux · {d.chinaMirrors}
|
||||
</p>
|
||||
<Link href={`/${locale}/install`} className="section-link">
|
||||
{d.installGuideLink}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
</div>
|
||||
<p className="dotline">
|
||||
<span>GitHub Releases · {d.binaries}</span>
|
||||
<span>npm</span>
|
||||
<span>Cargo</span>
|
||||
<span>Docker</span>
|
||||
<span>Nix</span>
|
||||
<span>Windows</span>
|
||||
<span>Android / Termux</span>
|
||||
<span>{d.chinaMirrors}</span>
|
||||
</p>
|
||||
<Link href={`/${locale}/install`}>{d.installGuideLink}</Link>
|
||||
<InstallCodeBlock
|
||||
cmd={GETTING_STARTED_STEPS[0].commands[0]}
|
||||
copyLabel={d.copy}
|
||||
copiedLabel={d.copied}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{/* Community */}
|
||||
<section className="product-community">
|
||||
<div className="product-container product-community-grid">
|
||||
<div>
|
||||
<h2>{d.communityHeading}</h2>
|
||||
<p>{d.communityBody}</p>
|
||||
{/* COMMUNITY */}
|
||||
<section className="home-section" aria-labelledby="home-community">
|
||||
<div className="split">
|
||||
<div className="section-head-text">
|
||||
<h2 className="section-title" id="home-community">{d.communityHeading}</h2>
|
||||
<p className="section-scope">{d.communityBody}</p>
|
||||
</div>
|
||||
<nav aria-label={d.communityLinksAria}>
|
||||
<ul className="dir-list dir-list-card" role="list">
|
||||
<li>
|
||||
<a href={REPO_URL} className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name="github" /></span>
|
||||
<span className="dir-text"><span className="dir-title">GitHub</span></span>
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="external" /></span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={REPO_ISSUES_URL} className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name="alert" /></span>
|
||||
<span className="dir-text"><span className="dir-title">Issues</span></span>
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="external" /></span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={DISCORD_URL} className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name="message" /></span>
|
||||
<span className="dir-text"><span className="dir-title">Discord</span></span>
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="external" /></span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={`/${locale}/contribute`} className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name="git-pull-request" /></span>
|
||||
<span className="dir-text"><span className="dir-title">{d.contribute}</span></span>
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="chevron-right" className="icon icon-flip" /></span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
{publishedRelease ? (
|
||||
<a href={publishedRelease.url} className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name="package" /></span>
|
||||
<span className="dir-text"><span className="dir-title">{publishedRelease.tag}</span></span>
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="external" /></span>
|
||||
</a>
|
||||
) : (
|
||||
<a href={REPO_RELEASES_URL} className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name="package" /></span>
|
||||
<span className="dir-text"><span className="dir-title">Releases</span></span>
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="external" /></span>
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<nav aria-label={d.communityLinksAria}>
|
||||
<a href={REPO_URL}>GitHub</a>
|
||||
<a href={REPO_ISSUES_URL}>Issues</a>
|
||||
<a href={DISCORD_URL}>Discord</a>
|
||||
<Link href={`/${locale}/contribute`}>{d.contribute}</Link>
|
||||
{publishedRelease ? (
|
||||
<a href={publishedRelease.url}>{publishedRelease.tag}</a>
|
||||
) : (
|
||||
<a href={REPO_RELEASES_URL}>Releases</a>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { PageHeader, Section } from "@/components/page-header";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
import { PLUGINS_COPY, PLUGIN_COMMANDS } from "@/lib/content/plugins";
|
||||
import type { LocalizedText } from "@/lib/content/vocabulary";
|
||||
@@ -16,118 +18,112 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
||||
description: pickText(PLUGINS_COPY.metaDescription, locale) });
|
||||
}
|
||||
|
||||
/**
|
||||
* /plugins — what a plugin adds, the marketplace catalogue, where plugins
|
||||
* come from, how trust works, and the commands. Nothing runs until the
|
||||
* reader reviews, trusts and enables it; the page says so where it matters.
|
||||
*/
|
||||
export default async function PluginsPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const t = (copy: LocalizedText) => pickText(copy, locale);
|
||||
const authoringLink = (
|
||||
<Link href={AUTHORING_DOC} className="section-link">
|
||||
{t(PLUGINS_COPY.docsCta)}
|
||||
<Icon name="external" className="icon" />
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="plugins-page">
|
||||
<section className="hero">
|
||||
<div className="portal-current" aria-hidden="true" />
|
||||
<div className="portal-container community-welcome-inner">
|
||||
<div className="eyebrow">{t(PLUGINS_COPY.kicker)}</div>
|
||||
<h1>{t(PLUGINS_COPY.title)}</h1>
|
||||
<p>{t(PLUGINS_COPY.lead)}</p>
|
||||
<div className="portal-actions">
|
||||
<Link href={MARKETPLACE_REPO} className="portal-button portal-button-primary">{t(PLUGINS_COPY.marketplaceCta)}</Link>
|
||||
<Link href={AUTHORING_DOC} className="portal-button portal-button-secondary">{t(PLUGINS_COPY.docsCta)}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<>
|
||||
<PageHeader
|
||||
kicker={t(PLUGINS_COPY.kicker)}
|
||||
title={t(PLUGINS_COPY.title)}
|
||||
lede={t(PLUGINS_COPY.lead)}
|
||||
pose="connect"
|
||||
actions={
|
||||
<>
|
||||
<Link href={MARKETPLACE_REPO} className="btn btn-primary btn-lg">{t(PLUGINS_COPY.marketplaceCta)}</Link>
|
||||
<Link href={AUTHORING_DOC} className="btn btn-secondary btn-lg">{t(PLUGINS_COPY.docsCta)}</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="portal-section portal-section-muted" aria-labelledby="cu-title">
|
||||
<div className="portal-container portal-section-grid">
|
||||
<div className="portal-section-copy">
|
||||
<span>{t(PLUGINS_COPY.cuLabel)}</span>
|
||||
<h2 id="cu-title">{t(PLUGINS_COPY.cuTitle)}</h2>
|
||||
<p>{t(PLUGINS_COPY.cuLead)}</p>
|
||||
<p className="mt-4"><code className="font-mono text-[0.85rem]">{t(PLUGINS_COPY.cuInstall)}</code></p>
|
||||
<p className="mt-3 text-ink-soft text-sm">{t(PLUGINS_COPY.cuToolsNote)}</p>
|
||||
</div>
|
||||
<dl className="folio-fact-list">
|
||||
{PLUGINS_COPY.cuFeatures.map((feature) => (
|
||||
<div key={t(feature.title)}>
|
||||
<dt>{t(feature.title)}</dt>
|
||||
<dd>{t(feature.detail)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="portal-section" aria-labelledby="catalog-title">
|
||||
<div className="portal-container portal-section-grid">
|
||||
<div className="portal-section-copy">
|
||||
<span>{t(PLUGINS_COPY.catalogLabel)}</span>
|
||||
<h2 id="catalog-title">{t(PLUGINS_COPY.catalogTitle)}</h2>
|
||||
<p>{t(PLUGINS_COPY.catalogLead)}</p>
|
||||
<p className="mt-4"><code className="font-mono text-[0.85rem]">{t(PLUGINS_COPY.catalogBrowse)}</code></p>
|
||||
</div>
|
||||
<div className="portal-topic-list">
|
||||
{PLUGINS_COPY.catalogEntries.map((entry) => (
|
||||
<Link key={entry.reference} href={MARKETPLACE_REPO}>
|
||||
<strong>{t(entry.title)}</strong>
|
||||
<span>{t(entry.detail)}</span>
|
||||
<span className="font-mono break-all">{entry.reference}</span>
|
||||
<div className="page-body">
|
||||
<Section id="cu-title" title={t(PLUGINS_COPY.cuTitle)} scope={t(PLUGINS_COPY.cuLead)}>
|
||||
<div className="split">
|
||||
<div className="stack">
|
||||
<pre className="code-block">{t(PLUGINS_COPY.cuInstall)}</pre>
|
||||
<p className="section-scope">{t(PLUGINS_COPY.cuToolsNote)}</p>
|
||||
<Link href={`/${locale}/computer-use`} className="section-link">
|
||||
Computer Use
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<dl className="def-rows">
|
||||
{PLUGINS_COPY.cuFeatures.map((feature) => (
|
||||
<div key={t(feature.title)} className="def-row">
|
||||
<dt>{t(feature.title)}</dt>
|
||||
<dd>{t(feature.detail)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="portal-container">
|
||||
<p className="mt-6 text-ink-soft text-sm">{t(PLUGINS_COPY.catalogNote)}</p>
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
<section className="portal-section portal-section-muted" aria-labelledby="sources-title">
|
||||
<div className="portal-container portal-section-grid">
|
||||
<div className="portal-section-copy">
|
||||
<span>{t(PLUGINS_COPY.sourcesLabel)}</span>
|
||||
<h2 id="sources-title">{t(PLUGINS_COPY.sourcesTitle)}</h2>
|
||||
<p>{t(PLUGINS_COPY.sourcesLead)}</p>
|
||||
</div>
|
||||
<div className="portal-topic-list">
|
||||
<Section id="catalog-title" title={t(PLUGINS_COPY.catalogTitle)} scope={t(PLUGINS_COPY.catalogLead)}>
|
||||
<pre className="code-block mb-4">{t(PLUGINS_COPY.catalogBrowse)}</pre>
|
||||
<ul className="dir-list dir-list-card" role="list">
|
||||
{PLUGINS_COPY.catalogEntries.map((entry) => (
|
||||
<li key={entry.reference}>
|
||||
<Link href={MARKETPLACE_REPO} className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name="plug" /></span>
|
||||
<span className="dir-text">
|
||||
<span className="dir-title">{t(entry.title)}</span>
|
||||
<span className="dir-purpose">{t(entry.detail)}</span>
|
||||
<span className="dir-meta">{entry.reference}</span>
|
||||
</span>
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="external" /></span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="section-scope mt-4">{t(PLUGINS_COPY.catalogNote)}</p>
|
||||
</Section>
|
||||
|
||||
<Section id="sources-title" title={t(PLUGINS_COPY.sourcesTitle)} scope={t(PLUGINS_COPY.sourcesLead)}>
|
||||
<ul className="dir-list dir-list-card" role="list">
|
||||
{PLUGINS_COPY.sources.map((source) => (
|
||||
<div key={source.reference}>
|
||||
<strong>{t(source.title)}</strong>
|
||||
<span>{t(source.detail)}</span>
|
||||
<span className="font-mono break-all">{source.reference}</span>
|
||||
</div>
|
||||
<li key={source.reference}>
|
||||
<div className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name="package" /></span>
|
||||
<span className="dir-text">
|
||||
<span className="dir-title">{t(source.title)}</span>
|
||||
<span className="dir-purpose">{t(source.detail)}</span>
|
||||
<span className="dir-meta">{source.reference}</span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</ul>
|
||||
</Section>
|
||||
|
||||
<section className="portal-section" aria-labelledby="trust-title">
|
||||
<div className="portal-container portal-section-grid">
|
||||
<div className="portal-section-copy">
|
||||
<span>{t(PLUGINS_COPY.trustLabel)}</span>
|
||||
<h2 id="trust-title">{t(PLUGINS_COPY.trustTitle)}</h2>
|
||||
<p>{t(PLUGINS_COPY.trustLead)}</p>
|
||||
</div>
|
||||
<div className="portal-topic-list">
|
||||
{PLUGINS_COPY.trustSteps.map((step) => (
|
||||
<div key={t(step.title)}>
|
||||
<strong>{t(step.title)}</strong>
|
||||
<span>{t(step.detail)}</span>
|
||||
</div>
|
||||
<Section id="trust-title" title={t(PLUGINS_COPY.trustTitle)} scope={t(PLUGINS_COPY.trustLead)}>
|
||||
<ol className="steps">
|
||||
{PLUGINS_COPY.trustSteps.map((step, index) => (
|
||||
<li key={t(step.title)}>
|
||||
<span className="gs-step-index" aria-hidden="true">{index + 1}</span>
|
||||
<h3>{t(step.title)}</h3>
|
||||
<p>{t(step.detail)}</p>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="portal-container">
|
||||
<p className="mt-6 text-ink-soft text-sm">{t(PLUGINS_COPY.trustNote)}</p>
|
||||
</div>
|
||||
</section>
|
||||
</ol>
|
||||
<p className="section-scope mt-4">{t(PLUGINS_COPY.trustNote)}</p>
|
||||
</Section>
|
||||
|
||||
<section className="portal-section portal-section-muted" aria-labelledby="commands-title">
|
||||
<div className="portal-container">
|
||||
<div className="portal-docs-heading">
|
||||
<h2 id="commands-title">{t(PLUGINS_COPY.commandsTitle)}</h2>
|
||||
<Link href={AUTHORING_DOC}>{t(PLUGINS_COPY.docsCta)}</Link>
|
||||
</div>
|
||||
<p className="mb-6 max-w-3xl text-ink-soft leading-relaxed">{t(PLUGINS_COPY.commandsLead)}</p>
|
||||
<pre className="code-block text-[0.78rem]">{PLUGIN_COMMANDS.join("\n")}</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<Section id="commands-title" title={t(PLUGINS_COPY.commandsTitle)} scope={t(PLUGINS_COPY.commandsLead)} link={authoringLink}>
|
||||
<pre className="code-block">{PLUGIN_COMMANDS.join("\n")}</pre>
|
||||
</Section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { Icon, type IconName } from "@/components/icon";
|
||||
import { PageHeader, Section } from "@/components/page-header";
|
||||
import { Status, type StatusTone } from "@/components/status-badge";
|
||||
import { getFacts } from "@/lib/facts";
|
||||
import { PRODUCT_COPY } from "@/lib/content/product";
|
||||
import { fill, getHome, pickText } from "@/lib/i18n/dictionaries";
|
||||
@@ -16,6 +19,14 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
||||
});
|
||||
}
|
||||
|
||||
// Row order is fixed by PRODUCT_COPY and the home dictionary, so marks and
|
||||
// states follow the row, not a translated word.
|
||||
const GAIN_ICONS: IconName[] = ["layers", "users", "shield"];
|
||||
const SURFACE_ICONS: IconName[] = ["terminal", "plug", "monitor", "folder", "users"];
|
||||
// Terminal released · local browser ships with it · hosted web preview ·
|
||||
// desktop development build · cloud computers in development.
|
||||
const AVAILABILITY_TONES: StatusTone[] = ["ready", "ready", "attention", "idle", "idle"];
|
||||
|
||||
/**
|
||||
* /product — what Codewhale is and what a person gains, with availability
|
||||
* stated per surface. Copy is the `PRODUCT_COPY` content module; counts come
|
||||
@@ -30,101 +41,112 @@ export default async function ProductPage({ params }: { params: Promise<{ locale
|
||||
const providerCount = facts.providers.length;
|
||||
|
||||
return (
|
||||
<div className="portal-home product-page">
|
||||
<section className="hero">
|
||||
<div className="portal-container community-welcome-inner">
|
||||
<h1>{t(PRODUCT_COPY.title)}</h1>
|
||||
<p>{t(PRODUCT_COPY.lede)}</p>
|
||||
<div className="portal-actions">
|
||||
<Link href={`/${locale}/install`} className="portal-button portal-button-primary">
|
||||
<>
|
||||
<PageHeader
|
||||
title={t(PRODUCT_COPY.title)}
|
||||
lede={t(PRODUCT_COPY.lede)}
|
||||
pose="busy"
|
||||
actions={
|
||||
<>
|
||||
<Link href={`/${locale}/install`} className="btn btn-primary btn-lg">
|
||||
{t(PRODUCT_COPY.actions.install)}
|
||||
</Link>
|
||||
<Link href={`/${locale}/docs`} className="portal-button portal-button-secondary">
|
||||
<Link href={`/${locale}/docs`} className="btn btn-secondary btn-lg">
|
||||
{t(PRODUCT_COPY.actions.docs)}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="folio-section">
|
||||
<div className="product-container">
|
||||
<h2>{t(PRODUCT_COPY.gainHeading)}</h2>
|
||||
<div className="folio-gain-grid">
|
||||
{PRODUCT_COPY.gain.map((row) => (
|
||||
<div key={row.title.en}>
|
||||
<div className="page-body">
|
||||
<Section
|
||||
id="product-gain"
|
||||
title={t(PRODUCT_COPY.gainHeading)}
|
||||
link={
|
||||
<Link href={`/${locale}/models`} className="section-link">
|
||||
{t(PRODUCT_COPY.actions.models)}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<div className="grid-3">
|
||||
{PRODUCT_COPY.gain.map((row, index) => (
|
||||
<div key={row.title.en} className="tile">
|
||||
<span className="tile-icon" aria-hidden="true">
|
||||
<Icon name={GAIN_ICONS[index] ?? "layers"} />
|
||||
</span>
|
||||
<h3>{t(row.title)}</h3>
|
||||
<p>{fill(t(row.body), { count: providerCount })}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Link href={`/${locale}/models`} className="folio-link">
|
||||
{t(PRODUCT_COPY.actions.models)}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
<section className="folio-section product-page-surfaces">
|
||||
<div className="product-container folio-chapter-grid">
|
||||
<div>
|
||||
<h2>{t(PRODUCT_COPY.surfacesHeading)}</h2>
|
||||
<p className="folio-section-lede">{t(PRODUCT_COPY.surfacesLede)}</p>
|
||||
<Link href={`/${locale}/runtime`} className="folio-link">
|
||||
<Section
|
||||
id="product-surfaces"
|
||||
title={t(PRODUCT_COPY.surfacesHeading)}
|
||||
scope={t(PRODUCT_COPY.surfacesLede)}
|
||||
link={
|
||||
<Link href={`/${locale}/runtime`} className="section-link">
|
||||
{t(PRODUCT_COPY.surfacesLink)}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
</div>
|
||||
<dl className="folio-fact-list">
|
||||
{home.surfaces.map(([name, description]) => (
|
||||
<div key={name}>
|
||||
<dt>{name}</dt>
|
||||
<dd>{description}</dd>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ul className="dir-list dir-list-card" role="list">
|
||||
{home.surfaces.map(([name, description], index) => (
|
||||
<li key={name}>
|
||||
<div className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true">
|
||||
<Icon name={SURFACE_ICONS[index] ?? "terminal"} />
|
||||
</span>
|
||||
<span className="dir-text">
|
||||
<span className="dir-title">{name}</span>
|
||||
<span className="dir-purpose">{description}</span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
</ul>
|
||||
</Section>
|
||||
|
||||
<section className="folio-section">
|
||||
<div className="product-container folio-chapter-grid">
|
||||
<div>
|
||||
<h2>{t(PRODUCT_COPY.controlHeading)}</h2>
|
||||
<p className="folio-section-lede">{t(PRODUCT_COPY.controlLede)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<dl className="folio-fact-list">
|
||||
<Section id="product-control" title={t(PRODUCT_COPY.controlHeading)} scope={t(PRODUCT_COPY.controlLede)}>
|
||||
<div className="grid-2">
|
||||
<dl className="def-rows">
|
||||
{PRODUCT_COPY.modes.map((row) => (
|
||||
<div key={row.title.en}>
|
||||
<div key={row.title.en} className="def-row">
|
||||
<dt>{t(row.title)}</dt>
|
||||
<dd>{t(row.body)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<dl className="folio-fact-list mt-8">
|
||||
<dl className="def-rows">
|
||||
{PRODUCT_COPY.permissions.map((row) => (
|
||||
<div key={row.title.en}>
|
||||
<div key={row.title.en} className="def-row">
|
||||
<dt>{t(row.title)}</dt>
|
||||
<dd>{t(row.body)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
<section className="folio-section">
|
||||
<div className="product-container">
|
||||
<h2>{t(PRODUCT_COPY.availabilityHeading)}</h2>
|
||||
<p className="folio-section-lede">{t(PRODUCT_COPY.availabilityLede)}</p>
|
||||
<dl className="folio-availability-list product-availability-paper">
|
||||
{PRODUCT_COPY.availability.map((row) => (
|
||||
<div key={row.surface.en}>
|
||||
<Section
|
||||
id="product-availability"
|
||||
title={t(PRODUCT_COPY.availabilityHeading)}
|
||||
scope={t(PRODUCT_COPY.availabilityLede)}
|
||||
>
|
||||
<dl className="def-rows">
|
||||
{PRODUCT_COPY.availability.map((row, index) => (
|
||||
<div key={row.surface.en} className="def-row">
|
||||
<dt>{t(row.surface)}</dt>
|
||||
<dd>
|
||||
<strong>{t(row.status)}</strong>
|
||||
<Status tone={AVAILABILITY_TONES[index] ?? "idle"}>{t(row.status)}</Status>
|
||||
{t(row.detail)}
|
||||
{row.href && row.linkLabel && (
|
||||
<>
|
||||
{" "}
|
||||
<Link href={`/${locale}${row.href}`} className="body-link">
|
||||
<Link href={`/${locale}${row.href}`} className="link">
|
||||
{t(row.linkLabel)}
|
||||
</Link>
|
||||
</>
|
||||
@@ -133,8 +155,8 @@ export default async function ProductPage({ params }: { params: Promise<{ locale
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { PageHeader, Section } from "@/components/page-header";
|
||||
import { Status, type StatusTone } from "@/components/status-badge";
|
||||
import { getCachedRoadmap, type RoadmapItem } from "@/lib/roadmap-feed";
|
||||
import { getEnv } from "@/lib/kv";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
@@ -119,7 +122,7 @@ const tracksZh = [
|
||||
{
|
||||
title: "暂不考虑",
|
||||
items: [
|
||||
{ title: "静默或内容级遥测", note: "0.9.12 源码中的匿名使用计数在明确接受当前处理方告知前保持关闭,所有关闭选项始终有效。仍被排除在外的是:对话、代码、prompt、文件、模型内容、凭据、逐轮或逐工具时间线、对用户隐藏的收集,以及运行时二进制中的任何第三方广告或分析 SDK。选用托管 provider 时仍会发送本轮所需上下文,回环地址路由可让推理保持本地。" },
|
||||
{ title: "静默或内容级遥测", note: "Codewhale 0.9.12 默认开启匿名使用计数,并在首次启动时告知;所有关闭选项始终有效。仍被排除在外的是:对话、代码、prompt、文件、模型内容、凭据、逐轮或逐工具时间线、对用户隐藏的收集,以及运行时二进制中的任何第三方广告或分析 SDK。选用托管 provider 时仍会发送本轮所需上下文,回环地址路由可让推理保持本地。" },
|
||||
{ title: "本地会话强制经过托管中继", note: "本地 Runtime 与自带提供商路由继续工作,无需把会话发送到 Codewhale 服务" },
|
||||
{ title: "本地 Runtime 强制注册账户", note: "本地安装和运行 Codewhale 不需要账户" },
|
||||
{ title: "赞助商模型推广", note: "模型选择器保持中立——无付费推荐位" },
|
||||
@@ -136,6 +139,10 @@ const tracksZh = [
|
||||
},
|
||||
];
|
||||
|
||||
// Track order is shared by both languages: shipped, underway, considered,
|
||||
// ruled out, and the open model platform direction (not started).
|
||||
const TRACK_TONES: StatusTone[] = ["ready", "accent", "idle", "idle", "idle"];
|
||||
|
||||
const roadmapText = (text: string) =>
|
||||
text.replace(/^>\s*/, "").replaceAll("**", "").replaceAll("CodeWhale", "Codewhale");
|
||||
|
||||
@@ -180,7 +187,7 @@ export default async function RoadmapPage({ params }: { params: Promise<{ locale
|
||||
introduction: "这里将已完成的仓库工作、正在推进的工作、仍在评估的方案和明确不在范围内的方向分开列出。路线图的“已完成”可包含已在源码候选版中实现的工作;安装页与首页另行标明最新已发布包。发布记录和 GitHub issues 会在可用时更新这些分类。",
|
||||
sectionLabel: "当前状态",
|
||||
sectionTitle: "按状态查看工作",
|
||||
browseIssues: "浏览 open issues ↗",
|
||||
browseIssues: "浏览 open issues",
|
||||
count: (value: number) => `${value} 项`,
|
||||
contributeLabel: "参与贡献",
|
||||
contributeTitle: "路线图决策公开进行。",
|
||||
@@ -197,7 +204,7 @@ export default async function RoadmapPage({ params }: { params: Promise<{ locale
|
||||
introduction: "This page separates completed repository work from work in progress, proposals still being evaluated, and directions intentionally kept out of scope. Roadmap Shipped can include work implemented in a source candidate; the install page and homepage separately identify the latest published package. Release records and GitHub issues refresh these categories when available.",
|
||||
sectionLabel: "Current status",
|
||||
sectionTitle: "Work grouped by status",
|
||||
browseIssues: "Browse open issues ↗",
|
||||
browseIssues: "Browse open issues",
|
||||
count: (value: number) => `${value} ${value === 1 ? "item" : "items"}`,
|
||||
contributeLabel: "Contribute",
|
||||
contributeTitle: "Keep roadmap decisions in the open.",
|
||||
@@ -210,64 +217,61 @@ export default async function RoadmapPage({ params }: { params: Promise<{ locale
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="roadmap-page">
|
||||
<section className="hero">
|
||||
<div className="portal-current" aria-hidden="true" />
|
||||
<div className="portal-container community-welcome-inner">
|
||||
<div className="eyebrow">{copy.eyebrow}</div>
|
||||
<h1>{copy.title}</h1>
|
||||
<p>{copy.introduction}</p>
|
||||
</div>
|
||||
</section>
|
||||
<>
|
||||
<PageHeader kicker={copy.eyebrow} title={copy.title} lede={copy.introduction} pose="browse" />
|
||||
|
||||
<section className="portal-section">
|
||||
<div className="portal-container">
|
||||
<div className="portal-docs-heading">
|
||||
<div>
|
||||
<span>{copy.sectionLabel}</span>
|
||||
<h2>{copy.sectionTitle}</h2>
|
||||
</div>
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/issues">{copy.browseIssues}</Link>
|
||||
</div>
|
||||
{tracks.map((track) => (
|
||||
<section key={track.title} className="portal-section-grid py-10 hairline-t">
|
||||
<div className="portal-section-copy">
|
||||
<span>{copy.count(track.items.length)}</span>
|
||||
<h2>{track.title}</h2>
|
||||
</div>
|
||||
<ul className="hairline-t">
|
||||
{track.items.map((item) => (
|
||||
<li key={`${item.title}-${item.note}`} className="py-4 hairline-b">
|
||||
<h3 className="font-display text-base">{roadmapText(item.title)}</h3>
|
||||
<p className={`mt-1 text-sm text-ink-soft ${isZh ? "leading-[1.9] tracking-wide" : "leading-relaxed"}`}>
|
||||
{roadmapText(item.note)}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="portal-section portal-section-muted">
|
||||
<div className="portal-container portal-section-grid">
|
||||
<div className="portal-section-copy">
|
||||
<span>{copy.contributeLabel}</span>
|
||||
<h2>{copy.contributeTitle}</h2>
|
||||
<p>{copy.contributeBody}</p>
|
||||
</div>
|
||||
<div className="portal-topic-list">
|
||||
{copy.links.map((link) => (
|
||||
<Link key={link.title} href={link.href}>
|
||||
<strong>{link.title}</strong>
|
||||
<span>{link.detail}</span>
|
||||
<span aria-hidden="true">↗</span>
|
||||
</Link>
|
||||
<div className="page-body">
|
||||
<Section
|
||||
id="roadmap-status"
|
||||
title={copy.sectionTitle}
|
||||
link={
|
||||
<Link href="https://github.com/Hmbown/CodeWhale/issues" className="section-link">
|
||||
{copy.browseIssues}
|
||||
<Icon name="external" className="icon" />
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<div className="roadmap-tracks">
|
||||
{tracks.map((track, index) => (
|
||||
<section key={track.title} className="roadmap-track" aria-labelledby={`track-${index}`}>
|
||||
<div className="roadmap-track-head">
|
||||
<h3 id={`track-${index}`}>
|
||||
<Status tone={TRACK_TONES[index] ?? "idle"}>{track.title}</Status>
|
||||
</h3>
|
||||
<span className="page-meta tabular">{copy.count(track.items.length)}</span>
|
||||
</div>
|
||||
<ul className="group-card" role="list">
|
||||
{track.items.map((item) => (
|
||||
<li key={`${item.title}-${item.note}`} className="group-row roadmap-item">
|
||||
<p className="roadmap-item-title">{roadmapText(item.title)}</p>
|
||||
<p className="roadmap-item-note">{roadmapText(item.note)}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section id="roadmap-contribute" title={copy.contributeTitle} scope={copy.contributeBody}>
|
||||
<ul className="dir-list dir-list-card" role="list">
|
||||
{copy.links.map((link) => (
|
||||
<li key={link.title}>
|
||||
<Link href={link.href} className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true">
|
||||
<Icon name={link.title === "Pull requests" ? "git-pull-request" : link.title === "Discussions" ? "message" : "alert"} />
|
||||
</span>
|
||||
<span className="dir-text">
|
||||
<span className="dir-title">{link.title}</span>
|
||||
<span className="dir-purpose">{link.detail}</span>
|
||||
</span>
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="external" /></span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Seal } from "@/components/seal";
|
||||
import { Icon, type IconName } from "@/components/icon";
|
||||
import { PageHeader, Section } from "@/components/page-header";
|
||||
import { Status } from "@/components/status-badge";
|
||||
import { getFacts } from "@/lib/facts";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
|
||||
@@ -19,6 +21,8 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
||||
|
||||
interface Integration {
|
||||
name: string;
|
||||
icon: IconName;
|
||||
experimental?: boolean;
|
||||
desc: string;
|
||||
descZh: string;
|
||||
href: string;
|
||||
@@ -27,219 +31,178 @@ interface Integration {
|
||||
const INTEGRATIONS: Integration[] = [
|
||||
{
|
||||
name: "HTTP / SSE Runtime API",
|
||||
icon: "terminal",
|
||||
desc: "Full local HTTP + Server-Sent Events runtime API on 127.0.0.1:7878. Create threads, stream turns, manage background jobs, and control approval decisions — all from any HTTP client or the bundled mobile page.",
|
||||
descZh: "完整的本地 HTTP + Server-Sent Events Runtime API,监听 127.0.0.1:7878。创建线程、流式对话、管理后台任务、控制审批决策——任意 HTTP 客户端或内置手机页面皆可调用。",
|
||||
href: `${REPO_BLOB_BASE}/docs/RUNTIME_API.md`,
|
||||
},
|
||||
{
|
||||
name: "ACP (Agent Client Protocol)",
|
||||
icon: "plug",
|
||||
desc: "Baseline JSON-RPC adapter over stdio for compatible editor clients such as Zed. It supports initialize, new session, prompt, and cancel with text responses; shell and file tools, checkpoint replay, and session loading remain on the full Runtime API.",
|
||||
descZh: "面向 Zed 等兼容编辑器客户端的基础 JSON-RPC stdio 适配器。它支持初始化、新建会话、提示和取消,并返回文本响应;shell 与文件工具、检查点回放和会话加载仍由完整 Runtime API 提供。",
|
||||
href: `${REPO_BLOB_BASE}/docs/RUNTIME_API.md`,
|
||||
},
|
||||
{
|
||||
name: "MCP (Model Context Protocol)",
|
||||
icon: "plug",
|
||||
desc: "Connect Codewhale to external tools and services through configured MCP servers over stdio or HTTP/SSE, or expose Codewhale's own tools to another MCP client.",
|
||||
descZh: "通过已配置的 MCP 服务器(stdio 或 HTTP/SSE)将 Codewhale 连接到外部工具和服务,或把 Codewhale 自身工具暴露给其他 MCP 客户端。",
|
||||
href: `${REPO_BLOB_BASE}/docs/MCP.md`,
|
||||
},
|
||||
{
|
||||
name: "VS Code Extension",
|
||||
icon: "monitor",
|
||||
desc: "Phase 0 companion for the local runtime. It can open Codewhale in a terminal, start and check the Runtime API, and show read-only thread summaries and restore points. It does not yet provide full chat, inline edits, or editor actions.",
|
||||
descZh: "本地 Runtime 的 Phase 0 配套扩展。它可以在终端中打开 Codewhale、启动并检查 Runtime API,以及显示只读线程摘要和还原点;目前尚不提供完整聊天、内联编辑或编辑器操作。",
|
||||
href: "https://github.com/Hmbown/CodeWhale/tree/main/extensions/vscode",
|
||||
},
|
||||
{
|
||||
name: "Telegram Bridge",
|
||||
icon: "message",
|
||||
desc: "First-party Telegram bot bridge. Start a headless Codewhale session, then chat with it from any Telegram client — approvals, tool results, and completions surface inline.",
|
||||
descZh: "官方 Telegram 机器人桥接。启动无头 Codewhale 会话,在任何 Telegram 客户端中与之对话——审批、工具结果和完成状态内联展示。",
|
||||
href: "https://github.com/Hmbown/CodeWhale/tree/main/integrations/telegram-bridge",
|
||||
},
|
||||
{
|
||||
name: "Feishu / Lark Bridge",
|
||||
icon: "message",
|
||||
desc: "First-party Feishu / Lark bot bridge. Chat-native agent loop inside your Feishu workspace with approval cards, session linking, and audit trail.",
|
||||
descZh: "官方飞书 / Lark 机器人桥接。在飞书工作区内实现聊天原生 Agent 循环,支持审批卡片、会话关联和审计日志。",
|
||||
href: "https://github.com/Hmbown/CodeWhale/tree/main/integrations/feishu-bridge",
|
||||
},
|
||||
{
|
||||
name: "Weixin Bridge (实验性)",
|
||||
name: "Weixin Bridge",
|
||||
icon: "message",
|
||||
experimental: true,
|
||||
desc: "Experimental Weixin / WeChat bridge. Receive agent completions and approvals inside WeChat; early-stage and not recommended for production deployments.",
|
||||
descZh: "实验性微信桥接。在微信中接收 Agent 完成通知和审批;早期阶段,不建议用于生产环境。",
|
||||
href: "https://github.com/Hmbown/CodeWhale/tree/main/integrations/weixin-bridge",
|
||||
},
|
||||
];
|
||||
|
||||
function FactRow({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="py-3 hairline-b grid gap-1 sm:grid-cols-[10rem_1fr] sm:gap-6 sm:items-baseline">
|
||||
<div className="eyebrow">{label}</div>
|
||||
<div className="min-w-0 font-mono text-sm text-ink-soft break-words">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function RuntimePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
const facts = await getFacts();
|
||||
|
||||
const trust = [
|
||||
{
|
||||
title: isZh ? "本机运行" : "Runs on your machine",
|
||||
body: isZh
|
||||
? "Runtime API 默认仅监听 127.0.0.1。本地运行时不需要 Codewhale 账户或托管中继。"
|
||||
: "The Runtime API binds 127.0.0.1 by default. The local runtime does not require a Codewhale account or hosted relay.",
|
||||
},
|
||||
{
|
||||
title: isZh ? "认证必需" : "Auth required",
|
||||
body: isZh
|
||||
? "所有 Runtime API 路由(/v1/*)需要 Bearer Token。配置 CODEWHALE_RUNTIME_TOKEN 环境变量或 config.toml 中的 auth_token。"
|
||||
: "All Runtime API routes (/v1/*) require a Bearer token. Set CODEWHALE_RUNTIME_TOKEN env var or auth_token in config.toml.",
|
||||
},
|
||||
{
|
||||
title: isZh ? "权限用户控制" : "Permissions user-controlled",
|
||||
body: isZh
|
||||
? "远程客户端通过经过认证的 Runtime API 提交请求与审批决定。本地模式、权限姿态和沙箱策略仍然生效。"
|
||||
: "Remote clients submit requests and approval decisions through the authenticated Runtime API. Local mode, permission posture, and sandbox policy still apply.",
|
||||
},
|
||||
{
|
||||
title: isZh ? "开放协议" : "Open protocols",
|
||||
body: isZh
|
||||
? "HTTP/SSE Runtime API、MCP 和基础 ACP stdio 适配器分别服务于不同集成场景;请根据客户端需要选择对应接口。"
|
||||
: "The HTTP/SSE Runtime API, MCP surface, and baseline ACP stdio adapter serve different integration needs; choose the interface your compatible client supports.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Hero */}
|
||||
<section className="site-container section">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="接" />
|
||||
<div className="eyebrow">{isZh ? "Runtime & 集成" : "Runtime & Integrations"}</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp mb-6">
|
||||
{isZh ? (
|
||||
<>运行时与集成<span className="font-cjk text-indigo text-5xl ml-2">Runtime & Integrations</span></>
|
||||
) : (
|
||||
<>Runtime & Integrations<span className="font-cjk text-indigo text-5xl ml-2">运行时与集成</span></>
|
||||
)}
|
||||
</h1>
|
||||
|
||||
<p className="max-w-2xl text-ink-soft leading-relaxed">
|
||||
{isZh
|
||||
<PageHeader
|
||||
kicker={isZh ? "Runtime & 集成" : "Runtime & Integrations"}
|
||||
title={isZh ? "运行时与集成" : "Runtime and integrations"}
|
||||
lede={
|
||||
isZh
|
||||
? "Codewhale 不仅是一个终端 Agent——它还是一个可通过多种协议和集成方式嵌入到你现有工作流中的本地控制平面。"
|
||||
: "Codewhale is more than a terminal agent — it is a local control plane you can embed into your existing workflow through multiple protocols and integrations."}
|
||||
</p>
|
||||
</section>
|
||||
: "Embed Codewhale in the tools you already use. Beyond the terminal, it runs a local control plane that editors, scripts and chat apps can talk to."
|
||||
}
|
||||
pose="run"
|
||||
/>
|
||||
|
||||
{/* Runtime facts */}
|
||||
<section className="site-container py-8 hairline-t">
|
||||
<div className="flex items-baseline gap-4 mb-4">
|
||||
<Seal char="数" />
|
||||
<div className="eyebrow">{isZh ? "运行时事实" : "Runtime facts"}</div>
|
||||
</div>
|
||||
<div className="hairline-t">
|
||||
<FactRow label={isZh ? "版本" : "Version"}>{facts.version ?? "—"}</FactRow>
|
||||
<FactRow label={isZh ? "工具数量" : "Tool count"}>{facts.toolCount ?? "—"}</FactRow>
|
||||
<FactRow label={isZh ? "沙箱后端" : "Sandbox backends"}>
|
||||
{facts.sandboxBackends.length ? facts.sandboxBackends.join(" · ") : "—"}
|
||||
</FactRow>
|
||||
<FactRow label={isZh ? "Crates" : "Crates"}>
|
||||
{facts.crates.length ? `${facts.crates.length} · ${facts.crates.join(", ")}` : "—"}
|
||||
</FactRow>
|
||||
<FactRow label={isZh ? "源码版本" : "Source revision"}>
|
||||
<code className="inline break-all">{facts.sourceRevision ?? "—"}</code>
|
||||
</FactRow>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Trust boundary */}
|
||||
<section className="site-container py-8 hairline-t">
|
||||
<div className="flex items-baseline gap-4 mb-4">
|
||||
<Seal char="信" />
|
||||
<div className="eyebrow">{isZh ? "信任边界" : "Trust boundary"}</div>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 gap-6 text-sm text-ink-soft leading-relaxed">
|
||||
<div>
|
||||
<strong className="text-ink">{isZh ? "本机运行" : "Runs on your machine"}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? "Runtime API 默认仅监听 127.0.0.1。本地运行时不需要 Codewhale 账户或托管中继。"
|
||||
: "The Runtime API binds 127.0.0.1 by default. The local runtime does not require a Codewhale account or hosted relay."}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong className="text-ink">{isZh ? "认证必需" : "Auth required"}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? "所有 Runtime API 路由(/v1/*)需要 Bearer Token。配置 CODEWHALE_RUNTIME_TOKEN 环境变量或 config.toml 中的 auth_token。"
|
||||
: "All Runtime API routes (/v1/*) require a Bearer token. Set CODEWHALE_RUNTIME_TOKEN env var or auth_token in config.toml."}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong className="text-ink">{isZh ? "权限用户控制" : "Permissions user-controlled"}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? "远程客户端通过经过认证的 Runtime API 提交请求与审批决定。本地模式、权限姿态和沙箱策略仍然生效。"
|
||||
: "Remote clients submit requests and approval decisions through the authenticated Runtime API. Local mode, permission posture, and sandbox policy still apply."}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong className="text-ink">{isZh ? "开放协议" : "Open protocols"}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? "HTTP/SSE Runtime API、MCP 和基础 ACP stdio 适配器分别服务于不同集成场景;请根据客户端需要选择对应接口。"
|
||||
: "The HTTP/SSE Runtime API, MCP surface, and baseline ACP stdio adapter serve different integration needs; choose the interface your compatible client supports."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Integration cards */}
|
||||
<section className="site-container py-10 hairline-t">
|
||||
<div className="flex items-baseline gap-4 mb-6">
|
||||
<Seal char="集" />
|
||||
<h2 className="eyebrow">{isZh ? "集成方式" : "Integration surfaces"}</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-6">
|
||||
{INTEGRATIONS.map((item) => (
|
||||
<div key={item.name} className="hairline rounded-lg p-5 bg-paper hover:bg-paper-deep transition-colors">
|
||||
<h3 className="font-semibold text-base mb-2">
|
||||
<a href={item.href} target="_blank" rel="noopener noreferrer" className="body-link">
|
||||
{item.name} ↗
|
||||
<div className="page-body">
|
||||
<Section id="runtime-integrations" title={isZh ? "集成方式" : "Integration surfaces"}>
|
||||
<ul className="dir-list dir-list-card" role="list">
|
||||
{INTEGRATIONS.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a href={item.href} target="_blank" rel="noopener noreferrer" className="dir-row">
|
||||
<span className="dir-mark" aria-hidden="true"><Icon name={item.icon} /></span>
|
||||
<span className="dir-text">
|
||||
<span className="dir-title">{item.name}</span>
|
||||
<span className="dir-purpose">{isZh ? item.descZh : item.desc}</span>
|
||||
</span>
|
||||
{item.experimental ? <Status tone="attention">{isZh ? "实验性" : "Experimental"}</Status> : null}
|
||||
<span className="dir-action" aria-hidden="true"><Icon name="external" /></span>
|
||||
</a>
|
||||
</h3>
|
||||
<p className="text-sm text-ink-soft leading-relaxed">
|
||||
{isZh ? item.descZh : item.desc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Section>
|
||||
|
||||
{/* Read more */}
|
||||
<section className="site-container py-8 hairline-t">
|
||||
<p className="text-sm text-ink-soft">
|
||||
{isZh ? (
|
||||
<>
|
||||
详细实现文档:{" "}
|
||||
<a
|
||||
href={`${REPO_BLOB_BASE}/docs/RUNTIME_API.md`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="body-link"
|
||||
>
|
||||
Runtime API 与 ACP stdio 适配器 ↗
|
||||
</a>
|
||||
{" · "}
|
||||
<a
|
||||
href={`${REPO_BLOB_BASE}/docs/MCP.md`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="body-link"
|
||||
>
|
||||
MCP 集成 ↗
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Detailed implementation docs:{" "}
|
||||
<a
|
||||
href={`${REPO_BLOB_BASE}/docs/RUNTIME_API.md`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="body-link"
|
||||
>
|
||||
Runtime API and ACP stdio adapter ↗
|
||||
</a>
|
||||
{" · "}
|
||||
<a
|
||||
href={`${REPO_BLOB_BASE}/docs/MCP.md`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="body-link"
|
||||
>
|
||||
MCP integration ↗
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</section>
|
||||
<Section id="runtime-trust" title={isZh ? "信任边界" : "Trust boundary"}>
|
||||
<div className="grid-2">
|
||||
{trust.map((item) => (
|
||||
<div key={item.title} className="tile">
|
||||
<h3>{item.title}</h3>
|
||||
<p>{item.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section id="runtime-facts" title={isZh ? "运行时事实" : "Runtime facts"}>
|
||||
<dl className="def-rows">
|
||||
<div className="def-row">
|
||||
<dt>{isZh ? "版本" : "Version"}</dt>
|
||||
<dd className="tabular">{facts.version ?? "—"}</dd>
|
||||
</div>
|
||||
<div className="def-row">
|
||||
<dt>{isZh ? "工具数量" : "Tool count"}</dt>
|
||||
<dd className="tabular">{facts.toolCount ?? "—"}</dd>
|
||||
</div>
|
||||
<div className="def-row">
|
||||
<dt>{isZh ? "沙箱后端" : "Sandbox backends"}</dt>
|
||||
<dd>{facts.sandboxBackends.length ? facts.sandboxBackends.join(" · ") : "—"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{/* Crate names and the source revision are for maintainers; they
|
||||
sit behind Details. */}
|
||||
<details className="group-card disclosure-row mt-4">
|
||||
<summary>
|
||||
{isZh ? "详情" : "Details"}
|
||||
<Icon name="chevron-down" className="disclosure-chevron" />
|
||||
</summary>
|
||||
<dl className="disclosure-body runtime-details">
|
||||
<div>
|
||||
<dt>Crates</dt>
|
||||
<dd>{facts.crates.length ? `${facts.crates.length} · ${facts.crates.join(", ")}` : "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{isZh ? "源码版本" : "Source revision"}</dt>
|
||||
<dd><code className="inline">{facts.sourceRevision ?? "—"}</code></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</details>
|
||||
</Section>
|
||||
|
||||
<section className="page-section">
|
||||
<p className="status-line">
|
||||
<span>{isZh ? "详细实现文档:" : "Detailed implementation docs:"}</span>
|
||||
<a href={`${REPO_BLOB_BASE}/docs/RUNTIME_API.md`} target="_blank" rel="noopener noreferrer" className="link">
|
||||
{isZh ? "Runtime API 与 ACP stdio 适配器" : "Runtime API and ACP stdio adapter"}
|
||||
</a>
|
||||
<a href={`${REPO_BLOB_BASE}/docs/MCP.md`} target="_blank" rel="noopener noreferrer" className="link">
|
||||
{isZh ? "MCP 集成" : "MCP integration"}
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 15 KiB |
@@ -6,14 +6,22 @@
|
||||
* later partials override earlier ones at equal specificity, and
|
||||
* overrides.css must stay last. Add rules to the partial for their
|
||||
* surface rather than to this file.
|
||||
*
|
||||
* tokens-roles the ocean, the roles, legacy aliases
|
||||
* base ground, type, code, legacy structures the docs use
|
||||
* primitives the components every page composes
|
||||
* shell header, mobile sheet, footer sea
|
||||
* home / pages page layouts
|
||||
* docs* the docs (owned with the docs pages)
|
||||
*/
|
||||
@import "./styles/tailwind.css";
|
||||
@import "./styles/tokens-roles.css";
|
||||
@import "./styles/base.css";
|
||||
@import "./styles/primitives.css";
|
||||
@import "./styles/shell.css";
|
||||
@import "./styles/utilities.css";
|
||||
@import "./styles/portal.css";
|
||||
@import "./styles/home.css";
|
||||
@import "./styles/pages.css";
|
||||
@import "./styles/docs.css";
|
||||
@import "./styles/install-guide.css";
|
||||
@import "./styles/states.css";
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><linearGradient id="cw-mark-gradient" x1="60" y1="84" x2="450" y2="432" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#264CBA"/><stop offset="100%" stop-color="#4F83CE"/></linearGradient></defs><rect width="512" height="512" rx="78" fill="#FFFFFF"/><path transform="translate(46.1 48.1) scale(0.816)" fill="url(#cw-mark-gradient)" fill-rule="evenodd" d="M 211.759 469.126 C 191.280 466.358 166.740 459.281 148.669 450.933 C 137.944 445.978 137.708 444.492 147.386 442.873 C 162.950 440.270 182.911 431.903 197.412 421.903 C 208.107 414.528 222.871 398.950 227.904 389.729 C 228.497 388.643 225.903 390.576 222.139 394.025 C 213.396 402.036 204.357 408.028 192.691 413.545 C 158.373 429.776 122.240 428.876 89.841 410.983 C 78.798 404.885 71.269 398.348 62.125 386.920 C 31.951 349.206 16.901 305.466 16.901 255.482 C 16.901 206.943 33.640 159.838 64.440 121.703 C 111.469 63.475 186.486 34.176 256.497 46.692 C 268.056 48.759 286.008 54.101 288.636 56.257 L 290.796 58.029 L 288.311 62.801 C 284.863 69.421 274.479 80.154 266.190 85.664 C 262.368 88.205 255.644 91.529 251.248 93.052 C 244.132 95.516 243.554 95.895 245.984 96.505 C 248.804 97.213 254.206 96.862 270.355 94.924 C 283.318 93.368 293.079 94.872 310.680 101.138 C 330.307 108.125 345.213 112.582 353.764 114.018 C 358.334 114.786 361.319 115.794 361.661 116.685 C 363.320 121.008 349.053 137.395 337.939 143.932 C 334.429 145.997 327.978 148.880 323.604 150.340 C 316.545 152.696 314.140 152.994 302.229 152.985 C 287.046 152.974 281.417 151.727 269.918 145.829 C 264.842 143.226 260.404 139.692 253.515 132.769 C 243.257 122.461 237.445 118.925 226.498 116.332 C 196.283 109.177 160.483 119.340 131.001 143.443 C 114.090 157.269 101.527 174.990 93.047 196.983 C 87.965 210.163 86.204 219.746 86.224 234.128 C 86.265 263.885 97.393 281.372 122.165 290.608 C 127.662 292.658 131.037 293.137 142.664 293.517 C 165.823 294.275 182.337 290.007 231.146 270.650 C 299.879 243.390 348.689 228.976 395.530 222.106 C 412.810 219.572 452.655 218.388 462.814 220.107 C 480.607 223.118 489.106 228.538 492.618 239.112 C 494.120 243.634 494.290 246.619 493.776 259.403 C 492.987 279.032 490.534 289.883 481.997 311.499 C 447.936 397.744 367.611 458.036 271.849 469.238 C 255.937 471.099 225.945 471.044 211.759 469.126 Z M 268.570 453.799 C 324.066 448.214 375.534 424.502 414.738 386.457 C 437.939 363.942 455.183 338.976 467.076 310.680 C 473.360 295.729 479.593 272.970 477.873 271.251 C 474.893 268.270 451.236 270.950 436.056 275.988 C 410.958 284.317 379.331 305.900 352.435 333.051 C 337.949 347.675 329.137 358.512 309.500 385.857 C 293.525 408.102 285.758 417.452 273.242 429.510 C 262.916 439.457 254.375 445.852 244.220 451.239 L 237.443 454.835 L 247.859 454.835 C 253.588 454.835 262.908 454.369 268.570 453.799 Z M 291.877 436.086 C 291.586 435.616 294.356 434.419 298.032 433.425 C 339.084 422.327 383.749 392.629 411.885 357.724 C 424.930 341.540 439.445 316.946 444.490 302.478 C 445.205 300.427 446.123 298.750 446.531 298.750 C 448.652 298.750 438.704 323.522 431.035 337.337 C 424.713 348.725 422.249 352.547 414.573 362.874 C 387.281 399.591 349.152 424.467 304.976 434.375 C 291.935 437.300 292.575 437.216 291.877 436.086 Z M 314.242 403.739 C 314.470 403.523 318.540 401.320 323.286 398.845 C 354.190 382.728 385.388 353.647 402.926 324.610 C 404.997 321.181 406.930 318.614 407.221 318.905 C 408.529 320.213 394.836 342.688 385.885 353.926 C 373.881 368.998 359.917 381.742 345.696 390.606 C 337.012 396.018 319.186 404.132 315.978 404.132 C 314.796 404.132 314.014 403.955 314.242 403.739 Z M 300.106 344.750 C 304.411 341.565 305.548 338.263 303.789 334.053 C 300.211 325.490 284.753 325.447 280.680 333.988 C 279.096 337.310 279.096 337.735 280.679 341.054 C 281.688 343.169 283.655 345.168 285.585 346.040 C 289.707 347.900 296.685 347.281 300.106 344.750 Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-49.5 -49 98 98"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.41 2.47 C44.22 2.53 44.39 2.58 44.42 2.75 C44.44 2.91 44.23 3.97 43.86 5.56 C43.77 5.97 43.53 6.8 43.27 7.63 C43.1 8.17 42.93 8.67 42.75 9.13 C42.69 9.26 42.61 9.47 42.56 9.6 C42.51 9.72 42.37 10.06 42.24 10.36 C42.12 10.65 42 10.95 41.97 11 C41.95 11.05 41.87 11.23 41.8 11.39 C41.73 11.55 41.63 11.78 41.59 11.88 C41.48 12.11 40.12 14.85 39.95 15.15 C39.88 15.28 39.77 15.46 39.71 15.57 C39.65 15.68 39.57 15.81 39.54 15.87 C39.48 15.98 39.38 16.15 39.12 16.53 C39.03 16.68 38.94 16.82 38.92 16.85 C38.91 16.88 38.81 17.03 38.71 17.2 C38.6 17.36 38.35 17.73 38.16 18.03 C37.67 18.8 37.69 18.77 37.29 19.32 C36.85 19.92 36.76 20.05 35.73 21.34 C32.53 25.39 28.27 29.19 23.9 31.88 C22.04 33.03 21.74 33.2 20.1 34.02 C18.09 35.02 17.11 35.45 14.75 36.33 C12.24 37.26 10.02 37.87 6.98 38.45 C3.87 39.05 1.29 39.28 -2.13 39.28 C-4.07 39.27 -4.26 39.22 -3.42 38.91 C-3.02 38.77 -2.21 38.35 -1.22 37.78 C2.32 35.75 6.11 31.96 9.76 26.79 C9.92 26.56 10.36 25.94 10.73 25.42 C11.11 24.91 11.7 24.07 12.06 23.58 C13.07 22.17 14.15 20.74 15.31 19.29 C17.34 16.76 20.77 13.27 23.08 11.39 C25.17 9.68 25.32 9.57 26.66 8.61 C27.51 8 28.73 7.17 29.19 6.89 C29.32 6.82 29.57 6.66 29.76 6.54 C30.16 6.29 30.95 5.83 30.98 5.83 C30.99 5.83 31.22 5.71 31.48 5.53 C35.3 3.35 39.76 2.21 43.41 2.47 Z"/><path d="M8.18 13.96 C9.97 14.31 10.7 15.87 9.61 17.04 C8.44 18.29 5.95 18.1 5.1 16.68 C4.88 16.32 4.88 15.54 5.1 15.13 C5.6 14.2 6.89 13.7 8.18 13.96 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-10.29 -28.39 C-7.67 -28.48 -5.65 -28.17 -3.7 -27.4 C-3.35 -27.26 -2.36 -26.74 -2.25 -26.64 C-2.21 -26.61 -2.16 -26.57 -2.14 -26.57 C-1.97 -26.57 -1.15 -25.8 -0.5 -25.03 C0.4 -23.96 1.59 -22.9 2.18 -22.64 C2.24 -22.61 2.39 -22.52 2.51 -22.45 C2.78 -22.28 3.79 -21.77 4.21 -21.6 C4.72 -21.39 5.01 -21.3 6.05 -21 C8.62 -20.27 12.47 -20.6 15.02 -21.76 C15.43 -21.95 16.16 -22.31 16.34 -22.42 C16.45 -22.49 16.67 -22.62 16.84 -22.72 C18.18 -23.52 19.92 -25.32 20.76 -26.77 C21.27 -27.66 21.23 -28 20.6 -28 C20.47 -28 20.02 -28.07 19.12 -28.25 C18.12 -28.44 17.15 -28.69 15.68 -29.12 C15.2 -29.26 13.47 -29.86 12.83 -30.11 C12.53 -30.23 12.14 -30.38 11.77 -30.51 C11.64 -30.56 11.38 -30.66 11.18 -30.73 C11 -30.8 10.76 -30.88 10.67 -30.9 C10.57 -30.93 10.43 -30.99 10.34 -31.03 C10.26 -31.07 10.16 -31.1 10.14 -31.1 C10.11 -31.1 9.85 -31.17 9.55 -31.27 C8.04 -31.78 6.3 -32.06 4.8 -32.05 C3.99 -32.05 2.22 -31.88 1.9 -31.78 C1.82 -31.76 1.64 -31.73 1.5 -31.71 C1.37 -31.69 0.99 -31.63 0.67 -31.58 C-0.04 -31.46 -1.24 -31.47 -1.91 -31.6 C-2.14 -31.65 -2.46 -31.7 -2.62 -31.72 C-3.13 -31.79 -3.02 -31.92 -2.33 -32.06 C-1.34 -32.26 0.51 -32.93 1.32 -33.39 C3.54 -34.66 5.38 -36.41 6.48 -38.3 C7.07 -39.31 7 -39.58 6.07 -39.92 C3.55 -40.84 0.91 -41.47 -1.94 -41.8 C-3.65 -42.01 -6.32 -42.1 -7.86 -42 C-9.06 -37.46 -9.87 -32.93 -10.29 -28.39 Z"/><path d="M-7.86 -42 C-9.27 -41.91 -9.98 -41.85 -10.66 -41.76 C-11.52 -41.65 -12.67 -41.47 -12.76 -41.44 C-12.8 -41.43 -12.97 -41.4 -13.15 -41.37 C-13.79 -41.27 -16.51 -40.6 -17.26 -40.36 C-17.53 -40.27 -17.97 -40.13 -18.23 -40.05 C-19.23 -39.73 -20.62 -39.19 -22.08 -38.56 C-22.54 -38.37 -24.48 -37.42 -24.59 -37.34 C-24.61 -37.32 -24.86 -37.18 -25.13 -37.04 C-25.4 -36.9 -25.93 -36.6 -26.31 -36.36 C-26.69 -36.13 -27.13 -35.87 -27.29 -35.77 C-30.75 -33.73 -34.65 -30.25 -37.67 -26.52 C-37.97 -26.16 -38.6 -25.34 -38.7 -25.2 C-38.76 -25.11 -38.89 -24.92 -38.99 -24.78 C-39.14 -24.57 -39.38 -24.22 -40.16 -23.06 C-40.51 -22.55 -40.69 -22.24 -41.27 -21.22 C-41.6 -20.64 -41.9 -20.11 -41.94 -20.04 C-42.03 -19.87 -42.94 -18.03 -43.14 -17.61 C-43.41 -17.03 -43.43 -16.98 -43.63 -16.48 C-44.02 -15.53 -44.14 -15.22 -44.36 -14.56 C-44.44 -14.32 -44.56 -14 -44.62 -13.85 C-44.67 -13.7 -44.77 -13.44 -44.82 -13.27 C-44.87 -13.1 -44.96 -12.82 -45.01 -12.63 C-45.2 -12.05 -45.53 -10.82 -45.6 -10.47 C-45.62 -10.35 -45.68 -10.15 -45.71 -10.01 C-45.78 -9.76 -45.86 -9.37 -46.08 -8.29 C-46.32 -7.15 -46.54 -5.76 -46.63 -4.86 C-46.66 -4.57 -46.69 -4.3 -46.7 -4.27 C-46.76 -4.06 -46.88 -1.34 -46.88 -0.17 C-46.87 3.28 -46.38 7.39 -45.69 9.78 C-45.64 9.96 -45.6 10.13 -45.6 10.18 C-45.6 10.22 -45.55 10.42 -45.49 10.62 C-45.44 10.83 -45.35 11.14 -45.3 11.34 C-45.04 12.26 -44.63 13.49 -44.05 15.1 C-44.01 15.22 -43.96 15.36 -43.93 15.42 C-43.9 15.47 -43.81 15.68 -43.73 15.88 C-43.65 16.08 -43.55 16.31 -43.51 16.4 C-43.48 16.48 -43.34 16.79 -43.21 17.09 C-42.97 17.65 -41.94 19.74 -41.72 20.11 C-41.64 20.24 -41.1 21.16 -41.05 21.26 C-40.9 21.52 -40.64 21.94 -40.41 22.25 C-40.18 22.57 -40.08 22.71 -39.63 23.38 C-38.69 24.77 -37.3 26.44 -35.99 27.75 C-35.16 28.58 -35.05 28.68 -34.25 29.2 C-31.72 30.86 -29.76 31.74 -26.83 32.52 C-24.6 33.1 -20.8 33.19 -18.15 32.71 C-16.59 32.42 -14.57 31.8 -12.89 31.09 CLine truncated
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 11 KiB |
@@ -2,11 +2,10 @@ import type { MetadataRoute } from "next";
|
||||
import { SITE_NAME } from "@/lib/page-meta";
|
||||
|
||||
/**
|
||||
* Web app manifest. Icons are the founder's whale mark in white on the
|
||||
* #142352 navy tile, rasterised from app/icon.svg by
|
||||
* scripts/brand/trace-brand.py. Static rasters live in public/ next to the
|
||||
* other shipped assets; the field naming follows the Next.js metadata-file
|
||||
* convention already used by app/icon.svg and app/opengraph-image.tsx.
|
||||
* Web app manifest. Icons are the brand mark itself — the C-shaped whale in
|
||||
* the logo gradient (#1E8FD8 -> #0B48BB) on a transparent ground, the
|
||||
* resting pose from public/whale/rest.svg rasterised to the sizes below
|
||||
* (app/icon.svg is the same drawing). No tile.
|
||||
*/
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
@@ -24,10 +23,10 @@ export default function manifest(): MetadataRoute.Manifest {
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
// Brand navy: the icon tile, so the installed shell's splash and chrome
|
||||
// are one surface with the mark.
|
||||
theme_color: "#142352",
|
||||
background_color: "#142352",
|
||||
// The ocean floor (tokens-roles.css --ocean-floor): the dark ground the
|
||||
// site prefers, so the splash and chrome sit in the same water.
|
||||
theme_color: "#061431",
|
||||
background_color: "#061431",
|
||||
display: "standalone",
|
||||
};
|
||||
}
|
||||
@@ -14,10 +14,10 @@ export const contentType = "image/png";
|
||||
// regeneration on the site fail with a 500 and serve its build snapshot forever.
|
||||
async function brandSvgs(): Promise<[string, string]> {
|
||||
const [mark, wordmark] = await Promise.all([
|
||||
readFile(join(process.cwd(), "public/brand/mark-reversed.svg")),
|
||||
readFile(join(process.cwd(), "public/whale/rest.svg")),
|
||||
readFile(join(process.cwd(), "public/brand/wordmark-inverted.svg")),
|
||||
]);
|
||||
return [mark.toString().replace("currentColor", "#ffffff"), wordmark.toString()];
|
||||
return [mark.toString(), wordmark.toString()];
|
||||
}
|
||||
|
||||
export default async function OpengraphImage() {
|
||||
@@ -25,8 +25,9 @@ export default async function OpengraphImage() {
|
||||
const markDataUrl = `data:image/svg+xml;base64,${Buffer.from(markSvg).toString("base64")}`;
|
||||
const wordmarkDataUrl = `data:image/svg+xml;base64,${Buffer.from(wordmarkSvg).toString("base64")}`;
|
||||
|
||||
// Brand navy ground, the white mark and inverted wordmark, and the identity
|
||||
// phrase once — the wordmark is the name, so no second "Codewhale" heading.
|
||||
// The ocean: navy rising into the logo's blue, the whale in the logo
|
||||
// gradient, the inverted wordmark, and the identity phrase once — the
|
||||
// wordmark is the name, so no second "Codewhale" heading.
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
@@ -38,14 +39,14 @@ export default async function OpengraphImage() {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 34,
|
||||
background: "#142352",
|
||||
backgroundImage: "linear-gradient(180deg, #0a1d48 0%, #061431 55%, #0b48bb 100%)",
|
||||
fontFamily: "sans-serif",
|
||||
}}
|
||||
>
|
||||
<img src={markDataUrl} width={200} height={200} alt="" />
|
||||
<img src={markDataUrl} width={240} height={240} alt="" />
|
||||
{/* The family wordmark is 1024x160 (6.4:1). */}
|
||||
<img src={wordmarkDataUrl} width={532} height={83} alt="Codewhale" />
|
||||
<div style={{ display: "flex", fontSize: 30, color: "#F6F2E8", marginTop: 14 }}>
|
||||
<div style={{ display: "flex", fontSize: 30, color: "#efeeeb", marginTop: 14 }}>
|
||||
{IDENTITY_PHRASE}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,226 +1,147 @@
|
||||
/* ---------- base ---------- */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Base — the ground, type, and the few legacy structures docs use */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
max-width: 100%;
|
||||
overflow-x: clip;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
body {
|
||||
/* The twilight floor: one opaque field in the page's appearance. The one
|
||||
descent into the stage is a hard horizon, never a page gradient. */
|
||||
background: var(--bg);
|
||||
/* The ground: paper under a pale sky in light; the navy ocean rising into
|
||||
the logo's blue at the top of the page in dark. */
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--ground), var(--bg);
|
||||
background-repeat: no-repeat;
|
||||
color: var(--text);
|
||||
font-family: var(--font-body);
|
||||
font-feature-settings: "ss01", "cv11", "tnum";
|
||||
font-size: var(--text-body);
|
||||
line-height: 1.5;
|
||||
font-feature-settings: "ss01", "cv11";
|
||||
max-width: 100%;
|
||||
overflow-x: clip;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.codewhale-mark-primary { fill: var(--mark-ink); }
|
||||
|
||||
main { flex: 1 0 auto; }
|
||||
main, header, footer, nav { position: relative; z-index: 1; }
|
||||
|
||||
.codewhale-mark-primary { fill: var(--mark-ink); }
|
||||
|
||||
/* ---------- type ---------- */
|
||||
/* One face, as in the GPUI app: Shannon Sans for headings, body and labels,
|
||||
with a little negative tracking on the big headings. Code uses the system
|
||||
monospace stack. No serif, no all-caps, no wide tracking. */
|
||||
/* One face: Shannon Sans for headings, body and labels, a little negative
|
||||
tracking on the big headings. Code uses the system monospace stack. No
|
||||
serif, no all-caps, no wide tracking. */
|
||||
.font-display { font-family: var(--font-display); font-weight: 600; }
|
||||
.font-cjk { font-family: var(--font-cjk); }
|
||||
|
||||
.font-mono { font-family: var(--font-mono); }
|
||||
|
||||
/* ---------- the ` · ` chain ---------- */
|
||||
/* The single most recognisable thing about the TUI — its header, empty state
|
||||
and footer all speak in dot chains — and the grammar the product's voice is
|
||||
carried by on this site, on both sides of the waterline. The separator is
|
||||
punctuation emitted by CSS, never copy: no locale translates it, and no
|
||||
string is ever concatenated around one. No all-caps and no wide tracking:
|
||||
the TUI header has neither, and both are what break Han. */
|
||||
.dotline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/* The separator hangs off the END of the preceding item, not the start of the
|
||||
next one. When a long chain wraps, a line ending in `·` reads as
|
||||
continuation; a line beginning with one reads as a bullet list. */
|
||||
.dotline > *:not(:last-child)::after {
|
||||
content: "·";
|
||||
margin-inline: 0.5em;
|
||||
color: var(--stage-dim);
|
||||
}
|
||||
|
||||
html[lang="zh"] .dotline,
|
||||
html[lang="ja"] .dotline,
|
||||
html[lang="ko"] .dotline {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.022em;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text);
|
||||
text-wrap: balance;
|
||||
word-break: keep-all;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
h3, h4 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
h1 { font-size: clamp(2.5rem, 5.6vw, 5rem); line-height: 1.02; word-break: keep-all; overflow-wrap: anywhere; }
|
||||
h2 { font-size: clamp(1.6rem, 2.9vw, 2.6rem); line-height: 1.1; word-break: keep-all; overflow-wrap: anywhere; }
|
||||
h3, h4 { font-size: 1.12rem; line-height: 1.25; font-weight: 600; }
|
||||
/* Han headings gain nothing from Latin tracking; set them flat and a
|
||||
touch heavier. */
|
||||
html[lang="zh"] h1, html[lang="ja"] h1, html[lang="ko"] h1,
|
||||
html[lang="zh"] h2, html[lang="ja"] h2, html[lang="ko"] h2 { letter-spacing: 0; font-weight: 600; }
|
||||
h1 { font-size: var(--text-display); line-height: 1.08; }
|
||||
h2 { font-size: clamp(1.375rem, 1.1rem + 1vw, 1.75rem); line-height: 1.2; }
|
||||
h3, h4 { font-size: var(--text-heading); line-height: 1.35; }
|
||||
|
||||
/* `overflow-wrap: anywhere` keeps long English headings safe, but can strand
|
||||
CJK punctuation — or a lone numeral — on a line of its own. Strict CJK
|
||||
line-breaking keeps punctuation attached while still allowing breaks between
|
||||
Han characters. ja and ko want the same rule: ja for the identical reason as
|
||||
zh, ko because it has real word boundaries and should break on them. */
|
||||
html[lang="zh"] h1,
|
||||
html[lang="zh"] h2,
|
||||
html[lang="zh"] h3,
|
||||
html[lang="ja"] h1,
|
||||
html[lang="ja"] h2,
|
||||
html[lang="ja"] h3,
|
||||
html[lang="ko"] h1,
|
||||
html[lang="ko"] h2,
|
||||
html[lang="ko"] h3 {
|
||||
p { text-wrap: pretty; }
|
||||
|
||||
/* Han headings gain nothing from Latin tracking; strict CJK line-breaking
|
||||
keeps punctuation attached while still breaking between characters. */
|
||||
html[lang="zh"] :is(h1, h2, h3),
|
||||
html[lang="ja"] :is(h1, h2, h3),
|
||||
html[lang="ko"] :is(h1, h2, h3) {
|
||||
letter-spacing: 0;
|
||||
line-break: strict;
|
||||
word-break: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
h1 .font-cjk {
|
||||
display: inline-block;
|
||||
font-size: clamp(1.6rem, 7.5vw, 2.2rem);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
/* Keep headings inside the viewport without inserting awkward English hyphens. */
|
||||
h1, h2 { hyphens: auto; }
|
||||
html[lang="en"] h1,
|
||||
html[lang="en"] h2 { hyphens: none; }
|
||||
}
|
||||
|
||||
/* ---------- structural primitives ---------- */
|
||||
/* One hairline everywhere: action blue at low alpha, visible on the deep
|
||||
field without ever becoming a border wall. */
|
||||
.hairline { border-color: var(--hairline); }
|
||||
.hairline-t { border-top: 1px solid var(--hairline); }
|
||||
.hairline-b { border-bottom: 1px solid var(--hairline); }
|
||||
.hairline-l { border-left: 1px solid var(--hairline); }
|
||||
.hairline-r { border-right: 1px solid var(--hairline); }
|
||||
.tabular { font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* The single site container: width + auto margins so every page's gutters
|
||||
line up with the nav. */
|
||||
.site-container {
|
||||
/* ---------- containers ---------- */
|
||||
.site-container,
|
||||
.portal-container,
|
||||
.product-container {
|
||||
width: var(--container);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* The single hero band shared by home, docs, and the community portals: a
|
||||
quiet, flat stage surface against the deep field. */
|
||||
/* ---------- the ` · ` chain (release-truth in the docs) ---------- */
|
||||
.dotline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
.dotline > *:not(:last-child)::after {
|
||||
content: "·";
|
||||
margin-inline: 0.5em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ---------- legacy structures still used by the docs ---------- */
|
||||
.hairline { border-color: var(--line); }
|
||||
.hairline-t { border-top: 1px solid var(--line); }
|
||||
.hairline-b { border-bottom: 1px solid var(--line); }
|
||||
.hairline-l { border-inline-start: 1px solid var(--line); }
|
||||
.hairline-r { border-inline-end: 1px solid var(--line); }
|
||||
|
||||
/* The docs band. New pages use .page-head. */
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
border-bottom: 1px solid var(--line);
|
||||
color: var(--text);
|
||||
padding-block: var(--hero-pad);
|
||||
}
|
||||
|
||||
/* The single section rhythm for per-route hero/section blocks. */
|
||||
.section {
|
||||
padding-block: var(--section-pad);
|
||||
}
|
||||
.section { padding-block: var(--section-pad); }
|
||||
|
||||
.col-rule > * + * {
|
||||
border-left: 1px solid var(--hairline);
|
||||
}
|
||||
/* Single-column phones: drop the column rules so cards stack flush. */
|
||||
@media (max-width: 767px) {
|
||||
.col-rule > * + * { border-left: 0; border-top: 1px solid var(--hairline); }
|
||||
}
|
||||
|
||||
/* eyebrow — the 12px muted label, sentence case */
|
||||
.eyebrow {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.75rem;
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 500;
|
||||
color: var(--ink-mute);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ---------- compact section marker ---------- */
|
||||
/* The seal keeps its Han glyph but speaks as Tideline chrome now: a bordered
|
||||
stage plate with muted ink, not an ink block on paper. */
|
||||
.seal {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--paper-deep);
|
||||
border: 1px solid var(--stage-line);
|
||||
color: var(--ink-mute);
|
||||
font-family: var(--font-cjk);
|
||||
font-weight: 700;
|
||||
width: 2.6rem;
|
||||
height: 2.6rem;
|
||||
border-radius: var(--radius-control);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
/* Action-blue variant for the few routes that need a stronger section marker. */
|
||||
.seal-indigo {
|
||||
color: var(--indigo);
|
||||
border-color: color-mix(in srgb, var(--indigo) 55%, transparent);
|
||||
}
|
||||
|
||||
/* Below the waterline the seal stays the same plate — the whole page is water
|
||||
now, so there is nothing left to invert. */
|
||||
.ocean-column .seal {
|
||||
background: var(--stage-elevated);
|
||||
color: var(--ink-mute);
|
||||
}
|
||||
|
||||
/* ---------- pills / status ---------- */
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.12rem 0.45rem;
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--paper-line);
|
||||
background: var(--paper-deep);
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
.pill-hot { background: var(--indigo); color: var(--paper); border-color: var(--indigo); }
|
||||
.pill-new { background: var(--paper-deep); color: var(--ink); border-color: var(--ink-mute); }
|
||||
.pill-jade { background: var(--jade); color: var(--paper); border-color: var(--jade); }
|
||||
.pill-ochre { background: var(--ochre); color: var(--paper); border-color: var(--ochre); }
|
||||
.pill-ghost { background: transparent; color: var(--ink-mute); border-color: var(--ink-mute); }
|
||||
|
||||
/* ---------- numbers ---------- */
|
||||
.tabular { font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* ---------- code blocks ---------- */
|
||||
/* Code is set into the floor: the recessed surface, no border, the page's
|
||||
own ink. Prompts and comments recede to muted; keys stay in the text ink
|
||||
at 500. Nothing here is decorative colour. */
|
||||
/* ---------- code ---------- */
|
||||
/* Code is set into the floor: the recessed surface, a hairline, the page's
|
||||
own ink. Prompts and comments recede; keys stay in the text ink. */
|
||||
pre.code-block {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
@@ -230,8 +151,8 @@ pre.code-block {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-mono);
|
||||
line-height: 1.55;
|
||||
border: 0;
|
||||
line-height: 1.6;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
@@ -243,7 +164,8 @@ pre.code-block .prompt,
|
||||
pre.code-block .comment { color: var(--muted); }
|
||||
pre.code-block .key { color: var(--text); font-weight: 500; }
|
||||
|
||||
code.inline {
|
||||
code.inline,
|
||||
.prose :not(pre) > code {
|
||||
background: var(--surface);
|
||||
padding: 0.0625rem 0.3125rem;
|
||||
font-family: var(--font-mono);
|
||||
@@ -252,28 +174,71 @@ code.inline {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
kbd,
|
||||
.kbd {
|
||||
display: inline-block;
|
||||
padding: 0 0.3125rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-caption);
|
||||
color: var(--muted);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
|
||||
/* ---------- search ---------- */
|
||||
/* The legacy search input takes the search-field recipe: a recessed
|
||||
field whose edge identifies it (--line-strong, 3:1), ringed on focus. No
|
||||
hover colour: only focus goes blue. */
|
||||
.search-input {
|
||||
width: 100%;
|
||||
min-height: 2.5rem;
|
||||
min-height: 2.75rem;
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-body);
|
||||
padding-block: var(--space-2);
|
||||
padding-inline: var(--space-3) var(--space-6);
|
||||
padding-inline: 2.5rem var(--space-3);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-control);
|
||||
transition: border-color var(--dur-state) var(--ease-out);
|
||||
transition: border-color var(--dur-state) var(--ease-out), box-shadow var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.search-input::placeholder { color: var(--muted); }
|
||||
.search-input:focus-visible { border-color: var(--ring); }
|
||||
.search-input:focus-visible { border-color: var(--ring); box-shadow: var(--glow-focus); }
|
||||
|
||||
mark.search-highlight {
|
||||
background: var(--selection);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ---------- a11y: skip link ---------- */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
inset-inline-start: 1rem;
|
||||
top: -3.5rem;
|
||||
z-index: 60;
|
||||
padding: 0.5rem 0.875rem;
|
||||
background: var(--text);
|
||||
color: var(--bg);
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
.skip-link:focus-visible { top: 0.5rem; }
|
||||
|
||||
.sr-only-focusable:not(:focus-visible) {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* The docs band's mark and label (docs/layout.tsx). */
|
||||
.portal-mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
margin-bottom: 1.25rem;
|
||||
color: var(--muted);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -1,138 +1,92 @@
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Changelog */
|
||||
/* Changelog — each release is a receipt */
|
||||
/* ------------------------------------------------------------------ */
|
||||
.changelog-facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.changelog-facts > div {
|
||||
padding: 1.1rem 1.25rem;
|
||||
border: 1px solid var(--stage-line);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--paper-card);
|
||||
}
|
||||
|
||||
.changelog-facts span {
|
||||
display: block;
|
||||
color: var(--ink-mute);
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.changelog-facts > div { min-width: 0; }
|
||||
|
||||
.changelog-facts strong {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
margin-top: 0.45rem;
|
||||
color: var(--ink);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.changelog-facts a {
|
||||
display: inline-block;
|
||||
margin-top: 0.55rem;
|
||||
color: var(--indigo);
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.changelog-facts a:hover { text-decoration: underline; text-underline-offset: 0.2rem; }
|
||||
|
||||
.changelog-facts [data-published="true"] strong { color: var(--jade); }
|
||||
.changelog-facts [data-candidate="unreleased"] strong { color: var(--ochre); }
|
||||
.changelog-list { display: grid; gap: var(--space-5); }
|
||||
|
||||
.changelog-release {
|
||||
padding-block: 2.25rem;
|
||||
border-top: 1px solid var(--hairline);
|
||||
padding: clamp(1.25rem, 3vw, 1.75rem);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
background: color-mix(in srgb, var(--panel) 72%, transparent);
|
||||
}
|
||||
|
||||
.changelog-release-head {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem 1.5rem;
|
||||
gap: var(--space-2) var(--space-5);
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.changelog-release-head h2 {
|
||||
font-size: clamp(1.4rem, 2.6vw, 1.9rem);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
font-size: var(--text-title);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.changelog-release-head h2 svg { width: 1.25rem; height: 1.25rem; color: var(--accent); }
|
||||
|
||||
.changelog-release-head .dotline {
|
||||
color: var(--ink-mute);
|
||||
.changelog-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: var(--space-1) var(--space-4);
|
||||
color: var(--muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.changelog-release-head .dotline a { color: var(--indigo); }
|
||||
.changelog-release-head .dotline a:hover { text-decoration: underline; text-underline-offset: 0.2rem; }
|
||||
.changelog-links a { color: var(--accent); }
|
||||
.changelog-links a:hover { text-decoration: underline; text-underline-offset: 0.2em; }
|
||||
|
||||
.changelog-unreleased-note {
|
||||
max-width: 44rem;
|
||||
margin-top: 0.6rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.86rem;
|
||||
margin-top: var(--space-2);
|
||||
color: var(--muted);
|
||||
font-size: var(--text-ui);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.changelog-sections {
|
||||
display: grid;
|
||||
gap: 1.35rem;
|
||||
margin-top: 1.4rem;
|
||||
}
|
||||
.changelog-sections { display: grid; gap: var(--space-5); margin-top: var(--space-5); }
|
||||
|
||||
.changelog-sections h3 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
gap: var(--space-3);
|
||||
color: var(--muted);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 600;
|
||||
color: var(--ink-mute);
|
||||
}
|
||||
|
||||
.changelog-sections h3 a {
|
||||
color: var(--ink-mute);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.changelog-sections h3 a:hover {
|
||||
color: var(--indigo);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2rem;
|
||||
}
|
||||
|
||||
.changelog-sections ul {
|
||||
margin-top: 0.55rem;
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
.changelog-sections h3 a { color: var(--muted); font-weight: 500; }
|
||||
.changelog-sections h3 a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 0.2em; }
|
||||
|
||||
.changelog-sections ul { display: grid; margin-top: var(--space-2); }
|
||||
.changelog-sections li {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
padding: 0.6rem 0;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.86rem;
|
||||
padding: var(--space-2) 0 var(--space-2) var(--space-5);
|
||||
color: var(--text);
|
||||
font-size: var(--text-ui);
|
||||
line-height: 1.6;
|
||||
/* Release notes quote commands, env names, and URLs verbatim; a long
|
||||
unbroken token must wrap inside the column, never widen the page. */
|
||||
/* Release notes quote commands, env names and URLs verbatim; a long
|
||||
unbroken token wraps inside the column, never widens the page. */
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.changelog-sections li + li { border-top: 1px solid var(--line); }
|
||||
.changelog-sections li::before {
|
||||
content: "·";
|
||||
margin-right: 0.6rem;
|
||||
color: var(--stage-dim);
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline-start: 0.375rem;
|
||||
top: 1.05em;
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--line-strong);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.changelog-facts { grid-template-columns: 1fr; }
|
||||
}
|
||||
.changelog-facts { max-width: 44rem; }
|
||||
.changelog-facts dd { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-4); }
|
||||
.changelog-facts dd > .status:first-child { display: inline-flex; margin: 0; font-size: var(--text-ui); }
|
||||
.changelog-links a { display: inline-flex; align-items: center; gap: 0.25rem; }
|
||||
@@ -1,38 +1,40 @@
|
||||
/* The verified Markdown guide owns the content; this surface owns its layout. */
|
||||
/* The verified Markdown guide owns the content; this surface owns its
|
||||
layout, on the shared prose measure. */
|
||||
.install-guide {
|
||||
max-width: 76ch;
|
||||
width: var(--container);
|
||||
max-width: var(--measure-prose);
|
||||
margin-inline: auto;
|
||||
padding: 3rem 1rem 5rem;
|
||||
color: var(--ink-soft);
|
||||
line-height: 1.7;
|
||||
padding-bottom: var(--section-gap);
|
||||
color: var(--text);
|
||||
font-size: var(--text-prose);
|
||||
line-height: var(--leading-prose);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.install-guide :is(h1, h2, h3, h4) {
|
||||
color: var(--ink);
|
||||
line-height: 1.3;
|
||||
scroll-margin-top: 6rem;
|
||||
}
|
||||
.install-guide h1 { font-size: clamp(1.8rem, 4vw, 2.5rem); margin-bottom: 1.5rem; }
|
||||
.install-guide h2 { font-size: 1.5rem; margin-top: 3rem; margin-bottom: 1rem; }
|
||||
.install-guide h3 { font-size: 1.2rem; margin-top: 2rem; margin-bottom: 0.75rem; }
|
||||
.install-guide h4 { font-size: 1rem; margin-top: 1.5rem; margin-bottom: 0.75rem; }
|
||||
.install-guide :is(p, ul, ol, blockquote, .install-guide-table, pre) { margin-block: 1rem; }
|
||||
.install-guide > .relative { margin-block: 1.5rem; }
|
||||
.install-guide :is(h1, h2, h3, h4) { color: var(--text); line-height: 1.25; scroll-margin-top: 6rem; }
|
||||
.install-guide h1 { font-size: var(--text-display); margin-bottom: var(--space-5); }
|
||||
.install-guide h2 { font-size: var(--text-title); margin-top: var(--space-7); margin-bottom: var(--space-3); }
|
||||
.install-guide h3 { font-size: var(--text-heading); margin-top: var(--space-6); margin-bottom: var(--space-2); }
|
||||
.install-guide h4 { font-size: var(--text-body); margin-top: var(--space-5); margin-bottom: var(--space-2); }
|
||||
.install-guide :is(p, ul, ol, blockquote, .install-guide-table, pre) { margin-block: var(--space-4); }
|
||||
.install-guide p,
|
||||
.install-guide li { color: var(--text); max-width: var(--measure-text); }
|
||||
.install-guide > .code { margin-block: var(--space-5); }
|
||||
.install-guide :is(ul, ol) { padding-inline-start: 1.5rem; }
|
||||
.install-guide ul { list-style: disc; }
|
||||
.install-guide ol { list-style: decimal; }
|
||||
.install-guide li { margin-block: 0.35rem; }
|
||||
.install-guide a { color: var(--indigo); text-decoration: underline; text-underline-offset: 0.2em; }
|
||||
.install-guide li { margin-block: var(--space-1); }
|
||||
.install-guide ::marker { color: var(--muted); }
|
||||
.install-guide a { color: var(--accent); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent); text-underline-offset: 0.2em; }
|
||||
.install-guide a:hover { text-decoration-color: var(--accent); }
|
||||
.install-guide a[id] { scroll-margin-top: 6rem; }
|
||||
.install-guide strong { color: var(--ink); font-weight: 600; }
|
||||
.install-guide blockquote { padding-inline-start: 1rem; border-inline-start: 2px solid var(--indigo); }
|
||||
.install-guide hr { margin-block: 2.5rem; border-color: var(--line); }
|
||||
.install-guide code { font-family: var(--font-mono); font-size: 0.88em; }
|
||||
.install-guide strong { font-weight: 600; }
|
||||
.install-guide blockquote { padding-inline-start: var(--space-4); border-inline-start: 2px solid var(--line-strong); color: var(--muted); }
|
||||
.install-guide hr { margin-block: var(--space-7); border: 0; border-top: 1px solid var(--line); }
|
||||
.install-guide :not(pre) > code { font-family: var(--font-mono); font-size: 0.875em; background: var(--surface); padding: 0.0625rem 0.3125rem; border-radius: var(--radius-control); }
|
||||
.install-guide pre { max-width: 100%; overflow-x: auto; white-space: pre; overflow-wrap: normal; }
|
||||
.install-guide pre:not(.code-block) { padding: 1rem; background: var(--paper-card); border: 1px solid var(--line); border-radius: var(--radius-surface); }
|
||||
.install-guide pre:not(.code-block) { padding: var(--space-4); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-surface); font-size: var(--text-mono); }
|
||||
.install-guide-table { max-width: 100%; overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius-surface); }
|
||||
.install-guide table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
||||
.install-guide :is(th, td) { padding: 0.75rem; min-width: 8rem; text-align: start; vertical-align: top; border-bottom: 1px solid var(--line); }
|
||||
.install-guide th { color: var(--ink); background: var(--paper-card); font-weight: 600; }
|
||||
.install-guide table { width: 100%; border-collapse: collapse; font-size: var(--text-ui); }
|
||||
.install-guide :is(th, td) { padding: var(--space-3); min-width: 8rem; text-align: start; vertical-align: top; border-bottom: 1px solid var(--line); }
|
||||
.install-guide th { background: var(--panel); font-weight: 600; }
|
||||
.install-guide tr:last-child td { border-bottom: 0; }
|
||||
.install-guide-source { font-size: 0.85rem; margin-bottom: 2rem; }
|
||||
@@ -1,55 +1,6 @@
|
||||
/* ---------- /product: the availability table on paper ---------- */
|
||||
/* The same list the homepage sets in the water, re-inked for the sheet. */
|
||||
.product-availability-paper,
|
||||
.product-availability-paper > div {
|
||||
border-color: var(--hairline);
|
||||
}
|
||||
.product-availability-paper dt,
|
||||
.product-availability-paper dd strong {
|
||||
color: var(--ink);
|
||||
}
|
||||
.product-availability-paper dd {
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
/* ---------- usage counting: the consent sheet and its footer control ---------- */
|
||||
/* A quiet sheet at the foot of the viewport, on paper, with the site's own
|
||||
inks. It is not a modal: the page stays usable, and the choice is one
|
||||
click either way. */
|
||||
.usage-counting {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.usage-counting-status {
|
||||
color: var(--ink);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.usage-counting-elsewhere {
|
||||
color: var(--ink-mute);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.usage-counting-button {
|
||||
display: inline-flex;
|
||||
justify-self: start;
|
||||
min-height: 2.75rem;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1.1rem;
|
||||
border: 1px solid var(--indigo);
|
||||
border-radius: var(--radius-control);
|
||||
color: var(--indigo);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--dur-state) var(--ease-spring), color var(--dur-state) var(--ease-spring);
|
||||
}
|
||||
|
||||
.usage-counting-button:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Docs overrides (owned with the docs pages; kept last in cascade) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* ---------- docs: the reading sheet ---------- */
|
||||
/* Read mode: comprehension first. The article gets a book measure, the
|
||||
@@ -184,19 +135,3 @@
|
||||
font-weight: 600;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
/* Keep the terminal centered and compact beneath the introductory copy. */
|
||||
.folio-hero-grid { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto auto; row-gap: 2rem; }
|
||||
.folio-hero-copy { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: clamp(2rem, 5vw, 5rem); max-width: none; grid-column: 1; grid-row: 1; padding-bottom: 0; }
|
||||
.folio-hero-copy h1 { grid-column: 1; grid-row: 1 / 3; max-width: none; font-size: clamp(2.75rem, 5vw, 4.5rem); line-height: 1.06; text-wrap: balance; }
|
||||
.folio-hero-copy .folio-lede { grid-column: 2; grid-row: 1; margin-top: 0; align-self: end; }
|
||||
.folio-hero-copy .folio-actions { grid-column: 2; grid-row: 2; align-self: start; margin-top: 1.25rem; }
|
||||
.folio-hero-copy .hero-install { grid-column: 2; grid-row: 3; }
|
||||
.folio-shot { grid-column: 1; grid-row: 2; width: min(100%, 56rem); justify-self: center; margin-top: 0; }
|
||||
.folio-chapter { grid-column: 1; grid-row: 3; justify-self: start; }
|
||||
.product-install-grid > div, .product-composer, .product-composer > div { min-width: 0; }
|
||||
@media (max-width: 760px) {
|
||||
.folio-hero-copy { display: block; }
|
||||
.folio-hero-copy .folio-lede { margin-top: 1.25rem; }
|
||||
.folio-shot { width: 100%; }
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Inner pages — layouts the primitives do not cover */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* A checklist row: a quiet check mark and the sentence. */
|
||||
.check-row { display: flex; align-items: flex-start; gap: var(--space-3); font-size: var(--text-ui); line-height: 1.6; }
|
||||
.check-row .icon { margin-top: 0.2em; color: var(--live); }
|
||||
|
||||
/* The models table's reasoning label. */
|
||||
.models-reasoning {
|
||||
display: inline-block;
|
||||
margin-inline-start: var(--space-2);
|
||||
padding: 0 0.375rem;
|
||||
color: var(--muted);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
|
||||
/* /install: the quick plate above the verified guide. */
|
||||
.install-head {
|
||||
width: var(--container);
|
||||
max-width: var(--measure-prose);
|
||||
margin-inline: auto;
|
||||
padding-block: clamp(2rem, 5vw, 3.5rem) var(--space-6);
|
||||
}
|
||||
.install-head-inner { display: flex; align-items: flex-start; gap: var(--space-5); }
|
||||
.install-head-pose { --pose-size: 5rem; }
|
||||
.install-head-text { display: grid; gap: var(--space-3); flex: 1; min-width: 0; }
|
||||
.install-head-source { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-ui); }
|
||||
.install-head-source .icon { color: var(--live); }
|
||||
.install-head-note { color: var(--muted); font-size: var(--text-ui); }
|
||||
@media (max-width: 640px) {
|
||||
.install-head-inner { flex-direction: column; }
|
||||
.install-head-pose { --pose-size: 3.5rem; }
|
||||
}
|
||||
|
||||
/* /computer-use: the download row. */
|
||||
.cu-download { display: grid; gap: var(--space-3); }
|
||||
|
||||
/* /roadmap: one group per status. */
|
||||
.roadmap-tracks { display: grid; gap: var(--space-6); }
|
||||
.roadmap-track-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-3); }
|
||||
.roadmap-track-head h3 .status { font-size: var(--text-heading); font-weight: 600; }
|
||||
.roadmap-track-head h3 .status-dot { width: 0.625rem; height: 0.625rem; }
|
||||
.roadmap-item { display: grid; gap: var(--space-1); }
|
||||
.roadmap-item-title { font-size: var(--text-ui); font-weight: 500; }
|
||||
.roadmap-item-note { color: var(--muted); font-size: var(--text-ui); line-height: 1.6; }
|
||||
|
||||
/* /faq */
|
||||
.faq-search { margin-bottom: var(--space-5); }
|
||||
.faq-search-count { min-height: 1.25rem; margin-top: var(--space-2); color: var(--muted); font-size: var(--text-caption); }
|
||||
.faq-index { flex: none; width: 1.75rem; color: var(--muted); font-size: var(--text-caption); font-weight: 500; }
|
||||
.faq-question { flex: 1; min-width: 0; }
|
||||
.faq-answer { max-width: none; padding-inline-start: calc(var(--space-4) + 1.75rem + var(--space-3)); font-size: var(--text-body); }
|
||||
.faq-sources { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-3); color: var(--muted); font-size: var(--text-caption); }
|
||||
.faq-more { margin-top: var(--space-6); }
|
||||
@media (max-width: 640px) {
|
||||
.faq-answer { padding-inline-start: var(--space-4); }
|
||||
}
|
||||
|
||||
/* /constitution: excerpts of a real session's reasoning. */
|
||||
.trace {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
.trace-head { display: grid; gap: var(--space-2); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--line); }
|
||||
.trace-context { color: var(--muted); font-size: var(--text-caption); line-height: 1.5; }
|
||||
.trace-body {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: var(--space-4);
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-mono);
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.trace-cites { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: 0 var(--space-4) var(--space-3); }
|
||||
.trace-decision {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
color: var(--text);
|
||||
font-size: var(--text-ui);
|
||||
line-height: 1.55;
|
||||
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.trace-decision .icon { margin-top: 0.2em; color: var(--accent); }
|
||||
|
||||
/* /runtime: maintainer details behind a disclosure. */
|
||||
.runtime-details { display: grid; gap: var(--space-3); font-size: var(--text-ui); }
|
||||
.runtime-details dt { font-weight: 500; }
|
||||
.runtime-details dd { color: var(--muted); overflow-wrap: anywhere; }
|
||||
|
||||
/* /community: contributor handles. */
|
||||
.credit-list { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
|
||||
.credit-list a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 1.75rem;
|
||||
padding-inline: var(--space-2);
|
||||
color: var(--text);
|
||||
font-size: var(--text-ui);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-control);
|
||||
transition: background-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.credit-list a:hover { background: var(--hover); }
|
||||
|
||||
/* /digest */
|
||||
.digest-list { display: grid; gap: var(--space-5); }
|
||||
.digest-head { display: grid; justify-items: start; gap: var(--space-2); }
|
||||
|
||||
/* /feed */
|
||||
.feed-columns { align-items: start; }
|
||||
.feed-column {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
background: color-mix(in srgb, var(--panel) 72%, transparent);
|
||||
overflow: hidden;
|
||||
}
|
||||
.feed-column-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.feed-column-head h2 { font-size: var(--text-heading); }
|
||||
.feed-items > li + li { border-top: 1px solid var(--line); }
|
||||
.feed-empty { padding: var(--space-4); }
|
||||
.feed-card { display: grid; gap: var(--space-2); padding: var(--space-3) var(--space-4); }
|
||||
.feed-card-meta,
|
||||
.feed-card-foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); color: var(--muted); font-size: var(--text-caption); }
|
||||
.feed-card-time,
|
||||
.feed-card-author { margin-inline-start: auto; }
|
||||
.feed-card-title { font-size: var(--text-ui); font-weight: 500; line-height: 1.45; overflow-wrap: anywhere; }
|
||||
.feed-card-title a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 0.2em; }
|
||||
.feed-action { border: 1px solid var(--line); border-radius: var(--radius-surface); background: color-mix(in srgb, var(--panel) 72%, transparent); }
|
||||
|
||||
/* Account and utility pages: one centred column. */
|
||||
.account-entry {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: var(--space-4);
|
||||
width: var(--container);
|
||||
max-width: 30rem;
|
||||
margin-inline: auto;
|
||||
padding-block: clamp(3rem, 8vw, 5rem);
|
||||
text-align: center;
|
||||
}
|
||||
.account-entry .page-title { max-width: none; }
|
||||
.account-entry .page-lede { max-width: 28rem; }
|
||||
.account-entry-pose { --pose-size: 6.5rem; }
|
||||
.account-entry .actions { justify-content: center; }
|
||||
.account-form { display: grid; gap: var(--space-4); width: 100%; text-align: start; }
|
||||
|
||||
/* /admin */
|
||||
.admin-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
margin-block: var(--space-6);
|
||||
padding-bottom: var(--space-4);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
/* /legal */
|
||||
.legal-tabs { margin-bottom: var(--space-6); }
|
||||
.legal-doc > section > h2 { margin-top: var(--space-6); }
|
||||
.legal-doc .status-line { margin-top: var(--space-7); }
|
||||
@@ -1,620 +0,0 @@
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Documentation-led public home */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
.portal-home {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.portal-container {
|
||||
width: var(--container);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.portal-current {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.22;
|
||||
pointer-events: none;
|
||||
background-image: repeating-radial-gradient(
|
||||
ellipse 82% 44% at 24% -22%,
|
||||
transparent 0,
|
||||
transparent 2.7rem,
|
||||
rgb(var(--c-stage-soft) / 0.18) 2.76rem,
|
||||
transparent 2.83rem
|
||||
);
|
||||
mask-image: linear-gradient(to bottom, transparent, black 24%, black 72%, transparent);
|
||||
}
|
||||
|
||||
.portal-hero-grid {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.12fr) minmax(20rem, 0.88fr);
|
||||
gap: clamp(2.5rem, 6vw, 5rem);
|
||||
align-items: center;
|
||||
padding-block: clamp(3.25rem, 6vw, 4.75rem);
|
||||
}
|
||||
|
||||
.portal-hero-copy {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.portal-mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
margin-bottom: 1.25rem;
|
||||
color: var(--ink-mute);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.portal-hero h1 {
|
||||
max-width: 41rem;
|
||||
font-size: clamp(2.3rem, 4.8vw, 3.75rem);
|
||||
line-height: 1.01;
|
||||
}
|
||||
|
||||
.portal-lede {
|
||||
max-width: 40rem;
|
||||
margin-top: 1.35rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: clamp(1rem, 1.7vw, 1.18rem);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.portal-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.7rem;
|
||||
margin-top: 1.7rem;
|
||||
}
|
||||
|
||||
.portal-button {
|
||||
display: inline-flex;
|
||||
min-height: 2.75rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.65rem 1rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-control);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
transition: background-color var(--dur-state) var(--ease-spring), border-color var(--dur-state) var(--ease-spring), color var(--dur-state) var(--ease-spring);
|
||||
}
|
||||
|
||||
.portal-button-primary {
|
||||
border-color: var(--indigo);
|
||||
background: var(--indigo);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.portal-button-primary:hover {
|
||||
background: var(--indigo-deep);
|
||||
border-color: var(--indigo-deep);
|
||||
}
|
||||
|
||||
.portal-button-secondary {
|
||||
border-color: var(--paper-edge);
|
||||
background: var(--paper-deep);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.portal-button-secondary:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
|
||||
.portal-meta {
|
||||
max-width: 38rem;
|
||||
margin-top: 0.9rem;
|
||||
color: var(--ink-mute);
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.portal-quickstart {
|
||||
padding: clamp(1.35rem, 3vw, 2rem);
|
||||
border: 1px solid rgb(var(--c-stage-soft) / 0.2);
|
||||
border-radius: var(--radius-surface);
|
||||
background: var(--ocean-deep);
|
||||
box-shadow: 0 1.25rem 3rem rgba(0, 0, 0, 0.45);
|
||||
color: var(--stage-soft);
|
||||
}
|
||||
|
||||
.portal-quickstart > span {
|
||||
color: var(--stage-muted);
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.portal-quickstart h2 {
|
||||
margin-top: 0.65rem;
|
||||
color: var(--stage-text);
|
||||
font-size: clamp(1.2rem, 2vw, 1.55rem);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.portal-quickstart p {
|
||||
margin-top: 0.7rem;
|
||||
color: rgb(var(--c-stage-soft) / 0.7);
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.portal-quickstart pre.code-block {
|
||||
margin-top: 1.15rem;
|
||||
border-color: rgb(var(--c-stage-soft) / 0.2);
|
||||
background: rgb(var(--c-stage-deep) / 0.6);
|
||||
}
|
||||
|
||||
.portal-quickstart > a {
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
color: var(--ocean-current);
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.portal-quickstart > a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.25rem;
|
||||
}
|
||||
|
||||
.portal-section {
|
||||
padding-block: clamp(3.25rem, 6vw, 4.75rem);
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.portal-section-muted {
|
||||
background: var(--paper-deep);
|
||||
}
|
||||
|
||||
.legal-doc {
|
||||
width: var(--container);
|
||||
margin-inline: auto;
|
||||
padding-block: clamp(3.25rem, 6vw, 4.75rem);
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.legal-doc-kicker {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--ink-muted, color-mix(in srgb, var(--ink) 62%, var(--paper)));
|
||||
}
|
||||
|
||||
.legal-doc h1 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.85rem, 3.6vw, 2.7rem);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.legal-doc-updated,
|
||||
.legal-doc p,
|
||||
.legal-doc section p {
|
||||
margin: 0 0 1.15rem;
|
||||
font-size: var(--text-prose);
|
||||
line-height: var(--leading-prose);
|
||||
}
|
||||
|
||||
.legal-doc section h2 {
|
||||
margin: 1.75rem 0 0.5rem;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.legal-doc-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem 1.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.legal-doc-nav a {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2rem;
|
||||
}
|
||||
|
||||
.public-account-entry {
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.public-account-mark {
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 0 1.15rem;
|
||||
border-radius: var(--radius-sheet);
|
||||
}
|
||||
|
||||
.portal-section-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(14rem, 0.62fr) minmax(0, 1.38fr);
|
||||
gap: clamp(2.5rem, 7vw, 6.5rem);
|
||||
}
|
||||
|
||||
.portal-section-copy > span,
|
||||
.portal-docs-heading span,
|
||||
.portal-community-grid > div:first-child > span {
|
||||
color: var(--ink-mute);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.portal-section-copy h2,
|
||||
.portal-docs-heading h2,
|
||||
.portal-community h2 {
|
||||
margin-top: 0.65rem;
|
||||
font-size: clamp(1.55rem, 3vw, 2.4rem);
|
||||
}
|
||||
|
||||
.portal-section-copy p {
|
||||
margin-top: 0.9rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.portal-topic-list {
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.portal-topic-list > a,
|
||||
.portal-topic-list > div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(8rem, 0.42fr) minmax(0, 1fr) auto;
|
||||
gap: 1.25rem;
|
||||
align-items: start;
|
||||
padding-block: 1.15rem;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
transition: background-color var(--dur-state) var(--ease-spring), color var(--dur-state) var(--ease-spring);
|
||||
}
|
||||
|
||||
.portal-topic-list > a:hover {
|
||||
color: var(--indigo);
|
||||
background: var(--hover);
|
||||
}
|
||||
|
||||
.portal-topic-list strong {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.portal-topic-list span {
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.portal-topic-list span:last-child {
|
||||
color: var(--ink-mute);
|
||||
}
|
||||
|
||||
.portal-docs-heading {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.portal-docs-heading a {
|
||||
flex: none;
|
||||
color: var(--indigo-deep);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.portal-docs-heading a:hover,
|
||||
.portal-community-links a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.25rem;
|
||||
}
|
||||
|
||||
/* Sortable model table — quiet column-header buttons; the active column
|
||||
carries a direction mark instead of a color change. */
|
||||
.models-sort {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.35rem;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--ink-mute);
|
||||
font: inherit;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.models-sort:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.models-sort.is-active {
|
||||
color: var(--indigo-deep);
|
||||
}
|
||||
|
||||
.models-sort-mark {
|
||||
font-size: 0.72em;
|
||||
color: var(--ink-mute);
|
||||
}
|
||||
|
||||
.models-sort.is-active .models-sort-mark {
|
||||
color: var(--indigo-deep);
|
||||
}
|
||||
|
||||
.models-reasoning {
|
||||
display: inline-block;
|
||||
margin-left: 0.5rem;
|
||||
padding: 0.05rem 0.4rem;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: var(--radius-pill);
|
||||
color: var(--ink-mute);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.04em;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.portal-doc-groups {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: clamp(2.5rem, 6vw, 5rem);
|
||||
}
|
||||
|
||||
.portal-doc-groups h3 {
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.portal-doc-groups section > p {
|
||||
min-height: 4.5rem;
|
||||
margin-top: 0.55rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.portal-doc-groups .portal-topic-list {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.portal-doc-groups .portal-topic-list > a {
|
||||
grid-template-columns: minmax(7rem, 0.42fr) minmax(0, 1fr) auto;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.portal-community {
|
||||
padding-block: clamp(3.25rem, 6vw, 4.75rem);
|
||||
background:
|
||||
radial-gradient(circle at 84% -30%, rgb(var(--c-primary-dark) / 0.1), transparent 30rem),
|
||||
var(--ocean-deep);
|
||||
color: var(--stage-soft);
|
||||
}
|
||||
|
||||
.portal-community-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(16rem, 0.85fr) minmax(0, 1.15fr);
|
||||
gap: clamp(2.5rem, 7vw, 7rem);
|
||||
}
|
||||
|
||||
.portal-community-grid > div:first-child > span {
|
||||
color: var(--stage-muted);
|
||||
}
|
||||
|
||||
.portal-community h2 {
|
||||
color: var(--stage-text);
|
||||
}
|
||||
|
||||
.portal-community p {
|
||||
color: rgb(var(--c-stage-soft) / 0.76);
|
||||
font-size: 0.96rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.portal-community-links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.8rem 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.25rem;
|
||||
border-top: 1px solid rgb(var(--c-stage-soft) / 0.18);
|
||||
}
|
||||
|
||||
.portal-community-links a {
|
||||
color: var(--ocean-current);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.contribute-path-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.contribute-path-grid article {
|
||||
padding: 1.5rem 1.5rem 1.5rem 0;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.contribute-path-grid article:nth-child(even) {
|
||||
padding-inline: 1.5rem 0;
|
||||
border-left: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.contribute-path-grid h3 {
|
||||
font-size: 1rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.contribute-path-grid p {
|
||||
margin-top: 0.55rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.contribute-path-grid a,
|
||||
.contribute-steps a {
|
||||
display: inline-block;
|
||||
margin-top: 0.9rem;
|
||||
color: var(--indigo-deep);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.contribute-path-grid a:hover,
|
||||
.contribute-steps a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.22rem;
|
||||
}
|
||||
|
||||
.contribute-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.contribute-steps li {
|
||||
display: grid;
|
||||
grid-template-columns: 2.25rem minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 1.5rem 1.5rem 1.5rem 0;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.contribute-steps li:nth-child(even) {
|
||||
padding-inline: 1.5rem 0;
|
||||
border-left: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.contribute-steps li > span {
|
||||
color: var(--indigo-deep);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.contribute-steps h3 {
|
||||
font-size: 1rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.contribute-steps p {
|
||||
margin-top: 0.5rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.contribute-review-list {
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.contribute-review-list li {
|
||||
padding-block: 1rem;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.contribute-dev-loop {
|
||||
padding-block: clamp(3.25rem, 6vw, 4.75rem);
|
||||
background: var(--ocean-deep);
|
||||
}
|
||||
|
||||
.contribute-dev-loop .portal-section-copy > span {
|
||||
color: var(--stage-muted);
|
||||
}
|
||||
|
||||
.contribute-dev-loop .portal-section-copy h2 {
|
||||
color: var(--stage-text);
|
||||
}
|
||||
|
||||
.contribute-dev-loop .portal-section-copy p {
|
||||
color: rgb(var(--c-stage-soft) / 0.76);
|
||||
}
|
||||
|
||||
.contribute-dev-loop pre.code-block {
|
||||
border-color: rgb(var(--c-stage-soft) / 0.2);
|
||||
background: rgb(var(--c-stage-deep) / 0.62);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.portal-hero-grid,
|
||||
.portal-section-grid,
|
||||
.portal-community-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.portal-hero-copy {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.portal-quickstart {
|
||||
max-width: 42rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.portal-doc-groups {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.portal-doc-groups section > p {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.contribute-path-grid,
|
||||
.contribute-steps {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.contribute-path-grid article,
|
||||
.contribute-path-grid article:nth-child(even),
|
||||
.contribute-steps li,
|
||||
.contribute-steps li:nth-child(even) {
|
||||
padding-inline: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.portal-hero-grid {
|
||||
gap: 2.25rem;
|
||||
padding-block: 2.75rem;
|
||||
}
|
||||
|
||||
.portal-actions,
|
||||
.portal-docs-heading {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.portal-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.portal-topic-list > a,
|
||||
.portal-topic-list > div,
|
||||
.portal-doc-groups .portal-topic-list > a,
|
||||
.portal-doc-groups .portal-topic-list > div {
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0.45rem 1rem;
|
||||
}
|
||||
|
||||
.portal-topic-list > a span:nth-child(2),
|
||||
.portal-topic-list > div span:nth-child(2) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.portal-topic-list > a span:last-child,
|
||||
.portal-topic-list > div span:last-child {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.portal-community-links {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,823 @@
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Primitives — the components every page composes */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/*
|
||||
* Class names are the API. Pages never define a colour, radius, shadow or
|
||||
* duration; they compose these. Recipes: buttons, page header, sections,
|
||||
* group cards, definition rows, directory rows, status (a mark and a
|
||||
* word), segmented choice, tabs, code with copy, plate, figure, callout,
|
||||
* disclosure rows, empty / loading / error states, data tables, fields,
|
||||
* prose, steps, the whale's poses, and the horizon.
|
||||
*/
|
||||
|
||||
/* ---------- icons ---------- */
|
||||
/* components/icon.tsx: 24-unit, 1.7 round stroke, currentColor. */
|
||||
.icon { display: inline-block; width: 1rem; height: 1rem; flex: none; vertical-align: -0.125em; }
|
||||
|
||||
/* ---------- buttons ---------- */
|
||||
/* One primary per view: the logo gradient with white text. Everything else
|
||||
is secondary (raised), outline, ghost or danger (never filled). */
|
||||
.btn,
|
||||
.portal-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
min-height: 2.25rem;
|
||||
padding-inline: 0.875rem;
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-control);
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition-property: background-color, border-color, color, box-shadow, opacity;
|
||||
transition-duration: var(--dur-state);
|
||||
transition-timing-function: var(--ease-out);
|
||||
}
|
||||
.btn svg,
|
||||
.portal-button svg { width: 1rem; height: 1rem; flex: none; }
|
||||
.btn:active,
|
||||
.portal-button:active { opacity: 0.8; }
|
||||
.btn:disabled,
|
||||
.btn[aria-disabled="true"],
|
||||
.portal-button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn[aria-busy="true"],
|
||||
.portal-button[aria-busy="true"] { cursor: progress; }
|
||||
|
||||
.btn-primary,
|
||||
.portal-button-primary {
|
||||
background: var(--brand-fill);
|
||||
color: var(--on-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-primary:hover,
|
||||
.portal-button-primary:hover {
|
||||
background: var(--brand-fill-hover);
|
||||
color: var(--on-brand);
|
||||
box-shadow: var(--glow-focus);
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.portal-button-secondary {
|
||||
background: var(--panel);
|
||||
border-color: var(--line);
|
||||
color: var(--text);
|
||||
}
|
||||
.btn-secondary:hover,
|
||||
.portal-button-secondary:hover { background: var(--hover); }
|
||||
|
||||
.btn-outline { border-color: var(--line-strong); }
|
||||
.btn-outline:hover { background: var(--hover); }
|
||||
|
||||
.btn-ghost { color: var(--text); }
|
||||
.btn-ghost:hover { background: var(--hover); }
|
||||
|
||||
.btn-danger { border-color: var(--line-strong); color: var(--danger); }
|
||||
.btn-danger:hover { background: var(--hover); }
|
||||
|
||||
.btn-sm { min-height: 1.75rem; padding-inline: 0.5rem; font-size: 0.8125rem; }
|
||||
.btn-lg { min-height: 2.75rem; padding-inline: 1.125rem; font-size: 0.9375rem; }
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.btn,
|
||||
.portal-button { min-height: 2.75rem; }
|
||||
}
|
||||
|
||||
.actions,
|
||||
.portal-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
/* ---------- the whale's poses ---------- */
|
||||
/* One character everywhere: the v2 whale's still poses (public/whale). A
|
||||
pose carries meaning — rest, listen, think, read, search, write, run,
|
||||
browse, talk, computer, connect, needs you, done, hmm, sleep, busy, and
|
||||
the pod for parallel agents. Never decorative filler. */
|
||||
.whale-pose {
|
||||
display: block;
|
||||
flex: none;
|
||||
width: var(--pose-size, 4rem);
|
||||
height: var(--pose-size, 4rem);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ---------- page header ---------- */
|
||||
/* What the reader knows (the title), why they are here (one lede), what to
|
||||
do next (the actions). The whale sits beside it, doing the page's job. */
|
||||
.page-head {
|
||||
width: var(--container);
|
||||
margin-inline: auto;
|
||||
padding-block: clamp(2.5rem, 6vw, 4.5rem) clamp(1.5rem, 3vw, 2.5rem);
|
||||
}
|
||||
|
||||
.page-head-inner {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: end;
|
||||
gap: var(--space-5) clamp(1.5rem, 5vw, 4rem);
|
||||
}
|
||||
|
||||
.page-head-text { display: grid; gap: var(--space-4); min-width: 0; }
|
||||
|
||||
.page-kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--muted);
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 500;
|
||||
}
|
||||
.page-kicker svg { width: 1rem; height: 1rem; color: var(--accent); }
|
||||
|
||||
.page-title { max-width: 22ch; font-size: var(--text-display); line-height: 1.08; }
|
||||
|
||||
.page-lede {
|
||||
max-width: 44rem;
|
||||
color: var(--muted);
|
||||
font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.page-meta { color: var(--muted); font-size: var(--text-ui); }
|
||||
|
||||
.page-head .actions { margin-top: var(--space-2); }
|
||||
|
||||
.page-head-pose { --pose-size: clamp(4.5rem, 10vw, 7.5rem); align-self: center; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.page-head-inner { grid-template-columns: minmax(0, 1fr); }
|
||||
.page-head-pose { --pose-size: 3.5rem; grid-row: 1; align-self: start; }
|
||||
}
|
||||
|
||||
/* ---------- page body and sections ---------- */
|
||||
.page-body {
|
||||
width: var(--container);
|
||||
margin-inline: auto;
|
||||
padding-bottom: var(--section-gap);
|
||||
}
|
||||
.page-body-narrow { max-width: var(--measure-prose); }
|
||||
|
||||
.page-section { padding-top: var(--section-gap); }
|
||||
.page-section:first-child { padding-top: var(--space-5); }
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2) var(--space-5);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
.section-head-text { display: grid; gap: var(--space-2); min-width: 0; max-width: 46rem; }
|
||||
.section-title { font-size: var(--text-title); line-height: 1.25; letter-spacing: -0.015em; }
|
||||
.section-scope { color: var(--muted); font-size: var(--text-body); line-height: 1.6; }
|
||||
|
||||
.section-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
color: var(--accent);
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
.section-link:hover { text-decoration: underline; text-underline-offset: 0.2em; }
|
||||
.section-link svg { width: 1rem; height: 1rem; }
|
||||
|
||||
/* Two- and three-up grids for group cards and tiles. */
|
||||
.grid-2,
|
||||
.grid-3 {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
@media (min-width: 720px) {
|
||||
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
/* A split: a short explanation beside the list it explains. */
|
||||
.split {
|
||||
display: grid;
|
||||
gap: var(--space-5) clamp(2rem, 6vw, 5rem);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
@media (min-width: 900px) {
|
||||
.split { grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); }
|
||||
}
|
||||
|
||||
/* ---------- group cards and tiles ---------- */
|
||||
/* A raised plate with a hairline edge. A card never sits inside a card. */
|
||||
.group-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
background: color-mix(in srgb, var(--panel) 72%, transparent);
|
||||
overflow: hidden;
|
||||
}
|
||||
.group-title { margin-bottom: var(--space-2); color: var(--muted); font-size: var(--text-caption); font-weight: 500; }
|
||||
.group-row { padding: var(--space-3) var(--space-4); }
|
||||
.group-row + .group-row { border-top: 1px solid var(--line); }
|
||||
|
||||
.tile {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-5);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
background: color-mix(in srgb, var(--panel) 72%, transparent);
|
||||
}
|
||||
.tile-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
.tile-icon svg { width: 1.25rem; height: 1.25rem; }
|
||||
.tile h3 { font-size: var(--text-heading); }
|
||||
.tile p { color: var(--muted); font-size: var(--text-ui); line-height: 1.6; }
|
||||
.tile .section-link { margin-top: var(--space-2); }
|
||||
a.tile { transition: background-color var(--dur-state) var(--ease-out), border-color var(--dur-state) var(--ease-out); }
|
||||
a.tile:hover { background: var(--hover); border-color: var(--line-strong); }
|
||||
|
||||
/* ---------- definition rows ---------- */
|
||||
.def-rows {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
background: color-mix(in srgb, var(--panel) 72%, transparent);
|
||||
overflow: hidden;
|
||||
}
|
||||
.def-row {
|
||||
display: grid;
|
||||
gap: var(--space-1) var(--space-4);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
.def-row + .def-row { border-top: 1px solid var(--line); }
|
||||
.def-row dt { color: var(--text); font-size: var(--text-ui); font-weight: 500; }
|
||||
.def-row dd { color: var(--muted); font-size: var(--text-ui); line-height: 1.6; min-width: 0; }
|
||||
.def-row dd strong { color: var(--text); font-weight: 500; }
|
||||
.def-row dd > .status:first-child { display: flex; width: max-content; margin-bottom: var(--space-1); }
|
||||
@media (min-width: 640px) {
|
||||
.def-row { grid-template-columns: minmax(8rem, 15rem) minmax(0, 1fr); }
|
||||
}
|
||||
|
||||
/* ---------- directory rows ---------- */
|
||||
/* A link that behaves like a row: a mark, a title and purpose, a status and
|
||||
an action. Docs topics, integrations, channels, feeds, roadmap items. */
|
||||
.dir-list { display: grid; gap: 2px; }
|
||||
.dir-list-card {
|
||||
gap: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
background: color-mix(in srgb, var(--panel) 72%, transparent);
|
||||
overflow: hidden;
|
||||
}
|
||||
.dir-list-card > li + li { border-top: 1px solid var(--line); }
|
||||
|
||||
.dir-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-control);
|
||||
transition: background-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.dir-list-card .dir-row { border-radius: 0; }
|
||||
a.dir-row:hover { background: var(--hover); }
|
||||
.dir-row[aria-current="page"] { background: var(--selected); }
|
||||
|
||||
.dir-mark {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
.dir-mark svg { width: 1.125rem; height: 1.125rem; }
|
||||
|
||||
.dir-text { display: grid; gap: 0.125rem; flex: 1; min-width: 0; }
|
||||
.dir-title { font-size: var(--text-ui); font-weight: 500; }
|
||||
.dir-purpose { color: var(--muted); font-size: var(--text-ui); line-height: 1.5; }
|
||||
.dir-meta { color: var(--muted); font-family: var(--font-mono); font-size: var(--text-caption); overflow-wrap: anywhere; }
|
||||
.dir-action { flex: none; color: var(--muted); }
|
||||
.dir-action svg { width: 1rem; height: 1rem; }
|
||||
a.dir-row:hover .dir-action { color: var(--text); }
|
||||
|
||||
/* ---------- status: a mark and a word ---------- */
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: var(--text);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
flex: none;
|
||||
border-radius: var(--radius-pill);
|
||||
background: currentColor;
|
||||
color: var(--muted);
|
||||
}
|
||||
.status-ready .status-dot,
|
||||
.status-live .status-dot { color: var(--live); }
|
||||
.status-live .status-dot { box-shadow: var(--glow); }
|
||||
.status-attention .status-dot { color: var(--attention); box-shadow: var(--glow); }
|
||||
.status-accent .status-dot { color: var(--accent); }
|
||||
.status-idle { color: var(--muted); }
|
||||
.status-danger,
|
||||
.status-danger .status-dot { color: var(--danger); }
|
||||
|
||||
/* A one-line health or release statement. */
|
||||
.status-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-2) var(--space-4);
|
||||
color: var(--muted);
|
||||
font-size: var(--text-ui);
|
||||
}
|
||||
|
||||
/* The legacy <StatusBadge> markup, as a mark and a word. */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: var(--text);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 500;
|
||||
}
|
||||
.status-badge-dot { width: 0.5rem; height: 0.5rem; border-radius: var(--radius-pill); background: var(--muted); }
|
||||
.status-badge-experimental .status-badge-dot,
|
||||
.status-badge-preview .status-badge-dot { background: var(--attention); }
|
||||
.status-badge-pending .status-badge-dot,
|
||||
.status-badge-unavailable .status-badge-dot { background: var(--muted); }
|
||||
|
||||
/* ---------- segmented choice ---------- */
|
||||
.segmented,
|
||||
.hero-install-switch {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
.segmented button,
|
||||
.hero-install-switch button {
|
||||
min-height: 1.75rem;
|
||||
padding-inline: 0.625rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-control);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--dur-state) var(--ease-out), color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.segmented button:hover,
|
||||
.hero-install-switch button:hover { background: var(--hover); color: var(--text); }
|
||||
.segmented button[aria-pressed="true"],
|
||||
.hero-install-switch button[aria-pressed="true"] { background: var(--selected); color: var(--text); }
|
||||
@media (pointer: coarse) {
|
||||
.segmented button,
|
||||
.hero-install-switch button { min-height: 2.5rem; }
|
||||
}
|
||||
|
||||
/* ---------- nav tabs ---------- */
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-1);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.tab {
|
||||
position: relative;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
color: var(--muted);
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-control) var(--radius-control) 0 0;
|
||||
transition: background-color var(--dur-state) var(--ease-out), color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.tab:hover { background: var(--hover); color: var(--text); }
|
||||
.tab[aria-current="page"] { color: var(--text); }
|
||||
.tab[aria-current="page"]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline: var(--space-3);
|
||||
bottom: -1px;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
/* ---------- code with copy ---------- */
|
||||
.code { position: relative; min-width: 0; }
|
||||
.code pre.code-block { padding-inline-end: 3rem; }
|
||||
.code .code-copy {
|
||||
position: absolute;
|
||||
background: var(--surface);
|
||||
top: 0.375rem;
|
||||
inset-inline-end: 0.375rem;
|
||||
}
|
||||
.code .code-copy:hover { background: var(--hover); }
|
||||
.code-copy[data-copied="true"] { color: var(--live); }
|
||||
|
||||
/* ---------- plate ---------- */
|
||||
/* The install plate: a raised sheet that holds a choice, a command and its
|
||||
copy button. */
|
||||
.plate {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
background: color-mix(in srgb, var(--panel) 88%, transparent);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sheet);
|
||||
transition: border-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.plate:focus-within { border-color: var(--line-strong); }
|
||||
.plate pre.code-block { background: var(--surface); }
|
||||
|
||||
/* ---------- figure ---------- */
|
||||
.figure { display: grid; gap: var(--space-3); min-width: 0; }
|
||||
.figure-frame {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
.figure-frame img { display: block; width: 100%; height: auto; }
|
||||
.figure-caption { color: var(--muted); font-size: var(--text-caption); line-height: 1.5; }
|
||||
|
||||
/* ---------- callout ---------- */
|
||||
.callout {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
background: color-mix(in srgb, var(--panel) 72%, transparent);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
}
|
||||
.callout-title { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); font-weight: 500; }
|
||||
.callout p:not(.callout-title) { color: var(--muted); font-size: var(--text-ui); line-height: 1.6; }
|
||||
|
||||
/* ---------- disclosure rows (FAQ) ---------- */
|
||||
.disclosure-row > summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
min-height: 3rem;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
color: var(--text);
|
||||
font-size: var(--text-body);
|
||||
font-weight: 500;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.disclosure-row > summary::-webkit-details-marker { display: none; }
|
||||
.disclosure-row > summary:hover { background: var(--hover); }
|
||||
.disclosure-row > summary .disclosure-chevron {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-inline-start: auto;
|
||||
flex: none;
|
||||
color: var(--muted);
|
||||
transition: transform var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.disclosure-row[open] > summary .disclosure-chevron { transform: rotate(180deg); }
|
||||
.disclosure-row + .disclosure-row { border-top: 1px solid var(--line); }
|
||||
.disclosure-body { padding: 0 var(--space-4) var(--space-4); }
|
||||
|
||||
/* ---------- empty / loading / error ---------- */
|
||||
/* A statement, never a placeholder: the whale's pose says what is going on,
|
||||
the title says it in words, one action recovers. */
|
||||
.empty-state,
|
||||
.state-block {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: var(--space-3);
|
||||
max-width: 36rem;
|
||||
margin-inline: auto;
|
||||
padding-block: clamp(3rem, 8vw, 4.5rem);
|
||||
text-align: center;
|
||||
}
|
||||
.empty-state .whale-pose { --pose-size: 6rem; margin-bottom: var(--space-2); }
|
||||
.empty-state-title,
|
||||
.state-title { font-size: var(--text-title); font-weight: 600; line-height: 1.25; }
|
||||
h1.empty-state-title,
|
||||
h1.state-title { font-size: var(--text-display); }
|
||||
.empty-state-reason,
|
||||
.state-body { max-width: 30rem; color: var(--muted); font-size: var(--text-body); line-height: 1.6; }
|
||||
.empty-state-actions,
|
||||
.state-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-3); margin-top: var(--space-2); }
|
||||
|
||||
.state-block-compact,
|
||||
.empty-state-compact {
|
||||
justify-items: start;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
text-align: start;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
background: color-mix(in srgb, var(--panel) 72%, transparent);
|
||||
}
|
||||
.state-block-compact .state-title,
|
||||
.empty-state-compact .empty-state-title { font-size: var(--text-ui); font-weight: 500; }
|
||||
.state-block-compact .whale-pose,
|
||||
.empty-state-compact .whale-pose { --pose-size: 2.5rem; margin: 0; }
|
||||
.state-block-compact .state-actions,
|
||||
.empty-state-compact .empty-state-actions { justify-content: flex-start; }
|
||||
|
||||
.state-block-error .state-title { color: var(--danger); }
|
||||
|
||||
.state-skeleton,
|
||||
.skeleton { display: grid; gap: var(--space-2); width: min(100%, 24rem); }
|
||||
.state-skeleton span,
|
||||
.skeleton-bar {
|
||||
display: block;
|
||||
height: 0.75rem;
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
|
||||
/* The working spinner: a sonar sweep. Motion that shows work; reduced motion
|
||||
shows the still ring. */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 2px solid color-mix(in srgb, var(--accent) 25%, transparent);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: var(--radius-pill);
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.spinner { animation: spin var(--dur-spin) linear infinite; }
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ---------- data table ---------- */
|
||||
.data-table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
background: color-mix(in srgb, var(--panel) 72%, transparent);
|
||||
}
|
||||
.data-table { width: 100%; border-collapse: collapse; font-size: var(--text-ui); }
|
||||
.data-table th,
|
||||
.data-table td { padding: var(--space-3) var(--space-4); text-align: start; vertical-align: top; }
|
||||
.data-table thead th {
|
||||
color: var(--muted);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid var(--line);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.data-table tbody tr + tr > * { border-top: 1px solid var(--line); }
|
||||
.data-table tbody th { font-weight: 500; }
|
||||
.data-table td { color: var(--muted); }
|
||||
.data-table code { font-family: var(--font-mono); font-size: var(--text-mono); color: var(--text); overflow-wrap: anywhere; }
|
||||
.data-table-sort {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
margin: -0.25rem -0.375rem;
|
||||
padding: 0.25rem 0.375rem;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
border-radius: var(--radius-control);
|
||||
cursor: pointer;
|
||||
}
|
||||
.data-table-sort:hover { background: var(--hover); color: var(--text); }
|
||||
.data-table-sort svg { width: 0.875rem; height: 0.875rem; transition: transform var(--dur-state) var(--ease-out); }
|
||||
.data-table-sort[data-dir="asc"] svg { transform: rotate(180deg); }
|
||||
.data-table-sort[data-dir="none"] svg { opacity: 0.45; }
|
||||
th[aria-sort="ascending"] .data-table-sort,
|
||||
th[aria-sort="descending"] .data-table-sort { color: var(--text); }
|
||||
|
||||
/* ---------- fields ---------- */
|
||||
.field { display: grid; gap: var(--space-2); }
|
||||
.field-label { font-size: var(--text-ui); font-weight: 500; }
|
||||
.field-input {
|
||||
width: 100%;
|
||||
min-height: 2.75rem;
|
||||
padding-inline: var(--space-3);
|
||||
font: inherit;
|
||||
font-size: var(--text-body);
|
||||
color: var(--text);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-control);
|
||||
transition: border-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.field-input:focus-visible { border-color: var(--ring); }
|
||||
.field-hint { color: var(--muted); font-size: var(--text-caption); }
|
||||
.field-hint-error { color: var(--danger); }
|
||||
|
||||
/* ---------- prose ---------- */
|
||||
.prose {
|
||||
max-width: var(--measure-prose);
|
||||
color: var(--text);
|
||||
font-size: var(--text-prose);
|
||||
line-height: var(--leading-prose);
|
||||
}
|
||||
.prose p,
|
||||
.prose li { max-width: var(--measure-text); }
|
||||
.prose > * + * { margin-top: var(--space-4); }
|
||||
.prose h2 { margin-top: var(--space-7); font-size: var(--text-title); line-height: 1.25; }
|
||||
.prose h3 { margin-top: var(--space-6); font-size: var(--text-heading); }
|
||||
.prose h2 + *,
|
||||
.prose h3 + * { margin-top: var(--space-2); }
|
||||
.prose ul,
|
||||
.prose ol { padding-inline-start: 1.25rem; }
|
||||
.prose ul { list-style: disc; }
|
||||
.prose ol { list-style: decimal; }
|
||||
.prose li + li { margin-top: var(--space-2); }
|
||||
.prose ::marker { color: var(--muted); }
|
||||
.prose strong { font-weight: 600; }
|
||||
.prose blockquote { border-inline-start: 2px solid var(--line-strong); padding-inline-start: var(--space-4); color: var(--muted); }
|
||||
.prose hr { border: 0; border-top: 1px solid var(--line); }
|
||||
.prose table { width: 100%; border-collapse: collapse; font-size: var(--text-ui); }
|
||||
.prose th,
|
||||
.prose td { padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--line); text-align: start; vertical-align: top; }
|
||||
.prose th { color: var(--muted); font-weight: 500; }
|
||||
html[lang="zh"] .prose,
|
||||
html[lang="ja"] .prose,
|
||||
html[lang="ko"] .prose { line-height: 1.8; letter-spacing: 0; }
|
||||
|
||||
/* ---------- steps (the shared getting-started path) ---------- */
|
||||
.gs-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
background: color-mix(in srgb, var(--panel) 72%, transparent);
|
||||
overflow: hidden;
|
||||
}
|
||||
.gs-steps li {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
padding: var(--space-5);
|
||||
}
|
||||
.gs-steps li + li { border-inline-start: 1px solid var(--line); }
|
||||
.gs-step-index {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
color: var(--accent);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
|
||||
border-radius: var(--radius-pill);
|
||||
}
|
||||
.gs-steps h3 { margin-top: var(--space-2); font-size: var(--text-heading); }
|
||||
.gs-steps p { color: var(--muted); font-size: var(--text-ui); line-height: 1.6; }
|
||||
pre.gs-step-commands { margin-top: var(--space-1); font-size: var(--text-caption); white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.gs-step-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
margin-top: var(--space-1);
|
||||
color: var(--accent);
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 500;
|
||||
}
|
||||
.gs-step-link:hover { text-decoration: underline; text-underline-offset: 0.2em; }
|
||||
.docs-content .gs-steps { grid-template-columns: minmax(0, 1fr); }
|
||||
.docs-content .gs-steps li + li { border-inline-start: 0; border-top: 1px solid var(--line); }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.gs-steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.gs-steps li:nth-child(odd) { border-inline-start: 0; }
|
||||
.gs-steps li:nth-child(n + 3) { border-top: 1px solid var(--line); }
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.gs-steps { grid-template-columns: 1fr; }
|
||||
.gs-steps li + li { border-inline-start: 0; border-top: 1px solid var(--line); }
|
||||
}
|
||||
|
||||
/* Any other numbered list of steps: as many columns as fit, divided by
|
||||
1px of the line colour showing through the gaps. */
|
||||
.steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
|
||||
gap: 1px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: var(--line);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
.steps > li {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
padding: var(--space-5);
|
||||
background: var(--panel);
|
||||
}
|
||||
.steps h3 { margin-top: var(--space-2); font-size: var(--text-heading); }
|
||||
.steps p { color: var(--muted); font-size: var(--text-ui); line-height: 1.6; }
|
||||
|
||||
.stack { display: grid; align-content: start; gap: var(--space-4); min-width: 0; }
|
||||
|
||||
/* ---------- session media (docs guide) ---------- */
|
||||
.session-media { border: 1px solid var(--line); border-radius: var(--radius-surface); overflow: hidden; }
|
||||
.session-media video { display: block; width: 100%; height: auto; background: var(--ocean-abyss); }
|
||||
.session-media-stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-3);
|
||||
padding: clamp(1.5rem, 4vw, 2.5rem);
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--surface);
|
||||
}
|
||||
.session-media-pending-note { max-width: 44rem; margin: 0; color: var(--muted); font-size: var(--text-ui); line-height: 1.7; }
|
||||
.session-media-caption { display: grid; gap: var(--space-2); padding: var(--space-4) var(--space-5); color: var(--muted); font-size: var(--text-ui); line-height: 1.6; }
|
||||
.session-media-caption strong { color: var(--text); font-size: var(--text-body); }
|
||||
.session-media-caption a { color: var(--accent); font-size: var(--text-caption); }
|
||||
|
||||
/* ---------- the horizon ---------- */
|
||||
/* One calm, empty horizon per page: a straight line where the sky meets the
|
||||
sea. Nothing stands on it but the whale. The sea below is the stage. */
|
||||
.horizon {
|
||||
position: relative;
|
||||
height: 1px;
|
||||
background: var(--horizon-glow);
|
||||
box-shadow: 0 0 18px 2px rgb(30 143 216 / 0.55);
|
||||
}
|
||||
|
||||
.sea { background: var(--sea), var(--ocean-abyss); }
|
||||
|
||||
/* A patient water texture (Celmins): fine static rules that fade with depth.
|
||||
Static SVG, never animated, hidden from assistive technology. */
|
||||
.sea-texture {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
height: min(22rem, 100%);
|
||||
pointer-events: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='210' height='18'%3E%3Cg stroke='%2390b9ff' stroke-width='1' stroke-linecap='round'%3E%3Cpath d='M4 5h30M60 5h14M112 5h40M170 5h8' stroke-opacity='.12'/%3E%3Cpath d='M30 14h22M88 14h36M140 14h10M186 14h20' stroke-opacity='.08'/%3E%3C/g%3E%3C/svg%3E");
|
||||
background-size: 210px 18px;
|
||||
-webkit-mask-image: linear-gradient(180deg, #000, transparent);
|
||||
mask-image: linear-gradient(180deg, #000, transparent);
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.horizon { background: CanvasText; box-shadow: none; }
|
||||
.sea-texture { display: none; }
|
||||
}
|
||||
|
||||
/* ---------- search field ---------- */
|
||||
.search-field { position: relative; }
|
||||
.search-field .search-input { padding-inline: 2.5rem 3rem; }
|
||||
.search-field-icon {
|
||||
position: absolute;
|
||||
inset-inline-start: 0.875rem;
|
||||
top: 50%;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--muted);
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.search-field .search-field-clear,
|
||||
.search-field .search-field-kbd {
|
||||
position: absolute;
|
||||
inset-inline-end: 0.375rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.search-field .search-field-kbd { inset-inline-end: 0.75rem; }
|
||||
@@ -1,49 +1,44 @@
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Site chrome — the header, the mobile sheet, the footer stage */
|
||||
/* Site chrome — the header, the mobile sheet, the footer sea */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* ---------- header ---------- */
|
||||
/* An opaque bar on the reading floor with one hairline under it. No glass,
|
||||
no blur: the page scrolls beneath a solid surface. */
|
||||
/* The header floats on the sky: it paints the same ground as the top of the
|
||||
page, pinned to the viewport, so at rest there is no bar at all. No glass,
|
||||
no blur. A hairline marks it once content scrolls beneath. */
|
||||
.site-nav {
|
||||
position: sticky;
|
||||
z-index: 30;
|
||||
top: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--bg);
|
||||
background: var(--ground), var(--bg);
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
|
||||
}
|
||||
|
||||
.site-nav-inner {
|
||||
display: flex;
|
||||
width: var(--container);
|
||||
min-height: 4.25rem;
|
||||
min-height: 4rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.paper-nav-inner { padding-block: var(--space-2); }
|
||||
|
||||
.site-wordmark {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--mark-ink);
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03em;
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
|
||||
.paper-nav-inner {
|
||||
min-height: 3.75rem;
|
||||
padding-block: var(--space-2);
|
||||
}
|
||||
|
||||
.paper-wordmark {
|
||||
flex: 0 1 auto;
|
||||
gap: 0.75rem;
|
||||
max-width: 15rem;
|
||||
min-width: 8.75rem;
|
||||
overflow: hidden;
|
||||
@@ -52,64 +47,74 @@
|
||||
.paper-wordmark-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
line-height: 1.1;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* The mark and the wordmark are one ink, drawn in currentColor, so both
|
||||
appearances re-ink them without a filter or a second file. */
|
||||
/* The mark is the logo gradient; the wordmark is one ink (--mark-ink),
|
||||
drawn through a mask so both appearances re-ink it without a filter or a
|
||||
second file. */
|
||||
.paper-wordmark-mark {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin-inline: -3px;
|
||||
}
|
||||
|
||||
.paper-wordmark-logo {
|
||||
.paper-wordmark-logo,
|
||||
.wordmark {
|
||||
display: block;
|
||||
flex: 0 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
width: 128px;
|
||||
max-width: 100%;
|
||||
height: 20px;
|
||||
background: var(--mark-ink);
|
||||
-webkit-mask: url("/brand/wordmark.svg") no-repeat left center / contain;
|
||||
mask: url("/brand/wordmark.svg") no-repeat left center / contain;
|
||||
}
|
||||
[dir="rtl"] .paper-wordmark-logo,
|
||||
[dir="rtl"] .wordmark {
|
||||
-webkit-mask-position: right center;
|
||||
mask-position: right center;
|
||||
}
|
||||
|
||||
/* The one primary action in the header. It keeps its class for layout;
|
||||
its paint is the .btn-primary recipe. */
|
||||
@media (forced-colors: active) {
|
||||
.paper-wordmark-logo,
|
||||
.wordmark { background: CanvasText; }
|
||||
}
|
||||
|
||||
/* The one primary action in the header, and the one identity door. */
|
||||
.paper-install-cta {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 2.25rem;
|
||||
padding-inline: var(--space-3);
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.8125rem;
|
||||
padding-inline: var(--space-4);
|
||||
background: var(--brand-fill);
|
||||
color: var(--on-brand);
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius-control);
|
||||
transition: background-color var(--dur-state) var(--ease-out);
|
||||
transition: box-shadow var(--dur-state) var(--ease-out);
|
||||
}
|
||||
|
||||
.paper-install-cta:hover {
|
||||
background: var(--indigo-deep);
|
||||
color: var(--on-accent);
|
||||
background: var(--brand-fill-hover);
|
||||
color: var(--on-brand);
|
||||
box-shadow: var(--glow-focus);
|
||||
}
|
||||
|
||||
/* The one identity door: a quiet text control beside the primary action. */
|
||||
.paper-auth-signin {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 2.25rem;
|
||||
padding-inline: var(--space-3);
|
||||
color: var(--text);
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-control);
|
||||
transition: background-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
|
||||
.paper-auth-signin:hover { background: var(--hover); }
|
||||
|
||||
/* Actions sit 2px apart, as related rail controls in the GPUI client; the
|
||||
@@ -120,14 +125,13 @@
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.site-nav-actions > * { flex-shrink: 0; }
|
||||
.site-nav-actions > .paper-auth-signin { margin-inline-start: 0.5rem; }
|
||||
.site-nav-actions > .paper-install-cta { margin-inline-start: 0.25rem; }
|
||||
|
||||
/* Quiet icon controls: a 40px target around a 20px glyph, the GPUI rail
|
||||
metrics. Muted at rest, full ink on hover and while expanded; a pressed
|
||||
or current control takes the neutral selected fill. */
|
||||
/* Quiet icon controls: a 40px target around a 20px glyph. Muted at rest,
|
||||
full ink on hover and while expanded; a pressed or current control takes
|
||||
the neutral selected fill. */
|
||||
.nav-icon-button {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
@@ -142,28 +146,20 @@
|
||||
transition-duration: var(--dur-state);
|
||||
transition-timing-function: var(--ease-out);
|
||||
}
|
||||
|
||||
.nav-icon-button:hover,
|
||||
.nav-icon-button[aria-expanded="true"] {
|
||||
background: var(--hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-icon-button[aria-pressed="true"],
|
||||
.nav-icon-button[aria-current="page"] {
|
||||
background: var(--selected);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-icon-button:active { opacity: 0.75; }
|
||||
.nav-icon-button:focus-visible { box-shadow: var(--glow-focus); }
|
||||
|
||||
/* The small variant for controls inside rows and code blocks; it grows to
|
||||
the touch floor on coarse pointers. */
|
||||
.nav-icon-button-sm {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
.nav-icon-button-sm { width: 2rem; height: 2rem; }
|
||||
.nav-icon-button-sm .nav-icon { width: 1rem; height: 1rem; }
|
||||
@media (pointer: coarse) {
|
||||
.nav-icon-button-sm { width: 2.75rem; height: 2.75rem; }
|
||||
@@ -183,11 +179,43 @@
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.nav-locale:has(select:focus-visible) {
|
||||
box-shadow: 0 0 0 var(--gpui-focus-width) var(--ring), var(--glow-focus);
|
||||
}
|
||||
|
||||
/* ---------- primary nav links ---------- */
|
||||
/* Hover is a neutral wash. The current page keeps a 2px accent bar under
|
||||
its label: the one place blue marks "here". */
|
||||
.nav-link {
|
||||
position: relative;
|
||||
padding: 0.375rem 0.5rem;
|
||||
margin-inline: -0.5rem;
|
||||
color: var(--text);
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-control);
|
||||
transition: background-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.nav-link:hover { background: var(--hover); }
|
||||
.nav-link::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline: 0.5rem;
|
||||
bottom: -2px;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
box-shadow: var(--glow);
|
||||
opacity: 0;
|
||||
}
|
||||
.nav-link[aria-current="page"]::after { opacity: 1; }
|
||||
|
||||
/* Compact viewports: the strip keeps the home control clickable. */
|
||||
@media (max-width: 520px) {
|
||||
.site-nav-inner { gap: 0.5rem; }
|
||||
.site-nav-actions { min-width: 0; }
|
||||
.paper-wordmark { max-width: 9.75rem; }
|
||||
}
|
||||
|
||||
/* ---------- the mobile sheet ---------- */
|
||||
.mm-list > li + li { border-top: 1px solid var(--line); }
|
||||
|
||||
@@ -202,7 +230,6 @@
|
||||
border-radius: var(--radius-control);
|
||||
transition: background-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
|
||||
.mm-link:hover { background: var(--hover); }
|
||||
.mm-link[aria-current="page"] { background: var(--selected); }
|
||||
.mm-link-chevron { margin-inline-start: auto; color: var(--muted); }
|
||||
@@ -213,69 +240,29 @@
|
||||
min-height: 2.75rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.mm-action.paper-install-cta { margin-top: 1.5rem; }
|
||||
|
||||
/* ---------- primary nav links ---------- */
|
||||
/* Hover is a neutral wash. The current page keeps the 2px accent bar under
|
||||
its label: the active-tab grammar, the one place blue marks "here". */
|
||||
.nav-link {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.8125rem;
|
||||
letter-spacing: 0;
|
||||
color: var(--text);
|
||||
position: relative;
|
||||
padding: 0.375rem 0.5rem;
|
||||
margin-inline: -0.5rem;
|
||||
border-radius: var(--radius-control);
|
||||
transition: background-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.nav-link:hover { background: var(--hover); }
|
||||
.nav-link::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline: 0.5rem;
|
||||
bottom: -2px;
|
||||
height: 2px;
|
||||
background: var(--indigo);
|
||||
opacity: 0;
|
||||
}
|
||||
.nav-link[aria-current="page"]::after { opacity: 1; }
|
||||
|
||||
/* Compact viewports: the strip keeps the home control clickable. These
|
||||
rules also exist in home.css until the home slice deletes them there. */
|
||||
@media (max-width: 520px) {
|
||||
.site-nav-inner {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.site-nav-actions {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.paper-wordmark {
|
||||
max-width: 9.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- footer — the stage below the horizon ---------- */
|
||||
/* The footer is midnight in both appearances (tokens-roles.css). Its top
|
||||
edge is the page's one horizon: two flat grounds meeting at a pixel, plus
|
||||
a hairline only where they are too close to tell apart (dark). */
|
||||
/* ---------- footer — the sea below the horizon ---------- */
|
||||
/* The footer is the stage in both appearances. Its top edge is the page's
|
||||
one horizon: a straight line of light, the sea bright at the waterline
|
||||
and deep within a few rows. */
|
||||
.site-footer {
|
||||
position: relative;
|
||||
border-top: 1px solid var(--horizon-edge);
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
background: var(--sea), var(--ocean-abyss);
|
||||
color: var(--muted);
|
||||
}
|
||||
.site-footer > .horizon { position: absolute; inset: 0 0 auto; z-index: 1; }
|
||||
|
||||
/* A page whose main already ends on the stage continues into the footer
|
||||
with no second edge. */
|
||||
main:has(> .midnight:last-child) + .site-footer,
|
||||
main:has(> .ocean-column) + .site-footer { border-top-color: transparent; }
|
||||
/* A page whose main already ends in the sea continues into the footer with
|
||||
no second horizon. */
|
||||
main:has(.sea-continues:last-child) + .site-footer { background: var(--ocean-abyss); }
|
||||
main:has(.sea-continues:last-child) + .site-footer > .horizon,
|
||||
main:has(.sea-continues:last-child) + .site-footer > .sea-texture { display: none; }
|
||||
|
||||
.site-footer-main,
|
||||
.site-footer-meta {
|
||||
position: relative;
|
||||
width: var(--container);
|
||||
margin-inline: auto;
|
||||
}
|
||||
@@ -284,18 +271,18 @@ main:has(> .ocean-column) + .site-footer { border-top-color: transparent; }
|
||||
display: grid;
|
||||
grid-template-columns: minmax(14rem, 1fr) minmax(0, 2fr);
|
||||
gap: var(--space-7);
|
||||
padding-block: var(--space-7) var(--space-6);
|
||||
padding-block: 5.5rem var(--space-7);
|
||||
}
|
||||
|
||||
.site-footer-brand { display: grid; gap: var(--space-3); align-content: start; }
|
||||
.site-footer-brand { display: grid; gap: var(--space-4); align-content: start; }
|
||||
.site-footer-brand p {
|
||||
max-width: 28rem;
|
||||
max-width: 26rem;
|
||||
color: var(--muted);
|
||||
font-size: var(--text-ui);
|
||||
line-height: 1.5;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.site-wordmark-footer { color: var(--text); justify-self: start; }
|
||||
.site-footer-brand .whale-pose { --pose-size: 3rem; }
|
||||
.site-wordmark-footer { color: var(--text); justify-self: start; gap: var(--space-3); }
|
||||
|
||||
.site-footer-links {
|
||||
display: grid;
|
||||
@@ -312,10 +299,10 @@ main:has(> .ocean-column) + .site-footer { border-top-color: transparent; }
|
||||
}
|
||||
|
||||
.site-footer-list { display: grid; gap: 2px; }
|
||||
|
||||
.site-footer-list a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-self: start;
|
||||
min-height: 1.75rem;
|
||||
padding-inline: var(--space-2);
|
||||
margin-inline: calc(-1 * var(--space-2));
|
||||
@@ -324,7 +311,6 @@ main:has(> .ocean-column) + .site-footer { border-top-color: transparent; }
|
||||
border-radius: var(--radius-control);
|
||||
transition: background-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
|
||||
.site-footer-list a:hover { background: var(--hover); }
|
||||
|
||||
.site-footer-meta {
|
||||
@@ -338,7 +324,6 @@ main:has(> .ocean-column) + .site-footer { border-top-color: transparent; }
|
||||
color: var(--muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.site-footer-meta p { min-width: 0; max-width: 70ch; overflow-wrap: anywhere; }
|
||||
.site-footer-meta a {
|
||||
color: var(--text);
|
||||
@@ -347,21 +332,34 @@ main:has(> .ocean-column) + .site-footer { border-top-color: transparent; }
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
.site-footer-meta a:hover { text-decoration-color: currentColor; }
|
||||
|
||||
.site-footer-meta-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2) var(--space-4);
|
||||
}
|
||||
.site-footer-meta-links { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.site-footer-main { grid-template-columns: 1fr; gap: var(--space-6); }
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.site-footer { border-top-color: CanvasText; }
|
||||
@media print {
|
||||
.site-footer { background: none; color: inherit; border-top: 1px solid currentColor; }
|
||||
.site-footer > .horizon { display: none; }
|
||||
}
|
||||
|
||||
@media print {
|
||||
.site-footer { border-top-color: currentColor; background: none; color: inherit; }
|
||||
/* ---------- usage counting: the privacy page's control ---------- */
|
||||
.usage-counting { display: grid; gap: var(--space-3); margin-top: var(--space-2); }
|
||||
.usage-counting-status { color: var(--text); font-weight: 500; }
|
||||
.usage-counting-elsewhere { color: var(--muted); font-size: var(--text-ui); }
|
||||
.usage-counting-button {
|
||||
display: inline-flex;
|
||||
justify-self: start;
|
||||
align-items: center;
|
||||
min-height: 2.5rem;
|
||||
padding-inline: var(--space-4);
|
||||
color: var(--text);
|
||||
background: var(--panel);
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-control);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.usage-counting-button:hover { background: var(--hover); }
|
||||
@@ -3,7 +3,8 @@
|
||||
/* ------------------------------------------------------------------ */
|
||||
/*
|
||||
* The empty / loading / error grammar itself lives in primitives.css
|
||||
* (.empty-state, .skeleton-bar); components/surface-state.tsx renders it.
|
||||
* (.empty-state, .state-block, .skeleton-bar); components/surface-state.tsx
|
||||
* renders it.
|
||||
* This file keeps the two containers around it.
|
||||
*/
|
||||
|
||||
@@ -14,7 +15,6 @@
|
||||
width: var(--container);
|
||||
margin-inline: auto;
|
||||
padding-block: var(--space-6) var(--space-8);
|
||||
max-width: 52rem;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -87,8 +87,8 @@
|
||||
}
|
||||
|
||||
.connection-button:hover { background: var(--hover); }
|
||||
.connection-button-primary { border-color: transparent; background: var(--accent); color: var(--on-accent); }
|
||||
.connection-button-primary:hover { background: var(--accent-hover); color: var(--on-accent); }
|
||||
.connection-button-primary { border-color: transparent; background: var(--brand-fill); color: var(--on-brand); font-weight: 600; }
|
||||
.connection-button-primary:hover { background: var(--brand-fill-hover); color: var(--on-brand); }
|
||||
.connection-button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.connection-button[aria-busy="true"] { cursor: progress; }
|
||||
|
||||
|
||||
@@ -2,82 +2,56 @@
|
||||
/* Tokens and roles — the whale-road */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/*
|
||||
* One token authority: vendor/codewhale-design/tokens.json, generated into
|
||||
* ../tokens.css as `--gpui-light-*` / `--gpui-dark-*`. This file names the
|
||||
* roles every surface paints with (codewhale-design/DIRECTION.md):
|
||||
* Direction: codewhale-design/DIRECTION.md. Token authority:
|
||||
* vendor/codewhale-design/tokens.json (1.1.x), generated into ../tokens.css
|
||||
* as `--gpui-light-*` / `--gpui-dark-*`. This file names the roles every
|
||||
* page paints with.
|
||||
*
|
||||
* Depth is elevation.
|
||||
* sunlit --panel raised plates, popovers, the thing in focus
|
||||
* twilight --bg the reading floor
|
||||
* recessed --surface code, rails, chrome set into the floor
|
||||
* midnight .midnight the stage below the one horizon (the footer,
|
||||
* a home descent); always the dark set, in both
|
||||
* appearances
|
||||
* The ocean. Dark is the preferred look: deep navy grounds that rise into
|
||||
* the logo's gradient (#1E8FD8 -> #0B48BB). Light is warm paper above one
|
||||
* calm horizon. Below the horizon (the footer, the home sea) is always
|
||||
* the deep water, in both appearances (`.stage`).
|
||||
*
|
||||
* Bioluminescence is how things light up. Focus, live work and "needs
|
||||
* you" carry a soft halo (--glow, --glow-focus): a point of light in the
|
||||
* dark, quieter on paper. Nothing else glows.
|
||||
* Depth is elevation. --panel is raised (nearer the surface, lighter);
|
||||
* --bg is the floor; --surface is recessed (code, rails, fields).
|
||||
*
|
||||
* State is four hues and always a word: --accent (action, focus),
|
||||
* --live (working, done, verified), --attention (needs you, warning),
|
||||
* --danger (failed, destructive). Idle and offline use --muted.
|
||||
* Light means something. Focus, live work and "needs you" carry a small
|
||||
* point of light (--glow). Nothing else glows.
|
||||
*
|
||||
* State is a mark and a word: --live (working, done, ready), --attention
|
||||
* (needs you), --danger (failed), --muted (idle). Never colour alone.
|
||||
*
|
||||
* Text, accent and state inks are the generated GPUI tokens in both
|
||||
* appearances. The navy grounds are this site's reach for the ombre; they
|
||||
* are measured against every ink here (gpui-role-tokens.test.ts checks
|
||||
* WCAG AA on every ground).
|
||||
*
|
||||
* NAMING TRAP: tokens.json `surface` is the RAISED colour (web --panel);
|
||||
* tokens.json `sidebar` is the RECESSED one (web --surface).
|
||||
*
|
||||
* Every role is declared in each scheme block — light (:root), OS-dark and
|
||||
* pinned-dark (identical), and midnight — because a custom property that
|
||||
* references another is computed where it is declared.
|
||||
*
|
||||
* The legacy names (--paper, --ink, --indigo, --stage-*, --ocean-*, ...)
|
||||
* stay only so pages that have not migrated keep painting; new work uses the
|
||||
* roles. They are removed in the post-renovation sweep.
|
||||
*/
|
||||
|
||||
/* ---------- scheme-independent tokens ---------- */
|
||||
:root {
|
||||
/* State and control-edge hues that tokens.json 1.0.0 has no slot for.
|
||||
Values and their contrast on all five grounds are measured in
|
||||
codewhale-design/reference/MEASUREMENTS.md §2.2–2.3 (proposed
|
||||
tokens.json 1.1.0). Light live #3a6352 ≥ 4.97:1, light danger #9e3f36
|
||||
≥ 4.77:1, dark live #9ec7b2 ≥ 6.21:1, dark danger #e39a90 ≥ 5.12:1 on
|
||||
background, panel, sidebar, hover and selected. border-strong clears
|
||||
the 3:1 non-text floor on the same five grounds. */
|
||||
--gpui-light-live: #3a6352;
|
||||
--gpui-dark-live: #9ec7b2;
|
||||
--gpui-light-danger: #9e3f36;
|
||||
--gpui-dark-danger: #e39a90;
|
||||
--gpui-light-border-strong: #807c76;
|
||||
--gpui-dark-border-strong: #828386;
|
||||
|
||||
/* Legacy GPUI aliases, still read by unmigrated page partials. */
|
||||
--gpui-paper: var(--gpui-light-bg);
|
||||
--gpui-paper-raised: var(--gpui-light-panel);
|
||||
--gpui-paper-muted: var(--gpui-light-sidebar);
|
||||
--gpui-ink: var(--gpui-light-text);
|
||||
--gpui-ink-soft: var(--gpui-light-text-muted);
|
||||
--gpui-ink-mute: var(--gpui-light-text-muted);
|
||||
--gpui-edge: var(--gpui-light-border);
|
||||
--gpui-primary: var(--gpui-light-primary);
|
||||
--gpui-primary-hover: rgb(var(--gpui-light-primary-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--gpui-stage: var(--gpui-dark-bg);
|
||||
--gpui-stage-deep: var(--gpui-dark-sidebar);
|
||||
--gpui-stage-muted: var(--gpui-dark-panel);
|
||||
--gpui-stage-raised: var(--gpui-dark-list-active);
|
||||
--gpui-stage-ink: var(--gpui-dark-text);
|
||||
--gpui-stage-ink-soft: var(--gpui-dark-text-muted);
|
||||
--gpui-stage-ink-dim: var(--gpui-dark-text-muted);
|
||||
--gpui-stage-edge: var(--gpui-dark-border);
|
||||
--gpui-primary-dark: var(--gpui-dark-primary);
|
||||
--gpui-primary-hover-dark: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--gpui-accent-dark: var(--gpui-dark-accent);
|
||||
--gpui-selection-dark: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-selection-opacity));
|
||||
--gpui-tan: #b1a17a;
|
||||
--gpui-moss: #739686;
|
||||
--gpui-rust: #aa6f6c;
|
||||
--c-stage-soft: var(--gpui-dark-text-muted-rgb);
|
||||
--c-stage-deep: var(--gpui-dark-sidebar-rgb);
|
||||
--c-primary-dark: var(--gpui-dark-primary-rgb);
|
||||
/* The ocean: the navy grounds and the logo gradient. */
|
||||
--ocean-abyss: #030a1c;
|
||||
--ocean-floor: #061431;
|
||||
--ocean-raised: #0c2152;
|
||||
--ocean-hover: #11295e;
|
||||
--ocean-selected: #16316b;
|
||||
--ocean-line: #1f3a78;
|
||||
--ocean-line-strong: #6f86b8;
|
||||
--ocean-sky: #0a1d48;
|
||||
--brand-light: #1e8fd8;
|
||||
--brand-deep: #0b48bb;
|
||||
/* The primary fill: the logo gradient, started where white text on it
|
||||
still clears 4.5:1 (#1877c9 is 4.65:1; #1e8fd8 itself is 3.5:1). */
|
||||
--brand-fill: linear-gradient(135deg, #1877c9, var(--brand-deep));
|
||||
--brand-fill-hover: linear-gradient(135deg, #1670c2, #0a3fa6);
|
||||
--on-brand: #ffffff;
|
||||
/* The sea below the horizon: bright at the waterline, deep within a few
|
||||
rows, so text from 3rem down sits on >= 4.5:1 muted contrast. */
|
||||
--sea: linear-gradient(180deg, #0b48bb 0, #0a3a94 2.5rem, #06205a 9rem, #041234 18rem, var(--ocean-abyss) 100%);
|
||||
--horizon-glow: rgb(144 185 255 / 0.9);
|
||||
|
||||
/* Type: Shannon Sans for every role, the system monospace stack for
|
||||
things a person could type or paste. No serif, no third family. */
|
||||
@@ -88,55 +62,51 @@
|
||||
"Liberation Mono", monospace;
|
||||
--font-cjk: var(--gpui-font-fallbacks), sans-serif;
|
||||
|
||||
/* Type scale (MEASUREMENTS §3.1). 12px is the floor: nothing renders
|
||||
smaller. Display sizes are website-only, one per page. */
|
||||
--text-caption: 0.75rem; /* 12/16 meta, timestamps, labels */
|
||||
--text-mono: var(--gpui-mono-size); /* 13/20 code, commands */
|
||||
--text-ui: 0.875rem; /* 14/20 controls, rows, panel body */
|
||||
--text-body: 1rem; /* 16/24 short copy, form inputs (no iOS zoom) */
|
||||
--text-prose: 1.0625rem; /* 17/26 reading: docs, legal, answers */
|
||||
--leading-prose: 1.53;
|
||||
--text-heading: 1.0625rem; /* 17/24 600 section heading */
|
||||
--text-title: 1.375rem; /* 22/28 600 page or sheet title */
|
||||
--text-display: clamp(1.75rem, 1.2rem + 2.2vw, 2.5rem); /* 28→40 inner-page hero */
|
||||
--text-display-lg: clamp(2.5rem, 1.6rem + 3.6vw, 3.5rem); /* 40→56 the home hero */
|
||||
/* Short aliases the spec uses. */
|
||||
/* Type scale. 12px is the floor. Display sizes are one per page. */
|
||||
--text-caption: 0.75rem; /* 12 meta, timestamps, labels */
|
||||
--text-mono: var(--gpui-mono-size); /* 13 code, commands */
|
||||
--text-ui: 0.875rem; /* 14 controls, rows, panel body */
|
||||
--text-body: 1rem; /* 16 short copy, form inputs (no iOS zoom) */
|
||||
--text-prose: 1.0625rem; /* 17 reading: docs, legal, answers */
|
||||
--leading-prose: 1.6;
|
||||
--text-heading: 1.0625rem; /* 17 600 group heading */
|
||||
--text-title: 1.375rem; /* 22 600 section title */
|
||||
--text-display: clamp(2rem, 1.3rem + 2.6vw, 3rem); /* inner-page title */
|
||||
--text-display-lg: clamp(2.5rem, 1.4rem + 4.4vw, 4.5rem); /* the home hero */
|
||||
--text-xs: var(--text-caption);
|
||||
--text-sm: var(--text-ui);
|
||||
--text-base: var(--text-body);
|
||||
|
||||
/* Space: a 4px program (MEASUREMENTS §4.1). */
|
||||
--space-1: 0.25rem; /* 4 */
|
||||
--space-2: 0.5rem; /* 8 */
|
||||
--space-3: 0.75rem; /* 12 */
|
||||
--space-4: 1rem; /* 16 */
|
||||
--space-5: 1.5rem; /* 24 */
|
||||
--space-6: 2rem; /* 32 */
|
||||
--space-7: 3rem; /* 48 */
|
||||
--space-8: 4rem; /* 64 */
|
||||
--section-gap: clamp(var(--space-7), 6vw, var(--space-8));
|
||||
/* Space: a 4px program. */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-5: 1.5rem;
|
||||
--space-6: 2rem;
|
||||
--space-7: 3rem;
|
||||
--space-8: 4rem;
|
||||
--section-gap: clamp(var(--space-7), 7vw, 5.5rem);
|
||||
|
||||
/* One container, one reading measure. --measure-prose is the GPUI
|
||||
PROSE_MEASURE (880px), not the 1440px READING_COLUMN. */
|
||||
/* One container, one reading measure. */
|
||||
--container: min(100% - var(--gpui-spacing-page), 76rem);
|
||||
--measure-prose: 55rem;
|
||||
--measure-text: 70ch;
|
||||
--hero-pad: clamp(3rem, 6vw, 4.75rem);
|
||||
--measure-prose: 46rem;
|
||||
--measure-text: 68ch;
|
||||
--hero-pad: clamp(3rem, 7vw, 5.5rem);
|
||||
--section-pad: clamp(2.75rem, 5vw, 4rem);
|
||||
|
||||
/* Radius: 6 controls, 10 surfaces (code, menus, cards), 14 sheets,
|
||||
pill for dots. No other radius. */
|
||||
/* Radius: 6 controls, 10 surfaces, 14 sheets, pill for dots. */
|
||||
--radius-control: var(--gpui-radius-control);
|
||||
--radius-surface: var(--gpui-radius-panel);
|
||||
--radius-sheet: 14px;
|
||||
--radius-sheet: var(--gpui-radius-sheet);
|
||||
--radius-pill: 999px;
|
||||
|
||||
/* Motion (MEASUREMENTS §9.1). Nothing bounces: every curve stays inside
|
||||
[0, 1]. --ease-spring is the GPUI PANEL_SPRING fit (no overshoot). */
|
||||
/* Motion. Nothing bounces: every curve stays inside [0, 1]. */
|
||||
--dur-state: 120ms; /* colour, border, opacity on hover and press */
|
||||
--dur-arrive: 180ms; /* something new arrives, rise <= 4px */
|
||||
--dur-exit: 120ms; /* something closes or leaves */
|
||||
--dur-spring: 340ms; /* panes and sheets */
|
||||
--dur-spin: 1.2s; /* a working spinner: motion that shows work */
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--ease-spring: cubic-bezier(0.2, 0.1, 0.16, 1);
|
||||
@@ -155,7 +125,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- light — the twilight floor on warm paper ---------- */
|
||||
/* ---------- light — warm paper above the horizon ---------- */
|
||||
:root {
|
||||
--bg: var(--gpui-light-bg);
|
||||
--surface: var(--gpui-light-sidebar);
|
||||
@@ -175,267 +145,162 @@
|
||||
--success: var(--gpui-light-live);
|
||||
--attention: var(--gpui-light-attention);
|
||||
--danger: var(--gpui-light-danger);
|
||||
--on-danger: var(--gpui-light-on-primary);
|
||||
--mark-ink: var(--gpui-light-text);
|
||||
--diff-add: rgb(104 152 119 / 0.14);
|
||||
--diff-del: rgb(189 119 116 / 0.11);
|
||||
--mark-ink: var(--gpui-light-text);
|
||||
/* Light in the dark: on paper the halo is a quiet ring. */
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 14%, transparent);
|
||||
/* The sky: paper that brightens toward blue at the top of the page. */
|
||||
--ground: linear-gradient(180deg, #e9f1fb 0, var(--gpui-light-bg) 26rem);
|
||||
/* On paper, light is a quiet ring. */
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 16%, transparent);
|
||||
--glow-focus: 0 0 0 6px rgb(var(--gpui-light-primary-rgb) / 0.12);
|
||||
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.16);
|
||||
/* The horizon needs a drawn edge only where the two grounds are within
|
||||
1.2:1 of each other — the dark appearance. */
|
||||
--horizon-edge: transparent;
|
||||
|
||||
/* Legacy names (sweep). */
|
||||
--c-paper: var(--gpui-light-bg-rgb);
|
||||
--c-paper-deep: var(--gpui-light-sidebar-rgb);
|
||||
--c-paper-edge: var(--gpui-light-border-rgb);
|
||||
--c-ink: var(--gpui-light-text-rgb);
|
||||
--c-ink-soft: var(--gpui-light-text-muted-rgb);
|
||||
--c-ink-mute: var(--gpui-light-text-muted-rgb);
|
||||
--c-indigo: var(--gpui-light-primary-rgb);
|
||||
--paper: var(--gpui-paper);
|
||||
--paper-deep: var(--gpui-paper-muted);
|
||||
--paper-edge: var(--gpui-edge);
|
||||
--paper-card: var(--gpui-paper-raised);
|
||||
--paper-line: var(--gpui-edge);
|
||||
--paper-line-soft: var(--gpui-edge);
|
||||
--ink: var(--gpui-ink);
|
||||
--ink-soft: var(--gpui-ink-soft);
|
||||
--ink-mute: var(--gpui-ink-mute);
|
||||
--indigo: var(--gpui-primary);
|
||||
--indigo-deep: var(--gpui-primary-hover);
|
||||
--indigo-pale: rgb(var(--c-indigo) / 0.1);
|
||||
--action-on-dark: var(--gpui-primary-dark);
|
||||
--ochre: #7d6a3f;
|
||||
--jade: #4e6f61;
|
||||
--seafoam: var(--gpui-moss);
|
||||
--cobalt: var(--gpui-primary);
|
||||
--cyan: var(--gpui-primary);
|
||||
--ocean-deep: var(--gpui-stage-deep);
|
||||
--ocean-mid: var(--gpui-stage-muted);
|
||||
--ocean-current: var(--gpui-primary-dark);
|
||||
--ocean-mist: var(--gpui-stage-ink-soft);
|
||||
--ocean-coral: #8f5a56;
|
||||
--signal-gold: #7d6a3f;
|
||||
--docs-accent: var(--gpui-primary);
|
||||
--docs-button-bg: var(--gpui-paper-raised);
|
||||
--docs-button-border: var(--gpui-edge);
|
||||
--docs-button-text: var(--gpui-ink);
|
||||
--stage-text: var(--gpui-stage-ink);
|
||||
--stage-soft: var(--gpui-stage-ink-soft);
|
||||
--stage-muted: var(--gpui-stage-ink-dim);
|
||||
--stage-field-top: var(--gpui-stage-muted);
|
||||
--stage-field-mid: var(--gpui-stage);
|
||||
--stage-field-deep: var(--gpui-stage-deep);
|
||||
--stage-ambient: var(--gpui-accent-dark);
|
||||
--stage-composer: var(--gpui-stage-muted);
|
||||
--stage-elevated: var(--gpui-stage-raised);
|
||||
--stage-line: var(--gpui-stage-edge);
|
||||
--stage-hint: var(--gpui-stage-ink-dim);
|
||||
--stage-dim: var(--gpui-stage-ink-soft);
|
||||
--violet: var(--gpui-selection-dark);
|
||||
--hairline: var(--gpui-light-border);
|
||||
--code-bg: var(--gpui-light-sidebar);
|
||||
--code-fg: var(--gpui-light-text);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* ---------- dark — graphite; the OS-dark and pinned blocks match ---------- */
|
||||
/* ---------- dark — the navy ocean; the OS-dark and pinned blocks match ---------- */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: var(--gpui-dark-bg);
|
||||
--surface: var(--gpui-dark-sidebar);
|
||||
--panel: var(--gpui-dark-panel);
|
||||
--bg: var(--ocean-floor);
|
||||
--surface: var(--ocean-abyss);
|
||||
--panel: var(--ocean-raised);
|
||||
--text: var(--gpui-dark-text);
|
||||
--muted: var(--gpui-dark-text-muted);
|
||||
--line: var(--gpui-dark-border);
|
||||
--line-strong: var(--gpui-dark-border-strong);
|
||||
--line: var(--ocean-line);
|
||||
--line-strong: var(--ocean-line-strong);
|
||||
--accent: var(--gpui-dark-primary);
|
||||
--accent-hover: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--gpui-dark-on-primary);
|
||||
--hover: var(--gpui-dark-accent);
|
||||
--selected: var(--gpui-dark-list-active);
|
||||
--hover: var(--ocean-hover);
|
||||
--selected: var(--ocean-selected);
|
||||
--selection: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-selection-opacity));
|
||||
--ring: var(--gpui-dark-primary);
|
||||
--live: var(--gpui-dark-live);
|
||||
--success: var(--gpui-dark-live);
|
||||
--attention: var(--gpui-dark-attention);
|
||||
--danger: var(--gpui-dark-danger);
|
||||
--on-danger: var(--gpui-dark-bg);
|
||||
--diff-add: rgb(36 61 69 / 0.22);
|
||||
--diff-del: rgb(116 72 84 / 0.15);
|
||||
--mark-ink: var(--gpui-dark-text);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 18%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 42%, transparent);
|
||||
--glow-focus: 0 0 14px 2px rgb(var(--gpui-dark-primary-rgb) / 0.32);
|
||||
--diff-add: rgb(80 170 140 / 0.16);
|
||||
--diff-del: rgb(227 154 144 / 0.12);
|
||||
--ground: radial-gradient(90rem 38rem at 50% -14rem, rgb(30 143 216 / 0.3), transparent 70%), linear-gradient(180deg, var(--ocean-sky) 0, var(--ocean-floor) 34rem);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 20%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 45%, transparent);
|
||||
--glow-focus: 0 0 14px 2px rgb(var(--gpui-dark-primary-rgb) / 0.35);
|
||||
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.4);
|
||||
--horizon-edge: var(--gpui-dark-border);
|
||||
|
||||
--c-paper: var(--gpui-dark-bg-rgb);
|
||||
--c-paper-deep: var(--gpui-dark-panel-rgb);
|
||||
--c-paper-edge: var(--gpui-dark-border-rgb);
|
||||
--c-ink: var(--gpui-dark-text-rgb);
|
||||
--c-ink-soft: var(--gpui-dark-text-muted-rgb);
|
||||
--c-ink-mute: var(--gpui-dark-text-muted-rgb);
|
||||
--c-indigo: var(--gpui-dark-primary-rgb);
|
||||
--paper: var(--gpui-stage);
|
||||
--paper-deep: var(--gpui-stage-muted);
|
||||
--paper-edge: var(--gpui-stage-edge);
|
||||
--paper-card: var(--gpui-stage-muted);
|
||||
--paper-line: var(--gpui-stage-edge);
|
||||
--paper-line-soft: var(--gpui-stage-edge);
|
||||
--ink: var(--gpui-stage-ink);
|
||||
--ink-soft: var(--gpui-stage-ink-soft);
|
||||
--ink-mute: var(--gpui-stage-ink-dim);
|
||||
--indigo: var(--gpui-primary-dark);
|
||||
--indigo-deep: var(--gpui-primary-hover-dark);
|
||||
--indigo-pale: rgb(var(--c-primary-dark) / 0.14);
|
||||
--ochre: #d6c78f;
|
||||
--jade: #9ec7b2;
|
||||
--cyan: var(--gpui-primary-dark);
|
||||
--ocean-coral: #d08a80;
|
||||
--signal-gold: #d6c78f;
|
||||
--docs-accent: var(--gpui-primary-dark);
|
||||
--docs-button-bg: var(--gpui-stage-muted);
|
||||
--docs-button-border: var(--gpui-stage-edge);
|
||||
--docs-button-text: var(--gpui-stage-ink);
|
||||
--hairline: var(--gpui-dark-border);
|
||||
--code-bg: var(--gpui-dark-sidebar);
|
||||
--code-fg: var(--gpui-dark-text);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--bg: var(--gpui-dark-bg);
|
||||
--surface: var(--gpui-dark-sidebar);
|
||||
--panel: var(--gpui-dark-panel);
|
||||
--bg: var(--ocean-floor);
|
||||
--surface: var(--ocean-abyss);
|
||||
--panel: var(--ocean-raised);
|
||||
--text: var(--gpui-dark-text);
|
||||
--muted: var(--gpui-dark-text-muted);
|
||||
--line: var(--gpui-dark-border);
|
||||
--line-strong: var(--gpui-dark-border-strong);
|
||||
--line: var(--ocean-line);
|
||||
--line-strong: var(--ocean-line-strong);
|
||||
--accent: var(--gpui-dark-primary);
|
||||
--accent-hover: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--gpui-dark-on-primary);
|
||||
--hover: var(--gpui-dark-accent);
|
||||
--selected: var(--gpui-dark-list-active);
|
||||
--hover: var(--ocean-hover);
|
||||
--selected: var(--ocean-selected);
|
||||
--selection: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-selection-opacity));
|
||||
--ring: var(--gpui-dark-primary);
|
||||
--live: var(--gpui-dark-live);
|
||||
--success: var(--gpui-dark-live);
|
||||
--attention: var(--gpui-dark-attention);
|
||||
--danger: var(--gpui-dark-danger);
|
||||
--on-danger: var(--gpui-dark-bg);
|
||||
--diff-add: rgb(36 61 69 / 0.22);
|
||||
--diff-del: rgb(116 72 84 / 0.15);
|
||||
--mark-ink: var(--gpui-dark-text);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 18%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 42%, transparent);
|
||||
--glow-focus: 0 0 14px 2px rgb(var(--gpui-dark-primary-rgb) / 0.32);
|
||||
--diff-add: rgb(80 170 140 / 0.16);
|
||||
--diff-del: rgb(227 154 144 / 0.12);
|
||||
--ground: radial-gradient(90rem 38rem at 50% -14rem, rgb(30 143 216 / 0.3), transparent 70%), linear-gradient(180deg, var(--ocean-sky) 0, var(--ocean-floor) 34rem);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 20%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 45%, transparent);
|
||||
--glow-focus: 0 0 14px 2px rgb(var(--gpui-dark-primary-rgb) / 0.35);
|
||||
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.4);
|
||||
--horizon-edge: var(--gpui-dark-border);
|
||||
|
||||
--c-paper: var(--gpui-dark-bg-rgb);
|
||||
--c-paper-deep: var(--gpui-dark-panel-rgb);
|
||||
--c-paper-edge: var(--gpui-dark-border-rgb);
|
||||
--c-ink: var(--gpui-dark-text-rgb);
|
||||
--c-ink-soft: var(--gpui-dark-text-muted-rgb);
|
||||
--c-ink-mute: var(--gpui-dark-text-muted-rgb);
|
||||
--c-indigo: var(--gpui-dark-primary-rgb);
|
||||
--paper: var(--gpui-stage);
|
||||
--paper-deep: var(--gpui-stage-muted);
|
||||
--paper-edge: var(--gpui-stage-edge);
|
||||
--paper-card: var(--gpui-stage-muted);
|
||||
--paper-line: var(--gpui-stage-edge);
|
||||
--paper-line-soft: var(--gpui-stage-edge);
|
||||
--ink: var(--gpui-stage-ink);
|
||||
--ink-soft: var(--gpui-stage-ink-soft);
|
||||
--ink-mute: var(--gpui-stage-ink-dim);
|
||||
--indigo: var(--gpui-primary-dark);
|
||||
--indigo-deep: var(--gpui-primary-hover-dark);
|
||||
--indigo-pale: rgb(var(--c-primary-dark) / 0.14);
|
||||
--ochre: #d6c78f;
|
||||
--jade: #9ec7b2;
|
||||
--cyan: var(--gpui-primary-dark);
|
||||
--ocean-coral: #d08a80;
|
||||
--signal-gold: #d6c78f;
|
||||
--docs-accent: var(--gpui-primary-dark);
|
||||
--docs-button-bg: var(--gpui-stage-muted);
|
||||
--docs-button-border: var(--gpui-stage-edge);
|
||||
--docs-button-text: var(--gpui-stage-ink);
|
||||
--hairline: var(--gpui-dark-border);
|
||||
--code-bg: var(--gpui-dark-sidebar);
|
||||
--code-fg: var(--gpui-dark-text);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ---------- midnight — the stage below the horizon ---------- */
|
||||
/* Always the dark set, in both appearances (MEASUREMENTS §5.1): the footer
|
||||
and any home descent. The ground is the deepest graphite, so raised and
|
||||
recessed plates step up from it. `.ocean-column` stays until the home
|
||||
slice retires it. --horizon-edge is deliberately not redeclared: it
|
||||
follows the page's appearance. */
|
||||
.midnight,
|
||||
.site-footer,
|
||||
.ocean-column {
|
||||
--bg: var(--gpui-dark-sidebar);
|
||||
--surface: var(--gpui-dark-bg);
|
||||
--panel: var(--gpui-dark-panel);
|
||||
/* ---------- the stage — below the horizon, in both appearances ---------- */
|
||||
/* The footer and the home sea. The ground is the deep water; raised and
|
||||
recessed plates step up and down from it. Legacy aliases are repeated
|
||||
here because a custom property that references another is computed where
|
||||
it is declared. */
|
||||
.stage,
|
||||
.site-footer {
|
||||
--bg: var(--ocean-floor);
|
||||
--surface: var(--ocean-abyss);
|
||||
--panel: var(--ocean-raised);
|
||||
--text: var(--gpui-dark-text);
|
||||
--muted: var(--gpui-dark-text-muted);
|
||||
--line: var(--gpui-dark-border);
|
||||
--line-strong: var(--gpui-dark-border-strong);
|
||||
--line: var(--ocean-line);
|
||||
--line-strong: var(--ocean-line-strong);
|
||||
--accent: var(--gpui-dark-primary);
|
||||
--accent-hover: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--gpui-dark-on-primary);
|
||||
--hover: var(--gpui-dark-accent);
|
||||
--selected: var(--gpui-dark-list-active);
|
||||
--hover: var(--ocean-hover);
|
||||
--selected: var(--ocean-selected);
|
||||
--selection: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-selection-opacity));
|
||||
--ring: var(--gpui-dark-primary);
|
||||
--live: var(--gpui-dark-live);
|
||||
--success: var(--gpui-dark-live);
|
||||
--attention: var(--gpui-dark-attention);
|
||||
--danger: var(--gpui-dark-danger);
|
||||
--on-danger: var(--gpui-dark-bg);
|
||||
--diff-add: rgb(36 61 69 / 0.22);
|
||||
--diff-del: rgb(116 72 84 / 0.15);
|
||||
--mark-ink: var(--gpui-dark-text);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 18%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 42%, transparent);
|
||||
--glow-focus: 0 0 14px 2px rgb(var(--gpui-dark-primary-rgb) / 0.32);
|
||||
--diff-add: rgb(80 170 140 / 0.16);
|
||||
--diff-del: rgb(227 154 144 / 0.12);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 20%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 45%, transparent);
|
||||
--glow-focus: 0 0 14px 2px rgb(var(--gpui-dark-primary-rgb) / 0.35);
|
||||
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.4);
|
||||
|
||||
--c-paper: var(--gpui-dark-sidebar-rgb);
|
||||
--c-paper-deep: var(--gpui-dark-panel-rgb);
|
||||
--c-paper-edge: var(--gpui-dark-border-rgb);
|
||||
--c-ink: var(--gpui-dark-text-rgb);
|
||||
--c-ink-soft: var(--gpui-dark-text-muted-rgb);
|
||||
--c-ink-mute: var(--gpui-dark-text-muted-rgb);
|
||||
--c-indigo: var(--gpui-dark-primary-rgb);
|
||||
--paper: var(--gpui-stage-deep);
|
||||
--paper-deep: var(--gpui-stage-muted);
|
||||
--paper-edge: var(--gpui-stage-edge);
|
||||
--paper-card: var(--gpui-stage-muted);
|
||||
--paper-line: var(--gpui-stage-edge);
|
||||
--paper-line-soft: var(--gpui-stage-edge);
|
||||
--ink: var(--gpui-stage-ink);
|
||||
--ink-soft: var(--gpui-stage-ink-soft);
|
||||
--ink-mute: var(--gpui-stage-ink-dim);
|
||||
--indigo: var(--gpui-primary-dark);
|
||||
--indigo-deep: var(--gpui-primary-hover-dark);
|
||||
--indigo-pale: rgb(var(--c-primary-dark) / 0.14);
|
||||
--ochre: #d6c78f;
|
||||
--jade: #9ec7b2;
|
||||
--cyan: var(--gpui-primary-dark);
|
||||
--ocean-coral: #d08a80;
|
||||
--signal-gold: #d6c78f;
|
||||
--docs-accent: var(--gpui-primary-dark);
|
||||
--docs-button-bg: var(--gpui-stage-muted);
|
||||
--docs-button-border: var(--gpui-stage-edge);
|
||||
--docs-button-text: var(--gpui-stage-ink);
|
||||
--hairline: var(--gpui-dark-border);
|
||||
--code-bg: var(--gpui-dark-bg);
|
||||
--code-fg: var(--gpui-dark-text);
|
||||
--paper: var(--bg);
|
||||
--paper-deep: var(--surface);
|
||||
--paper-card: var(--panel);
|
||||
--paper-edge: var(--line);
|
||||
--paper-line: var(--line);
|
||||
--paper-line-soft: var(--line);
|
||||
--hairline: var(--line);
|
||||
--ink: var(--text);
|
||||
--ink-soft: var(--muted);
|
||||
--ink-mute: var(--muted);
|
||||
--indigo: var(--accent);
|
||||
--indigo-deep: var(--accent-hover);
|
||||
--indigo-pale: color-mix(in srgb, var(--accent) 14%, transparent);
|
||||
--docs-accent: var(--accent);
|
||||
--code-bg: var(--surface);
|
||||
--code-fg: var(--text);
|
||||
color-scheme: dark;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ---------- legacy names ---------- */
|
||||
/* Pages and partials that have not moved to the roles (the docs) read these.
|
||||
Declared once on :root, they resolve against whichever scheme block wins
|
||||
there; `.stage` repeats them. Removed in the post-renovation sweep. */
|
||||
:root {
|
||||
--paper: var(--bg);
|
||||
--paper-deep: var(--surface);
|
||||
--paper-card: var(--panel);
|
||||
--paper-edge: var(--line);
|
||||
--paper-line: var(--line);
|
||||
--paper-line-soft: var(--line);
|
||||
--hairline: var(--line);
|
||||
--ink: var(--text);
|
||||
--ink-soft: var(--muted);
|
||||
--ink-mute: var(--muted);
|
||||
--indigo: var(--accent);
|
||||
--indigo-deep: var(--accent-hover);
|
||||
--indigo-pale: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
--cobalt: var(--accent);
|
||||
--cyan: var(--accent);
|
||||
--ochre: var(--attention);
|
||||
--signal-gold: var(--attention);
|
||||
--jade: var(--live);
|
||||
--seafoam: var(--live);
|
||||
--ocean-coral: var(--danger);
|
||||
--docs-accent: var(--accent);
|
||||
--docs-button-bg: var(--panel);
|
||||
--docs-button-border: var(--line);
|
||||
--docs-button-text: var(--text);
|
||||
--code-bg: var(--surface);
|
||||
--code-fg: var(--text);
|
||||
--stage-line: var(--line);
|
||||
--stage-dim: var(--muted);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Utilities — focus, selection, links, the mobile sheet's motion */
|
||||
/* Utilities — focus, selection, links, sheet motion */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* ---------- focus + selection ---------- */
|
||||
/* The one focus treatment on the site: a 2px ring at a 3px offset in the
|
||||
accent of whatever depth the control sits in (midnight re-declares
|
||||
accent of whatever depth the control sits in (the stage re-declares
|
||||
--ring). Components may add the bioluminescent halo (--glow-focus) as a
|
||||
box-shadow; none restyles or removes the ring. */
|
||||
::selection { background: var(--selection); }
|
||||
@@ -16,7 +16,8 @@ a { color: inherit; text-decoration: none; }
|
||||
/* A link in running text: accent ink, a quiet underline that firms on
|
||||
hover. `.body-link` is the legacy name for the same recipe. */
|
||||
.link,
|
||||
a.body-link {
|
||||
a.body-link,
|
||||
.prose a {
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
@@ -25,7 +26,9 @@ a.body-link {
|
||||
transition: text-decoration-color var(--dur-state) var(--ease-out);
|
||||
}
|
||||
.link:hover,
|
||||
a.body-link:hover { text-decoration-color: var(--accent); }
|
||||
a.body-link:hover,
|
||||
.prose a:hover { text-decoration-color: var(--accent); }
|
||||
.link .icon { margin-inline-start: 0.25em; }
|
||||
|
||||
/* ---------- the mobile sheet ---------- */
|
||||
/* The sheet arrives (opacity plus a 4px rise) and leaves faster than it
|
||||
@@ -39,6 +42,11 @@ a.body-link:hover { text-decoration-color: var(--accent); }
|
||||
animation: mm-out var(--dur-exit) var(--ease-in) both;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Content that arrives after an action (a retried feed, a result set). */
|
||||
.arrive {
|
||||
animation: mm-in var(--dur-arrive) var(--ease-out) both;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mm-in {
|
||||
@@ -57,9 +65,7 @@ a.body-link:hover { text-decoration-color: var(--accent); }
|
||||
.site-github-link { display: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.site-wordmark { font-size: 1.05rem; }
|
||||
}
|
||||
|
||||
/* Anchor fallback; page-level scroll-mt utilities can override it. */
|
||||
:where([id]) { scroll-margin-top: 5rem; }
|
||||
|
||||
[dir="rtl"] .icon-flip { transform: scaleX(-1); }
|
||||
@@ -267,6 +267,12 @@
|
||||
--gpui-dark-list-active-rgb: 55 57 61;
|
||||
--gpui-dark-attention: #e8b077;
|
||||
--gpui-dark-attention-rgb: 232 176 119;
|
||||
--gpui-dark-live: #9ec7b2;
|
||||
--gpui-dark-live-rgb: 158 199 178;
|
||||
--gpui-dark-danger: #e39a90;
|
||||
--gpui-dark-danger-rgb: 227 154 144;
|
||||
--gpui-dark-border-strong: #828386;
|
||||
--gpui-dark-border-strong-rgb: 130 131 134;
|
||||
--gpui-light-bg: #faf8f5;
|
||||
--gpui-light-bg-rgb: 250 248 245;
|
||||
--gpui-light-text: #28292b;
|
||||
@@ -289,11 +295,18 @@
|
||||
--gpui-light-list-active-rgb: 223 220 214;
|
||||
--gpui-light-attention: #86520d;
|
||||
--gpui-light-attention-rgb: 134 82 13;
|
||||
--gpui-light-live: #3a6352;
|
||||
--gpui-light-live-rgb: 58 99 82;
|
||||
--gpui-light-danger: #9e3f36;
|
||||
--gpui-light-danger-rgb: 158 63 54;
|
||||
--gpui-light-border-strong: #807c76;
|
||||
--gpui-light-border-strong-rgb: 128 124 118;
|
||||
}
|
||||
/* GPUI design 1.0.0; sha256 a0c23f8a235bbc61e42fb8a60795ae7d9825dbbd1730142acc9ffec9c0abb3af. */
|
||||
/* GPUI design 1.1.1; sha256 c08d7d4b253afe340207658b1f233dd1a9dbe94fa57e925bde025a3db70543ee. */
|
||||
:root {
|
||||
--gpui-radius-control: 6px;
|
||||
--gpui-radius-panel: 10px;
|
||||
--gpui-radius-sheet: 14px;
|
||||
--gpui-focus-width: 2px;
|
||||
--gpui-focus-offset: 3px;
|
||||
--gpui-spacing-compact: 4px;
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useState, useMemo, useRef, useCallback, useEffect } from "react";
|
||||
import { faqSourceHref } from "@/lib/faq-source";
|
||||
import { extractText } from "@/lib/react-text";
|
||||
import { highlightSpan } from "@/lib/search-utils";
|
||||
import { Icon } from "./icon";
|
||||
import { WhalePose } from "./whale-pose";
|
||||
|
||||
export interface FaqSearchItem {
|
||||
q: string;
|
||||
@@ -86,100 +88,81 @@ export function FaqSearch({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Search bar */}
|
||||
<div className="mb-6">
|
||||
<div className="relative">
|
||||
<div className="faq-search">
|
||||
<div className="search-field">
|
||||
<Icon name="search" className="search-field-icon" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={
|
||||
isZh
|
||||
? "搜索常见问题…(按 / 快速聚焦)"
|
||||
: "Search FAQ… (press / to focus)"
|
||||
}
|
||||
className="search-input w-full"
|
||||
placeholder={isZh ? "搜索常见问题" : "Search the FAQ"}
|
||||
className="search-input"
|
||||
aria-label={isZh ? "搜索常见问题" : "Search FAQ"}
|
||||
aria-keyshortcuts="/"
|
||||
/>
|
||||
{hasQuery && (
|
||||
{hasQuery ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setQuery("")}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 font-mono text-sm text-ink-mute hover:text-indigo transition-colors"
|
||||
className="nav-icon-button nav-icon-button-sm search-field-clear"
|
||||
aria-label={isZh ? "清除" : "Clear"}
|
||||
title={isZh ? "清除" : "Clear"}
|
||||
>
|
||||
✕
|
||||
<Icon name="x" className="nav-icon" />
|
||||
</button>
|
||||
) : (
|
||||
<kbd className="search-field-kbd" aria-hidden="true">/</kbd>
|
||||
)}
|
||||
</div>
|
||||
{hasQuery && (
|
||||
<div className="mt-2 font-mono text-[0.7rem] text-ink-mute" aria-live="polite">
|
||||
{matched > 0
|
||||
<p className="faq-search-count" aria-live="polite">
|
||||
{hasQuery
|
||||
? matched > 0
|
||||
? isZh
|
||||
? `${matched} / ${total} 个问题匹配 "${query.trim()}"`
|
||||
: `${matched} of ${total} questions match "${query.trim()}"`
|
||||
: isZh
|
||||
? `未找到匹配 "${query.trim()}" 的问题`
|
||||
: `No questions match "${query.trim()}"`}
|
||||
</div>
|
||||
)}
|
||||
: `No questions match "${query.trim()}"`
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* FAQ list */}
|
||||
{matched > 0 ? (
|
||||
<div className="space-y-0 hairline-t hairline-b">
|
||||
<div className="group-card">
|
||||
{filtered.map(({ item, i }) => (
|
||||
<details key={i} className="group hairline-b last:border-b-0">
|
||||
<summary className="px-0 py-5 cursor-pointer flex items-start gap-4 hover:text-indigo transition-colors">
|
||||
<span className="font-mono text-indigo tabular text-sm pt-0.5 shrink-0">
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<span className="font-display text-lg leading-snug flex-1">
|
||||
{highlight(item.q, query)}
|
||||
</span>
|
||||
<span className="font-mono text-ink-mute text-sm group-open:rotate-45 transition-transform shrink-0">+</span>
|
||||
<details key={i} className="disclosure-row">
|
||||
<summary>
|
||||
<span className="faq-index tabular" aria-hidden="true">{i + 1}</span>
|
||||
<span className="faq-question">{highlight(item.q, query)}</span>
|
||||
<Icon name="chevron-down" className="disclosure-chevron" />
|
||||
</summary>
|
||||
<div className="pb-5 pl-10 pr-4">
|
||||
<div className={`text-ink-soft leading-relaxed ${isZh ? "leading-[1.9] tracking-wide" : ""}`}>
|
||||
{item.a}
|
||||
</div>
|
||||
<div className="disclosure-body prose faq-answer">
|
||||
<div>{item.a}</div>
|
||||
{item.sources && item.sources.length > 0 && (
|
||||
<div className="mt-3 flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-ink-mute">
|
||||
{isZh ? "来源" : "Sources"}:
|
||||
</span>
|
||||
<p className="faq-sources">
|
||||
<span>{isZh ? "来源" : "Sources"}:</span>
|
||||
{item.sources.map((s) => {
|
||||
const href = faqSourceHref(s);
|
||||
return href ? (
|
||||
<a
|
||||
key={s}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-mono text-[0.7rem] text-indigo hover:underline"
|
||||
>
|
||||
<a key={s} href={href} target="_blank" rel="noreferrer">
|
||||
{s}
|
||||
</a>
|
||||
) : (
|
||||
<span key={s} className="font-mono text-[0.7rem] text-indigo">{s}</span>
|
||||
<code key={s}>{s}</code>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16 hairline-t hairline-b">
|
||||
<p className="font-display text-lg text-ink-mute mb-2">
|
||||
{isZh ? "未找到结果" : "No results found"}
|
||||
</p>
|
||||
<p className="text-sm text-ink-mute">
|
||||
{isZh
|
||||
? "尝试使用不同的关键字。"
|
||||
: "Try a different keyword."}
|
||||
</p>
|
||||
<div className="empty-state" role="status">
|
||||
<WhalePose pose="search" />
|
||||
<p className="empty-state-title">{isZh ? "未找到结果" : "No results found"}</p>
|
||||
<p className="empty-state-reason">{isZh ? "尝试使用不同的关键字。" : "Try a different keyword."}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,57 +1,44 @@
|
||||
import Link from "next/link";
|
||||
import type { FeedItem } from "@/lib/types";
|
||||
import { relativeTime } from "@/lib/github";
|
||||
import { Status, type StatusTone } from "./status-badge";
|
||||
|
||||
const KIND_LABEL: Record<FeedItem["kind"], { label: string; cn: string }> = {
|
||||
issue: { label: "Issue", cn: "议题" },
|
||||
pull: { label: "Pull", cn: "合并" },
|
||||
release: { label: "Release", cn: "发布" },
|
||||
discussion: { label: "Talk", cn: "讨论" },
|
||||
const KIND_LABEL: Record<FeedItem["kind"], string> = {
|
||||
issue: "Issue",
|
||||
pull: "Pull request",
|
||||
release: "Release",
|
||||
discussion: "Discussion",
|
||||
};
|
||||
|
||||
function statePill(state: FeedItem["state"]) {
|
||||
const map: Record<FeedItem["state"], string> = {
|
||||
open: "pill pill-jade",
|
||||
closed: "pill pill-ghost",
|
||||
merged: "pill pill-hot",
|
||||
draft: "pill pill-ghost",
|
||||
published: "pill pill-ochre",
|
||||
};
|
||||
return <span className={map[state]}>{state}</span>;
|
||||
}
|
||||
// State is a mark and a word; the word is GitHub's own state.
|
||||
const STATE_TONE: Record<FeedItem["state"], StatusTone> = {
|
||||
open: "ready",
|
||||
closed: "idle",
|
||||
merged: "accent",
|
||||
draft: "idle",
|
||||
published: "ready",
|
||||
};
|
||||
|
||||
export function FeedCard({ item, dense = false }: { item: FeedItem; dense?: boolean }) {
|
||||
const k = KIND_LABEL[item.kind];
|
||||
/** One GitHub item: its state, title, labels and who opened it. */
|
||||
export function FeedCard({ item }: { item: FeedItem; dense?: boolean }) {
|
||||
return (
|
||||
<article className={`hairline-b py-4 ${dense ? "" : "px-1"}`}>
|
||||
<div className="flex items-baseline gap-3 mb-1.5">
|
||||
<span className="text-xs text-indigo">
|
||||
{k.label} <span className="font-cjk text-ink-mute normal-case tracking-normal ml-1">{k.cn}</span>
|
||||
</span>
|
||||
<span className="font-mono text-[0.7rem] text-ink-mute tabular">#{item.number}</span>
|
||||
<span className="ml-auto font-mono text-[0.7rem] text-ink-mute tabular">{relativeTime(item.updatedAt)}</span>
|
||||
<article className="feed-card">
|
||||
<div className="feed-card-meta">
|
||||
<Status tone={STATE_TONE[item.state]}>{item.state}</Status>
|
||||
<span>{KIND_LABEL[item.kind]}</span>
|
||||
<span className="tabular">#{item.number}</span>
|
||||
<span className="feed-card-time tabular">{relativeTime(item.updatedAt)}</span>
|
||||
</div>
|
||||
|
||||
<h3 className="font-display text-base leading-snug">
|
||||
<Link href={item.url} className="hover:text-indigo transition-colors">
|
||||
{item.title}
|
||||
</Link>
|
||||
<h3 className="feed-card-title">
|
||||
<Link href={item.url}>{item.title}</Link>
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-3 mt-2 flex-wrap">
|
||||
{statePill(item.state)}
|
||||
<div className="feed-card-foot">
|
||||
{item.labels.slice(0, 3).map((l) => (
|
||||
<span
|
||||
key={l.name}
|
||||
className="pill pill-ghost"
|
||||
style={{ borderColor: `#${l.color}`, color: `#${l.color}` }}
|
||||
>
|
||||
{l.name}
|
||||
</span>
|
||||
<span key={l.name} className="pill">{l.name}</span>
|
||||
))}
|
||||
<span className="ml-auto flex items-center gap-2 font-mono text-[0.7rem] text-ink-mute">
|
||||
<span>@{item.author}</span>
|
||||
{item.comments > 0 && <span className="tabular">· {item.comments} reply{item.comments === 1 ? "" : "s"}</span>}
|
||||
<span className="feed-card-author">
|
||||
@{item.author}
|
||||
{item.comments > 0 && <span className="tabular"> · {item.comments} {item.comments === 1 ? "reply" : "replies"}</span>}
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -9,15 +9,14 @@ import {
|
||||
REPO_URL,
|
||||
} from "@/lib/i18n/links";
|
||||
import { SITE_CONTACT_EMAIL, SITE_SECURITY_EMAIL } from "@/lib/page-meta";
|
||||
import { Strata } from "./strata";
|
||||
import { USAGE_COUNTING_COPY } from "@/lib/content/usage-counting";
|
||||
import { pickText } from "@/lib/i18n/dictionaries";
|
||||
import { WhalePose } from "./whale-pose";
|
||||
|
||||
/**
|
||||
* Site footer. One dictionary path for every routed locale — the previous
|
||||
* en / zh / dictionary triple branch is gone, and the Product column now
|
||||
* carries the full six-link set everywhere (footerGuide and footerFaq exist
|
||||
* in ChromeDict as of #4934).
|
||||
* Site footer: the sea below the page's one horizon, in both appearances.
|
||||
* One dictionary path for every routed locale; the Product column carries
|
||||
* the full link set everywhere.
|
||||
*/
|
||||
export function Footer({ locale = "en" }: { locale?: Locale }) {
|
||||
const chrome = getChrome(locale);
|
||||
@@ -28,28 +27,29 @@ export function Footer({ locale = "en" }: { locale?: Locale }) {
|
||||
|
||||
return (
|
||||
<footer className="site-footer">
|
||||
{/* The waterline: every page still on paper descends here into the
|
||||
seabed. The homepage is already under water by now, and the
|
||||
stylesheet hides this band behind its ocean column. */}
|
||||
<div className="site-footer-waterline" aria-hidden="true">
|
||||
<Strata variant="band" />
|
||||
</div>
|
||||
<div className="horizon" aria-hidden="true" />
|
||||
<div className="sea-texture" aria-hidden="true" />
|
||||
<div className="site-footer-main">
|
||||
<div className="site-footer-brand">
|
||||
<Link href={homeHref} className="site-wordmark site-wordmark-footer">
|
||||
<img src="/brand/wordmark-inverted.svg" alt="Codewhale" height={18} />
|
||||
<Link href={homeHref} className="site-wordmark site-wordmark-footer" aria-label="Codewhale">
|
||||
<WhalePose pose="rest" />
|
||||
<span className="wordmark" aria-hidden="true" />
|
||||
</Link>
|
||||
<p>{chrome.footerTagline}</p>
|
||||
</div>
|
||||
|
||||
<div className="site-footer-links">
|
||||
<div>
|
||||
<span>{chrome.footerProduct}</span>
|
||||
{product.map((item) => <Link key={item.href} href={item.href}>{item.label}</Link>)}
|
||||
<div className="site-footer-group">
|
||||
<span className="site-footer-label">{chrome.footerProduct}</span>
|
||||
<div className="site-footer-list">
|
||||
{product.map((item) => <Link key={item.href} href={item.href}>{item.label}</Link>)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>{chrome.footerProject}</span>
|
||||
{project.map((item) => <Link key={item.href} href={item.href}>{item.label}</Link>)}
|
||||
<div className="site-footer-group">
|
||||
<span className="site-footer-label">{chrome.footerProject}</span>
|
||||
<div className="site-footer-list">
|
||||
{project.map((item) => <Link key={item.href} href={item.href}>{item.label}</Link>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,7 +61,7 @@ export function Footer({ locale = "en" }: { locale?: Locale }) {
|
||||
{chrome.footerReleases}
|
||||
<a href={REPO_RELEASES_URL}>{chrome.footerReleasesLink}</a>
|
||||
</p>
|
||||
<div>
|
||||
<div className="site-footer-meta-links">
|
||||
{legal.map((item) => <Link key={item.href} href={item.href}>{item.label}</Link>)}
|
||||
{GITEE_ENABLED && <a href="https://gitee.com/Hmbown/CodeWhale">Gitee</a>}
|
||||
<a href="https://cnb.cool/codewhale.net/codewhale">CNB</a>
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
* web/lib/content/getting-started.ts: install → provider connection →
|
||||
* first task → optional Fleet setup.
|
||||
*
|
||||
* Used by the homepage band and the /docs/guide page so the path reads
|
||||
* Used by the homepage and the /docs/guide page so the path reads
|
||||
* identically in both places. Server component, SSG-safe.
|
||||
*/
|
||||
|
||||
import Link from "next/link";
|
||||
import { GETTING_STARTED_STEPS } from "@/lib/content/getting-started";
|
||||
import { pickText } from "@/lib/i18n/dictionaries";
|
||||
import { Icon } from "./icon";
|
||||
|
||||
export function GettingStartedSteps({ locale = "en" }: { locale?: string }) {
|
||||
return (
|
||||
@@ -17,7 +18,7 @@ export function GettingStartedSteps({ locale = "en" }: { locale?: string }) {
|
||||
{GETTING_STARTED_STEPS.map((step, index) => (
|
||||
<li key={step.id} data-step-id={step.id}>
|
||||
<span className="gs-step-index" aria-hidden="true">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
{index + 1}
|
||||
</span>
|
||||
<h3>{pickText(step.title, locale)}</h3>
|
||||
<p>{pickText(step.body, locale)}</p>
|
||||
@@ -25,7 +26,8 @@ export function GettingStartedSteps({ locale = "en" }: { locale?: string }) {
|
||||
<pre className="code-block gs-step-commands"><code>{step.commands.join("\n")}</code></pre>
|
||||
)}
|
||||
<Link href={`/${locale}${step.link.href}`} className="gs-step-link">
|
||||
{pickText(step.link.label, locale)} →
|
||||
{pickText(step.link.label, locale)}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -5,10 +5,11 @@ import { INSTALL_COMMANDS } from "@/lib/content/install";
|
||||
import { InstallCodeBlock } from "./install-code-block";
|
||||
|
||||
/**
|
||||
* The hero's copyable install: one segmented choice between the checked
|
||||
* shell installer (macOS and Linux) and npm (any platform with Node 18+),
|
||||
* above the same copy block the install page uses. The option labels are
|
||||
* code-owned proper nouns; only the group's accessible name is translated.
|
||||
* The hero's copyable install plate: one segmented choice between the
|
||||
* checked shell installer (macOS and Linux) and npm (any platform with
|
||||
* Node 18+), above the same copy block the install page uses. The option
|
||||
* labels are code-owned proper nouns; only the group's accessible name is
|
||||
* translated.
|
||||
*/
|
||||
const OPTIONS = [
|
||||
{ id: "shell", label: "macOS · Linux", cmd: INSTALL_COMMANDS.shell },
|
||||
@@ -28,8 +29,8 @@ export function HeroInstall({
|
||||
const option = OPTIONS.find((o) => o.id === selected) ?? OPTIONS[0];
|
||||
|
||||
return (
|
||||
<div className="hero-install">
|
||||
<div className="hero-install-switch" role="group" aria-label={ariaLabel}>
|
||||
<div className="hero-install plate">
|
||||
<div className="segmented" role="group" aria-label={ariaLabel}>
|
||||
{OPTIONS.map((o) => (
|
||||
<button
|
||||
key={o.id}
|
||||
|
||||
@@ -56,6 +56,154 @@ const GLYPHS = {
|
||||
</>
|
||||
),
|
||||
"chevron-right": <path d="m9 18 6-6-6-6" />,
|
||||
"chevron-down": <path d="m6 9 6 6 6-6" />,
|
||||
"arrow-right": (
|
||||
<>
|
||||
<path d="M5 12h14" />
|
||||
<path d="m12 5 7 7-7 7" />
|
||||
</>
|
||||
),
|
||||
external: (
|
||||
<>
|
||||
<path d="M15 3h6v6" />
|
||||
<path d="M10 14 21 3" />
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||
</>
|
||||
),
|
||||
search: (
|
||||
<>
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<path d="m20 20-3.5-3.5" />
|
||||
</>
|
||||
),
|
||||
copy: (
|
||||
<>
|
||||
<rect width="13" height="13" x="8" y="8" rx="2" />
|
||||
<path d="M4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2" />
|
||||
</>
|
||||
),
|
||||
check: <path d="M20 6 9 17l-5-5" />,
|
||||
terminal: (
|
||||
<>
|
||||
<path d="m4 17 6-6-6-6" />
|
||||
<path d="M12 19h8" />
|
||||
</>
|
||||
),
|
||||
package: (
|
||||
<>
|
||||
<path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
|
||||
<path d="M12 22V12" />
|
||||
<path d="m3.3 7 8.7 5 8.7-5" />
|
||||
</>
|
||||
),
|
||||
plug: (
|
||||
<>
|
||||
<path d="M12 22v-5" />
|
||||
<path d="M9 8V2" />
|
||||
<path d="M15 8V2" />
|
||||
<path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z" />
|
||||
</>
|
||||
),
|
||||
book: (
|
||||
<>
|
||||
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" />
|
||||
</>
|
||||
),
|
||||
"git-pull-request": (
|
||||
<>
|
||||
<circle cx="18" cy="18" r="3" />
|
||||
<circle cx="6" cy="6" r="3" />
|
||||
<path d="M13 6h3a2 2 0 0 1 2 2v7" />
|
||||
<path d="M6 9v12" />
|
||||
</>
|
||||
),
|
||||
message: <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />,
|
||||
shield: <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />,
|
||||
cpu: (
|
||||
<>
|
||||
<rect width="16" height="16" x="4" y="4" rx="2" />
|
||||
<rect width="6" height="6" x="9" y="9" rx="1" />
|
||||
<path d="M15 2v2M15 20v2M2 15h2M2 9h2M20 15h2M20 9h2M9 2v2M9 20v2" />
|
||||
</>
|
||||
),
|
||||
repeat: (
|
||||
<>
|
||||
<path d="m17 2 4 4-4 4" />
|
||||
<path d="M3 11v-1a4 4 0 0 1 4-4h14" />
|
||||
<path d="m7 22-4-4 4-4" />
|
||||
<path d="M21 13v1a4 4 0 0 1-4 4H3" />
|
||||
</>
|
||||
),
|
||||
layers: (
|
||||
<>
|
||||
<path d="m12 2 10 5-10 5L2 7z" />
|
||||
<path d="m2 17 10 5 10-5" />
|
||||
<path d="m2 12 10 5 10-5" />
|
||||
</>
|
||||
),
|
||||
folder: <path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />,
|
||||
users: (
|
||||
<>
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</>
|
||||
),
|
||||
key: (
|
||||
<>
|
||||
<circle cx="7.5" cy="15.5" r="5.5" />
|
||||
<path d="m21 2-9.6 9.6" />
|
||||
<path d="m15.5 7.5 3 3L22 7l-3-3" />
|
||||
</>
|
||||
),
|
||||
alert: (
|
||||
<>
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</>
|
||||
),
|
||||
info: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 16v-4" />
|
||||
<path d="M12 8h.01" />
|
||||
</>
|
||||
),
|
||||
/* The sea's small icons (Kare-style, each always beside a word):
|
||||
sonar = searching, tide = real progress, shell = a receipt,
|
||||
kelp = a divider. */
|
||||
sonar: (
|
||||
<>
|
||||
<circle cx="5.5" cy="12" r="1.8" />
|
||||
<path d="M9.5 8.5a5 5 0 0 1 0 7" />
|
||||
<path d="M13 5.5a9.2 9.2 0 0 1 0 13" />
|
||||
<path d="M16.5 2.5a13.5 13.5 0 0 1 0 19" />
|
||||
</>
|
||||
),
|
||||
tide: (
|
||||
<>
|
||||
<path d="M2 7c1.7-1.6 3.3-1.6 5 0s3.3 1.6 5 0 3.3-1.6 5 0 3.3 1.6 5 0" />
|
||||
<path d="M2 12c1.7-1.6 3.3-1.6 5 0s3.3 1.6 5 0 3.3-1.6 5 0 3.3 1.6 5 0" />
|
||||
<path d="M2 17c1.7-1.6 3.3-1.6 5 0s3.3 1.6 5 0 3.3-1.6 5 0 3.3 1.6 5 0" />
|
||||
</>
|
||||
),
|
||||
shell: (
|
||||
<>
|
||||
<path d="M12 4C7 4 3 7.6 3 12.2c0 1.6.6 3 1.6 4.3L7 20h10l2.4-3.5c1-1.3 1.6-2.7 1.6-4.3C21 7.6 17 4 12 4Z" />
|
||||
<path d="M12 20V6" />
|
||||
<path d="m9 20-2.4-12" />
|
||||
<path d="m15 20 2.4-12" />
|
||||
<path d="M9 20v1.5h6V20" />
|
||||
</>
|
||||
),
|
||||
kelp: (
|
||||
<>
|
||||
<path d="M12 22c0-4 3-5.5 3-9.5S12 7 12 3" />
|
||||
<path d="M12 22c0-3-3.5-4-3.5-7.5 0-2 1.2-3.2 2-4.5" />
|
||||
</>
|
||||
),
|
||||
} satisfies Record<string, JSX.Element>;
|
||||
|
||||
export type IconName = keyof typeof GLYPHS;
|
||||
@@ -64,6 +212,8 @@ export function Icon({ name, className = "icon" }: { name: IconName; className?:
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
detectFromBrowserSignals,
|
||||
type Arch,
|
||||
type UserAgentArchitecture,
|
||||
} from "@/lib/install-platform";
|
||||
import { SNIPPETS, VERIFY } from "@/lib/install-binary-snippets";
|
||||
import { InstallCodeBlock } from "./install-code-block";
|
||||
|
||||
const LABELS: Record<Arch, string> = {
|
||||
"macos-arm64": "macOS · Apple Silicon",
|
||||
"macos-x64": "macOS · Intel",
|
||||
"linux-x64": "Linux · x64",
|
||||
"linux-arm64": "Linux · arm64",
|
||||
"windows-x64": "Windows · x64",
|
||||
"windows-arm64": "Windows · arm64",
|
||||
};
|
||||
|
||||
interface NavigatorWithUserAgentData extends Navigator {
|
||||
userAgentData?: {
|
||||
getHighEntropyValues(hints: string[]): Promise<UserAgentArchitecture>;
|
||||
};
|
||||
}
|
||||
|
||||
async function detect(): Promise<Arch> {
|
||||
if (typeof navigator === "undefined") return "macos-arm64";
|
||||
const browserNavigator = navigator as NavigatorWithUserAgentData;
|
||||
let architecture: UserAgentArchitecture | undefined;
|
||||
if (navigator.userAgent.toLowerCase().includes("win")) {
|
||||
try {
|
||||
architecture = await browserNavigator.userAgentData?.getHighEntropyValues([
|
||||
"architecture",
|
||||
"bitness",
|
||||
]);
|
||||
} catch {
|
||||
// The manual platform buttons and frozen-UA fallback remain available.
|
||||
}
|
||||
}
|
||||
return detectFromBrowserSignals(navigator.userAgent, architecture);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
copyLabel?: string;
|
||||
copiedLabel?: string;
|
||||
verifyHeading?: string;
|
||||
}
|
||||
|
||||
export function InstallBinary({ copyLabel, copiedLabel, verifyHeading = "Verify checksum" }: Props) {
|
||||
const [arch, setArch] = useState<Arch>("macos-arm64");
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
void detect().then((detected) => {
|
||||
if (active) setArch(detected);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-0 mb-3 hairline-t hairline-b hairline-l hairline-r">
|
||||
{(Object.keys(SNIPPETS) as Arch[]).map((a, i) => (
|
||||
<button
|
||||
key={a}
|
||||
type="button"
|
||||
onClick={() => setArch(a)}
|
||||
aria-pressed={arch === a}
|
||||
className={`px-3 py-1.5 text-xs transition-colors ${
|
||||
i > 0 ? "hairline-l" : ""
|
||||
} ${arch === a ? "bg-indigo text-paper" : "bg-paper hover:bg-paper-deep"}`}
|
||||
>
|
||||
{LABELS[a]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<InstallCodeBlock cmd={SNIPPETS[arch]} copyLabel={copyLabel} copiedLabel={copiedLabel} />
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="eyebrow mb-2">{verifyHeading}</div>
|
||||
<InstallCodeBlock cmd={VERIFY[arch]} copyLabel={copyLabel} copiedLabel={copiedLabel} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Icon } from "./icon";
|
||||
import { recordUsage } from "./usage-counting";
|
||||
|
||||
interface Props {
|
||||
@@ -10,7 +11,12 @@ interface Props {
|
||||
copyLocale?: string;
|
||||
}
|
||||
|
||||
export function InstallCodeBlock({ cmd, copyLabel = "Copy", copiedLabel = "Copied ✓", copyLocale }: Props) {
|
||||
/**
|
||||
* A command in a code block with one icon button that copies it. The check
|
||||
* mark and the "Copied" announcement appear only after the clipboard write
|
||||
* succeeds; a failed write never shows a false confirmation.
|
||||
*/
|
||||
export function InstallCodeBlock({ cmd, copyLabel = "Copy", copiedLabel = "Copied", copyLocale }: Props) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copy = async () => {
|
||||
@@ -27,17 +33,23 @@ export function InstallCodeBlock({ cmd, copyLabel = "Copy", copiedLabel = "Copie
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="code">
|
||||
<pre className="code-block">{cmd}</pre>
|
||||
<button
|
||||
type="button"
|
||||
lang={copyLocale}
|
||||
onClick={copy}
|
||||
aria-label={copied ? copiedLabel : copyLabel}
|
||||
aria-label={copyLabel}
|
||||
title={copyLabel}
|
||||
data-copied={copied}
|
||||
className="copy-btn absolute top-3 right-3 z-10 px-3 py-1 bg-paper hairline-t hairline-b hairline-l hairline-r rounded text-xs hover:bg-indigo hover:text-paper transition-colors"
|
||||
className="nav-icon-button nav-icon-button-sm code-copy"
|
||||
>
|
||||
{copied ? copiedLabel : copyLabel}
|
||||
<Icon name={copied ? "check" : "copy"} className="nav-icon" />
|
||||
</button>
|
||||
<pre className="code-block text-[0.78rem] m-0 max-w-full pr-20">{cmd}</pre>
|
||||
<span className="sr-only" aria-live="polite" lang={copyLocale}>
|
||||
{/* The check mark is drawn by the icon; announce only the word. */}
|
||||
{copied ? copiedLabel.replace(/\s*✓\s*$/u, "") : ""}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import Link from "next/link";
|
||||
import { getLegalPrivacy, getLegalTerms } from "@/lib/i18n/dictionaries";
|
||||
|
||||
/** Terms | Privacy: page-level tabs between the two legal documents. */
|
||||
export function LegalTabs({ locale, current }: { locale: string; current: "terms" | "privacy" }) {
|
||||
const termsLabel = getLegalPrivacy(locale).termsLink;
|
||||
const privacyLabel = getLegalTerms(locale).privacyLink;
|
||||
return (
|
||||
<nav className="tabs legal-tabs" aria-label={`${termsLabel} · ${privacyLabel}`}>
|
||||
<Link href={`/${locale}/legal/terms`} className="tab" aria-current={current === "terms" ? "page" : undefined}>
|
||||
{termsLabel}
|
||||
</Link>
|
||||
<Link href={`/${locale}/legal/privacy`} className="tab" aria-current={current === "privacy" ? "page" : undefined}>
|
||||
{privacyLabel}
|
||||
</Link>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -171,7 +171,7 @@ export function MobileMenu({
|
||||
createPortal(<div
|
||||
ref={menuRef}
|
||||
id="mobile-menu"
|
||||
className={`mm-panel lg:hidden fixed inset-0 z-40 bg-paper overflow-y-auto${closing ? " mm-closing" : ""}`}
|
||||
className={`mm-panel lg:hidden fixed inset-0 z-40 bg-canvas overflow-y-auto${closing ? " mm-closing" : ""}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={navAria}
|
||||
@@ -198,7 +198,7 @@ export function MobileMenu({
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
>
|
||||
<span>{l.label}</span>
|
||||
<Icon name="chevron-right" className="nav-icon mm-link-chevron" />
|
||||
<Icon name="chevron-right" className="nav-icon mm-link-chevron icon-flip" />
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { Icon } from "./icon";
|
||||
import type { ModelFact } from "@/lib/facts";
|
||||
import { MODELS_COPY } from "@/lib/content/models";
|
||||
import { pickText } from "@/lib/i18n/dictionaries";
|
||||
@@ -101,11 +102,11 @@ export function ModelsTable({
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<div className="data-table-wrap">
|
||||
<table className="data-table">
|
||||
<caption className="sr-only">{t(MODELS_COPY.modelsTitle)}</caption>
|
||||
<thead>
|
||||
<tr className="hairline-b">
|
||||
<tr>
|
||||
{headers.map((h) => {
|
||||
const active = key === h.key;
|
||||
const ascending = active && !desc;
|
||||
@@ -113,19 +114,17 @@ export function ModelsTable({
|
||||
<th
|
||||
key={h.key}
|
||||
scope="col"
|
||||
className="py-3 pr-4"
|
||||
aria-sort={active ? (ascending ? "ascending" : "descending") : "none"}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`models-sort${active ? " is-active" : ""}`}
|
||||
className="data-table-sort"
|
||||
data-dir={active ? (ascending ? "asc" : "desc") : "none"}
|
||||
onClick={() => setSort(h.key)}
|
||||
title={t(ascending ? MODELS_COPY.sortDesc : MODELS_COPY.sortAsc)}
|
||||
>
|
||||
{h.label}
|
||||
<span aria-hidden="true" className="models-sort-mark">
|
||||
{active ? (ascending ? "↑" : "↓") : "↕"}
|
||||
</span>
|
||||
<Icon name="chevron-down" />
|
||||
</button>
|
||||
</th>
|
||||
);
|
||||
@@ -134,16 +133,16 @@ export function ModelsTable({
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((model) => (
|
||||
<tr key={model.id} className="hairline-b">
|
||||
<th scope="row" className="py-3 pr-4 font-medium">
|
||||
<code className="break-all">{model.id}</code>
|
||||
<tr key={model.id}>
|
||||
<th scope="row">
|
||||
<code>{model.id}</code>
|
||||
{model.reasoning ? (
|
||||
<span className="models-reasoning">{t(MODELS_COPY.reasoning)}</span>
|
||||
) : null}
|
||||
</th>
|
||||
<td className="py-3 pr-4 text-ink-soft">{model.provider ?? "—"}</td>
|
||||
<td className="py-3 pr-4 tabular-nums">{formatTokens(model.contextWindow)}</td>
|
||||
<td className="py-3 tabular-nums">{formatDate(model.addedAt, locale)}</td>
|
||||
<td>{model.provider ?? "—"}</td>
|
||||
<td className="tabular">{formatTokens(model.contextWindow)}</td>
|
||||
<td className="tabular">{formatDate(model.addedAt, locale)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { getChrome } from "@/lib/i18n/dictionaries";
|
||||
import { navLinks, secondaryNavLinks, REPO_URL, APP_LOGIN_URL } from "@/lib/i18n/links";
|
||||
@@ -8,12 +7,13 @@ import { LocaleSwitcher } from "./locale-switcher";
|
||||
import { MobileMenu } from "./mobile-menu";
|
||||
import { NavLinks } from "./nav-links";
|
||||
import { ThemeToggle } from "./theme-toggle";
|
||||
import { WhalePose } from "./whale-pose";
|
||||
|
||||
/**
|
||||
* Masthead + primary nav. Like the GPUI titlebar it carries few visible
|
||||
* controls: the mark, four links, quiet icon controls with accessible names,
|
||||
* one identity door (Sign in; the sign-in page offers account creation) and
|
||||
* the one primary action, Install.
|
||||
* controls: the whale and the wordmark, four links, quiet icon controls
|
||||
* with accessible names, one identity door (Sign in; the sign-in page
|
||||
* offers account creation) and the one primary action, Install.
|
||||
*/
|
||||
export function Nav({ locale = "en" }: { locale?: Locale }) {
|
||||
const chrome = getChrome(locale);
|
||||
@@ -26,14 +26,10 @@ export function Nav({ locale = "en" }: { locale?: Locale }) {
|
||||
<div className="site-nav-inner paper-nav-inner">
|
||||
<Link href={homeHref} className="site-wordmark paper-wordmark" aria-label={chrome.navHomeAria}>
|
||||
<div className="paper-wordmark-text">
|
||||
<Image src="/brand/mark-gradient.svg" width={22} height={22} alt="" className="paper-wordmark-mark" unoptimized />
|
||||
<img
|
||||
className="paper-wordmark-logo"
|
||||
src="/brand/wordmark.svg"
|
||||
alt=""
|
||||
width={142}
|
||||
height={20}
|
||||
/>
|
||||
<WhalePose pose="rest" className="paper-wordmark-mark" priority />
|
||||
{/* The wordmark is one ink, drawn through a CSS mask so both
|
||||
appearances re-ink it; the link carries the name. */}
|
||||
<span className="paper-wordmark-logo" aria-hidden="true" />
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -48,7 +44,7 @@ export function Nav({ locale = "en" }: { locale?: Locale }) {
|
||||
titleLabel={chrome.themeTitle}
|
||||
/>
|
||||
<LocaleSwitcher current={locale} />
|
||||
<Link href={REPO_URL} className="nav-icon-button site-github-link" aria-label="GitHub">
|
||||
<Link href={REPO_URL} className="nav-icon-button site-github-link" aria-label="GitHub" title="GitHub">
|
||||
<Icon name="github" className="nav-icon" />
|
||||
</Link>
|
||||
<Link href={APP_LOGIN_URL} className="paper-auth-signin hidden lg:inline-flex" data-usage="login">
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/* The supplied game poster is the 404 joke, kept whole at every width. */
|
||||
.page {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: clamp(2rem, 6vw, 5.5rem);
|
||||
width: var(--container);
|
||||
max-width: 70rem;
|
||||
min-height: calc(100svh - 4.25rem);
|
||||
margin-inline: auto;
|
||||
padding-block: clamp(2rem, 5vw, 4rem);
|
||||
}
|
||||
|
||||
.title {
|
||||
max-width: 11ch;
|
||||
font-size: clamp(2.5rem, 4.8vw, 4.25rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.06;
|
||||
letter-spacing: -0.03em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.code {
|
||||
display: block;
|
||||
margin-bottom: 0.3em;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 1.5rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.65;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem 1.25rem;
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.docs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 2.75rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.875rem;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
.docs:hover { color: var(--ink); }
|
||||
|
||||
.poster {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 30rem;
|
||||
height: auto;
|
||||
justify-self: end;
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.title { max-width: 15ch; }
|
||||
.poster { justify-self: center; }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Icon, type IconName } from "./icon";
|
||||
import { WhalePose, type WhalePoseName } from "./whale-pose";
|
||||
|
||||
/**
|
||||
* The one page header: what the reader knows (the title), why they are here
|
||||
* (one lede), what to do next (the actions). An optional kicker names the
|
||||
* place; an optional whale pose shows the page's job.
|
||||
*/
|
||||
export function PageHeader({
|
||||
title,
|
||||
lede,
|
||||
kicker,
|
||||
kickerIcon,
|
||||
pose,
|
||||
actions,
|
||||
meta,
|
||||
children,
|
||||
titleId,
|
||||
}: {
|
||||
title: ReactNode;
|
||||
lede?: ReactNode;
|
||||
kicker?: ReactNode;
|
||||
kickerIcon?: IconName;
|
||||
pose?: WhalePoseName;
|
||||
actions?: ReactNode;
|
||||
meta?: ReactNode;
|
||||
children?: ReactNode;
|
||||
titleId?: string;
|
||||
}) {
|
||||
return (
|
||||
<header className="page-head">
|
||||
<div className="page-head-inner">
|
||||
<div className="page-head-text">
|
||||
{kicker ? (
|
||||
<p className="page-kicker">
|
||||
{kickerIcon ? <Icon name={kickerIcon} /> : null}
|
||||
{kicker}
|
||||
</p>
|
||||
) : null}
|
||||
<h1 className="page-title" id={titleId}>
|
||||
{title}
|
||||
</h1>
|
||||
{lede ? <p className="page-lede">{lede}</p> : null}
|
||||
{meta ? <div className="page-meta">{meta}</div> : null}
|
||||
{actions ? <div className="actions">{actions}</div> : null}
|
||||
{children}
|
||||
</div>
|
||||
{pose ? <WhalePose pose={pose} className="page-head-pose" priority /> : null}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/** A page section with one title, an optional scope line and one link. */
|
||||
export function Section({
|
||||
id,
|
||||
title,
|
||||
scope,
|
||||
link,
|
||||
children,
|
||||
className = "",
|
||||
}: {
|
||||
id: string;
|
||||
title: ReactNode;
|
||||
scope?: ReactNode;
|
||||
link?: ReactNode;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<section className={`page-section ${className}`.trim()} aria-labelledby={id}>
|
||||
<div className="section-head">
|
||||
<div className="section-head-text">
|
||||
<h2 className="section-title" id={id}>
|
||||
{title}
|
||||
</h2>
|
||||
{scope ? <p className="section-scope">{scope}</p> : null}
|
||||
</div>
|
||||
{link}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import Link from "next/link";
|
||||
import { ACCOUNT_ENTRY_COPY } from "@/lib/content/account-entry";
|
||||
import { pickText } from "@/lib/i18n/dictionaries";
|
||||
import {
|
||||
CANONICAL_MARK_SRC,
|
||||
publicAuthAppDestination,
|
||||
type PublicAuthKind,
|
||||
} from "@/lib/public-auth-routes";
|
||||
import { publicAuthAppDestination, type PublicAuthKind } from "@/lib/public-auth-routes";
|
||||
import { WhalePose } from "./whale-pose";
|
||||
|
||||
/**
|
||||
* The public sign-in and create-account doors. codewhale.net is not the
|
||||
* signed-in app: one primary action sends the reader to it, one secondary
|
||||
* action installs locally (no account needed), and one line switches
|
||||
* between signing in and creating an account. The whale listens.
|
||||
*/
|
||||
export function PublicAccountEntry({
|
||||
locale,
|
||||
kind,
|
||||
@@ -20,37 +23,24 @@ export function PublicAccountEntry({
|
||||
const otherHref = `/${locale}/${creating ? "signin" : "signup"}`;
|
||||
|
||||
return (
|
||||
<div className="portal-home">
|
||||
<section className="portal-section">
|
||||
<div className="portal-container public-account-entry">
|
||||
{/* Pinned app-icon raster; see CANONICAL_MARK_SRC for its source asset. */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
className="public-account-mark"
|
||||
src={CANONICAL_MARK_SRC}
|
||||
alt=""
|
||||
width={64}
|
||||
height={64}
|
||||
/>
|
||||
<p className="legal-doc-kicker">{pickText(copy.kicker, locale)}</p>
|
||||
<h1>{pickText(copy.title, locale)}</h1>
|
||||
<p className="portal-lede">{pickText(ACCOUNT_ENTRY_COPY.lede, locale)}</p>
|
||||
<div className="portal-actions">
|
||||
<a className="portal-button portal-button-primary" href={appHref} data-usage={creating ? "signup" : "login"}>
|
||||
{pickText(copy.action, locale)}
|
||||
</a>
|
||||
<Link className="portal-button portal-button-secondary" href={`/${locale}/install`}>
|
||||
{pickText(ACCOUNT_ENTRY_COPY.installLocally, locale)}
|
||||
</Link>
|
||||
</div>
|
||||
<p className="portal-meta">
|
||||
{pickText(copy.switchPrompt, locale)}{" "}
|
||||
<Link href={otherHref} className="body-link">
|
||||
{pickText(copy.switchLabel, locale)}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<div className="account-entry">
|
||||
<WhalePose pose="listen" className="account-entry-pose" priority />
|
||||
<h1 className="page-title">{pickText(copy.title, locale)}</h1>
|
||||
<p className="page-lede">{pickText(ACCOUNT_ENTRY_COPY.lede, locale)}</p>
|
||||
<div className="actions">
|
||||
<a className="btn btn-primary btn-lg" href={appHref} data-usage={creating ? "signup" : "login"}>
|
||||
{pickText(copy.action, locale)}
|
||||
</a>
|
||||
<Link className="btn btn-secondary btn-lg" href={`/${locale}/install`}>
|
||||
{pickText(ACCOUNT_ENTRY_COPY.installLocally, locale)}
|
||||
</Link>
|
||||
</div>
|
||||
<p className="page-meta">
|
||||
{pickText(copy.switchPrompt, locale)}{" "}
|
||||
<Link href={otherHref} className="link">
|
||||
{pickText(copy.switchLabel, locale)}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export function RetryAction({
|
||||
<button
|
||||
type="button"
|
||||
onClick={run}
|
||||
className={`portal-button portal-button-${variant} state-retry`}
|
||||
className={`btn btn-${variant} state-retry`}
|
||||
aria-busy={inFlight}
|
||||
disabled={inFlight}
|
||||
>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { defaultLocale } from "@/lib/i18n/config";
|
||||
import { getStates } from "@/lib/i18n/dictionaries";
|
||||
import { pathLocale } from "@/lib/i18n/path";
|
||||
import { RetryAction } from "./retry-action";
|
||||
import { ErrorState, LoadingState } from "./surface-state";
|
||||
import styles from "./not-found.module.css";
|
||||
import { EmptyState, ErrorState, LoadingState } from "./surface-state";
|
||||
|
||||
/**
|
||||
* Route boundaries — `loading.tsx`, `error.tsx`, `not-found.tsx` — receive
|
||||
@@ -36,7 +34,7 @@ export function ErrorRoute({ reset, digest }: { reset: () => void; digest?: stri
|
||||
action={
|
||||
<>
|
||||
<RetryAction label={t.retry} onRetry={reset} />
|
||||
<Link href={`/${locale}`} className="portal-button portal-button-secondary">
|
||||
<Link href={`/${locale}`} className="btn btn-secondary">
|
||||
{t.homeLink}
|
||||
</Link>
|
||||
</>
|
||||
@@ -48,32 +46,24 @@ export function ErrorRoute({ reset, digest }: { reset: () => void; digest?: stri
|
||||
export function NotFoundRoute() {
|
||||
const locale = useRouteLocale();
|
||||
const t = getStates(locale);
|
||||
// The whale is searching: the page is missing, not broken.
|
||||
return (
|
||||
<section className={styles.page} aria-labelledby="not-found-title">
|
||||
<div>
|
||||
<h1 id="not-found-title" className={styles.title}>
|
||||
<span className={styles.code}>404.</span>{" "}
|
||||
{t.notFoundTitle}
|
||||
</h1>
|
||||
<p className={styles.body}>{t.notFoundBody}</p>
|
||||
<div className={styles.actions}>
|
||||
<Link href={`/${locale}`} className="portal-button portal-button-primary">
|
||||
<EmptyState
|
||||
locale={locale}
|
||||
pose="search"
|
||||
titleAs="h1"
|
||||
title={t.notFoundTitle}
|
||||
body={t.notFoundBody}
|
||||
action={
|
||||
<>
|
||||
<Link href={`/${locale}`} className="btn btn-primary">
|
||||
{t.notFoundHomeLink}
|
||||
</Link>
|
||||
<Link href={`/${locale}/docs`} className={styles.docs}>
|
||||
<Link href={`/${locale}/docs`} className="btn btn-secondary">
|
||||
{t.docsIndexLink}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Image
|
||||
className={styles.poster}
|
||||
src="/codwhale-404.webp"
|
||||
alt={t.notFoundPosterAlt}
|
||||
width={1122}
|
||||
height={1402}
|
||||
priority
|
||||
unoptimized
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export function Seal({
|
||||
char = "深",
|
||||
size = "md",
|
||||
variant = "ink",
|
||||
}: {
|
||||
char?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
variant?: "ink" | "indigo";
|
||||
}) {
|
||||
const dim = size === "sm" ? "w-7 h-7 text-sm" : size === "lg" ? "w-12 h-12 text-2xl" : "w-10 h-10 text-lg";
|
||||
const cls = variant === "indigo" ? "seal seal-indigo" : "seal";
|
||||
return (
|
||||
<span className={`${cls} ${dim}`} aria-hidden>
|
||||
{char}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,29 @@
|
||||
/**
|
||||
* <StatusBadge> — honest availability labels for public surfaces.
|
||||
* State is a mark and a word, never colour alone.
|
||||
*
|
||||
* Used wherever the site shows something that is not a shipped, stable
|
||||
* feature: experimental bridges, preview platforms, pending media, and
|
||||
* unavailable states. The label is always visible text — never color alone —
|
||||
* so the state is conveyed accessibly in both locales.
|
||||
* <Status> is the general form: a dot in the tone's colour and the word in
|
||||
* text. `live` lights its dot (bioluminescence) and is only for something
|
||||
* the site actually observes; nothing static qualifies.
|
||||
*
|
||||
* <StatusBadge> keeps the older availability vocabulary (experimental,
|
||||
* preview, pending, unavailable) for the pages that still use it.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import type { LocalizedText } from "@/lib/content/vocabulary";
|
||||
import { pickText } from "@/lib/i18n/dictionaries";
|
||||
|
||||
export type StatusTone = "ready" | "attention" | "idle" | "live" | "danger" | "accent";
|
||||
|
||||
export function Status({ tone, children }: { tone: StatusTone; children: ReactNode }) {
|
||||
return (
|
||||
<span className={`status status-${tone}`}>
|
||||
<span className="status-dot" aria-hidden="true" />
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export type StatusKind = "experimental" | "preview" | "pending" | "unavailable";
|
||||
|
||||
const DEFAULT_LABELS: Record<StatusKind, LocalizedText> = {
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
/**
|
||||
* <Strata> — the folio's water, drawn as geometry.
|
||||
*
|
||||
* Four translucent strata of the GPUI palette stacked from the light end of
|
||||
* the ombre down to the deep charcoal field, each edge softened the way an
|
||||
* ink wash bleeds into paper, with a few fine current lines in the primary
|
||||
* blue and the human tan riding the crests. Every colour is a palette token
|
||||
* read from the cascade (`currentColor` and the `--gpui-*` custom
|
||||
* properties), so the drawing re-inks with the theme and never carries a
|
||||
* hex of its own.
|
||||
*
|
||||
* Two compositions share one vocabulary:
|
||||
*
|
||||
* hero — the water rises from the bottom right of the plate and leaves the
|
||||
* top left as paper for the title. Anchored to the plate's floor.
|
||||
* band — a horizontal waterline: paper above, deep field below. Used at
|
||||
* the homepage descent and before the footer on every other page.
|
||||
*
|
||||
* Decorative and static: `aria-hidden`, no animation, no interaction. This
|
||||
* is the one illustrated element on the site; nothing else is drawn.
|
||||
*/
|
||||
|
||||
type Variant = "hero" | "band";
|
||||
|
||||
const FILTERS = (
|
||||
<defs>
|
||||
<filter id="cw-wash-wide" x="-20%" y="-40%" width="140%" height="180%" colorInterpolationFilters="sRGB">
|
||||
<feGaussianBlur stdDeviation="34" />
|
||||
</filter>
|
||||
<filter id="cw-wash" x="-20%" y="-40%" width="140%" height="180%" colorInterpolationFilters="sRGB">
|
||||
<feGaussianBlur stdDeviation="20" />
|
||||
</filter>
|
||||
<filter id="cw-wash-tight" x="-20%" y="-40%" width="140%" height="180%" colorInterpolationFilters="sRGB">
|
||||
<feGaussianBlur stdDeviation="11" />
|
||||
</filter>
|
||||
<filter id="cw-wash-edge" x="-20%" y="-40%" width="140%" height="180%" colorInterpolationFilters="sRGB">
|
||||
<feGaussianBlur stdDeviation="5" />
|
||||
</filter>
|
||||
</defs>
|
||||
);
|
||||
|
||||
function HeroStrata() {
|
||||
return (
|
||||
<svg
|
||||
className="folio-strata"
|
||||
viewBox="0 0 1440 1000"
|
||||
preserveAspectRatio="xMidYMax slice"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
{FILTERS}
|
||||
{/* Paper haze — a breath of the primary's light end over the top right
|
||||
so the sheet is not flat where the water will rise. */}
|
||||
<ellipse cx="1180" cy="120" rx="520" ry="200" fill="var(--gpui-primary-dark)" opacity="0.4" filter="url(#cw-wash-wide)" />
|
||||
{/* Sky — the light end of the ombre, the first stratum. */}
|
||||
<path
|
||||
d="M -240 690 C 160 640, 420 720, 660 570 C 880 430, 1040 280, 1240 210 C 1340 175, 1440 150, 1700 110 L 1700 1120 L -240 1120 Z"
|
||||
fill="var(--gpui-primary-dark)"
|
||||
opacity="0.42"
|
||||
filter="url(#cw-wash-wide)"
|
||||
/>
|
||||
{/* Mid water. */}
|
||||
<path
|
||||
d="M -240 800 C 140 770, 400 830, 620 700 C 830 575, 1000 440, 1200 370 C 1330 325, 1500 280, 1700 250 L 1700 1120 L -240 1120 Z"
|
||||
fill="var(--gpui-stage-raised)"
|
||||
opacity="0.62"
|
||||
filter="url(#cw-wash)"
|
||||
/>
|
||||
{/* The stage plate's muted surface. */}
|
||||
<path
|
||||
d="M -240 890 C 200 860, 430 905, 650 810 C 870 715, 1060 580, 1260 520 C 1400 478, 1540 445, 1700 410 L 1700 1120 L -240 1120 Z"
|
||||
fill="var(--gpui-stage-muted)"
|
||||
opacity="0.9"
|
||||
filter="url(#cw-wash-tight)"
|
||||
/>
|
||||
{/* The deep field. Its top edge is the crispest, the way the darkest
|
||||
wash dries with a line. */}
|
||||
<path
|
||||
d="M -240 965 C 220 945, 470 975, 710 910 C 910 855, 1110 740, 1310 680 C 1430 645, 1560 615, 1700 1120 L 1700 1120 L -240 1120 Z"
|
||||
fill="var(--gpui-stage-deep)"
|
||||
filter="url(#cw-wash-edge)"
|
||||
/>
|
||||
{/* Current lines: fine strokes riding the crests. Primary blue on the
|
||||
water, ink on the paper, one tan thread for the human mark. */}
|
||||
<g fill="none" strokeLinecap="round">
|
||||
<path d="M 980 330 C 1100 285, 1200 255, 1340 215 C 1420 192, 1520 170, 1700 140" stroke="var(--gpui-primary-dark)" strokeOpacity="0.5" strokeWidth="1.1" />
|
||||
<path d="M 760 640 C 900 560, 1040 470, 1200 405 C 1330 352, 1480 310, 1700 275" stroke="var(--gpui-primary-dark)" strokeOpacity="0.36" strokeWidth="1" />
|
||||
<path d="M 620 860 C 840 780, 1040 660, 1240 590 C 1400 535, 1560 500, 1700 470" stroke="var(--gpui-primary-dark)" strokeOpacity="0.3" strokeWidth="1" />
|
||||
<path d="M 420 960 C 640 930, 860 860, 1060 780 C 1240 708, 1420 650, 1700 600" stroke="var(--gpui-tan)" strokeOpacity="0.45" strokeWidth="0.9" />
|
||||
<path d="M 1010 60 C 1100 110, 1140 180, 1120 260" stroke="var(--ink)" strokeOpacity="0.14" strokeWidth="0.9" />
|
||||
<path d="M 1060 40 C 1170 100, 1230 190, 1210 300 C 1200 340, 1180 370, 1150 400" stroke="var(--ink)" strokeOpacity="0.12" strokeWidth="0.9" />
|
||||
<path d="M 1150 20 C 1290 80, 1360 170, 1340 300" stroke="var(--ink)" strokeOpacity="0.1" strokeWidth="0.9" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function BandStrata() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 1600 400"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
{FILTERS}
|
||||
<path
|
||||
d="M -160 150 C 220 110, 520 190, 820 140 C 1100 95, 1340 160, 1760 120 L 1760 520 L -160 520 Z"
|
||||
fill="var(--gpui-primary-dark)"
|
||||
opacity="0.45"
|
||||
filter="url(#cw-wash-wide)"
|
||||
/>
|
||||
<path
|
||||
d="M -160 220 C 240 190, 520 250, 820 215 C 1100 180, 1360 235, 1760 200 L 1760 520 L -160 520 Z"
|
||||
fill="var(--gpui-stage-raised)"
|
||||
opacity="0.7"
|
||||
filter="url(#cw-wash)"
|
||||
/>
|
||||
<path
|
||||
d="M -160 285 C 240 260, 540 305, 840 280 C 1120 255, 1380 300, 1760 270 L 1760 520 L -160 520 Z"
|
||||
fill="var(--gpui-stage-muted)"
|
||||
opacity="0.92"
|
||||
filter="url(#cw-wash-tight)"
|
||||
/>
|
||||
<path
|
||||
d="M -160 345 C 240 325, 560 360, 860 340 C 1140 322, 1400 352, 1760 335 L 1760 520 L -160 520 Z"
|
||||
fill="var(--gpui-stage)"
|
||||
filter="url(#cw-wash-edge)"
|
||||
/>
|
||||
<g fill="none" strokeLinecap="round">
|
||||
<path d="M -40 175 C 300 140, 560 205, 860 165 C 1120 130, 1380 180, 1660 150" stroke="var(--gpui-primary-dark)" strokeOpacity="0.45" strokeWidth="1.1" />
|
||||
<path d="M 120 250 C 420 225, 700 270, 980 245 C 1220 223, 1440 262, 1680 235" stroke="var(--gpui-primary-dark)" strokeOpacity="0.3" strokeWidth="1" />
|
||||
<path d="M -40 320 C 300 300, 620 335, 900 315 C 1160 297, 1420 330, 1680 305" stroke="var(--gpui-tan)" strokeOpacity="0.42" strokeWidth="0.9" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Strata({ variant }: { variant: Variant }) {
|
||||
return variant === "hero" ? <HeroStrata /> : <BandStrata />;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { getStates } from "@/lib/i18n/dictionaries";
|
||||
import { WhalePose, type WhalePoseName } from "./whale-pose";
|
||||
|
||||
/**
|
||||
* Shared surface states — the one empty / loading / error vocabulary every
|
||||
@@ -9,8 +10,11 @@ import { getStates } from "@/lib/i18n/dictionaries";
|
||||
* Rules:
|
||||
* - An empty state is a statement that nothing exists, never a placeholder
|
||||
* pretending something does. Copy comes from the states dictionary.
|
||||
* - A loading state is announced (`role="status"`) and drawn as neutral
|
||||
* skeleton lines whose shimmer is gated on `prefers-reduced-motion`.
|
||||
* - The whale's pose says what is going on (rest when there is nothing,
|
||||
* busy while loading, hmm when something failed); the title says it in
|
||||
* words, so the pose is decorative.
|
||||
* - A loading state is announced (`role="status"`) and drawn as still
|
||||
* skeleton lines plus one spinner that only turns when motion is allowed.
|
||||
* - An error state says what failed and offers exactly one recovery.
|
||||
*/
|
||||
|
||||
@@ -25,6 +29,7 @@ function Block({
|
||||
compact,
|
||||
role,
|
||||
live,
|
||||
pose,
|
||||
titleAs: Title = "p",
|
||||
}: {
|
||||
tone: Tone;
|
||||
@@ -34,6 +39,7 @@ function Block({
|
||||
compact?: boolean;
|
||||
role?: "status" | "alert";
|
||||
live?: "polite" | "assertive";
|
||||
pose: WhalePoseName;
|
||||
/** A route-level plate that is the whole page owns its `<h1>`. */
|
||||
titleAs?: TitleTag;
|
||||
}) {
|
||||
@@ -44,12 +50,10 @@ function Block({
|
||||
aria-live={live}
|
||||
data-state={tone}
|
||||
>
|
||||
<span className="state-mark" aria-hidden="true" />
|
||||
<div className="state-copy">
|
||||
<Title className="state-title">{title}</Title>
|
||||
{body && <p className="state-body">{body}</p>}
|
||||
{action && <div className="state-actions">{action}</div>}
|
||||
</div>
|
||||
<WhalePose pose={pose} />
|
||||
<Title className="state-title">{title}</Title>
|
||||
{body && <p className="state-body">{body}</p>}
|
||||
{action && <div className="state-actions">{action}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -61,6 +65,7 @@ export function EmptyState({
|
||||
action,
|
||||
compact,
|
||||
titleAs,
|
||||
pose = "rest",
|
||||
}: {
|
||||
locale: string;
|
||||
title?: string;
|
||||
@@ -68,6 +73,7 @@ export function EmptyState({
|
||||
action?: ReactNode;
|
||||
compact?: boolean;
|
||||
titleAs?: TitleTag;
|
||||
pose?: WhalePoseName;
|
||||
}) {
|
||||
const t = getStates(locale);
|
||||
return (
|
||||
@@ -78,6 +84,7 @@ export function EmptyState({
|
||||
action={action}
|
||||
compact={compact}
|
||||
titleAs={titleAs}
|
||||
pose={pose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -100,12 +107,13 @@ export function UnavailableState({
|
||||
const t = getStates(locale);
|
||||
return (
|
||||
<Block
|
||||
tone="error"
|
||||
tone="empty"
|
||||
title={t.unavailableTitle}
|
||||
body={t.unavailableBody}
|
||||
action={action}
|
||||
compact={compact}
|
||||
role="status"
|
||||
pose="sleep"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -130,14 +138,14 @@ export function LoadingState({
|
||||
aria-busy="true"
|
||||
data-state="loading"
|
||||
>
|
||||
<span className="state-mark" aria-hidden="true" />
|
||||
<div className="state-copy">
|
||||
<p className="state-title">{label ?? t.loadingLabel}</p>
|
||||
<div className="state-skeleton" aria-hidden="true">
|
||||
{Array.from({ length: lines }, (_, i) => (
|
||||
<span key={i} style={{ width: `${88 - i * 14}%` }} />
|
||||
))}
|
||||
</div>
|
||||
<WhalePose pose="busy" />
|
||||
<p className="state-title">
|
||||
<span className="spinner" aria-hidden="true" /> {label ?? t.loadingLabel}
|
||||
</p>
|
||||
<div className="state-skeleton" aria-hidden="true">
|
||||
{Array.from({ length: lines }, (_, i) => (
|
||||
<span key={i} style={{ width: `${88 - i * 14}%` }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -168,6 +176,7 @@ export function ErrorState({
|
||||
compact={compact}
|
||||
role="alert"
|
||||
titleAs={titleAs}
|
||||
pose="hmm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Icon } from "./icon";
|
||||
/**
|
||||
* "See how it decides" — a terminal-styled pane that surfaces REAL reasoning
|
||||
* traces from a Codewhale session, paired with the decision each produced.
|
||||
@@ -68,54 +69,28 @@ export const SCENES: Scene[] = [
|
||||
export function ThinkingTrace({ locale = "en" }: { locale?: string }) {
|
||||
const isZh = locale === "zh";
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<div className="grid-3">
|
||||
{SCENES.map((s, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="hairline-t hairline-b hairline-l hairline-r bg-paper flex flex-col overflow-hidden"
|
||||
>
|
||||
{/* terminal title bar */}
|
||||
<div className="bg-paper-deep text-ink px-4 py-2.5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-jade inline-block" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-ochre inline-block" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-indigo inline-block" />
|
||||
<span className="ml-2.5 text-xs text-ink-mute">
|
||||
codewhale — thinking
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-cjk text-[0.6rem] text-ink-mute">
|
||||
{isZh ? "推理痕迹" : "reasoning trace"}
|
||||
<figure key={i} className="trace">
|
||||
<figcaption className="trace-head">
|
||||
<span className="status status-accent">
|
||||
<span className="status-dot" aria-hidden="true" />
|
||||
{isZh ? "推理痕迹" : "Reasoning trace"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* context */}
|
||||
<div className="px-4 pt-4 text-xs text-ink-mute">
|
||||
{isZh ? s.context.zh : s.context.en}
|
||||
</div>
|
||||
|
||||
{/* the trace */}
|
||||
<pre className="px-4 py-3 font-mono text-[0.82rem] text-ink leading-relaxed whitespace-pre-wrap flex-1">
|
||||
<span className="text-indigo">›</span>{" "}
|
||||
<span className="text-ink-soft">{s.trace}</span>
|
||||
</pre>
|
||||
|
||||
{/* cited authority */}
|
||||
<div className="px-4 pb-3 flex flex-wrap gap-1.5">
|
||||
<span className="trace-context">{isZh ? s.context.zh : s.context.en}</span>
|
||||
</figcaption>
|
||||
<pre className="trace-body">{s.trace}</pre>
|
||||
<p className="trace-cites">
|
||||
{s.cites.map((c) => (
|
||||
<span key={c} className="pill text-[0.58rem]">
|
||||
{c}
|
||||
</span>
|
||||
<span key={c} className="pill">{c}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* the decision it produced */}
|
||||
<div className="bg-indigo-pale px-4 py-3 hairline-t text-[0.8rem] leading-relaxed text-ink-soft">
|
||||
<span className="font-display text-indigo font-semibold mr-1">→</span>
|
||||
{isZh ? s.decision.zh : s.decision.en}
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<p className="trace-decision">
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
<span>{isZh ? s.decision.zh : s.decision.en}</span>
|
||||
</p>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* The v2 whale character's still poses (public/whale/*.svg, exported from
|
||||
* the whale-character-v2 kit by scripts/export-whale-poses.py). The web
|
||||
* shows the reduced-motion pose of each action; it never animates by
|
||||
* swapping posters.
|
||||
*
|
||||
* A pose carries meaning, so pick the one that matches what the page is
|
||||
* about: `search` for "not found", `hmm` for an error, `busy` while loading,
|
||||
* `done` for a finished action, `needs` when the reader must act, and `pod`
|
||||
* only where parallel agents are the subject. Decorative by default (the
|
||||
* surrounding words say the same thing); pass `label` when the pose alone
|
||||
* carries information.
|
||||
*/
|
||||
export const WHALE_POSES = [
|
||||
"rest",
|
||||
"listen",
|
||||
"think",
|
||||
"busy",
|
||||
"read",
|
||||
"search",
|
||||
"write",
|
||||
"run",
|
||||
"browse",
|
||||
"talk",
|
||||
"pod",
|
||||
"needs",
|
||||
"done",
|
||||
"hmm",
|
||||
"sleep",
|
||||
"computer",
|
||||
"connect",
|
||||
] as const;
|
||||
|
||||
export type WhalePoseName = (typeof WHALE_POSES)[number];
|
||||
|
||||
export function WhalePose({
|
||||
pose,
|
||||
label,
|
||||
className = "",
|
||||
priority = false,
|
||||
}: {
|
||||
pose: WhalePoseName;
|
||||
label?: string;
|
||||
className?: string;
|
||||
priority?: boolean;
|
||||
}) {
|
||||
return (
|
||||
// Static, cacheable SVG; next/image adds nothing for a vector file.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={`/whale/${pose}.svg`}
|
||||
alt={label ?? ""}
|
||||
aria-hidden={label ? undefined : true}
|
||||
width={124}
|
||||
height={124}
|
||||
className={`whale-pose ${className}`.trim()}
|
||||
data-pose={pose}
|
||||
decoding="async"
|
||||
loading={priority ? "eager" : "lazy"}
|
||||
fetchPriority={priority ? "high" : undefined}
|
||||
draggable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveWhale } from "./whale-tokens";
|
||||
import { siteCss } from "./site-css";
|
||||
|
||||
const CSS = siteCss();
|
||||
|
||||
function selectorBlock(selector: string): string {
|
||||
const match = CSS.match(
|
||||
new RegExp(`(?:^|\n)${selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\{([^}]*)\\}`, "s"),
|
||||
);
|
||||
if (!match) throw new Error(`Missing CSS selector: ${selector}`);
|
||||
return match[1];
|
||||
}
|
||||
|
||||
// The site stylesheet (app/styles/*.css) names the palette token (`--paper: var(--gpui-paper)`)
|
||||
// rather than repeating its hex; resolve one hop through the generated
|
||||
// app/tokens.css plus the hand-kept --gpui-* block.
|
||||
function cssHexIn(block: string, name: string): string {
|
||||
const match = block.match(new RegExp(`--${name}:\\s*([^;]+);`, "i"));
|
||||
if (!match) throw new Error(`Missing CSS token in block: --${name}`);
|
||||
const value = resolveWhale(match[1].trim());
|
||||
if (!/^#[0-9a-f]{6}$/i.test(value)) {
|
||||
throw new Error(`--${name} is not a hex color: ${value}`);
|
||||
}
|
||||
return value.toLowerCase();
|
||||
}
|
||||
|
||||
const ROOT = selectorBlock(":root");
|
||||
const BELOW_WATERLINE = selectorBlock(
|
||||
'.ocean-column,\n.site-footer,\n:root[data-theme="dark"]',
|
||||
);
|
||||
|
||||
describe("GPUI public-surface contract", () => {
|
||||
it("grounds the paper sheet in the GPUI light theme's warm paper and inks", () => {
|
||||
// Above the waterline the field is the GPUI light background — warm
|
||||
// paper — and the ink is its foreground. Values come from the versioned
|
||||
// vendor/codewhale-design/tokens.json artifact,
|
||||
// reached through the generated tokens in app/tokens.css.
|
||||
expect(cssHexIn(ROOT, "paper")).toBe("#faf8f5");
|
||||
expect(cssHexIn(ROOT, "paper-deep")).toBe("#f0ede8");
|
||||
expect(cssHexIn(ROOT, "paper-edge")).toBe("#d9d5cf");
|
||||
expect(cssHexIn(ROOT, "paper-card")).toBe("#ffffff");
|
||||
expect(cssHexIn(ROOT, "ink")).toBe("#28292b");
|
||||
expect(cssHexIn(ROOT, "ink-soft")).toBe("#5f605d");
|
||||
expect(cssHexIn(ROOT, "ink-mute")).toBe("#5f605d");
|
||||
// Action on paper is the GPUI light primary; hover is the same hue at
|
||||
// 0.9 opacity (`button_primary_hover`), not a second blue.
|
||||
expect(cssHexIn(ROOT, "indigo")).toBe("#245bc7");
|
||||
expect(resolveWhale("var(--gpui-primary-hover)")).toBe("rgb(var(--gpui-light-primary-rgb) / var(--gpui-primary-hover-opacity))");
|
||||
expect(ROOT).toMatch(/--indigo-deep:\s*var\(--gpui-primary-hover\);/);
|
||||
expect(cssHexIn(ROOT, "mark-ink")).toBe("#28292b");
|
||||
// The deep field is always the stage's darkest, and code plates keep the
|
||||
// stage deep on either side of the waterline.
|
||||
expect(cssHexIn(ROOT, "ocean-deep")).toBe("#191a1c");
|
||||
expect(cssHexIn(ROOT, "action-on-dark")).toBe("#90b9ff");
|
||||
expect(cssHexIn(ROOT, "ocean-current")).toBe("#90b9ff");
|
||||
expect(cssHexIn(ROOT, "code-bg")).toBe("#191a1c");
|
||||
});
|
||||
|
||||
it("re-inks every dark subtree with the GPUI charcoal tokens through one rule", () => {
|
||||
// The ocean column, the footer seabed, and the pinned dark scheme share
|
||||
// one dark rule (the OS-dark block repeats it), so a component never
|
||||
// needs to know which side of the surface it is on.
|
||||
expect(CSS).toMatch(/\.ocean-column,\s*\.site-footer,\s*:root\[data-theme="dark"\]\s*\{/);
|
||||
expect(cssHexIn(BELOW_WATERLINE, "paper")).toBe("#202123");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "paper-deep")).toBe("#2a2b2e");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "paper-edge")).toBe("#3b3c3f");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "ink")).toBe("#efeeeb");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "ink-soft")).toBe("#b1b1ad");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "ink-mute")).toBe("#b1b1ad");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "indigo")).toBe("#90b9ff");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "jade")).toBe("#9ec7b2");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "signal-gold")).toBe("#d6c78f");
|
||||
});
|
||||
|
||||
it("draws the water from palette tokens only, never a hex of its own", () => {
|
||||
const strata = readFileSync(new URL("../components/strata.tsx", import.meta.url), "utf8");
|
||||
expect(strata).not.toMatch(/#[0-9a-f]{3,8}\b/i);
|
||||
for (const token of ["--gpui-primary-dark", "--gpui-stage-raised", "--gpui-stage-muted", "--gpui-stage-deep", "--gpui-tan", "--ink"]) {
|
||||
expect(strata, token).toContain(`var(${token})`);
|
||||
}
|
||||
// Static and decorative: no animation, hidden from assistive technology.
|
||||
expect(strata).not.toMatch(/animate|@keyframes/);
|
||||
expect(strata).toContain('aria-hidden="true"');
|
||||
});
|
||||
|
||||
it("renders the whale mark in the sheet's mark ink while controls use action blue", () => {
|
||||
expect(CSS).toMatch(/\.codewhale-mark-primary \{ fill: var\(--mark-ink\); \}/);
|
||||
expect(CSS).toMatch(/\.portal-button-primary[\s\S]*background: var\(--indigo\)/);
|
||||
expect(CSS).toMatch(/\.nav-link::after[\s\S]*background: var\(--indigo\)/);
|
||||
});
|
||||
|
||||
it("keeps localized navigation controls inside compact viewports", () => {
|
||||
const mobile = CSS.split("@media (max-width: 520px)")[1];
|
||||
|
||||
expect(mobile).toMatch(/\.site-nav-inner\s*\{\s*gap:\s*0\.5rem/);
|
||||
expect(mobile).toMatch(/\.site-nav-actions\s*\{[\s\S]*?min-width:\s*0/);
|
||||
expect(CSS).toMatch(/\.paper-wordmark-mark\s*\{[^}]*height:\s*22px;/);
|
||||
expect(CSS).toMatch(/\.paper-wordmark-logo\s*\{[^}]*height:\s*20px;/);
|
||||
expect(CSS).toMatch(/\.site-nav-actions\s*\{[\s\S]*?flex-shrink:\s*0/);
|
||||
expect(CSS).toMatch(/\.site-nav-actions\s*>\s*\*\s*\{\s*flex-shrink:\s*0/);
|
||||
expect(CSS).toMatch(/@media \(max-width: 900px\)[\s\S]*?\.site-github-link\s*\{\s*display:\s*none/);
|
||||
// The locale <select> and the home wordmark must keep a usable hit
|
||||
// target on every viewport, not only below 520px. Long native option
|
||||
// labels and 2xl companion text used to collapse the wordmark to 0; the
|
||||
// select now sits inside the fixed icon box instead of sizing the row.
|
||||
expect(CSS).not.toMatch(/\.site-nav-actions select/);
|
||||
expect(CSS).toMatch(/\.nav-locale select\s*\{[^}]*position:\s*absolute;[^}]*inset:\s*0;/);
|
||||
// `min-width` is the floor that keeps the wordmark clickable; the shrink
|
||||
// factor stays at 1 so the compact controls are never the ones pushed
|
||||
// past `overflow-x: clip` when the row is over budget.
|
||||
expect(CSS).toMatch(/\.paper-wordmark\s*\{[^}]*flex:\s*0 1 auto;[^}]*min-width:\s*8\.75rem;/);
|
||||
expect(CSS).not.toMatch(/\.paper-wordmark\s*\{[^}]*flex:\s*0 0 auto;/);
|
||||
});
|
||||
});
|
||||
@@ -16,29 +16,55 @@ describe("design grammar contract", () => {
|
||||
expect(new Set(defined)).toEqual(new Set(["6px", "10px", "14px", "999px"]));
|
||||
const used = [...CSS.matchAll(/border-radius:\s*([^;]+);/g)].map((m) => m[1].trim());
|
||||
expect(used.length).toBeGreaterThan(0);
|
||||
for (const value of used) expect(value).toMatch(/^var\(--radius-(control|surface|sheet|pill)\)$/);
|
||||
// A corner is a grammar token or square; per-corner shorthands combine them.
|
||||
for (const value of used) {
|
||||
expect(value).toMatch(/^(?:(?:var\(--radius-(control|surface|sheet|pill)\)|0)\s*){1,4}$/);
|
||||
}
|
||||
const scale = TAILWIND.match(/borderRadius:\s*\{([\s\S]*?)\}/)?.[1] ?? "";
|
||||
expect(scale).not.toMatch(/\d+(px|rem)/);
|
||||
});
|
||||
|
||||
it("has one focus ring and the set_theme selection", () => {
|
||||
expect(CSS.match(/outline:/g)).toHaveLength(1);
|
||||
expect(CSS.match(/(?<![\w-])outline:/g)).toHaveLength(1);
|
||||
expect(CSS).toContain(":focus-visible { outline: var(--gpui-focus-width) solid var(--ring); outline-offset: var(--gpui-focus-offset); }");
|
||||
expect(resolveWhale("var(--gpui-focus-width)")).toBe("2px");
|
||||
expect(resolveWhale("var(--gpui-focus-offset)")).toBe("3px");
|
||||
expect(CSS).not.toMatch(/outline:\s*none/);
|
||||
expect(CSS).toMatch(/::selection \{ background: var\(--selection\); \}/);
|
||||
// The light ring on the dark stage would fall under 3:1.
|
||||
expect(CSS).toMatch(/\.site-footer,[\s\S]*?\{ --ring: var\(--gpui-dark-primary\); \}/);
|
||||
// The light ring on the dark stage would fall under 3:1, so the stage
|
||||
// re-declares it.
|
||||
expect(CSS).toMatch(/\.stage,\s*\.site-footer\s*\{[^}]*--ring: var\(--gpui-dark-primary\);/);
|
||||
});
|
||||
|
||||
it("hovers a primary fill at the primary @ 0.9", () => {
|
||||
for (const cls of ["portal-button-primary", "folio-button-primary", "paper-install-cta"]) {
|
||||
const hover = CSS.match(new RegExp(`\\.${cls}:hover \\{([^}]*)\\}`))?.[1] ?? "";
|
||||
expect(hover, cls).toMatch(/background: var\(--indigo-deep\)/);
|
||||
it("paints every primary fill with the logo gradient and white text", () => {
|
||||
for (const cls of ["btn-primary", "paper-install-cta"]) {
|
||||
const rest = CSS.match(new RegExp(`\\.${cls}[^{]*\\{([^}]*)\\}`))?.[1] ?? "";
|
||||
const hover = CSS.match(new RegExp(`\\.${cls}:hover[^{]*\\{([^}]*)\\}`))?.[1] ?? "";
|
||||
expect(rest, cls).toMatch(/background: var\(--brand-fill\)/);
|
||||
expect(rest, cls).toMatch(/color: var\(--on-brand\)/);
|
||||
expect(hover, cls).toMatch(/background: var\(--brand-fill-hover\)/);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps every keyframed animation behind a motion preference", () => {
|
||||
// Strip the blocks that only run when motion is allowed; nothing left
|
||||
// may start an animation.
|
||||
let rest = CSS;
|
||||
for (;;) {
|
||||
const at = rest.indexOf("@media (prefers-reduced-motion: no-preference)");
|
||||
if (at < 0) break;
|
||||
let depth = 0;
|
||||
let end = rest.indexOf("{", at);
|
||||
for (let i = end; i < rest.length; i++) {
|
||||
if (rest[i] === "{") depth++;
|
||||
else if (rest[i] === "}" && --depth === 0) { end = i; break; }
|
||||
}
|
||||
rest = rest.slice(0, at) + rest.slice(end + 1);
|
||||
}
|
||||
const starts = [...rest.matchAll(/animation:\s*([^;]+);/g)].map((m) => m[1].trim());
|
||||
for (const value of starts) expect(value).toBe("none");
|
||||
});
|
||||
|
||||
it("times motion with the spring tokens and stills it under reduced motion", () => {
|
||||
expect(CSS).not.toMatch(/\d+ms ease[,;]/);
|
||||
expect(CSS).not.toMatch(/cubic-bezier\(0\.25, 0\.46/);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveWhale } from "./whale-tokens";
|
||||
import { siteCss } from "./site-css";
|
||||
|
||||
const CSS = siteCss();
|
||||
@@ -11,36 +10,10 @@ function selectorBlock(selector: string): string {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function selectorVars(selector: string): Record<string, string> {
|
||||
const block = selectorBlock(selector);
|
||||
const vars: Record<string, string> = {};
|
||||
// Values may be a literal hex or a `var(--whale-*)` reference into the
|
||||
// generated app/tokens.css; non-color values (channel triples, lengths) are
|
||||
// skipped, exactly as the hex-only regex used to skip them.
|
||||
for (const match of block.matchAll(/--([\w-]+):\s*([^;]+);/g)) {
|
||||
const value = resolveWhale(match[2].trim());
|
||||
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value)) vars[match[1]] = value;
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
function relativeLuminance(hex: string): number {
|
||||
const full = hex.length === 4 ? hex.slice(1).split("").map((c) => c + c).join("") : hex.slice(1);
|
||||
const channels = full
|
||||
.match(/.{2}/g)!
|
||||
.map((value) => Number.parseInt(value, 16) / 255)
|
||||
.map((value) => (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4));
|
||||
|
||||
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
||||
}
|
||||
|
||||
function contrastRatio(foreground: string, background: string): number {
|
||||
const lighter = Math.max(relativeLuminance(foreground), relativeLuminance(background));
|
||||
const darker = Math.min(relativeLuminance(foreground), relativeLuminance(background));
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
const PINNED_DARK = '.ocean-column,\n.site-footer,\n:root[data-theme="dark"]';
|
||||
const PINNED_DARK = ':root[data-theme="dark"]';
|
||||
|
||||
/** Every custom property declared in the OS-dark block (inside the media query). */
|
||||
function osDarkVars(): Record<string, string> {
|
||||
@@ -59,15 +32,15 @@ describe("site-wide theme contract", () => {
|
||||
it("follows the OS by default and repeats the pinned dark scheme exactly", () => {
|
||||
// No region-scoped dark sheet: the docs portal follows the root theme.
|
||||
expect(CSS).not.toMatch(/html\[data-theme="dark"\] \.docs-(portal|theme)/);
|
||||
expect(selectorBlock(":root")).toMatch(/color-scheme:\s*light/);
|
||||
expect(CSS).toMatch(/(?:^|\n):root \{[^}]*color-scheme:\s*light;/);
|
||||
expect(osDarkVars()).toEqual(allVars(PINNED_DARK));
|
||||
expect(selectorBlock(PINNED_DARK)).toMatch(/color-scheme:\s*dark/);
|
||||
});
|
||||
|
||||
it("re-inks the navy nav wordmark under both dark selectors", () => {
|
||||
// wordmark.svg is fixed #142352 ink (~1.2:1 on the dark charcoal).
|
||||
expect(CSS).toMatch(/:root:not\(\[data-theme="light"\]\) \.paper-wordmark-logo\s*\{\s*filter:/);
|
||||
expect(CSS).toMatch(/:root\[data-theme="dark"\] \.paper-wordmark-logo\s*\{\s*filter:/);
|
||||
it("re-inks the nav wordmark from the mark ink in every appearance", () => {
|
||||
// wordmark.svg is fixed #142352 ink; the header draws it as a mask so
|
||||
// --mark-ink (the page's text colour) paints it on paper and on the ocean.
|
||||
expect(CSS).toMatch(/\.paper-wordmark-logo,\s*\.wordmark\s*\{[^}]*background: var\(--mark-ink\);/);
|
||||
});
|
||||
|
||||
it("shows the toggle on every page with one system|light|dark storage contract", () => {
|
||||
@@ -83,31 +56,18 @@ describe("site-wide theme contract", () => {
|
||||
});
|
||||
|
||||
describe("docs theme contrast contract", () => {
|
||||
// The docs sheet follows the site theme. Light is the bare :root; dark is
|
||||
// :root overlaid with the pinned dark block (which the OS-dark block
|
||||
// repeats). Both are checked.
|
||||
const themes = () => [
|
||||
selectorVars(":root"),
|
||||
{ ...selectorVars(":root"), ...selectorVars(PINNED_DARK) },
|
||||
];
|
||||
|
||||
it("keeps current and hover sidebar text at WCAG AA contrast", () => {
|
||||
for (const theme of themes()) {
|
||||
const accent = theme["docs-accent"];
|
||||
const background = theme["paper"];
|
||||
expect(contrastRatio(accent, background)).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
expect(CSS).toMatch(/\.docs-sidebar-link:hover,\s*\.docs-sidebar-link-current\s*{[^}]*color:\s*var\(--docs-accent\)/s);
|
||||
});
|
||||
|
||||
it("keeps secondary button text at WCAG AA contrast", () => {
|
||||
for (const theme of themes()) {
|
||||
const text = theme["docs-button-text"];
|
||||
const background = theme["docs-button-bg"];
|
||||
expect(contrastRatio(text, background)).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
expect(selectorBlock(".docs-theme .portal-button-secondary")).toContain(
|
||||
"color: var(--docs-button-text)",
|
||||
// The docs read the legacy names; they must resolve to the roles, whose
|
||||
// contrast gpui-role-tokens.test.ts checks in every scheme.
|
||||
it("points the docs accent and buttons at the role tokens", () => {
|
||||
const root = Object.fromEntries(
|
||||
[...CSS.matchAll(/(?:^|\n):root \{([^}]*)\}/g)].flatMap((m) =>
|
||||
[...m[1].matchAll(/--([\w-]+):\s*([^;]+);/g)].map((v) => [v[1], v[2].trim()]),
|
||||
),
|
||||
);
|
||||
expect(root["docs-accent"]).toBe("var(--accent)");
|
||||
expect(root["docs-button-text"]).toBe("var(--text)");
|
||||
expect(root["docs-button-bg"]).toBe("var(--panel)");
|
||||
expect(CSS).toMatch(/\.docs-sidebar-link:hover,\s*\.docs-sidebar-link-current\s*{[^}]*color:\s*var\(--docs-accent\)/s);
|
||||
expect(selectorBlock(".docs-theme .portal-button-secondary")).toContain("color: var(--docs-button-text)");
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,17 @@ import { describe, expect, it } from "vitest";
|
||||
import { resolveWhale } from "./whale-tokens";
|
||||
import { siteCss } from "./site-css";
|
||||
|
||||
// The role layer (app/styles/tokens-roles.css): every role in every scheme,
|
||||
// inks from the versioned GPUI artifact, the navy ocean grounds measured
|
||||
// against every ink at WCAG AA.
|
||||
const CSS = siteCss();
|
||||
const DESIGN = JSON.parse(readFileSync(new URL("../../vendor/codewhale-design/tokens.json", import.meta.url), "utf8"));
|
||||
const ROLES = ["bg", "surface", "panel", "text", "muted", "line", "accent", "on-accent", "hover", "selected", "selection", "ring"];
|
||||
const ROLES = [
|
||||
"bg", "surface", "panel", "text", "muted", "line", "line-strong", "accent", "on-accent",
|
||||
"hover", "selected", "selection", "ring", "live", "attention", "danger",
|
||||
];
|
||||
const GROUNDS = ["bg", "surface", "panel", "hover", "selected"];
|
||||
const TEXT_INKS = ["text", "muted", "accent", "live", "attention", "danger"];
|
||||
|
||||
function declarations(block: string): Record<string, string> {
|
||||
const vars: Record<string, string> = {};
|
||||
@@ -46,11 +54,12 @@ function contrast(a: string, b: string): number {
|
||||
const light = () => roleBlock(":root");
|
||||
const osDark = () => roleBlock(':root:not([data-theme="light"])');
|
||||
const pinnedDark = () => roleBlock(':root[data-theme="dark"]');
|
||||
const stage = () => roleBlock(".stage,\n.site-footer");
|
||||
|
||||
describe("GPUI role tokens", () => {
|
||||
it("defines every role in light, OS-dark, and pinned-dark schemes", () => {
|
||||
for (const scheme of [light(), osDark(), pinnedDark()]) {
|
||||
for (const role of ROLES) expect(scheme, role).toHaveProperty(role);
|
||||
describe("role tokens", () => {
|
||||
it("defines every role in light, OS-dark, pinned-dark and the stage", () => {
|
||||
for (const [name, scheme] of [["light", light()], ["os-dark", osDark()], ["pinned", pinnedDark()], ["stage", stage()]] as const) {
|
||||
for (const role of ROLES) expect(scheme, `${name}.${role}`).toHaveProperty(role);
|
||||
}
|
||||
// The OS-dark scheme is guarded so a pinned light page stays light, and
|
||||
// the pinned dark block repeats it exactly.
|
||||
@@ -58,39 +67,55 @@ describe("GPUI role tokens", () => {
|
||||
expect(pinnedDark()).toEqual(osDark());
|
||||
});
|
||||
|
||||
it("uses generated GPUI tokens only, never a literal hex, in role positions", () => {
|
||||
for (const scheme of [light(), osDark()]) {
|
||||
for (const role of ROLES) {
|
||||
expect(scheme[role], role).not.toMatch(/#[0-9a-f]{3,8}\b/i);
|
||||
expect(scheme[role], role).toMatch(/var\(--gpui-(light|dark)-[\w-]+\)/);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("paints the versioned GPUI artifact without a second palette", () => {
|
||||
const roles: Record<string, string> = {
|
||||
bg: "background", surface: "sidebar", panel: "surface", text: "foreground",
|
||||
muted: "muted_foreground", line: "border", accent: "primary",
|
||||
"on-accent": "primary_foreground", hover: "hover", selected: "selected", ring: "primary",
|
||||
it("inks with the versioned GPUI artifact in both appearances", () => {
|
||||
const inks: Record<string, string> = {
|
||||
text: "foreground", muted: "muted_foreground", accent: "primary", "on-accent": "primary_foreground",
|
||||
ring: "primary", live: "live", attention: "attention", danger: "danger",
|
||||
};
|
||||
for (const [mode, scheme] of [["light", light()], ["dark", osDark()]] as const) {
|
||||
for (const [role, key] of Object.entries(roles)) {
|
||||
for (const [mode, scheme] of [["light", light()], ["dark", osDark()], ["dark", stage()]] as const) {
|
||||
for (const [role, key] of Object.entries(inks)) {
|
||||
expect(scheme[role], `${mode}.${role}`).toMatch(/^var\(--gpui-(light|dark)-[\w-]+\)$/);
|
||||
expect(hex(scheme[role]), `${mode}.${role}`).toBe(`#${DESIGN.colors[mode][key]}`);
|
||||
}
|
||||
expect(scheme.selection).toBe(`rgb(var(--gpui-${mode}-primary-rgb) / var(--gpui-selection-opacity))`);
|
||||
}
|
||||
expect(Number(resolveWhale("var(--gpui-selection-opacity)"))).toBe(DESIGN.selection_opacity);
|
||||
expect(Number(resolveWhale("var(--gpui-primary-hover-opacity)"))).toBe(DESIGN.primary_hover_opacity);
|
||||
// Light keeps the artifact's paper grounds; dark reaches for the ocean.
|
||||
expect(hex(light().bg)).toBe(`#${DESIGN.colors.light.background}`);
|
||||
expect(hex(light().panel)).toBe(`#${DESIGN.colors.light.surface}`);
|
||||
for (const role of GROUNDS) expect(osDark()[role], role).toMatch(/^var\(--ocean-[\w-]+\)$/);
|
||||
});
|
||||
|
||||
it("keeps text, muted text, links, and button text at WCAG AA in both schemes", () => {
|
||||
for (const scheme of [light(), osDark()]) {
|
||||
const bg = hex(scheme.bg);
|
||||
expect(contrast(hex(scheme.muted), bg)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(scheme.muted), hex(scheme.panel))).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(scheme.text), bg)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(scheme.accent), bg)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(scheme["on-accent"]), hex(scheme.accent))).toBeGreaterThanOrEqual(4.5);
|
||||
it("keeps every text ink at WCAG AA on every ground, in every scheme", () => {
|
||||
for (const [name, scheme] of [["light", light()], ["dark", osDark()], ["stage", stage()]] as const) {
|
||||
for (const ground of GROUNDS) {
|
||||
for (const ink of TEXT_INKS) {
|
||||
expect(contrast(hex(scheme[ink]), hex(scheme[ground])), `${name}: ${ink} on ${ground}`).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
}
|
||||
// Control edges and state marks clear the 3:1 non-text floor.
|
||||
for (const ground of ["bg", "panel"]) {
|
||||
expect(contrast(hex(scheme["line-strong"]), hex(scheme[ground])), `${name}: line-strong on ${ground}`).toBeGreaterThanOrEqual(3);
|
||||
}
|
||||
expect(contrast(hex(scheme["on-accent"]), hex(scheme.accent)), `${name}: on-accent`).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
});
|
||||
|
||||
it("puts white text on the logo gradient only where it clears 4.5:1", () => {
|
||||
const fill = CSS.match(/--brand-fill:\s*linear-gradient\(([^;]+)\);/)?.[1] ?? "";
|
||||
const stops = [...fill.matchAll(/#[0-9a-f]{6}|var\(--brand-[\w-]+\)/gi)].map((m) => hex(m[0]));
|
||||
expect(stops.length).toBeGreaterThanOrEqual(2);
|
||||
for (const stop of stops) expect(contrast("#ffffff", stop), stop).toBeGreaterThanOrEqual(4.5);
|
||||
expect(hex("var(--brand-deep)")).toBe("#0b48bb");
|
||||
expect(hex("var(--brand-light)")).toBe("#1e8fd8");
|
||||
});
|
||||
|
||||
it("keeps footer text readable on the sea below its waterline", () => {
|
||||
const sea = CSS.match(/--sea:\s*linear-gradient\(([^;]+)\);/)?.[1] ?? "";
|
||||
const stops = [...sea.matchAll(/#[0-9a-f]{6}/gi)].map((m) => m[0].toLowerCase());
|
||||
// The first stop is the bright waterline; content starts below it.
|
||||
for (const stop of stops.slice(1)) {
|
||||
expect(contrast(hex(stage().muted), stop), `muted on ${stop}`).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(stage().text), stop), `text on ${stop}`).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -17,10 +17,10 @@ export const states: StatesDict = {
|
||||
reload: "Reload the page",
|
||||
homeLink: "Back to the home page",
|
||||
docsIndexLink: "Open the documentation index",
|
||||
notFoundTitle: "We all make typos.",
|
||||
notFoundTitle: "This page isn’t here",
|
||||
notFoundBody:
|
||||
"This page doesn’t exist yet.\nNeither does this game.",
|
||||
notFoundHomeLink: "Return to base",
|
||||
"The link may be out of date, or the address has a typo. Start again from the home page or the documentation.",
|
||||
notFoundHomeLink: "Go to the home page",
|
||||
notFoundPosterAlt:
|
||||
"A blue whale in tactical gear on the fictional Codwhale: Modern Whalefare game poster.",
|
||||
unavailableTitle: "The live record has not loaded",
|
||||
|
||||
@@ -14,9 +14,9 @@ export const states: StatesDict = {
|
||||
reload: "重新加载页面",
|
||||
homeLink: "返回首页",
|
||||
docsIndexLink: "打开文档目录",
|
||||
notFoundTitle: "谁还没打过错字呢。",
|
||||
notFoundBody: "这个页面还不存在。\n这款游戏也一样。",
|
||||
notFoundHomeLink: "返回基地",
|
||||
notFoundTitle: "这个页面不在这里",
|
||||
notFoundBody: "链接可能已经过期,或者地址有拼写错误。请从首页或文档重新开始。",
|
||||
notFoundHomeLink: "返回首页",
|
||||
notFoundPosterAlt: "虚构游戏《Codwhale: Modern Whalefare》的海报,一只身穿战术装备的蓝鲸。",
|
||||
unavailableTitle: "实时记录尚未加载",
|
||||
unavailableBody: "数据源没有响应上一次刷新,或者此页面自构建以来尚未刷新。这里不会用编造的内容填充。",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { siteCss } from "./site-css";
|
||||
|
||||
// The whale-road (codewhale-design/DIRECTION.md): one character drawn from
|
||||
// the v2 kit, one horizon, the sea below it, and a header that stays usable
|
||||
// in every locale.
|
||||
const CSS = siteCss();
|
||||
const web = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
|
||||
const POSES = [
|
||||
"rest", "listen", "think", "busy", "read", "search", "write", "run", "browse",
|
||||
"talk", "pod", "needs", "done", "hmm", "sleep", "computer", "connect",
|
||||
];
|
||||
|
||||
describe("the whale", () => {
|
||||
it("ships all seventeen v2 poses in the logo gradient", () => {
|
||||
const dir = new URL("../public/whale/", import.meta.url);
|
||||
expect(readdirSync(dir).filter((f) => f.endsWith(".svg")).sort()).toEqual(POSES.map((p) => `${p}.svg`).sort());
|
||||
for (const pose of POSES) {
|
||||
const svg = readFileSync(new URL(`${pose}.svg`, dir), "utf8");
|
||||
expect(svg, pose).toContain('stop-color="#1E8FD8"');
|
||||
expect(svg, pose).toContain('stop-color="#0B48BB"');
|
||||
// A still pose: never animated.
|
||||
expect(svg, pose).not.toMatch(/<animate|@keyframes/);
|
||||
}
|
||||
expect(web("components/whale-pose.tsx")).toContain(POSES.map((p) => `"${p}"`).join(",\n "));
|
||||
});
|
||||
|
||||
it("uses the brand mark for the site icons, not a tile", () => {
|
||||
const icon = web("app/icon.svg");
|
||||
expect(icon).toContain('stop-color="#1E8FD8"');
|
||||
expect(icon).not.toMatch(/<rect/);
|
||||
expect(web("app/manifest.ts")).not.toContain("#142352");
|
||||
});
|
||||
|
||||
it("keeps no parody or retired illustrations", () => {
|
||||
expect(existsSync(new URL("../public/codwhale-404.webp", import.meta.url))).toBe(false);
|
||||
for (const gone of ["strata", "seal", "install-binary"]) {
|
||||
expect(existsSync(new URL(`../components/${gone}.tsx`, import.meta.url)), gone).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("the horizon", () => {
|
||||
it("draws the footer as the sea under one horizon, and lets the home sea continue into it", () => {
|
||||
const footer = web("components/footer.tsx");
|
||||
expect(footer).toContain('className="horizon"');
|
||||
expect(CSS).toMatch(/\.site-footer \{[^}]*background: var\(--sea\)/);
|
||||
expect(CSS).toMatch(/main:has\(\.sea-continues:last-child\) \+ \.site-footer > \.horizon/);
|
||||
expect(web("app/[locale]/page.tsx")).toContain("sea-continues");
|
||||
});
|
||||
|
||||
it("paints the stage with the dark set in both appearances", () => {
|
||||
expect(CSS).toMatch(/\.stage,\s*\.site-footer\s*\{[^}]*--ring: var\(--gpui-dark-primary\);[^}]*color-scheme: dark;/);
|
||||
});
|
||||
|
||||
it("keeps the texture static and out of forced colours", () => {
|
||||
expect(CSS).toMatch(/@media \(forced-colors: active\) \{[\s\S]*?\.sea-texture \{ display: none; \}/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the header", () => {
|
||||
it("keeps localized navigation controls inside compact viewports", () => {
|
||||
expect(CSS).toMatch(/@media \(max-width: 520px\) \{\s*\.site-nav-inner\s*\{\s*gap:\s*0\.5rem;\s*\}\s*\.site-nav-actions\s*\{\s*min-width:\s*0;/);
|
||||
expect(CSS).toMatch(/\.site-nav-actions\s*\{[\s\S]*?flex-shrink:\s*0/);
|
||||
expect(CSS).toMatch(/\.site-nav-actions\s*>\s*\*\s*\{\s*flex-shrink:\s*0/);
|
||||
expect(CSS).toMatch(/@media \(max-width: 900px\)[\s\S]*?\.site-github-link\s*\{\s*display:\s*none/);
|
||||
// The locale <select> sits inside the fixed icon box instead of sizing
|
||||
// the row, and the wordmark keeps a clickable floor.
|
||||
expect(CSS).not.toMatch(/\.site-nav-actions select/);
|
||||
expect(CSS).toMatch(/\.nav-locale select\s*\{[^}]*position:\s*absolute;[^}]*inset:\s*0;/);
|
||||
expect(CSS).toMatch(/\.paper-wordmark\s*\{[^}]*flex:\s*0 1 auto;[^}]*min-width:\s*8\.75rem;/);
|
||||
});
|
||||
|
||||
it("re-inks the wordmark through the mark ink, never a filter", () => {
|
||||
expect(CSS).toMatch(/\.paper-wordmark-logo,\s*\.wordmark\s*\{[^}]*background: var\(--mark-ink\);[^}]*mask: url\("\/brand\/wordmark\.svg"\)/);
|
||||
expect(CSS).not.toMatch(/paper-wordmark-logo\s*\{\s*filter:/);
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,6 @@ import { INSTALL_GUIDE } from "./install-guide.generated";
|
||||
import { getChrome, getHome } from "./i18n/dictionaries";
|
||||
import { footerProjectLinks } from "./i18n/links";
|
||||
import { TERMINAL_SCREENSHOT } from "./media-manifest";
|
||||
import { siteCss } from "./site-css";
|
||||
|
||||
const root = new URL("../../", import.meta.url);
|
||||
|
||||
@@ -645,16 +644,6 @@ done
|
||||
expect(github).toContain('kind: "release"');
|
||||
});
|
||||
|
||||
it("keeps reduced motion static without hiding the reasoning trace", () => {
|
||||
const css = siteCss();
|
||||
|
||||
expect(css).toMatch(
|
||||
/@media \(prefers-reduced-motion: reduce\)\s*\{[\s\S]*?\.ticker-track\s*\{\s*animation:\s*none;\s*\}[\s\S]*?\}/,
|
||||
);
|
||||
// Freezing the track must not also hide the entries it stopped scrolling.
|
||||
expect(css).toMatch(/\.ticker-viewport\s*\{\s*overflow-x:\s*auto;\s*\}/);
|
||||
});
|
||||
|
||||
it("keeps the homepage free of fabricated demo panels", () => {
|
||||
const homepage = text("web/app/[locale]/page.tsx");
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("typography contract", () => {
|
||||
expect(rootVar("font-mono")).toMatch(/^ui-monospace,/);
|
||||
expect(rootVar("font-cjk")).not.toMatch(/Serif|(?<!sans-)serif/);
|
||||
expect(resolveWhale(rootVar("text-mono"))).toBe("0.8125rem");
|
||||
expect(rootVar("text-prose")).toBe("0.9375rem");
|
||||
expect(rootVar("text-prose")).toBe("1.0625rem");
|
||||
});
|
||||
|
||||
it("preloads only the Latin subset of Shannon Sans", () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ const RAW: Record<string, string> = (() => {
|
||||
for (const match of generated.matchAll(/--((?:whale|light|shoreline-light|shoreline|gpui)-[\w-]+):\s*([^;]+);/g)) {
|
||||
raw[match[1]] = match[2].trim();
|
||||
}
|
||||
for (const match of globals.matchAll(/--(gpui-[\w-]+):\s*([^;]+);/g)) {
|
||||
for (const match of globals.matchAll(/--((?:gpui|ocean|brand)-[\w-]+):\s*([^;]+);/g)) {
|
||||
raw[match[1]] = match[2].trim();
|
||||
}
|
||||
if (Object.keys(raw).length === 0) {
|
||||
@@ -44,8 +44,8 @@ function flatten(name: string, seen = new Set<string>()): string {
|
||||
return flatten(alias[1], seen.add(name));
|
||||
}
|
||||
|
||||
/** Resolve a `var(--whale-*)`, `var(--light-*)`, `var(--shoreline-*)`, or `var(--gpui-*)` reference to its literal value; pass anything else through. */
|
||||
/** Resolve a `var(--whale-*)`, `var(--light-*)`, `var(--shoreline-*)`, `var(--gpui-*)`, `var(--ocean-*)` or `var(--brand-*)` reference to its literal value; pass anything else through. */
|
||||
export function resolveWhale(value: string): string {
|
||||
const match = value.match(/^var\(--((?:whale|light|shoreline-light|shoreline|gpui)-[\w-]+)\)$/);
|
||||
const match = value.match(/^var\(--((?:whale|light|shoreline-light|shoreline|gpui|ocean|brand)-[\w-]+)\)$/);
|
||||
return match ? flatten(match[1]) : value;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.12 -1.88 C43.93 -1.85 44.11 -1.81 44.14 -1.66 C44.17 -1.5 43.98 -0.48 43.67 1.07 C43.59 1.45 43.39 2.27 43.16 3.09 C43.02 3.63 42.86 4.13 42.7 4.6 C42.65 4.74 42.57 4.95 42.53 5.07 C42.49 5.2 42.36 5.54 42.24 5.84 C42.13 6.14 42.02 6.44 42 6.49 C41.98 6.55 41.91 6.73 41.84 6.89 C41.78 7.06 41.69 7.28 41.65 7.38 C41.56 7.62 40.31 10.4 40.15 10.71 C40.08 10.84 39.99 11.03 39.93 11.14 C39.87 11.25 39.8 11.39 39.77 11.44 C39.71 11.56 39.62 11.73 39.38 12.12 C39.29 12.27 39.21 12.42 39.19 12.45 C39.18 12.48 39.09 12.63 38.99 12.8 C38.89 12.97 38.66 13.35 38.48 13.66 C38.01 14.45 38.03 14.42 37.66 14.98 C37.25 15.6 37.16 15.73 36.18 17.07 C33.12 21.26 28.94 25.27 24.59 28.17 C22.72 29.4 22.42 29.59 20.77 30.47 C18.73 31.56 17.73 32.02 15.32 32.99 C12.75 34 10.48 34.67 7.36 35.33 C4.16 35.99 1.52 36.26 -1.99 36.29 C-3.97 36.3 -4.16 36.25 -3.31 35.93 C-2.9 35.78 -2.08 35.36 -1.07 34.78 C2.55 32.72 6.39 28.86 10.03 23.61 C10.2 23.37 10.63 22.75 11 22.21 C11.37 21.69 11.96 20.84 12.32 20.33 C13.31 18.89 14.38 17.43 15.52 15.94 C17.51 13.33 20.86 9.71 23.12 7.72 C25.16 5.92 25.31 5.8 26.61 4.78 C27.44 4.13 28.63 3.25 29.08 2.95 C29.2 2.87 29.45 2.7 29.63 2.58 C30.02 2.31 30.8 1.82 30.82 1.82 C30.84 1.82 31.06 1.69 31.32 1.63 C35.06 -0.69 39.47 -1.99 43.12 -1.88 Z"/><path d="M8.54 10.91 C10.35 11.22 11.1 12.74 10.02 13.9 C8.86 15.15 6.35 15 5.47 13.62 C5.25 13.27 5.24 12.5 5.46 12.1 C5.95 11.18 7.24 10.68 8.54 10.91 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-10.29 -31.39 C-7.67 -31.48 -5.65 -31.17 -3.7 -30.4 C-3.35 -30.26 -2.36 -29.74 -2.25 -29.64 C-2.21 -29.61 -2.16 -29.57 -2.14 -29.57 C-1.97 -29.57 -1.15 -28.8 -0.5 -28.03 C0.4 -26.96 1.59 -25.9 2.18 -25.64 C2.24 -25.61 2.39 -25.52 2.51 -25.45 C2.78 -25.28 3.79 -24.77 4.21 -24.6 C4.72 -24.39 5.01 -24.3 6.05 -24 C8.62 -23.27 12.47 -23.6 15.02 -24.76 C15.43 -24.95 16.16 -25.31 16.34 -25.42 C16.45 -25.49 16.67 -25.62 16.84 -25.72 C18.18 -26.52 19.92 -28.32 20.76 -29.77 C21.27 -30.66 21.23 -31 20.6 -31 C20.47 -31 20.02 -31.07 19.12 -31.25 C18.12 -31.44 17.15 -31.69 15.68 -32.12 C15.2 -32.26 13.47 -32.86 12.83 -33.11 C12.53 -33.23 12.14 -33.38 11.77 -33.51 C11.64 -33.56 11.38 -33.66 11.18 -33.73 C11 -33.8 10.76 -33.88 10.67 -33.9 C10.57 -33.93 10.43 -33.99 10.34 -34.03 C10.26 -34.07 10.16 -34.1 10.14 -34.1 C10.11 -34.1 9.85 -34.17 9.55 -34.27 C8.04 -34.78 6.3 -35.06 4.8 -35.05 C3.99 -35.05 2.22 -34.88 1.9 -34.78 C1.82 -34.76 1.64 -34.73 1.5 -34.71 C1.37 -34.69 0.99 -34.63 0.67 -34.58 C-0.04 -34.46 -1.24 -34.47 -1.91 -34.6 C-2.14 -34.65 -2.46 -34.7 -2.62 -34.72 C-3.13 -34.79 -3.02 -34.92 -2.33 -35.06 C-1.34 -35.26 0.51 -35.93 1.32 -36.39 C3.54 -37.66 5.38 -39.41 6.48 -41.3 C7.07 -42.31 7 -42.58 6.07 -42.92 C3.55 -43.84 0.91 -44.47 -1.94 -44.8 C-3.65 -45.01 -6.32 -45.1 -7.86 -45 C-9.06 -40.46 -9.87 -35.93 -10.29 -31.39 Z"/><path d="M-7.86 -45 C-9.27 -44.91 -9.98 -44.85 -10.66 -44.76 C-11.52 -44.65 -12.67 -44.47 -12.76 -44.44 C-12.8 -44.43 -12.97 -44.4 -13.15 -44.37 C-13.79 -44.27 -16.51 -43.6 -17.26 -43.36 C-17.53 -43.27 -17.97 -43.13 -18.23 -43.05 C-19.23 -42.73 -20.62 -42.19 -22.08 -41.56 C-22.54 -41.37 -24.48 -40.42 -24.59 -40.34 C-24.61 -40.32 -24.86 -40.18 -25.13 -40.04 C-25.4 -39.9 -25.93 -39.6 -26.31 -39.36 C-26.69 -39.13 -27.13 -38.87 -27.29 -38.77 C-30.75 -36.73 -34.65 -33.25 -37.67 -29.52 C-37.97 -29.16 -38.6 -28.34 -38.7 -28.2 C-38.76 -28.11 -38.89 -27.92 -38.99 -27.78 C-39.14 -27.57 -39.38 -27.22 -40.16 -26.06 C-40.51 -25.55 -40.69 -25.24 -41.27 -24.22 C-41.6 -23.64 -41.9 -23.11 -41.94 -23.04 C-42.03 -22.87 -42.94 -21.03 -43.14 -20.61 C-43.41 -20.03 -43.43 -19.98 -43.63 -19.48 C-44.02 -18.53 -44.14 -18.22 -44.36 -17.56 C-44.44 -17.32 -44.56 -17 -44.62 -16.85 C-44.67 -16.7 -44.77 -16.44 -44.82 -16.27 C-44.87 -16.1 -44.96 -15.82 -45.01 -15.63 C-45.2 -15.05 -45.53 -13.82 -45.6 -13.47 C-45.62 -13.35 -45.68 -13.15 -45.71 -13.01 C-45.78 -12.76 -45.86 -12.37 -46.08 -11.29 C-46.32 -10.15 -46.54 -8.76 -46.63 -7.86 C-46.66 -7.57 -46.69 -7.3 -46.7 -7.27 C-46.76 -7.06 -46.88 -4.34 -46.88 -3.17 C-46.87 0.28 -46.38 4.39 -45.69 6.78 C-45.64 6.96 -45.6 7.13 -45.6 7.18 C-45.6 7.22 -45.55 7.42 -45.49 7.62 C-45.44 7.83 -45.35 8.14 -45.3 8.34 C-45.04 9.26 -44.63 10.49 -44.05 12.1 C-44.01 12.22 -43.96 12.36 -43.93 12.42 C-43.9 12.47 -43.81 12.68 -43.73 12.88 C-43.65 13.08 -43.55 13.31 -43.51 13.4 C-43.48 13.48 -43.34 13.79 -43.21 14.09 C-42.97 14.65 -41.94 16.74 -41.72 17.11 C-41.64 17.24 -41.1 18.16 -41.05 18.26 C-40.9 18.52 -40.64 18.94 -40.41 19.25 C-40.18 19.57 -40.08 19.71 -39.63 20.38 C-38.69 21.77 -37.3 23.44 -35.99 24.75 C-35.16 25.58 -35.05 25.68 -34.25 26.2 C-31.72 27.86 -29.76 28.74 -26.83 29.52 C-24.6 30.11 -20.8 30.19 -18.15 29.71 C-16.59 29.42 -14.57 28.8 -12.89 28.09 C-10.29 27Line truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.12 -6.24 C43.93 -6.34 44.11 -6.33 44.17 -6.17 C44.23 -6.01 44.23 -4.92 44.19 -3.27 C44.18 -2.87 44.11 -2 44.02 -1.14 C43.96 -0.58 43.89 -0.05 43.8 0.43 C43.77 0.57 43.73 0.8 43.7 0.93 C43.68 1.05 43.61 1.42 43.54 1.73 C43.48 2.04 43.42 2.35 43.4 2.41 C43.39 2.47 43.35 2.65 43.31 2.83 C43.27 3 43.22 3.23 43.2 3.35 C43.14 3.59 42.34 6.53 42.23 6.86 C42.18 6.99 42.12 7.2 42.08 7.32 C42.04 7.43 41.99 7.58 41.97 7.63 C41.93 7.76 41.86 7.94 41.69 8.36 C41.62 8.53 41.57 8.68 41.56 8.71 C41.55 8.74 41.48 8.91 41.41 9.09 C41.34 9.27 41.17 9.68 41.04 10.01 C40.7 10.86 40.72 10.83 40.44 11.43 C40.13 12.1 40.06 12.25 39.31 13.71 C36.96 18.27 33.54 22.79 29.81 26.25 C28.23 27.73 27.97 27.96 26.54 29.08 C24.78 30.46 23.91 31.07 21.79 32.4 C19.55 33.82 17.52 34.87 14.7 36.08 C11.8 37.31 9.35 38.1 6.03 38.83 C4.15 39.25 3.96 39.24 4.71 38.75 C5.06 38.52 5.76 37.94 6.61 37.17 C9.63 34.42 12.53 29.91 15.02 24.08 C15.13 23.82 15.43 23.13 15.68 22.54 C15.94 21.96 16.35 21.02 16.6 20.46 C17.3 18.87 18.07 17.25 18.91 15.6 C20.38 12.71 23.03 8.62 24.93 6.33 C26.64 4.25 26.76 4.11 27.89 2.92 C28.6 2.16 29.64 1.12 30.03 0.76 C30.14 0.66 30.36 0.46 30.52 0.31 C30.87 -0.02 31.55 -0.62 31.58 -0.63 C31.59 -0.63 31.79 -0.79 32.04 -0.89 C35.35 -3.78 39.5 -5.78 43.12 -6.24 Z"/><path d="M11.08 12.2 C12.89 12.14 13.88 13.33 13.02 14.56 C12.1 15.87 9.64 16.21 8.55 15.17 C8.27 14.9 8.14 14.22 8.28 13.84 C8.59 12.93 9.77 12.24 11.08 12.2 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-17.53 -22.52 C-14.73 -23.65 -12.5 -24.17 -10.24 -24.19 C-9.83 -24.2 -8.67 -24.09 -8.52 -24.03 C-8.47 -24.01 -8.41 -24 -8.39 -24.01 C-8.2 -24.08 -7.17 -23.64 -6.31 -23.14 C-5.13 -22.43 -3.66 -21.84 -2.97 -21.8 C-2.91 -21.8 -2.73 -21.76 -2.59 -21.73 C-2.27 -21.66 -1.1 -21.52 -0.63 -21.49 C-0.06 -21.45 0.27 -21.46 1.4 -21.5 C4.17 -21.54 7.92 -22.74 10.16 -24.41 C10.53 -24.67 11.17 -25.18 11.32 -25.33 C11.42 -25.42 11.61 -25.59 11.74 -25.72 C12.89 -26.79 14.22 -28.91 14.74 -30.5 C15.06 -31.48 14.94 -31.8 14.33 -31.67 C14.2 -31.64 13.74 -31.62 12.83 -31.61 C11.81 -31.59 10.81 -31.63 9.28 -31.75 C8.78 -31.78 6.97 -32.01 6.29 -32.12 C5.97 -32.17 5.55 -32.24 5.17 -32.29 C5.03 -32.31 4.75 -32.35 4.55 -32.38 C4.35 -32.41 4.1 -32.44 4 -32.44 C3.91 -32.45 3.75 -32.47 3.65 -32.48 C3.56 -32.5 3.46 -32.51 3.44 -32.5 C3.41 -32.5 3.14 -32.51 2.82 -32.53 C1.19 -32.63 -0.64 -32.4 -2.19 -31.91 C-3.03 -31.63 -4.86 -30.85 -5.18 -30.64 C-5.25 -30.59 -5.43 -30.49 -5.58 -30.43 C-5.71 -30.36 -6.1 -30.16 -6.43 -29.98 C-7.16 -29.6 -8.44 -29.15 -9.19 -29.02 C-9.45 -28.97 -9.8 -28.9 -9.97 -28.85 C-10.54 -28.72 -10.45 -28.89 -9.73 -29.3 C-8.71 -29.88 -6.88 -31.25 -6.12 -32 C-4.05 -34.03 -2.49 -36.36 -1.75 -38.56 C-1.36 -39.72 -1.48 -39.96 -2.51 -40.02 C-5.32 -40.08 -8.24 -39.75 -11.34 -38.99 C-13.22 -38.51 -16.11 -37.52 -17.75 -36.8 C-18.09 -31.89 -18.02 -27.13 -17.53 -22.52 Z"/><path d="M-17.75 -36.8 C-19.24 -36.15 -20 -35.81 -20.7 -35.46 C-21.6 -35.01 -22.79 -34.4 -22.88 -34.34 C-22.91 -34.31 -23.09 -34.22 -23.28 -34.12 C-23.93 -33.79 -26.65 -32.19 -27.38 -31.7 C-27.64 -31.53 -28.06 -31.25 -28.31 -31.09 C-29.27 -30.47 -30.57 -29.55 -31.91 -28.57 C-32.32 -28.27 -34.03 -26.93 -34.12 -26.82 C-34.14 -26.8 -34.35 -26.62 -34.59 -26.42 C-34.82 -26.23 -35.28 -25.82 -35.6 -25.51 C-35.92 -25.21 -36.3 -24.86 -36.43 -24.73 C-39.39 -22.01 -42.49 -17.8 -44.67 -13.53 C-44.88 -13.11 -45.33 -12.17 -45.39 -12.02 C-45.44 -11.91 -45.52 -11.7 -45.59 -11.55 C-45.7 -11.31 -45.86 -10.91 -46.38 -9.62 C-46.61 -9.04 -46.73 -8.71 -47.08 -7.59 C-47.28 -6.95 -47.46 -6.37 -47.49 -6.29 C-47.55 -6.11 -48.06 -4.12 -48.16 -3.67 C-48.3 -3.06 -48.3 -3.01 -48.39 -2.49 C-48.54 -1.56 -48.58 -1.25 -48.62 -0.63 C-48.64 -0.41 -48.66 -0.11 -48.68 0.02 C-48.69 0.16 -48.71 0.39 -48.71 0.54 C-48.71 0.69 -48.72 0.94 -48.71 1.1 C-48.72 1.6 -48.66 2.63 -48.62 2.9 C-48.6 3 -48.59 3.16 -48.58 3.28 C-48.57 3.48 -48.52 3.79 -48.38 4.61 C-48.23 5.48 -47.97 6.52 -47.74 7.18 C-47.67 7.39 -47.61 7.58 -47.61 7.61 C-47.6 7.77 -46.79 9.74 -46.4 10.61 C-45.34 13.31 -43.83 16.87 -42.66 19.07 C-42.57 19.23 -42.49 19.39 -42.48 19.44 C-42.48 19.48 -42.39 19.66 -42.29 19.85 C-42.19 20.04 -42.04 20.33 -41.95 20.51 C-41.51 21.36 -40.85 22.47 -39.95 23.93 C-39.88 24.04 -39.8 24.16 -39.77 24.21 C-39.73 24.26 -39.6 24.45 -39.47 24.63 C-39.35 24.81 -39.2 25.01 -39.15 25.09 C-39.1 25.16 -38.9 25.43 -38.71 25.7 C-38.36 26.2 -36.91 28.03 -36.62 28.34 C-36.52 28.45 -35.81 29.24 -35.73 29.33 C-35.53 29.56 -35.19 29.91 -34.9 30.16 C-34.61 30.43 -34.49 30.55 -33.9 31.11 C-32.69 32.27 -30.99 33.62 -29.43 34.63 C-28.45 35.27 -28.32 35.34 -27.43 35.69 C-24.61 36.79 -22.51 37.23 -19.48 37.38 C-17.18 37.5 -13.45 36.79 -10.96 35.77 C-9.49 35.16 -7.64 34.13 -6.15 33.09 C-3.85 31.48 -1.45 28Line truncated
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.25 1.7 C44.06 1.74 44.24 1.78 44.26 1.94 C44.29 2.11 44.1 3.14 43.76 4.71 C43.68 5.11 43.46 5.93 43.22 6.76 C43.06 7.3 42.9 7.81 42.73 8.27 C42.68 8.41 42.6 8.62 42.55 8.74 C42.51 8.87 42.37 9.21 42.25 9.51 C42.13 9.81 42.02 10.11 41.99 10.16 C41.97 10.21 41.9 10.39 41.83 10.56 C41.77 10.72 41.67 10.95 41.63 11.05 C41.53 11.29 40.23 14.05 40.07 14.37 C40 14.49 39.9 14.68 39.84 14.79 C39.78 14.9 39.71 15.03 39.68 15.09 C39.62 15.2 39.52 15.38 39.28 15.77 C39.19 15.91 39.1 16.06 39.09 16.09 C39.07 16.12 38.98 16.27 38.88 16.44 C38.77 16.6 38.54 16.99 38.35 17.29 C37.87 18.07 37.89 18.04 37.51 18.6 C37.09 19.21 36.99 19.35 36 20.67 C32.87 24.82 28.66 28.75 24.3 31.57 C22.43 32.77 22.13 32.95 20.49 33.8 C18.45 34.86 17.46 35.3 15.08 36.22 C12.53 37.2 10.28 37.83 7.2 38.45 C4.04 39.06 1.42 39.31 -2.05 39.3 C-4.01 39.3 -4.2 39.24 -3.36 38.93 C-2.95 38.79 -2.13 38.37 -1.14 37.8 C2.45 35.77 6.27 31.97 9.92 26.76 C10.08 26.53 10.52 25.91 10.89 25.38 C11.26 24.86 11.85 24.02 12.21 23.52 C13.21 22.09 14.29 20.65 15.44 19.18 C17.44 16.61 20.82 13.04 23.11 11.1 C25.17 9.34 25.32 9.22 26.64 8.22 C27.48 7.59 28.68 6.73 29.13 6.43 C29.25 6.36 29.51 6.19 29.69 6.07 C30.09 5.81 30.87 5.32 30.9 5.32 C30.91 5.32 31.13 5.19 31.39 5.01 C35.17 2.76 39.6 1.52 43.25 1.7 Z"/><path d="M8.47 13.76 C10.27 14.07 11.01 15.49 9.93 16.58 C8.76 17.74 6.26 17.59 5.39 16.29 C5.17 15.96 5.17 15.24 5.39 14.87 C5.88 14.01 7.18 13.54 8.47 13.76 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-10.29 -28.39 C-7.67 -28.48 -5.65 -28.17 -3.7 -27.4 C-3.35 -27.26 -2.36 -26.74 -2.25 -26.64 C-2.21 -26.61 -2.16 -26.57 -2.14 -26.57 C-1.97 -26.57 -1.15 -25.8 -0.5 -25.03 C0.4 -23.96 1.59 -22.9 2.18 -22.64 C2.24 -22.61 2.39 -22.52 2.51 -22.45 C2.78 -22.28 3.79 -21.77 4.21 -21.6 C4.72 -21.39 5.01 -21.3 6.05 -21 C8.62 -20.27 12.47 -20.6 15.02 -21.76 C15.43 -21.95 16.16 -22.31 16.34 -22.42 C16.45 -22.49 16.67 -22.62 16.84 -22.72 C18.18 -23.52 19.92 -25.32 20.76 -26.77 C21.27 -27.66 21.23 -28 20.6 -28 C20.47 -28 20.02 -28.07 19.12 -28.25 C18.12 -28.44 17.15 -28.69 15.68 -29.12 C15.2 -29.26 13.47 -29.86 12.83 -30.11 C12.53 -30.23 12.14 -30.38 11.77 -30.51 C11.64 -30.56 11.38 -30.66 11.18 -30.73 C11 -30.8 10.76 -30.88 10.67 -30.9 C10.57 -30.93 10.43 -30.99 10.34 -31.03 C10.26 -31.07 10.16 -31.1 10.14 -31.1 C10.11 -31.1 9.85 -31.17 9.55 -31.27 C8.04 -31.78 6.3 -32.06 4.8 -32.05 C3.99 -32.05 2.22 -31.88 1.9 -31.78 C1.82 -31.76 1.64 -31.73 1.5 -31.71 C1.37 -31.69 0.99 -31.63 0.67 -31.58 C-0.04 -31.46 -1.24 -31.47 -1.91 -31.6 C-2.14 -31.65 -2.46 -31.7 -2.62 -31.72 C-3.13 -31.79 -3.02 -31.92 -2.33 -32.06 C-1.34 -32.26 0.51 -32.93 1.32 -33.39 C3.54 -34.66 5.38 -36.41 6.48 -38.3 C7.07 -39.31 7 -39.58 6.07 -39.92 C3.55 -40.84 0.91 -41.47 -1.94 -41.8 C-3.65 -42.01 -6.32 -42.1 -7.86 -42 C-9.06 -37.46 -9.87 -32.93 -10.29 -28.39 Z"/><path d="M-7.86 -42 C-9.27 -41.91 -9.98 -41.85 -10.66 -41.76 C-11.52 -41.65 -12.67 -41.47 -12.76 -41.44 C-12.8 -41.43 -12.97 -41.4 -13.15 -41.37 C-13.79 -41.27 -16.51 -40.6 -17.26 -40.36 C-17.53 -40.27 -17.97 -40.13 -18.23 -40.05 C-19.23 -39.73 -20.62 -39.19 -22.08 -38.56 C-22.54 -38.37 -24.48 -37.42 -24.59 -37.34 C-24.61 -37.32 -24.86 -37.18 -25.13 -37.04 C-25.4 -36.9 -25.93 -36.6 -26.31 -36.36 C-26.69 -36.13 -27.13 -35.87 -27.29 -35.77 C-30.75 -33.73 -34.65 -30.25 -37.67 -26.52 C-37.97 -26.16 -38.6 -25.34 -38.7 -25.2 C-38.76 -25.11 -38.89 -24.92 -38.99 -24.78 C-39.14 -24.57 -39.38 -24.22 -40.16 -23.06 C-40.51 -22.55 -40.69 -22.24 -41.27 -21.22 C-41.6 -20.64 -41.9 -20.11 -41.94 -20.04 C-42.03 -19.87 -42.94 -18.03 -43.14 -17.61 C-43.41 -17.03 -43.43 -16.98 -43.63 -16.48 C-44.02 -15.53 -44.14 -15.22 -44.36 -14.56 C-44.44 -14.32 -44.56 -14 -44.62 -13.85 C-44.67 -13.7 -44.77 -13.44 -44.82 -13.27 C-44.87 -13.1 -44.96 -12.82 -45.01 -12.63 C-45.2 -12.05 -45.53 -10.82 -45.6 -10.47 C-45.62 -10.35 -45.68 -10.15 -45.71 -10.01 C-45.78 -9.76 -45.86 -9.37 -46.08 -8.29 C-46.32 -7.15 -46.54 -5.76 -46.63 -4.86 C-46.66 -4.57 -46.69 -4.3 -46.7 -4.27 C-46.76 -4.06 -46.88 -1.34 -46.88 -0.17 C-46.87 3.28 -46.38 7.39 -45.69 9.78 C-45.64 9.96 -45.6 10.13 -45.6 10.18 C-45.6 10.22 -45.55 10.42 -45.49 10.62 C-45.44 10.83 -45.35 11.14 -45.3 11.34 C-45.04 12.26 -44.63 13.49 -44.05 15.1 C-44.01 15.22 -43.96 15.36 -43.93 15.42 C-43.9 15.47 -43.81 15.68 -43.73 15.88 C-43.65 16.08 -43.55 16.31 -43.51 16.4 C-43.48 16.48 -43.34 16.79 -43.21 17.09 C-42.97 17.65 -41.94 19.74 -41.72 20.11 C-41.64 20.24 -41.1 21.16 -41.05 21.26 C-40.9 21.52 -40.64 21.94 -40.41 22.25 C-40.18 22.57 -40.08 22.71 -39.63 23.38 C-38.69 24.77 -37.3 26.44 -35.99 27.75 C-35.16 28.58 -35.05 28.68 -34.25 29.2 C-31.72 30.86 -29.76 31.74 -26.83 32.52 C-24.6 33.1 -20.8 33.19 -18.15 32.71 C-16.59 32.42 -14.57 31.8 -12.89 31.09 C-10.Line truncated
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.29 1.89 C44.1 1.94 44.28 1.98 44.3 2.14 C44.33 2.31 44.13 3.35 43.79 4.92 C43.7 5.32 43.48 6.15 43.23 6.98 C43.08 7.52 42.91 8.02 42.73 8.49 C42.68 8.62 42.6 8.83 42.55 8.96 C42.51 9.08 42.37 9.42 42.25 9.72 C42.13 10.02 42.02 10.32 41.99 10.37 C41.97 10.42 41.9 10.6 41.83 10.77 C41.76 10.93 41.66 11.15 41.62 11.26 C41.52 11.49 40.21 14.25 40.04 14.56 C39.97 14.69 39.87 14.88 39.81 14.98 C39.75 15.09 39.68 15.23 39.65 15.28 C39.59 15.4 39.49 15.57 39.24 15.96 C39.15 16.11 39.06 16.25 39.05 16.28 C39.03 16.31 38.94 16.46 38.84 16.63 C38.73 16.79 38.49 17.17 38.31 17.48 C37.82 18.25 37.84 18.23 37.46 18.78 C37.03 19.39 36.94 19.52 35.93 20.84 C32.79 24.96 28.56 28.86 24.2 31.65 C22.34 32.83 22.04 33.01 20.39 33.86 C18.36 34.9 17.37 35.34 14.99 36.25 C12.46 37.21 10.21 37.84 7.14 38.45 C3.99 39.06 1.39 39.3 -2.07 39.3 C-4.03 39.29 -4.22 39.23 -3.37 38.92 C-2.97 38.78 -2.15 38.37 -1.16 37.8 C2.42 35.77 6.23 31.96 9.88 26.77 C10.04 26.53 10.48 25.92 10.85 25.39 C11.22 24.87 11.81 24.03 12.17 23.53 C13.17 22.11 14.25 20.67 15.4 19.21 C17.42 16.65 20.81 13.1 23.1 11.17 C25.17 9.42 25.32 9.31 26.65 8.32 C27.49 7.69 28.69 6.84 29.15 6.55 C29.27 6.47 29.52 6.31 29.71 6.19 C30.11 5.93 30.89 5.45 30.92 5.45 C30.93 5.45 31.16 5.32 31.42 5.14 C35.2 2.91 39.65 1.7 43.29 1.89 Z"/><path d="M8.46 13.83 C10.26 14.14 11 15.6 9.91 16.71 C8.74 17.89 6.25 17.73 5.38 16.4 C5.16 16.07 5.17 15.33 5.38 14.95 C5.88 14.07 7.17 13.6 8.46 13.83 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-10.29 -28.39 C-7.67 -28.48 -5.65 -28.17 -3.7 -27.4 C-3.35 -27.26 -2.36 -26.74 -2.25 -26.64 C-2.21 -26.61 -2.16 -26.57 -2.14 -26.57 C-1.97 -26.57 -1.15 -25.8 -0.5 -25.03 C0.4 -23.96 1.59 -22.9 2.18 -22.64 C2.24 -22.61 2.39 -22.52 2.51 -22.45 C2.78 -22.28 3.79 -21.77 4.21 -21.6 C4.72 -21.39 5.01 -21.3 6.05 -21 C8.62 -20.27 12.47 -20.6 15.02 -21.76 C15.43 -21.95 16.16 -22.31 16.34 -22.42 C16.45 -22.49 16.67 -22.62 16.84 -22.72 C18.18 -23.52 19.92 -25.32 20.76 -26.77 C21.27 -27.66 21.23 -28 20.6 -28 C20.47 -28 20.02 -28.07 19.12 -28.25 C18.12 -28.44 17.15 -28.69 15.68 -29.12 C15.2 -29.26 13.47 -29.86 12.83 -30.11 C12.53 -30.23 12.14 -30.38 11.77 -30.51 C11.64 -30.56 11.38 -30.66 11.18 -30.73 C11 -30.8 10.76 -30.88 10.67 -30.9 C10.57 -30.93 10.43 -30.99 10.34 -31.03 C10.26 -31.07 10.16 -31.1 10.14 -31.1 C10.11 -31.1 9.85 -31.17 9.55 -31.27 C8.04 -31.78 6.3 -32.06 4.8 -32.05 C3.99 -32.05 2.22 -31.88 1.9 -31.78 C1.82 -31.76 1.64 -31.73 1.5 -31.71 C1.37 -31.69 0.99 -31.63 0.67 -31.58 C-0.04 -31.46 -1.24 -31.47 -1.91 -31.6 C-2.14 -31.65 -2.46 -31.7 -2.62 -31.72 C-3.13 -31.79 -3.02 -31.92 -2.33 -32.06 C-1.34 -32.26 0.51 -32.93 1.32 -33.39 C3.54 -34.66 5.38 -36.41 6.48 -38.3 C7.07 -39.31 7 -39.58 6.07 -39.92 C3.55 -40.84 0.91 -41.47 -1.94 -41.8 C-3.65 -42.01 -6.32 -42.1 -7.86 -42 C-9.06 -37.46 -9.87 -32.93 -10.29 -28.39 Z"/><path d="M-7.86 -42 C-9.27 -41.91 -9.98 -41.85 -10.66 -41.76 C-11.52 -41.65 -12.67 -41.47 -12.76 -41.44 C-12.8 -41.43 -12.97 -41.4 -13.15 -41.37 C-13.79 -41.27 -16.51 -40.6 -17.26 -40.36 C-17.53 -40.27 -17.97 -40.13 -18.23 -40.05 C-19.23 -39.73 -20.62 -39.19 -22.08 -38.56 C-22.54 -38.37 -24.48 -37.42 -24.59 -37.34 C-24.61 -37.32 -24.86 -37.18 -25.13 -37.04 C-25.4 -36.9 -25.93 -36.6 -26.31 -36.36 C-26.69 -36.13 -27.13 -35.87 -27.29 -35.77 C-30.75 -33.73 -34.65 -30.25 -37.67 -26.52 C-37.97 -26.16 -38.6 -25.34 -38.7 -25.2 C-38.76 -25.11 -38.89 -24.92 -38.99 -24.78 C-39.14 -24.57 -39.38 -24.22 -40.16 -23.06 C-40.51 -22.55 -40.69 -22.24 -41.27 -21.22 C-41.6 -20.64 -41.9 -20.11 -41.94 -20.04 C-42.03 -19.87 -42.94 -18.03 -43.14 -17.61 C-43.41 -17.03 -43.43 -16.98 -43.63 -16.48 C-44.02 -15.53 -44.14 -15.22 -44.36 -14.56 C-44.44 -14.32 -44.56 -14 -44.62 -13.85 C-44.67 -13.7 -44.77 -13.44 -44.82 -13.27 C-44.87 -13.1 -44.96 -12.82 -45.01 -12.63 C-45.2 -12.05 -45.53 -10.82 -45.6 -10.47 C-45.62 -10.35 -45.68 -10.15 -45.71 -10.01 C-45.78 -9.76 -45.86 -9.37 -46.08 -8.29 C-46.32 -7.15 -46.54 -5.76 -46.63 -4.86 C-46.66 -4.57 -46.69 -4.3 -46.7 -4.27 C-46.76 -4.06 -46.88 -1.34 -46.88 -0.17 C-46.87 3.28 -46.38 7.39 -45.69 9.78 C-45.64 9.96 -45.6 10.13 -45.6 10.18 C-45.6 10.22 -45.55 10.42 -45.49 10.62 C-45.44 10.83 -45.35 11.14 -45.3 11.34 C-45.04 12.26 -44.63 13.49 -44.05 15.1 C-44.01 15.22 -43.96 15.36 -43.93 15.42 C-43.9 15.47 -43.81 15.68 -43.73 15.88 C-43.65 16.08 -43.55 16.31 -43.51 16.4 C-43.48 16.48 -43.34 16.79 -43.21 17.09 C-42.97 17.65 -41.94 19.74 -41.72 20.11 C-41.64 20.24 -41.1 21.16 -41.05 21.26 C-40.9 21.52 -40.64 21.94 -40.41 22.25 C-40.18 22.57 -40.08 22.71 -39.63 23.38 C-38.69 24.77 -37.3 26.44 -35.99 27.75 C-35.16 28.58 -35.05 28.68 -34.25 29.2 C-31.72 30.86 -29.76 31.74 -26.83 32.52 C-24.6 33.1 -20.8 33.19 -18.15 32.71 C-16.59 32.42 -14.57 31.8 -12.89 31Line truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M42.66 -10.38 C43.46 -10.5 43.64 -10.49 43.7 -10.34 C43.76 -10.19 43.75 -9.16 43.71 -7.58 C43.72 -7.1 43.67 -6.24 43.59 -5.38 C43.54 -4.81 43.48 -4.28 43.4 -3.78 C43.38 -3.64 43.34 -3.41 43.32 -3.28 C43.3 -3.15 43.23 -2.78 43.17 -2.45 C43.12 -2.13 43.06 -1.82 43.05 -1.76 C43.04 -1.7 43 -1.51 42.97 -1.33 C42.94 -1.16 42.89 -0.92 42.87 -0.8 C42.82 -0.55 42.08 2.47 41.98 2.81 C41.94 2.95 41.88 3.16 41.84 3.28 C41.8 3.4 41.76 3.55 41.74 3.61 C41.7 3.74 41.64 3.93 41.48 4.37 C41.42 4.54 41.36 4.7 41.35 4.73 C41.34 4.77 41.28 4.94 41.22 5.12 C41.15 5.31 40.99 5.74 40.87 6.08 C40.55 6.96 40.56 6.93 40.3 7.56 C40 8.26 39.94 8.42 39.22 9.95 C36.95 14.73 33.56 19.52 29.79 23.21 C28.17 24.76 27.9 25 26.43 26.16 C24.6 27.58 23.7 28.2 21.49 29.54 C19.12 30.93 16.98 31.93 14.01 33.02 C10.95 34.09 8.37 34.71 4.89 35.19 C2.93 35.44 2.72 35.41 3.52 34.99 C3.9 34.8 4.64 34.28 5.54 33.59 C8.78 31.09 11.92 26.78 14.63 21.1 C14.74 20.84 15.07 20.17 15.34 19.59 C15.62 19.02 16.05 18.09 16.32 17.54 C17.05 15.97 17.86 14.37 18.73 12.72 C20.24 9.84 22.92 5.7 24.8 3.33 C26.49 1.18 26.61 1.04 27.72 -0.22 C28.42 -1.02 29.43 -2.12 29.81 -2.51 C29.92 -2.61 30.13 -2.83 30.29 -2.99 C30.63 -3.33 31.3 -3.98 31.33 -3.98 C31.34 -3.99 31.53 -4.16 31.65 -4.88 C34.92 -7.81 39.05 -9.86 42.66 -10.38 Z"/><path d="M10.61 8.65 C12.48 8.84 13.42 9.89 12.42 10.5 C11.38 11.29 8.91 11.72 7.93 11.19 C7.69 11.07 7.6 10.59 7.75 10.24 C8.12 9.45 9.31 8.73 10.61 8.65 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-15.06 -30.17 C-12.5 -30.71 -10.46 -30.76 -8.4 -30.32 C-8.04 -30.24 -6.98 -29.89 -6.85 -29.81 C-6.81 -29.77 -6.75 -29.75 -6.73 -29.75 C-6.56 -29.78 -5.63 -29.14 -4.87 -28.47 C-3.82 -27.53 -2.5 -26.63 -1.88 -26.44 C-1.82 -26.43 -1.66 -26.35 -1.53 -26.29 C-1.25 -26.16 -0.18 -25.77 0.25 -25.65 C0.78 -25.5 1.08 -25.44 2.14 -25.28 C4.78 -24.9 8.55 -25.77 10.9 -27.27 C11.29 -27.51 11.96 -27.98 12.12 -28.11 C12.22 -28.19 12.42 -28.36 12.57 -28.48 C13.79 -29.46 15.26 -31.49 15.88 -33.04 C16.27 -33.99 16.18 -34.32 15.55 -34.23 C15.42 -34.22 14.97 -34.23 14.05 -34.27 C13.03 -34.33 12.04 -34.43 10.53 -34.65 C10.03 -34.73 8.23 -35.08 7.56 -35.23 C7.25 -35.31 6.84 -35.4 6.46 -35.48 C6.32 -35.51 6.05 -35.57 5.85 -35.61 C5.65 -35.65 5.41 -35.7 5.31 -35.71 C5.21 -35.73 5.06 -35.76 4.97 -35.79 C4.88 -35.82 4.78 -35.83 4.76 -35.83 C4.73 -35.83 4.46 -35.87 4.16 -35.92 C2.58 -36.21 0.82 -36.25 -0.68 -36.06 C-1.48 -35.96 -3.24 -35.58 -3.55 -35.45 C-3.62 -35.41 -3.8 -35.36 -3.93 -35.33 C-4.06 -35.29 -4.43 -35.18 -4.75 -35.09 C-5.44 -34.87 -6.64 -34.72 -7.33 -34.75 C-7.57 -34.76 -7.9 -34.76 -8.06 -34.76 C-8.58 -34.74 -8.49 -34.89 -7.83 -35.13 C-6.87 -35.48 -5.12 -36.4 -4.37 -36.96 C-2.32 -38.49 -0.72 -40.45 0.12 -42.46 C0.57 -43.53 0.46 -43.79 -0.52 -44.01 C-3.22 -44.6 -6.04 -44.88 -9.04 -44.81 C-10.82 -44.73 -13.53 -44.36 -15.03 -44 C-15.43 -39.32 -15.44 -34.71 -15.06 -30.17 Z"/><path d="M-15.03 -44 C-16.41 -43.67 -17.1 -43.48 -17.75 -43.27 C-18.58 -43.01 -19.68 -42.64 -19.76 -42.59 C-19.8 -42.57 -19.96 -42.52 -20.14 -42.45 C-20.75 -42.25 -23.31 -41.12 -24.01 -40.75 C-24.26 -40.62 -24.67 -40.4 -24.9 -40.27 C-25.83 -39.78 -27.11 -39.01 -28.45 -38.14 C-28.86 -37.88 -30.61 -36.6 -30.7 -36.5 C-30.72 -36.48 -30.94 -36.3 -31.18 -36.12 C-31.42 -35.93 -31.89 -35.54 -32.23 -35.24 C-32.56 -34.95 -32.95 -34.61 -33.09 -34.49 C-36.14 -31.88 -39.38 -27.77 -41.71 -23.58 C-41.94 -23.17 -42.42 -22.25 -42.48 -22.1 C-42.53 -21.99 -42.63 -21.79 -42.7 -21.64 C-42.81 -21.4 -42.99 -21.01 -43.55 -19.74 C-43.8 -19.17 -43.93 -18.84 -44.33 -17.73 C-44.55 -17.1 -44.75 -16.53 -44.78 -16.45 C-44.85 -16.27 -45.42 -14.3 -45.54 -13.85 C-45.7 -13.23 -45.72 -13.18 -45.83 -12.65 C-46.05 -11.65 -46.12 -11.32 -46.22 -10.64 C-46.26 -10.38 -46.31 -10.05 -46.35 -9.89 C-46.37 -9.73 -46.42 -9.46 -46.44 -9.29 C-46.46 -9.11 -46.5 -8.81 -46.52 -8.63 C-46.6 -8.02 -46.72 -6.75 -46.73 -6.39 C-46.73 -6.27 -46.74 -6.06 -46.75 -5.92 C-46.78 -5.66 -46.79 -5.26 -46.82 -4.17 C-46.86 -3 -46.83 -1.59 -46.76 -0.69 C-46.74 -0.4 -46.73 -0.13 -46.73 -0.09 C-46.76 0.12 -46.4 2.82 -46.19 3.97 C-45.59 7.37 -44.39 11.33 -43.3 13.57 C-43.22 13.73 -43.15 13.9 -43.14 13.94 C-43.13 13.98 -43.05 14.17 -42.96 14.36 C-42.87 14.55 -42.72 14.85 -42.64 15.03 C-42.23 15.89 -41.61 17.03 -40.76 18.52 C-40.7 18.63 -40.62 18.76 -40.59 18.81 C-40.55 18.86 -40.43 19.05 -40.31 19.23 C-40.19 19.42 -40.05 19.63 -40 19.7 C-39.95 19.78 -39.76 20.06 -39.59 20.33 C-39.25 20.84 -37.87 22.73 -37.59 23.05 C-37.49 23.16 -36.81 23.98 -36.73 24.07 C-36.55 24.3 -36.21 24.66 -35.94 24.93 C-35.65 25.2 -35.53 25.33 -34.97 25.91 C-33.8 27.11 -32.14 28.52 -30.62 29.58 C-29.67 30.26 -29.54 30.33 -28.65 30.71 C-25.88 31.91 -23.8 32.43 -20.77 32.68 C-18.48 32.87 -14.72 32.3 -12.2 31.36 C-10.Line truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M33.76 22.08 C34.44 22.46 34.57 22.58 34.52 22.73 C34.48 22.9 33.86 23.75 32.55 25.73 C32.3 26.04 31.77 26.64 31.22 27.23 C30.87 27.6 30.52 27.94 30.18 28.25 C30.08 28.34 29.93 28.48 29.84 28.56 C29.75 28.64 29.49 28.86 29.26 29.06 C29.05 29.25 28.82 29.44 28.78 29.47 C28.74 29.51 28.61 29.63 28.48 29.73 C28.36 29.84 28.19 29.98 28.11 30.04 C27.93 30.19 25.7 31.87 25.43 32.05 C25.32 32.13 25.16 32.24 25.07 32.3 C24.97 32.36 24.86 32.44 24.81 32.47 C24.71 32.54 24.56 32.63 24.2 32.84 C24.06 32.92 23.93 33 23.9 33.02 C23.88 33.03 23.73 33.12 23.58 33.2 C23.43 33.29 23.08 33.49 22.8 33.66 C22.08 34.08 22.11 34.06 21.57 34.34 C20.97 34.64 20.83 34.71 19.47 35.32 C15.24 37.25 10.27 38.49 5.71 38.76 C3.77 38.89 3.46 38.9 1.82 38.88 C-0.2 38.89 -1.17 38.85 -3.44 38.67 C-5.87 38.52 -7.95 38.23 -10.71 37.68 C-13.56 37.14 -15.85 36.5 -18.79 35.4 C-20.47 34.78 -20.62 34.67 -19.78 34.67 C-19.38 34.67 -18.53 34.57 -17.48 34.41 C-13.71 33.82 -9.1 31.86 -4.12 28.76 C-3.89 28.62 -3.3 28.27 -2.79 27.96 C-2.28 27.67 -1.47 27.18 -0.99 26.9 C0.39 26.09 1.84 25.31 3.36 24.55 C6.01 23.25 10.22 21.77 12.88 21.2 C15.29 20.7 15.45 20.68 16.95 20.48 C17.89 20.36 19.23 20.23 19.73 20.2 C19.86 20.2 20.14 20.18 20.34 20.17 C20.77 20.14 21.62 20.11 21.64 20.13 C21.65 20.13 21.89 20.13 22.17 20.12 C26.45 19.54 30.74 20.37 33.76 22.08 Z"/><path d="M-2.87 17.48 C-1.57 18.32 -1.46 19.21 -2.66 19.31 C-3.95 19.43 -5.88 18.39 -6.14 17.49 C-6.21 17.26 -5.99 16.92 -5.71 16.83 C-5.05 16.6 -3.81 16.87 -2.87 17.48 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-1.35 -27.1 C0.92 -26.2 2.53 -25.15 3.84 -23.65 C4.07 -23.36 4.62 -22.44 4.67 -22.3 C4.68 -22.25 4.71 -22.2 4.73 -22.18 C4.86 -22.1 5.12 -21.01 5.24 -20 C5.36 -18.6 5.59 -17.04 5.81 -16.46 C5.83 -16.41 5.88 -16.24 5.91 -16.11 C5.98 -15.81 6.31 -14.77 6.46 -14.38 C6.65 -13.9 6.78 -13.64 7.27 -12.78 C8.76 -10.75 11.94 -8.88 14.56 -8.4 C14.98 -8.31 15.75 -8.2 15.95 -8.19 C16.07 -8.18 16.32 -8.16 16.49 -8.15 C17.98 -8.05 20.33 -8.5 21.73 -9.18 C22.6 -9.59 22.75 -9.88 22.27 -10.22 C22.16 -10.3 21.86 -10.6 21.25 -11.24 C20.58 -11.94 19.97 -12.67 19.07 -13.82 C18.77 -14.2 17.76 -15.62 17.4 -16.17 C17.23 -16.43 17 -16.75 16.79 -17.06 C16.71 -17.17 16.56 -17.39 16.45 -17.55 C16.34 -17.7 16.2 -17.89 16.14 -17.96 C16.09 -18.04 16.01 -18.16 15.96 -18.24 C15.92 -18.32 15.86 -18.39 15.84 -18.41 C15.82 -18.42 15.66 -18.63 15.49 -18.87 C14.6 -20.1 13.46 -21.36 12.37 -22.39 C11.79 -22.96 10.4 -24.11 10.11 -24.25 C10.04 -24.29 9.89 -24.38 9.78 -24.47 C9.67 -24.54 9.35 -24.75 9.07 -24.93 C8.46 -25.29 7.49 -26.03 6.98 -26.53 C6.8 -26.69 6.55 -26.91 6.42 -27.01 C6.01 -27.33 6.16 -27.39 6.81 -27.15 C7.74 -26.77 9.57 -26.18 10.44 -26 C12.81 -25.43 15.15 -25.45 17.02 -26.16 C18.03 -26.57 18.12 -26.81 17.62 -27.7 C16.1 -30.34 13.96 -32.98 11.24 -35.28 C9.55 -36.47 6.96 -37.79 5.58 -38.28 C2.92 -34.67 0.61 -30.95 -1.35 -27.1 Z"/><path d="M5.58 -38.28 C4.34 -38.73 3.71 -38.94 3.1 -39.12 C2.32 -39.34 1.27 -39.61 1.18 -39.62 C1.15 -39.62 0.99 -39.66 0.82 -39.7 C0.24 -39.85 -2.34 -40.28 -3.08 -40.34 C-3.34 -40.36 -3.77 -40.4 -4.02 -40.42 C-5 -40.5 -6.41 -40.54 -7.92 -40.52 C-8.38 -40.52 -10.44 -40.38 -10.56 -40.35 C-10.59 -40.34 -10.86 -40.31 -11.14 -40.28 C-11.43 -40.26 -12.01 -40.18 -12.43 -40.11 C-12.85 -40.04 -13.33 -39.97 -13.5 -39.94 C-17.29 -39.38 -21.99 -37.73 -25.99 -35.53 C-26.39 -35.31 -27.24 -34.82 -27.37 -34.73 C-27.46 -34.67 -27.64 -34.55 -27.78 -34.47 C-27.99 -34.33 -28.33 -34.11 -29.43 -33.37 C-29.92 -33.04 -30.19 -32.84 -31.06 -32.14 C-31.55 -31.75 -31.99 -31.39 -32.05 -31.35 C-32.19 -31.24 -33.57 -29.95 -33.88 -29.65 C-34.29 -29.24 -34.32 -29.21 -34.65 -28.84 C-35.28 -28.16 -35.48 -27.93 -35.88 -27.44 C-36.02 -27.26 -36.22 -27.02 -36.31 -26.91 C-36.41 -26.81 -36.57 -26.61 -36.66 -26.49 C-36.76 -26.36 -36.92 -26.14 -37.02 -26.01 C-37.36 -25.57 -38.02 -24.62 -38.19 -24.35 C-38.24 -24.26 -38.35 -24.1 -38.43 -23.99 C-38.56 -23.8 -38.76 -23.5 -39.28 -22.66 C-39.84 -21.77 -40.49 -20.66 -40.86 -19.91 C-40.98 -19.68 -41.1 -19.47 -41.12 -19.44 C-41.24 -19.28 -42.31 -16.99 -42.75 -15.98 C-44.09 -12.97 -45.25 -9.24 -45.55 -6.96 C-45.57 -6.79 -45.6 -6.62 -45.62 -6.59 C-45.63 -6.55 -45.67 -6.36 -45.69 -6.17 C-45.72 -5.97 -45.76 -5.67 -45.78 -5.49 C-45.91 -4.61 -46.01 -3.42 -46.11 -1.85 C-46.12 -1.73 -46.12 -1.59 -46.12 -1.53 C-46.12 -1.48 -46.12 -1.26 -46.12 -1.06 C-46.13 -0.86 -46.13 -0.63 -46.13 -0.54 C-46.12 -0.46 -46.12 -0.15 -46.12 0.15 C-46.13 0.72 -46.02 2.87 -45.97 3.26 C-45.95 3.4 -45.84 4.38 -45.83 4.49 C-45.8 4.76 -45.73 5.2 -45.66 5.55 C-45.58 5.91 -45.55 6.07 -45.41 6.8 C-45.13 8.32 -44.58 10.25 -43.95 11.84 C-43.56 12.85 -43.5 12.97 -43.03 13.71 C-41.68 16 -40.57 17.39 -38.76 19.01 C-37.38 20.24 -34.54 21.65 -32.15 22.23Line truncated
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M46.36 3.82 C47.19 3.88 47.37 3.93 47.4 4.09 C47.42 4.25 47.19 5.26 46.8 6.83 C46.7 7.23 46.45 8.05 46.19 8.87 C46.01 9.41 45.83 9.91 45.64 10.37 C45.58 10.5 45.5 10.71 45.45 10.84 C45.4 10.96 45.25 11.3 45.11 11.6 C44.99 11.89 44.86 12.18 44.84 12.24 C44.81 12.29 44.73 12.47 44.66 12.63 C44.59 12.79 44.48 13.02 44.44 13.12 C44.33 13.35 42.92 16.09 42.74 16.4 C42.67 16.52 42.56 16.71 42.49 16.82 C42.43 16.93 42.35 17.06 42.32 17.11 C42.26 17.23 42.15 17.4 41.89 17.78 C41.79 17.93 41.7 18.07 41.68 18.1 C41.67 18.13 41.56 18.29 41.46 18.45 C41.34 18.61 41.09 18.99 40.89 19.29 C40.38 20.06 40.4 20.03 39.99 20.58 C39.54 21.19 39.44 21.32 38.37 22.62 C35.04 26.71 30.58 30.56 25.99 33.28 C24.02 34.42 23.71 34.59 21.98 35.4 C19.84 36.39 18.8 36.8 16.3 37.65 C13.64 38.52 11.28 39.07 8.07 39.55 C4.78 40.01 2.06 40.13 -1.52 39.95 C-3.54 39.84 -3.74 39.78 -2.86 39.51 C-2.44 39.39 -1.57 39.02 -0.52 38.51 C3.26 36.66 7.34 33.05 11.29 28.02 C11.47 27.8 11.94 27.2 12.34 26.69 C12.74 26.19 13.38 25.37 13.77 24.89 C14.85 23.5 16 22.1 17.24 20.68 C19.39 18.18 22.99 14.73 25.4 12.85 C27.57 11.14 27.73 11.03 29.11 10.06 C29.99 9.45 31.25 8.61 31.73 8.33 C31.86 8.25 32.12 8.09 32.31 7.97 C32.73 7.72 33.54 7.25 33.57 7.25 C33.58 7.25 33.82 7.13 34.09 6.97 C38.03 4.66 42.62 3.55 46.36 3.82 Z"/><path d="M7.86 14.61 C9.57 15.22 10.03 16.83 8.74 17.94 C7.36 19.14 4.86 18.59 4.26 17.07 C4.11 16.69 4.23 15.88 4.5 15.48 C5.15 14.56 6.62 14.16 7.86 14.61 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-7.27 -29.8 C-4.58 -29.77 -2.51 -29.36 -0.54 -28.47 C-0.19 -28.31 0.81 -27.74 0.92 -27.63 C0.96 -27.59 1.01 -27.56 1.03 -27.56 C1.21 -27.55 2.02 -26.72 2.67 -25.91 C3.55 -24.78 4.74 -23.66 5.34 -23.36 C5.4 -23.34 5.55 -23.24 5.68 -23.15 C5.95 -22.97 6.97 -22.42 7.39 -22.23 C7.91 -22 8.21 -21.89 9.27 -21.56 C11.89 -20.74 15.86 -20.97 18.5 -22.09 C18.93 -22.27 19.69 -22.62 19.89 -22.73 C20 -22.79 20.23 -22.92 20.4 -23.02 C21.81 -23.81 23.68 -25.62 24.54 -27.05 C25.07 -27.93 25.04 -28.27 24.39 -28.3 C24.25 -28.31 23.8 -28.4 22.88 -28.62 C21.85 -28.86 20.87 -29.15 19.37 -29.66 C18.88 -29.83 17.13 -30.52 16.48 -30.8 C16.17 -30.93 15.77 -31.09 15.4 -31.24 C15.27 -31.29 15 -31.4 14.8 -31.47 C14.62 -31.55 14.38 -31.64 14.28 -31.67 C14.18 -31.7 14.04 -31.77 13.95 -31.81 C13.86 -31.85 13.77 -31.89 13.74 -31.89 C13.71 -31.89 13.45 -31.98 13.14 -32.09 C11.6 -32.67 9.83 -33.02 8.28 -33.08 C7.45 -33.11 5.61 -33.02 5.28 -32.93 C5.2 -32.91 5.01 -32.88 4.87 -32.87 C4.73 -32.86 4.34 -32.81 4 -32.77 C3.27 -32.68 2.04 -32.74 1.35 -32.9 C1.11 -32.96 0.79 -33.03 0.62 -33.05 C0.1 -33.14 0.22 -33.27 0.93 -33.39 C1.96 -33.56 3.88 -34.2 4.73 -34.64 C7.06 -35.87 9.02 -37.61 10.23 -39.51 C10.88 -40.53 10.82 -40.8 9.91 -41.2 C7.43 -42.28 4.69 -43 1.78 -43.42 C0.03 -43.68 -2.71 -43.83 -4.28 -43.77 C-5.68 -39.08 -6.68 -34.42 -7.27 -29.8 Z"/><path d="M-4.28 -43.77 C-5.73 -43.74 -6.47 -43.7 -7.17 -43.64 C-8.06 -43.56 -9.25 -43.43 -9.34 -43.4 C-9.38 -43.39 -9.56 -43.37 -9.75 -43.35 C-10.41 -43.27 -13.23 -42.71 -14.02 -42.49 C-14.3 -42.41 -14.76 -42.29 -15.02 -42.21 C-16.06 -41.93 -17.52 -41.44 -19.05 -40.87 C-19.53 -40.69 -21.57 -39.8 -21.68 -39.72 C-21.71 -39.7 -21.97 -39.58 -22.25 -39.44 C-22.54 -39.31 -23.09 -39.02 -23.5 -38.8 C-23.89 -38.58 -24.36 -38.33 -24.52 -38.24 C-28.17 -36.3 -32.35 -32.91 -35.62 -29.22 C-35.94 -28.86 -36.63 -28.04 -36.73 -27.9 C-36.8 -27.81 -36.94 -27.62 -37.05 -27.49 C-37.22 -27.28 -37.48 -26.92 -38.33 -25.77 C-38.71 -25.26 -38.91 -24.95 -39.54 -23.94 C-39.89 -23.36 -40.21 -22.84 -40.25 -22.77 C-40.35 -22.61 -41.29 -20.8 -41.49 -20.38 C-41.75 -19.82 -41.77 -19.78 -41.98 -19.29 C-42.35 -18.38 -42.47 -18.07 -42.68 -17.44 C-42.76 -17.21 -42.87 -16.91 -42.92 -16.76 C-42.97 -16.62 -43.06 -16.38 -43.1 -16.22 C-43.15 -16.05 -43.23 -15.78 -43.28 -15.61 C-43.45 -15.06 -43.75 -13.89 -43.81 -13.57 C-43.83 -13.46 -43.88 -13.27 -43.91 -13.13 C-43.97 -12.9 -44.05 -12.53 -44.24 -11.53 C-44.45 -10.46 -44.65 -9.15 -44.73 -8.3 C-44.76 -8.03 -44.79 -7.78 -44.8 -7.75 C-44.86 -7.56 -45.02 -4.95 -45.06 -3.8 C-45.25 -0.38 -45 3.75 -44.48 6.17 C-44.44 6.34 -44.41 6.52 -44.41 6.57 C-44.41 6.61 -44.38 6.81 -44.33 7.02 C-44.29 7.22 -44.22 7.54 -44.18 7.74 C-43.99 8.67 -43.67 9.92 -43.2 11.55 C-43.16 11.68 -43.12 11.82 -43.1 11.88 C-43.07 11.93 -43 12.15 -42.93 12.36 C-42.86 12.56 -42.78 12.79 -42.75 12.88 C-42.72 12.96 -42.6 13.28 -42.5 13.58 C-42.3 14.16 -41.41 16.31 -41.22 16.68 C-41.15 16.82 -40.69 17.77 -40.64 17.88 C-40.51 18.14 -40.28 18.57 -40.08 18.89 C-39.87 19.23 -39.79 19.38 -39.38 20.07 C-38.54 21.52 -37.29 23.27 -36.09 24.67 C-35.33 25.55 -35.23 25.65 -34.48 26.24 C-32.3 28.07 -30.68 29.1 -28.27 30.1 C-26.44 30.86 -23.07 31.27 -20.43 31.01 C-18.79 30.86 -16.63 30.54 -14.78 30.Line truncated
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M44.47 -18.72 C45.26 -19.03 45.45 -19.07 45.54 -18.93 C45.63 -18.8 45.87 -17.77 46.19 -16.2 C46.27 -15.8 46.4 -14.95 46.5 -14.09 C46.57 -13.54 46.62 -13.01 46.65 -12.51 C46.65 -12.37 46.66 -12.14 46.67 -12.01 C46.67 -11.88 46.69 -11.51 46.69 -11.18 C46.7 -10.87 46.71 -10.55 46.71 -10.49 C46.71 -10.43 46.72 -10.24 46.72 -10.06 C46.72 -9.88 46.72 -9.64 46.73 -9.53 C46.73 -9.27 46.61 -6.21 46.57 -5.85 C46.56 -5.71 46.54 -5.5 46.53 -5.37 C46.52 -5.25 46.5 -5.09 46.5 -5.03 C46.49 -4.9 46.46 -4.7 46.39 -4.25 C46.36 -4.07 46.34 -3.9 46.34 -3.87 C46.34 -3.84 46.31 -3.66 46.28 -3.46 C46.25 -3.27 46.17 -2.82 46.12 -2.47 C45.98 -1.55 45.99 -1.59 45.85 -0.92 C45.69 -0.18 45.66 -0.02 45.25 1.6 C43.95 6.7 41.53 12.06 38.52 16.46 C37.23 18.34 37.02 18.63 35.8 20.11 C34.29 21.93 33.53 22.75 31.64 24.6 C29.61 26.56 27.73 28.09 25.05 29.95 C22.29 31.85 19.91 33.18 16.6 34.65 C14.74 35.47 14.53 35.5 15.21 34.86 C15.54 34.56 16.15 33.84 16.87 32.9 C19.46 29.54 21.55 24.46 22.91 18.2 C22.97 17.91 23.14 17.17 23.28 16.53 C23.42 15.9 23.65 14.89 23.78 14.28 C24.16 12.57 24.6 10.8 25.1 8.98 C25.97 5.79 27.75 1.12 29.14 -1.61 C30.39 -4.09 30.48 -4.26 31.34 -5.73 C31.88 -6.66 32.67 -7.95 32.98 -8.41 C33.07 -8.53 33.24 -8.79 33.37 -8.98 C33.64 -9.39 34.19 -10.16 34.21 -10.17 C34.22 -10.17 34.38 -10.39 34.57 -10.64 C37.23 -14.32 40.94 -17.33 44.47 -18.72 Z"/><path d="M13.05 8.07 C14.81 8.01 15.76 9.24 15.02 10.76 C14.22 12.39 11.76 12.82 10.7 11.69 C10.43 11.41 10.24 10.65 10.34 10.18 C10.58 9.1 11.77 8.12 13.05 8.07 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-19.1 -26.07 C-16.67 -27.21 -14.65 -27.7 -12.51 -27.67 C-12.12 -27.66 -10.99 -27.49 -10.85 -27.43 C-10.8 -27.41 -10.73 -27.39 -10.71 -27.4 C-10.55 -27.44 -9.49 -26.96 -8.58 -26.42 C-7.34 -25.65 -5.87 -24.95 -5.22 -24.82 C-5.16 -24.81 -4.99 -24.75 -4.85 -24.7 C-4.55 -24.6 -3.44 -24.33 -3 -24.25 C-2.46 -24.16 -2.15 -24.13 -1.09 -24.09 C1.6 -24.15 5.18 -25.73 7.25 -27.68 C7.59 -27.99 8.17 -28.58 8.32 -28.74 C8.4 -28.84 8.57 -29.04 8.7 -29.19 C9.73 -30.4 10.93 -32.79 11.14 -34.3 C11.28 -35.23 11.15 -35.51 10.53 -35.33 C10.4 -35.3 9.94 -35.24 9 -35.16 C7.96 -35.06 6.94 -35.04 5.37 -35.05 C4.85 -35.06 2.98 -35.16 2.28 -35.2 C1.95 -35.22 1.52 -35.23 1.13 -35.24 C0.99 -35.24 0.71 -35.25 0.5 -35.25 C0.3 -35.25 0.05 -35.26 -0.05 -35.25 C-0.15 -35.25 -0.31 -35.26 -0.41 -35.27 C-0.5 -35.28 -0.6 -35.28 -0.63 -35.28 C-0.66 -35.27 -0.94 -35.27 -1.25 -35.28 C-2.89 -35.32 -4.72 -35.17 -6.29 -34.85 C-7.13 -34.68 -8.95 -34.15 -9.25 -33.98 C-9.33 -33.94 -9.5 -33.87 -9.64 -33.82 C-9.78 -33.77 -10.15 -33.62 -10.47 -33.48 C-11.16 -33.18 -12.39 -32.81 -13.12 -32.7 C-13.37 -32.66 -13.71 -32.6 -13.87 -32.56 C-14.4 -32.43 -14.34 -32.59 -13.73 -32.98 C-12.83 -33.54 -11.21 -34.8 -10.53 -35.48 C-8.62 -37.29 -7.2 -39.42 -6.58 -41.52 C-6.25 -42.66 -6.39 -42.88 -7.38 -43.08 C-10.18 -43.59 -13.59 -43.3 -16.9 -42.49 C-18.81 -41.91 -21.46 -40.72 -22.78 -39.91 C-21.87 -34.98 -20.64 -30.35 -19.1 -26.07 Z"/><path d="M-22.78 -39.91 C-24.06 -39.25 -24.71 -38.91 -25.3 -38.55 C-26.06 -38.1 -27.07 -37.47 -27.14 -37.41 C-27.17 -37.38 -27.32 -37.28 -27.48 -37.18 C-28.03 -36.83 -30.31 -35.13 -30.92 -34.6 C-31.14 -34.41 -31.49 -34.1 -31.7 -33.92 C-32.5 -33.23 -33.6 -32.17 -34.72 -31.01 C-35.07 -30.65 -36.51 -28.99 -36.58 -28.87 C-36.6 -28.84 -36.78 -28.62 -36.98 -28.39 C-37.17 -28.15 -37.55 -27.65 -37.81 -27.29 C-38.08 -26.92 -38.39 -26.5 -38.5 -26.35 C-40.93 -23.07 -43.23 -18.29 -44.59 -13.61 C-44.73 -13.15 -44.99 -12.13 -45.03 -11.96 C-45.05 -11.85 -45.1 -11.62 -45.13 -11.46 C-45.2 -11.2 -45.29 -10.76 -45.55 -9.38 C-45.67 -8.76 -45.72 -8.41 -45.85 -7.23 C-45.93 -6.56 -45.99 -5.96 -45.99 -5.88 C-46.02 -5.69 -46.1 -3.65 -46.11 -3.19 C-46.11 -2.57 -46.11 -2.52 -46.09 -1.99 C-46.05 -1 -46.03 -0.67 -45.95 -0.01 C-45.93 0.24 -45.9 0.57 -45.89 0.72 C-45.88 0.87 -45.85 1.14 -45.83 1.3 C-45.8 1.47 -45.76 1.75 -45.73 1.93 C-45.66 2.51 -45.44 3.71 -45.37 4.04 C-45.33 4.15 -45.3 4.35 -45.27 4.49 C-45.23 4.72 -45.14 5.1 -44.9 6.11 C-44.64 7.18 -44.27 8.47 -43.98 9.29 C-43.89 9.55 -43.82 9.8 -43.81 9.84 C-43.78 10.03 -42.81 12.48 -42.36 13.54 C-41.05 16.7 -39.05 20.3 -37.55 22.26 C-37.44 22.4 -37.34 22.55 -37.32 22.59 C-37.3 22.63 -37.18 22.8 -37.05 22.96 C-36.93 23.13 -36.73 23.39 -36.61 23.55 C-36.04 24.3 -35.22 25.29 -34.1 26.56 C-34.01 26.66 -33.91 26.77 -33.87 26.81 C-33.82 26.85 -33.66 27.01 -33.52 27.17 C-33.37 27.32 -33.19 27.5 -33.12 27.56 C-33.06 27.62 -32.83 27.86 -32.6 28.09 C-32.17 28.52 -30.46 30.08 -30.13 30.34 C-30.01 30.43 -29.19 31.09 -29.1 31.16 C-28.87 31.35 -28.48 31.63 -28.16 31.84 C-27.84 32.06 -27.69 32.16 -27.03 32.61 C-25.67 33.55 -23.8 34.6 -22.12 35.35 C-21.07 35.82 -20.93 35.87 -20.02 36.09 C-17.14 36.77 -15.05 36.94 -12.1 Line truncated
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M-36.19 -29.55 C-36.04 -29.54 -36.01 -29.54 -36.01 -29.51 C-36 -29.48 -36.04 -29.29 -36.1 -29 C-36.12 -28.93 -36.16 -28.78 -36.21 -28.63 C-36.24 -28.53 -36.27 -28.44 -36.3 -28.36 C-36.31 -28.33 -36.33 -28.3 -36.34 -28.27 C-36.35 -28.25 -36.37 -28.19 -36.4 -28.14 C-36.42 -28.08 -36.44 -28.03 -36.45 -28.02 C-36.45 -28.01 -36.46 -27.98 -36.48 -27.95 C-36.49 -27.92 -36.51 -27.88 -36.51 -27.86 C-36.53 -27.82 -36.78 -27.33 -36.81 -27.27 C-36.82 -27.25 -36.84 -27.22 -36.85 -27.2 C-36.86 -27.18 -36.88 -27.16 -36.88 -27.15 C-36.89 -27.12 -36.91 -27.09 -36.96 -27.03 C-36.98 -27 -36.99 -26.97 -36.99 -26.97 C-37 -26.96 -37.01 -26.94 -37.03 -26.91 C-37.05 -26.88 -37.1 -26.81 -37.13 -26.76 C-37.22 -26.62 -37.22 -26.62 -37.29 -26.53 C-37.37 -26.42 -37.38 -26.39 -37.57 -26.16 C-38.14 -25.43 -38.91 -24.75 -39.7 -24.27 C-40.03 -24.06 -40.09 -24.03 -40.38 -23.88 C-40.74 -23.7 -40.92 -23.62 -41.35 -23.47 C-41.8 -23.3 -42.2 -23.19 -42.74 -23.08 C-43.3 -22.97 -43.77 -22.93 -44.38 -22.93 C-44.73 -22.93 -44.77 -22.94 -44.62 -23 C-44.54 -23.02 -44.4 -23.1 -44.22 -23.2 C-43.58 -23.57 -42.9 -24.25 -42.24 -25.18 C-42.21 -25.22 -42.13 -25.33 -42.07 -25.43 C-42 -25.52 -41.89 -25.67 -41.83 -25.76 C-41.65 -26.01 -41.45 -26.27 -41.24 -26.53 C-40.88 -26.99 -40.26 -27.62 -39.84 -27.95 C-39.47 -28.26 -39.44 -28.28 -39.2 -28.45 C-39.05 -28.57 -38.83 -28.71 -38.74 -28.76 C-38.72 -28.78 -38.68 -28.8 -38.64 -28.83 C-38.57 -28.87 -38.43 -28.95 -38.42 -28.95 C-38.42 -28.95 -38.38 -28.98 -38.33 -29 C-37.65 -29.4 -36.84 -29.6 -36.19 -29.55 Z"/><path d="M-42.54 -27.5 C-42.22 -27.44 -42.09 -27.15 -42.28 -26.93 C-42.49 -26.69 -42.94 -26.73 -43.09 -27 C-43.13 -27.06 -43.13 -27.21 -43.09 -27.29 C-43 -27.46 -42.77 -27.55 -42.54 -27.5 Z"/><path d="M-36.19 0.45 C-36.04 0.46 -36.01 0.46 -36.01 0.49 C-36 0.53 -36.04 0.71 -36.1 1 C-36.12 1.07 -36.16 1.22 -36.21 1.37 C-36.24 1.47 -36.27 1.56 -36.3 1.64 C-36.31 1.67 -36.33 1.7 -36.34 1.73 C-36.35 1.75 -36.37 1.81 -36.4 1.86 C-36.42 1.92 -36.44 1.97 -36.45 1.98 C-36.45 1.99 -36.46 2.02 -36.48 2.05 C-36.49 2.08 -36.51 2.12 -36.51 2.14 C-36.53 2.18 -36.78 2.67 -36.81 2.73 C-36.82 2.75 -36.84 2.78 -36.85 2.8 C-36.86 2.82 -36.88 2.84 -36.88 2.85 C-36.89 2.88 -36.91 2.9 -36.96 2.97 C-36.98 3 -36.99 3.03 -36.99 3.03 C-37 3.04 -37.01 3.06 -37.03 3.09 C-37.05 3.12 -37.1 3.19 -37.13 3.24 C-37.22 3.38 -37.22 3.38 -37.29 3.47 C-37.37 3.58 -37.38 3.61 -37.57 3.84 C-38.14 4.57 -38.91 5.25 -39.7 5.73 C-40.03 5.94 -40.09 5.97 -40.38 6.12 C-40.74 6.3 -40.92 6.38 -41.35 6.53 C-41.8 6.7 -42.2 6.81 -42.74 6.92 C-43.3 7.03 -43.77 7.07 -44.38 7.07 C-44.73 7.07 -44.77 7.06 -44.62 7 C-44.54 6.98 -44.4 6.9 -44.22 6.8 C-43.58 6.43 -42.9 5.75 -42.24 4.82 C-42.21 4.78 -42.13 4.67 -42.07 4.57 C-42 4.48 -41.89 4.33 -41.83 4.24 C-41.65 3.99 -41.45 3.73 -41.24 3.47 C-40.88 3.01 -40.26 2.38 -39.84 2.04 C-39.47 1.74 -39.44 1.72 -39.2 1.54 C-39.05 1.44 -38.83 1.29 -38.74 1.24 C-38.72 1.22 -38.68 1.2 -38.64 1.17 C-38.57 1.13 -38.43 1.05 -38.42 1.05 C-38.42 1.05 -38.38 1.02 -38.33 1 C-37.65 0.6 -36.84 0.4 -36.19 0.45 Z"/><path d="M-42.54 2.5 C-42.22 2.56 -42.09 2.85 -42.28 3.07 C-42.49 3.31 -42.94 3.27 -43.09 3 C-43.13 2.94 -43.13 2.79 -43.09 2.71 C-43 2.54 -42.77 2.45 -42.54 2.5 Z"/><path d="M-36.19 30.45 C-36.04 30.46 -36.01 30.46 -36.01 30.49 C-36 30.52 -36.04 30.71 -36.1 31 C-36.12 31.07 -36.16 31.22 -36.21 31.37 C-36.24 31.47 -36.27 31.56 -36.3 31.64 C-36.31 31.67 -36.33 31.7 -36.34 31.73 C-36.35 31.75 -36.37 31.81 -36.4 31.86 C-36.42 31.92 -36.44 31.97 -36.45 31.98 C-36.45 31.99 -36.46 32.02 -36.48 32.05 C-36.49 32.08 -36.51 32.12 -36.51 32.14 C-36.53 32.18 -36.78 32.67 -36.81 32.73 C-36.82 32.75 -36.84 32.78 -36.85 32.8 C-36.86 32.82 -36.88 32.84 -36.88 32.85 C-36.89 32.88 -36.91 32.91 -36.96 32.97 C-36.98 33 -36.99 33.03 -36.99 33.03 C-37 33.04 -37.01 33.06 -37.03 33.09 C-37.05 33.12 -37.1 33.19 -37.13 33.24 C-37.22 33.38 -37.22 33.38 -37.29 33.47 C-37.37 33.58 -37.38 33.61 -37.57 33.84 C-38.14 34.57 -38.91 35.25 -39.7 35.73 C-40.03 35.94 -40.09 35.97 -40.38 36.12 C-40.74 36.3 -40.92 36.38 -41.35 36.53 C-41.8 36.7 -42.2 36.81 -42.74 36.92 C-43.3 37.03 -43.77 37.07 -44.38 37.07 C-44.73 37.07 -44.77 37.06 -44.62 37 C-44.54 36.98 -44.4 36.9 -44.22 36.8 C-43.58 36.43 -42.9 35.75 -42.24 34.82 C-42.21 34.78 -42.13 34.67 -42.07 34.57 C-42 34.48 -41.89 34.33 -41.83 34.24 C-41.65 33.99 -41.45 33.73 -41.24 33.47 C-40.88 33.01 -40.26 32.38 -39.84 32.05 C-39.47 31.74 -39.44 31.72 -39.2 31.55 C-39.05 31.43 -38.83 31.29 -38.74 31.24 C-38.72 31.22 -38.68 31.2 -38.64 31.17 C-38.57 31.13 -38.43 31.05 -38.42 31.05 C-38.42 31.05 -38.38 31.02 -38.33 31 C-37.65 30.6 -36.84 30.4 -36.19 30.45 Z"/><path d="M-42.54 32.49 C-42.22 32.56 -42.09 32.85 -42.28 33.07 C-42.49 33.31 -42.94 33.27 -43.0Line truncated
|
||||
|
After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.58 3.44 C44.39 3.52 44.56 3.58 44.59 3.75 C44.61 3.93 44.37 5.01 43.96 6.63 C43.86 7.02 43.6 7.85 43.32 8.68 C43.13 9.21 42.94 9.71 42.75 10.16 C42.69 10.29 42.6 10.5 42.55 10.62 C42.49 10.74 42.34 11.07 42.2 11.37 C42.07 11.65 41.94 11.94 41.91 11.99 C41.89 12.05 41.81 12.22 41.73 12.38 C41.66 12.54 41.55 12.76 41.51 12.86 C41.4 13.09 39.95 15.76 39.77 16.05 C39.7 16.18 39.59 16.36 39.52 16.46 C39.45 16.57 39.38 16.7 39.34 16.75 C39.28 16.86 39.17 17.02 38.91 17.39 C38.81 17.54 38.72 17.68 38.7 17.7 C38.68 17.73 38.58 17.88 38.47 18.04 C38.36 18.19 38.1 18.56 37.9 18.85 C37.39 19.59 37.41 19.56 37 20.09 C36.55 20.67 36.45 20.8 35.39 22.04 C32.09 25.93 27.77 29.53 23.4 32.04 C21.55 33.12 21.25 33.28 19.62 34.05 C17.63 35 16.66 35.4 14.34 36.23 C11.87 37.13 9.69 37.72 6.71 38.3 C3.66 38.9 1.13 39.16 -2.23 39.21 C-4.14 39.23 -4.33 39.18 -3.5 38.86 C-3.11 38.71 -2.31 38.28 -1.33 37.69 C2.17 35.62 5.92 31.81 9.56 26.64 C9.73 26.41 10.16 25.8 10.54 25.28 C10.91 24.77 11.51 23.94 11.87 23.45 C12.89 22.05 13.99 20.63 15.16 19.2 C17.22 16.72 20.7 13.32 23.04 11.52 C25.17 9.89 25.32 9.79 26.69 8.88 C27.55 8.31 28.79 7.53 29.26 7.27 C29.38 7.2 29.64 7.05 29.84 6.95 C30.24 6.71 31.05 6.28 31.08 6.28 C31.09 6.28 31.32 6.17 31.58 6.18 C35.45 4.11 39.94 3.07 43.58 3.44 Z"/><path d="M8.31 14.48 C10.1 14.82 10.8 16.09 9.71 16.98 C8.53 17.94 6.06 17.77 5.22 16.65 C5.01 16.37 5.02 15.74 5.24 15.42 C5.74 14.66 7.03 14.26 8.31 14.48 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-10.29 -28.39 C-7.67 -28.48 -5.65 -28.17 -3.7 -27.4 C-3.35 -27.26 -2.36 -26.74 -2.25 -26.64 C-2.21 -26.61 -2.16 -26.57 -2.14 -26.57 C-1.97 -26.57 -1.15 -25.8 -0.5 -25.03 C0.4 -23.96 1.59 -22.9 2.18 -22.64 C2.24 -22.61 2.39 -22.52 2.51 -22.45 C2.78 -22.28 3.79 -21.77 4.21 -21.6 C4.72 -21.39 5.01 -21.3 6.05 -21 C8.62 -20.27 12.47 -20.6 15.02 -21.76 C15.43 -21.95 16.16 -22.31 16.34 -22.42 C16.45 -22.49 16.67 -22.62 16.84 -22.72 C18.18 -23.52 19.92 -25.32 20.76 -26.77 C21.27 -27.66 21.23 -28 20.6 -28 C20.47 -28 20.02 -28.07 19.12 -28.25 C18.12 -28.44 17.15 -28.69 15.68 -29.12 C15.2 -29.26 13.47 -29.86 12.83 -30.11 C12.53 -30.23 12.14 -30.38 11.77 -30.51 C11.64 -30.56 11.38 -30.66 11.18 -30.73 C11 -30.8 10.76 -30.88 10.67 -30.9 C10.57 -30.93 10.43 -30.99 10.34 -31.03 C10.26 -31.07 10.16 -31.1 10.14 -31.1 C10.11 -31.1 9.85 -31.17 9.55 -31.27 C8.04 -31.78 6.3 -32.06 4.8 -32.05 C3.99 -32.05 2.22 -31.88 1.9 -31.78 C1.82 -31.76 1.64 -31.73 1.5 -31.71 C1.37 -31.69 0.99 -31.63 0.67 -31.58 C-0.04 -31.46 -1.24 -31.47 -1.91 -31.6 C-2.14 -31.65 -2.46 -31.7 -2.62 -31.72 C-3.13 -31.79 -3.02 -31.92 -2.33 -32.06 C-1.34 -32.26 0.51 -32.93 1.32 -33.39 C3.54 -34.66 5.38 -36.41 6.48 -38.3 C7.07 -39.31 7 -39.58 6.07 -39.92 C3.55 -40.84 0.91 -41.47 -1.94 -41.8 C-3.65 -42.01 -6.32 -42.1 -7.86 -42 C-9.06 -37.46 -9.87 -32.93 -10.29 -28.39 Z"/><path d="M-7.86 -42 C-9.27 -41.91 -9.98 -41.85 -10.66 -41.76 C-11.52 -41.65 -12.67 -41.47 -12.76 -41.44 C-12.8 -41.43 -12.97 -41.4 -13.15 -41.37 C-13.79 -41.27 -16.51 -40.6 -17.26 -40.36 C-17.53 -40.27 -17.97 -40.13 -18.23 -40.05 C-19.23 -39.73 -20.62 -39.19 -22.08 -38.56 C-22.54 -38.37 -24.48 -37.42 -24.59 -37.34 C-24.61 -37.32 -24.86 -37.18 -25.13 -37.04 C-25.4 -36.9 -25.93 -36.6 -26.31 -36.36 C-26.69 -36.13 -27.13 -35.87 -27.29 -35.77 C-30.75 -33.73 -34.65 -30.25 -37.67 -26.52 C-37.97 -26.16 -38.6 -25.34 -38.7 -25.2 C-38.76 -25.11 -38.89 -24.92 -38.99 -24.78 C-39.14 -24.57 -39.38 -24.22 -40.16 -23.06 C-40.51 -22.55 -40.69 -22.24 -41.27 -21.22 C-41.6 -20.64 -41.9 -20.11 -41.94 -20.04 C-42.03 -19.87 -42.94 -18.03 -43.14 -17.61 C-43.41 -17.03 -43.43 -16.98 -43.63 -16.48 C-44.02 -15.53 -44.14 -15.22 -44.36 -14.56 C-44.44 -14.32 -44.56 -14 -44.62 -13.85 C-44.67 -13.7 -44.77 -13.44 -44.82 -13.27 C-44.87 -13.1 -44.96 -12.82 -45.01 -12.63 C-45.2 -12.05 -45.53 -10.82 -45.6 -10.47 C-45.62 -10.35 -45.68 -10.15 -45.71 -10.01 C-45.78 -9.76 -45.86 -9.37 -46.08 -8.29 C-46.32 -7.15 -46.54 -5.76 -46.63 -4.86 C-46.66 -4.57 -46.69 -4.3 -46.7 -4.27 C-46.76 -4.06 -46.88 -1.34 -46.88 -0.17 C-46.87 3.28 -46.38 7.39 -45.69 9.78 C-45.64 9.96 -45.6 10.13 -45.6 10.18 C-45.6 10.22 -45.55 10.42 -45.49 10.62 C-45.44 10.83 -45.35 11.14 -45.3 11.34 C-45.04 12.26 -44.63 13.49 -44.05 15.1 C-44.01 15.22 -43.96 15.36 -43.93 15.42 C-43.9 15.47 -43.81 15.68 -43.73 15.88 C-43.65 16.08 -43.55 16.31 -43.51 16.4 C-43.48 16.48 -43.34 16.79 -43.21 17.09 C-42.97 17.65 -41.94 19.74 -41.72 20.11 C-41.64 20.24 -41.1 21.16 -41.05 21.26 C-40.9 21.52 -40.64 21.94 -40.41 22.25 C-40.18 22.57 -40.08 22.71 -39.63 23.38 C-38.69 24.77 -37.3 26.44 -35.99 27.75 C-35.16 28.58 -35.05 28.68 -34.25 29.2 C-31.72 30.86 -29.76 31.74 -26.83 32.52 C-24.6 33.1 -20.8 33.19 -18.15 32.71 C-16.59 32.42 -14.57 31.8 -12.89 31.Line truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.41 2.47 C44.22 2.53 44.39 2.58 44.42 2.75 C44.44 2.91 44.23 3.97 43.86 5.56 C43.77 5.97 43.53 6.8 43.27 7.63 C43.1 8.17 42.93 8.67 42.75 9.13 C42.69 9.26 42.61 9.47 42.56 9.6 C42.51 9.72 42.37 10.06 42.24 10.36 C42.12 10.65 42 10.95 41.97 11 C41.95 11.05 41.87 11.23 41.8 11.39 C41.73 11.55 41.63 11.78 41.59 11.88 C41.48 12.11 40.12 14.85 39.95 15.15 C39.88 15.28 39.77 15.46 39.71 15.57 C39.65 15.68 39.57 15.81 39.54 15.87 C39.48 15.98 39.38 16.15 39.12 16.53 C39.03 16.68 38.94 16.82 38.92 16.85 C38.91 16.88 38.81 17.03 38.71 17.2 C38.6 17.36 38.35 17.73 38.16 18.03 C37.67 18.8 37.69 18.77 37.29 19.32 C36.85 19.92 36.76 20.05 35.73 21.34 C32.53 25.39 28.27 29.19 23.9 31.88 C22.04 33.03 21.74 33.2 20.1 34.02 C18.09 35.02 17.11 35.45 14.75 36.33 C12.24 37.26 10.02 37.87 6.98 38.45 C3.87 39.05 1.29 39.28 -2.13 39.28 C-4.07 39.27 -4.26 39.22 -3.42 38.91 C-3.02 38.77 -2.21 38.35 -1.22 37.78 C2.32 35.75 6.11 31.96 9.76 26.79 C9.92 26.56 10.36 25.94 10.73 25.42 C11.11 24.91 11.7 24.07 12.06 23.58 C13.07 22.17 14.15 20.74 15.31 19.29 C17.34 16.76 20.77 13.27 23.08 11.39 C25.17 9.68 25.32 9.57 26.66 8.61 C27.51 8 28.73 7.17 29.19 6.89 C29.32 6.82 29.57 6.66 29.76 6.54 C30.16 6.29 30.95 5.83 30.98 5.83 C30.99 5.83 31.22 5.71 31.48 5.53 C35.3 3.35 39.76 2.21 43.41 2.47 Z"/><path d="M8.18 13.96 C9.97 14.31 10.7 15.87 9.61 17.04 C8.44 18.29 5.95 18.1 5.1 16.68 C4.88 16.32 4.88 15.54 5.1 15.13 C5.6 14.2 6.89 13.7 8.18 13.96 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-10.29 -28.39 C-7.67 -28.48 -5.65 -28.17 -3.7 -27.4 C-3.35 -27.26 -2.36 -26.74 -2.25 -26.64 C-2.21 -26.61 -2.16 -26.57 -2.14 -26.57 C-1.97 -26.57 -1.15 -25.8 -0.5 -25.03 C0.4 -23.96 1.59 -22.9 2.18 -22.64 C2.24 -22.61 2.39 -22.52 2.51 -22.45 C2.78 -22.28 3.79 -21.77 4.21 -21.6 C4.72 -21.39 5.01 -21.3 6.05 -21 C8.62 -20.27 12.47 -20.6 15.02 -21.76 C15.43 -21.95 16.16 -22.31 16.34 -22.42 C16.45 -22.49 16.67 -22.62 16.84 -22.72 C18.18 -23.52 19.92 -25.32 20.76 -26.77 C21.27 -27.66 21.23 -28 20.6 -28 C20.47 -28 20.02 -28.07 19.12 -28.25 C18.12 -28.44 17.15 -28.69 15.68 -29.12 C15.2 -29.26 13.47 -29.86 12.83 -30.11 C12.53 -30.23 12.14 -30.38 11.77 -30.51 C11.64 -30.56 11.38 -30.66 11.18 -30.73 C11 -30.8 10.76 -30.88 10.67 -30.9 C10.57 -30.93 10.43 -30.99 10.34 -31.03 C10.26 -31.07 10.16 -31.1 10.14 -31.1 C10.11 -31.1 9.85 -31.17 9.55 -31.27 C8.04 -31.78 6.3 -32.06 4.8 -32.05 C3.99 -32.05 2.22 -31.88 1.9 -31.78 C1.82 -31.76 1.64 -31.73 1.5 -31.71 C1.37 -31.69 0.99 -31.63 0.67 -31.58 C-0.04 -31.46 -1.24 -31.47 -1.91 -31.6 C-2.14 -31.65 -2.46 -31.7 -2.62 -31.72 C-3.13 -31.79 -3.02 -31.92 -2.33 -32.06 C-1.34 -32.26 0.51 -32.93 1.32 -33.39 C3.54 -34.66 5.38 -36.41 6.48 -38.3 C7.07 -39.31 7 -39.58 6.07 -39.92 C3.55 -40.84 0.91 -41.47 -1.94 -41.8 C-3.65 -42.01 -6.32 -42.1 -7.86 -42 C-9.06 -37.46 -9.87 -32.93 -10.29 -28.39 Z"/><path d="M-7.86 -42 C-9.27 -41.91 -9.98 -41.85 -10.66 -41.76 C-11.52 -41.65 -12.67 -41.47 -12.76 -41.44 C-12.8 -41.43 -12.97 -41.4 -13.15 -41.37 C-13.79 -41.27 -16.51 -40.6 -17.26 -40.36 C-17.53 -40.27 -17.97 -40.13 -18.23 -40.05 C-19.23 -39.73 -20.62 -39.19 -22.08 -38.56 C-22.54 -38.37 -24.48 -37.42 -24.59 -37.34 C-24.61 -37.32 -24.86 -37.18 -25.13 -37.04 C-25.4 -36.9 -25.93 -36.6 -26.31 -36.36 C-26.69 -36.13 -27.13 -35.87 -27.29 -35.77 C-30.75 -33.73 -34.65 -30.25 -37.67 -26.52 C-37.97 -26.16 -38.6 -25.34 -38.7 -25.2 C-38.76 -25.11 -38.89 -24.92 -38.99 -24.78 C-39.14 -24.57 -39.38 -24.22 -40.16 -23.06 C-40.51 -22.55 -40.69 -22.24 -41.27 -21.22 C-41.6 -20.64 -41.9 -20.11 -41.94 -20.04 C-42.03 -19.87 -42.94 -18.03 -43.14 -17.61 C-43.41 -17.03 -43.43 -16.98 -43.63 -16.48 C-44.02 -15.53 -44.14 -15.22 -44.36 -14.56 C-44.44 -14.32 -44.56 -14 -44.62 -13.85 C-44.67 -13.7 -44.77 -13.44 -44.82 -13.27 C-44.87 -13.1 -44.96 -12.82 -45.01 -12.63 C-45.2 -12.05 -45.53 -10.82 -45.6 -10.47 C-45.62 -10.35 -45.68 -10.15 -45.71 -10.01 C-45.78 -9.76 -45.86 -9.37 -46.08 -8.29 C-46.32 -7.15 -46.54 -5.76 -46.63 -4.86 C-46.66 -4.57 -46.69 -4.3 -46.7 -4.27 C-46.76 -4.06 -46.88 -1.34 -46.88 -0.17 C-46.87 3.28 -46.38 7.39 -45.69 9.78 C-45.64 9.96 -45.6 10.13 -45.6 10.18 C-45.6 10.22 -45.55 10.42 -45.49 10.62 C-45.44 10.83 -45.35 11.14 -45.3 11.34 C-45.04 12.26 -44.63 13.49 -44.05 15.1 C-44.01 15.22 -43.96 15.36 -43.93 15.42 C-43.9 15.47 -43.81 15.68 -43.73 15.88 C-43.65 16.08 -43.55 16.31 -43.51 16.4 C-43.48 16.48 -43.34 16.79 -43.21 17.09 C-42.97 17.65 -41.94 19.74 -41.72 20.11 C-41.64 20.24 -41.1 21.16 -41.05 21.26 C-40.9 21.52 -40.64 21.94 -40.41 22.25 C-40.18 22.57 -40.08 22.71 -39.63 23.38 C-38.69 24.77 -37.3 26.44 -35.99 27.75 C-35.16 28.58 -35.05 28.68 -34.25 29.2 C-31.72 30.86 -29.76 31.74 -26.83 32.52 C-24.6 33.1 -20.8 33.19 -18.15 32.71 C-16.59 32.42 -14.57 31.8 -12.89 31.09 CLine truncated
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.12 1.12 C43.93 1.15 44.11 1.19 44.14 1.34 C44.17 1.5 43.98 2.52 43.67 4.07 C43.59 4.45 43.39 5.28 43.16 6.1 C43.02 6.64 42.86 7.15 42.7 7.61 C42.65 7.75 42.57 7.96 42.53 8.09 C42.49 8.21 42.36 8.56 42.24 8.86 C42.13 9.16 42.02 9.46 42 9.51 C41.98 9.56 41.91 9.74 41.84 9.91 C41.78 10.07 41.69 10.3 41.65 10.4 C41.56 10.64 40.31 13.42 40.15 13.74 C40.08 13.86 39.99 14.05 39.93 14.16 C39.87 14.28 39.8 14.41 39.77 14.47 C39.71 14.58 39.62 14.76 39.38 15.15 C39.29 15.3 39.21 15.45 39.2 15.48 C39.18 15.51 39.09 15.66 38.99 15.83 C38.89 16 38.66 16.38 38.48 16.69 C38.02 17.48 38.03 17.45 37.66 18.01 C37.25 18.63 37.16 18.77 36.18 20.11 C33.12 24.31 28.94 28.33 24.59 31.24 C22.72 32.47 22.42 32.65 20.77 33.54 C18.73 34.63 17.73 35.09 15.32 36.05 C12.76 37.06 10.48 37.73 7.36 38.38 C4.16 39.02 1.52 39.29 -1.99 39.3 C-3.97 39.3 -4.16 39.25 -3.31 38.94 C-2.9 38.79 -2.08 38.37 -1.07 37.8 C2.55 35.75 6.39 31.91 10.04 26.66 C10.2 26.43 10.63 25.8 11 25.27 C11.37 24.75 11.96 23.89 12.32 23.39 C13.31 21.95 14.38 20.49 15.53 19 C17.51 16.4 20.86 12.78 23.13 10.79 C25.16 8.99 25.31 8.87 26.62 7.84 C27.44 7.19 28.63 6.31 29.08 6.01 C29.2 5.93 29.45 5.76 29.63 5.63 C30.02 5.37 30.8 4.87 30.83 4.87 C30.84 4.87 31.06 4.74 31.32 4.63 C35.06 2.31 39.47 1.01 43.12 1.12 Z"/><path d="M8.04 13.66 C9.85 13.96 10.6 15.41 9.52 16.53 C8.36 17.73 5.85 17.58 4.97 16.26 C4.75 15.93 4.75 15.18 4.96 14.8 C5.45 13.92 6.75 13.44 8.04 13.66 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-9.96 -28.8 C-7.4 -28.82 -5.42 -28.48 -3.48 -27.77 C-3.13 -27.66 -2.11 -27.26 -1.99 -27.18 C-1.95 -27.15 -1.89 -27.13 -1.87 -27.13 C-1.69 -27.16 -0.8 -26.55 -0.04 -25.94 C1.03 -25.13 2.45 -24.49 3.13 -24.45 C3.2 -24.45 3.37 -24.42 3.52 -24.39 C3.83 -24.34 5 -24.26 5.47 -24.27 C6.05 -24.28 6.37 -24.31 7.5 -24.42 C10.18 -24.44 13.83 -25.7 16 -27.45 C16.36 -27.74 16.97 -28.27 17.12 -28.43 C17.21 -28.52 17.39 -28.7 17.53 -28.84 C18.63 -29.95 19.87 -32.13 20.32 -33.73 C20.6 -34.72 20.47 -35.04 19.86 -34.89 C19.73 -34.85 19.28 -34.81 18.37 -34.76 C17.34 -34.7 16.35 -34.7 14.82 -34.75 C14.31 -34.77 12.49 -34.92 11.81 -35 C11.48 -35.04 11.07 -35.09 10.69 -35.13 C10.54 -35.14 10.27 -35.17 10.06 -35.19 C9.87 -35.21 9.62 -35.23 9.52 -35.23 C9.42 -35.24 9.27 -35.25 9.17 -35.27 C9.08 -35.29 8.98 -35.29 8.96 -35.29 C8.93 -35.28 8.66 -35.29 8.35 -35.32 C6.76 -35.44 5.06 -35.24 3.73 -34.69 C3.03 -34.39 1.55 -33.57 1.29 -33.36 C1.23 -33.31 1.08 -33.21 0.97 -33.15 C0.85 -33.09 0.54 -32.91 0.26 -32.76 C-0.34 -32.44 -1.4 -32.17 -2.01 -32.18 C-2.22 -32.19 -2.51 -32.2 -2.66 -32.2 C-3.12 -32.2 -3.03 -32.35 -2.42 -32.57 C-1.56 -32.94 -0.05 -34.05 0.55 -34.75 C2.11 -36.77 3.22 -39.15 3.69 -41.35 C3.97 -42.5 3.84 -42.74 2.95 -42.79 C0.86 -42.83 -0.95 -42.62 -2.79 -42.35 C-3.97 -42.37 -6.12 -42.41 -7.58 -42.35 C-8.76 -37.84 -9.55 -33.32 -9.96 -28.8 Z"/><path d="M-7.58 -42.35 C-8.96 -42.29 -9.66 -42.25 -10.33 -42.17 C-11.18 -42.08 -12.3 -41.93 -12.39 -41.9 C-12.43 -41.89 -12.6 -41.86 -12.78 -41.84 C-13.4 -41.75 -16.08 -41.14 -16.82 -40.91 C-17.09 -40.82 -17.52 -40.69 -17.78 -40.61 C-18.77 -40.3 -20.15 -39.78 -21.61 -39.16 C-22.06 -38.97 -24 -38.02 -24.11 -37.94 C-24.14 -37.92 -24.38 -37.78 -24.65 -37.64 C-24.92 -37.5 -25.45 -37.2 -25.83 -36.96 C-26.21 -36.73 -26.65 -36.47 -26.81 -36.37 C-30.27 -34.33 -34.17 -30.85 -37.19 -27.12 C-37.49 -26.76 -38.12 -25.94 -38.22 -25.8 C-38.28 -25.7 -38.41 -25.52 -38.51 -25.38 C-38.66 -25.17 -38.9 -24.82 -39.68 -23.66 C-40.03 -23.14 -40.21 -22.84 -40.79 -21.82 C-41.12 -21.24 -41.42 -20.71 -41.46 -20.64 C-41.55 -20.47 -42.46 -18.63 -42.66 -18.21 C-42.93 -17.63 -42.95 -17.58 -43.15 -17.07 C-43.55 -16.12 -43.68 -15.8 -43.9 -15.13 C-43.99 -14.89 -44.11 -14.56 -44.17 -14.41 C-44.23 -14.26 -44.33 -13.99 -44.38 -13.82 C-44.43 -13.65 -44.53 -13.36 -44.58 -13.17 C-44.78 -12.58 -45.14 -11.31 -45.22 -10.95 C-45.24 -10.83 -45.3 -10.62 -45.34 -10.47 C-45.41 -10.22 -45.51 -9.81 -45.76 -8.71 C-46.02 -7.53 -46.28 -6.08 -46.4 -5.15 C-46.43 -4.85 -46.48 -4.58 -46.49 -4.54 C-46.55 -4.33 -46.74 -1.51 -46.77 -0.3 C-46.83 3.23 -46.38 7.39 -45.69 9.78 C-45.64 9.96 -45.6 10.13 -45.6 10.18 C-45.6 10.22 -45.55 10.42 -45.49 10.62 C-45.44 10.83 -45.35 11.14 -45.3 11.34 C-45.04 12.26 -44.63 13.49 -44.05 15.1 C-44.01 15.22 -43.96 15.36 -43.93 15.42 C-43.9 15.47 -43.81 15.68 -43.73 15.88 C-43.65 16.08 -43.55 16.31 -43.51 16.4 C-43.48 16.48 -43.34 16.79 -43.21 17.09 C-42.97 17.65 -41.94 19.74 -41.72 20.11 C-41.64 20.24 -41.1 21.16 -41.05 21.26 C-40.9 21.52 -40.64 21.94 -40.41 22.25 C-40.18 22.57 -40.08 22.71 -39.63 23.38 C-38.69 24.77 -37.3 26.44 -35.99 27.75 C-35.16 28.58 -35.05 28.68 -34.25 29.2 C-31.72 30.86 -29.76 31.74 -26.83 32.52 C-24.6 33.1 -20.8 33.19 -18.15 32.71 C-16.59 32.42 -14.57 31.8 -12.8Line truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.48 2.86 C44.29 2.93 44.46 2.98 44.49 3.15 C44.51 3.32 44.29 4.39 43.91 5.99 C43.81 6.38 43.56 7.21 43.29 8.04 C43.12 8.57 42.94 9.07 42.75 9.53 C42.7 9.66 42.61 9.87 42.56 9.99 C42.51 10.11 42.36 10.45 42.23 10.75 C42.1 11.04 41.98 11.33 41.95 11.38 C41.93 11.43 41.85 11.61 41.77 11.77 C41.7 11.93 41.6 12.15 41.56 12.25 C41.45 12.48 40.05 15.19 39.88 15.49 C39.81 15.61 39.7 15.8 39.64 15.9 C39.57 16.01 39.5 16.14 39.47 16.2 C39.4 16.31 39.3 16.47 39.04 16.85 C38.94 16.99 38.85 17.14 38.84 17.16 C38.82 17.19 38.72 17.34 38.62 17.5 C38.51 17.66 38.26 18.03 38.06 18.33 C37.56 19.08 37.58 19.05 37.18 19.59 C36.73 20.18 36.63 20.31 35.6 21.58 C32.36 25.55 28.07 29.27 23.7 31.88 C21.84 32.99 21.54 33.16 19.91 33.96 C17.9 34.95 16.93 35.36 14.58 36.22 C12.09 37.14 9.88 37.75 6.88 38.34 C3.78 38.95 1.23 39.21 -2.17 39.24 C-4.1 39.25 -4.29 39.2 -3.45 38.88 C-3.06 38.73 -2.25 38.31 -1.27 37.73 C2.26 35.67 6.04 31.86 9.68 26.67 C9.84 26.44 10.28 25.83 10.65 25.31 C11.03 24.79 11.63 23.96 11.98 23.46 C13 22.05 14.09 20.63 15.25 19.19 C17.29 16.67 20.74 13.22 23.07 11.37 C25.17 9.7 25.32 9.59 26.67 8.65 C27.53 8.06 28.76 7.26 29.22 6.98 C29.34 6.91 29.6 6.76 29.79 6.64 C30.2 6.4 30.99 5.95 31.02 5.95 C31.03 5.95 31.26 5.83 31.52 5.79 C35.37 3.66 39.84 2.56 43.48 2.86 Z"/><path d="M8.44 13.97 C10.22 14.33 10.94 15.89 9.85 17.05 C8.67 18.3 6.2 18.09 5.35 16.68 C5.13 16.32 5.14 15.53 5.36 15.13 C5.86 14.2 7.15 13.71 8.44 13.97 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-10.29 -28.39 C-7.67 -28.48 -5.65 -28.17 -3.7 -27.4 C-3.35 -27.26 -2.36 -26.74 -2.25 -26.64 C-2.21 -26.61 -2.16 -26.57 -2.14 -26.57 C-1.97 -26.57 -1.15 -25.8 -0.5 -25.03 C0.4 -23.96 1.59 -22.9 2.18 -22.64 C2.24 -22.61 2.39 -22.52 2.51 -22.45 C2.78 -22.28 3.79 -21.77 4.21 -21.6 C4.72 -21.39 5.01 -21.3 6.05 -21 C8.62 -20.27 12.47 -20.6 15.02 -21.76 C15.43 -21.95 16.16 -22.31 16.34 -22.42 C16.45 -22.49 16.67 -22.62 16.84 -22.72 C18.18 -23.52 19.92 -25.32 20.76 -26.77 C21.27 -27.66 21.23 -28 20.6 -28 C20.47 -28 20.02 -28.07 19.12 -28.25 C18.12 -28.44 17.15 -28.69 15.68 -29.12 C15.2 -29.26 13.47 -29.86 12.83 -30.11 C12.53 -30.23 12.14 -30.38 11.77 -30.51 C11.64 -30.56 11.38 -30.66 11.18 -30.73 C11 -30.8 10.76 -30.88 10.67 -30.9 C10.57 -30.93 10.43 -30.99 10.34 -31.03 C10.26 -31.07 10.16 -31.1 10.14 -31.1 C10.11 -31.1 9.85 -31.17 9.55 -31.27 C8.04 -31.78 6.3 -32.06 4.8 -32.05 C3.99 -32.05 2.22 -31.88 1.9 -31.78 C1.82 -31.76 1.64 -31.73 1.5 -31.71 C1.37 -31.69 0.99 -31.63 0.67 -31.58 C-0.04 -31.46 -1.24 -31.47 -1.91 -31.6 C-2.14 -31.65 -2.46 -31.7 -2.62 -31.72 C-3.13 -31.79 -3.02 -31.92 -2.33 -32.06 C-1.34 -32.26 0.51 -32.93 1.32 -33.39 C3.54 -34.66 5.38 -36.41 6.48 -38.3 C7.07 -39.31 7 -39.58 6.07 -39.92 C3.55 -40.84 0.91 -41.47 -1.94 -41.8 C-3.65 -42.01 -6.32 -42.1 -7.86 -42 C-9.06 -37.46 -9.87 -32.93 -10.29 -28.39 Z"/><path d="M-7.86 -42 C-9.27 -41.91 -9.98 -41.85 -10.66 -41.76 C-11.52 -41.65 -12.67 -41.47 -12.76 -41.44 C-12.8 -41.43 -12.97 -41.4 -13.15 -41.37 C-13.79 -41.27 -16.51 -40.6 -17.26 -40.36 C-17.53 -40.27 -17.97 -40.13 -18.23 -40.05 C-19.23 -39.73 -20.62 -39.19 -22.08 -38.56 C-22.54 -38.37 -24.48 -37.42 -24.59 -37.34 C-24.61 -37.32 -24.86 -37.18 -25.13 -37.04 C-25.4 -36.9 -25.93 -36.6 -26.31 -36.36 C-26.69 -36.13 -27.13 -35.87 -27.29 -35.77 C-30.75 -33.73 -34.65 -30.25 -37.67 -26.52 C-37.97 -26.16 -38.6 -25.34 -38.7 -25.2 C-38.76 -25.11 -38.89 -24.92 -38.99 -24.78 C-39.14 -24.57 -39.38 -24.22 -40.16 -23.06 C-40.51 -22.55 -40.69 -22.24 -41.27 -21.22 C-41.6 -20.64 -41.9 -20.11 -41.94 -20.04 C-42.03 -19.87 -42.94 -18.03 -43.14 -17.61 C-43.41 -17.03 -43.43 -16.98 -43.63 -16.48 C-44.02 -15.53 -44.14 -15.22 -44.36 -14.56 C-44.44 -14.32 -44.56 -14 -44.62 -13.85 C-44.67 -13.7 -44.77 -13.44 -44.82 -13.27 C-44.87 -13.1 -44.96 -12.82 -45.01 -12.63 C-45.2 -12.05 -45.53 -10.82 -45.6 -10.47 C-45.62 -10.35 -45.68 -10.15 -45.71 -10.01 C-45.78 -9.76 -45.86 -9.37 -46.08 -8.29 C-46.32 -7.15 -46.54 -5.76 -46.63 -4.86 C-46.66 -4.57 -46.69 -4.3 -46.7 -4.27 C-46.76 -4.06 -46.88 -1.34 -46.88 -0.17 C-46.87 3.28 -46.38 7.39 -45.69 9.78 C-45.64 9.96 -45.6 10.13 -45.6 10.18 C-45.6 10.22 -45.55 10.42 -45.49 10.62 C-45.44 10.83 -45.35 11.14 -45.3 11.34 C-45.04 12.26 -44.63 13.49 -44.05 15.1 C-44.01 15.22 -43.96 15.36 -43.93 15.42 C-43.9 15.47 -43.81 15.68 -43.73 15.88 C-43.65 16.08 -43.55 16.31 -43.51 16.4 C-43.48 16.48 -43.34 16.79 -43.21 17.09 C-42.97 17.65 -41.94 19.74 -41.72 20.11 C-41.64 20.24 -41.1 21.16 -41.05 21.26 C-40.9 21.52 -40.64 21.94 -40.41 22.25 C-40.18 22.57 -40.08 22.71 -39.63 23.38 C-38.69 24.77 -37.3 26.44 -35.99 27.75 C-35.16 28.58 -35.05 28.68 -34.25 29.2 C-31.72 30.86 -29.76 31.74 -26.83 32.52 C-24.6 33.1 -20.8 33.19 -18.15 32.71 C-16.59 32.42 -14.57 31.8 -12.89 31.09Line truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.67 4.03 C44.48 4.12 44.65 4.18 44.67 4.36 C44.69 4.54 44.44 5.64 44.01 7.28 C43.9 7.67 43.62 8.51 43.33 9.34 C43.13 9.87 42.93 10.36 42.73 10.81 C42.67 10.94 42.58 11.14 42.52 11.27 C42.47 11.38 42.31 11.72 42.16 12.01 C42.03 12.29 41.89 12.58 41.87 12.63 C41.84 12.68 41.76 12.86 41.67 13.01 C41.6 13.17 41.49 13.39 41.44 13.49 C41.33 13.72 39.84 16.36 39.65 16.66 C39.57 16.77 39.46 16.96 39.39 17.06 C39.32 17.16 39.24 17.29 39.21 17.34 C39.14 17.45 39.03 17.61 38.76 17.98 C38.66 18.12 38.57 18.26 38.55 18.29 C38.53 18.31 38.43 18.46 38.32 18.61 C38.2 18.77 37.94 19.13 37.73 19.41 C37.2 20.15 37.23 20.12 36.81 20.64 C36.35 21.21 36.24 21.33 35.17 22.55 C31.82 26.37 27.46 29.88 23.1 32.3 C21.25 33.35 20.95 33.5 19.33 34.24 C17.35 35.16 16.39 35.54 14.09 36.34 C11.64 37.2 9.49 37.77 6.55 38.32 C3.53 38.91 1.04 39.16 -2.29 39.2 C-4.18 39.22 -4.37 39.17 -3.55 38.84 C-3.16 38.7 -2.37 38.27 -1.4 37.69 C2.07 35.62 5.8 31.83 9.45 26.69 C9.61 26.46 10.05 25.86 10.42 25.34 C10.8 24.83 11.4 24.01 11.76 23.52 C12.78 22.14 13.89 20.74 15.07 19.32 C17.14 16.87 20.65 13.53 23.02 11.77 C25.16 10.19 25.32 10.09 26.69 9.21 C27.57 8.65 28.82 7.91 29.29 7.65 C29.42 7.59 29.68 7.44 29.87 7.34 C30.28 7.11 31.09 6.69 31.12 6.7 C31.13 6.7 31.36 6.58 31.63 6.58 C35.54 4.56 40.04 3.59 43.67 4.03 Z"/><path d="M8.02 15.79 C9.8 15.83 10.52 15.9 9.43 15.91 C8.27 15.93 5.81 15.88 4.96 15.83 C4.74 15.81 4.75 15.79 4.96 15.78 C5.46 15.75 6.74 15.76 8.02 15.79 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-9.82 -28.97 C-7.28 -28.97 -5.32 -28.59 -3.42 -27.74 C-3.09 -27.59 -2.12 -27.04 -2.02 -26.94 C-1.98 -26.9 -1.93 -26.86 -1.91 -26.86 C-1.74 -26.86 -0.95 -26.06 -0.31 -25.27 C0.57 -24.16 1.72 -23.07 2.3 -22.79 C2.36 -22.76 2.5 -22.67 2.62 -22.59 C2.88 -22.41 3.88 -21.88 4.28 -21.7 C4.78 -21.48 5.07 -21.38 6.09 -21.06 C8.63 -20.29 12.47 -20.6 15.02 -21.76 C15.43 -21.95 16.16 -22.31 16.34 -22.42 C16.45 -22.49 16.67 -22.62 16.84 -22.72 C18.18 -23.52 19.92 -25.32 20.76 -26.77 C21.27 -27.66 21.23 -28 20.6 -28 C20.47 -28 20.02 -28.07 19.12 -28.25 C18.12 -28.44 17.15 -28.69 15.68 -29.12 C15.2 -29.26 13.47 -29.86 12.83 -30.11 C12.53 -30.23 12.14 -30.38 11.77 -30.51 C11.64 -30.56 11.38 -30.66 11.18 -30.73 C11 -30.8 10.76 -30.88 10.67 -30.91 C10.58 -30.93 10.43 -30.99 10.34 -31.03 C10.26 -31.07 10.16 -31.1 10.14 -31.1 C10.11 -31.1 9.85 -31.18 9.56 -31.28 C8.05 -31.8 6.35 -32.11 4.86 -32.13 C4.08 -32.15 2.34 -32.03 2.02 -31.94 C1.95 -31.92 1.77 -31.89 1.64 -31.88 C1.51 -31.87 1.14 -31.82 0.82 -31.77 C0.14 -31.68 -1.03 -31.73 -1.69 -31.88 C-1.91 -31.94 -2.22 -32 -2.37 -32.03 C-2.87 -32.11 -2.76 -32.24 -2.09 -32.35 C-1.13 -32.52 0.67 -33.13 1.46 -33.57 C3.63 -34.77 5.44 -36.48 6.52 -38.35 C7.1 -39.35 7.03 -39.62 6.12 -39.98 C3.64 -40.95 1.06 -41.65 -1.71 -42.09 C-3.37 -42.35 -5.97 -42.54 -7.46 -42.5 C-8.63 -38 -9.41 -33.49 -9.82 -28.97 Z"/><path d="M-7.46 -42.5 C-8.83 -42.46 -9.53 -42.42 -10.18 -42.36 C-11.02 -42.27 -12.14 -42.13 -12.23 -42.11 C-12.26 -42.09 -12.44 -42.07 -12.61 -42.05 C-13.23 -41.97 -15.89 -41.38 -16.63 -41.15 C-16.9 -41.06 -17.33 -40.93 -17.58 -40.85 C-18.56 -40.55 -19.94 -40.03 -21.4 -39.42 C-21.85 -39.23 -23.79 -38.28 -23.9 -38.2 C-23.93 -38.18 -24.17 -38.05 -24.44 -37.91 C-24.71 -37.76 -25.24 -37.46 -25.62 -37.23 C-26 -36.99 -26.44 -36.73 -26.6 -36.64 C-30.06 -34.59 -33.96 -31.11 -36.98 -27.39 C-37.28 -27.02 -37.91 -26.2 -38.01 -26.06 C-38.07 -25.97 -38.2 -25.78 -38.3 -25.64 C-38.45 -25.43 -38.69 -25.08 -39.47 -23.93 C-39.82 -23.41 -40 -23.1 -40.58 -22.08 C-40.91 -21.5 -41.21 -20.97 -41.25 -20.9 C-41.34 -20.74 -42.25 -18.89 -42.45 -18.47 C-42.72 -17.89 -42.74 -17.84 -42.95 -17.33 C-43.34 -16.38 -43.47 -16.06 -43.7 -15.38 C-43.79 -15.14 -43.91 -14.81 -43.97 -14.65 C-44.03 -14.5 -44.13 -14.23 -44.18 -14.06 C-44.24 -13.89 -44.34 -13.59 -44.4 -13.41 C-44.59 -12.81 -44.97 -11.52 -45.05 -11.16 C-45.07 -11.04 -45.13 -10.82 -45.17 -10.68 C-45.25 -10.42 -45.35 -10.01 -45.61 -8.89 C-45.89 -7.69 -46.17 -6.23 -46.29 -5.28 C-46.33 -4.98 -46.38 -4.7 -46.39 -4.66 C-46.46 -4.44 -46.68 -1.59 -46.72 -0.36 C-46.82 3.21 -46.38 7.39 -45.69 9.78 C-45.64 9.96 -45.6 10.13 -45.6 10.18 C-45.6 10.22 -45.55 10.42 -45.49 10.62 C-45.44 10.83 -45.35 11.14 -45.3 11.34 C-45.04 12.26 -44.63 13.49 -44.05 15.1 C-44.01 15.22 -43.96 15.36 -43.93 15.42 C-43.9 15.47 -43.81 15.68 -43.73 15.88 C-43.65 16.08 -43.55 16.31 -43.51 16.4 C-43.48 16.48 -43.34 16.79 -43.21 17.09 C-42.97 17.65 -41.94 19.74 -41.72 20.11 C-41.64 20.24 -41.1 21.16 -41.05 21.26 C-40.9 21.52 -40.64 21.94 -40.41 22.25 C-40.18 22.57 -40.08 22.71 -39.63 23.38 C-38.69 24.77 -37.3 26.44 -35.99 27.75 C-35.16 28.58 -35.05 28.68 -34.25 29.2 C-31.72 30.86 -29.76 31.74 -26.83 32.52 C-24.6 33.1 -20.8 33.19 -18.15 32.71 C-16.59 3Line truncated
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.1 2.21 C43.91 2.25 44.09 2.3 44.11 2.46 C44.14 2.62 43.94 3.64 43.6 5.2 C43.51 5.59 43.29 6.41 43.05 7.23 C42.89 7.75 42.72 8.25 42.55 8.71 C42.5 8.84 42.42 9.05 42.37 9.17 C42.33 9.29 42.19 9.63 42.07 9.93 C41.95 10.22 41.84 10.51 41.81 10.56 C41.79 10.61 41.72 10.79 41.65 10.95 C41.58 11.11 41.49 11.33 41.45 11.43 C41.35 11.66 40.04 14.38 39.87 14.69 C39.8 14.81 39.7 14.99 39.64 15.1 C39.58 15.21 39.51 15.34 39.48 15.4 C39.42 15.51 39.32 15.68 39.08 16.06 C38.98 16.2 38.9 16.35 38.88 16.38 C38.87 16.41 38.77 16.56 38.67 16.72 C38.57 16.88 38.33 17.26 38.15 17.55 C37.66 18.32 37.68 18.29 37.3 18.84 C36.88 19.44 36.78 19.57 35.78 20.87 C32.66 24.93 28.45 28.77 24.11 31.52 C22.26 32.68 21.96 32.86 20.32 33.69 C18.31 34.71 17.32 35.14 14.96 36.03 C12.44 36.97 10.2 37.58 7.15 38.16 C4.02 38.74 1.43 38.96 -2.01 38.94 C-3.96 38.92 -4.15 38.86 -3.31 38.56 C-2.91 38.42 -2.09 38.02 -1.1 37.47 C2.45 35.5 6.24 31.79 9.87 26.71 C10.03 26.48 10.47 25.88 10.84 25.37 C11.21 24.86 11.8 24.04 12.15 23.55 C13.15 22.16 14.22 20.75 15.37 19.31 C17.37 16.8 20.75 13.33 23.03 11.43 C25.09 9.71 25.23 9.6 26.55 8.63 C27.39 8.01 28.59 7.17 29.04 6.88 C29.16 6.81 29.41 6.64 29.6 6.53 C30 6.27 30.77 5.8 30.8 5.8 C30.81 5.8 31.04 5.67 31.3 5.51 C35.06 3.21 39.48 2.02 43.1 2.21 Z"/><path d="M7.14 13.86 C8.87 14.29 9.49 15.86 8.37 17.02 C7.16 18.27 4.71 17.97 3.96 16.52 C3.77 16.16 3.81 15.37 4.04 14.96 C4.57 14.02 5.9 13.54 7.14 13.86 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-10.27 -28.38 C-7.66 -28.47 -5.64 -28.16 -3.69 -27.39 C-3.34 -27.25 -2.35 -26.73 -2.24 -26.64 C-2.2 -26.6 -2.15 -26.57 -2.13 -26.57 C-1.95 -26.57 -1.14 -25.79 -0.49 -25.02 C0.41 -23.95 1.6 -22.9 2.19 -22.63 C2.25 -22.61 2.4 -22.52 2.52 -22.44 C2.79 -22.27 3.8 -21.77 4.22 -21.6 C4.73 -21.39 5.02 -21.3 6.06 -21 C8.63 -20.28 12.48 -20.6 15.02 -21.77 C15.43 -21.95 16.16 -22.32 16.34 -22.43 C16.45 -22.49 16.67 -22.62 16.84 -22.72 C18.18 -23.53 19.92 -25.33 20.75 -26.77 C21.26 -27.66 21.22 -28 20.59 -28 C20.46 -28 20.01 -28.08 19.11 -28.25 C18.11 -28.45 17.15 -28.69 15.68 -29.12 C15.19 -29.27 13.46 -29.86 12.82 -30.11 C12.52 -30.23 12.13 -30.38 11.77 -30.51 C11.64 -30.56 11.37 -30.66 11.18 -30.73 C10.99 -30.8 10.76 -30.88 10.66 -30.9 C10.57 -30.93 10.42 -30.99 10.34 -31.03 C10.25 -31.07 10.16 -31.09 10.13 -31.09 C10.1 -31.09 9.84 -31.17 9.55 -31.27 C8.03 -31.78 6.3 -32.06 4.8 -32.05 C3.99 -32.04 2.22 -31.88 1.9 -31.78 C1.82 -31.76 1.64 -31.72 1.5 -31.71 C1.37 -31.69 0.99 -31.63 0.67 -31.58 C-0.04 -31.46 -1.24 -31.47 -1.91 -31.6 C-2.14 -31.65 -2.46 -31.7 -2.62 -31.72 C-3.13 -31.79 -3.01 -31.92 -2.33 -32.06 C-1.33 -32.26 0.51 -32.93 1.32 -33.39 C3.54 -34.66 5.38 -36.41 6.48 -38.3 C7.07 -39.31 7 -39.58 6.07 -39.92 C3.55 -40.84 0.91 -41.46 -1.93 -41.8 C-3.64 -42 -6.31 -42.09 -7.85 -41.99 C-9.04 -37.45 -9.85 -32.91 -10.27 -28.38 Z"/><path d="M-7.85 -41.99 C-9.26 -41.9 -9.97 -41.84 -10.64 -41.75 C-11.51 -41.63 -12.66 -41.46 -12.75 -41.43 C-12.78 -41.41 -12.96 -41.39 -13.14 -41.36 C-13.78 -41.26 -16.49 -40.6 -17.25 -40.35 C-17.52 -40.26 -17.95 -40.12 -18.21 -40.04 C-19.2 -39.72 -20.6 -39.18 -22.06 -38.56 C-22.52 -38.37 -24.46 -37.42 -24.57 -37.33 C-24.59 -37.31 -24.84 -37.18 -25.11 -37.04 C-25.38 -36.9 -25.91 -36.59 -26.29 -36.36 C-26.66 -36.13 -27.11 -35.87 -27.26 -35.77 C-30.72 -33.73 -34.63 -30.25 -37.64 -26.53 C-37.94 -26.17 -38.58 -25.35 -38.67 -25.21 C-38.73 -25.12 -38.86 -24.93 -38.96 -24.79 C-39.11 -24.58 -39.35 -24.23 -40.13 -23.07 C-40.48 -22.56 -40.66 -22.25 -41.23 -21.24 C-41.55 -20.66 -41.84 -20.13 -41.88 -20.07 C-41.98 -19.9 -42.84 -18.09 -43.02 -17.68 C-43.27 -17.11 -43.29 -17.06 -43.48 -16.57 C-43.83 -15.65 -43.94 -15.35 -44.13 -14.71 C-44.2 -14.48 -44.31 -14.17 -44.36 -14.03 C-44.41 -13.88 -44.49 -13.63 -44.53 -13.47 C-44.58 -13.31 -44.65 -13.04 -44.7 -12.86 C-44.86 -12.31 -45.13 -11.12 -45.19 -10.79 C-45.21 -10.68 -45.25 -10.49 -45.28 -10.35 C-45.34 -10.12 -45.41 -9.74 -45.58 -8.72 C-45.77 -7.64 -45.94 -6.32 -46 -5.46 C-46.02 -5.19 -46.05 -4.93 -46.06 -4.9 C-46.11 -4.7 -46.18 -2.09 -46.18 -0.95 C-46.2 2.42 -45.75 6.46 -45.11 8.81 C-45.06 8.98 -45.02 9.16 -45.02 9.2 C-45.02 9.24 -44.98 9.44 -44.92 9.64 C-44.87 9.84 -44.79 10.15 -44.74 10.34 C-44.5 11.25 -44.12 12.46 -43.57 14.04 C-43.53 14.16 -43.48 14.3 -43.45 14.36 C-43.42 14.41 -43.34 14.62 -43.26 14.82 C-43.18 15.02 -43.09 15.24 -43.05 15.32 C-43.02 15.4 -42.89 15.71 -42.77 16 C-42.54 16.56 -41.55 18.62 -41.34 18.98 C-41.27 19.11 -40.76 20.02 -40.7 20.12 C-40.56 20.38 -40.31 20.79 -40.09 21.09 C-39.87 21.42 -39.78 21.56 -39.34 22.22 C-38.44 23.59 -37.11 25.25 -35.84 26.55 C-35.05 27.38 -34.93 27.47 -34.17 28 C-31.86 29.67 -30.15 30.57 -27.61 31.39 C-25.69 32.01 -22.25 32.18 -19.66 31.76 C-18.07 31.51 -15.96 30.99 -Line truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M42.98 0.55 C43.79 0.56 43.97 0.59 44 0.75 C44.03 0.9 43.86 1.9 43.57 3.43 C43.5 3.79 43.3 4.61 43.09 5.43 C42.96 5.96 42.81 6.47 42.66 6.94 C42.61 7.08 42.54 7.29 42.5 7.41 C42.45 7.54 42.33 7.88 42.22 8.19 C42.12 8.49 42.01 8.78 41.99 8.84 C41.97 8.89 41.91 9.07 41.84 9.24 C41.78 9.4 41.7 9.63 41.66 9.73 C41.57 9.97 40.37 12.76 40.21 13.08 C40.15 13.21 40.06 13.4 40 13.51 C39.94 13.62 39.88 13.76 39.85 13.81 C39.79 13.93 39.7 14.1 39.47 14.5 C39.38 14.65 39.3 14.8 39.29 14.83 C39.28 14.86 39.19 15.02 39.09 15.18 C38.99 15.35 38.77 15.74 38.6 16.05 C38.14 16.84 38.16 16.81 37.8 17.38 C37.4 18 37.31 18.14 36.35 19.5 C33.35 23.75 29.22 27.84 24.87 30.82 C23 32.09 22.7 32.28 21.04 33.2 C18.99 34.32 17.99 34.8 15.56 35.81 C12.97 36.86 10.67 37.56 7.52 38.26 C4.29 38.94 1.61 39.23 -1.93 39.29 C-3.92 39.31 -4.12 39.26 -3.26 38.93 C-2.85 38.79 -2.02 38.36 -1 37.78 C2.64 35.68 6.5 31.79 10.15 26.5 C10.31 26.26 10.74 25.63 11.11 25.09 C11.48 24.56 12.07 23.7 12.42 23.19 C13.41 21.73 14.48 20.25 15.61 18.75 C17.58 16.11 20.89 12.43 23.13 10.4 C25.15 8.56 25.29 8.43 26.58 7.39 C27.4 6.72 28.57 5.82 29.02 5.51 C29.14 5.43 29.38 5.26 29.56 5.13 C29.95 4.86 30.71 4.36 30.74 4.36 C30.75 4.36 30.97 4.22 31.24 4.24 C34.94 1.87 39.34 0.5 42.98 0.55 Z"/><path d="M8.45 13.67 C10.26 13.96 11.03 15.4 9.95 16.53 C8.79 17.73 6.28 17.6 5.39 16.29 C5.16 15.96 5.16 15.21 5.37 14.84 C5.86 13.95 7.15 13.46 8.45 13.67 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-10.14 -28.58 C-7.54 -28.64 -5.54 -28.3 -3.62 -27.48 C-3.28 -27.32 -2.31 -26.76 -2.21 -26.66 C-2.17 -26.61 -2.12 -26.58 -2.1 -26.58 C-1.93 -26.57 -1.15 -25.74 -0.54 -24.92 C0.3 -23.77 1.39 -22.59 1.94 -22.27 C2 -22.23 2.13 -22.13 2.25 -22.04 C2.49 -21.84 3.43 -21.21 3.81 -20.99 C4.29 -20.72 4.57 -20.59 5.55 -20.19 C8.03 -19.25 11.89 -19.31 14.52 -20.3 C14.94 -20.46 15.69 -20.77 15.88 -20.87 C16 -20.93 16.23 -21.05 16.39 -21.13 C17.79 -21.85 19.65 -23.52 20.58 -24.91 C21.15 -25.76 21.13 -26.11 20.5 -26.15 C20.37 -26.16 19.93 -26.26 19.05 -26.5 C18.06 -26.76 17.11 -27.07 15.67 -27.6 C15.2 -27.77 13.51 -28.49 12.89 -28.78 C12.6 -28.92 12.22 -29.09 11.87 -29.25 C11.74 -29.3 11.48 -29.42 11.29 -29.5 C11.11 -29.58 10.88 -29.68 10.79 -29.71 C10.7 -29.75 10.55 -29.81 10.47 -29.86 C10.39 -29.9 10.3 -29.94 10.27 -29.94 C10.24 -29.94 9.99 -30.04 9.7 -30.16 C8.23 -30.77 6.52 -31.19 5.01 -31.35 C4.21 -31.44 2.4 -31.5 2.08 -31.43 C2 -31.42 1.82 -31.4 1.68 -31.4 C1.54 -31.4 1.16 -31.38 0.83 -31.36 C0.11 -31.31 -1.1 -31.41 -1.78 -31.59 C-2.01 -31.65 -2.33 -31.73 -2.49 -31.75 C-3 -31.85 -2.89 -31.97 -2.19 -32.08 C-1.18 -32.21 0.72 -32.72 1.56 -33.1 C3.89 -34.11 5.87 -35.64 7.11 -37.4 C7.77 -38.36 7.72 -38.63 6.8 -39.06 C4.24 -40.28 1.45 -41.21 -1.61 -41.8 C-3.43 -42.09 -6.2 -42.24 -7.73 -42.16 C-8.92 -37.64 -9.72 -33.11 -10.14 -28.58 Z"/><path d="M-7.73 -42.16 C-9.12 -42.09 -9.84 -42.03 -10.5 -41.95 C-11.36 -41.85 -12.5 -41.69 -12.59 -41.66 C-12.62 -41.64 -12.8 -41.62 -12.98 -41.59 C-13.61 -41.49 -16.31 -40.86 -17.06 -40.62 C-17.33 -40.53 -17.76 -40.39 -18.02 -40.31 C-19.01 -39.99 -20.4 -39.47 -21.86 -38.84 C-22.32 -38.65 -24.26 -37.7 -24.36 -37.62 C-24.39 -37.6 -24.63 -37.47 -24.91 -37.32 C-25.18 -37.18 -25.71 -36.88 -26.09 -36.65 C-26.46 -36.41 -26.91 -36.15 -27.06 -36.05 C-30.52 -34.01 -34.43 -30.53 -37.45 -26.81 C-37.75 -26.44 -38.38 -25.62 -38.47 -25.48 C-38.54 -25.39 -38.67 -25.2 -38.76 -25.06 C-38.92 -24.85 -39.16 -24.5 -39.93 -23.34 C-40.28 -22.83 -40.46 -22.52 -41.04 -21.5 C-41.37 -20.92 -41.67 -20.39 -41.71 -20.32 C-41.81 -20.15 -42.72 -18.31 -42.92 -17.89 C-43.18 -17.31 -43.2 -17.26 -43.41 -16.76 C-43.8 -15.81 -43.92 -15.49 -44.15 -14.83 C-44.23 -14.59 -44.35 -14.26 -44.41 -14.11 C-44.46 -13.96 -44.56 -13.7 -44.61 -13.53 C-44.66 -13.36 -44.76 -13.07 -44.81 -12.89 C-45 -12.3 -45.35 -11.05 -45.42 -10.7 C-45.44 -10.58 -45.5 -10.37 -45.54 -10.22 C-45.61 -9.97 -45.7 -9.57 -45.93 -8.49 C-46.18 -7.33 -46.42 -5.91 -46.52 -4.99 C-46.55 -4.7 -46.59 -4.43 -46.6 -4.39 C-46.66 -4.19 -46.81 -1.42 -46.83 -0.23 C-46.85 3.26 -46.38 7.39 -45.69 9.78 C-45.64 9.96 -45.6 10.13 -45.6 10.18 C-45.6 10.22 -45.55 10.42 -45.49 10.62 C-45.44 10.83 -45.35 11.14 -45.3 11.34 C-45.04 12.26 -44.63 13.49 -44.05 15.1 C-44.01 15.22 -43.96 15.36 -43.93 15.42 C-43.9 15.47 -43.81 15.68 -43.73 15.88 C-43.65 16.08 -43.55 16.31 -43.51 16.4 C-43.48 16.48 -43.34 16.79 -43.21 17.09 C-42.97 17.65 -41.94 19.74 -41.72 20.11 C-41.64 20.24 -41.1 21.16 -41.05 21.26 C-40.9 21.52 -40.64 21.94 -40.41 22.25 C-40.18 22.57 -40.08 22.71 -39.63 23.38 C-38.69 24.77 -37.3 26.44 -35.99 27.75 C-35.16 28.58 -35.05 28.68 -34.25 29.2 C-31.72 30.86 -29.76 31.74 -26.83 32.52 C-24.6 33.1 -20.8 33.19 -18.15 32.71 C-16.59 32.42 -14.57 31.8 -12.89 31.09 C-10.29 30.01 -7Line truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-62 -62 124 124"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/></linearGradient></defs><g fill="#F4F8FF"><path d="M43.7 4.22 C44.51 4.32 44.68 4.38 44.7 4.56 C44.72 4.74 44.46 5.85 44.02 7.5 C43.86 8.77 43.58 9.6 43.28 10.41 C43.09 10.93 42.88 11.42 42.68 11.87 C42.62 11.99 42.52 12.2 42.47 12.32 C42.41 12.44 42.25 12.77 42.1 13.05 C41.97 13.34 41.83 13.62 41.8 13.67 C41.77 13.72 41.69 13.89 41.61 14.05 C41.53 14.21 41.42 14.42 41.37 14.52 C41.26 14.74 39.75 17.35 39.56 17.64 C39.49 17.76 39.38 17.94 39.31 18.04 C39.24 18.14 39.16 18.27 39.12 18.32 C39.05 18.43 38.94 18.58 38.67 18.94 C38.57 19.08 38.47 19.21 38.46 19.24 C38.44 19.27 38.33 19.41 38.22 19.56 C38.11 19.71 37.84 20.07 37.64 20.34 C37.1 21.06 37.12 21.04 36.71 21.54 C36.24 22.09 36.14 22.22 35.06 23.4 C31.7 27.09 27.34 30.4 22.99 32.54 C21.14 33.47 20.85 33.61 19.24 34.27 C17.26 35.12 16.3 35.48 14.01 36.23 C11.57 37.08 9.43 37.64 6.5 38.2 C3.49 38.81 1.01 39.09 -2.31 39.16 C-4.2 39.2 -4.38 39.16 -3.56 38.82 C-3.18 38.67 -2.38 38.23 -1.42 37.64 C2.04 35.54 5.76 31.72 9.41 26.6 C9.57 26.37 10.01 25.77 10.39 25.26 C10.76 24.76 11.37 23.94 11.73 23.46 C12.75 22.1 13.85 20.72 15.04 19.34 C17.11 16.95 20.63 13.74 23 12.07 C25.15 10.58 25.3 10.48 26.68 9.67 C27.55 9.15 28.81 8.46 29.28 8.22 C29.41 8.16 29.67 8.03 29.86 7.94 C30.27 7.73 31.09 7.35 31.11 7.35 C31.12 7.35 31.36 7.25 31.65 6.71 C35.56 4.71 40.07 3.77 43.7 4.22 Z"/><path d="M8.34 14.86 C10.11 15.18 10.8 16.3 9.71 17.02 C8.53 17.83 6.08 17.66 5.25 16.7 C5.03 16.46 5.05 15.92 5.26 15.64 C5.77 15 7.06 14.65 8.34 14.86 Z"/></g><g fill="url(#g)" fill-rule="evenodd"><path d="M-10.29 -28.39 C-7.67 -28.48 -5.65 -28.17 -3.7 -27.4 C-3.35 -27.26 -2.36 -26.74 -2.25 -26.64 C-2.21 -26.61 -2.16 -26.57 -2.14 -26.57 C-1.97 -26.57 -1.15 -25.8 -0.5 -25.03 C0.4 -23.96 1.59 -22.9 2.18 -22.64 C2.24 -22.61 2.39 -22.52 2.51 -22.45 C2.78 -22.28 3.79 -21.77 4.21 -21.6 C4.72 -21.39 5.01 -21.3 6.05 -21 C8.62 -20.27 12.47 -20.6 15.02 -21.76 C15.43 -21.95 16.16 -22.31 16.34 -22.42 C16.45 -22.49 16.67 -22.62 16.84 -22.72 C18.18 -23.52 19.92 -25.32 20.76 -26.77 C21.27 -27.66 21.23 -28 20.6 -28 C20.47 -28 20.02 -28.07 19.12 -28.25 C18.12 -28.44 17.15 -28.69 15.68 -29.12 C15.2 -29.26 13.47 -29.86 12.83 -30.11 C12.53 -30.23 12.14 -30.38 11.77 -30.51 C11.64 -30.56 11.38 -30.66 11.18 -30.73 C11 -30.8 10.76 -30.88 10.67 -30.9 C10.57 -30.93 10.43 -30.99 10.34 -31.03 C10.26 -31.07 10.16 -31.1 10.14 -31.1 C10.11 -31.1 9.85 -31.17 9.55 -31.27 C8.04 -31.78 6.3 -32.06 4.8 -32.05 C3.99 -32.05 2.22 -31.88 1.9 -31.78 C1.82 -31.76 1.64 -31.73 1.5 -31.71 C1.37 -31.69 0.99 -31.63 0.67 -31.58 C-0.04 -31.46 -1.24 -31.47 -1.91 -31.6 C-2.14 -31.65 -2.46 -31.7 -2.62 -31.72 C-3.13 -31.79 -3.02 -31.92 -2.33 -32.06 C-1.34 -32.26 0.51 -32.93 1.32 -33.39 C3.54 -34.66 5.38 -36.41 6.48 -38.3 C7.07 -39.31 7 -39.58 6.07 -39.92 C3.55 -40.84 0.91 -41.47 -1.94 -41.8 C-3.65 -42.01 -6.32 -42.1 -7.86 -42 C-9.06 -37.46 -9.87 -32.93 -10.29 -28.39 Z"/><path d="M-7.86 -42 C-9.27 -41.91 -9.98 -41.85 -10.66 -41.76 C-11.52 -41.65 -12.67 -41.47 -12.76 -41.44 C-12.8 -41.43 -12.97 -41.4 -13.15 -41.37 C-13.79 -41.27 -16.51 -40.6 -17.26 -40.36 C-17.53 -40.27 -17.97 -40.13 -18.23 -40.05 C-19.23 -39.73 -20.62 -39.19 -22.08 -38.56 C-22.54 -38.37 -24.48 -37.42 -24.59 -37.34 C-24.61 -37.32 -24.86 -37.18 -25.13 -37.04 C-25.4 -36.9 -25.93 -36.6 -26.31 -36.36 C-26.69 -36.13 -27.13 -35.87 -27.29 -35.77 C-30.75 -33.73 -34.65 -30.25 -37.67 -26.52 C-37.97 -26.16 -38.6 -25.34 -38.7 -25.2 C-38.76 -25.11 -38.89 -24.92 -38.99 -24.78 C-39.14 -24.57 -39.38 -24.22 -40.16 -23.06 C-40.51 -22.55 -40.69 -22.24 -41.27 -21.22 C-41.6 -20.64 -41.9 -20.11 -41.94 -20.04 C-42.03 -19.87 -42.94 -18.03 -43.14 -17.61 C-43.41 -17.03 -43.43 -16.98 -43.63 -16.48 C-44.02 -15.53 -44.14 -15.22 -44.36 -14.56 C-44.44 -14.32 -44.56 -14 -44.62 -13.85 C-44.67 -13.7 -44.77 -13.44 -44.82 -13.27 C-44.87 -13.1 -44.96 -12.82 -45.01 -12.63 C-45.2 -12.05 -45.53 -10.82 -45.6 -10.47 C-45.62 -10.35 -45.68 -10.15 -45.71 -10.01 C-45.78 -9.76 -45.86 -9.37 -46.08 -8.29 C-46.32 -7.15 -46.54 -5.76 -46.63 -4.86 C-46.66 -4.57 -46.69 -4.3 -46.7 -4.27 C-46.76 -4.06 -46.88 -1.34 -46.88 -0.17 C-46.87 3.28 -46.38 7.39 -45.69 9.78 C-45.64 9.96 -45.6 10.13 -45.6 10.18 C-45.6 10.22 -45.55 10.42 -45.49 10.62 C-45.44 10.83 -45.35 11.14 -45.3 11.34 C-45.04 12.26 -44.63 13.49 -44.05 15.1 C-44.01 15.22 -43.96 15.36 -43.93 15.42 C-43.9 15.47 -43.81 15.68 -43.73 15.88 C-43.65 16.08 -43.55 16.31 -43.51 16.4 C-43.48 16.48 -43.34 16.79 -43.21 17.09 C-42.97 17.65 -41.94 19.74 -41.72 20.11 C-41.64 20.24 -41.1 21.16 -41.05 21.26 C-40.9 21.52 -40.64 21.94 -40.41 22.25 C-40.18 22.57 -40.08 22.71 -39.63 23.38 C-38.69 24.77 -37.3 26.44 -35.99 27.75 C-35.16 28.58 -35.05 28.68 -34.25 29.2 C-31.72 30.86 -29.76 31.74 -26.83 32.52 C-24.6 33.1 -20.8 33.19 -18.15 32.71 C-16.59 32.42 -14.57 31.8 -12.9Line truncated
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export the v2 whale character's still poses as web SVGs.
|
||||
|
||||
Source: `handoff/posters.json` from the whale-character-v2 kit (vendored in
|
||||
codewhale-app under vendor/whale-character-v2/). Each poster is the one-ink,
|
||||
reduced-motion pose of one of the 17 actions. This keeps only the visible
|
||||
paths, rounds coordinates to 0.01, and fills everything with the logo
|
||||
gradient (#1E8FD8 -> #0B48BB), so the files work as plain <img> sources in
|
||||
both appearances. Eye and throat apertures are holes in the body path; like
|
||||
the kit's colour version, a light underlay (the pouch and eye shapes, drawn
|
||||
first) shows through them, so the belly reads light on any ground.
|
||||
|
||||
Usage:
|
||||
scripts/export-whale-poses.py <posters.json> [out-dir] # default public/whale
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
GRADIENT = (
|
||||
'<defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" '
|
||||
'x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/>'
|
||||
"</linearGradient></defs>"
|
||||
)
|
||||
PATH = re.compile(r'<path id="([^"]+)" d="([^"]+)"([^>]*)/>')
|
||||
NUM = re.compile(r"-?\d+\.\d+")
|
||||
|
||||
|
||||
def visible(attrs: str) -> float:
|
||||
m = re.search(r'opacity="([\d.]+)"', attrs)
|
||||
return float(m.group(1)) if m else 1.0
|
||||
|
||||
|
||||
def compact(d: str) -> str:
|
||||
d = NUM.sub(lambda m: f"{float(m.group(0)):.2f}".rstrip("0").rstrip("."), d)
|
||||
return re.sub(r"\s+", " ", d).strip()
|
||||
|
||||
|
||||
LIGHT = "#F4F8FF"
|
||||
UNDERLAY = re.compile(r"(^|-)(pouch|eye)$")
|
||||
|
||||
|
||||
def export(scene: str) -> str:
|
||||
view = re.search(r'viewBox="([^"]+)"', scene).group(1)
|
||||
under, paths = [], []
|
||||
for pid, d, attrs in PATH.findall(scene):
|
||||
if UNDERLAY.search(pid):
|
||||
under.append(f'<path d="{compact(d)}"/>')
|
||||
opacity = visible(attrs)
|
||||
if opacity <= 0.001:
|
||||
continue
|
||||
op = "" if opacity >= 0.999 else f' opacity="{opacity:.2f}"'
|
||||
paths.append(f'<path d="{compact(d)}"{op}/>')
|
||||
return (
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{view}">{GRADIENT}'
|
||||
f'<g fill="{LIGHT}">{"".join(under)}</g>'
|
||||
f'<g fill="url(#g)" fill-rule="evenodd">{"".join(paths)}</g></svg>\n'
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
posters = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
out = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(__file__).resolve().parent.parent / "public/whale"
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
for poster in posters:
|
||||
(out / f"{poster['state']}.svg").write_text(export(poster["scene"]), encoding="utf-8")
|
||||
print(f"wrote {len(posters)} poses to {out}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -5,25 +5,46 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// The surface, ink, and accent tokens all resolve through CSS custom
|
||||
// properties (app/styles/tokens-roles.css) so the dark subtrees can
|
||||
// re-theme themselves; the values are the generated GPUI set_theme
|
||||
// tokens. Hover is the primary at 0.9 opacity, as in set_theme.
|
||||
paper: "rgb(var(--c-paper) / <alpha-value>)",
|
||||
"paper-deep": "rgb(var(--c-paper-deep) / <alpha-value>)",
|
||||
"paper-edge": "rgb(var(--c-paper-edge) / <alpha-value>)",
|
||||
"paper-card": "var(--paper-card)",
|
||||
"paper-line": "var(--paper-line)",
|
||||
"paper-line-soft": "var(--paper-line-soft)",
|
||||
ink: "rgb(var(--c-ink) / <alpha-value>)",
|
||||
"ink-soft": "rgb(var(--c-ink-soft) / <alpha-value>)",
|
||||
"ink-mute": "rgb(var(--c-ink-mute) / <alpha-value>)",
|
||||
indigo: "rgb(var(--c-indigo) / <alpha-value>)",
|
||||
"indigo-deep": "var(--indigo-deep)",
|
||||
"indigo-pale": "var(--indigo-pale)",
|
||||
ochre: "var(--ochre)",
|
||||
jade: "var(--jade)",
|
||||
cobalt: "var(--cobalt)",
|
||||
// Every colour resolves through the role tokens
|
||||
// (app/styles/tokens-roles.css), so the stage and both appearances
|
||||
// re-ink it. Alpha modifiers mix the role toward transparent.
|
||||
...Object.fromEntries(
|
||||
Object.entries({
|
||||
canvas: "--bg",
|
||||
surface: "--surface",
|
||||
panel: "--panel",
|
||||
fg: "--text",
|
||||
muted: "--muted",
|
||||
line: "--line",
|
||||
accent: "--accent",
|
||||
"on-accent": "--on-accent",
|
||||
hover: "--hover",
|
||||
selected: "--selected",
|
||||
attention: "--attention",
|
||||
live: "--live",
|
||||
success: "--success",
|
||||
danger: "--danger",
|
||||
// Legacy names still used by the docs pages (post-renovation sweep).
|
||||
paper: "--paper",
|
||||
"paper-deep": "--paper-deep",
|
||||
"paper-edge": "--paper-edge",
|
||||
"paper-card": "--paper-card",
|
||||
"paper-line": "--paper-line",
|
||||
"paper-line-soft": "--paper-line-soft",
|
||||
ink: "--ink",
|
||||
"ink-soft": "--ink-soft",
|
||||
"ink-mute": "--ink-mute",
|
||||
indigo: "--indigo",
|
||||
"indigo-deep": "--indigo-deep",
|
||||
"indigo-pale": "--indigo-pale",
|
||||
ochre: "--ochre",
|
||||
jade: "--jade",
|
||||
cobalt: "--cobalt",
|
||||
}).map(([name, role]) => [
|
||||
name,
|
||||
`color-mix(in srgb, var(${role}) calc(<alpha-value> * 100%), transparent)`,
|
||||
]),
|
||||
),
|
||||
},
|
||||
fontFamily: {
|
||||
// One face, as GPUI set_theme: every family resolves through the
|
||||
|
||||