@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,37 @@
1
+ import { EMAIL_COLOR_SOURCE, EMAIL_COLOR_SOURCE_DARK } from "./tokens.generated.js";
2
+ const TRIPLET = /^\s*(-?\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%\s*$/;
3
+ const channel = (n) => Math.round(Math.min(255, Math.max(0, n * 255))).toString(16).padStart(2, "0");
4
+ function hslToHex(triplet) {
5
+ const parsed = TRIPLET.exec(triplet);
6
+ if (!parsed) {
7
+ throw new TypeError(
8
+ `hslToHex expects a bare HSL channel triplet like "204 100% 39%", received "${triplet}".`
9
+ );
10
+ }
11
+ const hue = (Number(parsed[1]) % 360 + 360) % 360 / 60;
12
+ const saturation = Number(parsed[2]) / 100;
13
+ const lightness = Number(parsed[3]) / 100;
14
+ const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
15
+ const second = chroma * (1 - Math.abs(hue % 2 - 1));
16
+ const base = lightness - chroma / 2;
17
+ 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];
18
+ return `#${channel(r + base)}${channel(g + base)}${channel(b + base)}`;
19
+ }
20
+ function resolve(source) {
21
+ const out = {};
22
+ for (const [token, entry] of Object.entries(source)) {
23
+ out[token] = hslToHex(entry.hsl);
24
+ }
25
+ return Object.freeze(out);
26
+ }
27
+ const EMAIL_COLORS = resolve(
28
+ EMAIL_COLOR_SOURCE
29
+ );
30
+ const EMAIL_COLORS_DARK = resolve(
31
+ EMAIL_COLOR_SOURCE_DARK
32
+ );
33
+ export {
34
+ EMAIL_COLORS,
35
+ EMAIL_COLORS_DARK,
36
+ hslToHex
37
+ };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Email geometry + typography contract — the `--email-*` component tokens resolved to the two
3
+ * forms an email template actually needs: a CSS string (`"480px"`, for `style="width:480px"`) and a
4
+ * bare number (`480`, for the legacy HTML attribute form `<table width="480">` that Outlook's Word
5
+ * renderer still prefers). Both come from src/tokens/components/email.css via
6
+ * scripts/gen-email-tokens.mjs — nothing here is hand-typed.
7
+ */
8
+ import { EMAIL_GEOMETRY_SOURCE } from "./tokens.generated.js";
9
+ /** Every `--email-*` token name declared in the component token tier. */
10
+ export type EmailTokenName = keyof typeof EMAIL_GEOMETRY_SOURCE;
11
+ /** The 480px transactional shell — the card and the page gutter around it. */
12
+ export interface EmailShellTokens {
13
+ /** Card width as a CSS length. */
14
+ readonly width: string;
15
+ /** Card width as a number, for `<table width="…">`. */
16
+ readonly widthPx: number;
17
+ /** Width of the column INSIDE the card padding (`width − 2 × padding`). */
18
+ readonly contentWidth: string;
19
+ readonly contentWidthPx: number;
20
+ /** Card inset on desktop. */
21
+ readonly padding: string;
22
+ readonly paddingPx: number;
23
+ /** Gutter between the client viewport and the card. */
24
+ readonly pagePadding: string;
25
+ readonly pagePaddingPx: number;
26
+ /** Card edge hairline. */
27
+ readonly borderWidth: string;
28
+ readonly borderWidthPx: number;
29
+ /** Card corner radius (ignored by Outlook's Word renderer — degrade to square, never to an image). */
30
+ readonly radius: string;
31
+ readonly radiusPx: number;
32
+ /** Vertical rhythm between blocks. */
33
+ readonly gap: string;
34
+ readonly gapPx: number;
35
+ /** Tight rhythm (heading ↔ lede). */
36
+ readonly gapSm: string;
37
+ readonly gapSmPx: number;
38
+ /**
39
+ * Height the canonical invitation reference card measures at this geometry (480×407). A
40
+ * visual-regression target, NOT a layout input — never set a fixed height on an email card.
41
+ */
42
+ readonly referenceHeight: string;
43
+ readonly referenceHeightPx: number;
44
+ }
45
+ export declare const EMAIL_SHELL: EmailShellTokens;
46
+ /** Type ramp. Sizes are literal px — no email client resolves the rem scale reliably. */
47
+ export interface EmailTypographyTokens {
48
+ /**
49
+ * The canonical DXS stack, M PLUS 2 first. Web-font `@font-face` is unavailable in most clients,
50
+ * so this NAMES the face and degrades: a client with M PLUS 2 installed (or a webmail that
51
+ * already loaded it) renders the canonical face; everything else falls to Hiragino (macOS/iOS) →
52
+ * Yu Gothic (Windows) → Noto Sans JP → Meiryo → the system UI face → Arial → `sans-serif`. Family
53
+ * names are SINGLE-quoted so the value drops straight into a double-quoted `style="…"` attribute.
54
+ */
55
+ readonly fontFamily: string;
56
+ /** Mono stack for invoice ids, masked card numbers, ISO dates and amounts. */
57
+ readonly monoFontFamily: string;
58
+ readonly bodyFontSize: string;
59
+ readonly bodyFontSizePx: number;
60
+ readonly bodyLineHeight: number;
61
+ readonly bodyFontWeight: number;
62
+ readonly headingFontSize: string;
63
+ readonly headingFontSizePx: number;
64
+ readonly headingLineHeight: number;
65
+ readonly headingFontWeight: number;
66
+ }
67
+ export declare const EMAIL_TYPOGRAPHY: EmailTypographyTokens;
68
+ /** The single primary call-to-action. */
69
+ export interface EmailCtaTokens {
70
+ readonly height: string;
71
+ readonly heightPx: number;
72
+ /** Equal to the height so the label centres without flexbox (Outlook has none). */
73
+ readonly lineHeight: string;
74
+ readonly lineHeightPx: number;
75
+ readonly paddingX: string;
76
+ readonly paddingXPx: number;
77
+ readonly radius: string;
78
+ readonly radiusPx: number;
79
+ readonly fontSize: string;
80
+ readonly fontSizePx: number;
81
+ readonly fontWeight: number;
82
+ }
83
+ export declare const EMAIL_CTA: EmailCtaTokens;
84
+ /** The legal band under the body — quiet type, a hairline, and the inter-link spacing. */
85
+ export interface EmailFooterTokens {
86
+ readonly fontSize: string;
87
+ readonly fontSizePx: number;
88
+ readonly lineHeight: number;
89
+ /** Horizontal separation between adjacent footer links. */
90
+ readonly linkGap: string;
91
+ readonly linkGapPx: number;
92
+ readonly paddingTop: string;
93
+ readonly paddingTopPx: number;
94
+ readonly borderWidth: string;
95
+ readonly borderWidthPx: number;
96
+ }
97
+ export declare const EMAIL_FOOTER: EmailFooterTokens;
98
+ /** Focus affordance for the webmail preview panes that DO honour `:focus` (WCAG 2.4.7/2.4.11). */
99
+ export interface EmailFocusTokens {
100
+ readonly borderWidth: string;
101
+ readonly borderWidthPx: number;
102
+ }
103
+ export declare const EMAIL_FOCUS: EmailFocusTokens;
104
+ /** Narrow-viewport reflow — apply under `@media (max-width: …)` and via `width="100%"` fallbacks. */
105
+ export interface EmailMobileTokens {
106
+ /** The reflow breakpoint (card width + both gutters). */
107
+ readonly maxWidth: string;
108
+ readonly maxWidthPx: number;
109
+ /** The card goes fluid below the breakpoint. */
110
+ readonly width: string;
111
+ readonly padding: string;
112
+ readonly paddingPx: number;
113
+ readonly pagePadding: string;
114
+ readonly pagePaddingPx: number;
115
+ readonly headingFontSize: string;
116
+ readonly headingFontSizePx: number;
117
+ /** The CTA goes full-bleed so the tap target spans the card. */
118
+ readonly ctaWidth: string;
119
+ }
120
+ export declare const EMAIL_MOBILE: EmailMobileTokens;
@@ -0,0 +1,90 @@
1
+ import { EMAIL_GEOMETRY_SOURCE } from "./tokens.generated.js";
2
+ const raw = (name) => EMAIL_GEOMETRY_SOURCE[name];
3
+ const num = (name) => {
4
+ const value = EMAIL_GEOMETRY_SOURCE[name];
5
+ const parsed = Number.parseFloat(value);
6
+ if (Number.isNaN(parsed)) {
7
+ throw new TypeError(`Email token ${name} ("${value}") has no numeric value.`);
8
+ }
9
+ return parsed;
10
+ };
11
+ const EMAIL_SHELL = Object.freeze({
12
+ width: raw("--email-shell-width"),
13
+ widthPx: num("--email-shell-width"),
14
+ contentWidth: `${num("--email-shell-width") - 2 * num("--email-shell-padding")}px`,
15
+ contentWidthPx: num("--email-shell-width") - 2 * num("--email-shell-padding"),
16
+ padding: raw("--email-shell-padding"),
17
+ paddingPx: num("--email-shell-padding"),
18
+ pagePadding: raw("--email-shell-page-padding"),
19
+ pagePaddingPx: num("--email-shell-page-padding"),
20
+ borderWidth: raw("--email-shell-border-width"),
21
+ borderWidthPx: num("--email-shell-border-width"),
22
+ radius: raw("--email-card-radius"),
23
+ radiusPx: num("--email-card-radius"),
24
+ gap: raw("--email-stack-gap"),
25
+ gapPx: num("--email-stack-gap"),
26
+ gapSm: raw("--email-stack-gap-sm"),
27
+ gapSmPx: num("--email-stack-gap-sm"),
28
+ referenceHeight: raw("--email-card-reference-height"),
29
+ referenceHeightPx: num("--email-card-reference-height")
30
+ });
31
+ const EMAIL_TYPOGRAPHY = Object.freeze({
32
+ fontFamily: raw("--email-font-family-sans"),
33
+ monoFontFamily: raw("--email-font-family-mono"),
34
+ bodyFontSize: raw("--email-body-font-size"),
35
+ bodyFontSizePx: num("--email-body-font-size"),
36
+ bodyLineHeight: num("--email-body-line-height"),
37
+ bodyFontWeight: num("--email-body-font-weight"),
38
+ headingFontSize: raw("--email-heading-font-size"),
39
+ headingFontSizePx: num("--email-heading-font-size"),
40
+ headingLineHeight: num("--email-heading-line-height"),
41
+ headingFontWeight: num("--email-heading-font-weight")
42
+ });
43
+ const EMAIL_CTA = Object.freeze({
44
+ height: raw("--email-cta-height"),
45
+ heightPx: num("--email-cta-height"),
46
+ lineHeight: raw("--email-cta-line-height"),
47
+ lineHeightPx: num("--email-cta-line-height"),
48
+ paddingX: raw("--email-cta-padding-x"),
49
+ paddingXPx: num("--email-cta-padding-x"),
50
+ radius: raw("--email-cta-radius"),
51
+ radiusPx: num("--email-cta-radius"),
52
+ fontSize: raw("--email-cta-font-size"),
53
+ fontSizePx: num("--email-cta-font-size"),
54
+ fontWeight: num("--email-cta-font-weight")
55
+ });
56
+ const EMAIL_FOOTER = Object.freeze({
57
+ fontSize: raw("--email-footer-font-size"),
58
+ fontSizePx: num("--email-footer-font-size"),
59
+ lineHeight: num("--email-footer-line-height"),
60
+ linkGap: raw("--email-footer-link-gap"),
61
+ linkGapPx: num("--email-footer-link-gap"),
62
+ paddingTop: raw("--email-footer-padding-top"),
63
+ paddingTopPx: num("--email-footer-padding-top"),
64
+ borderWidth: raw("--email-footer-border-width"),
65
+ borderWidthPx: num("--email-footer-border-width")
66
+ });
67
+ const EMAIL_FOCUS = Object.freeze({
68
+ borderWidth: raw("--email-focus-border-width"),
69
+ borderWidthPx: num("--email-focus-border-width")
70
+ });
71
+ const EMAIL_MOBILE = Object.freeze({
72
+ maxWidth: raw("--email-mobile-max-width"),
73
+ maxWidthPx: num("--email-mobile-max-width"),
74
+ width: raw("--email-mobile-width"),
75
+ padding: raw("--email-mobile-padding"),
76
+ paddingPx: num("--email-mobile-padding"),
77
+ pagePadding: raw("--email-mobile-page-padding"),
78
+ pagePaddingPx: num("--email-mobile-page-padding"),
79
+ headingFontSize: raw("--email-mobile-heading-font-size"),
80
+ headingFontSizePx: num("--email-mobile-heading-font-size"),
81
+ ctaWidth: raw("--email-mobile-cta-width")
82
+ });
83
+ export {
84
+ EMAIL_CTA,
85
+ EMAIL_FOCUS,
86
+ EMAIL_FOOTER,
87
+ EMAIL_MOBILE,
88
+ EMAIL_SHELL,
89
+ EMAIL_TYPOGRAPHY
90
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `@godxjp/ui/email` — the email-safe design-token contract for transactional templates (issue #227).
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * A transactional email is rendered by Blade/Twig/MJML, not React, and by clients that strip
7
+ * `<style>`, ignore CSS custom properties, refuse relative units and block remote images. So the
8
+ * two things a template usually reaches for — the component library and the CSS token file — are
9
+ * both unavailable, and templates end up hand-copying hex and hand-drawing the logo. That is
10
+ * exactly the drift this module removes: it is a FRAMEWORK-NEUTRAL, zero-dependency, React-free
11
+ * data export of literal `#rrggbb` / `px` values plus the canonical brand-mark markup.
12
+ *
13
+ * ## The no-drift guarantee
14
+ *
15
+ * Nothing here is hand-typed. `src/tokens/foundation.css` (colour roles) and
16
+ * `src/tokens/components/email.css` (geometry/typography) are read by
17
+ * `scripts/gen-email-tokens.mjs` into `tokens.generated.ts`, and the HSL→hex conversion runs at
18
+ * module load. There is no hex literal anywhere in `src/email/`, so an email palette that diverges
19
+ * from the web palette is not something a maintainer can accidentally write.
20
+ * `src/email/__tests__/email-tokens.test.ts` re-derives every value straight from the CSS and
21
+ * asserts equality, and `pnpm check:email-token-sync` fails CI if the generated file goes stale.
22
+ *
23
+ * ## Usage
24
+ *
25
+ * ```ts
26
+ * import { EMAIL_COLORS, EMAIL_URGENCY, EMAIL_SHELL, EMAIL_CTA, EMAIL_BRAND_MARK, emailInlineStyle } from "@godxjp/ui/email";
27
+ *
28
+ * const card = emailInlineStyle({
29
+ * width: EMAIL_SHELL.width,
30
+ * padding: EMAIL_SHELL.padding,
31
+ * backgroundColor: EMAIL_COLORS.surface,
32
+ * border: `${EMAIL_SHELL.borderWidth} solid ${EMAIL_COLORS.border}`,
33
+ * borderTopColor: EMAIL_URGENCY.accent,
34
+ * borderRadius: EMAIL_SHELL.radius,
35
+ * });
36
+ * ```
37
+ *
38
+ * A non-JS template engine (Blade, Twig, Liquid) consumes the same values through
39
+ * `EMAIL_TOKENS_JSON` — dump it once in the build step and read the JSON from PHP:
40
+ *
41
+ * ```sh
42
+ * node --input-type=module -e \
43
+ * 'import("@godxjp/ui/email").then(m => process.stdout.write(m.EMAIL_TOKENS_JSON))' \
44
+ * > resources/design/email-tokens.json
45
+ * ```
46
+ *
47
+ * ## Scope
48
+ *
49
+ * Tokens, geometry and brand markup only. Copy, recipients, links, locale, triggers and queue
50
+ * behaviour belong to the sending application — this module knows nothing about them.
51
+ */
52
+ export { hslToHex, EMAIL_COLORS, EMAIL_COLORS_DARK, type EmailColorToken, type EmailHex, } from "./color.js";
53
+ export { EMAIL_SHELL, EMAIL_TYPOGRAPHY, EMAIL_CTA, EMAIL_FOOTER, EMAIL_FOCUS, EMAIL_MOBILE, type EmailTokenName, type EmailShellTokens, type EmailTypographyTokens, type EmailCtaTokens, type EmailFooterTokens, type EmailFocusTokens, type EmailMobileTokens, } from "./geometry.js";
54
+ export { EMAIL_BRAND_MARK, EMAIL_BRAND_MARK_VIEWBOX, EMAIL_BRAND_MARK_CAPSULE, EMAIL_BRAND_MARK_GLYPH, emailBrandMarkSvg, emailBrandMarkDataUri, emailBrandMarkTableHtml, roundedRectPath, type EmailBrandMark, type EmailBrandMarkOptions, type EmailBrandMarkRect, } from "./brand-mark.js";
55
+ export { emailInlineStyle, type EmailStyleDeclarations } from "./inline-style.js";
56
+ export { EMAIL_URGENCY, EMAIL_URGENCY_DARK, type EmailUrgencyTokens } from "./urgency.js";
57
+ export { EMAIL_COLOR_SOURCE, EMAIL_COLOR_SOURCE_DARK, EMAIL_GEOMETRY_SOURCE, type EmailColorSource, } from "./tokens.generated.js";
58
+ import { EMAIL_COLORS, EMAIL_COLORS_DARK } from "./color.js";
59
+ import { EMAIL_SHELL, EMAIL_TYPOGRAPHY, EMAIL_CTA, EMAIL_FOOTER, EMAIL_FOCUS, EMAIL_MOBILE } from "./geometry.js";
60
+ import { EMAIL_BRAND_MARK } from "./brand-mark.js";
61
+ import { EMAIL_URGENCY, EMAIL_URGENCY_DARK } from "./urgency.js";
62
+ /** The whole contract in one object — the shape `EMAIL_TOKENS_JSON` serialises. */
63
+ export interface EmailTokens {
64
+ readonly colors: typeof EMAIL_COLORS;
65
+ readonly colorsDark: typeof EMAIL_COLORS_DARK;
66
+ readonly urgency: typeof EMAIL_URGENCY;
67
+ readonly urgencyDark: typeof EMAIL_URGENCY_DARK;
68
+ readonly shell: typeof EMAIL_SHELL;
69
+ readonly typography: typeof EMAIL_TYPOGRAPHY;
70
+ readonly cta: typeof EMAIL_CTA;
71
+ readonly footer: typeof EMAIL_FOOTER;
72
+ readonly focus: typeof EMAIL_FOCUS;
73
+ readonly mobile: typeof EMAIL_MOBILE;
74
+ readonly brandMark: typeof EMAIL_BRAND_MARK;
75
+ }
76
+ /** Every email token, grouped. Import a single group when you only need one. */
77
+ export declare const EMAIL_TOKENS: EmailTokens;
78
+ /**
79
+ * `EMAIL_TOKENS` as pretty-printed JSON — the framework-neutral bridge for template engines that
80
+ * cannot import ES modules (Blade, Twig, Liquid, Handlebars on another runtime). Write it to a
81
+ * file in the build step and read it from the template layer.
82
+ */
83
+ export declare const EMAIL_TOKENS_JSON: string;
@@ -0,0 +1,82 @@
1
+ import {
2
+ hslToHex,
3
+ EMAIL_COLORS,
4
+ EMAIL_COLORS_DARK
5
+ } from "./color.js";
6
+ import {
7
+ EMAIL_SHELL,
8
+ EMAIL_TYPOGRAPHY,
9
+ EMAIL_CTA,
10
+ EMAIL_FOOTER,
11
+ EMAIL_FOCUS,
12
+ EMAIL_MOBILE
13
+ } from "./geometry.js";
14
+ import {
15
+ EMAIL_BRAND_MARK,
16
+ EMAIL_BRAND_MARK_VIEWBOX,
17
+ EMAIL_BRAND_MARK_CAPSULE,
18
+ EMAIL_BRAND_MARK_GLYPH,
19
+ emailBrandMarkSvg,
20
+ emailBrandMarkDataUri,
21
+ emailBrandMarkTableHtml,
22
+ roundedRectPath
23
+ } from "./brand-mark.js";
24
+ import { emailInlineStyle } from "./inline-style.js";
25
+ import { EMAIL_URGENCY, EMAIL_URGENCY_DARK } from "./urgency.js";
26
+ import {
27
+ EMAIL_COLOR_SOURCE,
28
+ EMAIL_COLOR_SOURCE_DARK,
29
+ EMAIL_GEOMETRY_SOURCE
30
+ } from "./tokens.generated.js";
31
+ import { EMAIL_COLORS as EMAIL_COLORS2, EMAIL_COLORS_DARK as EMAIL_COLORS_DARK2 } from "./color.js";
32
+ import {
33
+ EMAIL_SHELL as EMAIL_SHELL2,
34
+ EMAIL_TYPOGRAPHY as EMAIL_TYPOGRAPHY2,
35
+ EMAIL_CTA as EMAIL_CTA2,
36
+ EMAIL_FOOTER as EMAIL_FOOTER2,
37
+ EMAIL_FOCUS as EMAIL_FOCUS2,
38
+ EMAIL_MOBILE as EMAIL_MOBILE2
39
+ } from "./geometry.js";
40
+ import { EMAIL_BRAND_MARK as EMAIL_BRAND_MARK2 } from "./brand-mark.js";
41
+ import { EMAIL_URGENCY as EMAIL_URGENCY2, EMAIL_URGENCY_DARK as EMAIL_URGENCY_DARK2 } from "./urgency.js";
42
+ const EMAIL_TOKENS = Object.freeze({
43
+ colors: EMAIL_COLORS2,
44
+ colorsDark: EMAIL_COLORS_DARK2,
45
+ urgency: EMAIL_URGENCY2,
46
+ urgencyDark: EMAIL_URGENCY_DARK2,
47
+ shell: EMAIL_SHELL2,
48
+ typography: EMAIL_TYPOGRAPHY2,
49
+ cta: EMAIL_CTA2,
50
+ footer: EMAIL_FOOTER2,
51
+ focus: EMAIL_FOCUS2,
52
+ mobile: EMAIL_MOBILE2,
53
+ brandMark: EMAIL_BRAND_MARK2
54
+ });
55
+ const EMAIL_TOKENS_JSON = JSON.stringify(EMAIL_TOKENS, null, 2);
56
+ export {
57
+ EMAIL_BRAND_MARK,
58
+ EMAIL_BRAND_MARK_CAPSULE,
59
+ EMAIL_BRAND_MARK_GLYPH,
60
+ EMAIL_BRAND_MARK_VIEWBOX,
61
+ EMAIL_COLORS,
62
+ EMAIL_COLORS_DARK,
63
+ EMAIL_COLOR_SOURCE,
64
+ EMAIL_COLOR_SOURCE_DARK,
65
+ EMAIL_CTA,
66
+ EMAIL_FOCUS,
67
+ EMAIL_FOOTER,
68
+ EMAIL_GEOMETRY_SOURCE,
69
+ EMAIL_MOBILE,
70
+ EMAIL_SHELL,
71
+ EMAIL_TOKENS,
72
+ EMAIL_TOKENS_JSON,
73
+ EMAIL_TYPOGRAPHY,
74
+ EMAIL_URGENCY,
75
+ EMAIL_URGENCY_DARK,
76
+ emailBrandMarkDataUri,
77
+ emailBrandMarkSvg,
78
+ emailBrandMarkTableHtml,
79
+ emailInlineStyle,
80
+ hslToHex,
81
+ roundedRectPath
82
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Inline-style serialisation for HTML email.
3
+ *
4
+ * Email clients do not run the cascade: Gmail strips `<style>`, Outlook resolves no custom
5
+ * property, and only the `style=""` attribute on the painting element is universally honoured.
6
+ * `emailInlineStyle` turns a declaration map into that attribute value AND enforces the three rules
7
+ * that silently break templates — no `var()` (nothing resolves it), no `calc()` (Outlook's Word
8
+ * renderer drops the whole declaration) and no `"` (it closes the `style="…"` attribute mid-value,
9
+ * which is how a double-quoted font family corrupts a whole tag). Failing loudly at template-build
10
+ * time beats a broken layout discovered in Mailpit.
11
+ */
12
+ /** A declaration map. Numbers are emitted verbatim (already-unitless values like `font-weight`). */
13
+ export type EmailStyleDeclarations = Record<string, string | number | null | undefined>;
14
+ /**
15
+ * Serialise declarations into an inline `style` attribute value.
16
+ *
17
+ * ```ts
18
+ * emailInlineStyle({ width: EMAIL_SHELL.width, backgroundColor: EMAIL_COLORS.surface })
19
+ * // "width:480px;background-color:#fdfdfb"
20
+ * ```
21
+ *
22
+ * `null` / `undefined` values are dropped, so a conditional declaration needs no branch.
23
+ *
24
+ * @throws if a value contains `var()` or `calc()` (neither survives an email client) or a double
25
+ * quote (it terminates the `style="…"` attribute).
26
+ */
27
+ export declare function emailInlineStyle(declarations: EmailStyleDeclarations): string;
@@ -0,0 +1,26 @@
1
+ const UNRESOLVABLE = /\bvar\(|\bcalc\(/;
2
+ function emailInlineStyle(declarations) {
3
+ const parts = [];
4
+ for (const [property, value] of Object.entries(declarations)) {
5
+ if (value === null || value === void 0 || value === "") continue;
6
+ const serialised = String(value);
7
+ if (UNRESOLVABLE.test(serialised)) {
8
+ throw new TypeError(
9
+ `emailInlineStyle: "${property}: ${serialised}" uses var()/calc(), which no email client resolves. Pass the literal value from the @godxjp/ui/email token export instead.`
10
+ );
11
+ }
12
+ if (serialised.includes('"')) {
13
+ throw new TypeError(
14
+ `emailInlineStyle: "${property}: ${serialised}" contains a double quote, which terminates the style="\u2026" attribute it is written into. Use single quotes \u2014 a quoted font family from EMAIL_TYPOGRAPHY already ships that way.`
15
+ );
16
+ }
17
+ parts.push(`${kebab(property)}:${serialised}`);
18
+ }
19
+ return parts.join(";");
20
+ }
21
+ function kebab(property) {
22
+ return property.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
23
+ }
24
+ export {
25
+ emailInlineStyle
26
+ };
@@ -0,0 +1,167 @@
1
+ /** One email colour slot and the web role it is derived from. `hsl` is the raw channel triplet. */
2
+ export interface EmailColorSource {
3
+ /** The semantic role in src/tokens/foundation.css this slot mirrors. */
4
+ readonly cssVar: string;
5
+ /** The role's HSL channel triplet, verbatim (`H S% L%`). Converted to hex at module load. */
6
+ readonly hsl: string;
7
+ }
8
+ /** Light-scheme colour sources — the canonical transactional-email palette. */
9
+ export declare const EMAIL_COLOR_SOURCE: {
10
+ readonly background: {
11
+ readonly cssVar: "--background";
12
+ readonly hsl: "60 33% 99%";
13
+ };
14
+ readonly foreground: {
15
+ readonly cssVar: "--foreground";
16
+ readonly hsl: "48 8% 13%";
17
+ };
18
+ readonly surface: {
19
+ readonly cssVar: "--card";
20
+ readonly hsl: "60 33% 99%";
21
+ };
22
+ readonly surfaceForeground: {
23
+ readonly cssVar: "--card-foreground";
24
+ readonly hsl: "48 8% 13%";
25
+ };
26
+ readonly muted: {
27
+ readonly cssVar: "--muted";
28
+ readonly hsl: "45 15% 95%";
29
+ };
30
+ readonly mutedForeground: {
31
+ readonly cssVar: "--muted-foreground";
32
+ readonly hsl: "44 5% 42%";
33
+ };
34
+ readonly border: {
35
+ readonly cssVar: "--border";
36
+ readonly hsl: "30 7% 83%";
37
+ };
38
+ readonly primary: {
39
+ readonly cssVar: "--primary";
40
+ readonly hsl: "204 100% 39%";
41
+ };
42
+ readonly primaryForeground: {
43
+ readonly cssVar: "--primary-foreground";
44
+ readonly hsl: "60 33% 99%";
45
+ };
46
+ readonly focus: {
47
+ readonly cssVar: "--ring";
48
+ readonly hsl: "204 100% 39%";
49
+ };
50
+ readonly brand: {
51
+ readonly cssVar: "--brand";
52
+ readonly hsl: "160.5 100% 29.6%";
53
+ };
54
+ readonly brandForeground: {
55
+ readonly cssVar: "--brand-foreground";
56
+ readonly hsl: "60 33% 99%";
57
+ };
58
+ readonly urgency: {
59
+ readonly cssVar: "--attention";
60
+ readonly hsl: "25 99% 46%";
61
+ };
62
+ readonly urgencyForeground: {
63
+ readonly cssVar: "--attention-foreground";
64
+ readonly hsl: "60 33% 99%";
65
+ };
66
+ };
67
+ /** Dark-scheme colour sources, for `@media (prefers-color-scheme: dark)` overrides. */
68
+ export declare const EMAIL_COLOR_SOURCE_DARK: {
69
+ readonly background: {
70
+ readonly cssVar: "--background";
71
+ readonly hsl: "48 9% 9%";
72
+ };
73
+ readonly foreground: {
74
+ readonly cssVar: "--foreground";
75
+ readonly hsl: "60 20% 96%";
76
+ };
77
+ readonly surface: {
78
+ readonly cssVar: "--card";
79
+ readonly hsl: "48 8% 12%";
80
+ };
81
+ readonly surfaceForeground: {
82
+ readonly cssVar: "--card-foreground";
83
+ readonly hsl: "60 20% 96%";
84
+ };
85
+ readonly muted: {
86
+ readonly cssVar: "--muted";
87
+ readonly hsl: "45 6% 18%";
88
+ };
89
+ readonly mutedForeground: {
90
+ readonly cssVar: "--muted-foreground";
91
+ readonly hsl: "44 6% 64%";
92
+ };
93
+ readonly border: {
94
+ readonly cssVar: "--border";
95
+ readonly hsl: "45 6% 22%";
96
+ };
97
+ readonly primary: {
98
+ readonly cssVar: "--primary";
99
+ readonly hsl: "204 90% 60%";
100
+ };
101
+ readonly primaryForeground: {
102
+ readonly cssVar: "--primary-foreground";
103
+ readonly hsl: "48 9% 9%";
104
+ };
105
+ readonly focus: {
106
+ readonly cssVar: "--ring";
107
+ readonly hsl: "204 90% 60%";
108
+ };
109
+ readonly brand: {
110
+ readonly cssVar: "--brand";
111
+ readonly hsl: "160.5 100% 36%";
112
+ };
113
+ readonly brandForeground: {
114
+ readonly cssVar: "--brand-foreground";
115
+ readonly hsl: "48 9% 9%";
116
+ };
117
+ readonly urgency: {
118
+ readonly cssVar: "--attention";
119
+ readonly hsl: "25 95% 56%";
120
+ };
121
+ readonly urgencyForeground: {
122
+ readonly cssVar: "--attention-foreground";
123
+ readonly hsl: "48 9% 9%";
124
+ };
125
+ };
126
+ /** Raw `--email-*` declarations from the component token tier, verbatim. */
127
+ export declare const EMAIL_GEOMETRY_SOURCE: {
128
+ readonly "--email-shell-width": "480px";
129
+ readonly "--email-shell-padding": "32px";
130
+ readonly "--email-shell-page-padding": "24px";
131
+ readonly "--email-shell-border-width": "1px";
132
+ readonly "--email-card-radius": "10px";
133
+ readonly "--email-card-reference-height": "407px";
134
+ readonly "--email-stack-gap": "24px";
135
+ readonly "--email-stack-gap-sm": "12px";
136
+ readonly "--email-mark-width": "22px";
137
+ readonly "--email-mark-height": "22px";
138
+ readonly "--email-mark-gap": "8px";
139
+ readonly "--email-wordmark-font-size": "13px";
140
+ readonly "--email-wordmark-font-weight": "700";
141
+ readonly "--email-font-family-sans": "'M PLUS 2', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic Medium', YuGothic, 'Noto Sans JP', Meiryo, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, system-ui, Arial, sans-serif";
142
+ readonly "--email-font-family-mono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
143
+ readonly "--email-body-font-size": "14px";
144
+ readonly "--email-body-line-height": "1.9";
145
+ readonly "--email-body-font-weight": "400";
146
+ readonly "--email-heading-font-size": "17px";
147
+ readonly "--email-heading-line-height": "1.7";
148
+ readonly "--email-heading-font-weight": "500";
149
+ readonly "--email-cta-height": "36px";
150
+ readonly "--email-cta-line-height": "36px";
151
+ readonly "--email-cta-padding-x": "16px";
152
+ readonly "--email-cta-radius": "6px";
153
+ readonly "--email-cta-font-size": "14px";
154
+ readonly "--email-cta-font-weight": "500";
155
+ readonly "--email-footer-font-size": "11px";
156
+ readonly "--email-footer-line-height": "1.8";
157
+ readonly "--email-footer-link-gap": "12px";
158
+ readonly "--email-footer-padding-top": "20px";
159
+ readonly "--email-footer-border-width": "1px";
160
+ readonly "--email-focus-border-width": "2px";
161
+ readonly "--email-mobile-max-width": "520px";
162
+ readonly "--email-mobile-width": "100%";
163
+ readonly "--email-mobile-padding": "20px";
164
+ readonly "--email-mobile-page-padding": "12px";
165
+ readonly "--email-mobile-heading-font-size": "16px";
166
+ readonly "--email-mobile-cta-width": "100%";
167
+ };