@godxjp/ui 18.4.0 → 18.6.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 (159) hide show
  1. package/README.md +16 -1
  2. package/dist/app/theme-axes.d.ts +11 -3
  3. package/dist/app/theme-axes.js +2 -1
  4. package/dist/components/charts/chart-frame.d.ts +17 -3
  5. package/dist/components/charts/chart-frame.js +7 -1
  6. package/dist/components/charts/chart-summary.d.ts +21 -0
  7. package/dist/components/charts/chart-summary.js +32 -5
  8. package/dist/components/charts/compact-bar-trend.d.ts +30 -0
  9. package/dist/components/charts/compact-bar-trend.js +74 -0
  10. package/dist/components/charts/index.d.ts +6 -0
  11. package/dist/components/charts/index.js +2 -0
  12. package/dist/components/data-display/badge.d.ts +8 -0
  13. package/dist/components/data-display/badge.js +7 -1
  14. package/dist/components/data-display/data-table.d.ts +40 -4
  15. package/dist/components/data-display/data-table.js +69 -2
  16. package/dist/components/data-display/empty-state.js +1 -1
  17. package/dist/components/data-display/index.d.ts +4 -2
  18. package/dist/components/data-display/index.js +10 -1
  19. package/dist/components/data-display/list-row.d.ts +41 -4
  20. package/dist/components/data-display/list-row.js +23 -3
  21. package/dist/components/data-display/qr-code.d.ts +1 -1
  22. package/dist/components/data-display/service-launcher-card.d.ts +40 -0
  23. package/dist/components/data-display/service-launcher-card.js +97 -0
  24. package/dist/components/data-display/table.d.ts +47 -2
  25. package/dist/components/data-display/table.js +29 -4
  26. package/dist/components/data-entry/command-palette.d.ts +37 -0
  27. package/dist/components/data-entry/command-palette.js +125 -0
  28. package/dist/components/data-entry/command.d.ts +10 -10
  29. package/dist/components/data-entry/index.d.ts +3 -0
  30. package/dist/components/data-entry/index.js +2 -0
  31. package/dist/components/data-entry/input-otp.d.ts +1 -0
  32. package/dist/components/feedback/dialog.d.ts +16 -1
  33. package/dist/components/feedback/dialog.js +46 -33
  34. package/dist/components/feedback/index.d.ts +5 -2
  35. package/dist/components/feedback/index.js +8 -2
  36. package/dist/components/feedback/sheet.d.ts +26 -4
  37. package/dist/components/feedback/sheet.js +54 -7
  38. package/dist/components/feedback/two-factor-setup.d.ts +31 -0
  39. package/dist/components/feedback/two-factor-setup.js +97 -0
  40. package/dist/components/general/index.d.ts +1 -1
  41. package/dist/components/general/logo.d.ts +49 -9
  42. package/dist/components/general/logo.js +67 -14
  43. package/dist/components/layout/app-shell.d.ts +1 -1
  44. package/dist/components/layout/app-shell.js +55 -30
  45. package/dist/components/layout/auth-account-summary.d.ts +5 -0
  46. package/dist/components/layout/auth-account-summary.js +31 -0
  47. package/dist/components/layout/auth-divider.d.ts +7 -0
  48. package/dist/components/layout/auth-divider.js +21 -0
  49. package/dist/components/layout/auth-footer.d.ts +5 -0
  50. package/dist/components/layout/auth-footer.js +24 -0
  51. package/dist/components/layout/auth-identity.d.ts +5 -0
  52. package/dist/components/layout/auth-identity.js +17 -0
  53. package/dist/components/layout/auth-shell.d.ts +9 -1
  54. package/dist/components/layout/auth-shell.js +25 -6
  55. package/dist/components/layout/auth-stack.d.ts +4 -0
  56. package/dist/components/layout/auth-stack.js +8 -0
  57. package/dist/components/layout/centered-shell.d.ts +5 -2
  58. package/dist/components/layout/centered-shell.js +29 -19
  59. package/dist/components/layout/error-surface.d.ts +35 -0
  60. package/dist/components/layout/error-surface.js +155 -0
  61. package/dist/components/layout/flex.d.ts +1 -1
  62. package/dist/components/layout/flex.js +4 -0
  63. package/dist/components/layout/index.d.ts +23 -3
  64. package/dist/components/layout/index.js +21 -2
  65. package/dist/components/layout/legal-document-shell.d.ts +34 -0
  66. package/dist/components/layout/legal-document-shell.js +186 -0
  67. package/dist/components/layout/master-detail.d.ts +24 -0
  68. package/dist/components/layout/master-detail.js +48 -0
  69. package/dist/components/layout/org-switcher.d.ts +9 -0
  70. package/dist/components/layout/org-switcher.js +218 -0
  71. package/dist/components/layout/page-container.d.ts +1 -1
  72. package/dist/components/layout/page-container.js +7 -2
  73. package/dist/components/layout/sidebar-link.d.ts +43 -0
  74. package/dist/components/layout/sidebar-link.js +13 -0
  75. package/dist/components/layout/sidebar.d.ts +42 -5
  76. package/dist/components/layout/sidebar.js +170 -85
  77. package/dist/components/navigation/app-setting-picker.js +17 -6
  78. package/dist/components/navigation/filter-bar.d.ts +12 -3
  79. package/dist/components/navigation/filter-bar.js +8 -2
  80. package/dist/components/navigation/index.d.ts +2 -2
  81. package/dist/components/navigation/index.js +3 -1
  82. package/dist/components/navigation/steps.js +38 -19
  83. package/dist/components/navigation/tabs-scroll.d.ts +70 -0
  84. package/dist/components/navigation/tabs-scroll.js +76 -0
  85. package/dist/components/navigation/tabs.js +47 -23
  86. package/dist/components/ui/avatar.d.ts +2 -1
  87. package/dist/components/ui/avatar.js +2 -1
  88. package/dist/components/ui/input-otp.d.ts +4 -1
  89. package/dist/components/ui/input-otp.js +2 -1
  90. package/dist/components/ui/toggle-group.d.ts +7 -4
  91. package/dist/components/ui/toggle-group.js +32 -21
  92. package/dist/email/brand-mark.d.ts +108 -0
  93. package/dist/email/brand-mark.js +88 -0
  94. package/dist/email/color.d.ts +53 -0
  95. package/dist/email/color.js +37 -0
  96. package/dist/email/geometry.d.ts +120 -0
  97. package/dist/email/geometry.js +90 -0
  98. package/dist/email/index.d.ts +83 -0
  99. package/dist/email/index.js +82 -0
  100. package/dist/email/inline-style.d.ts +27 -0
  101. package/dist/email/inline-style.js +26 -0
  102. package/dist/email/tokens.generated.d.ts +167 -0
  103. package/dist/email/tokens.generated.js +162 -0
  104. package/dist/email/urgency.d.ts +22 -0
  105. package/dist/email/urgency.js +14 -0
  106. package/dist/i18n/messages/en.json +36 -3
  107. package/dist/i18n/messages/ja.json +36 -3
  108. package/dist/i18n/messages/vi.json +36 -3
  109. package/dist/inertia/index.d.ts +50 -0
  110. package/dist/inertia/index.js +5 -0
  111. package/dist/props/components/app.prop.d.ts +11 -1
  112. package/dist/props/components/charts.prop.d.ts +40 -1
  113. package/dist/props/components/data-display.prop.d.ts +75 -1
  114. package/dist/props/components/data-entry.prop.d.ts +6 -0
  115. package/dist/props/components/feedback.prop.d.ts +10 -0
  116. package/dist/props/components/index.d.ts +3 -3
  117. package/dist/props/components/layout.prop.d.ts +560 -10
  118. package/dist/props/components/layout.prop.js +1 -0
  119. package/dist/props/components/navigation.prop.d.ts +24 -2
  120. package/dist/props/registry.d.ts +433 -8
  121. package/dist/props/registry.js +527 -9
  122. package/dist/props/vocabulary/data.prop.d.ts +27 -0
  123. package/dist/props/vocabulary/index.d.ts +3 -3
  124. package/dist/props/vocabulary/interaction.prop.d.ts +10 -1
  125. package/dist/props/vocabulary/layout.prop.d.ts +49 -0
  126. package/dist/styles/base.css +8 -2
  127. package/dist/styles/card-layout.css +161 -7
  128. package/dist/styles/chart-layout.css +97 -0
  129. package/dist/styles/control.css +76 -2
  130. package/dist/styles/data-display-layout.css +78 -6
  131. package/dist/styles/dialog-layout.css +105 -4
  132. package/dist/styles/fonts.css +15 -4
  133. package/dist/styles/index.css +11 -3
  134. package/dist/styles/layout.css +466 -1
  135. package/dist/styles/logo-layout.css +95 -0
  136. package/dist/styles/navigation-layout.css +152 -0
  137. package/dist/styles/shell-layout.css +790 -40
  138. package/dist/styles/table-layout.css +164 -1
  139. package/dist/theme/dxs.canonical.css +77 -0
  140. package/dist/tokens/axes.css +40 -0
  141. package/dist/tokens/base.css +6 -0
  142. package/dist/tokens/components/card.css +48 -0
  143. package/dist/tokens/components/chart.css +47 -0
  144. package/dist/tokens/components/control.css +16 -0
  145. package/dist/tokens/components/data-display.css +13 -0
  146. package/dist/tokens/components/email.css +93 -0
  147. package/dist/tokens/components/error-surface.css +36 -0
  148. package/dist/tokens/components/feedback.css +17 -0
  149. package/dist/tokens/components/legal-document.css +67 -0
  150. package/dist/tokens/components/list-row.css +43 -2
  151. package/dist/tokens/components/logo.css +74 -0
  152. package/dist/tokens/components/navigation.css +28 -0
  153. package/dist/tokens/components/sheet.css +19 -0
  154. package/dist/tokens/components/shell.css +210 -0
  155. package/dist/tokens/components/sidebar.css +33 -0
  156. package/dist/tokens/components/table.css +45 -0
  157. package/dist/tokens/foundation.css +31 -2
  158. package/dist/tokens/semantic/layout.css +69 -2
  159. package/package.json +22 -5
