CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 36s
CI & Build / integration (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m12s
CI & Build / Build & push image (push) Successful in 1m2s
The badge fix (#3132) exposed the same defect everywhere: 48 rules painting a token as TEXT on an inline color-mix tint of that same token. Worst raw measurements, across every tint strength in use, both modes, over page/raised/hover: accent 1.53:1 · success 1.67:1 · text-tertiary 2.15:1 warning 2.32:1 · error 2.36:1 against AA's 4.5 THE DEFECT IS IN THE HOUSE, NOT IN SCRIBE. The semantic hues are shared family-wide, and the accent case was measured against every app's real accent, not assumed from Scribe's: Minstrel 1.81, Forge 1.87, Steward 1.65, Roundtable 3.01 — all failing. So the six -fg tokens are recorded on FabledSword (design system 1), where their parents live, rather than copied into each app. 45% toward --fs-text-primary clears AA for ALL FIVE accents (4.56-5.00), so this is one house token rather than five overrides, and it keeps deriving from --fs-accent — an app that overrides its accent still gets a legible tinted-text colour in its own colour, the same mechanism as --fs-accent-soft. The tokens are additive: a sibling app is unaffected until it regenerates its own stylesheet. One token is honestly redundant. --fs-text-secondary already passes at 4.82:1, and --fs-text-secondary-fg barely moves it. It exists so the rule has NO exceptions, because the alternative is a permanent allow-list entry for the one case that happens to pass — and a guard with an invisible exception is a guard that erodes. 46 substitutions across 18 files, each rewriting only the `color:` inside a block that tints its own background. THE CHECK NOW GATES BOTH SPELLINGS. It previously reported the inline form, because a gate nobody can satisfy on the day it lands gets switched off. Both are clean, so both fail the build now. And the check had a false-positive bug worth naming: its `color\s*:` regex matched the tail of `border-color`, `border-left-color` and `outline-color`, so it flagged seven rules that were already correct. A border is a non-text graphic with a 3:1 floor, not text at 4.5. A check that cries wolf on correct code is one that gets muted, so that mattered more than the noise. Verified by construction, not by passing: reintroduced each defect form (exit 1 each), and confirmed a legitimate border-only rule still exits 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
175 lines
4.3 KiB
Vue
175 lines
4.3 KiB
Vue
<script setup lang="ts">
|
||
import { computed } from "vue";
|
||
import type { DiffLine } from "@/composables/useAssist";
|
||
|
||
const props = defineProps<{
|
||
diff: DiffLine[];
|
||
}>();
|
||
|
||
const CONTEXT_LINES = 3;
|
||
const MIN_COLLAPSE = 6; // Only collapse if there are more than this many consecutive equal lines
|
||
|
||
type CollapseMarker = { type: 'collapse'; count: number };
|
||
type DisplayItem = DiffLine | CollapseMarker;
|
||
|
||
const insertions = computed(() => props.diff.filter(l => l.type === 'insert').length);
|
||
const deletions = computed(() => props.diff.filter(l => l.type === 'delete').length);
|
||
|
||
const displayItems = computed<DisplayItem[]>(() => {
|
||
const d = props.diff;
|
||
if (!d.length) return [];
|
||
|
||
// Mark which indices are within CONTEXT_LINES of a change
|
||
const shown = new Set<number>();
|
||
for (let i = 0; i < d.length; i++) {
|
||
if (d[i].type !== 'equal') {
|
||
for (let c = Math.max(0, i - CONTEXT_LINES); c <= Math.min(d.length - 1, i + CONTEXT_LINES); c++) {
|
||
shown.add(c);
|
||
}
|
||
}
|
||
}
|
||
|
||
// If nothing changed, show all
|
||
if (shown.size === 0) return d;
|
||
|
||
const result: DisplayItem[] = [];
|
||
let i = 0;
|
||
while (i < d.length) {
|
||
if (shown.has(i)) {
|
||
result.push(d[i]);
|
||
i++;
|
||
} else {
|
||
let count = 0;
|
||
while (i < d.length && !shown.has(i)) { count++; i++; }
|
||
if (count >= MIN_COLLAPSE) {
|
||
result.push({ type: 'collapse', count });
|
||
} else {
|
||
// Too short to collapse — show as-is
|
||
for (let k = i - count; k < i; k++) result.push(d[k]);
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
});
|
||
|
||
function markerFor(type: DiffLine['type']): string {
|
||
if (type === 'insert') return '+';
|
||
if (type === 'delete') return '−';
|
||
return ' ';
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="diff-view-full">
|
||
<div class="diff-summary-bar">
|
||
<span class="diff-summary-ins">+{{ insertions }} insertion{{ insertions !== 1 ? 's' : '' }}</span>
|
||
<span class="diff-summary-del">−{{ deletions }} deletion{{ deletions !== 1 ? 's' : '' }}</span>
|
||
</div>
|
||
<div class="diff-scroll">
|
||
<div v-if="!diff.length" class="diff-empty">No changes.</div>
|
||
<template v-for="(item, i) in displayItems" :key="i">
|
||
<div v-if="item.type === 'collapse'" class="diff-line diff-collapse">
|
||
<span class="diff-marker">⋯</span>
|
||
<span class="diff-text">{{ item.count }} unchanged line{{ item.count !== 1 ? 's' : '' }}</span>
|
||
</div>
|
||
<div
|
||
v-else
|
||
:class="['diff-line', `diff-${item.type}`]"
|
||
>
|
||
<span class="diff-marker">{{ markerFor(item.type) }}</span>
|
||
<span class="diff-text">{{ item.text }}</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.diff-view-full {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
min-height: 0;
|
||
border: 1px solid var(--fs-border-color);
|
||
border-radius: var(--fs-radius-sm);
|
||
background: var(--fs-surface-page);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.diff-summary-bar {
|
||
position: sticky;
|
||
top: 0;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
gap: 1rem;
|
||
padding: 0.4rem 0.75rem;
|
||
background: var(--fs-surface-raised);
|
||
border-bottom: 1px solid var(--fs-border-color);
|
||
font-size: 0.78rem;
|
||
font-family: monospace;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.diff-summary-ins { color: var(--fs-success); }
|
||
.diff-summary-del { color: var(--fs-error); }
|
||
|
||
.diff-scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
padding: 0.3rem 0;
|
||
}
|
||
|
||
.diff-empty {
|
||
padding: 0.75rem;
|
||
font-size: 0.85rem;
|
||
color: var(--fs-text-tertiary);
|
||
}
|
||
|
||
.diff-line {
|
||
display: flex;
|
||
align-items: baseline;
|
||
padding: 0.04rem 0.5rem;
|
||
line-height: 1.55;
|
||
font-family: monospace;
|
||
font-size: 0.82rem;
|
||
}
|
||
|
||
.diff-delete {
|
||
background: color-mix(in srgb, var(--fs-error) 12%, transparent);
|
||
color: var(--fs-error-fg);
|
||
}
|
||
|
||
.diff-insert {
|
||
background: color-mix(in srgb, var(--fs-success) 12%, transparent);
|
||
color: var(--fs-success-fg);
|
||
}
|
||
|
||
.diff-equal {
|
||
color: var(--fs-text-tertiary);
|
||
}
|
||
|
||
.diff-collapse {
|
||
color: var(--fs-text-tertiary);
|
||
opacity: 0.6;
|
||
border-top: 1px dashed var(--fs-border-color);
|
||
border-bottom: 1px dashed var(--fs-border-color);
|
||
padding-top: 0.2rem;
|
||
padding-bottom: 0.2rem;
|
||
}
|
||
|
||
.diff-marker {
|
||
flex-shrink: 0;
|
||
width: 1rem;
|
||
text-align: center;
|
||
user-select: none;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.diff-text {
|
||
flex: 1;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
</style>
|