chore: adjust colors generator
This commit is contained in:
+113
-36
@@ -1,55 +1,132 @@
|
||||
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
||||
import { converter, modeHsl, modeRgb, useMode } from 'culori/fn';
|
||||
import { clampChroma, formatHex, modeOklch, modeRgb, useMode } from 'culori/fn';
|
||||
import useUserSettings from './use-user-settings';
|
||||
|
||||
const toHsl = converter('hsl');
|
||||
|
||||
const DEFAULT_BRAND_HUE = 211; // brand color (HSL hue)
|
||||
|
||||
// Cool palette spans blue -> violet. The range starts just above the brand hue
|
||||
// so generated colors never sit right next to the brand color.
|
||||
const COOL_HUE_START = 215;
|
||||
const COOL_HUE_END = 310;
|
||||
// Linear-style minimal/neutral palette: a TIGHT blue -> violet band in OKLCH
|
||||
// (perceptually uniform), kept moderate-chroma so fills read as clean and gentle
|
||||
// rather than candy-colored. We deliberately stay narrow and DON'T fan out to
|
||||
// green/magenta — in this aesthetic series are separated by LIGHTNESS, not by
|
||||
// spreading across the hue wheel. Every series (including the first) is generated
|
||||
// from this one ramp, so the whole palette stays in a single cohesive color
|
||||
// family — no special high-saturation brand color that clashes with the rest.
|
||||
const COOL_HUE_START = 250; // blue
|
||||
const COOL_HUE_END = 315; // violet (stops short of magenta/pink, stays neutral-cool)
|
||||
const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START;
|
||||
|
||||
function colorStringToHue(input?: string): number | undefined {
|
||||
if (!input) return undefined;
|
||||
const color = toHsl(input);
|
||||
if (!color || typeof color.h !== 'number') return undefined;
|
||||
return color.h;
|
||||
}
|
||||
|
||||
export default function useCoolColors() {
|
||||
// const brandHue = useMemo(
|
||||
// () => colorStringToHue(userSettings.colorPrimary) ?? DEFAULT_BRAND_HUE,
|
||||
// [userSettings.colorPrimary]
|
||||
// );
|
||||
/**
|
||||
* Vivid, distinct cool accents — for places that need a handful of "primary"
|
||||
* colors, one per card/section (e.g. the summary trend cards), NOT a stacked
|
||||
* multi-series palette. Every color is anchor-quality (bright + saturated) and
|
||||
* spread evenly across the blue→violet band, so the set reads as several equally
|
||||
* strong primaries rather than one bold + several washed-out fills.
|
||||
*/
|
||||
export function useCoolAccents() {
|
||||
useMode(modeRgb);
|
||||
useMode(modeHsl);
|
||||
useMode(modeOklch);
|
||||
|
||||
const brandHue = DEFAULT_BRAND_HUE;
|
||||
const { isDarkTheme } = useUserSettings();
|
||||
|
||||
return useMemoizedFn((count: number): string[] => {
|
||||
if (count <= 0) return [];
|
||||
|
||||
// First series keeps the brand color.
|
||||
const colors: string[] = [`hsl(${brandHue}, 100%, 50%)`];
|
||||
const l = isDarkTheme ? 0.62 : 0.64;
|
||||
const c = isDarkTheme ? 0.16 : 0.2;
|
||||
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const t = count <= 1 ? 0 : i / (count - 1);
|
||||
const hue = COOL_HUE_START + t * COOL_HUE_RANGE;
|
||||
out.push(
|
||||
formatHex(clampChroma({ mode: 'oklch', l, c, h: hue }, 'oklch'))
|
||||
);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
export default function useCoolColors() {
|
||||
useMode(modeRgb);
|
||||
useMode(modeOklch);
|
||||
|
||||
const { isDarkTheme } = useUserSettings();
|
||||
|
||||
return useMemoizedFn((count: number): string[] => {
|
||||
if (count <= 0) return [];
|
||||
|
||||
// Moderate chroma: clean and crisp, but gentle (not neon). Too low reads as
|
||||
// muddy/dirty; too high reads as harsh. Dark mode a touch lower so fills
|
||||
// stay calm against the dark canvas.
|
||||
const baseChroma = isDarkTheme ? 0.085 : 0.12;
|
||||
|
||||
// First series is the "primary" anchor: SAME blue family (the bluest end of
|
||||
// the ramp, nearest the brand hue) but clearly brighter and more saturated —
|
||||
// a vivid, clean brand-blue that reads as the base color. The rest of the
|
||||
// palette stays low-chroma, so the anchor pops as the primary while the
|
||||
// family still feels cohesive.
|
||||
const colors: string[] = [
|
||||
formatHex(
|
||||
clampChroma(
|
||||
{
|
||||
mode: 'oklch',
|
||||
l: isDarkTheme ? 0.62 : 0.66,
|
||||
c: isDarkTheme ? 0.16 : 0.2,
|
||||
h: COOL_HUE_START
|
||||
},
|
||||
'oklch'
|
||||
)
|
||||
)
|
||||
];
|
||||
|
||||
const rest = count - 1;
|
||||
if (rest <= 0) return colors;
|
||||
|
||||
// Separation is driven by LIGHTNESS, not hue. The hue band is narrow, so as
|
||||
// the count grows we add lightness TIERS — each tier reuses the same narrow
|
||||
// hue ramp at a distinct lightness level, multiplying how many separable
|
||||
// colors fit while keeping the whole palette in one cohesive family.
|
||||
const tiers = rest <= 6 ? 1 : rest <= 12 ? 2 : 3;
|
||||
const steps = Math.ceil(rest / tiers);
|
||||
|
||||
// Distinct lightness levels per tier count (index 0 → 2 tiers, 1 → 3 tiers).
|
||||
// Lighter, airier levels for a fresh/crisp feel; kept in the upper-mid range
|
||||
// so fills stay clean and legible.
|
||||
const lightTiers = isDarkTheme
|
||||
? [
|
||||
[0.68, 0.5],
|
||||
[0.72, 0.6, 0.48]
|
||||
]
|
||||
: [
|
||||
[0.82, 0.64],
|
||||
[0.84, 0.72, 0.6]
|
||||
];
|
||||
// Single-tier light/dark zig-zag for the common small-count case.
|
||||
const [lightLo, lightHi] = isDarkTheme ? [0.5, 0.68] : [0.64, 0.82];
|
||||
|
||||
for (let i = 0; i < rest; i++) {
|
||||
// Evenly divide the hue range. Because `count` is known up front, even
|
||||
// spacing guarantees the largest possible minimum gap between any two
|
||||
// hues — golden-ratio sampling can land two values close together after
|
||||
// wrapping into this narrow cool band.
|
||||
const t = rest <= 1 ? 0.5 : (i + 0.5) / rest;
|
||||
// Cycle the tier on every step so consecutive series always differ in
|
||||
// lightness — exactly where stacked bars are hardest to tell apart.
|
||||
const tier = i % tiers;
|
||||
const step = Math.floor(i / tiers);
|
||||
|
||||
// Even hue ramp across the narrow band. Each tier is offset by a fraction
|
||||
// of a step so same-step colors in different tiers don't share a hue.
|
||||
const t = steps <= 1 ? 0.5 : (step + tier / tiers) / steps;
|
||||
const hue = COOL_HUE_START + t * COOL_HUE_RANGE;
|
||||
|
||||
// Zig-zag saturation/lightness on a second axis. HSL is not perceptually
|
||||
// uniform (blues/purples look alike at equal hue steps), so alternating
|
||||
// lightness is what keeps adjacent series clearly distinct.
|
||||
const saturation = 72 + (i % 3) * 9; // 72 / 81 / 90
|
||||
const lightness = i % 2 ? 46 : 62; // alternate darker / lighter
|
||||
// 1 tier (≤6 series): light/dark zig-zag. 2-3 tiers: the tier's level.
|
||||
const lightness =
|
||||
tiers === 1 ? (i % 2 ? lightLo : lightHi) : lightTiers[tiers - 2][tier];
|
||||
|
||||
colors.push(`hsl(${Math.round(hue)}, ${saturation}%, ${lightness}%)`);
|
||||
// Clamp chroma into the sRGB gamut so values don't get distorted by a raw
|
||||
// channel clip when serialized to hex.
|
||||
colors.push(
|
||||
formatHex(
|
||||
clampChroma(
|
||||
{ mode: 'oklch', l: lightness, c: baseChroma, h: hue },
|
||||
'oklch'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return colors;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* each domain's natural unit (tokens / GPU-Hours / GB-Days); a true
|
||||
* cross-resource split needs a common unit.
|
||||
*/
|
||||
import useCoolColors from '@/hooks/use-cool-colors';
|
||||
import { useCoolAccents } from '@/hooks/use-cool-colors';
|
||||
import BarChart from '@/pages/_components/bar-chart';
|
||||
import PieChart from '@/pages/_components/pie-chart';
|
||||
import { formatLargeNumber } from '@/utils';
|
||||
@@ -209,7 +209,8 @@ const SummaryTab: React.FC = () => {
|
||||
const access = useAccess();
|
||||
const intl = useIntl();
|
||||
const t = (id: string) => intl.formatMessage({ id });
|
||||
const coolColors = useCoolColors()(8);
|
||||
// One vivid primary per summary card (Tokens / Compute / Storage).
|
||||
const coolColors = useCoolAccents()(3);
|
||||
|
||||
// No All/My dropdown (matches the Tokens tab): managers see the org-wide
|
||||
// view and narrow it with the user filter, others only their own rows.
|
||||
@@ -499,13 +500,13 @@ const SummaryTab: React.FC = () => {
|
||||
<Col span={24}>
|
||||
<DomainSection
|
||||
title={t('usage.tabs.storage')}
|
||||
accent={coolColors[3]}
|
||||
accent={coolColors[2]}
|
||||
donutData={storageDonut}
|
||||
donutTotalLabel={t('usage.metric.gbDays')}
|
||||
trendTitle={t('usage.summary.gbDaysOverTime')}
|
||||
trendXAxis={storageTrend.xAxis}
|
||||
trendData={storageTrend.data}
|
||||
trendColor={coolColors[3]}
|
||||
trendColor={coolColors[2]}
|
||||
trendGran={granularity}
|
||||
pieLoading={storageByTypeLoading}
|
||||
barLoading={storageByDateLoading}
|
||||
|
||||
Reference in New Issue
Block a user