Skip to content

feat(theme/llms): add configurable placement option for LLM UI buttons#3163

Merged
SoonIter merged 2 commits intoweb-infra-dev:mainfrom
Huxpro:Huxpro/llm-reader-button-ui
Feb 28, 2026
Merged

feat(theme/llms): add configurable placement option for LLM UI buttons#3163
SoonIter merged 2 commits intoweb-infra-dev:mainfrom
Huxpro:Huxpro/llm-reader-button-ui

Conversation

@Huxpro
Copy link
Copy Markdown
Contributor

@Huxpro Huxpro commented Feb 27, 2026

Summary

Add a placement option to llmsUI config that controls where LLM UI components (copy markdown, open in chat) are displayed:

  • 'title' (default): buttons below the H1 title, preserving the original behavior
  • 'outline': separate rows in the right-side outline panel, similar to "Scroll to top"

New outline components (LlmsCopyRow, LlmsOpenRow) are added with styling that matches existing outline bottom items. Documentation updated across all relevant pages (en/zh).

Related Issue

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).

- Add `placement` option to `llmsUI` config ('title' | 'outline')
- Create LlmsCopyRow and LlmsOpenRow components for outline display
- Original 'title' placement shows buttons below H1 (default)
- New 'outline' placement shows as separate rows in outline sidebar
- Refactor title.tsx to conditionally render based on placement
- Update Outline component to render LLM rows when placement='outline'
- Add styles for outline action rows with proper spacing (10px gap)
- Update documentation in config-theme.mdx, ssg-md.mdx, and plugin-llms.mdx (en/zh)
- Update e2e fixtures config for plugin-llms tests
Update API docs, guides, and plugin docs (en/zh) to document the new
placement configuration option. Add examples for outline placement.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new themeConfig.llmsUI.placement option to control whether LLMS UI actions render under the page title (default) or in the right-side outline panel, and updates docs accordingly.

Changes:

  • Extend LlmsUI theme config typing/docs with placement?: 'title' | 'outline'.
  • Add new outline-row UI components (LlmsCopyRow, LlmsOpenRow) and supporting outline styles.
  • Update EN/ZH documentation and adjust the plugin-llms e2e fixture config to use placement: 'outline'.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.

