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

@sheep-ng/ui

v0.1.0

Published

Standalone, signal-based Angular components with a single theme layer: buttons, inputs, chips, nav bar, lists, tree, table, dialogs and more.

Readme

@sheep-ng/ui

Standalone, signal-based Angular components with a single theme layer: buttons, inputs, chips, badges, a nav bar, lists, a tree, a table, dialogs, side sheets and tooltips. Zoneless-ready, OnPush throughout, light and dark themes built in.

Install

npm install @sheep-ng/ui @angular/cdk

Requires Angular 22 (@angular/core, @angular/forms and @angular/cdk, all ^22).

Quick start

  1. Import the theme once, from your app's global stylesheet (src/styles.scss). Without it the components render unstyled.

    @use '@sheep-ng/ui/styles/sheep-theme';
  2. Import components into a standalone component and use them:

    import { Component, signal } from '@angular/core';
    import { SHEEP_INPUT, SheepButtonComponent } from '@sheep-ng/ui';
    
    @Component({
      selector: 'app-profile',
      imports: [SheepButtonComponent, SHEEP_INPUT],
      template: `
        <sheep-input [(value)]="name" required>
          <span sheepInputLabel>Display name</span>
        </sheep-input>
        <sheep-button variant="solid" (click)="save()">Save</sheep-button>
      `,
    })
    export class Profile {
      protected readonly name = signal('');
      protected save(): void {}
    }

Multi-part components export a set to spread into imports: SHEEP_INPUT, SHEEP_LIST, SHEEP_TABLE, SHEEP_DIALOG, SHEEP_SIDE_SHEET, SHEEP_CHIP, SHEEP_NAV_BAR. The rest are imported singly: SheepButtonComponent, SheepBadgeComponent, SheepTreeComponent, SheepTooltipDirective, SheepThemeSwitcherComponent.

For AI assistants and tooling

The package ships two files for coding assistants and editor tooling:

  • AGENTS.md: how to use the library correctly, including the rules and the common mistakes.
  • custom-elements.json: a Custom Elements Manifest of the whole public API: every element and directive, with input types and defaults, outputs, slots and the import to use. package.json points at it through the standard customElements field.

The theme layer

Components carry no per-instance style API. Every colour, metric, radius and timing resolves from --sheep-* custom properties emitted by the theme layer, so two buttons in one app cannot drift apart. To change how components look, change the theme — not the call site.

Import the entry point once, from the app's global stylesheet:

@use '@sheep-ng/ui/styles/sheep-theme';

It emits the --sheep-* roles on :root, a prefers-color-scheme fallback, and a [data-sheep-theme] scope per theme. To re-brand, override roles after the import, at :root or on a [data-sheep-theme] scope, never per component:

@use '@sheep-ng/ui/styles/sheep-theme';

:root {
  --sheep-color-accent: #0f766e;
}

Choosing a theme

Selection is attribute-driven and SSR-safe — the initial paint needs no JS:

<html data-sheep-theme="dark"></html>

With no attribute the theme follows prefers-color-scheme. The attribute can also scope a theme to a subtree (a dark toolbar on a light page).

SheepThemeService owns that attribute — the attribute, not the service, is the source of truth for paint. It exposes preference() (including system), resolved(), set() and toggle(), and persists the choice to localStorage under sheep-ng:theme. SheepThemeSwitcherComponent (<sheep-theme-switcher>) is the ready-made Light / Dark / System control.

To avoid a flash of the wrong theme, paint the remembered value before Angular boots, from an inline script in index.html's <head>:

<script>
  (function () {
    try {
      var t = localStorage.getItem('sheep-ng:theme');
      if (t === 'light' || t === 'dark') {
        document.documentElement.setAttribute('data-sheep-theme', t);
      }
    } catch (e) {
      /* storage unavailable: fall back to prefers-color-scheme */
    }
  })();
</script>

Scope

This library ships components and a theme. It does not ship layout.

There is no sheep-stack, sheep-row or sheep-grid here, and there should not be: a layout DSL is not a standard part of a component library. It is a second, parallel way to write CSS that every consumer then has to learn, and it competes with the platform primitives — flexbox, grid, semantic HTML — that already do the job and that consumers already know.

