Collapsible headers

This commit is contained in:
Elio Struyf
2021-07-21 14:42:52 +02:00
parent 323807c0e1
commit bea11bf7df
12 changed files with 126 additions and 51 deletions

View File

@@ -161,6 +161,17 @@ Specifies the optimal description length for SEO (set to `-1` to turn it off). D
"frontMatter.taxonomy.seoDescriptionLength": 160
}
```
### `frontMatter.taxonomy.seoContentLength`
Specifies the optimal minimum length for your articles. Between 1,760 words 2,400 is the absolute ideal article length for SEO in 2021. (set to `-1` to turn it off).
```json
{
"frontMatter.taxonomy.seoContentLength": 1760
}
```
### `frontMatter.taxonomy.seoDescriptionLength`
Specifies the name of the SEO description field for your page. Default is `description`.

View File

@@ -21,6 +21,10 @@
}
}
.collapsible__body {
padding: 1rem 2rem;
}
#app, .frontmatter {
height: 100%;
}
@@ -63,7 +67,8 @@
}
.frontmatter {
padding: var(--input-margin-vertical) 0;
padding-top: 0;
padding-bottom: var(--input-margin-vertical);
display: flex;
flex-direction: column;
justify-content: space-between;
@@ -89,11 +94,11 @@
margin-right: 0.5rem;
}
.seo__status__details {
margin-bottom: 2rem;
.seo__status__details, .seo__status__keywords {
margin-bottom: 1rem;
}
.seo__status__details h4 {
.collapsible__body h4 {
text-align: center;
font-weight: bold;
}

View File

@@ -1,5 +1,5 @@
:root {
--container-paddding: 20px;
--container-padding: 20px;
--input-padding-vertical: 6px;
--input-padding-horizontal: 4px;
--input-margin-vertical: 4px;
@@ -11,7 +11,6 @@ html, body {
}
body {
padding: 0 var(--container-paddding);
color: var(--vscode-foreground);
font-size: var(--vscode-font-size);
font-weight: var(--vscode-font-weight);
@@ -21,7 +20,7 @@ body {
ol,
ul {
padding-left: var(--container-paddding);
padding-left: var(--container-padding);
}
*:focus {

View File

@@ -145,7 +145,7 @@
"frontMatter.taxonomy.seoContentLengh": {
"type": "number",
"default": 1760,
"description": "Specifies the optimal minimum length for your articles. Between 1,760 words 2,400 is the absolute ideal blog length for SEO in 2021. (set to `-1` to turn it off)."
"description": "Specifies the optimal minimum length for your articles. Between 1,760 words 2,400 is the absolute ideal article length for SEO in 2021. (set to `-1` to turn it off)."
},
"frontMatter.taxonomy.seoDescriptionField": {
"type": "string",

View File

@@ -1,8 +1,8 @@
import * as React from 'react';
import { PanelSettings } from '../../models/PanelSettings';
import { Collapsible } from './Collapsible';
import { CustomScript } from './CustomScript';
import { DateAction } from './DateAction';
import { Icon } from './Icon';
import { PublishAction } from './PublishAction';
import { SlugAction } from './SlugAction';
@@ -19,22 +19,22 @@ export const Actions: React.FunctionComponent<IActionsProps> = (props: React.Pro
}
return (
<div className={`section article__actions`}>
<h3><Icon name={`rocket`} /> Actions</h3>
<Collapsible title="Actions">
<div className={`article__actions`}>
{ metadata && metadata.title && <SlugAction value={metadata.title} crntValue={metadata.slug} slugOpts={settings.slug} /> }
{ metadata && metadata.title && <SlugAction value={metadata.title} crntValue={metadata.slug} slugOpts={settings.slug} /> }
<DateAction />
<DateAction />
{ metadata && typeof metadata.draft !== undefined && <PublishAction draft={metadata.draft} />}
{ metadata && typeof metadata.draft !== undefined && <PublishAction draft={metadata.draft} />}
{
(settings && settings.scripts && settings.scripts.length > 0) && (
settings.scripts.map((value) => (
<CustomScript key={value.title.replace(/ /g, '')} {...value} />
))
)
}
</div>
{
(settings && settings.scripts && settings.scripts.length > 0) && (
settings.scripts.map((value) => (
<CustomScript key={value.title.replace(/ /g, '')} {...value} />
))
)
}
</div>
</Collapsible>
);
};

View File

@@ -10,9 +10,7 @@ export interface IArticleDetailsProps {
}
export const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({details}: React.PropsWithChildren<IArticleDetailsProps>) => {
console.log(details);
if (!details || (details.headings === undefined && details.paragraphs === undefined)) {
return null;
}
@@ -21,7 +19,7 @@ export const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({d
<div className={`seo__status__details valid`}>
<h4>More details</h4>
<VsTable>
<VsTable bordered>
<VsTableHeader slot="header">
<VsTableHeaderCell>Type</VsTableHeaderCell>
<VsTableHeaderCell>Total</VsTableHeaderCell>

View File

@@ -0,0 +1,31 @@
import * as React from 'react';
import { VsCollapsible } from './VscodeComponents';
export interface ICollapsibleProps {
title: string;
sendUpdate?: (open: boolean) => void;
}
export const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({children, title, sendUpdate}: React.PropsWithChildren<ICollapsibleProps>) => {
const [ isOpen, setIsOpen ] = React.useState(true);
// This is a work around for a lit-element issue of duplicate slot names
const triggerClick = (e: React.MouseEvent<HTMLElement>) => {
if ((e.target as any).tagName === 'VSCODE-COLLAPSIBLE') {
setIsOpen(prev => {
if (sendUpdate) {
sendUpdate(!prev);
}
return !prev;
});
}
}
return (
<VsCollapsible title={title} onClick={triggerClick} open={isOpen}>
<div className={`section collapsible__body`} slot="body">
{children}
</div>
</VsCollapsible>
);
};

View File

@@ -24,7 +24,7 @@ export const SeoDetails: React.FunctionComponent<ISeoDetailsProps> = (props: Rea
<div className={`seo__status__details ${validate()}`}>
<h4>{title}</h4>
<VsTable>
<VsTable {...{bordered:true}}>
<VsTableHeader slot="header">
<VsTableHeaderCell className={validate()}>{valueTitle}</VsTableHeaderCell>
<VsTableHeaderCell>Recommended</VsTableHeaderCell>

View File

@@ -0,0 +1,15 @@
import * as React from 'react';
export interface ISeoKeywordsProps {
keywords: string[] | null;
}
export const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords}: React.PropsWithChildren<ISeoKeywordsProps>) => {
return (
<div className={`seo__status__keywords`}>
<h4>Keywords</h4>
<p>{keywords?.join(',')}</p>
</div>
);
};

View File

@@ -1,8 +1,9 @@
import * as React from 'react';
import { SEO } from '../../models/PanelSettings';
import { ArticleDetails } from './ArticleDetails';
import { Icon } from './Icon';
import { Collapsible } from './Collapsible';
import { SeoDetails } from './SeoDetails';
import { SeoKeywords } from './SeoKeywords';
export interface ISeoStatusProps {
seo: SEO;
@@ -12,6 +13,7 @@ export interface ISeoStatusProps {
export const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React.PropsWithChildren<ISeoStatusProps>) => {
const { data, seo } = props;
const { title } = data;
const [ isOpen, setIsOpen ] = React.useState(true);
const { descriptionField } = seo;
@@ -19,27 +21,39 @@ export const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React
return null;
}
const renderContent = () => {
console.log(`render`);
if (!isOpen) {
return null;
}
return (
<div>
{ (title && seo.title > 0) && <SeoDetails title="Title" valueTitle="Length" allowedLength={seo.title} value={title.length} /> }
{ (data[descriptionField] && seo.description > 0) && <SeoDetails title="Description" valueTitle="Length" allowedLength={seo.description} value={data[descriptionField].length} /> }
{
seo.content > 0 && data?.articleDetails?.wordCount && (
<SeoDetails title="Article length"
valueTitle="Words"
allowedLength={seo.content}
value={data?.articleDetails?.wordCount}
noValidation />
)
}
<SeoKeywords keywords={data?.keywords} />
<ArticleDetails details={data.articleDetails} />
</div>
);
};
return (
<div className="section seo__status">
<h3>
<Icon name="search" /> SEO Status
</h3>
{ (title && seo.title > 0) && <SeoDetails title="Title" valueTitle="Length" allowedLength={seo.title} value={title.length} /> }
{ (data[descriptionField] && seo.description > 0) && <SeoDetails title="Description" valueTitle="Length" allowedLength={seo.description} value={data[descriptionField].length} /> }
{
seo.content > 0 && data?.articleDetails?.wordCount && (
<SeoDetails title="Article length"
valueTitle="Words"
allowedLength={seo.content}
value={data?.articleDetails?.wordCount}
noValidation />
)
}
<ArticleDetails details={data.articleDetails} />
</div>
<Collapsible title="SEO Status" sendUpdate={(value) => setIsOpen(value)}>
{ renderContent() }
</Collapsible>
);
};

View File

@@ -5,4 +5,5 @@ export const VsTableHeader = wrapWc(`vscode-table-header`);
export const VsTableHeaderCell = wrapWc(`vscode-table-header-cell`);
export const VsTableBody = wrapWc(`vscode-table-body`);
export const VsTableRow = wrapWc(`vscode-table-row`);
export const VsTableCell = wrapWc(`vscode-table-cell`);
export const VsTableCell = wrapWc(`vscode-table-cell`);
export const VsCollapsible = wrapWc(`vscode-collapsible`);

View File

@@ -8,6 +8,7 @@ import '@bendera/vscode-webview-elements/dist/vscode-table-header-cell';
import '@bendera/vscode-webview-elements/dist/vscode-table-body';
import '@bendera/vscode-webview-elements/dist/vscode-table-row';
import '@bendera/vscode-webview-elements/dist/vscode-table-cell';
import '@bendera/vscode-webview-elements/dist/vscode-collapsible';
declare const acquireVsCodeApi: <T = unknown>() => {
getState: () => T;