Agents Index

This file contains the concatenated content of all cards to help LLMs understand the available documentation.


Introduction to Deck (/cards/introduction.md)

Introduction to Deck

Deck turns a directory of Markdown into a scalable, zero-config component playground and documentation site.

It is designed to handle hundreds or even thousands of documentation files without a slow initial load time, making it ideal for large projects and component libraries.

Core Features


Getting Started (/cards/getting-started.md)

Getting Started

A Deck project is typically created as a sub-project to document a larger library or application. It's common to create the deck inside a subdirectory like /deck or /docs within your main project.

This guide assumes you are setting up a new deck in a subdirectory.

1. Install

npm install --save-dev @3sln/deck

That is the only dependency. Deck brings its own dev server and its own bundler.

2. Configure Your Project

In your package.json, add scripts and a @3sln/deck configuration block. You must provide a title for your documentation site.

{
  "name": "my-cool-project-docs",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "deck-dev",
    "build": "deck-build"
  },
  "devDependencies": {
    "@3sln/deck": "^0.0.14"
  },
  "@3sln/deck": {
    "title": "My Cool Project"
  }
}

3. Add Content

Create your documentation files using Markdown (.md). You can organize them in any directory structure you like.

my-project/
├── deck/  <-- Your Deck project lives here
│   ├── docs/
│   │   ├── introduction.md
│   │   └── components/
│   │       └── button.md
│   └── package.json
└── src/ < -- Your main project code

4. Run the Dev Server

Start the development server from within your deck subdirectory and you're ready to go!

cd deck
npm run dev

5. Publish

npm run build

deck-build writes a static site to out/, ready for any file host.

Using a Dev Server You Already Have

deck-dev is the zero-config option, but a deck can also run inside a dev server your project already uses. Each of these is a drop-in replacement for step 4.

Vite

// vite.config.js
import {defineConfig} from 'vite';
import deck from '@3sln/deck/vite-plugin';

export default defineConfig({plugins: [deck()]});

Deck uses Vite's own HMR socket here rather than opening a second one.

@web/dev-server

// web-dev-server.config.mjs
import deck from '@3sln/deck/wds-plugin';

export default {
  nodeResolve: true,
  plugins: [deck()],
};

webpack-dev-server / Rspack

// rspack.config.js — webpack.config.js is identical
import {setupMiddlewares} from '@3sln/deck/webpack-plugin';

export default {
  mode: 'development',
  entry: {},
  devServer: {
    static: {directory: import.meta.dirname},
    setupMiddlewares: setupMiddlewares({root: import.meta.dirname}),
  },
};

Anything Else

Deck's dev server is a plain connect/express middleware, so express, connect, polka and Node's own http all work:

import {createDeckMiddleware} from '@3sln/deck/dev-core';

app.use(createDeckMiddleware({root: process.cwd()}));

The Element (/cards/deck-demo.md)

The <deck-demo> Element

The <deck-demo> custom element is the heart of Deck's interactive documentation. It allows you to embed live, stateful, and hot-reloading component demos directly in your Markdown files.

To use it, you place the tag in your Markdown, give it an id, and point its src attribute to the demo script.

<deck-demo id="my-awesome-demo" src="/demos/my-awesome-demo.js"></deck-demo>

The Demo Script

The script referenced in src must have a default export that is a function. This function receives a driver object as its first argument.

// /demos/my-awesome-demo.js
export default driver => {
  // Your demo logic goes here
};

The Demo Driver API

The driver object is an API that allows your demo to interact with the <deck-demo> element's UI, which includes a source code viewer, property editor, and content panels.

driver.panel(name, renderFn)

Creates a tabbed panel for rendering content.

driver.property(name, {type, defaultValue})

Creates a reactive property control in the "Properties" panel. This allows users to interact with your demo. It returns an observable that provides the current value of the property input.

driver.signal

An AbortSignal that's aborted when the demo is being torn down.

Full Example

Here is a simple demo script that shows a message and lets the user control its text and color.

import {p, reconcile} from '@3sln/dodo';

export default driver => {
  const message$ = driver.property('Message', { type: 'text', value: 'Hello, Deck!' });

  driver.panel('Demo', (container, signal) => {
    const render = (message) => {
      reconcile(container, [
        p({ $styling: { color: currentProps.textColor } },
          message
        )
      ]);
    };

    const sub = message$.subscribe(message => {
      render(message);
    });

    signal.addEventListener('abort', () => {
      sub.unsubscribe();
      reconcile(container, []);
    });
  });
};

A Live Example

This card embeds the demo below with a single tag. Edit /demos/counter-demo.js while the dev server is running and it re-runs in place, keeping the properties you have set.

Demo Code (/demos/counter-demo.js)

import * as d from '@3sln/dodo';
import {cell, watch} from '@3sln/dodo/reactive';

/**
 * The demo shown on the `<deck-demo>` card.
 *
 * A demo module's default export is called with a `driver` and re-called with
 * the same argument whenever this file changes, so anything it sets up has to
 * be torn down through `driver.signal`.
 */
