# Sheet

The sheet is an optional add-on from `@ogdakke/fluidbox/sheet`. It is a panel the viewer slides up from the bottom, with snap points, for details, comments or a form.

#### React

`sheet.tsx`

```tsx
import { itemLabel, type FluidboxItem } from "@ogdakke/fluidbox";
import { Fluidbox, useFluidboxSheet } from "@ogdakke/fluidbox/react";
import "@ogdakke/fluidbox/sheet.css";
import { photos } from "./data";

function Details({ item }: { item: FluidboxItem }) {
  const sheet = useFluidboxSheet();
  return (
    <>
      <h3>{itemLabel(item)}</h3>
      <p>
        Stop <output>{sheet.stop}</output>
      </p>
      <button type="button" onClick={() => sheet.set(2)}>
        Expand
      </button>
      <input placeholder="Add a note" aria-label="Note" />
    </>
  );
}

export function Sheet() {
  return (
    <Fluidbox.Root items={photos}>
      <div className="pattern-grid">
        {photos.map((photo) => (
          <Fluidbox.Trigger
            key={photo.src}
            item={photo}
            className="pattern-thumb"
          >
            <img src={photo.src} alt={itemLabel(photo)} />
          </Fluidbox.Trigger>
        ))}
      </div>
      {/* Stop 0 rests below the photo; stops 1 and 2 raise it to 40% and 90% of the screen. */}
      <Fluidbox.Sheet snapPoints={["40%", "90%"]} className="pattern-sheet">
        {({ item }) => <Details item={item} />}
      </Fluidbox.Sheet>
      <Fluidbox.Viewer>
        <Fluidbox.SheetStop stop={1} className="pattern-sheet-toggle">
          Details
        </Fluidbox.SheetStop>
        <Fluidbox.Controls />
      </Fluidbox.Viewer>
    </Fluidbox.Root>
  );
}
```

#### Vue

`Sheet.vue`

```vue
<script setup lang="ts">
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox } from "@ogdakke/fluidbox/vue";
import "@ogdakke/fluidbox/sheet.css";
import { ref } from "vue";
import { photos } from "./data";

// The stop the sheet rests at: 0 at rest, then one per snap point.
const stop = ref(0);
</script>

<template>
  <Fluidbox.Root :items="photos" @sheetchange="stop = $event.stop">
    <div class="pattern-grid">
      <Fluidbox.Trigger
        v-for="photo in photos"
        :key="photo.id"
        :item="photo"
        class="pattern-thumb"
      >
        <img :src="photo.src" :alt="itemLabel(photo)" />
      </Fluidbox.Trigger>
    </div>
    <Fluidbox.Viewer>
      <Fluidbox.Controls />
      <!-- At stop 0 the sheet is out of sight; this raises it. -->
      <Fluidbox.SheetStop :stop="1" class="pattern-sheet-toggle">
        Details
      </Fluidbox.SheetStop>
    </Fluidbox.Viewer>
    <!-- The sheet's content, for the active item. Its attributes go on the sheet. -->
    <Fluidbox.Sheet v-slot="{ item }" :snap-points="['40%', '90%']" class="pattern-sheet">
      <h3>{{ itemLabel(item) }}</h3>
      <p>
        Stop <output>{{ stop }}</output>
      </p>
      <Fluidbox.SheetStop :stop="2">Expand</Fluidbox.SheetStop>
      <input placeholder="Add a note" aria-label="Note" />
    </Fluidbox.Sheet>
  </Fluidbox.Root>
</template>
```

#### Solid

`sheet.tsx`

```tsx
import { For } from "solid-js";
import { itemLabel, type FluidboxImageItem } from "@ogdakke/fluidbox";
import { Fluidbox, useFluidboxSheet } from "@ogdakke/fluidbox/solid";
import "@ogdakke/fluidbox/sheet.css";
import { photos } from "./data";

function Details(props: { photo: FluidboxImageItem }) {
  const sheet = useFluidboxSheet();
  return (
    <>
      <h3>{itemLabel(props.photo)}</h3>
      <p>
        Stop <output>{sheet.stop()}</output>
      </p>
      <button type="button" onClick={() => sheet.set(2)}>
        Expand
      </button>
      <input placeholder="Add a note" aria-label="Note" />
    </>
  );
}

export function Sheet() {
  return (
    <Fluidbox.Root items={photos}>
      <div class="pattern-grid">
        <For each={photos}>
          {(photo) => (
            <Fluidbox.Trigger item={photo} class="pattern-thumb">
              <img src={photo.src} alt={itemLabel(photo)} />
            </Fluidbox.Trigger>
          )}
        </For>
      </div>
      {/* The sheet's content, for the active item. Its attributes go on the sheet itself. */}
      <Fluidbox.Sheet<FluidboxImageItem> snapPoints={["40%", "90%"]} class="pattern-sheet">
        {({ item }) => <Details photo={item} />}
      </Fluidbox.Sheet>
      <Fluidbox.Viewer>
        {/* Raises the sheet to its first stop. */}
        <Fluidbox.SheetStop stop={1} class="pattern-sheet-toggle">
          Details
        </Fluidbox.SheetStop>
        <Fluidbox.Controls />
      </Fluidbox.Viewer>
    </Fluidbox.Root>
  );
}
```

