Files
openobserve/web/src/styles/tailwind.css
T
1c849a9c71 refactor(web): migrate templates + shadows to tokens, delete the debt baseline (#13769)
## What

Deletes `web/scripts/design-debt-baseline.json` — **not by relaxing the
rules, by fixing the debt**. Every design-token category is now
zero-tolerance, SCSS is gone from the app, and 144 hand-written shadow
values collapse into one scale.

## The guard was under-reporting (three separate holes)

1. **Comment contamination.** `check-design-consistency.mjs` fed comment
text into its exemption checks, so a `keep()` comment that merely
*named* `:deep(` / `::after` / `@keyframes` silenced the rule after it —
and a `;` or `{` inside a comment corrupted the offsets, so stripping
comments afterwards couldn't help. Comments are now blanked
(offset-preserving) before the scan. **Uncovered 9 occurrences in 6
files.** Real debt was 110, not 101.
2. **`arbShadow` only scanned the template.** `box-shadow:` in a
`<style>` block and `boxShadow:` in a JS/`:style` object were invisible
— 40 literals sat there while the guard reported zero. It now scans all
three syntaxes across `.vue` and `.ts`.
3. **`rawVarInTemplate` missed the fallback form.**
`border-(--color-x,rgba(…))` and `bg-[var(--color-x,#fff)]` slipped
through. The token is always defined, so the fallback can never fire —
it only hid the read.

## stylelint had never run on 14 files

`postcss-scss` isn't installed, so `postcss-html` **silently skipped**
every `<style lang="scss">` block. The CI step named *"Lint styles
(stylelint — bans legacy --o2-* tokens)"* was a no-op for those files —
including `color-no-hex` and the `.body--dark` selector ban. All 14 are
plain CSS now and `vue/block-lang` errors on `scss`/`sass`/`less`, so it
cannot silently recur.

## What moved

| | before | after |
|---|---|---|
| `rawVarInComponent` (in `<style>`) | 110 | **0** |
| template `x-[var(--color-*)]`, utility exists | 225 | **0** |
| template reads, no utility | 27 | **0** |
| `style` / `:style` colour literals | 29 | **0** |
| `color-mix(token N%, transparent)` | 160 | **0** |
| hand-written shadows | 233 (144 distinct) | **0** |
| SFCs using SCSS | 14 | **0** |

Guard categories: 22 → 24. `rawVarInTemplate` and `arbShadow` are new;
`arbPx` was deleted because main's `local/no-hardcoded-px` ESLint rule
(#13541) now owns px across every file type.

## Shadows are a system now

Stripped of colour, the 144 values were the same handful of geometries
rewritten in px vs rem, with and without an explicit `0` spread.

```
--shadow-xs / sm / md / lg              elevation
--shadow-sticky-left / -right           directional (sticky columns)
--shadow-rail                           inset status rail
--shadow-glow-sm / -md / -lg            coloured, 3 intensities
--shadow-scroll-top / -bottom / -both   scroll affordance
--shadow-ring-hairline                  1px ring where a utility can't reach
--shadow-ai-focus                       AI composer focus glow (flips in dark)
```

Colour is a **separate axis**. Tailwind rewrites a `--shadow-*` token as
`<geometry> var(--tw-shadow-color, <default>)` — verified by compiling a
probe, and it holds for multi-layer tokens — so a template writes
`shadow-md shadow-ai-accent/35`. A plain stylesheet can't add a utility,
so the role and glow tokens read `--glow-color`; that keeps the pipeline
canvas colour-coding nodes by type from CSS and lets the status rail be
painted per row from JS.

Also reclassified: `0 0 0 2px <colour>` is a **ring**, not a shadow, and
`0 1px 0 0 <colour>` is a **border**.

## color-mix, analysed by intent

All 331 occurrences were classified rather than treated as one problem:

- **160** mixed a token with `transparent` — only an alpha change, so
it's Tailwind's `/N` modifier written the long way (verified: `/N`
compiles to `color-mix(in oklab, <token> N%, transparent)`, and mixing
with `transparent` is premultiplied, so the interpolation space doesn't
matter)
- **81** blended two colours — a derived colour, so it's a token now
(~50 minted)
- **75** were shadow/ring geometry, handled by the scale
- **8 gradients + 1 `calc()`-driven percentage** genuinely have no
utility form and remain; the guard exempts gradient/mask/filter
contexts, matching what the style-block counter already exempts

## Merge with main

Main was running the same migration in parallel (**#13541 "px to rem"**
removed all SCSS, added `no-hardcoded-px`, and built its own shadow
scale), so the merge collided in 68 files / 155 hunks. Resolved by
taking main's source files and re-applying this branch on top, with the
guards driving convergence (429 → 0) rather than hand-resolving hunks.

On shadows, **main's structure wins** — geometry in `semantic.css`
`@theme`, colours as `--shadow-*-c*` sub-tokens, wrapped in its px
disable; its `--shadow-sm/md/lg`, `--shadow-scroll-*` and
`--shadow-glow` keep their definitions. This branch adds what main
didn't have: `xs`, the directional/inset roles, and the
elevation-matched coloured glows.

## New tokens

No new *colours* — each names a value a component already hardcoded,
because a registered palette utility (`text-orange-500`) is banned by
`rawPalette`, so "just register the ramp" was never available.

`--color-splitter-hover`, `--color-status-flaky` (+`-text`),
`--color-chat-bubble-user-border`, `--color-thread-avatar-bg`,
`--color-thread-accent-strong`, `--color-menu-badge-ring`,
`--color-progress-track-fill`, `--color-ingest-accent-*` (5), ~50
derived blend tokens, plus `--color-border-strong` /
`--color-border-subtle` / `--color-warning-300` registered for utility
use.

## Visual deltas to review

Everything else is value-preserving. These are not:

- **4 hover accents** move from a fixed `primary-600` to `accent` — 600
→ 400 **in dark only** (2 chevrons, an empty-state icon fill, one link)
- **sticky table header** in `TraceDetailsSidebar` was a fixed light
grey that never flipped; now `border-default`, so it themes
- **shadow canonicalization** snaps near-identical geometries together
and moves ~40 hardcoded `rgba(0,0,0,x)` onto the scale's token colour

Highest-surface single file: `AlertSettingsHelpDrawer.vue` — a whole
style block deleted with ~44 class names moved into template utilities.

## Verification

`lint:design` · `lint:tokens` · `lint:token-purity` · `lint:styles` ·
`format:check` · `lint:ci` · `vue-tsc` — all clean.
**94 spec files covering every file this branch touches: 4395 passing, 0
failing.**

Not run: the full 1236-file unit suite, Playwright/E2E, and any browser
visual check.

---------

Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com>
Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech>
Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
Co-authored-by: Bhargav <BJP232004@GMAIL.COM>
2026-08-17 11:46:07 +00:00

205 lines
6.6 KiB
CSS

@import "tailwindcss";
@import "tw-animate-css";
/* Clear Tailwind's built-in colour palette BEFORE our tokens load, so only
registered design tokens generate colour utilities (see the file header). */
@import "./palette-reset.css";
/* Design tokens — these 4 CSS files are the sole source. */
@import "../lib/styles/tokens/base.css";
@import "../lib/styles/tokens/semantic.css";
@import "../lib/styles/tokens/component.css";
/* Element reset/typography layer. Imported here so its cascade order is kept. */
@import "./base-elements.css";
@import "../lib/styles/tokens/dark.css";
/* App-wide @keyframes referenced from template `[animation:…]` arbitrary
utilities — must be global, as Vue's scoped rewriter can't follow them. */
@import "./keyframes.css";
/* App-level `@utility` classes for treatments a template utility can't express
(pseudo-elements, non-utility CSS properties) but that are not one
component's private look. See the file header for when a rule belongs here. */
@import "./utilities.css";
@plugin "@tailwindcss/typography";
/* Bind `dark:` to the app's `.dark` class (set by utils/theme.ts), not the OS
`prefers-color-scheme` — Tailwind v4's default — so it follows the app theme. */
@custom-variant dark (&:where(.dark, .dark *));
@source "../**/*.{vue,js,ts}";
/*
* Light↔dark mode switch — soft curtain sweep (View Transitions API, started
* by switchThemeMode in utils/theme.ts). The outgoing theme holds fully opaque
* underneath while the new theme sweeps top→bottom behind a tall feathered
* mask edge (~half a viewport of blend), so the change reads as a soft light
* pass rather than a hard wipe or a flash.
* Mask math: the mask box is 250% of the viewport tall — solid for the first
* 40%, feathering to transparent by 60%. Sliding mask-position 100%→0% walks
* that feathered band across the full viewport height.
* REVERT: delete this block (browsers fall back to their default cross-fade)
* and reduce switchThemeMode to `applyChanges()` for a fully instant switch.
*/
::view-transition-old(root) {
animation: none;
}
::view-transition-new(root) {
animation: o2-theme-sweep 0.45s ease-in-out both;
mask-image: linear-gradient(to bottom, black 0%, black 40%, transparent 60%);
mask-size: 100% 250%;
mask-repeat: no-repeat;
}
@keyframes o2-theme-sweep {
from {
mask-position: 0% 100%;
}
to {
mask-position: 0% 0%;
}
}
/*
* While a theme switch is in flight, suppress per-element color transitions
* (inputs, buttons, table action icons carry their own `transition-colors`)
* so nothing animates a beat behind the page cross-fade. The class is added
* and removed by switchThemeMode in utils/theme.ts.
*/
html.theme-switching *,
html.theme-switching *::before,
html.theme-switching *::after {
transition: none !important;
}
/*
* Navbar theme-toggle icon swap (sun ↔ moon): rotate + scale out-in. Driven by
* the <Transition name="theme-icon"> wrapper in ThemeSwitcher.vue. Uses CSS
* animations rather than transitions so the `theme-switching` freeze above
* doesn't suppress it — it plays live inside the theme sweep.
*/
.theme-icon-enter-active {
animation: theme-icon-in 0.3s ease-out;
}
.theme-icon-leave-active {
animation: theme-icon-out 0.15s ease-in forwards;
}
@keyframes theme-icon-in {
from {
transform: rotate(-90deg) scale(0.6);
opacity: 0;
}
to {
transform: rotate(0deg) scale(1);
opacity: 1;
}
}
@keyframes theme-icon-out {
to {
transform: rotate(90deg) scale(0.6);
opacity: 0;
}
}
/*
* When reka-ui opens a modal dialog it sets `body { pointer-events: none }`
* and re-enables events only on the DialogContent DismissableLayer node.
* Any portaled floating element (dropdown, select, combobox, date-picker …)
* that is teleported to <body> as a sibling would inherit `pointer-events: none`,
* making items un-clickable and showing a disabled cursor.
* Force pointer-events back to auto for all known portal wrappers so they
* remain fully interactive while a modal is open.
*/
[data-reka-popper-content-wrapper] {
pointer-events: auto !important;
}
/*
* Query editor placeholder — the ONE definition for every page that overlays
* a placeholder on the Monaco query editor (logs, traces, RUM sessions,
* RUM error tracking, alerts). Mirrors Monaco's rendered text (monospace,
* same line height) so the placeholder reads as the future typed query.
* The placeholder text token is theme-paired, so no dark override is needed.
*/
.query-editor-placeholder-overlay .query-editor-placeholder-typewriter {
font-family: var(--font-mono);
font-size: var(--text-sm);
line-height: 1.3125rem;
color: var(--color-text-placeholder);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@layer base {
/* Tailwind's v3-compat default border colour, token-backed so it follows the
theme (a raw hex would paint light-grey borders in dark mode on any element
that sets a border-width without a colour). */
*,
:before,
:after {
border-color: var(--color-border-default);
}
}
/*
* Reduced-motion: users who ask for less motion get a plain, near-instant
* fade instead of the slide/zoom enter-exit animations (tw-animate-css
* `animate-in` / `animate-out`). Keeps overlays usable without the movement.
*/
@media (prefers-reduced-motion: reduce) {
[data-reka-popper-content-wrapper] *,
[data-o2-dialog],
[data-o2-drawer],
[data-test="o-dialog-overlay"],
[data-test="o-drawer-overlay"] {
animation-duration: 0.01ms !important;
animation-timing-function: linear !important;
}
}
/*
* Clip-path "reveal" for popups (dropdown / select / combobox).
* The menu renders at full size and is progressively unveiled with a
* `clip-path` wipe — content never scales or squishes, it just gets
* revealed from the trigger edge. `-down-*` wipes top→bottom (bottom-placed
* menus), `-up-*` wipes bottom→top (top-placed menus, via `data-[side=top]`).
* `round 0.5rem` keeps the corners rounded throughout the wipe.
*/
@keyframes o2-reveal-down-in {
from {
opacity: 0;
clip-path: inset(0 0 100% 0 round 0.5rem);
}
to {
opacity: 1;
clip-path: inset(0 0 0 0 round 0.5rem);
}
}
@keyframes o2-reveal-down-out {
from {
opacity: 1;
clip-path: inset(0 0 0 0 round 0.5rem);
}
to {
opacity: 0;
clip-path: inset(0 0 100% 0 round 0.5rem);
}
}
@keyframes o2-reveal-up-in {
from {
opacity: 0;
clip-path: inset(100% 0 0 0 round 0.5rem);
}
to {
opacity: 1;
clip-path: inset(0 0 0 0 round 0.5rem);
}
}
@keyframes o2-reveal-up-out {
from {
opacity: 1;
clip-path: inset(0 0 0 0 round 0.5rem);
}
to {
opacity: 0;
clip-path: inset(100% 0 0 0 round 0.5rem);
}
}