mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Prepare CLI 3.0.0, skill 3.6.0, extension 1.2.0
This commit is contained in:
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "3.5.0",
|
||||
"version": "3.6.0",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
"email": "paul@paulbakaus.com"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "3.5.0",
|
||||
"version": "3.6.0",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
"email": "paul@paulbakaus.com"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
---
|
||||
|
||||
Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft.
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"ignoreRules": [],
|
||||
"ignoreFiles": [
|
||||
"tests/fixtures/**",
|
||||
"tests/detect-antipatterns.test.js",
|
||||
"site/pages/slop/**"
|
||||
],
|
||||
"ignoreValues": [],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
license: Apache 2.0
|
||||
allowed-tools:
|
||||
- Bash(npx impeccable *)
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"version": "2.3.2",
|
||||
"version": "3.0.0",
|
||||
"author": "Paul Bakaus",
|
||||
"description": "Design skills, commands, and anti-pattern detection for AI coding agents",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "3.5.0",
|
||||
"version": "3.6.0",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
"email": "paul@paulbakaus.com"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.5.0
|
||||
version: 3.6.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
|
||||
license: Apache 2.0
|
||||
|
||||
@@ -23,6 +23,18 @@ function stripHtmlToText(html) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
|
||||
|
||||
function extFromFilePath(filePath) {
|
||||
return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function shouldRunPageAnalyzers(content, filePath) {
|
||||
if (!isFullPage(content)) return false;
|
||||
const ext = extFromFilePath(filePath);
|
||||
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
||||
}
|
||||
|
||||
function isNeutralBorderColor(str) {
|
||||
const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
|
||||
if (!m) return false;
|
||||
@@ -422,7 +434,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
|
||||
|
||||
function runTextContentAnalyzers(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
if (!isFullPage(content)) return [];
|
||||
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
||||
// The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
|
||||
const findings = [];
|
||||
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
||||
@@ -442,7 +454,7 @@ function detectText(content, filePath, options = {}) {
|
||||
const profile = options?.profile;
|
||||
const findings = [];
|
||||
const lines = content.split('\n');
|
||||
const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
|
||||
const ext = extFromFilePath(filePath);
|
||||
|
||||
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
|
||||
// Enable block context for CSS files where related properties span multiple lines
|
||||
@@ -498,7 +510,7 @@ function detectText(content, filePath, options = {}) {
|
||||
}
|
||||
|
||||
// Page-level analyzers only run on full pages
|
||||
if (isFullPage(content)) {
|
||||
if (shouldRunPageAnalyzers(content, filePath)) {
|
||||
const analyzerIds = [
|
||||
'single-font',
|
||||
'flat-type-hierarchy',
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -23,8 +23,20 @@ import '../styles/changelog-faq-kinpaku.css';
|
||||
<button type="button" class="cf-filter-btn" data-cf-filter="all" aria-pressed="false">All</button>
|
||||
</div>
|
||||
|
||||
<article id="v3.5.0" class="cf-entry cf-entry--current">
|
||||
<header class="cf-entry-head"><span class="cf-version">v3.5.0</span><span class="cf-date">May 28, 2026</span><span class="cf-current-badge">Current</span></header>
|
||||
<article id="v3.6.0" class="cf-entry cf-entry--current">
|
||||
<header class="cf-entry-head"><span class="cf-version">v3.6.0</span><span class="cf-date">June 14, 2026</span><span class="cf-current-badge">Current</span></header>
|
||||
<p class="cf-entry-lead">Project design hooks, deeper Live Mode support for Svelte and manual edits, and a broad detector accuracy pass across the skill, CLI, and extension.</p>
|
||||
<ul class="cf-items">
|
||||
<li><strong>Project design hooks.</strong> <code>/impeccable hooks</code> installs and repairs a project-local detector hook for Claude, Codex, and Cursor. Claude and Codex get post-edit reminders, Cursor can block proposed writes before they land, and <code>/impeccable hooks on</code> now handles manifest setup and consent instead of leaving users to wire files by hand.</li>
|
||||
<li><strong>Hook findings are actionable, not noisy.</strong> Hook runs track clean, pending, and fresh findings, cache duplicate reports, audit their own activity, and offer narrow ignore flows through <code>ignore-value</code>, <code>ignore-file</code>, and <code>ignore-rule</code>. Shared config lives in <code>.impeccable/config.json</code>, with local consent and overrides in <code>.impeccable/config.local.json</code>.</li>
|
||||
<li><strong>Svelte-native Live Mode.</strong> Svelte and SvelteKit variants now preview as temporary framework components with params stored in <code>params.json</code>, then accept back into the selected source component. That keeps stateful pages closer to their real shape and avoids the HMR resets caused by string-injected previews.</li>
|
||||
<li><strong>Manual and browser Live Mode got sturdier.</strong> Manual text edits have dedicated evidence, apply, and discard routes; Live Mode preserves insertion anchors and mapped-list accept cleanup; and the browser payload is split into DOM helpers, UI primitives, vocabulary, and manual-apply modules instead of one giant script.</li>
|
||||
<li><strong>Detector accuracy improved across the bundled skill.</strong> Hidden and unrendered elements are skipped in browser rules, sr-only and visually hidden text no longer trips <code>text-overflow</code>, repeated kicker false positives are reduced in card and list contexts, oversized H1 detection now requires viewport dominance, clipped overflow distinguishes decorative viewports from escaping content, OKLCH alpha parses correctly, Sass files count as CSS-like detector inputs, transparent borders or shadows no longer trigger the GPT thin-border rule, and page-level numbered-marker checks no longer treat JS, TS, JSX, TSX, or CSS implementation literals as visible page copy.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article id="v3.5.0" class="cf-entry">
|
||||
<header class="cf-entry-head"><span class="cf-version">v3.5.0</span><span class="cf-date">May 28, 2026</span></header>
|
||||
<p class="cf-entry-lead">The biggest release yet. Per-provider rules that lift GPT-5.5 and Codex most, a skill that adapts to new versus existing projects, and Live Mode in Beta.</p>
|
||||
|
||||
<figure class="cf-ba">
|
||||
@@ -71,6 +83,17 @@ import '../styles/changelog-faq-kinpaku.css';
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article id="cli-v3.0.0" class="cf-entry">
|
||||
<header class="cf-entry-head"><span class="cf-version">CLI v3.0.0</span><span class="cf-date">June 14, 2026</span></header>
|
||||
<ul class="cf-items">
|
||||
<li><strong>Breaking: Node 24 minimum.</strong> The CLI now declares <code>"node": ">=24"</code>. Upgrade Node before installing or running this version.</li>
|
||||
<li><strong>Hook-aware installs and updates.</strong> <code>impeccable skills install</code> and <code>impeccable skills update</code> can prompt once for hook consent, persist the local answer, install or repair <code>.claude/settings.local.json</code>, <code>.codex/hooks.json</code>, and <code>.cursor/hooks.json</code>, and still honor <code>--no-hooks</code> for teams that want skills without editor hooks.</li>
|
||||
<li><strong>Local and submodule workflows are cleaner.</strong> <code>skills link --source=.impeccable</code> supports repo-local development, symlink-safe updates avoid clobbering linked installs, provider aliases include Codex and Rovo Dev names, local bundle overrides are explicit, and ZIP extraction is safer on Windows.</li>
|
||||
<li><strong>The detector got a real accuracy pass.</strong> The CLI detector now skips hidden browser elements, handles sr-only text overflow, reduces repeated kicker false positives, tightens oversized H1 and clipped-overflow heuristics, understands OKLCH alpha and Sass inputs, avoids transparent-border false positives in the GPT thin-border rule, and keeps page-level numbered-marker analysis out of JS, TS, JSX, TSX, and CSS source literals.</li>
|
||||
<li><strong>Release and CI plumbing is stricter.</strong> Build commands are split between source validation and release-output sync, <code>scripts/run-tests.mjs</code> owns named test suites, and <code>bun run smoke:hooks</code> verifies provider hook manifests across the generated bundles.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article id="cli-v2.3.2" class="cf-entry">
|
||||
<header class="cf-entry-head"><span class="cf-version">CLI v2.3.2</span><span class="cf-date">May 29, 2026</span></header>
|
||||
<ul class="cf-items">
|
||||
@@ -105,9 +128,12 @@ import '../styles/changelog-faq-kinpaku.css';
|
||||
</article>
|
||||
|
||||
<article id="ext-v1.2.0" class="cf-entry">
|
||||
<header class="cf-entry-head"><span class="cf-version">Extension v1.2.0</span><span class="cf-date">June 1, 2026</span></header>
|
||||
<header class="cf-entry-head"><span class="cf-version">Extension v1.2.0</span><span class="cf-date">June 14, 2026</span></header>
|
||||
<ul class="cf-items">
|
||||
<li><strong>Firefox build.</strong> The same detector, popup, DevTools panel, and per-rule toggles now ship as a Firefox add-on. <code>bun run build:extension</code> emits a Gecko-compatible package next to the Chrome one, with the background worker declared as an event page and a data-collection declaration that states what the extension already does: the scan runs in the page, and nothing leaves your machine.</li>
|
||||
<li><strong>Firefox DevTools paths are fixed.</strong> DevTools panel and sidebar URLs are root-relative in the Firefox manifest, so packaged builds can open their extension pages reliably.</li>
|
||||
<li><strong>Detector results match the latest engine.</strong> The overlay picks up the same false-positive fixes as the CLI: hidden-element skips, sr-only text-overflow handling, tighter repeated kicker, oversized H1, clipped-overflow, OKLCH alpha, Sass-adjacent CSS parsing, and transparent-border handling.</li>
|
||||
<li><strong>Scan responses are easier to correlate.</strong> Extension scan messages echo scan IDs back to the caller, and the store metadata and icon set were refreshed for the current 41-rule detector.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
|
||||
@@ -2370,8 +2370,8 @@ main#main {
|
||||
|
||||
.prose blockquote {
|
||||
margin: 1.5em 0;
|
||||
padding: 0 0 0 var(--spacing-md);
|
||||
border-left: 3px solid var(--color-mist);
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-block: 1px solid var(--color-mist);
|
||||
color: var(--color-ash);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
function shouldShowHighlightTagTooltip() {
|
||||
// Configure/edit carry the tag in the bar selection pill — keep only the outline.
|
||||
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
|
||||
return state !== 'CONFIGURING' && state !== 'EDITING';
|
||||
}
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
syncPageChatFocus('update-bar-content');
|
||||
}
|
||||
|
||||
// Configure row — the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
|
||||
|
||||
const CONFIGURE_BAR_H = '36px';
|
||||
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
|
||||
@@ -1519,7 +1519,7 @@
|
||||
|
||||
function buildConfigureCountControl({ controlsLocked, onClick }) {
|
||||
const count = el('button', configureInlineControlStyle({
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.02em',
|
||||
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
|
||||
}));
|
||||
count.textContent = '\u00D7' + selectedCount;
|
||||
count.disabled = controlsLocked;
|
||||
@@ -5065,6 +5065,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
||||
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
||||
if (!dir || !variantNum) return '';
|
||||
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return '';
|
||||
return await res.text();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function extractSvelteComponentStyle(source) {
|
||||
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
async function applySvelteComponentVariantStyle(variantNum) {
|
||||
if (!svelteComponentSession || !variantNum) return;
|
||||
const { manifest, sessionId } = svelteComponentSession;
|
||||
const source = await loadSvelteComponentVariantSource(manifest, variantNum);
|
||||
const css = extractSvelteComponentStyle(source);
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (!css) return;
|
||||
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
||||
if (!scopedCss) return;
|
||||
const style = document.createElement('style');
|
||||
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
||||
style.dataset.impeccableVariant = String(variantNum);
|
||||
style.textContent = scopedCss;
|
||||
document.head.appendChild(style);
|
||||
svelteComponentSession.styleEl = style;
|
||||
}
|
||||
|
||||
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
||||
const style = session?.styleEl;
|
||||
if (style?.parentNode) style.parentNode.removeChild(style);
|
||||
if (session) session.styleEl = null;
|
||||
}
|
||||
|
||||
function scopeCssToSveltePreview(css, sessionId) {
|
||||
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
||||
return scopeCssBlock(String(css || ''), prefix).trim();
|
||||
}
|
||||
|
||||
function scopeCssBlock(css, prefix) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < css.length) {
|
||||
const open = css.indexOf('{', i);
|
||||
if (open === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const semi = css.indexOf(';', i);
|
||||
if (semi !== -1 && semi < open) {
|
||||
out += css.slice(i, semi + 1);
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
const prelude = css.slice(i, open).trim();
|
||||
const close = findMatchingCssBrace(css, open);
|
||||
if (close === -1) {
|
||||
out += css.slice(i);
|
||||
break;
|
||||
}
|
||||
const body = css.slice(open + 1, close);
|
||||
if (shouldScopeNestedCssAtRule(prelude)) {
|
||||
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
|
||||
} else if (prelude.startsWith('@')) {
|
||||
out += prelude + ' {' + body + '}';
|
||||
} else {
|
||||
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldScopeNestedCssAtRule(prelude) {
|
||||
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css, openIndex) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const ch = css[i];
|
||||
const prev = css[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function prefixCssSelectors(prelude, prefix) {
|
||||
return splitCssSelectorList(prelude)
|
||||
.map((selector) => {
|
||||
const s = unwrapSvelteGlobalSelector(selector.trim());
|
||||
if (!s) return '';
|
||||
if (s.startsWith(prefix.trim())) return s;
|
||||
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
|
||||
return prefix + s;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function splitCssSelectorList(selectorList) {
|
||||
const selectors = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = 0; i < selectorList.length; i++) {
|
||||
const ch = selectorList[i];
|
||||
const prev = selectorList[i - 1];
|
||||
if (quote) {
|
||||
if (ch === quote && prev !== '\\') quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
} else if (ch === '(' || ch === '[') {
|
||||
depth++;
|
||||
} else if ((ch === ')' || ch === ']') && depth > 0) {
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
selectors.push(selectorList.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(selectorList.slice(start));
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function unwrapSvelteGlobalSelector(selector) {
|
||||
return selector.replace(/:global\(([^()]*)\)/g, '$1');
|
||||
}
|
||||
|
||||
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
|
||||
const contract = manifest?.propContract || [];
|
||||
const values = {};
|
||||
@@ -5101,6 +5252,7 @@
|
||||
});
|
||||
svelteComponentSession.mountedVariant = variantNum;
|
||||
svelteComponentSession.runtime = runtime;
|
||||
await applySvelteComponentVariantStyle(variantNum);
|
||||
if (state === 'CYCLING') syncCyclingControls();
|
||||
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
||||
if (nextAnchor) {
|
||||
@@ -5134,6 +5286,7 @@
|
||||
function teardownSvelteComponentSession(restoreOriginal) {
|
||||
if (!svelteComponentSession) return;
|
||||
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
@@ -5173,6 +5326,7 @@
|
||||
if (mountedInstance && runtime?.unmount) {
|
||||
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
|
||||
}
|
||||
removeSvelteComponentVariantStyle(svelteComponentSession);
|
||||
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
|
||||
svelteComponentSession = null;
|
||||
svelteRuntimePromise = null;
|
||||
@@ -8843,7 +8997,7 @@ void main() {
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
width: PAGE_CHAT_COLLAPSED_W,
|
||||
transition: 'width 0.18s ease, border-color 0.15s ease',
|
||||
transition: 'border-color 0.15s ease',
|
||||
});
|
||||
pageChatEl.id = PREFIX + '-page-chat';
|
||||
pageChatEl.dataset.expanded = 'false';
|
||||
|
||||
@@ -242,6 +242,17 @@ describe('detectHtml — static HTML/CSS fixtures', () => {
|
||||
const f = await detectHtml(path.join(FIXTURES, 'typography-should-pass.html'));
|
||||
assert.equal(f.length, 0);
|
||||
});
|
||||
|
||||
it('numbered-section-markers: visible sequence flags while script/style/svg internals pass', async () => {
|
||||
const f = await detectHtml(path.join(FIXTURES, 'numbered-section-markers.html'));
|
||||
const numbered = f.filter(r => r.antipattern === 'numbered-section-markers');
|
||||
assert.equal(
|
||||
numbered.length,
|
||||
1,
|
||||
`expected one visible numbered-marker finding, got: ${numbered.map(r => r.snippet).join('; ')}`
|
||||
);
|
||||
assert.match(numbered[0].snippet, /01, 02, 03/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectHtml — icon-tile-stack', () => {
|
||||
|
||||
@@ -247,6 +247,32 @@ describe('partials skip page-level checks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectText — numbered section markers', () => {
|
||||
test('flags visible full-page numbered section labels', () => {
|
||||
const page = '<!DOCTYPE html><html><body>' +
|
||||
'<section><span>01</span><h2>Strategy</h2></section>' +
|
||||
'<section><span>02</span><h2>Prototype</h2></section>' +
|
||||
'<section><span>03</span><h2>Launch</h2></section>' +
|
||||
'</body></html>';
|
||||
const f = detectText(page, 'test.html');
|
||||
expect(f.some(r => r.antipattern === 'numbered-section-markers')).toBe(true);
|
||||
});
|
||||
|
||||
test('does not run page-level numbered marker analysis on JS source with embedded HTML strings', () => {
|
||||
const source = `
|
||||
const shell = '<!DOCTYPE html><html><head><title>Preview</title></head><body></body></html>';
|
||||
const palette = 'oklch(86% 0.07 84 / 0.08)';
|
||||
const shadow = '0 0 0 1px oklch(0% 0 0 / 0.04), 0 4px 16px oklch(0% 0 0 / 0.05), 0 1px 3px oklch(0% 0 0 / 0.06)';
|
||||
const size = '11.5px';
|
||||
const eye = '<svg viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/></svg>';
|
||||
const shader = 'float band = bandAt(uv.y - y, 0.05, 0.32);';
|
||||
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
||||
`;
|
||||
const f = detectText(source, 'live-browser.js');
|
||||
expect(f.filter(r => r.antipattern === 'numbered-section-markers')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layout anti-patterns
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Numbered section marker fixture</title>
|
||||
<style>
|
||||
.fixture-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32px;
|
||||
}
|
||||
.case {
|
||||
margin: 0 0 18px;
|
||||
padding: 16px;
|
||||
border: 1px solid #d7d0c2;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.marker {
|
||||
display: block;
|
||||
font: 700 12px/1.1 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #7c5c00;
|
||||
}
|
||||
.code-like::before {
|
||||
content: "01 02 03";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="fixture-grid">
|
||||
<section aria-labelledby="flag-title">
|
||||
<h1 id="flag-title">Should flag</h1>
|
||||
<article class="case">
|
||||
<span class="marker">01</span>
|
||||
<h2>Strategy</h2>
|
||||
</article>
|
||||
<article class="case">
|
||||
<span class="marker">02</span>
|
||||
<h2>Prototype</h2>
|
||||
</article>
|
||||
<article class="case">
|
||||
<span class="marker">03</span>
|
||||
<h2>Launch</h2>
|
||||
</article>
|
||||
<article class="case">
|
||||
<span class="marker">04</span>
|
||||
<h2>Measure</h2>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="pass-title">
|
||||
<h1 id="pass-title">Should pass</h1>
|
||||
<article class="case">
|
||||
<h2>Readable date</h2>
|
||||
<p>Updated June 14, 2026.</p>
|
||||
</article>
|
||||
<article class="case">
|
||||
<h2>Statistic cards</h2>
|
||||
<p>Revenue grew 18%, margin reached 24%, and churn fell to 3%.</p>
|
||||
</article>
|
||||
<article class="case">
|
||||
<h2>CSS generated content</h2>
|
||||
<p class="code-like">Generated numbers in CSS should not count as body copy.</p>
|
||||
</article>
|
||||
<article class="case">
|
||||
<h2>Inline SVG coordinates</h2>
|
||||
<svg width="120" height="32" viewBox="0 0 120 32" aria-hidden="true">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" fill="none" stroke="#333" />
|
||||
</svg>
|
||||
</article>
|
||||
<article class="case">
|
||||
<h2>Script constants</h2>
|
||||
<script>
|
||||
const colorStops = ['01', '02', '03', '04'];
|
||||
const luminance = 0.2126 + 0.7152 + 0.0722;
|
||||
const size = '11.5px';
|
||||
</script>
|
||||
<p>Implementation constants in script blocks are invisible.</p>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user