-
Notifications
You must be signed in to change notification settings - Fork 5.4k
feat(markdown): render frontmatter in file previews #8202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
39107ac
2fdf40b
46d6a27
30573d1
09e8576
f737f14
999709f
ae9eea8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import type { MarkdownFrontmatterEntry } from "@t3tools/client-runtime/markdown-frontmatter"; | ||
| import { useState } from "react"; | ||
| import { ScrollView, Text as NativeText, View } from "react-native"; | ||
|
|
||
| import { useThemeColor } from "../../lib/useThemeColor"; | ||
|
|
||
| const MIN_KEY_COLUMN_WIDTH = 160; | ||
| const MIN_VALUE_COLUMN_WIDTH = 400; | ||
|
|
||
| function MarkdownFrontmatterList({ | ||
| items, | ||
| textColor, | ||
| }: { | ||
| readonly items: ReadonlyArray<string>; | ||
| readonly textColor: string; | ||
| }) { | ||
| const occurrences = new Map<string, number>(); | ||
|
|
||
| return ( | ||
| <View className="flex-row flex-wrap gap-1"> | ||
| {items.map((item) => { | ||
| const occurrence = occurrences.get(item) ?? 0; | ||
| occurrences.set(item, occurrence + 1); | ||
|
|
||
| return ( | ||
| <View | ||
| key={JSON.stringify([item, occurrence])} | ||
| className="max-w-full rounded-full border border-secondary-border bg-secondary px-2.5 py-1" | ||
| > | ||
| <NativeText className="font-t3-regular text-sm" style={{ color: textColor }}> | ||
| {item} | ||
| </NativeText> | ||
| </View> | ||
| ); | ||
| })} | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| export function MarkdownFrontmatterTable({ | ||
| entries, | ||
| }: { | ||
| readonly entries: ReadonlyArray<MarkdownFrontmatterEntry>; | ||
| }) { | ||
| const textColor = String(useThemeColor("--color-md-body")); | ||
| const strongColor = String(useThemeColor("--color-md-strong")); | ||
| const codeColor = String(useThemeColor("--color-md-code-text")); | ||
| const [keyWidths, setKeyWidths] = useState<ReadonlyMap<string, number>>(() => new Map()); | ||
| let measuredKeyColumnWidth = MIN_KEY_COLUMN_WIDTH; | ||
| let hasEveryKeyWidth = true; | ||
| for (const entry of entries) { | ||
| const keyWidth = keyWidths.get(entry.key); | ||
| if (keyWidth === undefined) { | ||
| hasEveryKeyWidth = false; | ||
| break; | ||
| } | ||
| measuredKeyColumnWidth = Math.max(measuredKeyColumnWidth, keyWidth); | ||
| } | ||
| const keyColumnWidth = hasEveryKeyWidth ? measuredKeyColumnWidth : null; | ||
|
|
||
| return ( | ||
| <ScrollView | ||
| horizontal | ||
| className="mb-6" | ||
| contentContainerStyle={{ minWidth: "100%" }} | ||
| showsHorizontalScrollIndicator={false} | ||
| > | ||
| <View | ||
| className="flex-1 overflow-hidden border border-border" | ||
| style={{ minWidth: (keyColumnWidth ?? MIN_KEY_COLUMN_WIDTH) + MIN_VALUE_COLUMN_WIDTH }} | ||
| > | ||
| {entries.map((entry, index) => ( | ||
| <View | ||
| key={entry.key} | ||
| className={index === 0 ? "flex-row" : "flex-row border-t border-border"} | ||
| > | ||
| <View | ||
| className="min-w-40 shrink-0 items-end justify-center border-r border-border bg-card px-3 py-2" | ||
| style={keyColumnWidth === null ? undefined : { width: keyColumnWidth }} | ||
| onLayout={ | ||
| keyColumnWidth === null | ||
| ? (event) => { | ||
| const measuredWidth = Math.ceil(event.nativeEvent.layout.width); | ||
| setKeyWidths((current) => { | ||
| if (current.get(entry.key) === measuredWidth) { | ||
| return current; | ||
| } | ||
| const next = new Map(current); | ||
| next.set(entry.key, measuredWidth); | ||
| return next; | ||
| }); | ||
| } | ||
| : undefined | ||
| } | ||
| > | ||
| <NativeText | ||
| numberOfLines={1} | ||
| className="font-t3-bold text-sm" | ||
| style={{ color: strongColor }} | ||
| > | ||
| {entry.key} | ||
| </NativeText> | ||
| </View> | ||
| <View className="min-w-0 flex-1 px-3 py-2"> | ||
| {entry.value.kind === "text" ? ( | ||
| <NativeText className="font-t3-regular text-sm" style={{ color: textColor }}> | ||
| {entry.value.text} | ||
| </NativeText> | ||
| ) : entry.value.kind === "list" ? ( | ||
| <MarkdownFrontmatterList items={entry.value.items} textColor={textColor} /> | ||
| ) : ( | ||
| <NativeText className="font-mono text-xs" style={{ color: codeColor }}> | ||
| {entry.value.source} | ||
| </NativeText> | ||
| )} | ||
| </View> | ||
| </View> | ||
| ))} | ||
| </View> | ||
| </ScrollView> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ import { | |
| isAtomCommandInterrupted, | ||
| squashAtomCommandFailure, | ||
| } from "@t3tools/client-runtime/state/runtime"; | ||
| import { parseMarkdownFrontmatter } from "@t3tools/client-runtime/markdown-frontmatter"; | ||
| import { ChevronRight, Code2, Eye, FolderTree, Globe2, LoaderCircle } from "lucide-react"; | ||
| import * as Schema from "effect/Schema"; | ||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||
|
|
@@ -58,6 +59,7 @@ import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRev | |
| import { fileBreadcrumbs } from "./filePath"; | ||
| import { isMarkdownPreviewFile, setMarkdownTaskChecked } from "./filePreviewMode"; | ||
| import { FileSaveCoordinator } from "./fileSaveCoordinator"; | ||
| import { MarkdownFrontmatterTable } from "./MarkdownFrontmatterTable"; | ||
| import { | ||
| confirmProjectFileQueryData, | ||
| getOptimisticProjectFileQueryData, | ||
|
|
@@ -724,24 +726,34 @@ function RenderedMarkdownSurface({ | |
| relativePath, | ||
| onPendingChange, | ||
| }); | ||
| const frontmatter = useMemo(() => parseMarkdownFrontmatter(contents), [contents]); | ||
|
|
||
| return ( | ||
| <ScrollArea className="min-h-0 flex-1"> | ||
| <ChatMarkdown | ||
| text={contents} | ||
| cwd={cwd} | ||
| threadRef={threadRef} | ||
| className="mx-auto max-w-4xl px-6 py-5" | ||
| onTaskListChange={({ markerOffset, checked }) => { | ||
| const currentContents = | ||
| getOptimisticProjectFileQueryData(environmentId, cwd, relativePath)?.contents ?? | ||
| contents; | ||
| const nextContents = setMarkdownTaskChecked(currentContents, markerOffset, checked); | ||
| if (nextContents === currentContents) return; | ||
| setProjectFileQueryData(environmentId, cwd, relativePath, nextContents); | ||
| saveCoordinator.change(nextContents); | ||
| }} | ||
| /> | ||
| <div className="mx-auto max-w-4xl px-6 py-5"> | ||
| {frontmatter.entries.length > 0 ? ( | ||
| <MarkdownFrontmatterTable entries={frontmatter.entries} /> | ||
|
Comment on lines
+734
to
+735
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This introduces user-visible frontmatter rendering across the web, desktop, and mobile file previews, but the commit adds no corresponding AGENTS.md reference: AGENTS.md:L75-L75 Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. addressed! |
||
| ) : null} | ||
| <ChatMarkdown | ||
| text={frontmatter.body} | ||
| cwd={cwd} | ||
| threadRef={threadRef} | ||
| className={frontmatter.entries.length > 0 ? "mt-8" : ""} | ||
| onTaskListChange={({ markerOffset, checked }) => { | ||
| const currentContents = | ||
| getOptimisticProjectFileQueryData(environmentId, cwd, relativePath)?.contents ?? | ||
| contents; | ||
| const nextContents = setMarkdownTaskChecked( | ||
| currentContents, | ||
| frontmatter.bodyOffset + markerOffset, | ||
| checked, | ||
| ); | ||
| if (nextContents === currentContents) return; | ||
| setProjectFileQueryData(environmentId, cwd, relativePath, nextContents); | ||
| saveCoordinator.change(nextContents); | ||
| }} | ||
| /> | ||
| </div> | ||
| </ScrollArea> | ||
| ); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import type { MarkdownFrontmatterEntry } from "@t3tools/client-runtime/markdown-frontmatter"; | ||
|
|
||
| import { Badge } from "~/components/ui/badge"; | ||
| import { ScrollArea } from "~/components/ui/scroll-area"; | ||
|
|
||
| function MarkdownFrontmatterList({ items }: { readonly items: ReadonlyArray<string> }) { | ||
| const occurrences = new Map<string, number>(); | ||
|
|
||
| return ( | ||
| <span className="flex flex-wrap gap-1"> | ||
| {items.map((item) => { | ||
| const occurrence = occurrences.get(item) ?? 0; | ||
| occurrences.set(item, occurrence + 1); | ||
|
|
||
| return ( | ||
| <Badge key={JSON.stringify([item, occurrence])} variant="outline"> | ||
| {item} | ||
| </Badge> | ||
| ); | ||
| })} | ||
| </span> | ||
| ); | ||
| } | ||
|
|
||
| export function MarkdownFrontmatterTable({ | ||
| entries, | ||
| }: { | ||
| readonly entries: ReadonlyArray<MarkdownFrontmatterEntry>; | ||
| }) { | ||
| return ( | ||
| <ScrollArea | ||
| chainVerticalScroll | ||
| scrollFade | ||
| hideScrollbars | ||
| className="w-full max-w-full rounded-none" | ||
| > | ||
| <table className="markdown-table"> | ||
| <tbody> | ||
| {entries.map((entry) => ( | ||
| <tr key={entry.key}> | ||
| <th | ||
| scope="row" | ||
| className="w-px whitespace-nowrap bg-muted/30 text-right align-middle font-semibold text-foreground" | ||
| > | ||
| {entry.key} | ||
| </th> | ||
| <td className="align-top text-foreground/80"> | ||
| {entry.value.kind === "text" ? ( | ||
| <span className="whitespace-pre-wrap">{entry.value.text}</span> | ||
| ) : entry.value.kind === "list" ? ( | ||
| <MarkdownFrontmatterList items={entry.value.items} /> | ||
| ) : ( | ||
| <pre className="overflow-x-auto whitespace-pre-wrap font-mono text-xs leading-relaxed"> | ||
| {entry.value.source} | ||
| </pre> | ||
| )} | ||
| </td> | ||
| </tr> | ||
| ))} | ||
| </tbody> | ||
| </table> | ||
| </ScrollArea> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # File previews | ||
|
|
||
| Markdown files can switch between source and rendered views on web and desktop. Mobile opens | ||
| Markdown files in the rendered view. | ||
|
|
||
| ## YAML frontmatter | ||
|
|
||
| Rendered Markdown recognizes YAML frontmatter when the file starts with a `---` line, ends the | ||
| frontmatter with another `---` line, and contains a YAML mapping. T3 Code displays the mapping as a | ||
| metadata table above the Markdown body. | ||
|
|
||
| Scalar values appear as text. Arrays containing only scalar values appear as pills. Nested objects | ||
| and arrays remain formatted as YAML. | ||
|
|
||
| Invalid YAML, an unclosed frontmatter block, or a frontmatter value that is not a mapping remains in | ||
| the Markdown body unchanged. |
Uh oh!
There was an error while loading. Please reload this page.