From 343e342d52f6426c215f453f6286803f9678014c Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 15 Dec 2025 18:27:52 -0800 Subject: [PATCH] Add periodic table visualization and URL hash tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace subway map with periodic table of commands - Info panel shows command details on hover - Click element to jump to command in Commands section - URL hash updates as you scroll between sections - Direct linking to sections works - Fix spacing issues in solution section 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- biome.json | 2 + bun.lock | 3 + package.json | 3 +- public/app.js | 5 +- public/css/main.css | 17 +- public/index.html | 79 +--- public/js/components/art-gallery.js | 2 + public/js/components/framework-viz.js | 357 +++++++++++++++++++ public/js/components/glass-terminal.js | 2 + public/js/components/lens.js | 2 + public/js/demo-renderer.js | 2 + public/js/demos/commands/animate.js | 2 + public/js/demos/commands/bolder.js | 2 + public/js/demos/commands/index.js | 2 + public/js/demos/commands/normalize.js | 2 + public/js/demos/index.js | 2 + public/js/demos/skills/color-and-contrast.js | 2 + public/js/demos/skills/index.js | 2 + public/js/demos/skills/interaction-design.js | 2 + public/js/demos/skills/motion-design.js | 2 + public/js/demos/skills/responsive-design.js | 2 + public/js/demos/skills/spatial-design.js | 2 + public/js/demos/skills/typography.js | 2 + public/js/demos/skills/ux-writing.js | 2 + public/js/effects/liquid-canvas.js | 2 + public/js/utils/reveal.js | 2 + public/js/utils/scroll.js | 53 +++ 27 files changed, 479 insertions(+), 78 deletions(-) create mode 100644 public/js/components/framework-viz.js diff --git a/biome.json b/biome.json index c488ed2d3..701261b50 100644 --- a/biome.json +++ b/biome.json @@ -6,3 +6,5 @@ } } } + + diff --git a/bun.lock b/bun.lock index d7218b068..c20a134b0 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "lenis": "^1.3.16", "motion": "^12.23.26", "tailwindcss": "^4.1.18", + "three": "^0.182.0", }, "devDependencies": { "bun-plugin-tailwind": "^0.1.2", @@ -53,6 +54,8 @@ "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + "three": ["three@0.182.0", "", {}, "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], } } diff --git a/package.json b/package.json index f88f090d3..70f0951c3 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "dependencies": { "lenis": "^1.3.16", "motion": "^12.23.26", - "tailwindcss": "^4.1.18" + "tailwindcss": "^4.1.18", + "three": "^0.182.0" } } diff --git a/public/app.js b/public/app.js index d02e35f59..8bba49dbe 100644 --- a/public/app.js +++ b/public/app.js @@ -3,9 +3,10 @@ import { renderTerminalLayout, } from "./js/components/glass-terminal.js"; import { initLensEffect } from "./js/components/lens.js"; +import { initFrameworkViz } from "./js/components/framework-viz.js"; import { initHeroEffect } from "./js/effects/liquid-canvas.js"; import { initScrollReveal } from "./js/utils/reveal.js"; -import { initScrollIndicator, initSmoothScroll } from "./js/utils/scroll.js"; +import { initScrollIndicator, initSmoothScroll, initHashTracking } from "./js/utils/scroll.js"; // ============================================ // STATE @@ -118,10 +119,12 @@ document.addEventListener("click", (e) => { function init() { initSmoothScroll(); initScrollIndicator(); + initHashTracking(); initHeroEffect(); initLensEffect(); initScrollReveal(); initGlassTerminal(); + initFrameworkViz(); loadContent(); document.body.classList.add("loaded"); diff --git a/public/css/main.css b/public/css/main.css index 88a55d380..ca36ec8af 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -647,7 +647,11 @@ code { .solution-content { display: grid; - gap: var(--spacing-xl); + gap: var(--spacing-lg); +} + +.solution-content .section-lead { + margin-bottom: 0; } /* Two-pillar visual layout */ @@ -665,6 +669,17 @@ code { } } +/* Interactive Framework Viz - Periodic Table */ +.solution-visual-interactive { + width: 100%; + min-height: 380px; + background: var(--color-paper); + border: 1px solid var(--color-mist); + border-radius: 8px; + position: relative; + overflow: hidden; +} + .solution-pillar { background: var(--color-cream); border: 1px solid var(--color-mist); diff --git a/public/index.html b/public/index.html index b3ef09444..7c3b57a80 100644 --- a/public/index.html +++ b/public/index.html @@ -15,7 +15,8 @@ { "imports": { "lenis": "https://cdn.jsdelivr.net/npm/lenis@1.3.16/+esm", - "motion": "https://cdn.jsdelivr.net/npm/motion@12.23.26/+esm" + "motion": "https://cdn.jsdelivr.net/npm/motion@12.23.26/+esm", + "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js" } } @@ -180,80 +181,8 @@