#### Angular

`sheet.ts`

```ts
import { Component, Input } from "@angular/core";
import { itemLabel } from "@ogdakke/fluidbox";
import {
  FluidboxModule,
  injectFluidboxSheet,
  type FluidboxImageItem,
} from "@ogdakke/fluidbox/angular";
import "@ogdakke/fluidbox/sheet.css";
import { photos } from "./data";

@Component({
  selector: "div[appDetails]",
  standalone: true,
  host: { class: "pattern-sheet" },
  template: `
    <h3>{{ label(photo) }}</h3>
    <p>
      Stop <output>{{ sheet.stop() }}</output>
    </p>
    <button type="button" (click)="sheet.set(2)">Expand</button>
    <input placeholder="Add a note" aria-label="Note" />
  `,
})
export class Details {
  @Input({ required: true }) photo!: FluidboxImageItem;
  readonly sheet = injectFluidboxSheet();
  readonly label = itemLabel;
}

@Component({
  selector: "pattern-sheet",
  standalone: true,
  imports: [FluidboxModule, Details],
  template: `
    <div fluidboxRoot [items]="photos" class="pattern-grid">
      @for (photo of photos; track photo.id) {
        <button [fluidboxTrigger]="photo" class="pattern-thumb">
          <img [src]="photo.src" [alt]="label(photo)" />
        </button>
      }
      <!-- Stop 0 rests below the photo; stops 1 and 2 raise it to 40% and 90% of the screen. -->
      <ng-template fluidboxSheet [snapPoints]="['40%', '90%']" let-photo>
        <div appDetails [photo]="photo"></div>
      </ng-template>
      <ng-template fluidboxViewer>
        <div fluidboxControls></div>
        <!-- A fluidboxSheetStop button moves the sheet to that stop. -->
        <button fluidboxSheetStop="1" class="pattern-sheet-toggle">Details</button>
      </ng-template>
    </div>
  `,
})
export class Sheet {
  readonly photos = photos;
  readonly label = itemLabel;
}
```

#### Elements

`sheet.html`

```html
<fluidbox-gallery class="pattern-grid">
  <fluidbox-item item-id="photo-aurora">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/aurora.svg" alt="Aurora over the tundra" />
    </button>
    <template slot="content">
      <img src="/art/aurora.svg" alt="Aurora over the tundra" width="1100" height="1650" />
    </template>
  </fluidbox-item>
  <fluidbox-item item-id="photo-ridge">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/ridge.svg" alt="Night ridge" />
    </button>
    <template slot="content">
      <img src="/art/ridge.svg" alt="Night ridge" width="1200" height="1500" />
    </template>
  </fluidbox-item>
  <fluidbox-item item-id="photo-fjord">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/fjord.svg" alt="Fjord in the blue hour" />
    </button>
    <template slot="content">
      <img src="/art/fjord.svg" alt="Fjord in the blue hour" width="1200" height="1600" />
    </template>
  </fluidbox-item>
  <fluidbox-item item-id="photo-lagoon">
    <button slot="trigger" type="button" class="pattern-thumb">
      <img src="/art/lagoon.svg" alt="Lagoon after sunset" />
    </button>
    <template slot="content">
      <img src="/art/lagoon.svg" alt="Lagoon after sunset" width="1800" height="1200" />
    </template>
  </fluidbox-item>
  <template slot="viewer">
    <fluidbox-controls></fluidbox-controls>
    <button
      type="button"
      class="pattern-sheet-toggle"
      data-fluidbox-sheet-stop="1"
    >
      Details
    </button>
  </template>
  <fluidbox-sheet snap-points="40% 90%">
    <template>
      <div class="pattern-sheet">
        <h3></h3>
        <p>Stop <output name="stop">0</output></p>
        <button type="button" data-fluidbox-sheet-stop="2">Expand</button>
        <input type="text" aria-label="Note" placeholder="Add a note" />
      </div>
    </template>
  </fluidbox-sheet>
</fluidbox-gallery>
```

