@ctrl-ui/react 0.8.0 → 0.8.2

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 (62) hide show
  1. package/README.md +1 -0
  2. package/dist/blocks/coding-agent.js +3 -1
  3. package/dist/blocks/file-explorer.js +3 -1
  4. package/dist/blocks/settings.js +3 -1
  5. package/dist/control-props.d.ts +2 -1
  6. package/dist/email/email.d.ts +28 -0
  7. package/dist/email/email.js +79 -0
  8. package/dist/email/length.d.ts +4 -0
  9. package/dist/email/length.js +34 -0
  10. package/dist/email/templates.d.ts +51 -0
  11. package/dist/email/templates.js +228 -0
  12. package/dist/email/theme.d.ts +35 -0
  13. package/dist/email/theme.js +113 -0
  14. package/dist/extensions/create-track-highlight.d.ts +0 -2
  15. package/dist/extensions/create-track-highlight.js +49 -8
  16. package/dist/extensions/supports-anchor-transitions.d.ts +4 -0
  17. package/dist/extensions/supports-anchor-transitions.js +31 -0
  18. package/dist/extensions/track-highlight.d.ts +0 -6
  19. package/dist/extensions/track-highlight.js +25 -9
  20. package/dist/knob-contracts/button-knobs.d.ts +1 -1
  21. package/dist/knob-contracts/button-knobs.js +4 -1
  22. package/dist/knob-contracts/progressive-blur-knobs.d.ts +5 -0
  23. package/dist/knob-contracts/progressive-blur-knobs.js +9 -0
  24. package/dist/scripts/contrast-eval.d.ts +46 -0
  25. package/dist/scripts/contrast-eval.js +327 -0
  26. package/dist/skin.config.js +9 -1
  27. package/dist/skin.d.ts +11 -9
  28. package/dist/skin.js +0 -1
  29. package/dist/styles/index.css +3 -1
  30. package/dist/styles/recipes/button-group.css +46 -0
  31. package/dist/styles/recipes/button.css +53 -5
  32. package/dist/styles/recipes/progressive-blur.css +145 -0
  33. package/dist/styles/recipes/scroll-area.css +140 -5
  34. package/dist/styles/recipes/sidebar-menu.css +156 -0
  35. package/dist/styles/recipes/sidebar.css +25 -69
  36. package/dist/styles/recipes/table-of-contents.css +1 -1
  37. package/dist/styles/recipes/track-highlight.css +126 -2
  38. package/dist/styles/recipes/tree.css +4 -0
  39. package/dist/ui/button-group.d.ts +3 -1
  40. package/dist/ui/button-group.js +21 -18
  41. package/dist/ui/button.d.ts +3 -1
  42. package/dist/ui/button.js +13 -4
  43. package/dist/ui/checkbox-group.d.ts +5 -3
  44. package/dist/ui/checkbox-group.js +23 -6
  45. package/dist/ui/combobox.d.ts +2 -0
  46. package/dist/ui/dropdown-menu.d.ts +28 -21
  47. package/dist/ui/dropdown-menu.js +112 -13
  48. package/dist/ui/progressive-blur.d.ts +12 -0
  49. package/dist/ui/progressive-blur.js +47 -0
  50. package/dist/ui/scroll-area.d.ts +4 -1
  51. package/dist/ui/scroll-area.js +27 -4
  52. package/dist/ui/sidebar-menu.d.ts +49 -0
  53. package/dist/ui/sidebar-menu.js +160 -0
  54. package/dist/ui/sidebar-provider.d.ts +28 -0
  55. package/dist/ui/sidebar-provider.js +80 -0
  56. package/dist/ui/sidebar.d.ts +4 -61
  57. package/dist/ui/sidebar.js +23 -202
  58. package/dist/ui/table-of-contents.js +3 -11
  59. package/dist/ui/toggle.d.ts +3 -2
  60. package/dist/ui/toggle.js +22 -10
  61. package/dist/ui/tree.js +13 -19
  62. package/package.json +5 -1
package/README.md CHANGED
@@ -68,3 +68,4 @@ Install a peer only when you import the component that needs it.
68
68
  | `ui/phone-input` | `react-phone-number-input`, `libphonenumber-js`, `zod` |
69
69
  | `ui/resizable` | `react-resizable-panels` |
70
70
  | `chat-composer` | `prosemirror-*` |
71
+ | `email/email`, `email/templates` | `react-email` |
@@ -3,7 +3,9 @@ import { cn } from "../lib/cn.js";
3
3
  import { Button } from "../ui/button.js";
