@carlonicora/nextjs-jsonapi 2.3.0 → 2.5.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/{BlockNoteEditor-PNAVMGHB.js → BlockNoteEditor-TIMGYKAA.js} +19 -19
- package/dist/{BlockNoteEditor-PNAVMGHB.js.map → BlockNoteEditor-TIMGYKAA.js.map} +1 -1
- package/dist/{BlockNoteEditor-3PS3HYVI.mjs → BlockNoteEditor-YFJMXJEU.mjs} +4 -4
- package/dist/billing/index.d.mts +40 -1
- package/dist/billing/index.d.ts +40 -1
- package/dist/billing/index.js +519 -462
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +298 -241
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-QYUYQO2M.js → chunk-7HKJNUW7.js} +1144 -953
- package/dist/chunk-7HKJNUW7.js.map +1 -0
- package/dist/{chunk-MN2BUSEU.mjs → chunk-DJWNNV52.mjs} +2 -2
- package/dist/{chunk-EQEW64CH.mjs → chunk-FS4RVV3K.mjs} +242 -1
- package/dist/chunk-FS4RVV3K.mjs.map +1 -0
- package/dist/{chunk-FSMXD3N7.mjs → chunk-G4YS52SO.mjs} +2547 -2356
- package/dist/chunk-G4YS52SO.mjs.map +1 -0
- package/dist/{chunk-OHJQYVSI.js → chunk-OTEDZ6YW.js} +7 -7
- package/dist/{chunk-OHJQYVSI.js.map → chunk-OTEDZ6YW.js.map} +1 -1
- package/dist/{chunk-X5TPKMBS.js → chunk-RTOC53EZ.js} +247 -6
- package/dist/chunk-RTOC53EZ.js.map +1 -0
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +137 -4
- package/dist/components/index.d.ts +137 -4
- package/dist/components/index.js +16 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +15 -3
- package/dist/{tokenusage-admin.module-5wJ_tZTj.d.ts → config-BIjrg5wd.d.ts} +39 -1
- package/dist/{tokenusage-admin.module-DQVT4O4j.d.mts → config-BRUjtCd1.d.mts} +39 -1
- package/dist/{content.fields-xH3TGvVk.d.mts → content.fields-1AlHDtDb.d.mts} +1 -0
- package/dist/{content.fields-xH3TGvVk.d.ts → content.fields-1AlHDtDb.d.ts} +1 -0
- package/dist/contexts/index.d.mts +22 -1
- package/dist/contexts/index.d.ts +22 -1
- package/dist/contexts/index.js +8 -4
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +7 -3
- package/dist/core/index.d.mts +6 -3
- package/dist/core/index.d.ts +6 -3
- 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.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.d.mts +413 -14
- package/dist/features/tokenusage/index.d.ts +413 -14
- package/dist/features/tokenusage/index.js +481 -120
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +431 -70
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -2
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/src/billing/index.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/contexts/index.ts +1 -0
- package/src/core/index.ts +10 -0
- package/src/core/registry/ModuleRegistry.ts +4 -0
- package/src/features/administration/components/AdminIndexContainer.tsx +150 -0
- package/src/features/administration/components/__tests__/AdminIndexContainer.spec.tsx +96 -0
- package/src/features/administration/components/index.ts +1 -0
- package/src/features/administration/contexts/AdministrationContext.tsx +47 -0
- package/src/features/administration/contexts/index.ts +1 -0
- package/src/features/administration/data/admin-section.types.ts +30 -0
- package/src/features/administration/i18n-keys.ts +26 -0
- package/src/features/administration/index.ts +3 -0
- package/src/features/billing/contexts/BillingContext.tsx +95 -0
- package/src/features/billing/contexts/index.ts +1 -0
- package/src/features/billing/index.ts +1 -0
- package/src/features/billing/stripe-product/components/containers/ProductsAdminContainer.tsx +39 -27
- package/src/features/billing/stripe-product/components/containers/ProductsAdminPageContainer.tsx +27 -0
- package/src/features/billing/stripe-product/components/containers/index.ts +1 -0
- package/src/features/company/components/containers/CompaniesListContainer.tsx +25 -0
- package/src/features/company/components/containers/index.ts +1 -0
- package/src/features/company/components/lists/CompaniesList.tsx +12 -1
- package/src/features/company/contexts/CompanyContext.tsx +13 -1
- package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +20 -4
- package/src/features/tokenusage/components/TokenUsageAdminTiles.tsx +31 -18
- package/src/features/tokenusage/components/TokenUsageBreakdownTable.tsx +13 -13
- package/src/features/tokenusage/components/TokenUsageRankedBar.tsx +24 -7
- package/src/features/tokenusage/components/TokenUsageReportContainer.tsx +102 -0
- package/src/features/tokenusage/components/TokenUsageReportFilterBar.tsx +34 -0
- package/src/features/tokenusage/components/TokenUsageReportTiles.tsx +161 -0
- package/src/features/tokenusage/components/TokenUsageTimelineChart.tsx +15 -13
- package/src/features/tokenusage/components/__tests__/TokenUsageAdminTiles.spec.tsx +4 -2
- package/src/features/tokenusage/components/__tests__/TokenUsageRankedBar.spec.tsx +3 -1
- package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +6 -3
- package/src/features/tokenusage/contexts/TokenUsageReportContext.tsx +199 -0
- package/src/features/tokenusage/data/TokenUsageReportService.ts +65 -0
- package/src/features/tokenusage/data/index.ts +9 -0
- package/src/features/tokenusage/data/tokenusage-report-breakdown.interface.ts +12 -0
- package/src/features/tokenusage/data/tokenusage-report-breakdown.ts +94 -0
- package/src/features/tokenusage/data/tokenusage-report-summary.interface.ts +12 -0
- package/src/features/tokenusage/data/tokenusage-report-summary.ts +87 -0
- package/src/features/tokenusage/data/tokenusage-report-timeline.interface.ts +13 -0
- package/src/features/tokenusage/data/tokenusage-report-timeline.ts +94 -0
- package/src/features/tokenusage/data/tokenusage-report.types.ts +51 -0
- package/src/features/tokenusage/i18n-keys.ts +28 -0
- package/src/features/tokenusage/index.ts +18 -0
- package/src/features/tokenusage/lib/config.ts +22 -0
- package/src/features/tokenusage/lib/formatters.ts +100 -0
- package/src/features/tokenusage/lib/metrics.ts +8 -26
- package/src/features/tokenusage/lib/palette.ts +41 -14
- package/src/features/tokenusage/tokenusage-admin.module.ts +4 -0
- package/src/features/tokenusage/tokenusage.modules.ts +40 -0
- package/src/features/user/components/containers/PlatformUsersContainer.tsx +23 -0
- package/src/features/user/components/containers/index.ts +1 -0
- package/src/features/user/components/lists/PlatformUsersList.tsx +45 -0
- package/src/features/user/components/lists/index.ts +1 -0
- package/src/features/user/data/user.fields.ts +1 -0
- package/src/features/user/hooks/useUserTableStructure.tsx +17 -0
- package/src/shadcnui/ui/chart.tsx +30 -3
- package/dist/chunk-EQEW64CH.mjs.map +0 -1
- package/dist/chunk-FSMXD3N7.mjs.map +0 -1
- package/dist/chunk-QYUYQO2M.js.map +0 -1
- package/dist/chunk-X5TPKMBS.js.map +0 -1
- /package/dist/{BlockNoteEditor-3PS3HYVI.mjs.map → BlockNoteEditor-YFJMXJEU.mjs.map} +0 -0
- /package/dist/{chunk-MN2BUSEU.mjs.map → chunk-DJWNNV52.mjs.map} +0 -0
|
@@ -28,3 +28,21 @@ export {
|
|
|
28
28
|
} from "./tokenusage-admin.module";
|
|
29
29
|
|
|
30
30
|
export { TOKEN_USAGE_ADMIN_I18N_KEYS } from "./i18n-keys";
|
|
31
|
+
|
|
32
|
+
// Self-service ("report") token-usage dashboard — the company-scoped mirror of
|
|
33
|
+
// the administrative trio above, reusing its chart components verbatim.
|
|
34
|
+
|
|
35
|
+
export { TokenUsageReportProvider, useTokenUsageReport } from "./contexts/TokenUsageReportContext";
|
|
36
|
+
export type { TokenUsageReportContextType, TokenUsageReportFilterState } from "./contexts/TokenUsageReportContext";
|
|
37
|
+
|
|
38
|
+
export { TokenUsageReportContainer } from "./components/TokenUsageReportContainer";
|
|
39
|
+
export { TokenUsageReportFilterBar } from "./components/TokenUsageReportFilterBar";
|
|
40
|
+
export { TokenUsageReportTiles } from "./components/TokenUsageReportTiles";
|
|
41
|
+
export type { TokenUsageBalances } from "./components/TokenUsageReportTiles";
|
|
42
|
+
|
|
43
|
+
export { tokenUsageModules } from "./tokenusage.modules";
|
|
44
|
+
export { configureTokenUsage, getTokenUsageCurrency } from "./lib/config";
|
|
45
|
+
export { createUsageFormatters, useUsageFormatters } from "./lib/formatters";
|
|
46
|
+
export type { UsageFormatters } from "./lib/formatters";
|
|
47
|
+
|
|
48
|
+
export { TOKEN_USAGE_REPORT_I18N_KEYS } from "./i18n-keys";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The currency the token-usage surfaces render monetary values in.
|
|
3
|
+
*
|
|
4
|
+
* The package cannot infer this: the figures come from whatever per-million-token
|
|
5
|
+
* rates the consuming app configured, and those are denominated in whatever
|
|
6
|
+
* currency that app buys inference in — EUR for one consumer, USD for another
|
|
7
|
+
* whose rates are OpenRouter list prices. So the app declares it once at
|
|
8
|
+
* bootstrap, next to configureJsonApi().
|
|
9
|
+
*
|
|
10
|
+
* EUR is the default purely for backward compatibility: the previous
|
|
11
|
+
* implementation hard-coded a euro sign, so an app that never calls this keeps
|
|
12
|
+
* rendering exactly what it rendered before.
|
|
13
|
+
*/
|
|
14
|
+
let currency = "EUR";
|
|
15
|
+
|
|
16
|
+
export function configureTokenUsage(config: { currency: string }): void {
|
|
17
|
+
currency = config.currency;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getTokenUsageCurrency(): string {
|
|
21
|
+
return currency;
|
|
22
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useLocale } from "next-intl";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
import type { Metric } from "../data/tokenusage-admin.types";
|
|
6
|
+
import { getTokenUsageCurrency } from "./config";
|
|
7
|
+
|
|
8
|
+
export type UsageFormatters = {
|
|
9
|
+
/** Locale-aware fixed-decimal number, e.g. 9.46 → "9,46" in it-IT. */
|
|
10
|
+
decimal(value: number, decimals: number): string;
|
|
11
|
+
/** Currency for cost, 2 decimals for credits, whole numbers for tokens. */
|
|
12
|
+
metricValue(value: number, metric: Metric): string;
|
|
13
|
+
/** Currency with an explicit decimal count — the per-call tile needs 4. */
|
|
14
|
+
currency(value: number, decimals: number): string;
|
|
15
|
+
/** Percentage with one decimal by default, e.g. 60 → "60,0 %" in it-IT. */
|
|
16
|
+
percent(value: number, decimals?: number): string;
|
|
17
|
+
/** Compact notation for axis ticks, e.g. 1500000 → "1,5 Mln" in it-IT. */
|
|
18
|
+
compact(value: number): string;
|
|
19
|
+
/** A "YYYY-MM-DD" bucket key rendered as an axis or tooltip label, in UTC. */
|
|
20
|
+
bucketDate(iso: string, granularity: "day" | "week" | "month"): string;
|
|
21
|
+
/** Locale collation for client-side table sorting. */
|
|
22
|
+
compare(a: string, b: string): number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds every formatter the token-usage surfaces need, for one locale and one
|
|
27
|
+
* currency.
|
|
28
|
+
*
|
|
29
|
+
* A pure factory on purpose: it takes no React context, so the arithmetic and
|
|
30
|
+
* the formatting are unit-testable without rendering, and a server component or
|
|
31
|
+
* a test can construct a set for an arbitrary locale.
|
|
32
|
+
*
|
|
33
|
+
* The currency symbol is placed with a space rather than through
|
|
34
|
+
* `style: "currency"`, which is what the previous hard-coded implementation
|
|
35
|
+
* rendered ("€ 9,46"). Keeping that shape means a consuming app on EUR sees
|
|
36
|
+
* byte-identical output after this refactor.
|
|
37
|
+
*
|
|
38
|
+
* `bucketDate` formats in UTC, always. The wire value is a `type: "date"` — a
|
|
39
|
+
* calendar day — parsed to UTC midnight; reading it back with local getters
|
|
40
|
+
* would shift the label a day early west of UTC.
|
|
41
|
+
*/
|
|
42
|
+
export function createUsageFormatters(locale: string, currency: string): UsageFormatters {
|
|
43
|
+
const symbol =
|
|
44
|
+
new Intl.NumberFormat(locale, { style: "currency", currency, currencyDisplay: "narrowSymbol" })
|
|
45
|
+
.formatToParts(0)
|
|
46
|
+
.find((part) => part.type === "currency")?.value ?? currency;
|
|
47
|
+
|
|
48
|
+
const decimal = (value: number, decimals: number): string =>
|
|
49
|
+
value.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
|
50
|
+
|
|
51
|
+
const dayFormat = new Intl.DateTimeFormat(locale, { day: "numeric", month: "short", timeZone: "UTC" });
|
|
52
|
+
const monthFormat = new Intl.DateTimeFormat(locale, { month: "short", year: "numeric", timeZone: "UTC" });
|
|
53
|
+
const compactFormat = new Intl.NumberFormat(locale, { notation: "compact", maximumFractionDigits: 1 });
|
|
54
|
+
const collator = new Intl.Collator(locale);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
decimal,
|
|
58
|
+
|
|
59
|
+
metricValue(value, metric) {
|
|
60
|
+
if (metric === "cost") return `${symbol} ${decimal(value, 2)}`;
|
|
61
|
+
// Credits are stored to 4 decimals (round4(cost / creditCost)), but that
|
|
62
|
+
// precision is noise to a reader comparing rows: one decimal is enough to
|
|
63
|
+
// separate two values and keeps the columns narrow enough to scan.
|
|
64
|
+
if (metric === "credits") return decimal(value, 1);
|
|
65
|
+
return decimal(value, 0);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
currency(value, decimals) {
|
|
69
|
+
return `${symbol} ${decimal(value, decimals)}`;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
percent(value, decimals = 1) {
|
|
73
|
+
return `${decimal(value, decimals)} %`;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
compact(value) {
|
|
77
|
+
return compactFormat.format(value);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
bucketDate(iso, granularity) {
|
|
81
|
+
const date = new Date(`${iso}T00:00:00.000Z`);
|
|
82
|
+
return granularity === "month" ? monthFormat.format(date) : dayFormat.format(date);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
compare(a, b) {
|
|
86
|
+
return collator.compare(a, b);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The formatter set for the current request's locale and the app's configured
|
|
93
|
+
* currency. Memoised on both, so the Intl objects are built once per locale
|
|
94
|
+
* rather than on every render.
|
|
95
|
+
*/
|
|
96
|
+
export function useUsageFormatters(): UsageFormatters {
|
|
97
|
+
const locale = useLocale();
|
|
98
|
+
const currency = getTokenUsageCurrency();
|
|
99
|
+
return useMemo(() => createUsageFormatters(locale, currency), [locale, currency]);
|
|
100
|
+
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { Metric } from "../data/tokenusage-admin.types";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Locale-free arithmetic for the token-usage surfaces.
|
|
5
|
+
*
|
|
6
|
+
* Everything that turns a number into a STRING lives in `./formatters` instead,
|
|
7
|
+
* because it depends on the request's locale and the app's configured currency.
|
|
8
|
+
* This file stays pure arithmetic so it needs neither.
|
|
9
|
+
*/
|
|
10
|
+
|
|
3
11
|
/**
|
|
4
12
|
* The metric field set every admin token-usage resource carries. Declared
|
|
5
13
|
* structurally so it accepts the summary, timeline and breakdown interfaces
|
|
@@ -14,9 +22,6 @@ export type TokenUsageMetrics = {
|
|
|
14
22
|
calls: number;
|
|
15
23
|
};
|
|
16
24
|
|
|
17
|
-
/** UI copy is Italian, so every figure is formatted with the Italian locale. */
|
|
18
|
-
const LOCALE = "it-IT";
|
|
19
|
-
|
|
20
25
|
/** Reads the single number a row contributes for the currently selected metric. */
|
|
21
26
|
export function metricValue(row: TokenUsageMetrics, metric: Metric): number {
|
|
22
27
|
if (metric === "cost") return row.cost;
|
|
@@ -24,29 +29,6 @@ export function metricValue(row: TokenUsageMetrics, metric: Metric): number {
|
|
|
24
29
|
return row.tokensIn + row.tokensOut;
|
|
25
30
|
}
|
|
26
31
|
|
|
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
32
|
/**
|
|
51
33
|
* Share of cached input tokens. Returns 0 rather than NaN when the window holds
|
|
52
34
|
* no input tokens at all, so the tile renders "0,0 %" instead of "NaN %".
|
|
@@ -3,16 +3,29 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Every value here is a documented step of the dataviz skill's reference ramps —
|
|
5
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* skill's validator against THIS application's real chart surfaces in both modes.
|
|
7
|
+
* The verbatim output is below.
|
|
8
|
+
*
|
|
9
|
+
* The surface is `--card`, NOT `--background`. Every chart that uses this palette
|
|
10
|
+
* is painted inside a `<Card>`, so the card is the surface the marks actually sit
|
|
11
|
+
* on — and in dark mode it is the *lighter* of the two (`oklch(0.205 0 0)` vs the
|
|
12
|
+
* background's `oklch(0.145 0 0)`), which is the harder case for contrast. Taken
|
|
13
|
+
* from `apps/web/src/app/globals.css` (narr8 declares two `:root`/`.dark` pairs;
|
|
14
|
+
* the LATER pair wins, and it is the one read here) — light `oklch(1 0 0)`, dark
|
|
15
|
+
* `oklch(0.205 0 0)`. a360ai declares the same two values.
|
|
16
|
+
*
|
|
17
|
+
* oklch→hex conversion: the algebraic inverse of the Björn Ottosson OKLab
|
|
18
|
+
* matrices — the same matrices the validator applies in the forward direction —
|
|
19
|
+
* giving light `#ffffff` and dark `#171717`. Cross-checked by feeding it the
|
|
20
|
+
* previously recorded background value, which it reproduces exactly:
|
|
21
|
+
* `oklch(0.145 0 0)` → `#0a0a0a`. ✓
|
|
9
22
|
*
|
|
10
23
|
* Light and dark are two SELECTED sets of steps, chosen for their own surface —
|
|
11
24
|
* never one set with its lightness flipped at runtime. Consumers pick a set from
|
|
12
25
|
* the resolved theme (`seriesColor(i, mode)` / `sequentialColor(i, mode)`).
|
|
13
26
|
*
|
|
14
27
|
* ---------------------------------------------------------------------------
|
|
15
|
-
* VALIDATOR OUTPUT — dataviz `scripts/validate_palette.js`, run 2026-08-
|
|
28
|
+
* VALIDATOR OUTPUT — dataviz `scripts/validate_palette.js`, run 2026-08-15
|
|
16
29
|
* ---------------------------------------------------------------------------
|
|
17
30
|
*
|
|
18
31
|
* $ node scripts/validate_palette.js "#2a78d6,#eb6834,#1baf7a,#eda100,#e87ba4,#008300,#4a3aa7" --mode light --surface "#ffffff"
|
|
@@ -27,9 +40,9 @@
|
|
|
27
40
|
* → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)
|
|
28
41
|
* scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.
|
|
29
42
|
*
|
|
30
|
-
* $ node scripts/validate_palette.js "#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9" --mode dark --surface "#
|
|
43
|
+
* $ node scripts/validate_palette.js "#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9" --mode dark --surface "#171717"
|
|
31
44
|
*
|
|
32
|
-
* Palette (dark, surface #
|
|
45
|
+
* Palette (dark, surface #171717, categorical): 7 slots
|
|
33
46
|
* [PASS] Lightness band all 7 inside L 0.48–0.67
|
|
34
47
|
* [PASS] Chroma floor all 7 >= 0.1
|
|
35
48
|
* [PASS] CVD separation worst adjacent #c98500↔#199e70 ΔE 8.4 (protan) · tritan 8.7
|
|
@@ -49,29 +62,39 @@
|
|
|
49
62
|
*
|
|
50
63
|
* → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)
|
|
51
64
|
*
|
|
52
|
-
* $ node scripts/validate_palette.js "#9ec5f4,#6da7ec,#3987e5,#256abf,#184f95" --mode dark --surface "#
|
|
65
|
+
* $ node scripts/validate_palette.js "#9ec5f4,#6da7ec,#3987e5,#256abf,#184f95" --mode dark --surface "#171717" --ordinal
|
|
53
66
|
*
|
|
54
|
-
* Palette (dark, surface #
|
|
67
|
+
* Palette (dark, surface #171717, ordinal ramp): 5 slots
|
|
55
68
|
* [PASS] Lightness monotone steps read light→dark
|
|
56
69
|
* [PASS] Adjacent ΔL all gaps >= 0.06
|
|
57
|
-
* [PASS] Light-end contrast #184f95 at 2.
|
|
70
|
+
* [PASS] Light-end contrast #184f95 at 2.21:1 vs surface
|
|
58
71
|
* [PASS] Single hue hue spread 3°
|
|
59
72
|
*
|
|
60
73
|
* → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)
|
|
61
74
|
*
|
|
62
75
|
* OTHER_COLOR is achromatic, so the categorical checks (which gate hue identity)
|
|
63
76
|
* do not apply to it; it was gated on contrast alone with the validator's own
|
|
64
|
-
* `contrast()` export: 3.59:1 on `#ffffff`,
|
|
77
|
+
* `contrast()` export: 3.59:1 on `#ffffff`, 4.99:1 on `#171717` — both clear 3:1.
|
|
65
78
|
*
|
|
66
79
|
* ---------------------------------------------------------------------------
|
|
67
80
|
* WHAT THE RESULTS OBLIGE US TO DO
|
|
68
81
|
* ---------------------------------------------------------------------------
|
|
69
82
|
*
|
|
83
|
+
* - NO COLOUR VALUE CHANGED in the move from `--background` to `--card`. Every
|
|
84
|
+
* gate that passed on the background still passes on the card, so re-stepping
|
|
85
|
+
* a hue would have been churn, not a fix. Only the recorded surface, the
|
|
86
|
+
* recorded numbers and this prose changed.
|
|
87
|
+
* - The dark set still clears 3:1 on the LIGHTER card surface: the worst slot is
|
|
88
|
+
* green `#008300` at 3.63:1, and the ordinal ramp's surface-nearest step lands
|
|
89
|
+
* at 2.21:1 (down from 2.44:1 on the background) — still above the 2:1 gate.
|
|
90
|
+
* That headroom is thin, so a future card-surface lightening MUST re-run these
|
|
91
|
+
* four commands rather than assume the set still holds.
|
|
70
92
|
* - The light-mode contrast WARN is NOT dismissable. Three slots (aqua, yellow,
|
|
71
93
|
* magenta) sit below 3:1 on white, so every surface that paints with this
|
|
72
94
|
* palette MUST ship the relief channel: a visible legend, direct labels and a
|
|
73
95
|
* tooltip carrying the value in text. The timeline chart and the breakdown
|
|
74
|
-
* table both do.
|
|
96
|
+
* table both do. The light card is `#ffffff`, identical to the light
|
|
97
|
+
* background, so this WARN is unchanged rather than newly incurred.
|
|
75
98
|
* - CVD separation is measured on ADJACENT pairs, which is the correct pairlist
|
|
76
99
|
* for stacked bars, grouped bars and lines — the only forms this palette paints.
|
|
77
100
|
* A scatter / bubble / small-multiples chart would need `--pairs all`, which
|
|
@@ -82,10 +105,14 @@
|
|
|
82
105
|
/** Which surface the colours are being painted on. */
|
|
83
106
|
export type ChartMode = "light" | "dark";
|
|
84
107
|
|
|
85
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* The chart surfaces the palette was validated against (globals.css `--card`).
|
|
110
|
+
*
|
|
111
|
+
* `--card`, never `--background`: the charts are painted inside a `<Card>`.
|
|
112
|
+
*/
|
|
86
113
|
export const CHART_SURFACE: Readonly<Record<ChartMode, string>> = {
|
|
87
114
|
light: "#ffffff",
|
|
88
|
-
dark: "#
|
|
115
|
+
dark: "#171717",
|
|
89
116
|
};
|
|
90
117
|
|
|
91
118
|
/**
|
|
@@ -133,7 +160,7 @@ export const CATEGORICAL_CEILING = CATEGORICAL_LIGHT.length;
|
|
|
133
160
|
/**
|
|
134
161
|
* The "other" rollup colour: deliberately achromatic so it reads as "not an
|
|
135
162
|
* identity" beside the seven hues. Same step in both modes — it clears 3:1 on
|
|
136
|
-
* both surfaces (3.59:1
|
|
163
|
+
* both card surfaces (3.59:1 on `#ffffff`, 4.99:1 on `#171717`).
|
|
137
164
|
*/
|
|
138
165
|
export const OTHER_COLOR = "#898781";
|
|
139
166
|
|
|
@@ -11,6 +11,10 @@ import { TokenUsageAdminTimeline } from "./data/tokenusage-admin-timeline";
|
|
|
11
11
|
* Distinct from the consuming app's own TokenUsage module (name "tokenusages",
|
|
12
12
|
* pageUrl "/tokenusage") — that one is an AppModuleDefinitions entry the package
|
|
13
13
|
* cannot reference, which is why these three exist.
|
|
14
|
+
*
|
|
15
|
+
* PREFER `tokenUsageModules(factory)` from `./tokenusage.modules` — it returns
|
|
16
|
+
* these three plus the three self-service report modules as one spreadable
|
|
17
|
+
* object. These individual factories are kept for backward compatibility.
|
|
14
18
|
*/
|
|
15
19
|
export const TokenUsageAdminSummaryModule = (factory: ModuleFactory) =>
|
|
16
20
|
factory({ name: "tokenusages/administration/summary", model: TokenUsageAdminSummary });
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ModuleFactory, ModuleWithPermissions } from "../../permissions";
|
|
2
|
+
import { TokenUsageAdminBreakdown } from "./data/tokenusage-admin-breakdown";
|
|
3
|
+
import { TokenUsageAdminSummary } from "./data/tokenusage-admin-summary";
|
|
4
|
+
import { TokenUsageAdminTimeline } from "./data/tokenusage-admin-timeline";
|
|
5
|
+
import { TokenUsageReportBreakdown } from "./data/tokenusage-report-breakdown";
|
|
6
|
+
import { TokenUsageReportSummary } from "./data/tokenusage-report-summary";
|
|
7
|
+
import { TokenUsageReportTimeline } from "./data/tokenusage-report-timeline";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Every read-only reporting resource behind the two token-usage pages, as one
|
|
11
|
+
* object a consuming app spreads into its `allModules`.
|
|
12
|
+
*
|
|
13
|
+
* A bundle rather than six separate factories because registration is the APP'S
|
|
14
|
+
* job — `allModules` is also the source of `AllModuleDefinitions` and what
|
|
15
|
+
* `DataClassRegistry.bootstrap()` reads, so the package cannot self-register.
|
|
16
|
+
* Six names is six chances to forget one, and a forgotten name is not a compile
|
|
17
|
+
* error: `FoundationModuleDefinitions` declares them all, so `Modules.X`
|
|
18
|
+
* typechecks and is `undefined` at runtime. One spread cannot be partially done.
|
|
19
|
+
*
|
|
20
|
+
* `name` is the endpoint path EndpointCreator builds URLs from. None has a
|
|
21
|
+
* pageUrl — these are not navigable resources.
|
|
22
|
+
*
|
|
23
|
+
* The return type is the six named keys, NOT `Record<string,
|
|
24
|
+
* ModuleWithPermissions>`: the consuming app derives `AllModuleDefinitions`
|
|
25
|
+
* from `typeof allModules`, so spreading an index-signature type in would give
|
|
26
|
+
* that object a string index signature and make every `Modules.<anything>`
|
|
27
|
+
* lookup typecheck. `satisfies` keeps the constraint without widening.
|
|
28
|
+
*/
|
|
29
|
+
export const tokenUsageModules = (factory: ModuleFactory) =>
|
|
30
|
+
({
|
|
31
|
+
TokenUsageAdminSummary: factory({ name: "tokenusages/administration/summary", model: TokenUsageAdminSummary }),
|
|
32
|
+
TokenUsageAdminTimeline: factory({ name: "tokenusages/administration/timeline", model: TokenUsageAdminTimeline }),
|
|
33
|
+
TokenUsageAdminBreakdown: factory({
|
|
34
|
+
name: "tokenusages/administration/breakdown",
|
|
35
|
+
model: TokenUsageAdminBreakdown,
|
|
36
|
+
}),
|
|
37
|
+
TokenUsageReportSummary: factory({ name: "tokenusages/reports/summary", model: TokenUsageReportSummary }),
|
|
38
|
+
TokenUsageReportTimeline: factory({ name: "tokenusages/reports/timeline", model: TokenUsageReportTimeline }),
|
|
39
|
+
TokenUsageReportBreakdown: factory({ name: "tokenusages/reports/breakdown", model: TokenUsageReportBreakdown }),
|
|
40
|
+
}) satisfies Record<string, ModuleWithPermissions>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { RoundPageContainer } from "../../../../components";
|
|
4
|
+
import { Modules } from "../../../../core";
|
|
5
|
+
import { UserProvider } from "../../contexts/UserContext";
|
|
6
|
+
import { PlatformUsersList } from "../lists/PlatformUsersList";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Page container for the platform-wide (cross-company) user list.
|
|
10
|
+
*
|
|
11
|
+
* Client component by necessity: `module={Modules.User}` is a registry entry
|
|
12
|
+
* with an icon component and methods, which a Server Component cannot pass
|
|
13
|
+
* across the boundary.
|
|
14
|
+
*/
|
|
15
|
+
export function PlatformUsersContainer() {
|
|
16
|
+
return (
|
|
17
|
+
<UserProvider>
|
|
18
|
+
<RoundPageContainer module={Modules.User} fullWidth>
|
|
19
|
+
<PlatformUsersList fullWidth />
|
|
20
|
+
</RoundPageContainer>
|
|
21
|
+
</UserProvider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { ContentListTable } from "../../../../components";
|
|
5
|
+
import { Modules } from "../../../../core";
|
|
6
|
+
import { DataListRetriever, useDataListRetriever } from "../../../../hooks";
|
|
7
|
+
import { UserFields, UserInterface } from "../../data";
|
|
8
|
+
import { UserService } from "../../data/user.service";
|
|
9
|
+
|
|
10
|
+
type PlatformUsersListProps = {
|
|
11
|
+
includeDeleted?: boolean;
|
|
12
|
+
fullWidth?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Every user on the platform, across all companies.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately NOT company-scoped: `CompanyUsersList` and `AdminUsersList` both
|
|
19
|
+
* build `/companies/{id}/users` and need a company in context, so they render
|
|
20
|
+
* nothing for a system administrator — who has no Company. This one calls
|
|
21
|
+
* `UserService.findMany`, which hits `GET /users`; that controller widens the
|
|
22
|
+
* result set itself when the caller holds Administrator or CompanyAdministrator
|
|
23
|
+
* (see the backend user controller's `isAdmin` flag), so the same component is
|
|
24
|
+
* safe for both.
|
|
25
|
+
*/
|
|
26
|
+
export function PlatformUsersList({ includeDeleted, fullWidth }: PlatformUsersListProps) {
|
|
27
|
+
const t = useTranslations();
|
|
28
|
+
|
|
29
|
+
const data: DataListRetriever<UserInterface> = useDataListRetriever({
|
|
30
|
+
retriever: (params) => UserService.findMany(params),
|
|
31
|
+
retrieverParams: { includeDeleted: includeDeleted },
|
|
32
|
+
module: Modules.User,
|
|
33
|
+
}) as DataListRetriever<UserInterface>;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ContentListTable
|
|
37
|
+
data={data}
|
|
38
|
+
fields={[UserFields.name, UserFields.email, UserFields.company]}
|
|
39
|
+
tableGeneratorType={Modules.User}
|
|
40
|
+
fullWidth={fullWidth}
|
|
41
|
+
allowSearch
|
|
42
|
+
title={t(`entities.users`, { count: 2 })}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -68,6 +68,23 @@ export const useUserTableStructure: UseTableStructureHook<UserInterface, UserFie
|
|
|
68
68
|
enableSorting: false,
|
|
69
69
|
enableHiding: false,
|
|
70
70
|
}),
|
|
71
|
+
// Only meaningful on a platform-wide list (PlatformUsersList), where users
|
|
72
|
+
// from every company sit in one table; a company-scoped list would repeat
|
|
73
|
+
// the same value on every row.
|
|
74
|
+
[UserFields.company]: () => ({
|
|
75
|
+
id: "company",
|
|
76
|
+
accessorKey: "company",
|
|
77
|
+
header: t(`entities.companies`, { count: 1 }),
|
|
78
|
+
cell: ({ row }: { row: Row<TableContent<UserInterface>> }) => {
|
|
79
|
+
const company = (row.original.jsonApiData as UserInterface).company;
|
|
80
|
+
|
|
81
|
+
if (!company) return <span className="text-muted-foreground text-xs">—</span>;
|
|
82
|
+
|
|
83
|
+
return <Link href={generateUrl({ page: Modules.Company, id: company.id })}>{company.name}</Link>;
|
|
84
|
+
},
|
|
85
|
+
enableSorting: false,
|
|
86
|
+
enableHiding: false,
|
|
87
|
+
}),
|
|
71
88
|
[UserFields.relevance]: () => ({
|
|
72
89
|
id: "relevance",
|
|
73
90
|
accessorKey: "relevance",
|
|
@@ -115,6 +115,8 @@ function ChartTooltipContent({
|
|
|
115
115
|
labelFormatter,
|
|
116
116
|
labelClassName,
|
|
117
117
|
formatter,
|
|
118
|
+
valueFormatter,
|
|
119
|
+
hideZeroValues = false,
|
|
118
120
|
color,
|
|
119
121
|
nameKey,
|
|
120
122
|
labelKey,
|
|
@@ -124,6 +126,21 @@ function ChartTooltipContent({
|
|
|
124
126
|
label?: string | number;
|
|
125
127
|
labelFormatter?: (label: unknown, payload: PayloadItem[]) => React.ReactNode;
|
|
126
128
|
formatter?: (value: unknown, name: string, item: PayloadItem, index: number, payload: unknown) => React.ReactNode;
|
|
129
|
+
/**
|
|
130
|
+
* Formats the VALUE half of a row while keeping the default layout — colour
|
|
131
|
+
* swatch, series name, alignment. `formatter` replaces the whole row, so it
|
|
132
|
+
* is the wrong tool when all a chart needs is its own number formatting.
|
|
133
|
+
* Defaults to `toLocaleString()`, which is what every existing caller gets.
|
|
134
|
+
*/
|
|
135
|
+
valueFormatter?: (value: number | string) => React.ReactNode;
|
|
136
|
+
/**
|
|
137
|
+
* Drops rows whose value is zero. On a stacked chart every series appears in
|
|
138
|
+
* every bucket's payload, so a bucket with one active series still lists all
|
|
139
|
+
* of them — the reader has to scan a column of zeros to find the one number
|
|
140
|
+
* that matters. Off by default: a chart comparing a fixed set of series may
|
|
141
|
+
* want the zeros visible.
|
|
142
|
+
*/
|
|
143
|
+
hideZeroValues?: boolean;
|
|
127
144
|
hideLabel?: boolean;
|
|
128
145
|
hideIndicator?: boolean;
|
|
129
146
|
indicator?: "line" | "dot" | "dashed";
|
|
@@ -173,6 +190,7 @@ function ChartTooltipContent({
|
|
|
173
190
|
<div className="grid gap-1.5">
|
|
174
191
|
{payload
|
|
175
192
|
.filter((item) => item.type !== "none")
|
|
193
|
+
.filter((item) => !hideZeroValues || Number(item.value) !== 0)
|
|
176
194
|
.map((item, index) => {
|
|
177
195
|
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
|
178
196
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
@@ -212,7 +230,11 @@ function ChartTooltipContent({
|
|
|
212
230
|
)}
|
|
213
231
|
<div
|
|
214
232
|
className={cn(
|
|
215
|
-
|
|
233
|
+
// gap-4 is load-bearing: without it a long series name
|
|
234
|
+
// runs straight into its value ("Decision suggestions0")
|
|
235
|
+
// because justify-between leaves no room once the row
|
|
236
|
+
// fills. The gap also widens the tooltip to fit.
|
|
237
|
+
"flex flex-1 justify-between gap-4 leading-none",
|
|
216
238
|
nestLabel ? "items-end" : "items-center",
|
|
217
239
|
)}
|
|
218
240
|
>
|
|
@@ -220,8 +242,13 @@ function ChartTooltipContent({
|
|
|
220
242
|
{nestLabel ? tooltipLabel : null}
|
|
221
243
|
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
|
|
222
244
|
</div>
|
|
223
|
-
{item.value && (
|
|
224
|
-
|
|
245
|
+
{item.value !== undefined && item.value !== null && (
|
|
246
|
+
// Tested against undefined/null rather than truthiness:
|
|
247
|
+
// a value of exactly 0 is falsy, and the old guard
|
|
248
|
+
// dropped the number while still drawing its row.
|
|
249
|
+
<span className="text-foreground font-medium tabular-nums">
|
|
250
|
+
{valueFormatter ? valueFormatter(item.value) : item.value.toLocaleString()}
|
|
251
|
+
</span>
|
|
225
252
|
)}
|
|
226
253
|
</div>
|
|
227
254
|
</>
|