@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 +26 -8
- package/generate-tokens.cjs +22 -1
- package/package.json +5 -2
- package/pre-built/components.css +12 -2
- package/src/components.css +525 -0
- package/README.uk.md +0 -377
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 (
|
|
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 **
|
|
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
|
|
371
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
-
|
|
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
|
---
|
package/generate-tokens.cjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": "
|
|
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"
|
package/pre-built/components.css
CHANGED
|
@@ -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
|
-
*
|
|
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 —
|
|
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), повний журнал фаз і змін — там же.
|