mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 15:46:30 +03:00
Apply neo-kinpaku design system and improve live picker UX
Restyle the live picker to match the site kinpaku kit, persist pick mode in localStorage, fix DESIGN.md color swatches in the parser, and land the neo-kinpaku site refresh with new tokens, assets, palette script, and detector rules. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,26 +1,35 @@
|
||||
import { skillFocusAreas, dimensionGuidelineCounts } from '../data.js';
|
||||
import { skillFocusAreas, slopFocusAreas, dimensionGuidelineCounts } from '../data.js';
|
||||
import { foundationAnimations } from './foundation-animations.js';
|
||||
|
||||
// Renders the seven-discipline grid. The container picks its data source via
|
||||
// `data-source="foundation"` (the original "loaded on every command" copy) or
|
||||
// `data-source="slop"` (the slop-tells-we-prevent copy used on /catch-the-slop).
|
||||
// Both share the icon + plinth + card chrome so we get the elegant magazine
|
||||
// rail twice without two parallel components.
|
||||
export function initFoundationGrid() {
|
||||
const container = document.querySelector('.foundation-grid');
|
||||
if (!container) return;
|
||||
const containers = document.querySelectorAll('.foundation-grid');
|
||||
if (!containers.length) return;
|
||||
|
||||
const dimensions = skillFocusAreas['impeccable'];
|
||||
if (!dimensions) return;
|
||||
containers.forEach((container) => {
|
||||
const source = container.dataset.source || 'foundation';
|
||||
const data = source === 'slop' ? slopFocusAreas['impeccable'] : skillFocusAreas['impeccable'];
|
||||
if (!data) return;
|
||||
const showCount = source !== 'slop';
|
||||
|
||||
container.innerHTML = dimensions.map((dim, i) => `
|
||||
<div class="foundation-column">
|
||||
<div class="foundation-card">
|
||||
<div class="foundation-card-viz">
|
||||
${foundationAnimations[dim.area] || ''}
|
||||
container.innerHTML = data.map((dim, i) => `
|
||||
<div class="foundation-column">
|
||||
<div class="foundation-card">
|
||||
<div class="foundation-card-viz">
|
||||
${foundationAnimations[dim.area] || ''}
|
||||
</div>
|
||||
<div class="foundation-card-header">
|
||||
<span class="foundation-card-label">${dim.area}</span>
|
||||
${showCount ? `<span class="foundation-card-count">${dimensionGuidelineCounts[dim.area] || ''}</span>` : ''}
|
||||
</div>
|
||||
<p class="foundation-card-detail">${dim.detail}</p>
|
||||
</div>
|
||||
<div class="foundation-card-header">
|
||||
<span class="foundation-card-label">${dim.area}</span>
|
||||
<span class="foundation-card-count">${dimensionGuidelineCounts[dim.area] || ''}</span>
|
||||
</div>
|
||||
<p class="foundation-card-detail">${dim.detail}</p>
|
||||
<div class="foundation-plinth plinth-${i + 1}"></div>
|
||||
</div>
|
||||
<div class="foundation-plinth plinth-${i + 1}"></div>
|
||||
</div>
|
||||
`).join('');
|
||||
`).join('');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ export class PeriodicTable {
|
||||
const categoryOrder = ['create', 'evaluate', 'refine', 'simplify', 'harden', 'system'];
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'ptable-grid';
|
||||
grid.style.cssText = `
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
@@ -191,9 +192,11 @@ export class PeriodicTable {
|
||||
const colors = categoryColors[category];
|
||||
|
||||
const group = document.createElement('div');
|
||||
group.className = `ptable-group ptable-group--${category}`;
|
||||
group.style.cssText = `display: flex; flex-direction: column; gap: 6px;`;
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'ptable-group-label';
|
||||
label.style.cssText = `
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
@@ -207,6 +210,7 @@ export class PeriodicTable {
|
||||
group.appendChild(label);
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'ptable-row';
|
||||
row.style.cssText = `display: flex; flex-wrap: wrap; gap: 6px;`;
|
||||
|
||||
commands.forEach(cmd => {
|
||||
@@ -223,6 +227,8 @@ export class PeriodicTable {
|
||||
const display = commandDisplay[cmd];
|
||||
|
||||
const el = document.createElement('button');
|
||||
el.className = `ptable-element ptable-element--${category}`;
|
||||
el.dataset.category = category;
|
||||
el.type = 'button';
|
||||
// Build accessible label with the full invocation
|
||||
const invocation = cmd === 'impeccable'
|
||||
@@ -250,6 +256,7 @@ export class PeriodicTable {
|
||||
|
||||
// Atomic number
|
||||
const number = document.createElement('div');
|
||||
number.className = 'ptable-number';
|
||||
number.style.cssText = `
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
@@ -264,6 +271,7 @@ export class PeriodicTable {
|
||||
|
||||
// Symbol
|
||||
const symbol = document.createElement('div');
|
||||
symbol.className = 'ptable-symbol';
|
||||
symbol.style.cssText = `
|
||||
font-family: var(--font-display);
|
||||
font-size: 20px;
|
||||
@@ -274,10 +282,10 @@ export class PeriodicTable {
|
||||
symbol.textContent = commandSymbols[cmd];
|
||||
el.appendChild(symbol);
|
||||
|
||||
// Command name. The root "impeccable" is shown with its slash as the
|
||||
// entry point. All other commands are sub-commands and show their
|
||||
// bare name (the invocation is /impeccable <name>).
|
||||
// Command name. The palette lists names; invocation examples elsewhere
|
||||
// include the slash syntax.
|
||||
const name = document.createElement('div');
|
||||
name.className = 'ptable-name';
|
||||
name.style.cssText = `
|
||||
font-family: var(--font-mono);
|
||||
font-size: 8px;
|
||||
@@ -290,7 +298,7 @@ export class PeriodicTable {
|
||||
white-space: nowrap;
|
||||
`;
|
||||
if (cmd === 'impeccable') {
|
||||
name.textContent = '/impeccable';
|
||||
name.textContent = 'impeccable';
|
||||
} else if (display) {
|
||||
name.textContent = display.label;
|
||||
} else {
|
||||
@@ -301,6 +309,7 @@ export class PeriodicTable {
|
||||
// Alpha badge
|
||||
if (alphaCommands.includes(cmd)) {
|
||||
const badge = document.createElement('div');
|
||||
badge.className = 'ptable-alpha';
|
||||
badge.style.cssText = `
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
|
||||
@@ -135,12 +135,8 @@ function renderDesktopLayout(container, commands) {
|
||||
const fisheyeHTML = filteredCommands.map((cmd, i) => {
|
||||
const cat = commandCategories[cmd.id] || 'other';
|
||||
const isAlpha = alphaCommands.includes(cmd.id);
|
||||
// The root skill is shown as "/impeccable", everything else is a sub-command
|
||||
// displayed without a slash (invocation is /impeccable <name>)
|
||||
const isRoot = cmd.id === 'impeccable';
|
||||
const label = isRoot
|
||||
? `<span class="fisheye-slash">/</span>impeccable`
|
||||
: cmd.id;
|
||||
// The command palette lists command names, not raw invocations.
|
||||
const label = cmd.id;
|
||||
return `<button class="fisheye-item${i === startIndex ? ' is-active' : ''}" data-index="${i}" data-id="${cmd.id}" data-cat="${cat}">${label}${isAlpha ? '<span class="fisheye-beta">ALPHA</span>' : ''}</button>`;
|
||||
}).join('');
|
||||
|
||||
@@ -201,12 +197,12 @@ function renderSpread(cmd, index, isActive) {
|
||||
}
|
||||
}
|
||||
|
||||
// The root skill is rendered as /impeccable; sub-commands are rendered as
|
||||
// The root skill is rendered as impeccable; sub-commands are rendered as
|
||||
// /impeccable on a smaller line above the command name, so the command name
|
||||
// stays the visual anchor at full display size.
|
||||
const isRoot = cmd.id === 'impeccable';
|
||||
const nameHTML = isRoot
|
||||
? `<span class="spread-slash">/</span>impeccable`
|
||||
? 'impeccable'
|
||||
: `<span class="spread-namespace"><span class="spread-slash">/</span>impeccable</span>${cmd.id}`;
|
||||
|
||||
return `
|
||||
@@ -251,7 +247,8 @@ function initSpreadDemo(index) {
|
||||
const splitComparison = demoArea.querySelector('.demo-split-comparison');
|
||||
if (splitComparison) {
|
||||
currentSplitInstance = initSplitCompare(splitComparison, {
|
||||
defaultPosition: 50
|
||||
defaultPosition: 50,
|
||||
skewAngle: 0
|
||||
});
|
||||
}
|
||||
initCommandDemo(cmd.id, demoArea);
|
||||
@@ -547,6 +544,7 @@ function setupMobileInteractions(commands) {
|
||||
if (initialSplit) {
|
||||
currentSplitInstance = initSplitCompare(initialSplit, {
|
||||
defaultPosition: 50,
|
||||
skewAngle: 0,
|
||||
minPosition: 10,
|
||||
maxPosition: 90
|
||||
});
|
||||
@@ -587,7 +585,8 @@ function setupMobileInteractions(commands) {
|
||||
const splitComparison = demoArea.querySelector('.demo-split-comparison');
|
||||
if (splitComparison) {
|
||||
currentSplitInstance = initSplitCompare(splitComparison, {
|
||||
defaultPosition: 50
|
||||
defaultPosition: 50,
|
||||
skewAngle: 0
|
||||
});
|
||||
}
|
||||
initCommandDemo(cmdId, demoArea);
|
||||
@@ -664,4 +663,3 @@ async function updateSourceContent(cmdId) {
|
||||
contentEl.innerHTML = '<span class="source-loading">Source not available</span>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ export function initLensEffect() {
|
||||
if (!container) return;
|
||||
|
||||
initSplitCompare(container, {
|
||||
defaultPosition: 50
|
||||
defaultPosition: 50,
|
||||
skewAngle: 0
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,20 +16,22 @@ const PHASE = {
|
||||
const TIMELINE = [
|
||||
{ dt: 400, action: 'cursor-show' },
|
||||
{ dt: 400, action: 'cursor-to-target' },
|
||||
{ dt: 900, action: 'outline-show', caption: 'Hover to pick.' },
|
||||
{ dt: 500, action: 'cursor-click' },
|
||||
{ dt: 200, action: 'open-ctx', caption: 'Picked. Contextual bar appears.' },
|
||||
{ dt: 700, action: 'cursor-to-input' },
|
||||
{ dt: 300, action: 'type', text: 'more playful', caption: 'Type a refinement, or skip.' },
|
||||
{ dt: 1200, action: 'draw-stroke', caption: 'Annotate on the page, if you want.' },
|
||||
{ dt: 900, action: 'cursor-to-go' },
|
||||
{ dt: 800, action: 'outline-show', caption: 'Pick any element on your live page.' },
|
||||
{ dt: 450, action: 'cursor-click' },
|
||||
{ dt: 200, action: 'open-ctx' },
|
||||
{ dt: 550, action: 'cursor-to-price' },
|
||||
{ dt: 250, action: 'draw-circle', caption: 'Circle it. Leave a comment.' },
|
||||
{ dt: 750, action: 'drop-pin' },
|
||||
{ dt: 300, action: 'type-note', text: 'make it feel more exclusive' },
|
||||
{ dt: 1100, action: 'hold', caption: 'Your note + sketch are sent along.' },
|
||||
{ dt: 650, action: 'cursor-to-go' },
|
||||
{ dt: 300, action: 'click-go', caption: 'Generating three variants…' },
|
||||
{ dt: 1600, action: 'show-variant', n: 1, caption: 'Variant 1 of 3.' },
|
||||
{ dt: 1400, action: 'show-variant', n: 2, caption: 'Variant 2 of 3.' },
|
||||
{ dt: 1400, action: 'show-variant', n: 3, caption: 'Variant 3 of 3.' },
|
||||
{ dt: 900, action: 'cursor-to-accept' },
|
||||
{ dt: 300, action: 'click-accept', caption: 'Accepted. Written to source.' },
|
||||
{ dt: 1800, action: 'reset', caption: 'Hover to pick.' },
|
||||
{ dt: 1800, action: 'reset', caption: 'Pick any element on your live page.' },
|
||||
];
|
||||
|
||||
export function initLiveDemo() {
|
||||
@@ -43,6 +45,7 @@ export function initLiveDemo() {
|
||||
const cursor = root.querySelector('[data-demo-cursor]');
|
||||
const ctx = root.querySelector('[data-demo-ctx]');
|
||||
const inputText = root.querySelector('[data-demo-input-text]');
|
||||
const noteText = root.querySelector('[data-demo-note-text]');
|
||||
const counter = root.querySelector('[data-demo-counter]');
|
||||
const captionLabel = root.querySelector('[data-demo-caption-label]');
|
||||
const variants = Array.from(root.querySelectorAll('.live-demo-variant'));
|
||||
@@ -61,6 +64,18 @@ export function initLiveDemo() {
|
||||
outline.style.height = (targetRect.height + 8) + 'px';
|
||||
};
|
||||
|
||||
// Overlay the annotation layer (circle + comment pin) exactly on the target,
|
||||
// so the marks sit on the picked card rather than the stage center.
|
||||
const positionAnnotations = () => {
|
||||
if (!annotations) return;
|
||||
const stageRect = stage.getBoundingClientRect();
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
annotations.style.left = (targetRect.left - stageRect.left) + 'px';
|
||||
annotations.style.top = (targetRect.top - stageRect.top) + 'px';
|
||||
annotations.style.width = targetRect.width + 'px';
|
||||
annotations.style.height = targetRect.height + 'px';
|
||||
};
|
||||
|
||||
// Position the contextual bar below the target (or above if below would
|
||||
// collide with the global bar). Mirrors positionBar() in live-browser.js.
|
||||
const positionCtx = () => {
|
||||
@@ -68,7 +83,9 @@ export function initLiveDemo() {
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const ctxRect = ctx.getBoundingClientRect();
|
||||
const GAP = 10;
|
||||
const BAR_RESERVE = 60;
|
||||
// Clearance kept below the bar inside the stage. The global bar lives
|
||||
// outside the stage, so this only needs to be a small visual margin.
|
||||
const BAR_RESERVE = 24;
|
||||
const belowTop = targetRect.bottom - stageRect.top + GAP;
|
||||
const aboveTop = targetRect.top - stageRect.top - ctxRect.height - GAP;
|
||||
let top;
|
||||
@@ -80,6 +97,13 @@ export function initLiveDemo() {
|
||||
top = stage.clientHeight - ctxRect.height - BAR_RESERVE;
|
||||
}
|
||||
ctx.style.top = top + 'px';
|
||||
|
||||
// Center the bar on the target horizontally (the target is no longer the
|
||||
// stage centre), clamped to the stage edges.
|
||||
const rawLeft = targetRect.left - stageRect.left + targetRect.width / 2 - ctxRect.width / 2;
|
||||
const maxLeft = stage.clientWidth - ctxRect.width - 8;
|
||||
ctx.style.left = Math.max(8, Math.min(rawLeft, maxLeft)) + 'px';
|
||||
ctx.style.transform = 'none';
|
||||
};
|
||||
|
||||
const moveCursor = (selector, offsetX = 0, offsetY = 0) => {
|
||||
@@ -113,22 +137,27 @@ export function initLiveDemo() {
|
||||
setCtxPhase(PHASE.HIDDEN);
|
||||
cursor.classList.remove('is-visible', 'is-click');
|
||||
outline.classList.remove('is-visible');
|
||||
annotations.classList.remove('is-visible', 'is-comment-visible');
|
||||
inputText.textContent = '';
|
||||
annotations.classList.remove('is-visible', 'is-pin-visible', 'is-note-visible');
|
||||
if (noteText) noteText.textContent = '';
|
||||
if (inputText) inputText.textContent = '';
|
||||
showVariant(0);
|
||||
};
|
||||
|
||||
const clearAnnotations = () =>
|
||||
annotations.classList.remove('is-visible', 'is-pin-visible', 'is-note-visible');
|
||||
|
||||
const setCaption = (text) => {
|
||||
if (text && captionLabel) captionLabel.textContent = text;
|
||||
};
|
||||
|
||||
const typeText = (text, duration) => new Promise((resolve) => {
|
||||
inputText.textContent = '';
|
||||
const typeInto = (el, text, duration) => new Promise((resolve) => {
|
||||
if (!el) return resolve();
|
||||
el.textContent = '';
|
||||
const per = Math.max(30, Math.floor(duration / text.length));
|
||||
let i = 0;
|
||||
const tick = () => {
|
||||
if (i >= text.length) return resolve();
|
||||
inputText.textContent += text[i++];
|
||||
el.textContent += text[i++];
|
||||
setTimeout(tick, per);
|
||||
};
|
||||
tick();
|
||||
@@ -154,15 +183,26 @@ export function initLiveDemo() {
|
||||
case 'open-ctx':
|
||||
setCtxPhase(PHASE.CONFIGURING);
|
||||
break;
|
||||
case 'cursor-to-input':
|
||||
moveCursor(root.querySelector('[data-demo-input]'));
|
||||
case 'cursor-to-price':
|
||||
// Over the hero headline (upper-left of the wide hero).
|
||||
moveCursor(target, -150, -18);
|
||||
break;
|
||||
case 'type':
|
||||
await typeText(s.text, 700);
|
||||
break;
|
||||
case 'draw-stroke':
|
||||
case 'draw-circle':
|
||||
positionAnnotations();
|
||||
annotations.classList.add('is-visible');
|
||||
setTimeout(() => annotations.classList.add('is-comment-visible'), 600);
|
||||
break;
|
||||
case 'drop-pin':
|
||||
// Near the pin's CSS position (left 31%, top 60%).
|
||||
moveCursor(target, -120, 14);
|
||||
cursor.classList.add('is-click');
|
||||
setTimeout(() => cursor.classList.remove('is-click'), 220);
|
||||
annotations.classList.add('is-pin-visible');
|
||||
break;
|
||||
case 'type-note':
|
||||
annotations.classList.add('is-note-visible');
|
||||
await typeInto(noteText, s.text, 800);
|
||||
break;
|
||||
case 'hold':
|
||||
break;
|
||||
case 'cursor-to-go':
|
||||
moveCursor(root.querySelector('[data-demo-go]'));
|
||||
@@ -170,7 +210,7 @@ export function initLiveDemo() {
|
||||
case 'click-go':
|
||||
cursor.classList.add('is-click');
|
||||
setTimeout(() => cursor.classList.remove('is-click'), 260);
|
||||
annotations.classList.remove('is-visible', 'is-comment-visible');
|
||||
clearAnnotations();
|
||||
setCtxPhase(PHASE.GENERATING);
|
||||
break;
|
||||
case 'show-variant':
|
||||
@@ -237,6 +277,7 @@ export function initLiveDemo() {
|
||||
|
||||
window.addEventListener('resize', () => requestAnimationFrame(() => {
|
||||
positionOutline();
|
||||
positionAnnotations();
|
||||
positionCtx();
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user