@carlonicora/nextjs-jsonapi 1.141.3 → 1.143.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.
- package/dist/AbstractService-B0T7h8fX.d.mts +124 -0
- package/dist/AbstractService-Cew0PD0L.d.ts +124 -0
- package/dist/{AssistantMessageInterface-DH1QwtC_.d.mts → AssistantMessageInterface-Ce8FcllX.d.mts} +1 -1
- package/dist/{AssistantMessageInterface-giWvsOrX.d.ts → AssistantMessageInterface-Dh0BpbP2.d.ts} +1 -1
- package/dist/{AuthComponent-DdxCFgUZ.d.mts → AuthComponent-Cik96ElA.d.mts} +1 -1
- package/dist/{AuthComponent-Cd7lcYif.d.ts → AuthComponent-DTTKu3VK.d.ts} +1 -1
- package/dist/{BlockNoteEditor-JTKEBS2W.mjs → BlockNoteEditor-34QM4UR5.mjs} +4 -4
- package/dist/{BlockNoteEditor-4OXY6BKF.js → BlockNoteEditor-LMKWUOJG.js} +19 -19
- package/dist/{BlockNoteEditor-4OXY6BKF.js.map → BlockNoteEditor-LMKWUOJG.js.map} +1 -1
- package/dist/{auth.interface-yivTb46w.d.ts → auth.interface-COmBjh89.d.ts} +1 -1
- package/dist/{auth.interface-r3XKYOrQ.d.mts → auth.interface-CkmZqat3.d.mts} +1 -1
- package/dist/billing/index.js +357 -357
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-5GVY2M5X.mjs → chunk-BIL2WQEV.mjs} +264 -18
- package/dist/chunk-BIL2WQEV.mjs.map +1 -0
- package/dist/{chunk-AGKZCRG2.js → chunk-DS4IBFWA.js} +269 -23
- package/dist/chunk-DS4IBFWA.js.map +1 -0
- package/dist/{chunk-4TGGHNWL.js → chunk-F6PZZ4KI.js} +752 -752
- package/dist/chunk-F6PZZ4KI.js.map +1 -0
- package/dist/{chunk-KXTTYXJ6.mjs → chunk-HN5ED7OF.mjs} +2 -2
- package/dist/{chunk-K76QSQTC.mjs → chunk-UGSOBBZH.mjs} +28 -28
- package/dist/chunk-UGSOBBZH.mjs.map +1 -0
- package/dist/{chunk-SE2YSAER.js → chunk-UXJAS6C5.js} +7 -7
- package/dist/{chunk-SE2YSAER.js.map → chunk-UXJAS6C5.js.map} +1 -1
- package/dist/client/index.d.mts +5 -5
- package/dist/client/index.d.ts +5 -5
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +6 -6
- package/dist/components/index.d.ts +6 -6
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/{config-BrkU_Ynz.d.ts → config-Cd5xTEfZ.d.ts} +1 -1
- package/dist/{config-snd2ToEB.d.mts → config-YyhFHxkU.d.mts} +1 -1
- package/dist/contexts/index.d.mts +4 -4
- package/dist/contexts/index.d.ts +4 -4
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +24 -60
- package/dist/core/index.d.ts +24 -60
- package/dist/core/index.js +8 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +7 -1
- package/dist/features/help/index.d.mts +2 -2
- package/dist/features/help/index.d.ts +2 -2
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.css +29 -0
- package/dist/features/tokenusage/index.css.map +1 -0
- package/dist/features/tokenusage/index.d.mts +388 -0
- package/dist/features/tokenusage/index.d.ts +388 -0
- package/dist/features/tokenusage/index.js +1012 -0
- package/dist/features/tokenusage/index.js.map +1 -0
- package/dist/features/tokenusage/index.mjs +1012 -0
- package/dist/features/tokenusage/index.mjs.map +1 -0
- package/dist/index.d.mts +11 -9
- package/dist/index.d.ts +11 -9
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -2
- package/dist/{notification.interface-LKFmZndR.d.ts → notification.interface--27exGde.d.ts} +6 -6
- package/dist/{notification.interface-B3kNUTZs.d.mts → notification.interface-CG1vqoxg.d.mts} +6 -6
- package/dist/{s3.service-BrmeMjtk.d.ts → s3.service-CpwA3sl_.d.ts} +4 -125
- package/dist/{s3.service-CXKV_OT-.d.mts → s3.service-Dt6MfuHr.d.mts} +4 -125
- package/dist/server/index.d.mts +5 -4
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/tokenusage-admin.module-5wJ_tZTj.d.ts +61 -0
- package/dist/tokenusage-admin.module-DQVT4O4j.d.mts +61 -0
- package/dist/{types-CQSjy7et.d.mts → types-Bq_UA8Lg.d.mts} +1 -1
- package/dist/{types-DHOxe8rc.d.ts → types-bsm8Fp55.d.ts} +1 -1
- package/dist/{usePageUrlGenerator-tjq2mlDV.d.ts → usePageUrlGenerator-J_ECv-oQ.d.ts} +1 -1
- package/dist/{usePageUrlGenerator-uOnyJ6j2.d.mts → usePageUrlGenerator-uKBjAKqC.d.mts} +1 -1
- package/dist/{useSocket-BsbaGPqE.d.ts → useSocket-DgGAQW6h.d.ts} +1 -1
- package/dist/{useSocket-BSjn1Rlb.d.mts → useSocket-M3YY0aTS.d.mts} +1 -1
- package/package.json +6 -1
- package/src/core/index.ts +5 -0
- package/src/core/registry/ModuleRegistry.ts +4 -0
- package/src/features/company/components/details/TokenStatusIndicator.tsx +9 -9
- package/src/features/company/data/company.interface.ts +6 -6
- package/src/features/company/data/company.ts +17 -17
- package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +189 -0
- package/src/features/tokenusage/components/TokenUsageAdminFilterBar.tsx +138 -0
- package/src/features/tokenusage/components/TokenUsageAdminTiles.tsx +177 -0
- package/src/features/tokenusage/components/TokenUsageBreakdownTable.tsx +160 -0
- package/src/features/tokenusage/components/TokenUsageRankedBar.tsx +99 -0
- package/src/features/tokenusage/components/TokenUsageTimelineChart.tsx +225 -0
- package/src/features/tokenusage/components/__tests__/TokenUsageAdminTiles.spec.tsx +42 -0
- package/src/features/tokenusage/components/__tests__/TokenUsageRankedBar.spec.tsx +41 -0
- package/src/features/tokenusage/components/__tests__/TokenUsageTimelineChart.spec.tsx +47 -0
- package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +252 -0
- package/src/features/tokenusage/contexts/__tests__/TokenUsageAdminContext.spec.tsx +80 -0
- package/src/features/tokenusage/data/TokenUsageAdminService.ts +54 -0
- package/src/features/tokenusage/data/__tests__/tokenusage-admin-models.spec.ts +147 -0
- package/src/features/tokenusage/data/index.ts +8 -0
- package/src/features/tokenusage/data/tokenusage-admin-breakdown.interface.ts +18 -0
- package/src/features/tokenusage/data/tokenusage-admin-breakdown.ts +115 -0
- package/src/features/tokenusage/data/tokenusage-admin-summary.interface.ts +14 -0
- package/src/features/tokenusage/data/tokenusage-admin-summary.ts +90 -0
- package/src/features/tokenusage/data/tokenusage-admin-timeline.interface.ts +13 -0
- package/src/features/tokenusage/data/tokenusage-admin-timeline.ts +94 -0
- package/src/features/tokenusage/data/tokenusage-admin.types.ts +56 -0
- package/src/features/tokenusage/i18n-keys.ts +65 -0
- package/src/features/tokenusage/index.ts +30 -0
- package/src/features/tokenusage/lib/metrics.ts +69 -0
- package/src/features/tokenusage/lib/operation-label.ts +41 -0
- package/src/features/tokenusage/lib/palette.ts +197 -0
- package/src/features/tokenusage/tokenusage-admin.module.ts +22 -0
- package/src/features/user/contexts/CurrentUserContext.tsx +18 -18
- package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +21 -21
- package/dist/chunk-4TGGHNWL.js.map +0 -1
- package/dist/chunk-5GVY2M5X.mjs.map +0 -1
- package/dist/chunk-AGKZCRG2.js.map +0 -1
- package/dist/chunk-K76QSQTC.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-JTKEBS2W.mjs.map → BlockNoteEditor-34QM4UR5.mjs.map} +0 -0
- /package/dist/{chunk-KXTTYXJ6.mjs.map → chunk-HN5ED7OF.mjs.map} +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export interface TokenUsageAdminSummaryInterface extends ApiDataInterface {
|
|
4
|
+
/** "customer" | "platform" | "total" */
|
|
5
|
+
get scope(): string;
|
|
6
|
+
/** "current" | "previous" */
|
|
7
|
+
get window(): string;
|
|
8
|
+
get cost(): number;
|
|
9
|
+
get credits(): number;
|
|
10
|
+
get tokensIn(): number;
|
|
11
|
+
get tokensOut(): number;
|
|
12
|
+
get cached(): number;
|
|
13
|
+
get calls(): number;
|
|
14
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { TokenUsageAdminSummaryInterface } from "./tokenusage-admin-summary.interface";
|
|
3
|
+
import { TokenUsageAdminSummaryInput } from "./tokenusage-admin.types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One row of the administrative summary: a cost centre observed over one time
|
|
7
|
+
* window. Six rows are returned per request — scope crossed with window — so the
|
|
8
|
+
* KPI tiles can render a value and its delta without a second call.
|
|
9
|
+
*/
|
|
10
|
+
export class TokenUsageAdminSummary extends AbstractApiData implements TokenUsageAdminSummaryInterface {
|
|
11
|
+
private _scope: string = "";
|
|
12
|
+
private _window: string = "";
|
|
13
|
+
private _cost: number = 0;
|
|
14
|
+
private _credits: number = 0;
|
|
15
|
+
private _tokensIn: number = 0;
|
|
16
|
+
private _tokensOut: number = 0;
|
|
17
|
+
private _cached: number = 0;
|
|
18
|
+
private _calls: number = 0;
|
|
19
|
+
|
|
20
|
+
get scope(): string {
|
|
21
|
+
return this._scope;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get window(): string {
|
|
25
|
+
return this._window;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get cost(): number {
|
|
29
|
+
return this._cost;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get credits(): number {
|
|
33
|
+
return this._credits;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get tokensIn(): number {
|
|
37
|
+
return this._tokensIn;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get tokensOut(): number {
|
|
41
|
+
return this._tokensOut;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get cached(): number {
|
|
45
|
+
return this._cached;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get calls(): number {
|
|
49
|
+
return this._calls;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
53
|
+
super.rehydrate(data);
|
|
54
|
+
|
|
55
|
+
const attrs = data.jsonApi.attributes;
|
|
56
|
+
this._scope = attrs.scope ?? "";
|
|
57
|
+
this._window = attrs.window ?? "";
|
|
58
|
+
this._cost = attrs.cost ?? 0;
|
|
59
|
+
this._credits = attrs.credits ?? 0;
|
|
60
|
+
this._tokensIn = attrs.tokensIn ?? 0;
|
|
61
|
+
this._tokensOut = attrs.tokensOut ?? 0;
|
|
62
|
+
this._cached = attrs.cached ?? 0;
|
|
63
|
+
this._calls = attrs.calls ?? 0;
|
|
64
|
+
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
createJsonApi(data: TokenUsageAdminSummaryInput) {
|
|
69
|
+
const response: any = {
|
|
70
|
+
data: {
|
|
71
|
+
type: Modules.TokenUsageAdminSummary.name,
|
|
72
|
+
id: data.id,
|
|
73
|
+
attributes: {},
|
|
74
|
+
relationships: {},
|
|
75
|
+
},
|
|
76
|
+
included: [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (data.scope !== undefined) response.data.attributes.scope = data.scope;
|
|
80
|
+
if (data.window !== undefined) response.data.attributes.window = data.window;
|
|
81
|
+
if (data.cost !== undefined) response.data.attributes.cost = data.cost;
|
|
82
|
+
if (data.credits !== undefined) response.data.attributes.credits = data.credits;
|
|
83
|
+
if (data.tokensIn !== undefined) response.data.attributes.tokensIn = data.tokensIn;
|
|
84
|
+
if (data.tokensOut !== undefined) response.data.attributes.tokensOut = data.tokensOut;
|
|
85
|
+
if (data.cached !== undefined) response.data.attributes.cached = data.cached;
|
|
86
|
+
if (data.calls !== undefined) response.data.attributes.calls = data.calls;
|
|
87
|
+
|
|
88
|
+
return response;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export interface TokenUsageAdminTimelineInterface extends ApiDataInterface {
|
|
4
|
+
/** Backend declares this `type: "date"`, so it is a Date in memory. */
|
|
5
|
+
get bucket(): Date;
|
|
6
|
+
get series(): string;
|
|
7
|
+
get cost(): number;
|
|
8
|
+
get credits(): number;
|
|
9
|
+
get tokensIn(): number;
|
|
10
|
+
get tokensOut(): number;
|
|
11
|
+
get cached(): number;
|
|
12
|
+
get calls(): number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AbstractApiData, formatLocalDate, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { TokenUsageAdminTimelineInterface } from "./tokenusage-admin-timeline.interface";
|
|
3
|
+
import { TokenUsageAdminTimelineInput } from "./tokenusage-admin.types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One (bucket, series) cell of the usage-over-time chart. Rows arrive flat; the
|
|
7
|
+
* chart pivots them into stacked columns.
|
|
8
|
+
*/
|
|
9
|
+
export class TokenUsageAdminTimeline extends AbstractApiData implements TokenUsageAdminTimelineInterface {
|
|
10
|
+
private _bucket: Date = new Date(0);
|
|
11
|
+
private _series: string = "";
|
|
12
|
+
private _cost: number = 0;
|
|
13
|
+
private _credits: number = 0;
|
|
14
|
+
private _tokensIn: number = 0;
|
|
15
|
+
private _tokensOut: number = 0;
|
|
16
|
+
private _cached: number = 0;
|
|
17
|
+
private _calls: number = 0;
|
|
18
|
+
|
|
19
|
+
get bucket(): Date {
|
|
20
|
+
return this._bucket;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get series(): string {
|
|
24
|
+
return this._series;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get cost(): number {
|
|
28
|
+
return this._cost;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get credits(): number {
|
|
32
|
+
return this._credits;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get tokensIn(): number {
|
|
36
|
+
return this._tokensIn;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get tokensOut(): number {
|
|
40
|
+
return this._tokensOut;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get cached(): number {
|
|
44
|
+
return this._cached;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get calls(): number {
|
|
48
|
+
return this._calls;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
52
|
+
super.rehydrate(data);
|
|
53
|
+
|
|
54
|
+
const attrs = data.jsonApi.attributes;
|
|
55
|
+
// Wire format is "YYYY-MM-DD"; the interface promises a Date.
|
|
56
|
+
this._bucket = attrs.bucket ? new Date(attrs.bucket) : new Date(0);
|
|
57
|
+
this._series = attrs.series ?? "";
|
|
58
|
+
this._cost = attrs.cost ?? 0;
|
|
59
|
+
this._credits = attrs.credits ?? 0;
|
|
60
|
+
this._tokensIn = attrs.tokensIn ?? 0;
|
|
61
|
+
this._tokensOut = attrs.tokensOut ?? 0;
|
|
62
|
+
this._cached = attrs.cached ?? 0;
|
|
63
|
+
this._calls = attrs.calls ?? 0;
|
|
64
|
+
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
createJsonApi(data: TokenUsageAdminTimelineInput) {
|
|
69
|
+
const response: any = {
|
|
70
|
+
data: {
|
|
71
|
+
type: Modules.TokenUsageAdminTimeline.name,
|
|
72
|
+
id: data.id,
|
|
73
|
+
attributes: {},
|
|
74
|
+
relationships: {},
|
|
75
|
+
},
|
|
76
|
+
included: [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// `bucket` is type: "date" on the backend descriptor. formatLocalDate is
|
|
80
|
+
// mandatory — passing the raw Date lets JSON.stringify call .toISOString(),
|
|
81
|
+
// which UTC-shifts and can move the bucket a day. Imported from the package
|
|
82
|
+
// core, never reimplemented inline.
|
|
83
|
+
if (data.bucket !== undefined) response.data.attributes.bucket = formatLocalDate(data.bucket);
|
|
84
|
+
if (data.series !== undefined) response.data.attributes.series = data.series;
|
|
85
|
+
if (data.cost !== undefined) response.data.attributes.cost = data.cost;
|
|
86
|
+
if (data.credits !== undefined) response.data.attributes.credits = data.credits;
|
|
87
|
+
if (data.tokensIn !== undefined) response.data.attributes.tokensIn = data.tokensIn;
|
|
88
|
+
if (data.tokensOut !== undefined) response.data.attributes.tokensOut = data.tokensOut;
|
|
89
|
+
if (data.cached !== undefined) response.data.attributes.cached = data.cached;
|
|
90
|
+
if (data.calls !== undefined) response.data.attributes.calls = data.calls;
|
|
91
|
+
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type Granularity = "day" | "week" | "month";
|
|
2
|
+
export type StackBy = "scope" | "type" | "company";
|
|
3
|
+
export type Dimension = "company" | "user" | "operation";
|
|
4
|
+
export type Scope = "customer" | "platform";
|
|
5
|
+
export type Metric = "cost" | "credits" | "tokens";
|
|
6
|
+
|
|
7
|
+
export type TokenUsageAdminFilters = {
|
|
8
|
+
/** ISO 8601 instant. */
|
|
9
|
+
from: string;
|
|
10
|
+
/** ISO 8601 instant. */
|
|
11
|
+
to: string;
|
|
12
|
+
companyId?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Input types for createJsonApi(). `id` is always required — JSON:API resource
|
|
16
|
+
// objects are identified.
|
|
17
|
+
|
|
18
|
+
export type TokenUsageAdminSummaryInput = {
|
|
19
|
+
id: string;
|
|
20
|
+
scope: string;
|
|
21
|
+
window: string;
|
|
22
|
+
cost: number;
|
|
23
|
+
credits: number;
|
|
24
|
+
tokensIn: number;
|
|
25
|
+
tokensOut: number;
|
|
26
|
+
cached: number;
|
|
27
|
+
calls: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type TokenUsageAdminTimelineInput = {
|
|
31
|
+
id: string;
|
|
32
|
+
/** Backend field type is "date" — emitted via formatLocalDate, never raw. */
|
|
33
|
+
bucket: Date;
|
|
34
|
+
series: string;
|
|
35
|
+
cost: number;
|
|
36
|
+
credits: number;
|
|
37
|
+
tokensIn: number;
|
|
38
|
+
tokensOut: number;
|
|
39
|
+
cached: number;
|
|
40
|
+
calls: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type TokenUsageAdminBreakdownInput = {
|
|
44
|
+
id: string;
|
|
45
|
+
label: string;
|
|
46
|
+
sublabel?: string;
|
|
47
|
+
cost: number;
|
|
48
|
+
credits: number;
|
|
49
|
+
tokensIn: number;
|
|
50
|
+
tokensOut: number;
|
|
51
|
+
cached: number;
|
|
52
|
+
calls: number;
|
|
53
|
+
activeUsers?: number;
|
|
54
|
+
monthlyCredits?: number;
|
|
55
|
+
availableMonthlyCredits?: number;
|
|
56
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed namespace of i18n keys the administrative token-usage feature reads
|
|
3
|
+
* via useTranslations/getTranslations. Consuming apps must define each entry in
|
|
4
|
+
* their messages/<locale>.json — this list is the contract between the package
|
|
5
|
+
* and the app.
|
|
6
|
+
*
|
|
7
|
+
* One key is NOT listed because it is resolved from data rather than from a
|
|
8
|
+
* fixed string: the timeline chart labels each series through
|
|
9
|
+
* `token_usage.types.<camelCase>` and falls back to the raw series key when the
|
|
10
|
+
* app has no entry, so that namespace stays the app's own vocabulary.
|
|
11
|
+
*/
|
|
12
|
+
export const TOKEN_USAGE_ADMIN_I18N_KEYS = [
|
|
13
|
+
// Page + KPI tiles
|
|
14
|
+
"token_usage.admin.title",
|
|
15
|
+
"token_usage.admin.customer_spend",
|
|
16
|
+
"token_usage.admin.platform_spend",
|
|
17
|
+
"token_usage.admin.total_cost",
|
|
18
|
+
"token_usage.admin.avg_per_call",
|
|
19
|
+
"token_usage.admin.cache_hit",
|
|
20
|
+
"token_usage.admin.vs_previous",
|
|
21
|
+
|
|
22
|
+
// Panel titles
|
|
23
|
+
"token_usage.admin.usage_over_time",
|
|
24
|
+
"token_usage.admin.by_company",
|
|
25
|
+
"token_usage.admin.by_user",
|
|
26
|
+
"token_usage.admin.customer_by_operation",
|
|
27
|
+
"token_usage.admin.platform_by_operation",
|
|
28
|
+
"token_usage.admin.detail",
|
|
29
|
+
|
|
30
|
+
// Shared states
|
|
31
|
+
"token_usage.admin.other",
|
|
32
|
+
"token_usage.admin.no_data",
|
|
33
|
+
|
|
34
|
+
// Filter bar
|
|
35
|
+
"token_usage.admin.all_companies",
|
|
36
|
+
"token_usage.admin.granularity.label",
|
|
37
|
+
"token_usage.admin.granularity.day",
|
|
38
|
+
"token_usage.admin.granularity.week",
|
|
39
|
+
"token_usage.admin.granularity.month",
|
|
40
|
+
"token_usage.admin.metric.label",
|
|
41
|
+
"token_usage.admin.metric.cost",
|
|
42
|
+
"token_usage.admin.metric.credits",
|
|
43
|
+
"token_usage.admin.metric.tokens",
|
|
44
|
+
|
|
45
|
+
// Timeline stacking control
|
|
46
|
+
"token_usage.admin.stack_by",
|
|
47
|
+
"token_usage.admin.stack.scope",
|
|
48
|
+
"token_usage.admin.stack.type",
|
|
49
|
+
"token_usage.admin.stack.company",
|
|
50
|
+
|
|
51
|
+
// Breakdown table columns
|
|
52
|
+
"token_usage.admin.columns.label",
|
|
53
|
+
"token_usage.admin.columns.sublabel",
|
|
54
|
+
"token_usage.admin.columns.calls",
|
|
55
|
+
"token_usage.admin.columns.tokens_in",
|
|
56
|
+
"token_usage.admin.columns.tokens_out",
|
|
57
|
+
"token_usage.admin.columns.cost",
|
|
58
|
+
"token_usage.admin.columns.credits",
|
|
59
|
+
"token_usage.admin.columns.share",
|
|
60
|
+
"token_usage.admin.columns.active_users",
|
|
61
|
+
|
|
62
|
+
// Timeline chart
|
|
63
|
+
"token_usage.series.other",
|
|
64
|
+
"token_usage.timeline.empty",
|
|
65
|
+
] as const;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Client-only public barrel for the administrative token-usage feature.
|
|
2
|
+
// Consumed via `@carlonicora/nextjs-jsonapi/tokenusage`. Tsup adds a top-level
|
|
3
|
+
// "use client" directive to the bundled output via clientEntries.
|
|
4
|
+
//
|
|
5
|
+
// Deliberately NOT re-exported from `src/index.ts` — keeping the feature off the
|
|
6
|
+
// main barrel is what keeps its chart (recharts) out of the main client bundle,
|
|
7
|
+
// exactly as the `help` feature does.
|
|
8
|
+
//
|
|
9
|
+
// Pair this with `@carlonicora/nextjs-jsonapi/tokenusage/server` for the ready
|
|
10
|
+
// made server page (default export + generateMetadata).
|
|
11
|
+
|
|
12
|
+
export { TokenUsageAdminProvider, useTokenUsageAdmin } from "./contexts/TokenUsageAdminContext";
|
|
13
|
+
export type { TokenUsageAdminContextType, TokenUsageAdminFilterState } from "./contexts/TokenUsageAdminContext";
|
|
14
|
+
|
|
15
|
+
export { TokenUsageAdminContainer } from "./components/TokenUsageAdminContainer";
|
|
16
|
+
export { TokenUsageAdminFilterBar } from "./components/TokenUsageAdminFilterBar";
|
|
17
|
+
export { TokenUsageAdminTiles } from "./components/TokenUsageAdminTiles";
|
|
18
|
+
export { TokenUsageRankedBar } from "./components/TokenUsageRankedBar";
|
|
19
|
+
export { TokenUsageBreakdownTable } from "./components/TokenUsageBreakdownTable";
|
|
20
|
+
export { TokenUsageTimelineChart } from "./components/TokenUsageTimelineChart";
|
|
21
|
+
|
|
22
|
+
export * from "./data";
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
TokenUsageAdminBreakdownModule,
|
|
26
|
+
TokenUsageAdminSummaryModule,
|
|
27
|
+
TokenUsageAdminTimelineModule,
|
|
28
|
+
} from "./tokenusage-admin.module";
|
|
29
|
+
|
|
30
|
+
export { TOKEN_USAGE_ADMIN_I18N_KEYS } from "./i18n-keys";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Metric } from "../data/tokenusage-admin.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The metric field set every admin token-usage resource carries. Declared
|
|
5
|
+
* structurally so it accepts the summary, timeline and breakdown interfaces
|
|
6
|
+
* alike — all three expose exactly these getters.
|
|
7
|
+
*/
|
|
8
|
+
export type TokenUsageMetrics = {
|
|
9
|
+
cost: number;
|
|
10
|
+
credits: number;
|
|
11
|
+
tokensIn: number;
|
|
12
|
+
tokensOut: number;
|
|
13
|
+
cached: number;
|
|
14
|
+
calls: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** UI copy is Italian, so every figure is formatted with the Italian locale. */
|
|
18
|
+
const LOCALE = "it-IT";
|
|
19
|
+
|
|
20
|
+
/** Reads the single number a row contributes for the currently selected metric. */
|
|
21
|
+
export function metricValue(row: TokenUsageMetrics, metric: Metric): number {
|
|
22
|
+
if (metric === "cost") return row.cost;
|
|
23
|
+
if (metric === "credits") return row.credits;
|
|
24
|
+
return row.tokensIn + row.tokensOut;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Locale-aware fixed-decimal number, e.g. 9.46 → "9,46". */
|
|
28
|
+
export function formatDecimal(value: number, decimals: number): string {
|
|
29
|
+
return value.toLocaleString(LOCALE, {
|
|
30
|
+
minimumFractionDigits: decimals,
|
|
31
|
+
maximumFractionDigits: decimals,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Formats a metric value for display: euros carry a "€ " prefix and 2 decimals,
|
|
37
|
+
* credits 2 decimals, token counts are whole numbers.
|
|
38
|
+
*/
|
|
39
|
+
export function formatMetricValue(value: number, metric: Metric): string {
|
|
40
|
+
if (metric === "cost") return `€ ${formatDecimal(value, 2)}`;
|
|
41
|
+
if (metric === "credits") return formatDecimal(value, 2);
|
|
42
|
+
return formatDecimal(value, 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Percentage with one decimal, e.g. 60 → "60,0 %". */
|
|
46
|
+
export function formatPercent(value: number, decimals = 1): string {
|
|
47
|
+
return `${formatDecimal(value, decimals)} %`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Share of cached input tokens. Returns 0 rather than NaN when the window holds
|
|
52
|
+
* no input tokens at all, so the tile renders "0,0 %" instead of "NaN %".
|
|
53
|
+
*/
|
|
54
|
+
export function cacheHitPercentage(cached: number, tokensIn: number): number {
|
|
55
|
+
if (!tokensIn) return 0;
|
|
56
|
+
return (cached / tokensIn) * 100;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Percentage change of `current` against `previous`, rounded to a whole number.
|
|
61
|
+
*
|
|
62
|
+
* Returns `undefined` when the previous window is zero — there is no meaningful
|
|
63
|
+
* percentage change from nothing, and rendering Infinity would be worse than
|
|
64
|
+
* rendering nothing. Callers show an em dash in that slot.
|
|
65
|
+
*/
|
|
66
|
+
export function percentageDelta(current: number, previous: number): number | undefined {
|
|
67
|
+
if (!previous) return undefined;
|
|
68
|
+
return Math.round(((current - previous) / previous) * 100);
|
|
69
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The i18n namespace under which a consuming app supplies its own vocabulary for
|
|
3
|
+
* token-usage operation types (`summariser`, `massima_extraction`, …).
|
|
4
|
+
*
|
|
5
|
+
* The set of operations is application-specific — the package cannot know that
|
|
6
|
+
* a360ai calls `massima_extraction` "Estrazione Massime" — so the package looks
|
|
7
|
+
* the copy up here and falls back to the raw key when the app has no entry.
|
|
8
|
+
*/
|
|
9
|
+
export const OPERATION_LABEL_NAMESPACE = "token_usage.types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `snake_case` / `kebab-case` → `camelCase`, the shape the i18n keys use.
|
|
13
|
+
*
|
|
14
|
+
* Shared by every surface that renders an operation type — the timeline chart's
|
|
15
|
+
* series labels and the ranked bar's operation rows — so the two can never
|
|
16
|
+
* disagree about which key a given type maps to.
|
|
17
|
+
*/
|
|
18
|
+
export const toCamelCase = (value: string): string => {
|
|
19
|
+
const parts = value.split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
20
|
+
if (parts.length === 0) return value;
|
|
21
|
+
return parts
|
|
22
|
+
.map((part, index) =>
|
|
23
|
+
index === 0 ? part.charAt(0).toLowerCase() + part.slice(1) : part.charAt(0).toUpperCase() + part.slice(1),
|
|
24
|
+
)
|
|
25
|
+
.join("");
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolves an operation type to its Italian label, falling back to the raw key.
|
|
30
|
+
*
|
|
31
|
+
* `has` is passed rather than the whole translator so this stays a pure function
|
|
32
|
+
* and both callers can share it regardless of how they obtained `t`.
|
|
33
|
+
*/
|
|
34
|
+
export const operationLabel = (
|
|
35
|
+
type: string,
|
|
36
|
+
translate: (key: string) => string,
|
|
37
|
+
has: (key: string) => boolean,
|
|
38
|
+
): string => {
|
|
39
|
+
const key = `${OPERATION_LABEL_NAMESPACE}.${toCamelCase(type)}`;
|
|
40
|
+
return has(key) ? translate(key) : type;
|
|
41
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart palette for the administrative token-usage dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Every value here is a documented step of the dataviz skill's reference ramps —
|
|
5
|
+
* nothing was eyeballed, nothing was hand-mixed. The palette was run through the
|
|
6
|
+
* skill's validator against THIS application's real chart surfaces (from
|
|
7
|
+
* `apps/web/src/app/globals.css`: light `oklch(1 0 0)` = `#ffffff`, dark
|
|
8
|
+
* `oklch(0.145 0 0)` = `#0a0a0a`) in both modes. The verbatim output is below.
|
|
9
|
+
*
|
|
10
|
+
* Light and dark are two SELECTED sets of steps, chosen for their own surface —
|
|
11
|
+
* never one set with its lightness flipped at runtime. Consumers pick a set from
|
|
12
|
+
* the resolved theme (`seriesColor(i, mode)` / `sequentialColor(i, mode)`).
|
|
13
|
+
*
|
|
14
|
+
* ---------------------------------------------------------------------------
|
|
15
|
+
* VALIDATOR OUTPUT — dataviz `scripts/validate_palette.js`, run 2026-08-07
|
|
16
|
+
* ---------------------------------------------------------------------------
|
|
17
|
+
*
|
|
18
|
+
* $ node scripts/validate_palette.js "#2a78d6,#eb6834,#1baf7a,#eda100,#e87ba4,#008300,#4a3aa7" --mode light --surface "#ffffff"
|
|
19
|
+
*
|
|
20
|
+
* Palette (light, surface #ffffff, categorical): 7 slots
|
|
21
|
+
* [PASS] Lightness band all 7 inside L 0.43–0.77
|
|
22
|
+
* [PASS] Chroma floor all 7 >= 0.1
|
|
23
|
+
* [PASS] CVD separation worst adjacent #eda100↔#1baf7a ΔE 9.1 (protan) · tritan 5.8
|
|
24
|
+
* [PASS] Normal-vision floor worst adjacent #e87ba4↔#eda100 ΔE 19.6 (normal)
|
|
25
|
+
* [WARN] Contrast vs surface below 3:1 — relief required (visible labels or table view): [["#1baf7a",2.82],["#eda100",2.17],["#e87ba4",2.69]]
|
|
26
|
+
*
|
|
27
|
+
* → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)
|
|
28
|
+
* scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.
|
|
29
|
+
*
|
|
30
|
+
* $ node scripts/validate_palette.js "#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9" --mode dark --surface "#0a0a0a"
|
|
31
|
+
*
|
|
32
|
+
* Palette (dark, surface #0a0a0a, categorical): 7 slots
|
|
33
|
+
* [PASS] Lightness band all 7 inside L 0.48–0.67
|
|
34
|
+
* [PASS] Chroma floor all 7 >= 0.1
|
|
35
|
+
* [PASS] CVD separation worst adjacent #c98500↔#199e70 ΔE 8.4 (protan) · tritan 8.7
|
|
36
|
+
* [PASS] Normal-vision floor worst adjacent #d55181↔#c98500 ΔE 19.3 (normal)
|
|
37
|
+
* [PASS] Contrast vs surface all 7 >= 3:1
|
|
38
|
+
*
|
|
39
|
+
* → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)
|
|
40
|
+
* scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.
|
|
41
|
+
*
|
|
42
|
+
* $ node scripts/validate_palette.js "#104281,#1c5cab,#2a78d6,#5598e7,#86b6ef" --mode light --surface "#ffffff" --ordinal
|
|
43
|
+
*
|
|
44
|
+
* Palette (light, surface #ffffff, ordinal ramp): 5 slots
|
|
45
|
+
* [PASS] Lightness monotone steps read light→dark
|
|
46
|
+
* [PASS] Adjacent ΔL all gaps >= 0.06
|
|
47
|
+
* [PASS] Light-end contrast #86b6ef at 2.11:1 vs surface
|
|
48
|
+
* [PASS] Single hue hue spread 3°
|
|
49
|
+
*
|
|
50
|
+
* → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)
|
|
51
|
+
*
|
|
52
|
+
* $ node scripts/validate_palette.js "#9ec5f4,#6da7ec,#3987e5,#256abf,#184f95" --mode dark --surface "#0a0a0a" --ordinal
|
|
53
|
+
*
|
|
54
|
+
* Palette (dark, surface #0a0a0a, ordinal ramp): 5 slots
|
|
55
|
+
* [PASS] Lightness monotone steps read light→dark
|
|
56
|
+
* [PASS] Adjacent ΔL all gaps >= 0.06
|
|
57
|
+
* [PASS] Light-end contrast #184f95 at 2.44:1 vs surface
|
|
58
|
+
* [PASS] Single hue hue spread 3°
|
|
59
|
+
*
|
|
60
|
+
* → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)
|
|
61
|
+
*
|
|
62
|
+
* OTHER_COLOR is achromatic, so the categorical checks (which gate hue identity)
|
|
63
|
+
* do not apply to it; it was gated on contrast alone with the validator's own
|
|
64
|
+
* `contrast()` export: 3.59:1 on `#ffffff`, 5.51:1 on `#0a0a0a` — both clear 3:1.
|
|
65
|
+
*
|
|
66
|
+
* ---------------------------------------------------------------------------
|
|
67
|
+
* WHAT THE RESULTS OBLIGE US TO DO
|
|
68
|
+
* ---------------------------------------------------------------------------
|
|
69
|
+
*
|
|
70
|
+
* - The light-mode contrast WARN is NOT dismissable. Three slots (aqua, yellow,
|
|
71
|
+
* magenta) sit below 3:1 on white, so every surface that paints with this
|
|
72
|
+
* palette MUST ship the relief channel: a visible legend, direct labels and a
|
|
73
|
+
* tooltip carrying the value in text. The timeline chart and the breakdown
|
|
74
|
+
* table both do.
|
|
75
|
+
* - CVD separation is measured on ADJACENT pairs, which is the correct pairlist
|
|
76
|
+
* for stacked bars, grouped bars and lines — the only forms this palette paints.
|
|
77
|
+
* A scatter / bubble / small-multiples chart would need `--pairs all`, which
|
|
78
|
+
* caps the usable slot count at three; do not reuse this array there without
|
|
79
|
+
* re-running the validator with that flag.
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/** Which surface the colours are being painted on. */
|
|
83
|
+
export type ChartMode = "light" | "dark";
|
|
84
|
+
|
|
85
|
+
/** The chart surfaces the palette was validated against (globals.css). */
|
|
86
|
+
export const CHART_SURFACE: Readonly<Record<ChartMode, string>> = {
|
|
87
|
+
light: "#ffffff",
|
|
88
|
+
dark: "#0a0a0a",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Categorical slots, in FIXED order. Slot n is series n — the order is the
|
|
93
|
+
* CVD-safety mechanism, not decoration, so it is never re-ordered and never
|
|
94
|
+
* cycled. The two-series default (customer / platform) takes slots 0 and 1.
|
|
95
|
+
*
|
|
96
|
+
* Hues, in order: blue, orange, aqua, yellow, magenta, green, violet.
|
|
97
|
+
*/
|
|
98
|
+
const CATEGORICAL_LIGHT: readonly string[] = [
|
|
99
|
+
"#2a78d6",
|
|
100
|
+
"#eb6834",
|
|
101
|
+
"#1baf7a",
|
|
102
|
+
"#eda100",
|
|
103
|
+
"#e87ba4",
|
|
104
|
+
"#008300",
|
|
105
|
+
"#4a3aa7",
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
/** The same seven hues, re-stepped for the dark surface. Not a lightness flip. */
|
|
109
|
+
const CATEGORICAL_DARK: readonly string[] = [
|
|
110
|
+
"#3987e5",
|
|
111
|
+
"#d95926",
|
|
112
|
+
"#199e70",
|
|
113
|
+
"#c98500",
|
|
114
|
+
"#d55181",
|
|
115
|
+
"#008300",
|
|
116
|
+
"#9085e9",
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The default (light-surface) categorical set.
|
|
121
|
+
*
|
|
122
|
+
* Prefer `seriesColor(index, mode)` — this array exists for consumers that only
|
|
123
|
+
* need the documented default order.
|
|
124
|
+
*/
|
|
125
|
+
export const CATEGORICAL: readonly string[] = CATEGORICAL_LIGHT;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* How many series may carry their own identity hue. An 8th series is NEVER a
|
|
129
|
+
* generated hue: it folds into `other`.
|
|
130
|
+
*/
|
|
131
|
+
export const CATEGORICAL_CEILING = CATEGORICAL_LIGHT.length;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The "other" rollup colour: deliberately achromatic so it reads as "not an
|
|
135
|
+
* identity" beside the seven hues. Same step in both modes — it clears 3:1 on
|
|
136
|
+
* both surfaces (3.59:1 light, 5.51:1 dark).
|
|
137
|
+
*/
|
|
138
|
+
export const OTHER_COLOR = "#898781";
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Sequential (single-hue, blue) ramp for MAGNITUDE, indexed by rank position:
|
|
142
|
+
* index 0 is rank 1 — the largest value — so the strongest step leads.
|
|
143
|
+
*
|
|
144
|
+
* On the light surface "strongest" is the darkest step; on the dark surface it
|
|
145
|
+
* is the lightest. The anchor flips with the surface, which is why there are two
|
|
146
|
+
* selected sets rather than one array reversed at runtime.
|
|
147
|
+
*
|
|
148
|
+
* Five steps, not eight: with the adjacent-ΔL ≥ 0.06 gate and the ≥ 2:1
|
|
149
|
+
* surface-contrast gate on the step nearest the surface, the blue ramp fits
|
|
150
|
+
* exactly five distinguishable steps. Consumers clamp past the end.
|
|
151
|
+
*/
|
|
152
|
+
const SEQUENTIAL_RAMP_LIGHT: readonly string[] = ["#104281", "#1c5cab", "#2a78d6", "#5598e7", "#86b6ef"];
|
|
153
|
+
|
|
154
|
+
const SEQUENTIAL_RAMP_DARK: readonly string[] = ["#9ec5f4", "#6da7ec", "#3987e5", "#256abf", "#184f95"];
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The default (light-surface) sequential ramp.
|
|
158
|
+
*
|
|
159
|
+
* Prefer `sequentialColor(index, mode)` — this array exists for consumers that
|
|
160
|
+
* only need the documented default.
|
|
161
|
+
*/
|
|
162
|
+
export const SEQUENTIAL_RAMP: readonly string[] = SEQUENTIAL_RAMP_LIGHT;
|
|
163
|
+
|
|
164
|
+
const categoricalSet = (mode: ChartMode): readonly string[] => (mode === "dark" ? CATEGORICAL_DARK : CATEGORICAL_LIGHT);
|
|
165
|
+
|
|
166
|
+
const sequentialSet = (mode: ChartMode): readonly string[] =>
|
|
167
|
+
mode === "dark" ? SEQUENTIAL_RAMP_DARK : SEQUENTIAL_RAMP_LIGHT;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The identity colour for series `index`.
|
|
171
|
+
*
|
|
172
|
+
* Never generates a hue and never cycles: anything at or past the ceiling gets
|
|
173
|
+
* `OTHER_COLOR`, because a series past the ceiling should already have been
|
|
174
|
+
* folded into the "other" bucket by the caller.
|
|
175
|
+
*/
|
|
176
|
+
export function seriesColor(index: number, mode: ChartMode = "light"): string {
|
|
177
|
+
if (index < 0 || index >= CATEGORICAL_CEILING) return OTHER_COLOR;
|
|
178
|
+
return categoricalSet(mode)[index];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Both mode steps for series `index`, shaped for a shadcn `ChartConfig` entry's
|
|
183
|
+
* `theme` field.
|
|
184
|
+
*/
|
|
185
|
+
export function seriesTheme(index: number): { light: string; dark: string } {
|
|
186
|
+
return { light: seriesColor(index, "light"), dark: seriesColor(index, "dark") };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The magnitude colour for rank position `index`, clamped to the last step so a
|
|
191
|
+
* long list keeps its weakest step rather than falling off the ramp.
|
|
192
|
+
*/
|
|
193
|
+
export function sequentialColor(index: number, mode: ChartMode = "light"): string {
|
|
194
|
+
const ramp = sequentialSet(mode);
|
|
195
|
+
const clamped = Math.min(Math.max(index, 0), ramp.length - 1);
|
|
196
|
+
return ramp[clamped];
|
|
197
|
+
}
|