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

@@ -75,6 +75,12 @@ Erit. Si autem oculus tuus fuerit nequam, totum corpus tuum tenebrosum erit. Si
Autem introisset Capharnaum, accessit ad eum centurio, rogans eum, et dicens Domine, puer meus jacet in domo paralyticus, et male torquetur. Et ait illi Jesus Ego veniam, et curabo eum. Et respondens centurio, ait Domine, non sum dignus ut intres sub tectum meum sed tantum dic verbo, et sanabitur puer meus. Nam et ego homo sum sub potestate constitutus, habens sub me milites, et dico huic Vade, et vadit et alii Veni, et venit et servo meo [Ad eum discipuli](https://example.com/suamt/andi) Fac hoc, et facit. Audiens autem Jesus miratus est, et sequentibus se dixit Amen dico vobis, non inveni tantam fidem in Israël. Dico autem vobis, quod multi ab oriente et occidente venient, et recumbent [Qui dictus est](https://example.com/autem/etmalo) cum Abraham, et Isaac, et ==facies suas== Jacob [Modic fidei tunc surgens imperavit ventis](https://example.com/eating/adimple) in regno cælorum filii autem regni ejicientur in tenebras exteriores ibi erit fletus et stridor dentium. Et dixit Jesus centurioni Vade, et sicut credidisti, fiat tibi. Et sanatus est puer in illa hora. Et cum venisset Jesus in domum Petri, vidit socrum ejus jacentem, et febricitantem et tetigit manum ejus, et dimisit eam febris, et.
{% Accordion %}
{% AccordionItem title="Lorem" defaultOpen=false %}
Unum? Et de vestimento quid solliciti estis? Considerate lilia agri quomodo crescunt non laborant, neque nent. Dico autem vobis, quoniam nec Salomon in omni gloria sua coopertus est sicut unum ex istis. Si aut**em fœnum agri,** quod hodie est, et cras in clibanum mittitur, Deus sic vestit, quanto magis vos modicæ *fidei?* Nolite ergo solliciti esse, dicentes Quid manducabimus, aut quid bibemus, aut quo operiemur? hæc enim omnia gentes inquirunt. Scit enim Pater vester, quia his omnibus indigetis. Quærite ergo primum regnum Dei, et justitiam *aliam* ejus et hæc omnia adjicientur vobis. Nolite ergo *in via cum* solliciti.
{% /AccordionItem %}
{% /Accordion %}
#### Ubi christus nasceretur at illi dixerunt in bethlehem
```ruby

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);