Prose

Code Collapse

A collapsible code block wrapper, perfect for showing long code examples without overwhelming your documentation.

Overview

The ProseCodeCollapse component wraps code blocks and provides a toggle button to expand or collapse content. It's ideal for lengthy code examples that would take up too much vertical space, starting at a fixed height with a gradient fade effect.

Features
  • Clean Toggle - Simple expand/collapse functionality
  • Gradient Fade - Visual indicator when content is collapsed
  • Customizable Labels - Configure button text and labels
  • Accessible - Proper ARIA attributes for screen readers
  • Smart Defaults - Works great out of the box with sensible defaults

Basic Usage

Wrap any code block to make it collapsible:

ts

/app.config.ts

const repoBase = "https://github.com/BayBreezy/docd";

export default defineAppConfig({
  docd: {
    github: {
      repo: repoBase,
      branch: "main",
      contentDir: "docs/content",
    },
    ui: {
      borderType: "dashed",
      header: {
        title: "Docd",
        logo: {
          alt: "Docd Logo",
          light: "/logos/docd-logo-dark.svg",
          dark: "/logos/docd-logo-light.svg",
          favicon: "/favicon.svg",
        },
      },
      extraLinks: [
        { icon: "lucide:star", label: "Star on GitHub", external: true, href: repoBase },
        {
          icon: "lucide:bug",
          label: "Report an issue",
          external: true,
          href: `${repoBase}/issues/new?template=bug_report.yml`,
        },
        {
          icon: "lucide:lightbulb",
          label: "Feature request",
          external: true,
          href: `${repoBase}/issues/new?template=feature_request.yml`,
        },
        {
          icon: "lucide:coffee",
          label: "Buy me coffee",
          external: true,
          href: "https://buymeacoffee.com/llehXIrI8g",
        },
      ],
      transition: {
        name: "fade",
      },
    },
  },
});

Custom Labels

Customize the button text and name:

<script setup lang="ts">
  import { computed, ref } from "vue";
  import type { User } from "@/types";

  const users = ref<User[]>([]);
  const loading = ref(false);
  const error = ref<string | null>(null);

  const activeUsers = computed(() => users.value.filter((u) => u.role !== "guest"));

  const totalUsers = computed(() => users.value.length);

  async function fetchUsers() {
    loading.value = true;
    error.value = null;

    try {
      const response = await fetch("/api/users");
      if (!response.ok) throw new Error("Failed to fetch");
      users.value = await response.json();
    } catch (e) {
      error.value = e instanceof Error ? e.message : "Unknown error";
    } finally {
      loading.value = false;
    }
  }

  async function deleteUser(id: string) {
    try {
      await fetch(`/api/users/${id}`, { method: "DELETE" });
      users.value = users.value.filter((u) => u.id !== id);
    } catch (e) {
      console.error("Delete failed:", e);
    }
  }

  onMounted(() => {
    fetchUsers();
  });
</script>

<template>
  <div class="user-manager">
    <h2>Users ({{ totalUsers }})</h2>

    <div v-if="loading">Loading...</div>
    <div v-else-if="error" class="error">{{ error }}</div>
    <div v-else class="user-grid">
      <UserCard v-for="user in activeUsers" :key="user.id" :user="user" @delete="deleteUser" />
    </div>
  </div>
</template>

With Code Snippet

Combine with ::prose-code-snippet to show actual source files:

vue

BrowserFrame Component

<script setup lang="ts">
  defineProps<{
    /**
     * URL displayed in the address bar.
     * @example "https://example.com"
     */
    url?: string;
  }>();

  const emit = defineEmits<{
    /**
     * Emitted when the close (red) traffic light button is clicked.
     */
    close: [];
    /**
     * Emitted when the minimize (yellow) traffic light button is clicked.
     */
    minimize: [];
    /**
     * Emitted when the maximize (green) traffic light button is clicked.
     */
    maximize: [];
    /**
     * Emitted when the sidebar/panel toggle button is clicked.
     */
    panel: [];
    /**
     * Emitted when the back navigation button is clicked.
     */
    back: [];
    /**
     * Emitted when the forward navigation button is clicked.
     */
    forward: [];
    /**
     * Emitted when the page refresh button is clicked.
     */
    refresh: [];
    /**
     * Emitted when the download button is clicked.
     */
    download: [];
    /**
     * Emitted when the share button is clicked.
     */
    share: [];
    /**
     * Emitted when the new tab button is clicked.
     */
    newTab: [];
    /**
     * Emitted when the copy button is clicked.
     */
    copy: [];
  }>();

  defineSlots<{
    /**
     * Replaces the three traffic light buttons (close, minimize, maximize).
     */
    "traffic-lights": () => void;
    /**
     * Replaces the sidebar panel toggle and back/forward navigation buttons.
     */
    "nav-controls": () => void;
    /**
     * Replaces the address bar area.
     */
    "address-bar": () => void;
    /**
     * Replaces the right-side action buttons (download, share, new tab, copy).
     */
    actions: () => void;
    /**
     * Content rendered inside the browser frame body.
     */
    default: () => void;
  }>();
