Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/mobile/src/features/files/FileMarkdownPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from "react";
import { parseMarkdownFrontmatter } from "@t3tools/client-runtime/markdown-frontmatter";
import {
Markdown,
type CustomRenderers,
Expand All @@ -20,6 +21,7 @@ import {
SelectableMarkdownText,
type NativeMarkdownTextStyle,
} from "../../native/SelectableMarkdownText";
import { MarkdownFrontmatterTable } from "./MarkdownFrontmatterTable";

interface MarkdownPreviewStyles {
readonly theme: PartialMarkdownTheme;
Expand Down Expand Up @@ -187,6 +189,7 @@ export function FileMarkdownPreview(props: {
}
}, [props.onRefresh]);
const styles = useMarkdownPreviewStyles();
const frontmatter = useMemo(() => parseMarkdownFrontmatter(props.markdown), [props.markdown]);
const onLinkPress = useCallback((href: string) => {
void tryOpenExternalUrl(href, "markdown-link");
}, []);
Expand All @@ -205,9 +208,12 @@ export function FileMarkdownPreview(props: {
}
>
<View className="mx-auto w-full max-w-[760px]">
{frontmatter.entries.length > 0 ? (
<MarkdownFrontmatterTable entries={frontmatter.entries} />
) : null}
{hasNativeSelectableMarkdownText() ? (
<SelectableMarkdownText
markdown={props.markdown}
markdown={frontmatter.body}
onLinkPress={onLinkPress}
textStyle={styles.nativeTextStyle}
/>
Expand All @@ -218,7 +224,7 @@ export function FileMarkdownPreview(props: {
styles={styles.styles}
theme={styles.theme}
>
{props.markdown}
{frontmatter.body}
</Markdown>
)}
</View>
Expand Down
122 changes: 122 additions & 0 deletions apps/mobile/src/features/files/MarkdownFrontmatterTable.tsx
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}
Comment thread
cursor[bot] marked this conversation as resolved.
</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>
);
}
42 changes: 27 additions & 15 deletions apps/web/src/components/files/FilePreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the new frontmatter preview behavior

This introduces user-visible frontmatter rendering across the web, desktop, and mobile file previews, but the commit adds no corresponding docs/user/ documentation. Add shipped-product documentation explaining when leading YAML frontmatter is rendered and which values remain unchanged.

AGENTS.md reference: AGENTS.md:L75-L75

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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>
);
}
Expand Down
64 changes: 64 additions & 0 deletions apps/web/src/components/files/MarkdownFrontmatterTable.tsx
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>
);
}
27 changes: 21 additions & 6 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2343,29 +2343,44 @@ code {
wrapping rules (overflow-wrap: anywhere) would let columns shrink to single
characters and defeat the overflow — restore word-boundary wrapping so the
min column width is the longest word. */
.chat-markdown table {
.chat-markdown table,
.markdown-table {
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
width: 100%;
min-width: max-content;
border-collapse: collapse;
font-size: 0.75rem;
}

.chat-markdown table {
min-width: max-content;
overflow-wrap: normal;
word-break: normal;
font-size: 0.75rem;
}

.chat-markdown th,
.chat-markdown td {
.chat-markdown td,
.markdown-table th,
.markdown-table td {
padding: 0.45rem 0.75rem;
}

.chat-markdown th,
.chat-markdown td,
.markdown-table td {
text-align: left;
}

.chat-markdown thead th {
.chat-markdown thead th,
.markdown-table thead th {
border-bottom: 1px solid color-mix(in srgb, var(--contrast-border) 60%, transparent);
padding-block: 0.55rem;
font-weight: 600;
white-space: nowrap;
}

.chat-markdown tbody td {
.chat-markdown tbody th,
.chat-markdown tbody td,
.markdown-table tbody th,
.markdown-table tbody td {
border-bottom: 1px solid color-mix(in srgb, var(--contrast-border) 60%, transparent);
}

Expand Down
16 changes: 16 additions & 0 deletions docs/user/file-previews.md
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.
4 changes: 4 additions & 0 deletions packages/client-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"types": "./src/markdownImages.ts",
"default": "./src/markdownImages.ts"
},
"./markdown-frontmatter": {
"types": "./src/markdownFrontmatter.ts",
"default": "./src/markdownFrontmatter.ts"
},
"./errors": {
"types": "./src/errors/index.ts",
"default": "./src/errors/index.ts"
Expand Down
Loading
Loading