Designs mywant-gui
A design is how overlays look: the colour each tone gets, the cell's shape and hover, the focus ring, the words, the box and its tail, how it comes in. The layout is not the design's — every design lays out on the same cells — so switching designs never moves anything.
The built-in design is grid: full-bleed coloured tiles on a dark box. Other designs come from extensions, and a person picks one for themselves.
The OverlayDesign type
interface OverlayDesign {
id: string;
name: string; // what the picker calls it
tones: Record<OverlayTone, string>; // a cell's fill, per tone
disabled: string; // a cell that cannot be pressed
cell: string; // every cell: shape, not colour
cellInteractive: string; // hover / press feedback
cellOff: string; // the off half of an on/off cell
focus: string; // the ring's shape (its colour is --mw-focus-ring)
label: string; // the word under the icon
header: string; // the question over a grid
surface: string; // fill + outline, any shape
frame: string; // the bubble's box (shape + surface)
tailColor: string; // a CSS colour for the bubble's tail
backdrop: string; // laid over a card behind its overlay
input: string; // a text field in an overlay
enterAnimation: string; // CSS animation shorthand
cellEnterAnimation: string;
portable?: OverlayPortableStyle; // the same look as plain CSS values (below)
}Every field except tailColor and the animations is a class name.
Bring your own classes
The app's Tailwind build contains only the classes the app itself uses. A design from an extension cannot count on an arbitrary Tailwind class existing, so it names classes its own stylesheet defines — the styles of its manifest. The flat tutorial does exactly this.
A design registered with some fields missing gets the grid values for them, so a design can restyle only what it cares about.
Registering a design
From a design custom — one plugin.js, installed with mywant custom install (see Tutorial: a design plugin) — or from an extension:
registerExtension({ id: 'my-look', overlayDesigns: [myDesign] });Directly:
import { registerOverlayDesign } from '@/components/overlay';
registerOverlayDesign(myDesign);
// or, from a plain script with no imports:
window.__mywant.registerOverlayDesign(myDesign);Registering an id that already exists replaces it — grid included.
Choosing a design
The choice is a display setting of the person's character, stored at display.ext.overlay.design. A choice naming a design that is not installed falls back to grid.
import { overlayDesignOf, withOverlayDesign, listOverlayDesigns } from '@/components/overlay';
overlayDesignOf(character.display); // 'grid' | 'flat' | …
put({ ext: withOverlayDesign(display, 'flat') }); // in a display editorReading the design in use
Inside the library this is done for you. Code that draws overlay-like chrome of its own — a pill-shaped prompt, a speech bubble — reads the design rather than repeating its classes:
const design = useOverlayDesign(); // re-renders on a change
<div className={`rounded-full ${design.surface}`} style={{ animation: design.enterAnimation }} />
overlayDesign(); // the same, outside a React renderOutside the app: portable values mywant-gui
Class names only mean something inside the app. Overlays drawn on other people's pages — the browser extension's and the bookmarklet's menus — need the look as values, so a design can carry them:
interface OverlayPortableStyle {
tones: Record<OverlayTone, string>; // CSS colours
surface: string; outline: string; ink: string; // the box: fill, outline, words
radius: number; // box corner, px
cellRadius: number; cellGap: number; // 0 and 0 are full-bleed tiles
labelTransform: 'uppercase' | 'none'; labelWeight: number; labelSize: number;
backdrop: string; // laid over a page behind a dialog
}withOverlayDesign stores the chosen design's portable with the choice (display.ext.overlay.portable), so anything that can read the character can draw its menus the same way without knowing any design by name. A design without portable stores the grid's. Read it back with portableOverlayStyleOf(display).
In the extension and the bookmarklet mywant-guiex
The control pill reads the viewer's character anyway (who they are, their colour). It writes the stored portable values onto the page's root as custom properties — --mwo-confirm … --mwo-muted, --mwo-surface, --mwo-outline, --mwo-ink, --mwo-radius, --mwo-cell-radius, --mwo-cell-gap, --mwo-label-transform, --mwo-label-weight, --mwo-label-size, --mwo-backdrop — and every menu the extension draws reads them with the grid look as the fallback:
.mwc-tile-rose { background: var(--mwo-danger, rgba(190,18,60,.9)); }
.mwc-actions { gap: var(--mwo-cell-gap, 0px); padding: var(--mwo-cell-gap, 0px); }Custom properties cross into shadow roots, so the pill (in its own shadow root) and the inspector's menus (in the page) follow the same values.
The pickers
Two places list every registered design and save the same choice: Settings → Overlay Design (useOverlayDesignChoice, which falls back to the browser when no character is chosen) and a character's Display tab on the Characters page. Both re-render when a design registers late:
useEffect(() => onOverlayDesignRegistered(() => force(v => v + 1)), []);
{listOverlayDesigns().map(design => (
<Pill
key={design.id}
label={design.name}
active={overlayDesignOf(d) === design.id}
onClick={() => put({ ext: withOverlayDesign(d, design.id) })}
/>
))}