diff --git a/apps/admin-x-settings/src/components/settings/email-design/design-utils.ts b/apps/admin-x-settings/src/components/settings/email-design/design-utils.ts index ebf0958b296..340e8f5fe7d 100644 --- a/apps/admin-x-settings/src/components/settings/email-design/design-utils.ts +++ b/apps/admin-x-settings/src/components/settings/email-design/design-utils.ts @@ -122,3 +122,20 @@ export function resolveAllColors(settings: EmailDesignSettings, accentColor: str secondaryHeaderTextColor }; } + +export function resolveFontFamily(category: string | undefined) { + return category === 'serif' ? 'Georgia, serif' : '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; +} + +export function resolveButtonCorners(corners: string | undefined): string { + switch (corners) { + case 'square': return 'rounded-none'; + case 'pill': return 'rounded-full'; + case 'rounded': + default: return 'rounded-[6px]'; + } +} + +export function resolveImageCorners(corners: string | undefined): string { + return corners === 'rounded' ? 'rounded-md' : ''; +} diff --git a/apps/admin-x-settings/src/components/settings/email-design/email-preview.tsx b/apps/admin-x-settings/src/components/settings/email-design/email-preview.tsx index 4290046376c..28695e04420 100644 --- a/apps/admin-x-settings/src/components/settings/email-design/email-preview.tsx +++ b/apps/admin-x-settings/src/components/settings/email-design/email-preview.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {GhostOrb, cn} from '@tryghost/shade'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; -import {resolveAllColors} from './design-utils'; +import {resolveAllColors, resolveImageCorners} from './design-utils'; import {useGlobalData} from '../../providers/global-data-provider'; import type {EmailDesignSettings} from './types'; @@ -18,25 +18,6 @@ interface EmailPreviewProps { children?: React.ReactNode; } -// --- Helper functions --- - -export function resolveFontFamily(category: string | undefined) { - return category === 'serif' ? 'Georgia, serif' : '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; -} - -export function resolveButtonCorners(corners: string | undefined): string { - switch (corners) { - case 'square': return 'rounded-none'; - case 'pill': return 'rounded-full'; - case 'rounded': - default: return 'rounded-[6px]'; - } -} - -export function resolveImageCorners(corners: string | undefined): string { - return corners === 'rounded' ? 'rounded-md' : ''; -} - // --- Sub-components --- const EnvelopeHeader: React.FC<{senderName?: string; senderEmail?: string; subject?: string}> = ({senderName, senderEmail, subject}) => { @@ -45,7 +26,7 @@ const EnvelopeHeader: React.FC<{senderName?: string; senderEmail?: string; subje } return ( -
+
{senderName && (
{senderName} @@ -69,8 +50,7 @@ const PublicationHeader: React.FC<{ siteTitle?: string; backgroundColor?: string; textColor: string; - fontFamily: string; -}> = ({showTitle, siteTitle, backgroundColor, textColor, fontFamily}) => { +}> = ({showTitle, siteTitle, backgroundColor, textColor}) => { if (!showTitle || !siteTitle) { return null; } @@ -82,7 +62,7 @@ const PublicationHeader: React.FC<{ >

{siteTitle}

@@ -123,11 +103,10 @@ const EmailPreview: React.FC = ({settings, senderName, sender const accentColor = siteData.accent_color; const colors = resolveAllColors(settings, accentColor); - const titleFont = resolveFontFamily(settings.title_font_category); const imageCornerClass = resolveImageCorners(settings.image_corners); return ( -
+
= ({settings, senderName, sender = ({value, onChange}) => className="flex h-24 items-center justify-center p-0 text-sm" onDropAccepted={files => files[0] && handleUpload(files[0])} > - Upload header image + Upload header image 1200x600 recommended. Use a transparent PNG for best results on any background. diff --git a/apps/admin-x-settings/src/components/settings/email-design/welcome-email-preview-content.tsx b/apps/admin-x-settings/src/components/settings/email-design/welcome-email-preview-content.tsx index 8a2d3e57c90..e468b0240ac 100644 --- a/apps/admin-x-settings/src/components/settings/email-design/welcome-email-preview-content.tsx +++ b/apps/admin-x-settings/src/components/settings/email-design/welcome-email-preview-content.tsx @@ -1,8 +1,8 @@ +import CoverImage from '../../../assets/images/user-cover.jpg'; import React from 'react'; import {cn} from '@tryghost/shade'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; -import {resolveAllColors} from './design-utils'; -import {resolveButtonCorners, resolveFontFamily} from './email-preview'; +import {resolveAllColors, resolveButtonCorners, resolveFontFamily, resolveImageCorners} from './design-utils'; import {useEmailDesign} from './email-design-context'; import {useGlobalData} from '../../providers/global-data-provider'; @@ -29,9 +29,22 @@ const WelcomeEmailPreviewContent: React.FC = () => { return ( <> - {/* Divider */} -
-
+ {/* Heading */} +
+

+ Thanks for subscribing +

{/* Body content */} @@ -40,20 +53,30 @@ const WelcomeEmailPreviewContent: React.FC = () => { style={{color: colors.textColor, fontFamily: bodyFont}} >

- Welcome to {siteTitle || 'our publication'}! We're glad you're here. This is a preview of how your welcome email will look with the current design settings. + This is a preview of what your welcome email will look like when new members sign up to {siteTitle || 'your publication'}.

- You can customize the{' '} + You can customize the design using the settings on the right – from{' '} e.preventDefault()} > - colors, fonts, and styles + colors and fonts {' '} - to match your brand. + to buttons and images – to make it feel like part of your brand.

+
+ +

+ The actual content of your welcome email can be edited separately. This preview is just here to help you get the design right. +

+ + {/* Image */} +
+ +
{/* Button */} @@ -73,7 +96,7 @@ const WelcomeEmailPreviewContent: React.FC = () => { } onClick={e => e.preventDefault()} > - Subscribe + Get started
diff --git a/apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx b/apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx index 883acbc8879..e58d74afdb8 100644 --- a/apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/member-emails/member-email-editor.tsx @@ -1,4 +1,5 @@ import React, {Suspense, useCallback, useMemo, useRef} from 'react'; +import useFeatureFlag from '../../../../hooks/use-feature-flag'; import {ErrorBoundary, type KoenigInstance, LoadingIndicator, loadKoenig, useDesignSystem} from '@tryghost/admin-x-design-system'; import {cn} from '@tryghost/shade'; import {focusKoenigEditorOnBottomClick, useFramework} from '@tryghost/admin-x-framework'; @@ -102,6 +103,7 @@ const MemberEmailsEditor: React.FC = ({ const tenorConfig = config.tenor?.googleApiKey ? config.tenor : null; const {fetchKoenigLexical, darkMode} = useDesignSystem(); const editorResource = useMemo(() => loadKoenig(fetchKoenigLexical), [fetchKoenigLexical]); + const transistorEnabled = useFeatureFlag('transistor'); const cardConfig = useMemo(() => ({ unsplash: unsplashConfig, @@ -109,8 +111,12 @@ const MemberEmailsEditor: React.FC = ({ tenor: tenorConfig, fetchEmbed, fetchAutocompleteLinks, - searchLinks - }), [unsplashConfig, pinturaConfig, tenorConfig, fetchEmbed, fetchAutocompleteLinks, searchLinks]); + searchLinks, + feature: { + transistor: transistorEnabled + }, + visibilitySettings: 'none' + }), [unsplashConfig, pinturaConfig, tenorConfig, fetchEmbed, fetchAutocompleteLinks, searchLinks, transistorEnabled]); const registerEditorAPI = useCallback((API: KoenigInstance | null) => { editorAPIRef.current = API; diff --git a/apps/admin/package.json b/apps/admin/package.json index 85701098a5b..ad24e6635ed 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -15,7 +15,7 @@ "@tryghost/activitypub": "*", "@tryghost/admin-x-framework": "*", "@tryghost/admin-x-settings": "*", - "@tryghost/koenig-lexical": "1.7.25", + "@tryghost/koenig-lexical": "1.7.27", "@tryghost/posts": "*", "@tryghost/shade": "*", "@tryghost/stats": "*", diff --git a/apps/posts/src/views/members/components/members-list-item.tsx b/apps/posts/src/views/members/components/members-list-item.tsx index c1f62ef3ef8..53beec4a116 100644 --- a/apps/posts/src/views/members/components/members-list-item.tsx +++ b/apps/posts/src/views/members/components/members-list-item.tsx @@ -2,6 +2,9 @@ import moment from 'moment-timezone'; import {Member} from '@tryghost/admin-x-framework/api/members'; import {MemberAvatar} from '@components/member-avatar'; +import {TableCell, TableRow, cn} from '@tryghost/shade'; +import {getActiveColumnValue} from '../member-query-params'; +import type {ActiveColumn} from '../member-query-params'; // --- Helpers --- @@ -48,7 +51,7 @@ function getStatusLabel(status: Member['status']): string { // --- Sub-components --- -function MembersListItemName({item}: { item: Member }) { +function MembersListItemName({item, onClick}: { item: Member; onClick?: (memberId: string) => void }) { return (
-
- {item.name || item.email || 'Anonymous'} -
+ { + if ( + e.button !== 0 || + e.metaKey || + e.ctrlKey || + e.shiftKey || + e.altKey + ) { + return; + } + e.preventDefault(); + onClick(item.id); + } : undefined} + > + + {item.name || item.email || 'Anonymous'} + + {item.name && item.email && (
-
{getStatusLabel(status)}
+
{getStatusLabel(status)}
{tierNames && (
{tierNames} @@ -103,9 +124,7 @@ function MembersListItemOpenRate({ const isKnown = emailOpenRate !== null && emailOpenRate !== undefined; return (
{isKnown ? `${Math.round(emailOpenRate)}%` : 'N/A'}
@@ -121,9 +140,7 @@ function MembersListItemLocation({ return (
{location.text}
@@ -132,7 +149,7 @@ function MembersListItemLocation({ function MembersListItemCreated({createdAt}: { createdAt: string }) { return ( -
+
{moment.utc(createdAt).format('D MMM YYYY')}
@@ -143,38 +160,86 @@ function MembersListItemCreated({createdAt}: { createdAt: string }) { ); } +function MembersListItemDynamicColumn({ + column, + member, + timezone +}: { + column: ActiveColumn; + member: Member; + timezone: string; +}) { + const value = getActiveColumnValue(column, member, timezone); + + if (!value) { + return ( + - + ); + } + + return ( +
+
{value.text}
+ {value.subtext && ( +
+ {value.subtext} +
+ )} +
+ ); +} + // --- Main component --- interface MembersListItemProps { item: Member; - gridCols: string; + activeColumns: ActiveColumn[]; showEmailOpenRate: boolean; + timezone: string; onClick: (memberId: string) => void; } function MembersListItem({ item, - gridCols, + activeColumns, showEmailOpenRate, + timezone, onClick, ...props }: MembersListItemProps & - Omit, 'onClick'>) { + Omit, 'onClick'>) { return ( -
onClick(item.id)} > - - + + + + + + {showEmailOpenRate && ( - + + + )} - - -
+ + + + + + + {activeColumns.map(col => ( + + + + ))} + ); } @@ -184,5 +249,6 @@ export { MembersListItemStatus, MembersListItemOpenRate, MembersListItemLocation, - MembersListItemCreated + MembersListItemCreated, + MembersListItemDynamicColumn }; diff --git a/apps/posts/src/views/members/components/members-list.tsx b/apps/posts/src/views/members/components/members-list.tsx index bf2a21492d4..906d39094be 100644 --- a/apps/posts/src/views/members/components/members-list.tsx +++ b/apps/posts/src/views/members/components/members-list.tsx @@ -1,35 +1,35 @@ import MembersListItem from './members-list-item'; import {Member} from '@tryghost/admin-x-framework/api/members'; +import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade'; import {forwardRef, useRef} from 'react'; import {useInfiniteVirtualScroll} from '@components/virtual-table/use-infinite-virtual-scroll'; import {useScrollRestoration} from '@components/virtual-table/use-scroll-restoration'; +import type {ActiveColumn} from '../member-query-params'; const SpacerRow = ({height}: { height: number }) => ( -