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

@black-market/interface-system

v0.4.0

Published

Portable React primitives with scoped Abyss design tokens and plain CSS.

Readme

@black-market/interface-system

Shared React primitives and canonical product chrome for the Abyss interfaces: Button, BalanceShortcuts, Input, AmountInput, SelectField, native Select, Text, InterfaceTheme, ProductHeader, ProductFooter, WalletDrawer, WalletTokenRow, LaunchCard, Wizard, WizardSteps, DataTable, TableHeader, and SortableTableHeader. The package ships compiled ESM, TypeScript declarations, JavaScript source maps, and an explicitly imported stylesheet. React and React DOM are peers (^18.3.1 || ^19.0.0); there are no production dependencies, bundled React copies, Tailwind requirements, routers, wallet integrations, or motion providers.

The canonical theme is dark petrol/teal/brass/ivory. The light theme is a new, optional package palette, not a claim that the existing applications implement a shared light mode. Components also have dark token fallbacks when used without a theme wrapper.

This repository does not migrate or edit the sibling applications. The adoption instructions below describe changes a consuming application can deliberately make later.

Destination: one cohesive interface system

This package is the canonical interface system for Abyss UI, Black Market web, and Black Market lending—not a compatibility layer that preserves three separate visual systems. Incremental adoption is a rollout strategy; the destination is shared appearance, component APIs, and interaction behavior.

  • Shared components and tokens own colors, typography, spacing, control sizing, borders, focus, and disabled/loading/error treatments. Extend the canonical system when a reusable need emerges rather than adding competing app-local primitives.
  • Jost is the canonical UI/body font, Cormorant Garamond the display font, and Geist Mono the numeric/code font. All three apps should load the same assets and weights during adoption. Fallbacks are resilience, not an alternative brand choice.
  • Applications own composition, responsive page layout, domain-specific widgets, transaction state, and data. Tailwind may help with application layout; it must not create a second shared-component design system.
  • Storybook is the reference for shared component appearance and behavior. Changes to shared design belong here, with representative states, rather than in downstream .btn, .field, or component-internal overrides.
  • Token overrides are an explicit extension mechanism for reviewed themes or documented product requirements, not permission for each app to drift. The optional light palette is not a commitment to add light mode to all applications.
  • Inputs use border-only visible focus from 0.3.26: no outer outline or added inset focus ring. Native and compound fields retain a focus border-color change without layout shifts; hosts must not suppress that cue.
  • When a primitive is adopted, migrate its callers and remove the replaced app-local component and obsolete styling once no callers remain. Do not retain permanent wrappers, aliases, or parallel implementations solely to preserve old APIs.

The current package is the initial shared boundary, not a claim that every visual dimension is already tokenized or that the applications are unified. React/build-tool upgrades can be aligned separately; portable React 18/19 APIs let visual consolidation proceed without a simultaneous framework migration.

See MIGRATION.md for the complete cross-app rollout, ownership boundaries, deletion criteria, and actual-host acceptance gates. Work in this repository establishes the canonical foundation; app adoption is not implied by package completion.

What the existing applications actually share

The inspection baseline below refers to source files and declared dependency ranges, not lockfile-resolved versions or measured browser rendering. Source paths are relative to this repository.

| Concern | ../abyss-ui | ../black-market/apps/web | ../black-market/apps/lending | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | React / build | React/DOM ^18.3.1, Vite ^8.2.0, TypeScript ~5.9.3 | React/DOM ^19.1.0, Vite ^6.3.5, TypeScript ^5.8.3 | React/DOM ^19.1.0, Vite ^6.3.5, TypeScript ^5.8.3 | | App router (not a primitive dependency) | @tanstack/react-router ^1.170.31 | react-router-dom ^7.6.2 | react-router-dom ^7.6.2 | | Styling | Tailwind ^4.3.3 and authored global CSS; Vite's Tailwind plugin is enabled | Plain shared @black-market/ui CSS plus app-local deco.css | Plain shared CSS in a shared layer, local CSS in a later lending layer | | Buttons | React forwardRef, native props, loading, size="default\|large", stretch; Tailwind layout utilities plus .deco-primary | Shared Button, variant="solid\|ghost", size="md\|sm"; native props and React 19 ref-as-prop | Same shared API, with local .btn-solid/.btn-primary gold overrides | | Fields | Screen-specific native inputs; SelectField is a custom listbox, not a general text-input primitive | Shared TextField with optional label/id and default/search/amount/multiline paths | Same TextField; lending owns amount sanitization, validation and transaction state | | Typography / fonts | Native elements, Tailwind utilities, .deco-heading/.deco-kicker; entry imports Jost, Cormorant Garamond and Geist Mono | Specialized Money/Muted presentation; entry imports Jost, Cormorant Garamond and Geist Mono | Shared tokens name Jost, but the entry and manifest do not load it; Cormorant Garamond and Geist Mono are loaded, with Geist also present | | Distribution / coupling | App-local components and global theme/reset styles | @black-market/ui exports TypeScript source and includes React 19, router, icons, motion and toast peers | Same source-exported UI package and dependencies | | Storybook | 10.5.10, imports the whole app stylesheet | ^8.6.18, imports shared CSS and decorates with MemoryRouter/MotionConfig | ^8.6.18, imports layered app CSS and the same providers |

Abyss UI assessment

  • package.json and vite.config.ts establish the React 18/Vite 8/Tailwind 4 stack. src/index.css starts with Tailwind, defines theme tokens at lines 3–80, and also owns document resets and authored controls. Calling it “Tailwind-only” would miss the styling that actually determines the reusable appearance.
  • src/components/ui/button.tsx forwards a native button ref, defaults to type="button", and disables while loading. button-styles.ts mixes Tailwind utilities with global .deco-primary styling. The primitive itself does not need wallet/router code; the global stylesheet and application providers should not travel with it.
  • There is no general exported Text or text Input primitive in the inspected UI directory. pool-wizard.tsx has local price/amount inputs, and execution-settings.tsx owns native inputs and error associations. select-field.tsx is a larger listbox widget with its own keyboard/focus behavior; replacing it with a basic Input would be an API regression.
  • src/index.css:163–169 suppresses focus-visible outlines and Tailwind ring variables with !important. A portable button cannot rely only on an outline or a host Tailwind ring. This package uses an independent focus treatment; see the cascade caveats below.
  • The existing foundations story has stale hard-coded swatches compared with the live CSS. The source stylesheet, not those old swatches, is the palette evidence. Package token stories use CSS custom properties rather than repeating an independent palette.

