@hotelfriendag/design-tokens 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/UI_DESIGN.md ADDED
@@ -0,0 +1,608 @@
1
+ # UI Design System (HotelFriend Style)
2
+
3
+ > **Version:** 0.1.0 · **Last reconciled:** 2026-05-20 · **Branch:** `feature/ui-design-docs`
4
+ > **Maintainer:** Frontend platform team · **Changelog:** `docs/CHANGELOG.md`
5
+
6
+ Reference for UI work (Figma, CSS frameworks, hand-rolled markup). This document serves as the foundational design system for applications adhering to the brand style, **designed to be framework-agnostic**. AI agents must use this as the Single Source of Truth (SSOT) to generate UI in any tech stack (Tailwind, SCSS, CSS Modules, etc.).
7
+
8
+ ## Precedence rule (when artefacts disagree)
9
+
10
+ The design system ships in multiple places. They will drift over time. Use this priority chain to decide which source wins:
11
+
12
+ 1. **`components.html`** — **canonical visual reference**. Open in a browser. If a button in this doc looks different than in components.html, components.html wins. This is the file designers and devs use to verify visual decisions.
13
+ 2. **`UI_DESIGN.md`** (this file) — **canonical narrative SSOT**. Explains WHY decisions were made, anatomy, decision logs. Where components.html shows the rendered result, this doc explains the rationale.
14
+ 3. **`tokens.figma.json`** — atomic token export. Token-by-token values that drive `pre-built/*` generators and the Tokens Studio plugin in Figma. If it disagrees with components.html, regenerate.
15
+ 4. **`pre-built/*`** (tailwind.css / tokens.css / _tokens.scss / tokens.ts / shadcn-tokens.css / components.css) — **generated**, never hand-edit. Re-run `generate-tokens.cjs` after JSON changes. `components.css` is hand-extracted from components.html.
16
+ 5. **`web/css/scss/common/_variables.scss`** — runtime SCSS variables. The compiled CSS is what the browser actually paints in the legacy portal. **Live drift is logged in `docs/design-tokens-audit.md`** — when SCSS disagrees with this file, file an SCSS-only PR to fix the SCSS, never weaken this doc to match.
17
+ 6. **`portal-audit.html`** — **archival** snapshot of the legacy portal UI. NOT a source of truth for new code. Useful for migration tracking only.
18
+ 7. **`docs/ui-elements-catalog.json` / `.md`** — observational snapshot of what shipped. **Never used as truth** — only used for drift detection.
19
+
20
+ Concrete rules:
21
+ - A button class that exists in `_variables.scss` but not in this doc is **legacy** until documented.
22
+ - A token in `tokens.figma.json` that is missing from this doc is a **bug** — either remove it from Figma export or document it here.
23
+ - If you observe a legacy class in `ui-elements-catalog.md` or `portal-audit.html`, do not propagate it. Map it via `docs/migration-plan.md` instead.
24
+
25
+ > **Reality check (2026-05-07).** Cross-referenced against live values extracted from production CSS via `scratch/extract-all-pages.js`. See companion artefacts:
26
+ > - `docs/design-tokens.json` — every token observed live, with occurrence counts
27
+ > - `docs/design-tokens-audit.md` — discrepancies between this doc and the actual CSS
28
+ > - `docs/ui-elements-catalog.json` / `.md` — full inventory of components per page
29
+ > - `docs/tokens.figma.json` — Tokens Studio export (importable into Figma)
30
+ >
31
+ > Where this document and reality disagree, items marked **⚠ live** show the actual portal value; items marked **canonical** are the design intent we are migrating toward. New UI must follow **canonical**, but be aware legacy CSS still ships the live values.
32
+
33
+ ---
34
+
35
+ ## 1. Brand & Palette
36
+
37
+ ### Primary
38
+ | Token | Hex | Usage |
39
+ | ---------------- | --------- | --------------------------------------------------- |
40
+ | `--primary-color` (canonical) | `#24AFE8` | Links, primary buttons, focus, active tab underline |
41
+ | `$primary_hover` | `#149AD1` | Primary button hover |
42
+ | `$light_primary` | `#E9F6FC` | Active state bg, light tint |
43
+ | `$primary_tint_bg` | `#EFF6FF` | "Reset Filters" button bg (live) |
44
+ | `$primary_tint_border` | `rgba(27, 132, 255, 0.2)` | "Reset Filters" button border (live) |
45
+
46
+
47
+ *Note: `$st-arctic` is a legacy alias that equals `$primary`.*
48
+
49
+ ### Neutrals
50
+ | Token | Hex | Usage |
51
+ | ----------------------- | --------- | ---------------------------------- |
52
+ | `$white` | `#fff` | Surfaces, panels |
53
+ | `$light__Grey` | `#FBFBFC` | Page background top |
54
+ | `$bg_accent` | `#F6F7FB` | Section backgrounds |
55
+ | `$very_light_gray` | `#F1F3F6` | Header bars, hover, inputs bg |
56
+ | `$light_blue_gray` | `#E4E8EF` | Default border, dividers, disabled |
57
+ | `$border_color_primary` | `#D1D6DD` | Stronger border (buttons, chips) |
58
+ | `$border_color_hover` | `#B2BAC4` | Border on hover |
59
+
60
+ ### Text
61
+ | Token | Hex | Usage |
62
+ | ----------------------- | --------- | -------------------------------- |
63
+ | `$text_color_primary` | `#2B2B2B` | Body text, headings |
64
+ | `$text_color_secondary` | `#4B5675` | Secondary text, subdued labels |
65
+ | `$text_color_tertiary` | `#4A5565` | Captions |
66
+ | `$placeholder_color` | `#AEBCCF` | Input placeholder + disabled input text — portal `$dark__Grey` ⚠ canonical 2026-05 (was `#99A1B7`) |
67
+ | `$steel__blue` | `#485b78` | Tab labels, secondary navigation |
68
+ | `$useful` | `#7E8EA6` | Helper text on dark |
69
+
70
+ > **Decision log:** Placeholder color changed from `#99A1B7` → `#AEBCCF` on 2026-05-20 to match the live portal's `$dark__Grey` value used for input placeholders + disabled input text. The legacy `#99A1B7` is preserved in `tokens.figma.json` as `text.placeholderLegacy` for any backwards-compatible needs.
71
+
72
+ ### Status (semantic)
73
+ | Token | Hex | Meaning |
74
+ | ------------------------- | --------- | ----------------- |
75
+ | `$st-mint` / `$green` | `#59B59D` | Success, new |
76
+ | `$st-buttermilk` | `#FFBD5A` | Warning / waiting |
77
+ | `$st-peach` / `$st-coral` | `#F87921` | Booking / coral |
78
+ | `$st-rouge` / `$red` | `#EA6565` | Error / delete |
79
+ | `$st-violet` | `#5761D8` | Special / accent |
80
+ | `--status-frontDesk-out-of-order-color` ⚠ live | `#FFC900` | Out-of-order rooms (frontDesk) |
81
+ | `--status-booking-check-in-color` ⚠ live | `#5B7A02` | Check-in olive |
82
+ | `--status-booking-due-out-color` ⚠ live | `#A55505` | Due-out brown |
83
+ | `--status-*-canceled-by-*-color` ⚠ live | `#7F8FA4` | Canceled-by-* uniform muted blue-gray |
84
+
85
+ #### Status badge token system (live)
86
+
87
+ The portal ships a fully tokenized badge palette for every domain entity (booking, order, orderItem, frontDesk, roomItem cleaning). Each status has a `*-color` (text/border) and a `*-bg-color` (15% alpha tint of the same color). Pattern:
88
+
89
+ ```css
90
+ --status-{domain}-{status}-color: rgb(R, G, B);
91
+ --status-{domain}-{status}-bg-color: rgba(R, G, B, 0.15);
92
+ ```
93
+
94
+ | Domain | Statuses |
95
+ | -- | -- |
96
+ | booking | new, offer, confirmed, check-in, check-out, canceled-by-hotel, canceled-by-guest, canceled-by-hf, no-show, due-in, due-out, deleted |
97
+ | order, orderItem | waiting, confirmed, completed, in-progress, canceled-by-hotel, canceled-by-guest, canceled-by-pos, action-required, deleted |
98
+ | frontDesk | quota-free, quota-busy, out-of-order |
99
+ | roomItem.cleaning | clean, cleaning, dirty, out-of-service, inspected |
100
+
101
+ Use these tokens directly in markup (`status-hf status-order-confirmed`) — never hard-code badge colors.
102
+
103
+ **Dark Mode Policy:** Dark mode is natively **not supported** by default. Applications should stick to the light palette above unless explicitly instructed otherwise.
104
+
105
+ ---
106
+
107
+ ## 2. Typography
108
+
109
+ - **Family:** Roboto (`font-family: 'Roboto', sans-serif;`)
110
+ - **Weights:** 400 (Body), 500 (Medium/Tabs/Labels), 600 (Semibold/Headings)
111
+ - **Line-height:** `1.5` for body text, `1.2` for headings, `1` for strictly constrained UI elements (like chips).
112
+ - **Letter-spacing:** Standard `0`. For uppercase sub-labels, use `0.05em`.
113
+ - **Truncation:** Long text in table cells or tight containers must use standard truncation (`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`).
114
+
115
+ | Token | Size | Line-height | Usage |
116
+ | ----------------- | ---- | ----------- | ---------------------------------- |
117
+ | `$pagetitle` | 26px | 1.2 | Top page title |
118
+ | `$btn_size` | 15px | 1.5 | Primary buttons |
119
+ | `$general_size` | 14px | 1.5 | Body, labels |
120
+ | `$select__size` | 14px | 1.5 | Selects, inputs |
121
+ | `$grid_head_size` | 13px | 1.5 | Grid headers, chips, small buttons |
122
+ | `$label_size` | 11px | 1.2 | Tag labels, uppercase captions |
123
+
124
+ **Extended scale (live, not yet tokenized):**
125
+
126
+ | Size | Where it shows up |
127
+ | ---- | -- |
128
+ | 12px | Helper text, tooltip body, debug bar (rare in product UI) |
129
+ | 16px | Sub-headings, modal titles, larger body in info-blocks |
130
+ | 17px | Single-step dialog headings (rare) |
131
+ | 18px | Info-block titles (`$info-title`), section headers in cards |
132
+ | 20px | Section heading inside dashboards |
133
+ | 22px / 24px / 25px | Marketing-style headings (rare in admin) |
134
+ | 30px | Hero numbers (e.g. dashboard stat cards) |
135
+
136
+ > ⚠ The base scale documented above (26/15/14/13/11) is canonical — but production CSS uses **all of {11–18, 20, 22, 24, 25, 26, 30}**. When generating UI, prefer the canonical sizes. Do not introduce new sizes without an explicit token.
137
+
138
+ ---
139
+
140
+ ## 3. Layout, Skeleton & Spacing
141
+
142
+ - **Base unit:** 4 px increments. Common values: 4, 8, 12, 16, 20, 24, 32.
143
+ - **Default spacing:** `20px` (between sections).
144
+ - **App Shell Anatomy:**
145
+ - **Sidebar:** Width `215px` (expanded), `80px` (collapsed).
146
+ - **Header:** Height `~60px` (varies by context).
147
+ - **Main Content Max-Width:** `1440px`.
148
+ - **Breakpoints & Behavior:**
149
+ - `> 1024px` (Desktop): Sidebar fixed left.
150
+ - `< 1024px` (Tablet): Sidebar auto-collapses to 80px.
151
+ - `< 720px` (Mobile): Sidebar becomes a hidden off-canvas drawer. Padding shrinks.
152
+
153
+ ### Page Layout Anatomy (Portal Standard)
154
+ When building standard list/grid pages (like Guests, Rooms, Hotels), the page is assembled using these basic blocks in this order:
155
+ 1. **Title Block** (`.hf-title-block`): Contains the page title, navigation tabs, and main call-to-action buttons (e.g., "Add Hotel").
156
+ 2. **Filters Bar** (`.hf-filters`): Contains search inputs, Select2 dropdowns, and a "Reset" button.
157
+ 3. **Data Grid** (`table`): The main content area. In legacy code, often uses responsive classes like `.mobile-grid__body` and `.mobile-grid-unit`.
158
+ 4. **Pagination / Footer Grid** (`.footergrid`): Located below the table, containing a "per page" selector (`.footergrid-left-block`) and summary/pagination controls (`.footergrid-right-block`).
159
+
160
+ ### Z-Index Scale (Factual Implementation)
161
+ *Note: These are the exact z-indexes used in the live project.*
162
+ - `z-sidebar`: 1000
163
+ - `z-dropdowns / right-sidebar / tooltips`: 1001
164
+ - `z-datepicker`: 3000
165
+ - `z-sticky-topbars (search, notifications)`: 4000
166
+ - `z-modal-backdrop (.fade)`: 10050
167
+ - `z-modal-dialog / alert-box`: 10051
168
+ - `z-preloader`: 99999
169
+
170
+ ### Border-radius & Shadows
171
+ | Token | Value | Where |
172
+ | ------------------------ | ----- | ---------------------------------------- |
173
+ | `$br-sm` | 6 px | Buttons, inputs, checkboxes |
174
+ | `$br-xl` | 9 px | Dropdowns, alerts |
175
+ | `$br-xxl` | 12 px | Cards, Grids, modals, main wrappers |
176
+ | `$br-pill` | 99px | Status badges (Inline chips) |
177
+
178
+ | Token | Value | Where |
179
+ | ------------------ | -------------------------------- | ----------------- |
180
+ | `--shadow-default` | `0 1px 8px rgba(0,0,0,.1)` | Default elevation — buttons, sticky bars |
181
+ | `--shadow-subtle` | `0 2px 4px rgba(0,0,0,.05)` | **Portal `.dashboard-card`** — very faint elevation on KPI tiles |
182
+ | `--shadow-wrapper` | `0 3px 4px rgba(0,0,0,.03)` | Card frame (legacy) |
183
+ | `--shadow-card` | `0 12px 24px rgba(26,26,30,.06)` | Large elevated surface — main reference cards |
184
+ | `--shadow-modal` | `0 6px 18px rgba(0,0,0,.10)` | **`.hf-modal`, `.hf-toast`, dropdowns, popovers** — primary floating UI |
185
+ | `--shadow-outline` | `0 1px 2px rgba(72,91,120,.18)` | Outline button micro-elevation |
186
+ | `--shadow-hover` | `0 2px 3px rgba(0,0,0,.06)` | Hover lift |
187
+
188
+ > **Decision log (2026-05-20):** Added `--shadow-subtle` (portal `.dashboard-card` exact extraction), `--shadow-modal` (formerly `--tooltip-shadow`, renamed since modal usage is primary), and `--shadow-outline` (portal outline-default button shadow). All shadows now documented match either an extracted portal value or a canonical design decision — no more `⚠ NOT IN LIVE CSS` orphans.
189
+
190
+ > **Portal-exact modal shadow:** Note that the portal's `.modal-content` actually uses `0 2px 4px 0 rgba(72,91,120,0.18)` (a steel-blue tinted shadow) — not the more conventional `--shadow-modal`. The `.hf-modal` component in `components.html` and `pre-built/components.css` replicates this exact portal value.
191
+
192
+ ---
193
+
194
+ ## 3.1 Live extracted state values
195
+
196
+ For 17 component classes the actual `:hover` / `:focus` / `:active` / `:disabled` declarations have been pulled from compiled CSS. Authoritative export: **`docs/component-states.json`**.
197
+
198
+ Key rules (per-class, what the live CSS actually sets — not the canonical narrative):
199
+
200
+ | Class | `:hover` | `:focus` | `:active` | `:disabled` / `[disabled]` |
201
+ | -- | -- | -- | -- | -- |
202
+ | `.btn-primary` | `bg: $primary_hover` | `outline: 2px solid $primary; outline-offset: 2px` | `transform: scale(0.98)` | inherited from `.btn` |
203
+ | `.btn-hf-outline-primary` | border + text become `$primary` | outline ring | — | — |
204
+ | `.btn-cancel` | `bg: $border-light` | outline ring | — | — |
205
+ | `.btn-delete` | `filter: brightness(0.9)` | outline ring `$error` | — | — |
206
+ | `.btn-reset-filters` | `bg: #DDEEFE` | — | — | `.disabled { opacity: 0.6; pointer-events: none }` |
207
+ | `.form-control` | `border-color: $border_color_hover` | `border-color: $primary; box-shadow: 0 0 0 2px rgba(38,173,228,0.2)` | — | `[disabled] { bg: #f7f9fa; color: #7E8CA0; cursor: not-allowed }` |
208
+ | `.dropdown-toggle` | underline removed; primary tint | outline ring | `bg: $primary; color: white` | — |
209
+ | `.dropdown-menu .dropdown-item:hover` | `bg: $bg-muted` | — | — | `.disabled { opacity: 0.5; cursor: not-allowed }` |
210
+ | `.pagination .page` | `bg: $bg-muted` | — | active class adds `bg: $primary; color: white; box-shadow: $hover_shadow` | `.disabled` opacity 0.5 |
211
+ | `.select2-selection` | `border-color: $border_color_hover` | `border-color: $primary; box-shadow: 0 0 0 2px rgba(38,173,228,0.2)` | — | — |
212
+
213
+ ⚠ Use `docs/component-states.json` for the full set when generating Figma component variants.
214
+
215
+ ---
216
+
217
+ ## 4. Universal Component States & Interaction
218
+
219
+ - **Motion (canonical):** `transition: all 200ms ease-in-out;` for hover, focus, and state changes.
220
+ - ⚠ **Motion (live):** the codebase actually uses `transition: 0.3s` (35×) and `transition: 0.2s` (15×) most often. Pure `0.2s ease-in-out` shows up only 4 times in compiled CSS. New code should follow the canonical 200ms ease-in-out.
221
+ - **Hover:** Slightly darken backgrounds (`$primary_hover`) or add border color (`$primary` on outline buttons). Add `$hover_shadow` to cards.
222
+ - **Active/Pressed:** Scale down slightly (`transform: scale(0.98)`) or darken further.
223
+ - **Focus Ring (Accessibility):** `outline: 2px solid #24AFE8; outline-offset: 2px;`. Never remove outlines without providing a visual alternative.
224
+ - **Disabled:** `opacity: 0.6; cursor: not-allowed; pointer-events: none;`. Backgrounds turn to `$light_blue_gray` for solid elements.
225
+ - **Loading:** Inline spinner icon (`Lucide`/`Heroicons`) rotating, alongside text. Disable the element while loading.
226
+ - **Tap Targets:** Minimum clickable area for mobile is `44x44px`.
227
+
228
+ ---
229
+
230
+ ## 5. Components Anatomy
231
+
232
+ ### Cards / Panels
233
+ ```css
234
+ background: #ffffff;
235
+ border: 1px solid #d1d6dd;
236
+ border-radius: 12px;
237
+ padding: 20px;
238
+ box-shadow: 0 3px 4px rgba(0, 0, 0, 0.03);
239
+ ```
240
+ *Rule clarification:* Cards feature **both** a subtle border and a very soft shadow to provide structured definition without floating too high.
241
+
242
+ ### Buttons
243
+
244
+ Live observation: 25+ unique button variants across 13 sidebar sections + 7 sub-tabs + 7 edit forms. Below is the canonical set; legacy variants (e.g. `btn.btn-b-gray`, `btn-text-blue`) exist but should be migrated.
245
+
246
+ | Style | Class | Look |
247
+ | ----------- | ------------------------------ | ------------------------------------------------------------ |
248
+ | **Primary** | `.btn.btn-primary` | Solid `#24AFE8` bg, white text, 40px h, 15/600. Hover: `#149AD1` bg. |
249
+ | **Primary sm** | `.btn.btn-primary.btn-sm` | Solid bg, 32px h, 15/600, padding `7px 14px`. |
250
+ | **Primary search** | `.btn.btn-primary.pr13` | 38px h, padding `10px 13px` — used as inline search submit. |
251
+ | **Outline** | `.btn.btn-hf-outline-primary` | White bg, `#24AFE8` border + text. 40px h, 15/600. |
252
+ | **Outline neutral** | `.btn` (no modifier) | White bg, `#AEBCCF` border, `#50627E` text. 38px h, 15/600. Used for split toggles like `No App / With App`. |
253
+ | **Reset Filters** | `.btn-reset-filters` | Light blue tint bg `#EFF6FF`, primary text `#24AFE8`, 1px border `rgba(27,132,255,0.2)`. 38px h, 14/500. |
254
+ | **Filters trigger** | `.filters-btn` | Same as Reset Filters but with funnel icon. |
255
+ | **Cancel** | `.btn.btn-cancel` | `#F1F3F6` bg, `#4B5675` text. Hover: `#E4E8EF` bg. |
256
+ | **Delete** | `.btn.btn-delete` | Solid `#EA6565` bg, white text. Hover: darken 10%. |
257
+ | **Save** (form footer) | `.btn.btn-primary` | Same as Primary, lives in form `<footer>`. |
258
+ | **Show inline link** | `.btn-text-blue.p0` | Plain text `#24AFE8`, 14/400, no bg. Used as table-cell mini-actions. |
259
+ | **Icon-only** (Summernote toolbar) | `.btn.btn-default.btn-sm.note-btn` | 32×32, white bg, gray text `#333`. Used in WYSIWYG toolbar. |
260
+
261
+ **Sizing:** Default 40px / 15/600 / radius 6px / padding `10px 20px`. SM 32px / 15/600 / padding `7px 14px`. Search-inline 38px / padding `10px 13px`. Reset Filters 38px / 14/500.
262
+
263
+ ### Status Badges (Pills)
264
+ Use `$br-pill` (`99px`) for full rounded edges.
265
+ - **Soft (Default):** Background is 15% opacity of the semantic color. Text is 100% semantic color. Example Success: `bg: #EAF7F4, text: #59B59D`.
266
+ - **Outline:** `bg: #fff, border: 1px solid #D1D6DD, text: #4B5675`.
267
+
268
+ ### Modal / Dialog
269
+
270
+ **Canonical (target):**
271
+ - **Backdrop:** Fixed inset 0, `bg: rgba(0,0,0,0.4)`, `z-index: 10050`.
272
+ - **Container:** Centered, `bg: #fff`, `border-radius: 12px`, `box-shadow: $card_shadow`, `z-index: 10051`. Max-width typically `500px` (sm) or `800px` (lg).
273
+ - **Header:** `padding: 20px`, bottom border `1px solid #E4E8EF`. Includes Title + Close (X) button.
274
+ - **Body:** `padding: 20px`.
275
+ - **Footer:** `padding: 20px`, top border `1px solid #E4E8EF`, right-aligned action buttons (Cancel + Primary).
276
+
277
+ **⚠ Live (drift, see `docs/component-anatomy.json`):**
278
+ - Default modal width is **600px** (legacy Bootstrap), `modal-medium` is **400px**.
279
+ - `.modal-content` has `border-radius: 6px` (not 12px) and `border: 1px solid rgba(72,91,120,0.15)`.
280
+ - `.modal-content` shadow is `0 2px 4px rgba(72,91,120,0.18)` (not `$card_shadow`).
281
+ - `.modal-header` / `.modal-body` padding is **15px** (not 20px). Some headers also have `pb0` modifier (no bottom padding).
282
+ - Close button is a Bootstrap 3 `.close` element: 17×17, font-weight 700, font-size 21px (an "×" character).
283
+
284
+ Migration row: `docs/migration-plan.md` G1.
285
+
286
+ ### Tabs
287
+ - **Container:** Flex row, `border-bottom: 1px solid #d1d6dd`.
288
+ - **Item:** `padding: 22px 15px`, `font-size: 15px`, `font-weight: 500`, `color: #50627e`.
289
+ - **Active state:** `color: #24AFE8`, active visual border is often handled via pseudo-elements or inner `span` borders.
290
+
291
+ ### Toast / Notification
292
+ - Position: Fixed top-right, spaced `20px` from edges.
293
+ - Anatomy: `bg: #fff`, left accent border (`4px solid {status_color}`), padding `16px`, `box-shadow: $card_shadow`.
294
+
295
+ ### Dropdown Menus
296
+ - **Trigger:** Button or Icon (cursor pointer).
297
+ - **Menu Container:** `position: absolute`, `bg: #fff`, `border-radius: 9px` (`$br-xl`), `box-shadow: $card_shadow`, `z-index: 1001`. Minimum width typically `160px`.
298
+ - **Menu Item:** `padding: 8px 16px`, `font-size: 14px`, `color: #4B5675`.
299
+ - **Item Hover/Active:** `bg: #F1F3F6` (`$very_light_gray`), text changes to `#2B2B2B`.
300
+
301
+ ### Tooltips
302
+ - **Container:** `bg: #2B2B2B`, `color: #fff`, `font-size: 12px`, `padding: 6px 10px`, `border-radius: 4px`, `z-index: 1001`, `box-shadow: $hover_shadow`.
303
+ - **Arrow:** Small 4px triangle pointing to the trigger element.
304
+ - **Timing:** Delay of `200ms` before appearing to prevent flickering on fast mouse movements.
305
+
306
+ ### Pagination
307
+ - **Layout:** Flex row, `gap: 4px`. Align right by default.
308
+ - **Item (Page Number):** `width: 32px`, `height: 32px`, flex centered, `border-radius: 6px` (`$br-sm`), `font-size: 13px`, `color: #4B5675`, `cursor: pointer`.
309
+ - **Item Hover:** `bg: #F1F3F6`.
310
+ - **Item Active:** `bg: #24AFE8` (`$primary`), `color: #fff`, `box-shadow: $hover_shadow`.
311
+ - **Disabled State:** `opacity: 0.5`, `cursor: not-allowed` (e.g., for Prev/Next buttons when at limits).
312
+
313
+ ### Switch (Toggle)
314
+ - **Container:** `.bootstrap-switch` or custom `.switch-box`. Often wrapped in `.form-group`.
315
+ - **Visuals:** Usually composed of a primary-colored active state (`bg: #24AFE8`) and default gray inactive state.
316
+
317
+ ### Progress Bar
318
+ - **Container:** `.hf-progress.progress-rounded` with track `.hf-progress-track-gray`.
319
+ - **Value Bar:** `.progress-bar` with specific semantic colors (e.g., `.progress-bar-primary`, `.progress-bar-success`).
320
+ - **Width:** Controlled via CSS variable `--progress-value` inline (e.g., `style=\"--progress-value: 75%\"`).
321
+ - **Sizes:** Modifiers available like `.hf-progress-sm`, `.hf-progress-lg`.
322
+
323
+ ### Custom Inputs
324
+ - **Quantity Input:** `.hf-input-quantity` wrapper containing `button.btn-minus_gray`, `input[type=\"number\"]`, and `button.btn-plus_gray`.
325
+ - **Toggle Switcher Input:** `.input-toggle_wrap` containing an input and a `.toggle-box` that visually toggles between two units (e.g., `%` or `UAH`).
326
+
327
+ ### Title Block / Action Bars
328
+ - **Container:** `.hf-title-block` or `.x-row.align-items-center`.
329
+ - **Elements:** Back button (`.btn-hf-outline-default`), title (`.hf-title-block-h3`), tabs container, and right-aligned action buttons (`.hf-title-block-right`).
330
+
331
+ ### Info Blocks
332
+ - **Container:** `.hf-info-block` (usually `margin-bottom: 20px`).
333
+ - **Visuals:** An alert-style block with an icon, `18px / 600` title, and `15px` description text.
334
+
335
+ ### Copy Text Widget
336
+ - **Container:** `.copy-widget` with `.copy-widget__content`.
337
+ - **Text:** `.text-ellipsis.nowrap`.
338
+ - **Action:** A button with data-attribute `data-toggle="copy-to-clipboard"`.
339
+
340
+ ### Select2 / Dropdowns
341
+ - **Container:** `.select2-container--bootstrap`.
342
+ - **Elements:** Integrates via Vue 3 or Kartik wrappers.
343
+
344
+ ### Empty State / Skeleton
345
+ - **Empty State:** Centered layout. Large subdued icon (`color: #99A1B7`, size `48px`), Title (`16px / 600`), Description (`14px / 400`), optional Primary Action Button.
346
+ - **Skeleton:** `bg: #E4E8EF` with a subtle shimmer animation (`linear-gradient` moving left to right).
347
+
348
+ ### Switch (Bootstrap-Switch)
349
+ - **Library:** Bootstrap-switch (jQuery), wrapping classes `.bootstrap-switch.bootstrap-switch-animate.bootstrap-switch-wrapper`.
350
+ - **Live sizes:** 68×32 (settings list), 72×32 (SEO pages list), 83×32 (form fields).
351
+ - **States:** `.bootstrap-switch-on` (primary bg) / `.bootstrap-switch-off` (gray bg).
352
+ - **Recommendation:** All new switches must use this widget — do not roll custom toggles.
353
+
354
+ ### Rich-text editor (Summernote)
355
+ - **Library:** Summernote 0.8.18 — pulled from `cdn.jsdelivr.net` in `/portal/seo-pages/edit`.
356
+ - **Toolbar:** Container `.note-toolbar.panel-heading`, button row `.note-btn-group`. Each tool button is `.btn.btn-default.btn-sm.note-btn` 32×32.
357
+ - **Variants in toolbar:** `.note-btn-bold`, `.note-btn-italic`, `.note-btn-underline`, dropdown variants `.dropdown-toggle.note-btn` (with `#50627E` text) plus context-aware menus (`.dropdown-fontsize`, `.note-table`).
358
+ - **Editable area:** `.note-editable`, white bg, padding 10px, no border-radius (by default).
359
+
360
+ ### Date / Time picker (Krajee + Bootstrap-Timepicker)
361
+ - **Date input:** `input.form-control.krajee-datepicker` — 38px h, 6px radius, `#D1D6DD` border, calendar icon left.
362
+ - **Time picker:** `.bootstrap-timepicker-widget.dropdown-menu` — opens as dropdown with hour/minute scrollers, white bg, `--tooltip-shadow`.
363
+ - **Date range:** `input.custom-form-control.custom-form-daterange` — single field that opens a side-by-side calendar (krajee).
364
+
365
+ ### File input (Krajee FileInput)
366
+ - **Wrapper:** `.kv-fileinput-caption` — visually styled "fake" input with attached browse button.
367
+ - **Browse button:** `.btn.btn-primary` next to caption, often labeled "Browse...".
368
+ - **Preview area:** `.file-preview` (160px tall, gray bg, dashed border) appears below input after selection.
369
+ - **Recommendation:** Always use Krajee variant — never raw `<input type="file">`.
370
+
371
+ ### Sidebar (Portal nav)
372
+ - **Container:** `<aside class="menu">` (Vue 3 component). Width 215px expanded, 80px collapsed (< 1024px).
373
+ - **Item:** Anchor (`.menu-item`), padding `12px 5px 10px 31px` (icon + label), text `#2B2B2B` 14/400.
374
+ - **Active item:** `.menu-item.active` adds `#24AFE8` left border + tinted bg.
375
+ - **Mobile button:** `.menu-mobBtn` (hamburger), top-left, 32×32.
376
+
377
+ ---
378
+
379
+ ## 5.1 Component Primitives — `.hf-*` classes
380
+
381
+ These are the **canonical component classes** shipped in `pre-built/components.css` and rendered in `components.html`. Use them as drop-in primitives in new projects. Each entry below has: anatomy summary, key tokens, link to the rendered demo, and notes on portal-exact vs adapted decisions.
382
+
383
+ > Open `components.html` in a browser for the live rendered versions. Anchors like `#buttons` / `#inputs` correspond to the section IDs.
384
+
385
+ ### `.hf-pill` — Status badges
386
+ - **Anatomy:** `display: inline-flex; padding: 6px 20px; border-radius: 6px; font: 14px/500; border: 1px solid currentColor`
387
+ - **Colors:** 15% bg + 100% text + 1px border in the same `--status-{domain}-{state}-color` token
388
+ - **Modifiers:**
389
+ - `.is-active` — selected filter chip (transparent bg + `box-shadow: inset 0 0 0 1px currentColor`)
390
+ - `--striped` — diagonal repeating-linear-gradient pattern (out-of-service rooms)
391
+ - `--dd` — adds chevron + cursor (clickable status switcher in tables)
392
+ - `min-w-[130px/160px/180px]` — Tailwind utilities for table column alignment
393
+ - **Reference:** `components.html#status` · **Portal source:** `_status.scss .status-hf` · **Decision:** 1:1 portal match
394
+
395
+ ### `.hf-tab` / `.hf-tab--sm` / `.hf-pill-tabs` — Tabs
396
+ - **Large** (page nav): padding `22px 15px`, 16px/500, active = 3px bottom border `#24AFE8`
397
+ - **Small** (`--sm`, sub-filter Today/All): padding `10px 15px`, 15px/500
398
+ - **Pill tabs** (segmented control): inline group with section bg, active = white + subtle shadow
399
+ - **Count badge** (`.hf-tab__count`): 22min×18 pill inside tab — 11px/600
400
+ - **Reference:** `components.html#tabs` · **Portal source:** `ul.nav-tabs` Bootstrap 3 · **Decision:** 1:1 portal match for both sizes
401
+
402
+ ### `.hf-pagination` — Pagination
403
+ - **Anatomy:** 34×34 items, 8px radius, 14px/400, ink-steel `#485B78` inactive
404
+ - **Active state:** `bg: #E4E8EF; color: #252F4A` (subtle gray — **NOT primary blue!** This is portal-exact behavior, easy to miss)
405
+ - **Hover (inactive):** `bg: #F1F3F6; color: #2B2B2B`
406
+ - **Disabled:** `color: #AEBCCF; pointer-events: none`
407
+ - **Reference:** `components.html#pagination` · **Portal source:** Bootstrap `.pagination` · **Decision:** 1:1 portal match (the subtle gray active is what the portal actually ships — many other systems would use primary)
408
+
409
+ ### `.hf-modal` — Modal dialog
410
+ - **Anatomy:** 6px radius, `border: 1px solid rgba(72,91,120,.15)`, `box-shadow: 0 2px 4px 0 rgba(72,91,120,.18)`, white bg
411
+ - **Header:** padding 20px, bottom border 1px `#E4E8EF`, title 18px/600, close button 32×32 right-aligned
412
+ - **Body:** padding 20px
413
+ - **Footer:** padding 20px, **NO top border** (portal-exact), right-aligned actions with gap 12px
414
+ - **Sizes:** `max-w-[420px]` (sm/confirm), `max-w-[500px]` (md/form, default), `max-w-[720px]` (lg/multi-col), `max-w-[960px]` (xl/drill-down)
415
+ - **Variants:** `--with-footer-border` (opt-in separator)
416
+ - **Reference:** `components.html#modal` · **Portal source:** `.modal-content` in `_custom.scss` · **Decision:** 1:1 portal match for chrome/shadow/border-radius. **Adapted:** title is 18px/600 (modern) — portal uses 15px/900 uppercase which is dated. Document if your project needs portal-legacy title styling.
417
+
418
+ ### `.hf-alert` — In-page alerts
419
+ - **Anatomy:** white bg + 1px `rgba(72,91,120,.15)` border + `0 2px 4px rgba(72,91,120,.07)` shadow + **3px top accent bar** + 26×26 squared icon (filled accent color)
420
+ - **Title:** 18px/500 `#2B2B2B` · **Description:** 15px/normal `#50627E`
421
+ - **Variants** (set accent color via currentColor):
422
+ - `--success` (#59B59D) · `--info` (#24AFE8) · `--warn` (#FFBD5A) · `--error` (#EA6565)
423
+ - **Modifiers:**
424
+ - `--tinted` — accent-coloured bg instead of white (6–10% opacity)
425
+ - `--banner` — full-width strip, accent bg, white text, semi-transparent icon container (use for page-level critical messages like "contract expired")
426
+ - `--compact` — 13px text, smaller padding (for inside cards/forms)
427
+ - **Reference:** `components.html#alerts` · **Portal source:** `_widget.scss .alert` (with SVG data-URI icons) · **Decision:** 1:1 portal match for chrome. **Adapted:** uses inline SVG `<use>` instead of CSS-background data-URI (cleaner for theming).
428
+
429
+ ### `.hf-toast` — Floating notification
430
+ - **Anatomy:** white bg, 9px radius, `0 6px 18px rgba(0,0,0,.10)` shadow, 1px border `rgba(72,91,120,.15)`
431
+ - **Layout:** inline-flex with `__icon` (20×20) + `__text` (flex:1) + optional `__close` (20×20)
432
+ - **Sizing:** min-width 280px, max-width 420px
433
+ - **Positioning:** typically `position: fixed; bottom: 16px; right: 16px; z-index: 50`
434
+ - **Reference:** `components.html#alerts` · **Portal source:** `.alert-box` (different DOM structure) · **Decision:** New modern primitive — portal `.alert-box` is positioned but uses heavier `.alert` chrome. Toast is lighter.
435
+
436
+ ### `.hf-check` / `.hf-radio` — Checkbox & Radio
437
+ - **Anatomy:** 18×18, 1px solid `#DBDFE9` border, transparent bg
438
+ - **Checkbox:** 6px radius. Checked: filled `#26ADE4` bg, border same. After: white 5×10 ::after with bottom+right border, rotated 45deg.
439
+ - **Radio:** 9999px radius. Checked: filled `#26ADE4`. After: 6×6 white centered dot.
440
+ - **States:**
441
+ - Hover (not disabled): border becomes `#26ADE4`
442
+ - Disabled: opacity 0.5, cursor not-allowed
443
+ - Checked+disabled: bg/border `rgba(38,173,228,.5)`, opacity 1
444
+ - **Reference:** `components.html#inputs` · **Portal source:** `_custom.scss .mt-checkbox-outline` · **Decision:** 1:1 portal match (replaces unstable `accent-color` native styling)
445
+
446
+ ### `.hf-dropdown-menu` — Dropdown
447
+ - **Anatomy:** 9px radius, white bg, `box-shadow: 0 1px 10px 0 rgba(0,0,0,.1)` (portal's `$dropdown_shadow`), 1px border `rgba(228,232,239,.6)`, padding `6px 0`, min-width 180px
448
+ - **Children:**
449
+ - `__header` — uppercase 11px/600 `#99A1B7` section label
450
+ - `__item` — flex row, padding `8px 16px`, 14px text, gap 10px
451
+ - `__item__icon` — 16px, color `#99A1B7` (faint), becomes `#50627E` on item hover
452
+ - `__item__shortcut` — right-aligned 12px text (for ⌘keys or counts)
453
+ - `__divider` — 1px `#E4E8EF` separator
454
+ - **Item states:**
455
+ - Default → hover `bg: #F5F5F5`
456
+ - `.is-active` → `bg: #EFF6FF; color: #24AFE8` (primary tint + primary text)
457
+ - `:focus-visible` → outline 2px primary inset
458
+ - `.danger` → red text, red-tint hover
459
+ - `.is-disabled` / `[aria-disabled="true"]` → `#AEBCCF` color, pointer-events none
460
+ - **Reference:** `components.html#dropdown` · **Portal source:** `.dropdown-menu.open` in `_custom.scss` · **Decision:** 1:1 portal match for shadow/radius/colors. **Adapted:** uses flat `<div>` structure instead of Bootstrap 3 `<ul><li><a>` (modern, framework-agnostic).
461
+
462
+ ### `.skeleton` / `.hf-spin` — Loading primitives
463
+ - **Skeleton:** linear-gradient shimmer animation, 1.4s infinite, 4px radius. Use as block element with width/height utilities (e.g. `<div class="skeleton h-3 w-3/4">`)
464
+ - **Spinner:** `@keyframes hf-spin` rotate animation. Apply `.hf-spin` to an SVG.
465
+ - **Reference:** `components.html#empty` · **Decision:** New primitives — portal has no equivalent.
466
+
467
+ ---
468
+
469
+ ## 6. Forms
470
+
471
+ - **Inputs (Text / Select):** Height `39px`, Padding `6px 12px`, Radius `6px`, Border `1px solid #d1d6dd`. Placeholder `#AEBCCF`. **Focus (portal-exact):** border darkens only to `#B2BAC4`, **no ring**. **Focus (accessible, recommended for new projects):** add `box-shadow: 0 0 0 2px rgba(36,175,232,0.2)` for WCAG compliance.
472
+ - **Field Anatomy:**
473
+ 1. Label (`14px / 500`, color `#2B2B2B`, margin-bottom `8px`).
474
+ 2. Required marker (Red `*` next to label).
475
+ 3. Input Control.
476
+ 4. Helper / Error Text (`12px`, margin-top `4px`).
477
+ - **Error State:** Input border becomes `#EA6565`. Error text appears below input in `#EA6565`.
478
+ - **Validation Timing:** Prefer `onBlur` for inline validation, and `onSubmit` for overall form validation.
479
+ - **Multi-column Forms (Grid Layout):**
480
+ - Use CSS Grid for robust layout: `.form-grid { display: grid; grid-template-columns: 1fr; gap: 20px; }`.
481
+ - For screens `> 768px` (Tablet+): Switch to 2 columns `grid-template-columns: repeat(2, 1fr)`.
482
+ - **Full-width fields:** Use `grid-column: 1 / -1;` for textareas, wysiwyg editors, or long descriptions inside a 2-column grid.
483
+ - **Sections:** Separate large forms into logical blocks using `border-bottom: 1px solid #E4E8EF` or wrapping them in `<fieldset>` with an `<legend>` or heading.
484
+
485
+ ---
486
+
487
+ ## 7. Tables / Grids
488
+
489
+ - **Header:** `bg: #F1F3F6`, `13px / 500`, text `#4B5675`, `padding: 12px 16px`. Often sticky (`position: sticky; top: 0; z-index: 200`).
490
+ - **Sorting:** Header label acts as a button (`cursor: pointer`). Add an up/down arrow icon. On hover, text color changes to `#2B2B2B`. Active sorted column uses `#24AFE8` for the icon.
491
+ - **Rows:** `padding: 12px 16px`, bottom border `1px solid #E4E8EF`.
492
+ - **Checkbox Columns:** Width fixed to `40px-48px`, centered content. Used for bulk actions. Checkbox `border-radius: 6px`.
493
+ - **Hover:** Row background changes to `#FBFBFC`.
494
+ - **Empty State:** A single row `<tr>` with `colspan="100%"`. The cell contains the standard Empty State component (Subdued icon 48px, Title 16px, Text 14px, centered).
495
+ - **Pagination:** Bottom right aligned. Display "Showing X-Y of Z" on the left, and standard prev/next buttons on the right.
496
+
497
+ ---
498
+
499
+ ## 8. Canonical Page Example
500
+
501
+ When requested to generate a standard "List View" or dashboard page, AI agents should strictly follow this architectural composition:
502
+
503
+ ```html
504
+ <!-- App Shell Context assumed -->
505
+ <div class="page-container p-20 max-w-[1440px] mx-auto">
506
+
507
+ <!-- Page Header -->
508
+ <header class="flex justify-between items-center mb-20">
509
+ <div>
510
+ <!-- Optional Breadcrumbs: 12px, #4B5675 -->
511
+ <h1 class="text-[26px] font-semibold text-[#2B2B2B] leading-tight">Page Title</h1>
512
+ </div>
513
+ <div class="flex gap-12">
514
+ <!-- Secondary Action -->
515
+ <button class="btn-outline">Export</button>
516
+ <!-- Primary Action -->
517
+ <button class="btn-primary">+ Add New</button>
518
+ </div>
519
+ </header>
520
+
521
+ <!-- Filters / Tool bar -->
522
+ <div class="flex justify-between items-center bg-white p-16 border border-[#d1d6dd] rounded-t-[12px] border-b-0">
523
+ <div class="flex gap-16">
524
+ <!-- Search Input, Select filters -->
525
+ <input type="text" placeholder="Search..." class="form-control" />
526
+ </div>
527
+ </div>
528
+
529
+ <!-- Main Content / Table -->
530
+ <div class="bg-white border border-[#d1d6dd] rounded-b-[12px] shadow-[0_3px_4px_rgba(0,0,0,0.03)] overflow-hidden">
531
+ <table class="w-full text-left text-[14px]">
532
+ <thead class="bg-[#F1F3F6] text-[#4B5675] text-[13px] font-medium border-b border-[#E4E8EF]">
533
+ <tr>
534
+ <th class="p-16">Name</th>
535
+ <th class="p-16">Status</th>
536
+ <th class="p-16 text-right">Actions</th>
537
+ </tr>
538
+ </thead>
539
+ <tbody>
540
+ <!-- Data Rows or Empty State -->
541
+ <tr class="border-b border-[#E4E8EF] hover:bg-[#FBFBFC] transition-colors">
542
+ <td class="p-16 text-[#2B2B2B]">John Doe</td>
543
+ <td class="p-16"><span class="badge-soft-success">Active</span></td>
544
+ <td class="p-16 text-right">...</td>
545
+ </tr>
546
+ </tbody>
547
+ </table>
548
+
549
+ <!-- Pagination Footer -->
550
+ <div class="p-16 border-t border-[#E4E8EF] flex justify-between items-center text-[13px] text-[#4B5675]">
551
+ <span>Showing 1-10 of 42</span>
552
+ <!-- Pagination Controls -->
553
+ </div>
554
+ </div>
555
+
556
+ </div>
557
+ ```
558
+
559
+ ---
560
+
561
+ ## 8.1 Companion: live token export
562
+
563
+ For machine-readable tokens see `docs/tokens.figma.json` (Tokens Studio format, importable directly in Figma → "Tokens Studio" plugin → Sync → File → Drop the JSON). Includes:
564
+
565
+ - `colors/primary/*` — primary palette
566
+ - `colors/neutrals/*` — surfaces, borders, backgrounds
567
+ - `colors/text/*` — text hierarchy
568
+ - `colors/status/*` — semantic colors
569
+ - `colors/badges/*/{color,bg}` — full status-badge tokens for booking, order, frontDesk, roomItem
570
+ - `typography/*` — composite tokens (size + weight + lineHeight)
571
+ - `radius/*`, `shadow/*`, `spacing/*` — atomic tokens
572
+
573
+ ---
574
+
575
+ ## 9. JSON Token Reference
576
+
577
+ ```json
578
+ {
579
+ "colors": {
580
+ "primary": "#24AFE8",
581
+ "primaryHover": "#149AD1",
582
+ "white": "#FFFFFF",
583
+ "bgPage": "#FBFBFC",
584
+ "bgAccent": "#F6F7FB",
585
+ "bgMuted": "#F1F3F6",
586
+ "borderLight": "#E4E8EF",
587
+ "border": "#D1D6DD",
588
+ "textPrimary": "#2B2B2B",
589
+ "textSecondary": "#4B5675",
590
+ "placeholder": "#99A1B7",
591
+ "status": {
592
+ "success": "#59B59D",
593
+ "warning": "#FFBD5A",
594
+ "error": "#EA6565"
595
+ }
596
+ },
597
+ "radius": {
598
+ "sm": "6px",
599
+ "xl": "9px",
600
+ "xxl": "12px",
601
+ "pill": "99px"
602
+ },
603
+ "shadows": {
604
+ "card": "0 12px 24px rgba(26,26,30,.06)",
605
+ "wrapper": "0 3px 4px rgba(0,0,0,.03)"
606
+ }
607
+ }
608
+ ```