@godxjp/ui 28.13.0 → 30.0.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 (164) hide show
  1. package/agent/START-HERE.md +29 -10
  2. package/agent/components/Anchor.json +6 -1
  3. package/agent/components/AppShell.json +1 -1
  4. package/agent/components/AreaChart.json +19 -1
  5. package/agent/components/BarChart.json +11 -1
  6. package/agent/components/Cascader.json +1 -1
  7. package/agent/components/CompactBarTrend.json +1 -1
  8. package/agent/components/DataState.json +1 -1
  9. package/agent/components/DataTable.json +4 -4
  10. package/agent/components/FormField.json +1 -1
  11. package/agent/components/InfiniteQueryState.json +1 -1
  12. package/agent/components/Input.json +1 -1
  13. package/agent/components/LineChart.json +20 -2
  14. package/agent/components/ListRow.json +1 -1
  15. package/agent/components/Masonry.json +1 -1
  16. package/agent/components/MasterDetail.json +1 -1
  17. package/agent/components/PasswordStrength.json +1 -1
  18. package/agent/components/PermissionMatrix.json +1 -1
  19. package/agent/components/Select.json +1 -0
  20. package/agent/components/Sidebar.json +1 -1
  21. package/agent/components/Table.json +8 -3
  22. package/agent/components/ThemeScope.json +49 -0
  23. package/agent/components/Topbar.json +1 -0
  24. package/agent/components/TopbarItem.json +2 -1
  25. package/agent/components/Transfer.json +1 -1
  26. package/agent/components/TreeSelect.json +1 -1
  27. package/agent/components/UploadCropDialog.json +1 -1
  28. package/agent/components/formatDate.json +1 -1
  29. package/agent/components-index.json +5 -0
  30. package/agent/components.json +138 -30
  31. package/agent/index.json +19 -9
  32. package/agent/llms.txt +10 -10
  33. package/agent/patterns/tenant-brand-color.json +28 -0
  34. package/agent/patterns-index.json +27 -0
  35. package/agent/patterns.json +28 -0
  36. package/agent/rules.json +15 -0
  37. package/agent/tokens.json +5055 -1060
  38. package/dist/app/index.d.ts +3 -0
  39. package/dist/app/index.js +3 -0
  40. package/dist/app/tenant-theme.d.ts +80 -0
  41. package/dist/app/tenant-theme.js +154 -0
  42. package/dist/app/theme-axes.d.ts +14 -1
  43. package/dist/app/theme-axes.js +24 -31
  44. package/dist/components/charts/chart-cartesian.d.ts +5 -1
  45. package/dist/components/charts/chart-cartesian.js +15 -8
  46. package/dist/components/data-display/badge.d.ts +1 -1
  47. package/dist/components/data-display/badge.js +21 -3
  48. package/dist/components/data-display/carousel.js +4 -4
  49. package/dist/components/data-display/data-table.js +13 -2
  50. package/dist/components/data-display/permission-matrix.js +1 -1
  51. package/dist/components/data-display/table.d.ts +11 -2
  52. package/dist/components/data-display/table.js +19 -3
  53. package/dist/components/data-entry/control-appearance.d.ts +12 -6
  54. package/dist/components/data-entry/control-appearance.js +1 -1
  55. package/dist/components/data-entry/select.js +4 -3
  56. package/dist/components/feedback/dialog.js +6 -3
  57. package/dist/components/feedback/overlay-header-tone.d.ts +7 -0
  58. package/dist/components/feedback/overlay-header-tone.js +4 -4
  59. package/dist/components/feedback/sheet.d.ts +1 -1
  60. package/dist/components/feedback/sheet.js +6 -9
  61. package/dist/components/feedback/sonner.js +16 -3
  62. package/dist/components/general/button.js +22 -5
  63. package/dist/components/layout/affix.js +15 -1
  64. package/dist/components/layout/sidebar.js +7 -1
  65. package/dist/components/navigation/anchor.d.ts +1 -1
  66. package/dist/components/navigation/anchor.js +5 -4
  67. package/dist/components/navigation/app-setting-picker.js +1 -1
  68. package/dist/components/navigation/pagination.js +1 -1
  69. package/dist/components/navigation/tabs.js +15 -2
  70. package/dist/components/query/infinite-query-state.d.ts +22 -6
  71. package/dist/contracts/measurement.json +1 -1
  72. package/dist/lib/control-styles.d.ts +31 -11
  73. package/dist/lib/control-styles.js +6 -6
  74. package/dist/lib/overlay-portal.d.ts +20 -0
  75. package/dist/lib/overlay-portal.js +93 -0
  76. package/dist/props/components/app.prop.d.ts +12 -0
  77. package/dist/props/components/charts.prop.d.ts +30 -0
  78. package/dist/props/components/index.d.ts +1 -1
  79. package/dist/props/components/navigation.prop.d.ts +21 -2
  80. package/dist/props/components/query.prop.d.ts +36 -2
  81. package/dist/props/registry.d.ts +46 -1
  82. package/dist/props/registry.js +38 -3
  83. package/dist/styles/alert-layout.css +39 -19
  84. package/dist/styles/badge-layout.css +11 -7
  85. package/dist/styles/base.css +14 -5
  86. package/dist/styles/card-layout.css +29 -16
  87. package/dist/styles/chart-layout.css +27 -5
  88. package/dist/styles/control.css +225 -103
  89. package/dist/styles/data-display-layout.css +178 -76
  90. package/dist/styles/data-entry-layout.css +35 -99
  91. package/dist/styles/dialog-layout.css +58 -28
  92. package/dist/styles/float-button-layout.css +5 -5
  93. package/dist/styles/focus-ring.css +11 -7
  94. package/dist/styles/layout.css +53 -25
  95. package/dist/styles/logo-layout.css +3 -3
  96. package/dist/styles/motion.css +2 -2
  97. package/dist/styles/navigation-layout.css +117 -44
  98. package/dist/styles/shell-layout.css +90 -124
  99. package/dist/styles/table-layout.css +66 -24
  100. package/dist/styles/text-layout.css +13 -4
  101. package/dist/styles/toggle.css +9 -3
  102. package/dist/tokens/components/actions.css +1 -1
  103. package/dist/tokens/components/attachments.css +4 -4
  104. package/dist/tokens/components/badge.css +4 -4
  105. package/dist/tokens/components/banner.css +1 -1
  106. package/dist/tokens/components/callout.css +1 -1
  107. package/dist/tokens/components/card.css +13 -8
  108. package/dist/tokens/components/chart.css +11 -2
  109. package/dist/tokens/components/chat-bubble.css +2 -2
  110. package/dist/tokens/components/control.css +41 -22
  111. package/dist/tokens/components/conversations.css +2 -1
  112. package/dist/tokens/components/data-display.css +23 -18
  113. package/dist/tokens/components/data-entry.css +1 -1
  114. package/dist/tokens/components/descriptions.css +2 -2
  115. package/dist/tokens/components/draggable-panel.css +2 -2
  116. package/dist/tokens/components/feedback.css +29 -9
  117. package/dist/tokens/components/float-button.css +1 -1
  118. package/dist/tokens/components/legal-document.css +2 -2
  119. package/dist/tokens/components/logo.css +2 -2
  120. package/dist/tokens/components/mega-menu.css +6 -4
  121. package/dist/tokens/components/navigation.css +27 -13
  122. package/dist/tokens/components/segmented.css +11 -6
  123. package/dist/tokens/components/separator.css +1 -1
  124. package/dist/tokens/components/shell.css +24 -10
  125. package/dist/tokens/components/table.css +13 -5
  126. package/dist/tokens/components/thought-chain.css +2 -2
  127. package/dist/tokens/components/toggle.css +3 -1
  128. package/dist/tokens/components/tree.css +3 -1
  129. package/dist/tokens/components/upload.css +8 -8
  130. package/dist/tokens/components/welcome.css +1 -1
  131. package/dist/tokens/foundation.css +30 -3
  132. package/docs/COMPOSITION-VS-COMPONENT.md +31 -0
  133. package/docs/CUSTOMER-THEMING.md +637 -1
  134. package/docs/FRAME-COVERAGE-REPORT.md +3 -2
  135. package/docs/GLASSMORPHISM-STANDARD.md +196 -0
  136. package/docs/THEME-API-COVERAGE.md +538 -0
  137. package/docs/TOKEN-RESOLUTION.md +195 -0
  138. package/docs/TOKENS.md +63 -24
  139. package/docs/asset-modules.d.ts +7 -0
  140. package/docs/data-display/charts.tsx +80 -0
  141. package/docs/data-display/data-table/index.tsx +30 -0
  142. package/docs/data-display/popover.tsx +1 -1
  143. package/docs/data-display/table.tsx +52 -0
  144. package/docs/feedback/sheet.tsx +10 -10
  145. package/docs/foundation/density.tsx +4 -4
  146. package/docs/i18n/messages/en.json +506 -0
  147. package/docs/i18n/messages/ja.json +506 -0
  148. package/docs/i18n/messages/vi.json +506 -0
  149. package/docs/layout/account-chip.tsx +2 -2
  150. package/docs/layout/responsive-grid.tsx +1 -1
  151. package/docs/navigation/toolbar.tsx +20 -12
  152. package/docs/providers/theme-scope.tsx +186 -0
  153. package/docs/showcase/caimono-price-comparison.tsx +911 -0
  154. package/docs/showcase/case4-login.tsx +2 -2
  155. package/docs/showcase/permission-matrix.tsx +13 -5
  156. package/docs/showcase/table-pagination.tsx +2 -1
  157. package/docs/showcase/tenant-brand-color.tsx +338 -0
  158. package/docs/showcase/theme-lab.tsx +2125 -0
  159. package/docs/themes/flat.css +462 -0
  160. package/docs/themes/glassmorphism.css +958 -0
  161. package/docs/themes/index.ts +219 -0
  162. package/docs/themes/neubrutalism.css +475 -0
  163. package/package.json +4 -3
  164. package/scripts/explain-token.mjs +382 -0
