@troshab/slidev-theme-troshab 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +537 -0
- package/LICENSE +134 -0
- package/README.md +168 -0
- package/SKILL.md +414 -0
- package/components/AnimatedCounter.vue +35 -0
- package/components/Background.vue +204 -0
- package/components/Callout.vue +135 -0
- package/components/Card.vue +75 -0
- package/components/CardGrid.vue +67 -0
- package/components/CaseStudy.vue +66 -0
- package/components/CodeDiff.vue +229 -0
- package/components/CodeHighlight.vue +337 -0
- package/components/ColorSwatch.vue +114 -0
- package/components/Confetti.vue +292 -0
- package/components/Conversation.vue +405 -0
- package/components/Countdown.vue +476 -0
- package/components/Definition.vue +59 -0
- package/components/DeviceMockup.vue +392 -0
- package/components/Funnel.vue +87 -0
- package/components/Icon.vue +73 -0
- package/components/Iframe.vue +38 -0
- package/components/Image.vue +69 -0
- package/components/ImageCompare.vue +436 -0
- package/components/MatrixGrid.vue +85 -0
- package/components/MermaidChart.vue +299 -0
- package/components/Metric.vue +161 -0
- package/components/PersonCard.vue +165 -0
- package/components/PricingTable.vue +144 -0
- package/components/Progress.vue +100 -0
- package/components/Pyramid.vue +81 -0
- package/components/QRCode.vue +137 -0
- package/components/QuoteBlock.vue +101 -0
- package/components/SpeechBubble.vue +169 -0
- package/components/Stepper.vue +542 -0
- package/components/StyledList.vue +156 -0
- package/components/StyledText.vue +275 -0
- package/components/SwotGrid.vue +99 -0
- package/components/Tags.vue +20 -0
- package/components/Testimonial.vue +243 -0
- package/components/Typewriter.vue +181 -0
- package/components_base/AnimatedCounter.vue +208 -0
- package/components_base/CodeHighlight.vue +364 -0
- package/composables/useColors.ts +101 -0
- package/composables/useShiki.ts +81 -0
- package/example_content.md +371 -0
- package/example_dark.md +10 -0
- package/example_slides/001-cover.md +15 -0
- package/example_slides/002-agenda.md +25 -0
- package/example_slides/003-section-layouts.md +14 -0
- package/example_slides/004-fullscreen-centered.md +7 -0
- package/example_slides/005-fullscreen-align-bottom.md +14 -0
- package/example_slides/006-fullscreen-no-padding.md +14 -0
- package/example_slides/007-fullscreen-bg-image-dark.md +13 -0
- package/example_slides/008-fullscreen-bg-image-light.md +13 -0
- package/example_slides/009-fullscreen-bg-gradient.md +15 -0
- package/example_slides/010-fullscreen-bg-color.md +13 -0
- package/example_slides/011-split-basic.md +17 -0
- package/example_slides/012-split-image-text.md +18 -0
- package/example_slides/013-split-contrast.md +22 -0
- package/example_slides/014-columns-basic.md +13 -0
- package/example_slides/015-columns-two.md +26 -0
- package/example_slides/016-columns-ratios.md +22 -0
- package/example_slides/017-columns-three.md +31 -0
- package/example_slides/018-columns-four.md +22 -0
- package/example_slides/019-columns-alignment.md +23 -0
- package/example_slides/020-columns-styled.md +21 -0
- package/example_slides/021-footnote-prop.md +16 -0
- package/example_slides/022-iframe-fullscreen.md +8 -0
- package/example_slides/023-iframe-split.md +18 -0
- package/example_slides/024-section-components.md +14 -0
- package/example_slides/025-styled-text.md +9 -0
- package/example_slides/026-styled-text.md +15 -0
- package/example_slides/027-text-formatting.md +28 -0
- package/example_slides/028-text-spoiler.md +15 -0
- package/example_slides/029-icon-component.md +47 -0
- package/example_slides/030-metric-component.md +29 -0
- package/example_slides/031-person-card.md +33 -0
- package/example_slides/032-styled-list.md +50 -0
- package/example_slides/033-color-swatch.md +35 -0
- package/example_slides/034-code-highlight.md +9 -0
- package/example_slides/035-iframe-component.md +9 -0
- package/example_slides/036-callout.md +15 -0
- package/example_slides/037-card-grid.md +27 -0
- package/example_slides/038-stepper-variants.md +18 -0
- package/example_slides/039-stepper-clicks.md +49 -0
- package/example_slides/040-stepper-interactive.md +28 -0
- package/example_slides/041-tags-progress.md +21 -0
- package/example_slides/042-speech-bubble.md +30 -0
- package/example_slides/043-conversation.md +13 -0
- package/example_slides/044-device-iphone.md +26 -0
- package/example_slides/045-device-browser.md +7 -0
- package/example_slides/046-qrcode.md +26 -0
- package/example_slides/047-countdown.md +14 -0
- package/example_slides/048-typewriter.md +8 -0
- package/example_slides/049-confetti.md +16 -0
- package/example_slides/050-image-compare.md +13 -0
- package/example_slides/051-code-diff.md +24 -0
- package/example_slides/052-quote-block.md +8 -0
- package/example_slides/053-testimonial.md +26 -0
- package/example_slides/054-testimonial-featured.md +16 -0
- package/example_slides/055-funnel.md +12 -0
- package/example_slides/056-pyramid.md +13 -0
- package/example_slides/057-pricing-table.md +9 -0
- package/example_slides/058-swot-grid.md +12 -0
- package/example_slides/059-matrix-grid.md +12 -0
- package/example_slides/060-case-study.md +11 -0
- package/example_slides/061-definition.md +15 -0
- package/example_slides/062-mermaid-intro.md +34 -0
- package/example_slides/063-mermaid-flowchart.md +19 -0
- package/example_slides/064-mermaid-sequence.md +17 -0
- package/example_slides/065-mermaid-xy-chart.md +16 -0
- package/example_slides/066-mermaid-pie.md +17 -0
- package/example_slides/067-mermaid-class.md +19 -0
- package/example_slides/068-mermaid-state.md +19 -0
- package/example_slides/069-mermaid-er.md +22 -0
- package/example_slides/070-mermaid-gantt.md +24 -0
- package/example_slides/071-mermaid-timeline.md +17 -0
- package/example_slides/072-mermaid-mindmap.md +21 -0
- package/example_slides/073-mermaid-gitgraph.md +20 -0
- package/example_slides/074-mermaid-split.md +30 -0
- package/example_slides/075-mermaid-columns.md +32 -0
- package/example_slides/076-section-addons.md +14 -0
- package/example_slides/077-asciinema.md +27 -0
- package/example_slides/078-fancyarrow.md +31 -0
- package/example_slides/079-fancyarrow-demo.md +23 -0
- package/example_slides/080-section-theme.md +14 -0
- package/example_slides/081-color-architecture.md +22 -0
- package/example_slides/082-semantic-text-colors.md +25 -0
- package/example_slides/083-typography.md +16 -0
- package/example_slides/084-typography-rationale.md +22 -0
- package/example_slides/085-icons.md +24 -0
- package/example_slides/086-tables.md +14 -0
- package/example_slides/087-code-blocks.md +18 -0
- package/example_slides/088-motion-modes.md +35 -0
- package/example_slides/089-slide-transitions.md +31 -0
- package/example_slides/090-v-click-reveals.md +40 -0
- package/example_slides/091-accessibility.md +27 -0
- package/example_slides/092-safe-zone.md +17 -0
- package/example_slides/093-questions.md +8 -0
- package/example_white.md +10 -0
- package/fonts/IBMPlexMono-Medium.woff2 +1449 -0
- package/fonts/IBMPlexMono-Regular.woff2 +1449 -0
- package/fonts/IBMPlexSans-Bold.woff2 +1449 -0
- package/fonts/IBMPlexSans-Medium.woff2 +1449 -0
- package/fonts/IBMPlexSans-Regular.woff2 +1449 -0
- package/fonts/IBMPlexSans-SemiBold.woff2 +1449 -0
- package/fonts/LICENSE.txt +93 -0
- package/layouts/slide.vue +251 -0
- package/package.json +62 -0
- package/public/avatars/alice.png +0 -0
- package/public/avatars/bob.png +0 -0
- package/public/avatars/carol.png +0 -0
- package/scripts/chart-audit.mjs +216 -0
- package/scripts/contrast-audit.mjs +299 -0
- package/scripts/generate-palette.mjs +395 -0
- package/scripts/integrity-audit.mjs +357 -0
- package/scripts/shared/css-utils.mjs +216 -0
- package/scripts/shiki-audit.mjs +300 -0
- package/scripts/typography-audit.mjs +300 -0
- package/setup/main.ts +107 -0
- package/setup/mermaid.ts +237 -0
- package/setup/shiki.ts +40 -0
- package/snippets/demo.ts +26 -0
- package/styles/base.css +1053 -0
- package/styles/colors.css +422 -0
- package/styles/index.css +12 -0
- package/styles/motion.css +486 -0
- package/uno.config.ts +67 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Color Integrity Audit for slidev-theme-troshab
|
|
5
|
+
*
|
|
6
|
+
* Ensures the 3-layer color architecture stays clean:
|
|
7
|
+
* Layer 1 (Palette): hex ONLY in styles/colors.css
|
|
8
|
+
* Layer 2 (Semantic): only var() refs in :root/.dark blocks
|
|
9
|
+
* Layer 3 (Usage): components/layouts/slides use only semantic vars
|
|
10
|
+
*
|
|
11
|
+
* Run: node scripts/integrity-audit.mjs
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, readdirSync, statSync } from 'fs'
|
|
15
|
+
import { resolve, dirname, extname, relative } from 'path'
|
|
16
|
+
import { fileURLToPath } from 'url'
|
|
17
|
+
import { parsePalette, parseSemanticTokens } from './shared/css-utils.mjs'
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const root = resolve(__dirname, '..')
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// File collection
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
function collectFiles(dir, extensions) {
|
|
27
|
+
const results = []
|
|
28
|
+
let entries
|
|
29
|
+
try { entries = readdirSync(dir) } catch { return results }
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const full = resolve(dir, entry)
|
|
32
|
+
let stat
|
|
33
|
+
try { stat = statSync(full) } catch { continue }
|
|
34
|
+
if (stat.isDirectory()) {
|
|
35
|
+
if (entry === 'node_modules' || entry === '.git' || entry === 'dist' || entry === 'slides-export') continue
|
|
36
|
+
results.push(...collectFiles(full, extensions))
|
|
37
|
+
} else if (extensions.includes(extname(entry))) {
|
|
38
|
+
results.push(full)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return results
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Known CSS variable definitions
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
const colorsPath = resolve(root, 'styles', 'colors.css')
|
|
49
|
+
const palette = parsePalette(colorsPath)
|
|
50
|
+
const { light, dark } = parseSemanticTokens(colorsPath)
|
|
51
|
+
|
|
52
|
+
// Build set of all defined --color-* var names (without prefix)
|
|
53
|
+
const definedVars = new Set([
|
|
54
|
+
...Object.keys(palette),
|
|
55
|
+
...Object.keys(light),
|
|
56
|
+
...Object.keys(dark),
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
// Also parse raw CSS for any additional vars (gradients, charts, shiki)
|
|
60
|
+
const colorsCss = readFileSync(colorsPath, 'utf8')
|
|
61
|
+
const allCssVarDefs = new Set()
|
|
62
|
+
for (const m of colorsCss.matchAll(/--([\w-]+)\s*:/g)) {
|
|
63
|
+
allCssVarDefs.add(m[1])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Vars from base.css
|
|
67
|
+
const baseCss = readFileSync(resolve(root, 'styles', 'base.css'), 'utf8')
|
|
68
|
+
for (const m of baseCss.matchAll(/--([\w-]+)\s*:/g)) {
|
|
69
|
+
allCssVarDefs.add(m[1])
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Vars from motion.css
|
|
73
|
+
const motionCss = readFileSync(resolve(root, 'styles', 'motion.css'), 'utf8')
|
|
74
|
+
for (const m of motionCss.matchAll(/--([\w-]+)\s*:/g)) {
|
|
75
|
+
allCssVarDefs.add(m[1])
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Checks
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
let issues = []
|
|
83
|
+
let warns = []
|
|
84
|
+
|
|
85
|
+
// Hex pattern: #xxx, #xxxxxx, #xxxxxxxx (but not inside CSS var definitions)
|
|
86
|
+
const HEX_RE = /#(?:[0-9a-fA-F]{3}){1,2}(?:[0-9a-fA-F]{2})?\b/g
|
|
87
|
+
|
|
88
|
+
// Allowed hex locations
|
|
89
|
+
const ALLOWED_HEX_FILES = new Set([
|
|
90
|
+
resolve(root, 'styles', 'colors.css'),
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
// Files where hex in comments is OK (documentation)
|
|
94
|
+
function isComment(line) {
|
|
95
|
+
const trimmed = line.trim()
|
|
96
|
+
return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('<!--')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ===== MAIN =====
|
|
100
|
+
|
|
101
|
+
console.log('Color Integrity Audit - slidev-theme-troshab')
|
|
102
|
+
console.log('='.repeat(76))
|
|
103
|
+
|
|
104
|
+
// --- Section 1: Hex leakage ---
|
|
105
|
+
|
|
106
|
+
console.log('\n1. HEX LEAKAGE (no raw hex outside colors.css)')
|
|
107
|
+
console.log('-'.repeat(76))
|
|
108
|
+
|
|
109
|
+
const scanFiles = [
|
|
110
|
+
...collectFiles(resolve(root, 'components'), ['.vue']),
|
|
111
|
+
...collectFiles(resolve(root, 'layouts'), ['.vue']),
|
|
112
|
+
...collectFiles(resolve(root, 'composables'), ['.ts']),
|
|
113
|
+
...collectFiles(resolve(root, 'setup'), ['.ts']),
|
|
114
|
+
...collectFiles(resolve(root, 'example_slides'), ['.md']),
|
|
115
|
+
...collectFiles(resolve(root, 'styles'), ['.css']),
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
let hexLeaks = 0
|
|
119
|
+
for (const file of scanFiles) {
|
|
120
|
+
if (ALLOWED_HEX_FILES.has(file)) continue
|
|
121
|
+
const content = readFileSync(file, 'utf8')
|
|
122
|
+
const lines = content.split('\n')
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
const line = lines[i]
|
|
125
|
+
if (isComment(line)) continue
|
|
126
|
+
// Skip hex inside SVG path data (d="M...")
|
|
127
|
+
if (/\bd="[^"]*"/.test(line)) continue
|
|
128
|
+
|
|
129
|
+
const matches = line.matchAll(HEX_RE)
|
|
130
|
+
for (const m of matches) {
|
|
131
|
+
// Skip hex inside HTML entities like '
|
|
132
|
+
const before = line.slice(Math.max(0, m.index - 2), m.index)
|
|
133
|
+
if (before.endsWith('&#') || before.endsWith('&')) continue
|
|
134
|
+
// Skip hex inside URL encoded strings
|
|
135
|
+
if (before.endsWith('%')) continue
|
|
136
|
+
|
|
137
|
+
const rel = relative(root, file)
|
|
138
|
+
console.log(` ! ${rel}:${i + 1} ${m[0]} "${line.trim().slice(0, 60)}"`)
|
|
139
|
+
issues.push(`Hex ${m[0]} in ${rel}:${i + 1}`)
|
|
140
|
+
hexLeaks++
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (hexLeaks === 0) {
|
|
146
|
+
console.log(` Scanned ${scanFiles.length - ALLOWED_HEX_FILES.size} files: no hex leaks found. OK`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- Section 2: Undefined CSS variable references ---
|
|
150
|
+
|
|
151
|
+
console.log('\n\n2. UNDEFINED CSS VARIABLE REFERENCES')
|
|
152
|
+
console.log('-'.repeat(76))
|
|
153
|
+
|
|
154
|
+
const varRefFiles = [
|
|
155
|
+
...collectFiles(resolve(root, 'components'), ['.vue']),
|
|
156
|
+
...collectFiles(resolve(root, 'layouts'), ['.vue']),
|
|
157
|
+
...collectFiles(resolve(root, 'styles'), ['.css']),
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
let undefinedRefs = 0
|
|
161
|
+
const VAR_REF_RE = /var\(--([\w-]+)\)/g
|
|
162
|
+
|
|
163
|
+
// Known external vars (from Slidev/UnoCSS/browser, not ours to define)
|
|
164
|
+
const EXTERNAL_VARS = new Set([
|
|
165
|
+
'slidev-slide-scale',
|
|
166
|
+
'un-default',
|
|
167
|
+
'at-apply',
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
for (const file of varRefFiles) {
|
|
171
|
+
const content = readFileSync(file, 'utf8')
|
|
172
|
+
const lines = content.split('\n')
|
|
173
|
+
for (let i = 0; i < lines.length; i++) {
|
|
174
|
+
const line = lines[i]
|
|
175
|
+
if (isComment(line)) continue
|
|
176
|
+
|
|
177
|
+
const matches = line.matchAll(VAR_REF_RE)
|
|
178
|
+
for (const m of matches) {
|
|
179
|
+
const varName = m[1]
|
|
180
|
+
if (allCssVarDefs.has(varName)) continue
|
|
181
|
+
if (EXTERNAL_VARS.has(varName)) continue
|
|
182
|
+
// Component-local vars (defined in <style> or set via :style binding)
|
|
183
|
+
if (content.includes(`--${varName}:`)) continue
|
|
184
|
+
if (content.includes(`'--${varName}'`)) continue
|
|
185
|
+
|
|
186
|
+
const rel = relative(root, file)
|
|
187
|
+
console.log(` ~ ${rel}:${i + 1} var(--${varName}) (not in global CSS)`)
|
|
188
|
+
warns.push(`Undefined var(--${varName}) in ${rel}:${i + 1}`)
|
|
189
|
+
undefinedRefs++
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (undefinedRefs === 0) {
|
|
195
|
+
console.log(` Scanned ${varRefFiles.length} files: all var() references resolve. OK`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- Section 3: Inline styles with hardcoded colors in slides ---
|
|
199
|
+
|
|
200
|
+
console.log('\n\n3. HARDCODED COLORS IN SLIDES')
|
|
201
|
+
console.log('-'.repeat(76))
|
|
202
|
+
|
|
203
|
+
const slideFiles = collectFiles(resolve(root, 'example_slides'), ['.md'])
|
|
204
|
+
const INLINE_COLOR_RE = /style\s*=\s*"[^"]*(?:color|background|border-color)\s*:\s*(?!var\()[^"]*"/gi
|
|
205
|
+
|
|
206
|
+
let slideColorLeaks = 0
|
|
207
|
+
for (const file of slideFiles) {
|
|
208
|
+
const content = readFileSync(file, 'utf8')
|
|
209
|
+
const lines = content.split('\n')
|
|
210
|
+
for (let i = 0; i < lines.length; i++) {
|
|
211
|
+
const line = lines[i]
|
|
212
|
+
const matches = line.matchAll(INLINE_COLOR_RE)
|
|
213
|
+
for (const m of matches) {
|
|
214
|
+
// Allow style="...color: var(--..." patterns
|
|
215
|
+
if (/color\s*:\s*var\(--/.test(m[0])) continue
|
|
216
|
+
// Allow non-color styles like width/height that happen to match
|
|
217
|
+
if (!/(?:color|background|border-color)\s*:\s*[^v]/.test(m[0])) continue
|
|
218
|
+
|
|
219
|
+
const rel = relative(root, file)
|
|
220
|
+
console.log(` ! ${rel}:${i + 1} "${m[0].slice(0, 60)}"`)
|
|
221
|
+
issues.push(`Inline color in ${rel}:${i + 1}`)
|
|
222
|
+
slideColorLeaks++
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (slideColorLeaks === 0) {
|
|
228
|
+
console.log(` Scanned ${slideFiles.length} slide files: no hardcoded colors. OK`)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// --- Section 4: color-mix() with raw hex (should use vars) ---
|
|
232
|
+
|
|
233
|
+
console.log('\n\n4. color-mix() WITH RAW HEX (should use CSS vars)')
|
|
234
|
+
console.log('-'.repeat(76))
|
|
235
|
+
|
|
236
|
+
const COLOR_MIX_HEX_RE = /color-mix\([^)]*#[0-9a-fA-F]/g
|
|
237
|
+
|
|
238
|
+
let colorMixHexCount = 0
|
|
239
|
+
for (const file of [...scanFiles]) {
|
|
240
|
+
if (ALLOWED_HEX_FILES.has(file)) continue
|
|
241
|
+
const content = readFileSync(file, 'utf8')
|
|
242
|
+
const lines = content.split('\n')
|
|
243
|
+
for (let i = 0; i < lines.length; i++) {
|
|
244
|
+
const line = lines[i]
|
|
245
|
+
if (isComment(line)) continue
|
|
246
|
+
const matches = line.matchAll(COLOR_MIX_HEX_RE)
|
|
247
|
+
for (const m of matches) {
|
|
248
|
+
const rel = relative(root, file)
|
|
249
|
+
console.log(` ! ${rel}:${i + 1} "${m[0].slice(0, 60)}"`)
|
|
250
|
+
issues.push(`color-mix() with hex in ${rel}:${i + 1}`)
|
|
251
|
+
colorMixHexCount++
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (colorMixHexCount === 0) {
|
|
257
|
+
console.log(` All color-mix() calls use CSS variables. OK`)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// --- Section 5: Foreground pair completeness ---
|
|
261
|
+
|
|
262
|
+
console.log('\n\n5. FOREGROUND PAIR COMPLETENESS')
|
|
263
|
+
console.log(' Every --color-{name} semantic bg needs --color-{name}-foreground')
|
|
264
|
+
console.log('-'.repeat(76))
|
|
265
|
+
|
|
266
|
+
const semanticBgs = ['primary', 'success', 'warning', 'danger', 'info', 'secondary', 'accent']
|
|
267
|
+
let missingFg = 0
|
|
268
|
+
|
|
269
|
+
for (const name of semanticBgs) {
|
|
270
|
+
const hasFg = definedVars.has(name + '-foreground')
|
|
271
|
+
if (hasFg) {
|
|
272
|
+
console.log(` --color-${name}-foreground`.padEnd(42) + 'defined'.padEnd(12) + 'OK')
|
|
273
|
+
} else {
|
|
274
|
+
console.log(` ! --color-${name}-foreground`.padEnd(42) + 'MISSING'.padEnd(12) + 'FAIL')
|
|
275
|
+
issues.push(`Missing --color-${name}-foreground pair`)
|
|
276
|
+
missingFg++
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Also check tint pairs
|
|
281
|
+
const tintNames = ['primary-tint', 'success-tint', 'warning-tint', 'danger-tint', 'info-tint']
|
|
282
|
+
for (const name of tintNames) {
|
|
283
|
+
const hasTint = definedVars.has(name)
|
|
284
|
+
if (hasTint) {
|
|
285
|
+
console.log(` --color-${name}`.padEnd(42) + 'defined'.padEnd(12) + 'OK')
|
|
286
|
+
} else {
|
|
287
|
+
console.log(` ! --color-${name}`.padEnd(42) + 'MISSING'.padEnd(12) + 'FAIL')
|
|
288
|
+
issues.push(`Missing --color-${name}`)
|
|
289
|
+
missingFg++
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// --- Section 6: Duplicate color maps ---
|
|
294
|
+
|
|
295
|
+
console.log('\n\n6. COMPONENT COLOR MAP CONSISTENCY')
|
|
296
|
+
console.log(' Check that local colorVarMap in components cover all semantic colors')
|
|
297
|
+
console.log('-'.repeat(76))
|
|
298
|
+
|
|
299
|
+
const componentFiles = collectFiles(resolve(root, 'components'), ['.vue'])
|
|
300
|
+
const COLORMAP_RE = /const\s+(\w*[Cc]olor\w*)\s*(?::\s*[^=]+)?\s*=\s*\{([^}]+)\}/gs
|
|
301
|
+
|
|
302
|
+
let mapIssues = 0
|
|
303
|
+
for (const file of componentFiles) {
|
|
304
|
+
const content = readFileSync(file, 'utf8')
|
|
305
|
+
const maps = content.matchAll(COLORMAP_RE)
|
|
306
|
+
for (const m of maps) {
|
|
307
|
+
const mapName = m[1]
|
|
308
|
+
const body = m[2]
|
|
309
|
+
// Extract keys from the map
|
|
310
|
+
const keys = [...body.matchAll(/(\w+)\s*:/g)].map(k => k[1])
|
|
311
|
+
// Check if it covers the basic semantic set
|
|
312
|
+
const missing = semanticBgs.filter(s => !keys.includes(s))
|
|
313
|
+
const rel = relative(root, file)
|
|
314
|
+
if (missing.length > 0 && keys.length >= 3) {
|
|
315
|
+
// Only warn if the map has at least 3 entries (skip small purpose-specific maps)
|
|
316
|
+
console.log(` ~ ${rel}: ${mapName} missing: ${missing.join(', ')}`)
|
|
317
|
+
warns.push(`${rel}: ${mapName} missing semantic colors: ${missing.join(', ')}`)
|
|
318
|
+
mapIssues++
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (mapIssues === 0) {
|
|
324
|
+
console.log(` All component color maps cover semantic colors. OK`)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- Summary ---
|
|
328
|
+
|
|
329
|
+
console.log('\n' + '='.repeat(76))
|
|
330
|
+
console.log('SUMMARY')
|
|
331
|
+
console.log('='.repeat(76))
|
|
332
|
+
|
|
333
|
+
const stats = [
|
|
334
|
+
`Files scanned: ${scanFiles.length}`,
|
|
335
|
+
`Hex leaks: ${hexLeaks}`,
|
|
336
|
+
`Undefined vars: ${undefinedRefs}`,
|
|
337
|
+
`Slide color leaks: ${slideColorLeaks}`,
|
|
338
|
+
`color-mix hex: ${colorMixHexCount}`,
|
|
339
|
+
`Missing foreground pairs: ${missingFg}`,
|
|
340
|
+
]
|
|
341
|
+
console.log(`\n ${stats.join(' | ')}`)
|
|
342
|
+
|
|
343
|
+
if (issues.length === 0 && warns.length === 0) {
|
|
344
|
+
console.log('\n All integrity checks passed!')
|
|
345
|
+
} else {
|
|
346
|
+
if (issues.length > 0) {
|
|
347
|
+
console.log(`\n ${issues.length} FAIL(s):`)
|
|
348
|
+
issues.forEach((issue, i) => console.log(` ${i + 1}. ${issue}`))
|
|
349
|
+
}
|
|
350
|
+
if (warns.length > 0) {
|
|
351
|
+
console.log(`\n ${warns.length} WARN(s):`)
|
|
352
|
+
warns.forEach((w, i) => console.log(` ${i + 1}. ${w}`))
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
console.log()
|
|
357
|
+
process.exit(issues.length > 0 ? 1 : 0)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CSS utilities for accessibility audit scripts.
|
|
3
|
+
*
|
|
4
|
+
* Color math (WCAG 2.x + APCA), CSS parsing, formatting.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'fs'
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// sRGB → linear channel
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export function hexToLinear(hex) {
|
|
14
|
+
hex = hex.replace('#', '')
|
|
15
|
+
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
|
|
16
|
+
const r = parseInt(hex.slice(0, 2), 16) / 255
|
|
17
|
+
const g = parseInt(hex.slice(2, 4), 16) / 255
|
|
18
|
+
const b = parseInt(hex.slice(4, 6), 16) / 255
|
|
19
|
+
return [r, g, b].map(c =>
|
|
20
|
+
c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// WCAG 2.x relative luminance
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export function relativeLuminance(hex) {
|
|
29
|
+
const [r, g, b] = hexToLinear(hex)
|
|
30
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// WCAG 2.x contrast ratio
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
export function contrastRatio(hex1, hex2) {
|
|
38
|
+
const L1 = relativeLuminance(hex1)
|
|
39
|
+
const L2 = relativeLuminance(hex2)
|
|
40
|
+
const lighter = Math.max(L1, L2)
|
|
41
|
+
const darker = Math.min(L1, L2)
|
|
42
|
+
return (lighter + 0.05) / (darker + 0.05)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// APCA (Accessible Perceptual Contrast Algorithm) — Lc value
|
|
47
|
+
//
|
|
48
|
+
// Public domain formula from Myndex/SAPC-APCA.
|
|
49
|
+
// Returns signed Lc: positive = dark-on-light, negative = light-on-dark.
|
|
50
|
+
// Magnitude is what matters for thresholds (use Math.abs).
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
export function apcaContrast(fgHex, bgHex) {
|
|
54
|
+
// sRGB → Y (linearised luminance) with APCA coefficients
|
|
55
|
+
function sRGBtoY(hex) {
|
|
56
|
+
const [rL, gL, bL] = hexToLinear(hex)
|
|
57
|
+
// APCA uses slightly different coefficients than WCAG 2.x
|
|
58
|
+
let Y = 0.2126729 * rL + 0.7151522 * gL + 0.0721750 * bL
|
|
59
|
+
// Soft-clamp black
|
|
60
|
+
if (Y < 0.022) Y += Math.pow(0.022 - Y, 1.414)
|
|
61
|
+
return Y
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const Ytxt = sRGBtoY(fgHex)
|
|
65
|
+
const Ybg = sRGBtoY(bgHex)
|
|
66
|
+
|
|
67
|
+
let Sapc
|
|
68
|
+
// Normal polarity: dark text on light bg
|
|
69
|
+
if (Ybg > Ytxt) {
|
|
70
|
+
Sapc = (Math.pow(Ybg, 0.56) - Math.pow(Ytxt, 0.57)) * 1.14
|
|
71
|
+
} else {
|
|
72
|
+
// Reverse polarity: light text on dark bg
|
|
73
|
+
Sapc = (Math.pow(Ybg, 0.65) - Math.pow(Ytxt, 0.62)) * 1.14
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Clamp low contrast
|
|
77
|
+
if (Math.abs(Sapc) < 0.1) return 0
|
|
78
|
+
|
|
79
|
+
// Apply offset
|
|
80
|
+
if (Sapc > 0) {
|
|
81
|
+
Sapc -= 0.027
|
|
82
|
+
} else {
|
|
83
|
+
Sapc += 0.027
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return Sapc * 100
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// CSS parsing
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/** Find the index of the closing brace that matches the opening `{` after startOfBlock. */
|
|
94
|
+
export function findMatchingBrace(str, startOfBlock) {
|
|
95
|
+
const braceStart = str.indexOf('{', startOfBlock)
|
|
96
|
+
if (braceStart < 0) return -1
|
|
97
|
+
let depth = 0
|
|
98
|
+
for (let i = braceStart; i < str.length; i++) {
|
|
99
|
+
if (str[i] === '{') depth++
|
|
100
|
+
if (str[i] === '}') { depth--; if (depth === 0) return i }
|
|
101
|
+
}
|
|
102
|
+
return -1
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Extract hex palette `--color-{name}: #{hex};` from colors.css. */
|
|
106
|
+
export function parsePalette(cssPath) {
|
|
107
|
+
const css = readFileSync(cssPath, 'utf8')
|
|
108
|
+
const palette = {}
|
|
109
|
+
const re = /--color-([\w-]+):\s*#([0-9a-fA-F]{3,8})\s*;/g
|
|
110
|
+
let m
|
|
111
|
+
while ((m = re.exec(css)) !== null) {
|
|
112
|
+
palette[m[1]] = '#' + m[2]
|
|
113
|
+
}
|
|
114
|
+
return palette
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Extract light/dark semantic `var()` maps from colors.css. */
|
|
118
|
+
export function parseSemanticTokens(cssPath) {
|
|
119
|
+
const css = readFileSync(cssPath, 'utf8')
|
|
120
|
+
|
|
121
|
+
const darkStart = css.indexOf('.dark {')
|
|
122
|
+
const darkEnd = darkStart >= 0 ? findMatchingBrace(css, darkStart) : -1
|
|
123
|
+
|
|
124
|
+
function extractVarRefs(text) {
|
|
125
|
+
const tokens = {}
|
|
126
|
+
const re = /--color-([\w-]+):\s*var\(--color-([\w-]+)\)\s*;/g
|
|
127
|
+
let m
|
|
128
|
+
while ((m = re.exec(text)) !== null) {
|
|
129
|
+
tokens[m[1]] = m[2]
|
|
130
|
+
}
|
|
131
|
+
return tokens
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const darkBlock = darkStart >= 0 && darkEnd >= 0
|
|
135
|
+
? css.slice(darkStart, darkEnd + 1)
|
|
136
|
+
: ''
|
|
137
|
+
|
|
138
|
+
const lightBlock = darkStart >= 0
|
|
139
|
+
? css.slice(0, darkStart) + css.slice(darkEnd + 1)
|
|
140
|
+
: css
|
|
141
|
+
|
|
142
|
+
const light = extractVarRefs(lightBlock)
|
|
143
|
+
const dark = { ...light, ...extractVarRefs(darkBlock) }
|
|
144
|
+
|
|
145
|
+
return { light, dark }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Parse chart-N vars: `--chart-N: var(--color-XXX);` from colors.css. */
|
|
149
|
+
export function parseChartTokens(cssPath) {
|
|
150
|
+
const css = readFileSync(cssPath, 'utf8')
|
|
151
|
+
|
|
152
|
+
const darkStart = css.indexOf('.dark {')
|
|
153
|
+
const darkEnd = darkStart >= 0 ? findMatchingBrace(css, darkStart) : -1
|
|
154
|
+
|
|
155
|
+
function extractCharts(text) {
|
|
156
|
+
const tokens = {}
|
|
157
|
+
const re = /--chart-(\d+):\s*var\(--color-([\w-]+)\)\s*;/g
|
|
158
|
+
let m
|
|
159
|
+
while ((m = re.exec(text)) !== null) {
|
|
160
|
+
tokens[parseInt(m[1])] = m[2]
|
|
161
|
+
}
|
|
162
|
+
return tokens
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const darkBlock = darkStart >= 0 && darkEnd >= 0
|
|
166
|
+
? css.slice(darkStart, darkEnd + 1)
|
|
167
|
+
: ''
|
|
168
|
+
const lightBlock = darkStart >= 0
|
|
169
|
+
? css.slice(0, darkStart) + css.slice(darkEnd + 1)
|
|
170
|
+
: css
|
|
171
|
+
|
|
172
|
+
const lightCharts = extractCharts(lightBlock)
|
|
173
|
+
const darkCharts = { ...lightCharts, ...extractCharts(darkBlock) }
|
|
174
|
+
|
|
175
|
+
return { light: lightCharts, dark: darkCharts }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Resolve a semantic var name to hex via palette. */
|
|
179
|
+
export function resolveHex(palette, semanticMap, varName) {
|
|
180
|
+
const paletteRef = semanticMap[varName]
|
|
181
|
+
if (!paletteRef) return null
|
|
182
|
+
return palette[paletteRef] || null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Extract a single CSS custom property value (raw string) from CSS text. */
|
|
186
|
+
export function parseCssVar(css, varName) {
|
|
187
|
+
const re = new RegExp(`${varName}:\\s*([^;]+)\\s*;`)
|
|
188
|
+
const m = css.match(re)
|
|
189
|
+
return m ? m[1].trim() : null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Find all matches of a regex pattern in CSS text. Returns array of match objects. */
|
|
193
|
+
export function scanCssPattern(css, pattern) {
|
|
194
|
+
const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'g')
|
|
195
|
+
const matches = []
|
|
196
|
+
let m
|
|
197
|
+
while ((m = re.exec(css)) !== null) {
|
|
198
|
+
matches.push(m)
|
|
199
|
+
}
|
|
200
|
+
return matches
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Formatting helpers
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/** Format WCAG 2.x ratio as "N.NN:1". */
|
|
208
|
+
export function fmt(ratio) {
|
|
209
|
+
return ratio.toFixed(2) + ':1'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Format APCA Lc as "Lc NN". */
|
|
213
|
+
export function fmtLc(lc) {
|
|
214
|
+
const abs = Math.abs(lc)
|
|
215
|
+
return 'Lc ' + (abs < 10 ? ' ' : '') + abs.toFixed(0)
|
|
216
|
+
}
|