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

@softure-ai/theme-flat-paint

v0.3.0

Published

SOFTURE AI Flat Paint theme: near-black or white, cards with one hairline, one heavy grotesque and one flat lime accent; the tokens, the @softure-ai/ui bridge, the page base and the look of the ui components.

Readme

@softure-ai/theme-flat-paint

The Flat Paint design system as a package: near-black or white, cards with one hairline, one heavy grotesque (Ubuntu) and one accent, lime, laid on flat without light, shadow or gradient. Built on @softure-ai/theme-kit like @softure-ai/theme-notion, for an app whose components are @softure-ai/ui's: the theme ships the tokens, the bridge that points ui's --sft-* tokens at them, the page base and the class maps where the look differs from ui's defaults. It ships no components of its own.

Install and wire up (Next.js App Router)

npm i @softure-ai/theme-flat-paint @softure-ai/ui tailwindcss @tailwindcss/postcss
/* app/globals.css */
@import "@softure-ai/ui/styles.css";                 /* first: it declares the layer order */
@import "tailwindcss";
@import "@softure-ai/theme-flat-paint/theme.css";     /* tokens, utilities, dark:, the look maps as sources */
@import "@softure-ai/theme-flat-paint/sft-bridge.css"; /* ui's --sft-* tokens on the theme's roles */
@import "@softure-ai/theme-flat-paint/base.css";      /* body on the theme's background, text colour and font */
@import "@softure-ai/theme-flat-paint/page.css";      /* scheme, number classes, motion, reduced motion */

Every stylesheet comes from the theme. base.css (and preflight.css, maplibre.css) are the kit's (@softure-ai/theme-kit), re-exported by the theme: they resolve the kit from the theme's own dependency, so the app declares only the theme and gets the kit version the theme was built against, in any package manager layout.

Fonts. The theme names Ubuntu and Ubuntu Mono (the latin-ext subset carries Polish letters). An app that loads them with next/font points the stacks at its variables after the imports:

@theme inline {
  --font-sans: var(--font-ubuntu), system-ui, sans-serif;
  --font-mono: var(--font-ubuntu-mono), ui-monospace, monospace;
}

