# Device

A phone, a tablet or a laptop in CSS 3D, chosen by the stylesheet rather than by JavaScript, with a real screen in it.

## Home


The home page is inside one of these. Which one depends on the window you are
reading this in.

<!-- ::start:showcase demo="device" height="560" -->
<!-- ::end:showcase -->

## The machine is chosen by CSS

This is the decision everything else follows from, so it is worth stating on
its own.

**Nothing in this component measures anything.** It renders one screen and
every piece of chrome all three machines could need, and the stylesheet hides
what does not apply. A phone below 720px, a tablet from 720, a laptop from
1080.

The obvious alternative is to read the width and return one of three trees, and
it is wrong in three separate ways:

| What goes wrong | Why |
| --- | --- |
| Nothing renders on the server | There is no window to measure, so SSR emits whichever branch the fallback picked. |
| The first client frame is wrong | The correction happens after hydration, which is a visible flash of the wrong machine. |
| React throws the tree away | If the server guessed and the client disagrees, that is a hydration mismatch, and the recovery is a full client re-render with dead event handlers on the way. |

All three had already happened in this repo, on other components, for exactly
this reason. Four empty `<div>`s and a `display: none` cost less than any of
them, and are correct before a byte of JavaScript arrives.

> [!NOTE] The numbers are a table
> Widths, aspect ratios, bezels, corner radii, tilt and which chrome each
> machine shows all live in `packages/atoms/devices.md`. `pnpm doctor`
> compiles it to `devices.css` and `device-kinds.ts`. Editing either output is
> a change that gets reverted.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/device.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/device.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | layout · Scenes |
| Files | `device.tsx`, `device-kinds.ts` |
| Dependencies | None |
| Tags | block, 3d, perspective, responsive, no-js, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Guides


## Choosing one by hand

```tsx
<Device kind="tablet">{...}</Device>
```

Every generated rule is written twice for this: once under its media query for
`.device:not([data-device])`, once as `.device[data-device="tablet"]` with no
query at all. The two selectors tie on specificity, the attribute forms come
last, and so a choice beats a width.

That is what a settings panel writes, and it is how a showcase can put all
three on one page.

<!-- ::start:spacer size="6" rule="true" -->
<!-- ::end:spacer -->

## The 3D, and which element carries what

Three flat elements laid out in space. Two properties do the work and putting
either on the wrong element breaks the illusion in a way that is hard to name
and easy to see.

<!-- ::start:grid min="15rem" gap="4" -->

**`perspective`** goes on the outer stage. One vanishing point for the whole
machine. On the body instead, the body gets its own and the deck stops agreeing
with it about where the viewer is standing.

**`transform-style: preserve-3d`** goes on the body, so the screen inside it
stays in the same space rather than being flattened into a picture of itself.

**`rotateX`** is the tilt, and it is the only reason the perspective has
anything to do. At zero this is an ordinary rectangle and the entire 3D stage
is dead weight.

**`translateZ`** orders the screen and the back panel. Inside `preserve-3d`,
paint order comes from 3D position rather than from the DOM.

<!-- ::end:grid -->

The lens is longer on a laptop (1400px) than on a phone (1100px), which is the
same instinct as a photographer's: the machine should look like it is being
looked at, not like it is being photographed from six inches away.

> [!CAUTION] The back panel must never be clickable
> `.device-back` is `inset: 0` over the whole body and comes *after* the screen
> in the DOM. The only thing keeping it behind is `preserve-3d` honouring its
> `translateZ(-2px)`, and any ancestor that flattens the 3D context - a filter,
> an overflow, a browser quirk - puts it on top, where it silently swallows
> every click on the screen and looks completely fine doing it.
>
> That happened. It is why the panel carries `pointer-events: none` as well.

## The screen is a container

`.device-screen` declares `container-type: inline-size`, so everything inside
sizes against the screen rather than against the window.

That is the difference between the machine being a frame and being a costume. A
tablet chosen on a wide monitor is 40rem of screen inside a 1600px window; a
`@media` query would give its contents a laptop's spacing, which is the exact
thing choosing a tablet was meant to avoid.

```css
.device-desktop {
	padding: clamp(var(--s-3), 3cqi, var(--s-4));
}
```

## It does not animate open

It used to, on scroll. The lid rose as you reached it.

A frame around real controls should not make you wait to reach them, and this
one is full of real controls - folders that open, windows that drag, a dock.
The animation was a thing you sat through before the screen became readable,
and it ran again every time somebody scrolled back up.

## The dock belongs to the screen, not the desktop

The dock is a child of the *screen*, not of the desktop, so anything it opens
is measured against the screen and clipped by the screen. Inside the desktop it
would be measured against a scrolled box and cropped by it, which is a bug that
looks like a rendering glitch and is really a containing block.

## No window controls

Deliberately not three coloured dots. Those are one vendor's furniture, they
mean close, minimise and zoom, and none of those three things can happen to
this. Drawing controls that do nothing is worse than drawing none.

The windows *inside* the screen do have those actions, and they have real
buttons for them. See `DeskWindow`.

## Where this is used

| Where | What |
| --- | --- |
| The home page | with `FolderShelf` on the screen and a `Dock` along the bottom |
| `packages/atoms/devices.md` | the three machines, as a table |
| `packages/atoms/src/devices.css` | generated. The widths, as media queries |
| `packages/ui/src/device-kinds.ts` | generated. The same widths, as values |
| `useDeviceKind` | for code that has to *name* the machine rather than draw it |


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `children` | `ReactNode` | - | The desktop. It scrolls on its own and chains at the end. |
| `kind?` | `DeviceKind` | - | Which machine to draw. Left off, the stylesheet decides from the width of the window: a phone, a tablet from 720px, a laptop from 1080px. Set it and that choice wins everywhere, which is what a Settings panel writes and what a showcase uses to put all three on one page. |
| `wallpaper?` | `ReactNode` | - | Drawn behind the desktop, and never in the way of a click. |
| `title?` | `string` | - | Shown in the strip at the top of the screen. |
| `toolbar?` | `ReactNode` | - | Also in the strip, right-aligned. A search field, a clock, a count. |
| `dock?` | `ReactNode` | - | Pinned along the bottom of the screen, below the scrolling desktop. |

<!-- /generated:api -->

## Notes

Anything the types cannot say: which combinations are meaningless, which
prop is ignored when another is set, and what it does when handed
something it cannot render.

