diff --git a/src/components/Avatar.astro b/src/components/Avatar.astro index 411ad08..0e082a6 100644 --- a/src/components/Avatar.astro +++ b/src/components/Avatar.astro @@ -21,7 +21,7 @@ const initials = name .avatar { width: var(--avatar-size); height: var(--avatar-size); - border-radius: 50%; + border-radius: var(--radius-circle); overflow: hidden; flex: 0 0 auto; display: inline-flex; diff --git a/src/pages/about.astro b/src/pages/about.astro index 14c259a..ef3017a 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -217,7 +217,7 @@ const links = [ align-items: center; padding: 22px; border: 1px solid var(--line); - border-radius: 12px; + border-radius: var(--radius-lg); background: var(--paper-2); color: inherit !important; text-decoration: none; diff --git a/src/pages/authors/[handle].astro b/src/pages/authors/[handle].astro index 61088b5..af1cf01 100644 --- a/src/pages/authors/[handle].astro +++ b/src/pages/authors/[handle].astro @@ -204,7 +204,7 @@ const breadcrumbJsonLd = { .author-avatar-lg { width: 88px; height: 88px; - border-radius: 50%; + border-radius: var(--radius-circle); overflow: hidden; background: var(--paper-2); border: 1px solid var(--line); diff --git a/src/pages/authors/index.astro b/src/pages/authors/index.astro index e7083d0..10eab9d 100644 --- a/src/pages/authors/index.astro +++ b/src/pages/authors/index.astro @@ -91,7 +91,7 @@ const contributors = authors gap: 18px; padding: 22px; border: 1px solid var(--line); - border-radius: 12px; + border-radius: var(--radius-lg); background: var(--paper-2); color: inherit; text-decoration: none; @@ -110,7 +110,7 @@ const contributors = authors .contributor-avatar { width: 56px; height: 56px; - border-radius: 50%; + border-radius: var(--radius-circle); overflow: hidden; background: var(--paper); border: 1px solid var(--line); diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro index 3e340c7..399d1d3 100644 --- a/src/pages/blog/[slug].astro +++ b/src/pages/blog/[slug].astro @@ -107,7 +107,8 @@ const breadcrumbJsonLd = { ← Back to archive
- {topicName(post.data.topicId)} { diff --git a/src/pages/community/index.astro b/src/pages/community/index.astro index d93af6b..0076387 100644 --- a/src/pages/community/index.astro +++ b/src/pages/community/index.astro @@ -97,7 +97,7 @@ const channels = [ gap: 12px; padding: 24px 24px 20px; border: 1px solid var(--line); - border-radius: 12px; + border-radius: var(--radius-lg); background: var(--paper-2); color: inherit; text-decoration: none; diff --git a/src/pages/contact.astro b/src/pages/contact.astro index d1ed8fc..8b9cef9 100644 --- a/src/pages/contact.astro +++ b/src/pages/contact.astro @@ -121,7 +121,7 @@ const channels = [ align-items: center; padding: 22px 22px; border: 1px solid var(--line); - border-radius: 12px; + border-radius: var(--radius-lg); background: var(--paper-2); color: inherit; text-decoration: none; diff --git a/src/pages/index.astro b/src/pages/index.astro index e50d6e8..1e4d854 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -216,6 +216,35 @@ const featuredTools = allTools
+ +
+
+
+
+
Tools
+

Run the math yourself.

+
+ All tools → +
+ +
+
+
@@ -263,35 +292,6 @@ const featuredTools = allTools
- -
-
-
-
-
Tools
-

Run the math yourself.

-
- All tools → -
- -
-
-
@@ -371,7 +371,7 @@ const featuredTools = allTools padding: 8px 14px; background: transparent; border: 1px solid var(--line); - border-radius: 99px; + border-radius: var(--radius-pill); color: var(--ink-2); font: inherit; font-size: 13px; @@ -415,7 +415,7 @@ const featuredTools = allTools align-items: start; padding: 18px 22px; border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius-md); background: var(--paper-2); color: inherit; text-decoration: none; diff --git a/src/pages/playground/[tool].astro b/src/pages/playground/[tool].astro index 548ad93..0c1af4f 100644 --- a/src/pages/playground/[tool].astro +++ b/src/pages/playground/[tool].astro @@ -113,7 +113,7 @@ const breadcrumbJsonLd = { } .tool-tag { padding: 3px 10px; - border-radius: 99px; + border-radius: var(--radius-pill); text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink); diff --git a/src/pages/playground/index.astro b/src/pages/playground/index.astro index 8a24b6f..c69824b 100644 --- a/src/pages/playground/index.astro +++ b/src/pages/playground/index.astro @@ -304,7 +304,7 @@ const catalogTools = allTools.sort((a, b) => { color: var(--ink-3); padding: 3px 8px; border: 1px solid var(--line-2); - border-radius: 99px; + border-radius: var(--radius-pill); } .tool-card-live .tool-card-tag { color: var(--paper); @@ -416,7 +416,7 @@ const catalogTools = allTools.sort((a, b) => { padding: 10px 14px; background: var(--paper-2); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius-md); color: var(--ink-3); transition: border-color 0.15s ease, @@ -460,7 +460,7 @@ const catalogTools = allTools.sort((a, b) => { } .tools-catalog-scroll::-webkit-scrollbar-thumb { background: var(--line-2); - border-radius: 99px; + border-radius: var(--radius-pill); } .tools-catalog-scroll::-webkit-scrollbar-thumb:hover { background: var(--ink-3); @@ -510,7 +510,7 @@ const catalogTools = allTools.sort((a, b) => { letter-spacing: 0.1em; padding: 3px 9px; border: 1px solid var(--line-2); - border-radius: 99px; + border-radius: var(--radius-pill); color: var(--ink-3); background: color-mix(in srgb, var(--paper-2) 90%, transparent); } diff --git a/src/pages/topics/[id].astro b/src/pages/topics/[id].astro index 6615563..ab9ae73 100644 --- a/src/pages/topics/[id].astro +++ b/src/pages/topics/[id].astro @@ -72,12 +72,10 @@ const breadcrumbJsonLd = { jsonLd={[collectionJsonLd, breadcrumbJsonLd]} >
-
-
-
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 {