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

@flashmandu/app-bridge-ui

v0.5.0

Published

Theme kit for Flashmandu embedded apps — CSS tokens and framework-agnostic component classes that visually match the host platform (the Polaris-equivalent).

Downloads

475

Readme

@flashmandu/app-bridge-ui

The theme kit for Flashmandu embedded apps. CSS design tokens plus a tiny framework-agnostic stylesheet that mirrors the host platform's Flux UI look — the Polaris-equivalent for apps running inside the App Bridge iframe.

The package ships two layers:

  1. CSSindex.css, which bundles fonts.css + tokens.css + components.css in that order. This is the portable contract; PHP, Python and plain-HTML apps get the identical look by rendering the same .fm-* class names server-side. Every selector in components.css is .fm--prefixed, so it is safe to drop into a page you do not fully own. The unprefixed aliases that predate the contract (.btn-primary, .card-header, .app-table, .sortable-ghost, …) are a separate opt-in file, legacy.css — import it only if your markup already renders those names (see GUIDELINES §12).
  2. React@flashmandu/app-bridge-ui/react, typed wrappers that carry no styles of their own. Every visual decision lives in the stylesheet, so an app cannot drift from the platform look. Named exports only, so bundlers tree-shake what you do not use. There is no runtime CSS-in-JS.

Read GUIDELINES.md before building a page — it carries the rules (no desktop page heading, skeleton-first loading, the row-action colour contract) that keep an app looking native.

Install

npm install @flashmandu/app-bridge-ui

Usage

dist/index.css is the ONE sanctioned CSS import. Import it once, at the root of your app.

Do not import tokens.css or components.css directly. tokens.css names --fm-font-sans: 'Instrument Sans', … but the @font-face rules that name resolves to live in fonts.css; skipping index.css means your app silently renders the system font instead of Instrument Sans.

Next.js (App Router)

In app/layout.tsx:

import "@flashmandu/app-bridge-ui/dist/index.css";

Next.js (Pages Router) / Vite / Remix

In your root _app.tsx / main.tsx / root.tsx:

import "@flashmandu/app-bridge-ui/dist/index.css";

Vue (Nuxt 3)

In nuxt.config.ts:

export default defineNuxtConfig({
  css: ["@flashmandu/app-bridge-ui/dist/index.css"],
});

PHP-native / plain HTML

Copy dist/index.css, dist/fonts.css and the dist/fonts/ directory into your app's asset folder (their relative paths must stay side by side — index.css @imports ./fonts.css, which in turn loads ./fonts/*.woff2), or pull them from a CDN (jsDelivr/unpkg), and link the one file in <head>:

<link rel="stylesheet" href="/css/index.css">
<!-- Only if your markup renders the pre-contract aliases (.btn-primary, .sortable-*, …): -->
<link rel="stylesheet" href="/css/legacy.css">

Optional: auto-inject entry

For convenience, importing the JS entry will idempotently inject the stylesheet into the document head (browser only; no-op during SSR):

import "@flashmandu/app-bridge-ui";

This is sugar — the explicit CSS imports above are preferred for build-pipeline determinism.

Tokens (--fm-*)

All tokens are defined on :root. Dark overrides apply automatically via @media (prefers-color-scheme: dark), and can be forced on a subtree with data-fm-theme="dark" / "light".

Colors

| Token | Light | Dark | Notes | | --- | --- | --- | --- | | --fm-bg | #ffffff | #0b1220 | App canvas | | --fm-surface | #ffffff | #111827 | Cards, panels | | --fm-surface-muted | #f8fafc | #1f2937 | Subtle fills (slate-50) | | --fm-text | #0f172a | #f1f5f9 | Primary text (slate-900) | | --fm-text-muted | #64748b | #94a3b8 | Secondary text (slate-500) | | --fm-border | #e2e8f0 | #1f2937 | Hairlines (slate-200) | | --fm-primary | #09090b | #3b82f6 | Zinc-950 black action (blue-500 in dark, so it stays visible) | | --fm-primary-hover | #27272a | #60a5fa | Zinc-800 | | --fm-primary-active | #18181b | #2563eb | Zinc-900 | | --fm-primary-contrast | #ffffff | #0b1220 | Text on primary | | --fm-primary-subtle | #f4f4f5 | rgba(59,130,246,.12) | Tinted backgrounds | | --fm-primary-ring | rgba(9,9,11,.35) | rgba(59,130,246,.45) | Focus ring | | --fm-success | #16a34a | #4ade80 | Green-600 | | --fm-danger | #dc2626 | #f87171 | Red-600 | | --fm-warning | #d97706 | #fbbf24 | Amber-600 | | --fm-success-subtle / --fm-danger-subtle / --fm-warning-subtle | tinted bg | rgba | Status callouts |

Radii

| Token | Value | Tailwind equiv | | --- | --- | --- | | --fm-radius-sm | 0.375rem | rounded-md | | --fm-radius-md | 0.5rem | rounded-lg | | --fm-radius-lg | 0.75rem | rounded-xl (platform default) | | --fm-radius | var(--fm-radius-lg) | default | | --fm-radius-pill | 9999px | pill |

