# Newsletter Bookshelf

> A WebGL newsletter archive of cloth-bound books. Browse the shelf sideways, lift a spine on hover, and pull a book forward to inspect its cover.

A three.js shelf that turns each newsletter edition into a cloth-bound book with a generated cover, spine and page block. Drag, scroll sideways or use the arrow keys to move along the shelf. Hover lifts a book and shows its title. Click a book to fly it forward and turn its cover to you; it sways on its own and you can drag to rotate it. Click it again to open its link, or press Escape to put it back.

<demo-newsletter-bookshelf>



</demo-newsletter-bookshelf>

## Installation

```bash
npx shadcn-vue@latest add "https://nxui.geoql.in/r/newsletter-bookshelf.json"
```

## Usage

```vue
<script setup lang="ts">
  import NewsletterBookshelf from '~/components/ui/newsletter-bookshelf/NewsletterBookshelf.vue';
  import type { NewsletterBookshelfItem } from '~/components/ui/newsletter-bookshelf/types';

  const editions: NewsletterBookshelfItem[] = [
    {
      id: 'edition-04',
      title: 'The useful systems issue',
      date: 'JUL 24, 2026',
      href: '/posts/useful-systems',
    },
    {
      id: 'edition-03',
      title: 'A better creative workflow',
      date: 'JUL 17, 2026',
      href: '/posts/creative-workflow',
    },
    {
      id: 'edition-02',
      title: 'Notes on building in public',
      date: 'JUL 10, 2026',
      color: '#efe8d4',
    },
    {
      id: 'edition-01',
      title: 'The small team advantage',
      date: 'JUL 3, 2026',
      color: '#16277a',
    },
  ];

  function onSelect(edition: NewsletterBookshelfItem, index: number) {
    console.log('Selected', edition.id, index);
  }
</script>

<template>
  <NewsletterBookshelf
    :items="editions"
    brand="Studio Notes"
    @select="onSelect"
  />
</template>
```

Each book's width, height, cloth colour and cover motif come from a seeded
hash of its `id`, so the shelf looks the same on every load. Covers, spines
and pages are drawn once into 2D canvases and used as three.js textures.

## Props

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        items
      </code>
    </td>
    
    <td>
      <code>
        NewsletterBookshelfItem[]
      </code>
    </td>
    
    <td>
      32 sample books
    </td>
    
    <td>
      Editions on the shelf. An empty array uses the samples.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        height
      </code>
    </td>
    
    <td>
      <code>
        number | string
      </code>
    </td>
    
    <td>
      <code>
        620
      </code>
    </td>
    
    <td>
      Height of the stage. A number is pixels; a string is any CSS length, e.g. <code>
        '100%'
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        brand
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'The Brief'
      </code>
    </td>
    
    <td>
      Wordmark printed at the bottom of every generated cover.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Extra classes merged onto the root element.
    </td>
  </tr>
</tbody>
</table>

### `NewsletterBookshelfItem`

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        id
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Stable id. It seeds the book's size, colour and motif.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        title
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Printed on the cover and spine, and shown in the hover tooltip.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        date
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Printed on the cover and shown in the hover tooltip.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        subtitle
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Optional description kept with the item.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        href
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Opened when a focused book is activated again. Only <code>
        http
      </code>
      
       and <code>
        https
      </code>
      
       are used.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Cloth colour (hex). Defaults to a seeded pick from a blue and cream palette.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        foil
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Foil colour (hex). Defaults to cream on dark cloth and blue on light cloth.
    </td>
  </tr>
</tbody>
</table>

## Events

<table>
<thead>
  <tr>
    <th>
      Event
    </th>
    
    <th>
      Payload
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        select
      </code>
    </td>
    
    <td>
      <code>
        (item: NewsletterBookshelfItem, index: number)
      </code>
    </td>
    
    <td>
      Fired when a book enters the focused view.
    </td>
  </tr>
</tbody>
</table>

## Accessibility

- The stage is a focusable `region` with a visible inset focus ring. Its label names the focused book or the edition count.
- Arrow keys move along the shelf. When a book is focused, they switch to the next or previous book.
- Enter or Space focuses the book nearest the centre, or opens the focused book's link. Escape puts the book back.
- With `prefers-reduced-motion: reduce`, books snap into place and the focused book does not sway.
- The render loop pauses while the shelf is offscreen. All textures, geometry and the WebGL context are freed on unmount.
- Ported from [componentry.fun](https://componentry.dev/docs/components/newsletter-bookshelf) by Harsh Jadhav.