@@ -7,6 +7,7 @@ export * from "./time-format-labels.js";
7
7
  export * from "./date-format-labels.js";
8
8
  export * from "./storage.js";
9
9
  export * from "./theme-axes.js";
10
+ export * from "./tenant-theme.js";
10
11
  export * from "./request-headers.js";
11
12
  export { AppProvider, useAppContext, useOptionalAppContext, useAppLocale, useAppTimezone, useAppTimeFormat, useAppDateFormat, } from "./app-provider.js";
12
13
  export { useFormatting, useDateTime } from "./use-formatting.js";
@@ -14,3 +15,5 @@ export { syncDatetimeContext, getDatetimeContext, formatDate, isFormatDateValue,
14
15
  export { useTranslation, usePickerLocales } from "../i18n/use-translation.js";
15
16
  export { OverlayPortalProvider } from "../lib/overlay-portal.js";
16
17
  export type { OverlayPortalProviderProps } from "../lib/overlay-portal.js";
18
+ export { ThemeScope } from "../lib/overlay-portal.js";
19
+ export type { ThemeScopeProps } from "../lib/overlay-portal.js";
package/dist/app/index.js CHANGED
@@ -7,6 +7,7 @@ export * from "./time-format-labels.js";
7
7
  export * from "./date-format-labels.js";
8
8
  export * from "./storage.js";
9
9
  export * from "./theme-axes.js";
10
+ export * from "./tenant-theme.js";
10
11
  export * from "./request-headers.js";
11
12
  import {
12
13
  AppProvider,
@@ -37,9 +38,11 @@ import {
37
38
  } from "../lib/datetime/index.js";
38
39
  import { useTranslation, usePickerLocales } from "../i18n/use-translation.js";
39
40
  import { OverlayPortalProvider } from "../lib/overlay-portal.js";
41
+ import { ThemeScope } from "../lib/overlay-portal.js";
40
42
  export {
41
43
  AppProvider,
42
44
  OverlayPortalProvider,
45
+ ThemeScope,
43
46
  detectFormatDateKind,
44
47
  formatAppDate,
45
48
  formatAppDateLong,
@@ -0,0 +1,80 @@
1
+ /** A bare CSS HSL channel triplet, `"H S% L%"` — the shape every `@godxjp/ui` colour role uses. */
2
+ export type HslTriplet = string;
3
+ /** WCAG 2.2 SC 1.4.3 Contrast (Minimum) — normal text. */
4
+ export declare const AA_NORMAL_TEXT = 4.5;
5
+ /** WCAG 2.2 SC 1.4.3 Contrast (Minimum) — large text (≥18.66px bold / ≥24px). */
6
+ export declare const AA_LARGE_TEXT = 3;
7
+ /**
8
+ * L = 0.179, the pivot of the label choice (solve 1.05/(L+0.05) = (L+0.05)/0.05). A surface above it
9
+ * is one an ink walks DOWN away from, below it one an ink walks UP — and either end of the ramp is
10
+ * achromatic, since `hsl(h s 0%)` is black and `hsl(h s 100%)` is white at every hue and saturation.
11
+ * So the walk always reaches at least 4.58:1 and the guarantee is a property of the whole sRGB cube
12
+ * rather than of the seeds we happened to try.
13
+ *
14
+ * EXPORTED because a component that paints its own fill has to make the same choice against it and
15
+ * must not re-derive the number (gh#884: `Sidebar`'s brand mark).
16
+ */
17
+ export declare const CONTRAST_PIVOT = 0.179;
18
+ /**
19
+ * WCAG 2.2 relative luminance of an sRGB colour, per the definition quoted at the top of this file.
20
+ * Accepts `#abc` / `#aabbcc`; returns null for anything else.
21
+ */
22
+ export declare function relativeLuminance(hex: string): number | null;
23
+ /**
24
+ * WCAG 2.2 contrast ratio between two hex colours, `(L_lighter + 0.05) / (L_darker + 0.05)`, in
25
+ * 1–21. Returns null if either colour is not a hex. Compare against {@link AA_NORMAL_TEXT}.
26
+ */
27
+ export declare function contrastRatio(a: string, b: string): number | null;
28
+ /**
29
+ * `#0071bd` → `"204.13 100% 37.06%"` — the direction the package was missing (gh#868: `hslToHex`
30
+ * appears 9 times in `dist/`, `hexToHsl` zero). The exact inverse of
31
+ * {@link import("../email/color.js").hslToHex} within one hex step, which `tenant-theme.test.ts`
32
+ * round-trips over a grid. Returns null for anything that is not a 3- or 6-digit hex, so an
33
+ * unparseable customer value is ignored rather than breaking the page.
34
+ */
35
+ export declare function hexToHsl(hex: string): HslTriplet | null;
36
+ export type TenantThemeOptions = {
37
+ /**
38
+ * The label to paint ON the seed, as hex. Omit and the contrast-safe choice is made here. Supply
39
+ * it — Platform stores a server-computed, WCAG-checked pair — and it is used as given, with
40
+ * `contrast` / `meetsAA` reporting what it achieves and a dev warning under 4.5:1.
41
+ */
42
+ foreground?: string | null;
43
+ /**
44
+ * The surface the brand INK sits on, as hex — the DARKEST one it lands on in a light theme, the
45
+ * LIGHTEST in a dark one, because the guarantee has to hold in the hovered state too. Default: the
46
+ * package's light `--accent`, `#ebe9e5`. A region inside the dark theme passes `#3c3a34`
47
+ * (`--accent` there); a themed consumer passes its own. Anything that is not a hex is ignored.
48
+ */
49
+ surface?: string | null;
50
+ };
51
+ export type TenantTheme = {
52
+ /** The seed, normalized to `#rrggbb`, or null when the input was not a hex colour. */
53
+ primary: string | null;
54
+ /** The label on that seed, `#rrggbb`, or null when there is no seed. */
55
+ foreground: string | null;
56
+ /** WCAG 2.2 ratio of `foreground` on `primary`, 1–21. `0` when there is no seed. */
57
+ contrast: number;
58
+ /** `contrast >= 4.5` — WCAG 2.2 SC 1.4.3, normal text. */
59
+ meetsAA: boolean;
60
+ /**
61
+ * The custom properties to declare on the scope. Spread straight into a `style` prop, or
62
+ * serialize into a `<style>` block for SSR. EMPTY for an unusable seed, so an invalid customer
63
+ * value degrades to the app's own theme instead of breaking the page.
64
+ */
65
+ vars: Readonly<Record<string, string>>;
66
+ };
67
+ /**
68
+ * Turn a customer's hex into the scoped token declarations that make one REGION wear it —
69
+ * `--primary`, `--primary-foreground`, `--ring`, the hover / pressed steps, and the three brand INKS
70
+ * (`--text-link` / `--text-brand` / `--text-primary`) — all as literals, so the colour cannot
71
+ * half-apply on an engine without CSS relative colour and the ink cannot land under 4.5:1.
72
+ *
73
+ * ```tsx
74
+ * const brand = tenantTheme(tenant.primary_color, { foreground: tenant.primary_foreground });
75
+ * <div data-tenant={tenant.slug} style={brand.vars}>…</div>
76
+ * ```
77
+ *
78
+ * Never throws and never returns null: an unusable seed yields empty `vars`.
79
+ */
80
+ export declare function tenantTheme(primary: string, options?: TenantThemeOptions): TenantTheme;
@@ -0,0 +1,154 @@
1
+ import { isDevelopment } from "../lib/dev.js";
2
+ const AA_NORMAL_TEXT = 4.5;
3
+ const AA_LARGE_TEXT = 3;
4
+ const STEPS = {
5
+ darken: { hover: (l) => l - 8.4, active: (l) => l - 15.5 },
6
+ lighten: {
7
+ hover: (l) => l + 5.8,
8
+ active: (l) => l > 83.4 ? l - 5.8 : l + 11.6
9
+ }
10
+ };
11
+ const INK_STEPS = {
12
+ light: { link: (l) => l - 8.4, pressed: (l) => l - 15.5 },
13
+ dark: { link: (l) => l, pressed: (l) => l + 5.8 }
14
+ };
15
+ const CONTRAST_PIVOT = 0.179;
16
+ const INK_SURFACE_LIGHT = "#ebe9e5";
17
+ const HEX = /^#(?:[\da-f]{3}|[\da-f]{6})$/i;
18
+ const clamp = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
19
+ function parseHex(value) {
20
+ if (typeof value !== "string" || !HEX.test(value.trim())) return null;
21
+ const body = value.trim().slice(1);
22
+ const full = body.length === 3 ? body.replace(/./g, (c) => c + c) : body;
23
+ return [0, 2, 4].map((offset) => parseInt(full.slice(offset, offset + 2), 16) / 255);
24
+ }
25
+ const toHex = (rgb) => `#${rgb.map(
26
+ (v) => Math.round(clamp(v, 0, 1) * 255).toString(16).padStart(2, "0")
27
+ ).join("")}`;
28
+ function relativeLuminance(hex) {
29
+ const rgb = parseHex(hex);
30
+ return rgb === null ? null : luminanceOf(rgb);
31
+ }
32
+ function luminanceOf(rgb) {
33
+ const coefficients = [0.2126, 0.7152, 0.0722];
34
+ return rgb.reduce(
35
+ (sum, channel, index) => sum + (channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4) * coefficients[index],
36
+ 0
37
+ );
38
+ }
39
+ function contrastRatio(a, b) {
40
+ const [first, second] = [relativeLuminance(a), relativeLuminance(b)];
41
+ if (first === null || second === null) return null;
42
+ const [hi, lo] = first >= second ? [first, second] : [second, first];
43
+ return (hi + 0.05) / (lo + 0.05);
44
+ }
45
+ function hexToHsl(hex) {
46
+ const rgb = parseHex(hex);
47
+ return rgb === null ? null : tripletOf(rgb);
48
+ }
49
+ const round = (value, places) => {
50
+ const factor = 10 ** places;
51
+ return Math.round(value * factor) / factor;
52
+ };
53
+ const formatHsl = ([h, s, l]) => `${round(h, 2)} ${round(s, 2)}% ${round(l, 2)}%`;
54
+ function tripletOf(rgb) {
55
+ return formatHsl(toHsl(rgb));
56
+ }
57
+ function fromHsl([h, s, l]) {
58
+ const hue = (h % 360 + 360) % 360 / 60;
59
+ const chroma = (1 - Math.abs(2 * l / 100 - 1)) * (s / 100);
60
+ const second = chroma * (1 - Math.abs(hue % 2 - 1));
61
+ const base = l / 100 - chroma / 2;
62
+ const [r, g, b] = hue < 1 ? [chroma, second, 0] : hue < 2 ? [second, chroma, 0] : hue < 3 ? [0, chroma, second] : hue < 4 ? [0, second, chroma] : hue < 5 ? [second, 0, chroma] : [chroma, 0, second];
63
+ return [r, g, b].map((c) => Math.round(clamp(c + base, 0, 1) * 255) / 255);
64
+ }
65
+ function inkLightness(h, s, from, surface) {
66
+ const clears = (l) => {
67
+ const ink = luminanceOf(fromHsl([h, s, l]));
68
+ const [hi, lo] = ink >= surface ? [ink, surface] : [surface, ink];
69
+ return (hi + 0.05) / (lo + 0.05) >= AA_NORMAL_TEXT;
70
+ };
71
+ const start = clamp(from, 0, 100);
72
+ if (clears(start)) return start;
73
+ const limit = surface > CONTRAST_PIVOT ? 0 : 1e3;
74
+ let fails = clamp(limit === 0 ? Math.floor(start * 10) : Math.ceil(start * 10), 0, 1e3);
75
+ if (clears(fails / 10)) return fails / 10;
76
+ let passes = limit;
77
+ if (!clears(passes / 10)) return passes / 10;
78
+ while (Math.abs(passes - fails) > 1) {
79
+ const mid = Math.round((passes + fails) / 2);
80
+ if (clears(mid / 10)) passes = mid;
81
+ else fails = mid;
82
+ }
83
+ return passes / 10;
84
+ }
85
+ function toHsl(rgb) {
86
+ const [r, g, b] = rgb;
87
+ const max = Math.max(r, g, b);
88
+ const min = Math.min(r, g, b);
89
+ const delta = max - min;
90
+ const lightness = (max + min) / 2;
91
+ const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));
92
+ const hue = delta === 0 ? 0 : 60 * (max === r ? ((g - b) / delta + 6) % 6 : max === g ? (b - r) / delta + 2 : (r - g) / delta + 4);
93
+ return [hue, clamp(saturation, 0, 1) * 100, lightness * 100];
94
+ }
95
+ const EMPTY = Object.freeze({
96
+ primary: null,
97
+ foreground: null,
98
+ contrast: 0,
99
+ meetsAA: false,
100
+ vars: Object.freeze({})
101
+ });
102
+ function tenantTheme(primary, options = {}) {
103
+ const seed = parseHex(primary);
104
+ if (seed === null) return EMPTY;
105
+ const supplied = options.foreground ? parseHex(options.foreground) : null;
106
+ const label = supplied ?? (luminanceOf(seed) > CONTRAST_PIVOT ? [0, 0, 0] : [1, 1, 1]);
107
+ const seedLuminance = luminanceOf(seed);
108
+ const labelLuminance = luminanceOf(label);
109
+ const [hi, lo] = labelLuminance >= seedLuminance ? [labelLuminance, seedLuminance] : [seedLuminance, labelLuminance];
110
+ const contrast = (hi + 0.05) / (lo + 0.05);
111
+ if (isDevelopment() && supplied !== null && contrast < AA_NORMAL_TEXT) {
112
+ console.warn(
113
+ `[godxjp-ui] tenantTheme: the supplied foreground ${toHex(label)} measures ${round(contrast, 2)}:1 on ${toHex(seed)} \u2014 below WCAG 2.2 SC 1.4.3 AA (4.5:1). Text on this brand fill will be hard to read. Omit \`foreground\` to let the contrast-safe label be chosen, or raise the pair upstream.`
114
+ );
115
+ }
116
+ const steps = STEPS[labelLuminance > 0.5 ? "darken" : "lighten"];
117
+ const [h, s, l] = toHsl(seed);
118
+ const surface = luminanceOf(
119
+ (options.surface ? parseHex(options.surface) : null) ?? parseHex(INK_SURFACE_LIGHT)
120
+ );
121
+ const ink = INK_STEPS[surface > CONTRAST_PIVOT ? "light" : "dark"];
122
+ const [inkH, inkS] = [round(h, 2), round(s, 2)];
123
+ const inkAt = (step) => formatHsl([inkH, inkS, inkLightness(inkH, inkS, round(step(l), 2), surface)]);
124
+ const [linkInk, pressedInk] = [inkAt(ink.link), inkAt(ink.pressed)];
125
+ return Object.freeze({
126
+ primary: toHex(seed),
127
+ foreground: toHex(label),
128
+ contrast,
129
+ meetsAA: contrast >= AA_NORMAL_TEXT,
130
+ vars: Object.freeze({
131
+ "--primary": tripletOf(seed),
132
+ "--primary-foreground": tripletOf(label),
133
+ // derived.css declares `--ring: var(--primary)` at `:root`, where a var() substitutes ONCE;
134
+ // a scope below it inherits the root's ring unless it restates it (the freeze rule).
135
+ "--ring": tripletOf(seed),
136
+ "--primary-hover": formatHsl([h, s, clamp(steps.hover(l), 0, 100)]),
137
+ "--primary-active": formatHsl([h, s, clamp(steps.active(l), 0, 100)]),
138
+ // The three brand INKS, each ≥ 4.5:1 on `surface` — the roles derived.css already publishes,
139
+ // now with the floor its lightness steps never had. `--text-primary` also seeds `--mark-*`.
140
+ "--text-link": linkInk,
141
+ "--text-brand": linkInk,
142
+ "--text-primary": pressedInk
143
+ })
144
+ });
145
+ }
146
+ export {
147
+ AA_LARGE_TEXT,
148
+ AA_NORMAL_TEXT,
149
+ CONTRAST_PIVOT,
150
+ contrastRatio,
151
+ hexToHsl,
152
+ relativeLuminance,
153
+ tenantTheme
154
+ };
@@ -43,5 +43,18 @@ export declare const isAppFontSize: (v: unknown) => v is AppFontSize;
43
43
  * `data-brand` so the app's own `--primary` token applies.
44
44
  */
45
45
  export declare function applyThemeAxes(el: HTMLElement, axes: Partial<AppThemeAxes>): void;
46
- /** Apply an entity's action palette, including portals. Call the returned cleanup on scope exit. */
46
+ /**
47
+ * Apply an entity's action palette to an existing themed tree, including portals. Call the returned
48
+ * cleanup on scope exit.
49
+ *
50
+ * The arithmetic — hex → HSL triplet, and the WCAG 2.2 label choice — is {@link tenantTheme}'s, so
51
+ * there is one formula rather than two. This entry point adds what a RE-SEED needs and a fresh
52
+ * region does not: the knobs an ancestor service theme may have pinned to the PREVIOUS brand are
53
+ * reset to `initial`, so the new seed cannot be outranked by a stale literal (gh#678, gh#664). The
54
+ * three brand INKS are reset and then re-declared from the seed, because `tenantTheme` computes a
55
+ * 4.5:1 floor for them and a bare reset would discard it (gh#887 — see the palette below).
56
+ *
57
+ * Use {@link tenantTheme} instead when you are painting a region with a customer's colour — it
58
+ * returns declarations to put in `style`, with no effect, no unmount restore and no SSR flash.
59
+ */
47
60
  export declare function applyPrimaryColor(root: HTMLElement, color: string, foreground?: string | null): () => void;
@@ -1,3 +1,4 @@
1
+ import { relativeLuminance, tenantTheme } from "./tenant-theme.js";
1
2
  const APP_THEMES = ["light", "dark", "system"];
2
3
  const APP_BRANDS = [
3
4
  "brand",
@@ -40,43 +41,35 @@ function applyThemeAxes(el, axes) {
40
41
  }
41
42
  }
42
43
  function applyPrimaryColor(root, color, foreground) {
43
- const parse = (value) => {
44
- if (!/^#[\da-f]{6}$/i.test(value)) return null;
45
- return [1, 3, 5].map((offset) => parseInt(value.slice(offset, offset + 2), 16) / 255);
44
+ const seed = tenantTheme(color, { foreground });
45
+ if (seed.primary === null) return () => {
46
46
  };
47
- const rgb = parse(color);
48
- if (!rgb) return () => {
49
- };
50
- const luminance = (channels) => channels.reduce(
51
- (sum, value, index) => sum + (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4) * [0.2126, 0.7152, 0.0722][index],
52
- 0
53
- );
54
- const text = foreground && parse(foreground) || (luminance(rgb) > 0.179 ? [0, 0, 0] : [1, 1, 1]);
55
- const hsl = (channels) => {
56
- const [r, g, b] = channels;
57
- const max = Math.max(r, g, b), min = Math.min(r, g, b);
58
- const delta = max - min, lightness = (max + min) / 2;
59
- const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));
60
- const hue = delta === 0 ? 0 : 60 * (max === r ? ((g - b) / delta + 6) % 6 : max === g ? (b - r) / delta + 2 : (r - g) / delta + 4);
61
- return `${hue} ${saturation * 100}% ${lightness * 100}%`;
62
- };
63
- const polarity = luminance(text) > 0.5 ? "darken" : "lighten";
47
+ const polarity = (relativeLuminance(seed.foreground) ?? 0) > 0.5 ? "darken" : "lighten";
64
48
  const palette = {
65
- "--primary": hsl(rgb),
66
- "--primary-foreground": hsl(text),
67
- "--primary-hover": "initial",
68
- "--primary-active": "initial",
69
- "--primary-border": "initial",
70
- "--control-outline": "initial",
71
- /* The brand TEXT roles derive from the seed too (gh#664), so they are reset for the same
72
- * reason: an ancestor theme that pinned a link colour for the previous brand would otherwise
73
- * outrank the seed just set, and a re-tinted product would keep the old brand's links. */
49
+ /* THE STALE-PIN RESETS COME FIRST, AND THE ORDER IS THE WHOLE POINT (gh#887).
50
+ *
51
+ * They exist because a theme above this element may have pinned a knob to the PREVIOUS brand,
52
+ * and an inherited literal would outrank a seed that only re-derives in CSS (gh#678, gh#664):
53
+ * a link colour pinned for the old brand would survive on a re-tinted product. That reason
54
+ * still holds, so nothing is deleted.
55
+ *
56
+ * But `tenantTheme` now RETURNS the three brand inks as clamped literals (gh#887), and these
57
+ * three keys used to sit AFTER the spread — so the same call that walked each ink to 4.5:1 on
58
+ * the surface threw the result away one line later, and a re-seeded tree got the unclamped
59
+ * ramp step back. Declared BEFORE the spread they are overwritten by those literals, which
60
+ * defeat an ancestor's pin on their own (a literal ON the element beats an inherited one), and
61
+ * the guard still stands if that emission ever stops.
62
+ *
63
+ * `--primary-border` / `--control-outline` are NOT in `vars` — the reset IS their value here,
64
+ * and they go on deriving in CSS from the `--primary` in scope. */
74
65
  "--text-link": "initial",
75
66
  "--text-brand": "initial",
76
67
  "--text-primary": "initial",
68
+ ...seed.vars,
69
+ "--primary-border": "initial",
70
+ "--control-outline": "initial",
77
71
  "--primary-hover-channels": `var(--primary-hover-${polarity}-channels)`,
78
- "--primary-active-channels": `var(--primary-active-${polarity}-channels)`,
79
- "--ring": hsl(rgb)
72
+ "--primary-active-channels": `var(--primary-active-${polarity}-channels)`
80
73
  };
81
74
  const previous = Object.keys(palette).map((key) => [
82
75
  key,
@@ -17,16 +17,20 @@ type CartesianChartProps = {
17
17
  height?: number;
18
18
  showLegend?: boolean;
19
19
  showGrid?: boolean;
20
+ valueDomain?: [number, number];
21
+ valueTicks?: number[];
20
22
  numberFormat?: Intl.NumberFormatOptions;
21
23
  emptyMessage?: string;
22
24
  className?: string;
23
25
  id?: string;
24
26
  /** line/area */
25
27
  curved?: boolean;
28
+ /** line/area */
29
+ showDots?: boolean;
26
30
  /** bar/area */
27
31
  stacked?: boolean;
28
32
  /** bar */
29
33
  horizontal?: boolean;
30
34
  };
31
- export declare function CartesianChart({ kind, data, series, categoryKey, label, showCaption, description, size, height, showLegend, showGrid, numberFormat, emptyMessage, className, id, curved, stacked, horizontal, }: CartesianChartProps): React.JSX.Element;
35
+ export declare function CartesianChart({ kind, data, series, categoryKey, label, showCaption, description, size, height, showLegend, showGrid, valueDomain, valueTicks, numberFormat, emptyMessage, className, id, curved, showDots, stacked, horizontal, }: CartesianChartProps): React.JSX.Element;
32
36
  export {};
@@ -35,11 +35,14 @@ function CartesianChart({
35
35
  height,
36
36
  showLegend = true,
37
37
  showGrid = true,
38
+ valueDomain,
39
+ valueTicks,
38
40
  numberFormat,
39
41
  emptyMessage,
40
42
  className,
41
43
  id,
42
44
  curved = false,
45
+ showDots = false,
43
46
  stacked = false,
44
47
  horizontal = false
45
48
  }) {
@@ -59,6 +62,10 @@ function CartesianChart({
59
62
  const resolvedHeight = chartHeight(size, height);
60
63
  const tickFormatter = (value) => fmt.format(value);
61
64
  const curve = curved ? "monotone" : "linear";
65
+ const valueAxisScale = {
66
+ ...valueDomain ? { domain: valueDomain } : null,
67
+ ...valueTicks ? { ticks: valueTicks } : null
68
+ };
62
69
  const measuresCategoryAxis = kind === "bar" && horizontal;
63
70
  const canvasRef = React.useRef(null);
64
71
  const probeRef = React.useRef(null);
@@ -95,7 +102,7 @@ function CartesianChart({
95
102
  );
96
103
  };
97
104
  const axes = (numberAxisVertical) => /* @__PURE__ */ jsxs(Fragment, { children: [
98
- showGrid ? /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "hsl(var(--border))" }) : null,
105
+ showGrid ? /* @__PURE__ */ jsx(CartesianGrid, { stroke: "hsl(var(--border))" }) : null,
99
106
  numberAxisVertical ? /* @__PURE__ */ jsxs(Fragment, { children: [
100
107
  /* @__PURE__ */ jsx(
101
108
  XAxis,
@@ -112,7 +119,8 @@ function CartesianChart({
112
119
  tickFormatter,
113
120
  tickLine: false,
114
121
  stroke: "hsl(var(--muted-foreground))",
115
- fontSize: 12
122
+ fontSize: 12,
123
+ ...valueAxisScale
116
124
  }
117
125
  )
118
126
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -123,7 +131,8 @@ function CartesianChart({
123
131
  tickFormatter,
124
132
  tickLine: false,
125
133
  stroke: "hsl(var(--muted-foreground))",
126
- fontSize: 12
134
+ fontSize: 12,
135
+ ...valueAxisScale
127
136
  }
128
137
  ),
129
138
  /* @__PURE__ */ jsx(
@@ -153,8 +162,7 @@ function CartesianChart({
153
162
  dataKey: s.dataKey,
154
163
  name: s.label ?? s.dataKey,
155
164
  stroke: chartColor(i, s.color),
156
- strokeWidth: 2,
157
- dot: false
165
+ dot: showDots
158
166
  },
159
167
  s.dataKey
160
168
  ))
@@ -171,9 +179,8 @@ function CartesianChart({
171
179
  name: s.label ?? s.dataKey,
172
180
  stackId: stacked ? "stack" : void 0,
173
181
  stroke: chartColor(i, s.color),
174
- fill: chartColor(i, s.color),
175
- fillOpacity: 0.2,
176
- strokeWidth: 2
182
+ fill: s.fillColor ?? chartColor(i, s.color),
183
+ dot: showDots
177
184
  },
178
185
  s.dataKey
179
186
  ))
@@ -9,7 +9,7 @@ export type BadgeVariant = "default" | "secondary" | "outline" | "dashed";
9
9
  */
10
10
  export type BadgeTone = ToneProp | "primary";
11
11
  declare const badgeVariants: (props?: ({
12
- variant?: "dashed" | "default" | "outline" | "secondary" | "tinted" | null | undefined;
12
+ variant?: "dashed" | "default" | "outline" | "secondary" | "tinted" | "toned" | null | undefined;
13
13
  shape?: "default" | "pill" | "sharp" | null | undefined;
14
14
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
15
15
  export interface BadgeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children">, Omit<VariantProps<typeof badgeVariants>, "variant" | "shape"> {
@@ -55,7 +55,7 @@ const badgeVariants = cva(
55
55
  // `ui-focus-ring` = the system's single focus source (styles/focus-ring.css). It replaces a
56
56
  // hand-rolled `focus:ring-2 focus:ring-ring`, which was token-blind (no --focus-ring-* knob
57
57
  // reached it) and fired on `:focus`, so a mouse click on an interactive badge lit a ring.
58
- "ui-focus-ring inline-flex items-center border font-medium transition-colors",
58
+ "ui-focus-ring inline-flex items-center font-medium transition-colors",
59
59
  {
60
60
  variants: {
61
61
  variant: {
@@ -68,7 +68,22 @@ const badgeVariants = cva(
68
68
  * off the element so badge-layout.css's `[data-tinted]` rule is
69
69
  * reachable (a utility beats a components-layer rule whatever the
70
70
  * specificity). Selected by the `color` prop, never by hand. */
71
- tinted: ""
71
+ tinted: "",
72
+ /* A STATUS TONE owns the whole chip — fill, border and ink — so the variant contributes
73
+ * none of them. Selected automatically whenever `tone` is not `default`, never by hand.
74
+ *
75
+ * This used to be left to `cn` (tailwind-merge): `default` painted `bg-primary`, the tone
76
+ * added `bg-success/10`, twMerge saw one conflict group and deleted the first. gh#866
77
+ * respelled the tone fill as `[background-color:var(--surface-success, …)]` so a brand's
78
+ * own pale ground could reach it — and an ARBITRARY PROPERTY is a different twMerge group,
79
+ * so nothing was deleted, both survived, and plain CSS source order handed the chip to
80
+ * `bg-primary`. Every toned Badge painted violet under tone-coloured ink: measured 1.07:1
81
+ * (warning) to 1.30:1 (info), against 4.5:1 for AA.
82
+ *
83
+ * The classes were "correct" before and after; only the cascade moved. So the fix is to
84
+ * stop emitting the conflict at all rather than to restore a spelling that happened to
85
+ * collide — a fill deleted by a class-merging library is a fill nobody declared. */
86
+ toned: ""
72
87
  },
73
88
  // Corner shape — default inherits the badge radius token; pill/sharp override via the tokens.
74
89
  shape: {
@@ -137,7 +152,10 @@ function Badge({
137
152
  "data-removable": onRemove ? "" : void 0,
138
153
  className: cn(
139
154
  badgeVariants({
140
- variant: tinted ? "tinted" : variant ?? "default",
155
+ // A status tone paints the chip itself (`badgeToneClass` below), so the variant must not
156
+ // also paint one — see the `toned` variant. An EXPLICIT `variant` still wins, which is
157
+ // how `<Badge variant="outline" tone="success">` keeps its border treatment.
158
+ variant: tinted ? "tinted" : variant ?? (resolvedTone === "default" ? "default" : "toned"),
141
159
  shape: shape ?? "default"
142
160
  }),
143
161
  tinted ? void 0 : badgeToneClass[resolvedTone],
@@ -131,7 +131,7 @@ const CarouselItem = React.forwardRef(
131
131
  )
132
132
  );
133
133
  CarouselItem.displayName = "CarouselItem";
134
- const CarouselPrevious = React.forwardRef(({ className, ...props }, ref) => {
134
+ const CarouselPrevious = React.forwardRef(({ className, children, ...props }, ref) => {
135
135
  const { t } = useTranslation();
136
136
  const { canScrollPrev, scrollPrev } = useCarousel();
137
137
  return /* @__PURE__ */ jsxs(
@@ -145,14 +145,14 @@ const CarouselPrevious = React.forwardRef(({ className, ...props }, ref) => {
145
145
  onClick: scrollPrev,
146
146
  ...props,
147
147
  children: [
148
- /* @__PURE__ */ jsx(ChevronLeft, { className: "ui-carousel-arrow", "aria-hidden": "true" }),
148
+ /* @__PURE__ */ jsx("span", { className: "ui-carousel-arrow", "aria-hidden": "true", children: children ?? /* @__PURE__ */ jsx(ChevronLeft, {}) }),
149
149
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("dataDisplay.carousel.previous") })
150
150
  ]
151
151
  }
152
152
  );
153
153
  });
154
154
  CarouselPrevious.displayName = "CarouselPrevious";
155
- const CarouselNext = React.forwardRef(({ className, ...props }, ref) => {
155
+ const CarouselNext = React.forwardRef(({ className, children, ...props }, ref) => {
156
156
  const { t } = useTranslation();
157
157
  const { canScrollNext, scrollNext } = useCarousel();
158
158
  return /* @__PURE__ */ jsxs(
@@ -166,7 +166,7 @@ const CarouselNext = React.forwardRef(({ className, ...props }, ref) => {
166
166
  onClick: scrollNext,
167
167
  ...props,
168
168
  children: [
169
- /* @__PURE__ */ jsx(ChevronRight, { className: "ui-carousel-arrow", "aria-hidden": "true" }),
169
+ /* @__PURE__ */ jsx("span", { className: "ui-carousel-arrow", "aria-hidden": "true", children: children ?? /* @__PURE__ */ jsx(ChevronRight, {}) }),
170
170
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("dataDisplay.carousel.next") })
171
171
  ]
172
172
  }
@@ -86,6 +86,11 @@ function columnHideBelowProps(col) {
86
86
  const step = resolveColumnHideBelow(col);
87
87
  return step ? { "data-hide-below": step } : {};
88
88
  }
89
+ function stackedCellLabel(col) {
90
+ const header = col.header;
91
+ const headerIsEmpty = header == null || header === "" || header === false;
92
+ return headerIsEmpty ? col.ariaLabel : header;
93
+ }
89
94
  function dataTableFeatures() {
90
95
  return tableFeatures({
91
96
  rowSortingFeature,
@@ -798,6 +803,7 @@ DataTable.Content = function DataTableContent() {
798
803
  } = useDataTableContext();
799
804
  const { t } = useTranslation();
800
805
  const presetAttr = preset === "default" ? void 0 : preset;
806
+ const stacked = preset === "stacked-record-collection";
801
807
  const isDenied = denied != null && denied !== false;
802
808
  const isError = !isDenied && error != null && error !== false;
803
809
  const rowPadding = tableRowHeightClass;
@@ -1166,7 +1172,11 @@ DataTable.Content = function DataTableContent() {
1166
1172
  // Hover highlight when rows are clickable OR explicitly hoverable… The
1167
1173
  // --accent step TableRow itself uses, not --muted/50: that sat BELOW the
1168
1174
  // zebra stripe (--muted/0.8, gh#700), so hovering a striped row lightened it.
1169
- (onRowClick || hoverable) && "hover:bg-accent/70",
1175
+ // Same token-driven formula as TableRow's own hover fill (gh#894) — a bare
1176
+ // `/70` here would silently outrank a theme's retuned default at equal
1177
+ // specificity, and `color-mix` (not `hsl(x / var(y, 70%))`) because a `/`
1178
+ // inside a Tailwind arbitrary property is read as ITS OWN opacity suffix.
1179
+ (onRowClick || hoverable) && "hover:[background-color:color-mix(in_oklab,hsl(var(--accent))_var(--table-row-hover-background-alpha,70%),transparent)]",
1170
1180
  // …but the affordance (cursor + focus mark) only when clickable.
1171
1181
  //
1172
1182
  // `ui-focus-ring` = the single focus source (styles/focus-ring.css). It
@@ -1282,6 +1292,7 @@ DataTable.Content = function DataTableContent() {
1282
1292
  priority: col.priority,
1283
1293
  "data-align": col.align,
1284
1294
  title,
1295
+ label: stacked ? stackedCellLabel(col) : void 0,
1285
1296
  ...fixedCellProps(col.key, fixedEdge(col)),
1286
1297
  ...columnHideBelowProps(col),
1287
1298
  style: columnCellStyle(col),
@@ -1379,12 +1390,12 @@ function NumberedPagination({ pageSizeOptions, className }) {
1379
1390
  {
1380
1391
  value: String(pageSize),
1381
1392
  onValueChange: (v) => table.setPageSize(Number(v)),
1393
+ "aria-label": t("dataGrid.rowsPerPage"),
1382
1394
  children: [
1383
1395
  /* @__PURE__ */ jsx(
1384
1396
  SelectTrigger,
1385
1397
  {
1386
1398
  size: "sm",
1387
- "aria-label": t("dataGrid.rowsPerPage"),
1388
1399
  className: "ui-data-table-page-size-trigger w-auto shrink-0 tabular-nums",
1389
1400
  children: /* @__PURE__ */ jsx(SelectValue, {})
1390
1401
  }
@@ -162,7 +162,7 @@ const PermissionMatrix = React.forwardRef(
162
162
  return /* @__PURE__ */ jsxs(
163
163
  TableRow,
164
164
  {
165
- className: cn(isDiffRow && "bg-warning/[0.07] hover:bg-warning/10"),
165
+ "data-tone": isDiffRow ? "warning" : void 0,
166
166
  children: [
167
167
  /* @__PURE__ */ jsx(TableCell, { className: cn(PIN_START, "align-middle"), children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col leading-tight", children: [
168
168
  /* @__PURE__ */ jsxs("span", { className: "ui-permission-matrix-name", children: [
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import type { BreakpointProp, FlushProp, LabelProp, TableCellIndentProp, TableColumnPriorityProp, TablePresetProp, WidthProp } from "../../props/vocabulary/index.js";
2
+ import type { BreakpointProp, FlushProp, LabelProp, TableCellIndentProp, TableColumnPriorityProp, TablePresetProp, TableRowToneProp, WidthProp } from "../../props/vocabulary/index.js";
3
3
  export type TableProps = React.HTMLAttributes<HTMLTableElement> & {
4
4
  /**
5
5
  * Whether the Table owns its own horizontal-scroll region (default `true`). When `true` a table
@@ -157,7 +157,16 @@ export declare const Table: React.ForwardRefExoticComponent<React.HTMLAttributes
157
157
  } & React.RefAttributes<HTMLTableElement>>;
158
158
  export declare const TableHeader: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableSectionElement> & React.RefAttributes<HTMLTableSectionElement>>;
159
159
  export declare const TableBody: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableSectionElement> & React.RefAttributes<HTMLTableSectionElement>>;
160
- export declare const TableRow: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableRowElement> & React.RefAttributes<HTMLTableRowElement>>;
160
+ /**
161
+ * The row's STATE — same wash/rail/`--surface-*` grounding `DataTable`'s own `rowTone` already
162
+ * writes onto `data-tone` (gh#866). Typed here so a hand-composed row can discover it instead of
163
+ * reaching for the raw attribute; the attribute keeps working (`TableRow data-tone="warning"`
164
+ * is untouched — this is an addition, not a migration), and `tone` writes the same attribute.
165
+ */
166
+ type TableRowTone = {
167
+ tone?: TableRowToneProp;
168
+ };
169
+ export declare const TableRow: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableRowElement> & TableRowTone & React.RefAttributes<HTMLTableRowElement>>;
161
170
  /**
162
171
  * Column priority carried by BOTH the header cell and the body cells of a column. Read only by
163
172
  * `Table preset="action-collection"` below its collapse step, where it selects the column's