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

@ohmycorp/sarah-ui

v0.11.0

Published

Angular form and window primitives, themed entirely by CSS custom properties

Readme

sarah-ui

Angular form and window primitives, extracted from Sarah. Every component carries its own appearance and is themed entirely through CSS custom properties, so it drops into any Angular 22 app and looks right with no stylesheet to import and nothing to configure.

Install

npm install @ohmycorp/sarah-ui

Peer dependencies are @angular/core, @angular/common and @angular/forms. Nothing else — no @angular/localize, because the library ships no translatable strings, and no rxjs.

Use

Every component is standalone, takes signal inputs, and uses the app- selector prefix.

import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { TextFieldComponent, ToggleComponent } from '@ohmycorp/sarah-ui';

@Component({
  selector: 'app-account-form',
  imports: [ReactiveFormsModule, TextFieldComponent, ToggleComponent],
  template: `
    <app-text-field label="Email" type="email" [formControl]="email" hint="We never share it" />
    <app-toggle label="Weekly digest" [formControl]="digest" />
  `,
})
export class AccountFormComponent {
  email = new FormControl('');
  digest = new FormControl(false);
}

TextFieldComponent, SelectFieldComponent and ToggleComponent are ControlValueAccessors, so they bind with formControl, formControlName or ngModel.

FloatingWindowComponent renders inside a FloatingWindowLayerComponent, which provides WindowStackService: a window outside a layer throws NG0201. The layer is pointer-events: none so clicks pass through to the page, which means anything else you render inside it must set pointer-events: auto itself.

Every icon-only button takes its label as a required input, so the library never owns user-visible text: FloatingWindowComponent's closeLabel, maximizeLabel and restoreLabel, and ModalHeaderComponent's closeLabel. Pass your own, translated however your app translates.

Theming

The library reads 21 custom properties and declares none of them. Each is read as var(--ui-name, <default>), where the default is the value Sarah uses today, so an app that defines nothing gets Sarah's light palette in its own font. Define any of them anywhere that inherits down and the components follow.

:root {
  --ui-primary: #0f766e;
  --ui-radius-control: 10px;
  --ui-field-size: 1rem;
}

