# Part

One document of a packet, numbering its own pages and saying where it sits in the packet.

Canonical URL: https://docs.asterdoc.com/components/part/

A bundle on screen is a sequence of documents in one scroll, not one long
document, and `Part` is the boundary between them: everything inside one
numbers its pages from one, because that is what a reader of that document
expects to see. The packet's own numbering — where each part's pages land in
the whole — comes from the seal, not from the screen, so `Part` shows a page
range only while it has one to show.

## Preview


```tsx
import { Bundle } from "@/components/asterdoc/bundle";
import { Document } from "@/components/asterdoc/document";
import { Field } from "@/components/asterdoc/field";
import { Part } from "@/components/asterdoc/part";
import { PdfPages } from "@/components/asterdoc/pdf-pages";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";
import { vendorPacketAnnexBytes } from "@/artifacts/asterdoc/vendor-packet.annex";

/** The packet these two parts were sealed together as. */
const PACKET_HASH = "part-demo-packet";

export function PartDemo() {
  return (
    <Bundle id="part-demo" className="flex flex-col gap-8">
      <Part
        id="cover-note"
        kind="composition"
        label="Cover note · composed live"
        firstPage={1}
        pageCount={1}
        placedFor={PACKET_HASH}
        packetHash={PACKET_HASH}
      >
        <Document artifact={proposalForm} data={shortProposalData} id="part-demo-cover-note">
          <Field path="customer" />
        </Document>
      </Part>
      <Part
        id="insurance-annex"
        kind="annex"
        label="Certificate of insurance · annex"
        firstPage={2}
        pageCount={1}
        placedFor={PACKET_HASH}
        packetHash={PACKET_HASH}
      >
        <PdfPages bytes={vendorPacketAnnexBytes} filename="certificate-of-insurance.pdf" />
      </Part>
    </Bundle>
  );
}
```


## Installation


```bash
npx shadcn@4 add @asterdoc/part
```


```bash
asterdoc add part
```


Installs to components/asterdoc/part.tsx.


## Usage


```tsx
import { Part } from "@/components/asterdoc/part";

<Part id="part-variant-unplaced" kind="composition" label="Cover note · composed live">
  <Document artifact={proposalForm} data={shortProposalData}>
    <Field path="customer" />
  </Document>
</Part>
```

| Prop | Type | Optional | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | No |  | The bundle content key. |
| `kind` | `PartKind` | No |  | What the part is: a composition rendered live, a filled PDF form, or an annex. |
| `label` | `ReactNode` | Yes |  | Shown above the document. Nothing is shown when it is omitted. |
| `firstPage` | `number` | Yes |  | 1-based packet page this part's first page is, when the caller has sealed the packet. |
| `pageCount` | `number` | Yes |  | Pages this part contributes, when the caller has sealed the packet. |
| `attached` | `boolean` | Yes |  | True when the part is carried beside the packet rather than merged into it. |
| `placedFor` | `string` | Yes |  | The `packetHash` the placement above was computed for. |
| `packetHash` | `string` | Yes |  | The `packetHash` of the packet on screen now. Page numbers show only while it matches. |
| `className` | `string` | Yes |  | Application-owned classes on the part's own wrapping element. |
| `children` | `ReactNode` | No |  | The part's document: a composition, painted PDF pages, or an attachment. |


## Composition

`Part` always sits inside a `Bundle`, which groups the packet's documents in
one scroll and supplies the branding tokens every part reads. A part's own
children are whatever the part is: a composition rendered live (usually a
`Document`, often wrapped in its own pagination), a filled PDF form, or an
annex — both of the latter two painted by
[`PDF Pages`](/components/pdf-pages).

The `firstPage` and `pageCount` a part shows come from core's `sealBundle`,
which answers with exactly those two numbers per part once the packet is
sealed. A part carries the `packetHash` that placement was computed for
alongside the `packetHash` of the packet on screen now, and shows a page
range only while the two match. A session that changes a field repaginates
the composition, which moves every part after it; until the packet is
resealed, the part it moved says its pages are pending rather than naming a
page the packet no longer has.

An `attached` part is carried beside the packet rather than merged into its
page count — a certificate or receipt a reader downloads alongside the
packet rather than reads as one of its numbered pages.

The header's size follows the rhythm of the document it frames: the `Bundle`'s
token when there is one, otherwise the `Document` below, read through
`useDocumentTokensAround`. See [Typography](/components/typography).

## Variants

`Part`'s page range comes entirely from `firstPage`, `pageCount`,
`placedFor`, and `packetHash` — the sealed placement, not a prop a caller
chooses directly.

### Unplaced

No `firstPage` or `pageCount`: the state before the packet has ever been
sealed.


```tsx
import { Document } from "@/components/asterdoc/document";
import { Field } from "@/components/asterdoc/field";
import { Part } from "@/components/asterdoc/part";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

export function PartVariantUnplaced() {
  return (
    <Part id="part-variant-unplaced" kind="composition" label="Cover note · composed live">
      <Document artifact={proposalForm} data={shortProposalData} id="part-variant-unplaced-doc">
        <Field path="customer" />
      </Document>
    </Part>
  );
}
```


### Placed

`firstPage` and `pageCount` match the packet on screen, so the header names
the packet pages this part occupies.


```tsx
import { Document } from "@/components/asterdoc/document";
import { Field } from "@/components/asterdoc/field";
import { Part } from "@/components/asterdoc/part";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

const PACKET_HASH = "part-variant-placed-packet";

export function PartVariantPlaced() {
  return (
    <Part
      id="part-variant-placed"
      kind="composition"
      label="Cover note · composed live"
      firstPage={3}
      pageCount={2}
      placedFor={PACKET_HASH}
      packetHash={PACKET_HASH}
    >
      <Document artifact={proposalForm} data={shortProposalData} id="part-variant-placed-doc">
        <Field path="customer" />
      </Document>
    </Part>
  );
}
```


### Pending

`placedFor` no longer matches `packetHash`: a session's edit repaginated the
composition and the packet has not been resealed yet, so the stale page
numbers are suppressed.


```tsx
import { Document } from "@/components/asterdoc/document";
import { Field } from "@/components/asterdoc/field";
import { Part } from "@/components/asterdoc/part";
import { proposalForm } from "@/examples/proposal";
import { shortProposalData } from "@/examples/proposal-data";

export function PartVariantPending() {
  return (
    <Part
      id="part-variant-pending"
      kind="composition"
      label="Cover note · composed live"
      firstPage={3}
      pageCount={2}
      placedFor="part-variant-pending-old-packet"
      packetHash="part-variant-pending-new-packet"
    >
      <Document artifact={proposalForm} data={shortProposalData} id="part-variant-pending-doc">
        <Field path="customer" />
      </Document>
    </Part>
  );
}
```
