@hotelfriendag/design-tokens 0.3.0 → 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
 
@@ -367,8 +369,14 @@ import { tokens } from '@hotelfriendag/design-tokens/tokens.ts';
367
369
  ### Publishing (maintainer)
368
370
 
369
371
  Releases are tag-triggered via `.github/workflows/release.yml` — push a `v*` tag
370
- and the workflow builds, validates, then `npm publish`-es to npmjs.com using
371
- the `NPM_TOKEN` repository secret. No manual `npm publish` step required.
372
+ and the workflow builds, validates, then `npm publish`-es to npmjs.com via
373
+ **Trusted Publishing (OIDC)**. No `NPM_TOKEN` secret, no PATs: GitHub Actions
374
+ mints a short-lived OIDC token that npm exchanges for publish credentials.
375
+
376
+ `--provenance` is NOT used: npmjs rejects sigstore provenance from private
377
+ source repositories, and this repo stays private (only the built artifact is
378
+ public — see RFC-0001 for the rationale). The trade is a missing attestation
379
+ badge on the npm page in exchange for keeping the source code private.
372
380
 
373
381
  ```bash
374
382
  # From the repo root, on main:
@@ -376,9 +384,19 @@ npm version patch # or minor / major → updates package.json,
376
384
  git push --follow-tags # workflow runs on the pushed tag and publishes
377
385
  ```
378
386
 
