0.2, updated UI
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 2m44s
Details
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 2m44s
Details
This commit is contained in:
parent
c2b80047ae
commit
f8acc55f67
|
|
@ -0,0 +1,5 @@
|
|||
shots/
|
||||
before/
|
||||
before.json
|
||||
before.txt
|
||||
compare.html
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
{
|
||||
"mode": "redesign",
|
||||
"depth": "full",
|
||||
"scope": "page",
|
||||
"shape": "Workbench",
|
||||
"theme": "studio",
|
||||
"axes": "light/roman-serif/warm",
|
||||
"hero": "Stage",
|
||||
"nav": "edge-aligned",
|
||||
"footer": "letter close",
|
||||
"imagery": "the product",
|
||||
"photos": [],
|
||||
"entry": "app.py",
|
||||
"fingerprint": "asymmetric-grid/serif-display/proof-sheet/hairline-set/colophon-close/restrained",
|
||||
"sections": [
|
||||
"Hero (Stage)",
|
||||
"Bench notes (Set)",
|
||||
"Colophon (Footer)"
|
||||
],
|
||||
"signature": "crop-marked proof sheet with orange metric construction guides",
|
||||
"waived": [],
|
||||
"prompt": "Use the hallmark skill to give this web app a professional clean look for a more hip modern dad looking for a joke",
|
||||
"date": "2026-10-30"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"scanned": "2026-10-29",
|
||||
"findings": {
|
||||
"fonts": "System sans (-apple-system, BlinkMacSystemFont, Segoe UI, Roboto) + Georgia serif quote mark (app.py L43, L174)",
|
||||
"palette": "Hardcoded hex literals (#ff0844, #ff4b2b, #ffdd00, #00d2ff, #0052d4, Slate #0f172a..#f8fafc) in inline <style> (app.py L45-L270)",
|
||||
"motion": "CSS keyframes (gradientShift, floatOrbs, spin) + transition: all (app.py L47, L62, L213, L241)",
|
||||
"spacing": "Ad-hoc pixel values (24px, 44px, 38px, 26px, 14px, 28px) (app.py L42-L275)",
|
||||
"framework": "Flask 3.0.0 with inline render_template_string (app.py L4, L24)"
|
||||
},
|
||||
"preserve": [
|
||||
"Flask routes (/ and /api/joke)",
|
||||
"Ollama integration and version.ini metadata",
|
||||
"DOM hooks (#refreshBtn, #btnText, #spinner, #joke, fetchNewJoke())"
|
||||
],
|
||||
"introduce": [
|
||||
"Hallmark token system",
|
||||
"Editorial theme & typography",
|
||||
"Accessible focus, reduced-motion, and control states"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
3aca28a0
|
||||
812
app.py
812
app.py
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import configparser
|
||||
import requests
|
||||
from flask import Flask, render_template_string, jsonify
|
||||
from flask import Flask, render_template_string, jsonify, send_from_directory
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
|
@ -26,320 +26,652 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Programming Dad Jokes</title>
|
||||
<meta name="description" content="Dry programming dad jokes set on a typographic studio proof sheet, pulled live from your local Ollama model." />
|
||||
<title>Programming Dad Joke</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Hedvig+Letters+Serif:opsz@12..24&family=Geist:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/* Hallmark · shape: Workbench (draw 9cbbc9) · genre: editorial · theme: Studio (deal 3/3, seed programming-dad-joke-generator-254f·3aca28a0·2026-10-30·r0) · redesign: full
|
||||
* hero: Stage · nav: edge-aligned · footer: letter close · imagery: the product · signature: crop-marked proof sheet with orange metric construction guides
|
||||
* axes: light/roman-serif/warm · posture: restrained · contrast: pass · fold: pass · v2.0.0 */
|
||||
/* Hallmark · pre-emit critique: P5 H5 E5 S4 R5 V5 · weakest: S, generic joke card could fit any trivia app; framed the joke as a crop-marked Studio proof sheet with live typographic metric guides */
|
||||
/* Hallmark · reworded "Tell Me Another Joke! 🚀" to "Pull another joke" · 30a bans emoji in controls */
|
||||
/* Hallmark · reworded "Freshly compiled humor from our AI model" to "Dry humor for engineers after hours, pulled live from your local model." · 19 names generic AI filler */
|
||||
/* Hallmark · dropped "Ollama • Gemma 4" · 54 and 63 ban middle-dot eyebrow badges */
|
||||
@import url("./tokens.css");
|
||||
@import url("./base.css");
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
/* Vibrant animated gradient with bright red, yellow, and blue */
|
||||
background: linear-gradient(-45deg, #ff0844 0%, #ff4b2b 20%, #ffdd00 45%, #00d2ff 75%, #0052d4 100%);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 14s ease infinite;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Ambient glowing gradient orbs for depth */
|
||||
body::before,
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.65;
|
||||
animation: floatOrbs 10s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
body::before {
|
||||
width: 380px;
|
||||
height: 380px;
|
||||
background: radial-gradient(circle, #ff0055 0%, #ffaa00 100%);
|
||||
top: -80px;
|
||||
left: -80px;
|
||||
}
|
||||
|
||||
body::after {
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
background: radial-gradient(circle, #00d2ff 0%, #0044ff 100%);
|
||||
bottom: -100px;
|
||||
right: -100px;
|
||||
animation-delay: -5s;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
@keyframes floatOrbs {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
100% { transform: translate(40px, 40px) scale(1.1); }
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 28px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.85);
|
||||
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.22),
|
||||
0 0 35px rgba(255, 255, 255, 0.35);
|
||||
padding: 44px 38px;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 16px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #1e293b;
|
||||
background: linear-gradient(90deg, rgba(255, 75, 43, 0.15), rgba(255, 221, 0, 0.25), rgba(0, 210, 255, 0.2));
|
||||
border: 1px solid rgba(255, 75, 43, 0.3);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 8px #10b981;
|
||||
}
|
||||
|
||||
.badge-divider {
|
||||
opacity: 0.4;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.joke-box {
|
||||
background: #f8fafc;
|
||||
border-radius: 18px;
|
||||
padding: 26px 24px;
|
||||
margin-bottom: 30px;
|
||||
position: relative;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.joke-box::before {
|
||||
content: '"';
|
||||
.skip {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 18px;
|
||||
font-size: 3.5rem;
|
||||
line-height: 1;
|
||||
font-family: Georgia, serif;
|
||||
color: #ff0844;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
left: var(--space-md);
|
||||
top: calc(-1 * var(--space-3xl));
|
||||
z-index: var(--z-toast);
|
||||
padding: var(--space-xs) var(--space-md);
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.joke-text {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.7;
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
min-height: 80px;
|
||||
.skip:focus-visible {
|
||||
top: var(--space-md);
|
||||
}
|
||||
|
||||
.proof {
|
||||
background-image: radial-gradient(circle at 1px 1px, var(--color-dot) 1px, transparent 1.4px);
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
.crop {
|
||||
position: relative;
|
||||
--l: 8px;
|
||||
--o: 12px;
|
||||
}
|
||||
|
||||
.crop::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: calc(-1 * var(--o));
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(var(--color-mark), var(--color-mark)) 0 var(--o) / var(--l) 1px no-repeat,
|
||||
linear-gradient(var(--color-mark), var(--color-mark)) var(--o) 0 / 1px var(--l) no-repeat,
|
||||
linear-gradient(var(--color-mark), var(--color-mark)) 100% var(--o) / var(--l) 1px no-repeat,
|
||||
linear-gradient(var(--color-mark), var(--color-mark)) calc(100% - var(--o)) 0 / 1px var(--l) no-repeat,
|
||||
linear-gradient(var(--color-mark), var(--color-mark)) 0 calc(100% - var(--o)) / var(--l) 1px no-repeat,
|
||||
linear-gradient(var(--color-mark), var(--color-mark)) var(--o) 100% / 1px var(--l) no-repeat,
|
||||
linear-gradient(var(--color-mark), var(--color-mark)) 100% calc(100% - var(--o)) / var(--l) 1px no-repeat,
|
||||
linear-gradient(var(--color-mark), var(--color-mark)) calc(100% - var(--o)) 100% / 1px var(--l) no-repeat;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
border-bottom: var(--rule-hair) solid var(--color-rule);
|
||||
padding-block: var(--space-md);
|
||||
}
|
||||
|
||||
.site-header__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.wordmark {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--display-weight);
|
||||
letter-spacing: var(--tracking-display);
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wordmark:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.site-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-lg);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-ink-2);
|
||||
}
|
||||
|
||||
.site-nav a {
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.site-nav a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding-block: var(--space-3xl);
|
||||
border-bottom: var(--rule-hair) solid var(--color-rule);
|
||||
}
|
||||
|
||||
.hero__grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
|
||||
gap: var(--gap-columns);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero__lead {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.hero__lead .lead {
|
||||
color: var(--color-ink-2);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 24px;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.btn {
|
||||
position: relative;
|
||||
appearance: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
padding: 14px 28px;
|
||||
border-radius: 14px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #ff0844 0%, #ff8c00 40%, #0099ff 100%);
|
||||
background-size: 200% 200%;
|
||||
box-shadow: 0 10px 25px rgba(255, 8, 68, 0.35);
|
||||
transition: all 0.25s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
gap: var(--space-xs);
|
||||
min-height: 48px;
|
||||
padding: 0 var(--space-lg);
|
||||
border: var(--rule-hair) solid var(--color-ink);
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--color-paper-hi);
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-md);
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--dur-short) var(--ease-out), transform var(--dur-micro) var(--ease-out);
|
||||
}
|
||||
|
||||
.btn::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: var(--space-3xs);
|
||||
border: var(--rule-hair) solid var(--color-mark);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-accent-ink);
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 14px 30px rgba(0, 153, 255, 0.45);
|
||||
background-position: 100% 100%;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.65;
|
||||
.btn:focus-visible {
|
||||
outline: 2px solid var(--color-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn:disabled,
|
||||
.btn[aria-disabled="true"] {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn[aria-busy="true"] {
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.35);
|
||||
border-top-color: #ffffff;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--color-mark);
|
||||
border-top-color: var(--color-ink);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
animation: spin var(--dur-long) linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 0.82rem;
|
||||
color: #94a3b8;
|
||||
.sheet {
|
||||
background: var(--color-paper-hi);
|
||||
border: var(--rule-hair) solid var(--color-rule);
|
||||
border-radius: var(--radius-card);
|
||||
box-shadow: var(--shadow-sheet);
|
||||
padding: var(--space-xl);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
padding-top: 18px;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.meta span {
|
||||
display: inline-flex;
|
||||
.guide-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.meta code {
|
||||
background: #f1f5f9;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.78rem;
|
||||
color: #475569;
|
||||
.guide-line {
|
||||
flex: 1;
|
||||
height: var(--rule-hair);
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.card {
|
||||
padding: 30px 20px;
|
||||
.guide-readout {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
color: var(--color-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.joke-specimen {
|
||||
margin: 0;
|
||||
padding-block: var(--space-md);
|
||||
border-top: var(--rule-hair) solid var(--color-accent);
|
||||
border-bottom: var(--rule-hair) solid var(--color-accent);
|
||||
}
|
||||
|
||||
.joke-text {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--display-weight);
|
||||
line-height: 1.4;
|
||||
letter-spacing: var(--tracking-display);
|
||||
color: var(--color-ink);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
min-height: var(--space-3xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: opacity var(--dur-short) var(--ease-out);
|
||||
}
|
||||
|
||||
.sheet[data-loading="true"] .joke-text {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.proof-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.proof-progress__meta {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.proof-progress__track {
|
||||
position: relative;
|
||||
height: var(--space-2xs);
|
||||
background: var(--color-paper-3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.proof-progress__fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--color-accent);
|
||||
transform-origin: left center;
|
||||
transform: scaleX(var(--progress, 0));
|
||||
transition: transform var(--dur-short) var(--ease-out);
|
||||
}
|
||||
|
||||
.section--notes {
|
||||
padding-block: var(--section-gap-s);
|
||||
}
|
||||
|
||||
.section__head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
|
||||
gap: var(--gap-columns);
|
||||
align-items: end;
|
||||
margin-bottom: var(--space-2xl);
|
||||
}
|
||||
|
||||
.section__head p {
|
||||
color: var(--color-ink-2);
|
||||
}
|
||||
|
||||
.set {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-top: var(--rule-hair) solid var(--color-ink);
|
||||
counter-reset: rule-counter;
|
||||
}
|
||||
|
||||
.set__row {
|
||||
counter-increment: rule-counter;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
|
||||
gap: var(--gap-columns);
|
||||
align-items: baseline;
|
||||
padding-block: var(--space-lg);
|
||||
border-bottom: var(--rule-hair) solid var(--color-rule);
|
||||
}
|
||||
|
||||
.set__row h3::before {
|
||||
content: "0" counter(rule-counter) " ";
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-muted);
|
||||
margin-right: var(--space-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.set__row p {
|
||||
color: var(--color-ink-2);
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
background: var(--color-paper-2);
|
||||
color: var(--color-ink);
|
||||
border-top: var(--rule-hair) solid var(--color-ink);
|
||||
padding-block: var(--space-2xl);
|
||||
}
|
||||
|
||||
.footer__head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-xl);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer__head p {
|
||||
color: var(--color-ink-2);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.colophon-grid {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--gap-columns);
|
||||
border-top: var(--rule-hair) solid var(--color-rule);
|
||||
padding-top: var(--space-lg);
|
||||
}
|
||||
|
||||
.colophon-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2xs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.colophon-item dt {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-ink-2);
|
||||
}
|
||||
|
||||
.colophon-item dd {
|
||||
margin: 0;
|
||||
font-size: var(--text-md);
|
||||
color: var(--color-ink);
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 60rem) {
|
||||
.hero__grid,
|
||||
.section__head,
|
||||
.set__row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
|
||||
.colophon-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.joke-text {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
.colophon-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sheet {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.btn {
|
||||
transition: background-color var(--dur-short) linear;
|
||||
}
|
||||
|
||||
.joke-text,
|
||||
.proof-progress__fill {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<main class="card">
|
||||
<div class="badge">
|
||||
<span class="badge-dot"></span>
|
||||
<span>Ollama • Gemma 4</span>
|
||||
<span class="badge-divider">•</span>
|
||||
<span>v{{ version }}</span>
|
||||
</div>
|
||||
<h1>Programming Dad Joke</h1>
|
||||
<p class="subtitle">Freshly compiled humor from our AI model</p>
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
|
||||
<div class="joke-box">
|
||||
<div id="joke" class="joke-text">{{ joke }}</div>
|
||||
</div>
|
||||
<header class="site-header proof">
|
||||
<div class="container site-header__inner">
|
||||
<a href="#main" class="wordmark" aria-label="Programming Dad Joke home">Proof Sheet</a>
|
||||
<nav class="site-nav" aria-label="Page sections">
|
||||
<a href="#specimen">Specimen</a>
|
||||
<a href="#notes">Bench notes</a>
|
||||
<a href="#colophon">Colophon</a>
|
||||
<span class="num">v{{ version }}</span>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="actions">
|
||||
<button id="refreshBtn" class="btn" onclick="fetchNewJoke()">
|
||||
<span class="spinner" id="spinner"></span>
|
||||
<span id="btnText">Tell Me Another Joke! 🚀</span>
|
||||
</button>
|
||||
</div>
|
||||
<main id="main">
|
||||
<section class="hero proof">
|
||||
<div class="container hero__grid">
|
||||
<div class="hero__lead">
|
||||
<h1>Programming Dad Joke</h1>
|
||||
<p class="lead">Dry humor for engineers after hours, pulled live from your local model.</p>
|
||||
<div class="actions">
|
||||
<button id="refreshBtn" class="btn btn--primary crop" type="button" onclick="fetchNewJoke()">
|
||||
<span class="spinner keep-motion" id="spinner" aria-hidden="true"></span>
|
||||
<span id="btnText">Pull another joke</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<span>Host: <code>{{ host }}</code></span>
|
||||
<span>Model: <code>{{ model }}</code></span>
|
||||
<span>Prompt: <code>"{{ prompt }}"</code></span>
|
||||
<span>App Version: <code>v{{ version }}</code></span>
|
||||
<div id="specimen" class="hero__stage">
|
||||
<article id="jokeSheet" class="sheet crop" aria-live="polite" aria-atomic="true">
|
||||
<div class="guide-bar" aria-hidden="true">
|
||||
<span class="guide-line"></span>
|
||||
<span class="guide-readout num">Hedvig Letters Serif, 25pt / 1.4</span>
|
||||
</div>
|
||||
<blockquote class="joke-specimen">
|
||||
<p id="joke" class="joke-text">{{ joke }}</p>
|
||||
</blockquote>
|
||||
<div id="proofProgress" class="proof-progress" hidden>
|
||||
<div class="proof-progress__meta">
|
||||
<span id="progressStatus">Setting type on the press (30 to 50s typical)…</span>
|
||||
<span id="progressReadout" class="num">0% (0s)</span>
|
||||
</div>
|
||||
<div
|
||||
id="progressBar"
|
||||
class="proof-progress__track"
|
||||
role="progressbar"
|
||||
aria-label="Joke generation progress"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="0"
|
||||
>
|
||||
<div id="progressFill" class="proof-progress__fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guide-bar" aria-hidden="true">
|
||||
<span class="guide-readout num">Proofed on {{ model }}</span>
|
||||
<span class="guide-line"></span>
|
||||
<span class="guide-readout num">Release v{{ version }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section--notes" id="notes">
|
||||
<div class="container">
|
||||
<div class="section__head">
|
||||
<h2>Three rules on the press</h2>
|
||||
<p class="lead">Every groaner on the sheet clears three checks before it leaves the studio.</p>
|
||||
</div>
|
||||
<ol class="set">
|
||||
<li class="set__row">
|
||||
<h3>Pun on the literal</h3>
|
||||
<p>Take a noun every developer says ten times before lunch, such as cache, branch, or deadlock, and read it as plain kitchen English.</p>
|
||||
</li>
|
||||
<li class="set__row">
|
||||
<h3>Keep the setup short</h3>
|
||||
<p>One question across the counter. If the premise needs a whiteboard diagram, the punchline already missed its train.</p>
|
||||
</li>
|
||||
<li class="set__row">
|
||||
<h3>Aim for the eye roll</h3>
|
||||
<p>A proper dad joke never chases an applause break. It settles for a quiet sigh from across the table.</p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer" id="colophon">
|
||||
<div class="container">
|
||||
<div class="footer__head">
|
||||
<h2>Press colophon</h2>
|
||||
<p>Compiled on demand from your local inference runtime.</p>
|
||||
</div>
|
||||
<dl class="colophon-grid">
|
||||
<div class="colophon-item">
|
||||
<dt>Host</dt>
|
||||
<dd class="num">{{ host }}</dd>
|
||||
</div>
|
||||
<div class="colophon-item">
|
||||
<dt>Model</dt>
|
||||
<dd class="num">{{ model }}</dd>
|
||||
</div>
|
||||
<div class="colophon-item">
|
||||
<dt>Prompt</dt>
|
||||
<dd>“{{ prompt }}”</dd>
|
||||
</div>
|
||||
<div class="colophon-item">
|
||||
<dt>App Version</dt>
|
||||
<dd class="num">v{{ version }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
let progressTimer = null;
|
||||
|
||||
function computeExpectedProgress(elapsedSeconds) {
|
||||
if (elapsedSeconds <= 0) return 0.02;
|
||||
if (elapsedSeconds <= 30) {
|
||||
return 0.02 + (elapsedSeconds / 30) * 0.66;
|
||||
}
|
||||
if (elapsedSeconds <= 50) {
|
||||
return 0.68 + ((elapsedSeconds - 30) / 20) * 0.24;
|
||||
}
|
||||
return 0.92 + 0.06 * (1 - Math.exp(-(elapsedSeconds - 50) / 20));
|
||||
}
|
||||
|
||||
function setProgressState(ratio, elapsedSeconds, statusText, isComplete) {
|
||||
const progressBar = document.getElementById("progressBar");
|
||||
const progressFill = document.getElementById("progressFill");
|
||||
const progressReadout = document.getElementById("progressReadout");
|
||||
const progressStatus = document.getElementById("progressStatus");
|
||||
|
||||
const clamped = Math.max(0, Math.min(1, ratio));
|
||||
const pct = Math.round(clamped * 100);
|
||||
const secs = isComplete
|
||||
? Math.max(0.1, elapsedSeconds).toFixed(1)
|
||||
: String(Math.max(0, Math.floor(elapsedSeconds)));
|
||||
|
||||
progressFill.style.setProperty("--progress", clamped.toFixed(4));
|
||||
progressBar.setAttribute("aria-valuenow", String(pct));
|
||||
progressReadout.textContent = pct + "% (" + secs + "s)";
|
||||
if (statusText) {
|
||||
progressStatus.textContent = statusText;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNewJoke() {
|
||||
const btn = document.getElementById("refreshBtn");
|
||||
const btnText = document.getElementById("btnText");
|
||||
const spinner = document.getElementById("spinner");
|
||||
const jokeEl = document.getElementById("joke");
|
||||
const sheet = document.getElementById("jokeSheet");
|
||||
const progressWrap = document.getElementById("proofProgress");
|
||||
const progressFill = document.getElementById("progressFill");
|
||||
|
||||
if (progressTimer) {
|
||||
clearInterval(progressTimer);
|
||||
progressTimer = null;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.setAttribute("aria-busy", "true");
|
||||
spinner.style.display = "inline-block";
|
||||
btnText.textContent = "Compiling humor...";
|
||||
btnText.textContent = "Pulling fresh proof\u2026";
|
||||
|
||||
sheet.setAttribute("data-loading", "true");
|
||||
progressWrap.hidden = false;
|
||||
|
||||
progressFill.style.transition = "none";
|
||||
setProgressState(0, 0, "Setting type on the press (30 to 50s typical)\u2026", false);
|
||||
void progressFill.offsetWidth;
|
||||
progressFill.style.transition = "";
|
||||
|
||||
const startTime = performance.now();
|
||||
setProgressState(0.02, 0, "Setting type on the press (30 to 50s typical)\u2026", false);
|
||||
|
||||
progressTimer = setInterval(() => {
|
||||
const elapsed = (performance.now() - startTime) / 1000;
|
||||
const ratio = computeExpectedProgress(elapsed);
|
||||
const status =
|
||||
elapsed < 30
|
||||
? "Setting type on the press (30 to 50s typical)\u2026"
|
||||
: elapsed <= 50
|
||||
? "Inking the proof sheet (30 to 50s window)\u2026"
|
||||
: "Finishing slow impression\u2026";
|
||||
setProgressState(ratio, elapsed, status, false);
|
||||
}, 100);
|
||||
|
||||
let nextJoke = "";
|
||||
try {
|
||||
const response = await fetch("/api/joke");
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP error " + response.status);
|
||||
}
|
||||
const data = await response.json();
|
||||
jokeEl.textContent = data.joke || "No joke returned.";
|
||||
nextJoke = data.joke || "No joke returned.";
|
||||
} catch (err) {
|
||||
console.error("Failed to load joke:", err);
|
||||
jokeEl.textContent = "Oops! Could not fetch a new joke right now. Please try again.";
|
||||
nextJoke = "Could not reach the joke service right now. Check the Ollama host and try again.";
|
||||
} finally {
|
||||
if (progressTimer) {
|
||||
clearInterval(progressTimer);
|
||||
progressTimer = null;
|
||||
}
|
||||
|
||||
const totalElapsed = (performance.now() - startTime) / 1000;
|
||||
setProgressState(1, totalElapsed, "Proof complete (30 to 50s typical)", true);
|
||||
jokeEl.textContent = nextJoke;
|
||||
sheet.removeAttribute("data-loading");
|
||||
|
||||
btn.disabled = false;
|
||||
btn.removeAttribute("aria-busy");
|
||||
spinner.style.display = "none";
|
||||
btnText.textContent = "Tell Me Another Joke! 🚀";
|
||||
btnText.textContent = "Pull another joke";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -375,6 +707,14 @@ def index():
|
|||
version=version
|
||||
)
|
||||
|
||||
@app.route("/tokens.css")
|
||||
def tokens_css():
|
||||
return send_from_directory(os.path.dirname(os.path.abspath(__file__)), "tokens.css", mimetype="text/css")
|
||||
|
||||
@app.route("/base.css")
|
||||
def base_css():
|
||||
return send_from_directory(os.path.dirname(os.path.abspath(__file__)), "base.css", mimetype="text/css")
|
||||
|
||||
@app.route("/api/joke")
|
||||
def api_joke():
|
||||
joke = request_joke()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { overflow-x: clip; }
|
||||
html { scrollbar-color: var(--color-muted) var(--color-paper); scroll-padding-top: var(--space-3xl); }
|
||||
body { margin: 0; background: var(--color-paper); color: var(--color-ink); font-family: var(--font-body); font-size: var(--text-md); line-height: 1.55; -webkit-font-smoothing: antialiased; caret-color: var(--color-accent); }
|
||||
::selection { background: color-mix(in oklab, var(--color-accent) 22%, transparent); color: var(--color-ink); }
|
||||
:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
|
||||
a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.16em; }
|
||||
h1, h2, h3 { margin: 0; font-family: var(--font-display); font-weight: var(--display-weight, 700); font-synthesis: none; letter-spacing: var(--tracking-display); line-height: 1.05; overflow-wrap: break-word; min-width: 0; text-wrap: balance; }
|
||||
h1 { max-width: 20ch; font-size: var(--text-display); } h2 { max-width: 30ch; font-size: var(--text-section); } h3 { max-width: 40ch; font-size: var(--text-title); line-height: 1.2; }
|
||||
.lead { font-size: var(--text-lead); line-height: 1.45; max-width: 52ch; }
|
||||
p { margin: 0; text-wrap: pretty; }
|
||||
p:not([class]) { max-width: 65ch; }
|
||||
img, video { max-width: 100%; height: auto; display: block; }
|
||||
svg { display: block; }
|
||||
[hidden] { display: none !important; }
|
||||
button, input, select, textarea { font: inherit; }
|
||||
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); box-sizing: border-box; }
|
||||
.num, .figure, td.num { font-variant-numeric: tabular-nums; }
|
||||
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||
[data-slot] { position: relative; overflow: hidden; background: linear-gradient(160deg, var(--color-paper-2, var(--color-paper)), var(--color-paper-3, var(--color-paper))); }
|
||||
[data-slot] > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1; }
|
||||
[data-slot]:has(> img) > :not(img):not([data-keep]) { display: none !important; }
|
||||
@media (max-width: 60rem) { :root { --gutter: 24px; --section-gap-s: 80px; --section-gap: 96px; --section-gap-l: 120px; } }
|
||||
@media (max-width: 40rem) { :root { --gutter: 16px; --section-gap-s: 56px; --section-gap: 72px; --section-gap-l: 96px; --gap-columns: 16px; } }
|
||||
@media (prefers-reduced-motion: reduce) { *:not(.keep-motion), *:not(.keep-motion)::before, *:not(.keep-motion)::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
:root {
|
||||
color-scheme: light;
|
||||
--space-3xs: 2px; --space-2xs: 4px; --space-xs: 8px; --space-sm: 12px; --space-md: 16px; --space-lg: 24px; --space-xl: 40px; --space-2xl: 64px; --space-3xl: 96px; --space-4xl: 144px;
|
||||
--text-xs: 0.8125rem; --text-sm: 0.875rem; --text-md: 1rem; --text-lg: 1.25rem; --text-xl: 1.5625rem; --text-2xl: 1.953rem; --text-3xl: 2.441rem;
|
||||
--text-display-s: clamp(2.25rem, 4vw + 0.5rem, 3.75rem); --text-display: clamp(2.75rem, 5vw + 1rem, 5.25rem);
|
||||
--text-section: clamp(2rem, 2.5vw + 1rem, 3.5rem); --text-title: clamp(1.25rem, 0.75vw + 0.875rem, 1.625rem); --text-lead: clamp(1.125rem, 0.35vw + 1rem, 1.375rem);
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-in: cubic-bezier(0.7, 0, 0.84, 0); --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
--dur-micro: 120ms; --dur-short: 220ms; --dur-long: 420ms;
|
||||
--color-error: oklch(52% 0.19 27); --color-success: oklch(50% 0.12 150);
|
||||
--container: 1440px; --gutter: 64px; --gap-columns: 32px; --section-gap-s: 120px; --section-gap: 144px; --section-gap-l: 176px;
|
||||
--z-base: 1; --z-raised: 10; --z-dropdown: 100; --z-sticky: 200; --z-sticky-nav: 300; --z-modal: 400; --z-toast: 500; --z-tooltip: 600;
|
||||
--color-paper: oklch(97.2% 0.006 215); --color-paper-2: oklch(94% 0.009 215); --color-paper-3: oklch(90.5% 0.011 215); --color-paper-hi: oklch(99% 0.004 215);
|
||||
--color-ink: oklch(16% 0.032 250); --color-ink-2: oklch(34% 0.022 245); --color-muted: oklch(49% 0.018 240);
|
||||
--color-rule: oklch(84% 0.010 225);
|
||||
--color-accent: oklch(68% 0.172 50); --color-accent-ink: oklch(16% 0.032 250); --color-focus: oklch(52% 0.15 48);
|
||||
--color-dot: color-mix(in oklab, var(--color-ink) 20%, transparent); --color-mark: color-mix(in oklab, var(--color-ink) 72%, transparent);
|
||||
--font-display: "Hedvig Letters Serif", ui-serif, Georgia, serif; --font-body: "Geist", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-outlier: "Sofia Sans Extra Condensed", "Arial Narrow", ui-sans-serif, sans-serif;
|
||||
--radius-card: 3px; --radius-pill: 3px; --rule-hair: 1px;
|
||||
--shadow-card: none;
|
||||
--shadow-sheet: 1px 2px 0 color-mix(in oklab, var(--color-ink) 7%, transparent), 10px 18px 36px -14px color-mix(in oklab, var(--color-ink) 32%, transparent);
|
||||
--tracking-display: -0.022em; --display-weight: 400;
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
[metadata]
|
||||
version = 0.1
|
||||
version = 0.2
|
||||
|
|
|
|||
Loading…
Reference in New Issue