From 85986a2b875b67db21f881e92caf4736e2ecf535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Thu, 13 Aug 2026 18:46:12 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(docs):=20=EC=9D=B8=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=84=A0=ED=83=9D=20=EC=98=81=EC=97=AD?= =?UTF-8?q?=EA=B3=BC=20=EC=97=AC=EB=B0=B1=EC=9D=84=20=EC=A0=95=EB=A6=AC?= =?UTF-8?q?=ED=95=B4=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/app/global.css | 28 ++---- docs/components/changelog-entry-item.test.tsx | 42 +++++++++ docs/components/changelog-entry-item.tsx | 4 +- docs/components/inline-code.test.tsx | 86 +++++++++++++++++++ docs/components/inline-code.tsx | 37 ++++++++ docs/components/mdx-components.tsx | 3 + docs/components/table-of-contents.test.tsx | 54 ++++++++++++ docs/components/table-of-contents.tsx | 3 +- 8 files changed, 233 insertions(+), 24 deletions(-) create mode 100644 docs/components/changelog-entry-item.test.tsx create mode 100644 docs/components/inline-code.test.tsx create mode 100644 docs/components/inline-code.tsx create mode 100644 docs/components/table-of-contents.test.tsx diff --git a/docs/app/global.css b/docs/app/global.css index 5f70726f78..2408ff7e87 100644 --- a/docs/app/global.css +++ b/docs/app/global.css @@ -132,8 +132,8 @@ article .prose { pre, code, pre code { - user-select: text !important; - -webkit-user-select: text !important; + user-select: text; + -webkit-user-select: text; } /* Rebrand: SEED Callout은 MDX children(코드블록/Tabs/문단)을 인라인 description(span)으로 받는데, @@ -159,31 +159,17 @@ pre code { align-items: flex-start; } -/* Rebrand: brand-tinted text selection — the same lime highlight everywhere so light - and dark modes read as one brand. Lime is a light color, so on light surfaces the - inherited dark text sits on it fine (background only). Unlayered so it wins over - fumadocs/Tailwind. The colors live on :root so the search-result highlight can - reuse the same lime — and, as :root vars, they resolve inside the portaled search dialog - (where the surface-scoped selection selectors below never match). */ +/* Rebrand: brand-tinted text selection — the same dark-on-lime treatment everywhere so + light and dark surfaces read as one brand. Setting both colors also keeps links and + inline code from carrying their resting text color into the selected state. Unlayered + so it wins over fumadocs/Tailwind. The colors live on :root so the search-result + highlight can reuse the same lime, including inside the portaled search dialog. */ :root { --selection-bg: #ccffa3; --selection-fg: #212124; } ::selection { background-color: var(--selection-bg); -} - -/* Dark surfaces — docs dark mode + the landing's dark sections (both carry light text). - Keep the lime highlight but flip the SELECTED text dark, mirroring light mode's - dark-on-lime, so the lime stays legible instead of light-on-light. Landing dark - sections are enumerated by the section ids the landing already uses for scroll/tone - logic; hero/bento and the orange footer (dark text) fall through to the base above. */ -.dark ::selection, -#intro ::selection, -#values ::selection, -#showcase ::selection, -#blog ::selection { - background-color: var(--selection-bg); color: var(--selection-fg); } diff --git a/docs/components/changelog-entry-item.test.tsx b/docs/components/changelog-entry-item.test.tsx new file mode 100644 index 0000000000..528428bf45 --- /dev/null +++ b/docs/components/changelog-entry-item.test.tsx @@ -0,0 +1,42 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "bun:test"; +import type { ChangelogEntry } from "@/lib/parse-changelog"; +import { ChangelogEntryItem } from "./changelog-entry-item"; + +const entry: ChangelogEntry = { + commitRefs: [], + contentBlocks: [ + { + html: "

Use Dialog.

", + plainText: "Use Dialog.", + type: "markdown", + }, + ], + isDependencyOnly: false, + order: 0, + package: { + name: "@seed-design/react", + url: "https://github.com/daangn/seed-design", + version: "2.1.0", + }, + relatedPackages: [], +}; + +describe("ChangelogEntryItem", () => { + it("uses the shared inline code treatment for markdown backticks", () => { + const { getByText } = render(); + const code = getByText("Dialog"); + const prose = code.closest("div"); + + expect(code.tagName).toBe("CODE"); + expect(prose?.classList.contains("[&_code]:rounded-r1")).toBe(true); + expect(prose?.classList.contains("[&_code]:bg-bg-transparent-selected")).toBe(true); + expect(prose?.classList.contains("[&_code]:px-0")).toBe(true); + expect(prose?.classList.contains("[&_code]:py-x0_5")).toBe(true); + expect( + prose?.classList.contains( + "[&_code]:[box-shadow:2px_0_0_var(--seed-color-bg-transparent-selected),-2px_0_0_var(--seed-color-bg-transparent-selected)]", + ), + ).toBe(true); + }); +}); diff --git a/docs/components/changelog-entry-item.tsx b/docs/components/changelog-entry-item.tsx index 6d665cd106..8918f18d46 100644 --- a/docs/components/changelog-entry-item.tsx +++ b/docs/components/changelog-entry-item.tsx @@ -1,8 +1,8 @@ import type { ChangelogEntry } from "@/lib/parse-changelog"; import { SeedClientCodeBlock } from "@/components/codeblock/client-code-block"; +import { INLINE_CODE_DESCENDANT_CLASS_NAME } from "./inline-code"; -const PROSE_CLASS = - "prose prose-sm dark:prose-invert max-w-none min-w-0 break-words prose-p:my-0 prose-li:my-0.5 prose-a:text-fd-primary prose-code:text-fd-primary prose-code:bg-fd-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-xs [&>*:last-child]:mb-0 [&_a]:break-words [&_code]:break-words"; +const PROSE_CLASS = `prose prose-sm dark:prose-invert max-w-none min-w-0 break-words prose-p:my-0 prose-li:my-0.5 prose-a:text-fd-primary [&>*:last-child]:mb-0 [&_a]:break-words ${INLINE_CODE_DESCENDANT_CLASS_NAME}`; export function ChangelogEntryItem({ entry, diff --git a/docs/components/inline-code.test.tsx b/docs/components/inline-code.test.tsx new file mode 100644 index 0000000000..a25c59cf32 --- /dev/null +++ b/docs/components/inline-code.test.tsx @@ -0,0 +1,86 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it, mock } from "bun:test"; +import { InlineCode } from "./inline-code"; + +mock.module("server-only", () => ({})); +mock.module("@/app/env", () => ({ + env: { + figmaBypassCacheNodeIds: [], + figmaCacheDisabled: false, + figmaFileKey: undefined, + figmaPersonalAccessToken: undefined, + }, +})); + +function expectClassNames(element: HTMLElement, classNames: string[]) { + for (const className of classNames) { + expect(element.classList.contains(className)).toBe(true); + } +} + +describe("mdx inline code", () => { + it("adds a visual horizontal inset without interrupting text selection", () => { + const { getByText } = render( + @seed-design/react@2.1.0, + ); + const code = getByText("@seed-design/react@2.1.0"); + + expectClassNames(code, [ + "consumer-code", + "border-0", + "px-0", + "[box-shadow:2px_0_0_var(--seed-color-bg-transparent-selected),-2px_0_0_var(--seed-color-bg-transparent-selected)]", + ]); + }); + + it("uses the docs typographic marker treatment", () => { + const { getByText } = render(ResponsiveDialog); + const code = getByText("ResponsiveDialog"); + + expectClassNames(code, [ + "rounded-r1", + "bg-bg-transparent-selected", + "font-mono", + "font-medium", + "leading-[inherit]", + "wrap-anywhere", + ]); + }); + + it("keeps the marker compact when inline code wraps", () => { + const { getByText } = render(@seed-design/react@2.1.0); + const code = getByText("@seed-design/react@2.1.0"); + + expectClassNames(code, [ + "py-x0_5", + "text-[0.92em]", + "[box-decoration-break:clone]", + "[-webkit-box-decoration-break:clone]", + ]); + }); + + it("neutralizes the marker treatment inside a code block", () => { + const { getByText } = render( +
+        const dialog = true;
+      
, + ); + const code = getByText("const dialog = true;"); + + expectClassNames(code, [ + "[pre_&]:rounded-none", + "[pre_&]:bg-transparent", + "[pre_&]:p-0", + "[pre_&]:shadow-none", + "[pre_&]:text-[inherit]", + "[pre_&]:font-normal", + "[pre_&]:[overflow-wrap:normal]", + ]); + }); + + it("maps markdown backticks to the inline code treatment", async () => { + const { mdxComponents } = await import("./mdx-components"); + + expect(mdxComponents.code).toBe(InlineCode); + }); +}); diff --git a/docs/components/inline-code.tsx b/docs/components/inline-code.tsx new file mode 100644 index 0000000000..cd8ba051ef --- /dev/null +++ b/docs/components/inline-code.tsx @@ -0,0 +1,37 @@ +import clsx from "clsx"; +import { Children, cloneElement, isValidElement, type ComponentProps, type ReactNode } from "react"; + +export const INLINE_CODE_CLASS_NAME = + "rounded-r1 border-0 bg-bg-transparent-selected px-0 py-x0_5 font-mono text-[0.92em] font-medium leading-[inherit] wrap-anywhere [box-shadow:2px_0_0_var(--seed-color-bg-transparent-selected),-2px_0_0_var(--seed-color-bg-transparent-selected)] [box-decoration-break:clone] [-webkit-box-decoration-break:clone]"; + +// Changelog Markdown is sanitized HTML rather than React nodes, so it needs the same +// treatment expressed as descendant variants on its prose container. +export const INLINE_CODE_DESCENDANT_CLASS_NAME = + "[&_code]:rounded-r1 [&_code]:border-0 [&_code]:bg-bg-transparent-selected [&_code]:px-0 [&_code]:py-x0_5 [&_code]:font-mono [&_code]:text-[0.92em] [&_code]:font-medium [&_code]:leading-[inherit] [&_code]:wrap-anywhere [&_code]:[box-shadow:2px_0_0_var(--seed-color-bg-transparent-selected),-2px_0_0_var(--seed-color-bg-transparent-selected)] [&_code]:[box-decoration-break:clone] [&_code]:[-webkit-box-decoration-break:clone]"; + +export function InlineCode({ className, ...props }: ComponentProps<"code">) { + return ( + + ); +} + +export function renderInlineCode(node: ReactNode): ReactNode { + return Children.map(node, (child) => { + if (!isValidElement<{ children?: ReactNode }>(child)) return child; + + if (child.type === "code") { + return )} />; + } + + if (child.props.children === undefined) return child; + + return cloneElement(child, undefined, renderInlineCode(child.props.children)); + }); +} diff --git a/docs/components/mdx-components.tsx b/docs/components/mdx-components.tsx index fe3516da18..2055d9f5c6 100644 --- a/docs/components/mdx-components.tsx +++ b/docs/components/mdx-components.tsx @@ -41,6 +41,7 @@ import { DontImage } from "./guideline/dont-image"; import { Image } from "./guideline/image"; import { IconComponent, IconTerminal } from "./icons"; import { IconLibrary } from "./iconography/icon-library-lazy"; +import { InlineCode } from "./inline-code"; import { ColorMigrationIndex } from "./migration/color-migration-index"; import { IconographyMigrationIndex, V2Icon, V2IconColor, V3Icon } from "./migration/lazy"; import { TypographyMigrationIndex } from "./migration/typography-migration-index"; @@ -51,6 +52,8 @@ import { TableRoot } from "./table"; export const mdxComponents: MDXComponents = { ...defaultMdxComponents, + code: InlineCode, + // All fenced code blocks (```) render as the SEED "Codeblock" (Shiki colors preserved). // Inside a tabbed code card (`CodeBlockTabs`), SeedCodeBlockAuto renders the code bare. pre: ({ title, icon, children, ...props }: ComponentProps<"pre"> & { icon?: ReactNode }) => ( diff --git a/docs/components/table-of-contents.test.tsx b/docs/components/table-of-contents.test.tsx new file mode 100644 index 0000000000..3bec657018 --- /dev/null +++ b/docs/components/table-of-contents.test.tsx @@ -0,0 +1,54 @@ +import { render } from "@testing-library/react"; +import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import { TOCProvider } from "fumadocs-ui/components/toc"; +import { SeedTableOfContents } from "./table-of-contents"; + +class ResizeObserverStub implements ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +const originalResizeObserver = globalThis.ResizeObserver; + +beforeAll(() => { + globalThis.ResizeObserver = ResizeObserverStub; +}); + +afterAll(() => { + globalThis.ResizeObserver = originalResizeObserver; +}); + +describe("SeedTableOfContents", () => { + it("uses the shared inline code treatment in mixed-format headings", () => { + const { getByText } = render( + + onOpenChange Details + + ), + url: "#onopenchange-details", + }, + ]} + > + + , + ); + const code = getByText("onOpenChange"); + + expect(code.classList.contains("rounded-r1")).toBe(true); + expect(code.classList.contains("bg-bg-transparent-selected")).toBe(true); + expect(code.classList.contains("px-0")).toBe(true); + expect(code.classList.contains("py-x0_5")).toBe(true); + expect( + code.classList.contains( + "[box-shadow:2px_0_0_var(--seed-color-bg-transparent-selected),-2px_0_0_var(--seed-color-bg-transparent-selected)]", + ), + ).toBe(true); + expect(code.parentElement?.textContent).toBe("onOpenChange Details"); + }); +}); diff --git a/docs/components/table-of-contents.tsx b/docs/components/table-of-contents.tsx index 5d50a23cb1..79b098ce5c 100644 --- a/docs/components/table-of-contents.tsx +++ b/docs/components/table-of-contents.tsx @@ -13,6 +13,7 @@ import { type ReactNode, } from "react"; import { twMerge as cn } from "tailwind-merge"; +import { renderInlineCode } from "./inline-code"; import { buildTocTrackGeometry, getActiveTrackClip, @@ -196,7 +197,7 @@ function TocList({ list, onItemClick, variant }: TocListProps) { {item._step} ) : null} - {item.title} + {renderInlineCode(item.title)} ); })} From 87d4aeeeefaff624abb3c1d26f3f0e5979f2791b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=88=98?= Date: Thu, 13 Aug 2026 19:18:11 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test(docs):=20=EC=9D=B8=EB=9D=BC=EC=9D=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=B4=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/changelog-entry-item.test.tsx | 42 --------- docs/components/inline-code.test.tsx | 86 ------------------- docs/components/table-of-contents.test.tsx | 54 ------------ 3 files changed, 182 deletions(-) delete mode 100644 docs/components/changelog-entry-item.test.tsx delete mode 100644 docs/components/inline-code.test.tsx delete mode 100644 docs/components/table-of-contents.test.tsx diff --git a/docs/components/changelog-entry-item.test.tsx b/docs/components/changelog-entry-item.test.tsx deleted file mode 100644 index 528428bf45..0000000000 --- a/docs/components/changelog-entry-item.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it } from "bun:test"; -import type { ChangelogEntry } from "@/lib/parse-changelog"; -import { ChangelogEntryItem } from "./changelog-entry-item"; - -const entry: ChangelogEntry = { - commitRefs: [], - contentBlocks: [ - { - html: "

