Added Sidenote Component

This commit is contained in:
2025-10-02 14:18:46 +02:00
parent 0111cd71fe
commit 054d450273
78 changed files with 1218 additions and 524 deletions

View File

@@ -0,0 +1,13 @@
import { collection } from '@keystatic/core';
import { createArticleField } from '@/keystatic/fields/article';
export default collection({
label: 'Meta - Posts',
slugField: 'title',
path: 'content/meta/*',
format: { contentField: 'content' },
schema: {
...createArticleField('meta'),
},
});

View File

@@ -0,0 +1,46 @@
import { collection, fields } from '@keystatic/core';
export default collection({
label: 'Authors',
slugField: 'name',
path: 'content/taxonomy/authors/*',
format: { data: 'json' },
schema: {
name: fields.slug({ name: { label: 'Name' } }),
avatar: fields.image({
label: 'Avatar',
directory: 'public/images/authors',
publicPath: '/images/authors',
}),
description: fields.text({ label: 'Description', multiline: true }),
contacts: fields.array(
fields.object({
type: fields.select({
label: 'Contact Type',
defaultValue: 'email',
options: [
{
value: 'email',
label: 'E-Mail',
},
{
value: 'discord',
label: 'Discord',
},
{
value: 'other',
label: 'Other',
},
],
}),
url: fields.text({
label: 'URL',
}),
}),
{
label: 'Contact Type',
itemLabel: (props) => `${props.fields.type.value}`,
}
),
},
});

View File

@@ -0,0 +1,41 @@
import { collection, fields } from '@keystatic/core';
export default collection({
label: 'Tags',
slugField: 'name',
path: 'content/taxonomy/tags/*',
format: { data: 'json' },
schema: {
name: fields.slug({ name: { label: 'Name' } }),
icon: fields.conditional(
fields.select({
label: 'Icon Type',
defaultValue: 'none',
options: [
{
value: 'none',
label: 'None',
},
{
value: 'glyph',
label: 'Glyph',
},
{
value: 'img',
label: 'Image',
},
],
}),
{
none: fields.empty(),
glyph: fields.text({ label: 'Glyph' }),
img: fields.image({
label: 'icon',
publicPath: '/images/icons',
directory: 'public/images/icons',
}),
}
),
description: fields.text({ label: 'Description', multiline: true }),
},
});

View File

@@ -0,0 +1,61 @@
import { grid2X2Icon } from '@keystar/ui/icon/icons/grid2X2Icon';
import { fields } from '@keystatic/core';
import { repeating, wrapper } from '@keystatic/core/content-components';
export const gridComponents = {
Row: repeating({
label: 'Row',
icon: grid2X2Icon,
children: ['Column'], // This defines what can go inside
schema: {
style: fields.object({
cols: fields.integer({
label: 'Columns',
defaultValue: 2,
validation: { min: 2, max: 12 },
}),
gap: fields.text({
label: 'Gap',
description: 'Enter a variable or value',
}),
align: fields.select({
label: 'Align Items',
defaultValue: 'flex-start',
options: [
{ label: 'Start', value: 'flex-start' },
{ label: 'End', value: 'flex-end' },
{ label: 'Center', value: 'center' },
{ label: 'Baseline', value: 'baseline' },
{ label: 'Stretch', value: 'stretch' },
],
}),
justify: fields.select({
label: 'Justify Content',
defaultValue: 'flex-start',
options: [
{ label: 'Start', value: 'flex-start' },
{ label: 'End', value: 'flex-end' },
{ label: 'Center', value: 'center' },
{ label: 'Space Between', value: 'space-between' },
{ label: 'Space Around', value: 'space-around' },
{ label: 'Evenly', value: 'evenly' },
{ label: 'Stretch', value: 'stretch' },
{ label: 'Baseline', value: 'baseline' },
],
}),
}),
},
}),
Column: wrapper({
label: 'Column',
forSpecificLocations: true,
schema: {
style: fields.object({
colspan: fields.integer({
label: 'Colspan',
defaultValue: 1,
}),
}),
},
}),
};

View File

@@ -0,0 +1,4 @@
import { gridComponents } from '@/keystatic/components/general/grid';
import { sidenoteComponents } from '@/keystatic/components/general/sidenote';
export const generalComponents = { ...gridComponents, ...sidenoteComponents };