</script>

<template>
  <div class="w-full overflow-hidden rounded-xl border shadow-md">
    <div class="flex items-center gap-3 border-b px-3 py-2">
      <!-- Traffic lights -->
      <div class="flex shrink-0 items-center gap-1.5">
        <slot name="traffic-lights">
          <button
            type="button"
            aria-label="Close"
            class="size-3 rounded-full bg-red-500 transition-opacity hover:opacity-80"
            @click="emit('close')"
          />
          <button
            type="button"
            aria-label="Minimize"
            class="size-3 rounded-full bg-yellow-400 transition-opacity hover:opacity-80"
            @click="emit('minimize')"
          />
          <button
            type="button"
            aria-label="Maximize"
            class="size-3 rounded-full bg-green-500 transition-opacity hover:opacity-80"
            @click="emit('maximize')"
          />
        </slot>
      </div>

      <!-- Panel + back/forward -->
      <div class="flex shrink-0 items-center gap-0.5">
        <slot name="nav-controls">
          <button
            type="button"
            aria-label="Toggle sidebar"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('panel')"
          >
            <Icon name="lucide:panel-left" class="size-4" />
          </button>
          <button
            type="button"
            aria-label="Go back"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('back')"
          >
            <Icon name="lucide:chevron-left" class="size-4" />
          </button>
          <button
            type="button"
            aria-label="Go forward"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('forward')"
          >
            <Icon name="lucide:chevron-right" class="size-4" />
          </button>
        </slot>
      </div>

      <!-- Address bar -->
      <div class="flex min-w-0 flex-1 justify-center">
        <slot name="address-bar">
          <div
            class="flex w-full max-w-sm items-center gap-1.5 rounded-md border bg-background px-2.5 py-1 text-xs text-muted-foreground dark:border-muted/50 dark:bg-muted/50"
          >
            <Icon name="lucide:shield" class="size-3 shrink-0" />
            <span class="min-w-0 flex-1 truncate text-center">{{ url ?? "about:blank" }}</span>
            <button
              type="button"
              aria-label="Refresh"
              class="shrink-0 transition-opacity hover:opacity-70"
              @click="emit('refresh')"
            >
              <Icon name="lucide:rotate-cw" class="size-3" />
            </button>
          </div>
        </slot>
      </div>

      <!-- Right-side actions -->
      <div class="flex shrink-0 items-center gap-0.5">
        <slot name="actions">
          <button
            type="button"
            aria-label="Download"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('download')"
          >
            <Icon name="lucide:arrow-down-to-line" class="size-4" />
          </button>
          <button
            type="button"
            aria-label="Share"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('share')"
          >
            <Icon name="lucide:share" class="size-4" />
          </button>
          <button
            type="button"
            aria-label="New tab"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('newTab')"
          >
            <Icon name="lucide:plus" class="size-4" />
          </button>
          <button
            type="button"
            aria-label="Copy"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('copy')"
          >
            <Icon name="lucide:copy" class="size-4" />
          </button>
        </slot>
      </div>
    </div>

    <div>
      <slot mdc-unwrap="p" />
    </div>
  </div>
</template>

Custom Icon

Change the toggle icon:

vue

BrowserFrame Component

