@etus/seven-skill 0.1.0-beta.1
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/skills/seven/SKILL.md +162 -0
- package/.claude/skills/seven/reference/audit.md +120 -0
- package/.claude/skills/seven/reference/brand.md +24 -0
- package/.claude/skills/seven/reference/clarify.md +76 -0
- package/.claude/skills/seven/reference/color-and-contrast.md +84 -0
- package/.claude/skills/seven/reference/motion-design.md +71 -0
- package/.claude/skills/seven/reference/polish.md +55 -0
- package/.claude/skills/seven/reference/product.md +45 -0
- package/.claude/skills/seven/reference/shape.md +85 -0
- package/.claude/skills/seven/reference/spatial-design.md +60 -0
- package/.claude/skills/seven/reference/typography.md +43 -0
- package/.claude/skills/seven/reference/ux-writing.md +47 -0
- package/.claude/skills/seven/scripts/load-context.mjs +84 -0
- package/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE +190 -0
- package/NOTICE.md +26 -0
- package/README.md +118 -0
- package/cli/bin/commands/skills.mjs +664 -0
- package/cli/bin/seven.mjs +68 -0
- package/cli/engine/browser/injected/index.mjs +84 -0
- package/cli/engine/cli/main.mjs +215 -0
- package/cli/engine/detect-antipatterns-browser.js +3014 -0
- package/cli/engine/detect-antipatterns.mjs +44 -0
- package/cli/engine/engines/browser/detect-url.mjs +108 -0
- package/cli/engine/engines/regex/detect-text.mjs +508 -0
- package/cli/engine/engines/static-html/css-cascade.mjs +957 -0
- package/cli/engine/engines/static-html/detect-html.mjs +211 -0
- package/cli/engine/engines/visual/screenshot-contrast.mjs +192 -0
- package/cli/engine/findings.mjs +28 -0
- package/cli/engine/node/file-system.mjs +212 -0
- package/cli/engine/profile/profiler.mjs +169 -0
- package/cli/engine/registry/seven-antipatterns.mjs +494 -0
- package/cli/engine/rules/checks.mjs +1518 -0
- package/cli/engine/shared/color.mjs +204 -0
- package/cli/engine/shared/constants.mjs +91 -0
- package/cli/engine/shared/page.mjs +9 -0
- package/cli/engine/shared/tokens.mjs +153 -0
- package/cli/engine/token-data.generated.mjs +691 -0
- package/docs/detector-rules.md +605 -0
- package/docs/figma-token-rule-exploration.md +185 -0
- package/package.json +76 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// ETUS Digital — Seven Design System. Detector architecture adapted from
|
|
3
|
+
// impeccable (Paul Bakaus, Apache-2.0). See packages/seven-skill/NOTICE.md.
|
|
4
|
+
|
|
5
|
+
// ─── Color utilities ────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export function isNeutralColor(color) {
|
|
8
|
+
if (!color || color === 'transparent') return true
|
|
9
|
+
|
|
10
|
+
// rgb/rgba — channel spread. Threshold 30 is ~11.7% of the 0-255 range.
|
|
11
|
+
const rgb = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
|
|
12
|
+
if (rgb) {
|
|
13
|
+
return Math.max(+rgb[1], +rgb[2], +rgb[3]) - Math.min(+rgb[1], +rgb[2], +rgb[3]) < 30
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// oklch()/lch() — chroma is the second numeric component. Seven's tokens are
|
|
17
|
+
// OKLCH throughout, so this path matters: chroma >= 0.02 reads as tinted.
|
|
18
|
+
const oklch = color.match(/oklch\(\s*[\d.]+%?\s*([\d.-]+)/i)
|
|
19
|
+
if (oklch) return parseFloat(oklch[1]) < 0.02
|
|
20
|
+
const lch = color.match(/lch\(\s*[\d.]+%?\s*([\d.-]+)/i)
|
|
21
|
+
if (lch) return parseFloat(lch[1]) < 3
|
|
22
|
+
|
|
23
|
+
// oklab()/lab() — a and b are signed axes; chroma = hypot(a, b).
|
|
24
|
+
const oklab = color.match(/oklab\(\s*[\d.]+%?\s*([\d.-]+)\s+([\d.-]+)/i)
|
|
25
|
+
if (oklab) return Math.hypot(parseFloat(oklab[1]), parseFloat(oklab[2])) < 0.02
|
|
26
|
+
const lab = color.match(/lab\(\s*[\d.]+%?\s*([\d.-]+)\s+([\d.-]+)/i)
|
|
27
|
+
if (lab) return Math.hypot(parseFloat(lab[1]), parseFloat(lab[2])) < 3
|
|
28
|
+
|
|
29
|
+
// hsl/hsla — saturation is the second numeric component (percent).
|
|
30
|
+
const hsl = color.match(/hsla?\(\s*[\d.-]+\s*,?\s*([\d.]+)%/i)
|
|
31
|
+
if (hsl) return parseFloat(hsl[1]) < 10
|
|
32
|
+
|
|
33
|
+
// hwb(hue whiteness% blackness%) — gray when whiteness + blackness >= 100.
|
|
34
|
+
const hwb = color.match(/hwb\(\s*[\d.-]+\s+([\d.]+)%\s+([\d.]+)%/i)
|
|
35
|
+
if (hwb) {
|
|
36
|
+
const w = parseFloat(hwb[1])
|
|
37
|
+
const b = parseFloat(hwb[2])
|
|
38
|
+
return 1 - Math.min(100, w + b) / 100 < 0.1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Unknown format — err on the side of DETECTING rather than skipping.
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseRgb(color) {
|
|
46
|
+
if (!color || color === 'transparent') return null
|
|
47
|
+
const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/)
|
|
48
|
+
if (!m) return null
|
|
49
|
+
return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseHex(hex) {
|
|
53
|
+
if (!hex) return null
|
|
54
|
+
const m = hex.match(/^#([0-9a-f]{3,8})$/i)
|
|
55
|
+
if (!m) return null
|
|
56
|
+
const h = m[1]
|
|
57
|
+
if (h.length === 3) {
|
|
58
|
+
return { r: parseInt(h[0] + h[0], 16), g: parseInt(h[1] + h[1], 16), b: parseInt(h[2] + h[2], 16), a: 1 }
|
|
59
|
+
}
|
|
60
|
+
if (h.length === 6 || h.length === 8) {
|
|
61
|
+
return {
|
|
62
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
63
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
64
|
+
b: parseInt(h.slice(4, 6), 16),
|
|
65
|
+
a: h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function relativeLuminance({ r, g, b }) {
|
|
72
|
+
const [rs, gs, bs] = [r / 255, g / 255, b / 255].map((c) =>
|
|
73
|
+
c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
|
|
74
|
+
)
|
|
75
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function contrastRatio(c1, c2) {
|
|
79
|
+
const l1 = relativeLuminance(c1)
|
|
80
|
+
const l2 = relativeLuminance(c2)
|
|
81
|
+
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function hasChroma(c, threshold = 30) {
|
|
85
|
+
if (!c) return false
|
|
86
|
+
return Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b) >= threshold
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getHue(c) {
|
|
90
|
+
if (!c) return 0
|
|
91
|
+
const r = c.r / 255
|
|
92
|
+
const g = c.g / 255
|
|
93
|
+
const b = c.b / 255
|
|
94
|
+
const max = Math.max(r, g, b)
|
|
95
|
+
const min = Math.min(r, g, b)
|
|
96
|
+
if (max === min) return 0
|
|
97
|
+
const d = max - min
|
|
98
|
+
let h
|
|
99
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
|
100
|
+
else if (max === g) h = ((b - r) / d + 2) / 6
|
|
101
|
+
else h = ((r - g) / d + 4) / 6
|
|
102
|
+
return Math.round(h * 360)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function colorToHex(c) {
|
|
106
|
+
if (!c) return '?'
|
|
107
|
+
return '#' + [c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, '0')).join('')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fuchsia / magenta detection. Seven reserves fuchsia for charts; on UI chrome
|
|
112
|
+
* it is an anti-pattern. A color is "fuchsia" when it has real chroma and its
|
|
113
|
+
* hue sits in the magenta band (roughly 280-340 degrees).
|
|
114
|
+
*/
|
|
115
|
+
export function isFuchsia(c) {
|
|
116
|
+
if (!c || !hasChroma(c, 40)) return false
|
|
117
|
+
const hue = getHue(c)
|
|
118
|
+
return hue >= 280 && hue <= 340
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* OKLCH -> sRGB. Seven's color tokens are OKLCH; matching them to hex/rgb
|
|
123
|
+
* literals in code needs this conversion. Follows the CSS Color 4 / Björn
|
|
124
|
+
* Ottosson reference. L is 0-1, C is chroma, H is degrees.
|
|
125
|
+
*/
|
|
126
|
+
export function oklchToRgb(L, C, H) {
|
|
127
|
+
const hRad = (H * Math.PI) / 180
|
|
128
|
+
const a = C * Math.cos(hRad)
|
|
129
|
+
const b = C * Math.sin(hRad)
|
|
130
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b
|
|
131
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b
|
|
132
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b
|
|
133
|
+
const l = l_ ** 3
|
|
134
|
+
const m = m_ ** 3
|
|
135
|
+
const s = s_ ** 3
|
|
136
|
+
const lr = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
|
|
137
|
+
const lg = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
|
|
138
|
+
const lb = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
|
|
139
|
+
const gamma = (x) => (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055)
|
|
140
|
+
return [gamma(lr), gamma(lg), gamma(lb)].map((v) => Math.round(Math.max(0, Math.min(1, v)) * 255))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Canonical form for an exact-string token match: trimmed, lowercased,
|
|
144
|
+
* whitespace-collapsed. Shared by the token-data build step and the runtime
|
|
145
|
+
* accessor. */
|
|
146
|
+
export function normalizeRaw(value) {
|
|
147
|
+
return String(value).trim().toLowerCase().replace(/\s+/g, ' ')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Reduce any color literal (oklch, hex, rgb/rgba) to a canonical `r,g,b,a`
|
|
152
|
+
* key. Shared by the token-data build step and the runtime token accessor so
|
|
153
|
+
* a literal in consumer code matches a Seven token regardless of format.
|
|
154
|
+
* Returns null when the value is not a recognized color.
|
|
155
|
+
*/
|
|
156
|
+
export function colorToRgbaKey(value) {
|
|
157
|
+
if (!value || typeof value !== 'string') return null
|
|
158
|
+
const v = value.trim().toLowerCase()
|
|
159
|
+
|
|
160
|
+
const oklch = v.match(/oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+)(%?))?\s*\)/)
|
|
161
|
+
if (oklch) {
|
|
162
|
+
const L = oklch[2] === '%' ? parseFloat(oklch[1]) / 100 : parseFloat(oklch[1])
|
|
163
|
+
const C = parseFloat(oklch[3])
|
|
164
|
+
const H = parseFloat(oklch[4])
|
|
165
|
+
let alpha = 1
|
|
166
|
+
if (oklch[5] !== undefined) {
|
|
167
|
+
alpha = oklch[6] === '%' ? parseFloat(oklch[5]) / 100 : parseFloat(oklch[5])
|
|
168
|
+
}
|
|
169
|
+
const [r, g, b] = oklchToRgb(L, C, H)
|
|
170
|
+
return `${r},${g},${b},${alpha.toFixed(2)}`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const hex = v.match(/^#([0-9a-f]{3,8})$/)
|
|
174
|
+
if (hex) {
|
|
175
|
+
const h = hex[1]
|
|
176
|
+
let r
|
|
177
|
+
let g
|
|
178
|
+
let b
|
|
179
|
+
let a = 1
|
|
180
|
+
if (h.length === 3 || h.length === 4) {
|
|
181
|
+
r = parseInt(h[0] + h[0], 16)
|
|
182
|
+
g = parseInt(h[1] + h[1], 16)
|
|
183
|
+
b = parseInt(h[2] + h[2], 16)
|
|
184
|
+
if (h.length === 4) a = parseInt(h[3] + h[3], 16) / 255
|
|
185
|
+
} else if (h.length === 6 || h.length === 8) {
|
|
186
|
+
r = parseInt(h.slice(0, 2), 16)
|
|
187
|
+
g = parseInt(h.slice(2, 4), 16)
|
|
188
|
+
b = parseInt(h.slice(4, 6), 16)
|
|
189
|
+
if (h.length === 8) a = parseInt(h.slice(6, 8), 16) / 255
|
|
190
|
+
} else {
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
return `${r},${g},${b},${a.toFixed(2)}`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const rgb = v.match(/rgba?\(\s*(\d+)\s*[, ]\s*(\d+)\s*[, ]\s*(\d+)\s*(?:[,/]\s*([\d.]+)(%?))?\s*\)/)
|
|
197
|
+
if (rgb) {
|
|
198
|
+
let a = 1
|
|
199
|
+
if (rgb[4] !== undefined) a = rgb[5] === '%' ? parseFloat(rgb[4]) / 100 : parseFloat(rgb[4])
|
|
200
|
+
return `${+rgb[1]},${+rgb[2]},${+rgb[3]},${a.toFixed(2)}`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// ETUS Digital — Seven Design System. Detector architecture adapted from
|
|
3
|
+
// impeccable (Paul Bakaus, Apache-2.0). See packages/seven-skill/NOTICE.md.
|
|
4
|
+
|
|
5
|
+
// ─── Seven-specific catalogs ────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The only sans face allowed in Seven. Note this INVERTS impeccable's
|
|
9
|
+
* `OVERUSED_FONTS` rule: impeccable flags Inter as overused, Seven mandates it.
|
|
10
|
+
*/
|
|
11
|
+
export const INTER_FAMILY_NAMES = new Set([
|
|
12
|
+
'inter',
|
|
13
|
+
'inter-variable',
|
|
14
|
+
'intervariable',
|
|
15
|
+
'inter var',
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
/** The only mono face allowed in Seven. */
|
|
19
|
+
export const JETBRAINS_MONO_FAMILY_NAMES = new Set([
|
|
20
|
+
'jetbrains mono',
|
|
21
|
+
'jetbrains-mono',
|
|
22
|
+
'jetbrainsmono',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
/** Generic / system fallbacks that never count as a non-Inter face. */
|
|
26
|
+
export const SYSTEM_FONT_FALLBACK_TOKENS = new Set([
|
|
27
|
+
'system-ui',
|
|
28
|
+
'-apple-system',
|
|
29
|
+
'blinkmacsystemfont',
|
|
30
|
+
'segoe ui',
|
|
31
|
+
'roboto',
|
|
32
|
+
'helvetica',
|
|
33
|
+
'helvetica neue',
|
|
34
|
+
'arial',
|
|
35
|
+
'sans-serif',
|
|
36
|
+
'serif',
|
|
37
|
+
'monospace',
|
|
38
|
+
'ui-monospace',
|
|
39
|
+
'ui-sans-serif',
|
|
40
|
+
'ui-serif',
|
|
41
|
+
'ui-rounded',
|
|
42
|
+
'menlo',
|
|
43
|
+
'consolas',
|
|
44
|
+
'courier new',
|
|
45
|
+
'inherit',
|
|
46
|
+
'initial',
|
|
47
|
+
'unset',
|
|
48
|
+
'revert',
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
/** HTML tags whose own styling never trips element-level rules. */
|
|
52
|
+
export const SAFE_TAGS = new Set([
|
|
53
|
+
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'title', 'br',
|
|
54
|
+
'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'defs', 'use',
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
/** European Portuguese vocabulary that must not appear in pt-BR chrome. */
|
|
58
|
+
export const PT_PT_VOCAB_PAIRS = [
|
|
59
|
+
{ ptPt: 'ficheiro', ptBr: 'arquivo' },
|
|
60
|
+
{ ptPt: 'ficheiros', ptBr: 'arquivos' },
|
|
61
|
+
{ ptPt: 'telemóvel', ptBr: 'celular' },
|
|
62
|
+
{ ptPt: 'telemóveis', ptBr: 'celulares' },
|
|
63
|
+
{ ptPt: 'ecrã', ptBr: 'tela' },
|
|
64
|
+
{ ptPt: 'ecrãs', ptBr: 'telas' },
|
|
65
|
+
{ ptPt: 'rato', ptBr: 'mouse' },
|
|
66
|
+
{ ptPt: 'autocarro', ptBr: 'ônibus' },
|
|
67
|
+
{ ptPt: 'gestor', ptBr: 'gerente' },
|
|
68
|
+
{ ptPt: 'utilizador', ptBr: 'usuário' },
|
|
69
|
+
{ ptPt: 'utilizadores', ptBr: 'usuários' },
|
|
70
|
+
{ ptPt: 'sítio', ptBr: 'site' },
|
|
71
|
+
{ ptPt: 'apagar', ptBr: 'excluir' },
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Package names whose source is exempt from rules that would mis-fire on
|
|
76
|
+
* token-defining or skill-defining code. Resolved by walking up to the
|
|
77
|
+
* nearest package.json — works inside the monorepo and inside a consumer's
|
|
78
|
+
* node_modules alike.
|
|
79
|
+
*/
|
|
80
|
+
export const SEVEN_INTERNAL_PACKAGES = new Set(['@etus/ui', '@etus/tokens', '@etus/seven-skill'])
|
|
81
|
+
|
|
82
|
+
/** Path segments whose files define tokens and may carry raw color literals. */
|
|
83
|
+
export const TOKEN_DEFINITION_PATH_SEGMENTS = [
|
|
84
|
+
'packages/tokens/sources/',
|
|
85
|
+
'packages/tokens/sd/',
|
|
86
|
+
'packages/tokens/dist/',
|
|
87
|
+
'docs/tokens-json/',
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
/** Test fixtures are always scanned regardless of the package they sit in. */
|
|
91
|
+
export const FIXTURES_PATH_SEGMENT = '/tests/fixtures/'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// ETUS Digital — Seven Design System. Detector architecture adapted from
|
|
3
|
+
// impeccable (Paul Bakaus, Apache-2.0). See packages/seven-skill/NOTICE.md.
|
|
4
|
+
|
|
5
|
+
/** True when the content looks like a full HTML page, not a component partial. */
|
|
6
|
+
export function isFullPage(content) {
|
|
7
|
+
const stripped = content.replace(/<!--[\s\S]*?-->/g, '')
|
|
8
|
+
return /<!doctype\s|<html[\s>]|<head[\s>]/i.test(stripped)
|
|
9
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// ETUS Digital — Seven Design System. Detector architecture adapted from
|
|
3
|
+
// impeccable (Paul Bakaus, Apache-2.0). See packages/seven-skill/NOTICE.md.
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Token accessor. The single API the rule layer uses to consult Seven's
|
|
7
|
+
* design tokens. It wraps the build-time snapshot (token-data.generated.mjs,
|
|
8
|
+
* derived from @etus/tokens) behind semantic accessors so the rules never see
|
|
9
|
+
* the snapshot's raw shape — the snapshot can change without touching a rule.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { TOKEN_DATA } from '../token-data.generated.mjs'
|
|
13
|
+
import { colorToRgbaKey, normalizeRaw } from './color.mjs'
|
|
14
|
+
|
|
15
|
+
/** True when the snapshot carries usable data. Rules fall back to heuristics
|
|
16
|
+
* if this is ever false (defensive — not expected in a built package). */
|
|
17
|
+
export function tokenDataAvailable() {
|
|
18
|
+
return Boolean(TOKEN_DATA && Object.keys(TOKEN_DATA.colorByRgba || {}).length > 0)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── Color ──────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a literal color (hex, rgb, oklch) to the Seven token cssVar that
|
|
25
|
+
* declares it, or null when no token matches. Format-insensitive: a hex
|
|
26
|
+
* literal resolves the same OKLCH token as the OKLCH form.
|
|
27
|
+
*/
|
|
28
|
+
export function colorToCssVar(rawValue) {
|
|
29
|
+
if (!rawValue) return null
|
|
30
|
+
const key = colorToRgbaKey(rawValue)
|
|
31
|
+
if (key && TOKEN_DATA.colorByRgba[key]) return TOKEN_DATA.colorByRgba[key]
|
|
32
|
+
const raw = normalizeRaw(rawValue)
|
|
33
|
+
return TOKEN_DATA.colorByRaw[raw] || null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when the literal color is a declared Seven token value. */
|
|
37
|
+
export function isKnownTokenColor(rawValue) {
|
|
38
|
+
return colorToCssVar(rawValue) !== null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Typography ─────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The sans/mono families Seven sanctions. Derived from the `fontFamily`
|
|
45
|
+
* tokens, plus JetBrains Mono — Seven's documented code face, which the
|
|
46
|
+
* current token catalog does not yet declare as a `fontFamily` token.
|
|
47
|
+
*/
|
|
48
|
+
export function allowedFontFamilies() {
|
|
49
|
+
return [...new Set([...TOKEN_DATA.fontFamilies, 'jetbrains mono'])]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isAllowedFontFamily(name) {
|
|
53
|
+
if (!name) return false
|
|
54
|
+
return allowedFontFamilies().includes(String(name).trim().toLowerCase())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function fontSizesPx() {
|
|
58
|
+
return TOKEN_DATA.fontSizesPx
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The smallest declared font size — the floor `tiny-text` enforces. */
|
|
62
|
+
export function minFontSizePx() {
|
|
63
|
+
return TOKEN_DATA.fontSizesPx.length > 0 ? TOKEN_DATA.fontSizesPx[0] : 12
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function lineHeights() {
|
|
67
|
+
return TOKEN_DATA.lineHeights
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function letterSpacings() {
|
|
71
|
+
return TOKEN_DATA.letterSpacings
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Shape ──────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export function radiusScale() {
|
|
77
|
+
return TOKEN_DATA.radii
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function componentRadiiPx() {
|
|
81
|
+
return TOKEN_DATA.radii.component
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function surfaceRadiiPx() {
|
|
85
|
+
return TOKEN_DATA.radii.surface
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function pillRadiiPx() {
|
|
89
|
+
return TOKEN_DATA.radii.pill
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function spacingScalePx() {
|
|
93
|
+
return TOKEN_DATA.spacingPx
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Motion ─────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/** Motion duration tokens as `{ cssVar: value }`. */
|
|
99
|
+
export function motionDurations() {
|
|
100
|
+
return TOKEN_DATA.durations
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Easing tokens as `{ cssVar: cubic-bezier-value }`. Includes `--ease-bounce`
|
|
104
|
+
* and `--ease-spring` — Seven's catalog does declare them, so a bounce easing
|
|
105
|
+
* is drift only when it is NOT one of these sanctioned tokens. */
|
|
106
|
+
export function easingTokens() {
|
|
107
|
+
return TOKEN_DATA.easings
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The set of sanctioned cubic-bezier values, normalized for comparison. */
|
|
111
|
+
export function sanctionedEasingValues() {
|
|
112
|
+
return new Set(Object.values(TOKEN_DATA.easings).map((v) => normalizeRaw(v)))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Sanctioned easing tokens parsed to [x1, y1, x2, y2] tuples. A cubic-bezier
|
|
116
|
+
* in code that matches one of these is a Seven easing token value, not drift —
|
|
117
|
+
* even when its curve overshoots (the catalog declares --ease-bounce). */
|
|
118
|
+
export function sanctionedEasingBeziers() {
|
|
119
|
+
const out = []
|
|
120
|
+
for (const value of Object.values(TOKEN_DATA.easings)) {
|
|
121
|
+
const m = String(value).match(
|
|
122
|
+
/cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/
|
|
123
|
+
)
|
|
124
|
+
if (m) out.push([+m[1], +m[2], +m[3], +m[4]])
|
|
125
|
+
}
|
|
126
|
+
return out
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Elevation ──────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export function shadowTokens() {
|
|
132
|
+
return TOKEN_DATA.shadows
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Semantic roles ─────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The Seven semantic-color token for a role (success, destructive, warning,
|
|
139
|
+
* info, muted, accent, ...). Prefers a fill/background variant, then the bare
|
|
140
|
+
* role token. Returns null when no semantic token carries that role — used to
|
|
141
|
+
* point a Tailwind palette utility at its Seven equivalent.
|
|
142
|
+
*/
|
|
143
|
+
export function suggestSemanticToken(role) {
|
|
144
|
+
const vars = TOKEN_DATA.semanticColorVars || []
|
|
145
|
+
const matches = vars.filter((v) => v.includes(role))
|
|
146
|
+
if (matches.length === 0) return null
|
|
147
|
+
return (
|
|
148
|
+
matches.find((v) => v.endsWith('-background')) ||
|
|
149
|
+
matches.find((v) => v === `--${role}`) ||
|
|
150
|
+
matches.find((v) => !v.includes('-foreground') && !v.includes('-border')) ||
|
|
151
|
+
matches[0]
|
|
152
|
+
)
|
|
153
|
+
}
|