Skip to content

Commit 80a6fe1

Browse files
cmraibledaniellockyer
authored andcommitted
✨ Added Source as the new default theme
refs TryGhost/Product#3510 - Added `TryGhost/Source` as a submodule in `ghost/core/content/themes` so `Source` will ship with Ghost (along with Casper) - With this change, new installs will use `Source` as the default theme. Existing sites will have `Source` installed, but not activated, as this is a large change and we don't want to drastically change existing sites without warning. Users can upgrade to use `Source` simply by clicking 'Activate' in design settings. - Updated protections to prevent users from uploading their own conflicting version of `Source`
1 parent add7f12 commit 80a6fe1

File tree

82 files changed

+1294
-124
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+1294
-124
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ typings/
9797
/ghost/core/content/adapters/storage/**/*
9898
/ghost/core/content/adapters/scheduling/**/*
9999
/ghost/core/content/themes/casper
100+
/ghost/core/content/themes/source
100101
!/ghost/core/README.md
101102
!/ghost/core/content/**/README.md
102103

.gitmodules

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
path = ghost/core/content/themes/casper
33
url = ../../TryGhost/Casper.git
44
ignore = all
5+
[submodule "ghost/core/content/themes/source"]
6+
path = ghost/core/content/themes/source
7+
url = ../../TryGhost/Source.git
8+
ignore = all

apps/admin-x-settings/src/api/themes.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,13 @@ export function isActiveTheme(theme: Theme): boolean {
133133
}
134134

135135
export function isDefaultTheme(theme: Theme): boolean {
136+
return theme.name === 'source';
137+
}
138+
139+
export function isLegacyTheme(theme: Theme): boolean {
136140
return theme.name === 'casper';
137141
}
138142

139143
export function isDeletableTheme(theme: Theme): boolean {
140-
return !isDefaultTheme(theme) && !isActiveTheme(theme);
144+
return !isDefaultTheme(theme) && !isLegacyTheme(theme) && !isActiveTheme(theme);
141145
}

apps/admin-x-settings/src/components/settings/site/theme/AdvancedThemeSettings.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
77
import NiceModal from '@ebay/nice-modal-react';
88
import React from 'react';
99
import useHandleError from '../../../../utils/api/handleError';
10-
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
10+
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, isLegacyTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
1111
import {downloadFile, getGhostPaths} from '../../../../utils/helpers';
1212

