✨ 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
.gitignore
vendored
@ -97,6 +97,7 @@ typings/
|
|||||||
/ghost/core/content/adapters/storage/**/*
|
/ghost/core/content/adapters/storage/**/*
|
||||||
/ghost/core/content/adapters/scheduling/**/*
|
/ghost/core/content/adapters/scheduling/**/*
|
||||||
/ghost/core/content/themes/casper
|
/ghost/core/content/themes/casper
|
||||||
|
/ghost/core/content/themes/source
|
||||||
!/ghost/core/README.md
|
!/ghost/core/README.md
|
||||||
!/ghost/core/content/**/README.md
|
!/ghost/core/content/**/README.md
|
||||||
|
|
||||||
|
4
.gitmodules
vendored
@ -2,3 +2,7 @@
|
|||||||
path = ghost/core/content/themes/casper
|
path = ghost/core/content/themes/casper
|
||||||
url = ../../TryGhost/Casper.git
|
url = ../../TryGhost/Casper.git
|
||||||
ignore = all
|
ignore = all
|
||||||
|
[submodule "ghost/core/content/themes/source"]
|
||||||
|
path = ghost/core/content/themes/source
|
||||||
|
url = ../../TryGhost/Source.git
|
||||||
|
ignore = all
|
||||||
|
@ -133,9 +133,13 @@ export function isActiveTheme(theme: Theme): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isDefaultTheme(theme: Theme): boolean {
|
export function isDefaultTheme(theme: Theme): boolean {
|
||||||
|
return theme.name === 'source';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLegacyTheme(theme: Theme): boolean {
|
||||||
return theme.name === 'casper';
|
return theme.name === 'casper';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDeletableTheme(theme: Theme): boolean {
|
export function isDeletableTheme(theme: Theme): boolean {
|
||||||
return !isDefaultTheme(theme) && !isActiveTheme(theme);
|
return !isDefaultTheme(theme) && !isLegacyTheme(theme) && !isActiveTheme(theme);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
|
|||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import useHandleError from '../../../../utils/api/handleError';
|
import useHandleError from '../../../../utils/api/handleError';
|
||||||
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
|
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, isLegacyTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
|
||||||
import {downloadFile, getGhostPaths} from '../../../../utils/helpers';
|
import {downloadFile, getGhostPaths} from '../../../../utils/helpers';
|
||||||
|
|
||||||
interface ThemeActionProps {
|
interface ThemeActionProps {
|
||||||
@ -23,6 +23,8 @@ function getThemeLabel(theme: Theme): React.ReactNode {
|
|||||||
|
|
||||||
if (isDefaultTheme(theme)) {
|
if (isDefaultTheme(theme)) {
|
||||||
label += ' (default)';
|
label += ' (default)';
|
||||||
|
} else if (isLegacyTheme(theme)) {
|
||||||
|
label += ' (legacy)';
|
||||||
} else if (theme.package?.name !== theme.name) {
|
} else if (theme.package?.name !== theme.name) {
|
||||||
label =
|
label =
|
||||||
<span className='text-sm md:text-base'>
|
<span className='text-sm md:text-base'>
|
||||||
|
@ -4,6 +4,13 @@ import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {OfficialTheme, useOfficialThemes} from '../../../providers/ServiceProvider';
|
import {OfficialTheme, useOfficialThemes} from '../../../providers/ServiceProvider';
|
||||||
import {getGhostPaths, resolveAsset} from '../../../../utils/helpers';
|
import {getGhostPaths, resolveAsset} from '../../../../utils/helpers';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
const sourceDemos = [
|
||||||
|
{image: 'Source.png', category: 'News'},
|
||||||
|
{image: 'Source-Magazine.png', category: 'Magazine'},
|
||||||
|
{image: 'Source-Newsletter.png', category: 'Newsletter'}
|
||||||
|
];
|
||||||
|
|
||||||
const OfficialThemes: React.FC<{
|
const OfficialThemes: React.FC<{
|
||||||
onSelectTheme?: (theme: OfficialTheme) => void;
|
onSelectTheme?: (theme: OfficialTheme) => void;
|
||||||
@ -12,6 +19,20 @@ const OfficialThemes: React.FC<{
|
|||||||
}) => {
|
}) => {
|
||||||
const {adminRoot} = getGhostPaths();
|
const {adminRoot} = getGhostPaths();
|
||||||
const officialThemes = useOfficialThemes();
|
const officialThemes = useOfficialThemes();
|
||||||
|
const [currentSourceDemoIndex, setCurrentSourceDemoIndex] = useState(0);
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (isHovered) {
|
||||||
|
setCurrentSourceDemoIndex(prevIndex => (prevIndex + 1) % sourceDemos.length);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [isHovered]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalPage heading='Themes'>
|
<ModalPage heading='Themes'>
|
||||||
@ -22,16 +43,33 @@ const OfficialThemes: React.FC<{
|
|||||||
onSelectTheme?.(theme);
|
onSelectTheme?.(theme);
|
||||||
}}>
|
}}>
|
||||||
{/* <img alt={theme.name} src={`${assetRoot}/${theme.image}`}/> */}
|
{/* <img alt={theme.name} src={`${assetRoot}/${theme.image}`}/> */}
|
||||||
<div className='w-full bg-grey-100 shadow-md transition-all duration-500 hover:scale-[1.05]'>
|
<div className='relative w-full bg-grey-100 shadow-md transition-all duration-500 hover:scale-[1.05]' onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
|
||||||
|
{theme.name !== 'Source' ?
|
||||||
<img
|
<img
|
||||||
alt={`${theme.name} Theme`}
|
alt={`${theme.name} Theme`}
|
||||||
className='h-full w-full object-contain'
|
className='h-full w-full object-contain'
|
||||||
src={resolveAsset(theme.image, adminRoot)}
|
src={resolveAsset(theme.image, adminRoot)}
|
||||||
|
/> :
|
||||||
|
<>
|
||||||
|
{sourceDemos.map((demo, index) => (
|
||||||
|
<img
|
||||||
|
key={`source-theme-${demo.category}`}
|
||||||
|
alt={`${theme.name} Theme - ${demo.category}`}
|
||||||
|
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'}`}
|
||||||
|
src={resolveAsset(`assets/img/themes/${demo.image}`, adminRoot)}
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3'>
|
<div className='relative mt-3'>
|
||||||
<Heading level={4}>{theme.name}</Heading>
|
<Heading level={4}>{theme.name}</Heading>
|
||||||
<span className='text-sm text-grey-700'>{theme.category}</span>
|
{theme.name !== 'Source' ?
|
||||||
|
<span className='text-sm text-grey-700'>{theme.category}</span> :
|
||||||
|
sourceDemos.map((demo, index) => (
|
||||||
|
<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>
|
||||||
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -7,9 +7,16 @@ import MobileChrome from '../../../../admin-x-ds/global/chrome/MobileChrome';
|
|||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import PageHeader from '../../../../admin-x-ds/global/layout/PageHeader';
|
import PageHeader from '../../../../admin-x-ds/global/layout/PageHeader';
|
||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
|
import Select, {SelectOption} from '../../../../admin-x-ds/global/form/Select';
|
||||||
import {OfficialTheme} from '../../../providers/ServiceProvider';
|
import {OfficialTheme} from '../../../providers/ServiceProvider';
|
||||||
import {Theme} from '../../../../api/themes';
|
import {Theme} from '../../../../api/themes';
|
||||||
|
|
||||||
|
const sourceDemos = [
|
||||||
|
{label: 'News', value: 'news', url: 'https://source.ghost.io'},
|
||||||
|
{label: 'Magazine', value: 'magazine', url: 'https://source-magazine.ghost.io'},
|
||||||
|
{label: 'Newsletter', value: 'newsletter', url: 'https://source-newsletter.ghost.io'}
|
||||||
|
];
|
||||||
|
|
||||||
const ThemePreview: React.FC<{
|
const ThemePreview: React.FC<{
|
||||||
selectedTheme?: OfficialTheme;
|
selectedTheme?: OfficialTheme;
|
||||||
isInstalling?: boolean;
|
isInstalling?: boolean;
|
||||||
@ -26,6 +33,7 @@ const ThemePreview: React.FC<{
|
|||||||
onInstall
|
onInstall
|
||||||
}) => {
|
}) => {
|
||||||
const [previewMode, setPreviewMode] = useState('desktop');
|
const [previewMode, setPreviewMode] = useState('desktop');
|
||||||
|
const [currentSourceDemo, setCurrentSourceDemo] = useState<SelectOption>(sourceDemos[0]);
|
||||||
|
|
||||||
if (!selectedTheme) {
|
if (!selectedTheme) {
|
||||||
return null;
|
return null;
|
||||||
@ -68,6 +76,7 @@ const ThemePreview: React.FC<{
|
|||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
activeItemClassName='hidden md:!block md:!visible'
|
activeItemClassName='hidden md:!block md:!visible'
|
||||||
|
containerClassName='whitespace-nowrap'
|
||||||
itemClassName='hidden md:!block md:!visible'
|
itemClassName='hidden md:!block md:!visible'
|
||||||
items={[
|
items={[
|
||||||
{label: 'Design', onClick: onClose},
|
{label: 'Design', onClick: onClose},
|
||||||
@ -78,6 +87,24 @@ const ThemePreview: React.FC<{
|
|||||||
backIcon
|
backIcon
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
/>
|
/>
|
||||||
|
{selectedTheme.name === 'Source' ?
|
||||||
|
<>
|
||||||
|
<span className='hidden md:!visible md:!block'>–</span>
|
||||||
|
<Select
|
||||||
|
border={false}
|
||||||
|
containerClassName='text-sm font-bold'
|
||||||
|
controlClasses={{menu: 'w-24'}}
|
||||||
|
fullWidth={false}
|
||||||
|
options={sourceDemos}
|
||||||
|
selectedOption={currentSourceDemo}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setCurrentSourceDemo(option);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</> : null
|
||||||
|
}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
const right =
|
const right =
|
||||||
@ -118,13 +145,19 @@ const ThemePreview: React.FC<{
|
|||||||
<div className='flex h-[calc(100%-74px)] grow flex-col items-center justify-center bg-grey-50 dark:bg-black'>
|
<div className='flex h-[calc(100%-74px)] grow flex-col items-center justify-center bg-grey-50 dark:bg-black'>
|
||||||
{previewMode === 'desktop' ?
|
{previewMode === 'desktop' ?
|
||||||
<DesktopChrome>
|
<DesktopChrome>
|
||||||
<iframe className='h-full w-full'
|
<iframe
|
||||||
src={selectedTheme?.previewUrl} title='Theme preview' />
|
className='h-full w-full'
|
||||||
|
src={selectedTheme.name !== 'Source' ? selectedTheme?.previewUrl : sourceDemos.find(demo => demo.label === currentSourceDemo.label)?.url}
|
||||||
|
title='Theme preview'
|
||||||
|
/>
|
||||||
</DesktopChrome>
|
</DesktopChrome>
|
||||||
:
|
:
|
||||||
<MobileChrome>
|
<MobileChrome>
|
||||||
<iframe className='h-full w-full'
|
<iframe
|
||||||
src={selectedTheme?.previewUrl} title='Theme preview' />
|
className='h-full w-full'
|
||||||
|
src={selectedTheme.name !== 'Source' ? selectedTheme?.previewUrl : sourceDemos.find(demo => demo.label === currentSourceDemo.label)?.url}
|
||||||
|
title='Theme preview'
|
||||||
|
/>
|
||||||
</MobileChrome>
|
</MobileChrome>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,12 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|||||||
}}
|
}}
|
||||||
ghostVersion='5.x'
|
ghostVersion='5.x'
|
||||||
officialThemes={[{
|
officialThemes={[{
|
||||||
|
name: 'Source',
|
||||||
|
category: 'News',
|
||||||
|
previewUrl: 'https://source.ghost.io/',
|
||||||
|
ref: 'default',
|
||||||
|
image: 'assets/img/themes/Source.png'
|
||||||
|
}, {
|
||||||
name: 'Casper',
|
name: 'Casper',
|
||||||
category: 'Blog',
|
category: 'Blog',
|
||||||
previewUrl: 'https://demo.ghost.io/',
|
previewUrl: 'https://demo.ghost.io/',
|
||||||
|
@ -12,18 +12,17 @@ import {tracked} from '@glimmer/tracking';
|
|||||||
|
|
||||||
// TODO: Long term move asset management directly in AdminX
|
// TODO: Long term move asset management directly in AdminX
|
||||||
const officialThemes = [{
|
const officialThemes = [{
|
||||||
|
name: 'Source',
|
||||||
|
category: 'News',
|
||||||
|
previewUrl: 'https://source.ghost.io/',
|
||||||
|
ref: 'default',
|
||||||
|
image: 'assets/img/themes/Source.png'
|
||||||
|
}, {
|
||||||
name: 'Casper',
|
name: 'Casper',
|
||||||
category: 'Blog',
|
category: 'Blog',
|
||||||
previewUrl: 'https://demo.ghost.io/',
|
previewUrl: 'https://demo.ghost.io/',
|
||||||
ref: 'default',
|
ref: 'TryGhost/Casper',
|
||||||
image: 'assets/img/themes/Casper.png'
|
image: 'assets/img/themes/Casper.png'
|
||||||
}, {
|
|
||||||
name: 'Headline',
|
|
||||||
category: 'News',
|
|
||||||
url: 'https://github.com/TryGhost/Headline',
|
|
||||||
previewUrl: 'https://headline.ghost.io',
|
|
||||||
ref: 'TryGhost/Headline',
|
|
||||||
image: 'assets/img/themes/Headline.png'
|
|
||||||
}, {
|
}, {
|
||||||
name: 'Edition',
|
name: 'Edition',
|
||||||
category: 'Newsletter',
|
category: 'Newsletter',
|
||||||
@ -108,6 +107,13 @@ const officialThemes = [{
|
|||||||
previewUrl: 'https://ease.ghost.io',
|
previewUrl: 'https://ease.ghost.io',
|
||||||
ref: 'TryGhost/Ease',
|
ref: 'TryGhost/Ease',
|
||||||
image: 'assets/img/themes/Ease.png'
|
image: 'assets/img/themes/Ease.png'
|
||||||
|
}, {
|
||||||
|
name: 'Headline',
|
||||||
|
category: 'News',
|
||||||
|
url: 'https://github.com/TryGhost/Headline',
|
||||||
|
previewUrl: 'https://headline.ghost.io',
|
||||||
|
ref: 'TryGhost/Headline',
|
||||||
|
image: 'assets/img/themes/Headline.png'
|
||||||
}, {
|
}, {
|
||||||
name: 'Ruby',
|
name: 'Ruby',
|
||||||
category: 'Magazine',
|
category: 'Magazine',
|
||||||
|
@ -18,6 +18,14 @@ export default class GhThemeTableComponent extends Component {
|
|||||||
this.activateTaskInstance?.cancel();
|
this.activateTaskInstance?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isDefaultTheme(theme) {
|
||||||
|
return theme.name.toLowerCase() === 'source';
|
||||||
|
}
|
||||||
|
|
||||||
|
isLegacyTheme(theme) {
|
||||||
|
return theme.name.toLowerCase() === 'casper';
|
||||||
|
}
|
||||||
|
|
||||||
get sortedThemes() {
|
get sortedThemes() {
|
||||||
let themes = this.args.themes.map((t) => {
|
let themes = this.args.themes.map((t) => {
|
||||||
let theme = {};
|
let theme = {};
|
||||||
@ -30,7 +38,6 @@ export default class GhThemeTableComponent extends Component {
|
|||||||
theme.package = themePackage;
|
theme.package = themePackage;
|
||||||
theme.active = get(t, 'active');
|
theme.active = get(t, 'active');
|
||||||
theme.isDeletable = !theme.active;
|
theme.isDeletable = !theme.active;
|
||||||
|
|
||||||
return theme;
|
return theme;
|
||||||
});
|
});
|
||||||
let duplicateThemes = [];
|
let duplicateThemes = [];
|
||||||
@ -44,19 +51,24 @@ export default class GhThemeTableComponent extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
duplicateThemes.forEach((theme) => {
|
duplicateThemes.forEach((theme) => {
|
||||||
if (theme.name !== 'casper') {
|
if (!this.isDefaultTheme(theme) && !this.isLegacyTheme(theme)) {
|
||||||
theme.label = `${theme.label} (${theme.name})`;
|
theme.label = `${theme.label} (${theme.name})`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// "(default)" needs to be added to casper manually as it's always
|
// add (default) or (legacy) as appropriate and prevent deletion of default/legacy themes
|
||||||
// displayed and would mess up the duplicate checking if added earlier
|
// this needs to be after deduplicating by label
|
||||||
let casper = themes.findBy('name', 'casper');
|
themes.filter(this.isDefaultTheme).forEach((theme) => {
|
||||||
if (casper) {
|
theme.label = `${theme.label} (default)`;
|
||||||
casper.label = `${casper.label} (default)`;
|
theme.isDefault = true;
|
||||||
casper.isDefault = true;
|
theme.isDeletable = false;
|
||||||
casper.isDeletable = false;
|
});
|
||||||
}
|
|
||||||
|
themes.filter(this.isLegacyTheme).forEach((theme) => {
|
||||||
|
theme.label = `${theme.label} (legacy)`;
|
||||||
|
theme.isLegacy = true;
|
||||||
|
theme.isDeletable = false;
|
||||||
|
});
|
||||||
|
|
||||||
// sorting manually because .sortBy('label') has a different sorting
|
// sorting manually because .sortBy('label') has a different sorting
|
||||||
// algorithm to [...strings].sort()
|
// algorithm to [...strings].sort()
|
||||||
|
@ -31,8 +31,8 @@ export default class InstallThemeModal extends Component {
|
|||||||
return this.args.data.theme?.ref || this.args.data.ref;
|
return this.args.data.theme?.ref || this.args.data.ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isDefaultTheme() {
|
get isDefaultOrLegacyTheme() {
|
||||||
return this.themeName.toLowerCase() === 'casper';
|
return this.themeName.toLowerCase() === 'casper' || this.themeName.toLowerCase() === 'source';
|
||||||
}
|
}
|
||||||
|
|
||||||
get isConfirming() {
|
get isConfirming() {
|
||||||
@ -48,7 +48,7 @@ export default class InstallThemeModal extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get willOverwriteExisting() {
|
get willOverwriteExisting() {
|
||||||
return !this.isDefaultTheme && this.themes.findBy('name', this.themeName.toLowerCase());
|
return !this.isDefaultOrLegacyTheme && this.themes.findBy('name', this.themeName.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasWarningsOrErrors() {
|
get hasWarningsOrErrors() {
|
||||||
@ -67,9 +67,10 @@ export default class InstallThemeModal extends Component {
|
|||||||
@task
|
@task
|
||||||
*installThemeTask() {
|
*installThemeTask() {
|
||||||
try {
|
try {
|
||||||
if (this.isDefaultTheme) {
|
if (this.isDefaultOrLegacyTheme) {
|
||||||
// default theme can't be installed, only activated
|
// default theme can't be installed, only activated
|
||||||
const defaultTheme = this.store.peekRecord('theme', 'casper');
|
const themeName = this.themeName.toLowerCase();
|
||||||
|
const defaultTheme = this.store.peekRecord('theme', themeName);
|
||||||
yield this.themeManagement.activateTask.perform(defaultTheme, {skipErrors: true});
|
yield this.themeManagement.activateTask.perform(defaultTheme, {skipErrors: true});
|
||||||
this.installedTheme = defaultTheme;
|
this.installedTheme = defaultTheme;
|
||||||
|
|
||||||
|
@ -91,8 +91,8 @@ export default class UploadThemeModal extends Component {
|
|||||||
return new UnsupportedMediaTypeError();
|
return new UnsupportedMediaTypeError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.name.match(/^casper\.zip$/i)) {
|
if (file.name.match(/^casper\.zip$/i) || file.name.match(/^source\.zip$/i)) {
|
||||||
return {payload: {errors: [{message: 'Sorry, the default Casper theme cannot be overwritten.<br>Please rename your zip file to continue.'}]}};
|
return {payload: {errors: [{message: 'Sorry, the default theme cannot be overwritten.<br>Please rename your zip file to continue.'}]}};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._allowOverwrite && this.currentThemeNames.includes(themeName)) {
|
if (!this._allowOverwrite && this.currentThemeNames.includes(themeName)) {
|
||||||
|
@ -13,18 +13,17 @@ export default class ChangeThemeController extends Controller {
|
|||||||
themes = this.store.peekAll('theme');
|
themes = this.store.peekAll('theme');
|
||||||
|
|
||||||
officialThemes = [{
|
officialThemes = [{
|
||||||
|
name: 'Source',
|
||||||
|
category: 'News',
|
||||||
|
previewUrl: 'https://source.ghost.io/',
|
||||||
|
ref: 'default',
|
||||||
|
image: 'assets/img/themes/Source.png'
|
||||||
|
}, {
|
||||||
name: 'Casper',
|
name: 'Casper',
|
||||||
category: 'Blog',
|
category: 'Blog',
|
||||||
previewUrl: 'https://demo.ghost.io/',
|
previewUrl: 'https://demo.ghost.io/',
|
||||||
ref: 'default',
|
ref: 'TryGhost/Casper',
|
||||||
image: 'assets/img/themes/Casper.png'
|
image: 'assets/img/themes/Casper.png'
|
||||||
}, {
|
|
||||||
name: 'Headline',
|
|
||||||
category: 'News',
|
|
||||||
url: 'https://github.com/TryGhost/Headline',
|
|
||||||
previewUrl: 'https://headline.ghost.io',
|
|
||||||
ref: 'TryGhost/Headline',
|
|
||||||
image: 'assets/img/themes/Headline.png'
|
|
||||||
}, {
|
}, {
|
||||||
name: 'Edition',
|
name: 'Edition',
|
||||||
category: 'Newsletter',
|
category: 'Newsletter',
|
||||||
@ -109,6 +108,13 @@ export default class ChangeThemeController extends Controller {
|
|||||||
previewUrl: 'https://ease.ghost.io',
|
previewUrl: 'https://ease.ghost.io',
|
||||||
ref: 'TryGhost/Ease',
|
ref: 'TryGhost/Ease',
|
||||||
image: 'assets/img/themes/Ease.png'
|
image: 'assets/img/themes/Ease.png'
|
||||||
|
}, {
|
||||||
|
name: 'Headline',
|
||||||
|
category: 'News',
|
||||||
|
url: 'https://github.com/TryGhost/Headline',
|
||||||
|
previewUrl: 'https://headline.ghost.io',
|
||||||
|
ref: 'TryGhost/Headline',
|
||||||
|
image: 'assets/img/themes/Headline.png'
|
||||||
}, {
|
}, {
|
||||||
name: 'Ruby',
|
name: 'Ruby',
|
||||||
category: 'Magazine',
|
category: 'Magazine',
|
||||||
|
@ -6,6 +6,7 @@ import {tracked} from '@glimmer/tracking';
|
|||||||
|
|
||||||
const THEME_PROPERTIES = {
|
const THEME_PROPERTIES = {
|
||||||
casper: ['description', 'color', 'coverImage'],
|
casper: ['description', 'color', 'coverImage'],
|
||||||
|
source: ['description', 'color', 'coverImage'],
|
||||||
edition: ['description', 'color', 'coverImage'],
|
edition: ['description', 'color', 'coverImage'],
|
||||||
dawn: ['description', 'color', 'icon'],
|
dawn: ['description', 'color', 'icon'],
|
||||||
dope: ['description', 'color', 'logo'],
|
dope: ['description', 'color', 'logo'],
|
||||||
|
@ -19,7 +19,7 @@ function setting(group, key, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// These settings represent a default new site setup
|
// These settings represent a default new site setup
|
||||||
// Real default settings can be found in https://github.com/TryGhost/Ghost/blob/main/core/server/data/schema/default-settings/default-settings.json
|
// Real default settings can be found in https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/data/schema/default-settings/default-settings.json
|
||||||
export default [
|
export default [
|
||||||
// SITE
|
// SITE
|
||||||
setting('site', 'title', 'Test Blog'),
|
setting('site', 'title', 'Test Blog'),
|
||||||
@ -49,7 +49,7 @@ export default [
|
|||||||
setting('site', 'twitter_description', null),
|
setting('site', 'twitter_description', null),
|
||||||
|
|
||||||
// THEME
|
// THEME
|
||||||
setting('theme', 'active_theme', 'Casper'),
|
setting('theme', 'active_theme', 'Source'),
|
||||||
|
|
||||||
// PRIVATE
|
// PRIVATE
|
||||||
setting('private', 'is_private', false),
|
setting('private', 'is_private', false),
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
export default [
|
export default [
|
||||||
|
{
|
||||||
|
name: 'source',
|
||||||
|
package: {
|
||||||
|
name: 'source',
|
||||||
|
version: '1.0'
|
||||||
|
},
|
||||||
|
active: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'casper',
|
name: 'casper',
|
||||||
package: {
|
package: {
|
||||||
name: 'casper',
|
name: 'casper',
|
||||||
version: '1.0'
|
version: '1.0'
|
||||||
},
|
}
|
||||||
active: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
|
BIN
ghost/admin/public/assets/img/themes/Source-Magazine.png
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
ghost/admin/public/assets/img/themes/Source-Newsletter.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
ghost/admin/public/assets/img/themes/Source.png
Normal file
After Width: | Height: | Size: 66 KiB |
@ -46,7 +46,7 @@ describe('Acceptance: Settings - Design', function () {
|
|||||||
expect(findAll('[data-test-nav-group]'), 'no of groups open').to.have.lengthOf(1);
|
expect(findAll('[data-test-nav-group]'), 'no of groups open').to.have.lengthOf(1);
|
||||||
|
|
||||||
// current theme is shown in nav menu
|
// current theme is shown in nav menu
|
||||||
expect(find('[data-test-text="current-theme"]')).to.contain.text('casper - v1.0');
|
expect(find('[data-test-text="current-theme"]')).to.contain.text('source - v1.0');
|
||||||
|
|
||||||
// defaults to "home" desktop preview
|
// defaults to "home" desktop preview
|
||||||
expect(find('[data-test-button="desktop-preview"]')).to.have.class('gh-btn-group-selected');
|
expect(find('[data-test-button="desktop-preview"]')).to.have.class('gh-btn-group-selected');
|
||||||
@ -143,7 +143,7 @@ describe('Acceptance: Settings - Design', function () {
|
|||||||
config.hostSettings = {
|
config.hostSettings = {
|
||||||
limits: {
|
limits: {
|
||||||
customThemes: {
|
customThemes: {
|
||||||
allowlist: ['casper', 'dawn', 'lyra'],
|
allowlist: ['source', 'casper', 'dawn', 'lyra'],
|
||||||
error: 'All our official built-in themes are available the Starter plan, if you upgrade to one of our higher tiers you will also be able to edit and upload custom themes for your site.'
|
error: 'All our official built-in themes are available the Starter plan, if you upgrade to one of our higher tiers you will also be able to edit and upload custom themes for your site.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ describe('Integration: Component: gh-theme-table', function () {
|
|||||||
this.set('themes', [
|
this.set('themes', [
|
||||||
{name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true},
|
{name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true},
|
||||||
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
|
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
|
||||||
|
{name: 'source', package: {name: 'Source', version: '1.0.0'}},
|
||||||
{name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}},
|
{name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}},
|
||||||
{name: 'foo'}
|
{name: 'foo'}
|
||||||
]);
|
]);
|
||||||
@ -18,14 +19,15 @@ describe('Integration: Component: gh-theme-table', function () {
|
|||||||
await render(hbs`<GhThemeTable @themes={{themes}} />`);
|
await render(hbs`<GhThemeTable @themes={{themes}} />`);
|
||||||
|
|
||||||
expect(findAll('[data-test-themes-list]').length, 'themes list is present').to.equal(1);
|
expect(findAll('[data-test-themes-list]').length, 'themes list is present').to.equal(1);
|
||||||
expect(findAll('[data-test-theme-id]').length, 'number of rows').to.equal(4);
|
expect(findAll('[data-test-theme-id]').length, 'number of rows').to.equal(5);
|
||||||
|
|
||||||
let packageNames = findAll('[data-test-theme-title]').map(name => name.textContent.trim());
|
let packageNames = findAll('[data-test-theme-title]').map(name => name.textContent.trim());
|
||||||
|
|
||||||
expect(packageNames[0]).to.match(/Casper \(default\)/);
|
expect(packageNames[0]).to.match(/Casper \(legacy\)/);
|
||||||
expect(packageNames[1]).to.match(/Daring\s+Active/);
|
expect(packageNames[1]).to.match(/Daring\s+Active/);
|
||||||
expect(packageNames[2]).to.match(/foo/);
|
expect(packageNames[2]).to.match(/foo/);
|
||||||
expect(packageNames[3]).to.match(/Lanyon/);
|
expect(packageNames[3]).to.match(/Lanyon/);
|
||||||
|
expect(packageNames[4]).to.match(/Source \(default\)/);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
find('[data-test-theme-active="true"]').querySelector('[data-test-theme-title]'),
|
find('[data-test-theme-active="true"]').querySelector('[data-test-theme-title]'),
|
||||||
@ -35,7 +37,7 @@ describe('Integration: Component: gh-theme-table', function () {
|
|||||||
expect(
|
expect(
|
||||||
findAll('[data-test-button="activate"]').length,
|
findAll('[data-test-button="activate"]').length,
|
||||||
'non-active themes have an activate link'
|
'non-active themes have an activate link'
|
||||||
).to.equal(3);
|
).to.equal(4);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
find('[data-test-theme-active="true"]').querySelector('[data-test-button="activate"]'),
|
find('[data-test-theme-active="true"]').querySelector('[data-test-button="activate"]'),
|
||||||
@ -80,22 +82,31 @@ describe('Integration: Component: gh-theme-table', function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not show delete action for casper', async function () {
|
it('does not show delete action for default themes', async function () {
|
||||||
const themes = [
|
const themes = [
|
||||||
{name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true},
|
{name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true},
|
||||||
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
|
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
|
||||||
{name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}},
|
{name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}},
|
||||||
{name: 'foo'}
|
{name: 'foo'},
|
||||||
|
{name: 'source', package: {name: 'Source', version: '1.0.0'}}
|
||||||
];
|
];
|
||||||
this.set('themes', themes);
|
this.set('themes', themes);
|
||||||
|
|
||||||
await render(hbs`<GhThemeTable @themes={{themes}} />`);
|
await render(hbs`<GhThemeTable @themes={{themes}} />`);
|
||||||
|
|
||||||
|
// Casper should not be deletable
|
||||||
await click(`[data-test-theme-id="casper"] [data-test-button="actions"]`);
|
await click(`[data-test-theme-id="casper"] [data-test-button="actions"]`);
|
||||||
expect(find('[data-test-actions-for="casper"]')).to.exist;
|
expect(find('[data-test-actions-for="casper"]')).to.exist;
|
||||||
expect(
|
expect(
|
||||||
find(`[data-test-actions-for="casper"] [data-test-button="delete"]`)
|
find(`[data-test-actions-for="casper"] [data-test-button="delete"]`)
|
||||||
).to.not.exist;
|
).to.not.exist;
|
||||||
|
|
||||||
|
// Source should not be deletable
|
||||||
|
await click(`[data-test-theme-id="source"] [data-test-button="actions"]`);
|
||||||
|
expect(find('[data-test-actions-for="source"]')).to.exist;
|
||||||
|
expect(
|
||||||
|
find(`[data-test-actions-for="source"] [data-test-button="delete"]`)
|
||||||
|
).to.not.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not show delete action for active theme', async function () {
|
it('does not show delete action for active theme', async function () {
|
||||||
@ -120,6 +131,7 @@ describe('Integration: Component: gh-theme-table', function () {
|
|||||||
this.set('themes', [
|
this.set('themes', [
|
||||||
{name: 'daring', package: {name: 'Daring', version: '0.1.4'}},
|
{name: 'daring', package: {name: 'Daring', version: '0.1.4'}},
|
||||||
{name: 'daring-0.1.5', package: {name: 'Daring', version: '0.1.4'}},
|
{name: 'daring-0.1.5', package: {name: 'Daring', version: '0.1.4'}},
|
||||||
|
{name: 'source', package: {name: 'Source', version: '1.0.0'}},
|
||||||
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
|
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
|
||||||
{name: 'another', package: {name: 'Casper', version: '1.3.1'}},
|
{name: 'another', package: {name: 'Casper', version: '1.3.1'}},
|
||||||
{name: 'mine', package: {name: 'Casper', version: '1.3.1'}},
|
{name: 'mine', package: {name: 'Casper', version: '1.3.1'}},
|
||||||
@ -135,11 +147,12 @@ describe('Integration: Component: gh-theme-table', function () {
|
|||||||
'themes are ordered by label, folder names shown for duplicates'
|
'themes are ordered by label, folder names shown for duplicates'
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
'Casper (another)',
|
'Casper (another)',
|
||||||
'Casper (default)',
|
'Casper (legacy)',
|
||||||
'Casper (mine)',
|
'Casper (mine)',
|
||||||
'Daring (daring)',
|
'Daring (daring)',
|
||||||
'Daring (daring-0.1.5)',
|
'Daring (daring-0.1.5)',
|
||||||
'foo'
|
'foo',
|
||||||
|
'Source (default)'
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -32,6 +32,8 @@ content/settings/**
|
|||||||
content/themes/**
|
content/themes/**
|
||||||
!content/themes/casper/**
|
!content/themes/casper/**
|
||||||
content/themes/casper/yarn.lock
|
content/themes/casper/yarn.lock
|
||||||
|
!content/themes/source/**
|
||||||
|
content/themes/source/yarn.lock
|
||||||
node_modules/**
|
node_modules/**
|
||||||
core/server/lib/members/static/auth/node_modules/**
|
core/server/lib/members/static/auth/node_modules/**
|
||||||
**/*.db
|
**/*.db
|
||||||
@ -50,6 +52,7 @@ core/built/**/tests-*
|
|||||||
test/**
|
test/**
|
||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
content/themes/casper/SECURITY.md
|
content/themes/casper/SECURITY.md
|
||||||
|
content/themes/source/SECURITY.md
|
||||||
SECURITY.md
|
SECURITY.md
|
||||||
renovate.json
|
renovate.json
|
||||||
*.html
|
*.html
|
||||||
@ -61,7 +64,9 @@ bower_components/**
|
|||||||
.editorconfig
|
.editorconfig
|
||||||
gulpfile.js
|
gulpfile.js
|
||||||
!content/themes/casper/gulpfile.js
|
!content/themes/casper/gulpfile.js
|
||||||
|
!content/themes/source/gulpfile.js
|
||||||
package-lock.json
|
package-lock.json
|
||||||
content/themes/casper/config.*.json
|
content/themes/casper/config.*.json
|
||||||
|
content/themes/source/config.*.json
|
||||||
config.*.json
|
config.*.json
|
||||||
!core/shared/config/env/**
|
!core/shared/config/env/**
|
||||||
|
1
ghost/core/content/themes/source
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit e0483fd7f4d6a9e2d5c26c1e89d4322f1f2702d6
|
@ -0,0 +1,40 @@
|
|||||||
|
// For information on writing migrations, see https://www.notion.so/ghost/Database-migrations-eb5b78c435d741d2b34a582d57c24253
|
||||||
|
|
||||||
|
const logging = require('@tryghost/logging');
|
||||||
|
const {createTransactionalMigration} = require('../../utils');
|
||||||
|
|
||||||
|
// For DDL - schema changes
|
||||||
|
// const {createNonTransactionalMigration} = require('../../utils');
|
||||||
|
|
||||||
|
// For DML - data changes
|
||||||
|
// const {createTransactionalMigration} = require('../../utils');
|
||||||
|
|
||||||
|
// Or use a specific helper
|
||||||
|
// const {addTable, createAddColumnMigration} = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = createTransactionalMigration(
|
||||||
|
async function up() {
|
||||||
|
// don't do anything
|
||||||
|
// we don't want to change the active theme automatically
|
||||||
|
},
|
||||||
|
async function down(knex) {
|
||||||
|
// If the active theme is `source`, we want to revert the active theme to `casper`
|
||||||
|
// `source` is introduced in 5.67, so it does not exist in < 5.67
|
||||||
|
// Without this change, rolling back from 5.67 to < 5.67 results in an error:
|
||||||
|
// The currently active theme "source" is missing.
|
||||||
|
const rows = await knex
|
||||||
|
.select('value')
|
||||||
|
.from('settings')
|
||||||
|
.where({key: 'active_theme', value: 'source'});
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
logging.info(`Currently installed theme is not source - skipping migration`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info(`Resetting the active theme to casper`);
|
||||||
|
await knex('settings')
|
||||||
|
.where('key', 'active_theme')
|
||||||
|
.update({value: 'casper'});
|
||||||
|
}
|
||||||
|
);
|
@ -216,7 +216,7 @@
|
|||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"active_theme": {
|
"active_theme": {
|
||||||
"defaultValue": "casper",
|
"defaultValue": "source",
|
||||||
"flags": "RO",
|
"flags": "RO",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
@ -200,8 +200,8 @@ async function installTheme(data, api) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeName.toLowerCase() === 'tryghost/casper') {
|
if (themeName.toLowerCase() === 'tryghost/source') {
|
||||||
logging.warn('Skipping theme install as Casper is the default theme.');
|
logging.warn('Skipping theme install as Source is the default theme.');
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,7 +219,7 @@ async function installTheme(data, api) {
|
|||||||
context: {internal: true}
|
context: {internal: true}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//Fallback to Casper by doing nothing as the theme setting update is the last step
|
//Fallback to Casper/Source by doing nothing as the theme setting update is the last step
|
||||||
logging.warn(tpl(messages.failedThemeInstall, {themeName, error: error.message}));
|
logging.warn(tpl(messages.failedThemeInstall, {themeName, error: error.message}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,8 +18,8 @@ const settingsCache = require('../../../shared/settings-cache');
|
|||||||
const messages = {
|
const messages = {
|
||||||
themeDoesNotExist: 'Theme does not exist.',
|
themeDoesNotExist: 'Theme does not exist.',
|
||||||
invalidThemeName: 'Please select a valid theme.',
|
invalidThemeName: 'Please select a valid theme.',
|
||||||
overrideCasper: 'Please rename your zip, it\'s not allowed to override the default casper theme.',
|
overrideDefaultTheme: 'Please rename your zip, it\'s not allowed to override the default theme.',
|
||||||
destroyCasper: 'Deleting the default casper theme is not allowed.',
|
destroyDefaultTheme: 'Deleting the default theme is not allowed.',
|
||||||
destroyActive: 'Deleting the active theme is not allowed.'
|
destroyActive: 'Deleting the active theme is not allowed.'
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -49,10 +49,10 @@ module.exports = {
|
|||||||
const themeName = getStorage().getSanitizedFileName(zip.name.split('.zip')[0]);
|
const themeName = getStorage().getSanitizedFileName(zip.name.split('.zip')[0]);
|
||||||
const backupName = `${themeName}_${ObjectID()}`;
|
const backupName = `${themeName}_${ObjectID()}`;
|
||||||
|
|
||||||
// check if zip name is casper.zip
|
// check if zip name matches one of the default themes
|
||||||
if (zip.name === 'casper.zip') {
|
if (zip.name === 'casper.zip' || zip.name === 'source.zip') {
|
||||||
throw new errors.ValidationError({
|
throw new errors.ValidationError({
|
||||||
message: tpl(messages.overrideCasper)
|
message: tpl(messages.overrideDefaultTheme)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,9 +127,9 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroy: async function (themeName) {
|
destroy: async function (themeName) {
|
||||||
if (themeName === 'casper') {
|
if (themeName === 'casper' || themeName === 'source') {
|
||||||
throw new errors.ValidationError({
|
throw new errors.ValidationError({
|
||||||
message: tpl(messages.destroyCasper)
|
message: tpl(messages.destroyDefaultTheme)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
test/coverage/**
|
test/coverage/**
|
||||||
test/utils/fixtures/themes/casper/assets/**
|
test/utils/fixtures/themes/casper/assets/**
|
||||||
|
test/utils/fixtures/themes/source/assets/**
|
||||||
|
@ -98,7 +98,7 @@ Object {
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "active_theme",
|
"key": "active_theme",
|
||||||
"value": "casper",
|
"value": "source",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "is_private",
|
"key": "is_private",
|
||||||
@ -367,7 +367,7 @@ Object {
|
|||||||
"settings": Array [
|
"settings": Array [
|
||||||
Object {
|
Object {
|
||||||
"key": "active_theme",
|
"key": "active_theme",
|
||||||
"value": "casper",
|
"value": "source",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@ -508,7 +508,7 @@ Object {
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "active_theme",
|
"key": "active_theme",
|
||||||
"value": "casper",
|
"value": "source",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "is_private",
|
"key": "is_private",
|
||||||
@ -866,7 +866,7 @@ Object {
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "active_theme",
|
"key": "active_theme",
|
||||||
"value": "casper",
|
"value": "source",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "is_private",
|
"key": "is_private",
|
||||||
@ -1223,7 +1223,7 @@ Object {
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "active_theme",
|
"key": "active_theme",
|
||||||
"value": "casper",
|
"value": "source",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "is_private",
|
"key": "is_private",
|
||||||
@ -1585,7 +1585,7 @@ Object {
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "active_theme",
|
"key": "active_theme",
|
||||||
"value": "casper",
|
"value": "source",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "is_private",
|
"key": "is_private",
|
||||||
@ -2035,7 +2035,7 @@ Object {
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "active_theme",
|
"key": "active_theme",
|
||||||
"value": "casper",
|
"value": "source",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "is_private",
|
"key": "is_private",
|
||||||
@ -2457,7 +2457,7 @@ Object {
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "active_theme",
|
"key": "active_theme",
|
||||||
"value": "casper",
|
"value": "source",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "is_private",
|
"key": "is_private",
|
||||||
|
@ -45,17 +45,17 @@ describe('Themes API', function () {
|
|||||||
const jsonResponse = res.body;
|
const jsonResponse = res.body;
|
||||||
should.exist(jsonResponse.themes);
|
should.exist(jsonResponse.themes);
|
||||||
localUtils.API.checkResponse(jsonResponse, 'themes');
|
localUtils.API.checkResponse(jsonResponse, 'themes');
|
||||||
jsonResponse.themes.length.should.eql(6);
|
jsonResponse.themes.length.should.eql(7);
|
||||||
|
|
||||||
localUtils.API.checkResponse(jsonResponse.themes[0], 'theme');
|
localUtils.API.checkResponse(jsonResponse.themes[0], 'theme');
|
||||||
jsonResponse.themes[0].name.should.eql('broken-theme');
|
jsonResponse.themes[0].name.should.eql('broken-theme');
|
||||||
jsonResponse.themes[0].package.should.be.an.Object().with.properties('name', 'version');
|
jsonResponse.themes[0].package.should.be.an.Object().with.properties('name', 'version');
|
||||||
jsonResponse.themes[0].active.should.be.false();
|
jsonResponse.themes[0].active.should.be.false();
|
||||||
|
|
||||||
localUtils.API.checkResponse(jsonResponse.themes[1], 'theme', 'templates');
|
localUtils.API.checkResponse(jsonResponse.themes[1], 'theme');
|
||||||
jsonResponse.themes[1].name.should.eql('casper');
|
jsonResponse.themes[1].name.should.eql('casper');
|
||||||
jsonResponse.themes[1].package.should.be.an.Object().with.properties('name', 'version');
|
jsonResponse.themes[1].package.should.be.an.Object().with.properties('name', 'version');
|
||||||
jsonResponse.themes[1].active.should.be.true();
|
jsonResponse.themes[1].active.should.be.false();
|
||||||
|
|
||||||
localUtils.API.checkResponse(jsonResponse.themes[2], 'theme');
|
localUtils.API.checkResponse(jsonResponse.themes[2], 'theme');
|
||||||
jsonResponse.themes[2].name.should.eql('locale-theme');
|
jsonResponse.themes[2].name.should.eql('locale-theme');
|
||||||
@ -67,15 +67,20 @@ describe('Themes API', function () {
|
|||||||
jsonResponse.themes[3].package.should.be.an.Object().with.properties('name', 'version');
|
jsonResponse.themes[3].package.should.be.an.Object().with.properties('name', 'version');
|
||||||
jsonResponse.themes[3].active.should.be.false();
|
jsonResponse.themes[3].active.should.be.false();
|
||||||
|
|
||||||
localUtils.API.checkResponse(jsonResponse.themes[4], 'theme');
|
localUtils.API.checkResponse(jsonResponse.themes[4], 'theme', 'templates');
|
||||||
jsonResponse.themes[4].name.should.eql('test-theme');
|
jsonResponse.themes[4].name.should.eql('source');
|
||||||
jsonResponse.themes[4].package.should.be.an.Object().with.properties('name', 'version');
|
jsonResponse.themes[4].package.should.be.an.Object().with.properties('name', 'version');
|
||||||
jsonResponse.themes[4].active.should.be.false();
|
jsonResponse.themes[4].active.should.be.true();
|
||||||
|
|
||||||
localUtils.API.checkResponse(jsonResponse.themes[5], 'theme');
|
localUtils.API.checkResponse(jsonResponse.themes[5], 'theme');
|
||||||
jsonResponse.themes[5].name.should.eql('test-theme-channels');
|
jsonResponse.themes[5].name.should.eql('test-theme');
|
||||||
jsonResponse.themes[5].package.should.be.false();
|
jsonResponse.themes[5].package.should.be.an.Object().with.properties('name', 'version');
|
||||||
jsonResponse.themes[5].active.should.be.false();
|
jsonResponse.themes[5].active.should.be.false();
|
||||||
|
|
||||||
|
localUtils.API.checkResponse(jsonResponse.themes[6], 'theme');
|
||||||
|
jsonResponse.themes[6].name.should.eql('test-theme-channels');
|
||||||
|
jsonResponse.themes[6].package.should.be.false();
|
||||||
|
jsonResponse.themes[6].active.should.be.false();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Can download a theme', async function () {
|
it('Can download a theme', async function () {
|
||||||
@ -129,13 +134,13 @@ describe('Themes API', function () {
|
|||||||
|
|
||||||
should.exist(jsonResponse3.themes);
|
should.exist(jsonResponse3.themes);
|
||||||
localUtils.API.checkResponse(jsonResponse3, 'themes');
|
localUtils.API.checkResponse(jsonResponse3, 'themes');
|
||||||
jsonResponse3.themes.length.should.eql(7);
|
jsonResponse3.themes.length.should.eql(8);
|
||||||
|
|
||||||
// Casper should be present and still active
|
// Source should be present and still active
|
||||||
const casperTheme = _.find(jsonResponse3.themes, {name: 'casper'});
|
const sourceTheme = _.find(jsonResponse3.themes, {name: 'source'});
|
||||||
should.exist(casperTheme);
|
should.exist(sourceTheme);
|
||||||
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
|
localUtils.API.checkResponse(sourceTheme, 'theme', 'templates');
|
||||||
casperTheme.active.should.be.true();
|
sourceTheme.active.should.be.true();
|
||||||
|
|
||||||
// The added theme should be here
|
// The added theme should be here
|
||||||
const addedTheme = _.find(jsonResponse3.themes, {name: 'valid'});
|
const addedTheme = _.find(jsonResponse3.themes, {name: 'valid'});
|
||||||
@ -149,6 +154,7 @@ describe('Themes API', function () {
|
|||||||
'casper',
|
'casper',
|
||||||
'locale-theme',
|
'locale-theme',
|
||||||
'members-test-theme',
|
'members-test-theme',
|
||||||
|
'source',
|
||||||
'test-theme',
|
'test-theme',
|
||||||
'test-theme-channels',
|
'test-theme-channels',
|
||||||
'valid'
|
'valid'
|
||||||
@ -171,7 +177,7 @@ describe('Themes API', function () {
|
|||||||
tmpFolderContents.splice(i, 1);
|
tmpFolderContents.splice(i, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tmpFolderContents.should.be.an.Array().with.lengthOf(10);
|
tmpFolderContents.should.be.an.Array().with.lengthOf(11);
|
||||||
|
|
||||||
tmpFolderContents.should.eql([
|
tmpFolderContents.should.eql([
|
||||||
'broken-theme',
|
'broken-theme',
|
||||||
@ -180,6 +186,7 @@ describe('Themes API', function () {
|
|||||||
'invalid.zip',
|
'invalid.zip',
|
||||||
'locale-theme',
|
'locale-theme',
|
||||||
'members-test-theme',
|
'members-test-theme',
|
||||||
|
'source',
|
||||||
'test-theme',
|
'test-theme',
|
||||||
'test-theme-channels',
|
'test-theme-channels',
|
||||||
'valid.zip',
|
'valid.zip',
|
||||||
@ -196,13 +203,13 @@ describe('Themes API', function () {
|
|||||||
|
|
||||||
should.exist(jsonResponse2.themes);
|
should.exist(jsonResponse2.themes);
|
||||||
localUtils.API.checkResponse(jsonResponse2, 'themes');
|
localUtils.API.checkResponse(jsonResponse2, 'themes');
|
||||||
jsonResponse2.themes.length.should.eql(6);
|
jsonResponse2.themes.length.should.eql(7);
|
||||||
|
|
||||||
// Casper should be present and still active
|
// Source should be present and still active
|
||||||
const casperTheme = _.find(jsonResponse2.themes, {name: 'casper'});
|
const sourceTheme = _.find(jsonResponse2.themes, {name: 'source'});
|
||||||
should.exist(casperTheme);
|
should.exist(sourceTheme);
|
||||||
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
|
localUtils.API.checkResponse(sourceTheme, 'theme', 'templates');
|
||||||
casperTheme.active.should.be.true();
|
sourceTheme.active.should.be.true();
|
||||||
|
|
||||||
// The deleted theme should not be here
|
// The deleted theme should not be here
|
||||||
const deletedTheme = _.find(jsonResponse2.themes, {name: 'valid'});
|
const deletedTheme = _.find(jsonResponse2.themes, {name: 'valid'});
|
||||||
@ -238,12 +245,12 @@ describe('Themes API', function () {
|
|||||||
|
|
||||||
should.exist(jsonResponse.themes);
|
should.exist(jsonResponse.themes);
|
||||||
localUtils.API.checkResponse(jsonResponse, 'themes');
|
localUtils.API.checkResponse(jsonResponse, 'themes');
|
||||||
jsonResponse.themes.length.should.eql(6);
|
jsonResponse.themes.length.should.eql(7);
|
||||||
|
|
||||||
const casperTheme = _.find(jsonResponse.themes, {name: 'casper'});
|
const sourceTheme = _.find(jsonResponse.themes, {name: 'source'});
|
||||||
should.exist(casperTheme);
|
should.exist(sourceTheme);
|
||||||
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
|
localUtils.API.checkResponse(sourceTheme, 'theme', 'templates');
|
||||||
casperTheme.active.should.be.true();
|
sourceTheme.active.should.be.true();
|
||||||
|
|
||||||
const testTheme = _.find(jsonResponse.themes, {name: 'test-theme'});
|
const testTheme = _.find(jsonResponse.themes, {name: 'test-theme'});
|
||||||
should.exist(testTheme);
|
should.exist(testTheme);
|
||||||
@ -263,8 +270,8 @@ describe('Themes API', function () {
|
|||||||
localUtils.API.checkResponse(jsonResponse2, 'themes');
|
localUtils.API.checkResponse(jsonResponse2, 'themes');
|
||||||
jsonResponse2.themes.length.should.eql(1);
|
jsonResponse2.themes.length.should.eql(1);
|
||||||
|
|
||||||
const casperTheme2 = _.find(jsonResponse2.themes, {name: 'casper'});
|
const sourceTheme2 = _.find(jsonResponse2.themes, {name: 'source'});
|
||||||
should.not.exist(casperTheme2);
|
should.not.exist(sourceTheme2);
|
||||||
|
|
||||||
const testTheme2 = _.find(jsonResponse2.themes, {name: 'test-theme'});
|
const testTheme2 = _.find(jsonResponse2.themes, {name: 'test-theme'});
|
||||||
should.exist(testTheme2);
|
should.exist(testTheme2);
|
||||||
@ -318,7 +325,7 @@ describe('Themes API', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Can re-upload the active theme to override', async function () {
|
it('Can re-upload the active theme to override', async function () {
|
||||||
// The tricky thing about this test is the default active theme is Casper and you're not allowed to override it.
|
// The tricky thing about this test is the default active theme is Source and you're not allowed to override it.
|
||||||
// So we upload a valid theme, activate it, and then upload again.
|
// So we upload a valid theme, activate it, and then upload again.
|
||||||
sinon.stub(settingsCache, 'get').callsFake(function (key, options) {
|
sinon.stub(settingsCache, 'get').callsFake(function (key, options) {
|
||||||
if (key === 'active_theme') {
|
if (key === 'active_theme') {
|
||||||
|
@ -68,7 +68,6 @@ describe('Default Frontend routing', function () {
|
|||||||
|
|
||||||
$('body.home-template').length.should.equal(1);
|
$('body.home-template').length.should.equal(1);
|
||||||
$('article.post').length.should.equal(7);
|
$('article.post').length.should.equal(7);
|
||||||
$('article.tag-getting-started').length.should.equal(7);
|
|
||||||
|
|
||||||
res.text.should.not.containEql('__GHOST_URL__');
|
res.text.should.not.containEql('__GHOST_URL__');
|
||||||
});
|
});
|
||||||
|
@ -114,7 +114,7 @@ describe('Importer', function () {
|
|||||||
return models.Settings.findOne(_.merge({key: 'active_theme'}, testUtils.context.internal));
|
return models.Settings.findOne(_.merge({key: 'active_theme'}, testUtils.context.internal));
|
||||||
})
|
})
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
result.attributes.value.should.eql('casper');
|
result.attributes.value.should.eql('source');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -195,7 +195,7 @@ describe('Authentication API', function () {
|
|||||||
const password = 'thisissupersafe';
|
const password = 'thisissupersafe';
|
||||||
|
|
||||||
const requestMock = nock('https://api.github.com')
|
const requestMock = nock('https://api.github.com')
|
||||||
.get('/repos/tryghost/casper/zipball')
|
.get('/repos/tryghost/source/zipball')
|
||||||
.query(true)
|
.query(true)
|
||||||
.replyWithFile(200, fixtureManager.getPathForFixture('themes/valid.zip'));
|
.replyWithFile(200, fixtureManager.getPathForFixture('themes/valid.zip'));
|
||||||
|
|
||||||
@ -207,7 +207,7 @@ describe('Authentication API', function () {
|
|||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
blogTitle: 'a test blog',
|
blogTitle: 'a test blog',
|
||||||
theme: 'TryGhost/Casper',
|
theme: 'TryGhost/Source',
|
||||||
accentColor: '#85FF00',
|
accentColor: '#85FF00',
|
||||||
description: 'Custom Site Description on Setup — great for everyone'
|
description: 'Custom Site Description on Setup — great for everyone'
|
||||||
}]
|
}]
|
||||||
@ -236,7 +236,7 @@ describe('Authentication API', function () {
|
|||||||
const activeTheme = await settingsCache.get('active_theme');
|
const activeTheme = await settingsCache.get('active_theme');
|
||||||
const accentColor = await settingsCache.get('accent_color');
|
const accentColor = await settingsCache.get('accent_color');
|
||||||
const description = await settingsCache.get('description');
|
const description = await settingsCache.get('description');
|
||||||
assert.equal(activeTheme, 'casper', 'The theme casper should have been installed');
|
assert.equal(activeTheme, 'source', 'The theme Source should have been installed');
|
||||||
assert.equal(accentColor, '#85FF00', 'The accent color should have been set');
|
assert.equal(accentColor, '#85FF00', 'The accent color should have been set');
|
||||||
assert.equal(description, 'Custom Site Description on Setup — great for everyone', 'The site description should have been set');
|
assert.equal(description, 'Custom Site Description on Setup — great for everyone', 'The site description should have been set');
|
||||||
|
|
||||||
|
@ -65,8 +65,7 @@ describe('Dynamic Routing', function () {
|
|||||||
|
|
||||||
$('title').text().should.equal('Ghost');
|
$('title').text().should.equal('Ghost');
|
||||||
$('body.home-template').length.should.equal(1);
|
$('body.home-template').length.should.equal(1);
|
||||||
$('article.post').length.should.equal(5);
|
$('article.post').length.should.equal(7);
|
||||||
$('article.tag-getting-started').length.should.equal(5);
|
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -142,9 +141,8 @@ describe('Dynamic Routing', function () {
|
|||||||
should.not.exist(res.headers['set-cookie']);
|
should.not.exist(res.headers['set-cookie']);
|
||||||
should.exist(res.headers.date);
|
should.exist(res.headers.date);
|
||||||
|
|
||||||
$('body').attr('class').should.eql('tag-template tag-getting-started');
|
$('body').attr('class').should.eql('tag-template tag-getting-started has-sans-title has-sans-body');
|
||||||
$('article.post').length.should.equal(5);
|
$('article.post').length.should.equal(5);
|
||||||
$('article.tag-getting-started').length.should.equal(5);
|
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@ -37,7 +37,7 @@ describe('DB version integrity', function () {
|
|||||||
// Only these variables should need updating
|
// Only these variables should need updating
|
||||||
const currentSchemaHash = '1b75aae9befefea53b17c9c1991c8a1d';
|
const currentSchemaHash = '1b75aae9befefea53b17c9c1991c8a1d';
|
||||||
const currentFixturesHash = '4db87173699ad9c9d8a67ccab96dfd2d';
|
const currentFixturesHash = '4db87173699ad9c9d8a67ccab96dfd2d';
|
||||||
const currentSettingsHash = '3a7ca0aa6a06cba47e3e898aef7029c2';
|
const currentSettingsHash = '3128d4ec667a50049486b0c21f04be07';
|
||||||
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';
|
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';
|
||||||
|
|
||||||
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
|
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
|
||||||
|
@ -124,7 +124,7 @@ const prepareContentFolder = async ({contentFolder, redirectsFile = true, routes
|
|||||||
await fs.ensureDir(path.join(contentFolderForTests, 'adapters'));
|
await fs.ensureDir(path.join(contentFolderForTests, 'adapters'));
|
||||||
await fs.ensureDir(path.join(contentFolderForTests, 'settings'));
|
await fs.ensureDir(path.join(contentFolderForTests, 'settings'));
|
||||||
|
|
||||||
// Copy all themes into the new test content folder. Default active theme is always casper.
|
// Copy all themes into the new test content folder. Default active theme is always source.
|
||||||
// If you want to use a different theme, you have to set the active theme (e.g. stub)
|
// If you want to use a different theme, you have to set the active theme (e.g. stub)
|
||||||
await fs.copy(
|
await fs.copy(
|
||||||
path.join(__dirname, 'fixtures', 'themes'),
|
path.join(__dirname, 'fixtures', 'themes'),
|
||||||
|
@ -75,12 +75,12 @@ const prepareContentFolder = (options) => {
|
|||||||
fs.ensureDirSync(path.join(contentFolderForTests, 'settings'));
|
fs.ensureDirSync(path.join(contentFolderForTests, 'settings'));
|
||||||
|
|
||||||
if (options.copyThemes) {
|
if (options.copyThemes) {
|
||||||
// Copy all themes into the new test content folder. Default active theme is always casper. If you want to use a different theme, you have to set the active theme (e.g. stub)
|
// Copy all themes into the new test content folder. Default active theme is always source. If you want to use a different theme, you have to set the active theme (e.g. stub)
|
||||||
fs.copySync(path.join(__dirname, 'fixtures', 'themes'), path.join(contentFolderForTests, 'themes'));
|
fs.copySync(path.join(__dirname, 'fixtures', 'themes'), path.join(contentFolderForTests, 'themes'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy theme even if frontend is disabled, as admin can use casper when viewing themes section
|
// Copy theme even if frontend is disabled, as admin can use source when viewing themes section
|
||||||
fs.copySync(path.join(__dirname, 'fixtures', 'themes', 'casper'), path.join(contentFolderForTests, 'themes', 'casper'));
|
fs.copySync(path.join(__dirname, 'fixtures', 'themes', 'source'), path.join(contentFolderForTests, 'themes', 'source'));
|
||||||
|
|
||||||
if (options.redirectsFile) {
|
if (options.redirectsFile) {
|
||||||
redirects.setupFile(contentFolderForTests, options.redirectsFileExt);
|
redirects.setupFile(contentFolderForTests, options.redirectsFileExt);
|
||||||
|
@ -212,7 +212,7 @@
|
|||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"active_theme": {
|
"active_theme": {
|
||||||
"defaultValue": "casper",
|
"defaultValue": "source",
|
||||||
"flags": "RO",
|
"flags": "RO",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
@ -224,7 +224,7 @@
|
|||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"active_theme": {
|
"active_theme": {
|
||||||
"defaultValue": "casper",
|
"defaultValue": "source",
|
||||||
"flags": "RO",
|
"flags": "RO",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,25 @@ To update it:
|
|||||||
- `rm -rf casper`
|
- `rm -rf casper`
|
||||||
- `rsync -rv --exclude '.git*' --exclude 'assets/css*' --exclude 'assets/js*' --exclude 'gulpfile.js' --exclude 'yarn.lock' --exclude 'README.md' ../../../../content/themes/casper .`
|
- `rsync -rv --exclude '.git*' --exclude 'assets/css*' --exclude 'assets/js*' --exclude 'gulpfile.js' --exclude 'yarn.lock' --exclude 'README.md' ../../../../content/themes/casper .`
|
||||||
|
|
||||||
|
## Updating the Source theme fixture
|
||||||
|
The source fixture is a partial copy of the content/themes/source folder.
|
||||||
|
It should not include any files that aren't needed to run the theme.
|
||||||
|
|
||||||
|
To update it:
|
||||||
|
|
||||||
|
1. Ensure your `content/themes/source` folder is on the latest released version e.g.
|
||||||
|
|
||||||
|
- Run `yarn main`
|
||||||
|
- `cd content/themes/source`
|
||||||
|
- `git log -20` - find the latest tag
|
||||||
|
- `git checkout vx.y.z` - checkout the latest tag
|
||||||
|
|
||||||
|
2. Ensure you are in side this folder (the fixtures/themes directory), remove source entirely and then copy it across fresh:
|
||||||
|
|
||||||
|
- `cd tests/utils/fixtures/themes`
|
||||||
|
- `rm -rf source`
|
||||||
|
- `rsync -rv --exclude '.git*' --exclude 'assets/css*' --exclude 'assets/js*' --exclude 'gulpfile.js' --exclude 'yarn.lock' --exclude 'README.md' ../../../../content/themes/source .`
|
||||||
|
|
||||||
### Modifying theme fixtures
|
### Modifying theme fixtures
|
||||||
When a new rule is introduced in gscan one of these fixture files might break and you'll have to update a "zip" which isn't as easy as opening a text editor... It could become that one day but for now here are some commands to help out with the edit process
|
When a new rule is introduced in gscan one of these fixture files might break and you'll have to update a "zip" which isn't as easy as opening a text editor... It could become that one day but for now here are some commands to help out with the edit process
|
||||||
|
|
||||||
|
BIN
ghost/core/test/utils/fixtures/themes/source/assets/images/default-skin.png
Executable file
After Width: | Height: | Size: 547 B |
@ -0,0 +1 @@
|
|||||||
|
<svg width="264" height="88" viewBox="0 0 264 88" xmlns="http://www.w3.org/2000/svg"><title>default-skin 2</title><g fill="none" fill-rule="evenodd"><g><path d="M67.002 59.5v3.768c-6.307.84-9.184 5.75-10.002 9.732 2.22-2.83 5.564-5.098 10.002-5.098V71.5L73 65.585 67.002 59.5z" id="Shape" fill="#fff"/><g fill="#fff"><path d="M13 29v-5h2v3h3v2h-5zM13 15h5v2h-3v3h-2v-5zM31 15v5h-2v-3h-3v-2h5zM31 29h-5v-2h3v-3h2v5z" id="Shape"/></g><g fill="#fff"><path d="M62 24v5h-2v-3h-3v-2h5zM62 20h-5v-2h3v-3h2v5zM70 20v-5h2v3h3v2h-5zM70 24h5v2h-3v3h-2v-5z"/></g><path d="M20.586 66l-5.656-5.656 1.414-1.414L22 64.586l5.656-5.656 1.414 1.414L23.414 66l5.656 5.656-1.414 1.414L22 67.414l-5.656 5.656-1.414-1.414L20.586 66z" fill="#fff"/><path d="M111.785 65.03L110 63.5l3-3.5h-10v-2h10l-3-3.5 1.785-1.468L117 59l-5.215 6.03z" fill="#fff"/><path d="M152.215 65.03L154 63.5l-3-3.5h10v-2h-10l3-3.5-1.785-1.468L147 59l5.215 6.03z" fill="#fff"/><g><path id="Rectangle-11" fill="#fff" d="M160.957 28.543l-3.25-3.25-1.413 1.414 3.25 3.25z"/><path d="M152.5 27c3.038 0 5.5-2.462 5.5-5.5s-2.462-5.5-5.5-5.5-5.5 2.462-5.5 5.5 2.462 5.5 5.5 5.5z" id="Oval-1" stroke="#fff" stroke-width="1.5"/><path fill="#fff" d="M150 21h5v1h-5z"/></g><g><path d="M116.957 28.543l-1.414 1.414-3.25-3.25 1.414-1.414 3.25 3.25z" fill="#fff"/><path d="M108.5 27c3.038 0 5.5-2.462 5.5-5.5s-2.462-5.5-5.5-5.5-5.5 2.462-5.5 5.5 2.462 5.5 5.5 5.5z" stroke="#fff" stroke-width="1.5"/><path fill="#fff" d="M106 21h5v1h-5z"/><path fill="#fff" d="M109.043 19.008l-.085 5-1-.017.085-5z"/></g></g></g></svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
ghost/core/test/utils/fixtures/themes/source/assets/images/preloader.gif
Executable file
After Width: | Height: | Size: 866 B |
43
ghost/core/test/utils/fixtures/themes/source/author.hbs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{{!< default}}
|
||||||
|
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
|
||||||
|
|
||||||
|
<main class="gh-main gh-outer">
|
||||||
|
|
||||||
|
{{#author}}
|
||||||
|
<section class="gh-archive{{#if @custom.show_site_in_sidebar}} has-sidebar{{/if}} gh-inner">
|
||||||
|
<div class="gh-archive-inner">
|
||||||
|
<div class="gh-archive-wrapper">
|
||||||
|
<h1 class="gh-article-title is-title">
|
||||||
|
{{#if website}}
|
||||||
|
<a class="gh-author-social-link" href="{{website}}" target="_blank" rel="noopener">{{name}}</a>
|
||||||
|
{{else}}
|
||||||
|
{{name}}
|
||||||
|
{{/if}}
|
||||||
|
</h1>
|
||||||
|
{{#if bio}}
|
||||||
|
<p class="gh-article-excerpt">{{bio}}</p>
|
||||||
|
{{/if}}
|
||||||
|
<footer class="gh-author-meta">
|
||||||
|
<div class="gh-author-social">
|
||||||
|
{{#if facebook}}
|
||||||
|
<a class="gh-author-social-link" href="{{facebook_url}}" target="_blank" rel="noopener">{{> "icons/facebook"}}</a>
|
||||||
|
{{/if}}
|
||||||
|
{{#if twitter}}
|
||||||
|
<a class="gh-author-social-link" href="{{twitter_url}}" target="_blank" rel="noopener">{{> "icons/twitter"}}</a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{#if location}}
|
||||||
|
<div class="gh-author-location">{{location}}</div>
|
||||||
|
{{/if}}
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
{{#if profile_image}}
|
||||||
|
<img class="gh-article-image" src="{{img_url profile_image size="s"}}" alt="{{name}}">
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{/author}}
|
||||||
|
|
||||||
|
{{> "components/post-list" feed="archive" postFeedStyle=@custom.post_feed_style showTitle=false showSidebar=@custom.show_site_in_sidebar}}
|
||||||
|
|
||||||
|
</main>
|
67
ghost/core/test/utils/fixtures/themes/source/default.hbs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{@site.locale}}">
|
||||||
|
<head>
|
||||||
|
|
||||||
|
{{!-- Basic meta - advanced meta is output with {{ghost_head}} below --}}
|
||||||
|
<title>{{meta_title}}</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
{{!-- Preload main styles and scripts for better performance --}}
|
||||||
|
<link rel="preload" as="style" href="{{asset "built/screen.css"}}">
|
||||||
|
<link rel="preload" as="script" href="{{asset "built/source.js"}}">
|
||||||
|
|
||||||
|
{{!-- Theme assets - use the {{asset}} helper to reference styles & scripts, this will take care of caching and cache-busting automatically --}}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{asset "built/screen.css"}}">
|
||||||
|
|
||||||
|
{{!-- Custom background color --}}
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--background-color: {{@custom.site_background_color}}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script src="http://localhost:35729/livereload.js?snipver=1"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* The script for calculating the color contrast has been taken from
|
||||||
|
https://gomakethings.com/dynamically-changing-the-text-color-based-on-background-color-contrast-with-vanilla-js/ */
|
||||||
|
var accentColor = getComputedStyle(document.documentElement).getPropertyValue('--background-color');
|
||||||
|
accentColor = accentColor.trim().slice(1);
|
||||||
|
var r = parseInt(accentColor.substr(0, 2), 16);
|
||||||
|
var g = parseInt(accentColor.substr(2, 2), 16);
|
||||||
|
var b = parseInt(accentColor.substr(4, 2), 16);
|
||||||
|
var yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
||||||
|
var textColor = (yiq >= 128) ? 'dark' : 'light';
|
||||||
|
|
||||||
|
document.documentElement.className = `has-${textColor}-text`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{!-- This tag outputs all your advanced SEO meta, structured data, and other important settings, it should always be the last tag before the closing head tag --}}
|
||||||
|
{{ghost_head}}
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class="{{body_class}} has-{{#match @custom.title_font "Elegant serif"}}serif{{else match @custom.title_font "Clean slab"}}slab{{else}}sans{{/match}}-title has-{{#match @custom.body_font "Elegant serif"}}serif{{else}}sans{{/match}}-body">
|
||||||
|
|
||||||
|
<div class="gh-viewport">
|
||||||
|
|
||||||
|
{{> "components/navigation" navigationLayout=@custom.navigation_layout}}
|
||||||
|
|
||||||
|
{{{body}}}
|
||||||
|
|
||||||
|
{{> "components/footer"}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#is "post, page"}}
|
||||||
|
{{> "lightbox"}}
|
||||||
|
{{/is}}
|
||||||
|
|
||||||
|
{{!-- Scripts - handle responsive videos, infinite scroll, and navigation dropdowns --}}
|
||||||
|
<script src="{{asset "built/source.js"}}"></script>
|
||||||
|
|
||||||
|
{{!-- Ghost outputs required functional scripts with this tag, it should always be the last thing before the closing body tag --}}
|
||||||
|
{{ghost_foot}}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
12
ghost/core/test/utils/fixtures/themes/source/home.hbs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{{!< default}}
|
||||||
|
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
|
||||||
|
|
||||||
|
{{> "components/header" headerStyle=@custom.header_style}}
|
||||||
|
|
||||||
|
{{#match @custom.header_style "!=" "Highlight"}}
|
||||||
|
{{> "components/featured" showFeatured=@custom.highlight_featured_posts limit=4}}
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
{{> "components/cta"}}
|
||||||
|
|
||||||
|
{{> "components/post-list" feed="home" postFeedStyle=@custom.post_feed_style showTitle=true showSidebar=@custom.show_site_in_sidebar}}
|
6
ghost/core/test/utils/fixtures/themes/source/index.hbs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{{!< default}}
|
||||||
|
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
|
||||||
|
|
||||||
|
<main class="gh-main">
|
||||||
|
{{> "components/post-list" feed="index" postFeedStyle=@custom.post_feed_style showTitle=true showSidebar=@custom.show_site_in_sidebar}}
|
||||||
|
</main>
|
199
ghost/core/test/utils/fixtures/themes/source/package.json
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
{
|
||||||
|
"name": "source",
|
||||||
|
"description": "A default theme for the Ghost publishing platform",
|
||||||
|
"demo": "https://demo.ghost.io",
|
||||||
|
"version": "6.0.0",
|
||||||
|
"engines": {
|
||||||
|
"ghost": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "gulp",
|
||||||
|
"zip": "gulp zip",
|
||||||
|
"test": "gscan .",
|
||||||
|
"test:ci": "gscan --fatal --verbose .",
|
||||||
|
"pretest": "gulp build",
|
||||||
|
"preship": "yarn test",
|
||||||
|
"ship": "STATUS=$(git status --porcelain); echo $STATUS; if [ -z \"$STATUS\" ]; then yarn version && git push --follow-tags; else echo \"Uncomitted changes found.\" && exit 1; fi",
|
||||||
|
"postship": "git fetch && gulp release"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "Ghost Foundation",
|
||||||
|
"email": "hello@ghost.org",
|
||||||
|
"url": "https://ghost.org/"
|
||||||
|
},
|
||||||
|
"gpm": {
|
||||||
|
"type": "theme",
|
||||||
|
"categories": [
|
||||||
|
"Minimal",
|
||||||
|
"Magazine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"ghost",
|
||||||
|
"theme",
|
||||||
|
"ghost-theme"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/TryGhost/Source.git"
|
||||||
|
},
|
||||||
|
"bugs": "https://github.com/TryGhost/Source/issues",
|
||||||
|
"contributors": "https://github.com/TryGhost/Source/graphs/contributors",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tryghost/release-utils": "0.8.1",
|
||||||
|
"autoprefixer": "10.4.7",
|
||||||
|
"beeper": "2.1.0",
|
||||||
|
"cssnano": "5.1.12",
|
||||||
|
"gscan": "4.36.1",
|
||||||
|
"gulp": "4.0.2",
|
||||||
|
"gulp-concat": "2.6.1",
|
||||||
|
"gulp-livereload": "4.0.2",
|
||||||
|
"gulp-postcss": "9.0.1",
|
||||||
|
"gulp-uglify": "3.0.2",
|
||||||
|
"gulp-zip": "5.1.0",
|
||||||
|
"inquirer": "8.2.4",
|
||||||
|
"postcss": "8.2.13",
|
||||||
|
"postcss-easy-import": "4.0.0",
|
||||||
|
"pump": "3.0.0"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"defaults"
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"posts_per_page": 16,
|
||||||
|
"image_sizes": {
|
||||||
|
"xxs": {
|
||||||
|
"width": 30
|
||||||
|
},
|
||||||
|
"xs": {
|
||||||
|
"width": 100
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"width": 300
|
||||||
|
},
|
||||||
|
"m": {
|
||||||
|
"width": 600
|
||||||
|
},
|
||||||
|
"l": {
|
||||||
|
"width": 1000
|
||||||
|
},
|
||||||
|
"xl": {
|
||||||
|
"width": 2000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"card_assets": true,
|
||||||
|
"custom": {
|
||||||
|
"navigation_layout": {
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
"Logo in the middle",
|
||||||
|
"Logo on the left",
|
||||||
|
"Stacked"
|
||||||
|
],
|
||||||
|
"default": "Logo in the middle"
|
||||||
|
},
|
||||||
|
"site_background_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#ffffff"
|
||||||
|
},
|
||||||
|
"header_and_footer_color": {
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
"Accent color",
|
||||||
|
"Background color"
|
||||||
|
],
|
||||||
|
"default": "Accent color"
|
||||||
|
},
|
||||||
|
"title_font": {
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
"Modern sans-serif",
|
||||||
|
"Elegant serif",
|
||||||
|
"Clean slab"
|
||||||
|
],
|
||||||
|
"default": "Modern sans-serif"
|
||||||
|
},
|
||||||
|
"body_font": {
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
"Modern sans-serif",
|
||||||
|
"Elegant serif"
|
||||||
|
],
|
||||||
|
"default": "Modern sans-serif"
|
||||||
|
},
|
||||||
|
"signup_heading": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Used in your footer across your theme, defaults to site title when empty"
|
||||||
|
},
|
||||||
|
"signup_subheading": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Defaults to site description when empty"
|
||||||
|
},
|
||||||
|
"header_style": {
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
"Landing",
|
||||||
|
"Highlight",
|
||||||
|
"Magazine",
|
||||||
|
"Search",
|
||||||
|
"Off"
|
||||||
|
],
|
||||||
|
"description": "Highlight & Magazine styles will default to Landing until 7 posts have been published",
|
||||||
|
"default": "Landing",
|
||||||
|
"group": "homepage"
|
||||||
|
},
|
||||||
|
"use_publication_cover_as_background": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"group": "homepage",
|
||||||
|
"visibility": "header_style:[Landing, Search]"
|
||||||
|
},
|
||||||
|
"highlight_featured_posts": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"group": "homepage",
|
||||||
|
"visibility": "header_style:[Highlight, Magazine]"
|
||||||
|
},
|
||||||
|
"post_feed_style": {
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
"List",
|
||||||
|
"Grid"
|
||||||
|
],
|
||||||
|
"default": "List",
|
||||||
|
"group": "homepage"
|
||||||
|
},
|
||||||
|
"show_images_in_feed": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Toggles thumbnails of the post cards when the post feed style is List",
|
||||||
|
"group": "homepage",
|
||||||
|
"visibility": "post_feed_style:List"
|
||||||
|
},
|
||||||
|
"show_author": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Show the author below each post",
|
||||||
|
"group": "homepage"
|
||||||
|
},
|
||||||
|
"show_publish_date": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Show the date published below each post",
|
||||||
|
"group": "homepage"
|
||||||
|
},
|
||||||
|
"show_site_in_sidebar": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Include your site info on the side of the post feed",
|
||||||
|
"group": "homepage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"renovate": {
|
||||||
|
"extends": [
|
||||||
|
"@tryghost:theme"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
26
ghost/core/test/utils/fixtures/themes/source/page.hbs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{{!< default}}
|
||||||
|
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
|
||||||
|
|
||||||
|
{{#post}}
|
||||||
|
|
||||||
|
<main class="gh-main">
|
||||||
|
<article class="gh-article {{post_class}}">
|
||||||
|
|
||||||
|
{{#match @page.show_title_and_feature_image}}
|
||||||
|
<header class="gh-article-header gh-canvas">
|
||||||
|
<h1 class="gh-article-title is-title">{{title}}</h1>
|
||||||
|
{{#if custom_excerpt}}
|
||||||
|
<p class="gh-article-excerpt is-body">{{custom_excerpt}}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{> "feature-image"}}
|
||||||
|
</header>
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
<section class="gh-content gh-canvas is-body">
|
||||||
|
{{content}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{/post}}
|
@ -0,0 +1,25 @@
|
|||||||
|
{{#if @site.members_enabled}}
|
||||||
|
{{#unless @member}}
|
||||||
|
{{#match @custom.header_style "!=" "Landing"}}
|
||||||
|
{{#match @custom.header_style "!=" "Search"}}
|
||||||
|
{{#match @custom.header_style "!=" "Off"}}
|
||||||
|
{{#match posts.length ">=" 7}}
|
||||||
|
<section class="gh-cta gh-outer">
|
||||||
|
<div class="gh-cta-inner gh-inner">
|
||||||
|
<div class="gh-cta-content">
|
||||||
|
<h2 class="gh-cta-title is-title">
|
||||||
|
{{#if @custom.signup_heading}}{{@custom.signup_heading}}{{else}}{{@site.title}}{{/if}}
|
||||||
|
</h2>
|
||||||
|
<p class="gh-cta-description is-body">
|
||||||
|
{{#if @custom.signup_subheading}}{{@custom.signup_subheading}}{{else}}{{@site.description}}{{/if}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{> "email-subscription"}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{/match}}
|
||||||
|
{{/match}}
|
||||||
|
{{/match}}
|
||||||
|
{{/match}}
|
||||||
|
{{/unless}}
|
||||||
|
{{/if}}
|
@ -0,0 +1,14 @@
|
|||||||
|
{{#if showFeatured}}
|
||||||
|
{{#get "posts" filter="featured:true" include="authors" limit=limit as |featured|}}
|
||||||
|
<section class="gh-featured gh-outer">
|
||||||
|
<div class="gh-featured-inner gh-inner">
|
||||||
|
<h2 class="gh-featured-title">Featured</h2>
|
||||||
|
<div class="gh-featured-feed">
|
||||||
|
{{#foreach featured}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{/get}}
|
||||||
|
{{/if}}
|
@ -0,0 +1,35 @@
|
|||||||
|
<footer class="gh-footer{{#match @custom.header_and_footer_color "Accent color"}} has-accent-color{{/match}} gh-outer">
|
||||||
|
<div class="gh-footer-inner gh-inner">
|
||||||
|
|
||||||
|
<div class="gh-footer-bar">
|
||||||
|
<span class="gh-footer-logo is-title">
|
||||||
|
{{#if @site.logo}}
|
||||||
|
<img src="{{@site.logo}}" alt="{{@site.title}}">
|
||||||
|
{{else}}
|
||||||
|
{{@site.title}}
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
|
<nav class="gh-footer-menu">
|
||||||
|
{{navigation type="secondary"}}
|
||||||
|
</nav>
|
||||||
|
<div class="gh-footer-copyright">
|
||||||
|
Powered by <a href="https://ghost.org/" target="_blank" rel="noopener">Ghost</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if @site.members_enabled}}
|
||||||
|
{{#unless @member}}
|
||||||
|
<section class="gh-footer-signup">
|
||||||
|
<h2 class="gh-footer-signup-header is-title">
|
||||||
|
{{#if @custom.signup_heading}}{{@custom.signup_heading}}{{else}}{{@site.title}}{{/if}}
|
||||||
|
</h2>
|
||||||
|
<p class="gh-footer-signup-subhead is-body">
|
||||||
|
{{#if @custom.signup_subheading}}{{@custom.signup_subheading}}{{else}}{{@site.description}}{{/if}}
|
||||||
|
</p>
|
||||||
|
{{> "email-subscription"}}
|
||||||
|
</section>
|
||||||
|
{{/unless}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</footer>
|
@ -0,0 +1,77 @@
|
|||||||
|
<section class="gh-header is-{{#match headerStyle "Magazine"}}magazine{{else match headerStyle "Highlight"}}highlight{{else}}classic{{/match}}{{#if @custom.use_publication_cover_as_background}}{{#if @site.cover_image}} has-image{{/if}}{{/if}} gh-outer">
|
||||||
|
|
||||||
|
{{!-- Background image --}}
|
||||||
|
{{#if @custom.use_publication_cover_as_background}}
|
||||||
|
{{#match headerStyle "!=" "Magazine"}}
|
||||||
|
{{#match headerStyle "!=" "Highlight"}}
|
||||||
|
{{#if @site.cover_image}}
|
||||||
|
<img class="gh-header-image" src="{{@site.cover_image}}" alt="{{@site.title}}">
|
||||||
|
{{/if}}
|
||||||
|
{{/match}}
|
||||||
|
{{/match}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="gh-header-inner gh-inner">
|
||||||
|
|
||||||
|
{{!-- Highlight layout --}}
|
||||||
|
{{#match headerStyle "Highlight"}}
|
||||||
|
<div class="gh-header-left">
|
||||||
|
{{#foreach posts limit="1"}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
</div>
|
||||||
|
<div class="gh-header-middle">
|
||||||
|
{{#foreach posts from="2" limit="3"}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
</div>
|
||||||
|
<div class="gh-header-right">
|
||||||
|
{{#if @custom.highlight_featured_posts}}
|
||||||
|
{{> "components/featured" showFeatured=@custom.highlight_featured_posts limit=6}}
|
||||||
|
{{else}}
|
||||||
|
<div class="gh-featured-feed">
|
||||||
|
{{#foreach posts from="5" limit="6"}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
{{!-- Magazine layout --}}
|
||||||
|
{{#match headerStyle "Magazine"}}
|
||||||
|
{{#foreach posts limit="7"}}
|
||||||
|
{{#match @number 2}}
|
||||||
|
<div class="gh-header-left">
|
||||||
|
{{/match}}
|
||||||
|
{{#match @number 5}}
|
||||||
|
<div class="gh-header-right">
|
||||||
|
{{/match}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{#match @number 4}}
|
||||||
|
</div>
|
||||||
|
{{/match}}
|
||||||
|
{{#match @number 7}}
|
||||||
|
</div>
|
||||||
|
{{/match}}
|
||||||
|
{{/foreach}}
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
{{!-- Landing layout --}}
|
||||||
|
{{#match headerStyle "Landing"}}
|
||||||
|
<h1 class="gh-header-title is-title">{{@site.description}}</h1>
|
||||||
|
{{> "email-subscription"}}
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
{{!-- Search layout --}}
|
||||||
|
{{#match headerStyle "Search"}}
|
||||||
|
<h1 class="gh-header-title is-title">{{@site.description}}</h1>
|
||||||
|
<form class="gh-form">
|
||||||
|
{{> "icons/search"}}
|
||||||
|
<button class="gh-form-input" data-ghost-search>Search posts, tags and authors</button>
|
||||||
|
</form>
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
@ -0,0 +1,23 @@
|
|||||||
|
{{#match headerStyle "!=" "Off"}}
|
||||||
|
|
||||||
|
{{#match headerStyle "Highlight"}}
|
||||||
|
{{#match posts.length ">=" 7}}
|
||||||
|
{{> "components/header-content"}}
|
||||||
|
{{/match}}
|
||||||
|
{{else match headerStyle "Magazine"}}
|
||||||
|
{{#match posts.length ">=" 7}}
|
||||||
|
{{> "components/header-content"}}
|
||||||
|
{{/match}}
|
||||||
|
{{else}}
|
||||||
|
{{#match headerStyle "Landing"}}
|
||||||
|
{{#if @site.members_enabled}}
|
||||||
|
{{#unless @member}}
|
||||||
|
{{> "components/header-content"}}
|
||||||
|
{{/unless}}
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
{{> "components/header-content"}}
|
||||||
|
{{/match}}
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
{{/match}}
|
@ -0,0 +1,53 @@
|
|||||||
|
<header id="gh-navigation" class="gh-navigation is-{{#match navigationLayout "Logo on the left"}}left-logo{{else match navigationLayout "Stacked"}}stacked{{else}}middle-logo{{/match}}{{#match @custom.header_and_footer_color "Accent color"}} has-accent-color{{/match}} gh-outer">
|
||||||
|
<div class="gh-navigation-inner gh-inner">
|
||||||
|
|
||||||
|
<div class="gh-navigation-brand">
|
||||||
|
<a class="gh-navigation-logo is-title" href="{{@site.url}}">
|
||||||
|
{{#if @site.logo}}
|
||||||
|
<img src="{{@site.logo}}" alt="{{@site.title}}">
|
||||||
|
{{else}}
|
||||||
|
{{@site.title}}
|
||||||
|
{{/if}}
|
||||||
|
</a>
|
||||||
|
{{> "search-toggle"}}
|
||||||
|
<button class="gh-burger gh-icon-button">
|
||||||
|
{{> "icons/burger"}}
|
||||||
|
{{> "icons/close"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="gh-navigation-menu">
|
||||||
|
{{navigation}}
|
||||||
|
{{#unless @site.members_enabled}}
|
||||||
|
{{#match navigationLayout "Stacked"}}
|
||||||
|
{{> "search-toggle"}}
|
||||||
|
{{/match}}
|
||||||
|
{{/unless}}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="gh-navigation-actions">
|
||||||
|
{{#unless @site.members_enabled}}
|
||||||
|
{{^match navigationLayout "Stacked"}}
|
||||||
|
{{> "search-toggle"}}
|
||||||
|
{{/match}}
|
||||||
|
{{else}}
|
||||||
|
{{> "search-toggle"}}
|
||||||
|
<div class="gh-navigation-members">
|
||||||
|
{{#unless @member}}
|
||||||
|
{{#unless @site.members_invite_only}}
|
||||||
|
<a href="#/portal/signin" data-portal="signin">Sign in</a>
|
||||||
|
{{#unless hideSubscribeButton}}
|
||||||
|
<a class="gh-button" href="#/portal/signup" data-portal="signup">Subscribe</a>
|
||||||
|
{{/unless}}
|
||||||
|
{{else}}
|
||||||
|
<a class="gh-button" href="#/portal/signin" data-portal="signin">Sign in</a>
|
||||||
|
{{/unless}}
|
||||||
|
{{else}}
|
||||||
|
<a class="gh-button" href="#/portal/account" data-portal="account">Account</a>
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</header>
|
@ -0,0 +1,133 @@
|
|||||||
|
{{!--
|
||||||
|
Parameters:
|
||||||
|
* feed (index, home, archive, recent)
|
||||||
|
* postFeedStyle (list, grid)
|
||||||
|
* showTitle (true, false)
|
||||||
|
* showSidebar (true, false)
|
||||||
|
--}}
|
||||||
|
|
||||||
|
<section class="gh-container is-{{#match postFeedStyle "List"}}list{{else}}grid{{/match}}{{#if showSidebar}} has-sidebar{{/if}}{{#unless @custom.show_images_in_feed}} no-image{{/unless}} gh-outer">
|
||||||
|
<div class="gh-container-inner gh-inner">
|
||||||
|
|
||||||
|
{{#if showTitle}}
|
||||||
|
<h2 class="gh-container-title">
|
||||||
|
{{#unless title}}Latest{{else}}{{title}}{{/unless}}
|
||||||
|
</h2>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<main class="gh-main">
|
||||||
|
<div class="gh-feed">
|
||||||
|
|
||||||
|
{{!-- Homepage --}}
|
||||||
|
{{#match feed "home"}}
|
||||||
|
{{#match @custom.header_style "Highlight"}}
|
||||||
|
{{#match posts.length ">=" 7}}
|
||||||
|
{{#if @custom.highlight_featured_posts}}
|
||||||
|
{{#get "posts" include="authors" limit="16"}}
|
||||||
|
{{#foreach posts from="5" limit="12"}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
{{/get}}
|
||||||
|
{{else}}
|
||||||
|
{{#get "posts" include="authors" limit="22"}}
|
||||||
|
{{#foreach posts from="11" limit="12"}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
{{/get}}
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
{{#get "posts" include="authors" limit="12"}}
|
||||||
|
{{#foreach posts}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
{{/get}}
|
||||||
|
{{/match}}
|
||||||
|
{{else match @custom.header_style "Magazine"}}
|
||||||
|
{{#match posts.length ">=" 7}}
|
||||||
|
{{#get "posts" include="authors" limit="19"}}
|
||||||
|
{{#foreach posts from="8" limit="12"}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
{{/get}}
|
||||||
|
{{else}}
|
||||||
|
{{#get "posts" include="authors" limit="12"}}
|
||||||
|
{{#foreach posts}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
{{/get}}
|
||||||
|
{{/match}}
|
||||||
|
{{else}}
|
||||||
|
{{#get "posts" include="authors" limit="12"}}
|
||||||
|
{{#foreach posts}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
{{/get}}
|
||||||
|
{{/match}}
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
{{!-- All posts --}}
|
||||||
|
{{#match feed "index"}}
|
||||||
|
{{#match pagination.page 2}}
|
||||||
|
{{#get "posts" include="authors" limit=@config.posts_per_page as |recent|}}
|
||||||
|
{{#foreach recent}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
{{/get}}
|
||||||
|
{{/match}}
|
||||||
|
{{#foreach posts}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
{{!-- Tag and author pages --}}
|
||||||
|
{{#match feed "archive"}}
|
||||||
|
{{#foreach posts}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
{{!-- Recent posts --}}
|
||||||
|
{{#match feed "recent"}}
|
||||||
|
{{#get "posts" include="authors" filter="id:-{{post.id}}" limit="4" as |next|}}
|
||||||
|
{{#foreach next}}
|
||||||
|
{{> "post-card"}}
|
||||||
|
{{/foreach}}
|
||||||
|
{{/get}}
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{#if showSidebar}}
|
||||||
|
<aside class="gh-sidebar">
|
||||||
|
<section class="gh-about">
|
||||||
|
{{#if @site.icon}}
|
||||||
|
<img class="gh-about-icon" src="{{@site.icon}}" alt="{{@site.title}}">
|
||||||
|
{{/if}}
|
||||||
|
<h3 class="gh-about-title is-title">{{@site.title}}</h3>
|
||||||
|
{{#if @site.description}}
|
||||||
|
<p class="gh-about-description is-body">{{@site.description}}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if @site.members_enabled}}
|
||||||
|
{{#unless @member}}
|
||||||
|
<button class="gh-button" data-portal="signup">Subscribe</button>
|
||||||
|
{{else}}
|
||||||
|
{{#if @site.paid_members_enabled}}
|
||||||
|
{{#unless @member.paid}}
|
||||||
|
<button class="gh-button" data-portal="upgrade">Upgrade</button>
|
||||||
|
{{/unless}}
|
||||||
|
{{/if}}
|
||||||
|
{{/unless}}
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#match pagination.pages ">" 1}}
|
||||||
|
<div class="gh-more is-title">
|
||||||
|
<a href="/page/2">See all {{> "icons/arrow"}}</a>
|
||||||
|
</div>
|
||||||
|
{{/match}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
@ -0,0 +1,8 @@
|
|||||||
|
<form class="gh-form" data-members-form>
|
||||||
|
<input class="gh-form-input" type="email" placeholder="jamie@example.com" required data-members-email>
|
||||||
|
<button class="gh-button" type="submit">
|
||||||
|
<span><span>Subscribe</span> {{> "icons/arrow"}}</span>
|
||||||
|
{{> "icons/loader"}}
|
||||||
|
{{> "icons/checkmark"}}
|
||||||
|
</button>
|
||||||
|
</form>
|
@ -0,0 +1,17 @@
|
|||||||
|
{{#if feature_image}}
|
||||||
|
<figure class="gh-article-image">
|
||||||
|
<img
|
||||||
|
srcset="{{img_url feature_image size="s"}} 300w,
|
||||||
|
{{img_url feature_image size="m"}} 720w,
|
||||||
|
{{img_url feature_image size="l"}} 960w,
|
||||||
|
{{img_url feature_image size="xl"}} 1200w,
|
||||||
|
{{img_url feature_image size="xxl"}} 2000w"
|
||||||
|
sizes="(max-width: 1200px) 100vw, 1200px"
|
||||||
|
src="{{img_url feature_image size="xl"}}"
|
||||||
|
alt="{{title}}"
|
||||||
|
>
|
||||||
|
{{#if feature_image_caption}}
|
||||||
|
<figcaption>{{feature_image_caption}}</figcaption>
|
||||||
|
{{/if}}
|
||||||
|
</figure>
|
||||||
|
{{/if}}
|
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M224.49,136.49l-72,72a12,12,0,0,1-17-17L187,140H40a12,12,0,0,1,0-24H187L135.51,64.48a12,12,0,0,1,17-17l72,72A12,12,0,0,1,224.49,136.49Z"></path></svg>
|
After Width: | Height: | Size: 264 B |
@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M3.513 18.998C4.749 15.504 8.082 13 12 13s7.251 2.504 8.487 5.998C18.47 21.442 15.417 23 12 23s-6.47-1.558-8.487-4.002zM12 12c2.21 0 4-2.79 4-5s-1.79-4-4-4-4 1.79-4 4 1.79 5 4 5z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 308 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"></path></svg>
|
After Width: | Height: | Size: 282 B |
@ -0,0 +1,24 @@
|
|||||||
|
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
|
||||||
|
<path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
|
||||||
|
<style>
|
||||||
|
.checkmark {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: block;
|
||||||
|
stroke-width: 2.5;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark__check {
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
stroke-dasharray: 48;
|
||||||
|
stroke-dashoffset: 48;
|
||||||
|
animation: stroke .3s cubic-bezier(0.650, 0.000, 0.450, 1.000) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stroke {
|
||||||
|
100% { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 716 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path></svg>
|
After Width: | Height: | Size: 313 B |
@ -0,0 +1 @@
|
|||||||
|
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M23.9981 11.9991C23.9981 5.37216 18.626 0 11.9991 0C5.37216 0 0 5.37216 0 11.9991C0 17.9882 4.38789 22.9522 10.1242 23.8524V15.4676H7.07758V11.9991H10.1242V9.35553C10.1242 6.34826 11.9156 4.68714 14.6564 4.68714C15.9692 4.68714 17.3424 4.92149 17.3424 4.92149V7.87439H15.8294C14.3388 7.87439 13.8739 8.79933 13.8739 9.74824V11.9991H17.2018L16.6698 15.4676H13.8739V23.8524C19.6103 22.9522 23.9981 17.9882 23.9981 11.9991Z"/></svg>
|
After Width: | Height: | Size: 531 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.49365 4.58752C3.53115 6.03752 2.74365 7.70002 2.74365 9.25002C2.74365 10.6424 3.29678 11.9778 4.28134 12.9623C5.26591 13.9469 6.60127 14.5 7.99365 14.5C9.38604 14.5 10.7214 13.9469 11.706 12.9623C12.6905 11.9778 13.2437 10.6424 13.2437 9.25002C13.2437 6.00002 10.9937 3.50002 9.16865 1.68127L6.99365 6.25002L4.49365 4.58752Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 538 B |
@ -0,0 +1,17 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="currentColor" stroke="none" stroke-linejoin="round" class="nc-icon-wrapper">
|
||||||
|
<g class="nc-loop-dots-4-24-icon-o">
|
||||||
|
<circle cx="4" cy="12" r="3"></circle>
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
<circle cx="20" cy="12" r="3"></circle>
|
||||||
|
</g>
|
||||||
|
<style data-cap="butt">
|
||||||
|
.nc-loop-dots-4-24-icon-o{--animation-duration:0.8s}
|
||||||
|
.nc-loop-dots-4-24-icon-o *{opacity:.4;transform:scale(.75);animation:nc-loop-dots-4-anim var(--animation-duration) infinite}
|
||||||
|
.nc-loop-dots-4-24-icon-o :nth-child(1){transform-origin:4px 12px;animation-delay:-.3s;animation-delay:calc(var(--animation-duration)/-2.666)}
|
||||||
|
.nc-loop-dots-4-24-icon-o :nth-child(2){transform-origin:12px 12px;animation-delay:-.15s;animation-delay:calc(var(--animation-duration)/-5.333)}
|
||||||
|
.nc-loop-dots-4-24-icon-o :nth-child(3){transform-origin:20px 12px}
|
||||||
|
@keyframes nc-loop-dots-4-anim{0%,100%{opacity:.4;transform:scale(.75)}50%{opacity:1;transform:scale(1)}}
|
||||||
|
</style>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16.25 6.875H3.75C3.40482 6.875 3.125 7.15482 3.125 7.5V16.25C3.125 16.5952 3.40482 16.875 3.75 16.875H16.25C16.5952 16.875 16.875 16.5952 16.875 16.25V7.5C16.875 7.15482 16.5952 6.875 16.25 6.875Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M7.1875 6.875V4.0625C7.1875 3.31658 7.48382 2.60121 8.01126 2.07376C8.53871 1.54632 9.25408 1.25 10 1.25C10.7459 1.25 11.4613 1.54632 11.9887 2.07376C12.5162 2.60121 12.8125 3.31658 12.8125 4.0625V6.875" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M10 13.125C10.6904 13.125 11.25 12.5654 11.25 11.875C11.25 11.1846 10.6904 10.625 10 10.625C9.30964 10.625 8.75 11.1846 8.75 11.875C8.75 12.5654 9.30964 13.125 10 13.125Z" fill="currentColor"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 932 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="6.18" cy="17.82" r="2.18"/><path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z"/></svg>
|
After Width: | Height: | Size: 263 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" width="20" height="20"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
After Width: | Height: | Size: 248 B |
@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 24 24" fill="currentColor"><g><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></g></svg>
|
After Width: | Height: | Size: 231 B |
@ -0,0 +1,41 @@
|
|||||||
|
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
|
<div class="pswp__bg"></div>
|
||||||
|
|
||||||
|
<div class="pswp__scroll-wrap">
|
||||||
|
<div class="pswp__container">
|
||||||
|
<div class="pswp__item"></div>
|
||||||
|
<div class="pswp__item"></div>
|
||||||
|
<div class="pswp__item"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pswp__ui pswp__ui--hidden">
|
||||||
|
<div class="pswp__top-bar">
|
||||||
|
<div class="pswp__counter"></div>
|
||||||
|
|
||||||
|
<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
|
||||||
|
<button class="pswp__button pswp__button--share" title="Share"></button>
|
||||||
|
<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
|
||||||
|
<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
|
||||||
|
|
||||||
|
<div class="pswp__preloader">
|
||||||
|
<div class="pswp__preloader__icn">
|
||||||
|
<div class="pswp__preloader__cut">
|
||||||
|
<div class="pswp__preloader__donut"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
|
||||||
|
<div class="pswp__share-tooltip"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"></button>
|
||||||
|
<button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)"></button>
|
||||||
|
|
||||||
|
<div class="pswp__caption">
|
||||||
|
<div class="pswp__caption__center"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,35 @@
|
|||||||
|
<article class="gh-card {{post_class}}{{#unless @custom.show_images_in_feed}} no-image{{/unless}}">
|
||||||
|
<a class="gh-card-link" href="{{url}}">
|
||||||
|
{{#if feature_image}}
|
||||||
|
<figure class="gh-card-image">
|
||||||
|
<img
|
||||||
|
srcset="{{img_url feature_image size="s"}} 300w,
|
||||||
|
{{img_url feature_image size="m"}} 720w,
|
||||||
|
{{img_url feature_image size="l"}} 960w,
|
||||||
|
{{img_url feature_image size="xl"}} 1200w,
|
||||||
|
{{img_url feature_image size="xxl"}} 2000w"
|
||||||
|
sizes="(max-width: 1200px) 100vw, 1200px"
|
||||||
|
src="{{img_url feature_image size="m"}}"
|
||||||
|
alt="{{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}}"
|
||||||
|
>
|
||||||
|
</figure>
|
||||||
|
{{/if}}
|
||||||
|
<div class="gh-card-wrapper">
|
||||||
|
{{#if primary_tag}}
|
||||||
|
<p class="gh-card-tag">{{primary_tag.name}}</p>
|
||||||
|
{{/if}}
|
||||||
|
<h3 class="gh-card-title is-title">{{title}}</h3>
|
||||||
|
{{#if excerpt}}
|
||||||
|
<p class="gh-card-excerpt is-body">{{excerpt}}</p>
|
||||||
|
{{/if}}
|
||||||
|
<footer class="gh-card-meta"><!--
|
||||||
|
-->{{#if @custom.show_author}}
|
||||||
|
<span class="gh-card-author">By {{#foreach authors}}{{#if @first}}{{name}}{{else}}, {{name}}{{/if}}{{/foreach}}</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if @custom.show_publish_date}}
|
||||||
|
<time class="gh-card-date" datetime="{{date format="YYYY-MM-DD"}}">{{date}}</time>
|
||||||
|
{{/if}}<!--
|
||||||
|
--></footer>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</article>
|
@ -0,0 +1,3 @@
|
|||||||
|
<button class="gh-search gh-icon-button" aria-label="Search this site" data-ghost-search>
|
||||||
|
{{> "icons/search"}}
|
||||||
|
</button>
|
63
ghost/core/test/utils/fixtures/themes/source/post.hbs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
{{!< default}}
|
||||||
|
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
|
||||||
|
|
||||||
|
{{#post}}
|
||||||
|
|
||||||
|
<main class="gh-main">
|
||||||
|
|
||||||
|
<article class="gh-article {{post_class}}">
|
||||||
|
|
||||||
|
<header class="gh-article-header gh-canvas">
|
||||||
|
|
||||||
|
{{#if primary_tag}}
|
||||||
|
<a class="gh-article-tag" href="{{primary_tag.url}}">{{primary_tag.name}}</a>
|
||||||
|
{{/if}}
|
||||||
|
<h1 class="gh-article-title is-title">{{title}}</h1>
|
||||||
|
{{#if custom_excerpt}}
|
||||||
|
<p class="gh-article-excerpt is-body">{{custom_excerpt}}</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="gh-article-meta">
|
||||||
|
<div class="gh-article-author-image">
|
||||||
|
{{#foreach authors}}
|
||||||
|
{{#if profile_image}}
|
||||||
|
<a href="{{url}}">
|
||||||
|
<img class="author-profile-image" src="{{img_url profile_image size="xs"}}" alt="{{name}}" />
|
||||||
|
</a>
|
||||||
|
{{else}}
|
||||||
|
<a href="{{url}}">{{> "icons/avatar"}}</a>
|
||||||
|
{{/if}}
|
||||||
|
{{/foreach}}
|
||||||
|
</div>
|
||||||
|
<div class="gh-article-meta-wrapper">
|
||||||
|
<h4 class="gh-article-author-name">{{authors}}</h4>
|
||||||
|
<div class="gh-article-meta-content">
|
||||||
|
<time class="gh-article-meta-date" datetime="{{date format="YYYY-MM-DD"}}">{{date}}</time>
|
||||||
|
{{#if reading_time}}
|
||||||
|
<span class="gh-article-meta-length"><span class="bull">—</span> {{reading_time}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{> "feature-image"}}
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="gh-content gh-canvas is-body">
|
||||||
|
{{content}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{{#if comments}}
|
||||||
|
<div class="gh-comments gh-canvas">
|
||||||
|
{{comments}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{/post}}
|
||||||
|
|
||||||
|
{{> "components/post-list" feed="recent" postFeedStyle="Grid" title="Read more" showTitle=true showSidebar=false}}
|
22
ghost/core/test/utils/fixtures/themes/source/tag.hbs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{{!< default}}
|
||||||
|
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
|
||||||
|
|
||||||
|
<main class="gh-main gh-outer">
|
||||||
|
|
||||||
|
{{#tag}}
|
||||||
|
<section class="gh-archive{{#if feature_image}} has-image{{/if}}{{#if @custom.show_site_in_sidebar}} has-sidebar{{/if}} gh-inner">
|
||||||
|
<div class="gh-archive-inner">
|
||||||
|
<header class="gh-archive-wrapper">
|
||||||
|
<h1 class="gh-article-title is-title">{{name}}</h1>
|
||||||
|
{{#if description}}
|
||||||
|
<p class="gh-article-excerpt">{{description}}</p>
|
||||||
|
{{/if}}
|
||||||
|
</header>
|
||||||
|
{{> "feature-image"}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{/tag}}
|
||||||
|
|
||||||
|
{{> "components/post-list" feed="archive" postFeedStyle=@custom.post_feed_style showTitle=false showSidebar=@custom.show_site_in_sidebar}}
|
||||||
|
|
||||||
|
</main>
|