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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Every release corresponds to a `staging` to `main` pull request and a matching
`vX.Y.Z` tag on `main`.

## [0.1.3] - 2026-06-30

### Fixed

- Dark mode: the resume nav bar, the home "your resumes" / empty-state boxes, and
the template gallery's search box and filter chips now use dark surfaces
instead of staying light (which read as muddy grey over the dark canvas).
Template cards stay light "sheets", but their pills keep light styling so they
are not dark-grey on white.
- Template card actions are an even three-column row (copy / use / preview) that
fills the footer. The home featured templates use the same 3-column grid as the
gallery (two rows of three), so the cards match the gallery width and the
"preview" button no longer overflows on the narrower cards.

### Changed

- Home resume row: edit and delete are now icon buttons (pencil / trash) with
accessible labels, instead of text.
- Renaming a resume on the home hub now saves on Enter or blur (and reverts on
Escape), replacing the standalone "save" button that looked like it saved the
whole résumé.

## [0.1.2] - 2026-06-30

### Fixed
Expand Down Expand Up @@ -75,6 +97,7 @@ marketing site rebrand.
- Invalid nested `<a>` in template cards (hydration error).
- Tolerate a missing `text_align` column before the migration runs.

[0.1.3]: https://github.com/BiSemaphore/binarysemaphore.com/releases/tag/v0.1.3
[0.1.2]: https://github.com/BiSemaphore/binarysemaphore.com/releases/tag/v0.1.2
[0.1.1]: https://github.com/BiSemaphore/binarysemaphore.com/releases/tag/v0.1.1
[0.1.0]: https://github.com/BiSemaphore/binarysemaphore.com/releases/tag/v0.1.0
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "binarysemaphore",
"version": "0.1.2",
"version": "0.1.3",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
12 changes: 12 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,18 @@ body {
.dark .rx-pill:hover {
color: #f5f5f5;
}
/* The template card is an always-light "sheet" (it frames a white resume), so
its pills keep the light styling even in dark mode, like the card's
fixed-dark text. Same specificity as `.dark .rx-pill`, declared after it so it
wins inside a card. */
.rx-card .rx-pill {
background: rgba(255, 255, 255, 0.9);
border-color: rgba(20, 20, 19, 0.12);
color: #6b6b6b;
}
.rx-card .rx-pill:hover {
color: #171717;
}
/* Translucent floating surfaces (tune panel, control pills). */
.rx-panel {
background: rgba(255, 255, 255, 0.88);
Expand Down
2 changes: 1 addition & 1 deletion src/app/resume/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default async function ResumeAppLayout({
<div
className={`${figtree.variable} rx rx-canvas flex min-h-screen flex-col font-[family-name:var(--font-figtree)]`}
>
<header className="sticky top-0 z-30 border-b border-black/5 bg-white/70 backdrop-blur">
<header className="sticky top-0 z-30 border-b border-black/5 bg-white/70 backdrop-blur dark:border-white/10 dark:bg-[#161616]/75">
<div className="mx-auto flex h-12 w-full max-w-5xl items-center justify-between px-5">
<div className="flex items-center gap-2">
<a
Expand Down
52 changes: 20 additions & 32 deletions src/app/resume/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import { listResumes } from "@/lib/resume/db";
import { TEMPLATES } from "@/lib/resume/schema";
import { TemplateCard } from "@/components/resume/template-card";
import { SubmitButton } from "@/components/resume/submit-button";
import {
createResumeAction,
deleteResumeAction,
renameResumeAction,
} from "./actions";
import { PencilIcon, TrashIcon } from "@/components/icons";
import { createResumeAction, deleteResumeAction } from "./actions";
import { RenameTitle } from "@/components/resume/rename-title";

export const metadata: Metadata = {
title: "Resume builder",
Expand All @@ -29,7 +27,7 @@ function formatDate(iso: string): string {
export default async function ResumeHome() {
const user = await getCurrentUser();
const resumes = user ? await listResumes() : [];
const featured = TEMPLATES.slice(0, 4);
const featured = TEMPLATES.slice(0, 6);

return (
<div className="mx-auto w-full max-w-5xl px-5 py-12">
Expand Down Expand Up @@ -65,7 +63,7 @@ export default async function ResumeHome() {
</div>

{!user ? (
<div className="rounded-xl border border-black/10 bg-white/70 p-10 text-center">
<div className="rounded-xl border border-black/10 bg-white/70 p-10 text-center dark:border-white/10 dark:bg-white/[0.04]">
<p className="text-sm text-[color:var(--rx-muted)]">
Sign in to create and manage your resumes.
</p>
Expand All @@ -77,7 +75,7 @@ export default async function ResumeHome() {
</Link>
</div>
) : resumes.length === 0 ? (
<div className="rounded-xl border border-dashed border-black/15 bg-white/60 p-10 text-center">
<div className="rounded-xl border border-dashed border-black/15 bg-white/60 p-10 text-center dark:border-white/15 dark:bg-white/[0.03]">
<p className="text-sm text-[color:var(--rx-muted)]">
No resumes yet. Click{" "}
<span className="font-medium text-foreground">+ new resume</span>,
Expand All @@ -89,42 +87,32 @@ export default async function ResumeHome() {
{resumes.map((resume) => (
<li
key={resume.id}
className="rounded-xl border border-black/10 bg-white p-4 sm:flex sm:items-center sm:justify-between sm:gap-4"
className="rounded-xl border border-black/10 bg-white p-4 dark:border-white/10 dark:bg-white/[0.04] sm:flex sm:items-center sm:justify-between sm:gap-4"
>
<div className="min-w-0 flex-1">
<form
action={renameResumeAction}
className="flex items-center gap-2"
>
<input type="hidden" name="id" value={resume.id} />
<input
name="title"
defaultValue={resume.title}
aria-label="Resume title"
className="w-full max-w-xs rounded-lg border border-transparent bg-transparent px-2 py-1 text-base font-medium text-foreground hover:border-black/15 focus:border-black/20 focus:outline-none"
/>
<button
type="submit"
className="shrink-0 rounded-lg px-2 py-1 font-mono text-xs text-[color:var(--rx-muted)] transition-colors hover:text-foreground"
>
save
</button>
</form>
<RenameTitle id={resume.id} title={resume.title} />
<p className="mt-1 px-2 font-mono text-xs text-[color:var(--rx-muted)]">
{resume.templateId} · updated {formatDate(resume.updatedAt)}
</p>
</div>
<div className="mt-3 flex items-center gap-2 sm:mt-0">
<Link href={`/editor/${resume.id}`} className="rx-pill">
edit
<Link
href={`/editor/${resume.id}`}
aria-label={`Edit ${resume.title}`}
title="Edit"
className="rx-pill"
>
<PencilIcon className="h-4 w-4" />
</Link>
<form action={deleteResumeAction}>
<input type="hidden" name="id" value={resume.id} />
<button
type="submit"
className="rounded-lg px-3 py-2 font-mono text-xs text-[color:var(--rx-muted)] transition-colors hover:text-red-500"
aria-label={`Delete ${resume.title}`}
title="Delete"
className="inline-flex items-center rounded-lg p-2 text-[color:var(--rx-muted)] transition-colors hover:text-red-500"
>
delete
<TrashIcon className="h-4 w-4" />
</button>
</form>
</div>
Expand All @@ -147,7 +135,7 @@ export default async function ResumeHome() {
browse all ({TEMPLATES.length}) →
</Link>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{featured.map((t) => (
<TemplateCard key={t.id} template={t} />
))}
Expand Down
6 changes: 3 additions & 3 deletions src/app/resume/(app)/templates/templates-gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function TemplatesGallery() {
onChange={(e) => setQ(e.target.value)}
placeholder="search by name, tag, or description"
aria-label="search templates"
className="w-full rounded-lg border border-black/15 bg-white/80 py-2 pl-8 pr-3 font-mono text-sm text-foreground placeholder:text-[color:var(--rx-muted)] focus:border-[color:var(--rx-accent)] focus:outline-none"
className="w-full rounded-lg border border-black/15 bg-white/80 py-2 pl-8 pr-3 font-mono text-sm text-foreground placeholder:text-[color:var(--rx-muted)] focus:border-[color:var(--rx-accent)] focus:outline-none dark:border-white/15 dark:bg-white/[0.04]"
/>
</label>
<span className="font-mono text-xs text-[color:var(--rx-muted)]">
Expand All @@ -73,7 +73,7 @@ export function TemplatesGallery() {
className={`rounded-full border px-2.5 py-0.5 font-mono text-[11px] transition-colors ${
tags.length === 0
? "rx-accent border-transparent"
: "border-black/10 text-[color:var(--rx-muted)] hover:bg-black/5"
: "border-black/10 text-[color:var(--rx-muted)] hover:bg-black/5 dark:border-white/15 dark:hover:bg-white/10"
}`}
>
all
Expand All @@ -89,7 +89,7 @@ export function TemplatesGallery() {
className={`rounded-full border px-2.5 py-0.5 font-mono text-[11px] transition-colors ${
active
? "rx-accent border-transparent"
: "border-black/10 text-[color:var(--rx-muted)] hover:bg-black/5"
: "border-black/10 text-[color:var(--rx-muted)] hover:bg-black/5 dark:border-white/15 dark:hover:bg-white/10"
}`}
>
{tag}
Expand Down
38 changes: 38 additions & 0 deletions src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,41 @@ export function CheckIcon(props: IconProps) {
</svg>
);
}

export function PencilIcon(props: IconProps) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<path d="M12 20h9" />
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5Z" />
</svg>
);
}

export function TrashIcon(props: IconProps) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<path d="M3 6h18" />
<path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2" />
<path d="M19 6l-1 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1L5 6" />
<path d="M10 11v6M14 11v6" />
</svg>
);
}
51 changes: 51 additions & 0 deletions src/components/resume/rename-title.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { useRef } from "react";
import { renameResumeAction } from "@/app/resume/(app)/actions";

/**
* Inline résumé-title rename on the home hub. Persists on Enter or blur (only
* when the name actually changed), reverts on Escape, and shows no explicit
* save button. Submits the existing `renameResumeAction` via the form, so the
* server action + revalidation stay the source of truth.
*/
export function RenameTitle({ id, title }: { id: string; title: string }) {
const formRef = useRef<HTMLFormElement>(null);
const inputRef = useRef<HTMLInputElement>(null);

function commit() {
const input = inputRef.current;
if (!input) return;
const next = input.value.trim();
if (!next) {
input.value = title; // don't allow an empty title
return;
}
if (next !== title) formRef.current?.requestSubmit();
}

return (
<form ref={formRef} action={renameResumeAction}>
<input type="hidden" name="id" value={id} />
<input
ref={inputRef}
name="title"
defaultValue={title}
aria-label="Resume title"
title="Press Enter to rename"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
inputRef.current?.blur(); // triggers onBlur -> commit
} else if (e.key === "Escape") {
e.preventDefault();
if (inputRef.current) inputRef.current.value = title;
inputRef.current?.blur();
}
}}
onBlur={commit}
className="w-full max-w-xs rounded-lg border border-transparent bg-transparent px-2 py-1 text-base font-medium text-foreground hover:border-black/15 focus:border-black/20 focus:outline-none dark:hover:border-white/20 dark:focus:border-white/25"
/>
</form>
);
}
22 changes: 16 additions & 6 deletions src/components/resume/template-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { SubmitButton } from "@/components/resume/submit-button";
*/
export function TemplateCard({ template }: { template: Template }) {
return (
<div className="group flex flex-col overflow-hidden rounded-xl border border-black/10 bg-white shadow-[0_1px_2px_rgba(0,0,0,0.04)] transition-shadow hover:shadow-[0_6px_20px_rgba(0,0,0,0.08)]">
<div className="rx-card group flex h-full flex-col overflow-hidden rounded-xl border border-black/10 bg-white shadow-[0_1px_2px_rgba(0,0,0,0.04)] transition-shadow hover:shadow-[0_6px_20px_rgba(0,0,0,0.08)]">
{/* Live mini-preview: a fixed-height window onto the top of the page,
with a soft fade so the crop reads as intentional. The clickable
layer is an overlay link (sibling, not ancestor) so the resume's own
Expand Down Expand Up @@ -61,13 +61,23 @@ export function TemplateCard({ template }: { template: Template }) {
Fixed-dark text since the card is an always-white sheet. */}
<div className="mt-auto border-t border-black/5 pt-3 font-mono text-xs">
<p className="mb-2 truncate text-neutral-500">{template.id}</p>
<div className="flex items-center gap-2">
<CopyPromptButton template={template} />
<form action={useTemplateAction}>
{/* Three equal-width buttons that fill the footer, so the row reads as
a deliberate, balanced control bar in every card width. */}
<div className="grid grid-cols-3 gap-2">
<CopyPromptButton
template={template}
className="rx-pill w-full justify-center"
/>
<form action={useTemplateAction} className="contents">
<input type="hidden" name="templateId" value={template.id} />
<SubmitButton className="rx-pill">use</SubmitButton>
<SubmitButton className="rx-pill w-full justify-center">
use
</SubmitButton>
</form>
<Link href={`/preview/${template.id}`} className="rx-pill">
<Link
href={`/preview/${template.id}`}
className="rx-pill w-full justify-center"
>
preview →
</Link>
</div>
Expand Down