@@ -441,36 +482,52 @@ export default function App() {
))}
-
-
Preset
-
+
- {SPACING_GROUPS.map((group) => (
-
-
{group.label}
-
- {group.sliders.map(({ key, label, min, max, unit }) => (
- docStyle.set(key, value)}
- />
- ))}
-
-
- ))}
+
+
+ {showFineTune ? (
+
+ {SPACING_GROUPS.map((group) => (
+
+
{group.label}
+
+ {group.sliders.map(({ key, label, min, max, unit }) => (
+ docStyle.set(key, value)}
+ />
+ ))}
+
+
+ ))}
+
+
+ ) : null}
@@ -479,17 +536,49 @@ export default function App() {
-
Contact divider
+
Headings
+
+
Case
+
docStyle.set("headingCase", key)}
+ block
+ />
+
+
+ docStyle.set("boldHeadings", v)} />
+ docStyle.set("sectionRule", v)} />
+
+
+
+
+
Entries
+
+ docStyle.set("boldTitles", v)} />
+ docStyle.set("italicSubtitles", v)} />
+
+
+
+
+
Skills
+
+ docStyle.set("boldSkillLabels", v)} />
+
+
+
+
+
Contact
{COMMON_DIVIDERS.map((d) => (
-
-
-
Section headings
-
- docStyle.set("uppercaseHeadings", v)} />
- docStyle.set("sectionRule", v)} />
- docStyle.set("boldHeadings", v)} />
-
-
-
-
-
Entries & skills
-
- docStyle.set("boldTitles", v)} />
- docStyle.set("italicSubtitles", v)} />
- docStyle.set("italicDates", v)} />
- docStyle.set("boldSkillLabels", v)} />
-
-
diff --git a/src/hooks/useDocStyle.ts b/src/hooks/useDocStyle.ts
index 9478083..2ce19f2 100644
--- a/src/hooks/useDocStyle.ts
+++ b/src/hooks/useDocStyle.ts
@@ -1,5 +1,9 @@
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
+// Section-heading letter case — one mutually-exclusive choice (small caps, as in
+// Jake's \scshape; full ALL CAPS; or plain Title Case), so it's a segmented pick.
+export type HeadingCase = "smallcaps" | "uppercase" | "none";
+
// User-adjustable typography for the HTML resume page (the editor, its read-only
// print mirror, and therefore the "PDF · clean" export). The same values are
// also sent to the LaTeX renderer so `.tex`, PDF preview, and PDF · LaTeX use the
@@ -22,9 +26,8 @@ export type DocStyle = {
boldTitles: boolean; // entry title (role / project / school)
boldHeadings: boolean; // section headings
boldSkillLabels: boolean; // "Languages:" style labels
- italicSubtitles: boolean; // subtitle + location row
- italicDates: boolean; // the right-aligned date / link slot
- uppercaseHeadings: boolean; // force section headings to ALL CAPS (off = small caps, as in the PDF)
+ italicSubtitles: boolean; // subtitle + location/date rows
+ headingCase: HeadingCase; // section-heading letter case (small caps / uppercase / normal)
sectionRule: boolean; // the horizontal rule under each section heading
contactDivider: string; // 1–2 char separator between header contact items
};
@@ -46,14 +49,37 @@ export const DOC_STYLE_DEFAULTS: DocStyle = {
boldHeadings: false,
boldSkillLabels: true,
italicSubtitles: true,
- italicDates: false,
- // Off by default → small-caps headings that match the Jake template's \scshape.
- // The toggle forces ALL CAPS for users who prefer it.
- uppercaseHeadings: false,
+ // Small caps by default → matches the Jake template's \scshape headings.
+ headingCase: "smallcaps",
sectionRule: true,
contactDivider: "|"
};
+// The non-spacing "style" fields the Typography panel owns. Lifted as a type so
+// the "Jake's defaults" reset and applyStyle() stay in sync with the controls.
+export type DocStyleFields = Pick<
+ DocStyle,
+ | "boldTitles"
+ | "boldHeadings"
+ | "boldSkillLabels"
+ | "italicSubtitles"
+ | "headingCase"
+ | "sectionRule"
+ | "contactDivider"
+>;
+
+// The authentic upstream Jake's-Resume look for the style fields — applied by the
+// Typography panel's "Jake's defaults" reset. Spacing/zoom are left untouched.
+export const JAKE_STYLE_DEFAULTS: DocStyleFields = {
+ boldTitles: DOC_STYLE_DEFAULTS.boldTitles,
+ boldHeadings: DOC_STYLE_DEFAULTS.boldHeadings,
+ boldSkillLabels: DOC_STYLE_DEFAULTS.boldSkillLabels,
+ italicSubtitles: DOC_STYLE_DEFAULTS.italicSubtitles,
+ headingCase: DOC_STYLE_DEFAULTS.headingCase,
+ sectionRule: DOC_STYLE_DEFAULTS.sectionRule,
+ contactDivider: DOC_STYLE_DEFAULTS.contactDivider
+};
+
// Single source of truth for the spacing fields that make up a preset (the
// sliders, the built-in presets, and the saved Custom snapshot). The key type
// and the runtime list derive from this one array so they can't drift.
@@ -175,8 +201,15 @@ function coerce(raw: unknown, legacyMode: "current" | "v1" = "current"): DocStyl
boldHeadings: r.boldHeadings === true,
boldSkillLabels: r.boldSkillLabels !== false,
italicSubtitles: r.italicSubtitles !== false,
- italicDates: r.italicDates === true,
- uppercaseHeadings: r.uppercaseHeadings === true,
+ // headingCase replaced the old `uppercaseHeadings` boolean: a stored `true`
+ // migrates to "uppercase"; anything else (including the old `false`) falls to
+ // the small-caps default.
+ headingCase:
+ r.headingCase === "uppercase" || r.headingCase === "none"
+ ? r.headingCase
+ : (r as Record
).uppercaseHeadings === true
+ ? "uppercase"
+ : DOC_STYLE_DEFAULTS.headingCase,
sectionRule: r.sectionRule !== false,
// Cap at 2 chars; fall back to "|" when missing (not when intentionally blank).
contactDivider: typeof r.contactDivider === "string" ? r.contactDivider.slice(0, 2) : DOC_STYLE_DEFAULTS.contactDivider
@@ -253,9 +286,10 @@ export function useDocStyle() {
"--doc-heading-weight": style.boldHeadings ? "700" : "400",
"--doc-skill-label-weight": style.boldSkillLabels ? "700" : "400",
"--doc-subtitle-style": style.italicSubtitles ? "italic" : "normal",
- "--doc-date-style": style.italicDates ? "italic" : "normal",
- // Section headings render small-caps by default; the toggle forces caps.
- "--doc-heading-transform": style.uppercaseHeadings ? "uppercase" : "none",
+ // Heading case: small-caps via font-variant, ALL CAPS via text-transform,
+ // "none" leaves the stored Title Case untouched.
+ "--doc-heading-variant": style.headingCase === "smallcaps" ? "small-caps" : "normal",
+ "--doc-heading-transform": style.headingCase === "uppercase" ? "uppercase" : "none",
"--doc-rule-width": style.sectionRule ? "1px" : "0",
// Quoted so it's a valid CSS `content` token.
"--doc-contact-divider": JSON.stringify(style.contactDivider)
@@ -267,22 +301,10 @@ export function useDocStyle() {
setStyle((current) => ({ ...current, [key]: value }));
}
- // Reset only the typography fields (toggles + contact divider). Zoom and the
- // spacing values are left as-is — the spacing presets already act as a reset
- // for those.
- const TYPOGRAPHY_DEFAULTS = {
- boldTitles: DOC_STYLE_DEFAULTS.boldTitles,
- boldHeadings: DOC_STYLE_DEFAULTS.boldHeadings,
- boldSkillLabels: DOC_STYLE_DEFAULTS.boldSkillLabels,
- italicSubtitles: DOC_STYLE_DEFAULTS.italicSubtitles,
- italicDates: DOC_STYLE_DEFAULTS.italicDates,
- uppercaseHeadings: DOC_STYLE_DEFAULTS.uppercaseHeadings,
- sectionRule: DOC_STYLE_DEFAULTS.sectionRule,
- contactDivider: DOC_STYLE_DEFAULTS.contactDivider
- } satisfies Partial;
-
- function resetTypography() {
- setStyle((current) => ({ ...current, ...TYPOGRAPHY_DEFAULTS }));
+ // Apply a partial set of style fields at once (e.g. the Typography panel's
+ // "Jake's defaults" reset), leaving spacing and zoom untouched.
+ function applyStyle(partial: Partial) {
+ setStyle((current) => ({ ...current, ...partial }));
}
function applySpacingPreset(preset: DocSpacingPreset) {
@@ -300,19 +322,19 @@ export function useDocStyle() {
}
}
- const isTypographyDefault = useMemo(
- () => (Object.keys(TYPOGRAPHY_DEFAULTS) as (keyof typeof TYPOGRAPHY_DEFAULTS)[]).every((k) => style[k] === TYPOGRAPHY_DEFAULTS[k]),
+ const isStyleDefault = useMemo(
+ () => (Object.keys(JAKE_STYLE_DEFAULTS) as (keyof DocStyleFields)[]).every((k) => style[k] === JAKE_STYLE_DEFAULTS[k]),
[style]
);
return {
style,
set,
- resetTypography,
+ applyStyle,
applySpacingPreset,
saveCustomPreset,
customPreset,
- isTypographyDefault,
+ isStyleDefault,
cssVars
};
}
diff --git a/src/styles/index.css b/src/styles/index.css
index fac6962..db24eb3 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -30,12 +30,26 @@
.brand {
display: flex;
- flex-direction: column;
- gap: 2px;
+ align-items: center;
+ gap: var(--s2);
padding-bottom: var(--s3);
border-bottom: 1px solid var(--hairline-soft);
}
+.brand__icon {
+ flex: 0 0 auto;
+ width: 34px;
+ height: 34px;
+ border-radius: var(--r-md);
+}
+
+.brand__text {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
.brand__mark {
font-family: var(--font-serif);
font-size: 1.4rem;
@@ -433,6 +447,58 @@
border-color: var(--accent);
}
+/* Disclosure that hides the 11 fine-grained spacing sliders behind one row. */
+.ctl-disclosure {
+ display: flex;
+ align-items: center;
+ gap: var(--s2);
+ width: 100%;
+ padding: 6px 0;
+ font-size: 0.82rem;
+ font-weight: 600;
+ color: var(--ink-muted);
+ background: none;
+ border: 0;
+ cursor: pointer;
+ transition: color 0.12s var(--ease);
+}
+
+.ctl-disclosure:hover {
+ color: var(--ink-strong);
+}
+
+.ctl-disclosure__chev {
+ flex: 0 0 auto;
+ color: var(--ink-faint);
+ transition: transform 0.15s var(--ease);
+}
+
+.ctl-disclosure.is-open .ctl-disclosure__chev {
+ transform: rotate(90deg);
+}
+
+/* "Edited" tag when the live spacing matches no preset — a hint that there are
+ custom values tucked inside the collapsed disclosure. */
+.ctl-disclosure__hint {
+ margin-left: auto;
+ padding: 1px 7px;
+ font-size: 0.62rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--ink-faint);
+ background: var(--card-elev);
+ border: 1px solid var(--hairline-soft);
+ border-radius: var(--r-pill);
+}
+
+.ctl-disclosure__body {
+ display: flex;
+ flex-direction: column;
+ gap: var(--s3);
+ margin-top: var(--s1);
+}
+
.ctl-divider {
width: 44px;
padding: 4px 6px;
@@ -485,6 +551,31 @@
border-radius: var(--r-pill);
}
+/* Full-width track with equal-width options — used for the 3-option Preset and
+ Case controls, which don't fit beside an inline label in the narrow sidebar. */
+.seg--full {
+ display: flex;
+ width: 100%;
+}
+
+.seg--full .seg__opt {
+ flex: 1 1 0;
+ min-width: 0;
+ text-align: center;
+}
+
+/* Stacked label + full-width segmented control. */
+.ctl-seg-field {
+ display: flex;
+ flex-direction: column;
+ gap: var(--s2);
+}
+
+.ctl-seg-field__label {
+ font-size: 0.82rem;
+ color: var(--ink-muted);
+}
+
.seg__pill {
position: absolute;
top: 2px;
@@ -503,6 +594,7 @@
padding: 4px 12px;
font-size: 0.78rem;
font-weight: 600;
+ white-space: nowrap;
color: var(--ink-soft);
border-radius: var(--r-pill);
transition: color 0.2s ease;
diff --git a/src/styles/resume-document.css b/src/styles/resume-document.css
index 30931ce..ba44cfa 100644
--- a/src/styles/resume-document.css
+++ b/src/styles/resume-document.css
@@ -126,14 +126,16 @@
margin-top: 0.62em;
}
-/* Section heading: small caps with a rule below, matching the PDF's \scshape. */
+/* Section heading: case-driven (small caps / uppercase / normal), with a rule
+ below matching the PDF's \scshape + \titlerule. */
.resume-doc__heading {
margin: 0 0 0.16em;
padding-bottom: 0.05em;
border-bottom: 1px solid #000000;
font-size: 1.02em;
font-weight: 700;
- font-variant: small-caps;
+ font-variant: var(--doc-heading-variant, small-caps);
+ text-transform: var(--doc-heading-transform, none);
letter-spacing: 0.04em;
color: #000000;
}
diff --git a/src/styles/resume-editor.css b/src/styles/resume-editor.css
index f0168b9..5cb18f9 100644
--- a/src/styles/resume-editor.css
+++ b/src/styles/resume-editor.css
@@ -381,14 +381,15 @@
}
/* Section heading: small caps with a rule below, matching the PDF's \scshape.
- Headings are stored in title case ("Education") so CSS small-caps renders the
- first letter as a full capital and the rest as small capitals. The Uppercase
- toggle (--doc-heading-transform: uppercase) still forces ALL CAPS on demand. */
+ Headings are stored in title case ("Education"), so the Case control drives
+ appearance: small-caps (--doc-heading-variant) renders the lowercase run as
+ small capitals, Uppercase (--doc-heading-transform) forces ALL CAPS, and
+ Normal leaves the stored Title Case untouched. */
.rdx-section__heading {
/* \large at the 11pt documentclass = 12pt = 1.096em of \normalsize 10.95pt. */
font-size: 1.1em;
font-weight: var(--doc-heading-weight, 400);
- font-variant: small-caps;
+ font-variant: var(--doc-heading-variant, small-caps);
text-transform: var(--doc-heading-transform, none);
letter-spacing: 0.04em;
color: #000000;
@@ -530,7 +531,7 @@
min-width: 6ch;
text-align: right;
font-weight: 400;
- font-style: var(--doc-date-style, normal);
+ font-style: normal;
color: #000000;
field-sizing: content;
}