Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions apps/admin-x-design-system/src/global/form/toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface ToggleProps {
hint?: React.ReactNode;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
gap?: string;
align?: 'start' | 'center';
testId?: string;
}

Expand All @@ -42,6 +43,7 @@ const Toggle: React.FC<ToggleProps> = ({
name,
onChange,
gap = 'gap-2',
align = 'start',
testId
}) => {
const id = useId();
Expand All @@ -53,19 +55,19 @@ const Toggle: React.FC<ToggleProps> = ({
case 'sm':
sizeStyles = ' h-3 w-5';
thumbSizeStyles = ' h-2 w-2 data-[state=checked]:translate-x-[10px]';
labelStyles = 'mt-[-5.5px]';
labelStyles = align === 'start' ? 'mt-[-5.5px]' : '';
break;

case 'lg':
sizeStyles = ' h-5 w-8';
thumbSizeStyles = ' h-4 w-4 data-[state=checked]:translate-x-[14px]';
labelStyles = 'mt-[-1px]';
labelStyles = align === 'start' ? 'mt-[-1px]' : '';
break;

default:
sizeStyles = ' min-w-[28px] h-4 w-7';
thumbSizeStyles = ' h-3 w-3 data-[state=checked]:translate-x-[14px]';
labelStyles = 'mt-[-3px]';
labelStyles = align === 'start' ? 'mt-[-3px]' : '';
break;
}

Expand Down Expand Up @@ -104,7 +106,7 @@ const Toggle: React.FC<ToggleProps> = ({

return (
<div>
<div className={`group flex items-start ${gap} dark:text-white ${direction === 'rtl' && 'justify-between'} ${separator && 'pb-2'} ${containerClasses}`}>
<div className={`group flex ${align === 'center' ? 'items-center' : 'items-start'} ${gap} dark:text-white ${direction === 'rtl' && 'justify-between'} ${separator && 'pb-2'} ${containerClasses}`}>
<TogglePrimitive.Root className={clsx(
toggleBgClass,
'appearance-none rounded-full bg-grey-300 transition duration-100 dark:bg-grey-800',
Expand Down
24 changes: 11 additions & 13 deletions apps/admin-x-design-system/src/global/tab-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const TabButton: React.FC<TabButtonProps> = ({
return (
<TabsPrimitive.Trigger
className={clsx(
'-m-b-px cursor-pointer appearance-none whitespace-nowrap py-1 text-md font-semibold text-grey-700 transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)] data-[state=active]:text-black dark:text-white [&>span]:data-[state=active]:text-black [&>span]:data-[state=active]:dark:text-white',
'relative z-[1] cursor-pointer appearance-none whitespace-nowrap pb-1.5 pt-1 text-md font-semibold text-grey-700 transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)] data-[state=active]:text-black dark:text-white [&>span]:data-[state=active]:text-black [&>span]:data-[state=active]:dark:text-white',
border && 'border-b-2 border-transparent hover:border-grey-500 data-[state=active]:border-black data-[state=active]:dark:border-white data-[state=active]:dark:text-white'
)}
id={id}
Expand Down Expand Up @@ -80,26 +80,24 @@ export const TabList: React.FC<TabListProps> = ({
stickyHeader
}) => {
const containerClasses = clsx(
'no-scrollbar mb-px flex w-full overflow-x-auto',
'no-scrollbar relative flex w-full overflow-x-auto',
width === 'narrow' && 'gap-3',
width === 'normal' && 'gap-5',
width === 'wide' && 'gap-7',
border && 'border-b border-grey-300 dark:border-grey-900'
border && 'after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-grey-300 dark:after:bg-grey-900'
);
return (
<TabsPrimitive.List className={`${stickyHeader ? 'sticky top-0 z-50 bg-white dark:bg-black' : ''}`}>
<div className={containerClasses} role='tablist'>
{tabs.map(tab => (
<div>
<TabButton
border={buttonBorder}
counter={tab.counter}
icon={tab.icon}
id={tab.id}
title={tab.title}
onClick={handleTabChange}
/>
</div>
<TabButton
border={buttonBorder}
counter={tab.counter}
icon={tab.icon}
id={tab.id}
title={tab.title}
onClick={handleTabChange}
/>
))}
{topRightContent !== null ?
<div className='ml-auto'>{topRightContent}</div> :
Expand Down
1 change: 0 additions & 1 deletion apps/admin-x-framework/src/test/acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export const responseFixtures = {
};

const defaultLabFlags = {
audienceFeedback: false,
collections: false,
outboundLinkTagging: false,
announcementBar: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const NewsletterPreview: React.FC<{newsletter: Newsletter}> = ({newsletter}) =>
const headerSubtitle = (newsletter.show_header_title && newsletter.show_header_name) ? newsletter.name : undefined;

const showCommentCta = newsletter.show_comment_cta && commentsEnabled !== 'off';
const showFeedback = newsletter.feedback_enabled && config.labs.audienceFeedback;
const showFeedback = newsletter.feedback_enabled;

const backgroundColor = () => {
const value = newsletter.background_color;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
const inputs = (
<SettingGroupContent className="analytics-settings !gap-y-0" columns={1}>
<Toggle
align='center'
checked={isWebAnalyticsEnabled}
containerClasses='py-4'
direction='rtl'
disabled={!isWebAnalyticsConfigured || isWebAnalyticsLimited}
gap='gap-0'
Expand All @@ -61,7 +63,6 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
</>
}
label='Web analytics'
labelClasses='py-4 w-full'
onChange={(e) => {
handleToggleChange('web_analytics', e);
}}
Expand All @@ -88,49 +89,53 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
)
)}
<Toggle
align='center'
checked={trackEmailOpens}
containerClasses='py-4'
direction='rtl'
gap='gap-0'
hint='Record when a member opens an email'
label='Newsletter opens'
labelClasses='py-4 w-full'
onChange={(e) => {
handleToggleChange('email_track_opens', e);
}}
/>
<Separator className="border-grey-200 dark:border-grey-900" />
<Toggle
align='center'
checked={trackEmailClicks}
containerClasses='py-4'
direction='rtl'
disabled={isEmailTrackClicksReadOnly}
gap='gap-0'
hint='Record when a member clicks on any link in an email'
label='Newsletter clicks'
labelClasses='py-4 w-full'
onChange={(e) => {
handleToggleChange('email_track_clicks', e);
}}
/>
<Separator className="border-grey-200 dark:border-grey-900" />
<Toggle
align='center'
checked={trackMemberSources}
containerClasses='py-4'
direction='rtl'
gap='gap-0'
hint='Track the traffic sources and posts that drive the most member growth'
label='Member sources'
labelClasses='py-4 w-full'
onChange={(e) => {
handleToggleChange('members_track_sources', e);
}}
/>
<Separator className="border-grey-200 dark:border-grey-900" />
<Toggle
align='center'
checked={outboundLinkTagging}
containerClasses='py-4'
direction='rtl'
gap='gap-0'
hint='Make it easier for other sites to track the traffic you send them in their analytics'
label='Outbound link tagging'
labelClasses='py-4 w-full'
onChange={(e) => {
handleToggleChange('outbound_link_tagging', e);
}}
Expand Down
2 changes: 2 additions & 0 deletions apps/admin/src/layout/app-sidebar/app-sidebar-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import NavMain from "./nav-main";
import NavContent from "./nav-content";
import NavGhostPro from "./nav-ghost-pro";
import NavSettings from "./nav-settings";
import ThemeErrorsBanner from "./theme-errors-banner";
import UpgradeBanner from "./upgrade-banner";
import { useUpgradeStatus } from "./hooks/use-upgrade-status";

Expand All @@ -23,6 +24,7 @@ function AppSidebarContent() {
</div>
<div className="flex flex-col gap-2 sidebar:gap-4">
{showUpgradeBanner ? <UpgradeBanner trialDaysRemaining={trialDaysRemaining} /> : <WhatsNewBanner />}
<ThemeErrorsBanner />
<NavSettings className="pb-0" />
</div>
</SidebarContent>
Expand Down
29 changes: 29 additions & 0 deletions apps/admin/src/layout/app-sidebar/hooks/use-theme-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {useActiveTheme} from '@tryghost/admin-x-framework/api/themes';
import {useCurrentUser} from '@tryghost/admin-x-framework/api/current-user';
import {isContributorUser} from '@tryghost/admin-x-framework/api/users';
import type {ThemeProblem} from '@tryghost/admin-x-framework/api/themes';

// This error is handled inline next to the related setting in the design
// customization panel rather than shown in the sidebar error banner
function isFilteredError(error: ThemeProblem<'error'>): boolean {
return error.code === 'GS110-NO-MISSING-PAGE-BUILDER-USAGE'
&& !!error.failures?.[0]?.message?.includes('show_title_and_feature_image');
}

export function useActiveThemeErrors() {
const {data: currentUser} = useCurrentUser();
const isContributor = currentUser && isContributorUser(currentUser);

const {data: activeThemeData} = useActiveTheme({
enabled: !isContributor
});

const activeTheme = activeThemeData?.themes?.[0];
const allErrors = activeTheme?.errors ?? [];
const warnings = activeTheme?.warnings ?? [];

const errors = allErrors.filter(error => !isFilteredError(error));
const hasErrors = errors.length > 0;

return {hasErrors, errors, warnings};
}
41 changes: 41 additions & 0 deletions apps/admin/src/layout/app-sidebar/theme-errors-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {useState} from 'react';
import {Banner, LucideIcon} from '@tryghost/shade';
import {useActiveThemeErrors} from './hooks/use-theme-errors';
import ThemeErrorsDialog from './theme-errors-dialog';

function ThemeErrorsBanner() {
const {hasErrors, errors, warnings} = useActiveThemeErrors();
const [dialogOpen, setDialogOpen] = useState(false);

if (!hasErrors) {
return null;
}

return (
<>
<Banner
className="mx-2 cursor-pointer"
role="status"
size="md"
variant="destructive"
onClick={() => setDialogOpen(true)}
>
<div className="flex items-start gap-2">
<LucideIcon.AlertTriangle className="mt-0.5 size-4 shrink-0 text-red" />
<div>
<div className="font-semibold text-red">Your theme has errors</div>
<div className="text-sm text-muted-foreground">Some functionality on your site may be limited &rarr;</div>
</div>
</div>
</Banner>
<ThemeErrorsDialog
errors={errors}
open={dialogOpen}
warnings={warnings}
onOpenChange={setDialogOpen}
/>
</>
);
}

export default ThemeErrorsBanner;
102 changes: 102 additions & 0 deletions apps/admin/src/layout/app-sidebar/theme-errors-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {useState} from 'react';
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
LucideIcon
} from '@tryghost/shade';
import type {ThemeProblem} from '@tryghost/admin-x-framework/api/themes';

interface ThemeErrorsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
errors: ThemeProblem<'error'>[];
warnings: ThemeProblem<'warning'>[];
}

function ThemeErrorItem({error}: {error: ThemeProblem}) {
const [expanded, setExpanded] = useState(false);

return (
<li className="border-b border-border last:border-0">
<button
className="flex w-full items-center gap-2 py-3 text-left text-sm font-medium"
type="button"
onClick={() => setExpanded(!expanded)}
>
<LucideIcon.ChevronRight className={`size-4 shrink-0 text-muted-foreground transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`} />
<span>{error.rule}</span>
</button>
{expanded && (
<div className="pb-3 pl-6 text-sm text-muted-foreground">
<p dangerouslySetInnerHTML={{__html: error.details}} />
{error.failures?.length > 0 && (
<div className="mt-2">
<h6 className="text-xs font-semibold uppercase text-muted-foreground">Affected files:</h6>
<ul className="mt-1 list-disc pl-4">
{error.failures.map((failure, i) => (
<li key={i}>
<code className="text-xs">{failure.ref}</code>
{failure.message && <>: {failure.message}</>}
</li>
))}
</ul>
</div>
)}
</div>
)}
</li>
);
}

function ThemeErrorsDialog({open, onOpenChange, errors, warnings}: ThemeErrorsDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg max-h-[85vh] flex flex-col">
<DialogHeader>
<DialogTitle className="text-2xl tracking-tighter">
Theme errors
</DialogTitle>
</DialogHeader>

<section className="flex-1 overflow-y-auto -mx-6 px-6">
{errors.length > 0 && (
<div>
<h2 className="mb-1 text-sm font-semibold">Errors</h2>
<p className="mb-2 text-xs text-muted-foreground">
Highly recommended to fix, functionality could be restricted
</p>
<ul className="border-t border-border">
{errors.map((error, i) => (
<ThemeErrorItem key={i} error={error} />
))}
</ul>
</div>
)}

{warnings.length > 0 && (
<div className={errors.length > 0 ? 'mt-4' : ''}>
<h2 className="mb-1 text-sm font-semibold">Warnings</h2>
<ul className="border-t border-border">
{warnings.map((warning, i) => (
<ThemeErrorItem key={i} error={warning} />
))}
</ul>
</div>
)}
</section>

<DialogFooter>
<Button onClick={() => onOpenChange(false)}>
OK
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

export default ThemeErrorsDialog;
1 change: 1 addition & 0 deletions apps/admin/src/whats-new/components/whats-new-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function WhatsNewBanner() {
return (
<Banner
data-test-toast="whats-new"
className="mx-2"
role="status"
aria-label="What’s new notification"
aria-live="polite"
Expand Down
2 changes: 1 addition & 1 deletion apps/shade/src/components/ui/banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const bannerVariants = cva(
info: 'bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-800 border',
success: 'bg-green-50 border-green-200 dark:bg-green-950/30 dark:border-green-800 border',
warning: 'bg-yellow-50 border-yellow-200 dark:bg-yellow-950/30 dark:border-yellow-800 border',
destructive: 'bg-red-50 border-red-200 dark:bg-red-950/30 dark:border-red-800 border'
destructive: 'bg-white shadow-sm dark:bg-black'
},
size: {
sm: 'p-2 text-sm',
Expand Down
Loading
Loading