1313
interface ThemeActionProps {
@@ -23,6 +23,8 @@ function getThemeLabel(theme: Theme): React.ReactNode {
2323

2424
if (isDefaultTheme(theme)) {
2525
label += ' (default)';
26+
} else if (isLegacyTheme(theme)) {
27+
label += ' (legacy)';
2628
} else if (theme.package?.name !== theme.name) {
2729
label =
2830
<span className='text-sm md:text-base'>

apps/admin-x-settings/src/components/settings/site/theme/OfficialThemes.tsx

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
44
import React from 'react';
55
import {OfficialTheme, useOfficialThemes} from '../../../providers/ServiceProvider';
66
import {getGhostPaths, resolveAsset} from '../../../../utils/helpers';
7+
import {useEffect, useState} from 'react';
8+
9+
const sourceDemos = [
10+
{image: 'Source.png', category: 'News'},
11+
{image: 'Source-Magazine.png', category: 'Magazine'},
12+
{image: 'Source-Newsletter.png', category: 'Newsletter'}
13+
];
714

815
const OfficialThemes: React.FC<{
916
onSelectTheme?: (theme: OfficialTheme) => void;
@@ -12,6 +19,20 @@ const OfficialThemes: React.FC<{
1219
}) => {
1320
const {adminRoot} = getGhostPaths();
1421
const officialThemes = useOfficialThemes();
22+
const [currentSourceDemoIndex, setCurrentSourceDemoIndex] = useState(0);
23+
const [isHovered, setIsHovered] = useState(false);
24+
25+
useEffect(() => {
26+
const interval = setInterval(() => {
27+
if (isHovered) {
28+
setCurrentSourceDemoIndex(prevIndex => (prevIndex + 1) % sourceDemos.length);
29+
}
30+
}, 3000);
31+
32+
return () => {
33+
clearInterval(interval);
34+
};
35+
}, [isHovered]);
1536

1637
return (
1738
<ModalPage heading='Themes'>
@@ -22,16 +43,33 @@ const OfficialThemes: React.FC<{
2243
onSelectTheme?.(theme);
2344
}}>
2445
{/* <img alt={theme.name} src={`${assetRoot}/${theme.image}`}/> */}
25-
<div className='w-full bg-grey-100 shadow-md transition-all duration-500 hover:scale-[1.05]'>
26-
<img
27-
alt={`${theme.name} Theme`}
28-
className='h-full w-full object-contain'
29-
src={resolveAsset(theme.image, adminRoot)}
30-
/>
46+
<div className='relative w-full bg-grey-100 shadow-md transition-all duration-500 hover:scale-[1.05]' onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
47+
{theme.name !== 'Source' ?
48+
<img
49+
alt={`${theme.name} Theme`}
50+
className='h-full w-full object-contain'
51+
src={resolveAsset(theme.image, adminRoot)}
52+
/> :
53+
<>
54+
{sourceDemos.map((demo, index) => (
55+
<img
56+
key={`source-theme-${demo.category}`}
57+
alt={`${theme.name} Theme - ${demo.category}`}
58+
className={`${index === 0 ? 'relative' : 'absolute'} left-0 top-0 h-full w-full object-contain transition-opacity duration-500 ${index === currentSourceDemoIndex ? 'opacity-100' : 'opacity-0'}`}
59+
src={resolveAsset(`assets/img/themes/${demo.image}`, adminRoot)}
60+
/>
61+
))}
62+
</>
63+
}
3164
</div>
32-
<div className='mt-3'>
65+
<div className='relative mt-3'>
3366
<Heading level={4}>{theme.name}</Heading>
34-
<span className='text-sm text-grey-700'>{theme.category}</span>
67+
{theme.name !== 'Source' ?
68+
<span className='text-sm text-grey-700'>{theme.category}</span> :
69+
sourceDemos.map((demo, index) => (
70+
<span className={`${index === 0 ? 'relative' : 'absolute bottom-[1px]'} left-0 inline-block w-24 bg-white text-sm text-grey-700 ${index === currentSourceDemoIndex ? 'opacity-100' : 'opacity-0'}`}>{demo.category}</span>
71+
))
72+
}
3573
</div>
3674
</button>
3775
);

apps/admin-x-settings/src/components/settings/site/theme/ThemePreview.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@ import MobileChrome from '../../../../admin-x-ds/global/chrome/MobileChrome';
77
import NiceModal from '@ebay/nice-modal-react';
88
import PageHeader from '../../../../admin-x-ds/global/layout/PageHeader';
99
import React, {useState} from 'react';
10+
import Select, {SelectOption} from '../../../../admin-x-ds/global/form/Select';
1011
import {OfficialTheme} from '../../../providers/ServiceProvider';
1112
import {Theme} from '../../../../api/themes';
1213

14+
const sourceDemos = [
15+
{label: 'News', value: 'news', url: 'https://source.ghost.io'},
16+
{label: 'Magazine', value: 'magazine', url: 'https://source-magazine.ghost.io'},
17+
{label: 'Newsletter', value: 'newsletter', url: 'https://source-newsletter.ghost.io'}
18+
];
19+
1320
const ThemePreview: React.FC<{
1421
selectedTheme?: OfficialTheme;
1522
isInstalling?: boolean;
@@ -26,6 +33,7 @@ const ThemePreview: React.FC<{
2633
onInstall
2734
}) => {
2835
const [previewMode, setPreviewMode] = useState('desktop');
36+
const [currentSourceDemo, setCurrentSourceDemo] = useState<SelectOption>(sourceDemos[0]);
2937

3038
if (!selectedTheme) {
3139
return null;
@@ -68,6 +76,7 @@ const ThemePreview: React.FC<{
6876
<div className='flex items-center gap-2'>
6977
<Breadcrumbs
7078
activeItemClassName='hidden md:!block md:!visible'
79+
containerClassName='whitespace-nowrap'
7180
itemClassName='hidden md:!block md:!visible'
7281
items={[
7382
{label: 'Design', onClick: onClose},
@@ -78,6 +87,24 @@ const ThemePreview: React.FC<{
7887
backIcon
7988
onBack={onBack}
8089
/>
90+
{selectedTheme.name === 'Source' ?
91+
<>
92+
<span className='hidden md:!visible md:!block'></span>
93+
<Select
94+
border={false}
95+
containerClassName='text-sm font-bold'
96+
controlClasses={{menu: 'w-24'}}
97+
fullWidth={false}
98+
options={sourceDemos}
99+
selectedOption={currentSourceDemo}
100+
onSelect={(option) => {
101+
if (option) {
102+
setCurrentSourceDemo(option);
103+
}
104+
}}
105+
/>
106+
</> : null
107+
}
81108
</div>;
82109

83110
const right =
@@ -118,13 +145,19 @@ const ThemePreview: React.FC<{
118145
<div className='flex h-[calc(100%-74px)] grow flex-col items-center justify-center bg-grey-50 dark:bg-black'>
119146
{previewMode === 'desktop' ?
120147
<DesktopChrome>
121-
<iframe className='h-full w-full'
122-
src={selectedTheme?.previewUrl} title='Theme preview' />
148+
<iframe
149+
className='h-full w-full'
150+
src={selectedTheme.name !== 'Source' ? selectedTheme?.previewUrl : sourceDemos.find(demo => demo.label === currentSourceDemo.label)?.url}
151+
title='Theme preview'
152+
/>
123153
</DesktopChrome>
124154
:
125155
<MobileChrome>
126-
<iframe className='h-full w-full'
127-
src={selectedTheme?.previewUrl} title='Theme preview' />
156+
<iframe
157+
className='h-full w-full'
158+
src={selectedTheme.name !== 'Source' ? selectedTheme?.previewUrl : sourceDemos.find(demo => demo.label === currentSourceDemo.label)?.url}
159+
title='Theme preview'
160+
/>
128161
</MobileChrome>
129162
}
130163
</div>

apps/admin-x-settings/src/main.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
1313
}}
1414
ghostVersion='5.x'
1515
officialThemes={[{
16+
name: 'Source',
17+
category: 'News',
18+
previewUrl: 'https://source.ghost.io/',
19+
ref: 'default',
20+
image: 'assets/img/themes/Source.png'
21+
}, {
1622
name: 'Casper',
1723
category: 'Blog',
1824
previewUrl: 'https://demo.ghost.io/',

ghost/admin/app/components/admin-x/settings.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,17 @@ import {tracked} from '@glimmer/tracking';
1212

1313
// TODO: Long term move asset management directly in AdminX
1414
const officialThemes = [{
15+
name: 'Source',
16+
category: 'News',
17+
previewUrl: 'https://source.ghost.io/',
18+
ref: 'default',
19+
image: 'assets/img/themes/Source.png'
20+
}, {
1521
name: 'Casper',
1622
category: 'Blog',
1723
previewUrl: 'https://demo.ghost.io/',
18-
ref: 'default',
24+
ref: 'TryGhost/Casper',
1925
image: 'assets/img/themes/Casper.png'
20-
}, {
21-
name: 'Headline',
22-
category: 'News',
23-
url: 'https://github.com/TryGhost/Headline',
24-
previewUrl: 'https://headline.ghost.io',
25-
ref: 'TryGhost/Headline',
26-
image: 'assets/img/themes/Headline.png'
2726
}, {
2827
name: 'Edition',
2928
category: 'Newsletter',
@@ -108,6 +107,13 @@ const officialThemes = [{
108107
previewUrl: 'https://ease.ghost.io',
109108
ref: 'TryGhost/Ease',
110109
image: 'assets/img/themes/Ease.png'
110+
}, {
111+
name: 'Headline',
112+
category: 'News',
113+
url: 'https://github.com/TryGhost/Headline',
114+
previewUrl: 'https://headline.ghost.io',
115+
ref: 'TryGhost/Headline',
116+
image: 'assets/img/themes/Headline.png'
111117
}, {
112118
name: 'Ruby',
113119
category: 'Magazine',

ghost/admin/app/components/gh-theme-table.js

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ export default class GhThemeTableComponent extends Component {
1818
this.activateTaskInstance?.cancel();
1919
}
2020

21+
isDefaultTheme(theme) {
22+
return theme.name.toLowerCase() === 'source';
23+
}
24+
25+
isLegacyTheme(theme) {
26+
return theme.name.toLowerCase() === 'casper';
27+
}
28+
2129
get sortedThemes() {
2230
let themes = this.args.themes.map((t) => {
2331
let theme = {};
@@ -30,7 +38,6 @@ export default class GhThemeTableComponent extends Component {
3038
theme.package = themePackage;
3139
theme.active = get(t, 'active');
3240
theme.isDeletable = !theme.active;
33-
3441
return theme;
3542
});
3643
let duplicateThemes = [];
@@ -44,19 +51,24 @@ export default class GhThemeTableComponent extends Component {
4451
});
4552

4653
duplicateThemes.forEach((theme) => {
47-
if (theme.name !== 'casper') {
54+
if (!this.isDefaultTheme(theme) && !this.isLegacyTheme(theme)) {
4855
theme.label = `${theme.label} (${theme.name})`;
4956
}
5057
});
5158

52-
// "(default)" needs to be added to casper manually as it's always
53-
// displayed and would mess up the duplicate checking if added earlier
54-
let casper = themes.findBy('name', 'casper');
55-
if (casper) {
56-
casper.label = `${casper.label} (default)`;
57-
casper.isDefault = true;
58-
casper.isDeletable = false;
59-
}
59+
// add (default) or (legacy) as appropriate and prevent deletion of default/legacy themes
60+
// this needs to be after deduplicating by label
61+
themes.filter(this.isDefaultTheme).forEach((theme) => {
62+
theme.label = `${theme.label} (default)`;
63+
theme.isDefault = true;
64+
theme.isDeletable = false;
65+
});
66+
67+
themes.filter(this.isLegacyTheme).forEach((theme) => {
68+
theme.label = `${theme.label} (legacy)`;
69+
theme.isLegacy = true;
70+
theme.isDeletable = false;
71+
});
6072

6173
// sorting manually because .sortBy('label') has a different sorting
6274
// algorithm to [...strings].sort()

ghost/admin/app/components/modals/design/install-theme.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export default class InstallThemeModal extends Component {
3131
return this.args.data.theme?.ref || this.args.data.ref;
3232
}
3333

34-
get isDefaultTheme() {
35-
return this.themeName.toLowerCase() === 'casper';
34+
get isDefaultOrLegacyTheme() {
35+
return this.themeName.toLowerCase() === 'casper' || this.themeName.toLowerCase() === 'source';
3636
}
3737

3838
get isConfirming() {
@@ -48,7 +48,7 @@ export default class InstallThemeModal extends Component {
4848
}
4949

5050
get willOverwriteExisting() {
51-
return !this.isDefaultTheme && this.themes.findBy('name', this.themeName.toLowerCase());
51+
return !this.isDefaultOrLegacyTheme && this.themes.findBy('name', this.themeName.toLowerCase());
5252
}
5353

5454
get hasWarningsOrErrors() {
@@ -67,9 +67,10 @@ export default class InstallThemeModal extends Component {
6767
@task
6868
*installThemeTask() {
6969
try {
70-
if (this.isDefaultTheme) {
70+
if (this.isDefaultOrLegacyTheme) {
7171
// default theme can't be installed, only activated
72-
const defaultTheme = this.store.peekRecord('theme', 'casper');
72+
const themeName = this.themeName.toLowerCase();
73+
const defaultTheme = this.store.peekRecord('theme', themeName);
7374
yield this.themeManagement.activateTask.perform(defaultTheme, {skipErrors: true});
7475
this.installedTheme = defaultTheme;
7576

ghost/admin/app/components/modals/design/upload-theme.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ export default class UploadThemeModal extends Component {
9191
return new UnsupportedMediaTypeError();
9292
}
9393

94-
if (file.name.match(/^casper\.zip$/i)) {
95-
return {payload: {errors: [{message: 'Sorry, the default Casper theme cannot be overwritten.<br>Please rename your zip file to continue.'}]}};
94+
if (file.name.match(/^casper\.zip$/i) || file.name.match(/^source\.zip$/i)) {
95+
return {payload: {errors: [{message: 'Sorry, the default theme cannot be overwritten.<br>Please rename your zip file to continue.'}]}};
9696
}
9797

9898
if (!this._allowOverwrite && this.currentThemeNames.includes(themeName)) {

0 commit comments

Comments
 (0)