From 30859c5e84d7128d314fdd7e153dc6efb5179471 Mon Sep 17 00:00:00 2001 From: Ian Keane Date: Wed, 26 Aug 2026 13:09:47 -0400 Subject: [PATCH] Bunch of behavioral fixes --- generate.scm | 87 +++++++++++++++++++++++++--------- resume.html | 21 ++++++++- resume.scm | 30 ++++++++---- style.css | 131 ++++++++++++++++++++++++++++++--------------------- 4 files changed, 183 insertions(+), 86 deletions(-) diff --git a/generate.scm b/generate.scm index 1f7a9a3..429bb24 100644 --- a/generate.scm +++ b/generate.scm @@ -18,16 +18,20 @@ ;; ─── Individual cards (all become flat children of .columns) ────────────────── +;; Project card — optionally wraps in an anchor if url is set (define (render-project proj) - `(div (@ (class "project-card")) - (div (@ (class "project-header")) - (span (@ (class "project-name")) ,(field proj 'name)) - ,(date-range (field proj 'date-start) (field proj 'date-end))) - (div (@ (class "project-summary")) - ,(bullets (field proj 'summary-bullets))) - (div (@ (class "project-details")) - ,(bullets (field proj 'detail-bullets))) - ,(tech-list (field proj 'technologies)))) + (let* ((url (field proj 'url)) + (card `(div (@ (class ,(string-append "project-card" (if url " clickable" ""))) + ,@(if url `((onclick ,(string-append "window.open('" url "','_blank')"))) '())) + (div (@ (class "project-header")) + (span (@ (class "project-name")) ,(field proj 'name)) + ,(date-range (field proj 'date-start) (field proj 'date-end))) + (div (@ (class "project-summary")) + ,(bullets (field proj 'summary-bullets))) + (div (@ (class "project-details")) + ,(bullets (field proj 'detail-bullets))) + ,(tech-list (field proj 'technologies))))) + card)) ;; Title card: blurb only, no projects nested inside. ;; Projects are emitted as siblings after the title card. @@ -64,6 +68,15 @@ (span (@ (class "interest-title")) ,(field item 'title)) (span (@ (class "interest-description")) ,(field item 'description)))) +(define (render-interests sec) + `(div (@ (class "interests")) + ,@(map render-interest (field sec 'items)))) + +(define (render-favorite-software sec) + `(div (@ (class "favorite-software")) + ,@(map (lambda (s) `(span (@ (class "sw-item")) ,s)) + (field sec 'items)))) + ;; ─── Section → flat list of column children ─────────────────────────────────── (define (section->items sec) @@ -75,37 +88,44 @@ ((hobby-projects) (map render-project (field sec 'projects))) ((interests) - (map render-interest (field sec 'items))) + (list (render-interests sec))) + ((favorite-software) + (list (render-favorite-software sec))) (else '()))) (define (section-label sec) (case (car sec) - ((job) "Work Experience") - ((education) "Education") - ((hobby-projects) "Projects") - ((interests) "Interests") - (else ""))) + ((job) "Work Experience") + ((education) "Education") + ((hobby-projects) "Projects") + ((interests) "Interests") + ((favorite-software) "Favorite Software") + (else ""))) ;; Group consecutive sections of same type, emit one heading per group (define (sections->flat-items sections) (if (null? sections) '() - (let loop ((rest (cdr sections)) - (cur-type (caar sections)) + (let loop ((rest (cdr sections)) + (cur-type (caar sections)) (cur-label (section-label (car sections))) (cur-items (section->items (car sections))) - (acc '())) + (acc '())) (cond ((null? rest) - (append (reverse acc) + ;; flush the last group — acc is already in forward order + (append acc (list `(h2 (@ (class "sec-heading")) ,cur-label)) cur-items)) ((eq? (caar rest) cur-type) - ;; same type: accumulate items without a new heading + ;; same section type: merge items under one heading (loop (cdr rest) cur-type cur-label (append cur-items (section->items (car rest))) acc)) (else - (loop (cdr rest) (caar rest) (section-label (car rest)) + ;; new section type: flush current group into acc, start new + (loop (cdr rest) + (caar rest) + (section-label (car rest)) (section->items (car rest)) (append acc (list `(h2 (@ (class "sec-heading")) ,cur-label)) @@ -118,7 +138,6 @@ (h1 (@ (class "fm-name")) ,(field fm 'name)) (div (@ (class "fm-contact")) (span ,(field fm 'location)) - (span ,(field fm 'phone)) (span ,(field fm 'email))))) ;; ─── Document ───────────────────────────────────────────────────────────────── @@ -132,7 +151,29 @@ (meta (@ (charset "UTF-8"))) (meta (@ (name "viewport") (content "width=device-width, initial-scale=1"))) (title ,(field fm 'name)) - (link (@ (rel "stylesheet") (href "style.css")))) + (link (@ (rel "stylesheet") (href "style.css"))) + ;; Measure the front-matter height after every resize/zoom and + ;; set .columns height to exactly fill the remaining space. + (script " +function setColHeight() { + var fm = document.querySelector('.front-matter'); + var col = document.querySelector('.columns'); + if (!fm || !col) return; + var used = fm.getBoundingClientRect().height + + parseInt(getComputedStyle(document.body).paddingTop) * 2; + col.style.height = (window.innerHeight - used) + 'px'; +} +window.addEventListener('load', setColHeight); +window.addEventListener('resize', setColHeight); +// zoom via ctrl+/- fires resize in most browsers, but also poll as fallback +var _lastW = window.innerWidth, _lastH = window.innerHeight; +setInterval(function() { + if (window.innerWidth !== _lastW || window.innerHeight !== _lastH) { + _lastW = window.innerWidth; _lastH = window.innerHeight; + setColHeight(); + } +}, 200); +")) (body ,(render-front-matter fm) (main (@ (class "columns")) diff --git a/resume.html b/resume.html index db909c4..33f16f8 100644 --- a/resume.html +++ b/resume.html @@ -1,2 +1,21 @@ -Jane Doe

Jane Doe

Springfield, USA(555) 000-0000jane@example.com
Tiny Lisp InterpreterJan 2025 – Mar 2025
  • A 600-line Lisp interpreter in Python supporting tail-call optimisation.
  • Implemented environments as linked frames, closures, and a macro expander.
  • Added a REPL with readline history and pretty-printing.
  • Python
  • Lisp
schemeresumeAug 2026 – Present
  • A resume generator written in Scheme that produces HTML via SXML.
  • Defined a small DSL for resume data in resume.scm.
  • generate.scm walks the data and emits SXML serialised to HTML.
  • CSS handles all layout including the two-column split.
  • Scheme
  • SXML
  • HTML
  • CSS

Projects

Real-Time DashboardFeb 2019 – Dec 2019
  • Delivered a live analytics dashboard processing 50k events/s.
  • Replaced polling with WebSocket fan-out, cutting server load by 60%.
  • Implemented client-side time-series aggregation using D3.js.
  • Added user-configurable alert thresholds stored in IndexedDB.
  • React
  • Node.js
  • WebSockets
  • D3.js
  • AWS
Full-Stack EngineerJul 2017 – Dec 2019
Startup XYZ • Athens, GA
  • Built product features across the React frontend and Node.js backend.
  • Maintained CI/CD pipelines and AWS infrastructure.
  • Collaborated directly with design and product teams.
Rate Limiting LayerFeb 2021 – May 2022
  • Implemented token-bucket rate limiting across all public API endpoints.
  • Built as a gRPC interceptor; config hot-reloaded from the config service.
  • Reduced abusive traffic incidents by 80% in the first quarter after launch.
  • Added per-tenant override rules managed via an internal admin UI.
  • Go
  • gRPC
  • Redis
  • React
Auth Service RewriteJan 2020 – Dec 2020
  • Rewrote legacy PHP auth service in Go; cut p99 latency from 800 ms to 40 ms.
  • Migrated 12 M users with zero downtime using a dual-write/dark-read strategy.
  • Introduced PKCE OAuth 2.0 flow replacing home-grown session tokens.
  • Added comprehensive integration test suite with 92% coverage.
  • Go
  • PostgreSQL
  • Redis
  • OAuth2
  • Docker
Software Engineer IIJan 2020 – May 2022
Acme Corp • Remote
  • Owned the authentication service end-to-end.
  • Reduced API latency by 35% through caching improvements.
Developer PortalJun 2022 – Feb 2023
  • Unified internal service catalogue and runbook search into one portal.
  • Indexed 300+ services via OpenAPI scraping and custom metadata YAML.
  • Built a fuzzy-search backend in Go backed by Bleve.
  • Reduced mean time-to-runbook from ~8 min to <30 s.
  • Go
  • Bleve
  • React
  • OpenAPI
Distributed Config ServiceMar 2023 – Present
  • Built a Raft-based distributed config store used by 40+ services.
  • Designed the consensus layer using a custom Raft implementation in Go.
  • Added multi-region replication with conflict-free merge semantics.
  • Reduced config propagation time from ~30 s to <500 ms p99.
  • Go
  • Raft
  • gRPC
  • Kubernetes
  • Prometheus
Senior Software EngineerJun 2022 – Present
Acme Corp • Remote
  • Led a team of 4 engineers on the core platform.
  • Drove adoption of internal developer tooling.
  • Reviewed architecture proposals and mentored junior engineers.

Work Experience

Bachelor of Science in Computer ScienceAug 2011 – Dec 2016
The University of Georgia • Athens, GA
  • Graduated December 2016
  • ICPC Participant and Meeting Coordinator
  • WUOG 90.5 FM — Radio DJ and Operations Staff Member
  • UGA Game Development Club — founding member
  • Intramural soccer and volleyball

Education

Interests

Open-SourceRegular contributor to Guile and Chicken Scheme ecosystem libraries.
Rock ClimbingSport climber; currently projecting 5.12a outdoors.
Technical WritingBlog at janedoe.dev — compilers, distributed systems, functional programming.
+Jane Doe

Jane Doe

Springfield, USAjane@example.com

Education

Bachelor of Science in Computer ScienceAug 2011 – Dec 2016
The University of Georgia • Athens, GA
  • Graduated December 2016
  • ICPC Participant and Meeting Coordinator
  • WUOG 90.5 FM — Radio DJ and Operations Staff Member
  • UGA Game Development Club — founding member
  • Intramural soccer and volleyball

Work Experience

Senior Software EngineerJun 2022 – Present
Acme Corp • Remote
  • Led a team of 4 engineers on the core platform.
  • Drove adoption of internal developer tooling.
  • Reviewed architecture proposals and mentored junior engineers.
Distributed Config ServiceMar 2023 – Present
  • Built a Raft-based distributed config store used by 40+ services.
  • Designed the consensus layer using a custom Raft implementation in Go.
  • Added multi-region replication with conflict-free merge semantics.
  • Reduced config propagation time from ~30 s to <500 ms p99.
  • Go
  • Raft
  • gRPC
  • Kubernetes
  • Prometheus
Developer PortalJun 2022 – Feb 2023
  • Unified internal service catalogue and runbook search into one portal.
  • Indexed 300+ services via OpenAPI scraping and custom metadata YAML.
  • Built a fuzzy-search backend in Go backed by Bleve.
  • Reduced mean time-to-runbook from ~8 min to <30 s.
  • Go
  • Bleve
  • React
  • OpenAPI
Software Engineer IIJan 2020 – May 2022
Acme Corp • Remote
  • Owned the authentication service end-to-end.
  • Reduced API latency by 35% through caching improvements.
Auth Service RewriteJan 2020 – Dec 2020
  • Rewrote legacy PHP auth service in Go; cut p99 latency from 800 ms to 40 ms.
  • Migrated 12 M users with zero downtime using a dual-write/dark-read strategy.
  • Introduced PKCE OAuth 2.0 flow replacing home-grown session tokens.
  • Added comprehensive integration test suite with 92% coverage.
  • Go
  • PostgreSQL
  • Redis
  • OAuth2
  • Docker
Rate Limiting LayerFeb 2021 – May 2022
  • Implemented token-bucket rate limiting across all public API endpoints.
  • Built as a gRPC interceptor; config hot-reloaded from the config service.
  • Reduced abusive traffic incidents by 80% in the first quarter after launch.
  • Added per-tenant override rules managed via an internal admin UI.
  • Go
  • gRPC
  • Redis
  • React
Full-Stack EngineerJul 2017 – Dec 2019
Startup XYZ • Athens, GA
  • Built product features across the React frontend and Node.js backend.
  • Maintained CI/CD pipelines and AWS infrastructure.
  • Collaborated directly with design and product teams.
Real-Time DashboardFeb 2019 – Dec 2019
  • Delivered a live analytics dashboard processing 50k events/s.
  • Replaced polling with WebSocket fan-out, cutting server load by 60%.
  • Implemented client-side time-series aggregation using D3.js.
  • Added user-configurable alert thresholds stored in IndexedDB.
  • React
  • Node.js
  • WebSockets
  • D3.js
  • AWS

Projects

schemeresumeAug 2026 – Present
  • A resume generator written in Scheme that produces HTML via SXML.
  • Defined a small DSL for resume data in resume.scm.
  • generate.scm walks the data and emits SXML serialised to HTML.
  • CSS handles all layout including the two-column split.
  • Scheme
  • SXML
  • HTML
  • CSS
Tiny Lisp InterpreterJan 2025 – Mar 2025
  • A 600-line Lisp interpreter in Python supporting tail-call optimisation.
  • Implemented environments as linked frames, closures, and a macro expander.
  • Added a REPL with readline history and pretty-printing.
  • Python
  • Lisp

Interests

Open-SourceRegular contributor to Guile and Chicken Scheme ecosystem libraries.
Rock ClimbingSport climber; currently projecting 5.12a outdoors.
Technical WritingBlog at janedoe.dev — compilers, distributed systems, functional programming.

Favorite Software

Guile SchemeEmacsNeovimNixGuixTailscaleWireGuardmpvzathuratmuxAlacrittySyncthingBspwmRofi
diff --git a/resume.scm b/resume.scm index db194e8..1331a45 100644 --- a/resume.scm +++ b/resume.scm @@ -2,21 +2,21 @@ ;; ─── Data constructors ──────────────────────────────────────────────────────── -(define (make-front-matter name email phone location) +(define (make-front-matter name email location) (list 'front-matter (cons 'name name) (cons 'email email) - (cons 'phone phone) (cons 'location location))) -(define (make-project name date-start date-end summary-bullets detail-bullets technologies) +(define (make-project name date-start date-end summary-bullets detail-bullets technologies . rest) (list 'project (cons 'name name) (cons 'date-start date-start) (cons 'date-end date-end) (cons 'summary-bullets summary-bullets) (cons 'detail-bullets detail-bullets) - (cons 'technologies technologies))) + (cons 'technologies technologies) + (cons 'url (if (null? rest) #f (car rest))))) ;; Title card: lives inside a job card ;; blurb – responsibility bullets @@ -62,6 +62,12 @@ (cons 'title title) (cons 'description description))) +;; Favorite software section: flat list of names, displayed in N columns +(define (make-favorite-software heading items) + (list 'favorite-software + (cons 'heading heading) + (cons 'items items))) + (define (make-interests heading items) (list 'interests (cons 'heading heading) @@ -77,7 +83,6 @@ (make-front-matter "Jane Doe" "jane@example.com" - "(555) 000-0000" "Springfield, USA")) (define job-acme @@ -163,13 +168,15 @@ '("Defined a small DSL for resume data in resume.scm." "generate.scm walks the data and emits SXML serialised to HTML." "CSS handles all layout including the two-column split.") - '("Scheme" "SXML" "HTML" "CSS")) + '("Scheme" "SXML" "HTML" "CSS") + "https://github.com/janedoe/schemeresume") (make-project "Tiny Lisp Interpreter" "Jan 2025" "Mar 2025" '("A 600-line Lisp interpreter in Python supporting tail-call optimisation.") '("Implemented environments as linked frames, closures, and a macro expander." "Added a REPL with readline history and pretty-printing.") - '("Python" "Lisp"))))) + '("Python" "Lisp") + "https://github.com/janedoe/tinylisp")))) (define my-interests (make-interests @@ -179,6 +186,12 @@ (make-interest "Rock Climbing" "Sport climber; currently projecting 5.12a outdoors.") (make-interest "Technical Writing" "Blog at janedoe.dev — compilers, distributed systems, functional programming.")))) +(define my-favorite-software + (make-favorite-software + "Favorite Software" + '("Guile Scheme" "Emacs" "Neovim" "Nix" "Guix" "Tailscale" "WireGuard" + "mpv" "zathura" "tmux" "Alacritty" "Syncthing" "Bspwm" "Rofi"))) + ;; Flat ordered section list; CSS handles column placement (define resume (list 'resume @@ -187,4 +200,5 @@ job-acme job-startup my-hobby-projects - my-interests)))) + my-interests + my-favorite-software)))) diff --git a/style.css b/style.css index 2ebd4f1..5b3938f 100644 --- a/style.css +++ b/style.css @@ -16,7 +16,7 @@ } /* ─── Base ───────────────────────────────────────────────────────────────────── */ -html { font-size: 13px; } +html, body { height: 100%; margin: 0; } body { font-family: var(--font); @@ -25,12 +25,10 @@ body { line-height: 1.54; background: var(--bg); color: var(--text); - padding: 32px 36px; - max-width: 1000px; - margin: 0 auto; - /* Don't let the page grow beyond one screen */ - height: 100vh; - overflow: hidden; + padding: 28px 32px; + box-sizing: border-box; + overflow-y: auto; + overflow-x: hidden; } ul { list-style: none; } @@ -39,9 +37,11 @@ a { color: var(--accent); text-decoration: none; } /* ─── Front matter ───────────────────────────────────────────────────────────── */ .front-matter { text-align: center; - padding-bottom: 16px; - margin-bottom: 16px; + padding-bottom: 14px; + margin-bottom: 14px; border-bottom: 1px solid var(--border); + flex-shrink: 0; + width: 100%; } .fm-name { @@ -64,45 +64,30 @@ a { color: var(--accent); text-decoration: none; } margin: 0 6px; } -/* ─── Columns: flex column-wrap ──────────────────────────────────────────────── */ -/* - * All cards and headings are direct children of .columns. - * flex-flow: column wrap makes them stack vertically, and once they - * exceed the container height they wrap into the next column — - * exactly like text wrapping in a newspaper column. - * Any card, at any level, can be the wrap point. - * - * max-height is the remaining viewport after front-matter. - * The front-matter is roughly 80px tall with the padding above. - * JS sets this precisely via --col-height, but the CSS default is close. - */ +/* ─── Column layout ──────────────────────────────────────────────────────────── */ .columns { - display: flex; - flex-flow: column wrap; - align-content: space-between; - max-height: calc(100vh - 130px); - gap: 0; + column-width: 22em; + column-gap: 2em; + column-rule: 1px solid var(--border); + column-fill: balance; } -/* Every direct child is half the container width */ -.columns > * { - width: calc(50% - 20px); +/* Prevent any card from being split across a column boundary */ +.sec-heading, +.title-card, +.project-card, +.education-card, +.interests, +.favorite-software { + break-inside: avoid; } -/* Dividing rule: pseudo-element positioned in the middle */ -.columns::before { - content: ""; - position: absolute; - left: 50%; - top: 0; - bottom: 0; - width: 1px; - background: var(--border); - pointer-events: none; +/* Keep each section heading attached to whatever follows it */ +.sec-heading { + break-after: avoid; } -.columns { position: relative; } -/* Section headings */ +/* ─── Section headings ───────────────────────────────────────────────────────── */ .sec-heading { font-size: 0.7rem; font-weight: 800; @@ -180,6 +165,14 @@ a { color: var(--accent); text-decoration: none; } border-left-color: var(--accent); } +.project-card.clickable { cursor: pointer; } +.project-card.clickable:hover .project-name::after { + content: " ↗"; + font-size: 0.7rem; + color: var(--accent); + opacity: 0.7; +} + .project-name { font-size: 0.83rem; } .project-summary ul { padding-left: 12px; margin-top: 2px; } @@ -261,20 +254,50 @@ a { color: var(--accent); text-decoration: none; } } .edu-extras li::before { content: "- "; } -/* ─── Interests ──────────────────────────────────────────────────────────────── */ -.interest-card { - font-size: 0.82rem; - line-height: 1.54; - padding: 2px 0; +/* ─── Interests — 2-column micro-card grid ───────────────────────────────────── */ +.interests { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; +} + +.interest-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 3px; + padding: 5px 8px; +} +.interest-title { + display: block; + font-weight: 700; + font-size: 0.78rem; + color: var(--text); +} +.interest-description { + display: block; + font-size: 0.73rem; + color: var(--muted); + line-height: 1.4; +} + +/* ─── Favorite software — 4-column tag grid ─────────────────────────────────── */ +.favorite-software { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 4px; +} + +.sw-item { + background: var(--tag-bg); + color: var(--tag-text); + font-size: 0.72rem; + padding: 3px 6px; + border-radius: 2px; + text-align: center; } -.interest-title { font-weight: 700; } -.interest-title::after { content: ": "; color: var(--muted); font-weight: normal; } -.interest-description { color: var(--muted); } /* ─── Responsive ─────────────────────────────────────────────────────────────── */ -@media (max-width: 640px) { - body { height: auto; overflow: auto; padding: 20px 16px; } - .columns { flex-flow: column nowrap; max-height: none; } - .columns > * { width: 100%; } - .columns::before { display: none; } +@media (max-width: 400px) { + html, body { height: auto; overflow: auto; } + .columns { column-width: auto; columns: 1; } }