Files
openobserve/web/scripts/check-css-tokens.mjs
T
f15e27e5fd refactor: design token migration + CI guards (#13173)
# Design System Unification — one visual language, enforced end-to-end

Establishes **one source of truth for every visual decision**, wires the
whole app to it, and **locks it in with CI** so it can't drift back.

### Foundations
- **Design-token system ("one knob per decision").** Every colour, text
size, spacing, corner radius, and shadow is a named token defined in
exactly one place — change the token, every screen updates. Collapsed
hundreds of near-duplicate definitions and removed "fake" duplicates
that pretended to be different but weren't.
- **Shared component library (O2).** Buttons, tables, dialogs, drawers,
form fields, badges, etc. already know the correct colours, spacing, and
dark-mode behaviour; new screens compose them instead of hand-styling.
Added new primitives on this branch (`OContent`, `KpiCard`) and
standardized existing ones.
- **One page structure everywhere** (`PageLayout` / `AppPageHeader` /
`AppPage` / `OContent`) — titles, breadcrumbs, spacing, and content
edges line up identically across every screen.
- **One table implementation** — all tabular data uses the same table
(sorting, density, sticky headers, empty states).
- **Styling consolidated onto Tailwind; old systems removed** —
hand-written CSS driven to the practical minimum, the legacy **Quasar**
framework fully removed, surviving stylesheets required to justify
themselves.
- **Two-tier corner radius** — a tight **4px** for controls and a soft
**12px** for surfaces (dialogs, cards, the app shell), plus a
full-circle option — replacing five radius names that all secretly meant
the same thing.

### Detailed improvements across screens
**Page structure & alignment** — unified page shell rolled across ~40+
screens (IAM, Settings, Alerts, Dashboards, Functions, Pipelines, RUM,
Metrics, Synthetics, Billing, editors, lists); one header height; one
page-edge grid driving headers, tables, rails, and tabs; unified
side-rail widths; inset rail selection/hover pills; every tab strip on
the grid line; tightened title/subtitle spacing.

**Tables (cross-screen)** — action columns consistently centered (incl.
Organizations); unified row-number column with no "#" truncation;
non-standard tables restyled to match; loading skeletons made
geometrically identical to the loaded table; symmetric edge inset; IAM
first-column grid alignment fixed.

**Traces** — flame graph + span-detail correlation panel fixes;
correlated logs/metrics placeholder fit; sticky headers; removed a
double scrollbar + a 1px sliver; aligned table fonts/badges; polished
the service detail drawer; stream-selector + correlation alignment;
aligned loading spinners; floating overlay arrows for the
disabled-services strip.

**Logs** — field-list controls aligned (focus ring no longer clipped);
detail-drawer spacing + metrics-tab overflow fixed; Run Query toolbar
tidied; listing sizing + stray border fixed.

**Metrics** — search added to the "Type" facet rail and aligned with the
other rails; proper empty states for the explorer and facet rail.

**Dashboards** — removed inconsistent card shadows; tightened
borders/resizers/field-list spacing; aligned View Dashboard chrome to
the grid; chart colours via the token helper.

**Home** — redesigned the Usage tab; added reusable KPI stat cards so
summary strips stop drifting.

**Dark mode** — new theme-switch animation; fixed a 1px content shift on
toggle and a white flash on load; fixed AI-chat text and assorted
light/dark mismatches; completed the token-driven dark migration.

**Empty states, controls & small polish** — restored 3 broken
empty-state animations; adopted the standard empty state; added the
sliding indicator to the toggle control; fixed the Quality-page inset +
drawer badge; refreshed Search History / Schedulers; fixed the
alert-history drawer scrollbar + timeline hover; removed a stray
menu-bar border.

### Dead code & cruft removal
- **Deleted the entire legacy `utilities.css`** global stylesheet (well
over a thousand lines) — evacuated in waves, roughly half of it dead
once traced.
- **Removed Quasar entirely**, including dead `.q-*` styles and ~25
phantom `--q-*` variables that were silently breaking bits of the UI.
- Stripped hundreds of **dead CSS classes and tokens that compiled to
nothing** — e.g. 46 uses of a colour class with no backing token, a
no-op shadow class on 51 elements, dead layout/utility classes, a
leftover `card-container` block, a dead table-actions token, and a
mis-registered gradient that rendered nothing.
- Cut **unused keyframe animations (19 → 3)** and removed **unused
components** + a stray build artifact.
- Net effect: a much smaller, leaner styling surface — less to maintain,
fewer dead ends to copy.

## Safety & verification
Large but low-risk: this is overwhelmingly a **styling and structure**
change, not product behaviour or data. Guarded by the
design-token/consistency/type checks above plus the existing test suite,
and reviewed screen-by-screen.

---------

Co-authored-by: Kirtan Thakkar <kirtan@kiara.tech>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: ktx-mihir <mihir.patel@kiara.tech>
2026-07-20 18:06:21 +00:00

161 lines
7.3 KiB
JavaScript

#!/usr/bin/env node
// CI guard: fail if any `var(--x)` reference has no matching `--x:` definition
// anywhere in src (.css / .vue / .ts), and no CSS fallback (`var(--x, fallback)`).
// This catches the root-cause class of bug fixed in O2_TOKEN_MIGRATION_PLAN.md §3
// (undefined-token references that silently void the whole declaration).
//
// Run: node scripts/check-css-tokens.mjs (from web/)
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join, extname } from "node:path";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const SRC_DIR = join(__dirname, "..", "src");
const EXTS = new Set([".css", ".vue", ".ts"]);
// Library-injected custom properties that are never statically `--x: value;`
// defined in our source (Reka UI, Tailwind internals, vue-flow).
// These resolve at runtime, so a static "undefined" report on them would be a
// false positive — hence the prefix allowlist.
//
// HARD RULE: NO `--o2-*` name may ever appear in any allowlist here. The entire
// `--o2-*` vocabulary is banned and being migrated away; even the ones set at
// runtime via Vue `:style` (OTable tree indents, row status, row height) must be
// renamed off the `--o2-` namespace. If you find a `--o2-*` here, delete it and
// fix the usage — never re-exempt it.
const ALLOW_PREFIXES = [/^--reka-/, /^--tw-/, /^--vf-/];
const ALLOW_EXACT = new Set([
// Non-o2 custom properties set per-element at runtime via Vue `:style`
// bindings — dynamic values with no static `--x:` definition.
"--node-color", // plugins/pipelines/CustomNode.vue :style={ '--node-color': ... }
"--chip-color",
]);
// Guard against accidental re-introduction of an --o2-* exemption above.
for (const name of [...ALLOW_EXACT, ...ALLOW_PREFIXES.map((re) => re.source)]) {
if (String(name).includes("--o2-")) {
console.error(`check-css-tokens: '${name}' — --o2-* tokens must never be allowlisted. Remove it.`);
process.exit(2);
}
}
function isAllowed(name) {
if (ALLOW_EXACT.has(name)) return true;
return ALLOW_PREFIXES.some((re) => re.test(name));
}
function walk(dir, files = []) {
for (const entry of readdirSync(dir)) {
if (entry === "node_modules" || entry === "dist") continue;
const full = join(dir, entry);
const stat = statSync(full);
if (stat.isDirectory()) walk(full, files);
else if (EXTS.has(extname(entry))) files.push(full);
}
return files;
}
const DECL_RE = /(--[A-Za-z0-9_-]+)\s*:/g;
// var(--x) or var(--x, fallback) — only flag the no-fallback form.
const VAR_RE = /var\(\s*(--[A-Za-z0-9_-]+)\s*(,[^)]*)?\)/g;
// Tailwind v4 CSS-variable shorthand: `bg-(--x)`, `text-(--x)`, `border-(--x)`.
// The compiler expands these to a bare `var(--x)` with NO fallback, so a phantom
// token voids the declaration exactly like `var(--x)` does — but VAR_RE never
// sees the `var(` text, so this form slipped past the guard entirely. The
// fallback form `(--x,#fff)` is safe and skipped, mirroring VAR_RE.
// Requires a utility prefix (`bg`, `text`, `border-t`, …) before the `-(`, which
// is what distinguishes the shorthand from an ordinary `var(` / `calc(` call.
const TW_SHORTHAND_RE = /[A-Za-z0-9\]]-\(\s*(--[A-Za-z0-9_-]+)\s*(,[^)]*)?\)/g;
// ANY --o2-* occurrence used AS A CSS CUSTOM PROPERTY, in any syntax (var(),
// Tailwind `[var(--o2-*)]` / `(--o2-*)` shorthand, `[--o2-*]` arbitrary,
// `:style` keys, template-literal CSS). The whole vocabulary is banned, so a
// fallback does NOT make it acceptable — unlike the undefined-ref check.
const O2_RE = /--o2-[A-Za-z0-9-]+/g;
// The `--o2-` prefix collides with the OpenObserve collector's CLI flags (e.g.
// the k8s install command's `--o2-<flag>=<value>` URL flag) and with
// prose/comments that name those flags. Those are NOT CSS custom properties, so
// they must not trip the ban. A `--o2-*` is a CSS custom property only when used
// as one: inside var(), a Tailwind shorthand/arbitrary bracket, or as a
// declaration / `:style` key (immediately followed by `:`). Everything else (a
// bare flag in a shell-command string, or a backticked mention in a comment) is
// ignored.
function isO2CssUsage(line, index, name) {
const before = line.slice(Math.max(0, index - 6), index);
const after = line.slice(index + name.length);
return (
/var\(\s*$/.test(before) || // var(--o2-*
/[A-Za-z0-9\]]-\(\s*$/.test(before) || // bg-(--o2-* (Tailwind shorthand)
/\[\s*$/.test(before) || // [--o2-* (arbitrary property/value)
/^\s*['"]?\s*:/.test(after) // --o2-*: or '--o2-*': (decl / :style key)
);
}
const defined = new Set();
const referenced = new Map(); // name -> [{file, line}]
const bannedO2 = new Map(); // name -> [{file, line}]
for (const file of walk(SRC_DIR)) {
const text = readFileSync(file, "utf8");
const lines = text.split("\n");
lines.forEach((line, i) => {
for (const m of line.matchAll(DECL_RE)) defined.add(m[1]);
for (const re of [VAR_RE, TW_SHORTHAND_RE]) {
for (const m of line.matchAll(re)) {
const [, name, fallback] = m;
if (fallback) continue; // has a fallback, safe either way
if (!referenced.has(name)) referenced.set(name, []);
referenced.get(name).push({ file: file.replace(SRC_DIR + "/", "src/"), line: i + 1 });
}
}
for (const m of line.matchAll(O2_RE)) {
const name = m[0];
if (!isO2CssUsage(line, m.index, name)) continue; // CLI flag / prose, not a CSS token
if (!bannedO2.has(name)) bannedO2.set(name, []);
bannedO2.get(name).push({ file: file.replace(SRC_DIR + "/", "src/"), line: i + 1 });
}
});
}
const undefinedRefs = [];
for (const [name, sites] of referenced) {
if (defined.has(name) || isAllowed(name)) continue;
undefinedRefs.push({ name, sites });
}
function printGroup(map) {
for (const [name, sites] of map) {
console.error(` ${name}`);
for (const { file, line } of sites.slice(0, 5)) console.error(` ${file}:${line}`);
if (sites.length > 5) console.error(` ...and ${sites.length - 5} more`);
}
}
let failed = false;
// 1) Banned --o2-* vocabulary — zero tolerance, fallback or not.
if (bannedO2.size > 0) {
const total = [...bannedO2.values()].reduce((n, s) => n + s.length, 0);
console.error(`\nFound ${total} banned --o2-* token reference${total === 1 ? "" : "s"} (${bannedO2.size} distinct name${bannedO2.size === 1 ? "" : "s"}):\n`);
printGroup(bannedO2);
console.error("\nThe entire --o2-* vocabulary is banned. Migrate each to its --color-* equivalent");
console.error("(see scripts/o2-token-map.json) or a Tailwind utility. There is no allowlist.\n");
failed = true;
}
// 2) Undefined non-o2 references with no fallback (voids the whole declaration).
if (undefinedRefs.length > 0) {
console.error(`\nFound ${undefinedRefs.length} undefined CSS custom propert${undefinedRefs.length === 1 ? "y" : "ies"} referenced with no fallback:\n`);
printGroup(new Map(undefinedRefs.map((r) => [r.name, r.sites])));
console.error("\nEach of these voids its whole declaration in browsers (invalid var() = unset).");
console.error("Either define the token or add a var(--x, fallback).\n");
failed = true;
}
if (failed) {
process.exit(1);
} else {
console.log(`OK — no --o2-* tokens and no undefined CSS custom property references (${defined.size} tokens defined, ${referenced.size} distinct refs checked).`);
}