Show a summary per file
File Description
website/docs/zh/plugin/official-plugins/llms.mdx Documents optional outline placement for LLMS UI.
website/docs/zh/guide/basic/ssg-md.mdx Documents placement: 'outline' behavior for SSG-MD LLMS UI.
website/docs/zh/api/config/config-theme.mdx Adds placement to the LlmsUI config API docs (ZH).
website/docs/en/plugin/official-plugins/llms.mdx Documents optional outline placement for LLMS UI.
website/docs/en/guide/basic/ssg-md.mdx Documents placement: 'outline' behavior for SSG-MD LLMS UI.
website/docs/en/api/config/config-theme.mdx Adds placement to the LlmsUI config API docs (EN).
packages/shared/src/types/theme/index.ts Adds `placement?: 'title'
packages/core/src/theme/components/Outline/index.scss Adds styling hooks for outline “action rows” and dropdown positioning.
packages/core/src/theme/components/Outline/LlmsOpenRow.tsx New outline row dropdown for “Open in …” actions.
packages/core/src/theme/components/Outline/LlmsCopyRow.tsx New outline row action for copying Markdown content.
packages/core/src/theme/components/DocContent/docComponents/title.tsx Hides title-area LLMS UI when placement !== 'title'.
e2e/fixtures/plugin-llms/rspress.config.ts Sets fixture config to llmsUI.placement = 'outline'.
Comments suppressed due to low confidence (6)

packages/core/src/theme/components/Outline/LlmsOpenRow.tsx:116

  • LlmsOpenRow renders a custom dropdown but lacks basic accessibility wiring (e.g., aria-haspopup, aria-expanded, and keyboard support like closing on Escape / focusing the first item when opened). Since this is a new UI entry point in the outline sidebar, please add the relevant ARIA attributes and minimal keyboard handling to make it usable without a mouse.
      <button
        className="rp-outline__action-row"
        onClick={() => setIsOpen(!isOpen)}
      >
        <svg
          width="16"
          height="16"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
          <polyline points="15 3 21 3 21 9" />
          <line x1="10" y1="14" x2="21" y2="3" />
        </svg>
        <span>{t('openInText', { name: 'chat' })}</span>
      </button>
      {isOpen && (
        <div className="rp-llms-view-options__menu">
          {options.map(option => {

packages/core/src/theme/components/Outline/index.scss:80

  • .rp-outline__action-row is used by buttons/links (including a disabled loading state in LlmsCopyRow), but there is no :disabled styling. This means disabled rows still show a pointer cursor and hover color changes. Add a &:disabled rule (e.g., adjust cursor/opacity and suppress hover) to reflect the disabled state.
  &__action-row {
    cursor: pointer;
    font-size: 0.875rem;
    font-style: normal;
    font-weight: 400;
    line-height: 20px;
    color: var(--rp-c-text-2);
    background: transparent;
    display: flex;
    align-items: center;
    gap: 8px;
    border: none;
    padding: 0;
    text-decoration: none;
    transition: color 0.2s;

    &:hover {
      color: var(--rp-c-text-1);
    }

packages/core/src/theme/components/Outline/LlmsCopyRow.tsx:36

  • LlmsCopyRow duplicates the fetch+cache+clipboard logic from LlmsCopyButton (separate module-level cache, timer handling, etc.). To avoid divergence (e.g., different success timeout values and future bugfixes needing to be applied twice), consider extracting the shared copy-to-clipboard behavior into a small shared hook/utility used by both components.
const cache = new Map<string, string>();

export function LlmsCopyRow() {
  const t = useI18n();
  const { pathname } = useMdUrl();
  const [isLoading, setLoading] = useState(false);
  const [isFinished, setFinished] = useState(false);
  const timer = useRef<number | null>(null);

  const handleClick = useCallback(async () => {
    if (!pathname) return;
    setLoading(true);
    try {
      const content: string =
        cache.get(pathname) ?? (await fetch(pathname).then(res => res.text()));
      cache.set(pathname, content);
      await navigator.clipboard.writeText(content);
    } finally {
      setLoading(false);
      setFinished(true);
      if (timer.current) {
        clearTimeout(timer.current);
        timer.current = null;
      }
      timer.current = window.setTimeout(() => {
        setFinished(false);
        timer.current = null;
      }, 1500);
    }
  }, [pathname]);

e2e/fixtures/plugin-llms/rspress.config.ts:13

  • This fixture now sets llmsUI.placement: 'outline', but there is no e2e assertion covering the new placement behavior (e.g., buttons should appear in the outline panel and not under the H1). Please add/update a Playwright test in the plugin-llms fixture to exercise placement: 'outline' so regressions are caught.
  themeConfig: {
    llmsUI: { placement: 'outline' },
  },

packages/core/src/theme/components/DocContent/docComponents/title.tsx:27

  • placement: 'outline' disables rendering of the LLMS UI under the H1, but this PR doesn't add any corresponding render path in the outline sidebar (e.g., Outline still only renders ScrollToTop). As a result, enabling llmsUI.placement = 'outline' currently hides the UI entirely. Please wire the new LlmsCopyRow/LlmsOpenRow into the outline component (guarded by process.env.ENABLE_LLMS_UI and placement === 'outline').
  const { site } = useSite();
  const llmsUI = site?.themeConfig?.llmsUI;
  const placement =
    typeof llmsUI === 'object' ? (llmsUI?.placement ?? 'title') : 'title';

  return (
    <>
      <h1 className={clsx('rp-toc-include', className)} {...rest}>
        {children} <Tag tag={tag} />
      </h1>
      {process.env.ENABLE_LLMS_UI && placement === 'title' && (
        <LlmsContainer>
          <LlmsCopyButton />
          <LlmsViewOptions />
        </LlmsContainer>

packages/core/src/theme/components/Outline/LlmsOpenRow.tsx:113

  • The trigger label uses t('openInText', { name: 'chat' }), which will render as “Open in chat” / “在 chat 中打开” and is inconsistent with the menu items (“ChatGPT”, “Claude”). Consider using a more specific i18n key for the trigger (e.g., “Open in chat”) or pass a properly cased/display name (e.g., “Chat”).
          <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
          <polyline points="15 3 21 3 21 9" />
          <line x1="10" y1="14" x2="21" y2="3" />
        </svg>
        <span>{t('openInText', { name: 'chat' })}</span>
      </button>

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Huxpro
Copy link
Copy Markdown
Contributor Author

Huxpro commented Feb 27, 2026

image image

@SoonIter
Copy link
Copy Markdown
Member

SoonIter commented Feb 28, 2026

Thanks for the effort here, @Huxpro

The LLM UI buttons are intentionally placed near the title (and below the title on mobile / after the breadcrumb), following a common convention across documentation frameworks. Users have likely already developed muscle memory around this location, so adding an alternative placement may introduce confusion rather than convenience.

  • fumadocs(same as Rspress)

https://www.fumadocs.dev/docs/guides/rss

image
  • bun, claude (minitlify)

https://bun.com/docs/installation

https://platform.claude.com/docs/en/intro

image image
  • nuxt ui

https://ui.nuxt.com/docs/getting-started

image
  • nextjs

https://nextjs.org/docs/app/getting-started/project-structure

image

After looking into this, I think we'll keep the current behavior as-is for now. If you have a specific use case where the current placement falls short, it is more appropriate to configure through custom themes I think 🤔

@Huxpro
Copy link
Copy Markdown
Contributor Author

Huxpro commented Feb 28, 2026

@SoonIter

https://elements.ai-sdk.dev/ put it on outline and I want it on lynxjs.org:

image

I also made it configurable and default to "title" so it doesn't change the original behavior, right? It doesn't hurt to have more options

@SoonIter
Copy link
Copy Markdown
Member

@Huxpro
elements.ai-sdk.dev put it on outline and I want it on lynxjs.org:
I also made it configurable and default to "title" so it doesn't change the original behavior, right? It doesn't hurt to have more options

Get ❤, thanks

@SoonIter
Copy link
Copy Markdown
Member

SoonIter commented Feb 28, 2026

In addition to this PR, a fix will also be made. Rspress will optimize scrolling for overly long outlines like elements.al.

from (height: auto)

image

to (scrollable in one small container with fixed height)

image

I'll let agent to do this task

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants