|
| 1 | +# @engineio/ui — agent reference |
| 2 | + |
| 3 | +The Engine design system. This file ships inside the package, so it arrives in |
| 4 | +`node_modules/@engineio/ui/AGENTS.md` and versions with the code — point at it |
| 5 | +rather than copying its contents into a repo, or the copy goes stale. |
| 6 | + |
| 7 | +Authoritative source: `docs/brand/ENGINE-DESIGN-SYSTEM.md` and |
| 8 | +`ENGINE-BRAND.md` in the `engineio/engine` repo. Where this file and those |
| 9 | +disagree, **they win and this is a bug**. |
| 10 | + |
| 11 | +## Setup |
| 12 | + |
| 13 | +```css |
| 14 | +@import "tailwindcss"; |
| 15 | +@import "@engineio/ui/styles"; |
| 16 | +@source "../../../node_modules/@engineio/ui/dist"; |
| 17 | +``` |
| 18 | + |
| 19 | +**The `@source` line is not optional and its absence is silent.** Tailwind 4 |
| 20 | +does not scan `node_modules`, so without it the utility classes the components |
| 21 | +are written against are never generated. The tokens still land and any class |
| 22 | +that also appears in local source still works, so you get a half-styled app |
| 23 | +that reads like a component bug. Path is relative to the CSS file declaring it. |
| 24 | + |
| 25 | +## Importing |
| 26 | + |
| 27 | +```svelte |
| 28 | +import { Button, Card, CardHeader } from "@engineio/ui" |
| 29 | +import { Button } from "@engineio/ui/components/ui/button/index.js" |
| 30 | +import * as Card from "@engineio/ui/components/ui/card/index.js" |
| 31 | +import { EngineWordmark } from "@engineio/ui/components/brand/index.js" |
| 32 | +``` |
| 33 | + |
| 34 | +Root barrel exports flat prefixed names (`CardHeader`). Subpath modules export |
| 35 | +`Root`/`Header`/`Content`, so `import * as Card` works. Both `/index.js` and the |
| 36 | +bare subpath resolve. |
| 37 | + |
| 38 | +## Components |
| 39 | + |
| 40 | +Alert, Badge, Button, Card, Checkbox, Dialog, Input, Label, Popover, Progress, |
| 41 | +RadioGroup, Select, Separator, Skeleton, Switch, Table, Tabs, Textarea, Tooltip. |
| 42 | +Marks: `EngineWordmark`, `EngineIcon`. |
| 43 | + |
| 44 | +Not shipped, on purpose: `sonner`, `form`, `data-table`, `drawer`, `resizable`, |
| 45 | +`carousel`, `chart`. Copy from the engine repo if needed. |
| 46 | + |
| 47 | +| Component | Variants | |
| 48 | +| --- | --- | |
| 49 | +| Button | `default` `inverse` `outline` `ghost` `secondary` `link` `icon` `destructive` `success`; sizes `sm` `default` `lg` `icon` `icon-sm` `icon-lg` | |
| 50 | +| Badge | `default` `secondary` `outline` `partner` `success` `warning` `danger` `destructive` | |
| 51 | +| Alert | `default` `success` `warning` `danger` `destructive`; optional `onDismiss` | |
| 52 | +| Marks | `variant="primary"` (white) or `"secondary"` (Off Black) | |
| 53 | + |
| 54 | +There is **one** chip. Badge absorbed Tag — a soft tinted chip, not a solid |
| 55 | +pill. There is no `Tag` export and no `solid` badge variant: an opaque fill can |
| 56 | +only be correct on one surface, and badge fills are translucent so they read on |
| 57 | +the page, on a card and on a table tile alike. |
| 58 | + |
| 59 | +## Tokens |
| 60 | + |
| 61 | +Use these names; never a literal. |
| 62 | + |
| 63 | +``` |
| 64 | +colour --color-background #0E0E0E --color-foreground #FFFFFF |
| 65 | + --color-primary #FF006A --color-primary-press #D60059 |
| 66 | + --color-primary-300 #FF5C9B (ink on a magenta tint) |
| 67 | + --color-primary-tint-12 / -24 |
| 68 | + --color-card #161616 --color-popover #1C1C1C |
| 69 | + --color-grey-950 … --color-grey-050 (the only greys) |
| 70 | + --color-partner-yellow #FFDD00 (reserved, not in use) |
| 71 | +status --color-success #00C46A --color-warning #FFB020 --color-danger #FF3B30 |
| 72 | + each with -foreground (always Off Black) and -tint-12 |
| 73 | + --color-destructive aliases danger |
| 74 | +radii --radius-tag 6 --radius-field 10 --radius-card-inner 10 |
| 75 | + --radius-media 14 --radius-card 18 --radius-frame 26 |
| 76 | + --radius-control 999 |
| 77 | +motion --ease-brand --ease-brand-out --ease-brand-accelerate |
| 78 | + 140ms controls · 220ms surfaces · 360–640ms reveals |
| 79 | +type --font-brand (Proxima Nova) --font-mono (JetBrains Mono, see gaps) |
| 80 | +depth --shadow-panel --shadow-modal (product chrome and modals only) |
| 81 | +utility `field` — the shared input skin, incl. the focus ring |
| 82 | +``` |
| 83 | + |
| 84 | +## Rules |
| 85 | + |
| 86 | +- **Three brand colours**: Off Black, Pure White, Magenta. One accent per |
| 87 | + surface, never two. |
| 88 | +- **Status colour is functional.** `success`/`warning`/`danger` report state. |
| 89 | + Never use them as a categorical palette — `success` for "slots" because green |
| 90 | + looked right spends the only signal they carry. |
| 91 | +- **No off-palette colour.** Not Tailwind's stock ramps, not a hex literal. CI |
| 92 | + fails on both. |
| 93 | +- **Retired and unavailable**: Originals Orange `#FF6200`, Sportsbook Blue |
| 94 | + `#00CCFF`. No accents, no charts, no status. |
| 95 | +- **Ink on any status or accent fill is Off Black.** White fails AA on all three. |
| 96 | +- **Magenta text**: never below 15px bold. Use `--color-primary-300` on a tint. |
| 97 | +- No gradients. No light theme. No `dark:` variants — the dark palette is the |
| 98 | + only palette. No drop shadows on brand surfaces. No coloured borders, and no |
| 99 | + coloured left-edge accent to signal category or ownership. |
| 100 | +- Radii by role, and the outer frame is always larger than the inner panel. |
| 101 | +- Borders are 1px hairline or 1.5px container rule. Nothing else. |
| 102 | +- **No emoji, anywhere.** Only `×` for close and `✱` for footnotes. |
| 103 | +- Voice: declarative, British/AU spelling, headlines end in a full stop. |
| 104 | +- Icons: Lucide, 2px stroke, `currentColor`. |
| 105 | + |
| 106 | +## Extending |
| 107 | + |
| 108 | +Do not fork a component to add a variant, and do not upstream a product-only |
| 109 | +variant. Every variant map is exported: |
| 110 | + |
| 111 | +```ts |
| 112 | +import { buttonVariants } from "@engineio/ui" |
| 113 | +import { tv } from "tailwind-variants" |
| 114 | + |
| 115 | +export const appButtonVariants = tv({ |
| 116 | + extend: buttonVariants, |
| 117 | + variants: { variant: { drawer: "w-full justify-start rounded-none …" } }, |
| 118 | +}) |
| 119 | +``` |
| 120 | + |
| 121 | +Brand rules stay in the package; product variants stay in the product. Every |
| 122 | +primitive also passes `class` through `cn`, so `<Button class="w-full" />` works |
| 123 | +without `!important`. |
| 124 | + |
| 125 | +## Known gaps |
| 126 | + |
| 127 | +State these rather than working around them silently. |
| 128 | + |
| 129 | +1. **No categorical palette.** A per-topic hue set is an unmade brand decision. |
| 130 | +2. **Two deviations from the brand document**, both deliberate: Badge is one |
| 131 | + component where §6 specifies two, and Alert signals state with a coloured |
| 132 | + left-edge bar which §11 prohibits for *category or ownership* — state is a |
| 133 | + narrower reading, not an exemption. |
| 134 | +3. **JetBrains Mono is not loaded.** `--font-mono` falls back to the platform |
| 135 | + monospace, so every mono surface is off-brand until the binary ships. |
| 136 | +4. **Proxima Nova is commercially licensed** and ships inside this package. |
| 137 | +5. **Engine Integration's accent is unsettled** — build Integration in magenta; |
| 138 | + `--color-partner-yellow` exists but is not in use. |
0 commit comments