From fbd39f3dda043d9ce14806bf18c8cee9608ea8b0 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Thu, 19 Feb 2026 12:39:39 -0500 Subject: [PATCH 1/4] Improve MarkdownDef formats with rich rendering and distinct layouts Previously MarkdownDef used a single MarkdownFilePreview component for all formats, showing only title + plain-text excerpt. Now each format has a purpose-built component: isolated renders full markdown via MarkdownTemplate, embedded shows title + rendered content with fade-out, fitted uses icon + title + excerpt with container queries, and atom shows icon + title inline. Co-Authored-By: Claude Opus 4.6 --- packages/base/markdown-file-def.gts | 256 +++++++++++++++++++++++- packages/base/markdown-file-preview.gts | 48 ----- 2 files changed, 251 insertions(+), 53 deletions(-) delete mode 100644 packages/base/markdown-file-preview.gts diff --git a/packages/base/markdown-file-def.gts b/packages/base/markdown-file-def.gts index ec22c48242..31c4cc24a2 100644 --- a/packages/base/markdown-file-def.gts +++ b/packages/base/markdown-file-def.gts @@ -1,6 +1,13 @@ import { byteStreamToUint8Array } from '@cardstack/runtime-common'; -import { StringField, contains, field } from './card-api'; -import MarkdownFilePreview from './markdown-file-preview'; +import MarkdownIcon from '@cardstack/boxel-icons/align-box-left-middle'; +import { + BaseDefComponent, + Component, + StringField, + contains, + field, +} from './card-api'; +import MarkdownTemplate from './default-templates/markdown'; import { FileContentMismatchError, FileDef, @@ -90,6 +97,244 @@ function extractExcerpt(markdown: string): string { return ''; } +class Isolated extends Component { + get title() { + return this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown'; + } + + get hasContent() { + return Boolean(this.args.model?.content?.trim()); + } + + +} + +class Embedded extends Component { + get title() { + return this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown'; + } + + +} + +class Fitted extends Component { + get title() { + return this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown'; + } + + get excerpt() { + return this.args.model?.excerpt ?? ''; + } + + get hasExcerpt() { + return Boolean(this.excerpt); + } + + +} + +class Atom extends Component { + get title() { + return this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown'; + } + + +} + export class MarkdownDef extends FileDef { static displayName = 'Markdown'; static acceptTypes = '.md,.markdown'; @@ -98,9 +343,10 @@ export class MarkdownDef extends FileDef { @field excerpt = contains(StringField); @field content = contains(StringField); - static embedded = MarkdownFilePreview; - static fitted = MarkdownFilePreview; - static isolated = MarkdownFilePreview; + static isolated: BaseDefComponent = Isolated; + static embedded: BaseDefComponent = Embedded; + static fitted: BaseDefComponent = Fitted; + static atom: BaseDefComponent = Atom; static async extractAttributes( url: string, diff --git a/packages/base/markdown-file-preview.gts b/packages/base/markdown-file-preview.gts deleted file mode 100644 index ffcde9ca8c..0000000000 --- a/packages/base/markdown-file-preview.gts +++ /dev/null @@ -1,48 +0,0 @@ -import { Component } from './card-api'; -import type { MarkdownDef } from './markdown-file-def'; - -export default class MarkdownFilePreview extends Component { - get title() { - return ( - this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown' - ); - } - - get excerpt() { - return this.args.model?.excerpt ?? ''; - } - - get hasExcerpt() { - return Boolean(this.excerpt); - } - - -} From 4153b3fb2065a9504e576c3ec460bb086ea0c6ec Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Thu, 19 Feb 2026 18:03:07 -0500 Subject: [PATCH 2/4] Refine MarkdownDef: head format, embedded dedup, and first-heading margin - Add Head format with OG/Twitter meta tags using title and excerpt - Skip duplicate title in Embedded when content starts with matching heading - Add padding to Embedded format - Suppress top margin on first heading in Isolated and Embedded formats Co-Authored-By: Claude Opus 4.6 --- packages/base/markdown-file-def.gts | 90 ++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/packages/base/markdown-file-def.gts b/packages/base/markdown-file-def.gts index 31c4cc24a2..d8caf9c3fd 100644 --- a/packages/base/markdown-file-def.gts +++ b/packages/base/markdown-file-def.gts @@ -16,7 +16,7 @@ import { } from './file-api'; const MARKDOWN_EXTENSIONS = new Set(['.md', '.markdown']); -const EXCERPT_MAX_LENGTH = 240; +const EXCERPT_MAX_LENGTH = 500; function getExtension(url: string): string { try { @@ -99,7 +99,9 @@ function extractExcerpt(markdown: string): string { class Isolated extends Component { get title() { - return this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown'; + return ( + this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown' + ); } get hasContent() { @@ -125,18 +127,45 @@ class Isolated extends Component { font-weight: 600; font-size: var(--boxel-font-size-lg); } + + .markdown-isolated :deep(h1:first-child), + .markdown-isolated :deep(h2:first-child), + .markdown-isolated :deep(h3:first-child), + .markdown-isolated :deep(h4:first-child), + .markdown-isolated :deep(h5:first-child), + .markdown-isolated :deep(h6:first-child) { + margin-top: 0; + } } class Embedded extends Component { get title() { - return this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown'; + return ( + this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown' + ); + } + + get contentStartsWithTitle() { + let content = this.args.model?.content?.trim(); + if (!content) { + return false; + } + let firstLine = content.split('\n')[0].trim(); + let match = firstLine.match(/^\s*#{1,6}\s+(.+?)\s*#*\s*$/); + if (!match?.[1]) { + return false; + } + let headingText = stripMarkdown(match[1]); + return headingText === this.title; } @@ -165,7 +208,9 @@ class Embedded extends Component { class Fitted extends Component { get title() { - return this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown'; + return ( + this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown' + ); } get excerpt() { @@ -303,7 +348,9 @@ class Fitted extends Component { class Atom extends Component { get title() { - return this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown'; + return ( + this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown' + ); } } +class Head extends Component { + get title() { + return this.args.model?.title ?? this.args.model?.name ?? 'Untitled markdown'; + } + + get description() { + return this.args.model?.excerpt; + } + + +} + export class MarkdownDef extends FileDef { static displayName = 'Markdown'; static acceptTypes = '.md,.markdown'; @@ -347,6 +422,7 @@ export class MarkdownDef extends FileDef { static embedded: BaseDefComponent = Embedded; static fitted: BaseDefComponent = Fitted; static atom: BaseDefComponent = Atom; + static head: BaseDefComponent = Head; static async extractAttributes( url: string, From 78a763e751b722ba0661744de8460b926501536f Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Thu, 19 Feb 2026 22:06:04 -0500 Subject: [PATCH 3/4] Lint fixes --- packages/base/markdown-file-def.gts | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/base/markdown-file-def.gts b/packages/base/markdown-file-def.gts index d8caf9c3fd..feef00dc62 100644 --- a/packages/base/markdown-file-def.gts +++ b/packages/base/markdown-file-def.gts @@ -38,10 +38,14 @@ function normalizeMarkdown(markdown: string): string { return markdown.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); } +const FENCED_CODE_RE = /```[\s\S]*?```/g; +// content-tag misparses backticks inside regex literals in .gts files +const INLINE_CODE_RE = new RegExp('`([^`]+)`', 'g'); + function stripMarkdown(text: string): string { return text - .replace(/```[\s\S]*?```/g, '') - .replace(/`([^`]+)`/g, '$1') + .replace(FENCED_CODE_RE, '') + .replace(INLINE_CODE_RE, '$1') .replace(/!\[[^\]]*\]\([^)]+\)/g, '') .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') .replace(/^\s*#{1,6}\s+/gm, '') @@ -73,6 +77,8 @@ function extractTitle(markdown: string, fallback: string): string { return fallback; } +const HEADING_RE = /^\s*#{1,6}\s+/; + function extractExcerpt(markdown: string): string { let normalized = normalizeMarkdown(markdown); let paragraphs = normalized.split(/\n\s*\n/); @@ -82,12 +88,12 @@ function extractExcerpt(markdown: string): string { continue; } let lines = trimmed.split('\n'); - let hasNonHeading = lines.some((line) => !/^\s*#{1,6}\s+/.test(line)); + let hasNonHeading = lines.some((line) => !HEADING_RE.test(line)); if (!hasNonHeading) { continue; } let withoutHeadings = lines - .filter((line) => !/^\s*#{1,6}\s+/.test(line)) + .filter((line) => !HEADING_RE.test(line)) .join(' '); let excerpt = stripMarkdown(withoutHeadings); if (excerpt) { @@ -104,6 +110,10 @@ class Isolated extends Component { ); } + get content() { + return this.args.model?.content ?? null; + } + get hasContent() { return Boolean(this.args.model?.content?.trim()); } @@ -111,7 +121,7 @@ class Isolated extends Component {