No hand-written SVG: visx charts, lucide icons, Mermaid journey diagram

- desktop: Growth chart, Health and portfolio sparklines and the rollout bar drawn with visx (responsive width,
  crosshair tooltip with real values, arrow keys, theme tokens, no animation); icons from lucide-react behind the
  existing Icon names; empty states are lucide icons on token tiles; store marks are generic lucide icons; stars are
  lucide; the brand mark moves to ui/brand.tsx as the one allowed inline SVG
- fixtures: app icons and store art stand-ins are PNGs rendered by tools/fixture-art.ts (lucide on gradients)
- site: icons are lucide's own files in site/img/icons (tools/site-icons.ts, with the ISC licence) shown as CSS masks;
  the journey diagram is rendered from docs/diagrams/journey.mmd by tools/diagrams.ts (Mermaid CLI), with a tall
  variant for phones
- tests/no-handmade-svg.test.ts fails on new inline SVG drawing in apps/desktop/src and site/*.html
- desktop bundle: 598.7 -> 695.5 kB minified, 174.6 -> 206.0 kB gzip
This commit is contained in:
CRTkafa 2026-10-11 08:49:30 +08:00
parent 5d485e01b5
commit 035efa9fa1
62 changed files with 840 additions and 661 deletions

View file

@ -9,7 +9,8 @@ import { ago, date, share } from '../../lib/format';
import { type Lane, STAGES, type TestFlightStatus, trainLanes, versionHistory } from '../../lib/model';
import { useData, useRead, useWrite } from '../../store';
import { Icon, StoreGlyph } from '../../ui/icons';
import { Bar, Card, Chip, Empty, Notice, PhaseChip, storeName } from '../../ui/kit';
import { RolloutBar } from '../../ui/charts';
import { Card, Chip, Empty, Notice, PhaseChip, storeName } from '../../ui/kit';
import { ascUrl, PLAY_CONSOLE } from '../AppView';
const STAGE_KEY: Record<(typeof STAGES)[number], Key> = { build: 'stage.build', testing: 'stage.testing', review: 'stage.review', live: 'stage.live', rollout: 'stage.rollout' };
@ -40,7 +41,7 @@ function LaneRow({ lane, version }: { lane: Lane; version: string }) {
<div key={s} className={`tl-step ${cls}${i === 0 ? ' first' : ''}`} title={i === lane.at ? r?.state : undefined}>
<span className="tl-node">{i < lane.at && <Icon name="check" />}</span>
{last && i === lane.at && lane.share !== null ? (
<div className="tl-roll"><Bar value={lane.share} tone={lane.tone === 'bad' ? 'bad' : lane.share >= 1 ? 'ok' : 'accent'} label={t('tl.rolloutAria')} />
<div className="tl-roll"><RolloutBar value={lane.share} tone={lane.tone === 'bad' ? 'bad' : lane.share >= 1 ? 'ok' : 'accent'} label={t('tl.rolloutAria')} />
<span className="pct">{lane.share >= 1 ? t('tl.everyone') : `${share(lane.share)}${r?.phased?.day ? ` · ${t('tl.day', { day: r.phased.day })}` : ''}`}</span></div>
) : <span className="tl-cap">{stageCaption(lane, i)}</span>}
</div>