Theme-Aware UI Improvements for ClickStack (#1685)
12
.changeset/ui-fixes-clickstack.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
feat: Theme-aware UI improvements for ClickStack
|
||||
|
||||
- **Chart colors**: Made chart color palette theme-aware - ClickStack uses blue as primary color, HyperDX uses green. Charts now correctly display blue bars for ClickStack theme.
|
||||
- **Semantic colors**: Updated semantic color functions (getChartColorSuccess, getChartColorWarning, getChartColorError) to be theme-aware, reading from CSS variables or falling back to theme-appropriate palettes.
|
||||
- **Info log colors**: Changed info-level logs to use primary chart color (blue for ClickStack, green for HyperDX) instead of success green.
|
||||
- **Button variants**: Made ResumeLiveTailButton variant conditional - uses 'secondary' for ClickStack theme, 'primary' for HyperDX theme.
|
||||
- **Nav styles**: Fixed collapsed navigation styles for proper alignment and spacing when nav is collapsed to 50px width.
|
||||
- **Icon stroke width**: Added custom stroke width (1.5) for Tabler icons in ClickStack theme only, providing a more refined appearance.
|
||||
|
|
@ -2,4 +2,5 @@
|
|||
.storybook
|
||||
node_modules
|
||||
coverage
|
||||
playwright-report
|
||||
playwright-report
|
||||
styles/app.scss
|
||||
|
|
@ -96,22 +96,23 @@ function AppContent({
|
|||
confirmModal: React.ReactNode;
|
||||
}) {
|
||||
const { userPreferences } = useUserPreferences();
|
||||
const { themeName } = useAppTheme();
|
||||
|
||||
// Only override font if user has explicitly set a preference.
|
||||
// Otherwise, return undefined to let the theme use its default font:
|
||||
// - HyperDX theme: "IBM Plex Sans", monospace
|
||||
// - ClickStack theme: "Inter", sans-serif
|
||||
const selectedMantineFont = userPreferences.font
|
||||
? MANTINE_FONT_MAP[userPreferences.font] || undefined
|
||||
// ClickStack theme always uses Inter font - user preference is ignored
|
||||
// HyperDX theme allows user to select font preference
|
||||
const isClickStackTheme = themeName === 'clickstack';
|
||||
const effectiveFont = isClickStackTheme ? 'Inter' : userPreferences.font;
|
||||
const selectedMantineFont = effectiveFont
|
||||
? MANTINE_FONT_MAP[effectiveFont] || undefined
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
// Update CSS variable for global font cascading
|
||||
if (typeof document !== 'undefined') {
|
||||
const fontVar = FONT_VAR_MAP[userPreferences.font] || DEFAULT_FONT_VAR;
|
||||
const fontVar = FONT_VAR_MAP[effectiveFont] || DEFAULT_FONT_VAR;
|
||||
document.documentElement.style.setProperty('--app-font-family', fontVar);
|
||||
}
|
||||
}, [userPreferences.font]);
|
||||
}, [effectiveFont]);
|
||||
|
||||
const getLayout = Component.getLayout ?? (page => page);
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 511 B After Width: | Height: | Size: 596 B |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,6 +1,13 @@
|
|||
<svg width="260" height="260" viewBox="0 0 260 260" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="260" height="260" rx="40" fill="#1E1E1E" />
|
||||
<path
|
||||
d="M207.885 85V175L129.942 220L52 175V85L129.942 40L207.885 85ZM149.406 69.6953C148.192 68.8065 146.651 69.0886 145.688 70.376L97.3643 134.997C96.5385 136.102 96.3139 137.718 96.792 139.11C97.2698 140.502 98.3597 141.404 99.5645 141.404H119.637L108.759 185.901C108.346 187.591 108.912 189.417 110.126 190.306C111.34 191.194 112.882 190.912 113.845 189.625L162.168 125.003C162.994 123.898 163.219 122.282 162.741 120.89C162.263 119.498 161.173 118.597 159.969 118.597H139.896L150.774 74.0986C151.187 72.409 150.62 70.5841 149.406 69.6953Z"
|
||||
fill="#25E2A5" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="260" height="260"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="260" height="260"><svg width="260" height="260" viewBox="0 0 260 260" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_39_10)">
|
||||
<path d="M242.166 65V195L129.583 260.001L17 195V65L129.583 0L242.166 65Z" fill="#25E2A5"></path>
|
||||
<path d="M157.698 42.8931C159.452 44.177 160.271 46.8134 159.674 49.2541L143.961 113.528H172.955C174.695 113.528 176.269 114.83 176.959 116.841C177.65 118.852 177.325 121.187 176.132 122.782L106.331 216.124C104.94 217.984 102.714 218.391 100.96 217.108C99.2069 215.823 98.3878 213.187 98.9844 210.746L114.697 146.472H85.7037C83.9634 146.472 82.3892 145.17 81.6991 143.16C81.0084 141.149 81.3339 138.814 82.5268 137.218L152.328 43.8762C153.718 42.0166 155.944 41.6092 157.698 42.8931Z" fill="#1E1E1E"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_39_10">
|
||||
<rect width="260" height="260" rx="40" fill="white"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg></svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
||||
@media (prefers-color-scheme: dark) { :root { filter: none; } }
|
||||
</style></svg>
|
||||
|
Before Width: | Height: | Size: 738 B After Width: | Height: | Size: 1.2 KiB |
|
|
@ -44,7 +44,7 @@ import {
|
|||
TimeChartSeries,
|
||||
} from './types';
|
||||
import { NumberFormat } from './types';
|
||||
import { getColorProps, logLevelColor, logLevelColorOrder } from './utils';
|
||||
import { getColorProps, getLogLevelColorOrder, logLevelColor } from './utils';
|
||||
|
||||
export const SORT_ORDER = [
|
||||
{ value: 'asc' as const, label: 'Ascending' },
|
||||
|
|
@ -713,6 +713,7 @@ export function formatResponseForTimeChart({
|
|||
});
|
||||
}
|
||||
|
||||
const logLevelColorOrder = getLogLevelColorOrder();
|
||||
const sortedLineData = Object.values(lineDataMap).sort((a, b) => {
|
||||
return (
|
||||
logLevelColorOrder.findIndex(color => color === a.color) -
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ import {
|
|||
useSource,
|
||||
useSources,
|
||||
} from '@/source';
|
||||
import { useAppTheme } from '@/theme/ThemeProvider';
|
||||
import {
|
||||
parseRelativeTimeQuery,
|
||||
parseTimeQuery,
|
||||
|
|
@ -280,10 +281,13 @@ function ResumeLiveTailButton({
|
|||
}: {
|
||||
handleResumeLiveTail: () => void;
|
||||
}) {
|
||||
const { themeName } = useAppTheme();
|
||||
const variant = themeName === 'clickstack' ? 'secondary' : 'primary';
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="primary"
|
||||
variant={variant}
|
||||
onClick={handleResumeLiveTail}
|
||||
leftSection={<IconBolt size={14} />}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { IconFlask } from '@tabler/icons-react';
|
|||
|
||||
import { OPTIONS_FONTS } from './config/fonts';
|
||||
import { useAppTheme } from './theme/ThemeProvider';
|
||||
import { ThemeName } from './theme/types';
|
||||
import { isValidThemeName, themes } from './theme';
|
||||
import { UserPreferences, useUserPreferences } from './useUserPreferences';
|
||||
|
||||
|
|
@ -183,21 +182,25 @@ export const UserPreferencesModal = ({
|
|||
</SettingContainer>
|
||||
)}
|
||||
|
||||
<SettingContainer
|
||||
label="Font"
|
||||
description="If using custom font, make sure it's installed on your system"
|
||||
>
|
||||
<Autocomplete
|
||||
value={userPreferences.font}
|
||||
filter={({ options }) => options}
|
||||
onChange={value =>
|
||||
setUserPreference({
|
||||
font: value as UserPreferences['font'],
|
||||
})
|
||||
}
|
||||
data={OPTIONS_FONTS}
|
||||
/>
|
||||
</SettingContainer>
|
||||
{/* Font selection is only available for HyperDX theme */}
|
||||
{/* ClickStack theme always uses Inter font and doesn't show this setting */}
|
||||
{themeName !== 'clickstack' && (
|
||||
<SettingContainer
|
||||
label="Font"
|
||||
description="If using custom font, make sure it's installed on your system"
|
||||
>
|
||||
<Autocomplete
|
||||
value={userPreferences.font}
|
||||
filter={({ options }) => options}
|
||||
onChange={value =>
|
||||
setUserPreference({
|
||||
font: value as UserPreferences['font'],
|
||||
})
|
||||
}
|
||||
data={OPTIONS_FONTS}
|
||||
/>
|
||||
</SettingContainer>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
convertToTimeChartConfig,
|
||||
formatResponseForTimeChart,
|
||||
} from '@/ChartUtils';
|
||||
import { CHART_COLOR_ERROR, COLORS } from '@/utils';
|
||||
import { COLORS, getChartColorError } from '@/utils';
|
||||
|
||||
describe('ChartUtils', () => {
|
||||
describe('formatResponseForTimeChart', () => {
|
||||
|
|
@ -316,7 +316,7 @@ describe('ChartUtils', () => {
|
|||
isDashed: false,
|
||||
},
|
||||
{
|
||||
color: CHART_COLOR_ERROR,
|
||||
color: getChartColorError(),
|
||||
dataKey: 'error',
|
||||
currentPeriodKey: 'error',
|
||||
previousPeriodKey: 'error (previous)',
|
||||
|
|
|
|||
|
|
@ -44,11 +44,16 @@ export const AppNavCloudBanner = () => {
|
|||
<div className="my-3 bg-muted rounded p-2 text-center">
|
||||
<span className="fs-8">Ready to deploy on ClickHouse Cloud?</span>
|
||||
<div className="mt-2 mb-2">
|
||||
<Link href="https://clickhouse.com/docs/use-cases/observability/clickstack/getting-started#deploy-with-clickhouse-cloud">
|
||||
<Button variant="primary" size="xs" className="hover-color-white">
|
||||
Get Started for Free
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="xs"
|
||||
component="a"
|
||||
href="https://clickhouse.com/docs/use-cases/observability/clickstack/getting-started#deploy-with-clickhouse-cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Get Started for Free
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -86,7 +91,7 @@ export const AppNavUserMenu = ({
|
|||
})}
|
||||
>
|
||||
<Group gap="xs" wrap="nowrap" miw={0}>
|
||||
<Avatar size="sm" radius="xl" color="green">
|
||||
<Avatar size="sm" radius="xl" color="gray">
|
||||
{initials}
|
||||
</Avatar>
|
||||
{!isCollapsed && (
|
||||
|
|
@ -211,6 +216,8 @@ export const AppNavHelpMenu = ({
|
|||
data-testid="documentation-menu-item"
|
||||
href="https://clickhouse.com/docs/use-cases/observability/clickstack"
|
||||
component="a"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
leftSection={<IconBook size={16} />}
|
||||
>
|
||||
Documentation
|
||||
|
|
@ -221,6 +228,7 @@ export const AppNavHelpMenu = ({
|
|||
component="a"
|
||||
href="https://hyperdx.io/discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Discord Community
|
||||
</Menu.Item>
|
||||
|
|
@ -292,8 +300,8 @@ export const AppNavLink = ({
|
|||
{!isCollapsed && isBeta && (
|
||||
<Badge
|
||||
size="xs"
|
||||
radius="sm"
|
||||
color="gray"
|
||||
color="blue"
|
||||
variant="light"
|
||||
className={styles.navItemBadge}
|
||||
>
|
||||
Beta
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ $spacing-sm: 8px;
|
|||
$spacing-md: 12px;
|
||||
$spacing-lg: 16px;
|
||||
$nav-item-height: 34px;
|
||||
$nav-item-height-collapsed: 28px;
|
||||
$header-height: 58px;
|
||||
$help-button-size: 28px;
|
||||
$search-input-height: 28px;
|
||||
|
|
@ -51,13 +52,30 @@ $transition-slow: 0.2s ease;
|
|||
justify-content: space-between;
|
||||
border-right: 1px solid var(--color-border);
|
||||
background: var(--color-bg-sidenav);
|
||||
letter-spacing: 0.05em;
|
||||
overflow: hidden;
|
||||
transition: width 0.2s ease;
|
||||
|
||||
&Fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
&Collapsed {
|
||||
.navLinks {
|
||||
padding-inline: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
padding-inline: $spacing-sm;
|
||||
justify-content: center;
|
||||
height: $nav-item-height-collapsed;
|
||||
margin-block: $spacing-xs;
|
||||
}
|
||||
|
||||
.navItemIcon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
|
|
@ -71,15 +89,31 @@ $transition-slow: 0.2s ease;
|
|||
&Expanded {
|
||||
height: $header-height;
|
||||
}
|
||||
|
||||
&Collapsed {
|
||||
flex-flow: column nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding-inline: 0;
|
||||
padding-block: $spacing-md;
|
||||
min-height: $header-height;
|
||||
gap: $spacing-sm;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.logoLink {
|
||||
text-decoration: none;
|
||||
margin-left: -0.15rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.logoIconWrapper {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.collapseButton {
|
||||
|
|
@ -88,7 +122,9 @@ $transition-slow: 0.2s ease;
|
|||
|
||||
&Collapsed {
|
||||
transform: rotate(180deg);
|
||||
margin-top: $spacing-lg;
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +154,7 @@ $transition-slow: 0.2s ease;
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding-inline: $spacing-sm;
|
||||
padding-inline: $spacing-lg;
|
||||
margin-top: $spacing-sm;
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +170,7 @@ $transition-slow: 0.2s ease;
|
|||
|
||||
user-select: none;
|
||||
gap: 10px;
|
||||
padding-inline: $spacing-sm;
|
||||
padding-inline: $spacing-lg;
|
||||
height: $nav-item-height;
|
||||
border-radius: $radius-sm;
|
||||
|
||||
|
|
@ -151,6 +187,10 @@ $transition-slow: 0.2s ease;
|
|||
&Active {
|
||||
color: var(--color-text-sidenav-link-active);
|
||||
background: var(--color-bg-sidenav-link-active);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-sidenav-link-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +200,7 @@ $transition-slow: 0.2s ease;
|
|||
}
|
||||
|
||||
.navItemIcon {
|
||||
margin-right: $spacing-sm;
|
||||
margin-right: $spacing-md;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
|
|
@ -222,7 +262,6 @@ $transition-slow: 0.2s ease;
|
|||
|
||||
&:hover {
|
||||
color: var(--color-text-sidenav-link-active);
|
||||
background: var(--color-bg-sidenav-link-active);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
const isSmallScreen = (width ?? 1000) < 900;
|
||||
const isCollapsed = isSmallScreen || isPreferCollapsed;
|
||||
|
||||
const navWidth = isCollapsed ? 50 : 230;
|
||||
const navWidth = isCollapsed ? 50 : 250;
|
||||
|
||||
useEffect(() => {
|
||||
HyperDX.addAction('user navigated', {
|
||||
|
|
@ -663,6 +663,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
<div
|
||||
className={cx(styles.nav, {
|
||||
[styles.navFixed]: fixed,
|
||||
[styles.navCollapsed]: isCollapsed,
|
||||
})}
|
||||
style={{ width: navWidth }}
|
||||
>
|
||||
|
|
@ -670,6 +671,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
<div
|
||||
className={cx(styles.header, {
|
||||
[styles.headerExpanded]: !isCollapsed,
|
||||
[styles.headerCollapsed]: isCollapsed,
|
||||
})}
|
||||
>
|
||||
<Link href="/search" className={styles.logoLink}>
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ import { isAggregateFunction } from '@/ChartUtils';
|
|||
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
|
||||
import { getFirstTimestampValueExpression } from '@/source';
|
||||
import {
|
||||
CHART_COLOR_ERROR,
|
||||
CHART_COLOR_SUCCESS,
|
||||
getChartColorError,
|
||||
getChartColorSuccess,
|
||||
truncateMiddle,
|
||||
} from '@/utils';
|
||||
|
||||
|
|
@ -250,13 +250,13 @@ function PropertyComparisonChart({
|
|||
<Bar
|
||||
dataKey="outlierCount"
|
||||
name="Outliers"
|
||||
fill={CHART_COLOR_ERROR}
|
||||
fill={getChartColorError()}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="inlierCount"
|
||||
name="Inliers"
|
||||
fill={CHART_COLOR_SUCCESS}
|
||||
fill={getChartColorSuccess()}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</BarChart>
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ import {
|
|||
import TimelineChart from '@/TimelineChart';
|
||||
import { useFormatTime } from '@/useFormatTime';
|
||||
import {
|
||||
CHART_COLOR_ERROR,
|
||||
CHART_COLOR_ERROR_HIGHLIGHT,
|
||||
CHART_COLOR_WARNING,
|
||||
CHART_COLOR_WARNING_HIGHLIGHT,
|
||||
getChartColorError,
|
||||
getChartColorErrorHighlight,
|
||||
getChartColorWarning,
|
||||
getChartColorWarningHighlight,
|
||||
} from '@/utils';
|
||||
import {
|
||||
getHighlightedAttributesFromData,
|
||||
|
|
@ -89,9 +89,11 @@ function barColor(condition: {
|
|||
}) {
|
||||
const { isError, isWarn, isHighlighted } = condition;
|
||||
if (isError)
|
||||
return isHighlighted ? CHART_COLOR_ERROR_HIGHLIGHT : CHART_COLOR_ERROR;
|
||||
return isHighlighted ? getChartColorErrorHighlight() : getChartColorError();
|
||||
if (isWarn)
|
||||
return isHighlighted ? CHART_COLOR_WARNING_HIGHLIGHT : CHART_COLOR_WARNING;
|
||||
return isHighlighted
|
||||
? getChartColorWarningHighlight()
|
||||
: getChartColorWarning();
|
||||
return isHighlighted ? '#A9AFB7' : '#6A7077';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default function LogLevel({
|
|||
levelClass === 'error'
|
||||
? 'red'
|
||||
: levelClass === 'warn'
|
||||
? 'yellow'
|
||||
? 'var(--color-chart-warning)'
|
||||
: 'gray'
|
||||
}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
CHART_COLOR_ERROR,
|
||||
CHART_COLOR_SUCCESS,
|
||||
CHART_COLOR_WARNING,
|
||||
COLORS,
|
||||
getChartColorError,
|
||||
getChartColorSuccess,
|
||||
getChartColorWarning,
|
||||
} from '@/utils';
|
||||
|
||||
// Labels for chart colors - brand green first, then Observable palette
|
||||
|
|
@ -31,15 +31,19 @@ const CHART_COLORS = COLORS.map((hex, i) => ({
|
|||
const SEMANTIC_CHART_COLORS = [
|
||||
{
|
||||
name: 'color-chart-success',
|
||||
hex: CHART_COLOR_SUCCESS,
|
||||
hex: getChartColorSuccess(),
|
||||
label: 'Success (Green)',
|
||||
},
|
||||
{
|
||||
name: 'color-chart-warning',
|
||||
hex: CHART_COLOR_WARNING,
|
||||
hex: getChartColorWarning(),
|
||||
label: 'Warning (Orange)',
|
||||
},
|
||||
{ name: 'color-chart-error', hex: CHART_COLOR_ERROR, label: 'Error (Red)' },
|
||||
{
|
||||
name: 'color-chart-error',
|
||||
hex: getChartColorError(),
|
||||
label: 'Error (Red)',
|
||||
},
|
||||
];
|
||||
|
||||
const story = {
|
||||
|
|
|
|||
|
|
@ -146,6 +146,8 @@
|
|||
--color-state-hover: #2b2c3d;
|
||||
--color-state-selected: #34354a;
|
||||
--color-state-focus: #4d4f66;
|
||||
--color-outline-focus: var(--mantine-primary-color-filled);
|
||||
--input-bd-focus: var(--mantine-primary-color-filled);
|
||||
|
||||
/* Code / Misc UI */
|
||||
--color-bg-code: #1d1e30;
|
||||
|
|
@ -161,8 +163,40 @@
|
|||
--color-json-array: #ffd966;
|
||||
--color-json-punctuation: #666980;
|
||||
|
||||
/*
|
||||
* Chart Colors - Observable 10 categorical palette
|
||||
* NOTE: These colors are intentionally duplicated in both dark and light mode sections.
|
||||
* CSS specificity requires them to be defined within each [data-mantine-color-scheme] selector
|
||||
* to ensure they're applied correctly. A shared .theme-clickstack section would have lower
|
||||
* specificity and could be overridden by other styles.
|
||||
*/
|
||||
--color-chart-1: #437eef; /* Blue - Primary */
|
||||
--color-chart-2: #efb118; /* Orange */
|
||||
--color-chart-3: #ff725c; /* Red */
|
||||
--color-chart-4: #6cc5b0; /* Cyan */
|
||||
--color-chart-5: #3ca951; /* Green */
|
||||
--color-chart-6: #ff8ab7; /* Pink */
|
||||
--color-chart-7: #a463f2; /* Purple */
|
||||
--color-chart-8: #97bbf5; /* Light Blue */
|
||||
--color-chart-9: #9c6b4e; /* Brown */
|
||||
--color-chart-10: #9498a0; /* Gray */
|
||||
|
||||
/* Chart Semantic Colors */
|
||||
--color-chart-success: #3ca951; /* Green */
|
||||
--color-chart-warning: #efb118; /* Orange */
|
||||
--color-chart-error: #ff725c; /* Red */
|
||||
|
||||
/* Chart Semantic Colors - Highlighted (for hover/selection states) */
|
||||
--color-chart-error-highlight: #ffa090;
|
||||
--color-chart-warning-highlight: #f5c94d;
|
||||
|
||||
/* Mantine Overrides */
|
||||
--mantine-color-body: var(--color-bg-body) !important;
|
||||
|
||||
/* Tabler icons default stroke width - Set default stroke-width to 1.5 for Tabler icons (instead of default 2). This only affects SVGs that have a stroke attribute set. Can be overridden on individual icons by setting strokeWidth prop */
|
||||
.tabler-icon {
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
|
|
@ -306,6 +340,7 @@
|
|||
--color-state-hover: #e9ecef;
|
||||
--color-state-selected: #dee2e6;
|
||||
--color-state-focus: #ced4da;
|
||||
--color-outline-focus: var(--mantine-primary-color-filled);
|
||||
|
||||
/* Code / Misc UI */
|
||||
--color-bg-code: var(--click-global-color-background-muted);
|
||||
|
|
@ -321,6 +356,32 @@
|
|||
--color-json-array: #997300;
|
||||
--color-json-punctuation: #868e96;
|
||||
|
||||
/* Chart Colors - See dark mode section for explanation of why duplication is required */
|
||||
--color-chart-1: #437eef; /* Blue - Primary */
|
||||
--color-chart-2: #efb118; /* Orange */
|
||||
--color-chart-3: #ff725c; /* Red */
|
||||
--color-chart-4: #6cc5b0; /* Cyan */
|
||||
--color-chart-5: #3ca951; /* Green */
|
||||
--color-chart-6: #ff8ab7; /* Pink */
|
||||
--color-chart-7: #a463f2; /* Purple */
|
||||
--color-chart-8: #97bbf5; /* Light Blue */
|
||||
--color-chart-9: #9c6b4e; /* Brown */
|
||||
--color-chart-10: #9498a0; /* Gray */
|
||||
|
||||
/* Chart Semantic Colors */
|
||||
--color-chart-success: #3ca951; /* Green */
|
||||
--color-chart-warning: #efb118; /* Orange */
|
||||
--color-chart-error: #ff725c; /* Red */
|
||||
|
||||
/* Chart Semantic Colors - Highlighted (for hover/selection states) */
|
||||
--color-chart-error-highlight: #ffa090;
|
||||
--color-chart-warning-highlight: #f5c94d;
|
||||
|
||||
/* Mantine Overrides */
|
||||
--mantine-color-body: var(--color-bg-body);
|
||||
|
||||
/* Tabler icons default stroke width - Set default stroke-width to 1.5 for Tabler icons (instead of default 2). This only affects SVGs that have a stroke attribute set. Can be overridden on individual icons by setting strokeWidth prop */
|
||||
.tabler-icon {
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ export const makeTheme = ({
|
|||
baseVars['--button-bg'] = 'var(--color-primary-button-bg)';
|
||||
baseVars['--button-hover'] = 'var(--color-primary-button-bg-hover)';
|
||||
baseVars['--button-color'] = 'var(--color-primary-button-text)';
|
||||
baseVars['--button-color-hover'] = 'var(--color-primary-button-text)';
|
||||
}
|
||||
|
||||
if (props.variant === 'secondary') {
|
||||
|
|
|
|||
|
|
@ -424,10 +424,29 @@ export const CHART_PALETTE = {
|
|||
orangeHighlight: '#f5c94d',
|
||||
} as const;
|
||||
|
||||
// Ordered array for chart series - green first for brand consistency
|
||||
// ClickStack theme chart color palette - Observable 10 categorical palette
|
||||
// https://observablehq.com/@d3/color-schemes
|
||||
export const CLICKSTACK_CHART_PALETTE = {
|
||||
blue: '#437EEF', // Primary color for ClickStack
|
||||
orange: '#efb118',
|
||||
red: '#ff725c',
|
||||
cyan: '#6cc5b0',
|
||||
green: '#3ca951',
|
||||
pink: '#ff8ab7',
|
||||
purple: '#a463f2',
|
||||
lightBlue: '#97bbf5',
|
||||
brown: '#9c6b4e',
|
||||
gray: '#9498a0',
|
||||
// Highlighted variants (lighter shades for hover/selection states)
|
||||
redHighlight: '#ffa090',
|
||||
orangeHighlight: '#f5c94d',
|
||||
} as const;
|
||||
|
||||
// Ordered array for chart series - green first for brand consistency (HyperDX default)
|
||||
// Maps to CSS variables: COLORS[0] -> --color-chart-1, COLORS[1] -> --color-chart-2, etc.
|
||||
// NOTE: This is a fallback for SSR. In browser, getColorFromCSSVariable() reads from CSS variables
|
||||
export const COLORS = [
|
||||
CHART_PALETTE.green, // 1 - Brand green (primary)
|
||||
CHART_PALETTE.green, // 1 - Brand green (primary) - HyperDX default
|
||||
CHART_PALETTE.blue, // 2
|
||||
CHART_PALETTE.orange, // 3
|
||||
CHART_PALETTE.red, // 4
|
||||
|
|
@ -439,6 +458,63 @@ export const COLORS = [
|
|||
CHART_PALETTE.gray, // 10
|
||||
];
|
||||
|
||||
/**
|
||||
* Detects the active theme by checking for theme classes on documentElement.
|
||||
* Returns 'clickstack' if theme-clickstack class is present, 'hyperdx' otherwise.
|
||||
* Note: classList.contains() is O(1) and fast - no caching needed.
|
||||
*/
|
||||
function detectActiveTheme(): 'clickstack' | 'hyperdx' {
|
||||
if (typeof window === 'undefined') {
|
||||
// SSR: default to hyperdx (can't detect theme without DOM)
|
||||
return 'hyperdx';
|
||||
}
|
||||
|
||||
try {
|
||||
const isClickStack =
|
||||
document.documentElement.classList.contains('theme-clickstack');
|
||||
return isClickStack ? 'clickstack' : 'hyperdx';
|
||||
} catch {
|
||||
// Fallback if DOM access fails
|
||||
return 'hyperdx';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads chart color from CSS variable based on index.
|
||||
* CSS variables handle theme switching automatically via theme classes on documentElement.
|
||||
* Falls back to COLORS array if CSS variable is not available (SSR or getComputedStyle fails).
|
||||
*
|
||||
* Note on SSR/Hydration: During SSR, this returns fallback colors (HyperDX green palette).
|
||||
* On client hydration, it reads from CSS variables which may differ for ClickStack theme.
|
||||
* This is expected behavior - charts typically render after data fetching (client-side),
|
||||
* so hydration mismatches are rare. If needed, wrap chart components with suppressHydrationWarning.
|
||||
*/
|
||||
export function getColorFromCSSVariable(index: number): string {
|
||||
const colorArrayLength = COLORS.length;
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
// SSR: fallback to default colors (HyperDX palette)
|
||||
return COLORS[index % colorArrayLength];
|
||||
}
|
||||
|
||||
try {
|
||||
const cssVarName = `--color-chart-${(index % colorArrayLength) + 1}`;
|
||||
// Read from documentElement - CSS variables cascade from theme classes
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const color = computedStyle.getPropertyValue(cssVarName).trim();
|
||||
|
||||
// Only use CSS variable if it's actually set (non-empty)
|
||||
if (color && color !== '') {
|
||||
return color;
|
||||
}
|
||||
} catch {
|
||||
// Fallback if getComputedStyle fails
|
||||
}
|
||||
|
||||
// Fallback to default colors
|
||||
return COLORS[index % colorArrayLength];
|
||||
}
|
||||
|
||||
export function hashCode(str: string) {
|
||||
let hash = 0,
|
||||
i,
|
||||
|
|
@ -452,14 +528,81 @@ export function hashCode(str: string) {
|
|||
return hash;
|
||||
}
|
||||
|
||||
// Semantic colors for log levels (derived from palette)
|
||||
export const CHART_COLOR_SUCCESS = CHART_PALETTE.green;
|
||||
export const CHART_COLOR_WARNING = CHART_PALETTE.orange;
|
||||
export const CHART_COLOR_ERROR = CHART_PALETTE.red;
|
||||
/**
|
||||
* Gets theme-aware chart color from CSS variable or falls back to palette.
|
||||
* Reads from --color-chart-{type} CSS variable, falls back to theme-appropriate palette.
|
||||
*
|
||||
* Note on SSR/Hydration: During SSR, returns HyperDX colors as default.
|
||||
* On client, reads from CSS variables for accurate theme colors.
|
||||
* Charts typically render client-side after data fetching, minimizing hydration issues.
|
||||
*/
|
||||
function getSemanticChartColor(
|
||||
cssVarName: string,
|
||||
hyperdxColor: string,
|
||||
clickstackColor: string,
|
||||
): string {
|
||||
if (typeof window === 'undefined') {
|
||||
// SSR: use HyperDX as default (can't detect theme without DOM)
|
||||
return hyperdxColor;
|
||||
}
|
||||
|
||||
// Highlighted variants (derived from palette)
|
||||
export const CHART_COLOR_ERROR_HIGHLIGHT = CHART_PALETTE.redHighlight;
|
||||
export const CHART_COLOR_WARNING_HIGHLIGHT = CHART_PALETTE.orangeHighlight;
|
||||
try {
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const color = computedStyle.getPropertyValue(cssVarName).trim();
|
||||
if (color && color !== '') {
|
||||
return color;
|
||||
}
|
||||
} catch {
|
||||
// Fallback if getComputedStyle fails
|
||||
}
|
||||
|
||||
// Fallback to theme-appropriate palette
|
||||
const activeTheme = detectActiveTheme();
|
||||
return activeTheme === 'clickstack' ? clickstackColor : hyperdxColor;
|
||||
}
|
||||
|
||||
// Semantic colors for log levels (theme-aware)
|
||||
// These are functions that read from CSS variables with theme-appropriate fallbacks
|
||||
export function getChartColorSuccess(): string {
|
||||
return getSemanticChartColor(
|
||||
'--color-chart-success',
|
||||
CHART_PALETTE.green,
|
||||
CLICKSTACK_CHART_PALETTE.green,
|
||||
);
|
||||
}
|
||||
|
||||
export function getChartColorWarning(): string {
|
||||
return getSemanticChartColor(
|
||||
'--color-chart-warning',
|
||||
CHART_PALETTE.orange,
|
||||
CLICKSTACK_CHART_PALETTE.orange,
|
||||
);
|
||||
}
|
||||
|
||||
export function getChartColorError(): string {
|
||||
return getSemanticChartColor(
|
||||
'--color-chart-error',
|
||||
CHART_PALETTE.red,
|
||||
CLICKSTACK_CHART_PALETTE.red,
|
||||
);
|
||||
}
|
||||
|
||||
// Highlighted variants (theme-aware)
|
||||
export function getChartColorErrorHighlight(): string {
|
||||
return getSemanticChartColor(
|
||||
'--color-chart-error-highlight',
|
||||
CHART_PALETTE.redHighlight,
|
||||
CLICKSTACK_CHART_PALETTE.redHighlight,
|
||||
);
|
||||
}
|
||||
|
||||
export function getChartColorWarningHighlight(): string {
|
||||
return getSemanticChartColor(
|
||||
'--color-chart-warning-highlight',
|
||||
CHART_PALETTE.orangeHighlight,
|
||||
CLICKSTACK_CHART_PALETTE.orangeHighlight,
|
||||
);
|
||||
}
|
||||
|
||||
// Try to match log levels to colors
|
||||
export const semanticKeyedColor = (
|
||||
|
|
@ -469,47 +612,51 @@ export const semanticKeyedColor = (
|
|||
const logLevel = getLogLevelClass(`${key}`);
|
||||
if (logLevel != null) {
|
||||
return logLevel === 'error'
|
||||
? CHART_COLOR_ERROR
|
||||
? getChartColorError()
|
||||
: logLevel === 'warn'
|
||||
? CHART_COLOR_WARNING
|
||||
: CHART_COLOR_SUCCESS;
|
||||
? getChartColorWarning()
|
||||
: // Info-level logs use primary chart color (blue for ClickStack, green for HyperDX)
|
||||
getColorFromCSSVariable(0);
|
||||
}
|
||||
|
||||
return COLORS[index % COLORS.length];
|
||||
// Use CSS variable for theme-aware colors, fallback to hardcoded array
|
||||
return getColorFromCSSVariable(index);
|
||||
};
|
||||
|
||||
export const logLevelColor = (key: string | number | undefined) => {
|
||||
const logLevel = getLogLevelClass(`${key}`);
|
||||
return logLevel === 'error'
|
||||
? CHART_COLOR_ERROR
|
||||
? getChartColorError()
|
||||
: logLevel === 'warn'
|
||||
? CHART_COLOR_WARNING
|
||||
: CHART_COLOR_SUCCESS;
|
||||
? getChartColorWarning()
|
||||
: // Info-level logs use primary chart color (blue for ClickStack, green for HyperDX)
|
||||
getColorFromCSSVariable(0);
|
||||
};
|
||||
|
||||
// order of colors for sorting. green on bottom, then yellow, then red
|
||||
export const logLevelColorOrder = [
|
||||
logLevelColor('info'),
|
||||
logLevelColor('warn'),
|
||||
logLevelColor('error'),
|
||||
];
|
||||
// order of colors for sorting. primary color (blue/green) on bottom, then yellow, then red
|
||||
// Computed lazily to avoid DOM access at module initialization (SSR-safe)
|
||||
export function getLogLevelColorOrder(): string[] {
|
||||
return [logLevelColor('info'), logLevelColor('warn'), logLevelColor('error')];
|
||||
}
|
||||
|
||||
const getLevelColor = (logLevel?: string) => {
|
||||
if (logLevel == null) {
|
||||
return;
|
||||
}
|
||||
return logLevel === 'error'
|
||||
? CHART_COLOR_ERROR
|
||||
? getChartColorError()
|
||||
: logLevel === 'warn'
|
||||
? CHART_COLOR_WARNING
|
||||
: CHART_COLOR_SUCCESS;
|
||||
? getChartColorWarning()
|
||||
: // Info-level logs use primary chart color (blue for ClickStack, green for HyperDX)
|
||||
getColorFromCSSVariable(0);
|
||||
};
|
||||
|
||||
export const getColorProps = (index: number, level: string): string => {
|
||||
const logLevel = getLogLevelClass(level);
|
||||
const colorOverride = getLevelColor(logLevel);
|
||||
|
||||
return colorOverride ?? COLORS[index % COLORS.length];
|
||||
// Use CSS variable for theme-aware colors, fallback to hardcoded array
|
||||
return colorOverride ?? getColorFromCSSVariable(index);
|
||||
};
|
||||
|
||||
export const truncateMiddle = (str: string, maxLen = 10) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* stylelint-disable */
|
||||
/* stylelint-disable-file */
|
||||
// stylelint adds '}}' to end of file.. not sure why. Disabled for now.
|
||||
|
||||
@use '../src/theme/themes/base-tokens';
|
||||
|
|
@ -279,17 +279,6 @@ button:focus-visible {
|
|||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.hover-color-white {
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
a.mantine-focus-auto:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.recharts-tooltip-wrapper {
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
|
|
|
|||