View File

@@ -0,0 +1,59 @@
import { panelRightDashedIcon } from '@keystar/ui/icon/icons/panelRightDashedIcon';
import { fields } from '@keystatic/core';
import { inline } from '@keystatic/core/content-components';
export const sidenoteComponents = {
Sidenote: inline({
label: 'Sidenote',
icon: panelRightDashedIcon,
schema: {
id: fields.text({
label: 'ID',
description: 'Unique Identifier (Auto-generated)',
}),
marker: fields.text({
label: 'Marker',
description: 'Number or Glyph for reference',
defaultValue: '⋄',
validation: {
length: {
min: 1,
max: 3,
},
},
}),
content: fields.text({
label: 'Note Content',
multiline: true,
validation: {
length: {
min: 1,
},
},
}),
type: fields.select({
label: 'Type',
description: 'Visual Style',
options: [
{
label: 'Default',
value: 'default',
},
{
label: 'Lore',
value: 'lore',
},
{
label: 'Crunch',
value: 'crunch',
},
{
label: 'Example',
value: 'example',
},
],
defaultValue: 'default',
}),
},
}),
};

View File

@@ -0,0 +1,26 @@
import { fields } from '@keystatic/core';
import { createContentField } from '@/keystatic/fields/content';
import { createSEOField } from '@/keystatic/fields/seo';
import { createMetaField } from '@/keystatic/fields/meta';
export const createArticleField = (imageSubfolder: string) => ({
title: fields.slug({ name: { label: 'Title' } }),
summary: fields.text({ label: 'Summary', multiline: true }),
cover: fields.object({
src: fields.image({
label: 'Cover Image',
directory: `public/images/covers/${imageSubfolder}`,
publicPath: `/images/covers/${imageSubfolder}`,
}),
alt: fields.text({ label: 'Alt' }),
caption: fields.text({ label: 'Caption', multiline: true }),
showInHeader: fields.checkbox({
label: 'Show in Header',
defaultValue: false,
}),
}),
meta: createMetaField(),
seo: createSEOField(),
content: createContentField(imageSubfolder),
});

View File

@@ -0,0 +1,22 @@
import { fields } from '@keystatic/core';
import type { ContentComponent } from '@keystatic/core/content-components';
import { generalComponents } from '@/keystatic/components/general';
export const createContentField = (
imageSubfolder: string,
additionalComponents?: Record<string, ContentComponent>
) =>
fields.markdoc({
label: 'Content',
options: {
image: {
directory: `public/images/content/${imageSubfolder}`,
publicPath: `/images/content/${imageSubfolder}`,
},
},
components: {
...generalComponents,
...additionalComponents,
},
});

View File

@@ -0,0 +1,44 @@
import type { ComponentSchema } from '@keystatic/core';
import { fields } from '@keystatic/core';
export const createMetaField = (): ComponentSchema =>
fields.object(
{
publicationDate: fields.datetime({
label: 'Publication Date',
defaultValue: { kind: 'now' },
}),
updateDate: fields.datetime({ label: 'Update Date' }),
status: fields.select({
label: 'Status',
defaultValue: 'draft',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
{ label: 'Archived', value: 'archived' },
],
}),
isFeatured: fields.checkbox({
label: 'Featured',
description: 'Show on Homepage',
}),
tags: fields.array(
fields.relationship({
label: 'Tags',
collection: 'tags',
}),
{
label: 'Tags',
itemLabel: (props) => props.value || 'Select Tag',
}
),
author: fields.relationship({
label: 'Author',
collection: 'authors',
}),
},
{
label: 'Meta Information',
layout: [4, 4, 4, 12, 12, 12],
}
);

View File

@@ -0,0 +1,24 @@
import type { ComponentSchema } from '@keystatic/core';
import { fields } from '@keystatic/core';
export const createSEOField = (): ComponentSchema =>
fields.object(
{
title: fields.text({
label: 'SEO Title',
validation: { length: { max: 60 } },
}),
description: fields.text({
label: 'SEO Description',
multiline: true,
validation: { length: { max: 160 } },
}),
noIndex: fields.checkbox({
label: 'No Index',
description: 'Prevent search engines from indexing',
}),
},
{
label: 'SEO Settings',
}
);