mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 16:46:31 +03:00
Homepage UX improvements from design critique
- Add floating bottom section nav (appears after hero, hides at footer) - Fix hero split comparison to default 50/50 instead of 70/30 - Remove duplicate "Works with" badges from Install section - Periodic table: restore compact elements, add inline hover tooltips - Truncate command descriptions in Commands section for scannability - Collapse older changelog entries behind "View older releases" - Add GitHub star count (13.3k) next to repo link - Fix meta theme-color to match light mode - Reduce hero top padding, update section 03 subtitle - Clean up footer: remove redundant tagline and anchor links - Fix divider line accumulation in changelog and FAQ sections - Center glass terminal vertically in viewport Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1f484d0f5d
commit
8d1937d3c9
@@ -6,6 +6,7 @@ import { initLensEffect } from "./js/components/lens.js";
|
||||
import { initFrameworkViz } from "./js/components/framework-viz.js";
|
||||
import { initScrollReveal } from "./js/utils/reveal.js";
|
||||
import { initAnchorScroll, initHashTracking } from "./js/utils/scroll.js";
|
||||
import { initSectionNav } from "./js/components/section-nav.js";
|
||||
|
||||
// ============================================
|
||||
// STATE
|
||||
@@ -270,6 +271,7 @@ function init() {
|
||||
initScrollReveal();
|
||||
initGlassTerminal();
|
||||
initFrameworkViz();
|
||||
initSectionNav();
|
||||
loadContent();
|
||||
|
||||
document.body.classList.add("loaded");
|
||||
|
||||
+114
-8
@@ -133,6 +133,98 @@ button, input, textarea, select {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FLOATING BOTTOM NAV
|
||||
============================================ */
|
||||
|
||||
.section-nav {
|
||||
position: fixed;
|
||||
bottom: var(--spacing-md);
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
background: oklch(98% 0 0 / 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 100px;
|
||||
box-shadow: 0 4px 24px -4px rgba(0,0,0,0.12), 0 1px 3px rgba(0,0,0,0.06);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.4s var(--ease-out), transform 0.4s var(--ease-out);
|
||||
}
|
||||
|
||||
.section-nav.is-visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.section-nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
border-radius: 100px;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.section-nav-item:hover {
|
||||
background: var(--color-mist);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.section-nav-item.is-active {
|
||||
background: var(--color-ink);
|
||||
}
|
||||
|
||||
.section-nav-item.is-active .section-nav-num {
|
||||
color: var(--color-paper);
|
||||
}
|
||||
|
||||
.section-nav-item.is-active .section-nav-label {
|
||||
color: var(--color-paper);
|
||||
}
|
||||
|
||||
.section-nav-num {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-ash);
|
||||
letter-spacing: 0.02em;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.section-nav-label {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-charcoal);
|
||||
transition: color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.section-nav-label {
|
||||
display: none;
|
||||
}
|
||||
.section-nav-item {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.section-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ============================================
|
||||
BASE STYLES
|
||||
============================================ */
|
||||
@@ -297,7 +389,7 @@ code {
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
padding: var(--spacing-2xl) 0;
|
||||
padding: var(--spacing-lg) 0 var(--spacing-2xl);
|
||||
background: var(--color-paper);
|
||||
}
|
||||
|
||||
@@ -308,15 +400,27 @@ code {
|
||||
z-index: 10;
|
||||
color: var(--color-ash);
|
||||
transition: color 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.github-link:hover {
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.github-link svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
.github-stars {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-ash);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.github-link:hover .github-stars {
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.hero-combined-container {
|
||||
@@ -468,8 +572,9 @@ code {
|
||||
|
||||
.hero-logos-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hero-logos-inline .hero-logos-label {
|
||||
@@ -482,6 +587,7 @@ code {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-logos-inline .hero-logos-row img {
|
||||
@@ -586,7 +692,7 @@ code {
|
||||
|
||||
.split-after {
|
||||
/* Angled clip-path matching divider's skewX(-10deg): top-right, bottom-left */
|
||||
clip-path: polygon(78% 0%, 100% 0%, 100% 100%, 62% 100%);
|
||||
clip-path: polygon(58% 0%, 100% 0%, 100% 100%, 42% 100%);
|
||||
z-index: 2;
|
||||
background: var(--color-paper);
|
||||
}
|
||||
@@ -595,7 +701,7 @@ code {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 70%;
|
||||
left: 50%;
|
||||
width: 3px;
|
||||
background: var(--color-accent);
|
||||
transform: translateX(-50%) skewX(-10deg);
|
||||
|
||||
+4082
-1
File diff suppressed because one or more lines are too long
+48
-10
@@ -93,8 +93,8 @@
|
||||
}
|
||||
|
||||
.manual-cmd-desc {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-charcoal);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -123,10 +123,11 @@
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
/* Right: The Terminal Stack (Sticky) */
|
||||
/* Right: The Terminal Stack (Sticky, vertically centered) */
|
||||
.glass-terminal-wrapper {
|
||||
position: sticky;
|
||||
top: var(--spacing-xl);
|
||||
top: 50vh;
|
||||
transform: translateY(-50%);
|
||||
height: calc(100vh - var(--spacing-xl) * 2);
|
||||
max-height: 800px;
|
||||
min-height: 500px;
|
||||
@@ -742,10 +743,9 @@
|
||||
|
||||
.changelog-entry {
|
||||
padding: var(--spacing-md) 0;
|
||||
border-bottom: 1px solid var(--color-mist);
|
||||
}
|
||||
|
||||
.changelog-entry:first-child {
|
||||
.changelog-entry + .changelog-entry {
|
||||
border-top: 1px solid var(--color-mist);
|
||||
}
|
||||
|
||||
@@ -788,6 +788,48 @@
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
/* Changelog: collapsible older entries */
|
||||
.changelog-older {
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.changelog-older-toggle {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-accent);
|
||||
padding: var(--spacing-md) 0;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.changelog-older-toggle::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.changelog-older-toggle::before {
|
||||
content: '+';
|
||||
font-size: 1.25rem;
|
||||
font-weight: 300;
|
||||
transition: transform 0.3s var(--ease-out);
|
||||
}
|
||||
|
||||
.changelog-older[open] .changelog-older-toggle::before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.changelog-older-toggle:hover {
|
||||
color: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.changelog-older-entries {
|
||||
animation: faqFadeIn 0.3s var(--ease-out);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FAQ SECTION
|
||||
============================================ */
|
||||
@@ -808,10 +850,6 @@
|
||||
border-bottom: 1px solid var(--color-mist);
|
||||
}
|
||||
|
||||
.faq-item:first-child {
|
||||
border-top: 1px solid var(--color-mist);
|
||||
}
|
||||
|
||||
.faq-question {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.125rem;
|
||||
|
||||
+2
-6
@@ -60,7 +60,8 @@
|
||||
<!-- 1. HERO - Combined with Problem Section -->
|
||||
<section id="hero" class="hero-combined">
|
||||
<a href="https://github.com/pbakaus/impeccable" class="github-link" aria-label="View on GitHub">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
||||
<span class="github-stars">13.3k</span>
|
||||
</a>
|
||||
<div class="hero-combined-container">
|
||||
<!-- Left: Title & Info -->
|
||||
@@ -467,14 +468,9 @@
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<span class="footer-logo">Impeccable</span>
|
||||
<p class="footer-tagline">impeccable.style</p>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="#antidote">Anti-Patterns</a>
|
||||
<a href="#commands-section">Commands</a>
|
||||
<a href="/cheatsheet">Cheatsheet</a>
|
||||
<a href="#downloads">Downloads</a>
|
||||
<a href="#changelog">Changelog</a>
|
||||
<a href="/privacy">Privacy</a>
|
||||
<a href="https://github.com/pbakaus/impeccable">GitHub</a>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Periodic Table of Commands
|
||||
* Clean grid visualization showing all commands organized by category
|
||||
* Hover tooltips show description and relationships inline.
|
||||
*/
|
||||
|
||||
import { commandCategories, commandRelationships, betaCommands } from '../data.js';
|
||||
|
||||
// Colors now reference CSS custom properties for dark mode support
|
||||
const categoryColors = {
|
||||
diagnostic: { bg: 'var(--cat-diagnostic-bg)', border: 'var(--cat-diagnostic-border)', text: 'var(--cat-diagnostic-text)' },
|
||||
quality: { bg: 'var(--cat-quality-bg)', border: 'var(--cat-quality-border)', text: 'var(--cat-quality-text)' },
|
||||
@@ -24,7 +24,6 @@ const categoryLabels = {
|
||||
system: 'System'
|
||||
};
|
||||
|
||||
// Short symbols for each command (like element symbols)
|
||||
const commandSymbols = {
|
||||
'teach-impeccable': 'Ti',
|
||||
audit: 'Au',
|
||||
@@ -48,34 +47,18 @@ const commandSymbols = {
|
||||
overdrive: 'Od'
|
||||
};
|
||||
|
||||
// Atomic numbers (just for visual interest)
|
||||
const commandNumbers = {
|
||||
'teach-impeccable': 0,
|
||||
audit: 1,
|
||||
critique: 2,
|
||||
normalize: 3,
|
||||
polish: 4,
|
||||
optimize: 5,
|
||||
harden: 6,
|
||||
clarify: 7,
|
||||
distill: 8,
|
||||
adapt: 9,
|
||||
extract: 10,
|
||||
animate: 11,
|
||||
colorize: 12,
|
||||
delight: 13,
|
||||
bolder: 14,
|
||||
quieter: 15,
|
||||
onboard: 16,
|
||||
typeset: 17,
|
||||
arrange: 18,
|
||||
overdrive: 19
|
||||
audit: 1, critique: 2, normalize: 3, polish: 4, optimize: 5,
|
||||
harden: 6, clarify: 7, distill: 8, adapt: 9, extract: 10,
|
||||
animate: 11, colorize: 12, delight: 13, bolder: 14, quieter: 15,
|
||||
onboard: 16, typeset: 17, arrange: 18, overdrive: 19
|
||||
};
|
||||
|
||||
export class PeriodicTable {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.infoPanel = null;
|
||||
this.activeTooltip = null;
|
||||
this.activeElement = null;
|
||||
this.init();
|
||||
}
|
||||
@@ -89,24 +72,21 @@ export class PeriodicTable {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
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;
|
||||
@@ -118,7 +98,6 @@ export class PeriodicTable {
|
||||
categoryOrder.forEach(cat => {
|
||||
const commands = groups[cat];
|
||||
if (!commands) return;
|
||||
|
||||
const group = this.createCategoryGroup(cat, commands);
|
||||
grid.appendChild(group);
|
||||
});
|
||||
@@ -126,50 +105,10 @@ export class PeriodicTable {
|
||||
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;
|
||||
`;
|
||||
showTooltip(el, cmd) {
|
||||
this.hideTooltip();
|
||||
|
||||
// Default state
|
||||
this.showDefaultInfo();
|
||||
|
||||
this.container.appendChild(this.infoPanel);
|
||||
}
|
||||
|
||||
showDefaultInfo() {
|
||||
// Use "Tap" on touch devices, "Hover over" on desktop
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
const action = isTouchDevice ? 'Tap' : 'Hover over';
|
||||
|
||||
this.infoPanel.innerHTML = `
|
||||
<div style="
|
||||
font-family: var(--font-body);
|
||||
font-size: 14px;
|
||||
color: var(--color-ash);
|
||||
font-style: italic;
|
||||
">
|
||||
${action} a command to see details
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -180,69 +119,67 @@ export class PeriodicTable {
|
||||
const leadsTo = toArray(rel.leadsTo);
|
||||
const combinesWith = toArray(rel.combinesWith);
|
||||
|
||||
// Build related info
|
||||
let relatedHtml = '';
|
||||
// Build relationships line
|
||||
let relParts = [];
|
||||
if (pairs.length > 0) relParts.push(`pairs with ${pairs.map(p => '/' + p).join(', ')}`);
|
||||
if (combinesWith.length > 0) relParts.push(`+ ${combinesWith.map(p => '/' + p).join(', ')}`);
|
||||
if (leadsTo.length > 0) relParts.push(`then ${leadsTo.map(p => '/' + p).join(', ')}`);
|
||||
|
||||
if (pairs.length > 0) {
|
||||
const list = pairs.map(p => `<span style="font-family: var(--font-mono); color: var(--color-ink)">/${p}</span>`).join(', ');
|
||||
relatedHtml += `<span style="color: var(--color-ash)">Pairs with:</span> ${list}`;
|
||||
}
|
||||
if (combinesWith.length > 0) {
|
||||
const list = combinesWith.map(p => `<span style="font-family: var(--font-mono); color: var(--color-ink)">/${p}</span>`).join(', ');
|
||||
if (relatedHtml) relatedHtml += '<span style="color: var(--color-mist); margin: 0 8px;">•</span>';
|
||||
relatedHtml += `<span style="color: var(--color-ash)">Combines with:</span> ${list}`;
|
||||
}
|
||||
if (leadsTo.length > 0) {
|
||||
const list = leadsTo.map(p => `<span style="font-family: var(--font-mono); color: var(--color-ink)">/${p}</span>`).join(', ');
|
||||
if (relatedHtml) relatedHtml += '<span style="color: var(--color-mist); margin: 0 8px;">•</span>';
|
||||
relatedHtml += `<span style="color: var(--color-ash)">Then:</span> ${list}`;
|
||||
}
|
||||
// Strip category prefix from flow for cleaner display
|
||||
const flow = (rel.flow || '').replace(/^[^:]+:\s*/, '');
|
||||
|
||||
this.infoPanel.innerHTML = `
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
margin-bottom: 4px;
|
||||
">
|
||||
<span style="
|
||||
font-family: var(--font-mono);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--color-ink);
|
||||
">/${cmd}</span>
|
||||
<span style="
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: ${colors.text};
|
||||
font-weight: 500;
|
||||
">${categoryLabels[category]}</span>
|
||||
</div>
|
||||
<div style="
|
||||
font-family: var(--font-body);
|
||||
font-size: 13px;
|
||||
color: var(--color-charcoal);
|
||||
line-height: 1.4;
|
||||
margin-bottom: ${relatedHtml ? '6px' : '0'};
|
||||
">${rel.flow || 'Design command'}</div>
|
||||
${relatedHtml ? `<div style="font-size: 12px; line-height: 1.4;">${relatedHtml}</div>` : ''}
|
||||
</div>
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.className = 'ptable-tooltip';
|
||||
tooltip.style.cssText = `
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
background: var(--color-paper);
|
||||
border: 1px solid var(--color-mist);
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
box-shadow: 0 8px 24px -4px rgba(0,0,0,0.12);
|
||||
pointer-events: none;
|
||||
max-width: 280px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
`;
|
||||
|
||||
tooltip.innerHTML = `
|
||||
<div style="font-family: var(--font-body); font-size: 13px; color: var(--color-charcoal); line-height: 1.4; margin-bottom: ${relParts.length ? '6px' : '0'};">${flow}</div>
|
||||
${relParts.length ? `<div style="font-family: var(--font-mono); font-size: 11px; color: var(--color-ash); line-height: 1.4;">${relParts.join(' · ')}</div>` : ''}
|
||||
`;
|
||||
|
||||
this.container.appendChild(tooltip);
|
||||
|
||||
// Position relative to element
|
||||
const elRect = el.getBoundingClientRect();
|
||||
const containerRect = this.container.getBoundingClientRect();
|
||||
|
||||
const left = elRect.left - containerRect.left;
|
||||
const top = elRect.bottom - containerRect.top + 6;
|
||||
|
||||
tooltip.style.left = `${Math.min(left, containerRect.width - 290)}px`;
|
||||
tooltip.style.top = `${top}px`;
|
||||
|
||||
// Fade in
|
||||
requestAnimationFrame(() => { tooltip.style.opacity = '1'; });
|
||||
|
||||
this.activeTooltip = tooltip;
|
||||
}
|
||||
|
||||
hideTooltip() {
|
||||
if (this.activeTooltip) {
|
||||
this.activeTooltip.remove();
|
||||
this.activeTooltip = null;
|
||||
}
|
||||
}
|
||||
|
||||
createCategoryGroup(category, commands) {
|
||||
const colors = categoryColors[category];
|
||||
|
||||
const group = document.createElement('div');
|
||||
group.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
`;
|
||||
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);
|
||||
@@ -256,13 +193,8 @@ export class PeriodicTable {
|
||||
label.textContent = categoryLabels[category];
|
||||
group.appendChild(label);
|
||||
|
||||
// Elements row
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = `
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
`;
|
||||
row.style.cssText = `display: flex; flex-wrap: wrap; gap: 6px;`;
|
||||
|
||||
commands.forEach(cmd => {
|
||||
const element = this.createElement(cmd, category);
|
||||
@@ -352,16 +284,12 @@ export class PeriodicTable {
|
||||
el.appendChild(badge);
|
||||
}
|
||||
|
||||
// Shared handler for activation (hover or focus)
|
||||
// Hover/focus: show tooltip
|
||||
const activate = () => {
|
||||
// Visual feedback
|
||||
el.style.transform = 'translateY(-2px)';
|
||||
el.style.boxShadow = `0 4px 12px ${colors.border}40`;
|
||||
this.showTooltip(el, cmd);
|
||||
|
||||
// Update info panel
|
||||
this.showCommandInfo(cmd);
|
||||
|
||||
// Track active element
|
||||
if (this.activeElement && this.activeElement !== el) {
|
||||
this.activeElement.style.transform = 'translateY(0)';
|
||||
this.activeElement.style.boxShadow = 'none';
|
||||
@@ -369,29 +297,24 @@ export class PeriodicTable {
|
||||
this.activeElement = el;
|
||||
};
|
||||
|
||||
// Shared handler for deactivation
|
||||
const deactivate = () => {
|
||||
el.style.transform = 'translateY(0)';
|
||||
el.style.boxShadow = 'none';
|
||||
this.hideTooltip();
|
||||
};
|
||||
|
||||
// Mouse events (desktop)
|
||||
el.addEventListener('mouseenter', activate);
|
||||
el.addEventListener('mouseleave', deactivate);
|
||||
|
||||
// Keyboard focus events
|
||||
el.addEventListener('focus', activate);
|
||||
el.addEventListener('blur', deactivate);
|
||||
|
||||
// Touch events - activate on tap, stay active until another tap
|
||||
el.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault(); // Prevent double-firing with click
|
||||
e.preventDefault();
|
||||
activate();
|
||||
}, { passive: false });
|
||||
|
||||
// Click/Enter to scroll to command
|
||||
el.addEventListener('click', () => {
|
||||
activate(); // Also activate on click for touch devices
|
||||
activate();
|
||||
const target = document.getElementById(`cmd-${cmd}`);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
|
||||
@@ -127,6 +127,16 @@ function renderDesktopLayout(container, commands) {
|
||||
}
|
||||
}
|
||||
|
||||
function truncateDescription(text, maxLen = 120) {
|
||||
if (text.length <= maxLen) return text;
|
||||
// Cut at last sentence boundary within limit, or last word boundary
|
||||
const truncated = text.slice(0, maxLen);
|
||||
const lastPeriod = truncated.lastIndexOf('.');
|
||||
if (lastPeriod > maxLen * 0.5) return truncated.slice(0, lastPeriod + 1);
|
||||
const lastSpace = truncated.lastIndexOf(' ');
|
||||
return truncated.slice(0, lastSpace) + '...';
|
||||
}
|
||||
|
||||
function renderManualEntry(cmd) {
|
||||
const relationship = commandRelationships[cmd.id];
|
||||
let relationshipHTML = '';
|
||||
@@ -142,11 +152,12 @@ function renderManualEntry(cmd) {
|
||||
}
|
||||
|
||||
const isBeta = betaCommands.includes(cmd.id);
|
||||
const shortDesc = truncateDescription(cmd.description);
|
||||
|
||||
return `
|
||||
<div class="manual-entry" data-id="${cmd.id}" id="cmd-${cmd.id}">
|
||||
<h3 class="manual-cmd-name">/${cmd.id}${isBeta ? ' <span class="beta-badge">BETA</span>' : ''}</h3>
|
||||
<p class="manual-cmd-desc">${cmd.description}</p>
|
||||
<p class="manual-cmd-desc">${shortDesc}</p>
|
||||
${relationshipHTML}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -5,7 +5,7 @@ export function initLensEffect() {
|
||||
if (!container) return;
|
||||
|
||||
initSplitCompare(container, {
|
||||
defaultPosition: 70
|
||||
defaultPosition: 50
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Sticky Section Nav
|
||||
* Shows/hides based on scroll position and highlights current section.
|
||||
*/
|
||||
|
||||
export function initSectionNav() {
|
||||
const nav = document.getElementById('section-nav');
|
||||
if (!nav) return;
|
||||
|
||||
const items = nav.querySelectorAll('.section-nav-item');
|
||||
const sectionIds = Array.from(items).map(item => item.dataset.section);
|
||||
|
||||
// Show/hide nav based on scroll position
|
||||
const hero = document.getElementById('hero');
|
||||
const footer = document.querySelector('.site-footer');
|
||||
if (!hero) return;
|
||||
|
||||
let ticking = false;
|
||||
|
||||
function updateNav() {
|
||||
const scrollY = window.scrollY;
|
||||
const heroBottom = hero.offsetTop + hero.offsetHeight - 100;
|
||||
const footerTop = footer ? footer.offsetTop : Infinity;
|
||||
const viewportBottom = scrollY + window.innerHeight;
|
||||
|
||||
// Show nav after hero, hide when footer is visible
|
||||
if (scrollY > heroBottom && viewportBottom < footerTop + 60) {
|
||||
nav.classList.add('is-visible');
|
||||
} else {
|
||||
nav.classList.remove('is-visible');
|
||||
}
|
||||
|
||||
// Find current section
|
||||
let currentSection = null;
|
||||
const viewportMiddle = scrollY + window.innerHeight * 0.4;
|
||||
|
||||
for (let i = sectionIds.length - 1; i >= 0; i--) {
|
||||
const section = document.getElementById(sectionIds[i]);
|
||||
if (section && section.offsetTop <= viewportMiddle) {
|
||||
currentSection = sectionIds[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update active state
|
||||
items.forEach(item => {
|
||||
if (item.dataset.section === currentSection) {
|
||||
item.classList.add('is-active');
|
||||
} else {
|
||||
item.classList.remove('is-active');
|
||||
}
|
||||
});
|
||||
|
||||
ticking = false;
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(updateNav);
|
||||
ticking = true;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Initial check
|
||||
updateNav();
|
||||
}
|
||||
Reference in New Issue
Block a user