mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 17:16:46 +03:00
Mark overdrive as beta, remove product-level suggestions
Skill changes: - Remove real-time collaboration section (product decision, not UI) - Remove service workers / offline-first (product decision) - Remove SharedArrayBuffer (too niche) - Replace "application architecture" section with "performance-critical UI" — focused on making existing features feel fast - Add explicit note: this skill enhances how UI FEELS, not what a product DOES Beta label: - Add betaCommands list to data.js - Show BETA badge on /overdrive in command palette - Show β symbol on periodic table element - Show BETA badge on cheatsheet page 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
5b5bba6488
commit
9bdb836903
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: overdrive
|
||||
description: Push interfaces past conventional limits with technically ambitious implementations. Whether that's a shader, a 60fps virtual table, spring physics on a dialog, or real-time collaboration — make users ask "how did they do that?"
|
||||
description: Push interfaces past conventional limits with technically ambitious implementations. Whether that's a shader, a 60fps virtual table, spring physics on a dialog, or scroll-driven reveals — make users ask "how did they do that?"
|
||||
user-invokable: true
|
||||
args:
|
||||
- name: target
|
||||
@@ -28,8 +28,8 @@ Pages, hero sections, landing pages, portfolios — the "wow" is often sensory:
|
||||
### For functional UI
|
||||
Tables, forms, dialogs, navigation — the "wow" is in how it FEELS: a dialog that morphs from the button that triggered it via View Transitions, a data table that renders 100k rows at 60fps via virtual scrolling, a form with streaming validation that feels instant, drag-and-drop with spring physics.
|
||||
|
||||
### For application architecture
|
||||
The "wow" is invisible but felt: offline-first with service workers (the app works on a plane), real-time collaboration (cursors appearing as someone else types), near-native computation via WASM, background processing via Web Workers that never blocks the UI.
|
||||
### For performance-critical UI
|
||||
The "wow" is invisible but felt: a search that filters 50k items without a flicker, a complex form that never blocks the main thread, an image editor that processes in near-real-time. The interface just never hesitates.
|
||||
|
||||
### For data-heavy interfaces
|
||||
Charts and dashboards — the "wow" is in fluidity: GPU-accelerated rendering via Canvas/WebGL for massive datasets, animated transitions between data states, force-directed graph layouts that settle naturally.
|
||||
@@ -66,17 +66,14 @@ Organized by what you're trying to achieve, not by technology name.
|
||||
### Push performance boundaries
|
||||
- **Web Workers** — move computation off the main thread. Heavy data processing, image manipulation, search indexing — anything that would cause jank.
|
||||
- **OffscreenCanvas** — render in a Worker thread. The main thread stays free while complex visuals render in the background.
|
||||
- **WASM** — near-native performance for computation-heavy features. Image processing, encryption, physics simulations, codecs.
|
||||
- **Service Workers** — offline-first experiences, background sync, push notifications. The app works without a network.
|
||||
|
||||
### Enable real-time collaboration
|
||||
- **WebSockets / SSE** — live cursors, collaborative editing, streaming updates. The interface feels alive.
|
||||
- **SharedArrayBuffer** — shared memory between threads for high-performance real-time data.
|
||||
- **WASM** — near-native performance for computation-heavy features. Image processing, physics simulations, codecs.
|
||||
|
||||
### Interact with the device
|
||||
- **Web Audio API** — spatial audio, audio-reactive visualizations, sonic feedback. Requires user gesture to start.
|
||||
- **Device APIs** — orientation, ambient light, geolocation. Use sparingly and always with user permission.
|
||||
|
||||
**NOTE**: This skill is about enhancing how an interface FEELS, not changing what a product DOES. Adding real-time collaboration, offline support, or new backend capabilities are product decisions, not UI enhancements. Focus on making existing features feel extraordinary.
|
||||
|
||||
## Implement with Discipline
|
||||
|
||||
### Progressive enhancement is non-negotiable
|
||||
|
||||
@@ -267,8 +267,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
const isBeta = cmd.id === 'overdrive';
|
||||
html += `<div class="command">
|
||||
<div class="command-name">/${cmd.id}</div>
|
||||
<div class="command-name">/${cmd.id}${isBeta ? ' <span style="font-size:0.55rem;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-accent,#c8956c);border:1px solid;border-radius:3px;padding:1px 5px;vertical-align:middle;margin-left:4px;">BETA</span>' : ''}</div>
|
||||
<div class="command-info">
|
||||
<div class="command-desc">${cmd.description}</div>
|
||||
${metaHtml ? `<div class="command-meta">${metaHtml}</div>` : ''}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -78,6 +78,20 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.beta-badge {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.55rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-accent);
|
||||
border: 1px solid var(--color-accent);
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
vertical-align: middle;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.manual-cmd-desc {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Clean grid visualization showing all commands organized by category
|
||||
*/
|
||||
|
||||
import { commandCategories, commandRelationships } from '../data.js';
|
||||
import { commandCategories, commandRelationships, betaCommands } from '../data.js';
|
||||
|
||||
// Colors now reference CSS custom properties for dark mode support
|
||||
const categoryColors = {
|
||||
@@ -334,6 +334,24 @@ export class PeriodicTable {
|
||||
name.textContent = `/${cmd}`;
|
||||
el.appendChild(name);
|
||||
|
||||
// Beta badge
|
||||
if (betaCommands.includes(cmd)) {
|
||||
const badge = document.createElement('div');
|
||||
badge.style.cssText = `
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 3px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 5px;
|
||||
letter-spacing: 0.05em;
|
||||
color: ${colors.text};
|
||||
opacity: 0.45;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
badge.textContent = 'β';
|
||||
el.appendChild(badge);
|
||||
}
|
||||
|
||||
// Shared handler for activation (hover or focus)
|
||||
const activate = () => {
|
||||
// Visual feedback
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { renderCommandDemo, initCommandDemo } from "../demo-renderer.js";
|
||||
import { initSplitCompare } from "../effects/split-compare.js";
|
||||
import { commandProcessSteps, commandCategories, commandRelationships } from "../data.js";
|
||||
import { commandProcessSteps, commandCategories, commandRelationships, betaCommands } from "../data.js";
|
||||
|
||||
// Track current split instance and command for cleanup
|
||||
let currentSplitInstance = null;
|
||||
@@ -141,9 +141,11 @@ function renderManualEntry(cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
const isBeta = betaCommands.includes(cmd.id);
|
||||
|
||||
return `
|
||||
<div class="manual-entry" data-id="${cmd.id}" id="cmd-${cmd.id}">
|
||||
<h3 class="manual-cmd-name">/${cmd.id}</h3>
|
||||
<h3 class="manual-cmd-name">/${cmd.id}${isBeta ? ' <span class="beta-badge">BETA</span>' : ''}</h3>
|
||||
<p class="manual-cmd-desc">${cmd.description}</p>
|
||||
${relationshipHTML}
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,11 @@ export const readyCommands = [
|
||||
'normalize' // First command to be fully completed
|
||||
];
|
||||
|
||||
// Commands marked as beta — shown with a badge in the UI
|
||||
export const betaCommands = [
|
||||
'overdrive'
|
||||
];
|
||||
|
||||
// Consolidated frontend-design skill with reference domains
|
||||
export const skillFocusAreas = {
|
||||
'frontend-design': [
|
||||
|
||||
@@ -47,116 +47,140 @@ export default {
|
||||
ctx.fillStyle = '#0e0d0b';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// ── Signature path ──
|
||||
function buildSignaturePath() {
|
||||
const pts = [];
|
||||
function bez(x0,y0, cx1,cy1, cx2,cy2, x1,y1, n) {
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const t = i / n, mt = 1-t;
|
||||
pts.push({
|
||||
x: mt*mt*mt*x0 + 3*mt*mt*t*cx1 + 3*mt*t*t*cx2 + t*t*t*x1,
|
||||
y: mt*mt*mt*y0 + 3*mt*mt*t*cy1 + 3*mt*t*t*cy2 + t*t*t*y1
|
||||
});
|
||||
// ── Signature paths — two separate strokes ──
|
||||
function buildSignaturePaths() {
|
||||
function makePath(buildFn) {
|
||||
const pts = [];
|
||||
function bez(x0,y0, cx1,cy1, cx2,cy2, x1,y1, n) {
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const t = i / n, mt = 1-t;
|
||||
pts.push({
|
||||
x: mt*mt*mt*x0 + 3*mt*mt*t*cx1 + 3*mt*t*t*cx2 + t*t*t*x1,
|
||||
y: mt*mt*mt*y0 + 3*mt*mt*t*cy1 + 3*mt*t*t*cy2 + t*t*t*y1
|
||||
});
|
||||
}
|
||||
}
|
||||
buildFn(bez);
|
||||
return pts;
|
||||
}
|
||||
// P
|
||||
bez(6,44, 5,32, 4,18, 8,8, 14);
|
||||
bez(8,8, 16,5, 26,7, 26,16, 12);
|
||||
bez(26,16, 26,22, 18,26, 14,28, 10);
|
||||
// a
|
||||
bez(14,28, 18,22, 23,20, 26,22, 8);
|
||||
bez(26,22, 29,24, 28,30, 24,32, 6);
|
||||
bez(24,32, 28,34, 30,30, 32,28, 5);
|
||||
// u
|
||||
bez(32,28, 34,36, 38,40, 42,32, 8);
|
||||
bez(42,32, 44,26, 47,24, 48,28, 6);
|
||||
// l
|
||||
bez(48,28, 49,16, 50,6, 53,8, 10);
|
||||
bez(53,8, 55,14, 56,28, 58,32, 8);
|
||||
// B
|
||||
bez(66,44, 66,32, 67,16, 70,8, 14);
|
||||
bez(70,8, 78,4, 83,10, 79,18, 12);
|
||||
bez(79,18, 84,15, 87,24, 80,30, 12);
|
||||
bez(80,30, 78,34, 80,36, 84,32, 5);
|
||||
// akaus
|
||||
bez(84,32, 89,24, 94,22, 97,26, 8);
|
||||
bez(97,26, 99,30, 96,34, 100,30, 5);
|
||||
bez(100,30, 101,20, 102,14, 104,16, 8);
|
||||
bez(104,16, 106,24, 109,28, 107,32, 6);
|
||||
bez(107,32, 105,36, 110,36, 113,30, 6);
|
||||
bez(113,30, 118,24, 122,22, 125,28, 8);
|
||||
bez(125,28, 128,36, 133,38, 137,30, 8);
|
||||
bez(137,30, 139,26, 142,24, 144,28, 5);
|
||||
bez(144,28, 154,24, 170,22, 195,28, 16);
|
||||
|
||||
// Same scaling as the static preview version
|
||||
const paul = makePath(bez => {
|
||||
// P
|
||||
bez(6,44, 5,32, 4,18, 8,8, 14);
|
||||
bez(8,8, 16,5, 26,7, 26,16, 12);
|
||||
bez(26,16, 26,22, 18,26, 14,28, 10);
|
||||
// a
|
||||
bez(14,28, 18,22, 23,20, 26,22, 8);
|
||||
bez(26,22, 29,24, 28,30, 24,32, 6);
|
||||
bez(24,32, 28,34, 30,30, 32,28, 5);
|
||||
// u
|
||||
bez(32,28, 34,36, 38,40, 42,32, 8);
|
||||
bez(42,32, 44,26, 47,24, 48,28, 6);
|
||||
// l
|
||||
bez(48,28, 49,16, 50,6, 53,8, 10);
|
||||
bez(53,8, 55,14, 56,28, 58,32, 8);
|
||||
});
|
||||
|
||||
const bakaus = makePath(bez => {
|
||||
// B
|
||||
bez(66,44, 66,32, 67,16, 70,8, 14);
|
||||
bez(70,8, 78,4, 83,10, 79,18, 12);
|
||||
bez(79,18, 84,15, 87,24, 80,30, 12);
|
||||
bez(80,30, 78,34, 80,36, 84,32, 5);
|
||||
// akaus
|
||||
bez(84,32, 89,24, 94,22, 97,26, 8);
|
||||
bez(97,26, 99,30, 96,34, 100,30, 5);
|
||||
bez(100,30, 101,20, 102,14, 104,16, 8);
|
||||
bez(104,16, 106,24, 109,28, 107,32, 6);
|
||||
bez(107,32, 105,36, 110,36, 113,30, 6);
|
||||
bez(113,30, 118,24, 122,22, 125,28, 8);
|
||||
bez(125,28, 128,36, 133,38, 137,30, 8);
|
||||
bez(137,30, 139,26, 142,24, 144,28, 5);
|
||||
bez(144,28, 154,24, 170,22, 195,28, 16);
|
||||
});
|
||||
|
||||
// Scale both paths
|
||||
const rawW = 200;
|
||||
const scale = (w * 0.7) / rawW;
|
||||
const ox = (w - rawW * scale) / 2;
|
||||
const oy = h * 0.52;
|
||||
return pts.map(p => ({ x: p.x * scale + ox, y: p.y * scale * 0.75 + oy }));
|
||||
const transform = p => ({ x: p.x * scale + ox, y: p.y * scale * 0.75 + oy });
|
||||
return [paul.map(transform), bakaus.map(transform)];
|
||||
}
|
||||
|
||||
const sigPoints = buildSignaturePath();
|
||||
const segLengths = [];
|
||||
let totalLength = 0;
|
||||
for (let i = 1; i < sigPoints.length; i++) {
|
||||
const dx = sigPoints[i].x - sigPoints[i-1].x;
|
||||
const dy = sigPoints[i].y - sigPoints[i-1].y;
|
||||
segLengths.push(Math.sqrt(dx*dx + dy*dy));
|
||||
totalLength += segLengths[segLengths.length - 1];
|
||||
const strokes = buildSignaturePaths();
|
||||
|
||||
// Precompute lengths for each stroke
|
||||
function computeLengths(pts) {
|
||||
const lens = [];
|
||||
let total = 0;
|
||||
for (let i = 1; i < pts.length; i++) {
|
||||
const dx = pts[i].x - pts[i-1].x, dy = pts[i].y - pts[i-1].y;
|
||||
const l = Math.sqrt(dx*dx + dy*dy);
|
||||
lens.push(l); total += l;
|
||||
}
|
||||
return { lens, total };
|
||||
}
|
||||
|
||||
function posAt(dist) {
|
||||
const strokeData = strokes.map(pts => {
|
||||
const { lens, total } = computeLengths(pts);
|
||||
return { pts, lens, total };
|
||||
});
|
||||
|
||||
function posAtStroke(stroke, dist) {
|
||||
let d = 0;
|
||||
for (let i = 0; i < segLengths.length; i++) {
|
||||
if (d + segLengths[i] >= dist) {
|
||||
const t = segLengths[i] > 0 ? (dist - d) / segLengths[i] : 0;
|
||||
const p0 = sigPoints[i], p1 = sigPoints[i+1];
|
||||
for (let i = 0; i < stroke.lens.length; i++) {
|
||||
if (d + stroke.lens[i] >= dist) {
|
||||
const t = stroke.lens[i] > 0 ? (dist - d) / stroke.lens[i] : 0;
|
||||
const p0 = stroke.pts[i], p1 = stroke.pts[i+1];
|
||||
return { x: p0.x + (p1.x - p0.x) * t, y: p0.y + (p1.y - p0.y) * t };
|
||||
}
|
||||
d += segLengths[i];
|
||||
d += stroke.lens[i];
|
||||
}
|
||||
return sigPoints[sigPoints.length - 1];
|
||||
return stroke.pts[stroke.pts.length - 1];
|
||||
}
|
||||
|
||||
const totalLength = strokeData.reduce((s, d) => s + d.total, 0);
|
||||
|
||||
// ── State ──
|
||||
let currentStroke = 0;
|
||||
let drawnLength = 0;
|
||||
const drawSpeed = totalLength / 3.0;
|
||||
let prevTip = sigPoints[0];
|
||||
let prevTip = strokes[0][0];
|
||||
let sparks = [];
|
||||
let phase = 'drawing';
|
||||
let phase = 'drawing'; // drawing, lifting, holding, fading
|
||||
let phaseTimer = 0;
|
||||
let lastTime = 0;
|
||||
|
||||
// Track all drawn points for smooth rendering
|
||||
const drawnPts = [];
|
||||
// Track drawn points per stroke for smooth rendering
|
||||
const allDrawnStrokes = [[], []];
|
||||
|
||||
function drawBurnTrail() {
|
||||
if (drawnPts.length < 2) return;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
// Draw smooth path through all points using quadratic curves
|
||||
function strokeSmooth(color, width) {
|
||||
function strokeSmooth(pts, color, width) {
|
||||
if (pts.length < 2) return;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(drawnPts[0].x, drawnPts[0].y);
|
||||
for (let i = 1; i < drawnPts.length - 1; i++) {
|
||||
const mx = (drawnPts[i].x + drawnPts[i+1].x) / 2;
|
||||
const my = (drawnPts[i].y + drawnPts[i+1].y) / 2;
|
||||
ctx.quadraticCurveTo(drawnPts[i].x, drawnPts[i].y, mx, my);
|
||||
ctx.moveTo(pts[0].x, pts[0].y);
|
||||
for (let i = 1; i < pts.length - 1; i++) {
|
||||
const mx = (pts[i].x + pts[i+1].x) / 2;
|
||||
const my = (pts[i].y + pts[i+1].y) / 2;
|
||||
ctx.quadraticCurveTo(pts[i].x, pts[i].y, mx, my);
|
||||
}
|
||||
const last = drawnPts[drawnPts.length - 1];
|
||||
ctx.lineTo(last.x, last.y);
|
||||
ctx.lineTo(pts[pts.length-1].x, pts[pts.length-1].y);
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
strokeSmooth('rgba(180, 100, 30, 0.12)', 5);
|
||||
strokeSmooth('rgba(220, 140, 50, 0.3)', 2.5);
|
||||
strokeSmooth('rgba(255, 210, 130, 0.7)', 1.2);
|
||||
strokeSmooth('rgba(255, 248, 235, 0.6)', 0.4);
|
||||
// Draw all accumulated strokes
|
||||
for (const pts of allDrawnStrokes) {
|
||||
strokeSmooth(pts, 'rgba(180, 100, 30, 0.12)', 5);
|
||||
strokeSmooth(pts, 'rgba(220, 140, 50, 0.3)', 2.5);
|
||||
strokeSmooth(pts, 'rgba(255, 210, 130, 0.7)', 1.2);
|
||||
strokeSmooth(pts, 'rgba(255, 248, 235, 0.6)', 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
function emitSparks(x, y, count) {
|
||||
@@ -180,23 +204,42 @@ export default {
|
||||
lastTime = timestamp;
|
||||
|
||||
switch (phase) {
|
||||
case 'drawing':
|
||||
case 'drawing': {
|
||||
const sd = strokeData[currentStroke];
|
||||
drawnLength += drawSpeed * dt;
|
||||
if (drawnLength >= totalLength) {
|
||||
drawnLength = totalLength;
|
||||
phase = 'holding';
|
||||
phaseTimer = 0;
|
||||
emitSparks(prevTip.x, prevTip.y, 8);
|
||||
if (drawnLength >= sd.total) {
|
||||
drawnLength = sd.total;
|
||||
emitSparks(prevTip.x, prevTip.y, 6);
|
||||
if (currentStroke < strokes.length - 1) {
|
||||
// Lift — pause briefly before starting next stroke
|
||||
phase = 'lifting';
|
||||
phaseTimer = 0;
|
||||
} else {
|
||||
phase = 'holding';
|
||||
phaseTimer = 0;
|
||||
}
|
||||
}
|
||||
const tip = posAt(drawnLength);
|
||||
drawnPts.push({ x: tip.x, y: tip.y });
|
||||
const tip = posAtStroke(sd, drawnLength);
|
||||
allDrawnStrokes[currentStroke].push({ x: tip.x, y: tip.y });
|
||||
prevTip = tip;
|
||||
if (Math.random() < 0.4) emitSparks(tip.x, tip.y, 1);
|
||||
// Redraw full smooth trail (clear burn canvas first)
|
||||
// Redraw full smooth trail
|
||||
ctx.fillStyle = '#0e0d0b';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
drawBurnTrail();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'lifting':
|
||||
phaseTimer += dt;
|
||||
if (phaseTimer >= 0.25) {
|
||||
currentStroke++;
|
||||
drawnLength = 0;
|
||||
prevTip = strokes[currentStroke][0];
|
||||
phase = 'drawing';
|
||||
phaseTimer = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'holding':
|
||||
phaseTimer += dt;
|
||||
@@ -210,8 +253,11 @@ export default {
|
||||
if (phaseTimer >= 2.0) {
|
||||
ctx.fillStyle = '#0e0d0b';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
drawnLength = 0; prevTip = sigPoints[0];
|
||||
sparks = []; drawnPts.length = 0;
|
||||
currentStroke = 0; drawnLength = 0;
|
||||
prevTip = strokes[0][0];
|
||||
sparks = [];
|
||||
allDrawnStrokes[0].length = 0;
|
||||
allDrawnStrokes[1].length = 0;
|
||||
phase = 'drawing'; phaseTimer = 0;
|
||||
}
|
||||
break;
|
||||
@@ -255,8 +301,8 @@ export default {
|
||||
}
|
||||
|
||||
// Draw laser tip on overlay
|
||||
if (phase === 'drawing' && drawnLength < totalLength) {
|
||||
const tip = posAt(drawnLength);
|
||||
if (phase === 'drawing' && drawnLength < strokeData[currentStroke].total) {
|
||||
const tip = posAtStroke(strokeData[currentStroke], drawnLength);
|
||||
const fl = 0.85 + Math.random() * 0.15;
|
||||
|
||||
// Wide heat bloom
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: overdrive
|
||||
description: Push interfaces past conventional limits with technically ambitious implementations. Whether that's a shader, a 60fps virtual table, spring physics on a dialog, or real-time collaboration — make users ask "how did they do that?"
|
||||
description: Push interfaces past conventional limits with technically ambitious implementations. Whether that's a shader, a 60fps virtual table, spring physics on a dialog, or scroll-driven reveals — make users ask "how did they do that?"
|
||||
args:
|
||||
- name: target
|
||||
description: The feature or area to push into overdrive (optional)
|
||||
@@ -28,8 +28,8 @@ Pages, hero sections, landing pages, portfolios — the "wow" is often sensory:
|
||||
### For functional UI
|
||||
Tables, forms, dialogs, navigation — the "wow" is in how it FEELS: a dialog that morphs from the button that triggered it via View Transitions, a data table that renders 100k rows at 60fps via virtual scrolling, a form with streaming validation that feels instant, drag-and-drop with spring physics.
|
||||
|
||||
### For application architecture
|
||||
The "wow" is invisible but felt: offline-first with service workers (the app works on a plane), real-time collaboration (cursors appearing as someone else types), near-native computation via WASM, background processing via Web Workers that never blocks the UI.
|
||||
### For performance-critical UI
|
||||
The "wow" is invisible but felt: a search that filters 50k items without a flicker, a complex form that never blocks the main thread, an image editor that processes in near-real-time. The interface just never hesitates.
|
||||
|
||||
### For data-heavy interfaces
|
||||
Charts and dashboards — the "wow" is in fluidity: GPU-accelerated rendering via Canvas/WebGL for massive datasets, animated transitions between data states, force-directed graph layouts that settle naturally.
|
||||
@@ -66,17 +66,14 @@ Organized by what you're trying to achieve, not by technology name.
|
||||
### Push performance boundaries
|
||||
- **Web Workers** — move computation off the main thread. Heavy data processing, image manipulation, search indexing — anything that would cause jank.
|
||||
- **OffscreenCanvas** — render in a Worker thread. The main thread stays free while complex visuals render in the background.
|
||||
- **WASM** — near-native performance for computation-heavy features. Image processing, encryption, physics simulations, codecs.
|
||||
- **Service Workers** — offline-first experiences, background sync, push notifications. The app works without a network.
|
||||
|
||||
### Enable real-time collaboration
|
||||
- **WebSockets / SSE** — live cursors, collaborative editing, streaming updates. The interface feels alive.
|
||||
- **SharedArrayBuffer** — shared memory between threads for high-performance real-time data.
|
||||
- **WASM** — near-native performance for computation-heavy features. Image processing, physics simulations, codecs.
|
||||
|
||||
### Interact with the device
|
||||
- **Web Audio API** — spatial audio, audio-reactive visualizations, sonic feedback. Requires user gesture to start.
|
||||
- **Device APIs** — orientation, ambient light, geolocation. Use sparingly and always with user permission.
|
||||
|
||||
**NOTE**: This skill is about enhancing how an interface FEELS, not changing what a product DOES. Adding real-time collaboration, offline support, or new backend capabilities are product decisions, not UI enhancements. Focus on making existing features feel extraordinary.
|
||||
|
||||
## Implement with Discipline
|
||||
|
||||
### Progressive enhancement is non-negotiable
|
||||
|
||||
Reference in New Issue
Block a user