> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Artifact Panel – Code

> ArtifactPanel renders a side panel within the Atlas full-screen chat for supplemental content surfaced by the agent.

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

<Tabs>
  <Tab title="Implementation">
    <LiveCode showCode example="ext-atlas-artifact-panel-demo" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import {
        ArtifactCard,
        ArtifactPanel,
        ArtifactPanelLayout,
      } from "@servicetitan/anvil2-ext-atlas";
      import { Flex, Text } from "@servicetitan/anvil2";
      import { useState } from "react";

      function App() {
        const [activeId, setActiveId] = useState<string | null>(null);
        return (
          <Flex direction="column" style={{ width: "1000px", height: "500px" }}>
            <ArtifactPanelLayout style={{ flex: 1, minHeight: 0 }}>
              <Flex
                direction="column"
                gap={3}
                style={{ flex: 1, padding: 16, minWidth: 0 }}
              >
                <Text>
                  I checked invoice 10492 against the service agreement and technician
                  notes. One thing to flag: labor came in 1.5 hours over the approved
                  estimate.
                </Text>
                <ArtifactCard
                  title="Insights for Invoice #10492"
                  description="Single-visit industrial install, closed under quote with a 40% margin."
                  artifactId="insight-1"
                  active={activeId === "insight-1"}
                  onClick={() => setActiveId("insight-1")}
                />
              </Flex>
              <ArtifactPanel
                isOpen={activeId !== null}
                onOpenChange={(open: boolean) => {
                  if (!open) setActiveId(null);
                }}
                position="right"
                title="Supplemental info"
                description="Variance details for invoice 10492"
                triggerKey={activeId ?? undefined}
              >
                Side-by-side details on the variance flagged in the chat.
              </ArtifactPanel>
            </ArtifactPanelLayout>
          </Flex>
        );
      }

      export default App;
      ```
    </LiveCode>

    ## Common Examples

    <Note>
      `ArtifactPanel` reads its responsive mode (inline vs. overlay) from the surrounding `ArtifactPanelLayout`. Wrap chat content and any artifact panels in `ArtifactPanelLayout` so the panel can switch modes when the available width changes. Outside a layout the panel falls back to inline mode.
    </Note>

    ### Basic Usage

    Wrap chat content and the panel in `ArtifactPanelLayout`. Open and close the panel from consumer state:

    ```tsx theme={null}
    import { useState } from "react";
    import {
      ArtifactPanel,
      ArtifactPanelLayout,
    } from "@servicetitan/anvil2-ext-atlas";

    function ArtifactDetails({ children }) {
      const [open, setOpen] = useState(false);
      return (
        <ArtifactPanelLayout>
          {children}
          <ArtifactPanel
            isOpen={open}
            onOpenChange={setOpen}
            title="Supplemental info"
          >
            {/* artifact content */}
          </ArtifactPanel>
        </ArtifactPanelLayout>
      );
    }
    ```

    ### Swapping Artifacts While Open

    Pass `triggerKey` so the panel treats a value change as a new open event — focus shifts into the panel for the new artifact, and close-restore returns focus to the most recent trigger:

    ```tsx theme={null}
    import { useState } from "react";
    import { ArtifactCard, ArtifactPanel } from "@servicetitan/anvil2-ext-atlas";

    function ChatWithArtifacts() {
      const [activeId, setActiveId] = useState<string | null>(null);
      return (
        <>
          <ArtifactCard
            title="Insight 1"
            description="..."
            artifactId="insight-1"
            active={activeId === "insight-1"}
            onClick={() => setActiveId("insight-1")}
          />
          <ArtifactCard
            title="Insight 2"
            description="..."
            artifactId="insight-2"
            active={activeId === "insight-2"}
            onClick={() => setActiveId("insight-2")}
          />
          <ArtifactPanel
            isOpen={activeId !== null}
            onOpenChange={(open) => {
              if (!open) setActiveId(null);
            }}
            title={`Insight ${activeId ?? ""}`}
            triggerKey={activeId ?? undefined}
          >
            {/* artifact content */}
          </ArtifactPanel>
        </>
      );
    }
    ```

    ### Agent-Initiated Open

    For panels opened by the agent rather than a user gesture, set `focusOnOpen` to `false` so focus is not pulled away from the chat composer:

    ```tsx theme={null}
    <ArtifactPanel
      isOpen={agentOpened}
      onOpenChange={setAgentOpened}
      title="Atlas opened this for you"
      focusOnOpen={false}
    >
      {/* artifact content */}
    </ArtifactPanel>
    ```

    ## React Accessibility

    * The panel renders an `<aside>` landmark with `aria-labelledby` tied to the title and `aria-describedby` tied to the description when present.
    * Focus moves into the panel on open (default behavior) and returns to the previously focused element on close.
    * Slide and fade animations respect `prefers-reduced-motion`.
    * The panel is not modal — chat content remains interactive while the panel is open. The panel does not handle Escape or click-outside; those belong to the parent chat.
    * When `ArtifactPanelLayout` switches the panel from inline to overlay mode at narrow widths, the chat content beneath remains in the document flow and reachable by assistive technology.
  </Tab>

  <Tab title="ArtifactPanel API">
    ```tsx theme={null}
    <ArtifactPanel
      isOpen={open}
      onOpenChange={setOpen}
      title="Supplemental info"
      description="Variance details"
      position="right"
      triggerKey={artifactId}
    >
      {content}
    </ArtifactPanel>
    ```

    ## `ArtifactPanel` Props

    <ParamField path="children" type="ReactNode" required>
      Content rendered in the panel body. Accepts arbitrary content including markdown, GenUI components, and other interactive elements.
    </ParamField>

    <ParamField path="isOpen" type="boolean" required>
      Whether the panel is currently rendered.
    </ParamField>

    <ParamField path="onOpenChange" type="(open: boolean) => void" required>
      Called when the panel requests an open-state change (e.g. when the close button is clicked).
    </ParamField>

    <ParamField path="title" type="string" required>
      Panel heading rendered in the header and referenced by `aria-labelledby`.
    </ParamField>

    <ParamField path="description" type="string">
      Optional sub-header text rendered beneath the title.
    </ParamField>

    <ParamField path="focusOnOpen" type="boolean" default="true">
      Move focus into the panel when it opens, and return focus to the previously focused element when it closes. Set false for agent-initiated opens so focus is not pulled away from the chat composer.
    </ParamField>

    <ParamField path="initialFocusResolver" type="(focusables: FocusableElement[]) => FocusableElement">
      Pick the initial focus target when `focusOnOpen` is true. Defaults to the first non-close focusable.
    </ParamField>

    <ParamField path="position" type={`"left" | "right"`} default="right">
      Side the panel is anchored to. Drives border placement.
    </ParamField>

    <ParamField path="triggerKey" type="string | number">
      Identifier for the current artifact. When this value changes while the panel is already open, focus management treats it as a new open event: it captures the newly-focused trigger and re-shifts focus into the panel, so close-restore returns to the right element.
    </ParamField>
  </Tab>
</Tabs>