One comprehensive skill with deep expertise, plus 15 commands that form the language of design.

-
- -
-
- - - - - -

One Skill

-

Infinite expertise, progressive loading

-
-
-
- Frontend Design - Comprehensive design intelligence -
-
- Reference domains: -
-
- Typography - Color - Spatial - Responsive - Interaction - Motion - UX Writing -
-
-
- - -
- + -
- - -
-
- - - - - -

15 Commands

-

The language of design

-
-
-
- Diagnose - /audit -
-
- Calibrate - /bolder - /quieter - /simplify -
-
- Enhance - /animate - /colorize - /delight -
-
- Align - /normalize - /polish - /optimize -
-
+5 more commands
-
-
+
+
diff --git a/public/js/components/art-gallery.js b/public/js/components/art-gallery.js index ee0485472..bbe2a005e 100644 --- a/public/js/components/art-gallery.js +++ b/public/js/components/art-gallery.js @@ -131,3 +131,5 @@ function setupInteractions() { }); }); } + + diff --git a/public/js/components/framework-viz.js b/public/js/components/framework-viz.js new file mode 100644 index 000000000..e0e78f0bb --- /dev/null +++ b/public/js/components/framework-viz.js @@ -0,0 +1,357 @@ +/** + * Periodic Table of Commands + * Clean grid visualization showing all commands organized by category + */ + +import { commandCategories, commandRelationships } from '../data.js'; + +const categoryColors = { + diagnostic: { bg: '#fdf4ff', border: '#d946ef', text: '#a21caf' }, + quality: { bg: '#f0fdf4', border: '#22c55e', text: '#15803d' }, + intensity: { bg: '#fffbeb', border: '#f59e0b', text: '#b45309' }, + adaptation: { bg: '#eff6ff', border: '#3b82f6', text: '#1d4ed8' }, + enhancement: { bg: '#fdf2f8', border: '#ec4899', text: '#be185d' }, + system: { bg: '#f9fafb', border: '#6b7280', text: '#374151' } +}; + +const categoryLabels = { + diagnostic: 'Diagnostic', + quality: 'Quality', + intensity: 'Intensity', + adaptation: 'Adaptation', + enhancement: 'Enhancement', + system: 'System' +}; + +// Short symbols for each command (like element symbols) +const commandSymbols = { + audit: 'Au', + normalize: 'No', + polish: 'Po', + optimize: 'Op', + harden: 'Ha', + clarify: 'Cl', + simplify: 'Si', + adapt: 'Ad', + extract: 'Ex', + animate: 'An', + colorize: 'Co', + delight: 'De', + bolder: 'Bo', + quieter: 'Qu', + onboard: 'On' +}; + +// Atomic numbers (just for visual interest) +const commandNumbers = { + audit: 1, + normalize: 2, + polish: 3, + optimize: 4, + harden: 5, + clarify: 6, + simplify: 7, + adapt: 8, + extract: 9, + animate: 10, + colorize: 11, + delight: 12, + bolder: 13, + quieter: 14, + onboard: 15 +}; + +export class PeriodicTable { + constructor(container) { + this.container = container; + this.infoPanel = null; + this.activeElement = null; + this.init(); + } + + init() { + this.container.innerHTML = ''; + this.container.style.cssText = ` + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px; + height: 100%; + box-sizing: border-box; + `; + + this.renderTable(); + this.renderInfoPanel(); + } + + renderTable() { + // Group commands by category + const groups = {}; + Object.entries(commandCategories).forEach(([cmd, cat]) => { + if (!groups[cat]) groups[cat] = []; + groups[cat].push(cmd); + }); + + // Category order + const categoryOrder = ['diagnostic', 'quality', 'adaptation', 'enhancement', 'intensity', 'system']; + + // Create grid container + const grid = document.createElement('div'); + grid.style.cssText = ` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 16px; + flex: 1; + `; + + categoryOrder.forEach(cat => { + const commands = groups[cat]; + if (!commands) return; + + const group = this.createCategoryGroup(cat, commands); + grid.appendChild(group); + }); + + this.container.appendChild(grid); + } + + renderInfoPanel() { + this.infoPanel = document.createElement('div'); + this.infoPanel.style.cssText = ` + background: var(--color-cream); + border: 1px solid var(--color-mist); + border-radius: 8px; + padding: 16px 20px; + height: 96px; + display: flex; + align-items: center; + gap: 20px; + flex-shrink: 0; + overflow: hidden; + `; + + // Default state + this.showDefaultInfo(); + + this.container.appendChild(this.infoPanel); + } + + showDefaultInfo() { + this.infoPanel.innerHTML = ` +
+ Hover over a command to see details +
+ `; + } + + showCommandInfo(cmd) { + const category = commandCategories[cmd]; + const colors = categoryColors[category]; + const rel = commandRelationships[cmd] || {}; + + // Normalize to arrays (pairs can be string or array) + const toArray = (val) => { + if (!val) return []; + if (Array.isArray(val)) return val; + return [val]; + }; + + const pairs = toArray(rel.pairs); + const leadsTo = toArray(rel.leadsTo); + const combinesWith = toArray(rel.combinesWith); + + // Build related info + let relatedHtml = ''; + + if (pairs.length > 0) { + const list = pairs.map(p => `/${p}`).join(', '); + relatedHtml += `Pairs with: ${list}`; + } + if (combinesWith.length > 0) { + const list = combinesWith.map(p => `/${p}`).join(', '); + if (relatedHtml) relatedHtml += '•'; + relatedHtml += `Combines with: ${list}`; + } + if (leadsTo.length > 0) { + const list = leadsTo.map(p => `/${p}`).join(', '); + if (relatedHtml) relatedHtml += '•'; + relatedHtml += `Then: ${list}`; + } + + this.infoPanel.innerHTML = ` +
+
+ /${cmd} + ${categoryLabels[category]} +
+
${rel.flow || 'Design command'}
+ ${relatedHtml ? `
${relatedHtml}
` : ''} +
+ `; + } + + createCategoryGroup(category, commands) { + const colors = categoryColors[category]; + + const group = document.createElement('div'); + group.style.cssText = ` + display: flex; + flex-direction: column; + gap: 6px; + `; + + // Category label + const label = document.createElement('div'); + label.style.cssText = ` + font-family: var(--font-body); + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + color: ${colors.text}; + padding-left: 2px; + `; + label.textContent = categoryLabels[category]; + group.appendChild(label); + + // Elements row + const row = document.createElement('div'); + row.style.cssText = ` + display: flex; + flex-wrap: wrap; + gap: 6px; + `; + + commands.forEach(cmd => { + const element = this.createElement(cmd, category); + row.appendChild(element); + }); + + group.appendChild(row); + return group; + } + + createElement(cmd, category) { + const colors = categoryColors[category]; + + const el = document.createElement('div'); + el.style.cssText = ` + width: 56px; + height: 64px; + background: ${colors.bg}; + border: 1.5px solid ${colors.border}; + border-radius: 5px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease; + position: relative; + `; + + // Atomic number + const number = document.createElement('div'); + number.style.cssText = ` + position: absolute; + top: 3px; + left: 5px; + font-family: var(--font-mono); + font-size: 7px; + color: ${colors.text}; + opacity: 0.5; + `; + number.textContent = commandNumbers[cmd]; + el.appendChild(number); + + // Symbol + const symbol = document.createElement('div'); + symbol.style.cssText = ` + font-family: var(--font-display); + font-size: 20px; + font-weight: 500; + color: ${colors.text}; + line-height: 1; + `; + symbol.textContent = commandSymbols[cmd]; + el.appendChild(symbol); + + // Command name + const name = document.createElement('div'); + name.style.cssText = ` + font-family: var(--font-mono); + font-size: 8px; + color: ${colors.text}; + opacity: 0.7; + margin-top: 3px; + `; + name.textContent = `/${cmd}`; + el.appendChild(name); + + // Hover effects + el.addEventListener('mouseenter', () => { + // Visual feedback + el.style.transform = 'translateY(-2px)'; + el.style.boxShadow = `0 4px 12px ${colors.border}40`; + + // Update info panel + this.showCommandInfo(cmd); + + // Track active element + if (this.activeElement) { + this.activeElement.style.transform = 'translateY(0)'; + this.activeElement.style.boxShadow = 'none'; + } + this.activeElement = el; + }); + + el.addEventListener('mouseleave', () => { + el.style.transform = 'translateY(0)'; + el.style.boxShadow = 'none'; + }); + + // Click to scroll to command + el.addEventListener('click', () => { + const target = document.getElementById(`cmd-${cmd}`); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }); + + return el; + } +} + +export function initFrameworkViz() { + const container = document.getElementById('framework-viz-container'); + if (container) { + new PeriodicTable(container); + } +} diff --git a/public/js/components/glass-terminal.js b/public/js/components/glass-terminal.js index 4bc46d56f..8a0fce3b7 100644 --- a/public/js/components/glass-terminal.js +++ b/public/js/components/glass-terminal.js @@ -181,3 +181,5 @@ function updateTerminal(cmd, container, allCommands) { }); } } + + diff --git a/public/js/components/lens.js b/public/js/components/lens.js index 6e1fcdfb8..38ef79c04 100644 --- a/public/js/components/lens.js +++ b/public/js/components/lens.js @@ -11,3 +11,5 @@ export function initLensEffect() { maxPosition: 95 }); } + + diff --git a/public/js/demo-renderer.js b/public/js/demo-renderer.js index 2925452f0..d7805a059 100644 --- a/public/js/demo-renderer.js +++ b/public/js/demo-renderer.js @@ -125,3 +125,5 @@ export function setupDemoTabs() { }); }); } + + diff --git a/public/js/demos/commands/animate.js b/public/js/demos/commands/animate.js index bbcdebe6d..a5a31906e 100644 --- a/public/js/demos/commands/animate.js +++ b/public/js/demos/commands/animate.js @@ -39,3 +39,5 @@ export default { ` }; + + diff --git a/public/js/demos/commands/bolder.js b/public/js/demos/commands/bolder.js index 267f871c0..8c455211e 100644 --- a/public/js/demos/commands/bolder.js +++ b/public/js/demos/commands/bolder.js @@ -19,3 +19,5 @@ export default {
` }; + + diff --git a/public/js/demos/commands/index.js b/public/js/demos/commands/index.js index 131c459ee..ac2df7f53 100644 --- a/public/js/demos/commands/index.js +++ b/public/js/demos/commands/index.js @@ -37,3 +37,5 @@ export const commandDemos = { export function getCommandDemo(commandId) { return commandDemos[commandId] || null; } + + diff --git a/public/js/demos/commands/normalize.js b/public/js/demos/commands/normalize.js index 8aa2c78f8..6167aa370 100644 --- a/public/js/demos/commands/normalize.js +++ b/public/js/demos/commands/normalize.js @@ -37,3 +37,5 @@ export default {
` }; + + diff --git a/public/js/demos/index.js b/public/js/demos/index.js index 5bdfe8a5c..cad39aa70 100644 --- a/public/js/demos/index.js +++ b/public/js/demos/index.js @@ -1,3 +1,5 @@ // Main demos registry - exports all demos export { commandDemos, getCommandDemo } from "./commands/index.js"; export { getSkillDemo, skillDemos } from "./skills/index.js"; + + diff --git a/public/js/demos/skills/color-and-contrast.js b/public/js/demos/skills/color-and-contrast.js index 0f503755e..d5cd23be8 100644 --- a/public/js/demos/skills/color-and-contrast.js +++ b/public/js/demos/skills/color-and-contrast.js @@ -63,3 +63,5 @@ export default { } ] }; + + diff --git a/public/js/demos/skills/index.js b/public/js/demos/skills/index.js index 107b339db..ae935e15c 100644 --- a/public/js/demos/skills/index.js +++ b/public/js/demos/skills/index.js @@ -21,3 +21,5 @@ export const skillDemos = { export function getSkillDemo(skillId) { return skillDemos[skillId] || null; } + + diff --git a/public/js/demos/skills/interaction-design.js b/public/js/demos/skills/interaction-design.js index d27c14544..d580afbc3 100644 --- a/public/js/demos/skills/interaction-design.js +++ b/public/js/demos/skills/interaction-design.js @@ -74,3 +74,5 @@ export default { } ] }; + + diff --git a/public/js/demos/skills/motion-design.js b/public/js/demos/skills/motion-design.js index 7e7482f72..7fecd65df 100644 --- a/public/js/demos/skills/motion-design.js +++ b/public/js/demos/skills/motion-design.js @@ -64,3 +64,5 @@ export default { } ] }; + + diff --git a/public/js/demos/skills/responsive-design.js b/public/js/demos/skills/responsive-design.js index c161ed1b9..fbbe82062 100644 --- a/public/js/demos/skills/responsive-design.js +++ b/public/js/demos/skills/responsive-design.js @@ -86,3 +86,5 @@ export default { } ] }; + + diff --git a/public/js/demos/skills/spatial-design.js b/public/js/demos/skills/spatial-design.js index febc0de8c..2563b18f9 100644 --- a/public/js/demos/skills/spatial-design.js +++ b/public/js/demos/skills/spatial-design.js @@ -57,3 +57,5 @@ export default { } ] }; + + diff --git a/public/js/demos/skills/typography.js b/public/js/demos/skills/typography.js index fd60389d6..13acc3c18 100644 --- a/public/js/demos/skills/typography.js +++ b/public/js/demos/skills/typography.js @@ -33,3 +33,5 @@ export default { } ] }; + + diff --git a/public/js/demos/skills/ux-writing.js b/public/js/demos/skills/ux-writing.js index b3ab4cab8..187c3da27 100644 --- a/public/js/demos/skills/ux-writing.js +++ b/public/js/demos/skills/ux-writing.js @@ -66,3 +66,5 @@ export default { } ] }; + + diff --git a/public/js/effects/liquid-canvas.js b/public/js/effects/liquid-canvas.js index a25480f2d..5feda32e0 100644 --- a/public/js/effects/liquid-canvas.js +++ b/public/js/effects/liquid-canvas.js @@ -154,3 +154,5 @@ export function initHeroEffect() { observer.observe(canvas); } + + diff --git a/public/js/utils/reveal.js b/public/js/utils/reveal.js index 9611fad52..d2a8e11bf 100644 --- a/public/js/utils/reveal.js +++ b/public/js/utils/reveal.js @@ -13,3 +13,5 @@ export function initScrollReveal() { ); }); } + + diff --git a/public/js/utils/scroll.js b/public/js/utils/scroll.js index 5de42373f..cc4f7efe7 100644 --- a/public/js/utils/scroll.js +++ b/public/js/utils/scroll.js @@ -47,3 +47,56 @@ export function initScrollIndicator() { window.addEventListener("scroll", update, { passive: true }); update(); } + +export function initHashTracking() { + const sections = document.querySelectorAll('section[id]'); + if (!sections.length) return; + + let currentHash = window.location.hash.slice(1) || ''; + let ticking = false; + + function updateHash() { + const scrollY = window.scrollY; + const viewportHeight = window.innerHeight; + const triggerPoint = scrollY + viewportHeight * 0.3; + + let activeSection = ''; + + sections.forEach(section => { + const rect = section.getBoundingClientRect(); + const sectionTop = scrollY + rect.top; + const sectionBottom = sectionTop + rect.height; + + if (triggerPoint >= sectionTop && triggerPoint < sectionBottom) { + activeSection = section.id; + } + }); + + if (activeSection && activeSection !== currentHash) { + currentHash = activeSection; + history.replaceState(null, '', `#${activeSection}`); + } + + ticking = false; + } + + window.addEventListener('scroll', () => { + if (!ticking) { + requestAnimationFrame(updateHash); + ticking = true; + } + }, { passive: true }); + + // Handle initial hash on page load + if (window.location.hash) { + const target = document.querySelector(window.location.hash); + if (target) { + setTimeout(() => { + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, 100); + } + } + + // Initial check + updateHash(); +}