@@ -0,0 +1,70 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Keep-the-active-tab-visible logic for the horizontal `TabsList` scroll strip (gh#204).
4
+ *
5
+ * `TabsList` scrolls its own overflow (gh#175) so long localized labels never clip. That fixed
6
+ * clipping but not the SCROLL POSITION: after a responsive resize / route re-render the strip kept
7
+ * (or shifted) its internal `scrollLeft`, so the ACTIVE trigger — typically the FIRST one — could
8
+ * end up completely outside the scrollport while still carrying `data-state="active"` /
9
+ * `aria-selected="true"`. A tab you cannot see is a tab you cannot use.
10
+ *
11
+ * The fix observes the strip (size changes) and its triggers (`data-state` changes) and scrolls the
12
+ * element that must stay reachable back into view. Deliberate constraints:
13
+ *
14
+ * - **Never fights the user.** The scroll only runs when the target is NOT already fully inside the
15
+ * scrollport, so an intentional manual/touch scroll that leaves the active tab visible is left
16
+ * alone, and a click (target is under the pointer) or arrow-key move (the browser already
17
+ * scrolled focus into view) is a no-op.
18
+ * - **Focus wins over selection.** With `activationMode="manual"` the roving focus can sit on a
19
+ * trigger that is not the selected one; yanking the strip to the selected tab would strand the
20
+ * keyboard user. The focused trigger inside the list is therefore the preferred target.
21
+ * - **Reduced motion.** `prefers-reduced-motion: reduce` downgrades every correction to an instant
22
+ * jump; resize corrections are instant regardless (a smooth animation mid-resize reads as jank).
23
+ * - **RTL / vertical safe.** All geometry is physical viewport rects and the scroll uses
24
+ * `scrollIntoView({ block: "nearest", inline: "nearest" })`, which resolves direction natively —
25
+ * so a first trigger in `dir="rtl"` (visually on the right) and a vertical rail both work
26
+ * without direction branches. `nearest` also means no ancestor/page scroll when the strip is
27
+ * already where it should be.
28
+ */
29
+ /** Minimal rect shape — only the edges the visibility test needs. */
30
+ export type TabsScrollBox = {
31
+ left: number;
32
+ right: number;
33
+ top: number;
34
+ bottom: number;
35
+ };
36
+ /**
37
+ * True when `trigger` sits entirely inside the `scrollport` box. Pure geometry — physical viewport
38
+ * coordinates, so it is direction- and orientation-agnostic (LTR, RTL, vertical rail alike).
39
+ *
40
+ * A zero-area scrollport (unmounted, `display:none`, or an environment without layout such as
41
+ * jsdom) reports `true`: there is no measurable overflow to correct, and scrolling a hidden strip
42
+ * would be meaningless.
43
+ */
44
+ export declare function isTabFullyVisible(scrollport: TabsScrollBox, trigger: TabsScrollBox): boolean;
45
+ /**
46
+ * The trigger the strip must keep reachable: the focused trigger when the roving focus is inside
47
+ * this list (manual activation can park focus off the selected tab), otherwise the active one.
48
+ */
49
+ export declare function resolveTabScrollTarget(list: HTMLElement): HTMLElement | null;
50
+ /** `true` when the user has asked for reduced motion (falsy/unsupported environments say no). */
51
+ export declare function prefersReducedMotion(): boolean;
52
+ /**
53
+ * Scrolls the list's keep-visible target back into the scrollport when — and only when — it is not
54
+ * already fully inside it. Returns the element it scrolled, or `null` when nothing needed doing
55
+ * (which is the common case, and what keeps this from fighting a deliberate user scroll).
56
+ */
57
+ export declare function syncActiveTabIntoView(list: HTMLElement | null, behavior?: ScrollBehavior): HTMLElement | null;
58
+ /**
59
+ * Wires the two signals that can strand the active tab off-screen:
60
+ *
61
+ * - a `ResizeObserver` on the strip — a responsive width change (1440 → 1024 → 390) keeps/shifts
62
+ * the old `scrollLeft`; the observer's initial delivery also covers first mount at a new width;
63
+ * - a `MutationObserver` on descendant `data-state` — the selected tab changing from outside the
64
+ * strip (router/URL state, a controlled parent) must bring the newly-selected trigger into view.
65
+ *
66
+ * Resize corrections are instant; a selection change animates unless reduced motion is requested.
67
+ * Both observers are optional: without them (SSR, ancient runtimes) the component degrades to the
68
+ * pre-fix behaviour instead of throwing.
69
+ */
70
+ export declare function useKeepActiveTabVisible(listRef: React.RefObject<HTMLElement | null>): void;
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ const VISIBILITY_EPSILON = 1;
4
+ function isTabFullyVisible(scrollport, trigger) {
5
+ const width = scrollport.right - scrollport.left;
6
+ const height = scrollport.bottom - scrollport.top;
7
+ if (width <= 0 && height <= 0) return true;
8
+ return trigger.left >= scrollport.left - VISIBILITY_EPSILON && trigger.right <= scrollport.right + VISIBILITY_EPSILON && trigger.top >= scrollport.top - VISIBILITY_EPSILON && trigger.bottom <= scrollport.bottom + VISIBILITY_EPSILON;
9
+ }
10
+ function resolveTabScrollTarget(list) {
11
+ const focused = list.ownerDocument?.activeElement;
12
+ if (focused instanceof HTMLElement && focused !== list && list.contains(focused) && focused.getAttribute("role") === "tab") {
13
+ return focused;
14
+ }
15
+ return list.querySelector('[role="tab"][data-state="active"]');
16
+ }
17
+ function prefersReducedMotion() {
18
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
19
+ try {
20
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+ function syncActiveTabIntoView(list, behavior = "auto") {
26
+ if (!list) return null;
27
+ const target = resolveTabScrollTarget(list);
28
+ if (!target || typeof target.scrollIntoView !== "function") return null;
29
+ if (isTabFullyVisible(list.getBoundingClientRect(), target.getBoundingClientRect())) return null;
30
+ target.scrollIntoView({
31
+ block: "nearest",
32
+ inline: "nearest",
33
+ behavior: prefersReducedMotion() ? "auto" : behavior
34
+ });
35
+ return target;
36
+ }
37
+ function useKeepActiveTabVisible(listRef) {
38
+ React.useEffect(() => {
39
+ const list = listRef.current;
40
+ if (!list) return void 0;
41
+ syncActiveTabIntoView(list, "auto");
42
+ const cleanups = [];
43
+ if (typeof ResizeObserver !== "undefined") {
44
+ const resizeObserver = new ResizeObserver(() => {
45
+ syncActiveTabIntoView(list, "auto");
46
+ });
47
+ resizeObserver.observe(list);
48
+ cleanups.push(() => {
49
+ resizeObserver.disconnect();
50
+ });
51
+ }
52
+ if (typeof MutationObserver !== "undefined") {
53
+ const mutationObserver = new MutationObserver(() => {
54
+ syncActiveTabIntoView(list, "smooth");
55
+ });
56
+ mutationObserver.observe(list, {
57
+ subtree: true,
58
+ attributes: true,
59
+ attributeFilter: ["data-state"]
60
+ });
61
+ cleanups.push(() => {
62
+ mutationObserver.disconnect();
63
+ });
64
+ }
65
+ return () => {
66
+ for (const cleanup of cleanups) cleanup();
67
+ };
68
+ }, [listRef]);
69
+ }
70
+ export {
71
+ isTabFullyVisible,
72
+ prefersReducedMotion,
73
+ resolveTabScrollTarget,
74
+ syncActiveTabIntoView,
75
+ useKeepActiveTabVisible
76
+ };
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as TabsPrimitive from "@radix-ui/react-tabs";
5
5
  import { cn } from "../../lib/utils.js";
6
+ import { useKeepActiveTabVisible } from "./tabs-scroll.js";
6
7
  function resolveFallbackTabValue(items, requested) {
7
8
  if (!items || items.length === 0) return requested;
8
9
  if (requested !== void 0) {
@@ -41,8 +42,9 @@ function Tabs({
41
42
  TabsList,
42
43
  {
43
44
  "data-slot": "tabs-list",
45
+ variant: variant === "line" ? "line" : "default",
44
46
  className: cn(
45
- variant === "line" && "h-auto w-full justify-start rounded-none border-b bg-transparent p-0",
47
+ variant === "line" && "h-auto w-full justify-start border-b p-0",
46
48
  variant === "card" && "w-full justify-start",
47
49
  listClassName
48
50
  ),
@@ -52,7 +54,9 @@ function Tabs({
52
54
  value: item.value,
53
55
  disabled: item.disabled,
54
56
  className: cn(
55
- variant === "line" && "data-[state=active]:border-primary rounded-none border-b-2 border-transparent bg-transparent px-4 py-2 shadow-none data-[state=active]:bg-transparent data-[state=active]:shadow-none",
57
+ // Geometry only the active underline is the token-owned `::after` bar the
58
+ // line variant already owns (never a second, hand-rolled border-b indicator).
59
+ variant === "line" && "rounded-none px-4 py-2",
56
60
  variant === "card" && "data-[state=active]:shadow-sm"
57
61
  ),
58
62
  children: item.label
@@ -75,26 +79,40 @@ function Tabs({
75
79
  }
76
80
  );
77
81
  }
78
- const TabsList = React.forwardRef(({ className, variant = "default", ...props }, ref) => /* @__PURE__ */ jsx(
79
- TabsPrimitive.List,
80
- {
81
- ref,
82
- "data-slot": "tabs-list",
83
- "data-variant": variant,
84
- className: cn(
85
- // `min-w-0 max-w-full` let the list shrink to (and never exceed) whatever width its
86
- // ancestors actually give it instead of forcing them wider; horizontal orientation then
87
- // scrolls its own overflow rather than clipping/hiding long localized labels in a narrow
88
- // container (gh#175). Hidden scrollbar keeps the strip visually clean while staying
89
- // swipeable on touch and reachable via keyboard (arrow-key roving focus still scrolls the
90
- // newly-focused trigger into view natively). Vertical orientation is untouched — it already
91
- // stacks in a column and is sized by its own `h-*`/`w-*` overrides.
92
- "group/tabs-list text-muted-foreground data-[variant=default]:bg-muted inline-flex w-fit max-w-full min-w-0 items-center justify-center rounded-lg p-1 group-data-[orientation=vertical]/tabs:flex-col data-[orientation=horizontal]:[scrollbar-width:none] data-[orientation=horizontal]:overflow-x-auto data-[orientation=horizontal]:overflow-y-hidden data-[variant=line]:gap-1 data-[variant=line]:rounded-none data-[variant=line]:bg-transparent [&[data-orientation=horizontal]::-webkit-scrollbar]:hidden",
93
- className
94
- ),
95
- ...props
96
- }
97
- ));
82
+ const TabsList = React.forwardRef(({ className, variant = "default", ...props }, ref) => {
83
+ const listRef = React.useRef(null);
84
+ const setListRef = React.useCallback(
85
+ (node) => {
86
+ listRef.current = node;
87
+ if (typeof ref === "function") ref(node);
88
+ else if (ref) ref.current = node;
89
+ },
90
+ [ref]
91
+ );
92
+ useKeepActiveTabVisible(listRef);
93
+ return /* @__PURE__ */ jsx(
94
+ TabsPrimitive.List,
95
+ {
96
+ ref: setListRef,
97
+ "data-slot": "tabs-list",
98
+ "data-variant": variant,
99
+ className: cn(
100
+ // `min-w-0 max-w-full` let the list shrink to (and never exceed) whatever width its
101
+ // ancestors actually give it instead of forcing them wider; horizontal orientation then
102
+ // scrolls its own overflow rather than clipping/hiding long localized labels in a narrow
103
+ // container (gh#175). Hidden scrollbar keeps the strip visually clean while staying
104
+ // swipeable on touch and reachable via keyboard (arrow-key roving focus still scrolls the
105
+ // newly-focused trigger into view natively); `useKeepActiveTabVisible` above re-pins the
106
+ // active trigger when a resize would otherwise strand it off-strip (gh#204). Vertical
107
+ // orientation is untouched — it already stacks in a column and is sized by its own
108
+ // `h-*`/`w-*` overrides.
109
+ "group/tabs-list text-muted-foreground data-[variant=default]:bg-muted inline-flex w-fit max-w-full min-w-0 items-center justify-center rounded-lg p-1 group-data-[orientation=vertical]/tabs:flex-col data-[orientation=horizontal]:[scrollbar-width:none] data-[orientation=horizontal]:overflow-x-auto data-[orientation=horizontal]:overflow-y-hidden data-[variant=line]:gap-1 data-[variant=line]:rounded-none data-[variant=line]:bg-transparent [&[data-orientation=horizontal]::-webkit-scrollbar]:hidden",
110
+ className
111
+ ),
112
+ ...props
113
+ }
114
+ );
115
+ });
98
116
  TabsList.displayName = TabsPrimitive.List.displayName;
99
117
  const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
100
118
  TabsPrimitive.Trigger,
@@ -102,7 +120,13 @@ const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
102
120
  ref,
103
121
  "data-slot": "tabs-trigger",
104
122
  className: cn(
105
- "text-muted-foreground ring-offset-background hover:text-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-ring/50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:ring-primary/25 after:bg-foreground relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:ring-1 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
123
+ // The SELECTED-state ring (`ring-1 ring-primary/25`) is scoped to the default/card lists
124
+ // the `line` variant is underline-only (gh#248) and must NEVER paint a surrounding ring, or
125
+ // it competes with (and at equal specificity overrides) the `:focus-visible` ring. The
126
+ // focus-visible ring/outline below is deliberately left unscoped: every variant keeps a
127
+ // visible keyboard focus indicator (WCAG 2.4.7). The line indicator itself lives in
128
+ // src/styles/navigation-layout.css so it reads the --tabs-indicator-* tokens.
129
+ "text-muted-foreground ring-offset-background hover:text-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-ring/50 data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:ring-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=default]/tabs-list:data-[state=active]:ring-1 group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
106
130
  className
107
131
  ),
108
132
  ...props
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import * as AvatarPrimitive from "@radix-ui/react-avatar";
3
- export declare const Avatar: React.ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarProps & React.RefAttributes<HTMLSpanElement>, "ref"> & React.RefAttributes<HTMLSpanElement>>;
3
+ import type { AvatarProp } from "../../props/components/data-display.prop.js";
4
+ export declare const Avatar: React.ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarProps & React.RefAttributes<HTMLSpanElement>, "ref"> & Pick<AvatarProp, "shape"> & React.RefAttributes<HTMLSpanElement>>;
4
5
  export declare const AvatarImage: React.ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarImageProps & React.RefAttributes<HTMLImageElement>, "ref"> & React.RefAttributes<HTMLImageElement>>;
5
6
  export declare const AvatarFallback: React.ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarFallbackProps & React.RefAttributes<HTMLSpanElement>, "ref"> & React.RefAttributes<HTMLSpanElement>>;
@@ -3,11 +3,12 @@ import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as AvatarPrimitive from "@radix-ui/react-avatar";
5
5
  import { cn } from "../../lib/utils.js";
6
- const Avatar = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
6
+ const Avatar = React.forwardRef(({ className, shape = "circle", ...props }, ref) => /* @__PURE__ */ jsx(
7
7
  AvatarPrimitive.Root,
8
8
  {
9
9
  ref,
10
10
  "data-slot": "avatar",
11
+ "data-shape": shape === "square" ? "square" : void 0,
11
12
  className: cn("ui-avatar", className),
12
13
  ...props
13
14
  }
@@ -26,7 +26,10 @@ export declare const InputOTP: React.ForwardRefExoticComponent<(Omit<Omit<React.
26
26
  render?: never;
27
27
  children: React.ReactNode;
28
28
  } & React.RefAttributes<HTMLInputElement>, "ref">) & React.RefAttributes<HTMLInputElement>>;
29
- export declare const InputOTPGroup: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
29
+ export type { InputOTPGroupAppearanceProp, InputOTPGroupProp, InputOTPGroupProp as InputOTPGroupProps, } from "../../props/components/data-entry.prop.js";
30
+ export declare const InputOTPGroup: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & {
31
+ appearance?: import("./input-otp.js").InputOTPGroupAppearanceProp;
32
+ } & React.RefAttributes<HTMLDivElement>>;
30
33
  export declare const InputOTPSlot: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & {
31
34
  index: number;
32
35
  } & React.RefAttributes<HTMLDivElement>>;
@@ -16,11 +16,12 @@ const InputOTP = React.forwardRef(({ className, containerClassName, ...props },
16
16
  ));
17
17
  InputOTP.displayName = "InputOTP";
18
18
  const InputOTPGroup = React.forwardRef(
19
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx(
19
+ ({ appearance = "slots", className, ...props }, ref) => /* @__PURE__ */ jsx(
20
20
  "div",
21
21
  {
22
22
  ref,
23
23
  "data-slot": "input-otp-group",
24
+ "data-appearance": appearance,
24
25
  className: cn("ui-otp-group", className),
25
26
  ...props
26
27
  }
@@ -1,11 +1,14 @@
1
1
  import * as React from "react";
2
2
  import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
3
3
  import { type ToggleProps } from "./toggle.js";
4
+ type ToggleGroupVariant = ToggleProps["variant"];
5
+ type ToggleGroupSize = ToggleProps["size"];
4
6
  export declare const ToggleGroup: React.ForwardRefExoticComponent<((Omit<ToggleGroupPrimitive.ToggleGroupSingleProps & React.RefAttributes<HTMLDivElement>, "ref"> | Omit<ToggleGroupPrimitive.ToggleGroupMultipleProps & React.RefAttributes<HTMLDivElement>, "ref">) & {
5
- variant?: ToggleProps["variant"];
6
- size?: ToggleProps["size"];
7
+ variant?: ToggleGroupVariant;
8
+ size?: ToggleGroupSize;
7
9
  }) & React.RefAttributes<HTMLDivElement>>;
8
10
  export declare const ToggleGroupItem: React.ForwardRefExoticComponent<Omit<ToggleGroupPrimitive.ToggleGroupItemProps & React.RefAttributes<HTMLButtonElement>, "ref"> & {
9
- variant?: ToggleProps["variant"];
10
- size?: ToggleProps["size"];
11
+ variant?: ToggleGroupVariant;
12
+ size?: ToggleGroupSize;
11
13
  } & React.RefAttributes<HTMLButtonElement>>;
14
+ export {};
@@ -4,28 +4,39 @@ import * as React from "react";
4
4
  import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
5
5
  import { cn } from "../../lib/utils.js";
6
6
  import { toggleVariants } from "./toggle.js";
7
- const ToggleGroup = React.forwardRef(({ className, variant = "default", size = "default", children, ...props }, ref) => /* @__PURE__ */ jsx(
8
- ToggleGroupPrimitive.Root,
9
- {
10
- ref,
11
- "data-slot": "toggle-group",
12
- "data-variant": variant,
13
- "data-size": size,
14
- className: cn("ui-toggle-group", className),
15
- ...props,
16
- children
17
- }
18
- ));
7
+ const ToggleGroupContext = React.createContext({});
8
+ const ToggleGroup = React.forwardRef(({ className, variant, size, children, ...props }, ref) => {
9
+ const context = React.useMemo(() => ({ variant, size }), [variant, size]);
10
+ return /* @__PURE__ */ jsx(
11
+ ToggleGroupPrimitive.Root,
12
+ {
13
+ ref,
14
+ "data-slot": "toggle-group",
15
+ "data-variant": variant,
16
+ "data-size": size,
17
+ className: cn("ui-toggle-group", className),
18
+ ...props,
19
+ children: /* @__PURE__ */ jsx(ToggleGroupContext.Provider, { value: context, children })
20
+ }
21
+ );
22
+ });
19
23
  ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
20
- const ToggleGroupItem = React.forwardRef(({ className, variant, size, ...props }, ref) => /* @__PURE__ */ jsx(
21
- ToggleGroupPrimitive.Item,
22
- {
23
- ref,
24
- "data-slot": "toggle-group-item",
25
- className: cn(toggleVariants({ variant, size }), className),
26
- ...props
27
- }
28
- ));
24
+ const ToggleGroupItem = React.forwardRef(({ className, variant, size, ...props }, ref) => {
25
+ const context = React.useContext(ToggleGroupContext);
26
+ const resolvedVariant = variant ?? context.variant;
27
+ const resolvedSize = size ?? context.size;
28
+ return /* @__PURE__ */ jsx(
29
+ ToggleGroupPrimitive.Item,
30
+ {
31
+ ref,
32
+ "data-slot": "toggle-group-item",
33
+ "data-variant": resolvedVariant,
34
+ "data-size": resolvedSize,
35
+ className: cn(toggleVariants({ variant: resolvedVariant, size: resolvedSize }), className),
36
+ ...props
37
+ }
38
+ );
39
+ });
29
40
  ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
30
41
  export {
31
42
  ToggleGroup,
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The canonical GoDX brand mark, as email-safe markup.
3
+ *
4
+ * The mark is an emerald CAPSULE with an internal light GLYPH bar — the same artwork the React
5
+ * `<Logo mark="godx" />` paints, at the same 32×32 viewBox and the same coordinates (a test asserts
6
+ * the capsule path is byte-identical to the component's, so the two can never diverge). A capsule
7
+ * WITHOUT the internal glyph is the incomplete mark and must not ship. The ARTWORK matches the
8
+ * canonical SCR-302 raster exactly; only the rendered BOX is email-specific — the canonical email
9
+ * header sets it at 22px (`--email-mark-width`) beside the "GoDX" wordmark, where the web
10
+ * `<Logo mark="godx" />` box is 32px.
11
+ *
12
+ * Nothing here references an external or relative asset: an email template has no base URL it can
13
+ * trust, remote images are blocked by default in most clients, and a `cid:` attachment needs
14
+ * transport support. So the mark is delivered three ways, in order of fidelity:
15
+ *
16
+ * 1. `svg` — inline `<svg>`. Apple Mail, iOS Mail, Thunderbird, Samsung Mail. Best fidelity.
17
+ * 2. `dataUri` — the same SVG as a `data:` URL for `<img src>`. No network fetch.
18
+ * 3. `tableHtml` — bulletproof `<table>`/`<div>` markup with background-color + border-radius.
19
+ * Renders everywhere; degrades to a square emerald block with a light bar in Outlook's Word
20
+ * renderer (which drops `border-radius`) — still recognisably the mark, never a broken image.
21
+ *
22
+ * Colours come from `EMAIL_COLORS` (derived from `--brand` / `--brand-foreground`, the roles
23
+ * `--logo-godx-color` points at), so a re-themed identity role re-tints the mark. NOT `--success`:
24
+ * that is the wakatake STATUS green, and binding identity to it rendered the mark ~ΔE76 17 away
25
+ * from the canonical emerald until gh#250 introduced the dedicated `--brand` role.
26
+ */
27
+ import { type EmailHex } from "./color.js";
28
+ /** A rounded rectangle in the mark's 32×32 coordinate space. */
29
+ export interface EmailBrandMarkRect {
30
+ readonly x: number;
31
+ readonly y: number;
32
+ readonly width: number;
33
+ readonly height: number;
34
+ readonly radius: number;
35
+ }
36
+ /** Intrinsic coordinate space of the mark artwork. */
37
+ export declare const EMAIL_BRAND_MARK_VIEWBOX = "0 0 32 32";
38
+ /** The emerald capsule. */
39
+ export declare const EMAIL_BRAND_MARK_CAPSULE: EmailBrandMarkRect;
40
+ /** The internal light glyph bar inside the capsule. */
41
+ export declare const EMAIL_BRAND_MARK_GLYPH: EmailBrandMarkRect;
42
+ /**
43
+ * Path data for a rounded rectangle, in the exact serialisation the canonical mark uses — so the
44
+ * generated capsule string equals the artwork shipped by `<Logo mark="godx" />` character for
45
+ * character.
46
+ */
47
+ export declare function roundedRectPath({ x, y, width, height, radius }: EmailBrandMarkRect): string;
48
+ /** Options for rendering the mark. Every default comes from the token contract. */
49
+ export interface EmailBrandMarkOptions {
50
+ /** Rendered width in px. Default `EMAIL_BRAND_MARK.widthPx` (the canonical 22px header box). */
51
+ width?: number;
52
+ /** Rendered height in px. Default `EMAIL_BRAND_MARK.heightPx` (the canonical 22px header box). */
53
+ height?: number;
54
+ /** Capsule fill. Default `EMAIL_COLORS.brand`. */
55
+ color?: EmailHex;
56
+ /** Internal glyph fill. Default `EMAIL_COLORS.brandForeground`. */
57
+ glyphColor?: EmailHex;
58
+ /**
59
+ * Accessible name. Pass `""` for a decorative mark that sits next to a readable wordmark — the
60
+ * markup then carries `alt=""` / `aria-hidden` instead of a redundant announcement.
61
+ */
62
+ label?: string;
63
+ }
64
+ /**
65
+ * Inline `<svg>` markup for the mark — self-contained, no external or relative asset reference.
66
+ * The `xmlns` attribute is required: an email body is not always parsed as HTML5.
67
+ */
68
+ export declare function emailBrandMarkSvg(o?: EmailBrandMarkOptions): string;
69
+ /**
70
+ * The mark as a `data:image/svg+xml` URL for `<img src>`. Percent-encoded (not base64) so it stays
71
+ * readable in a template and needs no `Buffer`/`btoa`. Still zero network requests.
72
+ */
73
+ export declare function emailBrandMarkDataUri(o?: EmailBrandMarkOptions): string;
74
+ /**
75
+ * Bulletproof `<table>` markup for clients with no SVG support. Uses only `background-color`,
76
+ * `border-radius`, fixed cell sizes and `font-size:0` — the intersection every client renders.
77
+ */
78
+ export declare function emailBrandMarkTableHtml(o?: EmailBrandMarkOptions): string;
79
+ /** The canonical mark at its default size and colours, in all three delivery forms. */
80
+ export interface EmailBrandMark {
81
+ readonly viewBox: string;
82
+ readonly width: string;
83
+ readonly widthPx: number;
84
+ readonly height: string;
85
+ readonly heightPx: number;
86
+ readonly capsule: EmailBrandMarkRect;
87
+ readonly glyph: EmailBrandMarkRect;
88
+ readonly capsulePath: string;
89
+ readonly glyphPath: string;
90
+ /**
91
+ * Mark ↔ wordmark separation in the canonical header LOCKUP. The mark alone is not the header:
92
+ * the canonical card sets the mark beside a readable "GoDX" wordmark, and the wordmark — not the
93
+ * mark — carries the accessible name (render the mark with `label: ""` in that pairing).
94
+ */
95
+ readonly gap: string;
96
+ readonly gapPx: number;
97
+ /** Wordmark type size in the lockup. */
98
+ readonly wordmarkFontSize: string;
99
+ readonly wordmarkFontSizePx: number;
100
+ readonly wordmarkFontWeight: number;
101
+ /** Inline `<svg>` — highest fidelity. */
102
+ readonly svg: string;
103
+ /** `data:` URL of the same SVG, for `<img src>`. */
104
+ readonly dataUri: string;
105
+ /** `<table>` fallback for clients with no SVG support. */
106
+ readonly tableHtml: string;
107
+ }
108
+ export declare const EMAIL_BRAND_MARK: EmailBrandMark;
@@ -0,0 +1,88 @@
1
+ import { EMAIL_COLORS } from "./color.js";
2
+ import { EMAIL_GEOMETRY_SOURCE } from "./tokens.generated.js";
3
+ const EMAIL_BRAND_MARK_VIEWBOX = "0 0 32 32";
4
+ const EMAIL_BRAND_MARK_CAPSULE = Object.freeze({
5
+ x: 1,
6
+ y: 7,
7
+ width: 30,
8
+ height: 18,
9
+ radius: 7
10
+ });
11
+ const EMAIL_BRAND_MARK_GLYPH = Object.freeze({
12
+ x: 7,
13
+ y: 13,
14
+ width: 18,
15
+ height: 6,
16
+ radius: 1
17
+ });
18
+ const sep = (value) => value < 0 ? String(value) : ` ${value}`;
19
+ const arc = (r, dx, dy) => `a${r} ${r} 0 0 1${sep(dx)}${sep(dy)}`;
20
+ function roundedRectPath({ x, y, width, height, radius }) {
21
+ const h = width - 2 * radius;
22
+ const v = height - 2 * radius;
23
+ return `M${x + radius} ${y}h${h}${arc(radius, radius, radius)}v${v}${arc(radius, -radius, radius)}H${x + radius}${arc(radius, -radius, -radius)}v-${v}${arc(radius, radius, -radius)}Z`;
24
+ }
25
+ const CAPSULE_PATH = roundedRectPath(EMAIL_BRAND_MARK_CAPSULE);
26
+ const GLYPH_PATH = roundedRectPath(EMAIL_BRAND_MARK_GLYPH);
27
+ const DEFAULT_WIDTH = Number.parseFloat(EMAIL_GEOMETRY_SOURCE["--email-mark-width"]);
28
+ const DEFAULT_HEIGHT = Number.parseFloat(EMAIL_GEOMETRY_SOURCE["--email-mark-height"]);
29
+ function options(o = {}) {
30
+ return {
31
+ width: o.width ?? DEFAULT_WIDTH,
32
+ height: o.height ?? DEFAULT_HEIGHT,
33
+ color: o.color ?? EMAIL_COLORS.brand,
34
+ glyphColor: o.glyphColor ?? EMAIL_COLORS.brandForeground,
35
+ label: o.label ?? "GoDX"
36
+ };
37
+ }
38
+ const attr = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
39
+ function emailBrandMarkSvg(o = {}) {
40
+ const { width, height, color, glyphColor, label } = options(o);
41
+ const a11y = label ? ` role="img" aria-label="${attr(label)}"` : ` role="presentation" aria-hidden="true"`;
42
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${EMAIL_BRAND_MARK_VIEWBOX}" focusable="false"${a11y} style="display:block;border:0;outline:none;text-decoration:none"><path fill="${color}" d="${CAPSULE_PATH}"/><path fill="${glyphColor}" d="${GLYPH_PATH}"/></svg>`;
43
+ }
44
+ function emailBrandMarkDataUri(o = {}) {
45
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(emailBrandMarkSvg(o))}`;
46
+ }
47
+ function emailBrandMarkTableHtml(o = {}) {
48
+ const { width, height, color, glyphColor, label } = options(o);
49
+ const scale = width / 32;
50
+ const box = (rect) => ({
51
+ width: Math.round(rect.width * scale),
52
+ height: Math.round(rect.height * scale * height / width),
53
+ radius: Math.round(rect.radius * scale)
54
+ });
55
+ const capsule = box(EMAIL_BRAND_MARK_CAPSULE);
56
+ const glyph = box(EMAIL_BRAND_MARK_GLYPH);
57
+ const a11y = label ? ` role="img" aria-label="${attr(label)}"` : ` role="presentation" aria-hidden="true"`;
58
+ return `<table${a11y} border="0" cellpadding="0" cellspacing="0" width="${capsule.width}" style="border-collapse:separate;border-spacing:0;width:${capsule.width}px"><tr><td width="${capsule.width}" height="${capsule.height}" align="center" valign="middle" style="width:${capsule.width}px;height:${capsule.height}px;line-height:${capsule.height}px;font-size:0;mso-line-height-rule:exactly;background-color:${color};border-radius:${capsule.radius}px"><div style="width:${glyph.width}px;height:${glyph.height}px;line-height:${glyph.height}px;font-size:0;background-color:${glyphColor};border-radius:${glyph.radius}px">&#8203;</div></td></tr></table>`;
59
+ }
60
+ const EMAIL_BRAND_MARK = Object.freeze({
61
+ viewBox: EMAIL_BRAND_MARK_VIEWBOX,
62
+ width: EMAIL_GEOMETRY_SOURCE["--email-mark-width"],
63
+ widthPx: DEFAULT_WIDTH,
64
+ height: EMAIL_GEOMETRY_SOURCE["--email-mark-height"],
65
+ heightPx: DEFAULT_HEIGHT,
66
+ capsule: EMAIL_BRAND_MARK_CAPSULE,
67
+ glyph: EMAIL_BRAND_MARK_GLYPH,
68
+ capsulePath: CAPSULE_PATH,
69
+ glyphPath: GLYPH_PATH,
70
+ gap: EMAIL_GEOMETRY_SOURCE["--email-mark-gap"],
71
+ gapPx: Number.parseFloat(EMAIL_GEOMETRY_SOURCE["--email-mark-gap"]),
72
+ wordmarkFontSize: EMAIL_GEOMETRY_SOURCE["--email-wordmark-font-size"],
73
+ wordmarkFontSizePx: Number.parseFloat(EMAIL_GEOMETRY_SOURCE["--email-wordmark-font-size"]),
74
+ wordmarkFontWeight: Number.parseFloat(EMAIL_GEOMETRY_SOURCE["--email-wordmark-font-weight"]),
75
+ svg: emailBrandMarkSvg(),
76
+ dataUri: emailBrandMarkDataUri(),
77
+ tableHtml: emailBrandMarkTableHtml()
78
+ });
79
+ export {
80
+ EMAIL_BRAND_MARK,
81
+ EMAIL_BRAND_MARK_CAPSULE,
82
+ EMAIL_BRAND_MARK_GLYPH,
83
+ EMAIL_BRAND_MARK_VIEWBOX,
84
+ emailBrandMarkDataUri,
85
+ emailBrandMarkSvg,
86
+ emailBrandMarkTableHtml,
87
+ roundedRectPath
88
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Email colour contract — semantic web roles resolved to LITERAL hex.
3
+ *
4
+ * HTML email has no cascade worth trusting: Gmail strips <style>, Outlook ignores custom
5
+ * properties, and every colour must appear inline as `#rrggbb` on the element that paints it. The
6
+ * web tokens, meanwhile, are HSL channel triplets (`--primary: 204 100% 39%`) so they can be
7
+ * alpha-composed. This module bridges the two — the triplets are GENERATED from
8
+ * src/tokens/foundation.css (scripts/gen-email-tokens.mjs) and converted here, at module load, by
9
+ * the same `hslToHex` a consumer can call on its own brand roles. There is no hex literal in this
10
+ * directory, so an email palette that drifts from the web palette is structurally impossible.
11
+ */
12
+ import { EMAIL_COLOR_SOURCE } from "./tokens.generated.js";
13
+ /**
14
+ * The email palette slots. Each maps 1:1 to a semantic role in `src/tokens/foundation.css`
15
+ * (`EMAIL_COLOR_SOURCE[token].cssVar` names it), so a re-themed role re-tints the email.
16
+ */
17
+ export type EmailColorToken = keyof typeof EMAIL_COLOR_SOURCE;
18
+ /** A `#rrggbb` string — the only colour form every email client renders identically. */
19
+ export type EmailHex = `#${string}`;
20
+ /**
21
+ * Convert a CSS HSL channel triplet (`"204 100% 39%"` — the shape every `@godxjp/ui` colour role
22
+ * uses) into the `#rrggbb` literal an email client needs. Rounds each channel the way a browser
23
+ * does, so the hex is exactly what `hsl(var(--role))` paints on the web.
24
+ *
25
+ * Consumers with a custom brand role can call this directly:
26
+ * `hslToHex(getComputedStyle(el).getPropertyValue("--primary"))`.
27
+ *
28
+ * @throws if the input is not a bare `H S% L%` triplet (a hex or `hsl(...)` wrapper is rejected
29
+ * rather than silently mis-parsed).
30
+ */
31
+ export declare function hslToHex(triplet: string): EmailHex;
32
+ /**
33
+ * The canonical (light-scheme) transactional-email palette, as literal hex.
34
+ *
35
+ * | slot | web role | used for |
36
+ * | ----------------------------- | --------------------------------- | --------------------------------- |
37
+ * | `background` | `--background` | the page wrapper behind the card |
38
+ * | `foreground` | `--foreground` | body copy |
39
+ * | `surface` / `surfaceForeground`| `--card` / `--card-foreground` | the 480px card |
40
+ * | `muted` / `mutedForeground` | `--muted` / `--muted-foreground` | quiet bands, legal copy |
41
+ * | `border` | `--border` | card edge, footer hairline |
42
+ * | `primary` / `primaryForeground`| `--primary` / `--primary-foreground` | the one CTA |
43
+ * | `focus` | `--ring` | focus affordance in webmail panes |
44
+ * | `brand` / `brandForeground` | `--brand` / `--brand-foreground` | the GoDX capsule + its glyph |
45
+ * | `urgency` / `urgencyForeground`| `--attention` / `--attention-foreground` | action-required accent / solid large label |
46
+ */
47
+ export declare const EMAIL_COLORS: Readonly<Record<EmailColorToken, EmailHex>>;
48
+ /**
49
+ * The dark-scheme palette, derived from the same roles under `.dark` /
50
+ * `:root[data-theme="dark"]`. Use it inside `@media (prefers-color-scheme: dark)` for the clients
51
+ * that honour it — never as the only palette, since most clients do not.
52
+ */
53
+ export declare const EMAIL_COLORS_DARK: Readonly<Record<EmailColorToken, EmailHex>>;