sheep-button

import { SheepButtonComponent } from '@sheep-ng/ui';
<sheep-button tone="danger" (click)="remove()">Delete project</sheep-button>

It renders a real <button>, so keyboard interaction, form submission and assistive-technology semantics are the platform's, not a reimplementation.

outline is the default and the library's signature: an accent line on a transparent ground. solid exists but is reserved for the single most important action in a view — the library never floods a large area with a tone.

Inputs

| Input | Type | Default | Notes | | ----------- | ---------------------------------------------------- | --------- | ------------------------------------------------------- | | variant | outline \| solid \| soft \| ghost \| link | outline | Visual weight. | | tone | primary \| neutral \| success \| warning \| danger | primary | Semantic colour. | | size | sm \| md \| lg | md | Shared with every control in the library. | | shape | rounded \| square | rounded | square is the icon-only 1:1 box. | | type | button \| submit \| reset | button | Defaults to button so it never submits by accident. | | disabled | boolean | false | Accepts the bare attribute: <sheep-button disabled>. | | loading | boolean | false | Shows a spinner, sets aria-busy, and blocks re-entry. | | ariaLabel | string \| null | null | Required when the button has no text. |

There is no click output — the native button's event bubbles, so bind (click) on <sheep-button> directly. It does not fire while disabled or loading. focus() on the component instance forwards to the native button.

There is no fullWidth input: the caller sizes the wrapper.

Content slots

<sheep-button variant="soft" tone="success">
  <svg sheepButtonStart>…</svg>
  Approve
  <svg sheepButtonEnd>…</svg>
</sheep-button>

An empty slot collapses, so it contributes no gap. Projected svg and img are sized to the control's icon scale; paint is left alone, since an icon declares its own fill/stroke — normally currentColor.

While loading, the spinner takes the leading slot's place so the button keeps its width instead of jumping.

Icon-only buttons need an accessible name:

<sheep-button shape="square" ariaLabel="Delete item">
  <svg sheepButtonStart>…</svg>
</sheep-button>

Accessibility

Focus is a 2px accent outline, never the browser default and never a soft glow. prefers-reduced-motion drops the transitions and slows the spinner, and forced-colors hands the palette over to system colours.

sheep-input

import { SHEEP_INPUT } from '@sheep-ng/ui';
<sheep-input [(value)]="name" required>
  <span sheepInputLabel>Display name</span>
  <span sheepInputHint>Shown on your profile.</span>
</sheep-input>

It renders a real <input>, so typing, selection, autofill, IME composition and the password manager are the platform's. The component owns the label, the hint and the error line, and wires the id, for, aria-describedby, aria-invalid and aria-errormessage between them — the call site never labels an input by hand.

A rule, not a box

The field is one hairline under the value, with empty space doing the framing, so a column of fields reads as a page of writing rather than a stack of containers. There are no side or top borders in either variant, and the value starts flush with the label above it — the look depends on that column being straight.

The height follows from the type rather than from the shared control scale: a button's height is the size of a thing you press, and standing a rule off from its text by that much reads as a gap rather than as an underline. So an underline field is shorter than a button of the same size. soft — a sunk ground under the same rule, for a form dense enough that the rows need telling apart — keeps the button's height, because it has a ground to fill.

The label floats

At rest the label stands where the value will be, in place of a placeholder. It lifts to the row above on focus, and stays there once there is a value:

Only the label moves. Its row is reserved whether it is occupied or not, so the rule never shifts and a column of fields does not jitter as one is filled in. There is nothing to switch on, and two cases resolve themselves:

  • A field given a placeholder keeps its label lifted for good. The two would otherwise print on top of each other, and the placeholder is the one that was asked for.
  • A leading prefix keeps its width but fades out while the label rests over it, so the value never shifts sideways under the caret.

SHEEP_INPUT is the field plus its nine directives, ready to drop into a component's imports. The pieces are standalone and importable one at a time, but a directive that is never written is never instantiated, so importing the set costs nothing.

Signal forms

The field implements FormValueControl<string>, so [formField] binds it end to end and nothing else appears at the call site:

