@carlonicora/nextjs-jsonapi 6.3.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,24 +48,6 @@ function useUsageFormatters() {
48
48
  }
49
49
  __name(useUsageFormatters, "useUsageFormatters");
50
50
 
51
- // src/features/tokenusage/lib/metrics.ts
52
- function metricValue(row, metric) {
53
- if (metric === "cost") return row.cost;
54
- if (metric === "credits") return row.credits;
55
- return row.tokensIn + row.tokensOut;
56
- }
57
- __name(metricValue, "metricValue");
58
- function cacheHitPercentage(cached, tokensIn) {
59
- if (!tokensIn) return 0;
60
- return cached / tokensIn * 100;
61
- }
62
- __name(cacheHitPercentage, "cacheHitPercentage");
63
- function percentageDelta(current, previous) {
64
- if (!previous) return void 0;
65
- return Math.round((current - previous) / previous * 100);
66
- }
67
- __name(percentageDelta, "percentageDelta");
68
-
69
51
  // src/features/tokenusage/lib/palette.ts
70
52
  var CATEGORICAL_LIGHT = [
71
53
  "#2a78d6",
@@ -95,6 +77,28 @@ function seriesColor(index, mode = "light") {
95
77
  return categoricalSet(mode)[index];
96
78
  }
97
79
  __name(seriesColor, "seriesColor");
80
+ function seriesTheme(index) {
81
+ return { light: seriesColor(index, "light"), dark: seriesColor(index, "dark") };
82
+ }
83
+ __name(seriesTheme, "seriesTheme");
84
+
85
+ // src/features/tokenusage/lib/metrics.ts
86
+ function metricValue(row, metric) {
87
+ if (metric === "cost") return row.cost;
88
+ if (metric === "credits") return row.credits;
89
+ return row.tokensIn + row.tokensOut;
90
+ }
91
+ __name(metricValue, "metricValue");
92
+ function cacheHitPercentage(cached, tokensIn) {
93
+ if (!tokensIn) return 0;
94
+ return cached / tokensIn * 100;
95
+ }
96
+ __name(cacheHitPercentage, "cacheHitPercentage");
97
+ function percentageDelta(current, previous) {
98
+ if (!previous) return void 0;
99
+ return Math.round((current - previous) / previous * 100);
100
+ }
101
+ __name(percentageDelta, "percentageDelta");
98
102
 
99
103
  export {
100
104
  createUsageFormatters,
@@ -105,6 +109,7 @@ export {
105
109
  CATEGORICAL_CEILING,
106
110
  OTHER_COLOR,
107
111
  SEQUENTIAL_RAMP,
108
- seriesColor
112
+ seriesColor,
113
+ seriesTheme
109
114
  };
110
- //# sourceMappingURL=chunk-ZDUT5JPT.mjs.map
115
+ //# sourceMappingURL=chunk-DYOSXIEL.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/features/tokenusage/lib/formatters.ts","../src/features/tokenusage/lib/palette.ts","../src/features/tokenusage/lib/metrics.ts"],"sourcesContent":["\"use client\";\n\nimport { useLocale } from \"next-intl\";\nimport { useMemo } from \"react\";\nimport type { Metric } from \"../data/tokenusage-admin.types\";\nimport { getTokenUsageCurrency } from \"./config\";\n\nexport type UsageFormatters = {\n /** Locale-aware fixed-decimal number, e.g. 9.46 → \"9,46\" in it-IT. */\n decimal(value: number, decimals: number): string;\n /** Currency for cost, 2 decimals for credits, whole numbers for tokens. */\n metricValue(value: number, metric: Metric): string;\n /** Currency with an explicit decimal count — the per-call tile needs 4. */\n currency(value: number, decimals: number): string;\n /** Percentage with one decimal by default, e.g. 60 → \"60,0 %\" in it-IT. */\n percent(value: number, decimals?: number): string;\n /** Compact notation for axis ticks, e.g. 1500000 → \"1,5 Mln\" in it-IT. */\n compact(value: number): string;\n /** A \"YYYY-MM-DD\" bucket key rendered as an axis or tooltip label, in UTC. */\n bucketDate(iso: string, granularity: \"day\" | \"week\" | \"month\"): string;\n /** Locale collation for client-side table sorting. */\n compare(a: string, b: string): number;\n};\n\n/**\n * Builds every formatter the token-usage surfaces need, for one locale and one\n * currency.\n *\n * A pure factory on purpose: it takes no React context, so the arithmetic and\n * the formatting are unit-testable without rendering, and a server component or\n * a test can construct a set for an arbitrary locale.\n *\n * The currency symbol is placed with a space rather than through\n * `style: \"currency\"`, which is what the previous hard-coded implementation\n * rendered (\"€ 9,46\"). Keeping that shape means a consuming app on EUR sees\n * byte-identical output after this refactor.\n *\n * `bucketDate` formats in UTC, always. The wire value is a `type: \"date\"` — a\n * calendar day — parsed to UTC midnight; reading it back with local getters\n * would shift the label a day early west of UTC.\n */\nexport function createUsageFormatters(locale: string, currency: string): UsageFormatters {\n const symbol =\n new Intl.NumberFormat(locale, { style: \"currency\", currency, currencyDisplay: \"narrowSymbol\" })\n .formatToParts(0)\n .find((part) => part.type === \"currency\")?.value ?? currency;\n\n const decimal = (value: number, decimals: number): string =>\n value.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });\n\n const dayFormat = new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"short\", timeZone: \"UTC\" });\n const monthFormat = new Intl.DateTimeFormat(locale, { month: \"short\", year: \"numeric\", timeZone: \"UTC\" });\n // Axis ticks step by 1, 2, 2.5 or 5 times a power of ten, so a tick never needs\n // more than three significant digits. A fixed one-decimal cap rounded a 0.05\n // step into repeated labels (0.1, 0.1, 0.2, 0.2); significant digits keep every\n // tick distinct at any scale (0.05, 0.15, 1.25K).\n const compactFormat = new Intl.NumberFormat(locale, { notation: \"compact\", maximumSignificantDigits: 3 });\n const collator = new Intl.Collator(locale);\n\n return {\n decimal,\n\n metricValue(value, metric) {\n if (metric === \"cost\") return `${symbol} ${decimal(value, 2)}`;\n // Credits are stored to 4 decimals (round4(cost / creditCost)), but that\n // precision is noise to a reader comparing rows: one decimal is enough to\n // separate two values and keeps the columns narrow enough to scan.\n if (metric === \"credits\") return decimal(value, 1);\n return decimal(value, 0);\n },\n\n currency(value, decimals) {\n return `${symbol} ${decimal(value, decimals)}`;\n },\n\n percent(value, decimals = 1) {\n return `${decimal(value, decimals)} %`;\n },\n\n compact(value) {\n return compactFormat.format(value);\n },\n\n bucketDate(iso, granularity) {\n const date = new Date(`${iso}T00:00:00.000Z`);\n return granularity === \"month\" ? monthFormat.format(date) : dayFormat.format(date);\n },\n\n compare(a, b) {\n return collator.compare(a, b);\n },\n };\n}\n\n/**\n * The formatter set for the current request's locale and the app's configured\n * currency. Memoised on both, so the Intl objects are built once per locale\n * rather than on every render.\n */\nexport function useUsageFormatters(): UsageFormatters {\n const locale = useLocale();\n const currency = getTokenUsageCurrency();\n return useMemo(() => createUsageFormatters(locale, currency), [locale, currency]);\n}\n","/**\n * Chart palette for the administrative token-usage dashboard.\n *\n * Every value here is a documented step of the dataviz skill's reference ramps —\n * nothing was eyeballed, nothing was hand-mixed. The palette was run through the\n * skill's validator against THIS application's real chart surfaces in both modes.\n * The verbatim output is below.\n *\n * The surface is `--card`, NOT `--background`. Every chart that uses this palette\n * is painted inside a `<Card>`, so the card is the surface the marks actually sit\n * on — and in dark mode it is the *lighter* of the two (`oklch(0.205 0 0)` vs the\n * background's `oklch(0.145 0 0)`), which is the harder case for contrast. Taken\n * from `apps/web/src/app/globals.css` (narr8 declares two `:root`/`.dark` pairs;\n * the LATER pair wins, and it is the one read here) — light `oklch(1 0 0)`, dark\n * `oklch(0.205 0 0)`. a360ai declares the same two values.\n *\n * oklch→hex conversion: the algebraic inverse of the Björn Ottosson OKLab\n * matrices — the same matrices the validator applies in the forward direction —\n * giving light `#ffffff` and dark `#171717`. Cross-checked by feeding it the\n * previously recorded background value, which it reproduces exactly:\n * `oklch(0.145 0 0)` → `#0a0a0a`. ✓\n *\n * Light and dark are two SELECTED sets of steps, chosen for their own surface —\n * never one set with its lightness flipped at runtime. Consumers pick a set from\n * the resolved theme (`seriesColor(i, mode)` / `sequentialColor(i, mode)`).\n *\n * ---------------------------------------------------------------------------\n * VALIDATOR OUTPUT — dataviz `scripts/validate_palette.js`, run 2026-08-15\n * ---------------------------------------------------------------------------\n *\n * $ node scripts/validate_palette.js \"#2a78d6,#eb6834,#1baf7a,#eda100,#e87ba4,#008300,#4a3aa7\" --mode light --surface \"#ffffff\"\n *\n * Palette (light, surface #ffffff, categorical): 7 slots\n * [PASS] Lightness band all 7 inside L 0.43–0.77\n * [PASS] Chroma floor all 7 >= 0.1\n * [PASS] CVD separation worst adjacent #eda100↔#1baf7a ΔE 9.1 (protan) · tritan 5.8\n * [PASS] Normal-vision floor worst adjacent #e87ba4↔#eda100 ΔE 19.6 (normal)\n * [WARN] Contrast vs surface below 3:1 — relief required (visible labels or table view): [[\"#1baf7a\",2.82],[\"#eda100\",2.17],[\"#e87ba4\",2.69]]\n *\n * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n *\n * $ node scripts/validate_palette.js \"#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9\" --mode dark --surface \"#171717\"\n *\n * Palette (dark, surface #171717, categorical): 7 slots\n * [PASS] Lightness band all 7 inside L 0.48–0.67\n * [PASS] Chroma floor all 7 >= 0.1\n * [PASS] CVD separation worst adjacent #c98500↔#199e70 ΔE 8.4 (protan) · tritan 8.7\n * [PASS] Normal-vision floor worst adjacent #d55181↔#c98500 ΔE 19.3 (normal)\n * [PASS] Contrast vs surface all 7 >= 3:1\n *\n * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n *\n * $ node scripts/validate_palette.js \"#104281,#1c5cab,#2a78d6,#5598e7,#86b6ef\" --mode light --surface \"#ffffff\" --ordinal\n *\n * Palette (light, surface #ffffff, ordinal ramp): 5 slots\n * [PASS] Lightness monotone steps read light→dark\n * [PASS] Adjacent ΔL all gaps >= 0.06\n * [PASS] Light-end contrast #86b6ef at 2.11:1 vs surface\n * [PASS] Single hue hue spread 3°\n *\n * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)\n *\n * $ node scripts/validate_palette.js \"#9ec5f4,#6da7ec,#3987e5,#256abf,#184f95\" --mode dark --surface \"#171717\" --ordinal\n *\n * Palette (dark, surface #171717, ordinal ramp): 5 slots\n * [PASS] Lightness monotone steps read light→dark\n * [PASS] Adjacent ΔL all gaps >= 0.06\n * [PASS] Light-end contrast #184f95 at 2.21:1 vs surface\n * [PASS] Single hue hue spread 3°\n *\n * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)\n *\n * OTHER_COLOR is achromatic, so the categorical checks (which gate hue identity)\n * do not apply to it; it was gated on contrast alone with the validator's own\n * `contrast()` export: 3.59:1 on `#ffffff`, 4.99:1 on `#171717` — both clear 3:1.\n *\n * ---------------------------------------------------------------------------\n * WHAT THE RESULTS OBLIGE US TO DO\n * ---------------------------------------------------------------------------\n *\n * - NO COLOUR VALUE CHANGED in the move from `--background` to `--card`. Every\n * gate that passed on the background still passes on the card, so re-stepping\n * a hue would have been churn, not a fix. Only the recorded surface, the\n * recorded numbers and this prose changed.\n * - The dark set still clears 3:1 on the LIGHTER card surface: the worst slot is\n * green `#008300` at 3.63:1, and the ordinal ramp's surface-nearest step lands\n * at 2.21:1 (down from 2.44:1 on the background) — still above the 2:1 gate.\n * That headroom is thin, so a future card-surface lightening MUST re-run these\n * four commands rather than assume the set still holds.\n * - The light-mode contrast WARN is NOT dismissable. Three slots (aqua, yellow,\n * magenta) sit below 3:1 on white, so every surface that paints with this\n * palette MUST ship the relief channel: a visible legend, direct labels and a\n * tooltip carrying the value in text. The timeline chart and the breakdown\n * table both do. The light card is `#ffffff`, identical to the light\n * background, so this WARN is unchanged rather than newly incurred.\n * - CVD separation is measured on ADJACENT pairs, which is the correct pairlist\n * for stacked bars, grouped bars and lines — the only forms this palette paints.\n * A scatter / bubble / small-multiples chart would need `--pairs all`, which\n * caps the usable slot count at three; do not reuse this array there without\n * re-running the validator with that flag.\n */\n\n/** Which surface the colours are being painted on. */\nexport type ChartMode = \"light\" | \"dark\";\n\n/**\n * The chart surfaces the palette was validated against (globals.css `--card`).\n *\n * `--card`, never `--background`: the charts are painted inside a `<Card>`.\n */\nexport const CHART_SURFACE: Readonly<Record<ChartMode, string>> = {\n light: \"#ffffff\",\n dark: \"#171717\",\n};\n\n/**\n * Categorical slots, in FIXED order. Slot n is series n — the order is the\n * CVD-safety mechanism, not decoration, so it is never re-ordered and never\n * cycled. The two-series default (customer / platform) takes slots 0 and 1.\n *\n * Hues, in order: blue, orange, aqua, yellow, magenta, green, violet.\n */\nconst CATEGORICAL_LIGHT: readonly string[] = [\n \"#2a78d6\",\n \"#eb6834\",\n \"#1baf7a\",\n \"#eda100\",\n \"#e87ba4\",\n \"#008300\",\n \"#4a3aa7\",\n];\n\n/** The same seven hues, re-stepped for the dark surface. Not a lightness flip. */\nconst CATEGORICAL_DARK: readonly string[] = [\n \"#3987e5\",\n \"#d95926\",\n \"#199e70\",\n \"#c98500\",\n \"#d55181\",\n \"#008300\",\n \"#9085e9\",\n];\n\n/**\n * The default (light-surface) categorical set.\n *\n * Prefer `seriesColor(index, mode)` — this array exists for consumers that only\n * need the documented default order.\n */\nexport const CATEGORICAL: readonly string[] = CATEGORICAL_LIGHT;\n\n/**\n * How many series may carry their own identity hue. An 8th series is NEVER a\n * generated hue: it folds into `other`.\n */\nexport const CATEGORICAL_CEILING = CATEGORICAL_LIGHT.length;\n\n/**\n * The \"other\" rollup colour: deliberately achromatic so it reads as \"not an\n * identity\" beside the seven hues. Same step in both modes — it clears 3:1 on\n * both card surfaces (3.59:1 on `#ffffff`, 4.99:1 on `#171717`).\n */\nexport const OTHER_COLOR = \"#898781\";\n\n/**\n * Sequential (single-hue, blue) ramp for MAGNITUDE, indexed by rank position:\n * index 0 is rank 1 — the largest value — so the strongest step leads.\n *\n * On the light surface \"strongest\" is the darkest step; on the dark surface it\n * is the lightest. The anchor flips with the surface, which is why there are two\n * selected sets rather than one array reversed at runtime.\n *\n * Five steps, not eight: with the adjacent-ΔL ≥ 0.06 gate and the ≥ 2:1\n * surface-contrast gate on the step nearest the surface, the blue ramp fits\n * exactly five distinguishable steps. Consumers clamp past the end.\n */\nconst SEQUENTIAL_RAMP_LIGHT: readonly string[] = [\"#104281\", \"#1c5cab\", \"#2a78d6\", \"#5598e7\", \"#86b6ef\"];\n\nconst SEQUENTIAL_RAMP_DARK: readonly string[] = [\"#9ec5f4\", \"#6da7ec\", \"#3987e5\", \"#256abf\", \"#184f95\"];\n\n/**\n * The default (light-surface) sequential ramp.\n *\n * Prefer `sequentialColor(index, mode)` — this array exists for consumers that\n * only need the documented default.\n */\nexport const SEQUENTIAL_RAMP: readonly string[] = SEQUENTIAL_RAMP_LIGHT;\n\nconst categoricalSet = (mode: ChartMode): readonly string[] => (mode === \"dark\" ? CATEGORICAL_DARK : CATEGORICAL_LIGHT);\n\nconst sequentialSet = (mode: ChartMode): readonly string[] =>\n mode === \"dark\" ? SEQUENTIAL_RAMP_DARK : SEQUENTIAL_RAMP_LIGHT;\n\n/**\n * The identity colour for series `index`.\n *\n * Never generates a hue and never cycles: anything at or past the ceiling gets\n * `OTHER_COLOR`, because a series past the ceiling should already have been\n * folded into the \"other\" bucket by the caller.\n */\nexport function seriesColor(index: number, mode: ChartMode = \"light\"): string {\n if (index < 0 || index >= CATEGORICAL_CEILING) return OTHER_COLOR;\n return categoricalSet(mode)[index];\n}\n\n/**\n * Both mode steps for series `index`, shaped for a shadcn `ChartConfig` entry's\n * `theme` field.\n */\nexport function seriesTheme(index: number): { light: string; dark: string } {\n return { light: seriesColor(index, \"light\"), dark: seriesColor(index, \"dark\") };\n}\n\n/**\n * The magnitude colour for rank position `index`, clamped to the last step so a\n * long list keeps its weakest step rather than falling off the ramp.\n */\nexport function sequentialColor(index: number, mode: ChartMode = \"light\"): string {\n const ramp = sequentialSet(mode);\n const clamped = Math.min(Math.max(index, 0), ramp.length - 1);\n return ramp[clamped];\n}\n","import type { Metric } from \"../data/tokenusage-admin.types\";\n\n/**\n * Locale-free arithmetic for the token-usage surfaces.\n *\n * Everything that turns a number into a STRING lives in `./formatters` instead,\n * because it depends on the request's locale and the app's configured currency.\n * This file stays pure arithmetic so it needs neither.\n */\n\n/**\n * The metric field set every admin token-usage resource carries. Declared\n * structurally so it accepts the summary, timeline and breakdown interfaces\n * alike — all three expose exactly these getters.\n */\nexport type TokenUsageMetrics = {\n cost: number;\n credits: number;\n tokensIn: number;\n tokensOut: number;\n cached: number;\n calls: number;\n};\n\n/** Reads the single number a row contributes for the currently selected metric. */\nexport function metricValue(row: TokenUsageMetrics, metric: Metric): number {\n if (metric === \"cost\") return row.cost;\n if (metric === \"credits\") return row.credits;\n return row.tokensIn + row.tokensOut;\n}\n\n/**\n * Share of cached input tokens. Returns 0 rather than NaN when the window holds\n * no input tokens at all, so the tile renders \"0,0 %\" instead of \"NaN %\".\n */\nexport function cacheHitPercentage(cached: number, tokensIn: number): number {\n if (!tokensIn) return 0;\n return (cached / tokensIn) * 100;\n}\n\n/**\n * Percentage change of `current` against `previous`, rounded to a whole number.\n *\n * Returns `undefined` when the previous window is zero — there is no meaningful\n * percentage change from nothing, and rendering Infinity would be worse than\n * rendering nothing. Callers show an em dash in that slot.\n */\nexport function percentageDelta(current: number, previous: number): number | undefined {\n if (!previous) return undefined;\n return Math.round(((current - previous) / previous) * 100);\n}\n"],"mappings":";;;;;;;;AAEA,SAAS,iBAAiB;AAC1B,SAAS,eAAe;AAsCjB,SAAS,sBAAsB,QAAgB,UAAmC;AACvF,QAAM,SACJ,IAAI,KAAK,aAAa,QAAQ,EAAE,OAAO,YAAY,UAAU,iBAAiB,eAAe,CAAC,EAC3F,cAAc,CAAC,EACf,KAAK,CAAC,SAAS,KAAK,SAAS,UAAU,GAAG,SAAS;AAExD,QAAM,UAAU,wBAAC,OAAe,aAC9B,MAAM,eAAe,QAAQ,EAAE,uBAAuB,UAAU,uBAAuB,SAAS,CAAC,GADnF;AAGhB,QAAM,YAAY,IAAI,KAAK,eAAe,QAAQ,EAAE,KAAK,WAAW,OAAO,SAAS,UAAU,MAAM,CAAC;AACrG,QAAM,cAAc,IAAI,KAAK,eAAe,QAAQ,EAAE,OAAO,SAAS,MAAM,WAAW,UAAU,MAAM,CAAC;AAKxG,QAAM,gBAAgB,IAAI,KAAK,aAAa,QAAQ,EAAE,UAAU,WAAW,0BAA0B,EAAE,CAAC;AACxG,QAAM,WAAW,IAAI,KAAK,SAAS,MAAM;AAEzC,SAAO;AAAA,IACL;AAAA,IAEA,YAAY,OAAO,QAAQ;AACzB,UAAI,WAAW,OAAQ,QAAO,GAAG,MAAM,IAAI,QAAQ,OAAO,CAAC,CAAC;AAI5D,UAAI,WAAW,UAAW,QAAO,QAAQ,OAAO,CAAC;AACjD,aAAO,QAAQ,OAAO,CAAC;AAAA,IACzB;AAAA,IAEA,SAAS,OAAO,UAAU;AACxB,aAAO,GAAG,MAAM,IAAI,QAAQ,OAAO,QAAQ,CAAC;AAAA,IAC9C;AAAA,IAEA,QAAQ,OAAO,WAAW,GAAG;AAC3B,aAAO,GAAG,QAAQ,OAAO,QAAQ,CAAC;AAAA,IACpC;AAAA,IAEA,QAAQ,OAAO;AACb,aAAO,cAAc,OAAO,KAAK;AAAA,IACnC;AAAA,IAEA,WAAW,KAAK,aAAa;AAC3B,YAAM,OAAO,oBAAI,KAAK,GAAG,GAAG,gBAAgB;AAC5C,aAAO,gBAAgB,UAAU,YAAY,OAAO,IAAI,IAAI,UAAU,OAAO,IAAI;AAAA,IACnF;AAAA,IAEA,QAAQ,GAAG,GAAG;AACZ,aAAO,SAAS,QAAQ,GAAG,CAAC;AAAA,IAC9B;AAAA,EACF;AACF;AAnDgB;AA0DT,SAAS,qBAAsC;AACpD,QAAM,SAAS,UAAU;AACzB,QAAM,WAAW,sBAAsB;AACvC,SAAO,QAAQ,MAAM,sBAAsB,QAAQ,QAAQ,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAClF;AAJgB;;;ACyBhB,IAAM,oBAAuC;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,mBAAsC;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAcO,IAAM,sBAAsB,kBAAkB;AAO9C,IAAM,cAAc;AAc3B,IAAM,wBAA2C,CAAC,WAAW,WAAW,WAAW,WAAW,SAAS;AAUhG,IAAM,kBAAqC;AAElD,IAAM,iBAAiB,wBAAC,SAAwC,SAAS,SAAS,mBAAmB,mBAA9E;AAYhB,SAAS,YAAY,OAAe,OAAkB,SAAiB;AAC5E,MAAI,QAAQ,KAAK,SAAS,oBAAqB,QAAO;AACtD,SAAO,eAAe,IAAI,EAAE,KAAK;AACnC;AAHgB;AAST,SAAS,YAAY,OAAgD;AAC1E,SAAO,EAAE,OAAO,YAAY,OAAO,OAAO,GAAG,MAAM,YAAY,OAAO,MAAM,EAAE;AAChF;AAFgB;;;AC1LT,SAAS,YAAY,KAAwB,QAAwB;AAC1E,MAAI,WAAW,OAAQ,QAAO,IAAI;AAClC,MAAI,WAAW,UAAW,QAAO,IAAI;AACrC,SAAO,IAAI,WAAW,IAAI;AAC5B;AAJgB;AAUT,SAAS,mBAAmB,QAAgB,UAA0B;AAC3E,MAAI,CAAC,SAAU,QAAO;AACtB,SAAQ,SAAS,WAAY;AAC/B;AAHgB;AAYT,SAAS,gBAAgB,SAAiB,UAAsC;AACrF,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,KAAK,OAAQ,UAAU,YAAY,WAAY,GAAG;AAC3D;AAHgB;","names":[]}
@@ -48,24 +48,6 @@ function useUsageFormatters() {
48
48
  }
49
49
  _chunk7QVYU63Ejs.__name.call(void 0, useUsageFormatters, "useUsageFormatters");
50
50
 
51
- // src/features/tokenusage/lib/metrics.ts
52
- function metricValue(row, metric) {
53
- if (metric === "cost") return row.cost;
54
- if (metric === "credits") return row.credits;
55
- return row.tokensIn + row.tokensOut;
56
- }
57
- _chunk7QVYU63Ejs.__name.call(void 0, metricValue, "metricValue");
58
- function cacheHitPercentage(cached, tokensIn) {
59
- if (!tokensIn) return 0;
60
- return cached / tokensIn * 100;
61
- }
62
- _chunk7QVYU63Ejs.__name.call(void 0, cacheHitPercentage, "cacheHitPercentage");
63
- function percentageDelta(current, previous) {
64
- if (!previous) return void 0;
65
- return Math.round((current - previous) / previous * 100);
66
- }
67
- _chunk7QVYU63Ejs.__name.call(void 0, percentageDelta, "percentageDelta");
68
-
69
51
  // src/features/tokenusage/lib/palette.ts
70
52
  var CATEGORICAL_LIGHT = [
71
53
  "#2a78d6",
@@ -95,6 +77,29 @@ function seriesColor(index, mode = "light") {
95
77
  return categoricalSet(mode)[index];
96
78
  }
97
79
  _chunk7QVYU63Ejs.__name.call(void 0, seriesColor, "seriesColor");
80
+ function seriesTheme(index) {
81
+ return { light: seriesColor(index, "light"), dark: seriesColor(index, "dark") };
82
+ }
83
+ _chunk7QVYU63Ejs.__name.call(void 0, seriesTheme, "seriesTheme");
84
+
85
+ // src/features/tokenusage/lib/metrics.ts
86
+ function metricValue(row, metric) {
87
+ if (metric === "cost") return row.cost;
88
+ if (metric === "credits") return row.credits;
89
+ return row.tokensIn + row.tokensOut;
90
+ }
91
+ _chunk7QVYU63Ejs.__name.call(void 0, metricValue, "metricValue");
92
+ function cacheHitPercentage(cached, tokensIn) {
93
+ if (!tokensIn) return 0;
94
+ return cached / tokensIn * 100;
95
+ }
96
+ _chunk7QVYU63Ejs.__name.call(void 0, cacheHitPercentage, "cacheHitPercentage");
97
+ function percentageDelta(current, previous) {
98
+ if (!previous) return void 0;
99
+ return Math.round((current - previous) / previous * 100);
100
+ }
101
+ _chunk7QVYU63Ejs.__name.call(void 0, percentageDelta, "percentageDelta");
102
+
98
103
 
99
104
 
100
105
 
@@ -106,5 +111,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, seriesColor, "seriesColor");
106
111
 
107
112
 
108
113
 
109
- exports.createUsageFormatters = createUsageFormatters; exports.useUsageFormatters = useUsageFormatters; exports.metricValue = metricValue; exports.cacheHitPercentage = cacheHitPercentage; exports.percentageDelta = percentageDelta; exports.CATEGORICAL_CEILING = CATEGORICAL_CEILING; exports.OTHER_COLOR = OTHER_COLOR; exports.SEQUENTIAL_RAMP = SEQUENTIAL_RAMP; exports.seriesColor = seriesColor;
110
- //# sourceMappingURL=chunk-V4I3XKOM.js.map
114
+ exports.createUsageFormatters = createUsageFormatters; exports.useUsageFormatters = useUsageFormatters; exports.metricValue = metricValue; exports.cacheHitPercentage = cacheHitPercentage; exports.percentageDelta = percentageDelta; exports.CATEGORICAL_CEILING = CATEGORICAL_CEILING; exports.OTHER_COLOR = OTHER_COLOR; exports.SEQUENTIAL_RAMP = SEQUENTIAL_RAMP; exports.seriesColor = seriesColor; exports.seriesTheme = seriesTheme;
115
+ //# sourceMappingURL=chunk-ELQHE2DK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-ELQHE2DK.js","../src/features/tokenusage/lib/formatters.ts","../src/features/tokenusage/lib/palette.ts","../src/features/tokenusage/lib/metrics.ts"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B;AACE;AACF,sDAA4B;AAC5B;AACA;ACLA,qCAA0B;AAC1B,8BAAwB;AAsCjB,SAAS,qBAAA,CAAsB,MAAA,EAAgB,QAAA,EAAmC;AACvF,EAAA,MAAM,OAAA,mCACJ,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ,EAAE,KAAA,EAAO,UAAA,EAAY,QAAA,EAAU,eAAA,EAAiB,eAAe,CAAC,CAAA,mBAC3F,aAAA,mBAAc,CAAC,CAAA,qBACf,IAAA,mBAAK,CAAC,IAAA,EAAA,GAAS,IAAA,CAAK,KAAA,IAAS,UAAU,CAAA,6BAAG,OAAA,UAAS,UAAA;AAExD,EAAA,MAAM,QAAA,kBAAU,qCAAA,CAAC,KAAA,EAAe,QAAA,EAAA,GAC9B,KAAA,CAAM,cAAA,CAAe,MAAA,EAAQ,EAAE,qBAAA,EAAuB,QAAA,EAAU,qBAAA,EAAuB,SAAS,CAAC,CAAA,EADnF,SAAA,CAAA;AAGhB,EAAA,MAAM,UAAA,EAAY,IAAI,IAAA,CAAK,cAAA,CAAe,MAAA,EAAQ,EAAE,GAAA,EAAK,SAAA,EAAW,KAAA,EAAO,OAAA,EAAS,QAAA,EAAU,MAAM,CAAC,CAAA;AACrG,EAAA,MAAM,YAAA,EAAc,IAAI,IAAA,CAAK,cAAA,CAAe,MAAA,EAAQ,EAAE,KAAA,EAAO,OAAA,EAAS,IAAA,EAAM,SAAA,EAAW,QAAA,EAAU,MAAM,CAAC,CAAA;AAKxG,EAAA,MAAM,cAAA,EAAgB,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ,EAAE,QAAA,EAAU,SAAA,EAAW,wBAAA,EAA0B,EAAE,CAAC,CAAA;AACxG,EAAA,MAAM,SAAA,EAAW,IAAI,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA;AAEzC,EAAA,OAAO;AAAA,IACL,OAAA;AAAA,IAEA,WAAA,CAAY,KAAA,EAAO,MAAA,EAAQ;AACzB,MAAA,GAAA,CAAI,OAAA,IAAW,MAAA,EAAQ,OAAO,CAAA,EAAA;AAIJ,MAAA;AACH,MAAA;AACzB,IAAA;AAE0B,IAAA;AACI,MAAA;AAC9B,IAAA;AAE6B,IAAA;AACF,MAAA;AAC3B,IAAA;AAEe,IAAA;AACe,MAAA;AAC9B,IAAA;AAE6B,IAAA;AACd,MAAA;AACU,MAAA;AACzB,IAAA;AAEc,IAAA;AACgB,MAAA;AAC9B,IAAA;AACF,EAAA;AACF;AAnDgB;AA0DsC;AAC3B,EAAA;AACR,EAAA;AACI,EAAA;AACvB;AAJgB;ADlDqB;AACA;AE0EQ;AAC3C,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAG4C;AAC1C,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAcmC;AAOR;AAcuB;AAUA;AAE3B;AAYsC;AACjC,EAAA;AACO,EAAA;AACnC;AAHgB;AAS4D;AACvC,EAAA;AACrC;AAFgB;AFhIqB;AACA;AG3DuC;AACxC,EAAA;AACD,EAAA;AACP,EAAA;AAC5B;AAJgB;AAUmC;AAC3B,EAAA;AACO,EAAA;AAC/B;AAHgB;AAYiC;AACzB,EAAA;AACQ,EAAA;AAChC;AAHgB;AHsDqB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-ELQHE2DK.js","sourcesContent":[null,"\"use client\";\n\nimport { useLocale } from \"next-intl\";\nimport { useMemo } from \"react\";\nimport type { Metric } from \"../data/tokenusage-admin.types\";\nimport { getTokenUsageCurrency } from \"./config\";\n\nexport type UsageFormatters = {\n /** Locale-aware fixed-decimal number, e.g. 9.46 → \"9,46\" in it-IT. */\n decimal(value: number, decimals: number): string;\n /** Currency for cost, 2 decimals for credits, whole numbers for tokens. */\n metricValue(value: number, metric: Metric): string;\n /** Currency with an explicit decimal count — the per-call tile needs 4. */\n currency(value: number, decimals: number): string;\n /** Percentage with one decimal by default, e.g. 60 → \"60,0 %\" in it-IT. */\n percent(value: number, decimals?: number): string;\n /** Compact notation for axis ticks, e.g. 1500000 → \"1,5 Mln\" in it-IT. */\n compact(value: number): string;\n /** A \"YYYY-MM-DD\" bucket key rendered as an axis or tooltip label, in UTC. */\n bucketDate(iso: string, granularity: \"day\" | \"week\" | \"month\"): string;\n /** Locale collation for client-side table sorting. */\n compare(a: string, b: string): number;\n};\n\n/**\n * Builds every formatter the token-usage surfaces need, for one locale and one\n * currency.\n *\n * A pure factory on purpose: it takes no React context, so the arithmetic and\n * the formatting are unit-testable without rendering, and a server component or\n * a test can construct a set for an arbitrary locale.\n *\n * The currency symbol is placed with a space rather than through\n * `style: \"currency\"`, which is what the previous hard-coded implementation\n * rendered (\"€ 9,46\"). Keeping that shape means a consuming app on EUR sees\n * byte-identical output after this refactor.\n *\n * `bucketDate` formats in UTC, always. The wire value is a `type: \"date\"` — a\n * calendar day — parsed to UTC midnight; reading it back with local getters\n * would shift the label a day early west of UTC.\n */\nexport function createUsageFormatters(locale: string, currency: string): UsageFormatters {\n const symbol =\n new Intl.NumberFormat(locale, { style: \"currency\", currency, currencyDisplay: \"narrowSymbol\" })\n .formatToParts(0)\n .find((part) => part.type === \"currency\")?.value ?? currency;\n\n const decimal = (value: number, decimals: number): string =>\n value.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });\n\n const dayFormat = new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"short\", timeZone: \"UTC\" });\n const monthFormat = new Intl.DateTimeFormat(locale, { month: \"short\", year: \"numeric\", timeZone: \"UTC\" });\n // Axis ticks step by 1, 2, 2.5 or 5 times a power of ten, so a tick never needs\n // more than three significant digits. A fixed one-decimal cap rounded a 0.05\n // step into repeated labels (0.1, 0.1, 0.2, 0.2); significant digits keep every\n // tick distinct at any scale (0.05, 0.15, 1.25K).\n const compactFormat = new Intl.NumberFormat(locale, { notation: \"compact\", maximumSignificantDigits: 3 });\n const collator = new Intl.Collator(locale);\n\n return {\n decimal,\n\n metricValue(value, metric) {\n if (metric === \"cost\") return `${symbol} ${decimal(value, 2)}`;\n // Credits are stored to 4 decimals (round4(cost / creditCost)), but that\n // precision is noise to a reader comparing rows: one decimal is enough to\n // separate two values and keeps the columns narrow enough to scan.\n if (metric === \"credits\") return decimal(value, 1);\n return decimal(value, 0);\n },\n\n currency(value, decimals) {\n return `${symbol} ${decimal(value, decimals)}`;\n },\n\n percent(value, decimals = 1) {\n return `${decimal(value, decimals)} %`;\n },\n\n compact(value) {\n return compactFormat.format(value);\n },\n\n bucketDate(iso, granularity) {\n const date = new Date(`${iso}T00:00:00.000Z`);\n return granularity === \"month\" ? monthFormat.format(date) : dayFormat.format(date);\n },\n\n compare(a, b) {\n return collator.compare(a, b);\n },\n };\n}\n\n/**\n * The formatter set for the current request's locale and the app's configured\n * currency. Memoised on both, so the Intl objects are built once per locale\n * rather than on every render.\n */\nexport function useUsageFormatters(): UsageFormatters {\n const locale = useLocale();\n const currency = getTokenUsageCurrency();\n return useMemo(() => createUsageFormatters(locale, currency), [locale, currency]);\n}\n","/**\n * Chart palette for the administrative token-usage dashboard.\n *\n * Every value here is a documented step of the dataviz skill's reference ramps —\n * nothing was eyeballed, nothing was hand-mixed. The palette was run through the\n * skill's validator against THIS application's real chart surfaces in both modes.\n * The verbatim output is below.\n *\n * The surface is `--card`, NOT `--background`. Every chart that uses this palette\n * is painted inside a `<Card>`, so the card is the surface the marks actually sit\n * on — and in dark mode it is the *lighter* of the two (`oklch(0.205 0 0)` vs the\n * background's `oklch(0.145 0 0)`), which is the harder case for contrast. Taken\n * from `apps/web/src/app/globals.css` (narr8 declares two `:root`/`.dark` pairs;\n * the LATER pair wins, and it is the one read here) — light `oklch(1 0 0)`, dark\n * `oklch(0.205 0 0)`. a360ai declares the same two values.\n *\n * oklch→hex conversion: the algebraic inverse of the Björn Ottosson OKLab\n * matrices — the same matrices the validator applies in the forward direction —\n * giving light `#ffffff` and dark `#171717`. Cross-checked by feeding it the\n * previously recorded background value, which it reproduces exactly:\n * `oklch(0.145 0 0)` → `#0a0a0a`. ✓\n *\n * Light and dark are two SELECTED sets of steps, chosen for their own surface —\n * never one set with its lightness flipped at runtime. Consumers pick a set from\n * the resolved theme (`seriesColor(i, mode)` / `sequentialColor(i, mode)`).\n *\n * ---------------------------------------------------------------------------\n * VALIDATOR OUTPUT — dataviz `scripts/validate_palette.js`, run 2026-08-15\n * ---------------------------------------------------------------------------\n *\n * $ node scripts/validate_palette.js \"#2a78d6,#eb6834,#1baf7a,#eda100,#e87ba4,#008300,#4a3aa7\" --mode light --surface \"#ffffff\"\n *\n * Palette (light, surface #ffffff, categorical): 7 slots\n * [PASS] Lightness band all 7 inside L 0.43–0.77\n * [PASS] Chroma floor all 7 >= 0.1\n * [PASS] CVD separation worst adjacent #eda100↔#1baf7a ΔE 9.1 (protan) · tritan 5.8\n * [PASS] Normal-vision floor worst adjacent #e87ba4↔#eda100 ΔE 19.6 (normal)\n * [WARN] Contrast vs surface below 3:1 — relief required (visible labels or table view): [[\"#1baf7a\",2.82],[\"#eda100\",2.17],[\"#e87ba4\",2.69]]\n *\n * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n *\n * $ node scripts/validate_palette.js \"#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9\" --mode dark --surface \"#171717\"\n *\n * Palette (dark, surface #171717, categorical): 7 slots\n * [PASS] Lightness band all 7 inside L 0.48–0.67\n * [PASS] Chroma floor all 7 >= 0.1\n * [PASS] CVD separation worst adjacent #c98500↔#199e70 ΔE 8.4 (protan) · tritan 8.7\n * [PASS] Normal-vision floor worst adjacent #d55181↔#c98500 ΔE 19.3 (normal)\n * [PASS] Contrast vs surface all 7 >= 3:1\n *\n * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n *\n * $ node scripts/validate_palette.js \"#104281,#1c5cab,#2a78d6,#5598e7,#86b6ef\" --mode light --surface \"#ffffff\" --ordinal\n *\n * Palette (light, surface #ffffff, ordinal ramp): 5 slots\n * [PASS] Lightness monotone steps read light→dark\n * [PASS] Adjacent ΔL all gaps >= 0.06\n * [PASS] Light-end contrast #86b6ef at 2.11:1 vs surface\n * [PASS] Single hue hue spread 3°\n *\n * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)\n *\n * $ node scripts/validate_palette.js \"#9ec5f4,#6da7ec,#3987e5,#256abf,#184f95\" --mode dark --surface \"#171717\" --ordinal\n *\n * Palette (dark, surface #171717, ordinal ramp): 5 slots\n * [PASS] Lightness monotone steps read light→dark\n * [PASS] Adjacent ΔL all gaps >= 0.06\n * [PASS] Light-end contrast #184f95 at 2.21:1 vs surface\n * [PASS] Single hue hue spread 3°\n *\n * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)\n *\n * OTHER_COLOR is achromatic, so the categorical checks (which gate hue identity)\n * do not apply to it; it was gated on contrast alone with the validator's own\n * `contrast()` export: 3.59:1 on `#ffffff`, 4.99:1 on `#171717` — both clear 3:1.\n *\n * ---------------------------------------------------------------------------\n * WHAT THE RESULTS OBLIGE US TO DO\n * ---------------------------------------------------------------------------\n *\n * - NO COLOUR VALUE CHANGED in the move from `--background` to `--card`. Every\n * gate that passed on the background still passes on the card, so re-stepping\n * a hue would have been churn, not a fix. Only the recorded surface, the\n * recorded numbers and this prose changed.\n * - The dark set still clears 3:1 on the LIGHTER card surface: the worst slot is\n * green `#008300` at 3.63:1, and the ordinal ramp's surface-nearest step lands\n * at 2.21:1 (down from 2.44:1 on the background) — still above the 2:1 gate.\n * That headroom is thin, so a future card-surface lightening MUST re-run these\n * four commands rather than assume the set still holds.\n * - The light-mode contrast WARN is NOT dismissable. Three slots (aqua, yellow,\n * magenta) sit below 3:1 on white, so every surface that paints with this\n * palette MUST ship the relief channel: a visible legend, direct labels and a\n * tooltip carrying the value in text. The timeline chart and the breakdown\n * table both do. The light card is `#ffffff`, identical to the light\n * background, so this WARN is unchanged rather than newly incurred.\n * - CVD separation is measured on ADJACENT pairs, which is the correct pairlist\n * for stacked bars, grouped bars and lines — the only forms this palette paints.\n * A scatter / bubble / small-multiples chart would need `--pairs all`, which\n * caps the usable slot count at three; do not reuse this array there without\n * re-running the validator with that flag.\n */\n\n/** Which surface the colours are being painted on. */\nexport type ChartMode = \"light\" | \"dark\";\n\n/**\n * The chart surfaces the palette was validated against (globals.css `--card`).\n *\n * `--card`, never `--background`: the charts are painted inside a `<Card>`.\n */\nexport const CHART_SURFACE: Readonly<Record<ChartMode, string>> = {\n light: \"#ffffff\",\n dark: \"#171717\",\n};\n\n/**\n * Categorical slots, in FIXED order. Slot n is series n — the order is the\n * CVD-safety mechanism, not decoration, so it is never re-ordered and never\n * cycled. The two-series default (customer / platform) takes slots 0 and 1.\n *\n * Hues, in order: blue, orange, aqua, yellow, magenta, green, violet.\n */\nconst CATEGORICAL_LIGHT: readonly string[] = [\n \"#2a78d6\",\n \"#eb6834\",\n \"#1baf7a\",\n \"#eda100\",\n \"#e87ba4\",\n \"#008300\",\n \"#4a3aa7\",\n];\n\n/** The same seven hues, re-stepped for the dark surface. Not a lightness flip. */\nconst CATEGORICAL_DARK: readonly string[] = [\n \"#3987e5\",\n \"#d95926\",\n \"#199e70\",\n \"#c98500\",\n \"#d55181\",\n \"#008300\",\n \"#9085e9\",\n];\n\n/**\n * The default (light-surface) categorical set.\n *\n * Prefer `seriesColor(index, mode)` — this array exists for consumers that only\n * need the documented default order.\n */\nexport const CATEGORICAL: readonly string[] = CATEGORICAL_LIGHT;\n\n/**\n * How many series may carry their own identity hue. An 8th series is NEVER a\n * generated hue: it folds into `other`.\n */\nexport const CATEGORICAL_CEILING = CATEGORICAL_LIGHT.length;\n\n/**\n * The \"other\" rollup colour: deliberately achromatic so it reads as \"not an\n * identity\" beside the seven hues. Same step in both modes — it clears 3:1 on\n * both card surfaces (3.59:1 on `#ffffff`, 4.99:1 on `#171717`).\n */\nexport const OTHER_COLOR = \"#898781\";\n\n/**\n * Sequential (single-hue, blue) ramp for MAGNITUDE, indexed by rank position:\n * index 0 is rank 1 — the largest value — so the strongest step leads.\n *\n * On the light surface \"strongest\" is the darkest step; on the dark surface it\n * is the lightest. The anchor flips with the surface, which is why there are two\n * selected sets rather than one array reversed at runtime.\n *\n * Five steps, not eight: with the adjacent-ΔL ≥ 0.06 gate and the ≥ 2:1\n * surface-contrast gate on the step nearest the surface, the blue ramp fits\n * exactly five distinguishable steps. Consumers clamp past the end.\n */\nconst SEQUENTIAL_RAMP_LIGHT: readonly string[] = [\"#104281\", \"#1c5cab\", \"#2a78d6\", \"#5598e7\", \"#86b6ef\"];\n\nconst SEQUENTIAL_RAMP_DARK: readonly string[] = [\"#9ec5f4\", \"#6da7ec\", \"#3987e5\", \"#256abf\", \"#184f95\"];\n\n/**\n * The default (light-surface) sequential ramp.\n *\n * Prefer `sequentialColor(index, mode)` — this array exists for consumers that\n * only need the documented default.\n */\nexport const SEQUENTIAL_RAMP: readonly string[] = SEQUENTIAL_RAMP_LIGHT;\n\nconst categoricalSet = (mode: ChartMode): readonly string[] => (mode === \"dark\" ? CATEGORICAL_DARK : CATEGORICAL_LIGHT);\n\nconst sequentialSet = (mode: ChartMode): readonly string[] =>\n mode === \"dark\" ? SEQUENTIAL_RAMP_DARK : SEQUENTIAL_RAMP_LIGHT;\n\n/**\n * The identity colour for series `index`.\n *\n * Never generates a hue and never cycles: anything at or past the ceiling gets\n * `OTHER_COLOR`, because a series past the ceiling should already have been\n * folded into the \"other\" bucket by the caller.\n */\nexport function seriesColor(index: number, mode: ChartMode = \"light\"): string {\n if (index < 0 || index >= CATEGORICAL_CEILING) return OTHER_COLOR;\n return categoricalSet(mode)[index];\n}\n\n/**\n * Both mode steps for series `index`, shaped for a shadcn `ChartConfig` entry's\n * `theme` field.\n */\nexport function seriesTheme(index: number): { light: string; dark: string } {\n return { light: seriesColor(index, \"light\"), dark: seriesColor(index, \"dark\") };\n}\n\n/**\n * The magnitude colour for rank position `index`, clamped to the last step so a\n * long list keeps its weakest step rather than falling off the ramp.\n */\nexport function sequentialColor(index: number, mode: ChartMode = \"light\"): string {\n const ramp = sequentialSet(mode);\n const clamped = Math.min(Math.max(index, 0), ramp.length - 1);\n return ramp[clamped];\n}\n","import type { Metric } from \"../data/tokenusage-admin.types\";\n\n/**\n * Locale-free arithmetic for the token-usage surfaces.\n *\n * Everything that turns a number into a STRING lives in `./formatters` instead,\n * because it depends on the request's locale and the app's configured currency.\n * This file stays pure arithmetic so it needs neither.\n */\n\n/**\n * The metric field set every admin token-usage resource carries. Declared\n * structurally so it accepts the summary, timeline and breakdown interfaces\n * alike — all three expose exactly these getters.\n */\nexport type TokenUsageMetrics = {\n cost: number;\n credits: number;\n tokensIn: number;\n tokensOut: number;\n cached: number;\n calls: number;\n};\n\n/** Reads the single number a row contributes for the currently selected metric. */\nexport function metricValue(row: TokenUsageMetrics, metric: Metric): number {\n if (metric === \"cost\") return row.cost;\n if (metric === \"credits\") return row.credits;\n return row.tokensIn + row.tokensOut;\n}\n\n/**\n * Share of cached input tokens. Returns 0 rather than NaN when the window holds\n * no input tokens at all, so the tile renders \"0,0 %\" instead of \"NaN %\".\n */\nexport function cacheHitPercentage(cached: number, tokensIn: number): number {\n if (!tokensIn) return 0;\n return (cached / tokensIn) * 100;\n}\n\n/**\n * Percentage change of `current` against `previous`, rounded to a whole number.\n *\n * Returns `undefined` when the previous window is zero — there is no meaningful\n * percentage change from nothing, and rendering Infinity would be worse than\n * rendering nothing. Callers show an em dash in that slot.\n */\nexport function percentageDelta(current: number, previous: number): number | undefined {\n if (!previous) return undefined;\n return Math.round(((current - previous) / previous) * 100);\n}\n"]}
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- var _chunkV4I3XKOMjs = require('../../chunk-V4I3XKOM.js');
6
+ var _chunkELQHE2DKjs = require('../../chunk-ELQHE2DK.js');
7
7
 
8
8
 
9
9
 
@@ -526,7 +526,7 @@ var _lucidereact = require('lucide-react');
526
526
  var ZERO = { visitors: 0, sessions: 0, pageViews: 0, pagesPerSession: 0, consentShare: 0 };
527
527
  function AnalyticsAdminTiles({ summary }) {
528
528
  const t = _nextintl.useTranslations.call(void 0, );
529
- const { decimal } = _chunkV4I3XKOMjs.useUsageFormatters.call(void 0, );
529
+ const { decimal } = _chunkELQHE2DKjs.useUsageFormatters.call(void 0, );
530
530
  const rowFor = /* @__PURE__ */ _chunk7QVYU63Ejs.__name.call(void 0, (window2) => _nullishCoalesce(summary.find((r) => r.section === "total" && r.window === window2), () => ( ZERO)), "rowFor");
531
531
  const current = rowFor("current");
532
532
  const previous = rowFor("previous");
@@ -539,7 +539,7 @@ function AnalyticsAdminTiles({ summary }) {
539
539
  testId: "analytics-tile-visitors",
540
540
  label: t("analytics.admin.tiles.visitors"),
541
541
  value: decimal(current.visitors, 0),
542
- delta: _chunkV4I3XKOMjs.percentageDelta.call(void 0, current.visitors, previous.visitors),
542
+ delta: _chunkELQHE2DKjs.percentageDelta.call(void 0, current.visitors, previous.visitors),
543
543
  previousLabel,
544
544
  decimal
545
545
  }
@@ -550,7 +550,7 @@ function AnalyticsAdminTiles({ summary }) {
550
550
  testId: "analytics-tile-sessions",
551
551
  label: t("analytics.admin.tiles.sessions"),
552
552
  value: decimal(current.sessions, 0),
553
- delta: _chunkV4I3XKOMjs.percentageDelta.call(void 0, current.sessions, previous.sessions),
553
+ delta: _chunkELQHE2DKjs.percentageDelta.call(void 0, current.sessions, previous.sessions),
554
554
  previousLabel,
555
555
  decimal
556
556
  }
@@ -561,7 +561,7 @@ function AnalyticsAdminTiles({ summary }) {
561
561
  testId: "analytics-tile-page-views",
562
562
  label: t("analytics.admin.tiles.page_views"),
563
563
  value: decimal(current.pageViews, 0),
564
- delta: _chunkV4I3XKOMjs.percentageDelta.call(void 0, current.pageViews, previous.pageViews),
564
+ delta: _chunkELQHE2DKjs.percentageDelta.call(void 0, current.pageViews, previous.pageViews),
565
565
  previousLabel,
566
566
  decimal
567
567
  }
@@ -574,7 +574,7 @@ function AnalyticsAdminTiles({ summary }) {
574
574
  testId: "analytics-tile-pages-per-session",
575
575
  label: t("analytics.admin.tiles.pages_per_session"),
576
576
  value: decimal(current.pagesPerSession, 1),
577
- delta: _chunkV4I3XKOMjs.percentageDelta.call(void 0, current.pagesPerSession, previous.pagesPerSession),
577
+ delta: _chunkELQHE2DKjs.percentageDelta.call(void 0, current.pagesPerSession, previous.pagesPerSession),
578
578
  decimal
579
579
  }
580
580
  ),
@@ -676,7 +676,7 @@ function AnalyticsBreakdownTable({ breakdowns }) {
676
676
  _chunk7QVYU63Ejs.__name.call(void 0, AnalyticsBreakdownTable, "AnalyticsBreakdownTable");
677
677
  function BreakdownRows({ rows }) {
678
678
  const t = _nextintl.useTranslations.call(void 0, );
679
- const { decimal } = _chunkV4I3XKOMjs.useUsageFormatters.call(void 0, );
679
+ const { decimal } = _chunkELQHE2DKjs.useUsageFormatters.call(void 0, );
680
680
  const sortedRows = _react.useMemo.call(void 0, () => [...rows].sort((a, b) => b.visitors - a.visitors), [rows]);
681
681
  if (sortedRows.length === 0) {
682
682
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-muted-foreground text-sm", children: t("analytics.admin.no_data") });
@@ -712,7 +712,7 @@ function AnalyticsSessionsTable({ rows, sessionPageUrl, onLoadMore, hasMore }) {
712
712
  const t = _nextintl.useTranslations.call(void 0, );
713
713
  const locale = _nextintl.useLocale.call(void 0, );
714
714
  const generateUrl = _chunkY7JIQNM6js.usePageUrlGenerator.call(void 0, );
715
- const { decimal } = _chunkV4I3XKOMjs.useUsageFormatters.call(void 0, );
715
+ const { decimal } = _chunkELQHE2DKjs.useUsageFormatters.call(void 0, );
716
716
  const dateTime = _react.useMemo.call(void 0, () => new Intl.DateTimeFormat(locale, { dateStyle: "short", timeStyle: "short" }), [locale]);
717
717
  if (rows.length === 0) {
718
718
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-muted-foreground text-sm", children: t("analytics.admin.no_data") });
@@ -784,7 +784,7 @@ var inferGranularity = /* @__PURE__ */ _chunk7QVYU63Ejs.__name.call(void 0, (buc
784
784
  }, "inferGranularity");
785
785
  function AnalyticsTimelineChart({ rows, className }) {
786
786
  const t = _nextintl.useTranslations.call(void 0, );
787
- const { compact, bucketDate, decimal } = _chunkV4I3XKOMjs.useUsageFormatters.call(void 0, );
787
+ const { compact, bucketDate, decimal } = _chunkELQHE2DKjs.useUsageFormatters.call(void 0, );
788
788
  const { resolvedTheme } = _nextthemes.useTheme.call(void 0, );
789
789
  const mode = resolvedTheme === "dark" ? "dark" : "light";
790
790
  const chartData = _react.useMemo.call(void 0, () => {
@@ -860,7 +860,7 @@ function AnalyticsTimelineChart({ rows, className }) {
860
860
  {
861
861
  dataKey: series,
862
862
  maxBarSize: 28,
863
- fill: _chunkV4I3XKOMjs.seriesColor.call(void 0, index, mode),
863
+ fill: _chunkELQHE2DKjs.seriesColor.call(void 0, index, mode),
864
864
  stroke: "var(--background)",
865
865
  strokeWidth: 2,
866
866
  radius: [4, 4, 0, 0]
@@ -3,7 +3,7 @@ import {
3
3
  percentageDelta,
4
4
  seriesColor,
5
5
  useUsageFormatters
6
- } from "../../chunk-ZDUT5JPT.mjs";
6
+ } from "../../chunk-DYOSXIEL.mjs";
7
7
  import {
8
8
  Badge,
9
9
  Button,
@@ -782,4 +782,126 @@ declare function createUsageFormatters(locale: string, currency: string): UsageF
782
782
  */
783
783
  declare function useUsageFormatters(): UsageFormatters;
784
784
 
785
- export { type Dimension, type Granularity, type Metric, type ReportDimension, type ReportMetric, type Scope, type StackBy, TOKEN_USAGE_ADMIN_I18N_KEYS, TOKEN_USAGE_REPORT_I18N_KEYS, TokenUsageAdminBreakdown, type TokenUsageAdminBreakdownInput, type TokenUsageAdminBreakdownInterface, TokenUsageAdminContainer, type TokenUsageAdminContextType, TokenUsageAdminFilterBar, type TokenUsageAdminFilterState, type TokenUsageAdminFilters, TokenUsageAdminProvider, TokenUsageAdminService, TokenUsageAdminSummary, type TokenUsageAdminSummaryInput, type TokenUsageAdminSummaryInterface, TokenUsageAdminTiles, TokenUsageAdminTimeline, type TokenUsageAdminTimelineInput, type TokenUsageAdminTimelineInterface, type TokenUsageBalances, TokenUsageBreakdownTable, TokenUsageRankedBar, TokenUsageReportBreakdown, type TokenUsageReportBreakdownInput, type TokenUsageReportBreakdownInterface, TokenUsageReportContainer, type TokenUsageReportContextType, TokenUsageReportFilterBar, type TokenUsageReportFilterState, type TokenUsageReportFilters, TokenUsageReportProvider, TokenUsageReportService, TokenUsageReportSummary, type TokenUsageReportSummaryInput, type TokenUsageReportSummaryInterface, TokenUsageReportTiles, TokenUsageReportTimeline, type TokenUsageReportTimelineInput, type TokenUsageReportTimelineInterface, TokenUsageTimelineChart, type UsageFormatters, createUsageFormatters, useTokenUsageAdmin, useTokenUsageReport, useUsageFormatters };
785
+ /**
786
+ * Chart palette for the administrative token-usage dashboard.
787
+ *
788
+ * Every value here is a documented step of the dataviz skill's reference ramps —
789
+ * nothing was eyeballed, nothing was hand-mixed. The palette was run through the
790
+ * skill's validator against THIS application's real chart surfaces in both modes.
791
+ * The verbatim output is below.
792
+ *
793
+ * The surface is `--card`, NOT `--background`. Every chart that uses this palette
794
+ * is painted inside a `<Card>`, so the card is the surface the marks actually sit
795
+ * on — and in dark mode it is the *lighter* of the two (`oklch(0.205 0 0)` vs the
796
+ * background's `oklch(0.145 0 0)`), which is the harder case for contrast. Taken
797
+ * from `apps/web/src/app/globals.css` (narr8 declares two `:root`/`.dark` pairs;
798
+ * the LATER pair wins, and it is the one read here) — light `oklch(1 0 0)`, dark
799
+ * `oklch(0.205 0 0)`. a360ai declares the same two values.
800
+ *
801
+ * oklch→hex conversion: the algebraic inverse of the Björn Ottosson OKLab
802
+ * matrices — the same matrices the validator applies in the forward direction —
803
+ * giving light `#ffffff` and dark `#171717`. Cross-checked by feeding it the
804
+ * previously recorded background value, which it reproduces exactly:
805
+ * `oklch(0.145 0 0)` → `#0a0a0a`. ✓
806
+ *
807
+ * Light and dark are two SELECTED sets of steps, chosen for their own surface —
808
+ * never one set with its lightness flipped at runtime. Consumers pick a set from
809
+ * the resolved theme (`seriesColor(i, mode)` / `sequentialColor(i, mode)`).
810
+ *
811
+ * ---------------------------------------------------------------------------
812
+ * VALIDATOR OUTPUT — dataviz `scripts/validate_palette.js`, run 2026-08-15
813
+ * ---------------------------------------------------------------------------
814
+ *
815
+ * $ node scripts/validate_palette.js "#2a78d6,#eb6834,#1baf7a,#eda100,#e87ba4,#008300,#4a3aa7" --mode light --surface "#ffffff"
816
+ *
817
+ * Palette (light, surface #ffffff, categorical): 7 slots
818
+ * [PASS] Lightness band all 7 inside L 0.43–0.77
819
+ * [PASS] Chroma floor all 7 >= 0.1
820
+ * [PASS] CVD separation worst adjacent #eda100↔#1baf7a ΔE 9.1 (protan) · tritan 5.8
821
+ * [PASS] Normal-vision floor worst adjacent #e87ba4↔#eda100 ΔE 19.6 (normal)
822
+ * [WARN] Contrast vs surface below 3:1 — relief required (visible labels or table view): [["#1baf7a",2.82],["#eda100",2.17],["#e87ba4",2.69]]
823
+ *
824
+ * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)
825
+ * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.
826
+ *
827
+ * $ node scripts/validate_palette.js "#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9" --mode dark --surface "#171717"
828
+ *
829
+ * Palette (dark, surface #171717, categorical): 7 slots
830
+ * [PASS] Lightness band all 7 inside L 0.48–0.67
831
+ * [PASS] Chroma floor all 7 >= 0.1
832
+ * [PASS] CVD separation worst adjacent #c98500↔#199e70 ΔE 8.4 (protan) · tritan 8.7
833
+ * [PASS] Normal-vision floor worst adjacent #d55181↔#c98500 ΔE 19.3 (normal)
834
+ * [PASS] Contrast vs surface all 7 >= 3:1
835
+ *
836
+ * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)
837
+ * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.
838
+ *
839
+ * $ node scripts/validate_palette.js "#104281,#1c5cab,#2a78d6,#5598e7,#86b6ef" --mode light --surface "#ffffff" --ordinal
840
+ *
841
+ * Palette (light, surface #ffffff, ordinal ramp): 5 slots
842
+ * [PASS] Lightness monotone steps read light→dark
843
+ * [PASS] Adjacent ΔL all gaps >= 0.06
844
+ * [PASS] Light-end contrast #86b6ef at 2.11:1 vs surface
845
+ * [PASS] Single hue hue spread 3°
846
+ *
847
+ * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)
848
+ *
849
+ * $ node scripts/validate_palette.js "#9ec5f4,#6da7ec,#3987e5,#256abf,#184f95" --mode dark --surface "#171717" --ordinal
850
+ *
851
+ * Palette (dark, surface #171717, ordinal ramp): 5 slots
852
+ * [PASS] Lightness monotone steps read light→dark
853
+ * [PASS] Adjacent ΔL all gaps >= 0.06
854
+ * [PASS] Light-end contrast #184f95 at 2.21:1 vs surface
855
+ * [PASS] Single hue hue spread 3°
856
+ *
857
+ * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)
858
+ *
859
+ * OTHER_COLOR is achromatic, so the categorical checks (which gate hue identity)
860
+ * do not apply to it; it was gated on contrast alone with the validator's own
861
+ * `contrast()` export: 3.59:1 on `#ffffff`, 4.99:1 on `#171717` — both clear 3:1.
862
+ *
863
+ * ---------------------------------------------------------------------------
864
+ * WHAT THE RESULTS OBLIGE US TO DO
865
+ * ---------------------------------------------------------------------------
866
+ *
867
+ * - NO COLOUR VALUE CHANGED in the move from `--background` to `--card`. Every
868
+ * gate that passed on the background still passes on the card, so re-stepping
869
+ * a hue would have been churn, not a fix. Only the recorded surface, the
870
+ * recorded numbers and this prose changed.
871
+ * - The dark set still clears 3:1 on the LIGHTER card surface: the worst slot is
872
+ * green `#008300` at 3.63:1, and the ordinal ramp's surface-nearest step lands
873
+ * at 2.21:1 (down from 2.44:1 on the background) — still above the 2:1 gate.
874
+ * That headroom is thin, so a future card-surface lightening MUST re-run these
875
+ * four commands rather than assume the set still holds.
876
+ * - The light-mode contrast WARN is NOT dismissable. Three slots (aqua, yellow,
877
+ * magenta) sit below 3:1 on white, so every surface that paints with this
878
+ * palette MUST ship the relief channel: a visible legend, direct labels and a
879
+ * tooltip carrying the value in text. The timeline chart and the breakdown
880
+ * table both do. The light card is `#ffffff`, identical to the light
881
+ * background, so this WARN is unchanged rather than newly incurred.
882
+ * - CVD separation is measured on ADJACENT pairs, which is the correct pairlist
883
+ * for stacked bars, grouped bars and lines — the only forms this palette paints.
884
+ * A scatter / bubble / small-multiples chart would need `--pairs all`, which
885
+ * caps the usable slot count at three; do not reuse this array there without
886
+ * re-running the validator with that flag.
887
+ */
888
+ /** Which surface the colours are being painted on. */
889
+ type ChartMode = "light" | "dark";
890
+ /**
891
+ * The identity colour for series `index`.
892
+ *
893
+ * Never generates a hue and never cycles: anything at or past the ceiling gets
894
+ * `OTHER_COLOR`, because a series past the ceiling should already have been
895
+ * folded into the "other" bucket by the caller.
896
+ */
897
+ declare function seriesColor(index: number, mode?: ChartMode): string;
898
+ /**
899
+ * Both mode steps for series `index`, shaped for a shadcn `ChartConfig` entry's
900
+ * `theme` field.
901
+ */
902
+ declare function seriesTheme(index: number): {
903
+ light: string;
904
+ dark: string;
905
+ };
906
+
907
+ export { type ChartMode, type Dimension, type Granularity, type Metric, type ReportDimension, type ReportMetric, type Scope, type StackBy, TOKEN_USAGE_ADMIN_I18N_KEYS, TOKEN_USAGE_REPORT_I18N_KEYS, TokenUsageAdminBreakdown, type TokenUsageAdminBreakdownInput, type TokenUsageAdminBreakdownInterface, TokenUsageAdminContainer, type TokenUsageAdminContextType, TokenUsageAdminFilterBar, type TokenUsageAdminFilterState, type TokenUsageAdminFilters, TokenUsageAdminProvider, TokenUsageAdminService, TokenUsageAdminSummary, type TokenUsageAdminSummaryInput, type TokenUsageAdminSummaryInterface, TokenUsageAdminTiles, TokenUsageAdminTimeline, type TokenUsageAdminTimelineInput, type TokenUsageAdminTimelineInterface, type TokenUsageBalances, TokenUsageBreakdownTable, TokenUsageRankedBar, TokenUsageReportBreakdown, type TokenUsageReportBreakdownInput, type TokenUsageReportBreakdownInterface, TokenUsageReportContainer, type TokenUsageReportContextType, TokenUsageReportFilterBar, type TokenUsageReportFilterState, type TokenUsageReportFilters, TokenUsageReportProvider, TokenUsageReportService, TokenUsageReportSummary, type TokenUsageReportSummaryInput, type TokenUsageReportSummaryInterface, TokenUsageReportTiles, TokenUsageReportTimeline, type TokenUsageReportTimelineInput, type TokenUsageReportTimelineInterface, TokenUsageTimelineChart, type UsageFormatters, createUsageFormatters, seriesColor, seriesTheme, useTokenUsageAdmin, useTokenUsageReport, useUsageFormatters };
@@ -782,4 +782,126 @@ declare function createUsageFormatters(locale: string, currency: string): UsageF
782
782
  */
783
783
  declare function useUsageFormatters(): UsageFormatters;
784
784
 
785
- export { type Dimension, type Granularity, type Metric, type ReportDimension, type ReportMetric, type Scope, type StackBy, TOKEN_USAGE_ADMIN_I18N_KEYS, TOKEN_USAGE_REPORT_I18N_KEYS, TokenUsageAdminBreakdown, type TokenUsageAdminBreakdownInput, type TokenUsageAdminBreakdownInterface, TokenUsageAdminContainer, type TokenUsageAdminContextType, TokenUsageAdminFilterBar, type TokenUsageAdminFilterState, type TokenUsageAdminFilters, TokenUsageAdminProvider, TokenUsageAdminService, TokenUsageAdminSummary, type TokenUsageAdminSummaryInput, type TokenUsageAdminSummaryInterface, TokenUsageAdminTiles, TokenUsageAdminTimeline, type TokenUsageAdminTimelineInput, type TokenUsageAdminTimelineInterface, type TokenUsageBalances, TokenUsageBreakdownTable, TokenUsageRankedBar, TokenUsageReportBreakdown, type TokenUsageReportBreakdownInput, type TokenUsageReportBreakdownInterface, TokenUsageReportContainer, type TokenUsageReportContextType, TokenUsageReportFilterBar, type TokenUsageReportFilterState, type TokenUsageReportFilters, TokenUsageReportProvider, TokenUsageReportService, TokenUsageReportSummary, type TokenUsageReportSummaryInput, type TokenUsageReportSummaryInterface, TokenUsageReportTiles, TokenUsageReportTimeline, type TokenUsageReportTimelineInput, type TokenUsageReportTimelineInterface, TokenUsageTimelineChart, type UsageFormatters, createUsageFormatters, useTokenUsageAdmin, useTokenUsageReport, useUsageFormatters };
785
+ /**
786
+ * Chart palette for the administrative token-usage dashboard.
787
+ *
788
+ * Every value here is a documented step of the dataviz skill's reference ramps —
789
+ * nothing was eyeballed, nothing was hand-mixed. The palette was run through the
790
+ * skill's validator against THIS application's real chart surfaces in both modes.
791
+ * The verbatim output is below.
792
+ *
793
+ * The surface is `--card`, NOT `--background`. Every chart that uses this palette
794
+ * is painted inside a `<Card>`, so the card is the surface the marks actually sit
795
+ * on — and in dark mode it is the *lighter* of the two (`oklch(0.205 0 0)` vs the
796
+ * background's `oklch(0.145 0 0)`), which is the harder case for contrast. Taken
797
+ * from `apps/web/src/app/globals.css` (narr8 declares two `:root`/`.dark` pairs;
798
+ * the LATER pair wins, and it is the one read here) — light `oklch(1 0 0)`, dark
799
+ * `oklch(0.205 0 0)`. a360ai declares the same two values.
800
+ *
801
+ * oklch→hex conversion: the algebraic inverse of the Björn Ottosson OKLab
802
+ * matrices — the same matrices the validator applies in the forward direction —
803
+ * giving light `#ffffff` and dark `#171717`. Cross-checked by feeding it the
804
+ * previously recorded background value, which it reproduces exactly:
805
+ * `oklch(0.145 0 0)` → `#0a0a0a`. ✓
806
+ *
807
+ * Light and dark are two SELECTED sets of steps, chosen for their own surface —
808
+ * never one set with its lightness flipped at runtime. Consumers pick a set from
809
+ * the resolved theme (`seriesColor(i, mode)` / `sequentialColor(i, mode)`).
810
+ *
811
+ * ---------------------------------------------------------------------------
812
+ * VALIDATOR OUTPUT — dataviz `scripts/validate_palette.js`, run 2026-08-15
813
+ * ---------------------------------------------------------------------------
814
+ *
815
+ * $ node scripts/validate_palette.js "#2a78d6,#eb6834,#1baf7a,#eda100,#e87ba4,#008300,#4a3aa7" --mode light --surface "#ffffff"
816
+ *
817
+ * Palette (light, surface #ffffff, categorical): 7 slots
818
+ * [PASS] Lightness band all 7 inside L 0.43–0.77
819
+ * [PASS] Chroma floor all 7 >= 0.1
820
+ * [PASS] CVD separation worst adjacent #eda100↔#1baf7a ΔE 9.1 (protan) · tritan 5.8
821
+ * [PASS] Normal-vision floor worst adjacent #e87ba4↔#eda100 ΔE 19.6 (normal)
822
+ * [WARN] Contrast vs surface below 3:1 — relief required (visible labels or table view): [["#1baf7a",2.82],["#eda100",2.17],["#e87ba4",2.69]]
823
+ *
824
+ * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)
825
+ * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.
826
+ *
827
+ * $ node scripts/validate_palette.js "#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9" --mode dark --surface "#171717"
828
+ *
829
+ * Palette (dark, surface #171717, categorical): 7 slots
830
+ * [PASS] Lightness band all 7 inside L 0.48–0.67
831
+ * [PASS] Chroma floor all 7 >= 0.1
832
+ * [PASS] CVD separation worst adjacent #c98500↔#199e70 ΔE 8.4 (protan) · tritan 8.7
833
+ * [PASS] Normal-vision floor worst adjacent #d55181↔#c98500 ΔE 19.3 (normal)
834
+ * [PASS] Contrast vs surface all 7 >= 3:1
835
+ *
836
+ * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)
837
+ * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.
838
+ *
839
+ * $ node scripts/validate_palette.js "#104281,#1c5cab,#2a78d6,#5598e7,#86b6ef" --mode light --surface "#ffffff" --ordinal
840
+ *
841
+ * Palette (light, surface #ffffff, ordinal ramp): 5 slots
842
+ * [PASS] Lightness monotone steps read light→dark
843
+ * [PASS] Adjacent ΔL all gaps >= 0.06
844
+ * [PASS] Light-end contrast #86b6ef at 2.11:1 vs surface
845
+ * [PASS] Single hue hue spread 3°
846
+ *
847
+ * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)
848
+ *
849
+ * $ node scripts/validate_palette.js "#9ec5f4,#6da7ec,#3987e5,#256abf,#184f95" --mode dark --surface "#171717" --ordinal
850
+ *
851
+ * Palette (dark, surface #171717, ordinal ramp): 5 slots
852
+ * [PASS] Lightness monotone steps read light→dark
853
+ * [PASS] Adjacent ΔL all gaps >= 0.06
854
+ * [PASS] Light-end contrast #184f95 at 2.21:1 vs surface
855
+ * [PASS] Single hue hue spread 3°
856
+ *
857
+ * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)
858
+ *
859
+ * OTHER_COLOR is achromatic, so the categorical checks (which gate hue identity)
860
+ * do not apply to it; it was gated on contrast alone with the validator's own
861
+ * `contrast()` export: 3.59:1 on `#ffffff`, 4.99:1 on `#171717` — both clear 3:1.
862
+ *
863
+ * ---------------------------------------------------------------------------
864
+ * WHAT THE RESULTS OBLIGE US TO DO
865
+ * ---------------------------------------------------------------------------
866
+ *
867
+ * - NO COLOUR VALUE CHANGED in the move from `--background` to `--card`. Every
868
+ * gate that passed on the background still passes on the card, so re-stepping
869
+ * a hue would have been churn, not a fix. Only the recorded surface, the
870
+ * recorded numbers and this prose changed.
871
+ * - The dark set still clears 3:1 on the LIGHTER card surface: the worst slot is
872
+ * green `#008300` at 3.63:1, and the ordinal ramp's surface-nearest step lands
873
+ * at 2.21:1 (down from 2.44:1 on the background) — still above the 2:1 gate.
874
+ * That headroom is thin, so a future card-surface lightening MUST re-run these
875
+ * four commands rather than assume the set still holds.
876
+ * - The light-mode contrast WARN is NOT dismissable. Three slots (aqua, yellow,
877
+ * magenta) sit below 3:1 on white, so every surface that paints with this
878
+ * palette MUST ship the relief channel: a visible legend, direct labels and a
879
+ * tooltip carrying the value in text. The timeline chart and the breakdown
880
+ * table both do. The light card is `#ffffff`, identical to the light
881
+ * background, so this WARN is unchanged rather than newly incurred.
882
+ * - CVD separation is measured on ADJACENT pairs, which is the correct pairlist
883
+ * for stacked bars, grouped bars and lines — the only forms this palette paints.
884
+ * A scatter / bubble / small-multiples chart would need `--pairs all`, which
885
+ * caps the usable slot count at three; do not reuse this array there without
886
+ * re-running the validator with that flag.
887
+ */
888
+ /** Which surface the colours are being painted on. */
889
+ type ChartMode = "light" | "dark";
890
+ /**
891
+ * The identity colour for series `index`.
892
+ *
893
+ * Never generates a hue and never cycles: anything at or past the ceiling gets
894
+ * `OTHER_COLOR`, because a series past the ceiling should already have been
895
+ * folded into the "other" bucket by the caller.
896
+ */
897
+ declare function seriesColor(index: number, mode?: ChartMode): string;
898
+ /**
899
+ * Both mode steps for series `index`, shaped for a shadcn `ChartConfig` entry's
900
+ * `theme` field.
901
+ */
902
+ declare function seriesTheme(index: number): {
903
+ light: string;
904
+ dark: string;
905
+ };
906
+
907
+ export { type ChartMode, type Dimension, type Granularity, type Metric, type ReportDimension, type ReportMetric, type Scope, type StackBy, TOKEN_USAGE_ADMIN_I18N_KEYS, TOKEN_USAGE_REPORT_I18N_KEYS, TokenUsageAdminBreakdown, type TokenUsageAdminBreakdownInput, type TokenUsageAdminBreakdownInterface, TokenUsageAdminContainer, type TokenUsageAdminContextType, TokenUsageAdminFilterBar, type TokenUsageAdminFilterState, type TokenUsageAdminFilters, TokenUsageAdminProvider, TokenUsageAdminService, TokenUsageAdminSummary, type TokenUsageAdminSummaryInput, type TokenUsageAdminSummaryInterface, TokenUsageAdminTiles, TokenUsageAdminTimeline, type TokenUsageAdminTimelineInput, type TokenUsageAdminTimelineInterface, type TokenUsageBalances, TokenUsageBreakdownTable, TokenUsageRankedBar, TokenUsageReportBreakdown, type TokenUsageReportBreakdownInput, type TokenUsageReportBreakdownInterface, TokenUsageReportContainer, type TokenUsageReportContextType, TokenUsageReportFilterBar, type TokenUsageReportFilterState, type TokenUsageReportFilters, TokenUsageReportProvider, TokenUsageReportService, TokenUsageReportSummary, type TokenUsageReportSummaryInput, type TokenUsageReportSummaryInterface, TokenUsageReportTiles, TokenUsageReportTimeline, type TokenUsageReportTimelineInput, type TokenUsageReportTimelineInterface, TokenUsageTimelineChart, type UsageFormatters, createUsageFormatters, seriesColor, seriesTheme, useTokenUsageAdmin, useTokenUsageReport, useUsageFormatters };