diff --git a/.changeset/fix-tailwind-prefix-scanning.md b/.changeset/fix-tailwind-prefix-scanning.md new file mode 100644 index 00000000..dfb0c136 --- /dev/null +++ b/.changeset/fix-tailwind-prefix-scanning.md @@ -0,0 +1,18 @@ +--- +"streamdown": patch +--- + +fix(tailwind): export STREAMDOWN_CLASSES and getSourceInline() for Tailwind v4 prefix support + +When using Tailwind v4's `prefix()` option, the Tailwind scanner cannot match +unprefixed class names in streamdown's dist files to the prefixed utilities it +generates (e.g. it looks for `tw:flex` but dist files only contain `flex`). + +This patch adds a new `streamdown/tailwind` entry point that exports: +- `STREAMDOWN_CLASSES` – a readonly array of every Tailwind utility class used + by streamdown and its official plugins +- `getSourceInline(prefix?)` – returns a ready-to-paste Tailwind v4 + `@source inline(...)` directive with all classes, optionally prefixed + +Users can now generate the correct `@source inline()` directive for their +prefix and add it to their CSS file. diff --git a/packages/remend/__tests__/broken-markdown-variants.test.ts b/packages/remend/__tests__/broken-markdown-variants.test.ts index d5d02092..43d54a36 100644 --- a/packages/remend/__tests__/broken-markdown-variants.test.ts +++ b/packages/remend/__tests__/broken-markdown-variants.test.ts @@ -106,7 +106,9 @@ describe("multiple incomplete links", () => { it("should handle one complete and one incomplete link", () => { const result = remend("[first](url1) and [second"); - expect(result).toBe("[first](url1) and [second](streamdown:incomplete-link)"); + expect(result).toBe( + "[first](url1) and [second](streamdown:incomplete-link)" + ); }); it("should handle nested incomplete brackets", () => { @@ -250,9 +252,7 @@ describe("KaTeX with complex content", () => { }); it("should close inline katex when enabled", () => { - expect(remend("$x^2 + y^2", { inlineKatex: true })).toBe( - "$x^2 + y^2$" - ); + expect(remend("$x^2 + y^2", { inlineKatex: true })).toBe("$x^2 + y^2$"); }); it("should not treat currency as katex without inlineKatex", () => { @@ -493,9 +493,7 @@ describe("disabled handlers via options", () => { }); it("should close italic even when bold is disabled", () => { - expect(remend("**bold *italic", { bold: false })).toBe( - "**bold *italic*" - ); + expect(remend("**bold *italic", { bold: false })).toBe("**bold *italic*"); }); it("should not close anything when all are disabled", () => { @@ -512,9 +510,7 @@ describe("disabled handlers via options", () => { it("should not close bold when asterisk in content blocks pattern (italic disabled)", () => { // Bold pattern can't match when * appears in content; italic is disabled // So nothing closes - expect(remend("**bold *italic", { italic: false })).toBe( - "**bold *italic" - ); + expect(remend("**bold *italic", { italic: false })).toBe("**bold *italic"); }); it("should close strikethrough but not bold when bold is disabled", () => { @@ -598,8 +594,8 @@ describe("real-world AI streaming patterns", () => { }); it("should handle link with incomplete formatting after it", () => { - expect( - remend("[click here](https://example.com) for **more") - ).toBe("[click here](https://example.com) for **more**"); + expect(remend("[click here](https://example.com) for **more")).toBe( + "[click here](https://example.com) for **more**" + ); }); }); diff --git a/packages/streamdown/README.md b/packages/streamdown/README.md index 6727e2b2..75d909c9 100644 --- a/packages/streamdown/README.md +++ b/packages/streamdown/README.md @@ -103,6 +103,47 @@ If you are already using shadcn/ui, these variables are set up automatically. If You can also use the shadcn/ui [theme generator](https://ui.shadcn.com/themes) to create a custom palette and copy the generated CSS variables directly into your project. +### Tailwind v4 prefix + +If you use Tailwind v4's `prefix()` option (e.g. `prefix(tw)`), Tailwind's +scanner cannot match unprefixed class names from Streamdown's dist files to the +prefixed utilities it generates. Use Tailwind's `@source inline()` directive +to supply the correct prefixed class list instead. + +Streamdown exports a `getSourceInline` helper that generates the directive for +you. Run it once with a small build script and paste (or write) the output into +your CSS file: + +```js +// scripts/gen-streamdown-source.js +import { getSourceInline } from "streamdown/tailwind"; +// Also import helpers from installed plugins, e.g.: +// import { getSourceInline as codeSource } from "@streamdown/code/tailwind"; + +process.stdout.write(getSourceInline("tw") + " +"); +``` + +```sh +node scripts/gen-streamdown-source.js >> app/globals.css +``` + +Or copy the generated line directly into your CSS: + +```css +@import "tailwindcss" prefix(tw); + +/* Replace 'tw' with your actual prefix: */ +@source inline("tw:{absolute,animate-spin,appearance-none,...}"); +``` + +You can also import the raw class array and build your own tooling: + +```ts +import { STREAMDOWN_CLASSES } from "streamdown/tailwind"; +// string[] of every Tailwind class used by streamdown +``` + ## Usage Here's how you can use Streamdown in your React application with the AI SDK: diff --git a/packages/streamdown/lib/tailwind-classes.ts b/packages/streamdown/lib/tailwind-classes.ts new file mode 100644 index 00000000..cd7d8e85 --- /dev/null +++ b/packages/streamdown/lib/tailwind-classes.ts @@ -0,0 +1,218 @@ +/** + * Complete list of Tailwind CSS utility classes used by streamdown and its + * official plugins (@streamdown/code, @streamdown/math, @streamdown/mermaid, + * @streamdown/cjk). + * + * Use this list to configure Tailwind v4's `@source inline()` directive when + * you need a custom prefix. See the README for details. + */ +export const STREAMDOWN_CLASSES: readonly string[] = [ + "-mt-10", + "[&>p]:inline", + "[&_svg]:h-auto", + "[&_svg]:w-auto", + "[counter-increment:line_0]", + "[counter-reset:line]", + "[li_&]:pl-6", + "absolute", + "animate-spin", + "appearance-none", + "backdrop-blur-sm", + "before:[counter-increment:line]", + "before:content-[counter(line)]", + "before:font-mono", + "before:inline-block", + "before:mr-4", + "before:select-none", + "before:text-[13px]", + "before:text-muted-foreground/50", + "before:text-right", + "before:w-6", + "bg-[var(--sdm-bg,inherit]", + "bg-[var(--sdm-tbg)]", + "bg-background", + "bg-background/50", + "bg-background/80", + "bg-background/90", + "bg-background/95", + "bg-black/10", + "bg-muted", + "bg-muted/40", + "bg-muted/80", + "bg-primary", + "bg-red-100", + "bg-red-50", + "bg-sidebar", + "bg-sidebar/80", + "block", + "border", + "border-b", + "border-b-2", + "border-border", + "border-collapse", + "border-current", + "border-l-4", + "border-muted-foreground/30", + "border-red-200", + "border-sidebar", + "border-t", + "bottom-2", + "bottom-4", + "break-all", + "cursor-pointer", + "dark:bg-[var(--shiki-dark-bg,var(--sdm-bg,inherit)]", + "dark:bg-[var(--shiki-dark-bg,var(--sdm-tbg))]", + "dark:text-[var(--shiki-dark,var(--sdm-c,inherit))]", + "disabled:cursor-not-allowed", + "disabled:opacity-50", + "divide-border", + "divide-y", + "duration-150", + "duration-200", + "ease-out", + "fixed", + "flex", + "flex-1", + "flex-col", + "font-medium", + "font-mono", + "font-semibold", + "gap-1", + "gap-2", + "gap-4", + "group", + "group-hover:block", + "group-hover:opacity-100", + "h-4", + "h-8", + "h-[46px]", + "h-auto", + "h-full", + "hidden", + "hover:bg-background", + "hover:bg-muted", + "hover:bg-muted/40", + "hover:bg-primary/90", + "hover:text-foreground", + "inline-block", + "inset-0", + "italic", + "items-center", + "justify-between", + "justify-center", + "justify-end", + "left-2", + "left-4", + "list-decimal", + "list-disc", + "list-inside", + "lowercase", + "max-h-32", + "max-w-full", + "max-w-md", + "mb-2", + "min-h-28", + "min-h-[200px]", + "min-w-[120px]", + "ml-1", + "mt-1", + "mt-2", + "mt-6", + "mx-4", + "my-4", + "my-6", + "opacity-0", + "origin-center", + "overflow-hidden", + "overflow-x-auto", + "overflow-y-auto", + "overscroll-y-auto", + "p-1", + "p-1.5", + "p-2", + "p-3", + "p-4", + "p-6", + "pl-4", + "pointer-events-auto", + "pointer-events-none", + "px-1.5", + "px-3", + "px-4", + "py-0.5", + "py-1", + "py-2", + "relative", + "right-0", + "right-2", + "right-4", + "rounded", + "rounded-full", + "rounded-lg", + "rounded-md", + "rounded-xl", + "shadow-lg", + "shadow-sm", + "shrink-0", + "size-4", + "size-full", + "space-x-2", + "space-y-2", + "space-y-4", + "sticky", + "supports-[backdrop-filter]:backdrop-blur", + "supports-[backdrop-filter]:backdrop-blur-sm", + "supports-[backdrop-filter]:bg-background/70", + "supports-[backdrop-filter]:bg-sidebar/70", + "text-2xl", + "text-3xl", + "text-[var(--sdm-c,inherit)]", + "text-base", + "text-left", + "text-lg", + "text-muted-foreground", + "text-primary", + "text-primary-foreground", + "text-red-600", + "text-red-700", + "text-red-800", + "text-sm", + "text-xl", + "text-xs", + "top-2", + "top-4", + "top-full", + "transition-all", + "transition-colors", + "transition-transform", + "w-4", + "w-8", + "w-full", + "whitespace-normal", + "whitespace-nowrap", + "wrap-anywhere", + "z-10", + "z-50", +] as const; + +/** + * Generates a Tailwind v4 `@source inline()` directive containing all + * streamdown classes, optionally prefixed. + * + * @param prefix - Tailwind v4 prefix string (e.g. `"tw"` for `prefix(tw)`). + * Omit or pass `undefined` when you are not using a prefix. + * @returns A ready-to-paste CSS `@source inline(...)` rule string. + * + * @example + * // In a postcss.config.js build script, write this string to a CSS file: + * import { getSourceInline } from 'streamdown/tailwind'; + * console.log(getSourceInline('tw')); + * // → @source inline("tw:{absolute,animate-spin,...}"); + */ +export function getSourceInline(prefix?: string): string { + const names = [...STREAMDOWN_CLASSES]; + const body = prefix + ? `${prefix}:{${names.join(",")}}` + : `{${names.join(",")}}`; + return `@source inline("${body}");`; +} diff --git a/packages/streamdown/package.json b/packages/streamdown/package.json index c2a3a2db..4a125d8b 100644 --- a/packages/streamdown/package.json +++ b/packages/streamdown/package.json @@ -10,7 +10,11 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, - "./styles.css": "./styles.css" + "./styles.css": "./styles.css", + "./tailwind": { + "types": "./dist/tailwind-classes.d.ts", + "import": "./dist/tailwind-classes.js" + } }, "files": [ "dist", diff --git a/packages/streamdown/tsup.config.ts b/packages/streamdown/tsup.config.ts index c9ba4443..51c80505 100644 --- a/packages/streamdown/tsup.config.ts +++ b/packages/streamdown/tsup.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ dts: true, - entry: ["index.tsx"], + entry: ["index.tsx", "lib/tailwind-classes.ts"], format: ["esm"], minify: true, outDir: "dist",