Skip to content

Latest commit

 

History

History
214 lines (178 loc) · 9.71 KB

File metadata and controls

214 lines (178 loc) · 9.71 KB

@engineio/ui — agent reference

The Engine design system. This file ships inside the package, so it arrives in node_modules/@engineio/ui/AGENTS.md and versions with the code — point at it rather than copying its contents into a repo, or the copy goes stale.

Authoritative source: docs/brand/ENGINE-DESIGN-SYSTEM.md and ENGINE-BRAND.md in the engineio/engine repo. Where this file and those disagree, they win and this is a bug.

Setup

@import "tailwindcss";
@import "@engineio/ui/styles";
@source "../../../node_modules/@engineio/ui/dist";

The @source line is not optional and its absence is silent. Tailwind 4 does not scan node_modules, so without it the utility classes the components are written against are never generated. The tokens still land and any class that also appears in local source still works, so you get a half-styled app that reads like a component bug. Path is relative to the CSS file declaring it.

Importing

import { Button, Card, CardHeader } from "@engineio/ui"
import { Button } from "@engineio/ui/components/ui/button/index.js"
import * as Card from "@engineio/ui/components/ui/card/index.js"
import { EngineWordmark } from "@engineio/ui/components/brand/index.js"

Root barrel exports flat prefixed names (CardHeader). Subpath modules export Root/Header/Content, so import * as Card works. Both /index.js and the bare subpath resolve.

Components

Alert, Badge, Button, Card, Checkbox, Dialog, Input, Label, Popover, Progress, RadioGroup, ResponsiveDialog, Select, Separator, Skeleton, Switch, Table, Tabs, Textarea, Tooltip. Marks: EngineWordmark, EngineIcon.

Not shipped, on purpose: sonner, form, data-table, drawer, resizable, carousel, chart. Copy from the engine repo if needed. A standalone drawer is not on that list because ResponsiveDialog covers what one was wanted for.

Component Variants
Button default inverse outline ghost secondary link icon destructive success; sizes sm default lg icon icon-sm icon-lg
Badge default secondary outline partner success warning danger destructive
Alert default success warning danger destructive; optional onDismiss
Marks variant="primary" (white) or "secondary" (Off Black)

ResponsiveDialog

One panel, three surfaces. Below 768px it is a bottom sheet; above it, a centred modal, or an anchored popover with desktop="popover". Reach for it whenever a dialog has to survive a phone — a centred modal on a 390px viewport is the thing it exists to stop.