4
4
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
5
5
  import { ChatLayout, ChatThread } from "../chat-layout.js";
6
- import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarRail, SidebarTrigger, useSidebar } from "../ui/sidebar.js";
6
+ import { SidebarProvider, useSidebar } from "../ui/sidebar-provider.js";
7
+ import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar-menu.js";
8
+ import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarInset, SidebarRail, SidebarTrigger } from "../ui/sidebar.js";
7
9
  import { jsx, jsxs } from "react/jsx-runtime";
8
10
  import { BotIcon, ChevronRightIcon, CircleIcon, FolderIcon, PlusIcon, SearchIcon } from "lucide-react";
9
11
  //#region src/blocks/coding-agent.tsx
@@ -3,7 +3,9 @@ import { cn } from "../lib/cn.js";
3
3
  import { Button } from "../ui/button.js";
4
4
  import { ScrollArea } from "../ui/scroll-area.js";
5
5
  import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group.js";
6
- import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarRail, SidebarTrigger, useSidebar } from "../ui/sidebar.js";
6
+ import { SidebarProvider, useSidebar } from "../ui/sidebar-provider.js";
7
+ import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar-menu.js";
8
+ import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarInset, SidebarRail, SidebarTrigger } from "../ui/sidebar.js";
7
9
  import { groupFileExplorerItems, resolveFileExplorer, searchFileExplorer } from "./file-explorer-data.js";
8
10
  import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty.js";
9
11
  import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "../ui/resizable.js";
@@ -3,7 +3,9 @@ import { cn } from "../lib/cn.js";
3
3
  import { Button } from "../ui/button.js";
4
4
  import { ScrollArea } from "../ui/scroll-area.js";
5
5
  import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group.js";
6
- import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger, useSidebar } from "../ui/sidebar.js";
6
+ import { SidebarProvider, useSidebar } from "../ui/sidebar-provider.js";
7
+ import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar-menu.js";
8
+ import { Sidebar, SidebarContent, SidebarHeader, SidebarInset, SidebarTrigger } from "../ui/sidebar.js";
7
9
  import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty.js";
8
10
  import { searchSettings } from "./settings-data.js";
9
11
  import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet } from "../ui/field.js";
@@ -23,6 +23,7 @@ type OpenChangeEventDetails = {
23
23
  trigger: Element | undefined;
24
24
  };
25
25
  type SelectionIndicator = "none" | "slide";
26
+ type HoverIndicator = "none" | "hover";
26
27
  declare const SIDEBAR_COOKIE_NAME = "sidebar_state";
27
28
  //#endregion
