-
-
Topic
-
{topic.name}.
-
{topic.desc}
-
+
+
Topic
+
{topic.name}.
+
{topic.desc}
{
diff --git a/src/pages/write.astro b/src/pages/write.astro
index e8109f0..f8821c0 100644
--- a/src/pages/write.astro
+++ b/src/pages/write.astro
@@ -22,12 +22,7 @@ const repoUrl = SITE.repoUrl;
Write an article
- Draft your post here — no Markdown, no setup. When you’re done, download your post folder and
- email it to us — we’ll take care of publishing. Comfortable with GitHub? You can open a pull
- request instead. Prefer writing MDX by hand? See the authoring guide.
+ Write your post, then download it and send it over — by email or pull request.
diff --git a/src/styles/global.css b/src/styles/global.css
index 1e6b8e6..3a48d69 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -15,7 +15,7 @@ html {
background: var(--paper);
color: var(--accent);
border: 1px solid var(--accent);
- border-radius: 6px;
+ border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 13px;
text-decoration: none;
@@ -202,7 +202,7 @@ html {
:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
- border-radius: 3px;
+ border-radius: var(--radius-xs);
}
@media (prefers-reduced-motion: reduce) {
*,
@@ -399,7 +399,7 @@ main {
padding: 0;
background: transparent;
border: 1px solid var(--line);
- border-radius: 6px;
+ border-radius: var(--radius-sm);
cursor: pointer;
color: var(--ink-2);
transition:
@@ -438,7 +438,7 @@ main {
padding: 0;
background: transparent;
border: 1px solid var(--line);
- border-radius: 6px;
+ border-radius: var(--radius-sm);
cursor: pointer;
flex-direction: column;
gap: 4px;
@@ -469,7 +469,7 @@ main {
.nav-links a {
font-size: 14px;
padding: 6px 12px;
- border-radius: 6px;
+ border-radius: var(--radius-sm);
color: var(--ink-2);
font-weight: 500;
transition:
@@ -554,7 +554,7 @@ main {
font-size: 14px;
font-weight: 500;
padding: 10px 18px;
- border-radius: 8px;
+ border-radius: var(--radius-md);
border: 1px solid var(--line-2);
background: transparent;
color: var(--ink);
@@ -601,7 +601,7 @@ main {
.figure {
border: 1px solid var(--line-2);
background: var(--paper-2);
- border-radius: 6px;
+ border-radius: var(--radius-sm);
padding: 20px;
position: relative;
aspect-ratio: 1 / 1.05;
@@ -759,7 +759,7 @@ main {
.article-topic {
padding: 2px 8px;
border: 1px solid var(--line-2);
- border-radius: 99px;
+ border-radius: var(--radius-pill);
color: var(--ink-2);
}
.article-title {
@@ -884,7 +884,7 @@ main {
width: 32px;
height: 32px;
border: 1px solid var(--line-2);
- border-radius: 6px;
+ border-radius: var(--radius-sm);
background: var(--paper-2);
display: flex;
align-items: center;
@@ -924,7 +924,7 @@ main {
/* Side panel (right of community) */
.side-panel {
border: 1px solid var(--line);
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: var(--paper-2);
padding: 28px;
}
@@ -1025,7 +1025,7 @@ main {
/* CTA contribute */
.cta-banner {
border: 1px solid var(--line-2);
- border-radius: 12px;
+ border-radius: var(--radius-lg);
padding: 64px 56px;
background: var(--paper-2);
display: grid;
@@ -1042,7 +1042,7 @@ main {
top: -40px;
width: 200px;
height: 200px;
- border-radius: 50%;
+ border-radius: var(--radius-circle);
background: radial-gradient(circle, var(--accent-soft) 0%, transparent 70%);
pointer-events: none;
}
@@ -1145,7 +1145,7 @@ main {
justify-content: center;
width: 44px;
height: 44px;
- border-radius: 8px;
+ border-radius: var(--radius-md);
color: var(--ink-2);
transition:
color 0.15s ease,
@@ -1314,7 +1314,7 @@ main {
background: #24292e;
color: #e1e4e8;
border: 1px solid #3a3f46;
- border-radius: 8px;
+ border-radius: var(--radius-md);
padding: 20px;
overflow-x: auto;
font-family: var(--font-mono);
@@ -1332,10 +1332,60 @@ main {
line-height: 1.4;
color: var(--ink-2);
}
+/* GFM task lists (from editor Check List blocks) */
+.article-body ul.contains-task-list {
+ list-style: none;
+ padding-left: 0;
+}
+.article-body li.task-list-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+}
+.article-body li.task-list-item input[type='checkbox'] {
+ margin-top: 7px;
+ width: 15px;
+ height: 15px;
+ flex-shrink: 0;
+ accent-color: var(--accent);
+}
+
+/* Collapsible details (from editor Toggle List / Toggle Heading blocks) */
+.article-body details {
+ margin: 24px 0;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-md);
+ background: var(--paper-2);
+ padding: 0 20px;
+}
+.article-body details[open] {
+ padding-bottom: 16px;
+}
+.article-body summary {
+ cursor: pointer;
+ padding: 14px 0;
+ font-family: var(--font-sans);
+ font-weight: 600;
+ list-style: none;
+}
+.article-body summary::-webkit-details-marker {
+ display: none;
+}
+.article-body summary::before {
+ content: '▸';
+ display: inline-block;
+ margin-right: 10px;
+ color: var(--accent);
+ transition: transform 0.15s ease;
+}
+.article-body details[open] summary::before {
+ transform: rotate(90deg);
+}
+
.article-body .inline-figure {
margin: 32px 0;
border: 1px solid var(--line);
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: var(--paper-2);
padding: 20px;
}
@@ -1553,7 +1603,7 @@ main {
align-items: center;
padding: 20px 64px 20px 24px;
border: 1px solid var(--line-2);
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: var(--paper);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.14);
}
@@ -1567,7 +1617,7 @@ main {
align-items: center;
justify-content: center;
border: 0;
- border-radius: 6px;
+ border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-3);
cursor: pointer;
@@ -1661,7 +1711,7 @@ main {
font-size: 11px;
padding: 5px 11px;
border: 1px solid var(--line);
- border-radius: 99px;
+ border-radius: var(--radius-pill);
background: transparent;
cursor: pointer;
color: var(--ink-2);
@@ -1733,7 +1783,7 @@ main {
color: var(--ink-2);
padding: 2px 8px;
border: 1px solid var(--ink-3);
- border-radius: 99px;
+ border-radius: var(--radius-pill);
white-space: nowrap;
}
.blog-row-tag {
@@ -1943,7 +1993,7 @@ main {
.nav-links a {
padding: 12px 14px;
font-size: 15px;
- border-radius: 6px;
+ border-radius: var(--radius-sm);
}
.nav-links a.active::after {
display: none;
@@ -1961,7 +2011,7 @@ main {
padding: 6px 14px;
background: var(--accent);
color: var(--paper);
- border-radius: 6px;
+ border-radius: var(--radius-sm);
white-space: nowrap;
display: inline-flex;
align-items: center;
@@ -1982,7 +2032,7 @@ main {
min-width: 180px;
background: var(--paper-2);
border: 1px solid var(--line);
- border-radius: 6px;
+ border-radius: var(--radius-sm);
color: var(--ink-3);
font: inherit;
font-size: 13px;
@@ -2121,7 +2171,7 @@ main {
padding: 0;
background: var(--paper-2);
border: 1px solid var(--line);
- border-radius: 6px;
+ border-radius: var(--radius-sm);
color: var(--ink-2);
cursor: pointer;
transition:
@@ -2172,7 +2222,7 @@ main {
color: var(--ink-2);
background: var(--paper-2);
border: 1px solid var(--line);
- border-radius: 99px;
+ border-radius: var(--radius-pill);
padding: 4px 10px;
text-decoration: none;
transition:
@@ -2203,7 +2253,7 @@ main {
color: var(--ink-2);
background: var(--paper-2);
border: 1px solid var(--line);
- border-radius: 99px;
+ border-radius: var(--radius-pill);
padding: 3px 10px;
cursor: pointer;
transition:
@@ -2220,7 +2270,7 @@ main {
padding: 6px;
}
.search-results li {
- border-radius: 8px;
+ border-radius: var(--radius-md);
}
.search-results li.active {
background: var(--paper-2);
@@ -2230,7 +2280,7 @@ main {
padding: 12px 14px;
color: inherit;
text-decoration: none;
- border-radius: 8px;
+ border-radius: var(--radius-md);
}
.search-result-meta {
display: flex;
@@ -2258,7 +2308,7 @@ main {
background: var(--accent-soft);
color: var(--accent);
padding: 0 2px;
- border-radius: 3px;
+ border-radius: var(--radius-xs);
}
.search-foot {
display: flex;
@@ -2277,7 +2327,7 @@ main {
color: var(--ink-2);
background: var(--paper-2);
border: 1px solid var(--line);
- border-radius: 3px;
+ border-radius: var(--radius-xs);
padding: 1px 5px;
margin: 0 2px 0 0;
}
@@ -2320,7 +2370,7 @@ main {
padding: 14px 18px;
background: var(--paper-2);
border: 1px solid var(--line);
- border-radius: 12px;
+ border-radius: var(--radius-lg);
transition:
border-color 0.15s ease,
background 0.15s ease;
@@ -2356,7 +2406,7 @@ main {
padding: 0;
background: transparent;
border: none;
- border-radius: 50%;
+ border-radius: var(--radius-circle);
color: var(--ink-3);
cursor: pointer;
transition:
@@ -2388,7 +2438,7 @@ main {
padding: 0;
}
.search-results-page li {
- border-radius: 8px;
+ border-radius: var(--radius-md);
}
.search-results-page li.active {
background: var(--paper-2);
@@ -2458,7 +2508,7 @@ main {
padding: 14px 16px;
background: transparent;
border: 1px solid transparent;
- border-radius: 6px;
+ border-radius: var(--radius-sm);
cursor: pointer;
opacity: 1;
color: inherit;
diff --git a/src/write/WritePortal.tsx b/src/write/WritePortal.tsx
index 376cb7d..0f2fd75 100644
--- a/src/write/WritePortal.tsx
+++ b/src/write/WritePortal.tsx
@@ -3,7 +3,10 @@ import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
import { AllSelection, TextSelection } from 'prosemirror-state';
import { filterSuggestionItems } from '@blocknote/core';
import {
+ FormattingToolbar,
+ FormattingToolbarController,
SuggestionMenuController,
+ blockTypeSelectItems,
useCreateBlockNote,
useEditorSelectionChange,
} from '@blocknote/react';
@@ -220,6 +223,17 @@ export default function WritePortal({ authors, topics, repoUrl, contactEmail }:
const slashItems = useMemo(() => getSlashItems(editor), [editor]);
+ // Drop H4–H6 from the block-type dropdown — the serializer only emits h2–h4
+ // (editor H1–H3), so deeper levels would silently collapse on the published site.
+ const blockTypeItems = useMemo(
+ () =>
+ blockTypeSelectItems(editor.dictionary).filter(
+ (item) =>
+ !(item.type === 'heading' && !item.props?.isToggleable && Number(item.props?.level) > 3),
+ ),
+ [editor],
+ );
+
const handleSelectAll = (e: ReactKeyboardEvent) => {
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'a' || e.shiftKey || e.altKey) return;
const view = editor.prosemirrorView;
@@ -315,8 +329,12 @@ export default function WritePortal({ authors, topics, repoUrl, contactEmail }:
theme={siteTheme}
editable={!restore}
slashMenu={false}
+ formattingToolbar={false}
onChange={autosave}
>
+
}
+ />
filterSuggestionItems(slashItems, query)}
diff --git a/src/write/editor/slashMenu.tsx b/src/write/editor/slashMenu.tsx
index 2104af2..dadcfe3 100644
--- a/src/write/editor/slashMenu.tsx
+++ b/src/write/editor/slashMenu.tsx
@@ -7,12 +7,13 @@ const HIDDEN = [
'video',
'audio',
'file',
- 'check list',
- 'toggle',
'emoji',
'table',
'divider',
'page',
+ // Toggle headings stay dropdown-only to keep the slash menu uncluttered;
+ // Check List and Toggle List are offered here (both are exported).
+ 'toggle heading',
];
const GROUP_ORDER = ['Headings', 'Subheadings', 'Basic blocks', 'Media', 'Advanced'];
diff --git a/src/write/serialize/toMdx.test.ts b/src/write/serialize/toMdx.test.ts
index 03e26d7..01f930e 100644
--- a/src/write/serialize/toMdx.test.ts
+++ b/src/write/serialize/toMdx.test.ts
@@ -214,6 +214,33 @@ describe('block serialization', () => {
it('leaves left-aligned blocks unwrapped', () => {
expect(body([block('paragraph', { textAlignment: 'left' }, [t('normal')])])).toBe('normal');
});
+
+ it('renders check list items as GFM task lists', () => {
+ const out = body([
+ block('checkListItem', { checked: false }, [t('todo')]),
+ block('checkListItem', { checked: true }, [t('done')]),
+ ]);
+ expect(out).toBe('- [ ] todo\n- [x] done');
+ });
+
+ it('renders toggle list items as collapsible details', () => {
+ const out = body([
+ block('toggleListItem', {}, [t('Summary')], [block('paragraph', {}, [t('hidden body')])]),
+ ]);
+ expect(out).toBe('\nSummary
\n\nhidden body\n\n ');
+ });
+
+ it('renders toggle headings as collapsible details', () => {
+ const out = body([
+ block(
+ 'heading',
+ { level: 2, isToggleable: true },
+ [t('More')],
+ [block('paragraph', {}, [t('inside')])],
+ ),
+ ]);
+ expect(out).toBe('\nMore
\n\ninside\n\n ');
+ });
});
describe('video blocks', () => {
diff --git a/src/write/serialize/toMdx.ts b/src/write/serialize/toMdx.ts
index 70ee58f..9be5c2a 100644
--- a/src/write/serialize/toMdx.ts
+++ b/src/write/serialize/toMdx.ts
@@ -249,23 +249,39 @@ function aligned(block: SBlock, s: string): string {
return s;
}
+function detailsBlock(block: SBlock, ctx: Ctx): string {
+ const summary = serializeInline(block.content) || 'Details';
+ const body = block.children?.length ? serializeBlocks(block.children, ctx) : '';
+ const inner = body ? `\n\n${body}\n\n` : '\n';
+ return `\n${summary}
${inner} `;
+}
+
function serializeBlock(block: SBlock, ctx: Ctx, listNumber: number): string {
switch (block.type) {
case 'paragraph':
return aligned(block, serializeInline(block.content));
case 'heading': {
+ if (block.props.isToggleable) return detailsBlock(block, ctx);
const level = Math.min(Number(block.props.level ?? 1), 3);
return aligned(block, `${'#'.repeat(level + 1)} ${serializeInline(block.content)}`);
}
case 'bulletListItem':
- case 'numberedListItem': {
- const marker = block.type === 'bulletListItem' ? '- ' : `${listNumber}. `;
+ case 'numberedListItem':
+ case 'checkListItem': {
+ const marker =
+ block.type === 'numberedListItem'
+ ? `${listNumber}. `
+ : block.type === 'checkListItem'
+ ? `- [${block.props.checked ? 'x' : ' '}] `
+ : '- ';
let out = marker + serializeInline(block.content);
if (block.children?.length) {
out += `\n${indent(serializeBlocks(block.children, ctx), marker.length)}`;
}
return out;
}
+ case 'toggleListItem':
+ return detailsBlock(block, ctx);
case 'quote':
return aligned(block, `> ${serializeInline(block.content)}`);
case 'codeBlock': {
@@ -302,7 +318,7 @@ function serializeBlock(block: SBlock, ctx: Ctx, listNumber: number): string {
}
function isListItem(type?: string): boolean {
- return type === 'bulletListItem' || type === 'numberedListItem';
+ return type === 'bulletListItem' || type === 'numberedListItem' || type === 'checkListItem';
}
function serializeBlocks(blocks: SBlock[], ctx: Ctx): string {