Compare commits

..
9 changed files with 99 additions and 48 deletions
@@ -59,12 +59,12 @@ test("transitions a streaming shell from writing through command execution", asy
await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveCSS("height", "28px")
await expect(tool.locator('[data-component="tool-trigger"]')).toHaveCSS("gap", "6px")
await expect(title).toHaveCSS("font-size", "13px")
await expect(title).toHaveCSS("font-family", /^Inter,/)
await expect(title).toHaveCSS("font-family", "Inter, sans-serif")
await expect(title).toHaveCSS("font-weight", "530")
await expect(title).toHaveCSS("line-height", "16px")
await expect(title).toHaveCSS("color", "rgb(22, 22, 22)")
await expect(subtitle).toHaveCSS("font-size", "13px")
await expect(subtitle).toHaveCSS("font-family", /^Inter,/)
await expect(subtitle).toHaveCSS("font-family", "Inter, sans-serif")
await expect(subtitle).toHaveCSS("font-weight", "440")
await expect(subtitle).toHaveCSS("line-height", "16px")
await expect(subtitle).toHaveCSS("color", "rgb(92, 92, 92)")
-30
View File
@@ -1,30 +0,0 @@
import { describe, expect, test } from "bun:test"
import { monoDefault, monoFontFamily, sansDefault, sansFontFamily, terminalFontFamily } from "./model"
describe("settings font families", () => {
test("defaults normal text to Inter", () => {
expect(sansDefault).toBe("Inter")
expect(sansFontFamily(undefined)).toStartWith('"Inter", ')
expect(sansFontFamily("")).toStartWith('"Inter", ')
expect(sansFontFamily(" ")).toStartWith('"Inter", ')
})
test("keeps custom normal fonts ahead of the default", () => {
expect(sansFontFamily("Custom Sans")).toStartWith('"Custom Sans", "Inter", ')
})
test("defaults monospace text to IBM Plex Mono", () => {
expect(monoDefault).toBe("IBM Plex Mono")
expect(monoFontFamily(undefined)).toStartWith('"IBM Plex Mono", ')
expect(monoFontFamily("")).toStartWith('"IBM Plex Mono", ')
expect(monoFontFamily(" ")).toStartWith('"IBM Plex Mono", ')
})
test("keeps custom monospace fonts ahead of the default", () => {
expect(monoFontFamily("Custom Mono")).toStartWith('"Custom Mono", "IBM Plex Mono", ')
})
test("preserves the separate terminal font default", () => {
expect(terminalFontFamily(undefined)).toStartWith('"JetBrainsMono Nerd Font Mono", ')
})
})
+4 -4
View File
@@ -60,12 +60,12 @@ export interface Settings {
sounds: SoundSettings
}
export const monoDefault = "IBM Plex Mono"
export const sansDefault = "Inter"
export const monoDefault = "System Mono"
export const sansDefault = "System Sans"
export const terminalDefault = "JetBrainsMono Nerd Font Mono"
const monoFallback =
'"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
const sansFallback = '"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
const terminalFallback =
'"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
@@ -41,6 +41,32 @@ test("keeps completed block text compact", () => {
])
})
test("marks streamed inline code for animation", () => {
expect(parseMarkdownNodes("<p><code>file.ts:29-43</code></p>", true, true)).toEqual([
{
key: "0",
type: "element",
tag: "p",
attributes: {},
children: [
{
key: "0.0",
type: "element",
tag: "code",
attributes: {},
children: Array.from("file.ts:29-43", (text, index) => ({
key: `0.0.0:${index}`,
type: "word",
text,
animate: true,
})),
animate: true,
},
],
},
])
})
test("marks words for animation only when requested", () => {
expect(
parseMarkdownNodes("<p>Hello</p>", true).flatMap((node) => (node.type === "element" ? node.children : [node]))[0],
@@ -3,7 +3,14 @@ import { createStore, reconcile } from "solid-js/store"
import { Dynamic, render } from "solid-js/web"
type MarkdownNode =
| { key: string; type: "element"; tag: string; attributes: Record<string, string>; children: MarkdownNode[] }
| {
key: string
type: "element"
tag: string
attributes: Record<string, string>
children: MarkdownNode[]
animate?: true
}
| { key: string; type: "text"; text: string }
| { key: string; type: "word"; text: string; animate?: true }
@@ -38,8 +45,12 @@ function MarkdownDomNode(props: { node: MarkdownNode; animate: () => boolean })
</span>
)
}
let ref: HTMLElement | undefined
onMount(() => {
if (props.animate() && node.animate) ref?.setAttribute("data-markdown-enter", "")
})
return (
<Dynamic component={node.tag} {...node.attributes}>
<Dynamic component={node.tag} ref={ref} {...node.attributes}>
<For each={node.children}>{(node) => <MarkdownDomNode node={node} animate={props.animate} />}</For>
</Dynamic>
)
@@ -51,10 +62,10 @@ export function parseMarkdownNodes(html: string, words: boolean, animate = false
return Array.from(template.content.childNodes).flatMap((node, index) => parseNode(node, `${index}`, words, animate))
}
function parseNode(node: Node, key: string, words: boolean, animate: boolean): MarkdownNode[] {
function parseNode(node: Node, key: string, words: boolean, animate: boolean, inlineCode = false): MarkdownNode[] {
if (node instanceof Text) {
if (!words) return [{ key, type: "text", text: node.data }]
return node.data.split(/(\s+)/).flatMap((text, index): MarkdownNode[] => {
return (inlineCode ? Array.from(node.data) : node.data.split(/(\s+)/)).flatMap((text, index): MarkdownNode[] => {
if (!text) return []
if (/^\s+$/.test(text)) return [{ key: `${key}:${index}`, type: "text", text }]
return [{ key: `${key}:${index}`, type: "word", text, ...(animate ? { animate: true as const } : {}) }]
@@ -67,7 +78,18 @@ function parseNode(node: Node, key: string, words: boolean, animate: boolean): M
type: "element",
tag: node.tagName.toLowerCase(),
attributes: Object.fromEntries(Array.from(node.attributes).map((attribute) => [attribute.name, attribute.value])),
children: Array.from(node.childNodes).flatMap((child, index) => parseNode(child, `${key}.${index}`, words, animate)),
children: Array.from(node.childNodes).flatMap((child, index) =>
parseNode(
child,
`${key}.${index}`,
words,
animate,
inlineCode || (node.tagName === "CODE" && node.parentElement?.tagName !== "PRE"),
),
),
...(words && animate && node.tagName === "CODE" && node.parentElement?.tagName !== "PRE"
? { animate: true as const }
: {}),
},
]
}
@@ -20,7 +20,7 @@
display: inline;
}
[data-markdown-word][data-markdown-enter] {
:is([data-markdown-word], code)[data-markdown-enter] {
animation: markdown-word-enter 180ms ease-out both;
}
@@ -347,7 +347,7 @@
}
@media (prefers-reduced-motion: reduce) {
[data-component="markdown"] [data-markdown-word][data-markdown-enter] {
[data-component="markdown"] :is([data-markdown-word], code)[data-markdown-enter] {
animation: none;
}
}
@@ -52,3 +52,36 @@ export const StreamingResponse = {
</div>
),
}
function StreamingInlineCodeMarkdown() {
const chunks = [
"Updated ",
"`apps/cloud",
"flare/src/",
"editor/Cloud",
"Auth.ts:29",
"-43` and ",
"`packages/",
"session-ui/src/",
"components/markdown",
"-solid.tsx`, ",
"then verified ",
"the changes with ",
"`bun type",
"check` and ",
"`bun te",
"st`.",
]
const [count, setCount] = createSignal(1)
const timer = setInterval(() => setCount((value) => (value >= chunks.length ? 1 : value + 1)), 220)
onCleanup(() => clearInterval(timer))
return <Markdown text={chunks.slice(0, count()).join("")} streaming />
}
export const StreamingInlineCode = {
render: () => (
<div class="mx-auto max-w-[760px] rounded-lg border border-border-weak-base bg-background-base px-5 py-4">
<StreamingInlineCodeMarkdown />
</div>
),
}
+2 -2
View File
@@ -1,8 +1,8 @@
:root {
--font-family-sans: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-family-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-family-sans--font-feature-settings: normal;
--font-family-mono:
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-family-mono--font-feature-settings: normal;
--font-size-small: 13px;
+3 -3
View File
@@ -129,9 +129,9 @@
--v2-illustration-illustration-layer-02: var(--v2-grey-400);
--v2-illustration-illustration-layer-03: var(--v2-grey-500);
--font-family-text: var(--font-family-sans);
--v2-font-family-sans: var(--font-family-sans);
--v2-font-family-code: var(--font-family-mono);
--font-family-text: "Inter", sans-serif;
--v2-font-family-sans: "Inter", sans-serif;
--v2-font-family-code: "IBM Plex Mono", var(--font-family-mono);
--line-height-tight: 12px;
--line-height-compact: 16px;
--line-height-base: 20px;