@ctrl-ui/react 0.8.0 → 0.8.1
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 +70 -0
- package/dist/email/templates.d.ts +51 -0
- package/dist/email/templates.js +228 -0
- package/dist/email/theme.d.ts +23 -0
- package/dist/email/theme.js +98 -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 +2 -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.css +49 -0
- 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/color-picker.js +1 -1
- package/dist/ui/dropdown-menu.d.ts +2 -3
- package/dist/ui/dropdown-menu.js +1 -7
- 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 +39 -0
- package/dist/ui/sidebar-menu.js +135 -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,70 @@
|
|
|
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
|
+
fontFamily: {
|
|
10
|
+
body: theme.fonts.body,
|
|
11
|
+
display: theme.fonts.display
|
|
12
|
+
},
|
|
13
|
+
fontSize: Object.fromEntries(Object.entries(theme.text).map(([name, { fontSize, ...options }]) => [name, [fontSize, options]]))
|
|
14
|
+
} }
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function EmailLayout({ theme, preview, children, head, lang = "en", dir = "ltr" }) {
|
|
18
|
+
return /* @__PURE__ */ jsx(Html, {
|
|
19
|
+
lang,
|
|
20
|
+
dir,
|
|
21
|
+
children: /* @__PURE__ */ jsxs(Tailwind, {
|
|
22
|
+
config: tailwindTheme(theme),
|
|
23
|
+
children: [/* @__PURE__ */ jsx(Head, { children: head }), /* @__PURE__ */ jsxs(Body, {
|
|
24
|
+
className: "m-0 bg-background p-4 font-body text-body text-foreground",
|
|
25
|
+
children: [/* @__PURE__ */ jsx(Preview, { children: preview }), /* @__PURE__ */ jsx(Container, {
|
|
26
|
+
className: "mx-auto w-full max-w-[600px] bg-card p-6 text-card-foreground",
|
|
27
|
+
children
|
|
28
|
+
})]
|
|
29
|
+
})]
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const headingClasses = {
|
|
34
|
+
h1: "text-heading-1",
|
|
35
|
+
h2: "text-heading-2",
|
|
36
|
+
h3: "text-heading-3",
|
|
37
|
+
h4: "text-heading-4"
|
|
38
|
+
};
|
|
39
|
+
function EmailHeading({ as = "h1", className = "", ...props }) {
|
|
40
|
+
return /* @__PURE__ */ jsx(Heading, {
|
|
41
|
+
...props,
|
|
42
|
+
as,
|
|
43
|
+
className: `mb-4 mt-0 font-display text-card-foreground ${headingClasses[as]} ${className}`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function EmailText({ tone = "default", className = "", style, ...props }) {
|
|
47
|
+
const textColor = tone === "muted" ? "text-muted-foreground" : "text-card-foreground";
|
|
48
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
49
|
+
...props,
|
|
50
|
+
style: {
|
|
51
|
+
overflowWrap: "break-word",
|
|
52
|
+
...style
|
|
53
|
+
},
|
|
54
|
+
className: `mb-4 mt-0 font-body text-body ${textColor} ${className}`
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function EmailButton({ className = "", ...props }) {
|
|
58
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
59
|
+
...props,
|
|
60
|
+
className: `box-border bg-primary px-5 py-3 text-center font-body text-body font-semibold text-primary-foreground no-underline ${className}`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function EmailLink({ className = "", ...props }) {
|
|
64
|
+
return /* @__PURE__ */ jsx(Link, {
|
|
65
|
+
...props,
|
|
66
|
+
className: `font-body text-primary-text underline ${className}`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { EmailButton, EmailHeading, EmailLayout, EmailLink, EmailText };
|
|
@@ -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 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"
|
|
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"
|
|
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"
|
|
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 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,23 @@
|
|
|
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
|
+
text: Record<EmailHeadingSize | "body" | "caption", EmailTypeStyle>;
|
|
19
|
+
};
|
|
20
|
+
declare function createEmailTheme(tokens: ReadonlyMap<string, string>, rootFontSize?: number): EmailTheme;
|
|
21
|
+
declare function emailThemeFromCss(cssSources: string[], mode?: "light" | "dark", rootFontSize?: number): EmailTheme;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { EmailTheme, createEmailTheme, emailThemeFromCss };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { composite, resolveColor, tokenMaps } from "../scripts/contrast-eval.js";
|
|
2
|
+
//#region src/email/theme.ts
|
|
3
|
+
function resolveVariables(value, tokens, chain = []) {
|
|
4
|
+
if (chain.length > 16) throw new Error(`Email theme: token chain exceeds 16 references (${chain.join(" → ")}).`);
|
|
5
|
+
return value.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*((?:[^()]|\([^()]*\))*))?\)/g, (_, name, fallback) => {
|
|
6
|
+
if (chain.includes(name)) throw new Error(`Email theme: circular reference to ${name}.`);
|
|
7
|
+
const resolved = tokens.get(name) ?? fallback;
|
|
8
|
+
if (resolved === void 0) throw new Error(`Email theme: missing ${name}.`);
|
|
9
|
+
return resolveVariables(resolved, tokens, [...chain, name]);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function tokenValue(name, tokens, fallback) {
|
|
13
|
+
const value = tokens.get(name) ?? fallback;
|
|
14
|
+
if (value === void 0) throw new Error(`Email theme: missing ${name}. Include core theme.css before the skin theme.`);
|
|
15
|
+
const resolved = resolveVariables(value, tokens, [name]).trim();
|
|
16
|
+
if (!resolved || /var\(|[;{}<>]/.test(resolved)) throw new Error(`Email theme: unsupported ${name}: ${value}.`);
|
|
17
|
+
return resolved;
|
|
18
|
+
}
|
|
19
|
+
function pixelSize(name, tokens, rootFontSize) {
|
|
20
|
+
const value = tokenValue(name, tokens);
|
|
21
|
+
const match = /^(\d*\.?\d+)(px|rem)$/.exec(value);
|
|
22
|
+
if (!match) throw new Error(`Email theme: ${name} must resolve to px or rem, received ${value}.`);
|
|
23
|
+
const pixels = Number(match[1]) * (match[2] === "rem" ? rootFontSize : 1);
|
|
24
|
+
if (pixels <= 0) throw new Error(`Email theme: ${name} must be positive.`);
|
|
25
|
+
return `${pixels}px`;
|
|
26
|
+
}
|
|
27
|
+
function numericStyle(name, tokens, fallback) {
|
|
28
|
+
const value = tokenValue(name, tokens, fallback);
|
|
29
|
+
if (!/^\d*\.?\d+(px|em)?$/.test(value)) throw new Error(`Email theme: unsupported ${name}: ${value}.`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function createEmailTheme(tokens, rootFontSize = 16) {
|
|
33
|
+
if (!Number.isFinite(rootFontSize) || rootFontSize <= 0) throw new Error("Email theme: rootFontSize must be a positive pixel value.");
|
|
34
|
+
function color(name) {
|
|
35
|
+
const result = resolveColor(`var(--${name})`, tokens);
|
|
36
|
+
if ("unresolved" in result) throw new Error(`Email theme: --${name}: ${result.unresolved}.`);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
const background = color("background");
|
|
40
|
+
if (background.alpha !== 1) throw new Error("Email theme: --background must be opaque.");
|
|
41
|
+
const card = composite(color("card"), background);
|
|
42
|
+
const primary = composite(color("primary"), card);
|
|
43
|
+
function opaqueColor(name, surface = card) {
|
|
44
|
+
const result = composite(color(name), surface);
|
|
45
|
+
return `rgb(${Math.round(result.r)}, ${Math.round(result.g)}, ${Math.round(result.b)})`;
|
|
46
|
+
}
|
|
47
|
+
function heading(name) {
|
|
48
|
+
const spacing = tokenValue(`--text-${name}--letter-spacing`, tokens, "0");
|
|
49
|
+
if (!/^-?\d*\.?\d+(px|em)?$/.test(spacing)) throw new Error(`Email theme: unsupported ${name} letter spacing: ${spacing}.`);
|
|
50
|
+
return {
|
|
51
|
+
fontSize: pixelSize(`--text-${name}`, tokens, rootFontSize),
|
|
52
|
+
lineHeight: numericStyle(`--text-${name}--line-height`, tokens, "1.3"),
|
|
53
|
+
fontWeight: numericStyle(`--text-${name}--font-weight`, tokens, "600"),
|
|
54
|
+
letterSpacing: spacing
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
colors: {
|
|
59
|
+
background: opaqueColor("background", background),
|
|
60
|
+
foreground: opaqueColor("foreground", background),
|
|
61
|
+
card: opaqueColor("card", background),
|
|
62
|
+
"card-foreground": opaqueColor("card-foreground"),
|
|
63
|
+
primary: opaqueColor("primary"),
|
|
64
|
+
"primary-foreground": opaqueColor("primary-foreground", primary),
|
|
65
|
+
"primary-text": opaqueColor("primary-text"),
|
|
66
|
+
muted: opaqueColor("muted"),
|
|
67
|
+
"muted-foreground": opaqueColor("muted-foreground"),
|
|
68
|
+
border: opaqueColor("border")
|
|
69
|
+
},
|
|
70
|
+
fonts: {
|
|
71
|
+
body: tokenValue("--font-body", tokens),
|
|
72
|
+
display: tokenValue("--font-display", tokens)
|
|
73
|
+
},
|
|
74
|
+
text: {
|
|
75
|
+
body: {
|
|
76
|
+
fontSize: pixelSize("--text-body-lg", tokens, rootFontSize),
|
|
77
|
+
lineHeight: "1.6",
|
|
78
|
+
fontWeight: "400",
|
|
79
|
+
letterSpacing: "0"
|
|
80
|
+
},
|
|
81
|
+
caption: {
|
|
82
|
+
fontSize: pixelSize("--text-body", tokens, rootFontSize),
|
|
83
|
+
lineHeight: "1.5",
|
|
84
|
+
fontWeight: "400",
|
|
85
|
+
letterSpacing: "0"
|
|
86
|
+
},
|
|
87
|
+
"heading-1": heading("heading-1"),
|
|
88
|
+
"heading-2": heading("heading-2"),
|
|
89
|
+
"heading-3": heading("heading-3"),
|
|
90
|
+
"heading-4": heading("heading-4")
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function emailThemeFromCss(cssSources, mode = "light", rootFontSize = 16) {
|
|
95
|
+
return createEmailTheme(tokenMaps(cssSources)[mode], rootFontSize);
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
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;
|