npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@twobots/ui-theme

v0.6.0

Published

Shared visual material for the twobots.dev card-game lineup — design tokens, generic card rendering, table-shell chrome, a dialog/sheet overlay shell, the rail header strip + utility-button + control, a ranked stopping-point screen, and a bot thinking-dot

Downloads

361

Readme

@twobots/ui-theme

Shared visual material for the twobots.dev card-game lineup — design tokens, generic playing-card rendering, table-shell chrome, a dialog/sheet overlay shell, the rail header strip + utility-button + control, a ranked stopping-point screen, and a bot thinking-dots indicator. Ships raw source; consuming apps compile it with their own Vite/TypeScript pipeline.

Extracted from Tongits TwoBots after a sibling game hand-retyped these same values once already and it still took a live side-by-side screenshot comparison to catch what a per-property copy missed. This package exists so "matching the reference game's look" is an import, not a re-authoring exercise every sibling game repeats.

Sibling to @twobots/game-kit (non-visual game logic — a bot-naming pool and feedback-sending primitives) — kept as a separate package since neither belongs under a name that says "ui-theme."

What's in it, and what deliberately isn't

In scope — presentation material with no game logic behind it:

  • tokens.css — colors, the woven-mat texture tokens, shadows, card geometry, a shared z-index scale, the splash/title-screen palette.
  • table-surface.css — the fixed, full-bleed woven background layer.
  • card/ — CardFace, SuitSprite, and the generic portion of card rendering (rank/pip/back; not any one game's own card treatment).
  • overlay.css — .overlay-backdrop / .overlay-panel (+ .overlay-panel--elevated), the dimmed, centred dialog shell shared by every reference screen and settlement/reveal sheet. CSS only — each app keeps its own focus management, Escape handling, and panel content.
  • rail.css — .rail, the 3-slot header-strip grid (leading controls | flexible ticker/status | trailing controls) both apps already had the same column math for; and .chrome-button, the small utility-button look (Help/Report/Rules/Feedback pills) andarta's own CSS had already copied from karu's .rail__help by hand. No size opinion beyond the pill's own common one — a button that needs a different size (karu's action bar, deliberately large 48px primary tap targets rather than a small utility pill) overrides just that, keeping the shared look.
  • rail/ — RailButton, a control that shows a word on a roomy viewport and a glyph on a narrow one (Rules/House/Leave), karu's own real version, ported wholesale after andarta had already copied it by hand. It emits .rail__help/.rail__word/.rail__icon — classes this package does not style; rail.css's .chrome-button supplies the look, and each app's own board.css still owns the narrow-viewport word/icon swap for .rail__help. A control that opens a dialog gets aria-haspopup="dialog" by default (dialog={false} for one that just navigates, e.g. a "leave" action).
  • match-over/ — MatchOverScreen, the stopping-point screen every reference game already converged on independently (karu's own real version, ported wholesale after andarta had already copied it by hand). Takes plain data — a headline string, a because string, an already-ranked rows array, onDone — and renders the shell: dialog semantics, the ranked list, a focus-on-mount Done button, a footer. It does not sort rows; the caller ranks its own seats first. match-over.css styles only the ranked-list family (.match-over__list/__row/ __place/__name/__score) — the component also emits splash/splash__sheet splash__sheet--title/splash__title splash__title--table/splash__lede/splash__play/ splash__foot splash__foot--nav, which this package does not style, because both apps' title screens already own that shell today, and differ from each other (alignment, footer layout, short-viewport behavior) — shipping CSS for those classes here would collide with whichever app's own rule loads second, at equal specificity. A new consumer supplies that shell CSS itself, the same way both current apps already do for their own title screens.
  • thinking/ — ThinkingDots, three staggered dots for a bot turn's pause (the shared @twobots/game-kit/timing delay can run several seconds — long enough that a bare status line can start to feel like nothing is happening). Dots over a spinner deliberately: a spinner reads as "the app is loading/blocked," the opposite of what a bot's own turn is. No props — nothing varies per caller — and aria-hidden, since the meaning already lives in whichever status text each app renders alongside it. thinking.css's dots use currentColor, so they carry no color opinion of their own, and a prefers-reduced-motion fallback stills the animation without removing the cue (three static dots, not none).

Out of scope, stays in each app — anything with real game rules or a real layout decision behind it: melds, discard piles, settlement sheets, a widow slot, and the action console itself. karu's action console is a fixed 2-row grid whose action row deliberately never wraps ("wrapping would push the board up mid-turn") and scrolls horizontally instead; andarta's is a plain flex-wrap row. Same name, genuinely different layout intent — sharing the mechanism here would be a real behavior change on whichever side didn't already work that way, not a mechanical dedup. This is shared material, not a shared game-UI framework. A game that needs a special face-down treatment (a secret-set reveal, for instance) wraps CardFace rather than CardFace growing a prop for it.

MatchOverScreen is a deliberate, narrow exception to "CSS only, each app keeps its own focus management" above: it owns role="dialog" and a focus-on-mount Done button, because the whole point of extracting it was that both games' versions of exactly that behavior had already converged, "wholesale," independently — the same signal that justified rail.css and RailButton. What it does not own is any outcome-narration logic: headline/because arrive as plain strings, computed by each app's own headline()/because() (karu) or matchHeadline()/matchBecause() (andarta), which read real per-game match-end rules (karu's outcome union has a 'floor' case andarta has no equivalent of) and stay fully local. This is a component, not a game-UI framework, the same distinction the action-console call above draws.

Using it

npm install @twobots/ui-theme

Pin an exact version rather than ^/latest — deliberately, not by default. Two apps floating on "whatever's newest" is exactly the kind of silent divergence this package exists to prevent.

import '@twobots/ui-theme/tokens.css'
import '@twobots/ui-theme/table-surface.css'
import '@twobots/ui-theme/card.css'
import '@twobots/ui-theme/overlay.css'
import '@twobots/ui-theme/rail.css'
import '@twobots/ui-theme/match-over.css'
import '@twobots/ui-theme/thinking.css'
import { CardFace, SuitSprite, type Card } from '@twobots/ui-theme/card'
import { RailButton } from '@twobots/ui-theme/rail'
import { MatchOverScreen, type MatchOverRow } from '@twobots/ui-theme/match-over'
import { ThinkingDots } from '@twobots/ui-theme/thinking'

A stopping-point screen supplies its own already-ranked rows and outcome text — the shell (dialog, list, Done button, footer) is otherwise generic:

<MatchOverScreen
  headline={headline(outcome, names, you)}
  because={because(outcome, target, hands)}
  rows={ranked.map(({ seat, score }) => ({ id: seat, name: names[seat], score, isYou: seat === you }))}
  onDone={onDone}
/>

A dialog screen combines overlay-backdrop/overlay-panel with the app's own dialog markup:

<div className="overlay-backdrop">
  <div className="my-screen overlay-panel" role="dialog" aria-modal="true" aria-labelledby="…">
    {/* app-owned: header, close button, focus management, Escape handling, content */}
  </div>
</div>

Import tokens.css first, before any of your own app's stylesheets — same reasoning as in karu: two stylesheets defining the same custom property at equal specificity is a cascade-order lottery.

CardFace's card prop expects a plain string in `${Rank}${Suit}` form (e.g. "AS", "10H"). A consuming app's own Card type only needs to be structurally the same shape to pass straight through — no conversion needed if your engine already uses this notation.

License

MIT — see LICENSE.