@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.
Files changed (124) hide show
  1. package/dist/{BlockNoteEditor-PNAVMGHB.js → BlockNoteEditor-TIMGYKAA.js} +19 -19
  2. package/dist/{BlockNoteEditor-PNAVMGHB.js.map → BlockNoteEditor-TIMGYKAA.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-3PS3HYVI.mjs → BlockNoteEditor-YFJMXJEU.mjs} +4 -4
  4. package/dist/billing/index.d.mts +40 -1
  5. package/dist/billing/index.d.ts +40 -1
  6. package/dist/billing/index.js +519 -462
  7. package/dist/billing/index.js.map +1 -1
  8. package/dist/billing/index.mjs +298 -241
  9. package/dist/billing/index.mjs.map +1 -1
  10. package/dist/{chunk-QYUYQO2M.js → chunk-7HKJNUW7.js} +1144 -953
  11. package/dist/chunk-7HKJNUW7.js.map +1 -0
  12. package/dist/{chunk-MN2BUSEU.mjs → chunk-DJWNNV52.mjs} +2 -2
  13. package/dist/{chunk-EQEW64CH.mjs → chunk-FS4RVV3K.mjs} +242 -1
  14. package/dist/chunk-FS4RVV3K.mjs.map +1 -0
  15. package/dist/{chunk-FSMXD3N7.mjs → chunk-G4YS52SO.mjs} +2547 -2356
  16. package/dist/chunk-G4YS52SO.mjs.map +1 -0
  17. package/dist/{chunk-OHJQYVSI.js → chunk-OTEDZ6YW.js} +7 -7
  18. package/dist/{chunk-OHJQYVSI.js.map → chunk-OTEDZ6YW.js.map} +1 -1
  19. package/dist/{chunk-X5TPKMBS.js → chunk-RTOC53EZ.js} +247 -6
  20. package/dist/chunk-RTOC53EZ.js.map +1 -0
  21. package/dist/client/index.d.mts +1 -1
  22. package/dist/client/index.d.ts +1 -1
  23. package/dist/client/index.js +4 -4
  24. package/dist/client/index.mjs +3 -3
  25. package/dist/components/index.d.mts +137 -4
  26. package/dist/components/index.d.ts +137 -4
  27. package/dist/components/index.js +16 -4
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/components/index.mjs +15 -3
  30. package/dist/{tokenusage-admin.module-5wJ_tZTj.d.ts → config-BIjrg5wd.d.ts} +39 -1
  31. package/dist/{tokenusage-admin.module-DQVT4O4j.d.mts → config-BRUjtCd1.d.mts} +39 -1
  32. package/dist/{content.fields-xH3TGvVk.d.mts → content.fields-1AlHDtDb.d.mts} +1 -0
  33. package/dist/{content.fields-xH3TGvVk.d.ts → content.fields-1AlHDtDb.d.ts} +1 -0
  34. package/dist/contexts/index.d.mts +22 -1
  35. package/dist/contexts/index.d.ts +22 -1
  36. package/dist/contexts/index.js +8 -4
  37. package/dist/contexts/index.js.map +1 -1
  38. package/dist/contexts/index.mjs +7 -3
  39. package/dist/core/index.d.mts +6 -3
  40. package/dist/core/index.d.ts +6 -3
  41. package/dist/core/index.js +8 -2
  42. package/dist/core/index.js.map +1 -1
  43. package/dist/core/index.mjs +7 -1
  44. package/dist/features/help/index.js +37 -37
  45. package/dist/features/help/index.mjs +3 -3
  46. package/dist/features/tokenusage/index.d.mts +413 -14
  47. package/dist/features/tokenusage/index.d.ts +413 -14
  48. package/dist/features/tokenusage/index.js +481 -120
  49. package/dist/features/tokenusage/index.js.map +1 -1
  50. package/dist/features/tokenusage/index.mjs +431 -70
  51. package/dist/features/tokenusage/index.mjs.map +1 -1
  52. package/dist/index.d.mts +3 -3
  53. package/dist/index.d.ts +3 -3
  54. package/dist/index.js +9 -3
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +8 -2
  57. package/dist/server/index.js +3 -3
  58. package/dist/server/index.mjs +1 -1
  59. package/package.json +1 -1
  60. package/src/billing/index.ts +1 -0
  61. package/src/components/index.ts +1 -0
  62. package/src/contexts/index.ts +1 -0
  63. package/src/core/index.ts +10 -0
  64. package/src/core/registry/ModuleRegistry.ts +4 -0
  65. package/src/features/administration/components/AdminIndexContainer.tsx +150 -0
  66. package/src/features/administration/components/__tests__/AdminIndexContainer.spec.tsx +96 -0
  67. package/src/features/administration/components/index.ts +1 -0
  68. package/src/features/administration/contexts/AdministrationContext.tsx +47 -0
  69. package/src/features/administration/contexts/index.ts +1 -0
  70. package/src/features/administration/data/admin-section.types.ts +30 -0
  71. package/src/features/administration/i18n-keys.ts +26 -0
  72. package/src/features/administration/index.ts +3 -0
  73. package/src/features/billing/contexts/BillingContext.tsx +95 -0
  74. package/src/features/billing/contexts/index.ts +1 -0
  75. package/src/features/billing/index.ts +1 -0
  76. package/src/features/billing/stripe-product/components/containers/ProductsAdminContainer.tsx +39 -27
  77. package/src/features/billing/stripe-product/components/containers/ProductsAdminPageContainer.tsx +27 -0
  78. package/src/features/billing/stripe-product/components/containers/index.ts +1 -0
  79. package/src/features/company/components/containers/CompaniesListContainer.tsx +25 -0
  80. package/src/features/company/components/containers/index.ts +1 -0
  81. package/src/features/company/components/lists/CompaniesList.tsx +12 -1
  82. package/src/features/company/contexts/CompanyContext.tsx +13 -1
  83. package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +20 -4
  84. package/src/features/tokenusage/components/TokenUsageAdminTiles.tsx +31 -18
  85. package/src/features/tokenusage/components/TokenUsageBreakdownTable.tsx +13 -13
  86. package/src/features/tokenusage/components/TokenUsageRankedBar.tsx +24 -7
  87. package/src/features/tokenusage/components/TokenUsageReportContainer.tsx +102 -0
  88. package/src/features/tokenusage/components/TokenUsageReportFilterBar.tsx +34 -0
  89. package/src/features/tokenusage/components/TokenUsageReportTiles.tsx +161 -0
  90. package/src/features/tokenusage/components/TokenUsageTimelineChart.tsx +15 -13
  91. package/src/features/tokenusage/components/__tests__/TokenUsageAdminTiles.spec.tsx +4 -2
  92. package/src/features/tokenusage/components/__tests__/TokenUsageRankedBar.spec.tsx +3 -1
  93. package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +6 -3
  94. package/src/features/tokenusage/contexts/TokenUsageReportContext.tsx +199 -0
  95. package/src/features/tokenusage/data/TokenUsageReportService.ts +65 -0
  96. package/src/features/tokenusage/data/index.ts +9 -0
  97. package/src/features/tokenusage/data/tokenusage-report-breakdown.interface.ts +12 -0
  98. package/src/features/tokenusage/data/tokenusage-report-breakdown.ts +94 -0
  99. package/src/features/tokenusage/data/tokenusage-report-summary.interface.ts +12 -0
  100. package/src/features/tokenusage/data/tokenusage-report-summary.ts +87 -0
  101. package/src/features/tokenusage/data/tokenusage-report-timeline.interface.ts +13 -0
  102. package/src/features/tokenusage/data/tokenusage-report-timeline.ts +94 -0
  103. package/src/features/tokenusage/data/tokenusage-report.types.ts +51 -0
  104. package/src/features/tokenusage/i18n-keys.ts +28 -0
  105. package/src/features/tokenusage/index.ts +18 -0
  106. package/src/features/tokenusage/lib/config.ts +22 -0
  107. package/src/features/tokenusage/lib/formatters.ts +100 -0
  108. package/src/features/tokenusage/lib/metrics.ts +8 -26
  109. package/src/features/tokenusage/lib/palette.ts +41 -14
  110. package/src/features/tokenusage/tokenusage-admin.module.ts +4 -0
  111. package/src/features/tokenusage/tokenusage.modules.ts +40 -0
  112. package/src/features/user/components/containers/PlatformUsersContainer.tsx +23 -0
  113. package/src/features/user/components/containers/index.ts +1 -0
  114. package/src/features/user/components/lists/PlatformUsersList.tsx +45 -0
  115. package/src/features/user/components/lists/index.ts +1 -0
  116. package/src/features/user/data/user.fields.ts +1 -0
  117. package/src/features/user/hooks/useUserTableStructure.tsx +17 -0
  118. package/src/shadcnui/ui/chart.tsx +30 -3
  119. package/dist/chunk-EQEW64CH.mjs.map +0 -1
  120. package/dist/chunk-FSMXD3N7.mjs.map +0 -1
  121. package/dist/chunk-QYUYQO2M.js.map +0 -1
  122. package/dist/chunk-X5TPKMBS.js.map +0 -1
  123. /package/dist/{BlockNoteEditor-3PS3HYVI.mjs.map → BlockNoteEditor-YFJMXJEU.mjs.map} +0 -0
  124. /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 (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.
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-07
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 "#0a0a0a"
43
+ * $ node scripts/validate_palette.js "#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9" --mode dark --surface "#171717"
31
44
  *
32
- * Palette (dark, surface #0a0a0a, categorical): 7 slots
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 "#0a0a0a" --ordinal
65
+ * $ node scripts/validate_palette.js "#9ec5f4,#6da7ec,#3987e5,#256abf,#184f95" --mode dark --surface "#171717" --ordinal
53
66
  *
54
- * Palette (dark, surface #0a0a0a, ordinal ramp): 5 slots
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.44:1 vs surface
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`, 5.51:1 on `#0a0a0a` — both clear 3:1.
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
- /** The chart surfaces the palette was validated against (globals.css). */
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: "#0a0a0a",
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 light, 5.51:1 dark).
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
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./AllUsersListContainer";
2
+ export * from "./PlatformUsersContainer";
2
3
  export * from "./UserContainer";
3
4
  export * from "./UserIndexContainer";
4
5
  export * from "./UsersListContainer";
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./AdminUsersList";
2
2
  export * from "./CompanyUsersList";
3
+ export * from "./PlatformUsersList";
3
4
  export * from "./ContributorsList";
4
5
  export * from "./RelevantUsersList";
5
6
  export * from "./RoleUsersList";
@@ -2,6 +2,7 @@ export enum UserFields {
2
2
  userId = "userId",
3
3
  name = "name",
4
4
  email = "email",
5
+ company = "company",
5
6
  relevance = "relevance",
6
7
  createdAt = "createdAt",
7
8
  updatedAt = "updatedAt",
@@ -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
- "flex flex-1 justify-between leading-none",
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
- <span className="text-foreground font-medium tabular-nums">{item.value.toLocaleString()}</span>
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
  </>