<form (submit)="onSubmit($event)">
  <sheep-input [formField]="f.email">
    <span sheepInputLabel>Email</span>
  </sheep-input>
</form>

required, minLength, maxLength and pattern come down from the schema and reach the native input as real attributes. errors, disabled, readonly, invalid, pending and touched come down too; blur goes back up as touch, which is what submit() and debounce('blur') are keyed on.

Reactive forms bridge through compatForm() in @angular/forms/signals/compat. The field deliberately ships no ControlValueAccessor: [formField] picks the CVA path over the control contract when one is present, which would cost the errors, touched and constraint bindings above.

Errors

An error is { kind, message? } — the shape signal forms uses. Text resolves from the error's own message first, then from SHEEP_INPUT_MESSAGES, which is where required and email get one sentence for the whole app. A kind with no text renders nothing rather than leaking the kind.

providers: [
  provideSheepInputMessages({
    errors: { required: 'Bitte ausfüllen.' },
    showPassword: 'Passwort anzeigen',
  }),
];

Errors stay hidden until touched, which signal forms sets on blur and on every field at submit(). A field raises its own on blur, so it behaves the same with no form behind it. Handing errors in directly — the ones a server sent back — means setting touched alongside them.

Directives

Everything past a value and a label is a directive on the host element, so a plain field carries none of their weight.

| Directive | Adds | | ---------------------- | ------------------------------------------------------------- | | sheepInputLabel | The label. Projected into a real <label for>. | | sheepInputHint | Guidance in the footer, referenced by aria-describedby. | | sheepInputPrefix | Content before the value, inside the box. | | sheepInputSuffix | Content after the value, inside the box. | | sheepPassword | type="password" plus a reveal toggle. | | sheepEmail | type="email", the mobile keyboard hints, and the check. | | sheepNumber | A number pad, a keystroke filter, Arrow Up/Down stepping and min / max / step checks. The value stays a string. | | sheepCounter | A count in the footer, against maxLength when there is one. | | [sheepValidate] | A rule of your own: (value: string) => string \| null. |

sheepPassword and sheepEmail set the type themselves, so there is no type="password" beside them to drift out of sync.

Write your own by implementing SheepInputFeature and registering with the SheepInputFeatures the field provides. Each hook is a plain function of the field's state called inside the field's own computeds, so a signal the feature reads on the way through is tracked as if the field had read it.

Numbers use sheepNumber, not type="number":

<sheep-input sheepNumber min="0" step="0.01" [(value)]="price">
  <span sheepInputLabel>Price</span>
  <span sheepInputPrefix>$</span>
</sheep-input>

Who owns validity. sheepEmail, sheepNumber and sheepValidate raise errors the field renders. A signal form cannot see them, so a form-bound field would show the message while the form still thought itself valid. On a form-bound field, put the rule in the schema — email(), min(), max(), validate() — where the form can act on it. Reach for the directives on a field with no form behind it.

Inputs

| Input | Type | Default | Notes | | ------------------------- | --------------------------------------------------- | ----------- | -------------------------------------------- | | value | model<string> | '' | Two-way, and bound by [formField]. | | variant | underline \| soft | underline | Visual weight. | | size | sm \| md \| lg | md | Shared with every control in the library. | | type | text \| password \| email \| search \| tel \| url | text | A feature directive overrides it. | | placeholder | string \| null | null | Not a label — it is gone once typing starts. | | autocomplete | string \| null | null | Worth setting on every real form field. | | ariaLabel | string \| null | null | Required when no label is projected. | | disabled | boolean | false | Bare attribute allowed. Blocks focus. | | readonly | boolean | false | Keeps the value selectable and focusable. | | required | boolean | false | Native, plus an asterisk on the label. | | pending | boolean | false | Spinner, for an async rule in flight. | | touched | boolean | false | The gate on showing errors. | | invalid | boolean | false | Invalid with nothing to say. | | errors | ValidationError[] | [] | Filled by [formField]. | | name | string | '' | Empty means no attribute at all. | | minLength / maxLength | number \| undefined | undefined | Reach the input as real attributes. | | pattern | RegExp[] | [] | The first one reaches the DOM. |

There is no value output beside the model, and no fullWidth: the field fills its column, and the caller sizes the column.

