feat: binary glow effects, refined drawings and calls to action
Dark surfaces now carry light: frames glow along their edge and shed binary digits (BitAura), dark sections open with a lit seam (BitSeam), the career path is a line of light and "My work" became a dark stage. Buttons carry a travelling light and a spot that follows the pointer (BitGlow). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { BitAura } from "@/src/components/bits/BitAura";
|
||||
import * as React from "react";
|
||||
import { useNarrow } from "./useNarrow";
|
||||
import {
|
||||
@@ -242,193 +243,197 @@ export const AutomationFlow: React.FC = () => {
|
||||
{flow.summary}
|
||||
</p>
|
||||
|
||||
<div className="mt-8 overflow-hidden rounded-3xl bg-slate-900">
|
||||
<div className="flex items-center justify-between border-b border-slate-800 px-5 py-4 md:px-7">
|
||||
<span className={`${MONO} text-slate-400`}>{flow.name}</span>
|
||||
<span className={`${MONO} flex items-center gap-2 text-slate-500`}>
|
||||
<span
|
||||
className={`h-1.5 w-1.5 rounded-full ${finished ? "bg-emerald-400" : "bg-amber-300 animate-pulse"}`}
|
||||
/>
|
||||
{finished ? "run complete" : "running"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<svg
|
||||
viewBox={`0 0 ${canvas.w} ${canvas.h}`}
|
||||
role="img"
|
||||
aria-label={`${flow.name}: ${flow.summary}`}
|
||||
className="block h-auto w-full"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="flow-grid"
|
||||
width="20"
|
||||
height="20"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<circle cx="1" cy="1" r="1" fill="#1e293b" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width={canvas.w} height={canvas.h} fill="url(#flow-grid)" />
|
||||
|
||||
{flow.edges.map(([from, to]) => {
|
||||
const a = nodeOf(from);
|
||||
const b = nodeOf(to);
|
||||
if (!a || !b) return null;
|
||||
const done = still || travelled.has(`${from}>${to}`);
|
||||
return (
|
||||
<path
|
||||
key={`${from}>${to}`}
|
||||
d={pathOf(a, b, tall)}
|
||||
fill="none"
|
||||
stroke={done ? "#e2e8f0" : "#334155"}
|
||||
strokeWidth={done ? 2 : 1.5}
|
||||
style={{ transition: "stroke 300ms" }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{activeEdges.map(([from, to]) => {
|
||||
const a = nodeOf(from);
|
||||
const b = nodeOf(to);
|
||||
if (!a || !b || clock.t >= 1) return null;
|
||||
const eased = ease(clock.t);
|
||||
return (
|
||||
<g key={`packet-${from}>${to}`}>
|
||||
{[0.16, 0.1, 0.05].map((lag, i) => {
|
||||
const p = pointOn(a, b, Math.max(0, eased - lag), tall);
|
||||
return (
|
||||
<circle
|
||||
key={lag}
|
||||
cx={p.x}
|
||||
cy={p.y}
|
||||
r={5 - i}
|
||||
fill={MARK}
|
||||
opacity={0.12 + i * 0.12}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<circle
|
||||
cx={pointOn(a, b, eased, tall).x}
|
||||
cy={pointOn(a, b, eased, tall).y}
|
||||
r={7}
|
||||
fill={MARK}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{placed.map((node) => {
|
||||
const style = KIND_STYLE[node.kind];
|
||||
const on = reached.has(node.id);
|
||||
return (
|
||||
<g key={node.id}>
|
||||
<rect
|
||||
x={node.cx - NODE_W / 2}
|
||||
y={node.cy - NODE_H / 2}
|
||||
width={NODE_W}
|
||||
height={NODE_H}
|
||||
rx={14}
|
||||
fill={on ? "#1e293b" : "#0f172a"}
|
||||
stroke={on ? style.stroke : "#334155"}
|
||||
strokeWidth={on ? 1.75 : 1.25}
|
||||
strokeDasharray={style.dash}
|
||||
style={{ transition: "fill 300ms, stroke 300ms" }}
|
||||
/>
|
||||
<text
|
||||
x={node.cx - NODE_W / 2 + 16}
|
||||
y={node.cy - 8}
|
||||
fill={on ? style.tag : "#64748b"}
|
||||
style={{
|
||||
fontFamily:
|
||||
"var(--font-jetbrains-mono), ui-monospace, monospace",
|
||||
fontSize: 10,
|
||||
letterSpacing: "0.14em",
|
||||
textTransform: "uppercase",
|
||||
transition: "fill 300ms",
|
||||
}}
|
||||
>
|
||||
{node.system}
|
||||
</text>
|
||||
<text
|
||||
x={node.cx - NODE_W / 2 + 16}
|
||||
y={node.cy + 14}
|
||||
fill={on ? "#f8fafc" : "#94a3b8"}
|
||||
style={{
|
||||
fontFamily:
|
||||
"var(--font-inter), Inter, system-ui, sans-serif",
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
transition: "fill 300ms",
|
||||
}}
|
||||
>
|
||||
{node.label}
|
||||
</text>
|
||||
{on && (
|
||||
<circle
|
||||
cx={node.cx + NODE_W / 2 - 16}
|
||||
cy={node.cy - NODE_H / 2 + 16}
|
||||
r={4}
|
||||
fill={node.kind === "human" ? "#f8fafc" : "#34d399"}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 border-t border-slate-800 md:grid-cols-12">
|
||||
<div className="border-b border-slate-800 px-5 py-5 md:col-span-7 md:border-b-0 md:border-r md:px-7">
|
||||
<p className={`${MONO} m-0 text-slate-500`}>Log</p>
|
||||
<ol
|
||||
className="m-0 mt-3 min-h-[7.5rem] list-none p-0 font-mono text-[12px] leading-relaxed text-slate-300 md:text-[13px]"
|
||||
aria-live="off"
|
||||
>
|
||||
{logLines.map((line, index) => (
|
||||
<li key={line} className="flex gap-3">
|
||||
<span className="text-slate-600">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
line.startsWith("unclear") ? "text-amber-200" : undefined
|
||||
}
|
||||
>
|
||||
{line}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<BitAura source="around" tone="light" motion="pulse" className="mt-8">
|
||||
<div className="overflow-hidden rounded-3xl bg-slate-900">
|
||||
<div className="flex items-center justify-between border-b border-slate-800 px-5 py-4 md:px-7">
|
||||
<span className={`${MONO} text-slate-400`}>{flow.name}</span>
|
||||
<span className={`${MONO} flex items-center gap-2 text-slate-500`}>
|
||||
<span
|
||||
className={`h-1.5 w-1.5 rounded-full ${finished ? "bg-emerald-400" : "bg-amber-300 animate-pulse"}`}
|
||||
/>
|
||||
{finished ? "run complete" : "running"}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="m-0 flex list-none flex-col justify-center gap-2 px-5 py-5 md:col-span-5 md:px-7">
|
||||
{(Object.keys(KIND_LABEL) as NodeKind[])
|
||||
.filter((kind) => flow.nodes.some((node) => node.kind === kind))
|
||||
.map((kind) => (
|
||||
<li
|
||||
key={kind}
|
||||
className="flex items-center gap-3 font-mono text-[11px] text-slate-400"
|
||||
|
||||
<div>
|
||||
<svg
|
||||
viewBox={`0 0 ${canvas.w} ${canvas.h}`}
|
||||
role="img"
|
||||
aria-label={`${flow.name}: ${flow.summary}`}
|
||||
className="block h-auto w-full"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="flow-grid"
|
||||
width="20"
|
||||
height="20"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<svg width="22" height="12" aria-hidden="true">
|
||||
<rect
|
||||
x="1"
|
||||
y="1"
|
||||
width="20"
|
||||
height="10"
|
||||
rx="3"
|
||||
fill="none"
|
||||
stroke={KIND_STYLE[kind].stroke}
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray={KIND_STYLE[kind].dash}
|
||||
<circle cx="1" cy="1" r="1" fill="#1e293b" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width={canvas.w} height={canvas.h} fill="url(#flow-grid)" />
|
||||
|
||||
{flow.edges.map(([from, to]) => {
|
||||
const a = nodeOf(from);
|
||||
const b = nodeOf(to);
|
||||
if (!a || !b) return null;
|
||||
const done = still || travelled.has(`${from}>${to}`);
|
||||
return (
|
||||
<path
|
||||
key={`${from}>${to}`}
|
||||
d={pathOf(a, b, tall)}
|
||||
fill="none"
|
||||
stroke={done ? "#e2e8f0" : "#334155"}
|
||||
strokeWidth={done ? 2 : 1.5}
|
||||
style={{ transition: "stroke 300ms" }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{activeEdges.map(([from, to]) => {
|
||||
const a = nodeOf(from);
|
||||
const b = nodeOf(to);
|
||||
if (!a || !b || clock.t >= 1) return null;
|
||||
const eased = ease(clock.t);
|
||||
return (
|
||||
<g key={`packet-${from}>${to}`}>
|
||||
{[0.16, 0.1, 0.05].map((lag, i) => {
|
||||
const p = pointOn(a, b, Math.max(0, eased - lag), tall);
|
||||
return (
|
||||
<circle
|
||||
key={lag}
|
||||
cx={p.x}
|
||||
cy={p.y}
|
||||
r={5 - i}
|
||||
fill={MARK}
|
||||
opacity={0.12 + i * 0.12}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<circle
|
||||
cx={pointOn(a, b, eased, tall).x}
|
||||
cy={pointOn(a, b, eased, tall).y}
|
||||
r={7}
|
||||
fill={MARK}
|
||||
/>
|
||||
</svg>
|
||||
{KIND_LABEL[kind]}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{placed.map((node) => {
|
||||
const style = KIND_STYLE[node.kind];
|
||||
const on = reached.has(node.id);
|
||||
return (
|
||||
<g key={node.id}>
|
||||
<rect
|
||||
x={node.cx - NODE_W / 2}
|
||||
y={node.cy - NODE_H / 2}
|
||||
width={NODE_W}
|
||||
height={NODE_H}
|
||||
rx={14}
|
||||
fill={on ? "#1e293b" : "#0f172a"}
|
||||
stroke={on ? style.stroke : "#334155"}
|
||||
strokeWidth={on ? 1.75 : 1.25}
|
||||
strokeDasharray={style.dash}
|
||||
style={{ transition: "fill 300ms, stroke 300ms" }}
|
||||
/>
|
||||
<text
|
||||
x={node.cx - NODE_W / 2 + 16}
|
||||
y={node.cy - 8}
|
||||
fill={on ? style.tag : "#64748b"}
|
||||
style={{
|
||||
fontFamily:
|
||||
"var(--font-jetbrains-mono), ui-monospace, monospace",
|
||||
fontSize: 10,
|
||||
letterSpacing: "0.14em",
|
||||
textTransform: "uppercase",
|
||||
transition: "fill 300ms",
|
||||
}}
|
||||
>
|
||||
{node.system}
|
||||
</text>
|
||||
<text
|
||||
x={node.cx - NODE_W / 2 + 16}
|
||||
y={node.cy + 14}
|
||||
fill={on ? "#f8fafc" : "#94a3b8"}
|
||||
style={{
|
||||
fontFamily:
|
||||
"var(--font-inter), Inter, system-ui, sans-serif",
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
transition: "fill 300ms",
|
||||
}}
|
||||
>
|
||||
{node.label}
|
||||
</text>
|
||||
{on && (
|
||||
<circle
|
||||
cx={node.cx + NODE_W / 2 - 16}
|
||||
cy={node.cy - NODE_H / 2 + 16}
|
||||
r={4}
|
||||
fill={node.kind === "human" ? "#f8fafc" : "#34d399"}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 border-t border-slate-800 md:grid-cols-12">
|
||||
<div className="border-b border-slate-800 px-5 py-5 md:col-span-7 md:border-b-0 md:border-r md:px-7">
|
||||
<p className={`${MONO} m-0 text-slate-500`}>Log</p>
|
||||
<ol
|
||||
className="m-0 mt-3 min-h-[7.5rem] list-none p-0 font-mono text-[12px] leading-relaxed text-slate-300 md:text-[13px]"
|
||||
aria-live="off"
|
||||
>
|
||||
{logLines.map((line, index) => (
|
||||
<li key={line} className="flex gap-3">
|
||||
<span className="text-slate-600">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
line.startsWith("unclear")
|
||||
? "text-amber-200"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{line}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
<ul className="m-0 flex list-none flex-col justify-center gap-2 px-5 py-5 md:col-span-5 md:px-7">
|
||||
{(Object.keys(KIND_LABEL) as NodeKind[])
|
||||
.filter((kind) => flow.nodes.some((node) => node.kind === kind))
|
||||
.map((kind) => (
|
||||
<li
|
||||
key={kind}
|
||||
className="flex items-center gap-3 font-mono text-[11px] text-slate-400"
|
||||
>
|
||||
<svg width="22" height="12" aria-hidden="true">
|
||||
<rect
|
||||
x="1"
|
||||
y="1"
|
||||
width="20"
|
||||
height="10"
|
||||
rx="3"
|
||||
fill="none"
|
||||
stroke={KIND_STYLE[kind].stroke}
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray={KIND_STYLE[kind].dash}
|
||||
/>
|
||||
</svg>
|
||||
{KIND_LABEL[kind]}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BitAura>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="rounded-3xl border border-slate-200 p-6 md:p-8">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { BitAura } from "@/src/components/bits/BitAura";
|
||||
|
||||
export type CareerStation = {
|
||||
readonly years: string;
|
||||
@@ -10,21 +11,29 @@ export type CareerStation = {
|
||||
};
|
||||
|
||||
/**
|
||||
* CareerLine: the path so far as a line that draws itself, one station after another.
|
||||
* CareerLine: the path so far as a line of light that draws itself, one station
|
||||
* after another. Made for a dark surface.
|
||||
*/
|
||||
export const CareerLine: React.FC<{ stations: readonly CareerStation[] }> = ({
|
||||
stations,
|
||||
}) => (
|
||||
<ol className="relative m-0 list-none p-0">
|
||||
<motion.span
|
||||
className="absolute left-[5px] top-2 w-px origin-top bg-slate-900 md:left-[7px]"
|
||||
style={{ bottom: 8 }}
|
||||
initial={{ scaleY: 0 }}
|
||||
whileInView={{ scaleY: 1 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 1.4, ease: "easeOut" }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<BitAura
|
||||
source="left"
|
||||
tone="dark"
|
||||
radius={0}
|
||||
density={0.5}
|
||||
className="absolute bottom-2 left-[5px] top-2 w-px md:left-[7px]"
|
||||
>
|
||||
<motion.span
|
||||
className="block h-full w-px origin-top bg-indigo-100"
|
||||
initial={{ scaleY: 0 }}
|
||||
whileInView={{ scaleY: 1 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 1.4, ease: "easeOut" }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BitAura>
|
||||
{stations.map((station, index) => (
|
||||
<motion.li
|
||||
key={station.years}
|
||||
@@ -35,19 +44,21 @@ export const CareerLine: React.FC<{ stations: readonly CareerStation[] }> = ({
|
||||
transition={{ duration: 0.4, delay: 0.15 + index * 0.18 }}
|
||||
>
|
||||
<span
|
||||
className={`absolute left-0 top-1.5 h-[11px] w-[11px] rounded-full border-2 border-slate-900 md:h-[15px] md:w-[15px] ${
|
||||
index === stations.length - 1 ? "bg-slate-900" : "bg-white"
|
||||
className={`absolute left-0 top-1.5 h-[11px] w-[11px] rounded-full border-2 border-indigo-100 md:h-[15px] md:w-[15px] ${
|
||||
index === stations.length - 1
|
||||
? "bit-node-live bg-white"
|
||||
: "bg-slate-900"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="font-mono text-[11px] uppercase tracking-[0.2em] text-slate-500 md:col-span-3 md:pt-1.5">
|
||||
<span className="font-mono text-[11px] uppercase tracking-[0.2em] text-slate-400 md:col-span-3 md:pt-1.5">
|
||||
{station.years}
|
||||
</span>
|
||||
<span className="md:col-span-9">
|
||||
<span className="block font-sans text-xl font-bold tracking-tight text-slate-900 md:text-3xl">
|
||||
<span className="block font-sans text-xl font-bold tracking-tight text-white md:text-3xl">
|
||||
{station.role}
|
||||
</span>
|
||||
<span className="mt-1 block font-sans text-base text-slate-500 md:text-lg">
|
||||
<span className="mt-1 block font-sans text-base text-slate-400 md:text-lg">
|
||||
{station.place}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -15,6 +15,10 @@ const LINE = "#475569";
|
||||
const SOFT = "#1e293b";
|
||||
const MARK = "#fde68a";
|
||||
const LIVE = "#10b981";
|
||||
/** A lit surface: a light tint instead of a solid white block. */
|
||||
const TINT = "rgba(248, 250, 252, 0.14)";
|
||||
const RIM = "#64748b";
|
||||
const FINE = 1.1;
|
||||
const MONO = {
|
||||
fontFamily: "var(--font-jetbrains-mono), ui-monospace, monospace",
|
||||
// Set on the svg: larger on phones, where the drawing is scaled down.
|
||||
@@ -70,8 +74,11 @@ const Packet: React.FC<{
|
||||
pause?: number;
|
||||
}> = ({ xs, ys, delay = 0, duration = 1.2, pause = 1.4 }) => (
|
||||
<motion.circle
|
||||
r={5}
|
||||
r={3.5}
|
||||
fill={MARK}
|
||||
stroke={MARK}
|
||||
strokeOpacity={0.28}
|
||||
strokeWidth={6}
|
||||
initial={{ cx: xs[0], cy: ys[0], opacity: 0 }}
|
||||
animate={{
|
||||
cx: [...xs],
|
||||
@@ -96,18 +103,29 @@ const Person: React.FC<{ x: number; label: string; filled?: boolean }> = ({
|
||||
<g>
|
||||
<circle
|
||||
cx={x}
|
||||
cy={128}
|
||||
r={20}
|
||||
fill={filled ? INK : PAPER}
|
||||
cy={130}
|
||||
r={15}
|
||||
fill={filled ? TINT : "none"}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
<path
|
||||
d={`M ${x - 34} 196 Q ${x - 34} 158 ${x} 158 Q ${x + 34} 158 ${x + 34} 196`}
|
||||
fill={filled ? INK : PAPER}
|
||||
d={`M ${x - 28} 194 C ${x - 28} 172 ${x - 16} 160 ${x} 160 C ${x + 16} 160 ${x + 28} 172 ${x + 28} 194`}
|
||||
fill={filled ? TINT : "none"}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<line
|
||||
x1={x - 36}
|
||||
x2={x + 36}
|
||||
y1={194}
|
||||
y2={194}
|
||||
stroke={LINE}
|
||||
strokeWidth={1}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{filled && <circle cx={x + 12} cy={119} r={3.5} fill={MARK} />}
|
||||
<text x={x} y={222} textAnchor="middle" style={MONO}>
|
||||
{label}
|
||||
</text>
|
||||
@@ -129,31 +147,41 @@ const Talk: React.FC = () => (
|
||||
x={b.x}
|
||||
y={b.y}
|
||||
width={b.w}
|
||||
height={26}
|
||||
rx={13}
|
||||
fill={b.side === "left" ? SOFT : INK}
|
||||
height={24}
|
||||
rx={12}
|
||||
fill={b.side === "left" ? SOFT : TINT}
|
||||
stroke={b.side === "left" ? LINE : INK}
|
||||
strokeWidth={b.side === "left" ? 1 : FINE}
|
||||
/>
|
||||
<line
|
||||
x1={b.x + 14}
|
||||
x2={b.x + b.w - 14}
|
||||
y1={b.y + 13}
|
||||
y2={b.y + 13}
|
||||
stroke={b.side === "left" ? "#94a3b8" : PAPER}
|
||||
strokeWidth={2}
|
||||
y1={b.y + 12}
|
||||
y2={b.y + 12}
|
||||
stroke={b.side === "left" ? "#94a3b8" : INK}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</motion.g>
|
||||
))}
|
||||
<motion.g {...appear(1.7)}>
|
||||
<rect x={258} y={200} width={72} height={26} rx={13} fill={INK} />
|
||||
<rect
|
||||
x={270}
|
||||
y={200}
|
||||
width={60}
|
||||
height={24}
|
||||
rx={12}
|
||||
fill={TINT}
|
||||
stroke={INK}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
{[0, 1, 2].map((dot) => (
|
||||
<motion.circle
|
||||
key={dot}
|
||||
cx={280 + dot * 14}
|
||||
cy={213}
|
||||
r={3}
|
||||
fill={PAPER}
|
||||
cx={288 + dot * 12}
|
||||
cy={212}
|
||||
r={2.25}
|
||||
fill={INK}
|
||||
animate={{ opacity: [0.2, 1, 0.2] }}
|
||||
transition={{ duration: 1.1, delay: dot * 0.18, repeat: Infinity }}
|
||||
/>
|
||||
@@ -186,15 +214,16 @@ const WeeklyHours: React.FC = () => (
|
||||
return (
|
||||
<motion.rect
|
||||
key={h}
|
||||
x={52 + d * 72}
|
||||
y={64 + h * 22}
|
||||
width={52}
|
||||
height={16}
|
||||
rx={4}
|
||||
x={56 + d * 72}
|
||||
y={66 + h * 21}
|
||||
width={44}
|
||||
height={10}
|
||||
rx={5}
|
||||
strokeWidth={1}
|
||||
initial={{ fill: PAPER, stroke: LINE }}
|
||||
animate={{
|
||||
fill: reserved ? INK : PAPER,
|
||||
stroke: reserved ? INK : LINE,
|
||||
fill: reserved ? "#e2e8f0" : PAPER,
|
||||
stroke: reserved ? "#e2e8f0" : LINE,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.25,
|
||||
@@ -242,14 +271,14 @@ const WorkList: React.FC = () => (
|
||||
rx={4}
|
||||
fill={done ? INK : PAPER}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
{done && (
|
||||
<motion.path
|
||||
d={`M ${76} ${y + 16} l 3.5 3.5 l 6 -7`}
|
||||
fill="none"
|
||||
stroke={PAPER}
|
||||
strokeWidth={2}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...draw(0.3 + row * 0.45)}
|
||||
@@ -261,7 +290,7 @@ const WorkList: React.FC = () => (
|
||||
y1={y + 16}
|
||||
y2={y + 16}
|
||||
stroke={done ? INK : LINE}
|
||||
strokeWidth={2}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{done && (
|
||||
@@ -269,7 +298,7 @@ const WorkList: React.FC = () => (
|
||||
<path
|
||||
d={`M 322 ${y + 16} h 46`}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
markerEnd="url(#arrow)"
|
||||
/>
|
||||
<Packet
|
||||
@@ -319,7 +348,7 @@ const WorkList: React.FC = () => (
|
||||
/** Developer, step 4: two weeks notice to the end of a month. */
|
||||
const Notice: React.FC = () => (
|
||||
<g>
|
||||
<line x1={50} x2={430} y1={150} y2={150} stroke={LINE} strokeWidth={2} />
|
||||
<line x1={50} x2={430} y1={150} y2={150} stroke={LINE} strokeWidth={1.5} />
|
||||
{Array.from({ length: 5 }, (_, w) => (
|
||||
<g key={w}>
|
||||
<line
|
||||
@@ -328,7 +357,7 @@ const Notice: React.FC = () => (
|
||||
y1={140}
|
||||
y2={160}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
<text x={50 + w * 95} y={182} textAnchor="middle" style={MONO}>
|
||||
{w === 4 ? "end of month" : `week ${w + 1}`}
|
||||
@@ -337,9 +366,9 @@ const Notice: React.FC = () => (
|
||||
))}
|
||||
<motion.rect
|
||||
x={240}
|
||||
y={132}
|
||||
height={36}
|
||||
rx={6}
|
||||
y={139}
|
||||
height={22}
|
||||
rx={5}
|
||||
fill={MARK}
|
||||
initial={{ width: 0, opacity: 0.35 }}
|
||||
animate={{ width: 190 }}
|
||||
@@ -350,13 +379,13 @@ const Notice: React.FC = () => (
|
||||
d="M 240 118 v -14 h 190 v 14"
|
||||
fill="none"
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
<text x={335} y={92} textAnchor="middle" style={{ ...MONO, fill: INK }}>
|
||||
two weeks notice
|
||||
</text>
|
||||
</motion.g>
|
||||
<motion.circle cx={430} cy={150} r={7} fill={INK} {...appear(1.4, 0)} />
|
||||
<motion.circle cx={430} cy={150} r={4.5} fill={INK} {...appear(1.4, 0)} />
|
||||
</g>
|
||||
);
|
||||
|
||||
@@ -398,7 +427,7 @@ const RepairWeek: React.FC = () => (
|
||||
x2={to[0]}
|
||||
y2={to[1]}
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
strokeDasharray="3 5"
|
||||
/>
|
||||
<motion.line
|
||||
@@ -407,7 +436,7 @@ const RepairWeek: React.FC = () => (
|
||||
x2={to[0]}
|
||||
y2={to[1]}
|
||||
stroke={INK}
|
||||
strokeWidth={2}
|
||||
strokeWidth={1.5}
|
||||
{...draw(0.4 + i * 0.22)}
|
||||
/>
|
||||
</g>
|
||||
@@ -418,9 +447,9 @@ const RepairWeek: React.FC = () => (
|
||||
key={i}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={7}
|
||||
r={5}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
initial={{ fill: PAPER }}
|
||||
animate={{ fill: LIVE }}
|
||||
transition={{ duration: 0.4, delay: 1 + i * 0.28 }}
|
||||
@@ -449,7 +478,7 @@ const Summary: React.FC = () => (
|
||||
rx={10}
|
||||
fill={PAPER}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
{["found", "changed", "next"].map((label, s) => (
|
||||
<g key={label}>
|
||||
@@ -469,7 +498,7 @@ const Summary: React.FC = () => (
|
||||
y1={84 + s * 62 + l * 11}
|
||||
y2={84 + s * 62 + l * 11}
|
||||
stroke={LINE}
|
||||
strokeWidth={3}
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
{...draw(0.35 + s * 0.6 + l * 0.12)}
|
||||
/>
|
||||
@@ -491,14 +520,21 @@ const Summary: React.FC = () => (
|
||||
/** Rescue, step 4: the client decides how to continue. */
|
||||
const Decide: React.FC = () => (
|
||||
<g>
|
||||
<circle cx={90} cy={150} r={26} fill={INK} />
|
||||
<circle
|
||||
cx={90}
|
||||
cy={150}
|
||||
r={26}
|
||||
fill={TINT}
|
||||
stroke={INK}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
<text
|
||||
x={90}
|
||||
y={154}
|
||||
textAnchor="middle"
|
||||
style={{
|
||||
...MONO,
|
||||
fill: PAPER,
|
||||
fill: INK,
|
||||
fontSize: "calc(var(--scene-label, 10px) - 1px)",
|
||||
}}
|
||||
>
|
||||
@@ -514,7 +550,7 @@ const Decide: React.FC = () => (
|
||||
d={`M 116 150 C 190 150 180 ${option.y} 250 ${option.y}`}
|
||||
fill="none"
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
{...draw(0.3 + i * 0.4)}
|
||||
/>
|
||||
<Packet
|
||||
@@ -561,9 +597,9 @@ const Browser: React.FC<{
|
||||
rx={12}
|
||||
fill={PAPER}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
<line x1={70} x2={410} y1={58} y2={58} stroke={INK} strokeWidth={1.5} />
|
||||
<line x1={70} x2={410} y1={58} y2={58} stroke={INK} strokeWidth={FINE} />
|
||||
{[0, 1, 2].map((d) => (
|
||||
<circle key={d} cx={88 + d * 12} cy={44} r={3} fill={LINE} />
|
||||
))}
|
||||
@@ -617,6 +653,56 @@ const BLOCKS: readonly { x: number; y: number; w: number; h: number }[] = [
|
||||
{ x: 298, y: 174, w: 94, h: 62 },
|
||||
];
|
||||
|
||||
/** What a finished page holds: a headline, a button, and a line of text per card. */
|
||||
const PageDetail: React.FC<{ delay: number }> = ({ delay }) => (
|
||||
<motion.g {...appear(delay, 0)}>
|
||||
<line
|
||||
x1={102}
|
||||
x2={206}
|
||||
y1={118}
|
||||
y2={118}
|
||||
stroke={INK}
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<line
|
||||
x1={102}
|
||||
x2={176}
|
||||
y1={129}
|
||||
y2={129}
|
||||
stroke={INK}
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<rect x={102} y={140} width={40} height={11} rx={5.5} fill={INK} />
|
||||
{[BLOCKS[2], BLOCKS[3], BLOCKS[4], BLOCKS[5]].map(
|
||||
(block) =>
|
||||
block && (
|
||||
<g key={`${block.x}-${block.y}`}>
|
||||
<line
|
||||
x1={block.x + 10}
|
||||
x2={block.x + block.w - 24}
|
||||
y1={block.y + block.h - 20}
|
||||
y2={block.y + block.h - 20}
|
||||
stroke={RIM}
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<line
|
||||
x1={block.x + 10}
|
||||
x2={block.x + block.w - 44}
|
||||
y1={block.y + block.h - 12}
|
||||
y2={block.y + block.h - 12}
|
||||
stroke={LINE}
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
),
|
||||
)}
|
||||
</motion.g>
|
||||
);
|
||||
|
||||
/** Website, step 2: a first design as outlines. */
|
||||
const Wireframe: React.FC = () => (
|
||||
<g>
|
||||
@@ -662,10 +748,14 @@ const Built: React.FC = () => (
|
||||
height={b.h}
|
||||
rx={5}
|
||||
initial={{ fill: PAPER, stroke: LINE }}
|
||||
animate={{ fill: i === 1 ? INK : SOFT, stroke: i === 1 ? INK : LINE }}
|
||||
animate={{
|
||||
fill: i === 1 ? TINT : SOFT,
|
||||
stroke: i === 1 ? INK : LINE,
|
||||
}}
|
||||
transition={{ duration: 0.35, delay: 0.2 + i * 0.18 }}
|
||||
/>
|
||||
))}
|
||||
<PageDetail delay={1.3} />
|
||||
</Browser>
|
||||
<motion.text
|
||||
x={240}
|
||||
@@ -691,10 +781,11 @@ const ChangeByEmail: React.FC = () => (
|
||||
width={b.w}
|
||||
height={b.h}
|
||||
rx={5}
|
||||
fill={i === 1 ? INK : SOFT}
|
||||
fill={i === 1 ? TINT : SOFT}
|
||||
stroke={i === 1 ? INK : LINE}
|
||||
/>
|
||||
))}
|
||||
<PageDetail delay={0} />
|
||||
<motion.rect
|
||||
x={BLOCKS[4]!.x}
|
||||
y={BLOCKS[4]!.y}
|
||||
@@ -702,8 +793,6 @@ const ChangeByEmail: React.FC = () => (
|
||||
height={BLOCKS[4]!.h}
|
||||
rx={5}
|
||||
fill={MARK}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4, delay: 1.3 }}
|
||||
@@ -718,20 +807,20 @@ const ChangeByEmail: React.FC = () => (
|
||||
rx={5}
|
||||
fill={PAPER}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
<path
|
||||
d="M 16 190 l 23 16 l 23 -16"
|
||||
fill="none"
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
</motion.g>
|
||||
<motion.path
|
||||
d="M 66 202 C 120 202 150 205 188 205"
|
||||
fill="none"
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
markerEnd="url(#arrow)"
|
||||
{...draw(0.6)}
|
||||
/>
|
||||
@@ -766,9 +855,9 @@ const Tool: React.FC<{ x: number; y: number; label: string }> = ({
|
||||
width={112}
|
||||
height={44}
|
||||
rx={10}
|
||||
fill={PAPER}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
fill={SOFT}
|
||||
stroke={RIM}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
<text x={x} y={y + 4} textAnchor="middle" style={{ ...MONO, fill: INK }}>
|
||||
{label}
|
||||
@@ -788,7 +877,7 @@ const ByHand: React.FC = () => (
|
||||
d={`M ${tool.x + (tool.x < 240 ? 56 : -56)} ${tool.y} L ${tool.x < 240 ? 214 : 266} ${140 + (tool.y < 140 ? -12 : 12)}`}
|
||||
fill="none"
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
strokeDasharray="3 5"
|
||||
{...draw(0.3 + i * 0.25)}
|
||||
/>
|
||||
@@ -800,13 +889,13 @@ const ByHand: React.FC = () => (
|
||||
r={13}
|
||||
fill={PAPER}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
<path
|
||||
d="M 218 172 Q 218 146 240 146 Q 262 146 262 172"
|
||||
fill={PAPER}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
strokeWidth={FINE}
|
||||
/>
|
||||
</motion.g>
|
||||
<motion.text
|
||||
@@ -837,7 +926,7 @@ const Wiring: React.FC = () => (
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke={INK}
|
||||
strokeWidth={2}
|
||||
strokeWidth={1.5}
|
||||
{...draw(0.3 + i * 0.4)}
|
||||
/>
|
||||
))}
|
||||
@@ -860,7 +949,7 @@ const Wiring: React.FC = () => (
|
||||
const Running: React.FC = () => (
|
||||
<g>
|
||||
{WIRES.map((d) => (
|
||||
<path key={d} d={d} fill="none" stroke={INK} strokeWidth={2} />
|
||||
<path key={d} d={d} fill="none" stroke={INK} strokeWidth={1.5} />
|
||||
))}
|
||||
{[
|
||||
{ from: [146, 80], to: [334, 80], delay: 0 },
|
||||
@@ -870,10 +959,11 @@ const Running: React.FC = () => (
|
||||
].map((dot) => (
|
||||
<motion.circle
|
||||
key={dot.delay}
|
||||
r={6}
|
||||
r={3.5}
|
||||
fill={MARK}
|
||||
stroke={INK}
|
||||
strokeWidth={1.5}
|
||||
stroke={MARK}
|
||||
strokeOpacity={0.28}
|
||||
strokeWidth={6}
|
||||
initial={{ cx: dot.from[0], cy: dot.from[1], opacity: 0 }}
|
||||
animate={{
|
||||
cx: [dot.from[0]!, dot.to[0]!],
|
||||
@@ -932,8 +1022,8 @@ export const PathScene: React.FC<{
|
||||
viewBox="0 0 10 10"
|
||||
refX="8"
|
||||
refY="5"
|
||||
markerWidth="7"
|
||||
markerHeight="7"
|
||||
markerWidth="5"
|
||||
markerHeight="5"
|
||||
orient="auto-start-reverse"
|
||||
>
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill={INK} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { BitAura } from "@/src/components/bits/BitAura";
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
@@ -285,125 +286,127 @@ export const PipelineRun: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-8">
|
||||
<div className="overflow-hidden rounded-3xl bg-slate-900 font-mono text-[12px] leading-relaxed text-slate-300 md:text-[13px]">
|
||||
{/* Run header */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-800 px-5 py-4 md:px-7">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex gap-1.5" aria-hidden="true">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
|
||||
</span>
|
||||
<span className="text-slate-400">
|
||||
{scenario.branch}
|
||||
<span className="text-slate-600"> → </span>main
|
||||
<BitAura source="right" tone="light">
|
||||
<div className="overflow-hidden rounded-3xl bg-slate-900 font-mono text-[12px] leading-relaxed text-slate-300 md:text-[13px]">
|
||||
{/* Run header */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-800 px-5 py-4 md:px-7">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex gap-1.5" aria-hidden="true">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
|
||||
</span>
|
||||
<span className="text-slate-400">
|
||||
{scenario.branch}
|
||||
<span className="text-slate-600"> → </span>main
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-slate-500">
|
||||
run #{frame.attempt === 1 ? 481 : 482}
|
||||
{frame.attempt === 2 && (
|
||||
<span className="text-slate-400"> · fix pushed</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-slate-500">
|
||||
run #{frame.attempt === 1 ? 481 : 482}
|
||||
{frame.attempt === 2 && (
|
||||
<span className="text-slate-400"> · fix pushed</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stages */}
|
||||
<div className="grid grid-cols-2 gap-2 border-b border-slate-800 px-5 py-4 sm:grid-cols-4 md:px-7">
|
||||
{STAGES.map((stage, index) => {
|
||||
const state = stageState(index);
|
||||
return (
|
||||
<div
|
||||
key={stage}
|
||||
className={`flex items-center justify-between rounded-xl border px-3 py-2.5 transition-colors duration-200 ${STAGE_STYLE[state]}`}
|
||||
>
|
||||
<span>{stage}</span>
|
||||
<span aria-hidden="true">{STAGE_MARK[state]}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Test output */}
|
||||
<div className="min-h-[21rem] px-5 py-5 md:px-7 md:py-6">
|
||||
{frame.stagesDone < 2 && (
|
||||
<p className="m-0 text-[12px] leading-relaxed text-slate-500 md:text-[13px]">
|
||||
{frame.stagesDone === 0
|
||||
? "$ lint: checking the code style…"
|
||||
: "$ types: checking that the parts fit together…"}
|
||||
</p>
|
||||
)}
|
||||
{SUITES.slice(0, frame.suitesDone).map((suite, index) => {
|
||||
const failed = firstAttempt && index === failingIndex;
|
||||
return (
|
||||
<div key={suite.file}>
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span
|
||||
className={`w-10 shrink-0 font-bold ${failed ? "text-rose-400" : "text-emerald-400"}`}
|
||||
>
|
||||
{failed ? "FAIL" : "PASS"}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-slate-200">
|
||||
{suite.file}
|
||||
</span>
|
||||
<span className="hidden text-slate-500 sm:inline">
|
||||
{suite.tests} tests
|
||||
</span>
|
||||
<span className="w-14 shrink-0 text-right text-slate-600">
|
||||
{suite.ms} ms
|
||||
</span>
|
||||
{/* Stages */}
|
||||
<div className="grid grid-cols-2 gap-2 border-b border-slate-800 px-5 py-4 sm:grid-cols-4 md:px-7">
|
||||
{STAGES.map((stage, index) => {
|
||||
const state = stageState(index);
|
||||
return (
|
||||
<div
|
||||
key={stage}
|
||||
className={`flex items-center justify-between rounded-xl border px-3 py-2.5 transition-colors duration-200 ${STAGE_STYLE[state]}`}
|
||||
>
|
||||
<span>{stage}</span>
|
||||
<span aria-hidden="true">{STAGE_MARK[state]}</span>
|
||||
</div>
|
||||
{failed && (
|
||||
<div className="my-2 ml-[3.25rem] border-l border-rose-500/50 pl-4">
|
||||
<p className="m-0 text-[12px] leading-relaxed text-slate-200 md:text-[13px]">
|
||||
● {scenario.testName}
|
||||
</p>
|
||||
<p className="m-0 text-[12px] leading-relaxed text-emerald-400 md:text-[13px]">
|
||||
expected {scenario.expected}
|
||||
</p>
|
||||
<p className="m-0 text-[12px] leading-relaxed text-rose-400 md:text-[13px]">
|
||||
received {scenario.received}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{frame.stagesDone === 2 && frame.suitesDone < SUITES.length && (
|
||||
<p className="m-0 text-[12px] leading-relaxed text-slate-500 md:text-[13px]">
|
||||
running {SUITES[frame.suitesDone]?.file}…
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-slate-800 px-5 py-4 md:px-7">
|
||||
<span>
|
||||
<span className="text-slate-500">Tests </span>
|
||||
{failedCount > 0 && (
|
||||
<span className="text-rose-400">{failedCount} failed, </span>
|
||||
{/* Test output */}
|
||||
<div className="min-h-[21rem] px-5 py-5 md:px-7 md:py-6">
|
||||
{frame.stagesDone < 2 && (
|
||||
<p className="m-0 text-[12px] leading-relaxed text-slate-500 md:text-[13px]">
|
||||
{frame.stagesDone === 0
|
||||
? "$ lint: checking the code style…"
|
||||
: "$ types: checking that the parts fit together…"}
|
||||
</p>
|
||||
)}
|
||||
<span className="text-emerald-400">
|
||||
{reportedTests - failedCount} passed
|
||||
{SUITES.slice(0, frame.suitesDone).map((suite, index) => {
|
||||
const failed = firstAttempt && index === failingIndex;
|
||||
return (
|
||||
<div key={suite.file}>
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span
|
||||
className={`w-10 shrink-0 font-bold ${failed ? "text-rose-400" : "text-emerald-400"}`}
|
||||
>
|
||||
{failed ? "FAIL" : "PASS"}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-slate-200">
|
||||
{suite.file}
|
||||
</span>
|
||||
<span className="hidden text-slate-500 sm:inline">
|
||||
{suite.tests} tests
|
||||
</span>
|
||||
<span className="w-14 shrink-0 text-right text-slate-600">
|
||||
{suite.ms} ms
|
||||
</span>
|
||||
</div>
|
||||
{failed && (
|
||||
<div className="my-2 ml-[3.25rem] border-l border-rose-500/50 pl-4">
|
||||
<p className="m-0 text-[12px] leading-relaxed text-slate-200 md:text-[13px]">
|
||||
● {scenario.testName}
|
||||
</p>
|
||||
<p className="m-0 text-[12px] leading-relaxed text-emerald-400 md:text-[13px]">
|
||||
expected {scenario.expected}
|
||||
</p>
|
||||
<p className="m-0 text-[12px] leading-relaxed text-rose-400 md:text-[13px]">
|
||||
received {scenario.received}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{frame.stagesDone === 2 && frame.suitesDone < SUITES.length && (
|
||||
<p className="m-0 text-[12px] leading-relaxed text-slate-500 md:text-[13px]">
|
||||
running {SUITES[frame.suitesDone]?.file}…
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-slate-800 px-5 py-4 md:px-7">
|
||||
<span>
|
||||
<span className="text-slate-500">Tests </span>
|
||||
{failedCount > 0 && (
|
||||
<span className="text-rose-400">{failedCount} failed, </span>
|
||||
)}
|
||||
<span className="text-emerald-400">
|
||||
{reportedTests - failedCount} passed
|
||||
</span>
|
||||
<span className="text-slate-500"> of {TOTAL_TESTS}</span>
|
||||
</span>
|
||||
<span className="text-slate-500"> of {TOTAL_TESTS}</span>
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 font-bold uppercase tracking-[0.15em] ${
|
||||
!finished
|
||||
? "bg-slate-800 text-slate-400"
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 font-bold uppercase tracking-[0.15em] ${
|
||||
!finished
|
||||
? "bg-slate-800 text-slate-400"
|
||||
: firstAttempt
|
||||
? "bg-rose-500/15 text-rose-400"
|
||||
: "bg-emerald-500/15 text-emerald-400"
|
||||
}`}
|
||||
>
|
||||
{!finished
|
||||
? "release pending"
|
||||
: firstAttempt
|
||||
? "bg-rose-500/15 text-rose-400"
|
||||
: "bg-emerald-500/15 text-emerald-400"
|
||||
}`}
|
||||
>
|
||||
{!finished
|
||||
? "release pending"
|
||||
: firstAttempt
|
||||
? "release blocked"
|
||||
: "released"}
|
||||
</span>
|
||||
? "release blocked"
|
||||
: "released"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BitAura>
|
||||
<p className="m-0 mt-4 font-sans text-sm leading-relaxed text-slate-400">
|
||||
An illustration with made-up file names and numbers, modelled on a
|
||||
real test run. Tests do not catch every mistake, but they do catch
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as React from "react";
|
||||
import Image from "next/image";
|
||||
import { motion, useMotionValue, useSpring } from "framer-motion";
|
||||
import { BitAura } from "@/src/components/bits/BitAura";
|
||||
|
||||
export type WorkItem = {
|
||||
readonly name: string;
|
||||
@@ -107,7 +108,7 @@ const WorkStage: React.FC<{ item: WorkItem }> = ({ item }) => {
|
||||
ref={ref}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerLeave={onPointerLeave}
|
||||
className="relative overflow-hidden rounded-3xl bg-slate-900 px-5 pb-0 pt-8 [perspective:1400px] md:px-12 md:pt-14"
|
||||
className="relative overflow-hidden rounded-3xl border border-slate-700 bg-slate-950 px-5 pb-0 pt-8 [perspective:1400px] md:px-12 md:pt-14"
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-60"
|
||||
@@ -179,7 +180,7 @@ const WorkStage: React.FC<{ item: WorkItem }> = ({ item }) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* WorkShowcase: each live site on a dark stage, as a desktop and a phone view
|
||||
* WorkShowcase: for a dark section. Each live site on a lit stage, as a desktop and a phone view
|
||||
* that scroll through the real pages.
|
||||
*/
|
||||
export const WorkShowcase: React.FC<{ items: readonly WorkItem[] }> = ({
|
||||
@@ -192,17 +193,23 @@ export const WorkShowcase: React.FC<{ items: readonly WorkItem[] }> = ({
|
||||
className="grid grid-cols-1 gap-10 md:grid-cols-12 md:items-center md:gap-12"
|
||||
>
|
||||
<div className={`md:col-span-8 ${index % 2 === 1 ? "md:order-2" : ""}`}>
|
||||
<WorkStage item={item} />
|
||||
<BitAura
|
||||
source={index % 2 === 0 ? "bottom" : "top"}
|
||||
tone="dark"
|
||||
motion={index % 2 === 0 ? "sparks" : "rain"}
|
||||
>
|
||||
<WorkStage item={item} />
|
||||
</BitAura>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-4">
|
||||
<p className={`${MONO} m-0 text-slate-500`}>
|
||||
<p className={`${MONO} m-0 text-slate-400`}>
|
||||
0{index + 1} · {item.client}
|
||||
</p>
|
||||
<h3 className="m-0 mt-4 font-sans text-3xl font-bold tracking-tighter text-slate-900 md:text-5xl">
|
||||
<h3 className="m-0 mt-4 font-sans text-3xl font-bold tracking-tighter text-white md:text-5xl">
|
||||
{item.name}
|
||||
</h3>
|
||||
<p className="m-0 mt-5 font-sans text-lg leading-relaxed text-slate-500 md:text-xl">
|
||||
<p className="m-0 mt-5 font-sans text-lg leading-relaxed text-slate-300 md:text-xl">
|
||||
{item.text}
|
||||
</p>
|
||||
|
||||
@@ -210,7 +217,7 @@ export const WorkShowcase: React.FC<{ items: readonly WorkItem[] }> = ({
|
||||
{item.did.map((line) => (
|
||||
<li
|
||||
key={line}
|
||||
className="border-t border-slate-200 py-3 font-sans text-base text-slate-900 md:text-lg"
|
||||
className="border-t border-slate-800 py-3 font-sans text-base text-slate-100 md:text-lg"
|
||||
>
|
||||
{line}
|
||||
</li>
|
||||
@@ -221,7 +228,7 @@ export const WorkShowcase: React.FC<{ items: readonly WorkItem[] }> = ({
|
||||
{item.tech.map((tech) => (
|
||||
<li
|
||||
key={tech}
|
||||
className="rounded-full border border-slate-200 px-3 py-1.5 font-mono text-[10px] uppercase tracking-[0.12em] text-slate-600"
|
||||
className="rounded-full border border-slate-700 px-3 py-1.5 font-mono text-[10px] uppercase tracking-[0.12em] text-slate-300"
|
||||
>
|
||||
{tech}
|
||||
</li>
|
||||
@@ -232,7 +239,7 @@ export const WorkShowcase: React.FC<{ items: readonly WorkItem[] }> = ({
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-8 inline-flex items-center gap-3 rounded-full bg-slate-900 px-6 py-3 font-sans text-[11px] font-bold uppercase tracking-[0.15em] text-white no-underline transition-transform duration-300 hover:-translate-y-0.5"
|
||||
className="bit-cta bit-cta-light mt-8 inline-flex items-center gap-3 rounded-full bg-white px-6 py-3 font-sans text-[11px] font-bold uppercase tracking-[0.15em] text-slate-900 no-underline transition-transform duration-300 hover:-translate-y-0.5"
|
||||
>
|
||||
Open the live site <span aria-hidden="true">↗</span>
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user