@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,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WCAG Contrast Audit for slidev-theme-troshab
|
|
5
|
+
*
|
|
6
|
+
* Dual standard: WCAG 2.x ratio + APCA Lc checked in parallel.
|
|
7
|
+
* Run: node scripts/contrast-audit.mjs
|
|
8
|
+
*
|
|
9
|
+
* Standards:
|
|
10
|
+
* - WCAG 2.x: 4.5:1 AA (normal text), 3:1 AA (large text >=24px)
|
|
11
|
+
* - APCA Bronze: body Lc 75+, content Lc 60+, large Lc 45+, non-text Lc 15+
|
|
12
|
+
* - Non-text: 3:1 AA (borders, icons, chart lines, focus rings)
|
|
13
|
+
* - BDA spacing: word-spacing >= 3.5x letter-spacing
|
|
14
|
+
*
|
|
15
|
+
* Scientific basis:
|
|
16
|
+
* - Stevens (1961) power law → APCA nonlinear luminance model
|
|
17
|
+
* - Legge & Rubin (1986, JOSA A) → polarity sensitivity (~15-20%)
|
|
18
|
+
* - Campbell & Robson (1968, J. Physiology) → spatial frequency / CSF
|
|
19
|
+
* - Legge et al. (1985, JOSA A) → Lc 90 for max reading speed
|
|
20
|
+
* - Arditi & Cho (2005, IOVS) → Lc 60 for short-read content (24px+)
|
|
21
|
+
* - Zorzi et al. (2012, PNAS 109:11455) → letter-spacing reduces crowding
|
|
22
|
+
* - BDA Style Guide (2018) → word/letter ratio >= 3.5x
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readFileSync } from 'fs'
|
|
26
|
+
import { resolve, dirname } from 'path'
|
|
27
|
+
import { fileURLToPath } from 'url'
|
|
28
|
+
import {
|
|
29
|
+
contrastRatio, apcaContrast,
|
|
30
|
+
parsePalette, parseSemanticTokens,
|
|
31
|
+
resolveHex, fmt, fmtLc,
|
|
32
|
+
} from './shared/css-utils.mjs'
|
|
33
|
+
|
|
34
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Paths
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
const colorsPath = resolve(__dirname, '..', 'styles', 'colors.css')
|
|
41
|
+
const basePath = resolve(__dirname, '..', 'styles', 'base.css')
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Parse
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
const palette = parsePalette(colorsPath)
|
|
48
|
+
const { light, dark } = parseSemanticTokens(colorsPath)
|
|
49
|
+
|
|
50
|
+
function sem(theme, varName) {
|
|
51
|
+
return resolveHex(palette, theme === 'light' ? light : dark, varName)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// APCA thresholds (Bronze level)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
function apcaThreshold(sizeHint) {
|
|
59
|
+
// sizeHint like "base(24px)", "small(18px)", "h1(57px)", "h2(43px)", "non-text"
|
|
60
|
+
if (sizeHint === 'non-text') return 15
|
|
61
|
+
const pxMatch = sizeHint.match(/(\d+)px/)
|
|
62
|
+
if (!pxMatch) return 75 // default: body
|
|
63
|
+
const px = parseInt(pxMatch[1])
|
|
64
|
+
if (px >= 36) return 45 // large headlines
|
|
65
|
+
if (px >= 24) return 60 // content text (headings, 1-2 lines)
|
|
66
|
+
return 75 // body text (reading blocks)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function dualStatus(ratio, minRatio, lc, minLc) {
|
|
70
|
+
const wcagOk = ratio >= minRatio
|
|
71
|
+
const apcaOk = Math.abs(lc) >= minLc
|
|
72
|
+
if (wcagOk && apcaOk) return 'OK'
|
|
73
|
+
if (!wcagOk) return 'FAIL'
|
|
74
|
+
// WCAG passes but APCA fails — warn (APCA draft)
|
|
75
|
+
return 'WARN'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function statusIcon(s) {
|
|
79
|
+
if (s === 'FAIL') return '!'
|
|
80
|
+
if (s === 'WARN') return '~'
|
|
81
|
+
return ' '
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function wcagLabel(ratio, isLarge) {
|
|
85
|
+
const minAA = isLarge ? 3.0 : 4.5
|
|
86
|
+
const minAAA = isLarge ? 4.5 : 7.0
|
|
87
|
+
if (ratio >= minAAA) return 'AAA'
|
|
88
|
+
if (ratio >= minAA) return 'AA '
|
|
89
|
+
return 'FAIL'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Parse spacing from base.css
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
function parseSpacing(path) {
|
|
97
|
+
const css = readFileSync(path, 'utf8')
|
|
98
|
+
const letterMatch = css.match(/--letter-spacing-body:\s*([\d.]+)em/)
|
|
99
|
+
const wordMatch = css.match(/--word-spacing-body:\s*([\d.]+)em/)
|
|
100
|
+
return {
|
|
101
|
+
letterSpacing: letterMatch ? parseFloat(letterMatch[1]) : null,
|
|
102
|
+
wordSpacing: wordMatch ? parseFloat(wordMatch[1]) : null,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ===== MAIN =====
|
|
107
|
+
|
|
108
|
+
console.log('WCAG + APCA Contrast Audit - slidev-theme-troshab')
|
|
109
|
+
console.log('='.repeat(76))
|
|
110
|
+
|
|
111
|
+
let issues = []
|
|
112
|
+
|
|
113
|
+
// --- Section 1: Text contrast ---
|
|
114
|
+
|
|
115
|
+
console.log('\n1. TEXT CONTRAST')
|
|
116
|
+
console.log(' WCAG 2.x: normal <=24px 4.5:1 AA / 7:1 AAA | large >=24px 3:1 AA / 4.5:1 AAA')
|
|
117
|
+
console.log(' APCA Bronze: body(<=24px) Lc 75+ | content(24px) Lc 60+ | large(>=36px) Lc 45+')
|
|
118
|
+
console.log('-'.repeat(76))
|
|
119
|
+
|
|
120
|
+
const textPairs = [
|
|
121
|
+
{ name: 'text / bg', fg: 'text', bg: 'bg', size: 'base(24px)' },
|
|
122
|
+
{ name: 'text-secondary / bg', fg: 'text-secondary', bg: 'bg', size: 'base(24px)' },
|
|
123
|
+
{ name: 'text-tertiary / bg', fg: 'text-tertiary', bg: 'bg', size: 'small(18px)' },
|
|
124
|
+
{ name: 'primary / bg', fg: 'primary', bg: 'bg', size: 'base(24px)' },
|
|
125
|
+
{ name: 'success / bg', fg: 'success', bg: 'bg', size: 'base(24px)' },
|
|
126
|
+
{ name: 'warning / bg', fg: 'warning', bg: 'bg', size: 'base(24px)' },
|
|
127
|
+
{ name: 'danger / bg', fg: 'danger', bg: 'bg', size: 'base(24px)' },
|
|
128
|
+
{ name: 'info / bg', fg: 'info', bg: 'bg', size: 'base(24px)' },
|
|
129
|
+
{ name: 'accent / bg', fg: 'accent', bg: 'bg', size: 'base(24px)' },
|
|
130
|
+
{ name: 'secondary / bg', fg: 'secondary', bg: 'bg', size: 'base(24px)' },
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
for (const theme of ['light', 'dark']) {
|
|
134
|
+
console.log(`\n ${theme.toUpperCase()} THEME:`)
|
|
135
|
+
for (const pair of textPairs) {
|
|
136
|
+
const fgHex = sem(theme, pair.fg)
|
|
137
|
+
const bgHex = sem(theme, pair.bg)
|
|
138
|
+
if (!fgHex || !bgHex) {
|
|
139
|
+
console.log(` ? ${pair.name}: missing (${pair.fg}=${fgHex}, ${pair.bg}=${bgHex})`)
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
const ratio = contrastRatio(fgHex, bgHex)
|
|
143
|
+
const lc = apcaContrast(fgHex, bgHex)
|
|
144
|
+
const isLarge = pair.size.includes('24px') || pair.size.includes('h1') || pair.size.includes('h2')
|
|
145
|
+
const minAA = isLarge ? 3.0 : 4.5
|
|
146
|
+
const minLc = apcaThreshold(pair.size)
|
|
147
|
+
const wLabel = wcagLabel(ratio, isLarge)
|
|
148
|
+
const status = dualStatus(ratio, minAA, lc, minLc)
|
|
149
|
+
const icon = statusIcon(status)
|
|
150
|
+
console.log(` ${icon} ${pair.name.padEnd(25)} ${fmt(ratio).padStart(8)} ${wLabel} [${pair.size.padEnd(12)}] ${fmtLc(lc)} (${minLc}+ ${status})`)
|
|
151
|
+
if (status === 'FAIL') issues.push(`[${theme}] ${pair.name}: ${fmt(ratio)} < ${minAA}:1`)
|
|
152
|
+
if (status === 'WARN') issues.push(`[${theme}] ${pair.name}: APCA ${fmtLc(lc)} < Lc ${minLc} (WCAG OK)`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- Section 2: Non-text contrast ---
|
|
157
|
+
|
|
158
|
+
console.log('\n\n2. NON-TEXT CONTRAST (WCAG 3:1, APCA Lc 15+)')
|
|
159
|
+
console.log('-'.repeat(76))
|
|
160
|
+
|
|
161
|
+
const nonTextPairs = [
|
|
162
|
+
{ name: 'border / bg', fg: 'border', bg: 'bg' },
|
|
163
|
+
{ name: 'border-strong / bg', fg: 'border-strong', bg: 'bg' },
|
|
164
|
+
{ name: 'border / bg-soft', fg: 'border', bg: 'bg-soft' },
|
|
165
|
+
{ name: 'border-strong / bg-soft', fg: 'border-strong', bg: 'bg-soft' },
|
|
166
|
+
{ name: 'border / bg-muted', fg: 'border', bg: 'bg-muted' },
|
|
167
|
+
{ name: 'border-strong / bg-muted', fg: 'border-strong', bg: 'bg-muted' },
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
for (const theme of ['light', 'dark']) {
|
|
171
|
+
console.log(`\n ${theme.toUpperCase()} THEME:`)
|
|
172
|
+
for (const pair of nonTextPairs) {
|
|
173
|
+
const fgHex = sem(theme, pair.fg)
|
|
174
|
+
const bgHex = sem(theme, pair.bg)
|
|
175
|
+
if (!fgHex || !bgHex) continue
|
|
176
|
+
const ratio = contrastRatio(fgHex, bgHex)
|
|
177
|
+
const lc = apcaContrast(fgHex, bgHex)
|
|
178
|
+
const status = dualStatus(ratio, 3.0, lc, 15)
|
|
179
|
+
const icon = statusIcon(status)
|
|
180
|
+
console.log(` ${icon} ${pair.name.padEnd(28)} ${fmt(ratio).padStart(8)} ${fmtLc(lc)} (15+ ${status})`)
|
|
181
|
+
if (status === 'FAIL') issues.push(`[${theme}] non-text ${pair.name}: ${fmt(ratio)} < 3:1`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Section 3: Focus ring ---
|
|
186
|
+
|
|
187
|
+
console.log('\n\n3. FOCUS RING (WCAG 3:1, APCA Lc 15+)')
|
|
188
|
+
console.log('-'.repeat(76))
|
|
189
|
+
|
|
190
|
+
for (const theme of ['light', 'dark']) {
|
|
191
|
+
const ringHex = sem(theme, 'primary')
|
|
192
|
+
const bgHex = sem(theme, 'bg')
|
|
193
|
+
if (!ringHex || !bgHex) continue
|
|
194
|
+
const ratio = contrastRatio(ringHex, bgHex)
|
|
195
|
+
const lc = apcaContrast(ringHex, bgHex)
|
|
196
|
+
const status = dualStatus(ratio, 3.0, lc, 15)
|
|
197
|
+
console.log(` ${theme.padEnd(6)} primary / bg ${fmt(ratio).padStart(8)} ${fmtLc(lc)} (${status})`)
|
|
198
|
+
if (status === 'FAIL') issues.push(`[${theme}] focus ring: ${fmt(ratio)} < 3:1`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- Section 4: Tint readability ---
|
|
202
|
+
|
|
203
|
+
console.log('\n\n4. TINT BACKGROUNDS (text on -tint vars, WCAG 4.5:1, APCA Lc 75+)')
|
|
204
|
+
console.log('-'.repeat(76))
|
|
205
|
+
|
|
206
|
+
const tintPairs = [
|
|
207
|
+
{ name: 'text on primary-tint', text: 'text', tint: 'primary-tint' },
|
|
208
|
+
{ name: 'text on success-tint', text: 'text', tint: 'success-tint' },
|
|
209
|
+
{ name: 'text on warning-tint', text: 'text', tint: 'warning-tint' },
|
|
210
|
+
{ name: 'text on danger-tint', text: 'text', tint: 'danger-tint' },
|
|
211
|
+
{ name: 'text on info-tint', text: 'text', tint: 'info-tint' },
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
for (const theme of ['light', 'dark']) {
|
|
215
|
+
console.log(`\n ${theme.toUpperCase()} THEME:`)
|
|
216
|
+
for (const pair of tintPairs) {
|
|
217
|
+
const textHex = sem(theme, pair.text)
|
|
218
|
+
const tintHex = sem(theme, pair.tint)
|
|
219
|
+
if (!textHex || !tintHex) continue
|
|
220
|
+
const ratio = contrastRatio(textHex, tintHex)
|
|
221
|
+
const lc = apcaContrast(textHex, tintHex)
|
|
222
|
+
const status = dualStatus(ratio, 4.5, lc, 75)
|
|
223
|
+
const icon = statusIcon(status)
|
|
224
|
+
console.log(` ${icon} ${pair.name.padEnd(28)} ${fmt(ratio).padStart(8)} ${fmtLc(lc)} (75+ ${status})`)
|
|
225
|
+
if (status === 'FAIL') issues.push(`[${theme}] tint ${pair.name}: ${fmt(ratio)} < 4.5:1`)
|
|
226
|
+
if (status === 'WARN') issues.push(`[${theme}] tint ${pair.name}: APCA ${fmtLc(lc)} < Lc 75 (WCAG OK)`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- Section 5: Foreground pairs ---
|
|
231
|
+
|
|
232
|
+
console.log('\n\n5. FOREGROUND PAIRS (text on semantic bg, WCAG 4.5:1, APCA Lc 60+)')
|
|
233
|
+
console.log('-'.repeat(76))
|
|
234
|
+
|
|
235
|
+
const fgPairs = ['primary', 'success', 'warning', 'danger', 'info', 'secondary', 'accent']
|
|
236
|
+
|
|
237
|
+
for (const theme of ['light', 'dark']) {
|
|
238
|
+
console.log(`\n ${theme.toUpperCase()} THEME:`)
|
|
239
|
+
for (const name of fgPairs) {
|
|
240
|
+
const bgHex = sem(theme, name)
|
|
241
|
+
const fgHex = sem(theme, name + '-foreground')
|
|
242
|
+
if (!bgHex || !fgHex) continue
|
|
243
|
+
const ratio = contrastRatio(fgHex, bgHex)
|
|
244
|
+
const lc = apcaContrast(fgHex, bgHex)
|
|
245
|
+
// Foreground pairs are typically short labels on buttons — content text Lc 60
|
|
246
|
+
const status = dualStatus(ratio, 4.5, lc, 60)
|
|
247
|
+
const icon = statusIcon(status)
|
|
248
|
+
const wLabel = ratio >= 4.5 ? 'OK ' : ratio >= 3.0 ? 'AA-L' : 'FAIL'
|
|
249
|
+
console.log(` ${icon} ${(name + '-fg / ' + name).padEnd(28)} ${fmt(ratio).padStart(8)} ${wLabel} ${fmtLc(lc)} (60+ ${status})`)
|
|
250
|
+
if (ratio < 4.5) issues.push(`[${theme}] foreground ${name}-fg / ${name}: ${fmt(ratio)} < 4.5:1`)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --- Section 6: BDA Spacing ---
|
|
255
|
+
|
|
256
|
+
console.log('\n\n6. BDA DYSLEXIA SPACING')
|
|
257
|
+
console.log('-'.repeat(76))
|
|
258
|
+
|
|
259
|
+
const spacing = parseSpacing(basePath)
|
|
260
|
+
if (spacing.letterSpacing && spacing.wordSpacing) {
|
|
261
|
+
const ratio = spacing.wordSpacing / spacing.letterSpacing
|
|
262
|
+
const required = 3.5
|
|
263
|
+
const status = ratio >= required ? 'OK' : 'FAIL'
|
|
264
|
+
console.log(` letter-spacing: ${spacing.letterSpacing}em`)
|
|
265
|
+
console.log(` word-spacing: ${spacing.wordSpacing}em`)
|
|
266
|
+
console.log(` ratio: ${ratio.toFixed(1)}x (BDA requires >= ${required}x)`)
|
|
267
|
+
console.log(` status: ${status}`)
|
|
268
|
+
if (ratio < required) {
|
|
269
|
+
issues.push(`[spacing] word/letter ratio ${ratio.toFixed(1)}x < 3.5x`)
|
|
270
|
+
console.log(` fix: word-spacing >= ${(spacing.letterSpacing * required).toFixed(3)}em`)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --- Summary ---
|
|
275
|
+
|
|
276
|
+
console.log('\n' + '='.repeat(76))
|
|
277
|
+
console.log('SUMMARY')
|
|
278
|
+
console.log('='.repeat(76))
|
|
279
|
+
|
|
280
|
+
if (issues.length === 0) {
|
|
281
|
+
console.log('\nAll checks passed!')
|
|
282
|
+
} else {
|
|
283
|
+
const fails = issues.filter(i => !i.includes('APCA'))
|
|
284
|
+
const warns = issues.filter(i => i.includes('APCA'))
|
|
285
|
+
if (fails.length > 0) {
|
|
286
|
+
console.log(`\n${fails.length} FAIL(s):`)
|
|
287
|
+
fails.forEach((issue, i) => console.log(` ${i + 1}. ${issue}`))
|
|
288
|
+
}
|
|
289
|
+
if (warns.length > 0) {
|
|
290
|
+
console.log(`\n${warns.length} WARN(s) (APCA draft):`)
|
|
291
|
+
warns.forEach((issue, i) => console.log(` ${i + 1}. ${issue}`))
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.log()
|
|
296
|
+
|
|
297
|
+
// Exit code: FAIL = 1, WARN-only = 0
|
|
298
|
+
const hasFail = issues.some(i => !i.includes('APCA'))
|
|
299
|
+
process.exit(hasFail ? 1 : 0)
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dracula / Alucard Palette Generator
|
|
5
|
+
*
|
|
6
|
+
* Generates 11-shade OKLCH ramps (50-950) from Dracula and Alucard base colors.
|
|
7
|
+
* Pure math - no npm dependencies.
|
|
8
|
+
*
|
|
9
|
+
* Usage: node scripts/generate-palette.mjs
|
|
10
|
+
* Output: CSS custom properties ready to paste into colors.css
|
|
11
|
+
*
|
|
12
|
+
* Color science:
|
|
13
|
+
* sRGB -> linear RGB -> XYZ D65 -> OKLCH
|
|
14
|
+
* Vary Lightness while preserving Chroma and Hue
|
|
15
|
+
* OKLCH -> XYZ D65 -> linear RGB -> sRGB (gamut-clamp)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ===========================================================================
|
|
19
|
+
// Dracula base colors (from spec.draculatheme.com)
|
|
20
|
+
// ===========================================================================
|
|
21
|
+
|
|
22
|
+
const DRACULA = {
|
|
23
|
+
'drac-bg': { dark: '#282A36', darkAlt: '#44475A' },
|
|
24
|
+
'drac-fg': { dark: '#F8F8F2', light: '#1F1F1F' },
|
|
25
|
+
'drac-comment': { dark: '#6272A4', light: '#6C664B' },
|
|
26
|
+
'drac-cyan': { dark: '#8BE9FD', light: '#036A96' },
|
|
27
|
+
'drac-green': { dark: '#50FA7B', light: '#14710A' },
|
|
28
|
+
'drac-orange': { dark: '#FFB86C', light: '#A34D14' },
|
|
29
|
+
'drac-pink': { dark: '#FF79C6', light: '#A3144D' },
|
|
30
|
+
'drac-purple': { dark: '#BD93F9', light: '#644AC9' },
|
|
31
|
+
'drac-red': { dark: '#FF5555', light: '#CB3A2A' },
|
|
32
|
+
'drac-yellow': { dark: '#F1FA8C', light: '#846E15' },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Alucard background (warm cream)
|
|
36
|
+
const ALUCARD_BG = '#FFFBEB'
|
|
37
|
+
|
|
38
|
+
// ===========================================================================
|
|
39
|
+
// sRGB <-> Linear RGB
|
|
40
|
+
// ===========================================================================
|
|
41
|
+
|
|
42
|
+
function hexToRgb(hex) {
|
|
43
|
+
hex = hex.replace('#', '')
|
|
44
|
+
return [
|
|
45
|
+
parseInt(hex.slice(0, 2), 16) / 255,
|
|
46
|
+
parseInt(hex.slice(2, 4), 16) / 255,
|
|
47
|
+
parseInt(hex.slice(4, 6), 16) / 255,
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function srgbToLinear(c) {
|
|
52
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function linearToSrgb(c) {
|
|
56
|
+
c = Math.max(0, Math.min(1, c))
|
|
57
|
+
return c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function rgbToHex(r, g, b) {
|
|
61
|
+
const toHex = v => {
|
|
62
|
+
const clamped = Math.max(0, Math.min(255, Math.round(v * 255)))
|
|
63
|
+
return clamped.toString(16).padStart(2, '0')
|
|
64
|
+
}
|
|
65
|
+
return '#' + toHex(r) + toHex(g) + toHex(b)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ===========================================================================
|
|
69
|
+
// Linear RGB <-> XYZ D65
|
|
70
|
+
// ===========================================================================
|
|
71
|
+
|
|
72
|
+
function linearRgbToXyz(r, g, b) {
|
|
73
|
+
return [
|
|
74
|
+
0.4123907993 * r + 0.3575843394 * g + 0.1804807884 * b,
|
|
75
|
+
0.2126390059 * r + 0.7151686788 * g + 0.0721923154 * b,
|
|
76
|
+
0.0193308187 * r + 0.1191947798 * g + 0.9505321522 * b,
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function xyzToLinearRgb(x, y, z) {
|
|
81
|
+
return [
|
|
82
|
+
3.2409699419 * x - 1.5373831776 * y - 0.4986107603 * z,
|
|
83
|
+
-0.9692436363 * x + 1.8759675015 * y + 0.0415550574 * z,
|
|
84
|
+
0.0556300797 * x - 0.2039769589 * y + 1.0569715142 * z,
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ===========================================================================
|
|
89
|
+
// XYZ D65 <-> OKLab
|
|
90
|
+
// ===========================================================================
|
|
91
|
+
|
|
92
|
+
function xyzToOklab(x, y, z) {
|
|
93
|
+
const l_ = 0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z
|
|
94
|
+
const m_ = 0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z
|
|
95
|
+
const s_ = 0.0482003018 * x + 0.2643662691 * y + 0.6338517070 * z
|
|
96
|
+
|
|
97
|
+
const l = Math.cbrt(l_)
|
|
98
|
+
const m = Math.cbrt(m_)
|
|
99
|
+
const s = Math.cbrt(s_)
|
|
100
|
+
|
|
101
|
+
return [
|
|
102
|
+
0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
|
|
103
|
+
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
|
|
104
|
+
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function oklabToXyz(L, a, b) {
|
|
109
|
+
const l = L + 0.3963377774 * a + 0.2158037573 * b
|
|
110
|
+
const m = L - 0.1055613458 * a - 0.0638541728 * b
|
|
111
|
+
const s = L - 0.0894841775 * a - 1.2914855480 * b
|
|
112
|
+
|
|
113
|
+
const l3 = l * l * l
|
|
114
|
+
const m3 = m * m * m
|
|
115
|
+
const s3 = s * s * s
|
|
116
|
+
|
|
117
|
+
return [
|
|
118
|
+
1.2270138511 * l3 - 0.5577999807 * m3 + 0.2812561490 * s3,
|
|
119
|
+
-0.0405801784 * l3 + 1.1122568696 * m3 - 0.0716766787 * s3,
|
|
120
|
+
-0.0763812845 * l3 - 0.4214819784 * m3 + 1.5861632204 * s3,
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ===========================================================================
|
|
125
|
+
// OKLab <-> OKLCH
|
|
126
|
+
// ===========================================================================
|
|
127
|
+
|
|
128
|
+
function oklabToOklch(L, a, b) {
|
|
129
|
+
const C = Math.sqrt(a * a + b * b)
|
|
130
|
+
let h = Math.atan2(b, a) * (180 / Math.PI)
|
|
131
|
+
if (h < 0) h += 360
|
|
132
|
+
return [L, C, h]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function oklchToOklab(L, C, h) {
|
|
136
|
+
const hRad = h * (Math.PI / 180)
|
|
137
|
+
return [L, C * Math.cos(hRad), C * Math.sin(hRad)]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ===========================================================================
|
|
141
|
+
// Full pipeline: hex -> OKLCH and back
|
|
142
|
+
// ===========================================================================
|
|
143
|
+
|
|
144
|
+
function hexToOklch(hex) {
|
|
145
|
+
const [r, g, b] = hexToRgb(hex).map(srgbToLinear)
|
|
146
|
+
const [x, y, z] = linearRgbToXyz(r, g, b)
|
|
147
|
+
const [L, a, ob] = xyzToOklab(x, y, z)
|
|
148
|
+
return oklabToOklch(L, a, ob)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function oklchToHex(L, C, h) {
|
|
152
|
+
const [oL, oa, ob] = oklchToOklab(L, C, h)
|
|
153
|
+
const [x, y, z] = oklabToXyz(oL, oa, ob)
|
|
154
|
+
let [r, g, b] = xyzToLinearRgb(x, y, z)
|
|
155
|
+
return rgbToHex(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ===========================================================================
|
|
159
|
+
// Gamut-aware OKLCH -> hex with chroma reduction
|
|
160
|
+
// ===========================================================================
|
|
161
|
+
|
|
162
|
+
function isInGamut(r, g, b) {
|
|
163
|
+
const eps = -0.001
|
|
164
|
+
return r >= eps && r <= 1.001 && g >= eps && g <= 1.001 && b >= eps && b <= 1.001
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function oklchToHexGamut(L, C, h) {
|
|
168
|
+
// Binary search: reduce chroma until in sRGB gamut
|
|
169
|
+
let lo = 0, hi = C
|
|
170
|
+
// First check if full chroma is in gamut
|
|
171
|
+
{
|
|
172
|
+
const [oL, oa, ob] = oklchToOklab(L, hi, h)
|
|
173
|
+
const [x, y, z] = oklabToXyz(oL, oa, ob)
|
|
174
|
+
const [r, g, b] = xyzToLinearRgb(x, y, z)
|
|
175
|
+
if (isInGamut(r, g, b)) {
|
|
176
|
+
return rgbToHex(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Binary search
|
|
180
|
+
for (let i = 0; i < 32; i++) {
|
|
181
|
+
const mid = (lo + hi) / 2
|
|
182
|
+
const [oL, oa, ob] = oklchToOklab(L, mid, h)
|
|
183
|
+
const [x, y, z] = oklabToXyz(oL, oa, ob)
|
|
184
|
+
const [r, g, b] = xyzToLinearRgb(x, y, z)
|
|
185
|
+
if (isInGamut(r, g, b)) {
|
|
186
|
+
lo = mid
|
|
187
|
+
} else {
|
|
188
|
+
hi = mid
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const [oL, oa, ob] = oklchToOklab(L, lo, h)
|
|
192
|
+
const [x, y, z] = oklabToXyz(oL, oa, ob)
|
|
193
|
+
const [r, g, b] = xyzToLinearRgb(x, y, z)
|
|
194
|
+
return rgbToHex(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ===========================================================================
|
|
198
|
+
// Shade ramp generation
|
|
199
|
+
// ===========================================================================
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Target OKLCH Lightness values for each shade level.
|
|
203
|
+
* Calibrated to match Tailwind's perceptual distribution.
|
|
204
|
+
* Shade 50 = lightest, 950 = darkest.
|
|
205
|
+
*/
|
|
206
|
+
const SHADE_LIGHTNESS = {
|
|
207
|
+
50: 0.97,
|
|
208
|
+
100: 0.93,
|
|
209
|
+
200: 0.87,
|
|
210
|
+
300: 0.78,
|
|
211
|
+
400: 0.68,
|
|
212
|
+
500: 0.57,
|
|
213
|
+
600: 0.48,
|
|
214
|
+
700: 0.40,
|
|
215
|
+
800: 0.32,
|
|
216
|
+
900: 0.24,
|
|
217
|
+
950: 0.16,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const SHADE_LEVELS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Generate 11-shade ramp from a base color hex.
|
|
224
|
+
* Preserves hue, scales chroma proportionally with lightness.
|
|
225
|
+
*/
|
|
226
|
+
function generateRamp(baseHex, familyName) {
|
|
227
|
+
const [baseL, baseC, baseH] = hexToOklch(baseHex)
|
|
228
|
+
|
|
229
|
+
const shades = {}
|
|
230
|
+
for (const shade of SHADE_LEVELS) {
|
|
231
|
+
const targetL = SHADE_LIGHTNESS[shade]
|
|
232
|
+
// Scale chroma: reduce proportionally as we move away from base lightness
|
|
233
|
+
// toward extremes (very light or very dark), where sRGB gamut narrows
|
|
234
|
+
let chromaScale
|
|
235
|
+
if (targetL > 0.9) {
|
|
236
|
+
// Very light: aggressively reduce chroma
|
|
237
|
+
chromaScale = 0.15 + 0.85 * ((0.97 - targetL) / 0.07)
|
|
238
|
+
chromaScale = Math.max(0.05, chromaScale)
|
|
239
|
+
} else if (targetL < 0.2) {
|
|
240
|
+
// Very dark: reduce chroma
|
|
241
|
+
chromaScale = 0.3 + 0.7 * (targetL / 0.2)
|
|
242
|
+
} else {
|
|
243
|
+
// Mid-range: preserve chroma, slightly boosted near base
|
|
244
|
+
const dist = Math.abs(targetL - baseL)
|
|
245
|
+
chromaScale = 1.0 - dist * 0.3
|
|
246
|
+
chromaScale = Math.max(0.4, Math.min(1.2, chromaScale))
|
|
247
|
+
}
|
|
248
|
+
const targetC = baseC * chromaScale
|
|
249
|
+
const hex = oklchToHexGamut(targetL, targetC, baseH)
|
|
250
|
+
shades[shade] = hex
|
|
251
|
+
}
|
|
252
|
+
return shades
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Generate neutral ramp (low chroma, warm or cool).
|
|
257
|
+
* Chroma is much lower than chromatic families.
|
|
258
|
+
*/
|
|
259
|
+
function generateNeutralRamp(baseHex, familyName) {
|
|
260
|
+
const [baseL, baseC, baseH] = hexToOklch(baseHex)
|
|
261
|
+
// Neutrals get very low chroma (warm or cool tint)
|
|
262
|
+
const neutralChroma = Math.min(baseC, 0.02)
|
|
263
|
+
|
|
264
|
+
const shades = {}
|
|
265
|
+
for (const shade of SHADE_LEVELS) {
|
|
266
|
+
const targetL = SHADE_LIGHTNESS[shade]
|
|
267
|
+
// Chroma tapers even more at extremes for neutrals
|
|
268
|
+
let cScale
|
|
269
|
+
if (targetL > 0.9) {
|
|
270
|
+
cScale = 0.3
|
|
271
|
+
} else if (targetL < 0.2) {
|
|
272
|
+
cScale = 0.5
|
|
273
|
+
} else {
|
|
274
|
+
cScale = 1.0
|
|
275
|
+
}
|
|
276
|
+
const targetC = neutralChroma * cScale
|
|
277
|
+
const hex = oklchToHexGamut(targetL, targetC, baseH)
|
|
278
|
+
shades[shade] = hex
|
|
279
|
+
}
|
|
280
|
+
return shades
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ===========================================================================
|
|
284
|
+
// Generate all families
|
|
285
|
+
// ===========================================================================
|
|
286
|
+
|
|
287
|
+
const families = {}
|
|
288
|
+
|
|
289
|
+
// --- Neutral families ---
|
|
290
|
+
|
|
291
|
+
// drac-bg: dark neutral (cool, from Dracula Background #282A36)
|
|
292
|
+
families['drac-bg'] = generateNeutralRamp(DRACULA['drac-bg'].dark, 'drac-bg')
|
|
293
|
+
|
|
294
|
+
// drac-fg: warm neutral (from Foreground #F8F8F2 / #1F1F1F)
|
|
295
|
+
families['drac-fg'] = generateNeutralRamp(DRACULA['drac-fg'].dark, 'drac-fg')
|
|
296
|
+
|
|
297
|
+
// alu-bg: warm cream neutral (from Alucard Background #FFFBEB)
|
|
298
|
+
families['alu-bg'] = generateNeutralRamp(ALUCARD_BG, 'alu-bg')
|
|
299
|
+
|
|
300
|
+
// --- Chromatic families (dark base = primary reference) ---
|
|
301
|
+
|
|
302
|
+
for (const [name, colors] of Object.entries(DRACULA)) {
|
|
303
|
+
if (['drac-bg', 'drac-fg'].includes(name)) continue
|
|
304
|
+
// Use dark variant as the chromatic reference (brighter, more saturated)
|
|
305
|
+
families[name] = generateRamp(colors.dark, name)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ===========================================================================
|
|
309
|
+
// WCAG 2.x contrast ratio (for verification)
|
|
310
|
+
// ===========================================================================
|
|
311
|
+
|
|
312
|
+
function relLum(hex) {
|
|
313
|
+
const [r, g, b] = hexToRgb(hex).map(srgbToLinear)
|
|
314
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function contrast(hex1, hex2) {
|
|
318
|
+
const L1 = relLum(hex1), L2 = relLum(hex2)
|
|
319
|
+
return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ===========================================================================
|
|
323
|
+
// Output
|
|
324
|
+
// ===========================================================================
|
|
325
|
+
|
|
326
|
+
console.log('/* ===========================================')
|
|
327
|
+
console.log(' Generated Dracula/Alucard Palette')
|
|
328
|
+
console.log(' 11 families x 11 shades = 121 CSS vars')
|
|
329
|
+
console.log(' Generated: ' + new Date().toISOString().split('T')[0])
|
|
330
|
+
console.log(' =========================================== */')
|
|
331
|
+
console.log('')
|
|
332
|
+
console.log(':root {')
|
|
333
|
+
console.log(' /* --- Black & White --- */')
|
|
334
|
+
console.log(' --color-black: #000000;')
|
|
335
|
+
console.log(' --color-white: #ffffff;')
|
|
336
|
+
|
|
337
|
+
for (const [family, shades] of Object.entries(families)) {
|
|
338
|
+
const isNeutral = ['drac-bg', 'drac-fg', 'alu-bg'].includes(family)
|
|
339
|
+
const label = isNeutral ? '(neutral)' : '(chromatic)'
|
|
340
|
+
console.log('')
|
|
341
|
+
console.log(` /* --- ${family} ${label} --- */`)
|
|
342
|
+
for (const shade of SHADE_LEVELS) {
|
|
343
|
+
const hex = shades[shade]
|
|
344
|
+
console.log(` --color-${family}-${shade}: ${hex};`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log('}')
|
|
349
|
+
|
|
350
|
+
// ===========================================================================
|
|
351
|
+
// Verification: contrast of key semantic pairs
|
|
352
|
+
// ===========================================================================
|
|
353
|
+
|
|
354
|
+
console.log('')
|
|
355
|
+
console.log('/* === Contrast Verification ===')
|
|
356
|
+
|
|
357
|
+
// Dark theme: text (drac-fg-50) on bg (drac-bg-900)
|
|
358
|
+
const darkText = families['drac-fg'][50]
|
|
359
|
+
const darkBg = families['drac-bg'][900]
|
|
360
|
+
console.log(` Dark text/bg: ${darkText} on ${darkBg} = ${contrast(darkText, darkBg).toFixed(2)}:1`)
|
|
361
|
+
|
|
362
|
+
// Light theme: text (drac-fg-900) on bg (alu-bg-50)
|
|
363
|
+
const lightText = families['drac-fg'][900]
|
|
364
|
+
const lightBg = families['alu-bg'][50]
|
|
365
|
+
console.log(` Light text/bg: ${lightText} on ${lightBg} = ${contrast(lightText, lightBg).toFixed(2)}:1`)
|
|
366
|
+
|
|
367
|
+
// Dark primary: drac-cyan-400 on drac-bg-900
|
|
368
|
+
const darkPrimary = families['drac-cyan'][400]
|
|
369
|
+
console.log(` Dark primary/bg: ${darkPrimary} on ${darkBg} = ${contrast(darkPrimary, darkBg).toFixed(2)}:1`)
|
|
370
|
+
|
|
371
|
+
// Light primary: drac-purple-700 on alu-bg-50
|
|
372
|
+
const lightPrimary = families['drac-purple'][700]
|
|
373
|
+
console.log(` Light primary/bg: ${lightPrimary} on ${lightBg} = ${contrast(lightPrimary, lightBg).toFixed(2)}:1`)
|
|
374
|
+
|
|
375
|
+
// Dark danger: drac-red-300 on drac-bg-900
|
|
376
|
+
const darkDanger = families['drac-red'][300]
|
|
377
|
+
console.log(` Dark danger/bg: ${darkDanger} on ${darkBg} = ${contrast(darkDanger, darkBg).toFixed(2)}:1`)
|
|
378
|
+
|
|
379
|
+
// Dark accent: drac-purple-300 on drac-bg-900
|
|
380
|
+
const darkAccent = families['drac-purple'][300]
|
|
381
|
+
console.log(` Dark accent/bg: ${darkAccent} on ${darkBg} = ${contrast(darkAccent, darkBg).toFixed(2)}:1`)
|
|
382
|
+
|
|
383
|
+
// Dark secondary text: drac-comment-200 on drac-bg-900
|
|
384
|
+
const darkSecText = families['drac-comment'][200]
|
|
385
|
+
console.log(` Dark secondary/bg: ${darkSecText} on ${darkBg} = ${contrast(darkSecText, darkBg).toFixed(2)}:1`)
|
|
386
|
+
|
|
387
|
+
// Shiki dark: drac-cyan-400 on drac-bg-800
|
|
388
|
+
const shikiBgDark = families['drac-bg'][800]
|
|
389
|
+
console.log(` Shiki dark cyan/bg: ${darkPrimary} on ${shikiBgDark} = ${contrast(darkPrimary, shikiBgDark).toFixed(2)}:1`)
|
|
390
|
+
|
|
391
|
+
// Shiki light: drac-purple-700 on alu-bg-200
|
|
392
|
+
const shikiBgLight = families['alu-bg'][200]
|
|
393
|
+
console.log(` Shiki light purple/bg: ${lightPrimary} on ${shikiBgLight} = ${contrast(lightPrimary, shikiBgLight).toFixed(2)}:1`)
|
|
394
|
+
|
|
395
|
+
console.log(' === */')
|