export default driver => {
  const step$ = driver.property('Step', {type: 'range', min: 1, max: 10, defaultValue: 1});
  const label$ = driver.property('Label', {type: 'text', defaultValue: 'Clicks'});

  driver.panel('Counter', (container, signal) => {
    const count = cell(0);
    let step = 1;
    let label = 'Clicks';

    const rerender = () => {
      d.reconcile(container, [
        watch(count, value =>
          d.div(
            {
              $styling: {
                display: 'flex',
                'align-items': 'center',
                gap: '1em',
                'font-family': 'system-ui, sans-serif',
                color: 'var(--text-color, #222)',
              },
            },
            d.button(
              {
                $styling: {
                  padding: '0.5em 1em',
                  'border-radius': '6px',
                  border: '1px solid #8884',
                  cursor: 'pointer',
                },
              },
              `+${step}`,
            ).on({click: () => count.update(n => n + step)}),
            d.span(`${label}: ${value}`),
          ),
        ),
      ]);
    };

    const subscriptions = [
      step$.subscribe(value => {
        step = Number(value) || 1;
        rerender();
      }),
      label$.subscribe(value => {
        label = value || 'Clicks';
        rerender();
      }),
    ];

    rerender();

    signal.addEventListener('abort', () => {
      subscriptions.forEach(subscription => subscription.unsubscribe());
      d.reconcile(container, null);
    });
  });
};

Configuration (/cards/configuration.md)

Configuration

Deck is configured through a @3sln/deck field in your project's package.json file.

The Override Model

Deck uses a simple override system for configuration. You can define a base configuration at the root of the @3sln/deck object. Then, you can create dev and build sub-objects to override any of those settings for a specific environment.

When Deck loads, it merges the root configuration with the environment-specific block. For example, when running the deck-build command, Deck will merge the root {...} options with the build: {...} options.

Configuration Options

Any of the following options can be placed at the root or within the dev and build blocks.

Example

Here is an example demonstrating the override system.

{
  "@3sln/deck": {
    "title": "My Awesome Project",
    "importMap": {
      "imports": {
        "my-lib": "/node_modules/my-lib/index.js"
      }
    },
    "dev": {
      "title": "My Awesome Project (DEV)"
    },
    "build": {
      "outDir": "dist/docs",
      "pick": {
        "../node_modules/my-lib/dist": "lib/my-lib"
      },
      "importMap": {
        "imports": {
          "my-lib": "/lib/my-lib/index.js"
        }
      }
    }
  }
}

Behavior

Files for Crawlers and Agents

A deck is a single-page application: every card lives behind a ?c= query parameter and its body arrives by fetch. Anything that does not run JavaScript sees one nearly empty page. So deck-build also writes the content down where it can be read directly:

The generated index.html also carries <link rel="alternate"> tags pointing at /agents.md and /llms.txt, so an agent that starts at the page still finds them.


Core Concepts (/cards/concepts.md)

Core Concepts

Deck's design is based on a few key architectural choices.

Client-Side Indexing

Deck stores documentation content in a client-side database (IndexedDB). The initial index.html file contains a manifest of all card paths along with a hash of their content.

Upon loading, the application compares this manifest with the data already stored in its IndexedDB, and fetches only the cards that are new or have changed. Nothing blocks on the whole deck arriving: the shell renders immediately, and cards fill in behind it.

Assets that are not cards, such as the JavaScript files for <deck-demo> elements, are not pre-fetched. They are fetched and cached by the service worker only when a user views them for the first time.

The Precompiled Search Index

A published deck ships a search index built at publish time. The browser downloads it before any card, which is what lets the very first search cover the whole deck rather than the fraction of it that has finished loading. Because the index carries each card's title and summary, results render immediately, whether or not their bodies have arrived.

A dev server has no precompiled index — the deck is being edited, and an index built a moment ago already describes the previous version. There, search is answered from the IndexedDB index of whatever has been loaded, which is the right trade when the content changes every few seconds.

Loading Priority

Which card arrives first matters more than how many arrive per second. A published deck queues every card at once and lets priority decide the order:

  1. the precompiled search index, before any card;
  2. the card named by ?c=, so a deep link opens immediately rather than after the rest of the deck has drained;
  3. cards matching the search currently on screen;
  4. everything else.

Steps 2–4 are priorities on one queue, not phases. A search typed while the backlog is still draining moves its matches to the front of that queue in place, so clicking a result opens it instead of showing a spinner.

The <deck-demo> Element

The <deck-demo> custom element is used to embed live demos in Markdown files. Its src attribute points to a JavaScript file.

The script is executed within the element and is provided with a driver API. This API allows the demo to render content and manage its own state, keeping it isolated from the main Deck application and other demos.

In development the dev server serves two generated modules per demo: one that re-runs the demo when its file changes, and one that carries its source text for the Source panel. In a published deck the demo is bundled ahead of time, and the Source panel still shows the original file rather than the bundle.

Dev Servers and the Build

Deck has two modes of operation:

  1. Development: deck-dev serves a deck with no bundler and no config. Plugins for Vite (@3sln/deck/vite-plugin), @web/dev-server (@3sln/deck/wds-plugin) and webpack-dev-server / Rspack (@3sln/deck/webpack-plugin) do the same job inside a dev server you already run — and the middleware behind them is plain connect, so any Node server can host a deck. Deck's own client app is pre-bundled either way, so the host runtime is never asked to resolve Deck's own dependencies.

  2. Production (deck-build): Creates a production-ready static site. It bundles the Deck application and every demo module with esbuild, copies the project's documentation files, precompiles the search index, writes agents.md and agents.html, and generates a static index.html for deployment.