Added Callout Component

This commit is contained in:
2025-10-05 18:14:52 +02:00
parent b93f846156
commit 0bca3573f7
13 changed files with 375 additions and 17 deletions

View File

@@ -0,0 +1,144 @@
@layer component {
.container {
--callout-bg: var(--color-surface-inverse);
--callout-fg: var(--color-text-inverse);
--callout-symbol: "";
--callout-symbol-color: var(--color-text-inverse);
@mixin my var(--spacing-cozy);
position: relative;
padding: var(--spacing-cozy) var(--spacing-cozy) var(--spacing-cozy) var(--size-12) ;
border: var(--size-1) solid var(--color-surface-inverse);
box-shadow: 2px 2px 0 oklch(from var(--color-surface-inverse) calc(l - 0.075) c h), 4px 4px 0 oklch(from var(--color-surface-inverse) calc(l - 0.2) c h);
&::before {
@mixin pt 0.425em;
content: var(--callout-symbol);
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: flex-start;
justify-content: center;
width: var(--size-8);
height: 100%;
font-size: 1.5em;
color: var(--callout-symbol-color);
background-color: var(--color-surface-inverse);
}
}
.title {
@mixin text-xl;
font-family: var(--font-header);
font-weight: var(--typo-weight-black);
}
.badge {
position: absolute;
top: 0;
right: 0;
padding: 0.2em 0.5em;
font-family: var(--font-mono);
font-size: var(--typo-size-xs);
font-weight: var(--typo-weight-black);
color: var(--callout-fg);
background: var(--callout-bg);
}
.example {
--callout-bg: var(--color-palette-fuchsia);
--callout-symbol: "◆";
}
.info {
--callout-bg: var(--color-state-info);
--callout-symbol: "‽";
}
.warning {
--callout-bg: var(--color-state-warning);
--callout-symbol: "‼";
}
.tip {
--callout-bg: var(--color-palette-lime-green);
--callout-symbol: "★";
}
.spoiler {
& .label {
@mixin text-xl;
font-family: var(--font-header);
font-weight: var(--typo-weight-black);
&::before {
content: "[REDACTED] ";
color: var(--color-state-error);
}
&:hover {
cursor: pointer;
}
}
& .content {
pointer-events: none;
position: relative;
min-height: 3em;
&::after {
content: '████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
font-family: var(--font-mono);
line-height: inherit;
color: var(--color-text-primary);
letter-spacing: -0.15em;
word-break: break-all;
white-space: pre-wrap;
background: var(--color-text-primary);
}
}
& .toggle {
@util hide-visually;
&:checked {
& ~ .label {
&::before {
content: '[REVEALED] ';
color: var(--color-state-success);
}
}
& ~ .content {
&::after {
content: none;
}
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
import styles from './Callout.module.css';
interface CalloutProps {
type: 'default' | 'example' | 'info' | 'warning' | 'tip' | 'spoiler';
title?: string;
children: React.ReactNode;
}
export default function Callout({ type, title, children }: CalloutProps) {
const isSpoiler = type === 'spoiler';
const spoilerID = isSpoiler
? `spoiler-${Math.random().toString(36).substr(2, 9)}`
: undefined;
return (
<div className={`${styles.container} ${styles[type]}`}>
{isSpoiler ? (
<>
<input type="checkbox" id={spoilerID} className={styles.toggle} />
<label htmlFor={spoilerID} className={styles.label}>
{title || 'Show spoiler'}
</label>
</>
) : (
<p className={styles.title}>{title}</p>
)}
<span className={styles.badge}>{type.toUpperCase()}</span>
<div className={`${styles.content}`}>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
.figure {
position: relative;
border: var(--size-2) solid var(--color-surface-inverse);
clip-path: polygon(0 var(--size-12), var(--size-12) 0, 100% 0, 100% 100%, 0 100%);
}
.caption {
padding: var(--spacing-snug);
font-family: var(--font-mono);
font-size: var(--typo-size-sm);
line-height: var(--typo-leading-relaxed);
color: var(--color-text-inverse);
background: var(--color-surface-inverse);
}
.credit {
display: block;
margin-top: var(--spacing-tight);
font-family: var(--font-mono);
font-size: var(--typo-size-xs);
font-style: normal;
color: var(--color-text-inverse);
text-align: right;
}

View File

@@ -0,0 +1,34 @@
import Image from 'next/image';
import styles from './Figure.module.css';
interface FigureProps {
src: string;
alt: string;
caption?: string;
credit?: string;
}
export default function Figure({ src, alt, caption, credit }: FigureProps) {
const hasCaption = !!caption;
const hasCredit = !!credit;
const showFigcaption = hasCaption || hasCredit;
return (
<figure className={styles.figure}>
<Image
src={src}
alt={alt}
width={800}
height={600}
className={styles.image}
/>
{showFigcaption && (
<figcaption className={styles.caption}>
{hasCaption && <span className={styles.text}>{caption}</span>}
{hasCredit && <cite className={styles.credit}>[{credit}]</cite>}
</figcaption>
)}
</figure>
);
}

View File

@@ -6,6 +6,8 @@ import DefinitionItem from '@/components/Content/DefinitionList/DefinitionItem';
import Accordion from '@/components/Content/Accordion';
import AccordionItem from '@/components/Content/Accordion/AccordionItem';
import Blockquote from '@/components/Content/Blockquote';
import Figure from '@/components/Content/Figure';
import Callout from '@/components/Content/Callout';
const ContentComponents = {
Column,
@@ -16,6 +18,8 @@ const ContentComponents = {
Accordion,
AccordionItem,
Blockquote,
Figure,
Callout,
};
export default ContentComponents;

View File

@@ -2,7 +2,7 @@ import { textQuoteIcon } from '@keystar/ui/icon/icons/textQuoteIcon';
import { block } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
const blockquoteComponents = {
const blockquoteComponent = {
Blockquote: block({
label: 'Blockquote',
icon: textQuoteIcon,
@@ -31,4 +31,4 @@ const blockquoteComponents = {
}),
};
export default blockquoteComponents;
export default blockquoteComponent;

View File

@@ -0,0 +1,47 @@
import { wrapper } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
import { megaphoneIcon } from '@keystar/ui/icon/icons/megaphoneIcon';
const calloutComponent = {
Callout: wrapper({
label: 'Callout',
icon: megaphoneIcon,
schema: {
type: fields.select({
label: 'Type',
defaultValue: 'default',
options: [
{
label: 'Default',
value: 'default',
},
{
label: 'Example',
value: 'example',
},
{
label: 'Info',
value: 'info',
},
{
label: 'Warning',
value: 'warning',
},
{
label: 'Tip',
value: 'tip',
},
{
label: 'Spoiler',
value: 'spoiler',
},
],
}),
title: fields.text({
label: 'Title',
}),
},
}),
};
export default calloutComponent;

View File

@@ -0,0 +1,35 @@
import { imageIcon } from '@keystar/ui/icon/icons/imageIcon';
import { block } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
const figureComponent = {
Figure: block({
label: 'Figure',
icon: imageIcon,
schema: {
src: fields.image({
label: 'Image',
directory: 'public/images/figures',
publicPath: '/images/figures',
}),
alt: fields.text({
label: 'Alt Text',
validation: {
length: {
min: 1,
},
},
}),
caption: fields.text({
label: 'Caption',
multiline: true,
}),
credit: fields.text({
label: 'Credit/Attribution',
description: 'Photographer, artist, or source',
}),
},
}),
};
export default figureComponent;

View File

@@ -2,12 +2,16 @@ 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';
import blockquoteComponents from '@/keystatic/components/general/blockquote';
import blockquoteComponent from '@/keystatic/components/general/blockquote';
import figureComponent from '@/keystatic/components/general/figure';
import calloutComponent from '@/keystatic/components/general/callout';
export const generalComponents = {
...gridComponents,
...sidenoteComponents,
...definitionlistComponents,
...accordionComponents,
...blockquoteComponents,
...blockquoteComponent,
...figureComponent,
...calloutComponent,
};

View File

@@ -93,4 +93,35 @@ export const tags: Config['tags'] = {
},
},
},
Figure: {
render: 'Figure',
attributes: {
src: {
type: 'String',
required: true,
},
alt: {
type: 'String',
required: true,
},
caption: {
type: 'String',
},
credit: {
type: 'String',
},
},
},
Callout: {
render: 'Callout',
attributes: {
type: {
type: 'String',
default: 'default',
},
title: {
type: 'String',
},
},
},
};

View File

@@ -143,7 +143,7 @@
}
}
& p {
& p:not([class]) {
margin-block: var(--el-p-vspace-top) var(--el-p-vspace-bottom);
font-family: var(--el-p-font-family), sans-serif;
@@ -495,18 +495,6 @@
text-underline-offset: var(--size-1);
}
& dfn {
font-weight: var(--typo-weight-bold);
font-style: normal;
color: var(--color-text-primary);
}
& cite {
font-weight: var(--typo-weight-semibold);
font-style: normal;
color: var(--color-text-secondary);
}
& q {
font-style: normal;