@wippy-fe/theme 0.0.25 → 0.0.27

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.
Files changed (2) hide show
  1. package/THEMING.md +904 -492
  2. package/package.json +1 -1
package/THEMING.md CHANGED
@@ -1,492 +1,904 @@
1
- # Wippy Theming Reference
2
-
3
- Comprehensive reference for CSS variables, Tailwind utility classes, dark mode behavior, and PrimeVue styling used across Wippy web components and applications.
4
-
5
- ## 1. CSS Variables
6
-
7
- All variables are defined in `theme-config.css` and set on `:root`. At runtime, the host injects the real theme — these serve as the dev-time fallback and contract.
8
-
9
- ### Primary palette (11 vars)
10
-
11
- Base: `--p-primary` (default: `rgb(0, 95, 178)`)
12
-
13
- | Variable | Value |
14
- |---|---|
15
- | `--p-primary-50` | `color-mix(in srgb, var(--p-primary) 5%, white)` |
16
- | `--p-primary-100` | `color-mix(in srgb, var(--p-primary) 10%, white)` |
17
- | `--p-primary-200` | `color-mix(in srgb, var(--p-primary) 20%, white)` |
18
- | `--p-primary-300` | `color-mix(in srgb, var(--p-primary) 30%, white)` |
19
- | `--p-primary-400` | `color-mix(in srgb, var(--p-primary) 40%, white)` |
20
- | `--p-primary-500` | `var(--p-primary)` (base) |
21
- | `--p-primary-600` | `color-mix(in srgb, var(--p-primary) 80%, black)` |
22
- | `--p-primary-700` | `color-mix(in srgb, var(--p-primary) 70%, black)` |
23
- | `--p-primary-800` | `color-mix(in srgb, var(--p-primary) 60%, black)` |
24
- | `--p-primary-900` | `color-mix(in srgb, var(--p-primary) 50%, black)` |
25
- | `--p-primary-950` | `color-mix(in srgb, var(--p-primary) 40%, black)` |
26
-
27
- ### Secondary palette (11 vars)
28
-
29
- Base: `--p-secondary` (default: `#6f7385`)
30
-
31
- Same numbered scale pattern as primary (50–950) using `color-mix`.
32
-
33
- ### Surface palette (13 vars)
34
-
35
- | Variable | Light value | Dark value |
36
- |---|---|---|
37
- | `--p-surface-0` | `#fff` | `#fff` |
38
- | `--p-surface-50` | `#fafafa` | `#fafafa` |
39
- | `--p-surface-100` | `#f4f4f5` | `#f4f4f5` |
40
- | `--p-surface-200` | `#e4e4e7` | `#e4e4e7` |
41
- | `--p-surface-300` | `#d4d4d8` | `#d4d4d8` |
42
- | `--p-surface-400` | `#a1a1aa` | `#a1a1aa` |
43
- | `--p-surface-500` | `#71717a` | `#71717a` |
44
- | `--p-surface-600` | `#52525b` | `#545250` (warm) |
45
- | `--p-surface-700` | `#3f3f46` | `#403e3c` (warm) |
46
- | `--p-surface-800` | `#27272a` | `#2b2927` (warm) |
47
- | `--p-surface-900` | `#18181b` | `#1c1a19` (warm) |
48
- | `--p-surface-950` | `#09090b` | `#0f0e0d` (warm) |
49
-
50
- **Fixed light-to-dark scale** — 0 is always lightest, 950 always darkest. The scale does NOT flip with dark mode. In dark mode, only 600–950 get warmer undertones.
51
-
52
- ### Semantic variables (15 vars)
53
-
54
- These **flip with dark mode** — use these for theme-dependent styling.
55
-
56
- | Variable | Light | Dark |
57
- |---|---|---|
58
- | `--p-primary-color` | `primary-500` | `primary-400` |
59
- | `--p-primary-contrast-color` | `surface-0` | `surface-900` |
60
- | `--p-primary-hover-color` | `primary-600` | `primary-300` |
61
- | `--p-primary-active-color` | `primary-700` | `primary-200` |
62
- | `--p-text-color` | `surface-700` | `surface-0` |
63
- | `--p-text-hover-color` | `surface-800` | `surface-0` |
64
- | `--p-text-muted-color` | `surface-500` | `surface-400` |
65
- | `--p-text-hover-muted-color` | `surface-600` | `surface-300` |
66
- | `--p-content-background` | — | `surface-900` (via host) |
67
- | `--p-content-border-color` | `surface-200` | `surface-700` |
68
- | `--p-content-hover-background` | `surface-100` | `surface-800` |
69
- | `--p-content-hover-color` | `surface-800` | `surface-0` |
70
- | `--p-highlight-background` | `primary-50` | `primary-400 @ 16%` |
71
- | `--p-highlight-color` | `primary-700` | `white @ 87%` |
72
- | `--p-highlight-focus-background` | `primary-100` | `primary-400 @ 24%` |
73
- | `--p-highlight-focus-color` | `primary-800` | `white @ 87%` |
74
- | `--p-content-border-radius` | `6px` | `6px` |
75
-
76
- ## 2. Tailwind Utility Classes
77
-
78
- Provided by `tailwindcss-primeui` plugin (included in the shared Tailwind preset).
79
-
80
- ### Color utilities
81
-
82
- Work with `bg-`, `text-`, `border-`, `outline-`, `ring-` prefixes:
83
-
84
- | Class suffix | Maps to |
85
- |---|---|
86
- | `primary` | `--p-primary-color` |
87
- | `primary-emphasis` | `--p-primary-hover-color` |
88
- | `primary-emphasis-alt` | `--p-primary-active-color` |
89
- | `primary-contrast` | `--p-primary-contrast-color` |
90
- | `primary-{0,50,100,...,950}` | Full primary shade range |
91
- | `surface-{0,50,100,...,950}` | Full surface shade range |
92
-
93
- ### Semantic utilities
94
-
95
- | Class | Maps to |
96
- |---|---|
97
- | `.text-color` | `--p-text-color` |
98
- | `.text-color-emphasis` | `--p-text-hover-color` |
99
- | `.text-muted-color` | `--p-text-muted-color` |
100
- | `.text-muted-color-emphasis` | `--p-text-hover-muted-color` |
101
- | `.bg-emphasis` | Emphasis background + text |
102
- | `.bg-highlight` | Highlighted state (selected items, active rows) |
103
- | `.bg-highlight-emphasis` | Emphasized highlight |
104
- | `.border-surface` | `--p-content-border-color` |
105
- | `.rounded-border` | `--p-content-border-radius` |
106
-
107
- ### Animation utilities
108
-
109
- | Class | Description |
110
- |---|---|
111
- | `.animate-fadein` | Fade in |
112
- | `.animate-fadeout` | Fade out |
113
- | `.animate-slidedown` | Slide down |
114
- | `.animate-slideup` | Slide up |
115
- | `.animate-scalein` | Scale in |
116
- | `.animate-fadeinleft` | Fade in from left |
117
- | `.animate-fadeinright` | Fade in from right |
118
- | `.animate-fadeinup` | Fade in from below |
119
- | `.animate-fadeindown` | Fade in from above |
120
- | `.animate-duration-{ms}` | Animation duration |
121
- | `.animate-delay-{ms}` | Animation delay |
122
- | `.animate-ease-*` | Easing functions |
123
-
124
- ### Severity & accent color utilities (MANDATORY for semantic colors)
125
-
126
- **Rule: Always use semantic severity classes over raw Tailwind color names when the color conveys meaning.** If a color represents an error, success, warning, info, or help state, you MUST use `danger-*`, `success-*`, `warn-*`, `info-*`, or `help-*` — never `red-*`, `green-*`, `orange-*`, `sky-*`, or `purple-*`. Raw Tailwind colors are only appropriate for purely decorative use where no semantic meaning is attached. Semantics first, decorative later.
127
-
128
- Added by the shared Tailwind preset via `theme.extend.colors`. Each has a full 50–950 shade scale backed by CSS variables.
129
-
130
- | Family | Base variable | Default color | Purpose |
131
- |--------|--------------|---------------|---------|
132
- | `danger` | `--p-danger` | `rgb(239, 68, 68)` (red-500) | Errors, destructive actions |
133
- | `success` | `--p-success` | `rgb(34, 197, 94)` (green-500) | Success states, confirmations |
134
- | `warn` | `--p-warn` | `rgb(249, 115, 22)` (orange-500) | Warnings, caution |
135
- | `info` | `--p-info` | `rgb(14, 165, 233)` (sky-500) | Informational messages |
136
- | `help` | `--p-help` | `rgb(168, 85, 247)` (purple-500) | Help, hints |
137
- | `accent` | `--p-accent` | `rgb(20, 184, 166)` (teal-500) | Highlights, special callouts |
138
-
139
- Usage with Tailwind prefixes (`bg-`, `text-`, `border-`, `outline-`, `ring-`):
140
-
141
- ```
142
- bg-danger-500 text-success-700 border-warn-200
143
- bg-info-50 text-help-400 border-accent-300
144
- ```
145
-
146
- PrimeVue component CSS (button, tag, badge, toast, message, password) references these semantic names instead of hardcoded Tailwind color names.
147
-
148
- **Overriding:** Set the base variable on `:root` to retheme all severity shades:
149
-
150
- ```css
151
- :root {
152
- --p-danger: rgb(220, 38, 38); /* custom danger base */
153
- }
154
- ```
155
-
156
- The full 50–950 scale auto-derives via `color-mix()`. No dark-mode override block is needed — component CSS picks the right shade per mode (e.g., `dark:bg-danger-400`). You may optionally override the base in `@media (prefers-color-scheme: dark)` for mode-specific tuning.
157
-
158
- ### Secondary color utilities
159
-
160
- Added by the shared Tailwind preset via `theme.extend.colors`:
161
-
162
- ```
163
- bg-secondary-{50..950}
164
- text-secondary-{50..950}
165
- border-secondary-{50..950}
166
- ```
167
-
168
- ## 3. Dark Mode
169
-
170
- Variables switch at `@media (prefers-color-scheme: dark)`. Key changes:
171
-
172
- - `--p-primary` base shifts from `rgb(0, 95, 178)` to `rgb(0, 125, 178)` (brighter)
173
- - `--p-primary-color` shifts from `primary-500` to `primary-400`
174
- - `--p-content-background` shifts to `surface-900` (via host injection)
175
- - `--p-text-color` shifts from `surface-700` to `surface-0`
176
- - Surface 600–950 get warmer undertones in dark mode
177
-
178
- ### Rules
179
-
180
- - **Use semantic variables** (`--p-text-color`, `--p-content-background`) for colors that should flip with dark mode.
181
- - **Use `surface-0`/`surface-950`** only when you explicitly need the fixed shade regardless of mode.
182
- - **For derived shades**: `color-mix(in srgb, var(--p-content-background) 85%, var(--p-text-color) 15%)`.
183
- - **Never use `--p-surface-N`** for theme-dependent colors — the numbered scale does NOT flip.
184
-
185
- ## 4. PrimeVue Styling (opt-in)
186
-
187
- The `primevue/` directory contains CSS files that style PrimeVue component tags (`.p-button`, `.p-datatable`, etc.) using Tailwind `@apply` directives. These are **only needed if your component uses PrimeVue tags**.
188
-
189
- ### Setup
190
-
191
- 1. Add `primevue` as a dependency
192
- 2. Import `PrimeVuePlugin` from `@wippy-fe/theme/primevue-plugin` — installs PrimeVue with `{ theme: 'none' }`
193
- 3. Import PrimeVue CSS: `@import "@wippy-fe/theme/primevue/tailwind.css"` in your styles
194
- 4. Add `'primeVueCssUrl'` to `hostCssKeys` so the host injects PrimeVue CSS at runtime
195
-
196
- ### How it works
197
-
198
- The CSS files use theme variables via Tailwind classes. Example from `button.css`:
199
-
200
- ```css
201
- .p-button {
202
- @apply bg-primary text-primary-contrast border-primary
203
- enabled:hover:bg-primary-emphasis enabled:active:bg-primary-emphasis-alt ...
204
- }
205
- ```
206
-
207
- The `tailwind.css` master file imports all component CSS files organized by category (form, button, data, overlay, menu, panel, file, message, media, misc).
208
-
209
- ### Available PrimeVue component styles
210
-
211
- **Form**: autocomplete, cascadeselect, checkbox, colorpicker, datepicker, iconfield, iftalabel, inputgroup, inputnumber, inputotp, inputtext, floatlabel, knob, listbox, multiselect, password, radiobutton, rating, select, selectbutton, slider, textarea, togglebutton, toggleswitch, treeselect
212
-
213
- **Button**: button, buttongroup, speeddial, splitbutton
214
-
215
- **Data**: datatable, dataview, paginator, picklist, orderlist, organizationchart, timeline, tree, treetable
216
-
217
- **Overlay**: confirmdialog, confirmpopup, dialog, drawer, popover, tooltip
218
-
219
- **Menu**: breadcrumb, contextmenu, dock, menu, menubar, megamenu, panelmenu, tieredmenu
220
-
221
- **Panel**: accordion, card, divider, fieldset, panel, scrollpanel, splitter, stepper, tabs, toolbar
222
-
223
- **File**: fileupload
224
-
225
- **Message**: message, toast
226
-
227
- **Media**: carousel, galleria, image, imagecompare
228
-
229
- **Misc**: avatar, badge, blockui, chip, inplace, metergroup, overlaybadge, progressbar, progressspinner, ripple, scrolltop, skeleton, tag, terminal
230
-
231
- ## 5. Choosing Custom Colors
232
-
233
- When overriding `--p-primary`, `--p-secondary`, or the surface palette, follow these guidelines to ensure contrast and readability across both modes.
234
-
235
- ### How the shade scale works
236
-
237
- The `color-mix()` system blends your base color with white (lighter shades) or black (darker shades). The percentages are fixed:
238
-
239
- | Shade | Blend |
240
- |---|---|
241
- | 50 | 5% base + 95% white |
242
- | 100 | 10% base + 90% white |
243
- | 200 | 20% base + 80% white |
244
- | 300 | 30% base + 70% white |
245
- | 400 | 40% base + 60% white |
246
- | **500** | **100% base (no blend)** |
247
- | 600 | 80% base + 20% black |
248
- | 700 | 70% base + 30% black |
249
- | 800 | 60% base + 40% black |
250
- | 900 | 50% base + 50% black |
251
- | 950 | 40% base + 60% black |
252
-
253
- This means the base color's luminosity determines the contrast of the **entire scale**. A too-light base produces a washed-out 500 that can't contrast against white text; a too-dark base makes 400 too dim for dark mode.
254
-
255
- ### Primary color luminosity
256
-
257
- The primary base is used at two critical contrast points:
258
-
259
- 1. **Light mode**: `bg-primary` (500) with `text-primary-contrast` (white) — buttons, badges, checkboxes
260
- 2. **Dark mode**: `bg-primary` (400 = 40% base + 60% white) with `text-primary-contrast` (surface-900, near-black)
261
-
262
- **Recommended relative luminance for `--p-primary`: 0.05–0.15** (medium-dark, saturated).
263
-
264
- | Example | RGB | Rel. luminance | Light contrast (500 vs white) | Dark contrast (400 vs black) |
265
- |---|---|---|---|---|
266
- | Default blue | `rgb(0, 95, 178)` | ~0.10 | ~8.5:1 (AAA) | ~6.8:1 (AA) |
267
- | Deep teal | `rgb(0, 128, 128)` | ~0.14 | ~5.8:1 (AA) | ~8.7:1 (AAA) |
268
- | Indigo | `rgb(79, 70, 229)` | ~0.07 | ~8.0:1 (AAA) | ~5.5:1 (AA) |
269
- | Red | `rgb(185, 28, 28)` | ~0.05 | ~9.2:1 (AAA) | ~4.7:1 (borderline) |
270
- | Forest green | `rgb(22, 101, 52)` | ~0.08 | ~7.2:1 (AAA) | ~6.1:1 (AA) |
271
- | Orange | `rgb(194, 120, 3)` | ~0.18 | ~3.7:1 (FAIL) | ~9.5:1 (AAA) |
272
-
273
- **Key constraints:**
274
- - Luminance **> 0.18** → 500 shade is too light for white text (fails WCAG AA 4.5:1 in light mode)
275
- - Luminance **< 0.04** → 400 shade is too dark for dark-on-light text (fails WCAG AA in dark mode)
276
- - The sweet spot is **0.06–0.14** — both modes pass WCAG AA (4.5:1), with most passing AAA (7:1)
277
- - Pure, saturated colors (high chroma) work best. Desaturated/muted colors lose contrast faster
278
-
279
- **If your brand color is too light** (e.g., orange, yellow, lime): darken it for `--p-primary` and use the original as a secondary or accent. Light-mode buttons with white text will fail contrast otherwise.
280
-
281
- **If your brand color is too dark** (e.g., navy, dark brown): lighten it slightly. The 400 shade in dark mode needs enough contrast against `surface-900`.
282
-
283
- ### Secondary color luminosity
284
-
285
- Secondary is used for muted/deemphasized elements — less critical than primary but still appears in text and borders.
286
-
287
- **Recommended relative luminance for `--p-secondary`: 0.10–0.25** (mid-range, desaturated).
288
-
289
- The default `#6f7385` (luminance ~0.16) is a muted gray-purple that reads as neutral in both modes. Good secondary colors:
290
- - Are **less saturated** than primary (avoid competing for attention)
291
- - Have **mid-range luminance** so the full shade scale is usable
292
- - Work as `text-secondary-600` on white and `text-secondary-300` on dark backgrounds
293
-
294
- ### Surface palette
295
-
296
- Surfaces are a fixed grayscale ramp — they do NOT flip with dark mode. The semantic variables (`--p-content-background`, `--p-text-color`) reference different points on this scale per mode.
297
-
298
- **Light mode assignments:**
299
-
300
- | Role | Surface | Hex | Purpose |
301
- |---|---|---|---|
302
- | Page background | 0 | `#ffffff` | Main canvas |
303
- | Card/container background | 0 | `#ffffff` | Content containers |
304
- | Hover background | 100 | `#f4f4f5` | Interactive hover state |
305
- | Border | 200 | `#e4e4e7` | Borders, dividers |
306
- | Muted text | 500 | `#71717a` | Secondary text, labels |
307
- | Body text | 700 | `#3f3f46` | Primary readable text |
308
- | Strong emphasis | 800 | `#27272a` | Headings, hover text |
309
-
310
- **Dark mode assignments:**
311
-
312
- | Role | Surface | Hex | Purpose |
313
- |---|---|---|---|
314
- | Page background | 900 | `#1c1a19` (warm) | Main canvas |
315
- | Card/container background | 800 | `#2b2927` (warm) | Elevated containers |
316
- | Hover background | 800 | `#2b2927` (warm) | Interactive hover state |
317
- | Border | 700 | `#403e3c` (warm) | Borders, dividers |
318
- | Muted text | 400 | `#a1a1aa` | Secondary text, labels |
319
- | Body text | 0 | `#ffffff` | Primary readable text |
320
- | Strong emphasis | 0 | `#ffffff` | Headings, hover text |
321
-
322
- **Dark mode warm tones** (600–950): The default surfaces shift from cool zinc grays to slightly warm brown-grays in dark mode. This reduces visual fatigue on dark backgrounds. The shift is subtle — `#545250` vs `#52525b` for surface-600 — but creates a warmer, more natural feel.
323
-
324
- When providing custom surfaces, keep these rules:
325
- - **0 must always be `#ffffff`** (or very near it) — used as contrast text in dark mode
326
- - **950 must always be very dark** — used as the maximum-contrast anchor
327
- - **The scale must increase monotonically in darkness** — no inversions
328
- - **600–950 should shift warm** in dark mode for visual comfort (optional but recommended)
329
- - **Don't change 0–500** between modes unless you have a specific reason — the semantic variables handle mode switching
330
-
331
- ### Contrast verification
332
-
333
- After choosing colors, verify these critical pairs meet WCAG AA (4.5:1 minimum):
334
-
335
- | Pair | Light mode | Dark mode |
336
- |---|---|---|
337
- | `bg-primary` + `text-primary-contrast` | primary-500 vs white | primary-400 vs surface-900 |
338
- | `bg-highlight` + text | primary-50 vs primary-700 | primary-400@16% vs white@87% |
339
- | Body text on background | surface-700 vs white | surface-0 vs surface-900 |
340
- | Muted text on background | surface-500 vs white | surface-400 vs surface-900 |
341
-
342
- Quick formula for relative luminance of an sRGB color:
343
- ```
344
- L = 0.2126 * R' + 0.7152 * G' + 0.0722 * B'
345
- where R' = (R/255)^2.2 (simplified gamma)
346
-
347
- Contrast ratio = (L_lighter + 0.05) / (L_darker + 0.05)
348
- ```
349
-
350
- WCAG AA requires 4.5:1 for normal text, 3:1 for large text (18px+ bold or 24px+ regular).
351
-
352
- ## 6. Host UI Customization (CSS Variables & BEM Classes)
353
-
354
- The Wippy host exposes CSS custom properties and BEM class names on its core UI components. Override these via `AppConfig.customization.cssVariables` or `customCSS` to restyle the host without forking.
355
-
356
- > **Important:** Because `customCSS` and `cssVariables` are injected into both the host AND all nested child iframes, always scope host-only overrides to `.wippy-host-app`. For example, use `.wippy-host-app .chat-message { ... }` — never `.chat-message { ... }` alone, or the styles will leak into child iframes.
357
-
358
- ### Layout & Sidebar
359
-
360
- | Variable | Default | Description |
361
- |---|---|---|
362
- | `--wippy-host-sidebar-width-open` | `16rem` | Sidebar width when expanded |
363
- | `--wippy-host-sidebar-width-closed` | `3.5rem` | Sidebar width when collapsed |
364
-
365
- **BEM classes** (scope with `.wippy-host-app`):
366
-
367
- | Class | Element |
368
- |---|---|
369
- | `.layout` | Root layout wrapper |
370
- | `.layout__sidebar` | Sidebar container |
371
- | `.layout__sidebar-header` | Sidebar header (logo + toggle) |
372
- | `.layout__sidebar-nav` | Navigation list area |
373
- | `.layout__main` | Main content area (right of sidebar) |
374
-
375
- ### Splitter Gutter
376
-
377
- The resizable panel divider between main content and the right panel.
378
-
379
- | Variable | Default | Description |
380
- |---|---|---|
381
- | `--wippy-host-splitter-width` | `1px` | Visible line width |
382
- | `--wippy-host-splitter-hit-area` | `10px` | Draggable hit area width (transparent) |
383
- | `--wippy-host-splitter-color` | `var(--p-surface-200)` (light) / `var(--p-surface-600)` (dark) | Line color |
384
-
385
- ### Chat Messages
386
-
387
- | Variable | Default | Description |
388
- |---|---|---|
389
- | `--wippy-host-message-radius` | `1rem` | Message bubble border radius |
390
- | `--wippy-host-message-padding-x` | `1rem` | Message horizontal padding |
391
- | `--wippy-host-message-padding-y` | `0.5rem` | Message vertical padding |
392
- | `--wippy-host-message-user-bg` | `var(--p-primary-50)` | User message background |
393
- | `--wippy-host-message-agent-bg` | `var(--p-yellow-50)` (light) / `var(--p-surface-800)` (dark) | Agent message background |
394
- | `--wippy-host-tool-bg` | `var(--p-help-50)` | Tool call background |
395
- | `--wippy-host-tool-border` | `var(--p-help-300)` | Tool call left border |
396
- | `--wippy-host-avatar-size` | `2rem` | Message avatar diameter |
397
-
398
- **BEM classes** (scope with `.wippy-host-app`):
399
-
400
- | Class | Element |
401
- |---|---|
402
- | `.chat-message` | Message row container |
403
- | `.chat-message--user` | User message modifier |
404
- | `.chat-message--agent-message` | Agent message modifier |
405
- | `.chat-message--tool` | Tool call message modifier |
406
- | `.chat-message--error` | Error message modifier |
407
- | `.chat-message__avatar` | Avatar wrapper |
408
- | `.chat-message__avatar-icon` | Avatar icon circle |
409
- | `.chat-message__content` | Message bubble |
410
- | `.chat-message__body` | Message text content |
411
- | `.chat-message__footer` | Timestamp row |
412
- | `.chat-message__tool-name` | Tool name label |
413
- | `.chat-message__tool-icon` | Tool icon |
414
- | `.chat-message__agent-content` | Agent name system line |
415
- | `.chat-message__model-content` | Model name system line |
416
- | `.chat-message__files` | Attached files row |
417
- | `.chat-tool-group` | Inline tool call badge group |
418
- | `.chat-tool-group__badge` | Individual tool badge |
419
- | `.chat-tool-group__badge--success` | Completed tool badge |
420
- | `.chat-tool-group__badge--error` | Failed tool badge |
421
- | `.chat-tool-group__badge--processing` | In-progress tool badge |
422
- | `.chat-tool-group__icon` | Badge icon |
423
-
424
- ### Chat Input
425
-
426
- **BEM classes** (scope with `.wippy-host-app`):
427
-
428
- | Class | Element |
429
- |---|---|
430
- | `.chat-input` | Input bar container |
431
- | `.chat-input__group` | Input field + buttons wrapper |
432
- | `.chat-input__textarea` | Message textarea |
433
- | `.chat-input__attach-button` | Attachment button |
434
- | `.chat-input__send-button` | Send button |
435
- | `.chat-input__stop-button` | Stop generation button |
436
- | `.chat-input__upload-list` | Upload queue list |
437
- | `.chat-input__prompts` | Suggested prompts area |
438
-
439
- ### Chat Container
440
-
441
- **BEM classes** (scope with `.wippy-host-app`):
442
-
443
- | Class | Element |
444
- |---|---|
445
- | `.chat-container` | Outer chat wrapper |
446
- | `.chat-container--selected` | Has active session |
447
- | `.chat-container--non-selected` | No session selected |
448
- | `.chat-container__empty-state` | Empty state wrapper |
449
- | `.chat-container__empty-state-icon` | Empty state icon |
450
- | `.chat-container__empty-state-title` | Empty state heading |
451
- | `.chat-container__empty-state-description` | Empty state text |
452
- | `.chat-container__drop-zone` | File drag-and-drop overlay |
453
- | `.chat-container__drop-zone-icon` | Drop zone icon |
454
-
455
- ### Session Selector
456
-
457
- **BEM classes** (scope with `.wippy-host-app`):
458
-
459
- | Class | Element |
460
- |---|---|
461
- | `.session-selector` | Selector wrapper |
462
- | `.session-selector__dropdown` | Dropdown component |
463
- | `.session-selector__option` | Session option row |
464
- | `.session-selector__active-dot` | Active session indicator |
465
-
466
- ### Root
467
-
468
- | Class | Element |
469
- |---|---|
470
- | `.wippy-host-app` | Application root element — scope all host-only CSS overrides to this |
471
-
472
- ### Example: Custom-Styled Host
473
-
474
- ```css
475
- /* Via AppConfig.customization.customCSS */
476
-
477
- /* Variables are already prefixed — no scoping needed */
478
- :root {
479
- --wippy-host-message-radius: 0.5rem;
480
- --wippy-host-message-user-bg: #e0f2fe;
481
- --wippy-host-sidebar-width-open: 20rem;
482
- }
483
-
484
- /* Class selectors MUST be scoped to .wippy-host-app */
485
- .wippy-host-app .chat-message__footer {
486
- display: none; /* hide timestamps */
487
- }
488
-
489
- .wippy-host-app .session-selector {
490
- display: none; /* hide session picker */
491
- }
492
- ```
1
+ # Wippy Theming Guide
2
+
3
+ Authoritative guide for theming Wippy apps. Covers when to theme at the **facade** (host-wide), when to theme at the **page** (per-iframe), and the full CSS-variable / Tailwind / PrimeVue reference.
4
+
5
+ > **Mirrored at three locations.** Same content, three places — keep them in sync.
6
+ > - This file: `app-template/frontend/docs/theming.md` (in-repo, primary editing surface)
7
+ > - npm package: `@wippy-fe/theme/THEMING.md` (shipped with the theme package — `node_modules/@wippy-fe/theme/THEMING.md` after install)
8
+ > - Wippy KB: `wippy.frontend` → "Wippy Theming Guide" (semantic search via the `wippy-kb` MCP)
9
+
10
+ ---
11
+
12
+ ## Table of contents
13
+
14
+ - [Three levels of theming — quick start](#three-levels-of-theming--quick-start)
15
+ - [Level 1 — Basic theming (accent only, via facade)](#level-1--basic-theming-accent-only-via-facade)
16
+ - [Level 2 — Full theming (palette + custom CSS, via facade)](#level-2--full-theming-palette--custom-css-via-facade)
17
+ - [Level 3 — Per-page theme override (configOverrides / runtime)](#level-3--per-page-theme-override-configoverrides--runtime)
18
+ - [Reference — CSS variables](#reference--css-variables)
19
+ - [Reference — Tailwind utility classes](#reference--tailwind-utility-classes)
20
+ - [Reference — Dark mode](#reference--dark-mode)
21
+ - [Reference — PrimeVue styling (opt-in)](#reference--primevue-styling-opt-in)
22
+ - [Reference — Choosing custom colors (luminosity, WCAG)](#reference--choosing-custom-colors-luminosity-wcag)
23
+ - [Reference — Host UI customization (`--wippy-host-*` + BEM classes)](#reference--host-ui-customization---wippy-host---bem-classes)
24
+ - [Related docs](#related-docs)
25
+
26
+ ---
27
+
28
+ ## Three levels of theming — quick start
29
+
30
+ Almost every "I want to restyle this" task fits one of three levels. Pick the lightest one that solves the problem; never reach for a heavier level "just in case".
31
+
32
+ | Level | Where it lives | Reaches | Use when |
33
+ |---|---|---|---|
34
+ | **1 — Basic** | `wippy/facade` parameter `css_variables` (a few `--p-*` overrides) | Host UI **and** all child iframes | You want a brand accent color and otherwise want stock theme |
35
+ | **2 — Full** | `wippy/facade` parameters `css_variables` + `custom_css` (and the scoped variants) | Host UI **and** all child iframes (or scoped to one or the other) | You're shipping a product with bespoke visual identity — full palette, custom components, BEM overrides |
36
+ | **3 — Per-page** | A specific page's `package.json` (`wippy.configOverrides.customization.cssVariables` / `customCSS`) **or** runtime `window.__WIPPY_CONFIG_OVERRIDES__` | Just that one page (the iframe it loads in) | A demo page, an A/B variant, an experiment, a one-off embed — different look from the rest of the app |
37
+
38
+ > **Anti-pattern alert.** Don't theme by editing your child app's `src/styles.css` to override `--p-surface-0..950` or other tokens. That CSS is loaded inside the child iframe AFTER the host injects the real theme — it shadows host theming, never reaches the host UI itself, and creates per-iframe visual drift. Audit any project that has a multi-hundred-line `styles.css` in a child app. The right place is the facade (Levels 1–2) or `wippy.configOverrides` (Level 3).
39
+
40
+ ---
41
+
42
+ ## Level 1 — Basic theming (accent only, via facade)
43
+
44
+ **You want:** brand-color buttons, brand-color highlights, otherwise stock theme.
45
+
46
+ **You change:** the `wippy/facade` `ns.dependency` in your app's deps `_index.yaml`. Add a `css_variables` parameter with a JSON-string mapping just the primary tokens.
47
+
48
+ **Result:** the entire host UI, every child iframe, and every web component picks up the new accent. Dark mode keeps working. No CSS edits anywhere.
49
+
50
+ ### Sample
51
+
52
+ ```yaml
53
+ # src/<yourapp>/deps/_index.yaml
54
+ - name: facade
55
+ kind: ns.dependency
56
+ component: wippy/facade
57
+ parameters:
58
+ - name: server
59
+ value: app:gateway
60
+ - name: router
61
+ value: app:api.public
62
+ # Theming starts here:
63
+ - name: css_variables
64
+ value: '{"--p-primary":"#4f8ef7","--p-primary-color":"#4f8ef7","--p-primary-hover-color":"#5a9af8"}'
65
+ ```
66
+
67
+ ### What's actually happening
68
+
69
+ The facade reads `css_variables` (a JSON string), sends it down to the host as `AppConfig.customization.cssVariables`, the host writes those onto `:root` and forwards them into every child iframe via the proxy injection. So one place edits, everything updates.
70
+
71
+ > The default base color is `rgb(0, 95, 178)`. The full 11-step shade scale (50–950) **auto-derives** from your base via `color-mix()` — you only set `--p-primary`. See ["Choosing custom colors"](#reference--choosing-custom-colors-luminosity-wcag) for luminosity rules so light-mode buttons keep AA contrast against white text.
72
+
73
+ ### When to graduate to Level 2
74
+
75
+ When you find yourself wanting any of:
76
+
77
+ - A complete surface scale (`--p-surface-0..950`) shifted to brand-specific neutrals
78
+ - Bespoke component CSS (custom button variants, badge styles, dialog padding)
79
+ - BEM-class overrides for the host chrome (sidebar, chat messages, splitter)
80
+
81
+ …you're at Level 2.
82
+
83
+ ---
84
+
85
+ ## Level 2 — Full theming (palette + custom CSS, via facade)
86
+
87
+ **You want:** a fully-branded product. Custom palette, custom component variants, host chrome that matches your design system. Both light and dark modes look intentional.
88
+
89
+ **You change:** the same facade dependency, but with three richer scopes:
90
+
91
+ | Facade parameter | Reaches | Use for |
92
+ |---|---|---|
93
+ | `css_variables` / `custom_css` | **Host + all children** (global) | App-wide tokens (`--p-*`), accent palette, global custom-css that has to apply identically in host and inside iframes (e.g. font family, kbd pill styling) |
94
+ | `host_css_variables` / `host_custom_css` | **Host UI only** | `--wippy-host-*` overrides; BEM class overrides scoped to `.wippy-host-app` (sidebar, chat messages, splitter) |
95
+ | `children_css_variables` / `children_custom_css` | **Child iframes only** | CSS that should apply inside child apps but not leak into host chrome |
96
+
97
+ > **Why three scopes:** `customCSS` injection is shared by default — a `.chat-message { ... }` rule written at the global scope will affect both the host's chat AND any child page that happens to have a `.chat-message` class. Splitting by scope avoids leak.
98
+
99
+ ### Sample (drewaltizer-wippy reference implementation)
100
+
101
+ This is the canonical full-theming reference. See `C:/Projects/drewaltizer-wippy/src/drewapp/deps/_index.yaml` (the `facade` dependency entry) for the live source.
102
+
103
+ ```yaml
104
+ - name: facade
105
+ kind: ns.dependency
106
+ component: wippy/facade
107
+ parameters:
108
+ - name: server
109
+ value: app:gateway
110
+ - name: router
111
+ value: app:api.public
112
+ - name: app_title
113
+ value: Keeper
114
+ - name: app_icon
115
+ value: "tabler:camera"
116
+ - name: hide_nav_bar
117
+ value: "true"
118
+
119
+ # Full palette: neutrals + accents + severities + custom --k-* extensions
120
+ - name: css_variables
121
+ value: |
122
+ {
123
+ "--p-primary": "#4f8ef7",
124
+ "--p-primary-color": "#4f8ef7",
125
+ "--p-primary-hover-color": "#5a9af8",
126
+ "--p-info": "#4f8ef7",
127
+ "--p-danger": "#f44336",
128
+ "--p-warn": "#ffc107",
129
+ "--p-accent": "#ff9800",
130
+ "--p-success": "#4caf50",
131
+ "--p-surface-0": "#ffffff",
132
+ "--p-surface-50": "#fafafa",
133
+ "--p-surface-100": "#f2f2f2",
134
+ "--p-surface-200": "#e2e2e2",
135
+ "--p-surface-300": "#cccccc",
136
+ "--p-surface-400": "#9a9a9a",
137
+ "--p-surface-500": "#6b6b6b",
138
+ "--p-surface-600": "#4a4a4a",
139
+ "--p-surface-700": "#383838",
140
+ "--p-surface-800": "#2c2c2c",
141
+ "--p-surface-900": "#242424",
142
+ "--p-surface-950": "#1a1a1a",
143
+ "--p-text-muted-color": "#888888",
144
+ "--p-content-border-color": "var(--p-surface-700)",
145
+ "--p-form-field-focus-border-color": "var(--p-primary)",
146
+ "--p-form-field-border-radius": "5px",
147
+ "--p-button-border-radius": "4px",
148
+ "--p-card-border-radius": "6px",
149
+ "--p-dialog-border-radius": "10px",
150
+ "--p-chip-border-radius": "12px",
151
+ "--p-badge-border-radius": "10px",
152
+ "--k-surface-raised": "#1e1e1e",
153
+ "--k-wav": "#9c59d1",
154
+ "--k-avatar-gradient": "linear-gradient(135deg,#4f8ef7,#9c59d1)",
155
+ "--k-press-approved-bg": "#1a2e1a",
156
+ "--k-press-unapproved-bg": "#311515"
157
+ }
158
+
159
+ # App-wide font family + custom component variants. Applied to host AND children.
160
+ - name: custom_css
161
+ value: |
162
+ body, :root { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
163
+
164
+ /* Keyboard shortcut pill */
165
+ kbd {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ min-width: 20px;
170
+ height: 20px;
171
+ padding: 0 6px;
172
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
173
+ font-size: 10px;
174
+ color: var(--p-surface-900);
175
+ background: var(--p-surface-100);
176
+ border: 1px solid var(--p-surface-300);
177
+ border-radius: 3px;
178
+ }
179
+ @media (prefers-color-scheme: dark) {
180
+ kbd {
181
+ color: var(--p-surface-100);
182
+ background: var(--p-surface-800);
183
+ border-color: var(--p-surface-700);
184
+ }
185
+ }
186
+
187
+ /* Outlined.tinted button — persistent tint */
188
+ .p-button.p-button-outlined.p-button-tinted {
189
+ background: color-mix(in srgb, var(--p-primary) 13%, transparent);
190
+ }
191
+ .p-button.p-button-outlined.p-button-tinted:enabled:hover {
192
+ background: color-mix(in srgb, var(--p-primary) 20%, transparent);
193
+ }
194
+
195
+ /* Badge tint variants */
196
+ .p-badge.p-badge-success.p-badge-tinted {
197
+ background: color-mix(in srgb, var(--p-success) 20%, transparent);
198
+ color: var(--p-success);
199
+ border: 1px solid color-mix(in srgb, var(--p-success) 40%, transparent);
200
+ }
201
+
202
+ # Host-only chrome tweaks. Use BEM classes scoped to `.wippy-host-app`.
203
+ - name: host_custom_css
204
+ value: |
205
+ .wippy-host-app .session-selector { display: none; }
206
+ .wippy-host-app .chat-message__footer { display: none; }
207
+
208
+ - name: host_css_variables
209
+ value: '{"--wippy-host-sidebar-width-open":"20rem","--wippy-host-message-radius":"0.5rem"}'
210
+ ```
211
+
212
+ ### Custom `--k-*` tokens — extending the system
213
+
214
+ The `--p-*` namespace is reserved for PrimeVue-aligned tokens. For app-specific extensions, use a project-prefixed namespace like `--k-*` (Keeper), `--app-*`, etc. The drewaltizer sample defines `--k-surface-raised`, `--k-wav`, `--k-avatar-gradient`, `--k-press-approved-bg`, `--k-press-unapproved-bg` — they cascade like any CSS variable but don't collide with future `--p-*` additions.
215
+
216
+ ### Component-level overrides via `custom_css`
217
+
218
+ PrimeVue components expose stable class names (`.p-button`, `.p-button-outlined`, `.p-badge.p-badge-tinted`, `.p-accordionheader`, etc.). You can target them in `custom_css` to add variants, change padding, change border-radius, hide built-ins, etc. See drewaltizer's `_index.yaml:212-409` for ~200 lines of examples covering buttons, badges, accordions, EXIF tables, and gallery tiles.
219
+
220
+ ### Cross-iframe leak — scope with `.wippy-host-app`
221
+
222
+ `customCSS` is injected into both the host AND every child iframe. A bare `.chat-message { ... }` rule will hit child apps that happen to use the same class. To avoid that:
223
+
224
+ - Put host-only rules in `host_custom_css` (already scoped server-side)
225
+ - Or scope manually: `.wippy-host-app .my-thing { ... }`
226
+ - For child-only rules: `children_custom_css`
227
+
228
+ The host root element carries the `.wippy-host-app` class — child iframes do not — so prefixing with `.wippy-host-app ` is the canonical way to target host-only.
229
+
230
+ > **The drewaltizer-wippy `src/drewapp/deps/_index.yaml` file is the load-bearing reference.** When in doubt about full-theming syntax, read that file. It encodes the canonical mental model: facade-driven, three scopes, scoped BEM, custom `--k-*` extensions.
231
+
232
+ ---
233
+
234
+ ## Level 3 — Per-page theme override (configOverrides / runtime)
235
+
236
+ **You want:** a single page (one of many) that looks different. A demo page with a contrasting palette, an A/B test variant, a brand take-over for one experimental embed.
237
+
238
+ **You change:** that page's `package.json` (declarative) **or** set runtime overrides before `proxy.js` loads.
239
+
240
+ **You do NOT change:** the facade. Per-page overrides are local to the iframe that hosts the page; nothing else is affected.
241
+
242
+ ### Mechanism A — Declarative (registry entry YAML, primary)
243
+
244
+ The canonical place for per-page overrides is the `meta.config_overrides` block of the page's `registry.entry` YAML. Wippy compiles this block into the `WippyPageEntry.configOverrides` field that the host reads at request time and injects into the iframe srcdoc — no rebuild required to change theming.
245
+
246
+ ```yaml
247
+ # src/<yourapp>/views/_index.yaml
248
+ - name: iframe-demo-themed
249
+ kind: registry.entry
250
+ meta:
251
+ type: view.page
252
+ name: iframe-demo-themed
253
+ title: Iframe Demo (Custom Palette)
254
+ url: /app
255
+ base_path: app/iframe-demo
256
+ entry_point: app.html
257
+ config_overrides:
258
+ customization:
259
+ cssVariables:
260
+ "--p-primary": "#9c59d1"
261
+ "--p-primary-color": "#9c59d1"
262
+ "--p-content-background": "#1a0d22"
263
+ "@light":
264
+ "--p-content-background": "#faf5ff"
265
+ "--p-text-color": "#1a0d22"
266
+ "@dark":
267
+ "--p-content-background": "#1a0d22"
268
+ "--p-text-color": "#f4f0ff"
269
+ customCSS: |
270
+ .demo-banner { background: var(--p-primary-color); color: var(--p-primary-contrast-color); padding: 0.5rem 1rem; }
271
+ proxy:
272
+ enabled: true
273
+ css:
274
+ fonts: true
275
+ theme_config: true
276
+ iframe: true
277
+ prime_vue: true
278
+ custom_css: true
279
+ custom_variables: true
280
+ tailwind_config: true
281
+ iconify_icons: true
282
+ ```
283
+
284
+ > **Shape note.** `config_overrides` (snake-case in YAML — Wippy converts to `configOverrides` for the JS API) matches the `AppConfigOverrides` interface from `@wippy-fe/proxy`: `cssVariables`, `customCSS`, `i18n`, `iconSets` all nest under `customization`. Sibling fields like `axiosDefaults`, `routePrefix`, `apiRoutes` sit at the top level of `config_overrides`. The CSS variable map keys themselves stay as written (`--p-*`, `@dark`, `@light`).
285
+
286
+ > **Why YAML over package.json.** YAML is registry-native — operators can patch `config_overrides` from a customer-specific overlay or a Wippy admin tool without rebuilding the FE bundle. Lua tools and other modules can also `registry:get` the entry and read the same block. package.json `wippy.configOverrides` (below) is a duplicate for the host-less rendering path, not the source of truth.
287
+
288
+ > **Dual-mode `@dark` / `@light` keys.** `cssVariables` accepts two special keys — `@dark` and `@light`. Their values are flat string maps that compile to `@media (prefers-color-scheme: dark) { :root { ... } }` and the matching light-mode media query. **They are NOT triggered by a `[data-theme="dark"]` attribute** — only by the OS preference. Use `@dark`/`@light` for the bulk of mode-specific theming (palette swaps, component-token overrides); the top level of `cssVariables` holds defaults that don't depend on mode.
289
+
290
+ ### Mechanism A2 — package.json mirror (host-less mode)
291
+
292
+ Duplicate the same shape under `wippy.configOverrides` in the page's `package.json`:
293
+
294
+ ```jsonc
295
+ {
296
+ "name": "@example/iframe-demo",
297
+ "wippy": {
298
+ "type": "page",
299
+ "title": "Iframe Demo",
300
+ "path": "dist/app.html",
301
+ "configOverrides": {
302
+ "customization": {
303
+ "cssVariables": { "...same as YAML...": "" }
304
+ }
305
+ }
306
+ }
307
+ }
308
+ ```
309
+
310
+ The package.json copy is what renders the page when no Wippy host is present — for example, the standalone dev-proxy preview, a unit test that mounts `app.html` directly, or any environment where the iframe is loaded without the host's srcdoc-injection step. With a host present, the YAML wins (because the host applies it via `WippyPageEntry.configOverrides` before the iframe boots); the package.json copy is then redundant but harmless.
311
+
312
+ **Keep both copies in sync.** When you edit one, edit the other. The YAML block is the canonical source.
313
+
314
+ ### Mechanism B — Runtime (`window.__WIPPY_CONFIG_OVERRIDES__`)
315
+
316
+ Set the global before `proxy.js` runs. Useful when the override depends on a query param, a feature flag, or test harness state.
317
+
318
+ ```html
319
+ <!-- app.html (head section, BEFORE the proxy script tag) -->
320
+ <script>
321
+ // Read a query param: ?theme=purple → load a purple variant
322
+ const params = new URLSearchParams(window.location.search)
323
+ if (params.get('theme') === 'purple') {
324
+ window.__WIPPY_CONFIG_OVERRIDES__ = {
325
+ customization: {
326
+ cssVariables: {
327
+ '--p-primary': '#9c59d1',
328
+ '--p-primary-color': '#9c59d1',
329
+ },
330
+ customCSS: '.demo-banner { background: var(--p-primary-color); }',
331
+ },
332
+ }
333
+ }
334
+ </script>
335
+ <!-- The host's @wippy/scripts injection happens here -->
336
+ <script type="text/javascript" data-role="@wippy/scripts"></script>
337
+ ```
338
+
339
+ ### Merge semantics — what overrides what
340
+
341
+ When the host applies AppConfig (in order, last writer wins):
342
+
343
+ 1. `theme-config.css` defaults (the dev-time fallback)
344
+ 2. Facade `css_variables` / `custom_css` (host-wide)
345
+ 3. Page `wippy.configOverrides` (declarative, baked into the page)
346
+ 4. `window.__WIPPY_CONFIG_OVERRIDES__` (runtime, if set before proxy loads)
347
+
348
+ For object-shape fields:
349
+
350
+ | Field | Merge behavior |
351
+ |---|---|
352
+ | `cssVariables` | **Replace** — the override map fully replaces the AppConfig map (write the full set you want, don't expect partial merge) |
353
+ | `customCSS` | **Replace** — concat into one block when you write it; the override replaces wholesale |
354
+ | `icons` / `iconSets` | **Additive** — merged with facade-provided icons |
355
+ | `feature.*` | **Deep merge** — boolean flags merged per key |
356
+
357
+ ### Sample wired into iframe-demo
358
+
359
+ `app-template/frontend/applications/iframe-demo/` is the working demo. Its `package.json` description says "theme override support"; the chart and mermaid pages use `var(--p-primary)`, `var(--p-danger)`, etc., so an override visibly flips them. Wire-up sample:
360
+
361
+ ```jsonc
362
+ // app-template/frontend/applications/iframe-demo/package.json
363
+ {
364
+ "wippy": {
365
+ "type": "page",
366
+ "title": "Iframe Demo",
367
+ "configOverrides": {
368
+ "customization": {
369
+ "cssVariables": {
370
+ "--p-primary": "#9c59d1",
371
+ "--p-primary-color": "#9c59d1",
372
+ "--p-info": "#06b6d4",
373
+ "--p-danger": "#dc2626",
374
+ "--p-warn": "#facc15",
375
+ "--p-success": "#16a34a",
376
+ "--p-content-background": "#1a0d22",
377
+ "--p-text-color": "#f4f0ff"
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ ```
384
+
385
+ Open the iframe-demo page side-by-side with the main app — same component palette, fully different look. That's Level 3 in action.
386
+
387
+ ### When NOT to use this
388
+
389
+ - **Don't ship per-page overrides as your *primary* theming**. If most of your app is purple-flavored, the facade is the right place — then per-page overrides are reserved for genuine exceptions.
390
+ - **Don't override `--p-surface-*` per-page unless you also override the semantic variables that depend on them**. The semantic vars (`--p-text-color`, `--p-content-background`) reference different `--p-surface-N` per mode; overriding only the surfaces, not the semantic mappings, can produce broken contrast in the opposite mode.
391
+ - **Don't use Mechanism B (runtime) when Mechanism A (declarative) works**. Declarative is reproducible across reloads, surfaces in code review, and doesn't depend on script-load order.
392
+
393
+ ---
394
+
395
+ # Reference
396
+
397
+ The remaining sections are the precise contract: which variables exist, which Tailwind utilities they back, how dark mode flips, and which colors actually pass WCAG. Use these to verify your overrides.
398
+
399
+ ## Reference — CSS variables
400
+
401
+ All variables are defined in `theme-config.css` and set on `:root`. At runtime, the host injects the real theme — these serve as the dev-time fallback and contract.
402
+
403
+ ### Primary palette (11 vars)
404
+
405
+ Base: `--p-primary` (default: `rgb(0, 95, 178)`)
406
+
407
+ | Variable | Value |
408
+ |---|---|
409
+ | `--p-primary-50` | `color-mix(in srgb, var(--p-primary) 5%, white)` |
410
+ | `--p-primary-100` | `color-mix(in srgb, var(--p-primary) 10%, white)` |
411
+ | `--p-primary-200` | `color-mix(in srgb, var(--p-primary) 20%, white)` |
412
+ | `--p-primary-300` | `color-mix(in srgb, var(--p-primary) 30%, white)` |
413
+ | `--p-primary-400` | `color-mix(in srgb, var(--p-primary) 40%, white)` |
414
+ | `--p-primary-500` | `var(--p-primary)` (base) |
415
+ | `--p-primary-600` | `color-mix(in srgb, var(--p-primary) 80%, black)` |
416
+ | `--p-primary-700` | `color-mix(in srgb, var(--p-primary) 70%, black)` |
417
+ | `--p-primary-800` | `color-mix(in srgb, var(--p-primary) 60%, black)` |
418
+ | `--p-primary-900` | `color-mix(in srgb, var(--p-primary) 50%, black)` |
419
+ | `--p-primary-950` | `color-mix(in srgb, var(--p-primary) 40%, black)` |
420
+
421
+ ### Secondary palette (11 vars)
422
+
423
+ Base: `--p-secondary` (default: `#6f7385`)
424
+
425
+ Same numbered scale pattern as primary (50–950) using `color-mix`.
426
+
427
+ ### Surface palette (13 vars)
428
+
429
+ | Variable | Light value | Dark value |
430
+ |---|---|---|
431
+ | `--p-surface-0` | `#fff` | `#fff` |
432
+ | `--p-surface-50` | `#fafafa` | `#fafafa` |
433
+ | `--p-surface-100` | `#f4f4f5` | `#f4f4f5` |
434
+ | `--p-surface-200` | `#e4e4e7` | `#e4e4e7` |
435
+ | `--p-surface-300` | `#d4d4d8` | `#d4d4d8` |
436
+ | `--p-surface-400` | `#a1a1aa` | `#a1a1aa` |
437
+ | `--p-surface-500` | `#71717a` | `#71717a` |
438
+ | `--p-surface-600` | `#52525b` | `#545250` (warm) |
439
+ | `--p-surface-700` | `#3f3f46` | `#403e3c` (warm) |
440
+ | `--p-surface-800` | `#27272a` | `#2b2927` (warm) |
441
+ | `--p-surface-900` | `#18181b` | `#1c1a19` (warm) |
442
+ | `--p-surface-950` | `#09090b` | `#0f0e0d` (warm) |
443
+
444
+ **Fixed light-to-dark scale** — 0 is always lightest, 950 always darkest. The scale does NOT flip with dark mode. In dark mode, only 600–950 get warmer undertones.
445
+
446
+ ### Semantic variables (15 vars)
447
+
448
+ These **flip with dark mode** — use these for theme-dependent styling.
449
+
450
+ | Variable | Light | Dark |
451
+ |---|---|---|
452
+ | `--p-primary-color` | `primary-500` | `primary-400` |
453
+ | `--p-primary-contrast-color` | `surface-0` | `surface-900` |
454
+ | `--p-primary-hover-color` | `primary-600` | `primary-300` |
455
+ | `--p-primary-active-color` | `primary-700` | `primary-200` |
456
+ | `--p-text-color` | `surface-700` | `surface-0` |
457
+ | `--p-text-hover-color` | `surface-800` | `surface-0` |
458
+ | `--p-text-muted-color` | `surface-500` | `surface-400` |
459
+ | `--p-text-hover-muted-color` | `surface-600` | `surface-300` |
460
+ | `--p-content-background` | — | `surface-900` (via host) |
461
+ | `--p-content-border-color` | `surface-200` | `surface-700` |
462
+ | `--p-content-hover-background` | `surface-100` | `surface-800` |
463
+ | `--p-content-hover-color` | `surface-800` | `surface-0` |
464
+ | `--p-highlight-background` | `primary-50` | `primary-400 @ 16%` |
465
+ | `--p-highlight-color` | `primary-700` | `white @ 87%` |
466
+ | `--p-highlight-focus-background` | `primary-100` | `primary-400 @ 24%` |
467
+ | `--p-highlight-focus-color` | `primary-800` | `white @ 87%` |
468
+ | `--p-content-border-radius` | `6px` | `6px` |
469
+
470
+ ## Reference — Tailwind utility classes
471
+
472
+ Provided by `tailwindcss-primeui` plugin (included in the shared Tailwind preset).
473
+
474
+ ### Color utilities
475
+
476
+ Work with `bg-`, `text-`, `border-`, `outline-`, `ring-` prefixes:
477
+
478
+ | Class suffix | Maps to |
479
+ |---|---|
480
+ | `primary` | `--p-primary-color` |
481
+ | `primary-emphasis` | `--p-primary-hover-color` |
482
+ | `primary-emphasis-alt` | `--p-primary-active-color` |
483
+ | `primary-contrast` | `--p-primary-contrast-color` |
484
+ | `primary-{0,50,100,...,950}` | Full primary shade range |
485
+ | `surface-{0,50,100,...,950}` | Full surface shade range |
486
+
487
+ ### Semantic utilities
488
+
489
+ | Class | Maps to |
490
+ |---|---|
491
+ | `.text-color` | `--p-text-color` |
492
+ | `.text-color-emphasis` | `--p-text-hover-color` |
493
+ | `.text-muted-color` | `--p-text-muted-color` |
494
+ | `.text-muted-color-emphasis` | `--p-text-hover-muted-color` |
495
+ | `.bg-emphasis` | Emphasis background + text |
496
+ | `.bg-highlight` | Highlighted state (selected items, active rows) |
497
+ | `.bg-highlight-emphasis` | Emphasized highlight |
498
+ | `.border-surface` | `--p-content-border-color` |
499
+ | `.rounded-border` | `--p-content-border-radius` |
500
+
501
+ ### Animation utilities
502
+
503
+ | Class | Description |
504
+ |---|---|
505
+ | `.animate-fadein` | Fade in |
506
+ | `.animate-fadeout` | Fade out |
507
+ | `.animate-slidedown` | Slide down |
508
+ | `.animate-slideup` | Slide up |
509
+ | `.animate-scalein` | Scale in |
510
+ | `.animate-fadeinleft` | Fade in from left |
511
+ | `.animate-fadeinright` | Fade in from right |
512
+ | `.animate-fadeinup` | Fade in from below |
513
+ | `.animate-fadeindown` | Fade in from above |
514
+ | `.animate-duration-{ms}` | Animation duration |
515
+ | `.animate-delay-{ms}` | Animation delay |
516
+ | `.animate-ease-*` | Easing functions |
517
+
518
+ ### Severity & accent color utilities (MANDATORY for semantic colors)
519
+
520
+ **Rule: Always use semantic severity classes over raw Tailwind color names when the color conveys meaning.** If a color represents an error, success, warning, info, or help state, you MUST use `danger-*`, `success-*`, `warn-*`, `info-*`, or `help-*` — never `red-*`, `green-*`, `orange-*`, `sky-*`, or `purple-*`. Raw Tailwind colors are only appropriate for purely decorative use where no semantic meaning is attached. Semantics first, decorative later.
521
+
522
+ Added by the shared Tailwind preset via `theme.extend.colors`. Each has a full 50–950 shade scale backed by CSS variables.
523
+
524
+ | Family | Base variable | Default color | Purpose |
525
+ |--------|--------------|---------------|---------|
526
+ | `danger` | `--p-danger` | `rgb(239, 68, 68)` (red-500) | Errors, destructive actions |
527
+ | `success` | `--p-success` | `rgb(34, 197, 94)` (green-500) | Success states, confirmations |
528
+ | `warn` | `--p-warn` | `rgb(249, 115, 22)` (orange-500) | Warnings, caution |
529
+ | `info` | `--p-info` | `rgb(14, 165, 233)` (sky-500) | Informational messages |
530
+ | `help` | `--p-help` | `rgb(168, 85, 247)` (purple-500) | Help, hints |
531
+ | `accent` | `--p-accent` | `rgb(20, 184, 166)` (teal-500) | Highlights, special callouts |
532
+
533
+ Usage with Tailwind prefixes (`bg-`, `text-`, `border-`, `outline-`, `ring-`):
534
+
535
+ ```
536
+ bg-danger-500 text-success-700 border-warn-200
537
+ bg-info-50 text-help-400 border-accent-300
538
+ ```
539
+
540
+ PrimeVue component CSS (button, tag, badge, toast, message, password) references these semantic names instead of hardcoded Tailwind color names.
541
+
542
+ **Overriding:** Set the base variable on `:root` to retheme all severity shades:
543
+
544
+ ```css
545
+ :root {
546
+ --p-danger: rgb(220, 38, 38); /* custom danger base */
547
+ }
548
+ ```
549
+
550
+ The full 50–950 scale auto-derives via `color-mix()`. No dark-mode override block is needed — component CSS picks the right shade per mode (e.g., `dark:bg-danger-400`). You may optionally override the base in `@media (prefers-color-scheme: dark)` for mode-specific tuning.
551
+
552
+ ### Secondary color utilities
553
+
554
+ Added by the shared Tailwind preset via `theme.extend.colors`:
555
+
556
+ ```
557
+ bg-secondary-{50..950}
558
+ text-secondary-{50..950}
559
+ border-secondary-{50..950}
560
+ ```
561
+
562
+ ## Reference — Dark mode
563
+
564
+ Variables switch at `@media (prefers-color-scheme: dark)`. Key changes:
565
+
566
+ - `--p-primary` base shifts from `rgb(0, 95, 178)` to `rgb(0, 125, 178)` (brighter)
567
+ - `--p-primary-color` shifts from `primary-500` to `primary-400`
568
+ - `--p-content-background` shifts to `surface-900` (via host injection)
569
+ - `--p-text-color` shifts from `surface-700` to `surface-0`
570
+ - Surface 600–950 get warmer undertones in dark mode
571
+
572
+ ### Rules
573
+
574
+ - **Use semantic variables** (`--p-text-color`, `--p-content-background`) for colors that should flip with dark mode.
575
+ - **Use `surface-0`/`surface-950`** only when you explicitly need the fixed shade regardless of mode.
576
+ - **For derived shades**: `color-mix(in srgb, var(--p-content-background) 85%, var(--p-text-color) 15%)`.
577
+ - **Never use `--p-surface-N`** for theme-dependent colors — the numbered scale does NOT flip.
578
+
579
+ ## Reference — PrimeVue styling (opt-in)
580
+
581
+ The `primevue/` directory in `@wippy-fe/theme` contains CSS files that style PrimeVue component tags (`.p-button`, `.p-datatable`, etc.) using Tailwind `@apply` directives. These are **only needed if your component uses PrimeVue tags**.
582
+
583
+ ### Setup
584
+
585
+ 1. Add `primevue` as a dependency
586
+ 2. Import `PrimeVuePlugin` from `@wippy-fe/theme/primevue-plugin` — installs PrimeVue with `{ theme: 'none' }`
587
+ 3. Import PrimeVue CSS: `@import "@wippy-fe/theme/primevue/tailwind.css"` in your styles
588
+ 4. Add `'primeVueCssUrl'` to `hostCssKeys` so the host injects PrimeVue CSS at runtime
589
+
590
+ ### How it works
591
+
592
+ The CSS files use theme variables via Tailwind classes. Example from `button.css`:
593
+
594
+ ```css
595
+ .p-button {
596
+ @apply bg-primary text-primary-contrast border-primary
597
+ enabled:hover:bg-primary-emphasis enabled:active:bg-primary-emphasis-alt ...
598
+ }
599
+ ```
600
+
601
+ The `tailwind.css` master file imports all component CSS files organized by category (form, button, data, overlay, menu, panel, file, message, media, misc).
602
+
603
+ ### Available PrimeVue component styles
604
+
605
+ **Form**: autocomplete, cascadeselect, checkbox, colorpicker, datepicker, iconfield, iftalabel, inputgroup, inputnumber, inputotp, inputtext, floatlabel, knob, listbox, multiselect, password, radiobutton, rating, select, selectbutton, slider, textarea, togglebutton, toggleswitch, treeselect
606
+
607
+ **Button**: button, buttongroup, speeddial, splitbutton
608
+
609
+ **Data**: datatable, dataview, paginator, picklist, orderlist, organizationchart, timeline, tree, treetable
610
+
611
+ **Overlay**: confirmdialog, confirmpopup, dialog, drawer, popover, tooltip
612
+
613
+ **Menu**: breadcrumb, contextmenu, dock, menu, menubar, megamenu, panelmenu, tieredmenu
614
+
615
+ **Panel**: accordion, card, divider, fieldset, panel, scrollpanel, splitter, stepper, tabs, toolbar
616
+
617
+ **File**: fileupload
618
+
619
+ **Message**: message, toast
620
+
621
+ **Media**: carousel, galleria, image, imagecompare
622
+
623
+ **Misc**: avatar, badge, blockui, chip, inplace, metergroup, overlaybadge, progressbar, progressspinner, ripple, scrolltop, skeleton, tag, terminal
624
+
625
+ ## Reference — Choosing custom colors (luminosity, WCAG)
626
+
627
+ When overriding `--p-primary`, `--p-secondary`, or the surface palette, follow these guidelines to ensure contrast and readability across both modes.
628
+
629
+ ### How the shade scale works
630
+
631
+ The `color-mix()` system blends your base color with white (lighter shades) or black (darker shades). The percentages are fixed:
632
+
633
+ | Shade | Blend |
634
+ |---|---|
635
+ | 50 | 5% base + 95% white |
636
+ | 100 | 10% base + 90% white |
637
+ | 200 | 20% base + 80% white |
638
+ | 300 | 30% base + 70% white |
639
+ | 400 | 40% base + 60% white |
640
+ | **500** | **100% base (no blend)** |
641
+ | 600 | 80% base + 20% black |
642
+ | 700 | 70% base + 30% black |
643
+ | 800 | 60% base + 40% black |
644
+ | 900 | 50% base + 50% black |
645
+ | 950 | 40% base + 60% black |
646
+
647
+ This means the base color's luminosity determines the contrast of the **entire scale**. A too-light base produces a washed-out 500 that can't contrast against white text; a too-dark base makes 400 too dim for dark mode.
648
+
649
+ ### Primary color luminosity
650
+
651
+ The primary base is used at two critical contrast points:
652
+
653
+ 1. **Light mode**: `bg-primary` (500) with `text-primary-contrast` (white) — buttons, badges, checkboxes
654
+ 2. **Dark mode**: `bg-primary` (400 = 40% base + 60% white) with `text-primary-contrast` (surface-900, near-black)
655
+
656
+ **Recommended relative luminance for `--p-primary`: 0.05–0.15** (medium-dark, saturated).
657
+
658
+ | Example | RGB | Rel. luminance | Light contrast (500 vs white) | Dark contrast (400 vs black) |
659
+ |---|---|---|---|---|
660
+ | Default blue | `rgb(0, 95, 178)` | ~0.10 | ~8.5:1 (AAA) | ~6.8:1 (AA) |
661
+ | Deep teal | `rgb(0, 128, 128)` | ~0.14 | ~5.8:1 (AA) | ~8.7:1 (AAA) |
662
+ | Indigo | `rgb(79, 70, 229)` | ~0.07 | ~8.0:1 (AAA) | ~5.5:1 (AA) |
663
+ | Red | `rgb(185, 28, 28)` | ~0.05 | ~9.2:1 (AAA) | ~4.7:1 (borderline) |
664
+ | Forest green | `rgb(22, 101, 52)` | ~0.08 | ~7.2:1 (AAA) | ~6.1:1 (AA) |
665
+ | Orange | `rgb(194, 120, 3)` | ~0.18 | ~3.7:1 (FAIL) | ~9.5:1 (AAA) |
666
+
667
+ **Key constraints:**
668
+ - Luminance **> 0.18** → 500 shade is too light for white text (fails WCAG AA 4.5:1 in light mode)
669
+ - Luminance **< 0.04** → 400 shade is too dark for dark-on-light text (fails WCAG AA in dark mode)
670
+ - The sweet spot is **0.06–0.14** — both modes pass WCAG AA (4.5:1), with most passing AAA (7:1)
671
+ - Pure, saturated colors (high chroma) work best. Desaturated/muted colors lose contrast faster
672
+
673
+ **If your brand color is too light** (e.g., orange, yellow, lime): darken it for `--p-primary` and use the original as a secondary or accent. Light-mode buttons with white text will fail contrast otherwise.
674
+
675
+ **If your brand color is too dark** (e.g., navy, dark brown): lighten it slightly. The 400 shade in dark mode needs enough contrast against `surface-900`.
676
+
677
+ ### Secondary color luminosity
678
+
679
+ Secondary is used for muted/deemphasized elements — less critical than primary but still appears in text and borders.
680
+
681
+ **Recommended relative luminance for `--p-secondary`: 0.10–0.25** (mid-range, desaturated).
682
+
683
+ The default `#6f7385` (luminance ~0.16) is a muted gray-purple that reads as neutral in both modes. Good secondary colors:
684
+ - Are **less saturated** than primary (avoid competing for attention)
685
+ - Have **mid-range luminance** so the full shade scale is usable
686
+ - Work as `text-secondary-600` on white and `text-secondary-300` on dark backgrounds
687
+
688
+ ### Surface palette
689
+
690
+ Surfaces are a fixed grayscale ramp — they do NOT flip with dark mode. The semantic variables (`--p-content-background`, `--p-text-color`) reference different points on this scale per mode.
691
+
692
+ **Light mode assignments:**
693
+
694
+ | Role | Surface | Hex | Purpose |
695
+ |---|---|---|---|
696
+ | Page background | 0 | `#ffffff` | Main canvas |
697
+ | Card/container background | 0 | `#ffffff` | Content containers |
698
+ | Hover background | 100 | `#f4f4f5` | Interactive hover state |
699
+ | Border | 200 | `#e4e4e7` | Borders, dividers |
700
+ | Muted text | 500 | `#71717a` | Secondary text, labels |
701
+ | Body text | 700 | `#3f3f46` | Primary readable text |
702
+ | Strong emphasis | 800 | `#27272a` | Headings, hover text |
703
+
704
+ **Dark mode assignments:**
705
+
706
+ | Role | Surface | Hex | Purpose |
707
+ |---|---|---|---|
708
+ | Page background | 900 | `#1c1a19` (warm) | Main canvas |
709
+ | Card/container background | 800 | `#2b2927` (warm) | Elevated containers |
710
+ | Hover background | 800 | `#2b2927` (warm) | Interactive hover state |
711
+ | Border | 700 | `#403e3c` (warm) | Borders, dividers |
712
+ | Muted text | 400 | `#a1a1aa` | Secondary text, labels |
713
+ | Body text | 0 | `#ffffff` | Primary readable text |
714
+ | Strong emphasis | 0 | `#ffffff` | Headings, hover text |
715
+
716
+ **Dark mode warm tones** (600–950): The default surfaces shift from cool zinc grays to slightly warm brown-grays in dark mode. This reduces visual fatigue on dark backgrounds. The shift is subtle — `#545250` vs `#52525b` for surface-600 — but creates a warmer, more natural feel.
717
+
718
+ When providing custom surfaces, keep these rules:
719
+ - **0 must always be `#ffffff`** (or very near it) — used as contrast text in dark mode
720
+ - **950 must always be very dark** — used as the maximum-contrast anchor
721
+ - **The scale must increase monotonically in darkness** — no inversions
722
+ - **600–950 should shift warm** in dark mode for visual comfort (optional but recommended)
723
+ - **Don't change 0–500** between modes unless you have a specific reason — the semantic variables handle mode switching
724
+
725
+ ### Contrast verification
726
+
727
+ After choosing colors, verify these critical pairs meet WCAG AA (4.5:1 minimum):
728
+
729
+ | Pair | Light mode | Dark mode |
730
+ |---|---|---|
731
+ | `bg-primary` + `text-primary-contrast` | primary-500 vs white | primary-400 vs surface-900 |
732
+ | `bg-highlight` + text | primary-50 vs primary-700 | primary-400@16% vs white@87% |
733
+ | Body text on background | surface-700 vs white | surface-0 vs surface-900 |
734
+ | Muted text on background | surface-500 vs white | surface-400 vs surface-900 |
735
+
736
+ Quick formula for relative luminance of an sRGB color:
737
+ ```
738
+ L = 0.2126 * R' + 0.7152 * G' + 0.0722 * B'
739
+ where R' = (R/255)^2.2 (simplified gamma)
740
+
741
+ Contrast ratio = (L_lighter + 0.05) / (L_darker + 0.05)
742
+ ```
743
+
744
+ WCAG AA requires 4.5:1 for normal text, 3:1 for large text (18px+ bold or 24px+ regular).
745
+
746
+ ## Reference — Host UI customization (`--wippy-host-*` + BEM classes)
747
+
748
+ The Wippy host exposes CSS custom properties and BEM class names on its core UI components. Override these via the facade's `host_css_variables` / `host_custom_css` parameters (Level 2) or via `AppConfig.customization.cssVariables` / `customCSS` (Level 3) to restyle the host without forking.
749
+
750
+ > **Important:** Because `customCSS` and `cssVariables` are injected into both the host AND all nested child iframes by default, always scope host-only overrides to `.wippy-host-app`. For example, use `.wippy-host-app .chat-message { ... }` — never `.chat-message { ... }` alone, or the styles will leak into child iframes. (Putting them in `host_custom_css` does this server-side.)
751
+
752
+ ### Layout & sidebar
753
+
754
+ | Variable | Default | Description |
755
+ |---|---|---|
756
+ | `--wippy-host-sidebar-width-open` | `16rem` | Sidebar width when expanded |
757
+ | `--wippy-host-sidebar-width-closed` | `3.5rem` | Sidebar width when collapsed |
758
+
759
+ **BEM classes** (scope with `.wippy-host-app`):
760
+
761
+ | Class | Element |
762
+ |---|---|
763
+ | `.layout` | Root layout wrapper |
764
+ | `.layout__sidebar` | Sidebar container |
765
+ | `.layout__sidebar-header` | Sidebar header (logo + toggle) |
766
+ | `.layout__sidebar-nav` | Navigation list area |
767
+ | `.layout__main` | Main content area (right of sidebar) |
768
+
769
+ ### Splitter gutter
770
+
771
+ The resizable panel divider between main content and the right panel.
772
+
773
+ | Variable | Default | Description |
774
+ |---|---|---|
775
+ | `--wippy-host-splitter-width` | `1px` | Visible line width |
776
+ | `--wippy-host-splitter-hit-area` | `10px` | Draggable hit area width (transparent) |
777
+ | `--wippy-host-splitter-color` | `var(--p-surface-200)` (light) / `var(--p-surface-600)` (dark) | Line color |
778
+
779
+ ### Chat messages
780
+
781
+ | Variable | Default | Description |
782
+ |---|---|---|
783
+ | `--wippy-host-message-radius` | `1rem` | Message bubble border radius |
784
+ | `--wippy-host-message-padding-x` | `1rem` | Message horizontal padding |
785
+ | `--wippy-host-message-padding-y` | `0.5rem` | Message vertical padding |
786
+ | `--wippy-host-message-user-bg` | `var(--p-primary-50)` | User message background |
787
+ | `--wippy-host-message-agent-bg` | `var(--p-yellow-50)` (light) / `var(--p-surface-800)` (dark) | Agent message background |
788
+ | `--wippy-host-tool-bg` | `var(--p-help-50)` | Tool call background |
789
+ | `--wippy-host-tool-border` | `var(--p-help-300)` | Tool call left border |
790
+ | `--wippy-host-avatar-size` | `2rem` | Message avatar diameter |
791
+
792
+ **BEM classes** (scope with `.wippy-host-app`):
793
+
794
+ | Class | Element |
795
+ |---|---|
796
+ | `.chat-message` | Message row container |
797
+ | `.chat-message--user` | User message modifier |
798
+ | `.chat-message--agent-message` | Agent message modifier |
799
+ | `.chat-message--tool` | Tool call message modifier |
800
+ | `.chat-message--error` | Error message modifier |
801
+ | `.chat-message__avatar` | Avatar wrapper |
802
+ | `.chat-message__avatar-icon` | Avatar icon circle |
803
+ | `.chat-message__content` | Message bubble |
804
+ | `.chat-message__body` | Message text content |
805
+ | `.chat-message__footer` | Timestamp row |
806
+ | `.chat-message__tool-name` | Tool name label |
807
+ | `.chat-message__tool-icon` | Tool icon |
808
+ | `.chat-message__agent-content` | Agent name system line |
809
+ | `.chat-message__model-content` | Model name system line |
810
+ | `.chat-message__files` | Attached files row |
811
+ | `.chat-tool-group` | Inline tool call badge group |
812
+ | `.chat-tool-group__badge` | Individual tool badge |
813
+ | `.chat-tool-group__badge--success` | Completed tool badge |
814
+ | `.chat-tool-group__badge--error` | Failed tool badge |
815
+ | `.chat-tool-group__badge--processing` | In-progress tool badge |
816
+ | `.chat-tool-group__icon` | Badge icon |
817
+
818
+ ### Chat input
819
+
820
+ **BEM classes** (scope with `.wippy-host-app`):
821
+
822
+ | Class | Element |
823
+ |---|---|
824
+ | `.chat-input` | Input bar container |
825
+ | `.chat-input__group` | Input field + buttons wrapper |
826
+ | `.chat-input__textarea` | Message textarea |
827
+ | `.chat-input__attach-button` | Attachment button |
828
+ | `.chat-input__send-button` | Send button |
829
+ | `.chat-input__stop-button` | Stop generation button |
830
+ | `.chat-input__upload-list` | Upload queue list |
831
+ | `.chat-input__prompts` | Suggested prompts area |
832
+
833
+ ### Chat container
834
+
835
+ **BEM classes** (scope with `.wippy-host-app`):
836
+
837
+ | Class | Element |
838
+ |---|---|
839
+ | `.chat-container` | Outer chat wrapper |
840
+ | `.chat-container--selected` | Has active session |
841
+ | `.chat-container--non-selected` | No session selected |
842
+ | `.chat-container__empty-state` | Empty state wrapper |
843
+ | `.chat-container__empty-state-icon` | Empty state icon |
844
+ | `.chat-container__empty-state-title` | Empty state heading |
845
+ | `.chat-container__empty-state-description` | Empty state text |
846
+ | `.chat-container__drop-zone` | File drag-and-drop overlay |
847
+ | `.chat-container__drop-zone-icon` | Drop zone icon |
848
+
849
+ ### Session selector
850
+
851
+ **BEM classes** (scope with `.wippy-host-app`):
852
+
853
+ | Class | Element |
854
+ |---|---|
855
+ | `.session-selector` | Selector wrapper |
856
+ | `.session-selector__dropdown` | Dropdown component |
857
+ | `.session-selector__option` | Session option row |
858
+ | `.session-selector__active-dot` | Active session indicator |
859
+
860
+ ### Root
861
+
862
+ | Class | Element |
863
+ |---|---|
864
+ | `.wippy-host-app` | Application root element — scope all host-only CSS overrides to this |
865
+
866
+ ### Example: custom-styled host
867
+
868
+ ```css
869
+ /* Via facade host_custom_css, OR via AppConfig.customization.customCSS */
870
+
871
+ /* Variables are already prefixed — no scoping needed */
872
+ :root {
873
+ --wippy-host-message-radius: 0.5rem;
874
+ --wippy-host-message-user-bg: #e0f2fe;
875
+ --wippy-host-sidebar-width-open: 20rem;
876
+ }
877
+
878
+ /* Class selectors MUST be scoped to .wippy-host-app */
879
+ .wippy-host-app .chat-message__footer {
880
+ display: none; /* hide timestamps */
881
+ }
882
+
883
+ .wippy-host-app .session-selector {
884
+ display: none; /* hide session picker */
885
+ }
886
+ ```
887
+
888
+ ---
889
+
890
+ ## Related docs
891
+
892
+ - **`README.md`** — frontend docs index
893
+ - **`app-guide.md`** — building Wippy web apps; covers `proxy.injections.css.themeConfig` and the standard injection block
894
+ - **`component-guide.md`** — building Wippy web components; `hostCssKeys` controls which platform CSS gets loaded into a component's Shadow DOM
895
+ - **`best-practices.md`** — general Vue/Tailwind conventions; defers theming to this doc
896
+ - **`host-spec.md`** — host runtime contract for `wippy.proxy.injections` keys
897
+ - **`proxy-api.md`** — runtime proxy API; documents which `--p-*` and `--wippy-host-*` vars `<wippy-loading>` and `<wippy-error>` consume
898
+
899
+ External:
900
+
901
+ - **`@wippy-fe/theme/THEMING.md`** — same content as this file, shipped with the theme package (`node_modules/@wippy-fe/theme/THEMING.md` after install)
902
+ - **drewaltizer-wippy** (`C:/Projects/drewaltizer-wippy/src/drewapp/deps/_index.yaml`) — full Level 2 reference implementation
903
+ - **`app-template/frontend/applications/iframe-demo/`** — Level 3 reference implementation
904
+ - **wippy.ai docs** (`https://wippy.ai/llms.txt`) — Lua-side facade documentation