From 75a1b349f7282e900f8134478e54cb079d031027 Mon Sep 17 00:00:00 2001 From: Maya Kid Date: Fri, 16 Oct 2026 04:38:25 +1100 Subject: [PATCH 1/7] Keep documentation fragment links in the current page. (#36) * Keep documentation fragment links in the current page. Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> * Handle repeated and malformed documentation fragments Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> --------- Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> Co-authored-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com> --- src/site/docs.tsx | 16 ++++++++++++---- src/site/pages.ts | 3 ++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/site/docs.tsx b/src/site/docs.tsx index 502d557..1bb3335 100644 --- a/src/site/docs.tsx +++ b/src/site/docs.tsx @@ -85,6 +85,7 @@ const inlineCode = "relative rounded-md bg-muted px-[0.3rem] py-[0.2rem] font-mo const linkClass = "font-medium underline underline-offset-4" function Prose({ children }: { children: string }) { + const { search } = useLocation() return ( { const target = resolveHref(href) return target.to ? ( - + {label} ) : ( @@ -358,14 +359,21 @@ function DocShell({ title, description, url, toc, children }: { title: string; d export function DocPage({ section }: { section: "docs" | "components" }) { const { slug = "introduction" } = useParams() - const { hash } = useLocation() + const { hash, key } = useLocation() const page = pages.find((entry) => entry.slug === slug && (section === "docs" ? entry.section === "docs" : entry.section !== "docs")) const manager = React.useState("pnpm") // A link to a heading lands on it once the page is there. React.useEffect(() => { - if (hash) document.getElementById(decodeURIComponent(hash.slice(1)))?.scrollIntoView() - }, [hash, page]) + if (!hash) return + let id = hash.slice(1) + try { + id = decodeURIComponent(id) + } catch { + // A malformed escape can still be a literal heading ID. + } + document.getElementById(id)?.scrollIntoView() + }, [hash, key, page]) if (!page) return return ( diff --git a/src/site/pages.ts b/src/site/pages.ts index 8ccb6c4..ea99da4 100644 --- a/src/site/pages.ts +++ b/src/site/pages.ts @@ -47,7 +47,8 @@ export const ordered: { title: string; url: string }[] = [ * other page of theirs stays theirs. */ export function resolveHref(href: string): { to?: string; href?: string } { - if (/^([a-z]+:|#)/i.test(href)) return { href } + if (href.startsWith("#")) return { to: href } + if (/^[a-z]+:/i.test(href)) return { href } const [path, hash = ""] = href.split("#") const component = path.match(/^\/docs\/components\/(?:base\/|radix\/)?([a-z0-9-]+)\/?$/)?.[1] const tail = hash ? `#${hash}` : "" From f808611a515dab5efe9c9b1108193ce3de533763 Mon Sep 17 00:00:00 2001 From: Maya Kid Date: Fri, 16 Oct 2026 04:39:39 +1100 Subject: [PATCH 2/7] Preserve changed lines that resemble unified diff headers (#34) * Keep changed lines that resemble file headers in a diff Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> * Explain the file boundaries required for compact multi-file patches Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> --------- Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> Co-authored-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com> --- content/solid/diff-view.mdx | 4 ++++ src/components/ui/diff-view.tsx | 37 +++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/content/solid/diff-view.mdx b/content/solid/diff-view.mdx index 0f92a6c..4956563 100644 --- a/content/solid/diff-view.mdx +++ b/content/solid/diff-view.mdx @@ -27,3 +27,7 @@ import { DiffView } from "@/components/ui/diff-view" ```tsx ``` + +Pass a unified diff with complete hunk ranges. Compact or truncated hunks are +also supported; keep the `diff --git` separators when combining them across +multiple files so file headers can be distinguished from changed text. diff --git a/src/components/ui/diff-view.tsx b/src/components/ui/diff-view.tsx index 9f76a39..22ab52f 100644 --- a/src/components/ui/diff-view.tsx +++ b/src/components/ui/diff-view.tsx @@ -1,11 +1,39 @@ import { cn } from "@/lib/utils" +/** File headers and changed text can have the same prefix, even "+++ ". */ +function patchLines(patch: string): string[] { + const lines: string[] = [] + let inHunk = false + let oldLines = 0 + let newLines = 0 + for (const line of patch.replace(/\n$/, "").split("\n")) { + if (line.startsWith("diff --git ")) inHunk = false + const hunk = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/.exec(line) + if (hunk) { + oldLines = Number(hunk[1] ?? 1) + newLines = Number(hunk[2] ?? 1) + inHunk = oldLines > 0 || newLines > 0 + } else if (line.startsWith("@@")) { + // Also accept abbreviated hunks, as used in compact tool output. + inHunk = true + oldLines = newLines = Infinity + } else if (!inHunk && /^(---|\+\+\+) /.test(line)) { + continue + } else if (inHunk) { + if (line.startsWith("-") || line.startsWith(" ")) oldLines-- + if (line.startsWith("+") || line.startsWith(" ")) newLines-- + if (oldLines <= 0 && newLines <= 0) inHunk = false + } + lines.push(line) + } + return lines +} + /** Lines added and removed by a unified patch, headers aside. */ function patchStats(patch: string): { added: number; removed: number } { let added = 0 let removed = 0 - for (const line of patch.split("\n")) { - if (line.startsWith("+++") || line.startsWith("---")) continue + for (const line of patchLines(patch)) { if (line.startsWith("+")) added++ else if (line.startsWith("-")) removed++ } @@ -23,10 +51,7 @@ function DiffStats({ added, removed, className }: { added: number; removed: numb /** A patch as a diff: hunks with added and removed lines coloured, headers dropped. */ function DiffView({ patch, className }: { patch: string; className?: string }) { - const lines = patch - .replace(/\n$/, "") - .split("\n") - .filter((line) => !line.startsWith("+++") && !line.startsWith("---")) + const lines = patchLines(patch) return (
Date: Fri, 16 Oct 2026 04:40:59 +1100 Subject: [PATCH 3/7] Rewrite block source imports to match the displayed file tree (#32) * Match copied block imports to the file tree shown to readers. Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> * fix: omit preview metadata from copied block pages Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> --------- Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> Co-authored-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com> --- src/blocks/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/blocks/index.ts b/src/blocks/index.ts index 645c80c..f3cdfa9 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -58,7 +58,16 @@ function filesOf(name: string, own?: boolean) { path: "shared/overview.tsx", source: sources["/src/blocks/shared/overview.tsx"], }); - return files; + // Previews use the site's block folders; copied files live at the paths shown above. + return files.map((file) => ({ + ...file, + // Upstream preview metadata is not a supported Next App Router page export. + source: (file.path === "page.tsx" + ? file.source.replace(/^export const (?:iframeHeight|description) = [^\n]*\n\n?/gm, "") + : file.source) + .replaceAll(`@/blocks/${name}/`, "@/") + .replaceAll("@/blocks/shared/", "@/shared/"), + })); } const OVERVIEW = "@/blocks/shared/overview"; From e867a7a98208424e6abf8525e3409d8e387c6efb Mon Sep 17 00:00:00 2001 From: Maya Kid Date: Fri, 16 Oct 2026 04:42:18 +1100 Subject: [PATCH 4/7] Keep the draft visible when the composer changes width (#31) Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> Co-authored-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com> --- src/components/ui/composer.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/components/ui/composer.tsx b/src/components/ui/composer.tsx index 697e3ca..b6a3152 100644 --- a/src/components/ui/composer.tsx +++ b/src/components/ui/composer.tsx @@ -73,6 +73,20 @@ function Composer({ el.style.height = `${el.scrollHeight}px` }, []) React.useLayoutEffect(resize, [value, resize]) + React.useLayoutEffect(() => { + const el = textarea.current + if (!el || typeof ResizeObserver === "undefined") return + // Side panels and attachments change wrapping without changing the text. + // Observe width alone so writing the new height cannot feed back into it. + let width = -1 + const observer = new ResizeObserver(([entry]) => { + if (entry.contentRect.width === width) return + width = entry.contentRect.width + resize() + }) + observer.observe(el) + return () => observer.disconnect() + }, [resize]) const submit = () => { if (!ready) return From c60cb14f8e770a7c6208e151fa73345d43255a81 Mon Sep 17 00:00:00 2001 From: Maya Kid Date: Fri, 16 Oct 2026 04:43:25 +1100 Subject: [PATCH 5/7] Fix accessible labels for Composer inputs (#29) * Preserve accessible input names and expandable status button semantics Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> * Keep every turn status and message input named for assistive technology Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> * Keep the Composer labeling change independent of the client entrypoint split Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> --------- Signed-off-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> Co-authored-by: Maya Kid <29959320+Maya-Kid@users.noreply.github.com> Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com> --- content/solid/composer.mdx | 13 ++++++++++++- src/blocks/agent-workspace.tsx | 1 + src/components/ui/composer.tsx | 4 ++++ src/examples/composer-demo.tsx | 1 + src/examples/composer-running.tsx | 1 + 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/content/solid/composer.mdx b/content/solid/composer.mdx index 879f51a..86ea7fe 100644 --- a/content/solid/composer.mdx +++ b/content/solid/composer.mdx @@ -31,9 +31,19 @@ import { Composer } from "@/components/ui/composer" ```tsx const [text, setText] = React.useState("") - + ``` +`textareaProps` names and describes the input itself. It accepts `id`, `aria-label`, +`aria-labelledby`, and `aria-describedby`; use its `id` with an external label's +`htmlFor`. Other HTML attributes, including a top-level `id` or `aria-label`, +continue to apply to the outer wrapper. + ## While a turn runs The send mark has no block behind it in any state: its brightness says what it will do. With `running`, it is the quiet square that stops the turn, until something is typed, which is then sent behind it. @@ -52,6 +62,7 @@ The send mark has no block behind it in any state: its brightness says what it w | `onValueChange` | `(value: string) => void` | - | | `onSubmit` | `(value: string) => void` | - | | `onStop` | `() => void` | - | +| `textareaProps` | `Pick, "id" \| "aria-label" \| "aria-labelledby" \| "aria-describedby">` | - | | `running` | `boolean` | `false` | | `leading` | `ReactNode` | - | | `tags` | `ReactNode` | - | diff --git a/src/blocks/agent-workspace.tsx b/src/blocks/agent-workspace.tsx index 00da0d1..3466cee 100644 --- a/src/blocks/agent-workspace.tsx +++ b/src/blocks/agent-workspace.tsx @@ -564,6 +564,7 @@ export default function AgentWorkspace() {
void running?: boolean placeholder?: string + /** Names and describes the input; other HTML attributes belong to the wrapper. */ + textareaProps?: Pick, "id" | "aria-label" | "aria-labelledby" | "aria-describedby"> leading?: React.ReactNode tags?: React.ReactNode /** Stands above the line of text: pictures going with the message. */ @@ -115,6 +118,7 @@ function Composer({ mark is never left on a line of its own. */}