28
- export { ControlledChoice, ControlledMultiChoice, FormSubmitEvent, OpenChangeEventDetails, OpenChangeReason, RenderProp, SIDEBAR_COOKIE_NAME, SelectionIndicator };
29
+ export { ControlledChoice, ControlledMultiChoice, FormSubmitEvent, HoverIndicator, OpenChangeEventDetails, OpenChangeReason, RenderProp, SIDEBAR_COOKIE_NAME, SelectionIndicator };
@@ -0,0 +1,28 @@
1
+ import { EmailTheme } from "./theme.js";
2
+ import { ReactNode } from "react";
3
+ import { ButtonProps, HeadingProps, LinkProps, TextProps } from "react-email";
4
+ //#region src/email/email.d.ts
5
+ declare function EmailLayout({ theme, preview, children, head, lang, dir }: {
6
+ theme: EmailTheme;
7
+ preview: string;
8
+ children: ReactNode;
9
+ head?: ReactNode;
10
+ lang?: string;
11
+ dir?: "ltr" | "rtl";
12
+ }): import("react").JSX.Element;
13
+ declare const headingClasses: {
14
+ h1: string;
15
+ h2: string;
16
+ h3: string;
17
+ h4: string;
18
+ };
19
+ declare function EmailHeading({ as, className, ...props }: Omit<HeadingProps, "as"> & {
20
+ as?: keyof typeof headingClasses;
21
+ }): import("react").JSX.Element;
22
+ declare function EmailText({ tone, className, style, ...props }: TextProps & {
23
+ tone?: "default" | "muted";
24
+ }): import("react").JSX.Element;
25
+ declare function EmailButton({ className, ...props }: ButtonProps): import("react").JSX.Element;
26
+ declare function EmailLink({ className, ...props }: LinkProps): import("react").JSX.Element;
27
+ //#endregion
28
+ export { EmailButton, EmailHeading, EmailLayout, EmailLink, EmailText };
@@ -0,0 +1,79 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Body, Button, Container, Head, Heading, Html, Link, Preview, Tailwind, Text, pixelBasedPreset } from "react-email";
3
+ //#region src/email/email.tsx
4
+ function tailwindTheme(theme) {
5
+ return {
6
+ presets: [pixelBasedPreset],
7
+ theme: { extend: {
8
+ colors: theme.colors,
9
+ borderRadius: theme.radii,
10
+ minHeight: { control: theme.button.height },
11
+ padding: {
12
+ "control-x": theme.button.paddingInline,
13
+ "control-y": theme.button.paddingBlock
14
+ },
15
+ fontFamily: {
16
+ body: theme.fonts.body,
17
+ display: theme.fonts.display
18
+ },
19
+ fontSize: {
20
+ ...Object.fromEntries(Object.entries(theme.text).map(([name, { fontSize, ...options }]) => [name, [fontSize, options]])),
21
+ control: [theme.button.fontSize, { lineHeight: theme.button.lineHeight }]
22
+ }
23
+ } }
24
+ };
25
+ }
26
+ function EmailLayout({ theme, preview, children, head, lang = "en", dir = "ltr" }) {
27
+ return /* @__PURE__ */ jsx(Html, {
28
+ lang,
29
+ dir,
30
+ children: /* @__PURE__ */ jsxs(Tailwind, {
31
+ config: tailwindTheme(theme),
32
+ children: [/* @__PURE__ */ jsx(Head, { children: head }), /* @__PURE__ */ jsxs(Body, {
33
+ className: "m-0 bg-background p-4 font-body text-body text-foreground",
34
+ children: [/* @__PURE__ */ jsx(Preview, { children: preview }), /* @__PURE__ */ jsx(Container, {
35
+ className: "mx-auto w-full max-w-[600px] rounded-panel bg-card p-6 text-card-foreground",
36
+ children
37
+ })]
38
+ })]
39
+ })
40
+ });
41
+ }
42
+ const headingClasses = {
43
+ h1: "text-heading-1",
44
+ h2: "text-heading-2",
45
+ h3: "text-heading-3",
46
+ h4: "text-heading-4"
47
+ };
48
+ function EmailHeading({ as = "h1", className = "", ...props }) {
49
+ return /* @__PURE__ */ jsx(Heading, {
50
+ ...props,
51
+ as,
52
+ className: `mb-4 mt-0 font-display text-card-foreground ${headingClasses[as]} ${className}`
53
+ });
54
+ }
55
+ function EmailText({ tone = "default", className = "", style, ...props }) {
56
+ const textColor = tone === "muted" ? "text-muted-foreground" : "text-card-foreground";
57
+ return /* @__PURE__ */ jsx(Text, {
58
+ ...props,
59
+ style: {
60
+ overflowWrap: "break-word",
61
+ ...style
62
+ },
63
+ className: `mb-4 mt-0 font-body text-body ${textColor} ${className}`
64
+ });
65
+ }
66
+ function EmailButton({ className = "", ...props }) {
67
+ return /* @__PURE__ */ jsx(Button, {
68
+ ...props,
69
+ className: `box-border min-h-control rounded-control bg-primary px-control-x py-control-y text-center font-body text-control font-medium text-primary-foreground no-underline ${className}`
70
+ });
71
+ }
72
+ function EmailLink({ className = "", ...props }) {
73
+ return /* @__PURE__ */ jsx(Link, {
74
+ ...props,
75
+ className: `font-body text-primary-text underline ${className}`
76
+ });
77
+ }
78
+ //#endregion
79
+ export { EmailButton, EmailHeading, EmailLayout, EmailLink, EmailText };
@@ -0,0 +1,4 @@
1
+ //#region src/email/length.d.ts
2
+ declare function emailLengthPixels(name: string, value: string, rootFontSize: number, allowZero?: boolean): number;
3
+ //#endregion
4
+ export { emailLengthPixels };
@@ -0,0 +1,34 @@
1
+ //#region src/email/length.ts
2
+ function parseLengthQuantity(value, rootFontSize) {
3
+ const match = /^(-?\d*\.?\d+)(px|rem)?$/.exec(value.trim());
4
+ if (!match) return null;
5
+ return {
6
+ amount: Number(match[1]) * (match[2] === "rem" ? rootFontSize : 1),
7
+ unit: match[2] ? "px" : ""
8
+ };
9
+ }
10
+ function calculateLengthExpression(expression, rootFontSize) {
11
+ const match = /^\s*(-?\d*\.?\d+(?:px|rem)?)\s*([+*/-])\s*(-?\d*\.?\d+(?:px|rem)?)\s*$/.exec(expression);
12
+ if (!match) return null;
13
+ const left = parseLengthQuantity(match[1], rootFontSize);
14
+ const right = parseLengthQuantity(match[3], rootFontSize);
15
+ if (!left || !right) return null;
16
+ const operator = match[2];
17
+ if ((operator === "+" || operator === "-") && left.unit === right.unit) return `${left.amount + (operator === "+" ? right.amount : -right.amount)}${left.unit}`;
18
+ if (operator === "*" && !(left.unit && right.unit)) return `${left.amount * right.amount}${left.unit || right.unit}`;
19
+ if (operator === "/" && !right.unit && right.amount !== 0) return `${left.amount / right.amount}${left.unit}`;
20
+ return null;
21
+ }
22
+ function emailLengthPixels(name, value, rootFontSize, allowZero = false) {
23
+ let resolved = value;
24
+ for (let depth = 0; depth < 16 && resolved.includes("calc("); depth++) {
25
+ const simplified = resolved.replace(/calc\(([^()]*)\)/g, (original, expression) => calculateLengthExpression(expression, rootFontSize) ?? original);
26
+ if (simplified === resolved) break;
27
+ resolved = simplified;
28
+ }
29
+ const result = parseLengthQuantity(resolved, rootFontSize);
30
+ if (!(result && (result.unit === "px" || result.amount === 0) && Number.isFinite(result.amount) && (allowZero ? result.amount >= 0 : result.amount > 0))) throw new Error(`Email theme: ${name} must resolve to a ${allowZero ? "non-negative" : "positive"} px/rem length, received ${value}.`);
31
+ return result.amount;
32
+ }
33
+ //#endregion
34
+ export { emailLengthPixels };
@@ -0,0 +1,51 @@
1
+ import { EmailTheme } from "./theme.js";
2
+ //#region src/email/templates.d.ts
3
+ type EmailBrandProps = {
4
+ theme: EmailTheme;
5
+ brand: string;
6
+ footer: string;
7
+ };
8
+ declare function InvitationEmail({ theme, brand, footer, inviter, workspace, inviteUrl }: EmailBrandProps & {
9
+ inviter: string;
10
+ workspace: string;
11
+ inviteUrl: string;
12
+ }): import("react").JSX.Element;
13
+ declare function ProductEmail({ theme, brand, footer, title, description, imageUrl, imageAlt, actionUrl }: EmailBrandProps & {
14
+ title: string;
15
+ description: string;
16
+ imageUrl: string;
17
+ imageAlt: string;
18
+ actionUrl: string;
19
+ }): import("react").JSX.Element;
20
+ declare function EditorialEmail({ theme, brand, footer, title, description, imageUrl, imageAlt, articleUrl }: EmailBrandProps & {
21
+ title: string;
22
+ description: string;
23
+ imageUrl: string;
24
+ imageAlt: string;
25
+ articleUrl: string;
26
+ }): import("react").JSX.Element;
27
+ type EmailArticle = {
28
+ title: string;
29
+ description: string;
30
+ href: string;
31
+ imageUrl: string;
32
+ imageAlt: string;
33
+ };
34
+ declare function NewsletterEmail({ theme, brand, footer, articles, unsubscribeUrl }: EmailBrandProps & {
35
+ articles: EmailArticle[];
36
+ unsubscribeUrl: string;
37
+ }): import("react").JSX.Element;
38
+ declare function SummaryEmail({ theme, brand, footer, period, metrics, details, dashboardUrl }: EmailBrandProps & {
39
+ period: string;
40
+ metrics: {
41
+ label: string;
42
+ value: string;
43
+ }[];
44
+ details: {
45
+ label: string;
46
+ value: string;
47
+ }[];
48
+ dashboardUrl: string;
49
+ }): import("react").JSX.Element;
50
+ //#endregion
51
+ export { EditorialEmail, EmailArticle, InvitationEmail, NewsletterEmail, ProductEmail, SummaryEmail };
@@ -0,0 +1,228 @@
1
+ import { EmailButton, EmailHeading, EmailLayout, EmailLink, EmailText } from "./email.js";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Column, Hr, Img, Row, Section } from "react-email";
4
+ //#region src/email/templates.tsx
5
+ function EmailFooter({ brand, children }) {
6
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
7
+ /* @__PURE__ */ jsx(Hr, { className: "my-6 border-0 border-t border-solid border-t-border" }),
8
+ /* @__PURE__ */ jsx(EmailText, {
9
+ tone: "muted",
10
+ className: "mb-1 text-caption",
11
+ children: brand
12
+ }),
13
+ /* @__PURE__ */ jsx(EmailText, {
14
+ tone: "muted",
15
+ className: "mb-0 text-caption",
16
+ children
17
+ })
18
+ ] });
19
+ }
20
+ function InvitationEmail({ theme, brand, footer, inviter, workspace, inviteUrl }) {
21
+ return /* @__PURE__ */ jsxs(EmailLayout, {
22
+ theme,
23
+ preview: `${inviter} invited you to ${workspace}`,
24
+ children: [
25
+ /* @__PURE__ */ jsx(EmailText, {
26
+ tone: "muted",
27
+ className: "mb-8 text-caption",
28
+ children: brand
29
+ }),
30
+ /* @__PURE__ */ jsx(EmailHeading, { children: "A place for your next idea." }),
31
+ /* @__PURE__ */ jsxs(EmailText, { children: [
32
+ inviter,
33
+ " invited you to join ",
34
+ /* @__PURE__ */ jsx("strong", { children: workspace }),
35
+ ". Bring your work, share a little inspiration, and make something together."
36
+ ] }),
37
+ /* @__PURE__ */ jsxs(Section, {
38
+ className: "my-6 rounded-panel bg-muted p-5",
39
+ children: [/* @__PURE__ */ jsx(EmailHeading, {
40
+ as: "h3",
41
+ children: "Your team is waiting"
42
+ }), /* @__PURE__ */ jsx(EmailText, {
43
+ className: "mb-0",
44
+ children: "Projects, conversations, and the details that move work forward. All in one shared space."
45
+ })]
46
+ }),
47
+ /* @__PURE__ */ jsxs(EmailButton, {
48
+ href: inviteUrl,
49
+ children: ["Join ", workspace]
50
+ }),
51
+ /* @__PURE__ */ jsx(EmailFooter, {
52
+ brand,
53
+ children: footer
54
+ })
55
+ ]
56
+ });
57
+ }
58
+ function ProductEmail({ theme, brand, footer, title, description, imageUrl, imageAlt, actionUrl }) {
59
+ return /* @__PURE__ */ jsxs(EmailLayout, {
60
+ theme,
61
+ preview: title,
62
+ children: [
63
+ /* @__PURE__ */ jsxs(EmailText, {
64
+ tone: "muted",
65
+ className: "mb-6 text-caption",
66
+ children: [brand, " / NEW RELEASE"]
67
+ }),
68
+ /* @__PURE__ */ jsx(Img, {
69
+ src: imageUrl,
70
+ alt: imageAlt,
71
+ width: "552",
72
+ className: "mb-6 block h-auto w-full rounded-scene"
73
+ }),
74
+ /* @__PURE__ */ jsx(EmailHeading, { children: title }),
75
+ /* @__PURE__ */ jsx(EmailText, { children: description }),
76
+ /* @__PURE__ */ jsx(EmailButton, {
77
+ href: actionUrl,
78
+ children: "Explore what’s new"
79
+ }),
80
+ /* @__PURE__ */ jsx(EmailFooter, {
81
+ brand,
82
+ children: footer
83
+ })
84
+ ]
85
+ });
86
+ }
87
+ function EditorialEmail({ theme, brand, footer, title, description, imageUrl, imageAlt, articleUrl }) {
88
+ return /* @__PURE__ */ jsxs(EmailLayout, {
89
+ theme,
90
+ preview: title,
91
+ children: [
92
+ /* @__PURE__ */ jsxs(EmailText, {
93
+ tone: "muted",
94
+ className: "mb-6 text-caption",
95
+ children: [brand, " / IN GOOD COMPANY"]
96
+ }),
97
+ /* @__PURE__ */ jsx(EmailHeading, { children: "Fresh perspectives." }),
98
+ /* @__PURE__ */ jsxs(Row, { children: [/* @__PURE__ */ jsx(Column, {
99
+ width: "36%",
100
+ className: "align-top",
101
+ children: /* @__PURE__ */ jsx(Img, {
102
+ src: imageUrl,
103
+ alt: imageAlt,
104
+ width: "198",
105
+ className: "block h-auto w-full rounded-scene"
106
+ })
107
+ }), /* @__PURE__ */ jsxs(Column, {
108
+ width: "64%",
109
+ className: "pl-5 align-top",
110
+ children: [
111
+ /* @__PURE__ */ jsx(EmailHeading, {
112
+ as: "h2",
113
+ children: title
114
+ }),
115
+ /* @__PURE__ */ jsx(EmailText, { children: description }),
116
+ /* @__PURE__ */ jsx(EmailLink, {
117
+ href: articleUrl,
118
+ children: "Read the story"
119
+ })
120
+ ]
121
+ })] }),
122
+ /* @__PURE__ */ jsx(EmailFooter, {
123
+ brand,
124
+ children: footer
125
+ })
126
+ ]
127
+ });
128
+ }
129
+ function NewsletterEmail({ theme, brand, footer, articles, unsubscribeUrl }) {
130
+ return /* @__PURE__ */ jsxs(EmailLayout, {
131
+ theme,
132
+ preview: "A few things worth making time for.",
133
+ children: [
134
+ /* @__PURE__ */ jsxs(EmailText, {
135
+ tone: "muted",
136
+ className: "mb-6 text-caption",
137
+ children: [brand, " / THE WEEKLY EDIT"]
138
+ }),
139
+ /* @__PURE__ */ jsx(EmailHeading, { children: "A little room for inspiration." }),
140
+ /* @__PURE__ */ jsx(EmailText, {
141
+ tone: "muted",
142
+ children: "A few things worth making time for. Stories, spaces, and ideas from our community."
143
+ }),
144
+ articles.map((article) => /* @__PURE__ */ jsxs(Section, {
145
+ className: "mb-6",
146
+ children: [
147
+ /* @__PURE__ */ jsx(Img, {
148
+ src: article.imageUrl,
149
+ alt: article.imageAlt,
150
+ width: "552",
151
+ className: "mb-4 block h-auto w-full rounded-scene"
152
+ }),
153
+ /* @__PURE__ */ jsx(EmailHeading, {
154
+ as: "h2",
155
+ children: article.title
156
+ }),
157
+ /* @__PURE__ */ jsx(EmailText, { children: article.description }),
158
+ /* @__PURE__ */ jsx(EmailLink, {
159
+ href: article.href,
160
+ children: "Read the story"
161
+ })
162
+ ]
163
+ }, article.href)),
164
+ /* @__PURE__ */ jsx(EmailFooter, {
165
+ brand,
166
+ children: footer
167
+ }),
168
+ /* @__PURE__ */ jsx(EmailLink, {
169
+ href: unsubscribeUrl,
170
+ className: "text-caption",
171
+ children: "Unsubscribe"
172
+ })
173
+ ]
174
+ });
175
+ }
176
+ function SummaryEmail({ theme, brand, footer, period, metrics, details, dashboardUrl }) {
177
+ return /* @__PURE__ */ jsxs(EmailLayout, {
178
+ theme,
179
+ preview: `Your team’s progress · ${period}`,
180
+ children: [
181
+ /* @__PURE__ */ jsxs(EmailText, {
182
+ tone: "muted",
183
+ className: "mb-6 text-caption",
184
+ children: [
185
+ brand,
186
+ " / ",
187
+ period
188
+ ]
189
+ }),
190
+ /* @__PURE__ */ jsx(EmailHeading, { children: "Good work adds up." }),
191
+ /* @__PURE__ */ jsx(EmailText, { children: "Here’s what your team moved forward this week." }),
192
+ /* @__PURE__ */ jsx(Section, {
193
+ className: "my-6 rounded-panel bg-muted p-4",
194
+ children: /* @__PURE__ */ jsx(Row, {
195
+ className: "table-fixed",
196
+ children: metrics.map((metric) => /* @__PURE__ */ jsxs(Column, {
197
+ className: "px-2 align-top",
198
+ children: [/* @__PURE__ */ jsx(EmailText, {
199
+ className: "mb-1 text-heading-2 font-semibold",
200
+ children: metric.value
201
+ }), /* @__PURE__ */ jsx(EmailText, {
202
+ tone: "muted",
203
+ className: "mb-0 text-caption",
204
+ children: metric.label
205
+ })]
206
+ }, metric.label))
207
+ })
208
+ }),
209
+ details.map((detail) => /* @__PURE__ */ jsxs(Row, {
210
+ className: "table-fixed",
211
+ children: [/* @__PURE__ */ jsx(Column, { children: /* @__PURE__ */ jsx(EmailText, { children: detail.label }) }), /* @__PURE__ */ jsx(Column, {
212
+ align: "right",
213
+ children: /* @__PURE__ */ jsx(EmailText, { children: detail.value })
214
+ })]
215
+ }, detail.label)),
216
+ /* @__PURE__ */ jsx(EmailButton, {
217
+ href: dashboardUrl,
218
+ children: "View your workspace"
219
+ }),
220
+ /* @__PURE__ */ jsx(EmailFooter, {
221
+ brand,
222
+ children: footer
223
+ })
224
+ ]
225
+ });
226
+ }
227
+ //#endregion
228
+ export { EditorialEmail, InvitationEmail, NewsletterEmail, ProductEmail, SummaryEmail };
@@ -0,0 +1,35 @@
1
+ //#region src/email/theme.d.ts
2
+ declare const emailColors: readonly ["background", "foreground", "card", "card-foreground", "primary", "primary-foreground", "primary-text", "muted", "muted-foreground", "border"];
3
+ declare const headingSizes: readonly ["heading-1", "heading-2", "heading-3", "heading-4"];
4
+ type EmailColor = (typeof emailColors)[number];
5
+ type EmailHeadingSize = (typeof headingSizes)[number];
6
+ type EmailTypeStyle = {
7
+ fontSize: string;
8
+ lineHeight: string;
9
+ fontWeight: string;
10
+ letterSpacing: string;
11
+ };
12
+ type EmailTheme = {
13
+ colors: Record<EmailColor, string>;
14
+ fonts: {
15
+ body: string;
16
+ display: string;
17
+ };
18
+ radii: {
19
+ control: string;
20
+ panel: string;
21
+ scene: string;
22
+ };
23
+ button: {
24
+ height: string;
25
+ paddingInline: string;
26
+ paddingBlock: string;
27
+ fontSize: string;
28
+ lineHeight: string;
29
+ };
30
+ text: Record<EmailHeadingSize | "body" | "caption", EmailTypeStyle>;
31
+ };
32
+ declare function createEmailTheme(tokens: ReadonlyMap<string, string>, rootFontSize?: number): EmailTheme;
33
+ declare function emailThemeFromCss(cssSources: string[], mode?: "light" | "dark", rootFontSize?: number): EmailTheme;
34
+ //#endregion
35
+ export { EmailTheme, createEmailTheme, emailThemeFromCss };
@@ -0,0 +1,113 @@
1
+ import { emailLengthPixels } from "./length.js";
2
+ import { composite, resolveColor, tokenMaps } from "../scripts/contrast-eval.js";
3
+ //#region src/email/theme.ts
4
+ function resolveVariables(value, tokens, chain = []) {
5
+ if (chain.length > 16) throw new Error(`Email theme: token chain exceeds 16 references (${chain.join(" → ")}).`);
6
+ return value.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*((?:[^()]|\([^()]*\))*))?\)/g, (_, name, fallback) => {
7
+ if (chain.includes(name)) throw new Error(`Email theme: circular reference to ${name}.`);
8
+ const resolved = tokens.get(name) ?? fallback;
9
+ if (resolved === void 0) throw new Error(`Email theme: missing ${name}.`);
10
+ return resolveVariables(resolved, tokens, [...chain, name]);
11
+ });
12
+ }
13
+ function tokenValue(name, tokens, fallback) {
14
+ const value = tokens.get(name) ?? fallback;
15
+ if (value === void 0) throw new Error(`Email theme: missing ${name}. Include core theme.css before the skin theme.`);
16
+ const resolved = resolveVariables(value, tokens, [name]).trim();
17
+ if (!resolved || /var\(|[;{}<>]/.test(resolved)) throw new Error(`Email theme: unsupported ${name}: ${value}.`);
18
+ return resolved;
19
+ }
20
+ function pixelSize(name, tokens, rootFontSize) {
21
+ return `${emailLengthPixels(name, tokenValue(name, tokens), rootFontSize)}px`;
22
+ }
23
+ function numericStyle(name, tokens, fallback) {
24
+ const value = tokenValue(name, tokens, fallback);
25
+ if (!/^\d*\.?\d+(px|em)?$/.test(value)) throw new Error(`Email theme: unsupported ${name}: ${value}.`);
26
+ return value;
27
+ }
28
+ function createEmailTheme(tokens, rootFontSize = 16) {
29
+ if (!Number.isFinite(rootFontSize) || rootFontSize <= 0) throw new Error("Email theme: rootFontSize must be a positive pixel value.");
30
+ function lengthPixels(name, allowZero = false) {
31
+ return emailLengthPixels(name, tokenValue(name, tokens), rootFontSize, allowZero);
32
+ }
33
+ const buttonHeight = lengthPixels("--control-h-md");
34
+ const buttonFontSize = lengthPixels("--text-body");
35
+ const reactEmailButtonLineHeight = buttonFontSize * 1.2;
36
+ const buttonPaddingBlock = Math.max(0, (buttonHeight - reactEmailButtonLineHeight) / 2);
37
+ function color(name) {
38
+ const result = resolveColor(`var(--${name})`, tokens);
39
+ if ("unresolved" in result) throw new Error(`Email theme: --${name}: ${result.unresolved}.`);
40
+ return result;
41
+ }
42
+ const background = color("background");
43
+ if (background.alpha !== 1) throw new Error("Email theme: --background must be opaque.");
44
+ const card = composite(color("card"), background);
45
+ const primary = composite(color("primary"), card);
46
+ function opaqueColor(name, surface = card) {
47
+ const result = composite(color(name), surface);
48
+ return `rgb(${Math.round(result.r)}, ${Math.round(result.g)}, ${Math.round(result.b)})`;
49
+ }
50
+ function heading(name) {
51
+ const spacing = tokenValue(`--text-${name}--letter-spacing`, tokens, "0");
52
+ if (!/^-?\d*\.?\d+(px|em)?$/.test(spacing)) throw new Error(`Email theme: unsupported ${name} letter spacing: ${spacing}.`);
53
+ return {
54
+ fontSize: pixelSize(`--text-${name}`, tokens, rootFontSize),
55
+ lineHeight: numericStyle(`--text-${name}--line-height`, tokens, "1.3"),
56
+ fontWeight: numericStyle(`--text-${name}--font-weight`, tokens, "600"),
57
+ letterSpacing: spacing
58
+ };
59
+ }
60
+ return {
61
+ colors: {
62
+ background: opaqueColor("background", background),
63
+ foreground: opaqueColor("foreground", background),
64
+ card: opaqueColor("card", background),
65
+ "card-foreground": opaqueColor("card-foreground"),
66
+ primary: opaqueColor("primary"),
67
+ "primary-foreground": opaqueColor("primary-foreground", primary),
68
+ "primary-text": opaqueColor("primary-text"),
69
+ muted: opaqueColor("muted"),
70
+ "muted-foreground": opaqueColor("muted-foreground"),
71
+ border: opaqueColor("border")
72
+ },
73
+ fonts: {
74
+ body: tokenValue("--font-body", tokens),
75
+ display: tokenValue("--font-display", tokens)
76
+ },
77
+ radii: {
78
+ control: `${lengthPixels("--radius-control", true)}px`,
79
+ panel: `${lengthPixels("--radius-panel", true)}px`,
80
+ scene: `${lengthPixels("--radius-scene", true)}px`
81
+ },
82
+ button: {
83
+ height: `${buttonHeight}px`,
84
+ paddingInline: `${lengthPixels("--padding-x", true)}px`,
85
+ paddingBlock: `${buttonPaddingBlock}px`,
86
+ fontSize: `${buttonFontSize}px`,
87
+ lineHeight: `${reactEmailButtonLineHeight}px`
88
+ },
89
+ text: {
90
+ body: {
91
+ fontSize: pixelSize("--text-body-lg", tokens, rootFontSize),
92
+ lineHeight: "1.6",
93
+ fontWeight: "400",
94
+ letterSpacing: "0"
95
+ },
96
+ caption: {
97
+ fontSize: pixelSize("--text-body", tokens, rootFontSize),
98
+ lineHeight: "1.5",
99
+ fontWeight: "400",
100
+ letterSpacing: "0"
101
+ },
102
+ "heading-1": heading("heading-1"),
103
+ "heading-2": heading("heading-2"),
104
+ "heading-3": heading("heading-3"),
105
+ "heading-4": heading("heading-4")
106
+ }
107
+ };
108
+ }
109
+ function emailThemeFromCss(cssSources, mode = "light", rootFontSize = 16) {
110
+ return createEmailTheme(tokenMaps(cssSources)[mode], rootFontSize);
111
+ }
112
+ //#endregion
113
+ export { createEmailTheme, emailThemeFromCss };
@@ -1,8 +1,6 @@
1
1
  //#region src/extensions/create-track-highlight.d.ts
2
2
  type TrackHighlightOptions = {
3
- /** Selector for highlightable rows, resolved within track. */
4
3
  itemSelector: string;
5
- /** Selector for resting row (active/selected) used when nothing is hovered. */
6
4
  activeSelector: string;
7
5
  range?: boolean;
8
6
  followHover?: boolean;