Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright 2026 The resumelint Authors

import { useCallback, useState } from "react";
import { Chip } from "./components/ui/Chip.tsx";
import { DropZone } from "./components/DropZone";
import { Result } from "./components/Result";
import { runCascade } from "./lib/heuristics";
Expand Down Expand Up @@ -89,16 +90,26 @@ export default function App() {

return (
<main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-8 px-6 py-10">
<header className="flex items-baseline justify-between">
<div className="flex items-baseline gap-2">
<h1 className="text-2xl font-semibold tracking-tight">resumelint</h1>
<span className="text-[10px] font-semibold uppercase tracking-wider text-neutral-500">
alpha
</span>
<header className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="inline-grid h-8 w-8 place-items-center rounded-md bg-brand-amber text-base font-bold text-content-inverse">
R
</span>
<h1 className="text-2xl font-semibold tracking-tight">resumelint</h1>
<span className="text-[10px] font-semibold uppercase tracking-wider text-content-muted">
alpha
</span>
</div>
<p className="hidden text-xs text-content-muted sm:block">
PDF parser stress test for resumes
</p>
</div>
<div className="flex flex-wrap gap-2">
<Chip icon="⚡">~30 seconds</Chip>
<Chip icon="🔒">Runs in your browser</Chip>
<Chip icon="✓">No signup required</Chip>
</div>
<p className="hidden text-xs text-neutral-500 sm:block">
PDF parser stress test for resumes
</p>
</header>

<section className="flex flex-col gap-3">
Expand Down
32 changes: 21 additions & 11 deletions src/components/Result.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import type {
BulletObservation,
} from "../lib/score/score.ts";
import { PdfPreview } from "./PdfPreview";
import { Chip } from "./ui/Chip.tsx";
import { ScoreRing } from "./features/ScoreRing.tsx";
import { VerdictHeader } from "./features/VerdictHeader.tsx";

interface ResultProps {
result: CascadeResult;
Expand Down Expand Up @@ -116,9 +119,9 @@ function LayoutFlagsList({ triggers }: { triggers: readonly LayoutTrigger[] }) {
Layout flags
</h2>
{triggers.length === 0 ? (
<p className="text-sm text-content-tertiary">
None — single-column, text-PDF layout.
</p>
<Chip tone="success" icon="✓">
Clean ATS layout — single-column, selectable text
</Chip>
) : (
<ul className="flex flex-col gap-1.5">
{triggers.map((t) => (
Expand Down Expand Up @@ -153,9 +156,9 @@ function AtsScoreReadout({ score }: { score: AnonymousAtsScore }) {
</span>
)}
</div>
<div className="flex items-baseline gap-3">
<span className="text-3xl font-semibold">{score.overall}</span>
<span className="text-sm text-content-muted">/ 100</span>
<div className="flex items-center gap-4">
<ScoreRing score={score.overall} />
<VerdictHeader score={score.overall} />
</div>
<p className="max-w-prose text-xs text-content-tertiary">
Our reference number for iterating on the parser. Not a universal
Expand Down Expand Up @@ -212,12 +215,13 @@ function Dimension({
gradable: boolean;
hint: string;
}) {
const pct = max > 0 ? Math.round((value / max) * 100) : 0;
return (
<div className="flex flex-col gap-1 rounded border border-border-light p-2">
<div className="flex flex-col gap-1.5 rounded-lg border border-border-light bg-surface-subtle p-3">
<dt className="text-[10px] font-semibold uppercase tracking-wider text-content-muted">
{label}
</dt>
<dd className="text-base font-medium">
<dd className="text-sm font-medium">
{gradable ? (
<>
{value}
Expand All @@ -227,9 +231,15 @@ function Dimension({
<span className="text-content-muted">—</span>
)}
</dd>
<p className="text-[10px] text-content-muted">
{hint}
</p>
{gradable && (
<div className="h-1.5 w-full overflow-hidden rounded-full bg-surface-card">
<div
className="h-full rounded-full bg-brand-amber"
style={{ width: `${pct}%` }}
/>
</div>
)}
<p className="text-[10px] text-content-muted">{hint}</p>
</div>
);
}
Expand Down
67 changes: 67 additions & 0 deletions src/components/features/ScoreRing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 The resumelint Authors

import { getScoreTier } from "../../lib/score/score.ts";
import { scoreBandTextClass } from "./scoreBand.ts";

interface ScoreRingProps {
score: number;
max?: number;
}

export function ScoreRing({ score, max = 100 }: ScoreRingProps) {
const tier = getScoreTier(score);
const ringColorCls = scoreBandTextClass(tier);

const size = 96;
const strokeWidth = 8;
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const progress = Math.max(0, Math.min(1, score / max));
const dashOffset = circumference * (1 - progress);

return (
<div className="relative inline-flex items-center justify-center">
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
style={{ transform: "rotate(-90deg)" }}
aria-hidden="true"
>
{/* Track circle */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
strokeWidth={strokeWidth}
className="text-surface-subtle"
stroke="currentColor"
/>
{/* Progress arc */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
className={ringColorCls}
stroke="currentColor"
/>
</svg>
<div
className="absolute flex flex-col items-center justify-center"
style={{ transform: "rotate(0deg)" }}
>
<span className={`text-3xl font-semibold leading-none ${ringColorCls}`}>
{score}
</span>
<span className="text-[10px] text-content-muted">/ {max}</span>
</div>
</div>
);
}
24 changes: 24 additions & 0 deletions src/components/features/VerdictHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 The resumelint Authors

import { getScoreLabel, getScoreTier } from "../../lib/score/score.ts";
import { scoreBandTextClass } from "./scoreBand.ts";

interface VerdictHeaderProps {
score: number;
}

export function VerdictHeader({ score }: VerdictHeaderProps) {
const tier = getScoreTier(score);
const label = getScoreLabel(tier);
const colorCls = scoreBandTextClass(tier);

return (
<div className="flex flex-col justify-center gap-0.5">
<p className={`text-2xl font-semibold ${colorCls}`}>{label}</p>
<p className="text-sm text-content-muted">
ATS readiness · {score} / 100
</p>
</div>
);
}
18 changes: 18 additions & 0 deletions src/components/features/scoreBand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 The resumelint Authors

import type { ScoreTier } from "../../lib/score/types.ts";

/** Maps a score tier to the Tailwind text-color token for that band. */
export function scoreBandTextClass(tier: ScoreTier): string {
switch (tier) {
case "high":
return "text-feedback-success-icon";
case "medium":
return "text-brand-amber";
case "low":
return "text-feedback-warning-icon";
default:
return "text-feedback-warning-icon";
}
}
25 changes: 25 additions & 0 deletions src/components/ui/Chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 The resumelint Authors

export type ChipTone = "neutral" | "success";

interface ChipProps {
icon?: React.ReactNode;
children: React.ReactNode;
tone?: ChipTone;
}

export function Chip({ icon, children, tone = "neutral" }: ChipProps) {
const toneCls =
tone === "success"
? "bg-feedback-success-bg text-feedback-success-text"
: "bg-surface-subtle text-content-secondary";
return (
<span
className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs ${toneCls}`}
>
{icon && <span>{icon}</span>}
{children}
</span>
);
}