@hotelfriendag/design-tokens 0.3.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/LICENSE +21 -0
- package/README.md +558 -0
- package/README.uk.md +377 -0
- package/RFC-0001-cross-project-design-system.md +273 -0
- package/RFC-0002-semantic-tier-naming.md +296 -0
- package/UI_DESIGN.md +608 -0
- package/ai-rules/CLAUDE.md +80 -0
- package/ai-rules/cursorrules.template +39 -0
- package/ai-rules/github-copilot-instructions.md +45 -0
- package/ai-rules/system-prompt-compact.md +43 -0
- package/components.html +3018 -0
- package/generate-tokens.cjs +665 -0
- package/package.json +98 -0
- package/portal-audit.html +2306 -0
- package/pre-built/_tokens.scss +138 -0
- package/pre-built/components.css +515 -0
- package/pre-built/shadcn-tokens.css +67 -0
- package/pre-built/status.css +51 -0
- package/pre-built/stylelint-design-system.cjs +69 -0
- package/pre-built/tailwind.additive.css +158 -0
- package/pre-built/tailwind.css +158 -0
- package/pre-built/tailwind.preset.js +207 -0
- package/pre-built/tokens.css +185 -0
- package/pre-built/tokens.d.ts +240 -0
- package/pre-built/tokens.js +243 -0
- package/pre-built/tokens.ts +240 -0
- package/scripts/integration-smoke.sh +91 -0
- package/scripts/pre-commit.sh +55 -0
- package/scripts/validate-tokens.cjs +113 -0
- package/states-canonical.json +240 -0
- package/states.json +2950 -0
- package/status-map.json +47 -0
- package/tokens.figma.json +230 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* validate-tokens.cjs — drift detection for the design-system bundle.
|
|
4
|
+
*
|
|
5
|
+
* Runs three checks against pre-built/*.css:
|
|
6
|
+
* 1. Every `var(--*)` reference resolves to a defined CSS custom property.
|
|
7
|
+
* 2. No bare hex literals in components.css / status.css (outside comments + var() fallbacks).
|
|
8
|
+
* 3. Every doc-snippet token reference exists in pre-built/*.css (anti-RFC-0001 §2.2 drift).
|
|
9
|
+
*
|
|
10
|
+
* Exits 1 on any failure. Use as a pre-commit hook or CI step.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node scripts/validate-tokens.cjs # from portable-design/
|
|
14
|
+
* node docs/portable-design/scripts/validate-tokens.cjs # from repo root
|
|
15
|
+
*
|
|
16
|
+
* ESM-safe: `.cjs` extension. No deps.
|
|
17
|
+
*/
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
22
|
+
|
|
23
|
+
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
|
|
24
|
+
const exists = (p) => fs.existsSync(path.join(ROOT, p));
|
|
25
|
+
|
|
26
|
+
// ── 1. Index of CSS custom-property DEFINITIONS in pre-built/*.css ──
|
|
27
|
+
const preBuiltFiles = ['pre-built/tokens.css', 'pre-built/components.css', 'pre-built/status.css', 'pre-built/tailwind.css'];
|
|
28
|
+
const allDefs = new Set();
|
|
29
|
+
for (const f of preBuiltFiles) {
|
|
30
|
+
if (!exists(f)) continue;
|
|
31
|
+
for (const m of read(f).matchAll(/--([a-z0-9-]+):/g)) allDefs.add(m[1]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── 2. Strip CSS comments (so hex literals inside `/* ... */` don't trigger findings) ──
|
|
35
|
+
const stripComments = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
36
|
+
|
|
37
|
+
// ── 3. Collect findings ──
|
|
38
|
+
const findings = [];
|
|
39
|
+
|
|
40
|
+
// Check 1: every var(--*) in pre-built/*.css resolves to a def
|
|
41
|
+
for (const f of preBuiltFiles) {
|
|
42
|
+
if (!exists(f)) continue;
|
|
43
|
+
const src = read(f);
|
|
44
|
+
for (const m of src.matchAll(/var\(--([a-z0-9-]+)(?:,[^)]*)?\)/g)) {
|
|
45
|
+
const name = m[1];
|
|
46
|
+
if (!allDefs.has(name)) {
|
|
47
|
+
const line = src.slice(0, m.index).split('\n').length;
|
|
48
|
+
findings.push({ severity: 'error', file: f, line, msg: `Unresolved var(--${name})` });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check 2: no bare hex literals in components.css / status.css outside var() fallbacks
|
|
54
|
+
const checkBareHex = (file) => {
|
|
55
|
+
if (!exists(file)) return;
|
|
56
|
+
const src = stripComments(read(file));
|
|
57
|
+
const hexRe = /#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})\b/g;
|
|
58
|
+
// Drop anything inside var(...) (the comma fallback)
|
|
59
|
+
const stripped = src.replace(/var\([^)]*\)/g, 'var(...)');
|
|
60
|
+
for (const m of stripped.matchAll(hexRe)) {
|
|
61
|
+
const hex = '#' + m[1];
|
|
62
|
+
// Allow #fff / #FFFFFF / #000 / #000000 — neutral primitives that don't need tokens
|
|
63
|
+
if (/^#(fff|FFF|ffffff|FFFFFF|000|000000)$/.test(hex)) continue;
|
|
64
|
+
const line = stripped.slice(0, m.index).split('\n').length;
|
|
65
|
+
findings.push({ severity: 'warn', file, line, msg: `Bare hex literal ${hex} — should reference a token (or document why)` });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
checkBareHex('pre-built/components.css');
|
|
69
|
+
checkBareHex('pre-built/status.css');
|
|
70
|
+
|
|
71
|
+
// Check 3: doc-snippet token references vs pre-built defs
|
|
72
|
+
const docFiles = ['README.md', 'UI_DESIGN.md', 'ai-rules/CLAUDE.md', 'ai-rules/cursorrules.template', 'ai-rules/github-copilot-instructions.md', 'ai-rules/system-prompt-compact.md'];
|
|
73
|
+
// Refs INSIDE markdown code-fenced blocks are skipped to avoid changelog-history false positives
|
|
74
|
+
// (e.g. "Phase 1A renamed `var(--color-primary)` → `var(--color-hf-accent)`" — both shouldn't trip).
|
|
75
|
+
// We only validate refs in setup snippets (HTML/CSS code blocks) — which start with ``` followed by a non-empty language.
|
|
76
|
+
const collectDocRefs = (src) => {
|
|
77
|
+
const refs = new Set();
|
|
78
|
+
// Tokenise: scan code blocks ```html/```css/```jsx... but NOT prose backticks
|
|
79
|
+
const codeBlockRe = /```(html|css|jsx|tsx|js|ts|scss|vue|svelte)\s*\n([\s\S]*?)```/g;
|
|
80
|
+
for (const m of src.matchAll(codeBlockRe)) {
|
|
81
|
+
for (const r of m[2].matchAll(/var\(--([a-z0-9-]+)(?:,[^)]*)?\)/g)) {
|
|
82
|
+
refs.add(r[1]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return refs;
|
|
86
|
+
};
|
|
87
|
+
for (const f of docFiles) {
|
|
88
|
+
if (!exists(f)) continue;
|
|
89
|
+
const refs = collectDocRefs(read(f));
|
|
90
|
+
for (const name of refs) {
|
|
91
|
+
if (!allDefs.has(name)) {
|
|
92
|
+
findings.push({ severity: 'error', file: f, line: 0, msg: `Doc snippet references undefined var(--${name})` });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── 4. Report ──
|
|
98
|
+
const errors = findings.filter(f => f.severity === 'error');
|
|
99
|
+
const warns = findings.filter(f => f.severity === 'warn');
|
|
100
|
+
|
|
101
|
+
if (findings.length === 0) {
|
|
102
|
+
console.log('✓ validate-tokens: all checks passed');
|
|
103
|
+
console.log(` ${allDefs.size} CSS variables defined, all var() refs resolve, no bare hex.`);
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`validate-tokens: ${errors.length} error(s), ${warns.length} warning(s)\n`);
|
|
108
|
+
for (const f of findings) {
|
|
109
|
+
const loc = f.line ? `${f.file}:${f.line}` : f.file;
|
|
110
|
+
const tag = f.severity === 'error' ? '✗ ERROR' : '⚠ WARN ';
|
|
111
|
+
console.log(` ${tag} ${loc} ${f.msg}`);
|
|
112
|
+
}
|
|
113
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"description": "Canonical interactive states for new HotelFriend sub-products. Derived from the design system spec — use this as the authoritative reference when building new components. For the raw portal extraction see states.json. For visual rendering see components.html (each entry below maps to a section there).",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"lastReconciled": "2026-05-20",
|
|
6
|
+
"tokens": "tokens.figma.json",
|
|
7
|
+
"visualReference": "components.html",
|
|
8
|
+
"focusRingRule": "All focusable elements: outline:none on :focus, apply box-shadow ring on :focus-visible. Universal rule: box-shadow:0 0 0 2px rgba(36,175,232,0.2) — or use the generic 'global-focus-visible' entry below.",
|
|
9
|
+
"namingConvention": "Component-prefix .hf-* (e.g. .hf-modal, .hf-alert, .hf-pagination). BEM-style child selectors with __ separator (.hf-modal__header, .hf-alert__icon). Variant modifiers with -- (.hf-alert--success). State classes with .is-* prefix (.is-active, .is-disabled).",
|
|
10
|
+
"portalExactNote": "Entries marked 'portal-exact' replicate the live HotelFriend portal values 1:1 via getComputedStyle() extraction. Entries marked 'adapted' have been modernized — see UI_DESIGN.md §5.1 Component Primitives for decision rationale."
|
|
11
|
+
},
|
|
12
|
+
"components": {
|
|
13
|
+
"btn-primary": {
|
|
14
|
+
"description": "Primary action button — .btn.btn-primary",
|
|
15
|
+
"states": {
|
|
16
|
+
"default": { "background": "#24AFE8", "color": "#fff", "height": "40px", "border": "none", "border-radius": "6px", "font-size": "15px", "font-weight": "600", "transition": "200ms ease-in-out", "cursor": "pointer" },
|
|
17
|
+
":hover": { "background": "#149AD1" },
|
|
18
|
+
":focus": { "outline": "none" },
|
|
19
|
+
":focus-visible": { "background": "#149AD1", "box-shadow": "0 0 0 3px rgba(36,175,232,0.35)" },
|
|
20
|
+
":active": { "background": "#0F87B8" },
|
|
21
|
+
":disabled": { "opacity": "0.6", "cursor": "not-allowed", "pointer-events": "none" },
|
|
22
|
+
"[data-loading]": { "opacity": "0.7", "cursor": "wait" }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"btn-hf-outline-primary": {
|
|
26
|
+
"description": "Outline / secondary action — .btn.btn-hf-outline-primary",
|
|
27
|
+
"states": {
|
|
28
|
+
"default": { "background": "#fff", "color": "#24AFE8", "border": "1px solid #24AFE8", "height": "40px", "border-radius": "6px", "font-size": "15px", "font-weight": "600", "transition": "200ms ease-in-out" },
|
|
29
|
+
":hover": { "background": "#F1F3F6" },
|
|
30
|
+
":focus": { "outline": "none" },
|
|
31
|
+
":focus-visible": { "box-shadow": "0 0 0 3px rgba(36,175,232,0.35)" },
|
|
32
|
+
":disabled": { "opacity": "0.6", "cursor": "not-allowed" }
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"btn-cancel": {
|
|
36
|
+
"description": "Cancel / neutral — .btn.btn-cancel",
|
|
37
|
+
"states": {
|
|
38
|
+
"default": { "background": "#fff", "color": "#4B5675", "border": "1px solid #D1D6DD", "height": "40px", "border-radius": "6px", "font-size": "15px", "font-weight": "600", "transition": "200ms ease-in-out" },
|
|
39
|
+
":hover": { "background": "rgba(190,198,212,0.1)", "color": "#EA6565", "border-color": "#EA6565" },
|
|
40
|
+
":focus": { "outline": "none" },
|
|
41
|
+
":focus-visible": { "box-shadow": "0 0 0 3px rgba(234,101,101,0.2)" },
|
|
42
|
+
":disabled": { "opacity": "0.6", "cursor": "not-allowed" }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"btn-delete": {
|
|
46
|
+
"description": "Destructive action — .btn.btn-delete",
|
|
47
|
+
"states": {
|
|
48
|
+
"default": { "background": "#fff", "color": "#EA6565", "border": "1px solid #EA6565", "height": "40px", "border-radius": "6px", "font-size": "15px", "font-weight": "600", "transition": "200ms ease-in-out" },
|
|
49
|
+
":hover": { "background": "#EA6565", "color": "#fff" },
|
|
50
|
+
":focus": { "outline": "none" },
|
|
51
|
+
":focus-visible": { "background": "#EA6565", "color": "#fff", "box-shadow": "0 0 0 3px rgba(234,101,101,0.35)" },
|
|
52
|
+
":disabled": { "opacity": "0.6", "cursor": "not-allowed" }
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"btn-reset-filters": {
|
|
56
|
+
"description": "Reset filters / inline clear — .btn-reset-filters",
|
|
57
|
+
"states": {
|
|
58
|
+
"default": { "background": "#EFF6FF", "color": "#24AFE8", "border": "1px solid rgba(27,132,255,0.2)", "height": "38px", "border-radius": "6px", "font-size": "14px", "font-weight": "500", "transition": "200ms ease-in-out" },
|
|
59
|
+
":hover": { "background": "#E9F6FC" },
|
|
60
|
+
":disabled": { "opacity": "0.6", "cursor": "not-allowed" }
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"form-control": {
|
|
64
|
+
"description": "Text input / textarea — .form-control",
|
|
65
|
+
"states": {
|
|
66
|
+
"default": { "height": "39px", "border": "1px solid #D1D6DD", "border-radius": "6px", "font-size": "14px", "color": "#2B2B2B", "background": "#fff", "transition": "200ms ease-in-out" },
|
|
67
|
+
":hover": { "border-color": "#B2BAC4", "box-shadow": "0 2px 3px rgba(0,0,0,0.06)" },
|
|
68
|
+
":focus": { "border-color": "#24AFE8", "outline": "none", "box-shadow": "0 0 0 2px rgba(36,175,232,0.2)" },
|
|
69
|
+
":disabled": { "background": "#F1F3F6", "opacity": "0.75", "cursor": "not-allowed" },
|
|
70
|
+
"::placeholder": { "color": "#99A1B7" }
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"select2": {
|
|
74
|
+
"description": "Dropdown select — .select2-selection",
|
|
75
|
+
"states": {
|
|
76
|
+
"default": { "height": "39px", "border": "1px solid #D1D6DD", "border-radius": "6px", "transition": "200ms ease-in-out" },
|
|
77
|
+
".select2-container--open .select2-selection": { "border-color": "#24AFE8" },
|
|
78
|
+
":focus": { "border-color": "#24AFE8", "outline": "none", "box-shadow": "0 0 0 2px rgba(36,175,232,0.2)" }
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"hf-pill": {
|
|
82
|
+
"description": "Status badge — .hf-pill .status-{domain}-{state}. Color via --color-badge-{domain}-{state}-color, bg via --color-badge-{domain}-{state}-bg (15% alpha).",
|
|
83
|
+
"states": {
|
|
84
|
+
"default": { "border-radius": "99px", "font-size": "12px", "font-weight": "500", "padding": "3px 12px", "display": "inline-flex", "align-items": "center", "white-space": "nowrap" }
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"hf-tab": {
|
|
88
|
+
"description": "Large underline tab — .hf-tab (page nav). Portal-exact: padding 22px 15px, font 16px/500, ink-steel #50627E inactive, primary #24AFE8 active with 3px bottom border.",
|
|
89
|
+
"states": {
|
|
90
|
+
"default": { "padding": "22px 15px", "font-size": "16px", "font-weight": "500", "color": "#50627E", "border-bottom": "0", "background": "transparent", "transition": "color 200ms", "cursor": "pointer" },
|
|
91
|
+
":hover": { "color": "#2B2B2B" },
|
|
92
|
+
".is-active": { "color": "#24AFE8", "::after": { "content": "''", "position": "absolute", "left": "0", "right": "0", "bottom": "-1px", "height": "3px", "background": "#24AFE8" } },
|
|
93
|
+
":disabled": { "color": "#AEBCCF", "cursor": "not-allowed" },
|
|
94
|
+
":focus-visible": { "outline": "2px solid #24AFE8", "outline-offset": "2px" }
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"hf-tab--sm": {
|
|
98
|
+
"description": "Small sub-filter tab (Today/All style) — .hf-tab.hf-tab--sm. Same as .hf-tab but 10px/15px padding and 15px font.",
|
|
99
|
+
"states": {
|
|
100
|
+
"default": { "padding": "10px 15px", "font-size": "15px", "font-weight": "500", "color": "#50627E" },
|
|
101
|
+
":hover": { "color": "#2B2B2B" },
|
|
102
|
+
".is-active": { "color": "#24AFE8", "border-bottom-accent": "3px solid #24AFE8" },
|
|
103
|
+
":disabled": { "color": "#AEBCCF", "cursor": "not-allowed" }
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"hf-pill-tab": {
|
|
107
|
+
"description": "Pill tab inside .hf-pill-tabs container — segmented control (e.g. Day/Week/Month).",
|
|
108
|
+
"states": {
|
|
109
|
+
"default": { "padding": "8px 14px", "font-size": "14px", "font-weight": "500", "color": "#50627E", "background": "transparent", "border-radius": "6px", "transition": "background 200ms, color 200ms" },
|
|
110
|
+
":hover": { "color": "#2B2B2B" },
|
|
111
|
+
".is-active": { "background": "#fff", "color": "#24AFE8", "box-shadow": "0 1px 2px rgba(0,0,0,0.05)" }
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"hf-dropdown-item": {
|
|
115
|
+
"description": "Item inside .hf-dropdown-menu — portal-exact: 14px text, padding 8px 16px, ink color #2B2B2B, hover bg #F5F5F5 (NOT muted #F1F3F6).",
|
|
116
|
+
"states": {
|
|
117
|
+
"default": { "font-size": "14px", "color": "#2B2B2B", "padding": "8px 16px", "gap": "10px", "background": "transparent", "transition": "background 150ms" },
|
|
118
|
+
":hover": { "background": "#F5F5F5" },
|
|
119
|
+
".is-active": { "background": "#EFF6FF", "color": "#24AFE8" },
|
|
120
|
+
":focus-visible": { "background": "#F5F5F5", "outline": "2px solid #24AFE8", "outline-offset": "-2px" },
|
|
121
|
+
".danger": { "color": "#EA6565" },
|
|
122
|
+
".danger:hover": { "background": "rgba(234,101,101,0.08)" },
|
|
123
|
+
".is-disabled": { "color": "#AEBCCF", "cursor": "not-allowed", "pointer-events": "none" }
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"hf-check": {
|
|
127
|
+
"description": "Custom-styled checkbox — .hf-check. Portal-exact .mt-checkbox-outline: 18×18, 6px radius, 1px #DBDFE9 border, transparent bg, filled #26ADE4 + white tick on check.",
|
|
128
|
+
"states": {
|
|
129
|
+
"default": { "appearance": "none", "width": "18px", "height": "18px", "border-radius": "6px", "border": "1px solid #DBDFE9", "background": "transparent", "transition": "border-color 200ms, background-color 200ms", "cursor": "pointer" },
|
|
130
|
+
":hover:not(:disabled)": { "border-color": "#26ADE4" },
|
|
131
|
+
":checked": { "background": "#26ADE4", "border-color": "#26ADE4", "::after": { "content": "''", "position": "absolute", "top": "2px", "left": "5px", "width": "5px", "height": "10px", "border": "solid #fff", "border-width": "0 2px 2px 0", "transform": "rotate(45deg)" } },
|
|
132
|
+
":focus-visible": { "outline": "2px solid #24AFE8", "outline-offset": "2px" },
|
|
133
|
+
":disabled": { "opacity": "0.5", "cursor": "not-allowed" },
|
|
134
|
+
":checked:disabled": { "background": "rgba(38,173,228,0.5)", "border-color": "rgba(38,173,228,0.5)", "opacity": "1" }
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"hf-radio": {
|
|
138
|
+
"description": "Custom-styled radio — .hf-radio. Same anatomy as .hf-check but border-radius 9999px (circle). After: 6×6 white dot centered.",
|
|
139
|
+
"states": {
|
|
140
|
+
"default": { "appearance": "none", "width": "18px", "height": "18px", "border-radius": "9999px", "border": "1px solid #DBDFE9", "background": "transparent", "cursor": "pointer" },
|
|
141
|
+
":hover:not(:disabled)": { "border-color": "#26ADE4" },
|
|
142
|
+
":checked": { "background": "#26ADE4", "border-color": "#26ADE4", "::after": { "content": "''", "position": "absolute", "top": "50%", "left": "50%", "width": "6px", "height": "6px", "background": "#fff", "border-radius": "9999px", "transform": "translate(-50%, -50%)" } },
|
|
143
|
+
":focus-visible": { "outline": "2px solid #24AFE8", "outline-offset": "2px" },
|
|
144
|
+
":disabled": { "opacity": "0.5", "cursor": "not-allowed" },
|
|
145
|
+
":checked:disabled": { "background": "rgba(38,173,228,0.5)", "border-color": "rgba(38,173,228,0.5)", "opacity": "1" }
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
"toggle-switch": {
|
|
149
|
+
"description": "Boolean toggle — replicates portal .bootstrap-switch: 52×28 track, off #DFDFDF / on #24AFE8, thumb 20×20 white with shadow 0 1px 6px rgba(0,0,0,.3), slides 24px on check.",
|
|
150
|
+
"states": {
|
|
151
|
+
"default": { "width": "52px", "height": "28px", "background": "#DFDFDF", "border-radius": "9999px", "transition": "background 200ms" },
|
|
152
|
+
".on": { "background": "#24AFE8" },
|
|
153
|
+
"thumb": { "width": "20px", "height": "20px", "background": "#fff", "border-radius": "9999px", "box-shadow": "0 1px 6px rgba(0,0,0,0.3)", "transition": "transform 200ms" },
|
|
154
|
+
"thumb.on": { "transform": "translateX(24px)" },
|
|
155
|
+
":focus-visible": { "box-shadow": "0 0 0 2px rgba(36,175,232,0.35)" },
|
|
156
|
+
":disabled": { "opacity": "0.5", "cursor": "not-allowed" }
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"hf-pagination__item": {
|
|
160
|
+
"description": "Page number / pager button — .hf-pagination__item. Portal-exact: 34×34, 8px radius, ink-steel #485B78. Active is SUBTLE GRAY (#E4E8EF bg + #252F4A text), NOT primary blue — easy to get wrong!",
|
|
161
|
+
"states": {
|
|
162
|
+
"default": { "min-width": "34px", "height": "34px", "padding": "0 12px", "font-size": "14px", "font-weight": "400", "color": "#485B78", "background": "transparent", "border": "1px solid transparent", "border-radius": "8px", "transition": "background 200ms, color 200ms, border-color 200ms" },
|
|
163
|
+
":hover": { "background": "#F1F3F6", "color": "#2B2B2B" },
|
|
164
|
+
".is-active": { "background": "#E4E8EF", "border-color": "#E4E8EF", "color": "#252F4A", "cursor": "default" },
|
|
165
|
+
":disabled": { "color": "#AEBCCF", "cursor": "not-allowed", "pointer-events": "none" },
|
|
166
|
+
":focus-visible": { "outline": "2px solid #24AFE8", "outline-offset": "2px" }
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"hf-modal": {
|
|
170
|
+
"description": "Modal dialog container — .hf-modal. Portal-exact: 6px radius, 1px rgba(72,91,120,.15) border, 0 2px 4px 0 rgba(72,91,120,.18) shadow. Header has bottom border, footer does NOT.",
|
|
171
|
+
"states": {
|
|
172
|
+
"default": { "background": "#fff", "border": "1px solid rgba(72,91,120,0.15)", "border-radius": "6px", "box-shadow": "0 2px 4px 0 rgba(72,91,120,0.18)", "overflow": "hidden" },
|
|
173
|
+
"__header": { "padding": "20px", "border-bottom": "1px solid #E4E8EF", "display": "flex", "justify-content": "space-between" },
|
|
174
|
+
"__title": { "font-size": "18px", "font-weight": "600", "color": "#2B2B2B" },
|
|
175
|
+
"__body": { "padding": "20px" },
|
|
176
|
+
"__footer": { "padding": "20px", "border-top": "0", "display": "flex", "justify-content": "flex-end", "gap": "12px" }
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"hf-modal__close": {
|
|
180
|
+
"description": "Close (×) button in modal header.",
|
|
181
|
+
"states": {
|
|
182
|
+
"default": { "width": "32px", "height": "32px", "background": "transparent", "border": "0", "border-radius": "6px", "color": "#50627E", "cursor": "pointer", "transition": "background 200ms, color 200ms" },
|
|
183
|
+
":hover": { "background": "#F1F3F6", "color": "#2B2B2B" },
|
|
184
|
+
":focus-visible": { "outline": "2px solid #24AFE8", "outline-offset": "2px" }
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
"hf-alert": {
|
|
188
|
+
"description": "In-page alert — .hf-alert. Portal-exact: white bg, 1px rgba(72,91,120,.15) border, 0 2px 4px rgba(72,91,120,.07) shadow, 6px radius, ::before 3px top accent bar, __icon 26×26 squared filled accent.",
|
|
189
|
+
"states": {
|
|
190
|
+
"default": { "background": "#fff", "border": "1px solid rgba(72,91,120,0.15)", "border-radius": "6px", "box-shadow": "0 2px 4px 0 rgba(72,91,120,0.07)", "padding": "16px 48px 14px 20px", "position": "relative" },
|
|
191
|
+
"::before": { "content": "''", "position": "absolute", "left": "0", "right": "0", "top": "0", "height": "3px", "background": "currentColor" },
|
|
192
|
+
"__icon": { "width": "26px", "height": "26px", "border-radius": "5px", "background": "currentColor" },
|
|
193
|
+
"__icon > svg": { "width": "16px", "height": "16px", "color": "#fff", "stroke": "#fff", "fill": "none" },
|
|
194
|
+
"__title": { "font-size": "18px", "font-weight": "500", "color": "#2B2B2B", "line-height": "1.3" },
|
|
195
|
+
"__text": { "font-size": "15px", "color": "#50627E", "line-height": "1.4" },
|
|
196
|
+
"__close": { "position": "absolute", "top": "12px", "right": "12px", "width": "24px", "height": "24px", "color": "#99A1B7" },
|
|
197
|
+
"__close:hover": { "color": "#2B2B2B", "background": "#F1F3F6" },
|
|
198
|
+
"--success": { "color": "#59B59D" },
|
|
199
|
+
"--info": { "color": "#24AFE8" },
|
|
200
|
+
"--warn": { "color": "#FFBD5A" },
|
|
201
|
+
"--error": { "color": "#EA6565" }
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
"hf-toast": {
|
|
205
|
+
"description": "Floating notification — .hf-toast. 9px radius, modal-shadow, single-line.",
|
|
206
|
+
"states": {
|
|
207
|
+
"default": { "padding": "10px 14px", "background": "#fff", "border": "1px solid rgba(72,91,120,0.15)", "border-radius": "9px", "box-shadow": "0 6px 18px rgba(0,0,0,0.10)", "min-width": "280px", "max-width": "420px", "color": "#2B2B2B", "font-size": "14px" },
|
|
208
|
+
"__icon": { "width": "20px", "height": "20px" },
|
|
209
|
+
"__close": { "width": "20px", "height": "20px", "color": "#99A1B7", "transition": "color 200ms" },
|
|
210
|
+
"__close:hover": { "color": "#2B2B2B" }
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"hf-card-bordered": {
|
|
214
|
+
"description": "Bordered card (portal .portlet.light) — most common card variant. 12px radius, 1px #D1D6DD border, NO shadow.",
|
|
215
|
+
"states": {
|
|
216
|
+
"default": { "background": "#fff", "border": "1px solid #D1D6DD", "border-radius": "12px", "padding": "20px", "box-shadow": "none" }
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"hf-card-elevated": {
|
|
220
|
+
"description": "Elevated card (portal .dashboard-card) — KPI tiles. 6px radius, 1px rgba(72,91,120,.05) faint border, --shadow-subtle.",
|
|
221
|
+
"states": {
|
|
222
|
+
"default": { "background": "#fff", "border": "1px solid rgba(72,91,120,0.05)", "border-radius": "6px", "padding": "16px", "box-shadow": "0 2px 4px 0 rgba(0,0,0,0.05)" },
|
|
223
|
+
":hover": { "box-shadow": "0 2px 3px rgba(0,0,0,0.06)" }
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"tooltip": {
|
|
227
|
+
"description": "Tooltip / popover bubble",
|
|
228
|
+
"states": {
|
|
229
|
+
"default": { "background": "#2B2B2B", "color": "#fff", "border-radius": "6px", "font-size": "13px", "padding": "6px 10px", "box-shadow": "0 6px 18px rgba(0,0,0,0.1)" }
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
"global-focus-visible": {
|
|
233
|
+
"description": "Universal keyboard-focus rule. Apply to ALL focusable elements not covered above.",
|
|
234
|
+
"states": {
|
|
235
|
+
":focus": { "outline": "none" },
|
|
236
|
+
":focus-visible": { "outline": "2px solid #24AFE8", "outline-offset": "2px" }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|