379
- The `prepublishOnly` script re-runs the generator + validator so the published package is always self-consistent. If you need to publish manually from a local machine (bootstrap or hotfix), `pnpm publish` works the same way after `pnpm version <bump>` (you'll need to be logged in: `npm login` first).
387
+ The `prepublishOnly` script re-runs the generator + validator so the published package is always self-consistent. If you need to publish manually from a local machine (hotfix), `npm publish` works the same way after `npm version <bump>` and `npm login` but Trusted Publishing is the canonical path.
388
+
389
+ The Trusted Publisher binding on npmjs (Package → Settings → Trusted publishing) is configured for:
390
+
391
+ | Field | Value |
392
+ |---|---|
393
+ | Organization | `HotelFriendAG` |
394
+ | Repository | `design-system` |
395
+ | Workflow filename | `release.yml` |
396
+
397
+ Changing the workflow filename, repo name, or org requires updating the npmjs binding before the next release will succeed.
380
398
 
381
- > Historical: versions `0.2.x` were briefly published to GitHub Packages while the registry decision was pending. `0.3.0+` is published exclusively to npmjs.com; consumers should install from there.
399
+ > Historical: versions `0.2.x` were briefly published to GitHub Packages while the registry decision was pending. `0.3.0+` is published exclusively to npmjs.com; consumers should install from there. `0.3.0` itself was bootstrapped by a one-time manual publish from a maintainer's machine using a short-lived Granular Access Token (since Trusted Publishing requires the package to already exist on npm). The token was revoked immediately after; all subsequent versions use OIDC.
382
400
 
383
401
  ## Changelog
384
402
 
@@ -445,7 +463,7 @@ The embedded `@theme {}` block now mirrors `pre-built/tailwind.css` (canonical h
445
463
 
446
464
  **What was tracked as Phase 2 TODO but stays open for Phase 3 / future:**
447
465
 
448
- - 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.
449
467
  - Rewrite `components.html` demo HTML to use the new prefixed class refs (`bg-hf-accent` everywhere) and drop legacy aliases entirely
450
468
  - Add ESLint/Stylelint plugin (RFC-0001 §4.6 forbid raw hex)
451
469
  - Publish as `@hotelfriendag/design-tokens` npm package (RFC-0001 §4.4)
@@ -550,7 +568,7 @@ Addresses critique from `RFC-0001-cross-project-design-system.md` §2 + §8 —
550
568
  - Token namespace prefix (`--hf-*`) + `--target=tailwind-v4-additive` (RFC §4.2)
551
569
  - Three-tier token architecture: primitive → semantic → component (RFC §4.1)
552
570
  - `text-text-primary` awkward utility — fix when semantic tier introduces `--hf-color-fg` (RFC §8.3)
553
- - `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`.
554
572
  - Two competing visual SSOTs (`components.html` vs `UI_DESIGN.md`) — Phase 2 will pick one
555
573
 
556
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.0",
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; }
package/README.uk.md DELETED
@@ -1,377 +0,0 @@
1
- # HotelFriend Design System
2
-
3
- Спільна дизайн-основа для всіх проєктів: токени, згенеровані файли під кожен стек (CSS / SCSS / TS / Tailwind v3 + v4 / shadcn), шар компонентів `.hf-*` та правила для AI-інструментів.
4
-
5
- > **Виокремлено 2026-05-25** із [`hotelfriend/backend-hf`](https://bitbucket.org/hotelfriend/backend-hf) (`docs/portable-design/`), щоб цей репозиторій став єдиним джерелом істини для решти проєктів HotelFriend.
6
- >
7
- > **Статус:** RFC-0001 Фаза 1 завершена (семантична трирівнева модель + префіксація без колізій + CI-гейт на дрейф). Наступний крок — версійований npm-пакет (`@hotelfriend/design-tokens` через GitHub Packages). Повний чек-лист — у `RFC-0001-cross-project-design-system.md` §9.
8
-
9
- ## Ієрархія файлів (читати в цьому порядку)
10
-
11
- ```
12
- portable-design/
13
-
14
- ├── components.html ← ОСНОВНЕ · канонічний візуальний референс (відкрити у браузері)
15
- ├── UI_DESIGN.md ← НАРАТИВ · обґрунтування, анатомія, рішення (AI читає це першим)
16
- ├── tokens.figma.json ← ТОКЕНИ · атомарний експорт Tokens Studio → живить Figma + генератори
17
-
18
- ├── pre-built/ ← ЗГЕНЕРОВАНІ · підставляєте у свій білд-пайплайн
19
- │ ├── tailwind.css · Tailwind v4 @theme-блок (рекомендовано)
20
- │ ├── tailwind.preset.js · Tailwind v3 preset (легасі)
21
- │ ├── tokens.css · звичайні CSS custom properties
22
- │ ├── _tokens.scss · SCSS-змінні
23
- │ ├── tokens.ts · TypeScript const
24
- │ ├── shadcn-tokens.css · контракт shadcn/ui
25
- │ └── components.css · примітиви `.hf-*` (витяг із components.html)
26
-
27
- ├── states-canonical.json ← курований набір інтерактивних станів (використовуйте цей)
28
- ├── states.json ← сирий експорт із порталу (160 КБ — краще брати states-canonical.json)
29
- ├── generate-tokens.cjs ← Node-скрипт (без залежностей) — перетворює токени на Tailwind/CSS/SCSS/TS/shadcn
30
-
31
- ├── ai-rules/ ← покладіть ОДИН файл у корінь нового проєкту
32
- │ ├── CLAUDE.md · для Claude Code (підхоплюється автоматично)
33
- │ ├── cursorrules.template · перейменуйте на .cursorrules
34
- │ ├── github-copilot-instructions.md · покласти в .github/copilot-instructions.md
35
- │ └── system-prompt-compact.md · компактний промпт для ChatGPT/v0/Lovable
36
-
37
- ├── portal-audit.html ← АРХІВ · знімок аудиту старого порталу (НЕ для нового коду)
38
- └── README.md ← ви тут (швидкий старт)
39
- ```
40
-
41
- ### Правило пріоритету — коли файли суперечать одне одному
42
-
43
- 1. **`components.html`** — канон для **візуальних рішень** (кольори, розміри, анатомія)
44
- 2. **`tokens.figma.json`** — канон для **значень токенів**. Після редагування — перегенеруйте `pre-built/*`
45
- 3. **`UI_DESIGN.md`** — канон для **ЧОМУ було прийнято рішення** (історія, компроміси, нотатки про дрейф порталу)
46
- 4. **`pre-built/*`** — **згенеровані**, руками не правити. Після зміни JSON запускайте `generate-tokens.cjs`
47
- 5. **`portal-audit.html`** — **тільки архів**. Показує поточний вигляд легасі-порталу — корисно для трекінгу міграції, НЕ для нового UI
48
-
49
- Якщо `components.html` і `UI_DESIGN.md` суперечать одне одному — **виграє `components.html`**, а `UI_DESIGN.md` вважається застарілим.
50
-
51
- ## Швидкий старт за 60 секунд
52
-
53
- ```bash
54
- # 1. Скопіювати цю папку у новий проєкт (куди завгодно; рекомендуємо docs/)
55
- cp -r /path/to/portable-design ../new-project/docs/
56
-
57
- # 2. Підключити CSS у білд (оберіть ОДНЕ)
58
- # Tailwind v4: @import pre-built/tailwind.css
59
- # Vanilla CSS: підключити tokens.css + components.css
60
- # SCSS: @import _tokens.scss
61
-
62
- # 3. Підключити AI до правил системи (оберіть ОДНЕ)
63
- cp docs/portable-design/ai-rules/CLAUDE.md ../../CLAUDE.md # Claude Code
64
- cp docs/portable-design/ai-rules/cursorrules.template ../../.cursorrules
65
- mkdir -p ../../.github && cp docs/portable-design/ai-rules/github-copilot-instructions.md ../../.github/copilot-instructions.md
66
- ```
67
-
68
- ## ⚠️ Інтеграція у наявний проєкт
69
-
70
- > **Статус:** ✅ Вирішено у Фазі 1A (RFC-0001 §4.2). Усі згенеровані токени мають префікс `hf-` ВСЕРЕДИНІ категорії (`--color-hf-*`, `--text-hf-*`, `--radius-hf-*`, `--spacing-hf-*`, `--font-hf-*`, `--shadow-hf-*`). Жоден не може зіткнутися з дефолтами Tailwind v4. Повний `@import` у наявному проєкті — безпечний.
71
-
72
- **Рекомендована конфігурація для будь-якого проєкту (новий чи наявний):**
73
-
74
- ```css
75
- /* app/globals.css */
76
- @import "tailwindcss";
77
- @import "./docs/portable-design/pre-built/tailwind.css"; /* @theme — додає --color-hf-*, --text-hf-* тощо */
78
- @import "./docs/portable-design/pre-built/components.css"; /* примітиви .hf-* */
79
-
80
- /* Опційно: виключити демо-HTML із пакета зі сканування Tailwind,
81
- щоб легасі-класи / bg-[#hex] із showcase не потрапили у ваш бандл. */
82
- @source not "./docs/portable-design/components.html";
83
- @source not "./docs/portable-design/portal-audit.html";
84
- ```
85
-
86
- Що ви отримуєте:
87
-
88
- - `bg-hf-accent` (= `#24AFE8` — бренд) — `bg-blue-500` із Tailwind лишається дефолтним
89
- - `text-hf-base` (= 14px — основний текст) — `text-base` із Tailwind лишається 16px
90
- - `rounded-hf-sm` (= 6px) — `rounded-sm` із Tailwind лишається 2px
91
- - `shadow-hf-modal` (= тінь модалки порталу)
92
- - … тощо. Ваші наявні утиліти **не змінюються**.
93
-
94
- **Для проєктів, чиї інтеграційні скрипти просять "additive"-таргет за назвою**, `--target=tailwind-v4-additive` — явний alias для `--target=tailwind-v4`. Вихідний файл ідентичний — префікс зробив additive-фільтр непотрібним.
95
-
96
- ```bash
97
- node generate-tokens.cjs --target=tailwind-v4-additive > pre-built/tailwind.additive.css
98
- # (байт-у-байт як --target=tailwind-v4)
99
- ```
100
-
101
- ## Сніпети під конкретні стеки
102
-
103
- ### React + Tailwind v4 (рекомендовано, новий проєкт)
104
-
105
- ```css
106
- /* app/globals.css */
107
- @import "tailwindcss";
108
- @import "./docs/portable-design/pre-built/tailwind.css"; /* @theme токени */
109
- @import "./docs/portable-design/pre-built/components.css"; /* примітиви .hf-* */
110
- ```
111
-
112
- ```jsx
113
- <button className="bg-hf-primary hover:bg-hf-primary-hover text-white h-10 px-5 rounded-hf text-hf-md font-semibold">
114
- Зберегти
115
- </button>
116
- <span className="hf-pill status-booking-confirmed">Підтверджено</span>
117
- <div className="hf-modal max-w-[500px]">
118
- <div className="hf-modal__header">
119
- <h2 className="hf-modal__title">Редагувати гостя</h2>
120
- <button className="hf-modal__close">✕</button>
121
- </div>
122
- <div className="hf-modal__body">…</div>
123
- <div className="hf-modal__footer"><button>Скасувати</button><button>Зберегти</button></div>
124
- </div>
125
- ```
126
-
127
- ### React + Tailwind v3 (легасі)
128
-
129
- ```js
130
- // tailwind.config.js
131
- const hfPreset = require('./docs/portable-design/pre-built/tailwind.preset.js');
132
- module.exports = {
133
- presets: [hfPreset],
134
- content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
135
- };
136
- ```
137
-
138
- ```html
139
- <link rel="stylesheet" href="docs/portable-design/pre-built/components.css">
140
- ```
141
-
142
- ### Next.js + shadcn/ui
143
-
144
- Допишіть `pre-built/shadcn-tokens.css` у `app/globals.css`. Компоненти shadcn автоматично підхоплять `--primary`, `--background`, `--ring` тощо.
145
-
146
- ### Vue 3 / Nuxt
147
-
148
- ```ts
149
- // nuxt.config.ts
150
- export default defineNuxtConfig({
151
- css: [
152
- '~/docs/portable-design/pre-built/tokens.css',
153
- '~/docs/portable-design/pre-built/components.css',
154
- ],
155
- });
156
- ```
157
-
158
- У шаблонах використовуйте `var(--color-hf-accent)`, `var(--font-size-hf-base)` або `.hf-modal` / `.hf-pill .status-booking-confirmed`.
159
-
160
- ### SCSS-стек (Yii / Laravel / WP)
161
-
162
- ```scss
163
- // _app.scss
164
- @import 'docs/portable-design/pre-built/tokens';
165
- @import 'docs/portable-design/pre-built/components.css';
166
-
167
- .my-btn {
168
- background: $colorPrimaryDefault;
169
- height: $sizeBtnDefault;
170
- border-radius: $borderRadiusSm;
171
- }
172
- ```
173
-
174
- ### TS / CSS-in-JS
175
-
176
- ```ts
177
- import { tokens } from './docs/portable-design/pre-built/tokens';
178
-
179
- const Button = styled.button`
180
- background: ${tokens.color.primary.default};
181
- height: ${tokens.size.btnDefault};
182
- border-radius: ${tokens.borderRadius.sm};
183
- `;
184
- ```
185
-
186
- ### Ванільний / статичний HTML
187
-
188
- ```html
189
- <link rel="stylesheet" href="docs/portable-design/pre-built/tokens.css">
190
- <link rel="stylesheet" href="docs/portable-design/pre-built/components.css">
191
-
192
- <span class="hf-pill status-booking-confirmed">Підтверджено</span>
193
- <button class="hf-modal__close">✕</button>
194
- ```
195
-
196
- ## Примітиви компонентів у `components.css`
197
-
198
- | Клас | Анатомія | Дивись |
199
- |---|---|---|
200
- | `.hf-pill` + `.status-{domain}-{state}` | Статус-бейдж — радіус 6px, фон 15% + текст 100% + рамка 1px | components.html#status |
201
- | `.hf-tab` / `.hf-tab--sm` / `.hf-pill-tabs` | Таби з підкресленням та сегментовані | components.html#tabs |
202
- | `.hf-pagination` + `__item` / `__ellipsis` | Активний — стриманий сірий (НЕ primary!) — 34×34, радіус 8px | components.html#pagination |
203
- | `.hf-modal` + `__header/__title/__body/__footer/__close` | Радіус 6px, у футера немає верхньої рамки | components.html#modal |
204
- | `.hf-alert` + `--success/--info/--warn/--error` | Білий фон + 3px акцентна смуга зверху + квадратна іконка 26×26 | components.html#alerts |
205
- | `.hf-alert--tinted` / `--banner` / `--compact` | Модифікатори — тонований фон / на всю ширину / компактний у картці | components.html#alerts |
206
- | `.hf-toast` | Плаваюче сповіщення (правий нижній кут) — радіус 9px | components.html#alerts |
207
- | `.hf-check` / `.hf-radio` | Кастомні чекбокс та радіо — 18×18, активний — заливка primary | components.html#inputs |
208
- | `.hf-dropdown-menu` + `__item / __header / __shortcut / __icon / __divider` | Дропдаун з радіусом 9px та тінню `0 1px 10px rgba(0,0,0,.1)` | components.html#dropdown |
209
- | `.skeleton` + `.hf-spin` | Примітиви станів завантаження (шимер + обертання) | components.html#empty |
210
-
211
- ## Як цим користується AI
212
-
213
- Після того як ви поклали один із файлів `ai-rules/*` у корінь нового проєкту:
214
-
215
- 1. **Claude Code / Cursor / Copilot** автоматично додають його до кожного чату. Агент знає:
216
- - канонічну палітру (primary `#24AFE8`, кольори статусів, нейтральні)
217
- - типографіку (Roboto, розміри 11/13/14/15/16/18/20/22/26/30, ваги 400/500/600)
218
- - шкалу відступів (кратно 4)
219
- - шкалу радіусів (6/8/9/12/99)
220
- - набір тіней (default/subtle/wrapper/card/modal/outline/hover)
221
- - бібліотеку компонентів (`.hf-modal`, `.hf-alert`, `.hf-pill`, `.hf-tab`, `.hf-pagination`, `.hf-check`, `.hf-dropdown-menu` тощо)
222
- - жорсткі правила ("ніколи не зашивати hex", "завжди робити hover/focus/disabled", "не мішати іконкові набори")
223
-
224
- 2. **Візуальний референс для нових проєктів:** відкрийте `components.html` у браузері — це канон, де відрендерені всі патерни `.hf-*`.
225
-
226
- 3. **Для чат-AI** (ChatGPT, v0, Lovable): вставте `ai-rules/system-prompt-compact.md` або JSON-довідник токенів із `UI_DESIGN.md` §9 як system prompt перед запитом UI-завдання.
227
-
228
- ## Оновлення згенерованих файлів
229
-
230
- Коли змінюється `tokens.figma.json`, перегенеруйте:
231
-
232
- ```bash
233
- cd docs/portable-design
234
- node generate-tokens.cjs --target=tailwind-v4 > pre-built/tailwind.css # v4 (рекомендовано)
235
- node generate-tokens.cjs --target=tailwind > pre-built/tailwind.preset.js # v3 (легасі)
236
- node generate-tokens.cjs --target=css > pre-built/tokens.css
237
- node generate-tokens.cjs --target=scss > pre-built/_tokens.scss
238
- node generate-tokens.cjs --target=ts > pre-built/tokens.ts
239
- node generate-tokens.cjs --target=shadcn > pre-built/shadcn-tokens.css
240
- ```
241
-
242
- > `pre-built/components.css` поки **не генерується** — він вручну витягнутий із `components.html`. Якщо ви змінюєте блок `<style type="text/tailwindcss">` / `@layer components` у `components.html`, синхронізуйте витягнутий файл вручну. Заголовок `pre-built/components.css` містить координати джерела.
243
-
244
- Або вбудуйте регенерацію в `package.json`:
245
-
246
- ```json
247
- {
248
- "scripts": {
249
- "tokens:build:v4": "node docs/portable-design/generate-tokens.cjs --target=tailwind-v4 > tailwind.css",
250
- "tokens:build:v3": "node docs/portable-design/generate-tokens.cjs --target=tailwind > tailwind.preset.js",
251
- "prebuild": "npm run tokens:build:v3 && npm run tokens:build:v4"
252
- }
253
- }
254
- ```
255
-
256
- ## Чек-ліст валідації
257
-
258
- Після перенесення у новий проєкт перевірте:
259
-
260
- - [ ] `cat CLAUDE.md` (або `.cursorrules`) показує правила дизайн-системи.
261
- - [ ] `node docs/portable-design/generate-tokens.cjs --target=css` відпрацьовує чисто і дає стабільний вивід.
262
- - [ ] AI-агент на питання "який primary color?" відповідає `#24AFE8`.
263
- - [ ] Перша згенерована AI кнопка відповідає `.btn-primary` (40px / 15px / 600 / `#24AFE8` / радіус 6px).
264
- - [ ] Бейджі статусів резолвляться через токени `--status-{domain}-{state}-color`.
265
- - [ ] `.hf-modal`, `.hf-alert`, `.hf-pagination` рендеряться коректно, коли підвантажений `pre-built/components.css`.
266
-
267
- ## Що НЕ переноситься
268
-
269
- Ці файли у батьківському `docs/` — **специфічні для порталу HotelFriend** (легасі-аудит / детект дрейфу), копіювати їх НЕ треба:
270
-
271
- - `docs/migration-plan.md` — черга прибирання старого SCSS (тільки портал на Yii)
272
- - `docs/design-tokens-audit.md` — звіт про дрейф
273
- - `docs/ui-elements-catalog.*` — спостережені компоненти у порталі
274
- - `docs/icon-audit.*` — карта міграції FA→Lucide
275
- - `docs/component-anatomy.json` — обміри легасі-модалок
276
- - `scratch/` — Playwright-воркери, що сканують портал
277
-
278
- Вони лежать у вихідному репо для господарських потреб; новому проєкту вони не потрібні.
279
-
280
- `portal-audit.html` всередині цього пакета — єдиний легасі-артефакт, що подорожує разом із portable-папкою — залиште його як історичний референс і для трекінгу міграції. Новий код **ніколи** не повинен копіювати патерни з `portal-audit.html`.
281
-
282
- ---
283
-
284
- ## Детект дрейфу та інтеграція з CI
285
-
286
- ### Pre-commit hook (рекомендовано для будь-якого проєкту, що чіпає цей пакет)
287
-
288
- ```bash
289
- # З husky (рекомендовано)
290
- pnpm add -D husky
291
- pnpm husky init
292
- cp docs/portable-design/scripts/pre-commit.sh .husky/pre-commit
293
- chmod +x .husky/pre-commit
294
-
295
- # Без husky (сирий git-hook)
296
- cp docs/portable-design/scripts/pre-commit.sh .git/hooks/pre-commit
297
- chmod +x .git/hooks/pre-commit
298
- ```
299
-
300
- Хук запускає `validate-tokens.cjs`, коли в staging є будь-які файли з tokens / pre-built / docs / components.html. Падає на дрейфі. Мовчить на успіху.
301
-
302
- ### Конфіг Stylelint (рекомендовано для проєктів-споживачів)
303
-
304
- Сумісний Stylelint-конфіг, який забороняє сирий hex / іменовані кольори / літеральні `box-shadow`:
305
-
306
- ```js
307
- // .stylelintrc.cjs
308
- module.exports = {
309
- extends: [
310
- 'stylelint-config-standard',
311
- './node_modules/@hotelfriend/design-tokens/pre-built/stylelint-design-system.cjs'
312
- // — або, якщо підключено через копіювання папки:
313
- // './docs/portable-design/pre-built/stylelint-design-system.cjs'
314
- ],
315
- overrides: [
316
- // У згенерованих файлах hex допустимий (fallback-и, примітиви) — opt-out
317
- { files: ['**/pre-built/*.css'], rules: { 'color-no-hex': null } },
318
- ],
319
- };
320
- ```
321
-
322
- ### Самостійний валідатор (одноразова перевірка у CI)
323
-
324
- ```bash
325
- cd docs/portable-design
326
- node scripts/validate-tokens.cjs
327
- # ✓ validate-tokens: all checks passed
328
- # 168 CSS variables defined, all var() refs resolve, no bare hex.
329
- ```
330
-
331
- Використовуйте у CI поруч із лінтом та тестами. Виходить із ненульовим кодом, якщо:
332
- 1. `var(--*)` посилається на CSS-змінну, яка ніде не визначена
333
- 2. У `components.css` / `status.css` є сирий hex (поза коментарями та fallback-ами `var()`; `#fff`/`#000` дозволені)
334
- 3. У код-блоці документації згадано токен, якого нема в `pre-built/*`
335
-
336
- ## NPM-пакет (приватний реєстр)
337
-
338
- Пакет публікується як `@hotelfriend/design-tokens` у приватний реєстр GitHub Packages:
339
-
340
- ```bash
341
- # У проєкті-споживачі:
342
- echo "@hotelfriend:registry=https://npm.pkg.github.com" >> .npmrc
343
- pnpm add @hotelfriend/design-tokens
344
- ```
345
-
346
- Імпорт під свій стек:
347
-
348
- ```css
349
- /* CSS / Tailwind v4 */
350
- @import "@hotelfriend/design-tokens/tailwind.css";
351
- @import "@hotelfriend/design-tokens/components.css";
352
- @import "@hotelfriend/design-tokens/status.css";
353
- ```
354
-
355
- ```ts
356
- // TypeScript
357
- import { tokens } from '@hotelfriend/design-tokens/tokens.ts';
358
- ```
359
-
360
- ```scss
361
- // SCSS
362
- @import '@hotelfriend/design-tokens/_tokens';
363
- ```
364
-
365
- ### Публікація (для мейнтейнера)
366
-
367
- ```bash
368
- cd docs/portable-design
369
- pnpm version patch # або minor / major
370
- pnpm publish # запускає prepublishOnly → build + validate
371
- ```
372
-
373
- `prepublishOnly` перезбирає та валідує, тож опублікований пакет завжди узгоджений.
374
-
375
- ---
376
-
377
- Зібрано на основі HotelFriend Design System v0.1.0 — JSON-довідник токенів див. у `UI_DESIGN.md` §9, канонічний візуальний референс — у `components.html`. Англомовна версія цього файлу — [`README.md`](README.md), повний журнал фаз і змін — там же.