Use Dialog.

", - plainText: "Use Dialog.", - type: "markdown", - }, - ], - isDependencyOnly: false, - order: 0, - package: { - name: "@seed-design/react", - url: "https://github.com/daangn/seed-design", - version: "2.1.0", - }, - relatedPackages: [], -}; - -describe("ChangelogEntryItem", () => { - it("uses the shared inline code treatment for markdown backticks", () => { - const { getByText } = render(); - const code = getByText("Dialog"); - const prose = code.closest("div"); - - expect(code.tagName).toBe("CODE"); - expect(prose?.classList.contains("[&_code]:rounded-r1")).toBe(true); - expect(prose?.classList.contains("[&_code]:bg-bg-transparent-selected")).toBe(true); - expect(prose?.classList.contains("[&_code]:px-0")).toBe(true); - expect(prose?.classList.contains("[&_code]:py-x0_5")).toBe(true); - expect( - prose?.classList.contains( - "[&_code]:[box-shadow:2px_0_0_var(--seed-color-bg-transparent-selected),-2px_0_0_var(--seed-color-bg-transparent-selected)]", - ), - ).toBe(true); - }); -}); diff --git a/docs/components/inline-code.test.tsx b/docs/components/inline-code.test.tsx deleted file mode 100644 index a25c59cf32..0000000000 --- a/docs/components/inline-code.test.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it, mock } from "bun:test"; -import { InlineCode } from "./inline-code"; - -mock.module("server-only", () => ({})); -mock.module("@/app/env", () => ({ - env: { - figmaBypassCacheNodeIds: [], - figmaCacheDisabled: false, - figmaFileKey: undefined, - figmaPersonalAccessToken: undefined, - }, -})); - -function expectClassNames(element: HTMLElement, classNames: string[]) { - for (const className of classNames) { - expect(element.classList.contains(className)).toBe(true); - } -} - -describe("mdx inline code", () => { - it("adds a visual horizontal inset without interrupting text selection", () => { - const { getByText } = render( - @seed-design/react@2.1.0, - ); - const code = getByText("@seed-design/react@2.1.0"); - - expectClassNames(code, [ - "consumer-code", - "border-0", - "px-0", - "[box-shadow:2px_0_0_var(--seed-color-bg-transparent-selected),-2px_0_0_var(--seed-color-bg-transparent-selected)]", - ]); - }); - - it("uses the docs typographic marker treatment", () => { - const { getByText } = render(ResponsiveDialog); - const code = getByText("ResponsiveDialog"); - - expectClassNames(code, [ - "rounded-r1", - "bg-bg-transparent-selected", - "font-mono", - "font-medium", - "leading-[inherit]", - "wrap-anywhere", - ]); - }); - - it("keeps the marker compact when inline code wraps", () => { - const { getByText } = render(@seed-design/react@2.1.0); - const code = getByText("@seed-design/react@2.1.0"); - - expectClassNames(code, [ - "py-x0_5", - "text-[0.92em]", - "[box-decoration-break:clone]", - "[-webkit-box-decoration-break:clone]", - ]); - }); - - it("neutralizes the marker treatment inside a code block", () => { - const { getByText } = render( -
-        const dialog = true;
-      
, - ); - const code = getByText("const dialog = true;"); - - expectClassNames(code, [ - "[pre_&]:rounded-none", - "[pre_&]:bg-transparent", - "[pre_&]:p-0", - "[pre_&]:shadow-none", - "[pre_&]:text-[inherit]", - "[pre_&]:font-normal", - "[pre_&]:[overflow-wrap:normal]", - ]); - }); - - it("maps markdown backticks to the inline code treatment", async () => { - const { mdxComponents } = await import("./mdx-components"); - - expect(mdxComponents.code).toBe(InlineCode); - }); -}); diff --git a/docs/components/table-of-contents.test.tsx b/docs/components/table-of-contents.test.tsx deleted file mode 100644 index 3bec657018..0000000000 --- a/docs/components/table-of-contents.test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { render } from "@testing-library/react"; -import { afterAll, beforeAll, describe, expect, it } from "bun:test"; -import { TOCProvider } from "fumadocs-ui/components/toc"; -import { SeedTableOfContents } from "./table-of-contents"; - -class ResizeObserverStub implements ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -} - -const originalResizeObserver = globalThis.ResizeObserver; - -beforeAll(() => { - globalThis.ResizeObserver = ResizeObserverStub; -}); - -afterAll(() => { - globalThis.ResizeObserver = originalResizeObserver; -}); - -describe("SeedTableOfContents", () => { - it("uses the shared inline code treatment in mixed-format headings", () => { - const { getByText } = render( - - onOpenChange Details - - ), - url: "#onopenchange-details", - }, - ]} - > - - , - ); - const code = getByText("onOpenChange"); - - expect(code.classList.contains("rounded-r1")).toBe(true); - expect(code.classList.contains("bg-bg-transparent-selected")).toBe(true); - expect(code.classList.contains("px-0")).toBe(true); - expect(code.classList.contains("py-x0_5")).toBe(true); - expect( - code.classList.contains( - "[box-shadow:2px_0_0_var(--seed-color-bg-transparent-selected),-2px_0_0_var(--seed-color-bg-transparent-selected)]", - ), - ).toBe(true); - expect(code.parentElement?.textContent).toBe("onOpenChange Details"); - }); -});