Three granularities all work, because the library declares nothing to out-specify:

  • Document — :root { --ui-primary: … }
  • Per component kind — app-text-field, app-select-field { --ui-bg: #fff }
  • Per instance — a wrapper class, or [style.--ui-primary]

The contract

src/styles/_tokens.sass is the source of truth; token() fails the build on a name that is not in it. Defaults below.

| Token | Default | Affects | | --------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------ | | --ui-primary | #6366f1 | field focus border, toggle track when checked, active chip, window hover | | --ui-bg | #f1f5f9 | field background, chip group background, modal header close hover | | --ui-surface | #ffffff | active chip, skeleton card, window surface, modal header | | --ui-border | #cbd5e1 | field border, modal header rule | | --ui-text | #1e293b | field text, window text and bar, modal header title | | --ui-text-muted | #64748b | captions, placeholders, idle chips, hints, toggle track, modal header close | | --ui-danger | #ef4444 | invalid field border, error text, window close hover | | --ui-success | #22c55e | success text | | --ui-radius-control | 4px | fields | | --ui-radius-group | 8px | chip group and chips, skeleton logo, modal header close | | --ui-radius-panel | 14px | floating window | | --ui-radius-card | 20px | skeleton card | | --ui-shadow-raised | two-layer 7%/4% | toggle track and knob, chip group, active chip | | --ui-shadow-card | two-layer 8%/4% | skeleton card | | --ui-shadow-overlay | two-layer 12%/6% | floating window | | --ui-field-size | 15px | input, textarea and select text | | --ui-caption-size | 13px | the label above a field | | --ui-field-chevron | inline SVG data URI | the select arrow | | --ui-placeholder-fade | #ffffff | what the placeholder colour mixes toward, 30% | | --ui-hover-shade | #000000 | what chip and button hovers mix toward, 6% | | --ui-knob | #ffffff | the toggle switch knob | | --ui-series-1 … --ui-series-8 | blue, orange, aqua, yellow, magenta, green, violet, red | the categorical chart palette, in fixed slot order | | --ui-heat-0 … --ui-heat-4 | #eef2f7 → #184f95 | the sequential ramp, lightest meaning none | | --ui-grid | #e2e8f0 | chart gridlines and table rules |

Two of these want flipping in a dark-first app, because they mix toward a colour rather than being one: set --ui-placeholder-fade and --ui-hover-shade to #000000 and #ffffff respectively, or set --ui-hover-shade: var(--ui-text) to make it follow the palette in one line.

--ui-field-size takes a length, never inherit — on a custom property inherit is the CSS-wide keyword and inherits the parent's custom property, not its font size. An app on a 16px scale sets 1rem.

Dark mode

The library has no dark mode, deliberately. It reads tokens and never names a theme selector, so it is correct under any trigger you choose — a class, a data attribute, prefers-color-scheme, a cookie-driven attribute, or a class on one panel:

[data-theme='dark'] {
  --ui-primary: #818cf8;
  --ui-bg: #0f172a;
  --ui-surface: #1e293b;
  --ui-border: #334155;
  --ui-text: #e2e8f0;
  --ui-text-muted: #94a3b8;
  --ui-danger: #f87171;
  --ui-success: #4ade80;
}

If you use sass, the dark palette ships ready to include under whatever selector you like:

@use "pkg:@ohmycorp/sarah-ui/styles/tokens"

[data-theme="dark"]
  @include tokens.dark

@mixin light emits all 21 as declarations, for an app that wants them defined rather than falling back.

Only the eight colours and the three shadows change in dark mode. The radii and the two type sizes do not.

Already have a theme?

Alias your own names once and every component follows, including through your existing dark mode, because var() substitutes the computed value on the same element:

:root {
  --ui-primary: var(--brand);
  --ui-text: var(--ink);
}

Charts

Eight dependency-free SVG components, no d3 and no charting library: app-line-chart, app-bar-chart, app-pie-chart, app-radar-chart, app-candlestick-chart, app-tree-chart, app-contributions-chart, plus the shared app-chart-legend, app-chart-tooltip and app-chart-table they compose. Every one takes a required caption, ships a hover and keyboard readout, and renders a collapsed table of its own data wherever the page passes tableView.

import { LineChartComponent } from '@ohmycorp/sarah-ui'

@Component({
  imports: [LineChartComponent],
  template: `
    <app-line-chart
      caption="Documents per month"
      [labels]="months"
      [series]="[{ name: 'Invoices', values: invoices }]"
    />
  `,
})

Rules the components enforce for you

These are not style preferences; they are the reasons the charts stay readable, and several are enforced in code rather than left to the caller.

  • Series colours are assigned in fixed slot order and never cycled. A ninth series takes a muted grey rather than a generated hue, which under colour-vision deficiency would be indistinguishable from an existing slot. Fold a long tail into one "Other" series, or facet it.
  • Colour follows the entity, not its rank. The pie sorts slices by value for layout but keys each colour to the slice's position in the array you passed, so a slice does not change colour when the numbers move.
  • The pie folds past six segments. A pie is only legible at a glance below about six slices, so it keeps the five largest and sums the rest into one otherLabel slice.
  • The radar is an all-pairs form. Every polygon overlaps every other, and the palette only clears the stricter all-pairs separation gates for its first three slots. Past three series, facet instead.
  • There is no dual-axis chart and there never will be. Two y-scales on one plot invent a correlation the data does not contain. Use two charts, or index both series to a common base.
  • The table view is the relief for a palette slot below 3:1, and it is opt-in. tableView is off by default and [tableView]="true" renders the collapsed table of the chart's own data. Series slots 3, 4 and 5 sit below 3:1 against the light surface and slot 6 below it against the dark one, so a chart that reaches a fourth series must either turn the table view on or label its marks directly. The first three slots, the heat steps and any chart whose marks carry visible values stand without it.

The palette was validated against this library's own surfaces — #ffffff light and #1e293b dark — not against a reference surface. Both modes pass the lightness band, chroma floor, adjacent-pair colour-vision separation and normal-vision floor. If you substitute your own --ui-series-* hues, re-run that validation rather than eyeballing it.

Charts in dark mode

The eight series slots and the five heat steps are re-stepped for a dark surface in @mixin dark, not flipped. If your app themes dark and uses charts, include that mixin or copy its values — the light defaults are not legible on a dark surface.

Known limits

  • Hit targets are sized in viewBox units, so they meet the 24px minimum only while the chart renders at its natural width or wider. A chart squeezed into a narrow column scales them down with everything else.
  • The SVG carries role="img", which prunes its focusable marks from the accessibility tree in strict implementations. Keyboard users get the focus ring and the readout; a screen reader reaches the same readout through each mark's aria-label, and the table view when the page turns it on.
  • Axis text is sized in viewBox units at about a sixtieth of the chart width, so it scales with the chart rather than with the theme's --ui-caption-size.

Components

ChipGroupComponent, FloatingWindowComponent, FloatingWindowLayerComponent, IconComponent, ModalHeaderComponent, SelectFieldComponent, SkeletonCardsComponent, StatusTextComponent, TextFieldComponent, ToggleComponent, plus WindowStackService and the ChipOption, IconName and StatusTextVariant types.

IconComponent draws every stroke icon with stroke="currentColor", so it takes its colour from wherever you put it: <app-icon name="trash" />, [size] optional.

Develop

pnpm install           # the pnpm version is pinned by packageManager; `corepack enable pnpm` once provides it
pnpm build             # ng-packagr -> dist/
pnpm test              # vitest through the Angular unit-test builder
pnpm format:check      # Prettier over .ts and .html; .sass is not covered

pnpm-workspace.yaml denies every dependency's install script; each of those packages ships a prebuilt binary, and pnpm refuses to install while a new one is unlisted.

To publish, bump the version, commit and push first, then build and publish:

pnpm build
pnpm publish ./dist    # a bare `dist` would name a registry package instead of the folder

pnpm publish refuses an uncommitted tree, a branch other than main, or one behind origin, so every published version matches a commit on GitHub; dist/ is gitignored, so building after the commit keeps the tree clean. With two-factor authentication it asks for the one-time code in the terminal, or prints a login URL and QR code. The package is public, so a consumer needs no registry authentication — which is what lets a Docker build run a plain install against it.