@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
Maintainers
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:
- CSS —
index.css, which bundlesfonts.css+tokens.css+components.cssin 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 incomponents.cssis.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). - 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-uiUsage
dist/index.css is the ONE sanctioned CSS import. Import it once, at the root of your app.
Do not import
tokens.cssorcomponents.cssdirectly.tokens.cssnames--fm-font-sans: 'Instrument Sans', …but the@font-facerules that name resolves to live infonts.css; skippingindex.cssmeans 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-8 → 0.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:
- Automatic —
prefers-color-scheme: darkis honored by default. No markup required. - 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.
Skeletons — SkeletonPage / SkeletonTable / SkeletonText. Every route
and table paints a skeleton first; no embedded route may show a blank pane.
Mobile chrome — MobilePageHeader 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.
Accessibility — Modal 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 typecheckReleasing
bin/release.sh 0.3.1Bumps package.json, tags v0.3.1, and pushes. The Release workflow builds, publishes to npm via OIDC trusted publishing, and opens the GitHub release.