focus() on the component instance forwards to the native input. reset() forgets the blur it has seen, and so the errors it was showing.

Accessibility

This is the one control in the library that does not take the 2px accent outline on focus: an outline is a box, and a box is what the field is defined by not having. Instead the rule under the value doubles in weight, takes the accent, and grows out of the centre — a larger indicator than the outline it replaces, not a quieter one. The label takes the accent with it.

The reveal toggle is a real focusable button, but takes no focus on a mouse click, so the caret stays where it was mid-word. prefers-reduced-motion drops every transition, including the label's, and forced-colors hands the palette over to system colours.

sheep-dialog

import { SHEEP_DIALOG } from '@sheep-ng/ui';
<sheep-dialog [(open)]="confirming">
  <sheep-dialog-header>Delete project</sheep-dialog-header>

  <sheep-dialog-body>This cannot be undone.</sheep-dialog-body>

  <sheep-dialog-footer>
    <sheep-button (click)="confirming.set(false)">Cancel</sheep-button>
    <sheep-button tone="danger" variant="solid" (click)="remove()"> Delete </sheep-button>
  </sheep-dialog-footer>
</sheep-dialog>

The dialog owns being open, being modal and being dismissed. It owns nothing about what is inside it: the three parts are slots, and the footer's buttons and what they do are the caller's.

The parts are ordered by the component, not by the markup — the header is always the first row and the footer always the last, so a dialog cannot be assembled upside down. The body is the only part that scrolls: the panel stops growing at the viewport and the header and footer stay put.

Underneath is a native <dialog> opened with showModal(), so the focus trap, the backdrop, the top-layer stacking and Escape are the platform's, not a reimplementation of them. It follows that an ancestor's overflow or transform cannot clip the panel, and that no z-index needs choosing.

SHEEP_DIALOG is the dialog and its three parts, ready to drop into a component's imports.

State

open is the state, and it is two-way — nothing opens or closes the dialog behind the caller's back. A dismissal writes false back and says why:

<sheep-dialog [(open)]="editing" (closed)="track($event)">…</sheep-dialog>

show() and close(reason?) on the component instance are the same state, called instead of bound. Bind [(open)] rather than [open]: a one-way binding is re-applied on the next check, which would reopen a dialog the user just dismissed.

Content is created with the caller's view, not when the dialog opens. Wrap an expensive body in @if at the call site if that matters.

Inputs and outputs

| Input / output | Type | Default | Notes | | -------------- | ----------------------------- | ------- | ------------------------------------------------------------- | | open | boolean | false | Two-way. The single source of truth for whether it shows. | | size | sm \| md \| lg | md | Panel width and interior density, together. | | dismissible | boolean | true | Escape, backdrop click and the header's close button, as one. | | ariaLabel | string \| null | null | The name, when there is no header to take it from. | | (closed) | close \| escape \| backdrop | — | Emits once closed, with what closed it. | | align | end \| start \| between | end | On sheep-dialog-footer. Where the buttons sit on the row. |

Actions read left to right in rising commitment, so the one the dialog is for goes last. align="between" pushes a destructive or secondary action away from the pair the eye lands on.

Accessibility

A modal needs an accessible name. sheep-dialog-header carries the id the dialog points aria-labelledby at, so it is announced by the words on screen; a dialog with no header wants ariaLabel instead.

The close button in the header is the dismissal Escape and the backdrop already offer, made visible — it appears exactly when dismissible is set, so the three ways out cannot disagree. A dialog that must be answered turns it off and puts the answers in the footer.

Clicking off the dialog is a pointer gesture with no keyboard counterpart, so the backdrop is not focusable; Escape is the keyboard's way out. A drag that starts inside the panel and ends outside it does not dismiss, so selecting text past the panel's edge cannot throw the dialog away.

prefers-reduced-motion drops the entry animation, and forced-colors gives the panel a system-coloured border.

More components

sheep-chip, sheep-badge, sheep-nav-bar, sheep-list, sheep-tree, sheep-table, sheep-side-sheet and sheepTooltip are covered with examples in AGENTS.md, and in full, input by input, in custom-elements.json.

License

MIT