Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
600 changes: 600 additions & 0 deletions assets/css/dock.css

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions assets/css/variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,48 @@ body.os-active {
--os-dock-floating-highlight: rgba(255, 251, 255, 0.1);
--os-dock-floating-shadow: rgba(0, 0, 0, 0.55);

/*
* ---- Deck motion --------------------------------------------
*
* ONE timing, deliberately slower than the shell's usual
* `--os-ui-motion-slow`, and everything a deck switch moves is on
* it: the mesh plate travelling between tabs, the incoming tab's
* name unfurling to widen it, the outgoing tiles collapsing to
* zero width, the incoming ones opening back out — and, because
* the pill is `width: fit-content`, the pill itself resizing and
* re-centring around all of it.
*
* One token rather than several is the whole design. An earlier
* pass gave the tiles their own duration and their own staggered
* keyframe; what that produced was two clocks in one gesture, and
* the faster one always read as the thing going wrong. Anything
* added here later should join this token, not bring its own.
*
* Slower than the rest of the rail because this is the one place
* in the dock where a state change is worth WATCHING rather than
* just registering; everything else stays quick, which is what
* keeps this from reading as sluggishness.
*
* A theme that wants the old snap sets a shorter duration; `0s`
* removes the choreography without losing any of the state it
* carries.
*/
--os-dock-deck-slide: 720ms cubic-bezier(0.22, 1, 0.28, 1);

/*
* How far the unselected deck tabs stand back.
*
* Transparency rather than a dimmer ink, so the dock's own surface
* comes through them and they read as BEHIND the selection instead
* of as a paler copy of it. One tab answers "where am I" and the
* others are only offers; at equal weight the strip reads as four
* things competing for the same job.
*
* Hover takes one back to full. Raise this toward 1 for a flatter
* strip, lower it to push the unselected tabs further back.
*/
--os-dock-deck-idle-opacity: 0.6;

/*
* ---- The OpenStation layout ---------------------------------
*
Expand Down
2 changes: 2 additions & 0 deletions docs/api-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ The full surface is documented in [`javascript-reference.md`](./javascript-refer
| `dockPlacement` | `'bottom' \| 'left' \| 'right'` *(Unified + Spatial)* | Stable |
| `Dock.setBadge` | `( id: string, count: number ) => void` | Stable |
| `Dock.removeSystemItem` | `( id: string ) => void` | Stable |
| `Dock.getActiveDeck` | `() => string \| null` *(bottom rails; `null` when undecked)* | Stable |
| `Dock.setActiveDeck` | `( deckId: string ) => void` | Stable |
| `icons` | `IconsApi` *(see `icons.setBadge`)* | Stable |
| `icons.setBadge` | `( iconId: string, count: number ) => void` | Stable |
| `registerSystemTile` | `( item: SystemDockItem ) => void` | Stable |
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ OpenStation Preferences → Appearance lets the user pick one of four top-level

Inside a rail, tiles are grouped by what they do rather than by where they came from: the admin menus first, then a divider, then OpenStation's own controls (Preferences, the bin, the way out) — the cohort the dock calls *system tiles*. That divider is the rail's one structural line, because it is the only boundary where behaviour changes: before it a tile opens an admin screen, after it a tile acts on the desktop. A softer hairline also separates core menus from plugin apps, mirroring wp-admin's own menu, but that boundary is provenance rather than behaviour and is drawn quietly. The two read through `--os-dock-divider` and `--os-dock-divider-soft`; the OpenStation layout replaces the *first* with its own luminous seam.

**On a bottom rail those three groups can become decks** (`src/dock-decks/`). A pill has a width, and a site with a dozen plugins overran it; the rail's answer used to be a hidden horizontal scroll, which meant tiles that existed only for people who knew to swipe. Turn `dockDecksEnabled` on and exactly one group is on screen at a time — Favorites, WordPress, Plugins, OpenStation — with a tab strip at the leading edge naming it and offering the others. The selected tab is painted last on that strip, hard against the divider, so its label heads the icons that follow it; the mesh fill is anchored there and the tabs slide through it rather than the fill chasing the selection. The rearrangement is a flex `order` swap, so the DOM sequence — and with it `aria-selected`, the roving tabindex and the arrow keys — stays as registered. It is opt-in because decking trades "every tile is on screen" for "the rail fits": a good trade once there are enough tiles to feel the crowding, a bad one before then, and not the shell's call either way. **Favorites** is the one deck the user composes rather than inherits — starring a tile from its right-click menu appends its id to `dockFavorites`, and because that deck sorts first it takes the tile out of its provenance group entirely. The dividers stand down while it is on: they exist to say "these tiles are a different group from those", which is now the strip's whole job. The rail keeps its old shape wherever decking would be a loss: never on a vertical placement (a column has the shell's full height to spend and scrolls honestly), and not on a bottom rail whose tiles all land in one deck. Which tiles are hidden is decided in JS rather than in a stylesheet, because CSS cannot compare `data-os-deck` on a tile against `data-os-deck-active` on the rail without one rule per deck id — and the deck list is a filter (`os.dock.decks`), so plugin-registered decks would be the ones to silently break. A tab carries what its hidden tiles were saying: an indicator dot when the deck holds an open window, and the sum of their badges as a number. Whether focusing a window pulls the rail to its deck is a second call, next to the first in OpenStation Preferences → Appearance → Dock groups; the default is no, on the [event-driven framework](event-driven-framework.md)'s rule that the shell is a transport rather than a UX policy maker.

**Both one-rail layouts group before they draw.** `Dock.replaceItems()` inserts the `--group` divider at the first tile whose `isCore` is `false`, so it only produces one clean boundary when the list is already grouped — and the admin menu is not: a plugin that registers high up (Yoast, Jetpack) would otherwise put the line two tiles in and strand the rest of WordPress on the plugin side of it. `coreFirstRailItems()` in the dispatcher sorts core ahead of plugins for `unified` and `openstation` alike, preserving relative order inside each cluster so drag-to-reorder still holds within a group. One consequence to know about: a tile cannot be dragged from one cluster into the middle of the other.

**The way out is drawn differently.** The `os-exit` tile leaves the desktop rather than opening something closeable, and with the admin bar hidden by default it is the only route back to classic admin. `dock.css` gives it `order: 1` (always last, whatever order plugin-owned native-window tiles sync in), its own wider gap, a ring instead of a filled plate, and a hover that leans toward the edge it leads to instead of lifting toward the pointer. The rules key on `[data-system-id="os-exit"]`, the same idiom the Recycle Bin badge uses; `tests/vitest/dock-exit-tile.test.ts` pins that attribute.
Expand Down
41 changes: 41 additions & 0 deletions docs/desktop-themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,47 @@ disappears the moment you give the dock a light background.
Recolouring the strip alone is the most common way a first theme ends
up with an invisible dock.

Two more cover the **deck tabs** — the strip at the leading edge of a
bottom rail that names the group of tiles currently on screen (see
[Dock decks](javascript-reference.md#dock-decks)):

| Token | Role |
|---|---|
| `--os-dock-deck-fill` | The fill behind the active tab. Falls back to `--os-ui-holo-fill`, the brand mesh |
| `--os-dock-deck-ink` | Its label and glyph. Falls back to `--os-ui-holo-ink`, near-Void |

The inactive tabs deliberately have no tokens of their own: they read
through `--os-dock-icon-color` and `--os-dock-item-bg-hover`, the same
two the tiles beside them use, so a theme that recoloured its dock
glyphs gets the strip for free. **Set the pair together or not at
all** — an active tab with a repainted fill and default Void ink is
the one combination that can end up unreadable.

One more tunes how a deck switch *moves*:

| Token | Role |
|---|---|
| `--os-dock-deck-slide` | `<duration> <easing>` for the whole switch |
| `--os-dock-deck-idle-opacity` | How far the *unselected* tabs stand back (default `0.6`). Transparency rather than a dimmer ink, so the dock's surface comes through them; hover takes one back to full |

Everything a switch moves is on that single token: the tabs sliding
so the selected one lands at the trailing end of the strip, its name
unfurling to widen it, the outgoing tiles collapsing to zero width,
the incoming ones opening back out — and, since the pill is
`width: fit-content`, the pill resizing and re-centring around all of
it. The mesh fill itself is the one thing that does not move; it is
anchored at that trailing end and only its leading edge breathes as
labels of different lengths pass under it.

**One token rather than several is the design, not a shortcut.** An
earlier pass gave the tiles their own duration and their own staggered
entrance; the result was two clocks in one gesture, and the faster one
always read as the thing going wrong. It is deliberately slower than
the rest of the rail — this is the one dock state change worth
watching rather than just registering — so shorten it for a snappier
rail, and set `0s` to remove the choreography without losing any of
the state it carries.

`--os-dock-icon-color` is a **colour**, not a fill, which
matters if your iconset uses
[`"iconColor": "currentColor"`](#icon-slots): those icons are masked
Expand Down
59 changes: 59 additions & 0 deletions docs/dock-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,68 @@ See the full reference for the [`DockItem` shape](./javascript-reference.md#dock

---

## Decks — the third thing you can customize

The two registries above answer *what paints the rail* and *what
decorates a tile*. Decks answer a third question the default renderer
asks on a bottom rail: **which tiles are on screen right now.**

With `dockDecksEnabled` on (opt-in, in Appearance → Dock groups) the
rail folds itself into named groups — `favorites`, `wordpress`,
`apps`, `station` — and shows one at a time, with a tab strip at its
leading edge. The list is a filter, so it is yours to reorder, rename,
extend, or switch off:

```js
wp.hooks.addFilter( 'os.dock.decks', 'my-plugin', ( decks ) => [
...decks,
{
id: 'shop',
label: 'Shop',
icon: 'dashicons-cart',
order: 15, // between Apps (20) and WordPress (10)
matchItem: ( item ) => item.id.startsWith( 'woocommerce' ),
},
] );
```

Three things worth knowing before you reach for it:

- **A tile joins the first deck that claims it**, in `order` order. A
narrow deck at a low `order` therefore wins its tiles without you
having to also rewrite the built-ins' predicates — which is exactly
how `favorites` (order 5) pulls a starred tile out of `apps`.
- **Decks only exist on a bottom rail**, and only when at least two of
them are non-empty. A custom *rail renderer* replaces the default
`Dock` entirely and so opts out of decks along with everything else
the class paints — if your renderer wants grouping, it owns it.
- **Returning `[]` turns decks off** for every rail, which is the
supported way for a theme or plugin to get the old single-row rail
back.

Reading and driving the current deck from anywhere:

```js
wp.os.dock?.getActiveDeck(); // 'wordpress' | 'apps' | … | null
wp.os.dock?.setActiveDeck( 'apps' );

wp.hooks.addAction( 'os.dock.deck-changed', 'my-plugin', ( ctx ) => {
// ctx.reason: 'click' | 'keyboard' | 'wheel' | 'swipe'
// | 'restore' | 'auto'
console.log( ctx.previousDeckId, '→', ctx.deckId );
} );
```

Both methods are no-ops on a rail that isn't decked, so neither needs
a layout check around it. Full shapes in the
[JavaScript Reference](./javascript-reference.md#dock-decks).

---

## Where to go next

- **[Decoration hooks recipes](./examples/dock-decoration-hooks.md)** — six examples from one-line classNames to grid-wide IntersectionObservers.
- **[Rail renderer walk-through](./examples/dock-rail-renderer.md)** — full "ring" implementation with circular layout math.
- **[JavaScript Reference](./javascript-reference.md#dock-decoration)** — every API entry, every hook, every type.
- **[Deck tokens](./desktop-themes.md#dock-glyphs)** — the two tokens a desktop theme sets to repaint the active deck tab.
- **[Architecture](./architecture.md#dock-customization--two-registries)** — how the registries plug into the layout dispatcher and the live menu-refresh pipeline.
1 change: 1 addition & 0 deletions docs/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defined( 'ABSPATH' ) || exit;
- [Add a dock item with a badge](./dock-badge.md)
- [Give a tile two icons, one per state](./tile-icon-state.md)
- [Decorate the dock without forking the renderer](./dock-decoration-hooks.md)
- [Group the dock your way — custom decks](./dock-decks.md)
- [Replace the dock rail entirely](./dock-rail-renderer.md)
- [Gate OpenStation by role](./gate-by-role.md)
- [React to window events](./react-to-window-events.md)
Expand Down
Loading
Loading