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

@avament-pub/select

v0.3.3

Published

Canonical searchable Select + Radix primitives for Avament apps

Readme

@avament-pub/select

Canonical React Select for Avament apps — single or multiple choice, with a simple mode for short fixed lists and a searchable mode for growing catalogs (companies, types, clients, …).

| | | |---|---| | Package | @avament-pub/select | | React | ≥ 18 (peer) | | License | MIT |


Table of contents

  1. Install
  2. What you get
  3. When to use which mode
  4. Minimal usage
  5. Searchable + multi + recent
  6. Option actions & free footer
  7. Custom trigger, controlled open & portal
  8. Props API
  9. Exports
  10. Theming
  11. Localization
  12. Troubleshooting

1. Install

npm install @avament-pub/select
yarn add @avament-pub/select
pnpm add @avament-pub/select

Requirements

  • React 18+ and React DOM 18+
  • Client component in Next.js App Router ("use client")

Optional Next.js:

// next.config.ts
transpilePackages: ["@avament-pub/select"];

2. What you get

High-level Select (catalog / growing lists)

  • mode="simple" | mode="searchable"
  • Single or multiple, A–Z sort, search autofocus, keyboard, recent pins
  • Free header / footer / option actions

Radix primitives (drop-in for local shadcn Select)

For screens still built with SelectTrigger / SelectContent / SelectItem:

import {
  SelectRoot,      // was: Select (Radix Root)
  SelectTrigger,
  SelectValue,
  SelectContent,
  SelectItem,
  SelectGroup,
  SelectLabel,
  SelectSeparator,
} from "@avament-pub/select";

Rename only the Root import (Select → SelectRoot) to avoid clashing with the high-level catalog Select. Self-contained CSS variables — no Tailwind content scan needed.


3. When to use which mode

| Mode | Use for | Search | |---|---|---| | mode="simple" (default) | Gender, protocol type, ~3–5 fixed enums | No | | mode="searchable" | Companies, workshops, clients, suppliers, types, users — anything that can grow | Yes |


4. Minimal usage

Simple single (e.g. gender)

"use client";

import { useState } from "react";
import { Select } from "@avament-pub/select";

const GENDER = [
  { value: "female", label: "Female" },
  { value: "male", label: "Male" },
  { value: "other", label: "Other" },
];

export function GenderField() {
  const [value, setValue] = useState<string | null>(null);

  return (
    <Select
      mode="simple"
      label="Gender"
      placeholder="Select gender"
      options={GENDER}
      value={value}
      onChange={setValue}
    />
  );
}

Searchable single

"use client";

import { useState } from "react";
import { Select } from "@avament-pub/select";

export function TypeField({ options }: { options: { value: string; label: string }[] }) {
  const [value, setValue] = useState<string | null>(null);

  return (
    <Select
      mode="searchable"
      label="Type"
      options={options}
      value={value}
      onChange={setValue}
      labels={{ searchPlaceholder: "Search" }}
    />
  );
}

5. Searchable + multi + recent

"use client";

import { useState } from "react";
import { Select } from "@avament-pub/select";

export function CompaniesField({ options }) {
  const [value, setValue] = useState<string[]>([]);
  const [recent, setRecent] = useState<string[]>([]);

  return (
    <Select
      mode="searchable"
      selection="multiple"
      selectAll
      showRecent              // last 3; or showRecent={5}
      recentValues={recent}
      onRecentValuesChange={setRecent}
      label="Responsibles"
      options={options}
      value={value}
      onChange={setValue}
      selectedCountLabel={(n) => `${n} selected`}
      header={
        <button type="button" style={{ color: "#2b60e6", border: "none", background: "none" }}>
          + Add company
        </button>
      }
      footer={(ctx) => (
        <div style={{ padding: 8, borderTop: "1px solid #e5e7eb", fontSize: 12 }}>
          {ctx.selectedCount} selected
        </div>
      )}
    />
  );
}

showRecent pins MRU values above a separator; the same options remain in the A–Z list below. Omit recentValues to let Select track recent state internally.


6. Option actions & free footer

<Select
  mode="searchable"
  options={options}
  value={value}
  onChange={setValue}
  optionActionsVisibility="hover"
  renderOptionActions={(option, ctx) => (
    <>
      <button type="button" onClick={() => edit(option)}>Edit</button>
      <button
        type="button"
        onClick={() => {
          remove(option);
          ctx.close();
        }}
      >
        Delete
      </button>
    </>
  )}
  footer={(ctx) => (
    <div style={{ display: "flex", gap: 8, padding: 8, borderTop: "1px solid #eee" }}>
      <button type="button" onClick={ctx.close}>Cancel</button>
      <button type="button" onClick={ctx.clear}>Clear</button>
      <button
        type="button"
        onClick={() => {
          save(ctx.value);
          ctx.close();
        }}
      >
        Save
      </button>
    </div>
  )}
/>

Clicks inside endAdornment / renderOptionActions do not toggle the row selection.

SelectPanelContext (header / footer render fn)

| Field | Meaning | |---|---| | open / close() | Panel open state | | query / filtered | Search string and filtered options | | selectedCount / value | Current selection | | clear() | Clear selection | | selectAllFiltered() / allFilteredSelected | Multi helpers |


