Draw charts from one categorical palette, --chart-1…8
The one-hue ramp told lines apart by lightness alone. --chart-1 is now --primary and slots 2-8 are distinct hues (orange, violet, green, orchid, ochre, teal, magenta) that clear 3:1 on --card and stay apart under protanopia and deuteranopia; red and sage are left out as the status colours. A chart takes the slots in order instead of spreading a ramp, and a dashboard's own palette continues after its primary and listed colours with slots 2-8 rather than tints of its scheme. Line charts stay at five series. Tokens mirror index's, as design-check requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013GAeg4Jo6xjZzjwy9QDWMQ
This commit is contained in:
@@ -71,9 +71,9 @@ takes a `bool`, a gauge takes a number, an agenda takes a `list`, a
|
||||
notification takes a `record`. Bind one wrong and the editor says so rather
|
||||
than drawing nothing. The same table is enforced on the server.
|
||||
|
||||
Charts are the exception worth knowing: because identity across five series is
|
||||
carried by lightness alone, a chart with more than one series always draws a
|
||||
legend.
|
||||
Charts draw their series in one fixed order of colours: the primary first, then
|
||||
hues picked to stay apart, colour blindness included. A chart with more than
|
||||
one series still always draws a legend, so no line is told by colour alone.
|
||||
|
||||
## Two kinds of chart
|
||||
|
||||
@@ -200,9 +200,9 @@ A palette is an ordered list of colours, and **position is the role**:
|
||||
| 1 | the ground the dashboard sits on |
|
||||
| 2 | the surface a widget is |
|
||||
| 3 | the primary — fills, active controls, the first chart line |
|
||||
| 4 | the accent — the second chart line |
|
||||
| 4 | the accent |
|
||||
| 5 | text |
|
||||
| 6+ | further chart colours |
|
||||
| 6+ | the next chart lines, before the app's own chart colours |
|
||||
|
||||
Paste a [coolors.co](https://coolors.co) link (or a list of hex colours) and the
|
||||
whole dashboard is recoloured: every widget, the rail and the charts. Leave the
|
||||
|
||||
@@ -15,52 +15,31 @@ import { si } from "@/lib/utils"
|
||||
/**
|
||||
* How many lines one chart carries.
|
||||
*
|
||||
* The bound is the palette's: `--chart-1…5` is one designed ramp, and a sixth
|
||||
* line would either repeat a step or invent a colour outside the system.
|
||||
* Past five a legend is a puzzle, and five is as far as the palette keeps any
|
||||
* two lines apart for a colour-blind reader too, not only neighbouring ones.
|
||||
*/
|
||||
export const MAX_SERIES = 5
|
||||
|
||||
/**
|
||||
* The ramp's slots, as a palette names them.
|
||||
* The palette's slots, as a palette names them: `--chart-1…8`.
|
||||
*
|
||||
* The whole set one may draw from — five, for the same reason `MAX_SERIES` is
|
||||
* five, because they are the same five.
|
||||
* One categorical palette, taken in order. The first slot is the brand
|
||||
* primary and the rest are hues picked to stand apart from it and from each
|
||||
* other, so a line is told by its hue rather than by a step of lightness.
|
||||
*/
|
||||
export const CHART_SLOTS = Array.from({ length: MAX_SERIES }, (_, index) =>
|
||||
export const CHART_SLOTS = Array.from({ length: 8 }, (_, index) =>
|
||||
String(index + 1),
|
||||
)
|
||||
|
||||
/** No palette named; a chart left with this spreads itself. */
|
||||
/** No palette named; a chart left with this takes the slots in order. */
|
||||
export const NO_PALETTE: string[] = []
|
||||
|
||||
/**
|
||||
* What a chart of *n* lines draws with when no palette was named: the ramp
|
||||
* spread, rather than its first *n* steps.
|
||||
*
|
||||
* Three lines used to take slots 1, 2 and 3, which are adjacent steps of a
|
||||
* ramp that carries identity by lightness alone — at a 2px stroke they are
|
||||
* close to one picture. The ends and the middle are as far apart as five slots
|
||||
* allow, and the same reasoning gives every other count.
|
||||
*
|
||||
* This moved after the palette shipped, and moving it was safe *because* of
|
||||
* how the palette is stored: a named palette is honoured exactly as written,
|
||||
* so no stored document means anything different than it did. Only the
|
||||
* unwritten default moved, and any dashboard that dislikes where it moved to
|
||||
* can now name the old one. That inertness is the property worth keeping —
|
||||
* not this table, which is a design decision and may move again.
|
||||
* The slots a chart draws with: a named palette exactly as written, so no
|
||||
* stored document changes meaning, and otherwise the shared one in order.
|
||||
*/
|
||||
const SPREADS: Record<number, string[]> = {
|
||||
1: ["1"],
|
||||
2: ["1", "5"],
|
||||
3: ["1", "3", "5"],
|
||||
4: ["1", "2", "4", "5"],
|
||||
5: ["1", "2", "3", "4", "5"],
|
||||
}
|
||||
|
||||
/** The slots a chart of `count` lines draws with, named palette or not. */
|
||||
export function slotsFor(count: number, palette?: string[]): string[] {
|
||||
if (palette?.length) return palette
|
||||
return SPREADS[Math.min(Math.max(Math.trunc(count) || 1, 1), MAX_SERIES)]
|
||||
export function slotsFor(palette?: string[]): string[] {
|
||||
return palette?.length ? palette : CHART_SLOTS
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,9 +313,7 @@ export function UplotChart({
|
||||
let told: number | null = null
|
||||
/** Whether the click about to arrive is the end of a drag. */
|
||||
let dragging = false
|
||||
// Resolved once for the whole chart: how many lines there are is part of
|
||||
// which slots they take, when nothing named them.
|
||||
const slots = slotsFor(labels.length, palette)
|
||||
const slots = slotsFor(palette)
|
||||
// One builder for the whole chart: it is a factory, and the series only
|
||||
// need the function it returns.
|
||||
const spline = smooth ? { paths: uPlot.paths.spline?.() } : {}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function BarWidget({ widget }: WidgetProps) {
|
||||
const live = useBoundValues(rows.map((row) => row.message as string))
|
||||
// The dashboard's own data colours, in the order a chart would take them, so
|
||||
// a bar and a chart of the same readings agree about which line is which.
|
||||
const colors = slotsFor(rows.length, usePalette())
|
||||
const colors = slotsFor(usePalette())
|
||||
|
||||
if (rows.length === 0) {
|
||||
return <p className="text-muted-foreground">Pick a message.</p>
|
||||
|
||||
@@ -186,7 +186,7 @@ function RunsChart({ widget }: WidgetProps) {
|
||||
* The same chart, drawn from a data context.
|
||||
*
|
||||
* One line per member per bound message: three runs of a two-series chart is
|
||||
* six curves, capped like any other chart at what the ramp can tell apart.
|
||||
* six curves, capped like any other chart at what the palette can tell apart.
|
||||
*/
|
||||
function ContextChart({
|
||||
widget,
|
||||
@@ -262,7 +262,8 @@ function LiveChart({ widget }: WidgetProps) {
|
||||
) || DEFAULT_POINTS,
|
||||
)
|
||||
|
||||
// The dashboard's own data colours; outside one this is the whole ramp.
|
||||
// The dashboard's own data colours; outside one, none, and the chart takes
|
||||
// the shared palette.
|
||||
const palette = usePalette()
|
||||
|
||||
const histories = useQueries({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* The dashboard palette, checked.
|
||||
*
|
||||
* ponytail: a script rather than a suite, like `color.check.ts` beside it —
|
||||
* the frontend's only runner is Playwright and picking a ramp slot does not
|
||||
* the frontend's only runner is Playwright and picking a palette slot does not
|
||||
* need a browser:
|
||||
*
|
||||
* cd frontend && bun run src/components/Dashboard/palette.check.ts
|
||||
@@ -15,77 +15,46 @@
|
||||
* default safe to move afterwards.
|
||||
*
|
||||
* The one that is free to move is the **default** itself — where a chart that
|
||||
* named no palette lands. It started as the ramp in order, which gave three
|
||||
* lines the three adjacent steps 1, 2 and 3; it is now spread. That is a
|
||||
* design decision, pinned here so it changes on purpose rather than by
|
||||
* accident, and a five-line chart is unchanged either way.
|
||||
* named no palette lands. It started as a one-hue ramp in order, was spread
|
||||
* across that ramp, and is now the categorical `--chart-1…8` in order. That is
|
||||
* a design decision, pinned here so it changes on purpose rather than by
|
||||
* accident.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict"
|
||||
|
||||
import {
|
||||
CHART_SLOTS,
|
||||
MAX_SERIES,
|
||||
NO_PALETTE,
|
||||
slotsFor,
|
||||
} from "@/components/Common/UplotChart"
|
||||
|
||||
// --- the default: spread, not the first n steps ---------------------------
|
||||
// --- the default: the shared palette, in order ----------------------------
|
||||
|
||||
const SPREAD: Record<number, string[]> = {
|
||||
1: ["1"],
|
||||
2: ["1", "5"],
|
||||
3: ["1", "3", "5"],
|
||||
4: ["1", "2", "4", "5"],
|
||||
5: ["1", "2", "3", "4", "5"],
|
||||
}
|
||||
|
||||
for (const [lines, want] of Object.entries(SPREAD)) {
|
||||
assert.deepEqual(
|
||||
CHART_SLOTS,
|
||||
["1", "2", "3", "4", "5", "6", "7", "8"],
|
||||
"eight slots, named as --chart-1…8 is",
|
||||
)
|
||||
for (const stored of [undefined, NO_PALETTE]) {
|
||||
assert.deepEqual(
|
||||
slotsFor(Number(lines)),
|
||||
want,
|
||||
`${lines} lines with no palette named`,
|
||||
slotsFor(stored),
|
||||
CHART_SLOTS,
|
||||
"naming no palette, however it is stored, takes the slots in order",
|
||||
)
|
||||
}
|
||||
|
||||
// Five lines take the whole ramp in order, which is what they always took.
|
||||
assert.deepEqual(
|
||||
slotsFor(CHART_SLOTS.length),
|
||||
[...CHART_SLOTS],
|
||||
"a five-line chart is drawn exactly as it was before palettes existed",
|
||||
assert.ok(
|
||||
MAX_SERIES <= CHART_SLOTS.length,
|
||||
"a full chart never repeats a colour",
|
||||
)
|
||||
|
||||
// A chart is never left without a colour, whatever it is handed.
|
||||
for (const lines of [0, -3, 1.5, 9, Number.NaN]) {
|
||||
const slots = slotsFor(lines)
|
||||
assert.ok(slots?.length > 0, `${lines} lines still gets a colour`)
|
||||
}
|
||||
|
||||
// --- inertness: a named palette is drawn as written -----------------------
|
||||
|
||||
// A dashboard names colours now (`ui/core/theme.ts`), and a chart draws them
|
||||
// as they were written: not re-spread, not widened, not reordered.
|
||||
// as they were written: not re-spread, not widened, not reordered. A chart
|
||||
// with more lines than it names cycles through them.
|
||||
const NAMED = ["#2a9d8f", "#e9c46a"]
|
||||
|
||||
assert.deepEqual(
|
||||
slotsFor(2, NAMED),
|
||||
NAMED,
|
||||
"a named palette is never re-spread",
|
||||
)
|
||||
assert.deepEqual(
|
||||
slotsFor(5, NAMED),
|
||||
NAMED,
|
||||
"and it is not widened to fit more lines either — the chart cycles",
|
||||
)
|
||||
|
||||
// Naming none means the same thing however it is stored: the chart spreads.
|
||||
for (const stored of [undefined, NO_PALETTE]) {
|
||||
for (let lines = 1; lines <= CHART_SLOTS.length; lines++) {
|
||||
assert.deepEqual(
|
||||
slotsFor(lines, stored),
|
||||
SPREAD[lines],
|
||||
`${lines} lines still spread`,
|
||||
)
|
||||
}
|
||||
}
|
||||
assert.deepEqual(slotsFor(NAMED), NAMED, "a named palette is drawn as written")
|
||||
|
||||
console.log("palette: ok")
|
||||
|
||||
@@ -172,14 +172,13 @@ const PaletteContext = createContext(NO_PALETTE)
|
||||
/**
|
||||
* The data colours everything drawn under it uses.
|
||||
*
|
||||
* The colours a palette named, in draw order — the primary first, then the
|
||||
* accent, then whatever it listed after the roles. Empty when the dashboard
|
||||
* names no palette, which is not the same as "the ramp": a chart with no
|
||||
* palette spreads itself across the ramp by how many lines it draws
|
||||
* (`slotsFor`), and only a named palette overrides that.
|
||||
* The colours a palette named, in draw order — the primary first, then
|
||||
* whatever it listed after the roles, then the shared palette's own hues.
|
||||
* Empty when the dashboard names no palette: a chart then takes the shared
|
||||
* `--chart-1…8` in order (`slotsFor`).
|
||||
*
|
||||
* Mounted by the view *and* the editor: an editor drawing the automatic
|
||||
* spread while the panel beside it showed the dashboard's own palette would
|
||||
* Mounted by the view *and* the editor: an editor drawing the shared
|
||||
* palette while the panel beside it showed the dashboard's own palette would
|
||||
* be a preview that lies.
|
||||
*
|
||||
* Data colour only. A fault stays `--destructive` and a condition stays its
|
||||
@@ -203,7 +202,7 @@ export function PaletteProvider({
|
||||
)
|
||||
}
|
||||
|
||||
/** The ramp slots the dashboard around this widget named, or none. */
|
||||
/** The data colours the dashboard around this widget named, or none. */
|
||||
export const usePalette = () => useContext(PaletteContext)
|
||||
|
||||
const LockedContext = createContext(false)
|
||||
|
||||
@@ -125,7 +125,7 @@ export type BarReading = {
|
||||
fraction: number
|
||||
precision: number | null
|
||||
unit?: string
|
||||
/** A colour, or a slot of the app's ramp — whatever the palette named. */
|
||||
/** A colour, or a slot of the app's palette — whatever was named. */
|
||||
color: string
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict"
|
||||
import { readFileSync } from "node:fs"
|
||||
|
||||
import {
|
||||
contrast,
|
||||
isLight,
|
||||
luminance,
|
||||
MAX_ROLES,
|
||||
mix,
|
||||
onColor,
|
||||
parsePalette,
|
||||
@@ -96,14 +96,34 @@ assert.equal(named.surface, CHOSEN[1])
|
||||
assert.equal(named.primary, CHOSEN[2])
|
||||
assert.equal(named.accent, CHOSEN[3])
|
||||
assert.equal(named.foreground, CHOSEN[4])
|
||||
assert.deepEqual(
|
||||
named.series.slice(0, 2),
|
||||
[CHOSEN[2], CHOSEN[3]],
|
||||
"a chart draws with the primary first, then the accent",
|
||||
assert.equal(named.series[0], CHOSEN[2], "a chart draws with the primary first")
|
||||
assert.ok(
|
||||
!named.series.includes(CHOSEN[3]),
|
||||
"the accent is a role, not a data colour",
|
||||
)
|
||||
|
||||
const sixth = rolesOf([...CHOSEN, "#8ab17d"])
|
||||
assert.equal(sixth?.series[2], "#8ab17d", "a sixth colour is the third series")
|
||||
assert.equal(sixth?.series[1], "#8ab17d", "a sixth colour is the second series")
|
||||
|
||||
// After the palette's own, the shared hues: the literals `theme.ts` carries
|
||||
// for a canvas are the ones `index.css` declares, theme by theme.
|
||||
const css = readFileSync(
|
||||
new URL("../../../../index.css", import.meta.url),
|
||||
"utf8",
|
||||
)
|
||||
const block = (opener: string) => css.slice(css.indexOf(opener)).split("}")[0]
|
||||
const slots = (text: string) =>
|
||||
[...text.matchAll(/--chart-([2-8]): (#[0-9a-f]{6});/g)].map((m) => m[2])
|
||||
assert.deepEqual(
|
||||
rolesOf(["#ffffff"])?.series.slice(1),
|
||||
slots(block(":root {")),
|
||||
"a light dashboard continues with the light --chart-2…8",
|
||||
)
|
||||
assert.deepEqual(
|
||||
rolesOf(["#0a0a0a"])?.series.slice(1),
|
||||
slots(block(".dark {")),
|
||||
"a dark dashboard continues with the dark --chart-2…8",
|
||||
)
|
||||
|
||||
const dark = rolesOf(["#0f172a"])
|
||||
assert.ok(dark)
|
||||
@@ -126,8 +146,8 @@ assert.equal(rolesOf([]), null, "no colours is no palette")
|
||||
for (const roles of [named, dark, light, sixth]) {
|
||||
assert.ok(roles)
|
||||
assert.ok(
|
||||
roles.series.length >= MAX_ROLES,
|
||||
"a chart of five lines never has to repeat a colour",
|
||||
roles.series.length >= 8,
|
||||
"a chart never has to repeat a colour before its ninth line",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* A palette is an ordered list of hex colours — pasted from a coolors.co link
|
||||
* or typed — and position is the role: background, surface, primary, accent,
|
||||
* text, then anything after that is another colour for a chart to draw with.
|
||||
* A chart starts on the primary, then those, then the shared palette's own.
|
||||
* Trailing roles may simply be left off; what is missing is derived from what
|
||||
* is there, so three colours are a whole dashboard.
|
||||
*
|
||||
@@ -39,6 +40,32 @@ const BRAND = {
|
||||
dark: { primary: "#7ba3b8", accent: "#e5a184" },
|
||||
} as const
|
||||
|
||||
/**
|
||||
* `--chart-2…8` from `index.css`, per theme, as literals: a chart canvas cannot
|
||||
* read a custom property, and the slot a dashboard's own colours push them to
|
||||
* is not the one the stylesheet put them in. Keep in step with `index.css`.
|
||||
*/
|
||||
const CHART = {
|
||||
light: [
|
||||
"#e26f00",
|
||||
"#6f49ca",
|
||||
"#00721b",
|
||||
"#c66ae1",
|
||||
"#966a00",
|
||||
"#00a093",
|
||||
"#b32474",
|
||||
],
|
||||
dark: [
|
||||
"#d26600",
|
||||
"#8d71e6",
|
||||
"#55c15e",
|
||||
"#dd8bf6",
|
||||
"#ae7c00",
|
||||
"#00c0b1",
|
||||
"#bf4483",
|
||||
],
|
||||
} as const
|
||||
|
||||
/**
|
||||
* The hex colours in a piece of text, in the order they appear.
|
||||
*
|
||||
@@ -150,7 +177,7 @@ export type Roles = {
|
||||
primary: string
|
||||
accent: string
|
||||
foreground: string
|
||||
/** Data colours, in draw order. At least five, so a full chart never repeats. */
|
||||
/** Data colours, in draw order. At least eight, so a full chart never repeats. */
|
||||
series: string[]
|
||||
}
|
||||
|
||||
@@ -164,7 +191,8 @@ export function rolesOf(hexes: string[]): Roles | null {
|
||||
const [background, surface, primary, accent, foreground, ...extra] = hexes
|
||||
if (!background) return null
|
||||
|
||||
const brand = BRAND[isLight(background) ? "light" : "dark"]
|
||||
const theme = isLight(background) ? "light" : "dark"
|
||||
const brand = BRAND[theme]
|
||||
const fore = foreground ?? onColor(background)
|
||||
const roles: Roles = {
|
||||
background,
|
||||
@@ -178,17 +206,9 @@ export function rolesOf(hexes: string[]): Roles | null {
|
||||
foreground: fore,
|
||||
series: [],
|
||||
}
|
||||
roles.series = [roles.primary, roles.accent, ...extra]
|
||||
// Enough colours for a full chart, taken from the two the palette named
|
||||
// rather than from a ramp the dashboard never chose.
|
||||
for (const tint of [
|
||||
mix(roles.primary, fore, 0.35),
|
||||
mix(roles.accent, fore, 0.35),
|
||||
mix(roles.primary, roles.accent, 0.5),
|
||||
]) {
|
||||
if (roles.series.length >= MAX_ROLES) break
|
||||
roles.series.push(tint)
|
||||
}
|
||||
// The palette's own primary leads, as `--primary` leads the shared one, and
|
||||
// the rest are hues chosen to stand apart rather than shades of the scheme.
|
||||
roles.series = [roles.primary, ...extra, ...CHART[theme]]
|
||||
return roles
|
||||
}
|
||||
|
||||
@@ -229,5 +249,8 @@ export function tokenStyle(roles: Roles): Record<string, string> {
|
||||
"--chart-3": series[2],
|
||||
"--chart-4": series[3],
|
||||
"--chart-5": series[4],
|
||||
"--chart-6": series[5],
|
||||
"--chart-7": series[6],
|
||||
"--chart-8": series[7],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function useAnimatedFraction(fraction: number): MotionValue<number> {
|
||||
/**
|
||||
* A palette entry as CSS.
|
||||
*
|
||||
* A dashboard names colours, and a slot of the app's own ramp is what a
|
||||
* A dashboard names colours, and a slot of the app's own palette is what a
|
||||
* document written before it could says instead — so one is used as it stands
|
||||
* and the other is looked up.
|
||||
*/
|
||||
|
||||
@@ -6,10 +6,10 @@ import { Sparkline } from "@/components/Common/Sparkline"
|
||||
*
|
||||
* Sixty slices of whatever window is selected, so the curve stays the same
|
||||
* width and only its resolution moves. The same curve the node panel and the
|
||||
* edge popover draw, dot included, in the chart ramp this page's other graphs
|
||||
* use. The dot marks the newest slice rather than this instant: the server
|
||||
* holds the slice that is still filling back, so the curve ends on one that is
|
||||
* all there.
|
||||
* edge popover draw, dot included, in the first chart colour this page's
|
||||
* other graphs open with. The dot marks the newest slice rather than this
|
||||
* instant: the server holds the slice that is still filling back, so the
|
||||
* curve ends on one that is all there.
|
||||
*/
|
||||
export function Spark({ counts }: { counts: number[] }) {
|
||||
const points: HistoryPoint[] = counts.map((value, index) => ({
|
||||
|
||||
+30
-17
@@ -75,6 +75,9 @@
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-6: var(--chart-6);
|
||||
--color-chart-7: var(--chart-7);
|
||||
--color-chart-8: var(--chart-8);
|
||||
--font-mono:
|
||||
ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Menlo, Consolas,
|
||||
monospace;
|
||||
@@ -90,13 +93,17 @@
|
||||
*
|
||||
* `--primary-nested` is that same hue and saturation a few steps deeper, for
|
||||
* a reading drawn inside a `--primary` fill — the bar widget's stacked
|
||||
* segments. No slot of the chart ramp clears the 3:1 non-text guideline
|
||||
* against `--primary` (`--chart-5`, which the bar used, measures 2.53:1), so
|
||||
* the nested fill needs a token of its own. It cannot clear 3:1 against
|
||||
* `--muted` as well in dark: `--primary` and `--muted` are only 5.82:1 apart
|
||||
* there, and a colour 3:1 from both would have to sit in a 9:1 gap. The
|
||||
* segment is therefore drawn inside a gutter of the fill rather than ever
|
||||
* bordering the track.
|
||||
* segments. No slot of the one-hue chart ramp that came before the palette
|
||||
* cleared the 3:1 non-text guideline against `--primary` (`--chart-5`, which
|
||||
* the bar used, measured 2.53:1), so the nested fill needs a token of its
|
||||
* own. It cannot clear 3:1 against `--muted` as well in dark: `--primary` and
|
||||
* `--muted` are only 5.82:1 apart there, and a colour 3:1 from both would have
|
||||
* to sit in a 9:1 gap. The segment is therefore drawn inside a gutter of the
|
||||
* fill rather than ever bordering the track.
|
||||
*
|
||||
* `--chart-1…8` is one categorical palette, taken in order: slot 1 is
|
||||
* `--primary`, the rest are hues that clear 3:1 against `--card` and stay
|
||||
* apart under colour blindness. See DESIGN-GUIDELINES.md → Colour tokens.
|
||||
*/
|
||||
/* `.light` is the symmetric half of `.dark` below: both are plain classes, so
|
||||
either themes a subtree as well as the whole document — which is what lets a
|
||||
@@ -134,11 +141,14 @@
|
||||
--sidebar-border: #e4e4e4;
|
||||
--sidebar-ring: #4a7189;
|
||||
--status-success: #5e8b6d;
|
||||
--chart-1: #3f5f74;
|
||||
--chart-2: #4f768e;
|
||||
--chart-3: #688ba1;
|
||||
--chart-4: #83a2b5;
|
||||
--chart-5: #9eb8c9;
|
||||
--chart-1: #4a7189;
|
||||
--chart-2: #e26f00;
|
||||
--chart-3: #6f49ca;
|
||||
--chart-4: #00721b;
|
||||
--chart-5: #c66ae1;
|
||||
--chart-6: #966a00;
|
||||
--chart-7: #00a093;
|
||||
--chart-8: #b32474;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -172,11 +182,14 @@
|
||||
--sidebar-border: #2a2a2a;
|
||||
--sidebar-ring: #7ba3b8;
|
||||
--status-success: #87b596;
|
||||
--chart-1: #94b0be;
|
||||
--chart-2: #7999aa;
|
||||
--chart-3: #5f8396;
|
||||
--chart-4: #496d7f;
|
||||
--chart-5: #395767;
|
||||
--chart-1: #7ba3b8;
|
||||
--chart-2: #d26600;
|
||||
--chart-3: #8d71e6;
|
||||
--chart-4: #55c15e;
|
||||
--chart-5: #dd8bf6;
|
||||
--chart-6: #ae7c00;
|
||||
--chart-7: #00c0b1;
|
||||
--chart-8: #bf4483;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
Reference in New Issue
Block a user