<ResponsiveDialog bind:open>
  <ResponsiveDialogTrigger>
    {#snippet child({ props })}<Button {...props}>Adjust limits</Button>{/snippet}
  </ResponsiveDialogTrigger>
  <ResponsiveDialogContent>
    <ResponsiveDialogHeader>
      <ResponsiveDialogTitle>Adjust limits.</ResponsiveDialogTitle>
      <ResponsiveDialogDescription>…</ResponsiveDialogDescription>
    </ResponsiveDialogHeader>
    …
    <ResponsiveDialogFooter>…</ResponsiveDialogFooter>
  </ResponsiveDialogContent>
</ResponsiveDialog>
Root prop
open bindable; survives the swap when the viewport crosses over
desktop dialog (default) or popover — what auto picks above the breakpoint
mode auto (default), or pin to sheet / dialog / popover
breakpoint px, default 768

Content takes showClose (defaults on, off for a popover), swipeToClose (sheet only), overlayClass, and align / side / sideOffset for the popover. ResponsiveDialogFooter is the part that earns its keep: stacked full-width actions on the sheet, a right-aligned row everywhere else, from the same markup.

Two things worth knowing. The sheet and the modal are the same bits-ui Dialog — focus trap, scroll lock, Escape, outside click — laid out differently; the popover is a different primitive and is not modal and does not trap focus, so do not put a destructive confirmation behind desktop="popover". And crossing the breakpoint with the panel open remounts it, because there is no honest way to morph a popover into a sheet. Anything mid-edit inside it wants state that lives above the panel.

There is one chip. Badge absorbed Tag — a soft tinted chip, not a solid pill. There is no Tag export and no solid badge variant: an opaque fill can only be correct on one surface, and badge fills are translucent so they read on the page, on a card and on a table tile alike.

Tokens

Use these names; never a literal.

colour     --color-background #0E0E0E   --color-foreground #FFFFFF
           --color-primary #FF006A      --color-primary-press #D60059
           --color-primary-300 #FF5C9B  (ink on a magenta tint)
           --color-primary-tint-12 / -24
           --color-card #161616         --color-popover #1C1C1C
           --color-grey-950 … --color-grey-050   (the only greys)
           --color-partner-yellow #FFDD00        (reserved, not in use)
status     --color-success #00C46A  --color-warning #FFB020  --color-danger #FF3B30
           each with -foreground (always Off Black) and -tint-12
           --color-destructive aliases danger
radii      --radius-tag 6  --radius-field 10  --radius-card-inner 10
           --radius-media 14  --radius-card 18  --radius-frame 26
           --radius-control 999
motion     --ease-brand  --ease-brand-out  --ease-brand-accelerate
           140ms controls · 220ms surfaces · 360–640ms reveals
           --animate-{overlay,dialog,sheet,popover}-{in,out}
type       --font-brand --font-condensed --font-extra-condensed --font-mono
           ROLES ONLY — no face ships here. Bind them in your repo (see below)
depth      --shadow-panel  --shadow-modal   (product chrome and modals only)
utility    `field` — the shared input skin, incl. the focus ring

Rules

  • Three brand colours: Off Black, Pure White, Magenta. One accent per surface, never two.
  • Status colour is functional. success/warning/danger report state. Never use them as a categorical palette — success for "slots" because green looked right spends the only signal they carry.
  • No off-palette colour. Not Tailwind's stock ramps, not a hex literal. CI fails on both.
  • Retired and unavailable: Originals Orange #FF6200, Sportsbook Blue #00CCFF. No accents, no charts, no status.
  • Ink on any status or accent fill is Off Black. White fails AA on all three.
  • Magenta text: never below 15px bold. Use --color-primary-300 on a tint.
  • No gradients. No light theme. No dark: variants — the dark palette is the only palette. No drop shadows on brand surfaces. No coloured borders, and no coloured left-edge accent to signal category or ownership.
  • Radii by role, and the outer frame is always larger than the inner panel.
  • Borders are 1px hairline or 1.5px container rule. Nothing else.
  • No emoji, anywhere. Only × for close and for footnotes.
  • Voice: declarative, British/AU spelling, headlines end in a full stop.
  • Icons: Lucide, 2px stroke, currentColor.

Extending

Do not fork a component to add a variant, and do not upstream a product-only variant. Every variant map is exported:

import { buttonVariants } from "@engineio/ui"
import { tv } from "tailwind-variants"

export const appButtonVariants = tv({
  extend: buttonVariants,
  variants: { variant: { drawer: "w-full justify-start rounded-none …" } },
})

Brand rules stay in the package; product variants stay in the product. Every primitive also passes class through cn, so <Button class="w-full" /> works without !important.

Known gaps

State these rather than working around them silently.

  1. No categorical palette. A per-topic hue set is an unmade brand decision.

  2. Two deviations from the brand document, both deliberate: Badge is one component where §6 specifies two, and Alert signals state with a coloured left-edge bar which §11 prohibits for category or ownership — state is a narrower reading, not an exemption.

  3. No mono face. JetBrains Mono is retired; --font-mono resolves to the brand face. Set it yourself if a surface genuinely needs character-cell alignment.

  4. No typeface ships with this package. It names the type roles and binds none of them. Each consumer declares its own @font-face blocks, keeps the binaries in its own repo, and overrides the tokens:

    @import "./fonts.css";          /* your own @font-face blocks */
    @import "@engineio/ui/styles";
    
    @theme {
      --font-brand: "ProximaNova", "Helvetica Neue", Arial, sans-serif;
      --font-condensed: "ProximaNovaCondensed", Arial, sans-serif;
      --font-extra-condensed: "ProximaNovaExtraCondensed", Arial, sans-serif;
    }

    This is a required setup step and it fails quietly — exactly like a missing @source line. Skip it and the tokens still resolve, nothing errors, and the app renders in the system stack. Generic-looking type is the symptom.

    Proxima Nova used to ship here. It is commercially licensed and this package is MIT and public, which is why it no longer does.

  5. Dialog, Popover and Tooltip do not animate. They are written against animate-in / fade-in-0 / zoom-in-95 / slide-in-from-top-2, which come from the tailwindcss-animate plugin. This package neither ships nor depends on it, so those class names generate no CSS — the panels appear and vanish instantly, and the gallery's "enter over 220ms, exit over 140ms" note is aspirational. The --animate-* tokens ResponsiveDialog uses are the replacement; the three older primitives have not been moved onto them yet.

  6. Engine Integration's accent is unsettled — build Integration in magenta; --color-partner-yellow exists but is not in use.