<script setup lang="ts">
  defineProps<{
    /**
     * URL displayed in the address bar.
     * @example "https://example.com"
     */
    url?: string;
  }>();

  const emit = defineEmits<{
    /**
     * Emitted when the close (red) traffic light button is clicked.
     */
    close: [];
    /**
     * Emitted when the minimize (yellow) traffic light button is clicked.
     */
    minimize: [];
    /**
     * Emitted when the maximize (green) traffic light button is clicked.
     */
    maximize: [];
    /**
     * Emitted when the sidebar/panel toggle button is clicked.
     */
    panel: [];
    /**
     * Emitted when the back navigation button is clicked.
     */
    back: [];
    /**
     * Emitted when the forward navigation button is clicked.
     */
    forward: [];
    /**
     * Emitted when the page refresh button is clicked.
     */
    refresh: [];
    /**
     * Emitted when the download button is clicked.
     */
    download: [];
    /**
     * Emitted when the share button is clicked.
     */
    share: [];
    /**
     * Emitted when the new tab button is clicked.
     */
    newTab: [];
    /**
     * Emitted when the copy button is clicked.
     */
    copy: [];
  }>();

  defineSlots<{
    /**
     * Replaces the three traffic light buttons (close, minimize, maximize).
     */
    "traffic-lights": () => void;
    /**
     * Replaces the sidebar panel toggle and back/forward navigation buttons.
     */
    "nav-controls": () => void;
    /**
     * Replaces the address bar area.
     */
    "address-bar": () => void;
    /**
     * Replaces the right-side action buttons (download, share, new tab, copy).
     */
    actions: () => void;
    /**
     * Content rendered inside the browser frame body.
     */
    default: () => void;
  }>();
</script>

<template>
  <div class="w-full overflow-hidden rounded-xl border shadow-md">
    <div class="flex items-center gap-3 border-b px-3 py-2">
      <!-- Traffic lights -->
      <div class="flex shrink-0 items-center gap-1.5">
        <slot name="traffic-lights">
          <button
            type="button"
            aria-label="Close"
            class="size-3 rounded-full bg-red-500 transition-opacity hover:opacity-80"
            @click="emit('close')"
          />
          <button
            type="button"
            aria-label="Minimize"
            class="size-3 rounded-full bg-yellow-400 transition-opacity hover:opacity-80"
            @click="emit('minimize')"
          />
          <button
            type="button"
            aria-label="Maximize"
            class="size-3 rounded-full bg-green-500 transition-opacity hover:opacity-80"
            @click="emit('maximize')"
          />
        </slot>
      </div>

      <!-- Panel + back/forward -->
      <div class="flex shrink-0 items-center gap-0.5">
        <slot name="nav-controls">
          <button
            type="button"
            aria-label="Toggle sidebar"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('panel')"
          >
            <Icon name="lucide:panel-left" class="size-4" />
          </button>
          <button
            type="button"
            aria-label="Go back"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('back')"
          >
            <Icon name="lucide:chevron-left" class="size-4" />
          </button>
          <button
            type="button"
            aria-label="Go forward"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('forward')"
          >
            <Icon name="lucide:chevron-right" class="size-4" />
          </button>
        </slot>
      </div>

      <!-- Address bar -->
      <div class="flex min-w-0 flex-1 justify-center">
        <slot name="address-bar">
          <div
            class="flex w-full max-w-sm items-center gap-1.5 rounded-md border bg-background px-2.5 py-1 text-xs text-muted-foreground dark:border-muted/50 dark:bg-muted/50"
          >
            <Icon name="lucide:shield" class="size-3 shrink-0" />
            <span class="min-w-0 flex-1 truncate text-center">{{ url ?? "about:blank" }}</span>
            <button
              type="button"
              aria-label="Refresh"
              class="shrink-0 transition-opacity hover:opacity-70"
              @click="emit('refresh')"
            >
              <Icon name="lucide:rotate-cw" class="size-3" />
            </button>
          </div>
        </slot>
      </div>

      <!-- Right-side actions -->
      <div class="flex shrink-0 items-center gap-0.5">
        <slot name="actions">
          <button
            type="button"
            aria-label="Download"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('download')"
          >
            <Icon name="lucide:arrow-down-to-line" class="size-4" />
          </button>
          <button
            type="button"
            aria-label="Share"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('share')"
          >
            <Icon name="lucide:share" class="size-4" />
          </button>
          <button
            type="button"
            aria-label="New tab"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('newTab')"
          >
            <Icon name="lucide:plus" class="size-4" />
          </button>
          <button
            type="button"
            aria-label="Copy"
            class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
            @click="emit('copy')"
          >
            <Icon name="lucide:copy" class="size-4" />
          </button>
        </slot>
      </div>
    </div>

    <div>
      <slot mdc-unwrap="p" />
    </div>
  </div>
</template>

Behavior

Initial State

  • Code starts collapsed at 200px height
  • Gradient fade overlay indicates more content below
  • Button shows: "{openText} {name}" (e.g., "Expand Code")

Expanded State

  • Code expands to full height (max 80vh)
  • Gradient fade removed
  • Button shows: "{closeText} {name}" (e.g., "Collapse Code")