Schemes. Light by default, dark by the system setting; data-theme="light" | "dark" (@softure-ai/ui's ThemeScript and ThemeSwitch) or the classes light / dark choose one for the page or for a band of it: a black hero on a light page is <section class="dark bg-ink">, a white band on a dark one <section class="light bg-paper">. Each scope recomputes the roles and the ui bridge, so a ui component inside a band takes the band's scheme.

import { Button, Card, Stat } from "@softure-ai/ui";
import { BUTTON_VARIANT_LOOK, CARD_TITLE_LOOK, ROLE_TONE, STAT_VALUE_LOOK } from "@softure-ai/theme-flat-paint";

<Card title="Retirement date" variant="lead" accent={ROLE_TONE.accessible} classNames={{ title: CARD_TITLE_LOOK.card }}>
  <Stat label="Net worth" value="2,480,000" size="lg" classNames={{ value: STAT_VALUE_LOOK.lg }} />
</Card>
<Button variant="primary" className={BUTTON_VARIANT_LOOK.primary}>Save</Button>

Rules, in short

  • Components paint with roles only: background, surface, surface-raised, border, line-strong, foreground, muted, accent, accent-fill, on-accent, accessible, locked, debt. The palette (ink, lime, lime-deep, paper, graphite, …) feeds the roles and the white and black bands. Tailwind's default palette is cleared.
  • Two equal schemes, one geometry: check every screen in both, at 390 and 1440 px.
  • Lime is paint (accent-fill): the primary button, a chart flag, a switch or checkbox that is on, an active tab's underline, a selected row's bar, always with black on-accent. On white it has 1.27:1, so in the light scheme it is never text or a thin line; accent text is accent (lime-deep in light, lime in dark).
  • A second carrier: every state lime carries has another one (bold, the black knob and an accent line, a raised surface, a line-strong outline).
  • Data colour = meaning: accessible (sea green), locked (amber), debt (crimson: debt, missing money, error, deletion). Lime never paints a data series. ROLE_TONE maps the three roles to ui's success, warning, danger.
  • position-* colours are a palette for user-chosen series and markers only, never a button, a card or text.
  • Flat, no exceptions: depth is the step background → surface → surface-raised and one 1 px border. No shadow, glow, gradient or backdrop-filter; the overlay is ink at 60 % without blur. The only "shadows" are lines: shadow-selected-row (a 3 px bar) and shadow-shot-frame (a 1 px frame). A product shot takes the shot-frame class from page.css (block, full width, rounded-card, that frame, cropped from the top, paper behind it).
  • At most one lead card and one primary button per screen.
  • Type: one face, Ubuntu (400 text, 500 labels and buttons, 700 headings and figures), Ubuntu Mono only in columns of digits. Headings text-hero 88, text-display 60, text-headline 40, text-page and text-section 30, text-band 26, text-subsection 18; text text-body 16, text-body-sm and text-label 14, text-caption 12, text-eyebrow 11 (uppercase, only a data label above a value). Each style carries its line height and tracking: write no leading-* or tracking-* next to it. Numbers: figure (one hero figure per block), num (in a row or a sentence), tabular (mono, only digits compared down a column).
  • Space: p-card 24, p-card-panel 20, mt-section 96, w-hairbar 3, px-control-x 12, px-button-x 16; controls h-control-sm 32, h-control-md 40, h-control-lg 48, targets size-touch 44; widths max-w-measure 42 rem, max-w-panel 80 rem, max-w-apex 96 rem.
  • Radii: rounded-card 12 (card, dialog), rounded-nested 8 (always smaller than its parent), rounded-control 8 (button, field, segment gutter), rounded-segment 6, rounded-checkbox 5, rounded-flag 4, rounded-pill.
  • Motion: --duration-fast 160 ms (hover, press, focus; every bare transition), --duration-base 200 ms (reveal), --duration-slow 240 ms (a panel, a step); easings ease-out-soft (default), ease-out-snap (reveals), ease-out-settle (a result settling, never a hover). Nothing overshoots, nothing animates width or height, a press is scale(0.97), and the still state is the final state.
  • Focus: a 2 px accent ring 2 px off the element on a background gap; a field takes an accent line and a 1 px ring. accent holds 3:1 on every surface of both schemes. An error stands at its field: a debt line and the message under it.

Look maps

Class strings for className / classNames of the ui components, holding only what differs from ui's defaults (an app class beats ui's @layer softure). They are literals, so theme.css lists their folder as a Tailwind source and the app adds no @source. A state that ui draws per item (a done checklist row, the current step) is a variant on the state attribute ui sets on the item (in-data-done:, in-data-[state=current]:), so one class string covers every state. App-specific motion (a wizard's step stamp) stays in the app.

| Export | For | | --- | --- | | BUTTON_VARIANT_LOOK | Button, ButtonLink, ButtonAnchor className by variant (ink, ink-outline on a white band) | | getIconButtonLook(size, tone), ICON_BUTTON_SIZE_LOOK, ICON_BUTTON_TONE_LOOK | IconButton root | | CARD_ROOT_LOOK, CARD_TITLE_LOOK, CARD_STEP_LOOK | Card slots root, title (card or section), step | | STAT_VALUE_LOOK, STAT_SECONDARY_LOOK | Stat slots value (by size) and secondary | | FIELD_LABEL_LOOK | Field slot label, and a label built next to a field | | SWITCH_INPUT_LOOK, SWITCH_TRACK_LOOK, SWITCH_KNOB_LOOK | a switch drawn over a native checkbox | | CHECKBOX_LOOK | Checkbox slots with unstyled | | getModalLook(width), MODAL_LOOK, STANDING_PANEL_LOOK | Modal and StandingPanel classNames: flat veil, entrance motion, no shadow, title on text-page | | SEGMENTED_GROUP_LOOK, SEGMENT_ACTIVE_LOOK, SEGMENT_IDLE_LOOK, SEGMENT_ACTIVE_WHEN_CURRENT_LOOK | a segmented control, and ui's SegmentedNav | | HINT_LOOK | Hint as a whole appearance (classNames and the 8 px triggerGap): <Hint {...HINT_LOOK}> | | CHECKLIST_LOOK | Checklist classNames: hairline list, done marker and label (by ui's data-done) | | STEPPER_LOOK | Stepper classNames: marker per state (by ui's data-state), label hidden on phones | | MENU_PANEL_LOOK | DisclosureMenu, AccountMenu, MobileMenu slots trigger and panel | | COLOR_FIELD_LOOK | ColorField slots preset (square, framed when picked) and swatch (the native picker) | | SEGMENTED_CONTROL_LOOK, THEME_SWITCH_LOOK | SegmentedControl and ThemeSwitch slots with unstyled: the checked segment without lime, the legend hidden (spread the map and set legend to show it) | | SEGMENT_ACTIVE_WHEN_CHECKED_LOOK | the active segment as has-checked: variants, for an option wrapping a native radio | | DATA_TABLE_LOOK | an app's own <table>: headerCell, row, cell, amountCell, emptyCell, selectedRow, stickyColumn | | getChartFlagLook(look, size), CHART_FLAG_VARIANT_LOOK, CHART_FLAG_SIZE_LOOK | @softure-ai/charts ChartFlag variant="flag" className: accent (lime, ink text) or deep (a variant date, white text), by size | | CHART_PIN_LOOK | ChartPin classNames: dot (fill by variant, plus size by size), ring by the chart's surface, line by surface | | CHART_ROLE_TONE | the charts tone (ChartToneName) of each role: accessible, locked, debt, accent, muted | | RADIO_LOOK | a radio's box, input (the drawn circle) and mark (a 12 px check icon): a lime disc with an accent line and a black check when checked, as the checkbox | | CHOICE_ROW_LOOK, CHOICE_CARD_LOOK | a single choice drawn as a list row (lime bar when checked) or a bordered tile: label, control, body, title, description; markup below | | UPDATE_PROMPT_LOOK, INSTALL_BAR_LOOK | ui's UpdatePrompt and InstallBar classNames: flat panel rising in (prompt-enter), title on subsection, accent icon, borderless primary action | | LAUNCH_SCREEN_LOOK, ROUTE_LOADING_LOOK | ui's LaunchScreen and RouteLoading classNames: name on band, bar on a border track, percentage as a num; TopLoadingBar needs none (its line is accent) | | MINI_PROGRESS_LOOK | ui's MiniProgress classNames: flag radius, track at 8 % of the text colour, figure as a caption num |

A data table (ui has no table component): column headers 12 px 500 muted, 44 px rows with a border hairline, amounts right-aligned in tabular with the last column bold, an empty cell — in muted, the selected row on surface-raised with the lime bar and a bold year, and a column stuck to the right with a hairline on its left below xl. Add a slot on top of another with cn, so the later one wins:

const t = DATA_TABLE_LOOK;
<tr className={cn(t.row, isSelected && t.selectedRow)}>
  <th scope="row" className={t.cell}>2031</th>
  <td className={cn(t.amountCell, amount === null && t.emptyCell)}>{amount ?? "—"}</td>
  <td className={cn(t.amountCell, t.stickyColumn)}>{total}</td>
</tr>

The pin's ring carries its contrast (lime on white does not hold 3:1), so it follows the surface the chart sits on: the text colour on the page, ink on a dark band, white on a paper card. charts draws the pin's dashed line itself, so CHART_PIN_LOOK.line colours that dash (--sft-chart-pin-line, the chart axis at 60 %, 70 % on paper) rather than adding a border of its own.

import { ChartFlag, ChartPin } from "@softure-ai/charts";
import { CHART_PIN_LOOK, getChartFlagLook } from "@softure-ai/theme-flat-paint";

const surface = "paper";
<ChartFlag xPercent={62} size="sm" className={getChartFlagLook("deep", "sm")}>2041</ChartFlag>
<ChartPin
  xPercent={62}
  yPercent={40}
  classNames={{
    dot: `${CHART_PIN_LOOK.dot.flag} ${CHART_PIN_LOOK.size.md} ${CHART_PIN_LOOK.ring[surface]}`,
    line: CHART_PIN_LOOK.line[surface],
  }}
/>

A line or marker in a role's colour takes the role's tone: tone={CHART_ROLE_TONE.locked}.

The screens of @softure-ai/pwa are ui's components with these maps (the pwa README has the wiring):

import { InstallBar, RouteLoading, UpdatePrompt } from "@softure-ai/ui";
import { INSTALL_BAR_LOOK, ROUTE_LOADING_LOOK, UPDATE_PROMPT_LOOK } from "@softure-ai/theme-flat-paint";

<UpdatePrompt open={isUpdateReady} onReload={activate} classNames={UPDATE_PROMPT_LOOK} {...messages.update} />
<InstallBar offer={offer} onInstall={() => void install()} onClose={close} classNames={INSTALL_BAR_LOOK} {...messages.install} />
<RouteLoading name="Workspace" iconSrc={`${ICONS_PATH}/icon-192.png`} label={messages.launch.label} classNames={ROUTE_LOADING_LOOK} />

A radio choice: a label wraps the native radio, so a click on the whole row or tile checks it and the label carries the checked (has-checked:) and focus (has-focus-visible:) states. The card puts control after body.

import { CHOICE_ROW_LOOK as look, RADIO_LOOK as radio } from "@softure-ai/theme-flat-paint";

<label className={look.label}>
  <span className={look.control}>
    <span className={radio.box}>
      <input type="radio" name="plan" value="yearly" className={radio.input} />
      <span className={radio.mark} aria-hidden="true"><CheckIcon size={12} /></span>
    </span>
  </span>
  <span className={look.body}>
    <span className={look.title}>Yearly</span>
    <span className={look.description}>Billed once a year</span>
  </span>
</label>

Tokens

FLAT_PAINT_THEME (src/tokens.ts, also exported as theme) is the source, declared with the kit's defineTheme; tests/tokens.test.ts keeps it equal to the design system's token snapshot (tests/fixtures/flat-paint-tokens.json, the values of the design system's tokens.json). When the design system changes, refresh the snapshot and the definition in one change. The shadcn colour roles are aliases on top; two of them keep the design system's meaning, which differs from shadcn's: muted is the secondary text colour (shadcn's muted text is muted-foreground, an alias of it here) and accent is the accent text and focus colour. A raised background is secondary.

Contrast. tests/contrast.test.ts measures WCAG 2.2 AA in both schemes: foreground, muted, accent and the three data roles reach 4.5:1 on background, surface and surface-raised, on-accent on accent-fill, and accent and line-strong 3:1 as focus ring and control boundary.

Overriding a token. Redefine its CSS variable after theme.css, for both schemes, or build a variant with extendTheme(theme, { name, colors: { … } }) and buildThemeCss (the kit README, "Overriding in an app").