@hotelfriendag/design-tokens 0.3.2 → 0.3.3

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/README.md CHANGED
@@ -22,7 +22,9 @@ portable-design/
22
22
  │ ├── _tokens.scss · SCSS variables
23
23
  │ ├── tokens.ts · TypeScript const
24
24
  │ ├── shadcn-tokens.css · shadcn/ui contract
25
- │ └── components.css · .hf-* component primitives (extracted from components.html)
25
+ │ └── components.css · .hf-* component primitives (generated from src/components.css)
26
+
27
+ ├── src/ ← hand-edited sources for the generator (currently: components.css only)
26
28
 
27
29
  ├── states-canonical.json ← curated interactive states for new components (use this)
28
30
  ├── states.json ← raw portal extraction (160 KB — prefer states-canonical.json)
@@ -239,7 +241,7 @@ node generate-tokens.cjs --target=ts > pre-built/tokens.ts
239
241
  node generate-tokens.cjs --target=shadcn > pre-built/shadcn-tokens.css
240
242
  ```
241
243
 
242
- > `pre-built/components.css` is **not generated** it's hand-extracted from `components.html`. If you change the `<style type="text/tailwindcss">` `@layer components` block in components.html, manually re-sync the extracted file. The `pre-built/components.css` file header has the source coordinates.
244
+ > `pre-built/components.css` is **generated** from `src/components.css` by `generate-tokens.cjs --target=components-css` (`npm run build`). Edit the source file under `src/`, never the output under `pre-built/`. CI runs `git diff --exit-code -- pre-built/` so any hand-edit to the output is caught as drift.
243
245
 
244
246
  Or wire token regeneration into `package.json`:
245
247
 
@@ -461,7 +463,7 @@ The embedded `@theme {}` block now mirrors `pre-built/tailwind.css` (canonical h
461
463
 
462
464
  **What was tracked as Phase 2 TODO but stays open for Phase 3 / future:**
463
465
 
464
- - Fully *generate* `pre-built/components.css` from sources (currently still hand-authored, but verified by `validate-tokens.cjs`)
466
+ - ✅ ~~Fully *generate* `pre-built/components.css` from sources~~ — done 2026-05-27 (ROADMAP #4): source moved to `src/components.css`, `--target=components-css` added to the generator, drift gate covers regeneration.
465
467
  - Rewrite `components.html` demo HTML to use the new prefixed class refs (`bg-hf-accent` everywhere) and drop legacy aliases entirely
466
468
  - Add ESLint/Stylelint plugin (RFC-0001 §4.6 forbid raw hex)
467
469
  - Publish as `@hotelfriendag/design-tokens` npm package (RFC-0001 §4.4)
@@ -566,7 +568,7 @@ Addresses critique from `RFC-0001-cross-project-design-system.md` §2 + §8 —
566
568
  - Token namespace prefix (`--hf-*`) + `--target=tailwind-v4-additive` (RFC §4.2)
567
569
  - Three-tier token architecture: primitive → semantic → component (RFC §4.1)
568
570
  - `text-text-primary` awkward utility — fix when semantic tier introduces `--hf-color-fg` (RFC §8.3)
569
- - `pre-built/components.css` is hand-mirrored from `components.html` rather than generated (RFC §5b)
571
+ - ✅ ~~`pre-built/components.css` is hand-mirrored from `components.html` rather than generated (RFC §5b)~~ — resolved 2026-05-27 (ROADMAP #4): now generated from `src/components.css` by `--target=components-css`.
570
572
  - Two competing visual SSOTs (`components.html` vs `UI_DESIGN.md`) — Phase 2 will pick one
571
573
 
572
574
  ---
@@ -15,6 +15,8 @@
15
15
  * --target=js → module.exports = { tokens: {...} } (CommonJS module — Node consumers)
16
16
  * --target=dts → export declare const tokens: {...} (TypeScript declarations)
17
17
  * --target=shadcn → :root { --primary: ...; --background: ... } (shadcn/ui CSS contract — unprefixed by contract)
18
+ * --target=status-css → .status-{domain}-{state} { color: var(...) } (read from status-map.json)
19
+ * --target=components-css → .hf-* component primitives (read from src/components.css)
18
20
  *
19
21
  * Usage (from this folder, or anywhere — paths are relative to the script):
20
22
  * node generate-tokens.cjs --target=css > pre-built/tokens.css
@@ -26,6 +28,8 @@
26
28
  * node generate-tokens.cjs --target=js > pre-built/tokens.js
27
29
  * node generate-tokens.cjs --target=dts > pre-built/tokens.d.ts
28
30
  * node generate-tokens.cjs --target=shadcn > pre-built/shadcn-tokens.css
31
+ * node generate-tokens.cjs --target=status-css > pre-built/status.css
32
+ * node generate-tokens.cjs --target=components-css > pre-built/components.css
29
33
  *
30
34
  * Pass --in=path/to/tokens.figma.json to override the input file.
31
35
  *
@@ -638,6 +642,22 @@ function emitStatusCss() {
638
642
  return lines.join('\n');
639
643
  }
640
644
 
645
+ // ── components-css target ────────────────────────────────────────────────────
646
+ // Reads `src/components.css` and emits it verbatim to pre-built/components.css.
647
+ // The .hf-* rules have heterogeneous shapes (pseudo-elements, keyframes, BEM
648
+ // modifiers, hardcoded geometry) so a data-driven JSON model would just be
649
+ // CSS-in-JSON — net negative ergonomics. The split here is purely "source vs
650
+ // generated output": you edit src/, the build emits pre-built/, the CI drift
651
+ // gate fails any hand-edit to pre-built/components.css.
652
+ function emitComponentsCss() {
653
+ const srcPath = path.resolve(__dirname, 'src/components.css');
654
+ if (!fs.existsSync(srcPath)) {
655
+ throw new Error(`src/components.css not found at ${srcPath}. Required for --target=components-css.`);
656
+ }
657
+ // Strip trailing whitespace so dispatch's appended '\n' yields exactly one trailing newline.
658
+ return fs.readFileSync(srcPath, 'utf8').replace(/\s*$/, '');
659
+ }
660
+
641
661
  // ── Dispatch ─────────────────────────────────────────────────────────────────
642
662
  //
643
663
  // `tailwind-v4-additive` is an explicit alias for `tailwind-v4`:
@@ -660,6 +680,7 @@ const out =
660
680
  target === 'tailwind-v4-additive' ? emitTailwind4() : // alias — see comment above
661
681
  target === 'shadcn' ? emitShadcn() :
662
682
  target === 'status-css' ? emitStatusCss() :
663
- (() => { throw new Error(`Unknown target: ${target}. Use css | scss | ts | js | dts | tailwind | tailwind-v4 | tailwind-v4-additive | status-css | shadcn`); })();
683
+ target === 'components-css' ? emitComponentsCss() :
684
+ (() => { throw new Error(`Unknown target: ${target}. Use css | scss | ts | js | dts | tailwind | tailwind-v4 | tailwind-v4-additive | status-css | components-css | shadcn`); })();
664
685
 
665
686
  process.stdout.write(out + '\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotelfriendag/design-tokens",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "HotelFriend Design System — portable bundle (tokens, components, AI rules). Three-tier model per RFC-0002.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -41,6 +41,7 @@
41
41
  "pre-built/",
42
42
  "ai-rules/",
43
43
  "scripts/",
44
+ "src/",
44
45
  "tokens.figma.json",
45
46
  "status-map.json",
46
47
  "states-canonical.json",
@@ -65,7 +66,9 @@
65
66
  "build:tokens:js": "node generate-tokens.cjs --target=js > pre-built/tokens.js",
66
67
  "build:tokens:dts": "node generate-tokens.cjs --target=dts > pre-built/tokens.d.ts",
67
68
  "build:tokens:shadcn": "node generate-tokens.cjs --target=shadcn > pre-built/shadcn-tokens.css",
68
- "build:components": "node generate-tokens.cjs --target=status-css > pre-built/status.css",
69
+ "build:components": "npm-run-all build:components:*",
70
+ "build:components:status": "node generate-tokens.cjs --target=status-css > pre-built/status.css",
71
+ "build:components:css": "node generate-tokens.cjs --target=components-css > pre-built/components.css",
69
72
  "validate": "node scripts/validate-tokens.cjs",
70
73
  "smoke": "bash scripts/integration-smoke.sh",
71
74
  "prepublishOnly": "npm-run-all build validate"
@@ -1,6 +1,10 @@
1
1
  /*
2
2
  * HotelFriend Design System — Component classes (.hf-*)
3
3
  *
4
+ * ➜ THIS IS THE SOURCE FILE. `pre-built/components.css` is generated from
5
+ * this file by `generate-tokens.cjs --target=components-css` (`npm run build`).
6
+ * Edit here, then rebuild — do not edit pre-built/.
7
+ *
4
8
  * Drop-in companion to `tokens.css` / `tailwind.css`. Defines the `.hf-*`
5
9
  * component primitives used throughout `components.html`. Framework-agnostic
6
10
  * — works with Tailwind v4, plain CSS, SCSS, Vue, React.
@@ -13,7 +17,9 @@
13
17
  * ✅ Class names equal token names — domain→semantic mapping NOW LIVES in `status-map.json`
14
18
  * and is emitted to `pre-built/status.css` by `generate-tokens.cjs --target=status-css`.
15
19
  * This file no longer contains the mapping — consumers should `@import` both files.
16
- * `pre-built/components.css` is still hand-mirrored from components.html (Phase 2D will generate).
20
+ * `pre-built/components.css` is now GENERATED from this file by
21
+ * `generate-tokens.cjs --target=components-css`. CI drift gate prevents
22
+ * hand-edits to the output.
17
23
  *
18
24
  * Phase 2 TODOs (tracked here):
19
25
  * 1. ✅ Phase 2A done — status-map.json + emitStatusCss(). See `pre-built/status.css`.
@@ -22,7 +28,11 @@
22
28
  * --color-hf-pagination-fg-inactive / -fg-active
23
29
  * --color-hf-input-border (#DBDFE9 — was bare hex)
24
30
  * --color-hf-menu-bg-hover (#F5F5F5 — was bare hex)
25
- * 3. Phase 2C — generate this whole file from tokens.figma.json + status-map.json (TODO).
31
+ * 3. Phase 2C done emitted by `generate-tokens.cjs --target=components-css`
32
+ * from `src/components.css` (this file). Going further to a fully data-driven
33
+ * model would not pay off: the .hf-* rule shapes are heterogeneous (pseudo-
34
+ * elements, keyframes, BEM modifiers, hardcoded geometry) so a CSS-in-JSON
35
+ * DSL would just reformat CSS with worse ergonomics.
26
36
  * 4. Phase 2D — reconcile components.html @theme block with Phase 1B token names (TODO).
27
37
  *
28
38
  * BREAKING CHANGES vs Phase 1A:
@@ -0,0 +1,525 @@
1
+ /*
2
+ * HotelFriend Design System — Component classes (.hf-*)
3
+ *
4
+ * ➜ THIS IS THE SOURCE FILE. `pre-built/components.css` is generated from
5
+ * this file by `generate-tokens.cjs --target=components-css` (`npm run build`).
6
+ * Edit here, then rebuild — do not edit pre-built/.
7
+ *
8
+ * Drop-in companion to `tokens.css` / `tailwind.css`. Defines the `.hf-*`
9
+ * component primitives used throughout `components.html`. Framework-agnostic
10
+ * — works with Tailwind v4, plain CSS, SCSS, Vue, React.
11
+ *
12
+ * Phase 1B (RFC-0002) status:
13
+ * ✅ All chrome bound to SEMANTIC tokens (var(--color-hf-fg), -bg-*, -border, -accent, -status-*).
14
+ * ✅ Primitives never referenced directly (except Phase-2 escape hatches — see TODOs below).
15
+ * ✅ One namespace — no forked --status-* declarations; all status colors come from
16
+ * `--color-hf-status-{role}` (semantic) which alias `--color-hf-{family}-{step}` (primitive).
17
+ * ✅ Class names equal token names — domain→semantic mapping NOW LIVES in `status-map.json`
18
+ * and is emitted to `pre-built/status.css` by `generate-tokens.cjs --target=status-css`.
19
+ * This file no longer contains the mapping — consumers should `@import` both files.
20
+ * ✅ `pre-built/components.css` is now GENERATED from this file by
21
+ * `generate-tokens.cjs --target=components-css`. CI drift gate prevents
22
+ * hand-edits to the output.
23
+ *
24
+ * Phase 2 TODOs (tracked here):
25
+ * 1. ✅ Phase 2A done — status-map.json + emitStatusCss(). See `pre-built/status.css`.
26
+ * 2. ✅ Phase 2B done — 4 component-tier tokens introduced:
27
+ * --color-hf-tab-fg-inactive / -count-fg
28
+ * --color-hf-pagination-fg-inactive / -fg-active
29
+ * --color-hf-input-border (#DBDFE9 — was bare hex)
30
+ * --color-hf-menu-bg-hover (#F5F5F5 — was bare hex)
31
+ * 3. ✅ Phase 2C done — emitted by `generate-tokens.cjs --target=components-css`
32
+ * from `src/components.css` (this file). Going further to a fully data-driven
33
+ * model would not pay off: the .hf-* rule shapes are heterogeneous (pseudo-
34
+ * elements, keyframes, BEM modifiers, hardcoded geometry) so a CSS-in-JSON
35
+ * DSL would just reformat CSS with worse ergonomics.
36
+ * 4. Phase 2D — reconcile components.html @theme block with Phase 1B token names (TODO).
37
+ *
38
+ * BREAKING CHANGES vs Phase 1A:
39
+ * - `--color-hf-primary*` → `--color-hf-accent[ -hover / -subtle / -subtler ]`
40
+ * - `--color-hf-text-*` → `--color-hf-fg[ -muted / -subtle / -faint ]`
41
+ * - `--color-hf-neutral-*` → `--color-hf-bg-*` / `--color-hf-border[ -subtle ]`
42
+ * - `--color-hf-badge-*` → removed; replaced by domain→semantic CSS map below
43
+ *
44
+ * Usage:
45
+ * <link rel="stylesheet" href="tokens.css"> ← variables first
46
+ * <link rel="stylesheet" href="components.css"> ← components after
47
+ * <link rel="stylesheet" href="status.css"> ← .status-{domain}-{state} rules (Phase 2A — generated from status-map.json)
48
+ *
49
+ * Or with Tailwind v4:
50
+ * @import "tailwindcss";
51
+ * @import "./tailwind.css";
52
+ * @import "./components.css";
53
+ * @import "./status.css";
54
+ *
55
+ * STATUS PILLS — the domain→semantic mapping lives in `status-map.json` and is
56
+ * emitted to `pre-built/status.css` by `generate-tokens.cjs --target=status-css`.
57
+ * To add a new status (e.g. `booking.partial-refund`), add it to status-map.json
58
+ * and re-run the generator — no CSS edit needed here.
59
+ */
60
+
61
+ /* ════════════════════════════════════════════════════════════════════════════
62
+ * PILL — .hf-pill (uses currentColor inherited from .status-* above)
63
+ * ════════════════════════════════════════════════════════════════════════════ */
64
+ .hf-pill {
65
+ display: inline-flex; align-items: center; justify-content: center;
66
+ padding: 6px 20px;
67
+ border-radius: var(--radius-hf-sm, 6px);
68
+ font-size: var(--font-size-hf-base, 14px);
69
+ font-weight: var(--font-weight-hf-medium, 500);
70
+ line-height: var(--line-height-hf-tight, 1);
71
+ white-space: nowrap;
72
+ border: 1px solid currentColor;
73
+ text-align: center;
74
+ user-select: none;
75
+ transition: opacity 200ms, background-color 200ms;
76
+ }
77
+ .hf-pill:hover { opacity: .9; }
78
+ .hf-pill.is-active {
79
+ background: transparent !important;
80
+ box-shadow: inset 0 0 0 1px currentColor;
81
+ }
82
+ .hf-pill--striped {
83
+ background-image: repeating-linear-gradient(
84
+ -45deg, currentColor 0, currentColor 1px, transparent 1px, transparent 6px
85
+ );
86
+ background-color: var(--color-hf-white, #fff) !important;
87
+ }
88
+ .hf-pill--dd {
89
+ padding-right: 32px;
90
+ cursor: pointer;
91
+ position: relative;
92
+ }
93
+ .hf-pill--dd::after {
94
+ content: '';
95
+ position: absolute; right: 12px; top: 50%;
96
+ width: 8px; height: 5px;
97
+ background-color: currentColor;
98
+ clip-path: polygon(0 0, 100% 0, 50% 100%);
99
+ transform: translateY(-50%);
100
+ }
101
+
102
+ /* ════════════════════════════════════════════════════════════════════════════
103
+ * TABS (.hf-tab) — large underline (page-nav) + small (sub-filter)
104
+ *
105
+ * ⚠ Phase 2 TODO: gray-700 used here for inactive tab color is portal-specific.
106
+ * Promote to component-tier token `--hf-tab-fg-inactive`.
107
+ * ════════════════════════════════════════════════════════════════════════════ */
108
+ .hf-tab {
109
+ padding: 22px 15px;
110
+ font-size: var(--font-size-hf-lg, 16px);
111
+ font-weight: var(--font-weight-hf-medium, 500);
112
+ color: var(--color-hf-tab-fg-inactive, #50627E);
113
+ cursor: pointer;
114
+ transition: color 200ms;
115
+ position: relative;
116
+ white-space: nowrap;
117
+ border: 0; background: 0;
118
+ line-height: var(--line-height-hf-tight, 1);
119
+ display: inline-flex; align-items: center; gap: 8px;
120
+ }
121
+ .hf-tab:hover { color: var(--color-hf-fg, #2B2B2B); }
122
+ .hf-tab.is-active { color: var(--color-hf-accent, #24AFE8); }
123
+ .hf-tab.is-active::after {
124
+ content: ''; position: absolute; left: 0; right: 0; bottom: -1px;
125
+ height: 3px; background: var(--color-hf-accent, #24AFE8);
126
+ }
127
+ .hf-tab:disabled { color: var(--color-hf-fg-faint, #AEBCCF); cursor: not-allowed; }
128
+ .hf-tab:disabled:hover { color: var(--color-hf-fg-faint, #AEBCCF); }
129
+
130
+ .hf-tab--sm {
131
+ padding: 10px 15px;
132
+ font-size: var(--font-size-hf-md, 15px);
133
+ }
134
+
135
+ .hf-tab__count {
136
+ display: inline-flex; align-items: center; justify-content: center;
137
+ min-width: 22px; height: 18px; padding: 0 6px;
138
+ background: rgba(72,91,120,.1);
139
+ color: var(--color-hf-tab-count-fg, #50627E);
140
+ border-radius: 9999px;
141
+ font-size: var(--font-size-hf-xs, 11px);
142
+ font-weight: var(--font-weight-hf-semibold, 600);
143
+ line-height: var(--line-height-hf-tight, 1);
144
+ }
145
+ .hf-tab.is-active .hf-tab__count {
146
+ background: var(--color-hf-accent-subtler, #EFF6FF);
147
+ color: var(--color-hf-accent, #24AFE8);
148
+ }
149
+
150
+ .hf-pill-tabs {
151
+ display: inline-flex; gap: 4px; padding: 4px;
152
+ background: var(--color-hf-bg-section, #F6F7FB);
153
+ border-radius: var(--radius-hf-lg, 9px);
154
+ }
155
+ .hf-pill-tab {
156
+ padding: 8px 14px;
157
+ font-size: var(--font-size-hf-base, 14px);
158
+ font-weight: var(--font-weight-hf-medium, 500);
159
+ color: var(--color-hf-tab-fg-inactive, #50627E);
160
+ background: transparent;
161
+ border: 0;
162
+ border-radius: var(--radius-hf-sm, 6px);
163
+ cursor: pointer;
164
+ transition: background 200ms, color 200ms;
165
+ white-space: nowrap;
166
+ }
167
+ .hf-pill-tab:hover { color: var(--color-hf-fg, #2B2B2B); }
168
+ .hf-pill-tab.is-active {
169
+ background: var(--color-hf-bg-surface, #fff);
170
+ color: var(--color-hf-accent, #24AFE8);
171
+ box-shadow: 0 1px 2px rgba(0,0,0,.05);
172
+ }
173
+
174
+ /* ════════════════════════════════════════════════════════════════════════════
175
+ * PAGINATION (.hf-pagination) — subtle gray active (NOT accent!)
176
+ *
177
+ * ⚠ Phase 2 TODO: gray-950 (#252F4A) used for active text is portal-specific.
178
+ * Promote to component-tier `--hf-pagination-fg-active`.
179
+ * ════════════════════════════════════════════════════════════════════════════ */
180
+ .hf-pagination {
181
+ display: inline-flex; align-items: center; gap: 4px;
182
+ padding: 0; list-style: none;
183
+ }
184
+ .hf-pagination__item {
185
+ display: inline-flex; align-items: center; justify-content: center;
186
+ min-width: 34px; height: 34px;
187
+ padding: 0 12px;
188
+ font-size: var(--font-size-hf-base, 14px);
189
+ font-weight: var(--font-weight-hf-regular, 400);
190
+ color: var(--color-hf-pagination-fg-inactive, #485B78);
191
+ background: transparent;
192
+ border: 1px solid transparent;
193
+ border-radius: var(--radius-hf-md, 8px);
194
+ cursor: pointer;
195
+ transition: background-color 200ms, color 200ms, border-color 200ms;
196
+ text-decoration: none;
197
+ }
198
+ .hf-pagination__item:hover {
199
+ background: var(--color-hf-bg-muted, #F1F3F6);
200
+ color: var(--color-hf-fg, #2B2B2B);
201
+ }
202
+ .hf-pagination__item.is-active {
203
+ background: var(--color-hf-border-subtle, #E4E8EF);
204
+ border-color: var(--color-hf-border-subtle, #E4E8EF);
205
+ color: var(--color-hf-pagination-fg-active, #252F4A);
206
+ cursor: default;
207
+ }
208
+ .hf-pagination__item.is-active:hover { background: var(--color-hf-border-subtle, #E4E8EF); }
209
+ .hf-pagination__item:disabled,
210
+ .hf-pagination__item.is-disabled {
211
+ color: var(--color-hf-fg-faint, #AEBCCF);
212
+ cursor: not-allowed;
213
+ pointer-events: none;
214
+ }
215
+ .hf-pagination__ellipsis {
216
+ display: inline-flex; align-items: center; justify-content: center;
217
+ min-width: 34px; height: 34px;
218
+ color: var(--color-hf-gray-700, #485B78);
219
+ cursor: default;
220
+ }
221
+
222
+ /* ════════════════════════════════════════════════════════════════════════════
223
+ * ALERT (.hf-alert)
224
+ * ════════════════════════════════════════════════════════════════════════════ */
225
+ .hf-alert {
226
+ position: relative;
227
+ display: flex; align-items: flex-start; gap: 16px;
228
+ padding: 16px 48px 14px 20px;
229
+ background: var(--color-hf-bg-surface, #fff);
230
+ border: 1px solid rgba(72,91,120,.15);
231
+ border-radius: var(--radius-hf-sm, 6px);
232
+ box-shadow: 0 2px 4px 0 rgba(72,91,120,.07);
233
+ overflow: hidden;
234
+ }
235
+ .hf-alert::before {
236
+ content: '';
237
+ position: absolute;
238
+ left: 0; right: 0; top: 0;
239
+ height: 3px;
240
+ background: currentColor;
241
+ }
242
+ .hf-alert__icon {
243
+ flex-shrink: 0;
244
+ width: 26px; height: 26px;
245
+ border-radius: 5px;
246
+ display: inline-flex; align-items: center; justify-content: center;
247
+ background: currentColor;
248
+ }
249
+ .hf-alert__icon > svg {
250
+ width: 16px; height: 16px;
251
+ color: var(--color-hf-on-accent, #fff);
252
+ stroke: var(--color-hf-on-accent, #fff);
253
+ fill: none;
254
+ }
255
+ .hf-alert__body { flex: 1; min-width: 0; color: var(--color-hf-fg, #2B2B2B); }
256
+ .hf-alert__title {
257
+ font-size: var(--font-size-hf-xl, 18px);
258
+ font-weight: var(--font-weight-hf-medium, 500);
259
+ line-height: 1.3;
260
+ margin: 0 0 4px;
261
+ color: var(--color-hf-fg, #2B2B2B);
262
+ }
263
+ .hf-alert__text {
264
+ font-size: var(--font-size-hf-md, 15px);
265
+ line-height: 1.4;
266
+ color: var(--color-hf-gray-700, #50627E); /* TODO Phase 2 — review fg axis */
267
+ margin: 0;
268
+ }
269
+ .hf-alert__close {
270
+ position: absolute; top: 12px; right: 12px;
271
+ width: 24px; height: 24px;
272
+ display: inline-flex; align-items: center; justify-content: center;
273
+ background: transparent; border: 0; border-radius: 4px;
274
+ color: var(--color-hf-gray-500, #99A1B7);
275
+ cursor: pointer;
276
+ transition: color 200ms, background 200ms;
277
+ }
278
+ .hf-alert__close:hover {
279
+ color: var(--color-hf-fg, #2B2B2B);
280
+ background: var(--color-hf-bg-muted, #F1F3F6);
281
+ }
282
+
283
+ /* Variants — set accent color via currentColor */
284
+ .hf-alert--success { color: var(--color-hf-status-success); }
285
+ .hf-alert--info { color: var(--color-hf-accent); }
286
+ .hf-alert--warn { color: var(--color-hf-status-warning); }
287
+ .hf-alert--error { color: var(--color-hf-status-error); }
288
+
289
+ /* Tinted modifier */
290
+ .hf-alert--tinted.hf-alert--success { background: rgba(89,181,157,.06); }
291
+ .hf-alert--tinted.hf-alert--info { background: rgba(36,175,232,.06); }
292
+ .hf-alert--tinted.hf-alert--warn { background: rgba(255,189,90,.10); }
293
+ .hf-alert--tinted.hf-alert--error { background: rgba(234,101,101,.06); }
294
+
295
+ /* Page banner */
296
+ .hf-alert--banner {
297
+ border-radius: 0;
298
+ border-left: 0; border-right: 0;
299
+ box-shadow: none;
300
+ background: currentColor;
301
+ }
302
+ .hf-alert--banner::before { display: none; }
303
+ .hf-alert--banner .hf-alert__body { color: var(--color-hf-on-accent, #fff); }
304
+ .hf-alert--banner .hf-alert__title,
305
+ .hf-alert--banner .hf-alert__text { color: var(--color-hf-on-accent, #fff); }
306
+ .hf-alert--banner .hf-alert__icon { background: rgba(255,255,255,.18); }
307
+ .hf-alert--banner .hf-alert__close { color: rgba(255,255,255,.7); }
308
+ .hf-alert--banner .hf-alert__close:hover { color: var(--color-hf-on-accent, #fff); background: rgba(255,255,255,.12); }
309
+
310
+ /* Compact */
311
+ .hf-alert--compact { padding: 10px 36px 10px 14px; gap: 10px; }
312
+ .hf-alert--compact .hf-alert__icon { width: 20px; height: 20px; border-radius: 4px; }
313
+ .hf-alert--compact .hf-alert__icon > svg { width: 12px; height: 12px; }
314
+ .hf-alert--compact .hf-alert__title {
315
+ font-size: var(--font-size-hf-base, 14px);
316
+ font-weight: var(--font-weight-hf-semibold, 600);
317
+ margin-bottom: 0;
318
+ }
319
+ .hf-alert--compact .hf-alert__text { font-size: var(--font-size-hf-sm, 13px); }
320
+ .hf-alert--compact .hf-alert__close { top: 8px; right: 8px; }
321
+
322
+ /* ════════════════════════════════════════════════════════════════════════════
323
+ * TOAST (.hf-toast)
324
+ * ════════════════════════════════════════════════════════════════════════════ */
325
+ .hf-toast {
326
+ display: inline-flex; align-items: center; gap: 10px;
327
+ padding: 10px 14px;
328
+ background: var(--color-hf-bg-surface, #fff);
329
+ border: 1px solid rgba(72,91,120,.15);
330
+ border-radius: var(--radius-hf-lg, 9px);
331
+ box-shadow: var(--shadow-hf-modal, 0 6px 18px rgba(0,0,0,.10));
332
+ min-width: 280px; max-width: 420px;
333
+ color: var(--color-hf-fg, #2B2B2B);
334
+ font-size: var(--font-size-hf-base, 14px);
335
+ }
336
+ .hf-toast__icon { flex-shrink: 0; width: 20px; height: 20px; }
337
+ .hf-toast__text { flex: 1; }
338
+ .hf-toast__close {
339
+ flex-shrink: 0;
340
+ width: 20px; height: 20px;
341
+ display: inline-flex; align-items: center; justify-content: center;
342
+ background: transparent; border: 0; border-radius: 4px;
343
+ color: var(--color-hf-gray-500, #99A1B7);
344
+ cursor: pointer; transition: color 200ms;
345
+ }
346
+ .hf-toast__close:hover { color: var(--color-hf-fg, #2B2B2B); }
347
+
348
+ /* ════════════════════════════════════════════════════════════════════════════
349
+ * MODAL (.hf-modal)
350
+ * ════════════════════════════════════════════════════════════════════════════ */
351
+ .hf-modal {
352
+ background: var(--color-hf-bg-surface, #fff);
353
+ border: 1px solid rgba(72,91,120,.15);
354
+ border-radius: var(--radius-hf-sm, 6px);
355
+ box-shadow: 0 2px 4px 0 rgba(72,91,120,.18);
356
+ overflow: hidden;
357
+ width: 100%;
358
+ }
359
+ .hf-modal__header {
360
+ display: flex; align-items: center; justify-content: space-between;
361
+ padding: 20px;
362
+ border-bottom: 1px solid var(--color-hf-border-subtle, #E4E8EF);
363
+ }
364
+ .hf-modal__title {
365
+ font-size: var(--font-size-hf-xl, 18px);
366
+ font-weight: var(--font-weight-hf-semibold, 600);
367
+ color: var(--color-hf-fg, #2B2B2B);
368
+ margin: 0;
369
+ }
370
+ .hf-modal__close {
371
+ width: 32px; height: 32px;
372
+ display: inline-flex; align-items: center; justify-content: center;
373
+ background: transparent;
374
+ border: 0;
375
+ border-radius: var(--radius-hf-sm, 6px);
376
+ color: var(--color-hf-gray-700, #50627E);
377
+ cursor: pointer;
378
+ transition: background 200ms, color 200ms;
379
+ }
380
+ .hf-modal__close:hover {
381
+ background: var(--color-hf-bg-muted, #F1F3F6);
382
+ color: var(--color-hf-fg, #2B2B2B);
383
+ }
384
+ .hf-modal__body { padding: 20px; }
385
+ .hf-modal__footer {
386
+ display: flex; justify-content: flex-end; gap: 12px;
387
+ padding: 20px;
388
+ border-top: 0;
389
+ background: transparent;
390
+ }
391
+ .hf-modal--with-footer-border .hf-modal__footer {
392
+ border-top: 1px solid var(--color-hf-border-subtle, #E4E8EF);
393
+ }
394
+
395
+ /* ════════════════════════════════════════════════════════════════════════════
396
+ * CHECKBOX & RADIO (.hf-check, .hf-radio)
397
+ * ════════════════════════════════════════════════════════════════════════════ */
398
+ .hf-check,
399
+ .hf-radio {
400
+ appearance: none; -webkit-appearance: none;
401
+ position: relative; flex-shrink: 0;
402
+ width: 18px; height: 18px;
403
+ margin: 0;
404
+ background: transparent;
405
+ border: 1px solid var(--color-hf-input-border, #DBDFE9);
406
+ cursor: pointer;
407
+ transition: border-color 200ms, background-color 200ms;
408
+ }
409
+ .hf-check { border-radius: var(--radius-hf-sm, 6px); }
410
+ .hf-radio { border-radius: 9999px; }
411
+ .hf-check:hover:not(:disabled),
412
+ .hf-radio:hover:not(:disabled) { border-color: var(--color-hf-accent, #26ADE4); }
413
+ .hf-check:checked,
414
+ .hf-radio:checked {
415
+ background: var(--color-hf-accent, #26ADE4);
416
+ border-color: var(--color-hf-accent, #26ADE4);
417
+ }
418
+ .hf-check:checked::after {
419
+ content: '';
420
+ position: absolute; top: 2px; left: 5px;
421
+ width: 5px; height: 10px;
422
+ border: solid var(--color-hf-on-accent, #fff);
423
+ border-width: 0 2px 2px 0;
424
+ transform: rotate(45deg);
425
+ }
426
+ .hf-radio:checked::after {
427
+ content: '';
428
+ position: absolute; top: 50%; left: 50%;
429
+ width: 6px; height: 6px;
430
+ background: var(--color-hf-on-accent, #fff); border-radius: 9999px;
431
+ transform: translate(-50%, -50%);
432
+ }
433
+ .hf-check:disabled,
434
+ .hf-radio:disabled { cursor: not-allowed; opacity: .5; }
435
+ .hf-check:checked:disabled,
436
+ .hf-radio:checked:disabled {
437
+ background: rgba(38,173,228,.5);
438
+ border-color: rgba(38,173,228,.5);
439
+ opacity: 1;
440
+ }
441
+
442
+ /* ════════════════════════════════════════════════════════════════════════════
443
+ * DROPDOWN (.hf-dropdown-menu)
444
+ * ════════════════════════════════════════════════════════════════════════════ */
445
+ .hf-dropdown-menu {
446
+ background: var(--color-hf-bg-surface, #fff);
447
+ border-radius: var(--radius-hf-lg, 9px);
448
+ box-shadow: 0 1px 10px 0 rgba(0,0,0,.1);
449
+ min-width: 180px;
450
+ padding: 6px 0;
451
+ border: 1px solid rgba(228,232,239,.6);
452
+ }
453
+ .hf-dropdown-header {
454
+ padding: 10px 16px 8px;
455
+ font-size: var(--font-size-hf-xs, 11px);
456
+ font-weight: var(--font-weight-hf-semibold, 600);
457
+ color: var(--color-hf-gray-500, #99A1B7);
458
+ text-transform: uppercase;
459
+ letter-spacing: .04em;
460
+ }
461
+ .hf-dropdown-item {
462
+ display: flex; align-items: center; gap: 10px;
463
+ padding: 8px 16px;
464
+ font-size: var(--font-size-hf-base, 14px);
465
+ color: var(--color-hf-fg, #2B2B2B);
466
+ cursor: pointer; transition: background 150ms;
467
+ line-height: 1.4;
468
+ }
469
+ .hf-dropdown-item:hover { background: var(--color-hf-menu-bg-hover, #F5F5F5); }
470
+ .hf-dropdown-item.is-active {
471
+ background: var(--color-hf-accent-subtler, #EFF6FF);
472
+ color: var(--color-hf-accent, #24AFE8);
473
+ }
474
+ .hf-dropdown-item:focus-visible {
475
+ background: var(--color-hf-menu-bg-hover, #F5F5F5);
476
+ outline: 2px solid var(--color-hf-accent, #24AFE8);
477
+ outline-offset: -2px;
478
+ }
479
+ .hf-dropdown-item.danger { color: var(--color-hf-status-error, #EA6565); }
480
+ .hf-dropdown-item.danger:hover { background: rgba(234,101,101,.08); }
481
+ .hf-dropdown-item[aria-disabled="true"],
482
+ .hf-dropdown-item.is-disabled {
483
+ color: var(--color-hf-fg-faint, #AEBCCF);
484
+ cursor: not-allowed;
485
+ pointer-events: none;
486
+ }
487
+ .hf-dropdown-item__shortcut {
488
+ margin-left: auto;
489
+ color: var(--color-hf-gray-500, #99A1B7);
490
+ font-size: 12px;
491
+ }
492
+ .hf-dropdown-item__icon {
493
+ color: var(--color-hf-gray-500, #99A1B7);
494
+ width: 16px; height: 16px;
495
+ flex-shrink: 0;
496
+ }
497
+ .hf-dropdown-item:hover .hf-dropdown-item__icon { color: var(--color-hf-gray-700, #50627E); }
498
+ .hf-dropdown-item.danger .hf-dropdown-item__icon { color: currentColor; }
499
+ .hf-dropdown-divider {
500
+ height: 1px;
501
+ background: var(--color-hf-border-subtle, #E4E8EF);
502
+ margin: 4px 0;
503
+ }
504
+
505
+ /* ════════════════════════════════════════════════════════════════════════════
506
+ * SKELETON + SPINNER
507
+ * ════════════════════════════════════════════════════════════════════════════ */
508
+ @keyframes hf-shimmer {
509
+ 0% { background-position: -200% 0; }
510
+ 100% { background-position: 200% 0; }
511
+ }
512
+ .skeleton {
513
+ background: linear-gradient(
514
+ 90deg,
515
+ var(--color-hf-bg-muted, #F1F3F6) 25%,
516
+ var(--color-hf-border-subtle, #E4E8EF) 50%,
517
+ var(--color-hf-bg-muted, #F1F3F6) 75%
518
+ );
519
+ background-size: 200% 100%;
520
+ animation: hf-shimmer 1.4s infinite;
521
+ border-radius: 4px;
522
+ }
523
+
524
+ @keyframes hf-spin { to { transform: rotate(360deg); } }
525
+ .hf-spin { animation: hf-spin .6s linear infinite; }