Added Accordion and AccordionItem Components

This commit is contained in:
2025-10-03 17:57:41 +02:00
parent 70e226057a
commit 3a79f59f03
17 changed files with 225 additions and 26 deletions

View File

@@ -2,6 +2,9 @@ import React from 'react';
import Markdoc from '@markdoc/markdoc';
import type { Node } from '@markdoc/markdoc';
import ContentComponents from '@/components/Content';
import SidenoteContainer from '@/components/Content/Sidenote/Container';
import styles from './MarkdocRenderer.module.css';
import { nodes } from '@/lib/markdoc/nodes';
@@ -9,13 +12,6 @@ import { tags } from '@/lib/markdoc/tags';
import { collectSideNotes, hasComponents } from '@/lib/markdoc/utils';
import { Column } from '@/components/Content/Grid/Column';
import { Row } from '@/components/Content/Grid/Row';
import Sidenote from '@/components/Content/Sidenote/Item';
import SidenoteContainer from '@/components/Content/Sidenote/Container';
import DefinitionList from '@/components/Content/DefinitionList';
import DefinitionItem from '@/components/Content/DefinitionList/DefinitionItem';
interface MarkdocRendererProps {
content: () => Promise<{ node: Node }>;
className?: string;
@@ -27,14 +23,6 @@ export default async function MarkdocRenderer({
content,
className,
}: MarkdocRendererProps) {
const components = {
Column,
Row,
Sidenote,
DefinitionList,
DefinitionItem,
};
const { node } = await content();
const errors = Markdoc.validate(node, { tags, nodes });
@@ -52,7 +40,9 @@ export default async function MarkdocRenderer({
className={`${styles.wrapper} ${className} ${showMargin ? styles.hasMargin : ''}`}
>
<div className={`${styles.content} content`}>
{Markdoc.renderers.react(renderable, React, { components })}
{Markdoc.renderers.react(renderable, React, {
components: ContentComponents,
})}
</div>
<SidenoteContainer items={sidenotes} />
</div>

View File

@@ -0,0 +1,56 @@
.item {
& .summary {
--el-summary-marker-symbol-closed: "▾";
--el-summary-marker-symbol-open: "▾";
--el-summary-marker-symbol-transform-open: rotate(180deg);
position: relative;
padding: var(--spacing-snug);
font-family: var(--el-summary-font-family);
font-size: var(--el-summary-font-size);
font-weight: var(--el-summary-font-weight);
line-height: var(--el-summary-line-height);
color: var(--el-summary-fg);
text-transform: var(--el-summary-text-transform);
list-style: none;
background: var(--el-summary-bg);
&::before {
content: var(--el-summary-marker-symbol-closed);
position: absolute;
right: 0;
font-size: 1em;
transition: transform 0.2s ease;
}
}
& .content {
overflow: hidden;
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 2s ease;
& .inner {
min-height: 0;
}
}
&[open] {
& .summary {
&::before {
content: var(--el-summary-marker-symbol-open);
transform: var(--el-summary-marker-symbol-transform-open);
}
}
& .content {
grid-template-rows: 1fr;
}
}
}

View File

@@ -0,0 +1,39 @@
'use client';
import React from 'react';
import styles from './AccordionItem.module.css';
interface AccordionItemProps {
title: string;
defaultOpen: boolean;
children: React.ReactNode;
}
export default function AccordionItem({
title,
defaultOpen,
children,
}: AccordionItemProps) {
const [isOpen, setIsOpen] = React.useState(defaultOpen);
return (
<details
className={styles.item}
open={isOpen}
onToggle={(e) => setIsOpen(e.currentTarget.open)}
>
<summary
className={styles.summary}
onClick={(e) => {
e.preventDefault();
setIsOpen(!isOpen);
}}
>
{title}
</summary>
<div className={styles.content}>
<div className={styles.inner}>{children}</div>
</div>
</details>
);
}

View File

@@ -0,0 +1,11 @@
import React from 'react';
import styles from './Accordion.module.css';
interface AccordionProps {
children: React.ReactNode;
}
export default function Accordion({ children }: AccordionProps) {
return <div className={styles.accordion}>{children}</div>;
}

View File

@@ -9,7 +9,7 @@ interface ColumnProps {
children?: React.ReactNode;
}
export function Column({ style, children }: ColumnProps) {
export default function Column({ style, children }: ColumnProps) {
return (
<div
className={styles.column}

View File

@@ -12,7 +12,7 @@ interface RowProps {
children: React.ReactNode;
}
export function Row({ style, children }: RowProps) {
export default function Row({ style, children }: RowProps) {
return (
<div
className={`${styles.row}`}

View File

@@ -0,0 +1,19 @@
import Row from '@/components/Content/Grid/Row';
import Column from '@/components/Content/Grid/Column';
import Sidenote from '@/components/Content/Sidenote/Item';
import DefinitionList from '@/components/Content/DefinitionList';
import DefinitionItem from '@/components/Content/DefinitionList/DefinitionItem';
import Accordion from '@/components/Content/Accordion';
import AccordionItem from '@/components/Content/Accordion/AccordionItem';
const ContentComponents = {
Column,
Row,
Sidenote,
DefinitionList,
DefinitionItem,
Accordion,
AccordionItem,
};
export default ContentComponents;

View File

@@ -7,6 +7,7 @@ export default collection({
slugField: 'title',
path: 'content/meta/*',
format: { contentField: 'content' },
entryLayout: 'content',
schema: {
...createArticleField('meta'),
},

View File

@@ -0,0 +1,32 @@
import { listCollapseIcon } from '@keystar/ui/icon/icons/listCollapseIcon';
import { repeating, wrapper } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
const accordionComponents = {
Accordion: repeating({
label: 'Accordion',
icon: listCollapseIcon,
children: ['AccordionItem'],
schema: {},
}),
AccordionItem: wrapper({
label: 'Accordion Item',
forSpecificLocations: true,
schema: {
title: fields.text({
label: 'Title',
validation: {
length: {
min: 1,
},
},
}),
defaultOpen: fields.checkbox({
label: 'Open by default',
defaultValue: false,
}),
},
}),
};
export default accordionComponents;

View File

@@ -2,7 +2,7 @@ import { listTreeIcon } from '@keystar/ui/icon/icons/listTreeIcon';
import { fields } from '@keystatic/core';
import { repeating, block } from '@keystatic/core/content-components';
export const definitionlistComponents = {
const definitionlistComponents = {
DefinitionList: repeating({
label: 'Definition List',
icon: listTreeIcon,
@@ -11,6 +11,7 @@ export const definitionlistComponents = {
}),
DefinitionItem: block({
label: 'Definition Item',
forSpecificLocations: true,
schema: {
term: fields.text({ label: 'Term' }),
definitions: fields.array(
@@ -23,3 +24,5 @@ export const definitionlistComponents = {
},
}),
};
export default definitionlistComponents;

View File

@@ -2,7 +2,7 @@ import { grid2X2Icon } from '@keystar/ui/icon/icons/grid2X2Icon';
import { fields } from '@keystatic/core';
import { repeating, wrapper } from '@keystatic/core/content-components';
export const gridComponents = {
const gridComponents = {
Row: repeating({
label: 'Row',
icon: grid2X2Icon,
@@ -59,3 +59,4 @@ export const gridComponents = {
},
}),
};
export default gridComponents;

View File

@@ -1,9 +1,11 @@
import { gridComponents } from '@/keystatic/components/general/grid';
import { sidenoteComponents } from '@/keystatic/components/general/sidenote';
import { definitionlistComponents } from '@/keystatic/components/general/definitionlist';
import gridComponents from '@/keystatic/components/general/grid';
import sidenoteComponents from '@/keystatic/components/general/sidenote';
import definitionlistComponents from '@/keystatic/components/general/definitionlist';
import accordionComponents from '@/keystatic/components/general/accordion';
export const generalComponents = {
...gridComponents,
...sidenoteComponents,
...definitionlistComponents,
...accordionComponents,
};

View File

@@ -2,7 +2,7 @@ import { panelRightDashedIcon } from '@keystar/ui/icon/icons/panelRightDashedIco
import { fields } from '@keystatic/core';
import { inline } from '@keystatic/core/content-components';
export const sidenoteComponents = {
const sidenoteComponents = {
Sidenote: inline({
label: 'Sidenote',
icon: panelRightDashedIcon,
@@ -57,3 +57,5 @@ export const sidenoteComponents = {
},
}),
};
export default sidenoteComponents;

View File

@@ -58,4 +58,21 @@ export const tags: Config['tags'] = {
},
},
},
Accordion: {
render: 'Accordion',
children: ['tag'],
},
AccordionItem: {
render: 'AccordionItem',
attributes: {
title: {
type: 'String',
required: true,
},
defaultOpen: {
type: 'Boolean',
default: false,
},
},
},
};

View File

@@ -243,7 +243,7 @@
--el-small-font-family: var(--font-body);
--el-small-font-size: var(--typo-size-xs);
/* HR */
/* === HR === */
--hr-height: var(--size-3);
--hr-margin: var(--spacing-relaxed) 0;
--hr-color: var(--color-text-tertiary);
@@ -254,7 +254,27 @@
--hr-symbol-color: var(--color-text-tertiary);
--hr-symbol-background: var(--color-surface-base);
/* Header */
/* === SUMMARY === */
--el-summary-fg: var(--color-text-inverse);
--el-summary-bg: var(--color-surface-inverse);
--el-summary-font-family: var(--font-mono);
--el-summary-font-size: var(--typo-size-lg);
--el-summary-font-weight: var(--typo-weight-black);
--el-summary-text-transform: uppercase;
--el-summary-line-height: var(--typo-leading-normal);
--el-summary-marker-symbol-closed: "▾";
--el-summary-marker-symbol-open: "▴";
--el-summary-marker-symbol-transition-open: none;
/* === DETAILS === */
--el-details-color: var(--color-text-primary);
--el-details-font-family: var(--font-body);
--el-details-font-size: var(--typo-size-md);
--el-details-line-height: var(--typo-leading-normal);
--el-details-padding: var(--spacing-snug);
--el-details-border: var(--size-1) solid var(--color-text-secondary);
/* === Header === */;
--el-header-font-size: var(--typo-size-responsive);
--el-header-line-height: var(--typo-leading-snug);
--el-header-paddingY: var(--spacing-snug);