{
  "schemaVersion": "1.0.0",
  "package": "@ds-mo/ui",
  "packageVersion": "14.17.0",
  "kind": "patterns",
  "entries": [
    {
      "id": "pattern:agent-chat",
      "status": "experimental",
      "summary": "Compose provider-neutral agent turns from serializable or slotted response content, normalized tool lifecycle, pending human input, inline sources, and reader-respecting transcript movement.",
      "useWhen": [
        "A conversation presents rich agent output and may pause for structured participant input before the agent continues."
      ],
      "avoidWhen": [
        "The channel is person-to-person or the design-system layer would be expected to own models, provider schemas, transport, persistence, tool execution, approval consequences, or private reasoning."
      ],
      "components": [
        {
          "component": "component:ds-message-scroller",
          "role": "Positions new turn anchors, preserves reader-controlled live-edge behavior, and measures pending interaction with composer clearance.",
          "required": true
        },
        {
          "component": "component:ds-message",
          "role": "Renders participant turns with channel-neutral semantics and marks application-owned outgoing turn anchors.",
          "required": true
        },
        {
          "component": "component:ds-message-bubble",
          "role": "Renders user-sent prompts with the shared message baseline.",
          "required": true
        },
        {
          "component": "component:ds-agent-response",
          "role": "Renders ordered normalized parts or an application-composed ordered response body.",
          "required": true
        },
        {
          "component": "component:ds-agent-tool-call",
          "role": "Presents a normalized provider-neutral tool lifecycle and optional custom result or diagnostic detail.",
          "required": false
        },
        {
          "component": "component:ds-agent-questionnaire",
          "role": "Collects a temporary local answer draft above the composer and renders a quiet answered history record.",
          "required": false
        },
        {
          "component": "component:ds-agent-source-list",
          "role": "Presents stable deduplicated sources inline after discovery is complete.",
          "required": false
        },
        {
          "component": "component:ds-message-actions",
          "role": "Provides completed-response Copy and controlled feedback without owning response data or consequences.",
          "required": false
        },
        {
          "component": "component:ds-message-composer",
          "role": "Emits participant submission and stop intent while keeping visible recoverable submission errors adjacent to the draft.",
          "required": true
        }
      ],
      "stateOwnership": [
        "The application normalizes every provider-specific tool and human-input state before rendering, and owns transport, immutable response parts, stop, retry, persistence, generated titles, permissions, and consequences.",
        "Pending questionnaires render through the scroller interaction surface; the questionnaire owns only its current step and unsaved draft, while the application submits answers, resumes the agent, preserves failures, and replaces a successful interaction with an answered response record.",
        "Tool calls display lifecycle only. Custom summary, result, and detail content must remain user-safe; execution, approval, retry, and authorization stay application-owned.",
        "Choose response parts when ordered content must be serializable and registry-friendly. Choose composed mode when application-specific components must interleave with CompoMo primitives; the application owns that slotted content and its serialization.",
        "Applications append stable deduplicated sources once retrieval settles. Active source discovery is an activity or tool state rather than a changing source list.",
        "Applications mark only outgoing prompts that begin new turns as turn anchors. Saved transcript entry points may independently choose the last stored anchor.",
        "Composer error actions are application-owned. The component presents failure above the editable box as one attached, measured support unit, and recovery never discards the participant draft.",
        "Prompt suggestions and model selection are optional product composition. Suggestion selection normally fills and focuses the draft; model selection remains an application-owned tool-region control.",
        "Markdown parsing and safe node creation belong to the renderer; the renderer-neutral public prose stylesheet owns semantic-tree styling and text measure without owning response ordering or streaming state.",
        "TokoMo owns the semantic interface and code font-family tokens, CompoMo assigns text or diagnostic-code intent, and the application loads Inter and Fira Code assets once at its root; agent components never fetch font assets.",
        "Agent activity is concise user-safe progress and never reveals private model reasoning or chain-of-thought."
      ],
      "accessibility": [
        "Tool lifecycle, questionnaire progress and validation, composer failure, and streaming state remain meaningful as text without relying on motion or color.",
        "Applications move focus into a pending questionnaire only when context makes that shift appropriate; the component never steals focus on mount.",
        "Source links use descriptive external-link names, derived hostnames, and noninteractive rendering for malformed or unsafe schemes.",
        "Reader wheel, touch, keyboard, selection, and jump actions release live-edge following until Scroll to latest explicitly restores it."
      ],
      "responsiveBehavior": [
        "Let structured response content use the available lane while direct prose uses its readable measure, code and tables retain local overflow, questionnaire actions stack on narrow screens, and the combined interaction and composer stack remains fully measured."
      ],
      "implementations": {
        "customElements": {
          "status": "verified",
          "recipes": [
            {
              "id": "pending-questionnaire",
              "title": "Pending questionnaire above a conversation composer",
              "summary": "Mark the outgoing turn, normalize the paused tool state, place temporary human input in the interaction surface, and keep submission transport in application code.",
              "files": [
                {
                  "path": "agent-conversation.html",
                  "language": "html",
                  "content": "<ds-message-scroller messages-label=\"Agent conversation\" default-position=\"end\">\n  <ds-message message-id=\"turn-1\" direction=\"outgoing\" scroll-anchor>\n    <ds-message-bubble variant=\"user\">Prepare the maintenance follow-up.</ds-message-bubble>\n  </ds-message>\n  <ds-agent-response message-id=\"answer-1\" render-mode=\"composed\">\n    <ds-agent-tool-call name=\"work.prepare\" label=\"Choose the first follow-up\" state=\"waiting-for-user\"></ds-agent-tool-call>\n  </ds-agent-response>\n  <div slot=\"interaction\">\n    <ds-agent-questionnaire request-id=\"request-1\"></ds-agent-questionnaire>\n  </div>\n  <div slot=\"overlay\">\n    <ds-message-composer label=\"Message agent\"></ds-message-composer>\n  </div>\n</ds-message-scroller>"
                },
                {
                  "path": "agent-conversation.ts",
                  "language": "typescript",
                  "content": "const questionnaire = document.querySelector('ds-agent-questionnaire')!;\nquestionnaire.questions = [{\n  id: 'priority',\n  type: 'single',\n  question: 'Which issue should be first?',\n  required: true,\n  choices: [\n    { value: 'battery', label: 'Repeated battery failures' },\n    { value: 'tires', label: 'Overdue tire inspections' },\n  ],\n}];\nquestionnaire.addEventListener('dsAnswer', async event => {\n  questionnaire.status = 'submitting';\n  try {\n    await submitAgentAnswers(event.detail);\n    questionnaire.status = 'answered';\n  } catch {\n    questionnaire.status = 'error';\n    questionnaire.errorMessage = 'Your answers could not be sent. Try again.';\n  }\n});\n\ndeclare function submitAgentAnswers(detail: { requestId: string; answers: unknown[] }): Promise<void>;"
                }
              ],
              "notes": [
                "On successful transport, remove the pending interaction and append an answered questionnaire response part to durable history rather than leaving the interactive surface in place."
              ]
            }
          ],
          "references": [
            "src/wc/components/MessageScroller/AgentChatReview.stories.ts",
            "src/wc/components/MessageScroller/MessageScroller.stories.ts",
            "src/docs/AgentConversations.mdx"
          ]
        },
        "react": {
          "status": "planned",
          "references": [
            "docs/framework-integration.md"
          ]
        },
        "vue": {
          "status": "planned",
          "references": [
            "docs/framework-integration.md"
          ]
        },
        "angular": {
          "status": "planned",
          "references": [
            "docs/framework-integration.md"
          ]
        }
      },
      "references": [
        "agent/patterns/conversation/pattern.agent.json",
        "src/docs/AgentConversations.mdx",
        "src/wc/components/MessageScroller/AgentChatReview.stories.ts",
        "src/wc/components/AgentResponse/AgentResponse.tsx"
      ]
    },
    {
      "id": "pattern:application-shell",
      "status": "stable",
      "summary": "Standard authenticated application architecture: one managed ShellApp receives router-owned navigation and page data plus product-owned tool data and persistent slotted content.",
      "useWhen": [
        "Building a standard authenticated application or workspace with CompoMo.",
        "Navigation, page chrome, global tools, and routed content must adapt across desktop, tablet, and mobile without parallel implementations.",
        "Tool owners and routed content must preserve element identity while their responsive presentation changes."
      ],
      "avoidWhen": [
        "Building authentication, onboarding, public, marketing, print, error, or intentionally focused full-screen experiences.",
        "The content is already rendered inside a ShellApp.",
        "A specialized product has a verified reason to own every chrome component through composition=\"slotted\"."
      ],
      "components": [
        {
          "component": "component:ds-shell-app",
          "role": "Standard managed owner of responsive application chrome, global-surface state, focus restoration, and stable application content slots.",
          "required": true
        },
        {
          "component": "component:ds-panel-nav",
          "role": "Managed desktop and tablet primary-navigation presentation; remains public for advanced slotted shells.",
          "required": false
        },
        {
          "component": "component:ds-bar-nav",
          "role": "Managed desktop and tablet route-section presentation; remains public for advanced slotted shells.",
          "required": false
        },
        {
          "component": "component:ds-shell-tools",
          "role": "Managed responsive adapter that presents persistent product-owned tool views through PanelTools or the mobile stage.",
          "required": false
        },
        {
          "component": "component:ds-shell-page",
          "role": "Managed routed-page boundary with desktop BarTitle and mobile MobileHeader presentations.",
          "required": false
        },
        {
          "component": "component:ds-mobile-sheet-nav",
          "role": "Managed mobile browser for Dashboard and Settings destinations.",
          "required": false
        },
        {
          "component": "component:ds-mobile-bar-nav",
          "role": "Managed persistent mobile destinations for Menu, current area, Search, Inbox, Messages, Agents, and Help.",
          "required": false
        },
        {
          "component": "component:ds-banner",
          "role": "Optional application-owned global message placed in the single shell banner lane.",
          "required": false
        }
      ],
      "stateOwnership": [
        "Lab or the consuming application supplies navigation, pageChrome, and tools objects to managed ShellApp.",
        "The application router owns authorization, current URLs, history, route changes, and the routed content placed in ShellApp's default slot.",
        "ShellApp emits stable navigation and page intent events; it never mutates router history.",
        "ShellApp owns responsive mode, PanelNav presentation, tool open state, active tool, mobile Inbox grouping, Menu state, fullscreen presentation, focus restoration, and breakpoint state preservation.",
        "Tool products and routed content are instantiated once by the application in named tool-view and default slots; ShellApp changes presentation without recreating those owners.",
        "The application owns account menus, tool product data, page actions, and the consequences of every emitted intent event.",
        "The application owns viewport and theme-color metadata, the dynamic-viewport framework root, and containing document overscroll so body never becomes a second application scroller; ShellApp fills that owner and keeps its existing internal scrollers.",
        "The application resolves competing global messages to one Banner and owns its actions, controlled open state, orientation for the available shell capacity, dismissal persistence, and removal after exit motion.",
        "Use composition=\"slotted\" only when a team intentionally needs to assemble the public lower-level chrome components itself."
      ],
      "accessibility": [
        "Render exactly one managed ShellApp and one active routed main landmark.",
        "Provide visible labels and accessible names in navigation, pageChrome, and tool definitions before passing them to ShellApp.",
        "Handle ShellApp intent events without replacing focusable owners during responsive or fullscreen transitions.",
        "Keep the canonical Search and Help tools available and preserve Inbox unread state when Stacks and Activity are grouped on mobile; Messages remains directly reachable.",
        "Do not override browser keyboard chords when enabling shell shortcuts."
      ],
      "responsiveBehavior": [
        "ShellApp automatically resolves desktop at 1200px and wider, tablet from 768px through 1199px, and mobile below 768px.",
        "Desktop and tablet use PanelNav, BarNav, PanelTools through ShellTools, and ShellPage chrome.",
        "Mobile uses MobileSheetNav, MobileBarNav, MobileHeader, ShellTools mobile presentation, and the same routed content owner. MobileBarNav paints its primary surface through the bottom safe-area inset so light browser chrome and application chrome meet without a secondary-surface seam.",
        "Below 768px the application root owns a 100dvh fixed stage with a 100vh fallback while ShellApp continues to fill its owner with height 100%; JavaScript viewport measurement synchronizes only the chrome wash and does not own layout height.",
        "With viewport-fit=cover, ShellApp consumes the shared top safe-area inset once and MobileBarNav consumes the persistent bottom inset; nested routed, tool, sheet, and consumer headers do not duplicate either inset.",
        "An optional Banner spans the complete application above desktop navigation, workspace, tools, and mobile global surfaces while remaining below the operating-system top safe area.",
        "Switching breakpoints changes presentation and inert state without recreating route or tool elements.",
        "Messages uses its own mobile destination; Stacks and Activity use explicit mobile Inbox grouping metadata."
      ],
      "implementations": {
        "customElements": {
          "status": "verified",
          "recipes": [
            {
              "id": "managed-shell",
              "title": "Managed application shell",
              "summary": "Supply typed data and persistent application-owned content to one ShellApp.",
              "files": [
                {
                  "path": "app-shell.html",
                  "language": "html",
                  "content": "<ds-shell-app id=\"shell\">\n  <app-search slot=\"search-view\"></app-search>\n  <app-agents slot=\"agents-view\"></app-agents>\n  <app-messages slot=\"messages-view\"></app-messages>\n  <app-stacks slot=\"stacks-view\"></app-stacks>\n  <app-activity slot=\"activity-view\"></app-activity>\n  <app-help slot=\"help-view\"></app-help>\n  <app-route-content><!-- router-owned page --></app-route-content>\n</ds-shell-app>"
                },
                {
                  "path": "app-shell.ts",
                  "language": "typescript",
                  "content": "import '@ds-mo/ui/components/ds-shell-app.js'\nimport { PANEL_TOOLS_DEFAULT_ITEMS } from '@ds-mo/ui/shell'\nimport type { ShellNavigationConfig, ShellPageChromeConfig, ShellToolsConfig } from '@ds-mo/ui/shell'\n\nconst shell = document.querySelector<HTMLDsShellAppElement>('#shell')!\nconst navigation: ShellNavigationConfig = {\n  groups: [{ items: [{ id: 'overview', icon: 'Home', label: 'Overview', href: '/dashboard/overview' }] }],\n  dashboardGroups: [{ items: [{ id: 'overview', icon: 'Home', label: 'Overview', href: '/dashboard/overview' }] }],\n  currentUrl: location.pathname,\n  browseContext: 'dashboard',\n  routerMode: 'event',\n  userName: 'User Name',\n  userInitial: 'U',\n}\nconst pageChrome: ShellPageChromeConfig = {\n  heading: 'Overview',\n  tabs: [{ id: 'overview', label: 'Overview', href: '/dashboard/overview' }],\n  value: 'overview',\n  currentUrl: location.pathname,\n}\nconst tools: ShellToolsConfig = { items: PANEL_TOOLS_DEFAULT_ITEMS }\nshell.navigation = navigation\nshell.pageChrome = pageChrome\nshell.tools = tools\nshell.addEventListener('dsNavSelect', event => appRouter.navigate(event.detail))\nshell.addEventListener('dsTabChange', event => appRouter.navigate(event.detail))"
                }
              ],
              "notes": [
                "Update navigation and pageChrome together after router navigation.",
                "Keep every tool owner and the router-owned page mounted in its slot."
              ]
            }
          ],
          "references": [
            "src/wc/components/ShellApp/ShellApp.stories.ts",
            "docs/framework-integration.md"
          ]
        },
        "react": {
          "status": "verified",
          "recipes": [
            {
              "id": "managed-shell",
              "title": "Managed React application shell",
              "summary": "Use the generated ShellApp wrapper with memoized router-owned configuration and stable children.",
              "files": [
                {
                  "path": "AppShell.tsx",
                  "language": "tsx",
                  "content": "import { useMemo } from 'react'\nimport { DsShellApp } from '@ds-mo/ui/react'\nimport { PANEL_TOOLS_DEFAULT_ITEMS } from '@ds-mo/ui/shell'\nimport type { ShellNavigationConfig, ShellPageChromeConfig } from '@ds-mo/ui/shell'\n\nexport function AppShell({ route, navigate, children }) {\n  const navigation = useMemo<ShellNavigationConfig>(() => ({ groups: route.groups, dashboardGroups: route.dashboardGroups, settingsGroups: route.settingsGroups, currentUrl: route.url, browseContext: route.context, routerMode: 'event', userName: route.user.name, userInitial: route.user.initial }), [route])\n  const pageChrome = useMemo<ShellPageChromeConfig>(() => ({ heading: route.heading, tabs: route.sections, value: route.section, currentUrl: route.url }), [route])\n  return <DsShellApp navigation={navigation} pageChrome={pageChrome} tools={{ items: PANEL_TOOLS_DEFAULT_ITEMS }} onDsNavSelect={event => navigate(event.detail)} onDsTabChange={event => navigate(event.detail)}>\n    <SearchProduct slot=\"search-view\" />\n    <AgentsProduct slot=\"agents-view\" />\n    <InboxProduct slot=\"messages-view\" />\n    <HelpProduct slot=\"help-view\" />\n    {children}\n  </DsShellApp>\n}"
                }
              ],
              "notes": [
                "Memoize structured configuration so unrelated product renders do not produce artificial shell input changes.",
                "Do not conditionally recreate routed or tool children when responsive mode changes."
              ]
            }
          ],
          "references": [
            "src/wc/components/ShellApp/ShellApp.mdx",
            "docs/framework-integration.md"
          ]
        },
        "vue": {
          "status": "planned",
          "references": [
            "docs/framework-integration.md"
          ]
        },
        "angular": {
          "status": "verified",
          "recipes": [
            {
              "id": "managed-shell",
              "title": "Managed Angular application shell",
              "summary": "Bind router-derived object properties to the generated standalone ShellApp adapter.",
              "files": [
                {
                  "path": "app-shell.component.html",
                  "language": "html",
                  "content": "<ds-shell-app [navigation]=\"navigation\" [pageChrome]=\"pageChrome\" [tools]=\"tools\" (dsNavSelect)=\"navigate($event.detail)\" (dsTabChange)=\"navigate($event.detail)\">\n  <app-search slot=\"search-view\" />\n  <app-agents slot=\"agents-view\" />\n  <app-messages slot=\"messages-view\" />\n  <app-help slot=\"help-view\" />\n  <router-outlet />\n</ds-shell-app>"
                },
                {
                  "path": "app-shell.component.ts",
                  "language": "typescript",
                  "content": "import { Component } from '@angular/core'\nimport { Router, RouterOutlet } from '@angular/router'\nimport { DsShellApp } from '@ds-mo/ui/angular/ds-shell-app'\nimport { PANEL_TOOLS_DEFAULT_ITEMS } from '@ds-mo/ui/shell'\nimport type { ShellNavigationConfig, ShellPageChromeConfig, ShellToolsConfig } from '@ds-mo/ui/shell'\n\n@Component({ selector: 'app-shell', standalone: true, imports: [DsShellApp, RouterOutlet], templateUrl: './app-shell.component.html' })\nexport class AppShellComponent {\n  navigation: ShellNavigationConfig = { groups: [], dashboardGroups: [], settingsGroups: [], routerMode: 'event' }\n  pageChrome: ShellPageChromeConfig = { heading: '' }\n  tools: ShellToolsConfig = { items: PANEL_TOOLS_DEFAULT_ITEMS }\n  constructor(private router: Router) {}\n  navigate(target: string) { void this.router.navigateByUrl(target) }\n}"
                }
              ],
              "notes": [
                "Resolve and replace navigation and pageChrome on completed router navigation.",
                "The router outlet and tool components remain application-owned slots."
              ]
            }
          ],
          "references": [
            "scripts/verify-package-imports.mjs",
            "docs/framework-integration.md"
          ]
        }
      },
      "references": [
        "src/wc/components/ShellApp/ShellApp.mdx",
        "docs/framework-integration.md",
        "docs/migrations/v13.md"
      ]
    },
    {
      "id": "pattern:conversation",
      "status": "experimental",
      "summary": "Compose channel-neutral history, transcript, message surfaces, attachments, and composer behavior for reusable conversations.",
      "useWhen": [
        "A product needs threaded conversation UX that may serve agent or person-to-person channels."
      ],
      "avoidWhen": [
        "The experience is a static activity feed or form without conversational turns."
      ],
      "components": [
        {
          "component": "component:ds-conversation-list",
          "role": "Owns history scrolling, empty layout, and floating action placement.",
          "required": true
        },
        {
          "component": "component:ds-panel-tool-search",
          "role": "Collects a product-owned conversation history search query when the conversation is presented as a PanelTools product.",
          "required": false
        },
        {
          "component": "component:ds-message-scroller",
          "role": "Owns transcript position and live-edge following.",
          "required": true
        },
        {
          "component": "component:ds-message",
          "role": "Owns each turn's direction, grouping, and metadata.",
          "required": true
        },
        {
          "component": "component:ds-message-bubble",
          "role": "Owns plain user-sent and received person-to-person surfaces and their body-medium default independently of row semantics.",
          "required": true
        },
        {
          "component": "component:ds-message-actions",
          "role": "Provides optional message Copy and controlled response-feedback actions in the Message metadata region.",
          "required": false
        },
        {
          "component": "component:ds-typing-indicator",
          "role": "Presents transient participant typing status without representing it as persisted message content.",
          "required": false
        },
        {
          "component": "component:ds-message-composer",
          "role": "Emits draft, submit, and stop intent without transport ownership.",
          "required": true
        },
        {
          "component": "component:ds-attachment-list",
          "role": "Displays existing attachment metadata when present.",
          "required": false
        }
      ],
      "stateOwnership": [
        "The application owns conversations, messages, selection, drafts, persistence, transport, unread policy, and any history search query or matching policy. ConversationListItem presents every positive unreadCount as one dot aligned with the title row; unread titles are emphasized and primary while read titles use regular weight and secondary foreground.",
        "The application owns contextual row-menu commands such as pin, rename, members, and delete; ConversationListItem supplies the sibling actions slot and hover/focus/touch presentation.",
        "When history search is present, distinguish an empty history from a populated history with no matching conversations.",
        "Plain user-sent and received person-to-person copy uses MessageBubble's body-medium regular default; user bubbles use secondary foreground, and the application must not promote ordinary conversation turns to body-large.",
        "TypingIndicator owns transient participant typing status and is composed directly in an incoming Message; it is never approximated with a MessageBubble variant.",
        "MessageActions owns browser Copy behavior and controlled feedback intent. The application supplies copyable content and owns feedback persistence, submission, permissions, analytics, and consequences.",
        "MessageScroller owns reader-relative transcript position and follow behavior. Compose MessageComposer in its overlay slot with 8px padding on every side so transcript content moves behind the surface while measured bottom clearance keeps the latest turn readable.",
        "MessageComposer emits intent while the application controls its value and status. Its submit action defaults to brand for agent and person-to-person channels; select another submitIntent only for a different semantic consequence, never merely to identify AI content."
      ],
      "accessibility": [
        "Give history and transcript regions distinct localized names.",
        "Preserve message author, order, time, status, and delivery meaning in text.",
        "When metadata actions use hover visibility, fade the complete reserved metadata row, preserve keyboard access, and keep the row visible for touch input.",
        "Do not force the reader back to the live edge after deliberate transcript interaction."
      ],
      "responsiveBehavior": [
        "Let the containing product select drawer or full-width layout while keeping transcript measure readable and rich content horizontally scrollable.",
        "When history and transcript are simultaneously visible, establish one shared master-detail layout contract for pane width, divider, transcript measure, composer containment, and empty-region placement; do not independently recreate that geometry for each conversation channel."
      ],
      "implementations": {
        "customElements": {
          "status": "planned",
          "references": [
            "src/wc/components/MessageScroller/MessageScroller.stories.ts"
          ]
        },
        "react": {
          "status": "planned",
          "references": [
            "docs/framework-integration.md"
          ]
        },
        "vue": {
          "status": "planned",
          "references": [
            "docs/framework-integration.md"
          ]
        },
        "angular": {
          "status": "planned",
          "references": [
            "docs/framework-integration.md"
          ]
        }
      },
      "references": [
        "src/wc/components/ConversationList/ConversationList.stories.ts",
        "src/wc/components/MessageScroller/MessageScroller.stories.ts",
        "docs/layout-recipe-foundation.md"
      ]
    },
    {
      "id": "pattern:menu-trigger",
      "status": "stable",
      "summary": "Connect a button trigger to an anchored Menu for a compact command or view-selection popup without application-authored positioning.",
      "useWhen": [
        "A button should reveal a short contextual collection of commands.",
        "A compact button should show the current view or mode and let the user choose another one from a menu.",
        "A row, card, or toolbar needs an overflow or more-options menu."
      ],
      "avoidWhen": [
        "The choice is a submitted form value with a field label or validation; use Select.",
        "The available local views can remain visible; use TabGroup.",
        "The choices are primary or secondary application destinations; use the matching navigation component.",
        "The popup contains substantial content, confirmation, or a form; use Modal."
      ],
      "components": [
        {
          "component": "component:ds-button-unfilled",
          "role": "Provides the visible trigger, popup relationship attributes, and focus target. Carries both shapes, and is the only component that carries the icon-only overflow role.",
          "required": true
        },
        {
          "component": "component:ds-menu",
          "role": "Owns anchored placement, menu semantics, keyboard navigation, viewport clamping, selection, and close behavior.",
          "required": true
        },
        {
          "component": "component:ds-button-filled",
          "role": "Substitutes for the trigger when the menu belongs to the single primary action in a local decision area, using the label or icon-label shape only.",
          "required": false
        }
      ],
      "stateOwnership": [
        "The application owns the selected command or view value and derives the trigger label and each item's isSelected state from it.",
        "The application synchronizes one open boolean to the trigger's expanded prop and Menu.open.",
        "An open popup is a transient pressed state, not a selected one. The trigger holds only its pressed wash for the popup's rendered lifecycle from expanded alone, without changing foreground, so never also set isActive to represent an open menu.",
        "Menu owns popup placement and keyboard navigation; the application never calculates popup coordinates or applies positional CSS.",
        "On dsSelect from a command or single-selection row, update the selected value, close the Menu, and restore focus to the trigger.",
        "Switch rows and reorderable rows report dsSelect or dsReorder without closing; replace the items array from those events.",
        "On dsClose, synchronize the application open state to false."
      ],
      "accessibility": [
        "A button either has a menu or is a menu, and the trigger's variant selects between the two shapes.",
        "Has a menu: a label or icon-label action that opens a menu. Set hasMenu, which implies aria-haspopup of menu and renders the trailing chevron that communicates the popup. Both button primitives support this shape.",
        "Is a menu: an icon-only control. Set hasMenu, which adds no chevron, so the glyph must convey the menu alone. Use Ellipses for generic more-options or overflow. Use a specific icon when the menu has a named purpose, such as Table for Customize table. Only ButtonUnfilled carries this shape, because the role is chrome rather than a primary action.",
        "Set controls to the Menu id and expanded from the same open state passed to Menu.",
        "Give an icon-only trigger an accessible name that states the role, such as More options or Customize table, because no visible label or chevron carries it.",
        "Use haspopup directly instead of hasMenu for a non-menu popup such as a listbox or dialog; those cases get no chevron.",
        "Give Menu a concise menuLabel describing the available commands or views.",
        "Set Menu.initialFocusVisible only when the trigger MouseEvent detail is zero, indicating keyboard activation.",
        "Keep visible selected styling synchronized with the application's selected value; Menu exposes the selected command through aria-current.",
        "Return focus to the trigger after selection; Menu already restores it for Escape and outside dismissal."
      ],
      "responsiveBehavior": [
        "Keep Menu's default choice-cell anchor alignment unless deliberate custom frame geometry is required.",
        "Let Menu clamp to the viewport and avoid application-authored fixed or absolute positioning.",
        "Use a concise trigger label that can fit the owning layout without hiding the selected meaning."
      ],
      "implementations": {
        "customElements": {
          "status": "verified",
          "recipes": [
            {
              "id": "selected-view-menu",
              "title": "Selected view menu",
              "summary": "A ButtonUnfilled displays the active view and opens a Menu whose selected row updates that view.",
              "files": [
                {
                  "path": "view-menu.html",
                  "language": "html",
                  "content": "<ds-button-unfilled\n  id=\"view-menu-trigger\"\n  label=\"View · Overview\"\n  has-menu\n  controls=\"view-menu\"\n></ds-button-unfilled>\n<ds-menu\n  id=\"view-menu\"\n  anchor-id=\"view-menu-trigger\"\n  menu-label=\"Choose view\"\n></ds-menu>\n\n<!-- Overflow shape: Ellipses means more options, so no chevron is added. -->\n<ds-button-unfilled\n  id=\"row-menu-trigger\"\n  variant=\"icon\"\n  icon=\"Ellipses\"\n  aria-label=\"More options\"\n  has-menu\n  controls=\"row-menu\"\n></ds-button-unfilled>\n<ds-menu\n  id=\"row-menu\"\n  anchor-id=\"row-menu-trigger\"\n  menu-label=\"More options\"\n  align=\"end\"\n></ds-menu>\n\n<!-- Named menu: icon-label carries the visible name. -->\n<ds-button-unfilled\n  id=\"customize-menu-trigger\"\n  variant=\"icon-label\"\n  icon=\"Table\"\n  label=\"Customize\"\n  aria-label=\"Customize table\"\n  has-menu\n  controls=\"customize-menu\"\n></ds-button-unfilled>\n<ds-menu\n  id=\"customize-menu\"\n  anchor-id=\"customize-menu-trigger\"\n  menu-label=\"Customize table\"\n  align=\"end\"\n></ds-menu>"
                },
                {
                  "path": "view-menu.ts",
                  "language": "typescript",
                  "content": "import '@ds-mo/ui/components/ds-button-unfilled.js'\nimport '@ds-mo/ui/components/ds-menu.js'\nimport type { MenuItemData } from '@ds-mo/ui'\n\nconst choices: MenuItemData[] = [\n  { label: 'Overview', value: 'overview' },\n  { label: 'Map', value: 'map' },\n  { label: 'Timeline', value: 'timeline' },\n]\n\nconst trigger = document.querySelector<HTMLDsButtonUnfilledElement>('#view-menu-trigger')!\nconst menu = document.querySelector<HTMLDsMenuElement>('#view-menu')!\nlet selectedValue = 'overview'\n\nfunction render() {\n  const selected = choices.find(item => item.value === selectedValue) ?? choices[0]\n  trigger.label = `View · ${selected.label}`\n  menu.items = choices.map(item => ({ ...item, isSelected: item.value === selectedValue }))\n}\n\nfunction setOpen(open: boolean) {\n  trigger.expanded = open\n  menu.open = open\n}\n\ntrigger.addEventListener('dsClick', event => {\n  const mouseEvent = (event as CustomEvent<MouseEvent>).detail\n  menu.initialFocusVisible = mouseEvent.detail === 0\n  setOpen(!menu.open)\n})\nmenu.addEventListener('dsClose', () => setOpen(false))\nmenu.addEventListener('dsSelect', event => {\n  selectedValue = (event as CustomEvent<MenuItemData>).detail.value ?? selectedValue\n  render()\n  setOpen(false)\n  requestAnimationFrame(() => trigger.setFocus())\n})\n\nrender()"
                }
              ],
              "notes": [
                "Assign Menu.items as a JavaScript property; complex arrays cannot be expressed as an HTML attribute.",
                "Do not add positioning styles to either element."
              ]
            }
          ],
          "references": [
            "src/wc/components/Tag/Tag.stories.ts",
            "src/wc/components/Menu/Menu.stories.ts"
          ]
        },
        "react": {
          "status": "verified",
          "recipes": [
            {
              "id": "selected-view-menu",
              "title": "Selected view menu",
              "summary": "A controlled React composition using generated ButtonUnfilled and Menu wrappers.",
              "files": [
                {
                  "path": "ViewMenu.tsx",
                  "language": "tsx",
                  "content": "import { useState } from 'react'\nimport type { HTMLDsButtonUnfilledElement, MenuItemData } from '@ds-mo/ui'\nimport { DsButtonUnfilled, DsMenu } from '@ds-mo/ui/react'\n\nconst choices: MenuItemData[] = [\n  { label: 'Overview', value: 'overview' },\n  { label: 'Map', value: 'map' },\n  { label: 'Timeline', value: 'timeline' },\n]\n\nexport function ViewMenu() {\n  const [selectedValue, setSelectedValue] = useState('overview')\n  const [open, setOpen] = useState(false)\n  const [initialFocusVisible, setInitialFocusVisible] = useState(false)\n  const selected = choices.find(item => item.value === selectedValue) ?? choices[0]\n  const items = choices.map(item => ({ ...item, isSelected: item.value === selectedValue }))\n\n  function focusTrigger() {\n    requestAnimationFrame(() => {\n      const trigger = document.querySelector<HTMLDsButtonUnfilledElement>('#view-menu-trigger')\n      trigger?.setFocus()\n    })\n  }\n\n  return (\n    <>\n      <DsButtonUnfilled\n        id=\"view-menu-trigger\"\n        label={`View · ${selected.label}`}\n        hasMenu\n        controls=\"view-menu\"\n        expanded={open}\n        onDsClick={event => {\n          setInitialFocusVisible(event.detail.detail === 0)\n          setOpen(current => !current)\n        }}\n      />\n      <DsMenu\n        id=\"view-menu\"\n        anchorId=\"view-menu-trigger\"\n        menuLabel=\"Choose view\"\n        items={items}\n        open={open}\n        initialFocusVisible={initialFocusVisible}\n        onDsClose={() => setOpen(false)}\n        onDsSelect={event => {\n          setSelectedValue(event.detail.value ?? selectedValue)\n          setOpen(false)\n          focusTrigger()\n        }}\n      />\n    </>\n  )\n}"
                }
              ],
              "notes": [
                "Use one React state value for both the trigger expanded prop and Menu open prop.",
                "Keep the stable trigger and menu ids unique when rendering more than one composition."
              ]
            }
          ],
          "references": [
            "docs/framework-integration.md",
            "src/wc/components/Menu/Menu.stories.ts"
          ]
        },
        "vue": {
          "status": "verified",
          "recipes": [
            {
              "id": "selected-view-menu",
              "title": "Selected view menu",
              "summary": "A controlled Vue composition using generated ButtonUnfilled and Menu wrappers.",
              "files": [
                {
                  "path": "ViewMenu.vue",
                  "language": "vue",
                  "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport type { HTMLDsButtonUnfilledElement, MenuItemData } from '@ds-mo/ui'\nimport { DsButtonUnfilled, DsMenu } from '@ds-mo/ui/vue'\n\nconst choices: MenuItemData[] = [\n  { label: 'Overview', value: 'overview' },\n  { label: 'Map', value: 'map' },\n  { label: 'Timeline', value: 'timeline' },\n]\n\nconst selectedValue = ref('overview')\nconst open = ref(false)\nconst initialFocusVisible = ref(false)\nconst selected = computed(() => choices.find(item => item.value === selectedValue.value) ?? choices[0])\nconst items = computed(() => choices.map(item => ({ ...item, isSelected: item.value === selectedValue.value })))\n\nfunction focusTrigger() {\n  requestAnimationFrame(() => {\n    const trigger = document.querySelector<HTMLDsButtonUnfilledElement>('#view-menu-trigger')\n    trigger?.setFocus()\n  })\n}\n\nfunction toggleMenu(event: CustomEvent<MouseEvent>) {\n  initialFocusVisible.value = event.detail.detail === 0\n  open.value = !open.value\n}\n\nfunction selectView(event: CustomEvent<MenuItemData>) {\n  selectedValue.value = event.detail.value ?? selectedValue.value\n  open.value = false\n  focusTrigger()\n}\n</script>\n\n<template>\n  <DsButtonUnfilled\n    id=\"view-menu-trigger\"\n    :label=\"`View · ${selected.label}`\"\n    has-menu\n    controls=\"view-menu\"\n    :expanded=\"open\"\n    @ds-click=\"toggleMenu\"\n  />\n  <DsMenu\n    id=\"view-menu\"\n    anchor-id=\"view-menu-trigger\"\n    menu-label=\"Choose view\"\n    :items=\"items\"\n    :open=\"open\"\n    :initial-focus-visible=\"initialFocusVisible\"\n    @ds-close=\"open = false\"\n    @ds-select=\"selectView\"\n  />\n</template>"
                }
              ],
              "notes": [
                "Use one Vue ref for both the trigger expanded prop and Menu open prop.",
                "Keep the stable trigger and menu ids unique when rendering more than one composition."
              ]
            }
          ],
          "references": [
            "docs/framework-integration.md",
            "src/wc/components/Menu/Menu.stories.ts"
          ]
        },
        "angular": {
          "status": "verified",
          "recipes": [
            {
              "id": "selected-view-menu",
              "title": "Selected view menu",
              "summary": "A controlled Angular composition using generated standalone ButtonUnfilled and Menu adapters.",
              "files": [
                {
                  "path": "view-menu.component.html",
                  "language": "html",
                  "content": "<ds-button-unfilled\n  id=\"view-menu-trigger\"\n  [label]=\"'View · ' + selectedLabel\"\n  has-menu\n  controls=\"view-menu\"\n  [expanded]=\"open\"\n  (dsClick)=\"toggleMenu($event)\"\n></ds-button-unfilled>\n<ds-menu\n  id=\"view-menu\"\n  anchorId=\"view-menu-trigger\"\n  menuLabel=\"Choose view\"\n  [items]=\"menuItems\"\n  [open]=\"open\"\n  [initialFocusVisible]=\"initialFocusVisible\"\n  (dsClose)=\"open = false\"\n  (dsSelect)=\"selectView($event)\"\n></ds-menu>"
                },
                {
                  "path": "view-menu.component.ts",
                  "language": "typescript",
                  "content": "import { Component } from '@angular/core'\nimport type { HTMLDsButtonUnfilledElement, MenuItemData } from '@ds-mo/ui'\nimport { DsButtonUnfilled } from '@ds-mo/ui/angular/ds-button-unfilled'\nimport { DsMenu } from '@ds-mo/ui/angular/ds-menu'\n\nconst choices: MenuItemData[] = [\n  { label: 'Overview', value: 'overview' },\n  { label: 'Map', value: 'map' },\n  { label: 'Timeline', value: 'timeline' },\n]\n\n@Component({\n  selector: 'app-view-menu',\n  standalone: true,\n  imports: [DsButtonUnfilled, DsMenu],\n  templateUrl: './view-menu.component.html',\n})\nexport class ViewMenuComponent {\n  selectedValue = 'overview'\n  open = false\n  initialFocusVisible = false\n\n  get selectedLabel() {\n    return choices.find(item => item.value === this.selectedValue)?.label ?? choices[0].label\n  }\n\n  get menuItems() {\n    return choices.map(item => ({ ...item, isSelected: item.value === this.selectedValue }))\n  }\n\n  toggleMenu(event: CustomEvent<MouseEvent>) {\n    this.initialFocusVisible = event.detail.detail === 0\n    this.open = !this.open\n  }\n\n  selectView(event: CustomEvent<MenuItemData>) {\n    this.selectedValue = event.detail.value ?? this.selectedValue\n    this.open = false\n    requestAnimationFrame(() => {\n      const trigger = document.querySelector<HTMLDsButtonUnfilledElement>('#view-menu-trigger')\n      trigger?.setFocus()\n    })\n  }\n}"
                }
              ],
              "notes": [
                "Bind items as a property and import both generated standalone adapters.",
                "Keep the stable trigger and menu ids unique when rendering more than one composition."
              ]
            }
          ],
          "references": [
            "docs/framework-integration.md",
            "src/wc/components/Menu/Menu.stories.ts"
          ]
        }
      },
      "references": [
        "src/wc/components/Tag/Tag.stories.ts",
        "src/wc/components/Menu/Menu.agent.json",
        "docs/framework-integration.md"
      ]
    },
    {
      "id": "pattern:shell-page",
      "status": "stable",
      "summary": "Supply route-owned page chrome and content to managed ShellApp, which composes ShellPage, BarTitle, and MobileHeader without duplicate page state.",
      "useWhen": [
        "A standard authenticated route needs a shared heading, sections, depth, actions, sticky compaction, content inset, and page canvas surface.",
        "The routed page must keep one content owner while desktop and mobile header presentations differ."
      ],
      "avoidWhen": [
        "The experience does not use the standard authenticated ShellApp layout.",
        "The content belongs to a drawer, modal, card, or global tool pane.",
        "Another route owner already supplies the active ShellApp pageChrome."
      ],
      "components": [
        {
          "component": "component:ds-shell-app",
          "role": "Receives pageChrome and persistent routed content, then owns responsive page presentation.",
          "required": true
        },
        {
          "component": "component:ds-shell-page",
          "role": "Managed internal page boundary; remains public for advanced slotted ShellApp composition.",
          "required": false
        },
        {
          "component": "component:ds-bar-title",
          "role": "Managed desktop/tablet page-header presentation; remains public for advanced slotted composition.",
          "required": false
        },
        {
          "component": "component:ds-mobile-header",
          "role": "Managed mobile page-header presentation using the same route-owned pageChrome data.",
          "required": false
        }
      ],
      "stateOwnership": [
        "The router owns heading copy, selected section, depth, available actions, content inset, divider visibility, scroll-compaction policy, content surface, route data, and product outcomes.",
        "The application replaces pageChrome from completed route state and handles ShellApp page intent events.",
        "ShellApp owns ShellPage, BarTitle, MobileHeader, the content scroller, sticky presentation, and responsive switching.",
        "The default slot contains application-owned routed content and is not recreated for header compaction or breakpoint changes.",
        "Use a directly slotted ShellPage only inside composition=\"slotted\"."
      ],
      "accessibility": [
        "Keep exactly one active routed main landmark, supplied by the managed ShellPage.",
        "Provide one concise route heading and destination-specific Back and action labels through pageChrome.",
        "Keep route loading, error, empty, success, and validation communication in the default page-content slot.",
        "Handle section and Back intent through the router without replacing focused tool or page owners unnecessarily."
      ],
      "responsiveBehavior": [
        "ShellApp derives the desktop BarTitle and mobile MobileHeader from the same pageChrome object.",
        "Route-level sections remain pageChrome tabs; subordinate local views use subsections.",
        "Use contentInset none only for deliberately full-bleed products such as maps and canvases.",
        "Table pages can retain responsive inline and bottom gutters while tuning their roomy and compact block-start seams; set scrollCompaction false when their roomy title must stay pinned, and let shell capacity continue to compact it for tools or tablet layouts.",
        "Choose contentSurface independently: primary and secondary canvases both support default responsive gutters or full-bleed content.",
        "Desktop and tablet BarTitle presentations match a secondary page canvas across expanded, compact, and constrained variants; the separate mobile header remains primary.",
        "ShellPage keeps its scroll-compaction spacer on the selected content surface so the routed canvas remains continuous.",
        "The routed content owner remains mounted while header presentation compacts or changes breakpoint."
      ],
      "implementations": {
        "customElements": {
          "status": "verified",
          "recipes": [
            {
              "id": "managed-page-chrome",
              "title": "Managed routed page chrome",
              "summary": "Assign route state to ShellApp and leave page structure to the managed composition.",
              "files": [
                {
                  "path": "drivers-page.ts",
                  "language": "typescript",
                  "content": "import type { ShellPageChromeConfig } from '@ds-mo/ui/shell'\n\nconst shell = document.querySelector<HTMLDsShellAppElement>('ds-shell-app')!\nconst pageChrome: ShellPageChromeConfig = {\n  heading: 'Drivers',\n  tabs: [{ id: 'active', label: 'Active' }, { id: 'inactive', label: 'Inactive' }],\n  value: 'active',\n  primaryAction: { id: 'create', label: 'Create driver' },\n}\nshell.pageChrome = pageChrome\nshell.addEventListener('dsTabChange', event => appRouter.navigate(event.detail))\nshell.addEventListener('dsPageAction', event => driverActions.run(event.detail))"
                }
              ],
              "notes": [
                "Replace pageChrome after router navigation instead of maintaining duplicate local route selection.",
                "Render routed content in the managed ShellApp default slot."
              ]
            }
          ],
          "references": [
            "src/wc/components/ShellApp/ShellApp.stories.ts",
            "src/wc/components/ShellPage/ShellPage.stories.ts"
          ]
        },
        "react": {
          "status": "verified",
          "recipes": [
            {
              "id": "managed-page-chrome",
              "title": "React managed routed page",
              "summary": "Pass router-derived page chrome to the persistent ShellApp owner.",
              "files": [
                {
                  "path": "DriversRoute.tsx",
                  "language": "tsx",
                  "content": "import type { ShellPageChromeConfig } from '@ds-mo/ui/shell'\n\nexport const driversPageChrome: ShellPageChromeConfig = {\n  heading: 'Drivers',\n  tabs: [{ id: 'active', label: 'Active' }, { id: 'inactive', label: 'Inactive' }],\n  value: 'active',\n  primaryAction: { id: 'create', label: 'Create driver' },\n}\n\nexport function DriversRoute() {\n  return <section aria-label=\"Driver results\">Page content</section>\n}"
                }
              ],
              "notes": [
                "The persistent application layout supplies driversPageChrome to DsShellApp.",
                "Do not wrap the route in another ShellApp or ShellPage."
              ]
            }
          ],
          "references": [
            "src/wc/components/ShellApp/ShellApp.mdx",
            "docs/framework-integration.md"
          ]
        },
        "vue": {
          "status": "planned",
          "references": [
            "docs/framework-integration.md"
          ]
        },
        "angular": {
          "status": "verified",
          "recipes": [
            {
              "id": "managed-page-chrome",
              "title": "Angular managed routed page",
              "summary": "Expose route-owned pageChrome to the persistent shell layout.",
              "files": [
                {
                  "path": "drivers-page-chrome.ts",
                  "language": "typescript",
                  "content": "import type { ShellPageChromeConfig } from '@ds-mo/ui/shell'\n\nexport const DRIVERS_PAGE_CHROME: ShellPageChromeConfig = {\n  heading: 'Drivers',\n  tabs: [{ id: 'active', label: 'Active' }, { id: 'inactive', label: 'Inactive' }],\n  value: 'active',\n  primaryAction: { id: 'create', label: 'Create driver' },\n}"
                }
              ],
              "notes": [
                "The persistent Angular shell binds the resolved pageChrome object to DsShellApp.",
                "The routed component renders only product content."
              ]
            }
          ],
          "references": [
            "docs/framework-integration.md",
            "scripts/verify-package-imports.mjs"
          ]
        }
      },
      "references": [
        "src/wc/components/ShellPage/ShellPage.agent.json",
        "src/wc/components/BarTitle/BarTitle.agent.json",
        "src/wc/components/ShellApp/ShellApp.agent.json"
      ]
    }
  ]
}