Typography

| Token | Value | | --- | --- | | --fm-font-sans | ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, … | | --fm-font-mono | ui-monospace, SFMono-Regular, Menlo, Consolas, … | | --fm-text-xs / --fm-text-sm / --fm-text-base / --fm-text-lg | 0.75rem / 0.875rem / 1rem / 1.125rem | | --fm-leading | 1.5 |

Spacing

--fm-space-1--fm-space-80.25rem, 0.5rem, 0.75rem, 1rem, 1.25rem, 1.5rem, 2rem.

Shadow & motion

--fm-shadow-sm, --fm-shadow, --fm-shadow-md and --fm-transition (150ms ease).

Component classes

Opt-in utility classes. All consume the --fm-* tokens, so theme + dark-mode overrides propagate for free.

| Class | Purpose | | --- | --- | | .fm-btn (+ --primary / --subtle / --ghost / --danger) | Buttons — primary is zinc-900/black | | .fm-btn--view / --edit / --delete (+ .fm-btn--icon) | Row actions: teal / blue / red, fixed meaning | | .fm-index-shell, .fm-index-table, .fm-index-xbar | Index shell: clipped viewport, sticky thead, scrollbar strip | | .fm-filter-bar, .fm-filter-chip | Filter bar + active-filter chips | | .fm-skeleton, .fm-skeleton-table, .fm-skeleton-page | Loading placeholders | | .fm-tabs / .fm-tab | Segmented control (active segment filled black) | | .fm-mobile-page-header | The only in-page heading — hidden at lg and up | | .fm-range | Slider — always black, never indigo/purple | | .fm-card (+ .fm-card__title, .fm-card__subtitle) | Surface panels | | .fm-input, .fm-textarea, .fm-select | Form controls | | .fm-field (+ .fm-field__label, __hint, __error) | Label + control + helper | | .fm-callout (+ --info / --success / --warning / --danger) | Banner alerts | | .fm-table | Styled <table> with header, hover, dividers | | .fm-surface, .fm-text-muted | Primitives |

Example:

<div class="fm-card">
  <h2 class="fm-card__title">Order #1042</h2>
  <p class="fm-card__subtitle">Awaiting confirmation</p>

  <button class="fm-btn fm-btn--primary">Confirm</button>
  <button class="fm-btn fm-btn--subtle">Cancel</button>
</div>

<div class="fm-callout fm-callout--warning">
  Inventory for <strong>Chamomile 250g</strong> is below the reorder point.
</div>

Dark mode

Two strategies, both supported:

  1. Automaticprefers-color-scheme: dark is honored by default. No markup required.
  2. Forced — set data-fm-theme="dark" (or "light") on any ancestor element (often <html>). This wins over the OS preference, useful when the host shell dictates the theme.

To override a single token in your own app, redefine it after importing tokens.css:

:root {
  --fm-primary: #0f766e; /* teal-700, for example */
}

Purple/violet is deliberately off the table — the platform palette is green/black, and a purple accent reads as a different product.

React components

import {
  IndexTable, IndexShell, FilterBar, SegmentedControl, Pagination,
  MobilePageHeader, Breadcrumbs, SkeletonPage, SkeletonTable, SkeletonText,
  Button, IconButton, Modal, Dropdown, ControlledTabs, useConfirm,
} from "@flashmandu/app-bridge-ui/react";

IndexTable / IndexShell — a faithful port of the host's <x-ui.index-shell>. The page scrolls normally, the <thead> pins, and a wide table travels sideways from a scrollbar strip parked above the pagination. The viewport is overflow-x: clip (never auto — that would make it a scroll container and unpin the header), and the shell performs zero React renders while scrolling: scroll handlers write CSS custom properties through refs. Adds row selection with a floating bulk-actions bar, controlled sorting, and a pinnable first column.

FilterBar — the platform's filter section. One bordered group holding an attached Filter ▾ button with an active-count badge, the search box, its X and the pin; a menu of auto-derived quick filters plus one row per field; one popover at a time; removable chips whose label reopens the filter that made them; relative-date presets; is / is not operators on multiselects; and defaults — save the current filter set as this list's default and restore it on the next visit, exactly as every host list does. Fully controlled: the route (or URL) stays the source of truth.

SkeletonsSkeletonPage / SkeletonTable / SkeletonText. Every route and table paints a skeleton first; no embedded route may show a blank pane.

Mobile chromeMobilePageHeader and Breadcrumbs render below lg only, via a media query rather than a JS breakpoint. Desktop title, trail and actions belong to the host command bar (SET_PAGE). The old PageHeader was deleted in 0.3.0.

AccessibilityModal traps focus and closes on Escape, Dropdown implements the ARIA menu-button pattern with arrow-key navigation, ControlledTabs implements the tabs pattern with roving tabindex, and every icon-only button requires a label.

Development

npm run build      # CSS copy + Tailwind + tsc
npm test           # vitest (jsdom + Testing Library)
npm run typecheck

Releasing

bin/release.sh 0.3.1

Bumps package.json, tags v0.3.1, and pushes. The Release workflow builds, publishes to npm via OIDC trusted publishing, and opens the GitHub release.