mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
## 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>
206 lines
7.4 KiB
TypeScript
206 lines
7.4 KiB
TypeScript
// Copyright 2026 OpenObserve Inc.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
|
import { chartColor } from "@/utils/chartTheme";
|
|
|
|
export function useStickyColumns(props: any, store: any) {
|
|
const stickyColumnOffsets = ref<{ [key: string]: number }>({});
|
|
let styleElement: HTMLStyleElement | null = null;
|
|
// Unique ID to scope injected CSS so multiple panels don't override each other
|
|
const tableId = `sticky-${Math.random().toString(36).slice(2)}`;
|
|
|
|
// Watch for columns changes to update sticky offsets
|
|
watch(
|
|
() => props.columns,
|
|
(columns: any[]) => {
|
|
const offsets: { [key: string]: number } = {};
|
|
let cumulativeWidth = 0;
|
|
|
|
if (columns) {
|
|
columns.forEach((col: any, idx: number) => {
|
|
// Add index attribute for CSS targeting
|
|
col.__colIndex = idx;
|
|
|
|
if (col.sticky) {
|
|
offsets[col.name] = cumulativeWidth;
|
|
cumulativeWidth += col.width ? parseInt(String(col.width)) : 100;
|
|
}
|
|
});
|
|
}
|
|
|
|
stickyColumnOffsets.value = offsets;
|
|
},
|
|
{ immediate: true, deep: true },
|
|
);
|
|
|
|
const getStickyColumnStyle = (column: any) => {
|
|
if (!column?.sticky) return {};
|
|
|
|
// Get the precomputed left offset for this column
|
|
const leftOffset = stickyColumnOffsets.value[column.name] ?? 0;
|
|
|
|
return {
|
|
position: "sticky",
|
|
left: `${leftOffset}px`,
|
|
"z-index": 2,
|
|
"background-color": chartColor("--color-surface-base"),
|
|
"box-shadow": "var(--shadow-sticky-left)",
|
|
};
|
|
};
|
|
|
|
const updateStickyColumnStyles = () => {
|
|
// Remove old style element if it exists
|
|
if (styleElement?.parentNode) {
|
|
styleElement.parentNode.removeChild(styleElement);
|
|
}
|
|
|
|
// Create new style element with current offsets
|
|
styleElement = document.createElement("style");
|
|
styleElement.setAttribute("data-sticky-styles", "true");
|
|
|
|
const columns = (props.columns || []) as any[];
|
|
const bgColor = chartColor("--color-surface-base");
|
|
let css = "";
|
|
|
|
const stickyColTotals = !!props.stickyColTotals;
|
|
const TOTAL_COL_WIDTH = 150;
|
|
|
|
const scope = `.my-sticky-virtscroll-table[data-sticky-id="${tableId}"]`;
|
|
|
|
// Shadow constants — right-sticky uses inset shadow to match TableRenderer scoped style
|
|
const shadowRight = "var(--shadow-sticky-left)";
|
|
const shadowLeft = "var(--shadow-sticky-right)";
|
|
const shadowBoth = `${shadowRight}, ${shadowLeft}`;
|
|
|
|
// Generate CSS rules for each column position
|
|
columns.forEach((col: any, colIndex: number) => {
|
|
if (col.sticky) {
|
|
const offset = stickyColumnOffsets.value[col.name] ?? 0;
|
|
// Left-sticky: shadow on right. If also right-sticky (middle column), shadow on both.
|
|
const shadow = col._isTotalColumn ? shadowBoth : shadowRight;
|
|
css += `
|
|
${scope} thead tr th:nth-child(${colIndex + 1}) {
|
|
position: sticky !important;
|
|
left: ${offset}px !important;
|
|
z-index: 4 !important;
|
|
background-color: var(--color-sticky-col-header-bg) !important;
|
|
box-shadow: ${shadow} !important;
|
|
}
|
|
${scope} tbody td:nth-child(${colIndex + 1}).sticky-column {
|
|
left: ${offset}px !important;
|
|
}
|
|
`;
|
|
}
|
|
|
|
// Right-sticky total columns (for standard single-row headers)
|
|
if (stickyColTotals && col._isTotalColumn) {
|
|
const rightOffset = (col._totalColRightIndex ?? 0) * TOTAL_COL_WIDTH;
|
|
// Right-sticky: shadow on left. If also left-sticky (middle column), shadow on both.
|
|
const shadow = col.sticky ? shadowBoth : shadowLeft;
|
|
css += `
|
|
${scope} thead tr:first-child th:nth-child(${colIndex + 1}) {
|
|
position: sticky !important;
|
|
right: ${rightOffset}px !important;
|
|
z-index: 4 !important;
|
|
min-width: ${TOTAL_COL_WIDTH}px !important;
|
|
background-color: var(--color-sticky-col-header-bg) !important;
|
|
box-shadow: ${shadow} !important;
|
|
}
|
|
`;
|
|
}
|
|
});
|
|
|
|
// Add base styling for all sticky columns
|
|
css =
|
|
`
|
|
/* Left-sticky body cells: shadow on right */
|
|
${scope} tbody td.sticky-column {
|
|
position: sticky !important;
|
|
z-index: 2 !important;
|
|
box-shadow: var(--shadow-sticky-left) !important;
|
|
}
|
|
|
|
/* Right-sticky total column body cells: shadow on the left edge. The
|
|
original drew an INSET shadow inside the column; the token casts
|
|
outward onto the scrolling cells instead. Same boundary, opposite
|
|
side, and it matches what OTable's sticky cells already do. */
|
|
${scope} tbody td.pivot-total-col {
|
|
position: sticky !important;
|
|
z-index: 2 !important;
|
|
background-color: ${bgColor} !important;
|
|
box-shadow: var(--shadow-sticky-right) !important;
|
|
}
|
|
|
|
/* Middle sticky body cells: outward on both edges */
|
|
${scope} tbody td.sticky-column.pivot-total-col {
|
|
box-shadow: var(--shadow-sticky-left), var(--shadow-sticky-right) !important;
|
|
}
|
|
|
|
/* Sticky total row (bottom sticky). --shadow-sticky-footer, NOT
|
|
--shadow-scroll-bottom: a pinned footer casts OUTWARD and UPWARD onto
|
|
the rows it overlaps. scroll-bottom is an INSET bottom-edge gradient
|
|
meaning "content continues below" — a different affordance entirely. */
|
|
${scope}.pivot-sticky-totals .pivot-sticky-total-row td {
|
|
position: sticky !important;
|
|
bottom: 0 !important;
|
|
z-index: 2 !important;
|
|
background-color: ${bgColor} !important;
|
|
font-weight: bold !important;
|
|
box-shadow: var(--shadow-sticky-footer) !important;
|
|
}
|
|
|
|
/* Corner: sticky total row + sticky total column intersection */
|
|
${scope}.pivot-sticky-totals .pivot-sticky-total-row td.pivot-total-col {
|
|
z-index: 5 !important;
|
|
}
|
|
|
|
/* Column-specific positions for headers and body cells */
|
|
` + css;
|
|
|
|
styleElement.textContent = css;
|
|
document.head.appendChild(styleElement);
|
|
};
|
|
|
|
// Watch sticky column offsets and update styles
|
|
watch(() => stickyColumnOffsets.value, updateStickyColumnStyles, {
|
|
deep: true,
|
|
});
|
|
|
|
// Watch theme changes and update styles
|
|
watch(() => store.state.theme, updateStickyColumnStyles);
|
|
|
|
onMounted(() => {
|
|
// Defer to next frame so the synchronous style injection from the watch
|
|
// chain (which already ran during setup) doesn't invalidate the style cache
|
|
// right before other code reads layout properties (e.g. getRect).
|
|
requestAnimationFrame(() => {
|
|
updateStickyColumnStyles();
|
|
});
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
if (styleElement?.parentNode) {
|
|
styleElement.parentNode.removeChild(styleElement);
|
|
}
|
|
});
|
|
|
|
return {
|
|
stickyColumnOffsets,
|
|
getStickyColumnStyle,
|
|
tableId,
|
|
};
|
|
}
|