`sheet.ts`

```ts
import { itemLabel } from "@ogdakke/fluidbox";
import "@ogdakke/fluidbox/sheet.css";

export function sheet(root: HTMLElement) {
  const gallery = root.querySelector("fluidbox-gallery")!;
  const sheet = gallery.querySelector("fluidbox-sheet")!;
  const content = sheet.querySelector("template")!;
  // Fill the sheet for the active item; without this it shows the template as is.
  sheet.render = ({ item, target, signal }) => {
    target.append(content.content.cloneNode(true));
    target.querySelector("h3")!.textContent = itemLabel(item);
    const stop = target.querySelector<HTMLOutputElement>('output[name="stop"]')!;
    stop.value = String(gallery.sheet!.stop);
    gallery.addEventListener("sheetchange", (event) => (stop.value = String(event.stop)), {
      signal,
    });
  };
}
```

#### JavaScript

`sheet.ts`

```ts
import {
  createFluidbox,
  itemLabel,
  type Fluidbox,
  type FluidboxImageItem,
} from "@ogdakke/fluidbox";
import "@ogdakke/fluidbox/sheet";
import "@ogdakke/fluidbox/sheet.css";
import { photos } from "./data";

export function mountSheet(root: HTMLElement) {
  const fluidbox: Fluidbox<FluidboxImageItem> = createFluidbox({
    items: photos,
    sheet: {
      snapPoints: ["40%", "90%"],
      // Fills the sheet for the active item.
      render({ item, target, signal }) {
        const sheet = fluidbox.sheet!;
        target.innerHTML = `
          <div class="pattern-sheet">
            <h3></h3>
            <p>Stop <output name="stop"></output></p>
            <button type="button" data-action="expand">Expand</button>
            <input placeholder="Add a note" aria-label="Note" />
          </div>`;
        target.querySelector("h3")!.textContent = itemLabel(item);
        target
          .querySelector('[data-action="expand"]')!
          .addEventListener("click", () => sheet.set(2));
        const stop = target.querySelector<HTMLOutputElement>('output[name="stop"]')!;
        stop.value = String(sheet.stop);
        fluidbox.addEventListener("sheetchange", (event) => (stop.value = String(event.stop)), {
          signal,
        });
      },
    },
    // A button marked data-fluidbox-sheet-stop moves the sheet to that stop.
    viewer({ target }) {
      target.insertAdjacentHTML(
        "beforeend",
        `<button type="button" class="pattern-sheet-toggle" data-fluidbox-sheet-stop="1">Details</button>`,
      );
    },
  });

  const grid = document.createElement("div");
  grid.className = "pattern-grid";
  photos.forEach((photo, index) => {
    const button = document.createElement("button");
    button.type = "button";
    button.className = "pattern-thumb";
    button.append(Object.assign(new Image(), { src: photo.src, alt: itemLabel(photo) }));
    grid.append(button);
    fluidbox.bind(button, index);
  });
  root.append(grid);

  return () => {
    fluidbox.destroy();
    grid.remove();
  };
}
```

- `snapPoints: ["40%", "90%"]` adds stops above the resting stop `0`. Set it in the `sheet` option in plain JavaScript, as the `Sheet` part's prop, or as `snap-points="40% 90%"` on `<fluidbox-sheet>`.
- `fluidbox.sheet.set(stop)` moves the sheet, and a `sheetchange` event reports each new stop. In the frameworks, `useFluidboxSheet()` returns `{ stop, set }` and the root takes `onSheetChange`, `@sheetchange` or `(sheetChange)`. On custom elements, `gallery.sheet` is the same handle and the gallery fires `sheetchange`.
- Any button with `data-fluidbox-sheet-stop="N"` in the viewer moves the sheet to stop `N`, and lowers it again when the sheet is already there. The framework `SheetStop` part renders one.
- Keys typed in the sheet's fields stay there, so arrow keys don't change the slide.
- `import "@ogdakke/fluidbox/sheet"` once to turn the add-on on (the custom elements load it themselves), and import `@ogdakke/fluidbox/sheet.css` with it.
