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
5.4 KiB
Vue
175 lines
5.4 KiB
Vue
<script setup lang="ts">
|
|
import type { Editor } from "@tiptap/vue-3";
|
|
import {
|
|
Bold,
|
|
Italic,
|
|
Strikethrough,
|
|
Heading1,
|
|
Heading2,
|
|
Heading3,
|
|
List,
|
|
ListOrdered,
|
|
ListChecks,
|
|
Code,
|
|
SquareCode,
|
|
Quote,
|
|
Link as LinkIcon,
|
|
Brackets,
|
|
} from "lucide-vue-next";
|
|
import type { Component } from "vue";
|
|
|
|
const props = defineProps<{ editor: Editor | null }>();
|
|
|
|
const ICONS: Record<string, Component> = {
|
|
bold: Bold,
|
|
italic: Italic,
|
|
strike: Strikethrough,
|
|
h1: Heading1,
|
|
h2: Heading2,
|
|
h3: Heading3,
|
|
ul: List,
|
|
ol: ListOrdered,
|
|
task: ListChecks,
|
|
code: Code,
|
|
codeblock: SquareCode,
|
|
quote: Quote,
|
|
link: LinkIcon,
|
|
wikilink: Brackets,
|
|
};
|
|
|
|
const groups = [
|
|
[
|
|
{ id: "bold", title: "Bold (Ctrl+B)", isActive: () => props.editor?.isActive("bold") ?? false, command: () => props.editor?.chain().focus().toggleBold().run() },
|
|
{ id: "italic", title: "Italic (Ctrl+I)", isActive: () => props.editor?.isActive("italic") ?? false, command: () => props.editor?.chain().focus().toggleItalic().run() },
|
|
{ id: "strike", title: "Strikethrough", isActive: () => props.editor?.isActive("strike") ?? false, command: () => props.editor?.chain().focus().toggleStrike().run() },
|
|
],
|
|
[
|
|
{ id: "h1", title: "Heading 1", isActive: () => props.editor?.isActive("heading", { level: 1 }) ?? false, command: () => props.editor?.chain().focus().toggleHeading({ level: 1 }).run() },
|
|
{ id: "h2", title: "Heading 2", isActive: () => props.editor?.isActive("heading", { level: 2 }) ?? false, command: () => props.editor?.chain().focus().toggleHeading({ level: 2 }).run() },
|
|
{ id: "h3", title: "Heading 3", isActive: () => props.editor?.isActive("heading", { level: 3 }) ?? false, command: () => props.editor?.chain().focus().toggleHeading({ level: 3 }).run() },
|
|
],
|
|
[
|
|
{ id: "ul", title: "Bullet List", isActive: () => props.editor?.isActive("bulletList") ?? false, command: () => props.editor?.chain().focus().toggleBulletList().run() },
|
|
{ id: "ol", title: "Ordered List", isActive: () => props.editor?.isActive("orderedList") ?? false, command: () => props.editor?.chain().focus().toggleOrderedList().run() },
|
|
{ id: "task", title: "Task List", isActive: () => props.editor?.isActive("taskList") ?? false, command: () => props.editor?.chain().focus().toggleTaskList().run() },
|
|
],
|
|
[
|
|
{ id: "code", title: "Inline Code", isActive: () => props.editor?.isActive("code") ?? false, command: () => props.editor?.chain().focus().toggleCode().run() },
|
|
{ id: "codeblock", title: "Code Block", isActive: () => props.editor?.isActive("codeBlock") ?? false, command: () => props.editor?.chain().focus().toggleCodeBlock().run() },
|
|
{ id: "quote", title: "Blockquote", isActive: () => props.editor?.isActive("blockquote") ?? false, command: () => props.editor?.chain().focus().toggleBlockquote().run() },
|
|
],
|
|
[
|
|
{
|
|
id: "link",
|
|
title: "Link",
|
|
isActive: () => props.editor?.isActive("link") ?? false,
|
|
command: () => {
|
|
const ed = props.editor;
|
|
if (!ed) return;
|
|
if (ed.isActive("link")) {
|
|
ed.chain().focus().unsetLink().run();
|
|
} else {
|
|
const url = prompt("URL:");
|
|
if (url) ed.chain().focus().setLink({ href: url }).run();
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: "wikilink",
|
|
title: "Insert wikilink [[ ]]",
|
|
isActive: () => false,
|
|
command: () => props.editor?.chain().focus().insertContent("[[").run(),
|
|
},
|
|
],
|
|
];
|
|
</script>
|
|
|
|
<template>
|
|
<div class="md-toolbar" role="toolbar" aria-label="Text formatting">
|
|
<template v-for="(group, gi) in groups" :key="gi">
|
|
<div class="toolbar-group">
|
|
<button
|
|
v-for="btn in group"
|
|
:key="btn.id"
|
|
:class="['md-btn', { active: btn.isActive() }]"
|
|
:title="btn.title"
|
|
type="button"
|
|
tabindex="-1"
|
|
@mousedown.prevent="btn.command()"
|
|
>
|
|
<component :is="ICONS[btn.id]" class="btn-icon" :size="16" />
|
|
</button>
|
|
</div>
|
|
<span v-if="gi < groups.length - 1" class="toolbar-sep" aria-hidden="true" />
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.md-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
flex-wrap: wrap;
|
|
background: var(--fs-surface-raised);
|
|
border: 1px solid var(--fs-border-color);
|
|
border-radius: var(--fs-radius-lg);
|
|
padding: 3px 4px;
|
|
}
|
|
|
|
.toolbar-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1px;
|
|
}
|
|
|
|
.toolbar-sep {
|
|
display: block;
|
|
width: 1px;
|
|
height: 18px;
|
|
background: var(--fs-border-color);
|
|
flex-shrink: 0;
|
|
margin: 0 3px;
|
|
}
|
|
|
|
.md-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px;
|
|
height: 28px;
|
|
border: none;
|
|
border-radius: 5px;
|
|
background: transparent;
|
|
color: var(--fs-text-secondary);
|
|
cursor: pointer;
|
|
padding: 0;
|
|
transition: background 0.12s, color 0.12s, box-shadow 0.12s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.md-btn:hover {
|
|
background: var(--fs-surface-raised);
|
|
color: var(--fs-text-primary);
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.md-btn.active {
|
|
background: color-mix(in srgb, var(--fs-accent) 14%, transparent);
|
|
color: var(--fs-accent-fg);
|
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--fs-accent) 35%, transparent);
|
|
}
|
|
|
|
.md-btn.active:hover {
|
|
background: color-mix(in srgb, var(--fs-accent) 22%, transparent);
|
|
}
|
|
|
|
.btn-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
line-height: 0;
|
|
pointer-events: none;
|
|
}
|
|
</style>
|