mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b913668ba4 | ||
|
|
e10cff397b | ||
|
|
0c05cb8d2b | ||
|
|
4e985f68c0 | ||
|
|
c8e973b324 | ||
|
|
63074dd362 |
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -18,18 +18,18 @@ colors:
|
||||
graphite-2: "oklch(19% 0.008 95)" # one step up from graphite
|
||||
|
||||
# Text
|
||||
champagne: "oklch(84% 0.035 82)" # headlines, <strong>
|
||||
text-warm: "oklch(81% 0.03 82)" # body
|
||||
text-muted: "oklch(63% 0.024 82)" # captions, meta
|
||||
text-faint: "oklch(52% 0.018 82)" # subdued
|
||||
text-mute-deep: "oklch(48% 0.018 82)" # disabled
|
||||
champagne: "oklch(91% 0 0)" # headlines, <strong> — neutral white (name kept for compat)
|
||||
text-warm: "oklch(88% 0 0)" # body — neutral near-white
|
||||
text-muted: "oklch(72% 0 0)" # captions, meta
|
||||
text-faint: "oklch(62% 0 0)" # subdued
|
||||
text-mute-deep: "oklch(52% 0 0)" # disabled
|
||||
|
||||
# Gold ramp
|
||||
kinpaku-pale: "oklch(86% 0.07 84)" # hover lift, pale fills
|
||||
kinpaku-rich: "oklch(77% 0.13 82)" # active CTA, severity-medium
|
||||
kinpaku-deep: "oklch(61% 0.085 78)" # borders against the brand
|
||||
gold-hairline: "oklch(58% 0.065 82 / 0.32)" # default rule
|
||||
gold-hairline-strong: "oklch(74% 0.09 82 / 0.6)" # active rule
|
||||
gold-hairline: "oklch(78% 0 0 / 0.16)" # default rule — neutral (name kept for compat)
|
||||
gold-hairline-strong: "oklch(74% 0.09 82 / 0.6)" # active rule — gold
|
||||
|
||||
# Patina ramp
|
||||
patina-pale: "oklch(82% 0.07 188)" # hover lift on patina
|
||||
@@ -45,7 +45,7 @@ typography:
|
||||
# reads too thin in the small lockup, so the wordmark uses the solid cut.
|
||||
fontFamily: "Alumni Sans, Alumni Sans Pinstripe, Albert Sans, Arial, sans-serif"
|
||||
fontSize: "1.3rem"
|
||||
fontWeight: 500
|
||||
fontWeight: 400
|
||||
letterSpacing: "0.15em"
|
||||
lineHeight: 1
|
||||
display:
|
||||
@@ -183,7 +183,7 @@ Every class below is a global primitive. Drop it on any element on any page usin
|
||||
|
||||
- `.ks-brand` — wrapper for the brand mark + wordmark lockup (anchor or div).
|
||||
- `.ks-mark` — the carved-tile glyph (a solid kinpaku square split by a diagonal slash), 38×38, no container border.
|
||||
- `.ks-wordmark` — the IMPECCABLE wordmark text, solid Alumni Sans (`--ks-font-wordmark`), uppercase, weight 500, letter-spacing 0.15em.
|
||||
- `.ks-wordmark` — the IMPECCABLE wordmark text, solid Alumni Sans (`--ks-font-wordmark`), uppercase, weight 400, letter-spacing 0.15em.
|
||||
|
||||
**Section scaffolding**
|
||||
|
||||
@@ -272,16 +272,16 @@ Do not hand-type oklch values or font sizes in page CSS. If a value isn't in the
|
||||
- **Kinpaku Rich** (`oklch(77% 0.13 82)`): Active CTA fill and severity-medium markers.
|
||||
- **Kinpaku Deep** (`oklch(61% 0.085 78)`): Secondary gold for borders, subdued icons, and large technical diagrams.
|
||||
- **Kinpaku Pale** (`oklch(86% 0.07 84)`): Hover lift and pale fills.
|
||||
- **Gold Hairline** (`oklch(58% 0.065 82 / 0.32)`): Default border and divider. The homepage's busier surfaces lift this to alpha 0.48 locally.
|
||||
- **Strong Gold Hairline** (`oklch(74% 0.09 82 / 0.6)`): Active borders, focus outlines, and structural anchors.
|
||||
- **Default Hairline** (`oklch(78% 0 0 / 0.16)`): Default border and divider. Neutral, so borders and labels don't carry warmth (token name `gold-hairline` / `--ks-rule` is legacy).
|
||||
- **Strong Gold Hairline** (`oklch(74% 0.09 82 / 0.6)`): Active borders, focus outlines, and structural anchors. Stays gold — this is where the hairline is meant to read as brand.
|
||||
|
||||
### Text
|
||||
|
||||
- **Champagne** (`oklch(84% 0.035 82)`): Headlines, `<strong>`, important labels.
|
||||
- **Warm Text** (`oklch(81% 0.03 82)`): Body copy on dark surfaces.
|
||||
- **Muted Text** (`oklch(63% 0.024 82)`): Metadata, captions, secondary labels. The homepage lifts this to 65% on busier tile backgrounds.
|
||||
- **Faint Text** (`oklch(52% 0.018 82)`): Subdued labels.
|
||||
- **Mute Deep** (`oklch(48% 0.018 82)`): Disabled copy.
|
||||
- **Champagne** (`oklch(91% 0 0)`): Headlines, `<strong>`, important labels. The brightest text tier, fully neutral (token name is legacy). Text carries no warmth at any tier; the gold accents and surfaces do.
|
||||
- **Body Text** (`oklch(88% 0 0)`): Body copy on dark surfaces. Neutral and bright so reading copy reads crisp, not mushy.
|
||||
- **Muted Text** (`oklch(72% 0 0)`): Metadata, captions, secondary labels.
|
||||
- **Faint Text** (`oklch(62% 0 0)`): Subdued labels.
|
||||
- **Mute Deep** (`oklch(52% 0 0)`): Disabled copy.
|
||||
|
||||
### Secondary and State
|
||||
|
||||
@@ -310,7 +310,7 @@ The voice is geometric and restrained. The pinstripe display face is reserved fo
|
||||
|
||||
### Hierarchy
|
||||
|
||||
- **Wordmark**: solid Alumni Sans (`--ks-font-wordmark`), weight 500, uppercase, `1.3rem`, letter-spacing `0.15em`. Brand lockup only. The pinstripe sibling reads too thin at lockup size, so the wordmark uses the weightable cut.
|
||||
- **Wordmark**: solid Alumni Sans (`--ks-font-wordmark`), weight 400, uppercase, `1.3rem`, letter-spacing `0.15em`. Brand lockup only. The pinstripe sibling reads too thin at lockup size, so the wordmark uses the weightable cut.
|
||||
- **Display · h1**: Alumni Sans Pinstripe, `clamp(3.4rem, 6.5vw, 5.6rem)`, **weight 300**, line-height 1.02, letter-spacing `-0.01em`. Hero and major statements.
|
||||
- **Headline · h2**: Alumni Sans Pinstripe, `clamp(2.6rem, 4vw, 3.4rem)`, **weight 600**, line-height 1.04. Section titles.
|
||||
- **Title · h3**: Albert Sans, `1.18rem`, weight 500, line-height 1.35. Component and panel headings.
|
||||
@@ -387,16 +387,16 @@ The footer can carry the strongest oxidation accent. Use the gold seam plus pati
|
||||
The global bottom bar and the contextual bar (configure / cycling / accept) share one chrome treatment. Source of truth: `skill/scripts/live-browser.js` (`barPaletteForTheme`, `initGlobalBar`, `initBar`). Homepage and `/live-mode` demos mirror it via `.live-demo-gbar` and `.live-demo-ctx` in `site/styles/kinpaku-kit.css`.
|
||||
|
||||
- **Surface:** Lacquer Deep (`oklch(4% 0.004 95)`), always. Picker chrome does not adapt to the host page's light/dark theme.
|
||||
- **Border:** 1.5px solid Kinpaku Gold (`oklch(84% 0.19 80.46)`).
|
||||
- **Shadow:** `0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)` (kinpaku halo + drop shadow).
|
||||
- **Border:** 1px solid neutral hairline (`oklch(92% 0 0 / 0.13)`), radius 8px. The bar reads as a quiet precise tool; gold is reserved for the brand mark and the active control, not the container outline.
|
||||
- **Shadow:** `0 16px 36px -12px oklch(0% 0 0 / 0.6)` (tight neutral drop, no gold halo ring).
|
||||
- **Brand mark:** Impeccable carved-tile icon (same SVG paths as `site/components/Header.astro` / `favicon.svg`), kinpaku fill on transparent ground. Not a "/" slash or rounded-square placeholder.
|
||||
- **Default controls:** Champagne labels at rest (`oklch(84% 0.035 82)`), muted icons (`oklch(63% 0.024 82)`).
|
||||
- **Active toggle:** Kinpaku-dim pill background (`oklch(78% 0.12 82 / 0.18)`) with kinpaku text.
|
||||
- **Active toggle:** Crisp graphite pill (`oklch(27% 0 0)`) with kinpaku text/icon. The gold carries the "selected" signal; the pill itself is neutral, not a kinpaku-dim wash.
|
||||
- **Exit hover:** Vermilion (`oklch(58% 0.15 35)`), not a neutral gray lift.
|
||||
- **Context bar internals:** Graphite-2 pills, gold hairline dividers, kinpaku Go/Accept CTAs with lacquer-deep text.
|
||||
- **DESIGN.md toggle icon:** Four-quadrant swatch; bottom-right uses warm charcoal (`oklch(34% 0.014 82)`) so the tile reads against lacquer-deep, not void-black.
|
||||
|
||||
**The Picker Is Brand Rule.** Live mode UI is Impeccable product chrome, not host-page chrome. It always ships the full kinpaku border, lacquer-deep fill, and carved-tile mark.
|
||||
**The Picker Is Brand Rule.** Live mode UI is Impeccable product chrome, not host-page chrome. It always ships the lacquer-deep fill, the carved-tile mark, and gold on the mark + active control. Brand reads through the mark and the gold accent rather than a gold border ringing the bar; the container itself stays a quiet neutral-edged tool.
|
||||
|
||||
## 7. Do and Do Not
|
||||
|
||||
|
||||
+46
-173
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, readFileSync, readdirSync, statSync, lstatSync, symlinkSync, readlinkSync, unlinkSync, mkdirSync, writeFileSync, rmSync, renameSync, createWriteStream, realpathSync } from 'node:fs';
|
||||
import { existsSync, readFileSync, readdirSync, statSync, lstatSync, unlinkSync, mkdirSync, writeFileSync, rmSync, renameSync, createWriteStream, realpathSync } from 'node:fs';
|
||||
import { join, resolve, dirname } from 'node:path';
|
||||
import { createInterface } from 'node:readline';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -237,31 +237,6 @@ function isAlreadyInstalled(root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function escapeRegex(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function prefixSkillContent(content, prefix, allSkillNames) {
|
||||
// Prefix the name in frontmatter
|
||||
let result = content.replace(/^name:\s*(.+)$/m, (_, name) => `name: ${prefix}${name.trim()}`);
|
||||
|
||||
// Prefix cross-references: /skillname -> /prefix-skillname
|
||||
const sorted = [...allSkillNames].sort((a, b) => b.length - a.length);
|
||||
for (const name of sorted) {
|
||||
// Command invocations: /skillname
|
||||
result = result.replace(
|
||||
new RegExp(`/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'),
|
||||
`/${prefix}`
|
||||
);
|
||||
// Prose references: "the skillname skill"
|
||||
result = result.replace(
|
||||
new RegExp(`(the) ${escapeRegex(name)} skill`, 'gi'),
|
||||
(_, article) => `${article} ${prefix}${name} skill`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isSkillDir(skillsDir, name) {
|
||||
// Skill entries can be real directories or symlinks to directories (npx skills uses symlinks)
|
||||
const full = join(skillsDir, name);
|
||||
@@ -279,63 +254,40 @@ function isRealSkillDir(skillsDir, name) {
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function renameSkillsWithPrefix(root, prefix) {
|
||||
// First pass: collect all skill names across all providers (use first provider found)
|
||||
let allSkillNames = [];
|
||||
/**
|
||||
* One-way migration for installs from the era when the CLI offered a command
|
||||
* prefix (default `i-`), renaming the skill to e.g. `i-impeccable`. The prefix
|
||||
* only earned its keep when every command was its own skill; with a single
|
||||
* `impeccable` skill it does nothing, so it is no longer offered. Rename any
|
||||
* prefixed impeccable skill back to the canonical `impeccable` (the fresh
|
||||
* install/update content lands there next) so users aren't left with a stale,
|
||||
* orphaned `i-impeccable` alongside the new one. Scoped to the impeccable skill
|
||||
* by name -- never touches third-party skills that happen to start with `i-`.
|
||||
* Returns the number of skills migrated.
|
||||
*/
|
||||
function migrateUnprefixImpeccable(root) {
|
||||
let migrated = 0;
|
||||
for (const d of PROVIDER_DIRS) {
|
||||
const skillsDir = join(root, d, 'skills');
|
||||
if (!existsSync(skillsDir)) continue;
|
||||
const entries = readdirSync(skillsDir);
|
||||
allSkillNames = entries.filter(name => isSkillDir(skillsDir, name));
|
||||
if (allSkillNames.length > 0) break;
|
||||
let entries;
|
||||
try { entries = readdirSync(skillsDir); } catch { continue; }
|
||||
for (const name of entries) {
|
||||
// A prefixed impeccable skill is `<prefix>impeccable` -- not the canonical
|
||||
// `impeccable`, and not the legacy `teach-impeccable` (cleanup handles that).
|
||||
if (name === 'impeccable' || name === 'teach-impeccable') continue;
|
||||
if (!name.endsWith('-impeccable')) continue;
|
||||
if (!isRealSkillDir(skillsDir, name)) continue;
|
||||
|
||||
const dest = join(skillsDir, 'impeccable');
|
||||
try {
|
||||
rmSync(dest, { recursive: true, force: true });
|
||||
renameSync(join(skillsDir, name), dest);
|
||||
migrated++;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: rename real dirs and update their content
|
||||
let count = 0;
|
||||
for (const d of PROVIDER_DIRS) {
|
||||
const skillsDir = join(root, d, 'skills');
|
||||
if (!existsSync(skillsDir)) continue;
|
||||
try {
|
||||
const entries = readdirSync(skillsDir);
|
||||
for (const name of entries) {
|
||||
if (name.startsWith(prefix)) continue;
|
||||
if (!isRealSkillDir(skillsDir, name)) continue;
|
||||
|
||||
const src = join(skillsDir, name);
|
||||
const dest = join(skillsDir, prefix + name);
|
||||
|
||||
renameSync(src, dest);
|
||||
|
||||
// Prefix frontmatter name + all cross-references in SKILL.md
|
||||
let content = readFileSync(join(dest, 'SKILL.md'), 'utf8');
|
||||
content = prefixSkillContent(content, prefix, allSkillNames);
|
||||
writeFileSync(join(dest, 'SKILL.md'), content);
|
||||
count++;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Third pass: fix symlinks that now point to renamed targets (npx skills uses these)
|
||||
for (const d of PROVIDER_DIRS) {
|
||||
const skillsDir = join(root, d, 'skills');
|
||||
if (!existsSync(skillsDir)) continue;
|
||||
try {
|
||||
const entries = readdirSync(skillsDir);
|
||||
for (const name of entries) {
|
||||
if (name.startsWith(prefix)) continue;
|
||||
const full = join(skillsDir, name);
|
||||
try {
|
||||
if (!lstatSync(full).isSymbolicLink()) continue;
|
||||
const target = readlinkSync(full);
|
||||
const newTarget = target.replace(new RegExp(`/${escapeRegex(name)}$`), `/${prefix}${name}`);
|
||||
unlinkSync(full);
|
||||
symlinkSync(newTarget, join(skillsDir, prefix + name));
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return count;
|
||||
return migrated;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,7 +353,6 @@ function copyProviderSkills(bundleDir, root, targets) {
|
||||
async function install(flags) {
|
||||
const force = flags.includes('--force');
|
||||
const yes = flags.includes('-y') || flags.includes('--yes');
|
||||
const prefixFlag = flags.find(f => f.startsWith('--prefix='));
|
||||
const providersFlag = flags.find(f => f.startsWith('--providers='));
|
||||
const root = findProjectRoot();
|
||||
const existing = isAlreadyInstalled(root);
|
||||
@@ -442,6 +393,10 @@ async function install(flags) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Retire any old `i-`-prefixed install so the fresh copy lands on the
|
||||
// canonical `impeccable` dir instead of orphaning the prefixed one.
|
||||
migrateUnprefixImpeccable(root);
|
||||
|
||||
let written = 0;
|
||||
try {
|
||||
written = copyProviderSkills(bundleDir, root, targets);
|
||||
@@ -458,27 +413,6 @@ async function install(flags) {
|
||||
}
|
||||
console.log(`Installed impeccable into: ${targets.join(', ')}`);
|
||||
|
||||
// Ask about prefixing (skip in CI mode unless --prefix= is set)
|
||||
let prefix = '';
|
||||
if (prefixFlag) {
|
||||
prefix = prefixFlag.split('=')[1] || 'i-';
|
||||
} else if (!yes) {
|
||||
console.log();
|
||||
const wantPrefix = await ask('Prefix commands to avoid conflicts? e.g. /i-audit instead of /audit (y/N) ');
|
||||
if (wantPrefix === 'y' || wantPrefix === 'yes') {
|
||||
const custom = await ask('Prefix (default: i-): ');
|
||||
prefix = custom || 'i-';
|
||||
}
|
||||
}
|
||||
|
||||
if (prefix) {
|
||||
const count = renameSkillsWithPrefix(root, prefix);
|
||||
if (count > 0) {
|
||||
console.log(`\nRenamed ${count} skills with "${prefix}" prefix.`);
|
||||
console.log(`Commands are now available as /${prefix}<command> (e.g. /${prefix}audit).`);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up deprecated skills from previous versions
|
||||
try {
|
||||
const { cleanup } = await import('../../../skill/scripts/cleanup-deprecated.mjs');
|
||||
@@ -491,71 +425,7 @@ async function install(flags) {
|
||||
// Cleanup script not available -- skip
|
||||
}
|
||||
|
||||
console.log(`\nDone! Run /${prefix}impeccable init in your AI harness to set up design context.\n`);
|
||||
}
|
||||
|
||||
/** Detect prefix by looking for the 'impeccable' skill (or legacy 'teach-impeccable') */
|
||||
function detectPrefix(root) {
|
||||
for (const d of PROVIDER_DIRS) {
|
||||
const skillsDir = join(root, d, 'skills');
|
||||
if (!existsSync(skillsDir)) continue;
|
||||
for (const name of readdirSync(skillsDir)) {
|
||||
if (name === 'impeccable') return '';
|
||||
if (name.endsWith('-impeccable') && name !== 'teach-impeccable') return name.slice(0, -'impeccable'.length);
|
||||
// Legacy fallback
|
||||
if (name === 'teach-impeccable') return '';
|
||||
if (name.endsWith('-teach-impeccable')) return name.slice(0, -'teach-impeccable'.length);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Undo prefixing: rename folders back and strip prefix from SKILL.md content */
|
||||
function undoPrefix(root, prefix) {
|
||||
if (!prefix) return;
|
||||
// Collect the unprefixed names (strip our prefix)
|
||||
let allPrefixedNames = [];
|
||||
for (const d of PROVIDER_DIRS) {
|
||||
const skillsDir = join(root, d, 'skills');
|
||||
if (!existsSync(skillsDir)) continue;
|
||||
allPrefixedNames = readdirSync(skillsDir).filter(n => n.startsWith(prefix) && isRealSkillDir(skillsDir, n));
|
||||
if (allPrefixedNames.length > 0) break;
|
||||
}
|
||||
const unprefixedNames = allPrefixedNames.map(n => n.slice(prefix.length));
|
||||
|
||||
for (const d of PROVIDER_DIRS) {
|
||||
const skillsDir = join(root, d, 'skills');
|
||||
if (!existsSync(skillsDir)) continue;
|
||||
for (const name of readdirSync(skillsDir)) {
|
||||
if (!name.startsWith(prefix)) continue;
|
||||
const unprefixed = name.slice(prefix.length);
|
||||
const src = join(skillsDir, name);
|
||||
const dest = join(skillsDir, unprefixed);
|
||||
|
||||
if (lstatSync(src).isSymbolicLink()) {
|
||||
const target = readlinkSync(src);
|
||||
const newTarget = target.replace(`/${name}`, `/${unprefixed}`);
|
||||
unlinkSync(src);
|
||||
symlinkSync(newTarget, dest);
|
||||
} else {
|
||||
renameSync(src, dest);
|
||||
// Strip prefix from SKILL.md content
|
||||
const skillMd = join(dest, 'SKILL.md');
|
||||
if (existsSync(skillMd)) {
|
||||
let content = readFileSync(skillMd, 'utf8');
|
||||
// Reverse the prefixing: replace prefixed names with unprefixed
|
||||
content = content.replace(new RegExp(`^name:\\s*${escapeRegex(prefix)}`, 'm'), 'name: ');
|
||||
const sorted = [...allPrefixedNames].sort((a, b) => b.length - a.length);
|
||||
for (const pName of sorted) {
|
||||
const uName = pName.slice(prefix.length);
|
||||
content = content.replace(new RegExp(`/${escapeRegex(pName)}(?=[^a-zA-Z0-9_-]|$)`, 'g'), `/${uName}`);
|
||||
content = content.replace(new RegExp(`(the) ${escapeRegex(pName)} skill`, 'gi'), `$1 ${uName} skill`);
|
||||
}
|
||||
writeFileSync(skillMd, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('\nDone! Run /impeccable init in your AI harness to set up design context.\n');
|
||||
}
|
||||
|
||||
// ─── skills update ────────────────────────────────────────────────────────────
|
||||
@@ -684,6 +554,11 @@ async function update(flags = []) {
|
||||
|
||||
try {
|
||||
|
||||
// Retire any old `i-`-prefixed install up front so the refresh lands on the
|
||||
// canonical `impeccable` dir rather than orphaning the prefixed copy.
|
||||
const migrated = migrateUnprefixImpeccable(root);
|
||||
if (migrated > 0) console.log('Migrated a prefixed install back to /impeccable (the i- prefix is no longer used).');
|
||||
|
||||
// Copy from the bundle to each unique provider folder.
|
||||
// Deduplicate so symlinked dirs (e.g. .claude/skills -> .agents/skills)
|
||||
// are only written once with the correct provider's content.
|
||||
@@ -706,13 +581,6 @@ async function update(flags = []) {
|
||||
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
|
||||
// Re-apply prefix if detected
|
||||
const prefix = detectPrefix(root);
|
||||
if (prefix) {
|
||||
const count = renameSkillsWithPrefix(root, prefix);
|
||||
if (count > 0) console.log(`Re-applied "${prefix}" prefix to ${count} skills.`);
|
||||
}
|
||||
|
||||
// Run cleanup to remove deprecated stubs from the fresh download
|
||||
try {
|
||||
const { cleanup: postCleanup } = await import('../../../skill/scripts/cleanup-deprecated.mjs');
|
||||
@@ -744,6 +612,11 @@ function copyDirSync(src, dest) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Test surface ───────────────────────────────────────────────────────────
|
||||
// Exported so the test suite exercises the real implementation rather than a
|
||||
// reimplementation in a helper script (which is how bugs slip through).
|
||||
export { migrateUnprefixImpeccable };
|
||||
|
||||
// ─── Router ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function run(args) {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"version": "2.3.1",
|
||||
"version": "2.3.2",
|
||||
"author": "Paul Bakaus",
|
||||
"description": "Design skills, commands, and anti-pattern detection for AI coding agents",
|
||||
"keywords": [
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
@@ -34,9 +34,25 @@ const { activeNav } = Astro.props;
|
||||
<a href="/slop" data-nav="slop" aria-current={activeNav === 'slop' ? 'page' : undefined}>Slop</a>
|
||||
<a href="/live-mode" data-nav="live" aria-current={activeNav === 'live' ? 'page' : undefined}>Live</a>
|
||||
</nav>
|
||||
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 30k stars">
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
data-theme-toggle
|
||||
aria-label="Switch to light mode"
|
||||
title="Light mode"
|
||||
data-next-theme="light"
|
||||
>
|
||||
<svg class="theme-toggle-icon--light" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4.5"/>
|
||||
<path d="M12 2v2M12 20v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M2 12h2M20 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
<svg class="theme-toggle-icon--dark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
|
||||
<path d="M21 14.5A8.5 8.5 0 0 1 9.5 3a7 7 0 1 0 11.5 11.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 31k stars">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
||||
<span class="site-header-github-label">30k</span>
|
||||
<span class="site-header-github-label">31k</span>
|
||||
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,6 @@ const rowB = all.slice(half);
|
||||
---
|
||||
|
||||
<section class="testimonials-section" id="testimonials" aria-label="What people say about Impeccable">
|
||||
<div class="t-plinth t-plinth--shelf">
|
||||
<div class="t-marquee" aria-hidden="false">
|
||||
|
||||
<div class="t-marquee-row">
|
||||
@@ -71,5 +70,4 @@ const rowB = all.slice(half);
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -75,7 +75,7 @@ Useful pins to try:
|
||||
- `/impeccable pin live` for the browser iteration flow
|
||||
- `/impeccable pin critique` for design review
|
||||
|
||||
To remove: `/impeccable unpin critique`. Pins live as directories prefixed with `i-` in your harness skills folder (`.claude/skills/i-critique/`, `.cursor/skills/i-critique/`, etc.), so you can also delete them manually.
|
||||
To remove: `/impeccable unpin critique`. Pins live as directories named after the command in your harness skills folder (`.claude/skills/critique/`, `.cursor/skills/critique/`, etc.), so you can also delete them manually.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ This auto-detects your harness and writes the build compiled for it to the right
|
||||
|
||||
Prefer a different setup? Claude Code users can install the plugin with `/plugin marketplace add pbakaus/impeccable`, and the general-purpose `npx skills add pbakaus/impeccable` still works (though it installs one shared build for all harnesses rather than the one compiled for yours).
|
||||
|
||||
When a new version ships later, run `npx impeccable skills update` from the same project root. `npx impeccable skills check` tells you first whether you are behind, and plugin users update from the `/plugin` menu instead.
|
||||
|
||||
## Step 2. Set up Impeccable for your project
|
||||
|
||||
This is the most important step. Design without context produces generic output. The `/impeccable init` command runs a short discovery interview and writes a `PRODUCT.md` file at the root of your project.
|
||||
|
||||
+21
-1
@@ -6,6 +6,7 @@ import Footer from '../components/Footer.astro';
|
||||
// to re-import from page-level stylesheets.
|
||||
import '../styles/kinpaku-tokens.css';
|
||||
import '../styles/kinpaku-kit.css';
|
||||
import '../styles/light-mode.css';
|
||||
import '../styles/footer.css';
|
||||
|
||||
interface Props {
|
||||
@@ -63,6 +64,21 @@ const resolvedOgDescription = ogDescription || description;
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description}>
|
||||
<meta name="theme-color" content="#010101">
|
||||
<script is:inline>
|
||||
(function () {
|
||||
try {
|
||||
var t = localStorage.getItem('impeccable-theme');
|
||||
var root = document.documentElement;
|
||||
if (t === 'light') {
|
||||
root.classList.add('light');
|
||||
var meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) meta.setAttribute('content', '#f7f4ef');
|
||||
} else {
|
||||
root.classList.add('dark');
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
{noIndex && <meta name="robots" content="noindex">}
|
||||
{canonical && <link rel="canonical" href={canonical}>}
|
||||
|
||||
@@ -84,7 +100,7 @@ const resolvedOgDescription = ogDescription || description;
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Albert+Sans:wght@300;400;500;600;700&family=Alumni+Sans:wght@500;600;700&family=Alumni+Sans+Pinstripe&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Albert+Sans:wght@300;400;500;600;700&family=Alumni+Sans:wght@400;500;600;700&family=Alumni+Sans+Pinstripe&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<slot name="head" />
|
||||
</head>
|
||||
<body class={bodyClass}>
|
||||
@@ -97,5 +113,9 @@ const resolvedOgDescription = ogDescription || description;
|
||||
</main>
|
||||
{!hideFooter && <Footer />}
|
||||
<slot name="scripts" />
|
||||
<script>
|
||||
import { initThemeToggle } from '../scripts/utils/theme.js';
|
||||
initThemeToggle();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -67,11 +67,18 @@ import '../styles/changelog-faq-kinpaku.css';
|
||||
<li><strong>A faster detector with no jsdom.</strong> The HTML/CSS engine was rebuilt from the ground up on <code>htmlparser2</code> and a real CSS cascade resolver, replacing jsdom. On the same 160-file HTML corpus it runs about 20x faster under Node: 0.34s where the old jsdom engine took 6.8s, roughly 2 ms per file instead of 43 ms. Dependency-free and small enough to bundle straight into the skill and run inline, not just in the CLI and the extension.</li>
|
||||
<li><strong>Detector: 14 new rules.</strong> <code>cream-palette</code>, <code>em-dash-overuse</code>, <code>marketing-buzzword</code>, <code>numbered-section-markers</code>, <code>aphoristic-cadence</code>, <code>theater-slop-phrase</code>, <code>oversized-h1</code>, <code>extreme-negative-tracking</code>, <code>gpt-thin-border-wide-shadow</code>, <code>repeating-stripes-gradient</code>, <code>image-hover-transform</code>, <code>broken-image</code>, <code>text-overflow</code>, and <code>clipped-overflow-container</code>. 41 deterministic rules total, one canonical registry feeding the CLI, the browser extension, critique, and the evals.</li>
|
||||
<li><strong>The skill keeps itself current.</strong> On the first session of the day, Impeccable quietly checks whether a newer version shipped. If one has, it offers to run <code>npx impeccable skills update</code> for you. It always asks first, never nags about a version you declined, and never interrupts the task you're on. Set <code>IMPECCABLE_NO_UPDATE_CHECK=1</code> to turn it off.</li>
|
||||
<li><strong>Codex gets its asset-producer subagent automatically.</strong> Codex reads custom subagents from <code>.codex/agents/</code>, a directory separate from where it reads skills, so installers used to leave the agent behind. Now the skill bundles the agent, notices on boot when a Codex project is missing it, and offers to copy it into place. Fixes the gap reported in #161.</li>
|
||||
<li><strong>Sharper craft under the hood.</strong> Beyond the bans, the craft itself got tighter. Small defaults landed where they pay off, like <code>text-wrap: balance</code> on headings, which cleaned up ragged hero type across the ablation runs. And the instructions themselves got leaner: orphan reference files folded into their commands, context loading simplified, and a new LLM-backed test suite that catches instruction-following regressions across three providers on every change. Plus dedicated <a href="/changelog">/changelog</a> and <a href="/faq">/faq</a> pages.</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">
|
||||
<li><strong>The <code>i-</code> command prefix is gone.</strong> Opting into a prefix at install time was a holdover from when every command was its own skill. With a single <code>impeccable</code> skill it only ever renamed that one skill to <code>i-impeccable</code>, while the install message wrongly promised <code>/i-audit</code> style commands that never existed, and the rename could clobber unrelated third-party skills in the same harness folder. The flag and the prompt are removed.</li>
|
||||
<li><strong>Existing prefixed installs heal themselves.</strong> <code>skills install</code> and <code>skills update</code> now rename any old <code>i-impeccable</code> (or custom-prefixed) skill back to the canonical <code>impeccable</code>, scoped by name so a third-party skill that happens to start with <code>i-</code> is left untouched. Want a short top-level command? <code>/impeccable pin audit</code> still makes <code>/audit</code> a standalone shortcut.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article id="cli-v2.3.1" class="cf-entry">
|
||||
<header class="cf-entry-head"><span class="cf-version">CLI v2.3.1</span><span class="cf-date">May 28, 2026</span></header>
|
||||
<ul class="cf-items">
|
||||
|
||||
@@ -41,6 +41,22 @@ import '../../styles/design-system.css';
|
||||
<a href="#components">Components</a>
|
||||
<a href="#proof">Proof</a>
|
||||
</nav>
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
data-theme-toggle
|
||||
aria-label="Switch to light mode"
|
||||
title="Light mode"
|
||||
data-next-theme="light"
|
||||
>
|
||||
<svg class="theme-toggle-icon--light" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4.5"/>
|
||||
<path d="M12 2v2M12 20v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M2 12h2M20 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
<svg class="theme-toggle-icon--dark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
|
||||
<path d="M21 14.5A8.5 8.5 0 0 1 9.5 3a7 7 0 1 0 11.5 11.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<a class="ks-button ks-button-primary" href="#actions">
|
||||
Start audit
|
||||
<span class="ks-button-arrow" aria-hidden="true">
|
||||
|
||||
@@ -391,17 +391,27 @@ import '../../styles/designing-kinpaku.css';
|
||||
<div class="designing-phase-body">
|
||||
<div class="ks-bento designing-lanes">
|
||||
<article class="ks-bento-tile ks-bento-tile--span-6">
|
||||
<p class="designing-lane-rule">Design IS the product. Marketing, landing, editorial, long-form, portfolio.</p>
|
||||
<p class="designing-lane-rule"><strong>Brand.</strong> Design is the deliverable: landing pages, campaigns, editorial, portfolios. Distinctive type, committed palette, image-led heroes. The impression is the product.</p>
|
||||
<div class="designing-lane-mock designing-lane-mock--brand" aria-hidden="true">
|
||||
<span class="designing-lane-mock-label">No. 04 · Dispatch</span>
|
||||
<span class="designing-lane-mock-title">Letters, occasionally.</span>
|
||||
<span class="designing-lane-mock-label">Issue 04 · Dispatch</span>
|
||||
<span class="designing-lane-mock-title"><span class="designing-lane-mock-accent">Shape</span> the story.</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="ks-bento-tile ks-bento-tile--span-6">
|
||||
<p class="designing-lane-rule">Design serves the task. App UI, admin, dashboards, tools.</p>
|
||||
<p class="designing-lane-rule"><strong>Product.</strong> Design serves the task: app UI, admin, dashboards, tools. Fluent density, semantic states, repeatable components. Users are here to finish something.</p>
|
||||
<div class="designing-lane-mock designing-lane-mock--product" aria-hidden="true">
|
||||
<span class="designing-lane-mock-label">Newsletter</span>
|
||||
<span class="designing-lane-mock-title">Subscribe to updates</span>
|
||||
<div class="designing-lane-product-row">
|
||||
<span class="designing-lane-product-k">Users</span>
|
||||
<span class="designing-lane-product-v">12,482</span>
|
||||
</div>
|
||||
<div class="designing-lane-product-row">
|
||||
<span class="designing-lane-product-k">Active now</span>
|
||||
<span class="designing-lane-product-v">1,207</span>
|
||||
</div>
|
||||
<div class="designing-lane-product-row">
|
||||
<span class="designing-lane-product-k">Conversion</span>
|
||||
<span class="designing-lane-product-v">4.8%</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ import '../styles/changelog-faq-kinpaku.css';
|
||||
<details id="update" class="cf-faq-item">
|
||||
<summary class="cf-faq-question">How do I update to the latest version?</summary>
|
||||
<div class="cf-faq-answer">
|
||||
<p>Run <code>npx impeccable skills update</code> from your project root. It downloads the latest skills, cleans up deprecated files, and preserves any prefix you use.</p>
|
||||
<p>Run <code>npx impeccable skills update</code> from your project root. It downloads the latest skills and cleans up deprecated files. Not sure you're behind? <code>npx impeccable skills check</code> compares what you have installed against the latest release first.</p>
|
||||
<ul>
|
||||
<li><strong>Reinstall:</strong> <code>npx impeccable skills install --force</code> installs fresh.</li>
|
||||
<li><strong>Claude Code plugin:</strong> Open <code>/plugin</code> in Claude Code.</li>
|
||||
@@ -51,7 +51,7 @@ import '../styles/changelog-faq-kinpaku.css';
|
||||
<li><code>/impeccable pin audit</code> → <code>/audit</code> works again</li>
|
||||
<li><code>/impeccable pin live</code> → <code>/live</code> works again</li>
|
||||
</ul>
|
||||
<p>To remove: <code>/impeccable unpin critique</code>. To see your current pins, check your harness skills directory (<code>.claude/skills/</code>, <code>.cursor/skills/</code>, etc.) for directories prefixed with <code>i-</code>.</p>
|
||||
<p>To remove: <code>/impeccable unpin critique</code>. To see your current pins, check your harness skills directory (<code>.claude/skills/</code>, <code>.cursor/skills/</code>, etc.) for directories named after the command you pinned, like <code>.claude/skills/critique/</code>.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
+43
-36
@@ -71,7 +71,7 @@ import '../styles/testimonials.css';
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
<a href="/designing" class="ks-button ks-button-secondary">How it works</a>
|
||||
<a href="/designing" class="ks-button ks-button-ghost">How it works</a>
|
||||
</div>
|
||||
|
||||
<div class="hero-rebuild-logos" aria-label="AI coding harnesses Impeccable works with">
|
||||
@@ -250,7 +250,7 @@ import '../styles/testimonials.css';
|
||||
<div class="slop-teaser-head">
|
||||
<h2 id="slop-teaser-title" class="slop-teaser-title">Desloppification</h2>
|
||||
<p class="slop-teaser-body">
|
||||
Skills can't make a model fundamentally better at design. What they can do is remove inherent bias: the AI slop tells, the bad defaults, across every design discipline. That's what Impeccable does.
|
||||
Impeccable strips the AI slop tells and bad defaults out of every design discipline, from type to motion to copy. A skill can't make the model a better designer; it can clear the reflexes that make its output look generated.
|
||||
</p>
|
||||
<div class="slop-teaser-actions">
|
||||
<a class="slop-teaser-cta-primary" href="/slop">
|
||||
@@ -541,11 +541,13 @@ import '../styles/testimonials.css';
|
||||
<span class="why-live-pick-note">more elegant<span class="why-live-pick-caret"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="why-live-gbar">
|
||||
<span class="why-live-gbar-brand">/</span>
|
||||
<span class="why-live-gbar-cmd">bolder</span>
|
||||
<span class="why-live-gbar-count">×3</span>
|
||||
<span class="why-live-gbar-go">Go →</span>
|
||||
<div class="why-live-ctx live-demo-ctx" data-phase="configuring">
|
||||
<div class="live-demo-ctx-row live-demo-ctx-row--configure">
|
||||
<span class="live-demo-ctx-pill">bolder<span class="live-demo-ctx-pill-caret" aria-hidden="true">▾</span></span>
|
||||
<span class="live-demo-ctx-input"><span class="live-demo-ctx-caret"></span></span>
|
||||
<span class="live-demo-ctx-count">×3</span>
|
||||
<span class="live-demo-ctx-go">Go <span aria-hidden="true">→</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -587,17 +589,37 @@ import '../styles/testimonials.css';
|
||||
</div>
|
||||
|
||||
<div class="downloads-rebuild-install">
|
||||
<div class="downloads-rebuild-cmd">
|
||||
<code>npx impeccable skills install</code>
|
||||
<button class="downloads-rebuild-copy" type="button" aria-label="Copy install command" data-copy="npx impeccable skills install">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
||||
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
|
||||
</svg>
|
||||
<span>Copy</span>
|
||||
</button>
|
||||
<div class="downloads-rebuild-cmd-group">
|
||||
<span class="downloads-rebuild-cmd-label">Install</span>
|
||||
<div class="downloads-rebuild-cmd">
|
||||
<span class="downloads-rebuild-prompt" aria-hidden="true">$</span>
|
||||
<code>npx impeccable skills install</code>
|
||||
<button class="downloads-rebuild-copy" type="button" aria-label="Copy install command" data-copy="npx impeccable skills install">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
||||
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
|
||||
</svg>
|
||||
<span>Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="downloads-rebuild-note">Installs the build compiled for your harness. Works with Cursor, Claude Code, Gemini CLI, Codex CLI, and every other major AI coding harness.</p>
|
||||
|
||||
<div class="downloads-rebuild-cmd-group">
|
||||
<span class="downloads-rebuild-cmd-label downloads-rebuild-cmd-label--update">Update</span>
|
||||
<div class="downloads-rebuild-cmd downloads-rebuild-cmd--update">
|
||||
<span class="downloads-rebuild-prompt" aria-hidden="true">$</span>
|
||||
<code>npx impeccable skills update</code>
|
||||
<button class="downloads-rebuild-copy" type="button" aria-label="Copy update command" data-copy="npx impeccable skills update">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
||||
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
|
||||
</svg>
|
||||
<span>Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="downloads-rebuild-note">Works with Cursor, Claude Code, Gemini CLI, Codex CLI, and every other major AI coding harness. <code>install</code> sets up the build for your harness; <code>update</code> pulls the latest. Run <code>npx impeccable skills check</code> to see if you're behind. Installed the Claude Code plugin? Update it from the <code>/plugin</code> menu instead.</p>
|
||||
|
||||
<details class="downloads-rebuild-alts">
|
||||
<summary class="downloads-rebuild-alts-summary">
|
||||
@@ -652,29 +674,14 @@ import '../styles/testimonials.css';
|
||||
<a href="https://www.npmjs.com/package/impeccable" target="_blank" rel="noopener">View on npm →</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Stay updated</strong>
|
||||
<span>New commands, new anti-patterns, and the design thinking behind them. Subscribe to the Substack, or follow along on X.</span>
|
||||
<a href="https://impeccablestyle.substack.com" target="_blank" rel="noopener" class="downloads-rebuild-link-primary">Subscribe on Substack →</a>
|
||||
<strong>Follow along</strong>
|
||||
<span>The design thinking behind new commands and anti-patterns, in long form. Read the newsletter, or catch the shorter notes on X.</span>
|
||||
<a href="https://impeccablestyle.substack.com" target="_blank" rel="noopener" class="downloads-rebuild-link-primary">Read the newsletter →</a>
|
||||
<a href="https://x.com/impeccable_ai" target="_blank" rel="noopener" class="downloads-rebuild-link-secondary">Follow @impeccable_ai on X →</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ol class="downloads-rebuild-examples">
|
||||
<li>
|
||||
<code><span class="downloads-rebuild-slash">/</span>impeccable redo this hero</code>
|
||||
<span>Let the skill pick the approach. No command names to memorize.</span>
|
||||
</li>
|
||||
<li>
|
||||
<code><span class="downloads-rebuild-slash">/</span>impeccable audit checkout</code>
|
||||
<span>Reach for a command by name. Type <code>/impeccable</code> alone to see all 23.</span>
|
||||
</li>
|
||||
<li>
|
||||
<code><span class="downloads-rebuild-slash">/</span>impeccable pin audit</code>
|
||||
<span>Pin your favorites. <code>/audit</code> becomes a standalone shortcut.</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<p class="downloads-rebuild-more">Go deeper: <a href="/designing">Designing with Impeccable</a> · <a href="/docs">the command docs</a></p>
|
||||
<p class="downloads-rebuild-more">Learn more: <a href="/designing">Designing with Impeccable</a> · <a href="/docs">the command docs</a></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
@@ -240,15 +240,13 @@ export class PeriodicTable {
|
||||
el.style.cssText = `
|
||||
width: 56px;
|
||||
height: 64px;
|
||||
background: ${colors.bg};
|
||||
border: 1.5px solid ${colors.border};
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
transition: transform 0.15s ease, border-color 0.15s ease;
|
||||
position: relative;
|
||||
font-family: inherit;
|
||||
padding: 0;
|
||||
@@ -328,19 +326,16 @@ export class PeriodicTable {
|
||||
// Hover/focus: show tooltip
|
||||
const activate = () => {
|
||||
el.style.transform = 'translateY(-2px)';
|
||||
el.style.boxShadow = `0 4px 12px ${colors.border}40`;
|
||||
this.showTooltip(el, cmd);
|
||||
|
||||
if (this.activeElement && this.activeElement !== el) {
|
||||
this.activeElement.style.transform = 'translateY(0)';
|
||||
this.activeElement.style.boxShadow = 'none';
|
||||
}
|
||||
this.activeElement = el;
|
||||
};
|
||||
|
||||
const deactivate = () => {
|
||||
el.style.transform = 'translateY(0)';
|
||||
el.style.boxShadow = 'none';
|
||||
this.hideTooltip();
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@ let sourceCache = {}; // Cache fetched source content
|
||||
|
||||
const MOBILE_BREAKPOINT = 900;
|
||||
|
||||
// Setup / management commands that aren't "steering" verbs. They're kept off
|
||||
// the command palette (fisheye + mobile carousel) but still appear in the
|
||||
// periodic table (rendered separately by framework-viz.js).
|
||||
const PALETTE_EXCLUDED = new Set(['impeccable', 'init', 'extract', 'document', 'live']);
|
||||
|
||||
function isMobile() {
|
||||
return window.innerWidth <= MOBILE_BREAKPOINT;
|
||||
}
|
||||
@@ -75,7 +80,7 @@ function renderDesktopLayout(container, commands) {
|
||||
// too (when they were rendered as 'impeccable craft' etc.) but are now
|
||||
// first-class sub-commands that should appear in the gallery.
|
||||
const deprecated = new Set(['teach-impeccable', 'frontend-design', 'arrange', 'normalize', 'onboard', 'impeccable craft', 'impeccable teach', 'impeccable extract']);
|
||||
const filteredCommands = commands.filter(c => !deprecated.has(c.id));
|
||||
const filteredCommands = commands.filter(c => !deprecated.has(c.id) && !PALETTE_EXCLUDED.has(c.id));
|
||||
|
||||
const categoryOrder = ['create', 'evaluate', 'refine', 'simplify', 'harden', 'system'];
|
||||
const categoryLabelsShort = {
|
||||
@@ -308,7 +313,7 @@ function setupFisheyeList(commands, headerIndices = []) {
|
||||
// to a fractional "center index" which drives everything.
|
||||
|
||||
const BASE_H = 36; // height of the center (scale=1) item
|
||||
const MIN_SCALE = 0.35;
|
||||
const MIN_SCALE = 0.52; // off-center items stay legibly sized, not microscopic
|
||||
const RADIUS = 5;
|
||||
const count = items.length;
|
||||
const listH = list.clientHeight;
|
||||
@@ -367,7 +372,10 @@ function setupFisheyeList(commands, headerIndices = []) {
|
||||
items.forEach((item, i) => {
|
||||
const h = heights[i];
|
||||
const scale = getScale(Math.abs(i - center));
|
||||
const opacity = 0.25 + (scale - MIN_SCALE) / (1 - MIN_SCALE) * 0.75;
|
||||
// Floor at 0.62 so off-center command names stay readable (WCAG): the
|
||||
// full vocabulary is the point of this view. Focus still reads clearly
|
||||
// via scale + the gold/weight active state, not by crushing legibility.
|
||||
const opacity = 0.62 + (scale - MIN_SCALE) / (1 - MIN_SCALE) * 0.38;
|
||||
|
||||
item.style.top = `${y}px`;
|
||||
item.style.transform = `scale(${scale})`;
|
||||
@@ -478,6 +486,9 @@ function truncateDescription(text, maxLen = 120) {
|
||||
// ============================================
|
||||
|
||||
function renderMobileLayout(container, commands) {
|
||||
// Keep setup/management commands off the palette (they stay in the periodic
|
||||
// table); match the desktop fisheye filter.
|
||||
commands = commands.filter(c => !PALETTE_EXCLUDED.has(c.id));
|
||||
// Build carousel pills
|
||||
// Carousel pills show bare command names for sub-commands, and /impeccable
|
||||
// for the root entry.
|
||||
|
||||
@@ -34,13 +34,13 @@ export const skillFocusAreas = {
|
||||
// detail line communicates what bad-default we catch — not what we teach.
|
||||
export const slopFocusAreas = {
|
||||
'impeccable': [
|
||||
{ area: 'Typography', detail: 'No Inter monoculture. No flat hierarchy. No all-caps body.' },
|
||||
{ area: 'Color & Contrast', detail: 'No purple gradients. No sub-WCAG text. No washed pastels.' },
|
||||
{ area: 'Spatial Design', detail: 'No cards in cards. No identical grids. No template layouts.' },
|
||||
{ area: 'Responsive', detail: 'No stiff breakpoints. No mobile-as-afterthought.' },
|
||||
{ area: 'Interaction', detail: 'No modal abuse. No generic CTAs. No gradient text.' },
|
||||
{ area: 'Motion', detail: 'No bouncy easing. No decorative fades. No spring overshoot.' },
|
||||
{ area: 'UX Writing', detail: 'No "Welcome to our platform." No em dashes. No "let\'s dive in".' }
|
||||
{ area: 'Typography', detail: 'A type scale with real contrast, set in a face chosen on purpose, not Inter by reflex.' },
|
||||
{ area: 'Color & Contrast', detail: 'Palettes that clear WCAG and commit to a hue. The purple-gradient default never ships.' },
|
||||
{ area: 'Spatial Design', detail: 'Structure from spacing and rhythm, not a grid of identical cards nested in more cards.' },
|
||||
{ area: 'Responsive', detail: 'Built for the small screen first, with breakpoints that follow the content.' },
|
||||
{ area: 'Interaction', detail: 'Honest states and clear affordances, without defaulting to a modal or gradient text.' },
|
||||
{ area: 'Motion', detail: 'Motion that eases like physics and settles. Nothing bounces or springs past its mark.' },
|
||||
{ area: 'UX Writing', detail: 'Specific copy that names what the thing does, not "Welcome to our platform."' }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
+22
-34
@@ -1,56 +1,44 @@
|
||||
// Theme toggle - supports light, dark, and system preference
|
||||
// Theme toggle — light / dark with localStorage persistence.
|
||||
|
||||
const STORAGE_KEY = 'impeccable-theme';
|
||||
|
||||
function getStoredTheme() {
|
||||
export function getStoredTheme() {
|
||||
return localStorage.getItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
function setStoredTheme(theme) {
|
||||
export function setStoredTheme(theme) {
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
export function applyTheme(theme) {
|
||||
const html = document.documentElement;
|
||||
const resolved = theme === 'light' ? 'light' : 'dark';
|
||||
|
||||
// Remove both classes first
|
||||
html.classList.remove('light', 'dark');
|
||||
html.classList.add(resolved);
|
||||
|
||||
if (theme === 'light') {
|
||||
html.classList.add('light');
|
||||
} else if (theme === 'dark') {
|
||||
html.classList.add('dark');
|
||||
const meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) {
|
||||
meta.setAttribute('content', resolved === 'light' ? '#f7f4ef' : '#010101');
|
||||
}
|
||||
// 'system' = no class, falls back to media query
|
||||
|
||||
// Update active state on buttons
|
||||
document.querySelectorAll('.theme-toggle-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.theme === theme);
|
||||
document.querySelectorAll('[data-theme-toggle]').forEach((btn) => {
|
||||
const next = resolved === 'light' ? 'dark' : 'light';
|
||||
btn.setAttribute('aria-label', resolved === 'light' ? 'Switch to dark mode' : 'Switch to light mode');
|
||||
btn.setAttribute('title', resolved === 'light' ? 'Dark mode' : 'Light mode');
|
||||
btn.dataset.nextTheme = next;
|
||||
});
|
||||
}
|
||||
|
||||
export function initThemeToggle() {
|
||||
const toggle = document.querySelector('.theme-toggle');
|
||||
if (!toggle) return;
|
||||
const stored = getStoredTheme();
|
||||
applyTheme(stored === 'light' ? 'light' : 'dark');
|
||||
|
||||
// Get stored theme or default to system
|
||||
const storedTheme = getStoredTheme() || 'system';
|
||||
applyTheme(storedTheme);
|
||||
|
||||
// Handle button clicks
|
||||
toggle.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.theme-toggle-btn');
|
||||
if (!btn) return;
|
||||
|
||||
const theme = btn.dataset.theme;
|
||||
setStoredTheme(theme);
|
||||
applyTheme(theme);
|
||||
});
|
||||
|
||||
// Listen for system preference changes (only matters when in 'system' mode)
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (getStoredTheme() === 'system' || !getStoredTheme()) {
|
||||
applyTheme('system');
|
||||
}
|
||||
document.querySelectorAll('[data-theme-toggle]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const next = btn.dataset.nextTheme || (document.documentElement.classList.contains('light') ? 'dark' : 'light');
|
||||
setStoredTheme(next);
|
||||
applyTheme(next);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,10 +98,11 @@
|
||||
.cf-items strong { color: var(--ks-champagne); font-weight: 600; }
|
||||
.cf-items code {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.92em;
|
||||
color: var(--ks-kinpaku);
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font-size: 0.84em;
|
||||
color: var(--ks-code-fg);
|
||||
background: var(--ks-code-bg);
|
||||
padding: var(--ks-code-pad);
|
||||
border-radius: var(--ks-code-radius);
|
||||
}
|
||||
.cf-items a {
|
||||
color: var(--ks-patina);
|
||||
@@ -329,10 +330,11 @@
|
||||
.cf-faq-answer strong { color: var(--ks-champagne); font-weight: 600; }
|
||||
.cf-faq-answer code {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.92em;
|
||||
color: var(--ks-kinpaku);
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font-size: 0.84em;
|
||||
color: var(--ks-code-fg);
|
||||
background: var(--ks-code-bg);
|
||||
padding: var(--ks-code-pad);
|
||||
border-radius: var(--ks-code-radius);
|
||||
}
|
||||
.cf-faq-answer a {
|
||||
color: var(--ks-patina);
|
||||
|
||||
@@ -46,11 +46,22 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.kinpaku-system-page a {
|
||||
.kinpaku-system-page a:not(.ks-brand) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Brand lockup — kinpaku gold on dark surfaces; html.light swaps to ink
|
||||
(see light-mode.css) to match impeccable-logo-on-light.svg. */
|
||||
.kinpaku-system-page a.ks-brand {
|
||||
color: var(--ks-kinpaku);
|
||||
}
|
||||
|
||||
.kinpaku-system-page a.ks-brand .ks-mark,
|
||||
.kinpaku-system-page a.ks-brand .ks-wordmark {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.kinpaku-system-page button,
|
||||
.kinpaku-system-page input,
|
||||
.kinpaku-system-page select {
|
||||
|
||||
@@ -47,10 +47,12 @@
|
||||
|
||||
.designing-kinpaku code {
|
||||
font-family: var(--ks-mono);
|
||||
color: var(--ks-kinpaku);
|
||||
background: transparent;
|
||||
font-size: 0.84em;
|
||||
color: var(--ks-code-fg);
|
||||
background: var(--ks-code-bg);
|
||||
border: 0;
|
||||
padding: 0;
|
||||
padding: var(--ks-code-pad);
|
||||
border-radius: var(--ks-code-radius);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
@@ -272,8 +274,10 @@
|
||||
|
||||
.designing-kinpaku .designing-loop-name {
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font-display);
|
||||
font-weight: 300;
|
||||
/* Pinstripe is single-weight and reads broken at ~1.5–2rem; wordmark
|
||||
carries real stroke weight at compass-label scale (cf. iterate-name). */
|
||||
font-family: var(--ks-font-wordmark);
|
||||
font-weight: 600;
|
||||
font-size: clamp(1.5rem, 2.2vw, 2rem);
|
||||
line-height: 1.05;
|
||||
letter-spacing: 0.01em;
|
||||
@@ -444,7 +448,7 @@
|
||||
============================================================================ */
|
||||
|
||||
.designing-kinpaku .designing-phase-sub code {
|
||||
color: var(--ks-kinpaku);
|
||||
color: var(--ks-code-fg);
|
||||
}
|
||||
|
||||
.designing-kinpaku .designing-phase-commands {
|
||||
@@ -459,7 +463,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: oklch(11% 0.006 95);
|
||||
background: oklch(11% 0 0);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
color: var(--ks-kinpaku);
|
||||
@@ -530,7 +534,7 @@
|
||||
.designing-kinpaku .docs-viz-file-header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
background: oklch(13% 0.006 95);
|
||||
background: oklch(13% 0 0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -740,7 +744,7 @@
|
||||
frame opposite it. */
|
||||
.designing-kinpaku .designing-iterate-terminal {
|
||||
padding: 20px 22px;
|
||||
background: oklch(13% 0.006 95);
|
||||
background: oklch(13% 0 0);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 4px;
|
||||
font-family: var(--ks-mono);
|
||||
@@ -763,7 +767,7 @@
|
||||
|
||||
/* Live frame mock — restyle docs-viz-live-* on dark surface */
|
||||
.designing-kinpaku .docs-viz-live-frame {
|
||||
background: oklch(13% 0.006 95);
|
||||
background: oklch(13% 0 0);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
@@ -774,7 +778,7 @@
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
background: oklch(11% 0.006 95);
|
||||
background: oklch(11% 0 0);
|
||||
}
|
||||
.designing-kinpaku .docs-viz-live-dot {
|
||||
width: 9px;
|
||||
@@ -804,7 +808,7 @@
|
||||
}
|
||||
.designing-kinpaku .docs-viz-live-target {
|
||||
width: 100%;
|
||||
background: oklch(11% 0.006 95);
|
||||
background: oklch(11% 0 0);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
@@ -832,7 +836,7 @@
|
||||
.designing-kinpaku .docs-viz-live-btn {
|
||||
align-self: start;
|
||||
padding: 8px 14px;
|
||||
background: var(--ks-kinpaku-pale);
|
||||
background: var(--ks-kinpaku);
|
||||
color: var(--ks-lacquer-deep);
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
@@ -846,8 +850,7 @@
|
||||
position: absolute;
|
||||
inset: 14px 14px 108px;
|
||||
border: 1.5px solid var(--ks-kinpaku);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 4px oklch(78% 0.12 82 / 0.1);
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* top: auto is required — the base/legacy rule sets top: 16px, and leaving it
|
||||
@@ -863,11 +866,9 @@
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
background: var(--ks-lacquer-deep);
|
||||
border: 1.5px solid var(--ks-kinpaku);
|
||||
border-radius: 6px;
|
||||
box-shadow:
|
||||
0 0 0 1px oklch(78% 0.12 82 / 0.18),
|
||||
0 6px 18px oklch(0% 0 0 / 0.4);
|
||||
border: 1px solid oklch(92% 0 0 / 0.13);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 6px 18px -4px oklch(0% 0 0 / 0.5);
|
||||
}
|
||||
.designing-kinpaku .docs-viz-live-ctx-nav,
|
||||
.designing-kinpaku .docs-viz-live-ctx-discard,
|
||||
@@ -885,7 +886,7 @@
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.designing-kinpaku .docs-viz-live-ctx-accept {
|
||||
background: var(--ks-kinpaku-pale);
|
||||
background: var(--ks-kinpaku);
|
||||
color: var(--ks-lacquer-deep);
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
@@ -916,11 +917,9 @@
|
||||
padding: 5px 7px;
|
||||
max-width: calc(100% - 24px);
|
||||
background: var(--ks-lacquer-deep);
|
||||
border: 1.5px solid var(--ks-kinpaku);
|
||||
border-radius: 9px;
|
||||
box-shadow:
|
||||
0 0 0 1px oklch(78% 0.12 82 / 0.18),
|
||||
0 10px 28px oklch(0% 0 0 / 0.28);
|
||||
border: 1px solid oklch(92% 0 0 / 0.13);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 28px -6px oklch(0% 0 0 / 0.5);
|
||||
}
|
||||
.designing-kinpaku .docs-viz-live-gbar-mark {
|
||||
display: inline-flex;
|
||||
@@ -944,7 +943,7 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.designing-kinpaku .docs-viz-live-gbar-btn.is-active {
|
||||
background: oklch(78% 0.12 82 / 0.18);
|
||||
background: oklch(27% 0 0);
|
||||
color: var(--ks-kinpaku);
|
||||
}
|
||||
.designing-kinpaku .docs-viz-live-gbar-btn-mono {
|
||||
@@ -1029,13 +1028,12 @@
|
||||
the 3-col grid below, not as a card competing with them. */
|
||||
.designing-kinpaku .designing-polish-band {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 18px 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--ks-rule);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
border-radius: 0;
|
||||
margin-bottom: 36px;
|
||||
@@ -1061,16 +1059,18 @@
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
.designing-kinpaku .designing-polish-band-meta {
|
||||
color: var(--ks-muted);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.18em;
|
||||
display: none; /* cryptic "03 · 04" step counter — removed */
|
||||
}
|
||||
|
||||
.designing-kinpaku .designing-polish-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 24px;
|
||||
/* Flatten the legacy outer box (docs-visuals adds a cream bg + L/R/B border +
|
||||
padding); the cards are the only surface, no box-in-box. */
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.designing-kinpaku .designing-polish-grid { grid-template-columns: 1fr; }
|
||||
@@ -1131,7 +1131,7 @@
|
||||
gap: 10px;
|
||||
margin: 2px 0;
|
||||
padding: 11px 14px;
|
||||
background: oklch(13% 0.006 95);
|
||||
background: oklch(13% 0 0);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 4px;
|
||||
font-family: var(--ks-mono);
|
||||
@@ -1150,10 +1150,11 @@
|
||||
}
|
||||
|
||||
.designing-kinpaku .designing-maintain-stage {
|
||||
background: var(--ks-lacquer-raised);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 4px;
|
||||
padding: 28px;
|
||||
/* No inner box — the demo sits directly in the bento tile (removing the
|
||||
box-in-box: plinth > tile > stage). */
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 8px 0;
|
||||
min-height: 180px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -1211,7 +1212,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 5px 11px;
|
||||
background: oklch(13% 0.006 95);
|
||||
background: oklch(13% 0 0);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 3px;
|
||||
color: var(--ks-muted);
|
||||
@@ -1226,7 +1227,7 @@
|
||||
.designing-kinpaku .designing-extract-after {
|
||||
justify-self: end;
|
||||
padding: 8px 18px;
|
||||
background: var(--ks-kinpaku-pale);
|
||||
background: var(--ks-kinpaku);
|
||||
color: var(--ks-lacquer-deep);
|
||||
border-radius: 2px;
|
||||
font-family: var(--ks-font);
|
||||
@@ -1269,6 +1270,26 @@
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Flatten the bento plinth: drop the 0.17 plinth fill + 8px gutter (which drew
|
||||
the weird gutter "borders") and the 0.07 tile fill. The two lanes sit on the
|
||||
page, split by a single center hairline. */
|
||||
.designing-kinpaku .ks-bento.designing-lanes {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
gap: 0;
|
||||
}
|
||||
.designing-kinpaku .designing-lanes .ks-bento-tile {
|
||||
background: transparent;
|
||||
padding: 0 clamp(20px, 3.5vw, 44px);
|
||||
}
|
||||
.designing-kinpaku .designing-lanes .ks-bento-tile:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
.designing-kinpaku .designing-lanes .ks-bento-tile + .ks-bento-tile {
|
||||
border-left: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.designing-kinpaku .designing-lane-kind {
|
||||
color: var(--ks-kinpaku);
|
||||
font-family: var(--ks-mono);
|
||||
@@ -1283,41 +1304,85 @@
|
||||
.designing-kinpaku .designing-lane-rule {
|
||||
color: var(--ks-text);
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
line-height: 1.65;
|
||||
margin: 0;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.designing-kinpaku .designing-lane-rule strong {
|
||||
color: var(--ks-champagne);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* The brand mock title used the pinstripe display face at ~1.6rem, which reads
|
||||
broken/washed at that size (Two-Face rule); use the clean body face. */
|
||||
.designing-kinpaku .designing-lane-mock-title {
|
||||
font-family: var(--ks-font);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.designing-kinpaku .designing-lane-mock {
|
||||
margin-top: 6px;
|
||||
padding: 24px;
|
||||
background: oklch(11% 0.006 95);
|
||||
margin-top: 18px;
|
||||
padding: 20px 18px;
|
||||
background: var(--ks-lacquer-raised);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 4px;
|
||||
min-height: 110px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
.designing-kinpaku .designing-lane-mock--brand {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.designing-kinpaku .designing-lane-mock--product {
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
}
|
||||
.designing-kinpaku .designing-lane-mock-label {
|
||||
color: var(--ks-patina);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.66rem;
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.designing-kinpaku .designing-lane-mock--brand .designing-lane-mock-title {
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font-display);
|
||||
font-weight: 300;
|
||||
font-size: 1.6rem;
|
||||
font-family: var(--ks-font);
|
||||
font-weight: 500;
|
||||
font-size: 1.35rem;
|
||||
line-height: 1.15;
|
||||
}
|
||||
.designing-kinpaku .designing-lane-mock-accent {
|
||||
color: var(--ks-kinpaku);
|
||||
}
|
||||
.designing-kinpaku .designing-lane-product-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
.designing-kinpaku .designing-lane-product-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.designing-kinpaku .designing-lane-product-k {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
.designing-kinpaku .designing-lane-product-v {
|
||||
font-family: var(--ks-font);
|
||||
font-size: 0.92rem;
|
||||
font-weight: 500;
|
||||
color: var(--ks-champagne);
|
||||
}
|
||||
|
||||
/* Legacy single-line product mock title — kept for any downstream refs. */
|
||||
.designing-kinpaku .designing-lane-mock--product .designing-lane-mock-title {
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
@@ -1344,16 +1409,20 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
gap: 0; /* divided list: items separated by hairlines, not gaps */
|
||||
}
|
||||
.designing-kinpaku .designing-avoid li {
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr;
|
||||
gap: 14px;
|
||||
padding: 22px 24px;
|
||||
background: var(--ks-lacquer-raised);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
padding: 22px 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
border-radius: 0;
|
||||
}
|
||||
.designing-kinpaku .designing-avoid li:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.designing-kinpaku .designing-avoid-x {
|
||||
color: var(--ks-vermilion);
|
||||
@@ -1370,7 +1439,7 @@
|
||||
font-size: 1rem;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.designing-kinpaku .designing-avoid-title code { color: var(--ks-kinpaku); }
|
||||
.designing-kinpaku .designing-avoid-title code { color: var(--ks-code-fg); }
|
||||
.designing-kinpaku .designing-avoid-desc {
|
||||
color: var(--ks-text);
|
||||
font-size: 0.94rem;
|
||||
|
||||
@@ -396,7 +396,7 @@
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
color: var(--ks-champagne);
|
||||
color: var(--ks-code-fg);
|
||||
font-family: var(--ks-mono);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -471,7 +471,7 @@
|
||||
.detector-table-panel td code {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--ks-champagne);
|
||||
color: var(--ks-code-fg);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
@@ -1044,17 +1044,17 @@
|
||||
.docs-kinpaku .prose code {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.84em;
|
||||
color: var(--ks-champagne);
|
||||
background: var(--ks-graphite-2);
|
||||
color: var(--ks-code-fg);
|
||||
background: var(--ks-code-bg);
|
||||
border: 0;
|
||||
padding: 4px 7px;
|
||||
border-radius: 3px;
|
||||
padding: var(--ks-code-pad);
|
||||
border-radius: var(--ks-code-radius);
|
||||
}
|
||||
|
||||
/* Inline code that is itself a link — get the kinpaku cue back, since
|
||||
the link semantic is the meaningful one. */
|
||||
.docs-kinpaku .prose a code {
|
||||
color: var(--ks-kinpaku);
|
||||
color: var(--ks-code-cmd);
|
||||
}
|
||||
|
||||
/* Fenced code blocks — deep lacquer terminal. Shiki injects inline
|
||||
@@ -1067,10 +1067,10 @@
|
||||
.docs-kinpaku .prose pre.astro-code {
|
||||
margin: 0;
|
||||
padding: 22px 24px;
|
||||
background: var(--ks-lacquer-deep) !important;
|
||||
color: oklch(86% 0.025 82) !important;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
background: var(--ks-code-block-bg) !important;
|
||||
color: var(--ks-code-block-fg) !important;
|
||||
border: 1px solid var(--ks-code-block-border);
|
||||
border-radius: var(--ks-code-block-radius);
|
||||
overflow-x: auto;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.84rem;
|
||||
@@ -1227,7 +1227,7 @@
|
||||
background: var(--ks-lacquer-raised);
|
||||
}
|
||||
|
||||
.docs-kinpaku .docs-command-demo .split-comparison {
|
||||
.docs-kinpaku .docs-command-demo :is(.split-comparison, .demo-split-comparison) {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -1253,6 +1253,9 @@
|
||||
}
|
||||
|
||||
.docs-kinpaku .docs-command-demo .split-after {
|
||||
/* Doc.astro initSplitCompare uses skewAngle: 0 — vertical seam, not the
|
||||
angled default from sub-pages.css / main.css. */
|
||||
clip-path: polygon(50% 0%, 100% 0%, 100% 100%, 50% 100%);
|
||||
background:
|
||||
linear-gradient(90deg, oklch(8% 0.006 95 / 0.92), oklch(10% 0.006 95 / 0.96)),
|
||||
radial-gradient(circle at 82% 26%, oklch(78% 0.12 82 / 0.14), transparent 12rem);
|
||||
@@ -1261,6 +1264,7 @@
|
||||
.docs-kinpaku .docs-command-demo .split-divider {
|
||||
background: var(--ks-kinpaku);
|
||||
box-shadow: 0 0 20px oklch(78% 0.12 82 / 0.4);
|
||||
transform: translateX(-50%) !important;
|
||||
}
|
||||
|
||||
.docs-kinpaku .docs-command-demo .demo-caption {
|
||||
|
||||
@@ -3347,10 +3347,10 @@
|
||||
.neon-case-command .code-block {
|
||||
margin: 0;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--ks-lacquer);
|
||||
color: var(--ks-champagne);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
background: var(--ks-code-block-bg);
|
||||
color: var(--ks-code-block-fg);
|
||||
border: 1px solid var(--ks-code-block-border);
|
||||
border-radius: var(--ks-code-block-radius);
|
||||
overflow-x: auto;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.8125rem;
|
||||
|
||||
+247
-185
@@ -6,18 +6,12 @@
|
||||
@import "./kinpaku-tokens.css";
|
||||
|
||||
.home-kinpaku {
|
||||
/* Intentional overrides: the homepage uses brighter rule alphas because
|
||||
bento tile borders need to read against busy backgrounds. The default
|
||||
--ks-rule from kinpaku-tokens.css is the quieter doctrinal value used
|
||||
on /design-system and elsewhere. */
|
||||
--ks-rule: oklch(58% 0.065 82 / 0.48);
|
||||
--ks-rule-strong: oklch(74% 0.09 82 / 0.68);
|
||||
/* --ks-rule / --ks-rule-strong inherit the global tokens (neutral default,
|
||||
gold strong); the homepage no longer diverges on border color. */
|
||||
|
||||
/* Slightly lighter muted text on the homepage's busier surfaces. The
|
||||
default --ks-text-muted (63% L) is used everywhere else; here we lift
|
||||
to 65% so captions stay readable against bento tile fills. Kept under
|
||||
the old --ks-muted name for code-churn reasons. */
|
||||
--ks-muted: oklch(65% 0.024 82);
|
||||
/* Legacy alias — homepage code reads --ks-muted; map it to the global muted
|
||||
text token (no divergent value). */
|
||||
--ks-muted: var(--ks-text-muted);
|
||||
|
||||
/* Legacy alias — homepage code still reads --ks-faint and --ks-oxide. The
|
||||
forward names live in kinpaku-tokens.css as --ks-text-faint and
|
||||
@@ -98,12 +92,13 @@
|
||||
(3) keep the chip treatment subtle so it doesn't drag attention off the
|
||||
prose. */
|
||||
.home-kinpaku code {
|
||||
color: oklch(82% 0.11 82);
|
||||
background: oklch(78% 0.12 82 / 0.08);
|
||||
border: 1px solid oklch(78% 0.12 82 / 0.14);
|
||||
border-radius: 3px;
|
||||
color: var(--ks-code-fg);
|
||||
background: var(--ks-code-bg);
|
||||
border: 0;
|
||||
border-radius: var(--ks-code-radius);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.88em;
|
||||
padding: 0.05em 0.4em;
|
||||
padding: var(--ks-code-pad);
|
||||
}
|
||||
|
||||
.home-kinpaku ::selection {
|
||||
@@ -214,7 +209,7 @@
|
||||
|
||||
.home-kinpaku .section-lead,
|
||||
.home-kinpaku .section-subtitle {
|
||||
max-width: 68ch;
|
||||
max-width: 62ch; /* 68ch rendered ~86 actual chars/line; tighten into the band */
|
||||
color: var(--ks-text);
|
||||
font-size: 1.02rem;
|
||||
line-height: 1.8;
|
||||
@@ -233,7 +228,6 @@
|
||||
.home-kinpaku .downloads-rebuild-note,
|
||||
.home-kinpaku .why-panel-title,
|
||||
.home-kinpaku .live-demo-support-v,
|
||||
.home-kinpaku .downloads-rebuild-examples span,
|
||||
.home-kinpaku .downloads-rebuild-extras strong,
|
||||
.home-kinpaku .downloads-rebuild-extras span {
|
||||
text-wrap: balance;
|
||||
@@ -907,7 +901,10 @@
|
||||
padding: 24px;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 4px;
|
||||
background: oklch(9% 0.006 95 / 0.92);
|
||||
/* Was 9% on a 7% ground (~1 level in sRGB): the card body vanished into the
|
||||
page and only the hairline hinted at an edge. Lift to a clear raised
|
||||
neutral tone so the specimen card reads as a crisp object. */
|
||||
background: oklch(15% 0 0);
|
||||
}
|
||||
|
||||
.home-kinpaku .foundation-column:hover .foundation-card {
|
||||
@@ -940,15 +937,11 @@
|
||||
|
||||
.home-kinpaku .foundation-plinth {
|
||||
border-color: var(--ks-rule);
|
||||
/* Neutral hatch (was kinpaku gold, which washed the section champagne) on a
|
||||
neutral base, for a crisp pedestal that matches the de-warmed section. */
|
||||
background:
|
||||
repeating-linear-gradient(45deg, oklch(78% 0.12 82 / 0.12), oklch(78% 0.12 82 / 0.12) 1px, transparent 1px, transparent 9px),
|
||||
oklch(10% 0.006 95 / 0.42);
|
||||
/* Soften the bottom so the plinth fades into the section bg like a real
|
||||
shadow instead of terminating in a hard horizontal line. Mask fades the
|
||||
bottom 45% of the element, taking the diagonal stripes + side borders
|
||||
down with it. */
|
||||
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 100%);
|
||||
repeating-linear-gradient(45deg, oklch(80% 0 0 / 0.07), oklch(80% 0 0 / 0.07) 1px, transparent 1px, transparent 9px),
|
||||
oklch(10% 0 0 / 0.42);
|
||||
}
|
||||
|
||||
.home-kinpaku .language-intro-row {
|
||||
@@ -982,6 +975,8 @@
|
||||
surface (flat oklch(0.17 0 0), no border, no shadow). All inner demo
|
||||
surfaces sit transparently on top — no plinth-in-plinth, no nested rings. */
|
||||
.home-kinpaku .magazine-container {
|
||||
/* Solid panel, no border. The demo preview inside gets its own thin border
|
||||
(see .spread-demo-area below) and inherits this fill. */
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
background: oklch(0.17 0 0);
|
||||
@@ -989,7 +984,7 @@
|
||||
}
|
||||
|
||||
.home-kinpaku .fisheye-item {
|
||||
color: var(--ks-muted);
|
||||
color: var(--ks-text);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 1.18rem;
|
||||
}
|
||||
@@ -1018,13 +1013,23 @@
|
||||
}
|
||||
|
||||
.home-kinpaku .spread-flow-cmd {
|
||||
color: var(--ks-kinpaku);
|
||||
background: oklch(78% 0.12 82 / 0.08);
|
||||
border: 1px solid oklch(78% 0.12 82 / 0.18);
|
||||
color: var(--ks-code-cmd);
|
||||
background: var(--ks-code-bg);
|
||||
border: 0;
|
||||
border-radius: var(--ks-code-radius);
|
||||
}
|
||||
|
||||
/* Inner demo surfaces are transparent — the plinth lives on the outer
|
||||
.magazine-container; these sit on top with no chrome of their own. */
|
||||
/* The demo's frame is the .split-container's own border; the caption sits
|
||||
outside it. Strip the leftover chrome: no grid ::before, no inner shadow. */
|
||||
.home-kinpaku .spread-demo-area .split-container {
|
||||
box-shadow: none;
|
||||
}
|
||||
.home-kinpaku .spread-demo-area .split-container::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Inner demo surfaces are transparent — they sit on the panel inside the
|
||||
framed .split-container, with no chrome of their own. */
|
||||
.home-kinpaku .spread-demo-area .demo-split-comparison .split-container,
|
||||
.home-kinpaku .terminal-preview .demo-split-comparison .split-container,
|
||||
.home-kinpaku .demo-container,
|
||||
@@ -1115,14 +1120,16 @@
|
||||
--ptable-accent: var(--ks-kinpaku);
|
||||
width: clamp(64px, 5vw, 76px) !important;
|
||||
height: clamp(76px, 5.8vw, 90px) !important;
|
||||
border-width: 1px !important;
|
||||
/* Crisp neutral card. Was a category-tinted gradient bg + strong colored
|
||||
border + muddy multi-inset shadow that read as dim blobs; now a clean
|
||||
graphite tile with a neutral hairline. Category is cued by the group label
|
||||
and the accent line below, not a muddy fill. */
|
||||
background: oklch(15% 0 0) !important;
|
||||
border: 1px solid var(--ks-rule) !important;
|
||||
border-radius: 4px !important;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px oklch(100% 0 0 / 0.035),
|
||||
inset 0 -18px 34px oklch(2% 0.004 95 / 0.28),
|
||||
0 18px 42px oklch(2% 0.004 95 / 0.22) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.home-kinpaku #framework-viz-container .ptable-element--create {
|
||||
@@ -1149,40 +1156,20 @@
|
||||
--ptable-accent: var(--cat-system-text);
|
||||
}
|
||||
|
||||
/* No gold-leaf texture overlay (was washing the tile bg) and no bottom accent
|
||||
line — the tile is a clean flat neutral surface. */
|
||||
.home-kinpaku #framework-viz-container .ptable-element::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background:
|
||||
radial-gradient(circle at 74% 12%, oklch(100% 0 0 / 0.09), transparent 32%),
|
||||
url("/assets/neo-kinpaku/kinpaku-gold-leaf.png") center / 190px auto;
|
||||
mix-blend-mode: screen;
|
||||
opacity: 0.055;
|
||||
pointer-events: none;
|
||||
content: none;
|
||||
}
|
||||
|
||||
.home-kinpaku #framework-viz-container .ptable-element::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: 9px;
|
||||
z-index: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--ptable-accent), transparent);
|
||||
opacity: 0.24;
|
||||
pointer-events: none;
|
||||
content: none;
|
||||
}
|
||||
|
||||
.home-kinpaku #framework-viz-container .ptable-element:is(:hover, :focus-visible) {
|
||||
transform: translateY(-3px) !important;
|
||||
border-color: var(--ptable-accent) !important;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px oklch(100% 0 0 / 0.05),
|
||||
0 0 0 1px oklch(78% 0.12 82 / 0.12),
|
||||
0 22px 48px oklch(2% 0.004 95 / 0.36),
|
||||
0 0 26px oklch(78% 0.12 82 / 0.16) !important;
|
||||
border-color: oklch(85% 0 0 / 0.4) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.home-kinpaku #framework-viz-container .ptable-number,
|
||||
@@ -1201,16 +1188,17 @@
|
||||
|
||||
.home-kinpaku #framework-viz-container .ptable-symbol {
|
||||
font-size: clamp(1.45rem, 1.8vw, 1.8rem) !important;
|
||||
font-weight: 300 !important;
|
||||
font-weight: 400 !important;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--ks-champagne) !important;
|
||||
}
|
||||
|
||||
.home-kinpaku #framework-viz-container .ptable-name {
|
||||
max-width: 64px !important;
|
||||
color: var(--ptable-accent) !important;
|
||||
font-size: 0.54rem !important;
|
||||
color: var(--ks-text-muted) !important;
|
||||
font-size: 0.56rem !important;
|
||||
letter-spacing: 0.04em;
|
||||
opacity: 0.72 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.home-kinpaku #framework-viz-container .ptable-alpha {
|
||||
@@ -1222,12 +1210,14 @@
|
||||
|
||||
.home-kinpaku .spread-demo-area .demo-split-comparison .split-before,
|
||||
.home-kinpaku .terminal-preview .demo-split-comparison .split-before {
|
||||
background: oklch(10% 0.006 95);
|
||||
/* Left half inherits the panel fill — no separate background. */
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.home-kinpaku .spread-demo-area .demo-split-comparison .split-after,
|
||||
.home-kinpaku .terminal-preview .demo-split-comparison .split-after {
|
||||
background: oklch(8% 0.006 95);
|
||||
/* Right half is near-black: the clean before/after contrast. */
|
||||
background: oklch(2% 0 0);
|
||||
}
|
||||
|
||||
.home-kinpaku .demo-caption,
|
||||
@@ -1262,12 +1252,12 @@
|
||||
}
|
||||
|
||||
.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison .split-container {
|
||||
background:
|
||||
linear-gradient(90deg, oklch(78% 0.12 82 / 0.028) 1px, transparent 1px),
|
||||
linear-gradient(180deg, oklch(78% 0.12 82 / 0.022) 1px, transparent 1px),
|
||||
radial-gradient(circle at 74% 48%, oklch(70% 0.105 190 / 0.055), transparent 18rem),
|
||||
oklch(7% 0.005 95);
|
||||
background-size: 36px 36px, 36px 36px, auto, auto;
|
||||
/* No grid lines, no radial glow — the container is transparent so the before
|
||||
half shows the panel fill and the after half its own near-black. A thin
|
||||
neutral border frames the demo itself (the caption sits outside it). */
|
||||
background: transparent;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison .split-before,
|
||||
@@ -1490,7 +1480,6 @@
|
||||
— dropped, the bento structure carries the brand voice now. */
|
||||
.home-kinpaku .why-terminal,
|
||||
.home-kinpaku .why-productmd-file,
|
||||
.home-kinpaku .why-register,
|
||||
.home-kinpaku .why-ci-window,
|
||||
.home-kinpaku .why-browser,
|
||||
.home-kinpaku .why-dm-grid,
|
||||
@@ -1501,6 +1490,104 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-register {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-register-label {
|
||||
color: var(--ks-text-muted);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-register-mock--brand,
|
||||
.home-kinpaku .why-register-mock--product {
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 4px;
|
||||
background: var(--ks-lacquer-raised);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-brand-hero-mono {
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-brand-hero-title {
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font-wordmark);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-brand-hero-title em {
|
||||
color: var(--ks-kinpaku);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-product-k {
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-product-v {
|
||||
color: var(--ks-champagne);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-product-row {
|
||||
border-color: var(--ks-rule);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-productmd-file,
|
||||
.home-kinpaku .why-terminal {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-productmd-header,
|
||||
.home-kinpaku .why-productmd-footer,
|
||||
.home-kinpaku .why-terminal-header {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Bento window chrome dots — one subtle traffic-light set across mocks. */
|
||||
.home-kinpaku :is(
|
||||
.why-terminal-dot,
|
||||
.why-live-dot,
|
||||
.why-visual--extension .why-browser-dot,
|
||||
.why-visual--v2 .v2-new-browser-dot
|
||||
) {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.5;
|
||||
background: oklch(58% 0.012 95);
|
||||
}
|
||||
|
||||
.home-kinpaku :is(
|
||||
.why-terminal-dot,
|
||||
.why-live-dot,
|
||||
.why-visual--extension .why-browser-dot,
|
||||
.why-visual--v2 .v2-new-browser-dot
|
||||
):nth-child(1) {
|
||||
background: var(--ks-kinpaku-deep);
|
||||
}
|
||||
|
||||
.home-kinpaku :is(
|
||||
.why-terminal-dot,
|
||||
.why-live-dot,
|
||||
.why-visual--extension .why-browser-dot,
|
||||
.why-visual--v2 .v2-new-browser-dot
|
||||
):nth-child(2) {
|
||||
background: var(--ks-patina-deep);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-terminal-header,
|
||||
.home-kinpaku .why-live-chrome {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-panel-title {
|
||||
color: var(--ks-champagne);
|
||||
font-size: 2rem;
|
||||
@@ -1519,8 +1606,7 @@
|
||||
.home-kinpaku .why-productmd-k,
|
||||
.home-kinpaku .why-terminal-ok,
|
||||
.home-kinpaku .why-terminal-path,
|
||||
.home-kinpaku .why-dm-badge,
|
||||
.home-kinpaku .why-ci-status {
|
||||
.home-kinpaku .why-dm-badge {
|
||||
color: var(--ks-patina);
|
||||
}
|
||||
|
||||
@@ -1550,6 +1636,9 @@
|
||||
|
||||
.home-kinpaku .why-ci-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 54px;
|
||||
padding: 14px 18px 14px 72px;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
@@ -1561,29 +1650,35 @@
|
||||
position: absolute;
|
||||
left: 22px;
|
||||
top: 50%;
|
||||
width: 34px;
|
||||
height: 9px;
|
||||
width: 31px;
|
||||
height: 7px;
|
||||
transform: translateY(-50%);
|
||||
background:
|
||||
radial-gradient(circle at 4px 50%, var(--ks-kinpaku) 0 3px, transparent 3.5px),
|
||||
radial-gradient(circle at 17px 50%, var(--ks-patina) 0 3px, transparent 3.5px),
|
||||
radial-gradient(circle at 30px 50%, var(--ks-kinpaku-deep) 0 3px, transparent 3.5px);
|
||||
opacity: 0.88;
|
||||
radial-gradient(circle at 3.5px 50%, var(--ks-kinpaku-deep) 0 3.5px, transparent 3.6px),
|
||||
radial-gradient(circle at 13.5px 50%, var(--ks-patina-deep) 0 3.5px, transparent 3.6px),
|
||||
radial-gradient(circle at 23.5px 50%, oklch(58% 0.012 95) 0 3.5px, transparent 3.6px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-ci-branch {
|
||||
font-family: var(--ks-mono);
|
||||
color: var(--ks-champagne);
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-ci-status {
|
||||
.home-kinpaku .why-ci-status,
|
||||
.home-kinpaku .why-ci-status--fail {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
padding: 5px 13px;
|
||||
border: 1px solid oklch(58% 0.15 35 / 0.3);
|
||||
color: var(--ks-patina);
|
||||
background:
|
||||
linear-gradient(90deg, oklch(58% 0.15 35 / 0.22), oklch(58% 0.15 35 / 0.1));
|
||||
box-shadow: inset 0 0 0 1px oklch(100% 0 0 / 0.02);
|
||||
border-radius: 999px;
|
||||
border: 1px solid oklch(58% 0.15 35 / 0.42);
|
||||
color: oklch(74% 0.14 35);
|
||||
background: oklch(58% 0.15 35 / 0.14);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-ci-body {
|
||||
@@ -1652,9 +1747,13 @@
|
||||
}
|
||||
|
||||
.home-kinpaku .why-visual--live {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
padding: clamp(16px, 2vw, 22px);
|
||||
overflow: hidden;
|
||||
min-height: 240px;
|
||||
min-height: min(280px, 42vw);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
background:
|
||||
linear-gradient(90deg, oklch(78% 0.12 82 / 0.018) 1px, transparent 1px),
|
||||
linear-gradient(180deg, oklch(78% 0.12 82 / 0.015) 1px, transparent 1px),
|
||||
@@ -1665,7 +1764,8 @@
|
||||
|
||||
.home-kinpaku .why-live-frame {
|
||||
position: relative;
|
||||
margin: 24px clamp(20px, 3vw, 32px) 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: 1px solid oklch(78% 0.12 82 / 0.22);
|
||||
border-radius: 6px;
|
||||
background:
|
||||
@@ -1675,25 +1775,17 @@
|
||||
inset 0 0 0 1px oklch(100% 0 0 / 0.02),
|
||||
0 18px 50px oklch(2% 0.004 95 / 0.32);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-live-chrome {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 9px 14px;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
background: oklch(13% 0.006 95);
|
||||
}
|
||||
.home-kinpaku .why-live-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: oklch(58% 0.018 82);
|
||||
opacity: 0.55;
|
||||
}
|
||||
.home-kinpaku .why-live-dot:nth-child(1) { background: var(--ks-kinpaku-deep); }
|
||||
.home-kinpaku .why-live-dot:nth-child(2) { background: var(--ks-patina-deep); }
|
||||
.home-kinpaku .why-live-url {
|
||||
margin-left: 12px;
|
||||
padding: 3px 10px;
|
||||
@@ -1707,8 +1799,12 @@
|
||||
|
||||
.home-kinpaku .why-live-stage {
|
||||
position: relative;
|
||||
min-height: 130px;
|
||||
padding: 24px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 148px;
|
||||
padding: 32px 28px;
|
||||
background:
|
||||
repeating-linear-gradient(0deg, transparent 0 9px, oklch(100% 0 0 / 0.012) 9px 10px),
|
||||
oklch(8% 0.006 95);
|
||||
@@ -1717,9 +1813,9 @@
|
||||
.home-kinpaku .why-live-pick {
|
||||
position: relative;
|
||||
width: max-content;
|
||||
max-width: calc(100% - 8px);
|
||||
padding: 18px 22px;
|
||||
margin: 12px auto 4px;
|
||||
max-width: min(100%, 320px);
|
||||
padding: 20px 24px;
|
||||
margin: 0;
|
||||
border: 2px solid var(--ks-kinpaku);
|
||||
border-radius: 3px;
|
||||
background:
|
||||
@@ -1732,20 +1828,20 @@
|
||||
.home-kinpaku .why-live-pick-tag {
|
||||
display: block;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.74rem;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--ks-kinpaku);
|
||||
}
|
||||
.home-kinpaku .why-live-pick-note {
|
||||
position: absolute;
|
||||
top: -14px;
|
||||
right: -18px;
|
||||
padding: 5px 10px;
|
||||
top: -12px;
|
||||
right: -14px;
|
||||
padding: 6px 11px;
|
||||
background: var(--ks-kinpaku);
|
||||
border-radius: 2px;
|
||||
color: oklch(8% 0.006 95);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 14px oklch(78% 0.12 82 / 0.25);
|
||||
@@ -1763,46 +1859,35 @@
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
.home-kinpaku .why-live-gbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.home-kinpaku .why-live-frame > .why-live-ctx.live-demo-ctx {
|
||||
position: static;
|
||||
left: auto;
|
||||
transform: none;
|
||||
display: block;
|
||||
opacity: 1;
|
||||
z-index: auto;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
padding: 10px 14px;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--ks-rule);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: oklch(13% 0.006 95);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
.home-kinpaku .why-live-gbar-brand {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--ks-kinpaku);
|
||||
color: oklch(8% 0.006 95);
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
|
||||
.home-kinpaku .why-live-frame > .why-live-ctx .live-demo-ctx-row--configure {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.home-kinpaku .why-live-gbar-cmd {
|
||||
padding: 3px 10px;
|
||||
border: 1px solid oklch(78% 0.12 82 / 0.3);
|
||||
border-radius: 3px;
|
||||
color: var(--ks-kinpaku);
|
||||
|
||||
.home-kinpaku .why-live-frame > .why-live-ctx .live-demo-ctx-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.home-kinpaku .why-live-gbar-count {
|
||||
padding: 3px 8px;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 3px;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
.home-kinpaku .why-live-gbar-go {
|
||||
margin-left: auto;
|
||||
padding: 4px 12px;
|
||||
background: var(--ks-kinpaku);
|
||||
color: oklch(8% 0.006 95);
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
|
||||
.home-kinpaku .why-live-frame > .why-live-ctx .live-demo-ctx-caret {
|
||||
animation: why-live-caret 1s steps(2, end) infinite;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -1908,20 +1993,6 @@
|
||||
linear-gradient(180deg, oklch(13% 0.006 95), oklch(8% 0.005 95));
|
||||
}
|
||||
|
||||
.home-kinpaku .why-visual--extension .why-browser-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--ks-kinpaku-deep);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-visual--extension .why-browser-dot:nth-child(2) {
|
||||
background: var(--ks-patina);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-visual--extension .why-browser-dot:nth-child(3) {
|
||||
background: var(--ks-kinpaku);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-visual--extension .why-browser-url {
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
@@ -2024,14 +2095,11 @@
|
||||
}
|
||||
|
||||
.home-kinpaku .why-visual--designmd-v2 {
|
||||
gap: 16px;
|
||||
padding: clamp(18px, 2vw, 26px);
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 13% 24%, oklch(78% 0.12 82 / 0.055), transparent 18rem),
|
||||
radial-gradient(circle at 88% 18%, oklch(70% 0.105 190 / 0.06), transparent 18rem),
|
||||
linear-gradient(135deg, oklch(78% 0.12 82 / 0.035), transparent 46%),
|
||||
oklch(6% 0.005 95 / 0.92);
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-dm-header {
|
||||
@@ -2058,11 +2126,9 @@
|
||||
}
|
||||
|
||||
.home-kinpaku .why-dm-tile {
|
||||
border-color: var(--ks-rule);
|
||||
background:
|
||||
linear-gradient(135deg, oklch(100% 0 0 / 0.024), transparent 38%),
|
||||
oklch(5% 0.004 95 / 0.78);
|
||||
box-shadow: inset 0 0 0 1px oklch(78% 0.12 82 / 0.026);
|
||||
border: 1px solid var(--ks-rule);
|
||||
background: var(--ks-lacquer-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.home-kinpaku .why-dm-tile-meta {
|
||||
@@ -2298,20 +2364,14 @@
|
||||
}
|
||||
|
||||
.home-kinpaku .why-visual--v2 .v2-new-browser-dots {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
background:
|
||||
linear-gradient(90deg, oklch(78% 0.12 82 / 0.08), transparent),
|
||||
oklch(8% 0.006 95);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-visual--v2 .v2-new-browser-dot {
|
||||
background: var(--ks-kinpaku-deep);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-visual--v2 .v2-new-browser-dot:nth-child(2) {
|
||||
background: var(--ks-patina);
|
||||
}
|
||||
|
||||
.home-kinpaku .why-visual--v2 .v2-new-browser-frame {
|
||||
background:
|
||||
linear-gradient(90deg, oklch(78% 0.12 82 / 0.035) 1px, transparent 1px),
|
||||
@@ -2385,10 +2445,12 @@
|
||||
}
|
||||
|
||||
.home-kinpaku .live-demo-frame {
|
||||
border: 1px solid var(--ks-rule);
|
||||
border: 1px solid oklch(92% 0 0 / 0.1);
|
||||
border-radius: 4px;
|
||||
background: var(--ks-lacquer-raised);
|
||||
box-shadow: 0 30px 84px oklch(2% 0.004 95 / 0.42);
|
||||
box-shadow:
|
||||
inset 0 1px 0 oklch(100% 0 0 / 0.05),
|
||||
0 20px 48px -14px oklch(0% 0 0 / 0.6);
|
||||
}
|
||||
|
||||
/* Picker chrome (chrome bar, gbar, ctx, outline, comment, stroke, etc.)
|
||||
|
||||
+107
-93
@@ -29,6 +29,9 @@
|
||||
never tucks under the nav; on tall viewports the content still centers. */
|
||||
padding: 113px 0 96px;
|
||||
overflow: hidden;
|
||||
/* Hairline divider into the testimonials below, matching the testimonial
|
||||
card border color. */
|
||||
border-bottom: 1px solid oklch(0.64 0 0 / 0.22);
|
||||
}
|
||||
|
||||
/* Header sits on top of the hero image like the /design-system topbar:
|
||||
@@ -55,10 +58,7 @@
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
/* Top scrim darkens the band behind the overlaid nav so the links stay
|
||||
legible over the bright kintsugi gold, fading out before the hero copy. */
|
||||
background:
|
||||
linear-gradient(180deg, oklch(7% 0.006 95 / 0.9) 0%, oklch(7% 0.006 95 / 0.5) 8%, transparent 18%),
|
||||
url("/assets/neo-kinpaku/candidates/finalists/m-01-v2-01.png") center / cover no-repeat;
|
||||
filter: saturate(1.2) contrast(1.08);
|
||||
}
|
||||
@@ -116,7 +116,9 @@
|
||||
|
||||
/* Hero CTAs migrated to .ks-button.ks-button-primary / .ks-button-secondary
|
||||
(see kinpaku-kit.css). The .hero-rebuild-arrow class is gone too — the
|
||||
arrow now lives in .ks-button-arrow inside the button. */
|
||||
arrow now lives in .ks-button-arrow inside the button.
|
||||
"How it works" is the kit ghost variant: plain champagne (white) text, no
|
||||
border or fill, hovering to gold. */
|
||||
|
||||
.hero-rebuild-meta {
|
||||
margin: 24px 0 0;
|
||||
@@ -497,13 +499,13 @@
|
||||
than .home-kinpaku code alone). Keeps the CLI block in the softer code-gold
|
||||
so the line reads as a terminal command, not a pumpkin headline. */
|
||||
.home-kinpaku code.slop-teaser-cli {
|
||||
color: oklch(82% 0.11 82);
|
||||
color: var(--ks-code-block-fg);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.86rem;
|
||||
background: var(--ks-lacquer-raised);
|
||||
background: var(--ks-code-block-bg);
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--ks-code-block-border);
|
||||
border-radius: var(--ks-code-block-radius);
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
@@ -553,8 +555,8 @@
|
||||
--card-fg-strong: var(--ks-champagne);
|
||||
--card-fg-muted: var(--ks-muted);
|
||||
--card-accent: var(--ks-kinpaku);
|
||||
--card-cmd-bg: oklch(0.11 0 0);
|
||||
--card-cmd-fg: var(--ks-champagne);
|
||||
--card-cmd-bg: var(--ks-code-block-bg);
|
||||
--card-cmd-fg: var(--ks-code-block-fg);
|
||||
--card-copy-border: var(--ks-rule);
|
||||
--card-copy-fg: var(--ks-kinpaku);
|
||||
--card-copy-hover-bg: oklch(77% 0.14 82 / 0.08);
|
||||
@@ -562,6 +564,14 @@
|
||||
--card-link-underline: var(--ks-kinpaku);
|
||||
--card-link-secondary: var(--ks-muted);
|
||||
|
||||
/* Command-frame accents. The install box is the primary CTA and carries
|
||||
kinpaku; the update box carries patina, which in this system reads as the
|
||||
"improved / updated" state. Both re-theme in light-mode.css. */
|
||||
--card-cmd-border: var(--ks-kinpaku);
|
||||
--card-prompt: var(--ks-kinpaku);
|
||||
--card-cmd-border-update: oklch(70% 0.12 188 / 0.5);
|
||||
--card-prompt-update: var(--ks-patina);
|
||||
|
||||
/* Width of the accent's reserved right column. Content padding-right
|
||||
matches this so the accent never overlaps text. */
|
||||
--card-accent-w: 280px;
|
||||
@@ -569,11 +579,11 @@
|
||||
position: relative;
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
padding: 88px var(--card-accent-w) 88px 56px;
|
||||
padding: 80px var(--card-accent-w) 80px 56px;
|
||||
border-radius: 2px;
|
||||
background: oklch(0.17 0 0);
|
||||
color: var(--card-fg);
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -618,9 +628,9 @@
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
max-width: 760px;
|
||||
margin: 0 auto 48px;
|
||||
text-align: center;
|
||||
justify-items: center;
|
||||
margin: 0 0 40px;
|
||||
text-align: left;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.downloads-rebuild-eyebrow {
|
||||
@@ -643,24 +653,56 @@
|
||||
|
||||
.downloads-rebuild-install {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0 auto 56px;
|
||||
max-width: 720px;
|
||||
gap: 22px;
|
||||
margin: 0 0 48px;
|
||||
max-width: 600px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Install command is THE primary CTA of the Get Started section. Strong
|
||||
plinth bg + bigger font + more padding make it the hero. Copy button is
|
||||
nested inside on the right. No side-stripe border (brand-ban). */
|
||||
/* Each command is a labeled group: a small mono cap above its box. */
|
||||
.downloads-rebuild-cmd-group {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.downloads-rebuild-cmd-label {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--card-prompt);
|
||||
}
|
||||
.downloads-rebuild-cmd-label--update {
|
||||
color: var(--card-prompt-update);
|
||||
}
|
||||
|
||||
/* Install command is THE primary CTA of the Get Started section. A full
|
||||
kinpaku hairline frame + gold prompt glyph + left-aligned mono make it read
|
||||
as a runnable terminal line, not a decorative chip. Copy button nests on the
|
||||
right. Full border, not a side-stripe (brand-ban). */
|
||||
.home-kinpaku .downloads-rebuild-cmd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 14px 14px 14px 28px;
|
||||
gap: 14px;
|
||||
padding: 15px 14px 15px 22px;
|
||||
background: var(--card-cmd-bg);
|
||||
border: 0;
|
||||
border: 1px solid var(--card-cmd-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* The shell prompt that signals "this is a command you run." Carries the
|
||||
row's accent (gold for install, patina for update). */
|
||||
.home-kinpaku .downloads-rebuild-prompt {
|
||||
flex: none;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 1.18rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: var(--card-prompt);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.home-kinpaku .downloads-rebuild-cmd > code {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
@@ -668,7 +710,7 @@
|
||||
white-space: nowrap;
|
||||
color: var(--card-cmd-fg);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.22rem;
|
||||
letter-spacing: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
@@ -676,6 +718,14 @@
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
/* Update box: same frame, quieter patina accent (the "improved" signal). */
|
||||
.home-kinpaku .downloads-rebuild-cmd--update {
|
||||
border-color: var(--card-cmd-border-update);
|
||||
}
|
||||
.home-kinpaku .downloads-rebuild-cmd--update .downloads-rebuild-prompt {
|
||||
color: var(--card-prompt-update);
|
||||
}
|
||||
|
||||
/* Compact icon-only copy button nested inside the cmd box, on the right. */
|
||||
.home-kinpaku .downloads-rebuild-copy {
|
||||
flex: none;
|
||||
@@ -706,11 +756,20 @@
|
||||
border-color: var(--card-copy-fg);
|
||||
}
|
||||
|
||||
/* Update copy button tracks the patina accent rather than gold. */
|
||||
.home-kinpaku .downloads-rebuild-cmd--update .downloads-rebuild-copy {
|
||||
color: var(--card-prompt-update);
|
||||
}
|
||||
.home-kinpaku .downloads-rebuild-cmd--update .downloads-rebuild-copy:hover {
|
||||
background: oklch(70% 0.12 188 / 0.08);
|
||||
border-color: var(--card-prompt-update);
|
||||
}
|
||||
|
||||
/* "Other install methods" — a quiet disclosure under the primary command.
|
||||
Native <details> so it works without JS and stays accessible. */
|
||||
.downloads-rebuild-alts {
|
||||
grid-column: 1 / -1;
|
||||
margin: 2px 0 0;
|
||||
text-align: left;
|
||||
}
|
||||
.downloads-rebuild-alts-summary {
|
||||
display: inline-flex;
|
||||
@@ -770,18 +829,25 @@
|
||||
}
|
||||
|
||||
.downloads-rebuild-note {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0;
|
||||
max-width: 60ch;
|
||||
text-align: left;
|
||||
color: var(--card-fg-muted);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.home-kinpaku .downloads-rebuild-note code {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.86em;
|
||||
color: var(--card-fg-strong);
|
||||
}
|
||||
|
||||
/* Quieter than the note — the changelog + FAQ hints sit just under the
|
||||
primary "works with" line. Patina links so they read as related but
|
||||
distinct from the kinpaku primary CTA above. */
|
||||
.downloads-rebuild-secondary {
|
||||
grid-column: 1 / -1;
|
||||
margin: 6px 0 0;
|
||||
text-align: left;
|
||||
font-size: 0.84rem;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
@@ -795,12 +861,10 @@
|
||||
border-bottom-color: var(--ks-patina-pale);
|
||||
}
|
||||
|
||||
/* Closing "go deeper" links under the usage examples — point to the full
|
||||
workflow page and the command docs. */
|
||||
/* Closing links under the setup extras — workflow + command docs. */
|
||||
.downloads-rebuild-more {
|
||||
margin: 30px auto 0;
|
||||
max-width: 720px;
|
||||
text-align: center;
|
||||
margin: 28px 0 0;
|
||||
text-align: left;
|
||||
font-size: 0.9rem;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
@@ -814,70 +878,15 @@
|
||||
border-bottom-color: var(--ks-patina-pale);
|
||||
}
|
||||
|
||||
/* Center-aligned examples: each row is a vertical stack — the command line
|
||||
on top, the description below, both centered horizontally. */
|
||||
/* Usage examples now sit after the extras (the logical next step once the
|
||||
skill, extension, CLI, and subscriptions are set up), so the separating
|
||||
gap moves to the top. */
|
||||
.downloads-rebuild-examples {
|
||||
list-style: none;
|
||||
margin: 48px auto 0;
|
||||
padding: 48px 0 0;
|
||||
/* Subtle hairline marking the "now use it" step after the setup block. */
|
||||
border-top: 1px solid var(--ks-rule);
|
||||
display: grid;
|
||||
gap: 22px;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.downloads-rebuild-examples li {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Strip the global .home-kinpaku code chrome for these example rows. */
|
||||
.home-kinpaku .downloads-rebuild-examples > li > code {
|
||||
font-family: var(--ks-mono);
|
||||
color: var(--card-accent);
|
||||
font-size: 1rem;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
letter-spacing: 0.005em;
|
||||
}
|
||||
|
||||
.downloads-rebuild-slash {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.downloads-rebuild-examples span {
|
||||
color: var(--card-fg);
|
||||
font-size: 0.94rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.home-kinpaku .downloads-rebuild-examples span code {
|
||||
color: var(--card-fg-strong);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.downloads-rebuild-extras {
|
||||
list-style: none;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 40px 0 0;
|
||||
border-top: 1px solid var(--ks-rule);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 36px;
|
||||
max-width: 900px;
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
/* Each cell is a flex column so primary links can be bottom-anchored.
|
||||
@@ -950,7 +959,6 @@
|
||||
@media (max-width: 720px) {
|
||||
.downloads-rebuild { padding: 64px 0; }
|
||||
.downloads-rebuild-install { grid-template-columns: 1fr; }
|
||||
.downloads-rebuild-examples li { grid-template-columns: 1fr; gap: 8px; }
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
@@ -1014,6 +1022,12 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* The hero-scoped demo-chrome refinements (crisp outline, neutral chrome dots /
|
||||
URL, quiet gbar border, crisp active-Pick pill, tighter radii, refined frame
|
||||
elevation) were promoted into the shared picker chrome in kinpaku-kit.css /
|
||||
live-mode-kinpaku.css / home-kinpaku.css so /live-mode and the injected
|
||||
picker (skill/scripts/live-browser.js) match. Nothing hero-specific remains. */
|
||||
|
||||
/* The stage holds a full (fake) website now, not a centered card. */
|
||||
.hero-live-demo .live-demo-stage {
|
||||
display: block;
|
||||
|
||||
+54
-21
@@ -38,7 +38,7 @@
|
||||
.ks-wordmark {
|
||||
color: var(--ks-kinpaku);
|
||||
font-family: var(--ks-font-wordmark);
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
font-size: 1.3rem;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
@@ -872,7 +872,7 @@
|
||||
.kinpaku-chrome .footer-logo {
|
||||
color: var(--ks-kinpaku);
|
||||
font-family: var(--ks-font-wordmark);
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
font-size: 1.3rem;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
@@ -892,6 +892,17 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.kinpaku-chrome .theme-toggle,
|
||||
.kinpaku-chrome .site-header-github {
|
||||
height: 36px;
|
||||
min-height: 36px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.kinpaku-chrome .site-header-github {
|
||||
padding: 0 12px 0 10px;
|
||||
}
|
||||
|
||||
.kinpaku-chrome .site-header-nav a,
|
||||
.kinpaku-chrome .site-header-github {
|
||||
color: var(--ks-champagne);
|
||||
@@ -904,7 +915,7 @@
|
||||
|
||||
/* GitHub star pill. Previously home-only; now every kinpaku page gets it. */
|
||||
.kinpaku-chrome .site-header-github {
|
||||
border: 1px solid var(--ks-rule);
|
||||
border: 1px solid oklch(80% 0 0);
|
||||
border-radius: 999px;
|
||||
background: oklch(10% 0.006 95 / 0.74);
|
||||
}
|
||||
@@ -1099,16 +1110,21 @@
|
||||
constants are kept in sync with these tokens by hand.
|
||||
============================================================ */
|
||||
|
||||
/* Browser-chrome bar at the top of the mock (URL pill + dots). */
|
||||
/* Browser-chrome bar at the top of the mock (URL pill + dots). Refined to read
|
||||
like a precise tool, not a stock screenshot frame: slimmer bar, small uniform
|
||||
neutral dots, and a clean neutral-edged URL pill (no warm gold hairline). */
|
||||
.home-kinpaku .live-demo-chrome,
|
||||
.live-mode-kinpaku .live-demo-chrome {
|
||||
padding: 9px 14px;
|
||||
background: var(--ks-graphite);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
border-bottom: 1px solid oklch(92% 0 0 / 0.07);
|
||||
}
|
||||
|
||||
.home-kinpaku .live-demo-dot,
|
||||
.live-mode-kinpaku .live-demo-dot {
|
||||
background: oklch(28% 0.01 95);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: oklch(34% 0 0);
|
||||
}
|
||||
|
||||
.home-kinpaku .live-demo-url,
|
||||
@@ -1119,7 +1135,7 @@
|
||||
color: var(--ks-text-muted);
|
||||
letter-spacing: 0.04em;
|
||||
background: transparent;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border: 1px solid oklch(92% 0 0 / 0.08);
|
||||
}
|
||||
|
||||
/* Active control surfaces — the bottom picker bar and the context bar
|
||||
@@ -1131,10 +1147,12 @@
|
||||
.home-kinpaku .live-demo-ctx,
|
||||
.live-mode-kinpaku .live-demo-ctx {
|
||||
background: var(--ks-lacquer-deep);
|
||||
border: 1.5px solid var(--ks-kinpaku);
|
||||
box-shadow:
|
||||
0 0 0 1px oklch(78% 0.12 82 / 0.18),
|
||||
0 10px 28px oklch(0% 0 0 / 0.28);
|
||||
/* Quiet neutral hairline instead of the loud 1.5px gold border + gold halo.
|
||||
Gold is reserved for the brand mark and the active control, not the
|
||||
container outline. */
|
||||
border: 1px solid oklch(92% 0 0 / 0.13);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 16px 36px -12px oklch(0% 0 0 / 0.6);
|
||||
color: var(--ks-champagne);
|
||||
}
|
||||
|
||||
@@ -1150,13 +1168,14 @@
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Active state — soft kinpaku-dim pill with kinpaku text. The outer
|
||||
gbar border carries the loud signal; the active button is the
|
||||
quieter "currently selected" pill inside. */
|
||||
/* Active state — a crisp neutral pill with kinpaku text. The translucent
|
||||
kinpaku-dim wash read as a murky smudge; a solid graphite chip with gold
|
||||
text/icon makes the selected control precise. */
|
||||
.home-kinpaku .live-demo-gbar-btn.is-active,
|
||||
.live-mode-kinpaku .live-demo-gbar-btn.is-active {
|
||||
background: oklch(78% 0.12 82 / 0.18);
|
||||
background: oklch(27% 0 0);
|
||||
color: var(--ks-kinpaku);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Page chat — collapsed pill with chat + mic; expands on click. */
|
||||
@@ -1167,9 +1186,9 @@
|
||||
height: 28px;
|
||||
width: 88px;
|
||||
margin: 0 4px 0 6px;
|
||||
border-radius: 7px;
|
||||
border-radius: 5px;
|
||||
background: var(--ks-graphite);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border: 1px solid oklch(92% 0 0 / 0.1);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
@@ -1292,10 +1311,20 @@
|
||||
.home-kinpaku .live-demo-ctx-pill,
|
||||
.live-mode-kinpaku .live-demo-ctx-pill {
|
||||
background: var(--ks-graphite-2);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border: 1px solid oklch(92% 0 0 / 0.12);
|
||||
color: var(--ks-champagne);
|
||||
}
|
||||
|
||||
/* Even heights across the configure row: the pill, input, count, and Go button
|
||||
share one 30px baseline instead of stepping up and down. */
|
||||
.home-kinpaku .live-demo-ctx-row--configure > *,
|
||||
.live-mode-kinpaku .live-demo-ctx-row--configure > * {
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.home-kinpaku .live-demo-ctx-pill-caret,
|
||||
.live-mode-kinpaku .live-demo-ctx-pill-caret {
|
||||
color: var(--ks-text-muted);
|
||||
@@ -1304,7 +1333,7 @@
|
||||
.home-kinpaku .live-demo-ctx-input,
|
||||
.live-mode-kinpaku .live-demo-ctx-input {
|
||||
background: var(--ks-graphite);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border: 1px solid oklch(92% 0 0 / 0.12);
|
||||
color: var(--ks-champagne);
|
||||
}
|
||||
|
||||
@@ -1316,7 +1345,7 @@
|
||||
.home-kinpaku .live-demo-ctx-count,
|
||||
.live-mode-kinpaku .live-demo-ctx-count {
|
||||
background: var(--ks-graphite-2);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border: 1px solid oklch(92% 0 0 / 0.12);
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
@@ -1377,8 +1406,12 @@
|
||||
.home-kinpaku .live-demo-outline,
|
||||
.live-mode-kinpaku .live-demo-outline {
|
||||
border-color: var(--ks-kinpaku);
|
||||
border-width: 1.5px;
|
||||
border-radius: 3px;
|
||||
color: var(--ks-kinpaku);
|
||||
box-shadow: 0 0 0 4px oklch(78% 0.12 82 / 0.16);
|
||||
/* No soft gold glow ring (off-brand accent halo); a crisp outline reads as a
|
||||
precise selection, not a highlighter. */
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.home-kinpaku .live-demo-annotations,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* color-scheme follows html.light / html.dark class on <html> */
|
||||
/* ============================================================
|
||||
Brand anchors. Two anchors carry the brand — kinpaku gold for
|
||||
commitment and brand voice, verdigris patina for state and
|
||||
@@ -45,22 +46,43 @@
|
||||
--ks-graphite-2: oklch(19% 0.008 95); /* one step up from graphite */
|
||||
|
||||
/* ============================================================
|
||||
Text. Warm champagne family. Body text reads at ≥0.92rem on
|
||||
lacquer; smaller than that loses to the warm noise floor.
|
||||
Text. The whole text ramp runs near-neutral and bright so copy,
|
||||
headings, and labels read crisp on lacquer instead of mushing
|
||||
into the warm floor. Warmth now lives only in the gold accents
|
||||
and the surfaces, not in the type. (--ks-champagne keeps its name
|
||||
for compatibility but is no longer warm.)
|
||||
============================================================ */
|
||||
--ks-champagne: oklch(84% 0.035 82); /* headlines, <strong> */
|
||||
--ks-text: oklch(81% 0.03 82); /* body */
|
||||
--ks-text-muted: oklch(63% 0.024 82); /* captions, meta */
|
||||
--ks-text-faint: oklch(52% 0.018 82); /* subdued */
|
||||
--ks-text-mute-deep: oklch(48% 0.018 82); /* disabled */
|
||||
--ks-champagne: oklch(91% 0 0); /* headlines, <strong> — neutral white */
|
||||
--ks-text: oklch(88% 0 0); /* body — neutral near-white */
|
||||
--ks-text-muted: oklch(72% 0 0); /* captions, meta */
|
||||
--ks-text-faint: oklch(62% 0 0); /* subdued */
|
||||
--ks-text-mute-deep: oklch(52% 0 0); /* disabled */
|
||||
|
||||
/* ============================================================
|
||||
Rules. Gold hairlines. Default is quiet; "strong" appears on
|
||||
focus, active state, and structural anchors.
|
||||
Rules. The default hairline is neutral so borders/dividers/labels
|
||||
don't carry warmth (warmth lives in the gold accents and surfaces).
|
||||
"strong" stays gold — it appears on focus, active state, and
|
||||
structural brand anchors where the gold is the signal.
|
||||
============================================================ */
|
||||
--ks-rule: oklch(58% 0.065 82 / 0.32);
|
||||
--ks-rule: oklch(78% 0 0 / 0.16);
|
||||
--ks-rule-strong: oklch(74% 0.09 82 / 0.6);
|
||||
|
||||
/* ============================================================
|
||||
Code. One treatment site-wide. Inline code is a neutral graphite
|
||||
chip; blocks / CLI commands are a deep lacquer terminal with a
|
||||
neutral hairline. Gold is only for code that is itself a command
|
||||
link (--ks-code-cmd). All code rules read these — don't hand-type.
|
||||
============================================================ */
|
||||
--ks-code-fg: oklch(91% 0 0); /* inline code text */
|
||||
--ks-code-bg: var(--ks-graphite-2); /* inline code chip */
|
||||
--ks-code-radius: 3px;
|
||||
--ks-code-pad: 0.3em 0.5em; /* inline code chip padding */
|
||||
--ks-code-block-fg: oklch(86% 0 0); /* block / CLI text */
|
||||
--ks-code-block-bg: var(--ks-lacquer-deep);
|
||||
--ks-code-block-border: var(--ks-rule);
|
||||
--ks-code-block-radius: 2px;
|
||||
--ks-code-cmd: var(--ks-kinpaku); /* code that's a command link */
|
||||
|
||||
/* ============================================================
|
||||
Typography — three faces.
|
||||
- --ks-font-display: Alumni Sans Pinstripe. Pinstripe horizontal
|
||||
@@ -124,3 +146,64 @@
|
||||
============================================================ */
|
||||
--ks-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Light mode — warm paper surfaces, dark ink type, kinpaku gold
|
||||
accents deepened for contrast. Activated via html.light on
|
||||
<html> (set by theme.js + inline FOUC guard in Base.astro).
|
||||
============================================================ */
|
||||
html.light {
|
||||
color-scheme: light;
|
||||
|
||||
/* Brand gold — same fills as dark mode (logo, buttons, marks).
|
||||
Use --ks-kinpaku-ink for small text on paper where bright gold
|
||||
lacks contrast; never swap the core token to a muddy orange. */
|
||||
--ks-kinpaku: oklch(84% 0.19 80.46);
|
||||
--ks-kinpaku-pale: oklch(86% 0.07 84);
|
||||
--ks-kinpaku-rich: oklch(77% 0.13 82);
|
||||
--ks-kinpaku-deep: oklch(61% 0.085 78);
|
||||
--ks-kinpaku-ink: oklch(56% 0.11 82);
|
||||
|
||||
/* Text links + active nav on paper — patina, not fill gold. */
|
||||
--ks-link-on-paper: var(--ks-patina-deep);
|
||||
--ks-link-on-paper-hover: var(--ks-patina);
|
||||
--ks-link-on-paper-line: oklch(49% 0.08 188 / 0.42);
|
||||
--ks-nav-active: var(--ks-patina-deep);
|
||||
|
||||
/* Patina — brand values for fills; deep for text on paper. */
|
||||
--ks-patina: oklch(70% 0.12 188);
|
||||
--ks-patina-pale: oklch(82% 0.07 188);
|
||||
--ks-patina-deep: oklch(49% 0.08 188);
|
||||
|
||||
--ks-vermilion: oklch(52% 0.16 35);
|
||||
|
||||
/* Surfaces — warm paper and cream panels. */
|
||||
--ks-lacquer: oklch(97% 0.012 95);
|
||||
--ks-lacquer-deep: oklch(94% 0.014 95);
|
||||
--ks-lacquer-raised: oklch(99% 0.008 95);
|
||||
--ks-graphite: oklch(91% 0.012 95);
|
||||
--ks-graphite-2: oklch(88% 0.014 95);
|
||||
|
||||
/* Text — dark ink on paper. */
|
||||
--ks-champagne: oklch(18% 0.02 95);
|
||||
--ks-text: oklch(25% 0.018 95);
|
||||
--ks-text-muted: oklch(45% 0.015 95);
|
||||
--ks-text-faint: oklch(55% 0.012 95);
|
||||
--ks-text-mute-deep: oklch(65% 0.01 95);
|
||||
|
||||
/* Rules — dark hairlines. */
|
||||
--ks-rule: oklch(25% 0.02 95 / 0.12);
|
||||
--ks-rule-strong: oklch(61% 0.085 78 / 0.45);
|
||||
|
||||
/* Code — light chips, dark text. */
|
||||
--ks-code-fg: oklch(28% 0.02 95);
|
||||
--ks-code-bg: var(--ks-graphite);
|
||||
--ks-code-block-fg: oklch(30% 0.02 95);
|
||||
--ks-code-block-bg: var(--ks-lacquer-raised);
|
||||
--ks-code-block-border: var(--ks-rule);
|
||||
--ks-code-cmd: var(--ks-kinpaku-ink);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -190,10 +190,14 @@
|
||||
|
||||
.live-mode-kinpaku .live-demo-frame {
|
||||
background: var(--ks-lacquer-deep);
|
||||
border: 1px solid var(--ks-rule);
|
||||
/* Crisp neutral hairline + tighter, deeper shadow that registers, instead of
|
||||
a warm gold edge and a broad soft shadow that washes out on dark. */
|
||||
border: 1px solid oklch(92% 0 0 / 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 32px 80px oklch(2% 0.004 95 / 0.55);
|
||||
box-shadow:
|
||||
inset 0 1px 0 oklch(100% 0 0 / 0.05),
|
||||
0 20px 48px -14px oklch(0% 0 0 / 0.6);
|
||||
}
|
||||
|
||||
/* All .live-demo-* picker chrome (chrome bar, gbar, ctx, outline,
|
||||
|
||||
@@ -10,68 +10,43 @@
|
||||
spans the full viewport. */
|
||||
width: 100vw;
|
||||
margin-left: calc(50% - 50vw);
|
||||
padding: 20px 0 clamp(56px, 7vw, 96px);
|
||||
background: var(--ks-lacquer);
|
||||
/* No background of its own: the body's lacquer gradient is the page ground,
|
||||
so the section sits on it directly and an opaque fill here would only
|
||||
duplicate (and, when overlapped, mask) it. */
|
||||
padding: 0 0 1em;
|
||||
color: var(--ks-text);
|
||||
border-top: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Flat dotted top divider (matches the hero → testimonials boundary). */
|
||||
.home-kinpaku .testimonials-section::before {
|
||||
/* Dotted accent at the bottom edge — a deliberate separator between the
|
||||
testimonials and the section below. Neutral dots, matching the de-warmed
|
||||
borders. (This is the dot rail that used to sit above the section.) */
|
||||
.home-kinpaku .testimonials-section::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 100vw;
|
||||
height: 18px;
|
||||
transform: translateX(-50%);
|
||||
background-image: radial-gradient(circle, oklch(0.42 0.014 82) 0.9px, transparent 1.4px);
|
||||
background-image: radial-gradient(circle, oklch(0.45 0 0) 0.9px, transparent 1.4px);
|
||||
background-size: 7px 6px;
|
||||
background-position: 0 1px;
|
||||
background-repeat: repeat;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Plinth — the tinted graphite shelf the marquee rides on
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.home-kinpaku .t-plinth {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
padding: 20px 0;
|
||||
background:
|
||||
linear-gradient(180deg, oklch(20% 0.01 78) 0%, oklch(17% 0.008 78) 100%);
|
||||
border-top: 1px solid oklch(78% 0.12 82 / 0.12);
|
||||
border-bottom: 1px solid oklch(78% 0.12 82 / 0.10);
|
||||
}
|
||||
.home-kinpaku .t-plinth--shelf::before,
|
||||
.home-kinpaku .t-plinth--shelf::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
height: 8px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.home-kinpaku .t-plinth--shelf::before {
|
||||
top: 0;
|
||||
background: linear-gradient(180deg, oklch(0% 0 0 / 0.18), transparent);
|
||||
}
|
||||
.home-kinpaku .t-plinth--shelf::after {
|
||||
bottom: 0;
|
||||
background: linear-gradient(0deg, oklch(0% 0 0 / 0.22), transparent);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Marquee engine
|
||||
Marquee engine. The cards are outlined and ride directly on the section
|
||||
ground — the old graphite plinth wrapper was removed once it stopped being
|
||||
a visible shelf, so the marquee carries its own vertical inset here.
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.home-kinpaku .t-marquee {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
width: 100%;
|
||||
padding: 22px 0;
|
||||
}
|
||||
.home-kinpaku .t-marquee-row { overflow: hidden; }
|
||||
|
||||
@@ -119,14 +94,16 @@
|
||||
transition: transform 200ms ease, background 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
|
||||
}
|
||||
|
||||
/* Plinth-resident card — borderless, slightly raised tone */
|
||||
/* Outlined card: no fill, just a solid neutral border on the page ground.
|
||||
On dark, a clean outline separates more honestly than a near-invisible
|
||||
fill or a dead drop-shadow ever did. */
|
||||
.home-kinpaku .t-card--plinth {
|
||||
background: oklch(13% 0.008 95);
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 1px 0 0 oklch(78% 0.12 82 / 0.06) inset, 0 4px 12px -8px oklch(0% 0 0 / 0.6);
|
||||
background: none;
|
||||
border: 1px solid oklch(0.64 0 0 / 0.22);
|
||||
box-shadow: none;
|
||||
}
|
||||
.home-kinpaku .t-card--plinth:hover {
|
||||
background: oklch(15% 0.012 78);
|
||||
border-color: oklch(0.64 0 0 / 0.42);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
||||
@@ -106,3 +106,19 @@ button, input, textarea, select {
|
||||
--cat-system-border: #78716c;
|
||||
--cat-system-text: #44403c;
|
||||
}
|
||||
|
||||
/* Light mode legacy aliases — mirror html.light --ks-* tokens. */
|
||||
html.light {
|
||||
--color-ink: var(--ks-champagne);
|
||||
--color-text: var(--ks-text);
|
||||
--color-paper: var(--ks-lacquer);
|
||||
--color-cream: var(--ks-lacquer-raised);
|
||||
--color-charcoal: var(--ks-text);
|
||||
--color-ash: var(--ks-text-muted);
|
||||
--color-mist: var(--ks-rule);
|
||||
--color-bg: var(--ks-lacquer-raised);
|
||||
--color-accent: var(--ks-kinpaku);
|
||||
--color-accent-hover: var(--ks-kinpaku-pale);
|
||||
--color-accent-dim: oklch(77% 0.13 82 / 0.14);
|
||||
--color-accent-soft: oklch(77% 0.13 82 / 0.24);
|
||||
}
|
||||
|
||||
@@ -46,9 +46,11 @@
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
|
||||
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
|
||||
// mark and the active control, not the container outline).
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -995,8 +997,8 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + BP.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
@@ -1071,7 +1073,7 @@
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.border = '1px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') {
|
||||
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
|
||||
@@ -2339,8 +2341,8 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
@@ -2786,13 +2788,14 @@
|
||||
inlineEditRows = rows;
|
||||
inlineEditDrafts = new Map();
|
||||
for (const row of rows) {
|
||||
row.inlineWhiteSpace = row.el.style.whiteSpace;
|
||||
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
|
||||
row.el.setAttribute('contenteditable', 'true');
|
||||
row.el.dataset.impeccableEditable = 'true';
|
||||
row.el.dataset.impeccableOriginalText = row.text;
|
||||
row.el.style.userSelect = 'text';
|
||||
row.el.style.cursor = 'text';
|
||||
row.el.style.outline = 'none';
|
||||
row.el.style.webkitUserModify = 'read-write-plaintext-only';
|
||||
row.el.addEventListener('input', onInlineInput);
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2806,10 @@
|
||||
row.el.removeAttribute('contenteditable');
|
||||
delete row.el.dataset.impeccableEditable;
|
||||
delete row.el.dataset.impeccableOriginalText;
|
||||
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
|
||||
row.el.style.userSelect = '';
|
||||
row.el.style.cursor = '';
|
||||
row.el.style.outline = '';
|
||||
row.el.style.webkitUserModify = '';
|
||||
row.el.removeEventListener('input', onInlineInput);
|
||||
}
|
||||
inlineEditRows = [];
|
||||
@@ -6122,8 +6125,15 @@ void main() {
|
||||
return {
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
|
||||
// the brand mark and the active control instead.
|
||||
border: 'oklch(92% 0 0 / 0.13)',
|
||||
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
|
||||
// wash); the gold text/icon carries the "selected" signal.
|
||||
toggleActive: 'oklch(27% 0 0)',
|
||||
// Neutral hairline for internal control borders / dividers (was a warm
|
||||
// gold rule that read as muddy champagne edges on the pill / input / count).
|
||||
hairline: 'oklch(92% 0 0 / 0.12)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
@@ -7032,8 +7042,8 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '0',
|
||||
background: P.surface,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid ' + P.border,
|
||||
borderRadius: '8px',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
@@ -7443,7 +7453,7 @@ void main() {
|
||||
// Sync one toggle's active state, colors, and slide-label visibility.
|
||||
function sync(btn, active) {
|
||||
if (!btn) return;
|
||||
btn.style.background = active ? P.accentSoft : 'transparent';
|
||||
btn.style.background = active ? P.toggleActive : 'transparent';
|
||||
btn.style.color = active ? P.accent : P.textDim;
|
||||
btn.dataset.active = active ? 'true' : 'false';
|
||||
if (active && btn._expandLabel) btn._expandLabel();
|
||||
|
||||
+93
-258
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Creates real temp directories, runs the CLI, and verifies results.
|
||||
*
|
||||
* Pure blocks (already-installed detection, prefix rename/round-trip) run in the
|
||||
* Pure blocks (already-installed detection, unprefix migration) run in the
|
||||
* default `bun run test`. Network blocks that download the universal bundle use
|
||||
* `describeNet` and run only under `bun run test:cli-e2e` (IMPECCABLE_CLI_E2E=1),
|
||||
* skipping gracefully when impeccable.style is unreachable.
|
||||
@@ -13,6 +13,7 @@ import { execSync } from 'child_process';
|
||||
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { migrateUnprefixImpeccable } from '../cli/bin/commands/skills.mjs';
|
||||
|
||||
const CLI = join(import.meta.dir, '..', 'cli', 'bin', 'cli.js');
|
||||
|
||||
@@ -43,6 +44,25 @@ function createFakeSkills(root, skills = ['audit', 'polish', 'impeccable'], prov
|
||||
}
|
||||
}
|
||||
|
||||
/** Write one fake skill dir with a SKILL.md naming itself. */
|
||||
function writeSkill(root, provider, name) {
|
||||
const dir = join(root, provider, 'skills', name);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, 'SKILL.md'), `---\nname: ${name}\n---\nRun /${name}.\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate an install from the era when the CLI offered a command prefix: the
|
||||
* skill lives at `<prefix>impeccable`. Optionally drop in a third-party skill
|
||||
* (one that even starts with the same prefix) that migration must NOT touch.
|
||||
*/
|
||||
function createPrefixedInstall(root, { prefix = 'i-', providers = ['.claude'], foreign = null } = {}) {
|
||||
for (const provider of providers) {
|
||||
writeSkill(root, provider, `${prefix}impeccable`);
|
||||
if (foreign) writeSkill(root, provider, foreign);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Already-installed detection ─────────────────────────────────────────────
|
||||
|
||||
// Network e2e blocks (real bundle downloads from impeccable.style) run only
|
||||
@@ -86,103 +106,79 @@ describe('skills install: already-installed detection', () => {
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
// ─── Prefix rename (real filesystem) ─────────────────────────────────────────
|
||||
// ─── Unprefix migration (real implementation, real filesystem) ───────────────
|
||||
//
|
||||
// The CLI no longer offers a command prefix (the `i-` rename only made sense
|
||||
// when each command was its own skill). migrateUnprefixImpeccable retires any
|
||||
// old `<prefix>impeccable` install back to the canonical `impeccable`, so an
|
||||
// update lands fresh content there instead of orphaning the prefixed copy.
|
||||
// These call the EXPORTED function -- not a reimplementation -- so a regression
|
||||
// in the real code fails the suite.
|
||||
|
||||
describe('skills install: prefix rename', () => {
|
||||
let tmp;
|
||||
describe('skills: unprefix migration', () => {
|
||||
test('renames i-impeccable back to impeccable across every provider', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-mig-'));
|
||||
createPrefixedInstall(tmp, { prefix: 'i-', providers: ['.claude', '.cursor'] });
|
||||
|
||||
beforeAll(() => {
|
||||
tmp = mkdtempSync(join(tmpdir(), 'imp-test-pfx-'));
|
||||
createFakeSkills(tmp, ['audit', 'polish', 'impeccable'], ['.claude', '.cursor']);
|
||||
const migrated = migrateUnprefixImpeccable(tmp);
|
||||
expect(migrated).toBe(2); // one skill x two providers
|
||||
|
||||
for (const provider of ['.claude', '.cursor']) {
|
||||
const skills = readdirSync(join(tmp, provider, 'skills'));
|
||||
expect(skills).toContain('impeccable');
|
||||
expect(skills).not.toContain('i-impeccable');
|
||||
}
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (tmp) rmSync(tmp, { recursive: true, force: true });
|
||||
test('migrates a custom prefix too (x-impeccable)', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-mig-x-'));
|
||||
createPrefixedInstall(tmp, { prefix: 'x-' });
|
||||
|
||||
expect(migrateUnprefixImpeccable(tmp)).toBe(1);
|
||||
expect(readdirSync(join(tmp, '.claude', 'skills'))).toContain('impeccable');
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('renames folders with prefix', () => {
|
||||
// Write a helper script that imports and runs renameSkillsWithPrefix
|
||||
const helperScript = join(tmp, '_test_rename.mjs');
|
||||
writeFileSync(helperScript, `
|
||||
import { existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
test('REGRESSION: never touches third-party skills, even ones starting with i-', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-mig-scope-'));
|
||||
// A foreign skill that shares the i- prefix but is NOT impeccable.
|
||||
createPrefixedInstall(tmp, { prefix: 'i-', foreign: 'i-cool-skill' });
|
||||
|
||||
function escapeRegex(str) {
|
||||
return str.replace(/[.*+?^$\{\}()|[\\]\\\\]/g, '\\\\$&');
|
||||
}
|
||||
const migrated = migrateUnprefixImpeccable(tmp);
|
||||
expect(migrated).toBe(1); // only i-impeccable
|
||||
|
||||
function prefixSkillContent(content, prefix, allSkillNames) {
|
||||
let result = content.replace(/^name:\\s*(.+)$/m, (_, name) => 'name: ' + prefix + name.trim());
|
||||
const sorted = [...allSkillNames].sort((a, b) => b.length - a.length);
|
||||
for (const name of sorted) {
|
||||
result = result.replace(
|
||||
new RegExp('/' + '(?=' + escapeRegex(name) + '(?:[^a-zA-Z0-9_-]|$))', 'g'),
|
||||
'/' + prefix
|
||||
);
|
||||
result = result.replace(
|
||||
new RegExp('(the) ' + escapeRegex(name) + ' skill', 'gi'),
|
||||
(_, article) => article + ' ' + prefix + name + ' skill'
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const DIRS = ['.claude', '.cursor'];
|
||||
const root = process.argv[2];
|
||||
const prefix = process.argv[3];
|
||||
|
||||
let allNames = [];
|
||||
for (const d of DIRS) {
|
||||
const dir = join(root, d, 'skills');
|
||||
if (!existsSync(dir)) continue;
|
||||
allNames = readdirSync(dir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name);
|
||||
if (allNames.length > 0) break;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const d of DIRS) {
|
||||
const dir = join(root, d, 'skills');
|
||||
if (!existsSync(dir)) continue;
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || entry.name.startsWith(prefix)) continue;
|
||||
const skillMd = join(dir, entry.name, 'SKILL.md');
|
||||
if (!existsSync(skillMd)) continue;
|
||||
renameSync(join(dir, entry.name), join(dir, prefix + entry.name));
|
||||
let content = readFileSync(join(dir, prefix + entry.name, 'SKILL.md'), 'utf8');
|
||||
content = prefixSkillContent(content, prefix, allNames);
|
||||
writeFileSync(join(dir, prefix + entry.name, 'SKILL.md'), content);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ count }));
|
||||
`);
|
||||
|
||||
const output = JSON.parse(execSync(`node ${helperScript} ${tmp} i-`, { encoding: 'utf8' }));
|
||||
expect(output.count).toBe(6); // 3 skills x 2 providers
|
||||
|
||||
// Verify folders renamed
|
||||
const skills = readdirSync(join(tmp, '.claude', 'skills'));
|
||||
expect(skills).toContain('i-audit');
|
||||
expect(skills).toContain('i-polish');
|
||||
expect(skills).toContain('i-impeccable');
|
||||
expect(skills).not.toContain('audit');
|
||||
expect(skills).not.toContain('polish');
|
||||
}, 15000);
|
||||
expect(skills).toContain('impeccable');
|
||||
expect(skills).toContain('i-cool-skill'); // untouched, NOT renamed to cool-skill
|
||||
expect(skills).not.toContain('cool-skill');
|
||||
|
||||
test('prefixed SKILL.md has correct name and cross-references', () => {
|
||||
const content = readFileSync(join(tmp, '.claude', 'skills', 'i-audit', 'SKILL.md'), 'utf8');
|
||||
expect(content).toContain('name: i-audit');
|
||||
expect(content).toContain('/i-audit');
|
||||
expect(content).toContain('/i-polish');
|
||||
expect(content).toContain('the i-impeccable skill');
|
||||
// Original unprefixed references should be gone
|
||||
expect(content).not.toMatch(/\/audit(?=[^a-zA-Z0-9_-]|$)/);
|
||||
const foreign = readFileSync(join(tmp, '.claude', 'skills', 'i-cool-skill', 'SKILL.md'), 'utf8');
|
||||
expect(foreign).toContain('name: i-cool-skill');
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('also prefixed in second provider', () => {
|
||||
const skills = readdirSync(join(tmp, '.cursor', 'skills'));
|
||||
expect(skills).toContain('i-audit');
|
||||
expect(skills).toContain('i-impeccable');
|
||||
test('leaves a clean impeccable install alone (no-op)', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-mig-clean-'));
|
||||
createFakeSkills(tmp, ['impeccable'], ['.claude']);
|
||||
|
||||
expect(migrateUnprefixImpeccable(tmp)).toBe(0);
|
||||
expect(readdirSync(join(tmp, '.claude', 'skills'))).toContain('impeccable');
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('leaves the legacy teach-impeccable name alone (cleanup owns that)', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-mig-legacy-'));
|
||||
createFakeSkills(tmp, ['teach-impeccable'], ['.claude']);
|
||||
|
||||
expect(migrateUnprefixImpeccable(tmp)).toBe(0);
|
||||
expect(readdirSync(join(tmp, '.claude', 'skills'))).toContain('teach-impeccable');
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -223,162 +219,6 @@ describeNet('skills update: refreshes from the universal bundle', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Prefix round-trip: detect, undo, re-apply ──────────────────────────────
|
||||
|
||||
describe('prefix round-trip: detect, undo, re-apply', () => {
|
||||
let tmp;
|
||||
|
||||
beforeAll(() => {
|
||||
tmp = mkdtempSync(join(tmpdir(), 'imp-test-roundtrip-'));
|
||||
// Create prefixed skills to simulate post-install state
|
||||
for (const skill of ['audit', 'polish', 'impeccable']) {
|
||||
const skillDir = join(tmp, '.claude', 'skills', 'i-' + skill);
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(join(skillDir, 'SKILL.md'), [
|
||||
'---',
|
||||
`name: i-${skill}`,
|
||||
'user-invocable: true',
|
||||
'---',
|
||||
'',
|
||||
'Run /i-audit first, then /i-polish to finish.',
|
||||
'Use the i-impeccable skill for setup.',
|
||||
].join('\n'));
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (tmp) rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('detectPrefix finds the prefix from skill names', () => {
|
||||
const script = join(tmp, '_detect.mjs');
|
||||
writeFileSync(script, `
|
||||
import { existsSync, readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
const DIRS = ['.claude', '.cursor', '.agents'];
|
||||
const root = ${JSON.stringify(tmp)};
|
||||
for (const d of DIRS) {
|
||||
const dir = join(root, d, 'skills');
|
||||
if (!existsSync(dir)) continue;
|
||||
for (const name of readdirSync(dir)) {
|
||||
if (name === 'impeccable') { console.log(''); process.exit(); }
|
||||
if (name.endsWith('-impeccable')) { console.log(name.slice(0, -'impeccable'.length)); process.exit(); }
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
`);
|
||||
const output = execSync(`node ${script}`, { encoding: 'utf8' }).trim();
|
||||
expect(output).toBe('i-');
|
||||
});
|
||||
|
||||
test('undo removes prefix from folders and content', () => {
|
||||
// Write undo helper script
|
||||
const script = join(tmp, '_undo.mjs');
|
||||
writeFileSync(script, `
|
||||
import { existsSync, readdirSync, readFileSync, lstatSync, readlinkSync, unlinkSync, renameSync, writeFileSync, symlinkSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
function escapeRegex(str) { return str.replace(/[.*+?^$\{\\}()|[\\]\\\\]/g, '\\\\$&'); }
|
||||
|
||||
const root = ${JSON.stringify(tmp)};
|
||||
const prefix = 'i-';
|
||||
const skillsDir = join(root, '.claude', 'skills');
|
||||
const entries = readdirSync(skillsDir);
|
||||
const prefixedNames = entries.filter(n => n.startsWith(prefix));
|
||||
|
||||
for (const name of entries) {
|
||||
if (!name.startsWith(prefix)) continue;
|
||||
const unprefixed = name.slice(prefix.length);
|
||||
const src = join(skillsDir, name);
|
||||
const dest = join(skillsDir, unprefixed);
|
||||
|
||||
if (lstatSync(src).isSymbolicLink()) {
|
||||
const target = readlinkSync(src);
|
||||
unlinkSync(src);
|
||||
symlinkSync(target.replace('/' + name, '/' + unprefixed), dest);
|
||||
} else {
|
||||
renameSync(src, dest);
|
||||
const skillMd = join(dest, 'SKILL.md');
|
||||
if (existsSync(skillMd)) {
|
||||
let content = readFileSync(skillMd, 'utf8');
|
||||
content = content.replace(new RegExp('^name:\\\\s*' + escapeRegex(prefix), 'm'), 'name: ');
|
||||
const sorted = [...prefixedNames].sort((a, b) => b.length - a.length);
|
||||
for (const pName of sorted) {
|
||||
const uName = pName.slice(prefix.length);
|
||||
content = content.replace(new RegExp('/' + escapeRegex(pName) + '(?=[^a-zA-Z0-9_-]|$)', 'g'), '/' + uName);
|
||||
content = content.replace(new RegExp('(the) ' + escapeRegex(pName) + ' skill', 'gi'), '$1 ' + uName + ' skill');
|
||||
}
|
||||
writeFileSync(skillMd, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify(readdirSync(skillsDir)));
|
||||
`);
|
||||
const output = JSON.parse(execSync(`node ${script}`, { encoding: 'utf8' }));
|
||||
expect(output).toContain('audit');
|
||||
expect(output).toContain('polish');
|
||||
expect(output).toContain('impeccable');
|
||||
expect(output).not.toContain('i-audit');
|
||||
|
||||
// Verify content was un-prefixed
|
||||
const content = readFileSync(join(tmp, '.claude', 'skills', 'audit', 'SKILL.md'), 'utf8');
|
||||
expect(content).toContain('name: audit');
|
||||
expect(content).toContain('/audit');
|
||||
expect(content).toContain('/polish');
|
||||
expect(content).toContain('the impeccable skill');
|
||||
expect(content).not.toContain('/i-audit');
|
||||
expect(content).not.toContain('i-impeccable');
|
||||
});
|
||||
|
||||
test('re-applying prefix restores original state', () => {
|
||||
// Now re-apply using the same helper from the rename test
|
||||
const script = join(tmp, '_reprefix.mjs');
|
||||
writeFileSync(script, `
|
||||
import { existsSync, readdirSync, readFileSync, statSync, lstatSync, renameSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
function escapeRegex(str) { return str.replace(/[.*+?^$\{\\}()|[\\]\\\\]/g, '\\\\$&'); }
|
||||
|
||||
const root = ${JSON.stringify(tmp)};
|
||||
const prefix = 'i-';
|
||||
const skillsDir = join(root, '.claude', 'skills');
|
||||
const allNames = readdirSync(skillsDir).filter(n => {
|
||||
const full = join(skillsDir, n);
|
||||
try { return statSync(full).isDirectory() && existsSync(join(full, 'SKILL.md')); } catch { return false; }
|
||||
});
|
||||
|
||||
for (const name of allNames) {
|
||||
if (name.startsWith(prefix)) continue;
|
||||
const src = join(skillsDir, name);
|
||||
const dest = join(skillsDir, prefix + name);
|
||||
const ls = lstatSync(src);
|
||||
if (ls.isSymbolicLink() || !ls.isDirectory()) continue;
|
||||
renameSync(src, dest);
|
||||
let content = readFileSync(join(dest, 'SKILL.md'), 'utf8');
|
||||
content = content.replace(/^name:\\s*(.+)$/m, (_, n) => 'name: ' + prefix + n.trim());
|
||||
const sorted = [...allNames].sort((a, b) => b.length - a.length);
|
||||
for (const n of sorted) {
|
||||
content = content.replace(new RegExp('/' + '(?=' + escapeRegex(n) + '(?:[^a-zA-Z0-9_-]|$))', 'g'), '/' + prefix);
|
||||
content = content.replace(new RegExp('(the) ' + escapeRegex(n) + ' skill', 'gi'), (_, art) => art + ' ' + prefix + n + ' skill');
|
||||
}
|
||||
writeFileSync(join(dest, 'SKILL.md'), content);
|
||||
}
|
||||
console.log(JSON.stringify(readdirSync(skillsDir)));
|
||||
`);
|
||||
const output = JSON.parse(execSync(`node ${script}`, { encoding: 'utf8' }));
|
||||
expect(output).toContain('i-audit');
|
||||
expect(output).toContain('i-polish');
|
||||
expect(output).toContain('i-impeccable');
|
||||
expect(output).not.toContain('audit');
|
||||
|
||||
// Verify content was re-prefixed
|
||||
const content = readFileSync(join(tmp, '.claude', 'skills', 'i-audit', 'SKILL.md'), 'utf8');
|
||||
expect(content).toContain('name: i-audit');
|
||||
expect(content).toContain('/i-polish');
|
||||
expect(content).toContain('the i-impeccable skill');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Full install e2e (downloads the universal bundle) ───────────────────────
|
||||
|
||||
describeNet('skills install: full e2e (universal bundle download)', () => {
|
||||
@@ -404,22 +244,17 @@ describeNet('skills install: full e2e (universal bundle download)', () => {
|
||||
expect(hasSkills).toBe(true);
|
||||
}, 90000);
|
||||
|
||||
test('install with --prefix= renames all skills', () => {
|
||||
const output = run('skills install -y --force --prefix=x-', { cwd: tmp });
|
||||
test('--force reinstall over an old prefixed install lands on canonical impeccable', () => {
|
||||
// Seed a stale prefixed install, then reinstall. The migration should
|
||||
// retire i-impeccable so we are left with the canonical name only.
|
||||
const prefixed = join(tmp, '.claude', 'skills', 'i-impeccable');
|
||||
mkdirSync(prefixed, { recursive: true });
|
||||
writeFileSync(join(prefixed, 'SKILL.md'), '---\nname: i-impeccable\n---\n');
|
||||
|
||||
// Find the provider that has skills
|
||||
let found = false;
|
||||
for (const d of ['.claude', '.cursor', '.gemini', '.agents', '.kiro']) {
|
||||
const dir = join(tmp, d, 'skills');
|
||||
if (!existsSync(dir)) continue;
|
||||
const skills = readdirSync(dir);
|
||||
if (skills.length === 0) continue;
|
||||
found = true;
|
||||
const prefixed = skills.filter(s => s.startsWith('x-'));
|
||||
// All skills should be prefixed
|
||||
expect(prefixed.length).toBe(skills.length);
|
||||
break;
|
||||
}
|
||||
expect(found).toBe(true);
|
||||
run('skills install -y --force', { cwd: tmp });
|
||||
|
||||
const skills = readdirSync(join(tmp, '.claude', 'skills'));
|
||||
expect(skills).toContain('impeccable');
|
||||
expect(skills).not.toContain('i-impeccable');
|
||||
}, 90000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user