7. Custom trigger, controlled open & portal

Use these for table cells (TipTap HTML, status letter) and any overflow-clipped container:

"use client";

import { useState } from "react";
import { Select } from "@avament-pub/select";

const STATUS = [
  { value: "o", label: "Open" },
  { value: "d", label: "Done" },
  { value: "i", label: "In progress" },
];

export function StatusCell({ isEditing, onEditingChange }) {
  const [value, setValue] = useState<string | null>("o");

  return (
    <Select
      mode="simple"
      options={STATUS}
      value={value}
      onChange={(v) => {
        setValue(v);
        onEditingChange(false);
      }}
      open={isEditing}
      onOpenChange={onEditingChange}
      portal                 // body portal + position:fixed
      portalZIndex={300}     // optional; default 200
      renderTrigger={({ triggerProps, selectedOption, open }) => (
        <button
          type="button"
          {...triggerProps}
          style={{
            width: 36,
            height: 36,
            fontWeight: 700,
            border: open ? "1.5px solid #2b60e6" : "1px solid #d1d5db",
          }}
        >
          {(selectedOption?.value ?? "?").toString().toUpperCase()}
        </button>
      )}
    />
  );
}

| Prop | Role | |---|---| | renderTrigger | Replace the default button. Spread triggerProps for a11y + keyboard. | | open / defaultOpen / onOpenChange | Controlled or uncontrolled panel lifecycle (e.g. table isEditing). | | portal | Render panel in document.body with position: fixed under the trigger. | | portalZIndex | Stacking for the portaled panel (default 200). |


8. Props API

Shared

| Prop | Type | Default | Description | |---|---|---|---| | options | SelectOption<T>[] | — | { value, label, disabled?, icon?, endAdornment? } | | mode | "simple" \| "searchable" | "simple" | Searchable adds search + autofocus | | autoFocusSearch | boolean | true | Searchable only | | selectAll | boolean | false | Searchable + multiple | | header / footer | ReactNode \| (ctx) => ReactNode | — | Unstyled slots | | renderTrigger | (props) => ReactNode | — | Custom trigger (table cells, etc.) | | open / defaultOpen | boolean | — | Controlled / uncontrolled open | | onOpenChange | (open: boolean) => void | — | Open lifecycle | | portal | boolean | false | Body portal + fixed position | | portalZIndex | number | 200 | Portaled panel z-index | | renderOptionActions | (option, ctx) => ReactNode | — | Trailing actions | | optionActionsVisibility | "always" \| "hover" | "always" | Hide actions until highlight/hover | | sortAlphabetically | boolean | true | Sort by label | | showRecent | boolean \| number | off | true → last 3 | | recentValues | T[] | — | Controlled MRU | | onRecentValuesChange | (T[]) => void | — | MRU updates | | label / placeholder | string | — | Field chrome | | labels | SelectLabels | English defaults | Search / empty / a11y strings | | disabled | boolean | false | |

Single (selection omitted or "single")

| Prop | Type | |---|---| | value | T \| null | | onChange | (value: T \| null) => void | | clearable | boolean (default true) |

Multiple (selection="multiple")

| Prop | Type | |---|---| | value | readonly T[] | | onChange | (value: T[]) => void | | selectedCountLabel | (count: number) => string |


9. Exports

import {
  Select,
  useSelect,
  filterOptions,
  sortOptions,
  pushRecentValue,
  selectCssVars,
} from "@avament-pub/select";

import type {
  SelectProps,
  SelectOption,
  SelectLabels,
  SelectPanelContext,
  SelectOptionActionsContext,
  SelectTriggerRenderProps,
  SelectMode,
  SelectSelection,
  SelectSlotRender,
} from "@avament-pub/select";

10. Theming

Select ships with inline CSS variables (no required CSS import). Override on a parent:

.my-form {
  --ava-select-primary: #2b60e6;
  --ava-select-border-focus: #2b60e6;
  --ava-select-selected-bg: #e8effc;
  --ava-select-radius: 8px;
}

Available tokens include --ava-select-border, --ava-select-bg, --ava-select-text, --ava-select-muted, --ava-select-hover, --ava-select-selected-text, --ava-select-shadow, --ava-select-font.

Or import selectCssVars and spread onto a wrapper style.


11. Localization

Pass labels:

labels={{
  searchPlaceholder: "Suchen…",
  selectAllLabel: "Alle auswählen",
  emptyMessage: "Keine Treffer",
  clearAriaLabel: "Auswahl löschen",
  listAriaLabel: "Optionen",
}}

12. Troubleshooting

| Issue | Fix | |---|---| | Search missing | Set mode="searchable" | | Options not A–Z | Default sorts; check sortAlphabetically={false} isn’t set | | Footer looks unstyled | Expected — you style footer yourself | | Actions toggle selection | Put buttons in renderOptionActions / endAdornment (clicks are stopped) | | Recent empty | Select something first, or pass recentValues | | Panel clipped in table | Set portal | | Need custom cell trigger | Use renderTrigger + spread triggerProps | | Sync with table isEditing | Controlled open / onOpenChange | | Next.js SSR error | Use "use client" on the consuming component |


Versioning & publish

Package version lives in package.json. After README-only changes, bump the version before npm publish.