@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.
- package/README.md +1 -0
- package/dist/blocks/coding-agent.js +3 -1
- package/dist/blocks/file-explorer.js +3 -1
- package/dist/blocks/settings.js +3 -1
- package/dist/control-props.d.ts +2 -1
- package/dist/email/email.d.ts +28 -0
- package/dist/email/email.js +79 -0
- package/dist/email/length.d.ts +4 -0
- package/dist/email/length.js +34 -0
- package/dist/email/templates.d.ts +51 -0
- package/dist/email/templates.js +228 -0
- package/dist/email/theme.d.ts +35 -0
- package/dist/email/theme.js +113 -0
- package/dist/extensions/create-track-highlight.d.ts +0 -2
- package/dist/extensions/create-track-highlight.js +49 -8
- package/dist/extensions/supports-anchor-transitions.d.ts +4 -0
- package/dist/extensions/supports-anchor-transitions.js +31 -0
- package/dist/extensions/track-highlight.d.ts +0 -6
- package/dist/extensions/track-highlight.js +25 -9
- package/dist/knob-contracts/button-knobs.d.ts +1 -1
- package/dist/knob-contracts/button-knobs.js +4 -1
- package/dist/knob-contracts/progressive-blur-knobs.d.ts +5 -0
- package/dist/knob-contracts/progressive-blur-knobs.js +9 -0
- package/dist/scripts/contrast-eval.d.ts +46 -0
- package/dist/scripts/contrast-eval.js +327 -0
- package/dist/skin.config.js +9 -1
- package/dist/skin.d.ts +11 -9
- package/dist/skin.js +0 -1
- package/dist/styles/index.css +3 -1
- package/dist/styles/recipes/button-group.css +46 -0
- package/dist/styles/recipes/button.css +53 -5
- package/dist/styles/recipes/progressive-blur.css +145 -0
- package/dist/styles/recipes/scroll-area.css +140 -5
- package/dist/styles/recipes/sidebar-menu.css +156 -0
- package/dist/styles/recipes/sidebar.css +25 -69
- package/dist/styles/recipes/table-of-contents.css +1 -1
- package/dist/styles/recipes/track-highlight.css +126 -2
- package/dist/styles/recipes/tree.css +4 -0
- package/dist/ui/button-group.d.ts +3 -1
- package/dist/ui/button-group.js +21 -18
- package/dist/ui/button.d.ts +3 -1
- package/dist/ui/button.js +13 -4
- package/dist/ui/checkbox-group.d.ts +5 -3
- package/dist/ui/checkbox-group.js +23 -6
- package/dist/ui/combobox.d.ts +2 -0
- package/dist/ui/dropdown-menu.d.ts +28 -21
- package/dist/ui/dropdown-menu.js +112 -13
- package/dist/ui/progressive-blur.d.ts +12 -0
- package/dist/ui/progressive-blur.js +47 -0
- package/dist/ui/scroll-area.d.ts +4 -1
- package/dist/ui/scroll-area.js +27 -4
- package/dist/ui/sidebar-menu.d.ts +49 -0
- package/dist/ui/sidebar-menu.js +160 -0
- package/dist/ui/sidebar-provider.d.ts +28 -0
- package/dist/ui/sidebar-provider.js +80 -0
- package/dist/ui/sidebar.d.ts +4 -61
- package/dist/ui/sidebar.js +23 -202
- package/dist/ui/table-of-contents.js +3 -11
- package/dist/ui/toggle.d.ts +3 -2
- package/dist/ui/toggle.js +22 -10
- package/dist/ui/tree.js +13 -19
- 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 {
|
|
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 {
|
|
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";
|
package/dist/blocks/settings.js
CHANGED
|
@@ -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 {
|
|
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";
|
package/dist/control-props.d.ts
CHANGED
|
@@ -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,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;
|