From ad956b19ca2c604ca35b59d7c8e6825efda38225 Mon Sep 17 00:00:00 2001 From: Jack Howard Date: Mon, 15 Dec 2025 17:04:36 -0700 Subject: [PATCH 1/6] refactor: redesign landing page with Material Design 3 theme system and interactive color picker --- _includes/styles.css | 340 +++++++++++++++++++++++++++++++++++++------ index.njk | 158 ++++++++++++++++++++ 2 files changed, 452 insertions(+), 46 deletions(-) diff --git a/_includes/styles.css b/_includes/styles.css index e608cc4..049fd35 100644 --- a/_includes/styles.css +++ b/_includes/styles.css @@ -1,74 +1,322 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + :root { - /* setting color scheme for light/dark text */ color-scheme: light dark; - --light-color: #ffcfb8; - --dark-color: #113134; + --md-primary: #6750a4; + --md-primary-light: #8777b9; + --md-primary-dark: #4f3c89; + --md-surface: #ffffff; + --md-surface-container: #f3edf7; + --md-surface-container-high: #ece6f0; + --md-on-surface: #1c1b1f; + --md-on-surface-variant: #49454f; + --md-outline: #79747e; + --md-background: #fef7ff; } html { - --primary-color: var(--dark-color); - --secondary-color: var(--light-color); + font-family: 'Roboto', -apple-system, system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif; + background: var(--md-background); + min-height: 100vh; + transition: background 0.3s ease; +} - background-color: var(--primary-color); - text-align: center; +body { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + overscroll-behavior: none; +} + +.theme-picker { position: fixed; - width: 100%; - height: 100%; - margin: 0; - padding: 0; - /* system specific font */ - font-family: -apple-system, system-ui, "Helvetica Neue", "Helvetica", "Arial", - sans-serif; + top: 20px; + right: 20px; + background: var(--md-surface-container-high); + backdrop-filter: blur(10px); + border-radius: 16px; + padding: 16px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 3px 1px rgba(0, 0, 0, 0.15); + z-index: 1000; + max-width: 200px; + transform: translateX(0); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; + opacity: 1; } -@media (prefers-color-scheme: light) { - html { - --primary-color: var(--light-color); - --secondary-color: var(--dark-color); - } +.theme-picker.hidden { + transform: translateX(calc(100% + 20px)); + opacity: 0; + pointer-events: none; } -body { - margin: 0; - padding: 0; - overscroll-behavior: none; +.theme-fab { + position: fixed; + bottom: 24px; + right: 24px; + width: 56px; + height: 56px; + background: var(--md-primary); + border: none; + border-radius: 16px; + color: white; + cursor: pointer; + box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.3); + z-index: 999; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + font-size: 24px; } -main { - margin: 0; - height: 90vh; +.theme-fab:hover { + background: var(--md-primary-light); + box-shadow: 0 2px 6px 2px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.theme-fab:active { + transform: scale(0.95); +} + +.theme-fab .material-icons { + font-size: 24px; +} + +.theme-picker h3 { + font-size: 14px; + font-weight: 500; + color: var(--md-on-surface); + margin-bottom: 12px; + text-align: left; +} + +.color-input-group { display: flex; - flex-direction: column; + gap: 8px; + align-items: center; + margin-bottom: 12px; +} + +.color-input { + width: 48px; + height: 48px; + border: 2px solid #e0e0e0; + border-radius: 50%; + cursor: pointer; + transition: transform 0.2s ease; +} + +.color-input:hover { + transform: scale(1.1); +} + +.color-label { + font-size: 12px; + color: var(--md-on-surface-variant); +} + +.preset-label { + font-size: 11px; + color: var(--md-outline); + margin-bottom: 8px; + text-align: left; +} + +.preset-colors { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; +} + +.preset-btn { + width: 40px; + height: 40px; + border: none; + border-radius: 50%; + cursor: pointer; + transition: transform 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.preset-btn:hover { + transform: scale(1.15); +} + +main { + width: 100%; + max-width: 480px; + background: var(--md-surface); + border-radius: 28px; + padding: 48px 32px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 2px 6px 2px rgba(0, 0, 0, 0.15); + text-align: center; +} + +.avatar { + width: 88px; + height: 88px; + background: var(--md-primary); + border-radius: 50%; + display: inline-flex; + align-items: center; justify-content: center; - padding: 0 10vw 0 10vw; + font-size: 40px; + font-weight: 500; + color: white; + margin-bottom: 24px; + transition: background 0.3s ease; + box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.15); } h1 { - font-size: 5vmax; - margin: 0; - padding: 0 0 1rem 0; + font-size: 32px; + font-weight: 400; + color: var(--md-on-surface); + letter-spacing: 0; + margin-bottom: 8px; } h2 { - font-size: 3vmax; - margin: 0; - padding: 0 0 1rem 0; + font-size: 16px; + font-weight: 400; + color: var(--md-on-surface-variant); + margin-bottom: 32px; + line-height: 24px; } a { - padding: 1rem; - font-size: 3vmax; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + height: 56px; + border-radius: 100px; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.1px; + text-decoration: none; + transition: all 0.2s cubic-bezier(0.2, 0, 0, 1); + margin-bottom: 12px; +} - /* setting text color of links to match other canvastext */ - color: var(--canvastext); - text-align: center; - margin: 1rem 0; - /* seems to cause the border I want via apple */ - border-radius: 980px; - border: 5px solid var(--secondary-color); +a:first-of-type { + background: var(--md-primary); + color: white; + border: none; + box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.3); +} + +a:first-of-type:hover, +a:first-of-type:focus { + background: var(--md-primary-light); + box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.15), 0 2px 6px 2px rgba(0, 0, 0, 0.15); +} + +a:last-of-type { + background: transparent; + color: var(--md-primary); + border: 1px solid var(--md-outline); +} + +a:last-of-type:hover, +a:last-of-type:focus { + background: rgba(103, 80, 164, 0.08); +} + +a:active { + transform: scale(0.98); +} + +.material-icons { + font-size: 18px; +} + +@media (max-width: 640px) { + .theme-picker { + top: 10px; + right: 10px; + padding: 12px; + } + + .theme-fab { + bottom: 16px; + right: 16px; + } + + main { + padding: 40px 24px; + } + + h1 { + font-size: 28px; + } } -a:hover, a:focus { - font-weight: bold; - border-style: dashed; +@media (prefers-color-scheme: dark) { + :root { + --md-primary: #d0bcff; + --md-primary-light: #eaddff; + --md-primary-dark: #9a82db; + --md-surface: #1c1b1f; + --md-surface-container: #211f26; + --md-surface-container-high: #2b2930; + --md-on-surface: #e6e1e5; + --md-on-surface-variant: #cac4d0; + --md-outline: #938f99; + --md-background: #141218; + } + + .theme-picker { + background: var(--md-surface-container-high); + } + + .theme-picker h3 { + color: var(--md-on-surface); + } + + .color-label { + color: var(--md-on-surface-variant); + } + + .preset-label { + color: var(--md-outline); + } + + .theme-fab { + background: var(--md-primary); + color: #381e72; + } + + .theme-fab:hover { + background: var(--md-primary-light); + } + + main { + background: var(--md-surface); + } + + h1 { + color: var(--md-on-surface); + } + + h2 { + color: var(--md-on-surface-variant); + } + + a:first-of-type { + background: var(--md-primary); + color: #381e72; + } + + a:last-of-type { + color: var(--md-primary); + } } \ No newline at end of file diff --git a/index.njk b/index.njk index 78570c6..b39eae6 100644 --- a/index.njk +++ b/index.njk @@ -8,6 +8,7 @@ metaCanonicalURL: "https://jackhowa.com" # content contentTitle: Jack Howard +contentTitleAbbreviation: JH contentSubtitle: Principal Software Engineer at Expel firstLinkURL: https://github.com/JackHowa firstLinkTitle: GitHub @@ -40,12 +41,169 @@ secondLinkTitle: AI & React talk + + + +
+
{{contentTitleAbbreviation}}

{{contentTitle}}

{{contentSubtitle}}

{{firstLinkTitle}} {{secondLinkTitle}}
+ + From eefdb9a3bb11bf01d49d477766a5b9d5946e311c Mon Sep 17 00:00:00 2001 From: Jack Howard Date: Mon, 15 Dec 2025 17:15:37 -0700 Subject: [PATCH 2/6] refactor: improve color picker UX with enhanced theme generation and system preference detection --- _includes/styles.css | 2 +- index.njk | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/_includes/styles.css b/_includes/styles.css index 049fd35..5d58782 100644 --- a/_includes/styles.css +++ b/_includes/styles.css @@ -124,7 +124,7 @@ body { .preset-label { font-size: 11px; - color: var(--md-outline); + color: var(--md-on-surface-variant); margin-bottom: 8px; text-align: left; } diff --git a/index.njk b/index.njk index b39eae6..5d029b4 100644 --- a/index.njk +++ b/index.njk @@ -47,7 +47,7 @@ secondLinkTitle: AI & React talk

Theme Color

- Pick + Pick Any Color 🌈
Presets:
@@ -135,11 +135,11 @@ secondLinkTitle: AI & React talk primary: baseColor, primaryLight: `hsl(${hsl.h}, ${hsl.s}%, ${Math.min(lightness + 15, 85)}%)`, primaryDark: `hsl(${hsl.h}, ${hsl.s}%, ${Math.max(lightness - 15, 15)}%)`, - background: `hsl(${hsl.h}, ${Math.min(hsl.s * 0.8, 100)}%, 99%)`, - surface: '#ffffff', - surfaceContainer: `hsl(${hsl.h}, ${Math.min(hsl.s * 0.6, 100)}%, 96%)`, - surfaceContainerHigh: `hsl(${hsl.h}, ${Math.min(hsl.s * 0.6, 100)}%, 94%)`, - outlinedHover: `hsla(${hsl.h}, ${hsl.s}%, ${lightness}%, 0.08)` + background: `hsl(${hsl.h}, ${Math.min(hsl.s * 1.2, 100)}%, 95%)`, + surface: `hsl(${hsl.h}, ${Math.min(hsl.s * 0.6, 100)}%, 95%)`, + surfaceContainer: `hsl(${hsl.h}, ${Math.min(hsl.s * 0.8, 100)}%, 92%)`, + surfaceContainerHigh: `hsl(${hsl.h}, ${Math.min(hsl.s * 0.8, 100)}%, 88%)`, + outlinedHover: `hsla(${hsl.h}, ${hsl.s}%, ${lightness}%, 0.12)` }; } } @@ -203,6 +203,11 @@ secondLinkTitle: AI & React talk }); applyTheme(colorPicker.value); + + // Re-apply theme when system color scheme changes + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + applyTheme(colorPicker.value); + }); From 915026ad35116f58755ca71c67a0507b0b97ff20 Mon Sep 17 00:00:00 2001 From: Jack Howard Date: Mon, 15 Dec 2025 17:16:54 -0700 Subject: [PATCH 3/6] refactor: remove Roboto font from font stack, use system fonts only --- _includes/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_includes/styles.css b/_includes/styles.css index 5d58782..308b3ee 100644 --- a/_includes/styles.css +++ b/_includes/styles.css @@ -19,7 +19,7 @@ } html { - font-family: 'Roboto', -apple-system, system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: -apple-system, system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif; background: var(--md-background); min-height: 100vh; transition: background 0.3s ease; From d819692275de141d28203ae2da2fb09c9b605ca5 Mon Sep 17 00:00:00 2001 From: Jack Howard Date: Mon, 15 Dec 2025 17:26:55 -0700 Subject: [PATCH 4/6] refactor: enhance Content-Security-Policy to allow inline scripts and self-hosted styles --- netlify.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netlify.toml b/netlify.toml index 04686f3..e576041 100644 --- a/netlify.toml +++ b/netlify.toml @@ -10,7 +10,7 @@ X-Frame-Options = "DENY" X-XSS-Protection = "1; mode=block" X-Content-Type-Options = "nosniff" - Content-Security-Policy = "default-src 'self'; style-src 'unsafe-inline';" + Content-Security-Policy = "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';" # Multi-value headers are expressed with multi-line strings. cache-control = ''' From 97c32b61819d63f5549d2d540ac59be3b24f3a42 Mon Sep 17 00:00:00 2001 From: Jack Howard Date: Mon, 15 Dec 2025 17:36:14 -0700 Subject: [PATCH 5/6] refactor: add dynamic on-primary color calculation for improved contrast in theme system --- _includes/styles.css | 8 +++++--- index.njk | 12 +++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/_includes/styles.css b/_includes/styles.css index 308b3ee..64ac704 100644 --- a/_includes/styles.css +++ b/_includes/styles.css @@ -16,6 +16,7 @@ --md-on-surface-variant: #49454f; --md-outline: #79747e; --md-background: #fef7ff; + --md-on-primary: #ffffff; } html { @@ -169,7 +170,7 @@ main { justify-content: center; font-size: 40px; font-weight: 500; - color: white; + color: var(--md-on-primary); margin-bottom: 24px; transition: background 0.3s ease; box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.15); @@ -209,7 +210,7 @@ a { a:first-of-type { background: var(--md-primary); - color: white; + color: var(--md-on-primary); border: none; box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.3); } @@ -272,6 +273,7 @@ a:active { --md-on-surface-variant: #cac4d0; --md-outline: #938f99; --md-background: #141218; + --md-on-primary: #381e72; } .theme-picker { @@ -313,7 +315,7 @@ a:active { a:first-of-type { background: var(--md-primary); - color: #381e72; + color: var(--md-on-primary); } a:last-of-type { diff --git a/index.njk b/index.njk index 5d029b4..3b31336 100644 --- a/index.njk +++ b/index.njk @@ -120,15 +120,19 @@ secondLinkTitle: AI & React talk const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { + const primaryL = Math.min(lightness + 40, 90); return { - primary: `hsl(${hsl.h}, ${hsl.s}%, ${Math.min(lightness + 40, 90)}%)`, + primary: `hsl(${hsl.h}, ${hsl.s}%, ${primaryL}%)`, primaryLight: `hsl(${hsl.h}, ${Math.min(hsl.s + 10, 100)}%, ${Math.min(lightness + 50, 95)}%)`, primaryDark: `hsl(${hsl.h}, ${hsl.s}%, ${Math.min(lightness + 20, 75)}%)`, background: `hsl(${hsl.h}, ${Math.max(hsl.s - 30, 10)}%, 8%)`, surface: `hsl(${hsl.h}, ${Math.max(hsl.s - 20, 10)}%, 11%)`, surfaceContainer: `hsl(${hsl.h}, ${Math.max(hsl.s - 20, 10)}%, 13%)`, surfaceContainerHigh: `hsl(${hsl.h}, ${Math.max(hsl.s - 20, 10)}%, 17%)`, - outlinedHover: `hsla(${hsl.h}, ${hsl.s}%, ${lightness + 40}%, 0.08)` + outlinedHover: `hsla(${hsl.h}, ${hsl.s}%, ${lightness + 40}%, 0.08)`, + // If primary lightness > 60%, uses dark text (same hue at 20% lightness) + // Otherwise uses white text + onPrimary: primaryL > 60 ? `hsl(${hsl.h}, ${hsl.s}%, 20%)` : '#ffffff' }; } else { return { @@ -139,7 +143,8 @@ secondLinkTitle: AI & React talk surface: `hsl(${hsl.h}, ${Math.min(hsl.s * 0.6, 100)}%, 95%)`, surfaceContainer: `hsl(${hsl.h}, ${Math.min(hsl.s * 0.8, 100)}%, 92%)`, surfaceContainerHigh: `hsl(${hsl.h}, ${Math.min(hsl.s * 0.8, 100)}%, 88%)`, - outlinedHover: `hsla(${hsl.h}, ${hsl.s}%, ${lightness}%, 0.12)` + outlinedHover: `hsla(${hsl.h}, ${hsl.s}%, ${lightness}%, 0.12)`, + onPrimary: lightness > 60 ? `hsl(${hsl.h}, ${hsl.s}%, 20%)` : '#ffffff' }; } } @@ -155,6 +160,7 @@ secondLinkTitle: AI & React talk root.style.setProperty('--md-surface', scheme.surface); root.style.setProperty('--md-surface-container', scheme.surfaceContainer); root.style.setProperty('--md-surface-container-high', scheme.surfaceContainerHigh); + root.style.setProperty('--md-on-primary', scheme.onPrimary); document.body.parentElement.style.background = scheme.background; From b11648dafd754f8e12005fbf167431e98b489d95 Mon Sep 17 00:00:00 2001 From: Jack Howard Date: Mon, 15 Dec 2025 17:54:27 -0700 Subject: [PATCH 6/6] refactor: update default theme color from purple to orange with adjusted Material Design 3 color palette --- _includes/styles.css | 14 +++++++------- index.njk | 7 +++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/_includes/styles.css b/_includes/styles.css index 64ac704..368bb72 100644 --- a/_includes/styles.css +++ b/_includes/styles.css @@ -6,9 +6,9 @@ :root { color-scheme: light dark; - --md-primary: #6750a4; - --md-primary-light: #8777b9; - --md-primary-dark: #4f3c89; + --md-primary: #ea6a00; + --md-primary-light: #ff8a33; + --md-primary-dark: #b85200; --md-surface: #ffffff; --md-surface-container: #f3edf7; --md-surface-container-high: #ece6f0; @@ -263,9 +263,9 @@ a:active { @media (prefers-color-scheme: dark) { :root { - --md-primary: #d0bcff; - --md-primary-light: #eaddff; - --md-primary-dark: #9a82db; + --md-primary: #ffb68a; + --md-primary-light: #ffd0b0; + --md-primary-dark: #ea6a00; --md-surface: #1c1b1f; --md-surface-container: #211f26; --md-surface-container-high: #2b2930; @@ -273,7 +273,7 @@ a:active { --md-on-surface-variant: #cac4d0; --md-outline: #938f99; --md-background: #141218; - --md-on-primary: #381e72; + --md-on-primary: #4a2800; } .theme-picker { diff --git a/index.njk b/index.njk index 3b31336..63ef066 100644 --- a/index.njk +++ b/index.njk @@ -46,7 +46,8 @@ secondLinkTitle: AI & React talk