Black Market web assessment

  • packages/ui/src/tokens.css:1–50 defines the same key palette (#07161c, #0e3b43, #c7a05a, #f8f1e0) and Jost/Cormorant/Geist Mono font families. src/styles.css owns global resets at lines 1–48, .btn styles at 905–959, and .field styling at 1696–1760. Reusing that entire sheet would also import document and application styling.
  • Button.tsx takes ref as a function-component prop, a React 19 API. The new package instead uses forwardRef for native button/input/div refs so its public API can serve both React generations.
  • TextField.tsx has optional labels/IDs and specialized search, amount and textarea branches. The shared package's required visible label and generated input ID are an intentional basic-input contract, not a replacement for all those specialized widgets. Money.tsx styles numeric content and related small presentation helpers; it is not a generic semantic text primitive or a number formatter. PageHead.tsx directly imports router and heroicons code and is outside the extraction boundary.
  • packages/ui/package.json exports source files and declares React 19, router, framer-motion, sonner and icon peers. Those dependencies serve broader application widgets, not the four primitives here.
  • apps/web/src/main.tsx imports Jost and the shared stylesheet; App.tsx:15 imports local deco.css. NewLaunchPage.tsx:2411–2447 shows that callers still own touched/error state and explicit ARIA/error markup. Component extraction must leave that business logic at the callsite.

Black Market lending assessment

  • apps/lending/src/styles/globals.css declares @layer shared, lending and imports the corresponding sheets. lending.css:804–834 makes solid buttons gold, overriding the shared ivory treatment. Shared tokens do not mean identical cascade or button appearance across the apps.
  • apps/lending/src/main.tsx imports Geist, Geist Mono and Cormorant, but not Jost; its package.json also lacks the Jost dependency. Since the shared UI font token names Jost, fallback-font rendering is a credible disparity to resolve deliberately during adoption, not proof of a measured visual defect.
  • ActionDrawer.tsx:525–553 combines the amount TextField with local sanitization/error associations and a button whose disabled/busy/label behavior follows transaction state. The new primitives do not own those amount or pending-state rules.
  • Both Black Market Storybook previews (lending) import host CSS and use router/motion decorators. This package's isolated Storybook needs its own CSS/fonts, not those providers.

Recommendation and tradeoffs

Use authored, namespaced component CSS plus scoped --ais-* custom properties as the canonical design source. Keep Tailwind optional in the host for page layout, not for independently restyling shared controls. The initial rollout does not require changing each application's Vite, router, or Storybook configuration; the shared component catalog lives here.

This boundary preserves the genuinely shared palette/fonts and native semantics without shipping .btn, .field, .deco-*, a document reset, or application providers. It also avoids requiring a non-Tailwind app to configure utility scanning or Tailwind theme compilation.

A Tailwind-authored library could compile and ship its CSS, so distributing CSS does not inherently rule out Tailwind. Here it would add author tooling without solving an existing host requirement: one consumer already relies on plain CSS, and Abyss already mixes utilities with significant authored CSS. Plain CSS is the smaller maintainable dependency boundary. The tradeoff is owning the component rules and explicit tokens rather than inheriting every host utility/theme convention; visual adoption needs review rather than promising pixel-identical replacements.

React 18 versus 19 is addressed by peer ranges and compatible component APIs. Vite 6 versus 8, Storybook 8 versus 10, and the different CSS stacks do not require framework/build-version unification to consume compiled ESM and CSS. Storybook/Vite are development tools here, not package runtime requirements.

Cursor pagination

CursorPagination is controlled, router- and data-source-independent keyset navigation. Supply a positive one-based page, observed hasNext and onPageChange(direction); the host owns cursors, filtering and data. Previous is omitted on page one, Next without a cursor is omitted, and no total page count is guessed. pending="next" | "previous" disables navigation and marks only the initiating control busy. Use disabled for actual query transitions, never ordinary background refetch. Page changes are announced once; when an activated destination disappears, focus falls back to the page indicator without stealing focus from another control.

See Components/CursorPagination for first/middle/last/single-page, explicit pending and keyboard specimens. Version 0.3.23 adds this component; consumer adoption requires its newly packed versioned artifact, not an overwritten older tarball.

Launch cards

Version 0.4.0 provides one pure-display LaunchCard with standard, Spotlight and featured variants. The package owns the identity, market data, token-type disclosure, venue images, allocation strip and featured frame. Hosts own data reads, exact arithmetic/formatting, artwork selection, navigation and chart behavior. There are no fetches or domain hooks in the card.

| Prop | Contract | | ----------------- | -------------------------------------------------------------------------------------------------------------------------- | | name, symbol | Required plain strings with full title attributes | | artwork | Required bare artwork node; host owns URLs, errors and fallback selection; an img can use data-fallback | | variant? | "standard" (default), "spotlight" or "featured" | | layout? | "responsive" (default) keeps compact standard rows on mobile; "card" preserves standard portrait previews | | loading? | Initial-read geometry only; hidden from assistive technology, unlinked and without status prose | | description? | Plain Spotlight/featured metadata; Spotlight preserves whitespace and clamps to three lines; blank text is omitted | | tokenType? | { label: string; kind?: string }, or null; classification is exposed as data-kind, never a border variant | | marketCap? | Host-formatted display string; omit to hide, explicitly pass "—" for unavailable observations | | marketCapTitle? | Optional exact value used for its title and accessible card description | | marketContext? | Host-formatted pairing or market-count text | | venueBadges? | Readonly array of { label: string; iconUrl: string }; package renders accessible 18px images | | seedAllocation? | { abyssPercent: number; label: string }, or null; finite display percentage 0–100 and exact host-formatted description | | highlight? | Featured-only node, for example a chart with a host-owned accessible summary | | renderLink? | (props: LaunchCardLinkProps) => ReactNode; forward class, children, label and description ids to a real link | | describedBy? | Additional host description ids, merged with package-owned type/allocation/exact-value ids |

The public exports also include LaunchCardVariant, LaunchCardTokenType, LaunchCardVenueBadge, LaunchCardSeedAllocation and LaunchCardLinkProps. See MIGRATION.md for the breaking replacement of the old node-only metadata slots; no compatibility aliases remain.

Standard portrait cards retain square artwork with inset market-cap/context overlays; layout="card" preserves that geometry for mobile previews. Responsive rows use 4rem artwork and compact full-width footers. Spotlight uses 4rem mobile/10rem desktop artwork, inline symbol/name, three-line descriptions, a petrol/gold sheen, opposing brass corner joints and a double-rule footer. Its venue badges stack above pairing context. Cards retain a uniform 1px perimeter regardless of type.

Featured cards carry the protocol's supplied Art Deco frame and petrol/brass surface, featured identity/description typography and a full market-cap label. The optional highlight grid puts chart content beside market data when room permits and stacks when constrained. All cards use intrinsic tracks and wrapping values without size containers or root/content percentage heights.

The package owns type disclosure ids and the bottom 3px allocation strip. Type text reveals on hover or keyboard focus; typed noninteractive previews remain keyboard-inspectable. Allocation colors are canonical brass for Abyss and venue pink for Uniswap, with system-color equivalents in forced colors. Exact amounts and percentage rounding remain host-owned; invalid percentages omit the strip instead of inventing a clamped value.

<LaunchCard
  name={name}
  symbol={symbol}
  variant="spotlight"
  artwork={<img src={imageUrl} alt="" />}
  description={metadataDescription}
  tokenType={{ label: "Creator", kind: "creator" }}
  marketCap={formattedCap}
  marketContext={pairing}
  venueBadges={[{ label: "Deployed on Abyss", iconUrl: "/abyss-icon.png" }]}
  seedAllocation={{ abyssPercent: 75, label: allocationLabel }}
  renderLink={(props) => <a href={detailUrl} {...props} />}
/>

Omit renderLink for a noninteractive preview: content is a plain div without action/navigation roles. Typed previews add one tab stop solely for disclosure inspection; untyped previews and initial-read placeholders add none. Do not put controls or nested links in linked artwork/highlight slots. The host region owns initial-read aria-busy and retains observed cards during background refresh. Components/LaunchCard covers all three variants, keyboard previews, long metadata, fallback artwork, allocation, initial placeholders and a 28-card directory.

Wizard presentation

Version 0.3.28 adds Wizard, WizardSteps, WizardProps, WizardStep<T> and WizardStepsProps<T>. Both components are presentation-only: hosts retain current-step state, step reachability, forms, previews, validation, financial review and all transaction guards.

Wizard extends native div attributes, forwards its div ref and requires navigation: ReactNode and children: ReactNode. Optional sidebar: ReactNode appears below navigation. Its square-edged contained surfaces, brass borders and decorative-frame rendering belong to the shared package, not consumer selectors. Hosts can inherit --ais-wizard-background to reuse their established panel recipe and --ais-wizard-frame-image for a trusted corner-artwork URL; defaults use the theme base and no artwork. Version 0.3.30 includes these public surface tokens. Forced colors retain system-color borders and hide decorative artwork. The root is at most 56rem wide; an internal layout uses a 14rem rail when 48rem is available and otherwise stacks navigation above content using a container query. Extraction must preserve the adopted host's opacity, border geometry and artwork rather than substituting a generic filled card.

Version 0.3.32 exposes --ais-wizard-rail-width for supporting content that needs a wider desktop rail. Its default remains 14rem; hosts can set a scoped value such as 18rem without overriding package selectors. Available-width stacking below 48rem is unchanged.

WizardSteps<T extends string | number> accepts:

| Prop | Contract | | --------------------- | ----------------------------------------------------------------------------------------------------------------------- | | steps | Readonly array of { id: T; label: string; disabled?: boolean }; ids are stable and unique | | currentStep | Host-owned current id; no fallback step is invented when the id is absent | | onStepChange? | Enabled native button activation sends the stable id, never the displayed index | | disabled? | Disables every step without changing the current state | | mobileNumbersOnly? | Below 48rem, show one row of numbers while retaining step labels for assistive technology; desktop labels are unchanged | | Native nav attributes | All HTML attributes except children and onChange; aria-label defaults to Wizard steps |

With a callback, steps are native canonical ghost buttons: Tab moves focus, Enter and Space activate, and disabled steps are skipped. Without one, the ordered progress indicator has no clickable controls. Current state is exposed with aria-current="step" on the current button or static list item. Decorative two-digit numbers are hidden from assistive technology. The square 2rem mono badge is gold with inset petrol detail for the current step, paired with an accent label; there is no left-line highlight, active gradient or connecting line. Labels wrap and the ordered list auto-fits items of at least 8.5rem where space permits, becoming a single column inside the narrow desktop rail.

import { useState } from "react";
import { Text, Wizard, WizardSteps } from "@black-market/interface-system";

const steps = [
  { id: "token", label: "Token" },
  { id: "market", label: "Market" },
  { id: "review", label: "Review", disabled: true },
] as const;

function DraftWizard() {
  const [currentStep, setCurrentStep] =
    useState<(typeof steps)[number]["id"]>("token");

  return (
    <Wizard
      navigation={
        <WizardSteps
          steps={steps}
          currentStep={currentStep}
          onStepChange={setCurrentStep}
          aria-label="Deploy steps"
        />
      }
      sidebar={
        <Text variant="caption" tone="muted">
          Deploy
        </Text>
      }
    >
      <Text as="h2" variant="heading">
        {steps.find((step) => step.id === currentStep)?.label}
      </Text>
    </Wizard>
  );
}

For a static liquidity rail, omit onStepChange and supply the real current id. Hosts decide whether each step is reachable and why; disabling navigation is not a substitute for financial guards on the initiating controls. See Components/Wizard and Components/WizardSteps for interactive three/four-step, static two-step, locked, long-label and constrained-container specimens. The package's local 0.3.28 candidate is not a registry publication or a claim of consumer acceptance.

Tables and controlled sorting

Version 0.3.31 adds DataTable, TableHeader, SortableTableHeader and their native-attribute/ref types. Compose ordinary thead, tbody, tr, td and row-header th children. The package owns header typography, borders, cell rhythm, row hover, focus, active gold underline and two-chevron sorting indicators. Hosts own column geometry, data, exact sorting and URL state.

TableHeader defaults to scope="col" and align="start"; use align="end" for numeric headers and align their body cells in host composition. SortableTableHeader takes label: string, onSort: () => void and optional controlled direction: "asc" | "desc". Omitted direction means an inactive column. Activation requests sorting without mutating internal state; the updated prop sets aria-sort and the active indicator.

<DataTable aria-label="Markets">
  <thead>
    <tr>
      <TableHeader>Asset</TableHeader>
      <SortableTableHeader
        label="Supplied"
        align="end"
        direction={sort === "supplied" ? direction : undefined}
        onSort={() => changeSort("supplied")}
      />
    </tr>
  </thead>
  <tbody>{rows}</tbody>
</DataTable>

See Components/Table for inactive, ascending, descending and keyboard/column-switching specimens. The production-built interaction story exercised Enter, Space, column switching, row ordering, numeric alignment and retained focus. This local candidate is not a registry publication or blanket consumer-accessibility claim.

Develop and build

Use Node 24 (.nvmrc) and pnpm 10.11.0 (packageManager). With nvm and Corepack available:

nvm use
corepack enable
corepack prepare [email protected] --activate
pnpm install
pnpm build
pnpm typecheck
pnpm test
pnpm build-storybook
pnpm storybook --host 0.0.0.0 --no-open

Keep the generated pnpm-lock.yaml under version control. Subsequent reproducible/CI installs use pnpm install --frozen-lockfile. The commands above describe the checks to run; this README does not claim a verification result.

| Command | Purpose | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | pnpm build | Cleans only generated dist, emits ESM/declarations/source maps, copies component CSS, and prepares canonical font CSS/assets/licenses | | pnpm build:watch | Starts the same clean build, watches TypeScript/CSS edits and editor file replacement, and regenerates fonts when src/fonts.json changes | | pnpm typecheck | Checks source, stories, Storybook configuration, Vitest configuration and tests without emitting files | | pnpm test / pnpm test:watch | Runs native-control tests in jsdom / starts interactive test watching | | pnpm storybook | Prepares the canonical font entry and starts component documentation on port 6006 | | pnpm build-storybook | Prepares the same fonts and builds the standalone workbench into storybook-static |

The distributable build excludes tests, stories and configuration. There is no application Vite config or JS bundler: emitted imports resolve React and react/jsx-runtime from the consuming application. JavaScript source maps embed source text. Do a full build after moving/removing source files to discard obsolete watch outputs; packing also runs a fresh build automatically.

The pinned development stack is TypeScript 5.9.3, Storybook 10.5.10 with docs/a11y and React/Vite, Vite 8.2.2, React plugin 6.0.4, React/DOM 18.3.1 and React 18 types. Tests use Vitest 4.1.11, jsdom 26.1.0, Testing Library React 16.3.0, user-event 14.6.6 and jest-dom 6.9.1. Story play interactions use storybook/test.

Pack and adopt without linking React

From this repository's root:

mkdir -p .artifacts
pnpm pack --pack-destination .artifacts

prepack rebuilds the package. Version 0.3.46 produces .artifacts/black-market-interface-system-0.3.46.tgz; the package ships dist, package metadata, the root MIT license, upstream font licenses, and these docs, not tests/stories/development dependencies. The root JS/type export is dist/index.js / dist/index.d.ts; the component stylesheet export is @black-market/interface-system/styles.css → dist/styles.css, and the font entry is @black-market/interface-system/fonts.css → dist/fonts.css. CSS is marked as a side effect, but you must import it explicitly. There is no CommonJS export or source-file public entry point.

The name follows the existing @black-market/sdk npm organization. It intentionally does not collide with the existing @black-market/ui workspace package. Registry releases are public and the component code is MIT-licensed; bundled font binaries retain their separate upstream SIL OFL notices. Local packing remains available for unreleased development candidates.

Registry installs

After publication, install the exact release from npm and commit the consumer manifest and lockfile:

pnpm add @black-market/[email protected] --save-exact

Registry consumers do not need a vendor artifact or sibling checkout in their deployment build context.

Local installs that survive deployment checkouts

Copy the tarball into each consumer repository rather than recording a dependency outside its checkout. From ../abyss-ui:

mkdir -p vendor
cp ../abyss-interface-system/.artifacts/black-market-interface-system-0.3.46.tgz vendor/
pnpm add ./vendor/black-market-interface-system-0.3.46.tgz

From ../black-market, the workspace root:

mkdir -p vendor
cp ../abyss-interface-system/.artifacts/black-market-interface-system-0.3.46.tgz vendor/
pnpm --dir apps/web add ../../vendor/black-market-interface-system-0.3.46.tgz
pnpm --dir apps/lending add ../../vendor/black-market-interface-system-0.3.46.tgz

Commit each consumer's vendor tarball, changed manifest(s), and pnpm lockfile together for deployment trials. Inspect the recorded file: paths: from Black Market's apps/web and apps/lending they must resolve to the workspace-root vendor directory (normally ../../vendor/...). The deployment build context must include that directory and the workspace lockfile; a checkout containing only an app subdirectory is insufficient. Use pnpm install --frozen-lockfile in deployment builds.

Prefer the tarball to pnpm link: an installed tarball uses the host's React peers, whereas a live source link can resolve the library's development React and cause duplicate-React/invalid-hook problems. For every deployment candidate, increment the package version, rebuild/pack, copy the new versioned tarball into both consumer repositories, and reinstall it in all adopting apps. Do not overwrite a previously locked tarball with different bytes or rely on source-link synchronization.

Public npm release

A Git repository is not required for manual publishing. Authenticate with an npm account authorized for the npm black-market organization; direct publishing requires the registry's current 2FA policy. Never commit credentials or pass tokens/OTPs through documentation or chat.

Run pnpm install --frozen-lockfile, pnpm typecheck, pnpm test, and pnpm build-storybook, then pack the exact candidate with pnpm pack --pack-destination .artifacts. Inspect its manifest, exports, embedded source maps, documentation and all license files; smoke-test that tarball in isolated React 18 and React 19 consumers. Publishing is public disclosure of the included artifact.

npm login --auth-type=web --registry=https://registry.npmjs.org/
npm publish ./.artifacts/black-market-interface-system-0.3.27.tgz --access public --registry=https://registry.npmjs.org/

Publish the tested tarball, not a newly rebuilt directory. Do not overwrite a published name/version or change a previously locked vendor artifact. No repository/provenance claim is made for a local release. If a repository is added later, configure npm OIDC trusted publishing against its actual workflow rather than storing a long-lived publish token.

After registry publication is confirmed, consumers can use pnpm add @black-market/[email protected]. Migrate adopting manifests/lockfiles together and remove only their now-unused vendor files. Publishing this library does not automatically migrate sibling apps.

Import the stylesheet once, after host CSS

In each app's entry module, keep its reset/application imports and load the package fonts once. Replace duplicate imports of the same canonical font assets when adopting this entry. Import package component CSS after host CSS. For example:

// Abyss UI: src/main.tsx
import "./index.css";
import "@black-market/interface-system/fonts.css";
import "@black-market/interface-system/styles.css";
// Black Market web: src/main.tsx
// Keep App's existing deco.css dependency before this package stylesheet too.
import "@black-market/ui/styles.css";
import "@black-market/interface-system/fonts.css";
import "@black-market/interface-system/styles.css";
// Black Market lending: src/main.tsx
import "./styles/globals.css";
import "@black-market/interface-system/fonts.css";
import "@black-market/interface-system/styles.css";

No Tailwind plugin/content configuration, router decorator, theme context, reset or wallet provider is required. Lending can keep its existing shared/lending layers; the explicit package import is an independent component stylesheet, not another copy of the global shared reset.

Namespacing is class/token isolation, not Shadow DOM isolation. Host element selectors, inheritance, higher-specificity rules and !important rules can still affect native elements. Import order helps with ordinary cascade conflicts but does not beat !important or replace a visual/keyboard review. In particular, Abyss's existing global outline: none !important suppression remains untouched here. Package controls use independent focus styling rather than fighting it with global !important; removing that host suppression is a recommended future host change, not something this repository performs. Check focus visibility in the actual host, including forced-colors settings, before adopting.

Native selection

Select uses native <select> behavior and the canonical field presentation. Its required visible label, optional description/error, generated ID, forwarded ref, wrapperClassName, and sm/md/lg control sizes match Input. Pass native <option>/<optgroup> children and native value/change props. size controls visual dimensions, not the native number of visible options. No custom listbox engine or wallet/router dependency is added.

The launch-port candidate passed package typecheck, 28 existing tests and the production Storybook build. Its real Chromium story exercised ArrowDown/Enter selection, visible focus, composed descriptions, invalid state in the accessibility tree and disabled state at 390px. This is not a screen-reader or WCAG conformance claim. The consuming app must also exercise the control in its actual layout.

Styled dropdowns

Version 0.3.10 adds SelectField for uniform app dropdowns. Select remains a deliberate native platform picker, not an alias. The three styled patterns share one API:

  • Plain: typed value, options and onChange(value).
  • Searchable: add searchable; label/searchText/value filtering is local by default.
  • Searchable with highlights: additionally supply highlightedOptions. Highlights remain available during search and are deduplicated from ordinary rows. The caller owns the reviewed membership list.

Each option has a unique string or number value, a text label, and optional description, decorative icon, searchText and disabled. Numeric zero remains numeric in callbacks. size="sm|md|lg", description, error, id, name, required, disabled and wrapperClassName follow the field contract. labelHidden is only for compact controls with existing visible context.

For app-owned API/address lookup, pass searchValue, onSearchChange, filterOptions={false}, loading and emptyMessage. The component does not fetch, debounce, cache, persist, attest tokens or replay writes. Closing clears the search. An unavailable selection is never silently replaced with the first option.

The trigger and search field expose the applicable combobox/listbox semantics. Arrow keys move active focus without changing the selected value; Enter commits; Escape cancels and restores trigger focus; Tab and outside clicks dismiss without trapping focus. Disabled options cannot commit, including inside disabled fieldsets. A visually hidden native select carries name/required validation; invalid form submission focuses the visible trigger.

Popups use the browser's non-modal top layer where available, with a fixed-position fallback. Placement respects viewport edges and opens upward when space below is limited. Labels/descriptions wrap, option lists are bounded and scroll using shared petrol/brass styling, and reduced motion/forced colors retain usable controls. The package owns all dropdown appearance; hosts compose layout, not .ais-* overrides. See Primitives/SelectField for plain, search, highlights, disabled, error, loading, sizes and inside-dialog examples.

<SelectField
  label="Asset"
  value={asset}
  onChange={setAsset}
  options={assetOptions}
  searchable
  highlightedOptions={reviewedHighlights}
  searchPlaceholder="Name, symbol or address"
/>

Balance shortcuts

Version 0.3.14 adds BalanceShortcuts, BalanceShortcutsProps and BalanceShortcutPercentage. The controlled group renders canonical small ghost buttons for 25%, 50%, 75% and Max. onSelect receives 25 | 50 | 75 | 100; hosts own exact amounts, gas reserves, availability and all transaction logic. disabled disables the four native buttons without changing their layout or submitting an enclosing form.

The default group name is “Percentage of available balance”; percentage buttons have explicit accessible names. maxAccessibleLabel defaults to “Use maximum available balance” and can describe a host-specific reserve policy. Native div/ARIA attributes, className and a forwarded div ref support host composition. Ordinary small groups retain their four-column/two-column responsive layout. From 0.3.21, size="xs" and variant="secondary" use the canonical 24px compact button, not bespoke shortcut styling. In AmountInput.actions, the group occupies only the upper-right half above the asset selector and reflows within that half on constrained widths.

See Components/BalanceShortcuts for enabled and disabled interaction stories.

Amount input

AmountInput and AmountInputProps provide a native labeled text input with large amount typography, an inline accessory slot, compact actions and a supporting footer. The label is visible by default; labelHidden preserves its accessible association without a visible heading. The shell owns canonical input background, brass border and radius. From 0.3.34, keyboard focus changes the shell border rather than adding an amount underline; forced colors use a dashed Highlight border. Static loading skeletons reserve the amount footprint without fabricating values.

Native refs, controlled/uncontrolled strings, selection, disabled/read-only semantics and caller descriptions are preserved. description and error join aria-describedby, and an error sets invalid state. From 0.3.18, errors are live alerts, preserving trading validation announcements. className targets the native input; wrapperClassName targets the control. Hosts own exact decimal strings, validation, formatting, token identity, balance availability and all financial rules. Passing disabled disables the input only: the host must independently disable interactive accessory/actions/footer content as needed.

Text-relative container reflow preserves inline amount/token controls at ordinary mobile sizes and wraps them only when enlarged text leaves insufficient room. Compact action buttons remain in the right half, using four columns where they fit, two at narrow widths and one only under extreme text constraints. The amount and balance rows keep the full control width. See Primitives/AmountInput for selected-token actions, read-only, invalid, disabled, pending and long-amount states. Local adoption uses black-market-interface-system-0.3.22.tgz; no registry publication is involved.

Wallet presentation

Version 0.3.12 adds WalletDrawer and WalletTokenRow. They present app-owned account identity, formatted balances, read states and activity without wallet, router or query dependencies.

WalletDrawer

Render the drawer conditionally inside the host InterfaceTheme. Mounted means open; onClose asks the parent to unmount after exit and cleanup. A hidden host marker resolves the nearest .ais-theme root, and the native <dialog> is portaled directly into that root. This preserves inherited tokens/fonts and React context while escaping navigation wrappers that become hidden at mobile breakpoints. Only unthemed consumers fall back to document.body and the canonical dark token fallbacks. Native showModal() owns the top layer and inert background; opening/cleanup wait until the portal exists.

The full-height panel anchors to the right, is at most 28rem wide, and fills narrower viewports. Header and footer remain anchored while the body scrolls. Each pinned region is bounded to 35dvh (with a 35vh fallback) and scrolls internally when enlarged text or wrapped actions need more space, so the body and controls remain reachable without page overflow. The header vertically centers its heading group and close control. An identity of null, false or undefined omits the identity wrapper and its row gap, leaving a centered title-only header. Supplied identity content remains below the title and wraps; account/address/chain disclosure remains app-owned.

import type { ReactNode } from "react";

export type WalletDrawerDismiss = (afterClose?: () => void) => void;

export interface WalletDrawerProps {
  title?: string; // Defaults to "Wallet"; also names the native dialog.
  identity: ReactNode;
  children: ReactNode;
  footer: (dismiss: WalletDrawerDismiss) => ReactNode;
  onClose: () => void;
  restoreFocus?: () => void;
}

The close button is always named Close wallet, independent of the dialog title, and receives initial focus. Close, native Escape/cancel and primary backdrop pointer dismissal share one exit path. Escape does not bubble into host navigation; children may handle their own Escape first. Native modal inertness owns the background, with explicit Tab/Shift+Tab wrapping at the first/last visible enabled control so browser chrome cannot strand focus. Native summary disclosures participate. The body is a focusable region named Wallet contents, with canonical focus styling, so loading/read-only content remains keyboard-scrollable when its actions are disabled. The visual header/footer use non-landmark containers to remain valid inside host navigation.

Entry/exit slide and scrim fade reverse from the current position if interrupted. Reduced motion skips the transitions, including when the preference changes during exit. Completion has a computed-duration fallback if transitionend is missing; body overflow, animation frames, timer and media listeners are cleaned up on dismissal or external unmount.

dismiss(action) defers a network modal or other caller action until the native dialog is closed, body scrolling is restored and focus restoration has finished. Cleanup precedes onClose, then the deferred action executes. The first dismissal wins; repeated requests cannot replace/replay the action. External unmount, such as an account change, cancels the deferred action.

Focus returns to a connected, visible, enabled opener. If that opener has been hidden or replaced, restoreFocus can find the current host account control. Background content/callback updates do not reopen the dialog or refocus it. Cleanup does not reclaim focus already moved outside the drawer.

import { useRef, useState, type ReactNode } from "react";
import {
  Button,
  InterfaceTheme,
  WalletDrawer,
} from "@black-market/interface-system";

export function AccountPresentation({
  identity,
  children,
  onSwitchNetwork,
}: {
  identity: ReactNode;
  children: ReactNode;
  onSwitchNetwork: () => void;
}) {
  const [open, setOpen] = useState(false);
  const accountControl = useRef<HTMLButtonElement>(null);
  return (
    <InterfaceTheme>
      <Button ref={accountControl} onClick={() => setOpen(true)}>
        Wallet
      </Button>
      {open && (
        <WalletDrawer
          identity={identity}
          onClose={() => setOpen(false)}
          restoreFocus={() => accountControl.current?.focus()}
          footer={(dismiss) => (
            <>
              <Button
                variant="secondary"
                onClick={() => dismiss(onSwitchNetwork)}
              >
                Switch network
              </Button>
              <Button variant="ghost" onClick={() => dismiss()}>
                Done
              </Button>
            </>
          )}
        >
          {children}
        </WalletDrawer>
      )}
    </InterfaceTheme>
  );
}

There is no controlled open, imperative ref, onAfterClose prop or wallet hook. The footer render function is the dismissal API. Hosts own copy/disconnect/network actions, observation errors, skeletons, valuation uncertainty, activity, and any slot composition using public tokens; do not override .ais-* internals.

WalletTokenRow

export interface WalletTokenRowProps {
  identity: ReactNode;
  icon?: ReactNode;
  balance: ReactNode;
  value?: ReactNode;
  details?: ReactNode;
}

Place rows inside a native ul/ol, with role="list" to preserve list semantics when list markers are removed. Each row is an accessible li, with compact identity, decorative icon, monospaced balance and optional secondary value. Numeric conversion, decimal precision and valuation coverage stay with the app; missing slots do not invent observations. Use Text for symbol/name hierarchy and preserve accessible full values.

Providing details makes the row a native details/summary disclosure. Put address inspection, provenance or actions in the details slot, not interactive identity/amount content inside its summary.

<ul role="list" aria-label="Token balances">
  <WalletTokenRow
    identity={
      <>
        <Text variant="label">ABYSS</Text>
        <Text variant="caption" tone="muted">
          Abyss
        </Text>
      </>
    }
    icon={<img src={tokenArtwork} alt="" />}
    balance={formattedBalance}
    value={formattedValue}
    details={<Text variant="mono">{tokenAddress}</Text>}
  />
</ul>

Components/WalletDrawer includes portfolio, long account/rows, activity, geometry-preserving loading and mobile specimens. Set a real 320px/390px browser viewport for mobile; narrowing a story wrapper cannot resize the native top layer. Components/WalletTokenRow covers compact, long-value, omitted-slot and expandable rows. Authored jsdom tests cover dismissal/focus/scroll/resource/deferred-action boundaries; browser story interactions exercise native background inertness, focus restoration and disclosure. Their existence is not a claim that checks or browser acceptance have run.

Use the primitives

import { useState } from "react";
import {
  Button,
  Input,
  InterfaceTheme,
  Text,
} from "@black-market/interface-system";

export function AccountForm() {
  const [name, setName] = useState("");
  const [error, setError] = useState<string>();

  return (
    <InterfaceTheme theme="dark" style={{ padding: "1.5rem" }}>
      <Text as="h2" variant="heading">
        Account name
      </Text>
      <Text tone="muted">Choose a name for this browser session.</Text>
      <form
        onSubmit={(event) => {
          event.preventDefault();
          setError(name.trim() ? undefined : "Enter a non-blank name.");
        }}
      >
        <Input
          label="Name"
          name="displayName"
          autoComplete="nickname"
          required
          value={name}
          onChange={(event) => setName(event.currentTarget.value)}
          description="This does not change your wallet address."
          error={error}
        />
        <Button type="submit">Check name</Button>
      </form>
    </InterfaceTheme>
  );
}

Public API and native behavior

Native primitives accept caller className and attributes appropriate to their element; SelectField exposes the field, option and form props listed above. Root public types include ButtonProps, ButtonSize, ButtonVariant, InputProps, AmountInputProps, BalanceShortcutsProps, BalanceShortcutPercentage, SelectProps, SelectFieldProps, SelectFieldOption, TextProps, TextElement, TextVariant, TextTone, InterfaceThemeProps, Theme, ThemeStyle, ControlSize, WalletDrawerProps, WalletDrawerDismiss, WalletTokenRowProps, WizardProps, WizardStep<T>, and WizardStepsProps<T>. Import types from that same entry point rather than deep-importing source or private helpers.

| Component | Package props / defaults | Native contract | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Button | variant: primary (default), cta, secondary, ghost, danger; size: xs, sm, md (default), lg; loading, loadingLabel, fullWidth | forwardRef<HTMLButtonElement>; defaults to type="button"; pass type="submit" deliberately for forms; explicit disabled or loading blocks native activation; loading exposes aria-busy | | Input | Required visible label: string; optional description: ReactNode, error: string, size: sm\|md\|lg (default md), wrapperClassName | forwardRef<HTMLInputElement>; className styles the input, wrapperClassName styles the field; explicit or SSR-safe generated ID links the label; description/error IDs are composed with caller aria-describedby; errors expose invalid state | | SelectField | Required label, typed options, onChange(value); optional value, searchable, highlightedOptions, controlled search, description/error, sizes and form props | Explicit selection, labeled combobox/listbox, non-modal popup, native required/form-value proxy; caller owns all async data and option admission | | Text | as: p (default), span, h1–h6; variant: display, heading, body (default), label, caption, mono; tone: default, muted, accent, danger | Semantic element and visual variant are independent; choose a heading level for document structure, not its font size | | InterfaceTheme | theme: dark (default) or light; style: ThemeStyle | forwardRef<HTMLDivElement>; normal div attributes; sets a scoped .ais-theme/data-ais-theme boundary; no context, storage, global DOM mutation or provider requirement |

ControlSize remains the shared "sm" | "md" | "lg" field-size type; ButtonSize also includes "xs" for compact actions. Input intentionally excludes the native numeric HTML size attribute from its props. Other native form behavior remains native: use name, type, value/onChange or defaultValue, required, readOnly, disabled, inputMode, autocomplete and refs normally. Disabled controls follow the browser's form-submission/focus behavior; read-only controls are still distinct from disabled ones. The package does not parse amounts, debounce changes, submit transactions or invent validation rules.

Control minimum heights are 24px (xs buttons), 36px (sm), 44px (md) and 52px (lg). Small inputs keep a 1rem font size; visual sizing should not make editable text unnecessarily small. These are default minimums, not fixed heights that prevent content wrapping or caller styling.

Version 0.3.13 keeps input focus within the existing border: no outer outline or shadow ring. Keyboard focus uses a brighter border with an inset stroke; forced colors use a dashed Highlight border without changing dimensions. Buttons retain their separate focus treatment.

Native textareas and compound amount controls use the same public CSS contract when their application-owned labels, refs or inline selectors need custom composition: className="ais-input ais-input--sm" on a textarea; className="ais-input-group" on a compound shell with a direct className="ais-input" input child. The package owns borders, background, typography, disabled/error appearance and focus; hosts own grid/flex layout, amount emphasis, labels and ARIA associations. Do not recreate those styles or override .ais-* internals. See Primitives/Input/NativeComposition.

From 0.3.25, form inputs, native textareas, ais-input-group shells and AmountInput shells share --ais-input-background. Native <input type="search"> controls with ais-input (including <Input type="search">) use --ais-input-search-background. The type selects presentation only: hosts still own fetching, filtering, debouncing, caching and result state. No new React prop or automatic search behavior is introduced.

Read-only native inputs/textareas mix 65% of their contextual form/search background with 35% --ais-bg; disabled controls mix 70% with 30% --ais-bg, retaining muted text, disabled opacity and native semantics. Disabled wins when both states apply. These recipes move toward the theme background rather than back toward the brighter raised surface. Direct compound input children remain transparent, including read-only and disabled states, so their shell owns the surface. AmountInput retains its transparent native input and existing state treatments. Borders, foregrounds, contained focus, error and forced-colors treatments are unchanged. See Primitives/Input/ContextualSurfaces in both theme-toolbar palettes.

Version 0.3.9 adds variant="cta" for exceptional product entry points such as Create launch. It owns the approved landing's metallic-gold gradient, bright border and glow without hover translation; ordinary primary actions remain flat brass. Use sparingly, not on every approval or financial action. Native navigation links use className="ais-button ais-button--cta ais-button--md" instead of button semantics. Shared theme tokens are --ais-cta-background, --ais-cta-border, --ais-cta-fg, --ais-cta-shadow and --ais-cta-hover-shadow; light and dark themes retain dark CTA text on metallic gold. Existing focus, disabled/loading, reduced-motion and forced-color contracts apply. Primitives/Button/CtaStates shows buttons and links.

For asynchronous actions, supply operation-specific text rather than relying on a spinner:

<Button
  loading={isPending}
  loadingLabel="Confirming deposit…"
  onClick={submitDeposit}
>
  Deposit
</Button>

loadingLabel describes the busy action; without it, keep meaningful children. A disabled/busy button is not a complete announcement strategy. The caller still owns wallet/signature/confirmation stages, status/error live regions, success/error focus management and explaining why an action is unavailable. Preserve native ARIA where useful; do not override the component's loading/invalid semantics with contradictory state.

Theme overrides, nesting and fonts

The stylesheet contains only namespaced component/theme selectors and custom properties. It has no :root, html, body, global control reset or font imports. Override tokens at the theme boundary rather than copying internal component CSS:

import { Button, InterfaceTheme } from "@black-market/interface-system";
import type { ThemeStyle } from "@black-market/interface-system";

const panelStyle: ThemeStyle = {
  "--ais-radius": "0.5rem",
  "--ais-text-heading-size": "2rem",
  "--ais-accent": "#c7a05a",
  "--ais-font-sans": '"Jost Variable", system-ui, sans-serif',
  maxWidth: "36rem",
  padding: "1.5rem",
};

<InterfaceTheme theme="dark" style={panelStyle} className="account-panel">
  <Button>Dark action</Button>
  <InterfaceTheme theme="light" style={{ padding: "1rem" }}>
    <Button variant="secondary">Light action</Button>
  </InterfaceTheme>
</InterfaceTheme>;

A nested theme establishes its own scoped palette without changing siblings or document colors. Apply overrides at the wrapper whose theme should change. Ordinary CSS and caller-owned classes can override tokens too; prefer tokens for brand changes and className/wrapperClassName for host layout. Avoid reusing legacy .btn, .field or .deco-* classes on new components unless deliberately reviewing their additional rules.

| Token | Purpose | | ---------------------------------------------------------- | --------------------------------------------------------------- | | --ais-bg, --ais-surface | Theme background and raised surfaces | | --ais-input-background | Recessed form, textarea, compound and amount shell surface | | --ais-input-search-background | Quieter native search input surface | | --ais-fg, --ais-muted | Default and supporting text | | --ais-accent, --ais-accent-hover, --ais-on-accent | Brand action fill, hover fill, and its foreground | | --ais-border, --ais-focus, --ais-danger | Control boundaries, focus indicator, error/destructive emphasis | | --ais-font-sans, --ais-font-display, --ais-font-mono | UI/body, display/heading, and monospaced families | | --ais-radius | Shared control corner radius | | --ais-color-scheme | Native control color scheme, selected by the theme |

Each .ais-theme defines the input tokens from its scoped palette:

--ais-input-background: color-mix(
  in srgb,
  var(--ais-surface, #0e3b43) 55%,
  var(--ais-bg, #07161c)
);
--ais-input-search-background: color-mix(
  in srgb,
  var(--ais-surface, #0e3b43) 30%,
  var(--ais-bg, #07161c)
);

The omitted shares are 45% and 70% background respectively. Unthemed controls use the same derived recipes as fallbacks. The optional light theme and nested scopes derive from their own --ais-surface/--ais-bg values; there is no separate fixed input palette. Customize either public input token at the appropriate InterfaceTheme boundary when needed, not through app-local .ais-* overrides.

The type-scale tokens control Text variants and underpin default medium control/field typography. Explicit control-size tokens can override that relationship; compact inputs retain the medium text-entry font size:

| Variant | Size token | Default | | ---------------- | ------------------------- | ----------------------------------- | | display | --ais-text-display-size | clamp(2.5rem, 2rem + 2vw, 4.5rem) | | heading | --ais-text-heading-size | 1.75rem | | body (default) | --ais-text-body-size | 1rem | | label | --ais-text-label-size | 0.875rem | | caption | --ais-text-caption-size | 0.8125rem | | mono | --ais-text-mono-size | 0.875rem |

Every variant has its own standalone fallback. A nested theme resets these sizes to its scoped defaults along with its palette; set the scale overrides on that nested wrapper when needed. Keep document heading hierarchy intact when changing visual scale.

Canonical dimensions and interaction

The live Foundations / Dimensions stories are the reference for inherited tokens and resolved browser metrics. CSS is the single source of defaults; Storybook does not keep a second values table.

| Token family | Contract | | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | --ais-space-{0,1,2,3,4,5,6,8,10,12,16} | Quarter-rem spacing scale; 2-5, 3-5, and 4-5 add compact half steps | | --ais-control-min-size-{xs,sm,md,lg} | Minimum sizes: 24px compact buttons, 36px, 44px, 52px; controls can grow for wrapping and zoom | | --ais-button-padding-{block,inline}-{xs,sm,md,lg}, --ais-input-padding-{block,inline}-{sm,md,lg} | Role-specific padding, generally derived from the spacing scale | | --ais-control-font-size-{md,lg}, --ais-button-font-size-{xs,sm}