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

@akinon/akifilter

v1.12.2

Published

Akifilter is a filtering library for Akinon frontend applications.

Readme

akifilter

Legacy Functionality Snapshot (@akinon/ui/akifilter-old)

  • Schema-driven forms: Builds the main filter UI from filterSchema using FormBuilderWatch, applying labelRightAligned props and reflecting initial values from useAppliedFilters[0] or persisted storage.
  • Context-managed state: FilterContextProvider derives mainPath from the router (or customMainPath), stores appliedFilters, applicableFilters, and shownFilters via useImmerReducer, and exposes actions for updates.
  • Local storage persistence: Reads/writes appliedFilters-{mainPath} and shownFilters-{mainPath} so filter selections survive reloads; defaults to the first eight filters when no history exists.
  • Parent synchronization: Propagates every applied-filter change back through useAppliedFilters[1], allowing host apps to react or override initial state.
  • Applied filter chips: Renders active filters with labels sourced from placeholders, formats values (dates via akidate utilities, arrays, booleans) for display, and supports removing individual filters or clearing all.
  • Dynamic filter visibility: SelectShownFilters opens a modal with searchable, paginated checkboxes for every applicable filter; selections update shownFilters and trigger form recreation to avoid stale state.
  • Conditional filters: Optional conditionalFiltersSchema renders a secondary form whose values merge into appliedFilters without local storage persistence, rebuilding whenever the schema changes.
  • Utility helpers: Provides path parsing (getPathFragments) to scope state per route and a tree search helper (getTreeNodeByValue) for nested option lookups.
  • Internationalization scaffolding: Loads static i18n bundles (EN/TR) through Akilocale for component copy such as the panel title.

Implementation Plan (User Experience Focus)

  1. Filter Workspace Shell

    • Recreate the legacy layout with updated styling hooks, keeping the header title, filter-selection trigger, applied filter strip, and main filter grid so existing users feel familiar while allowing future visual tweaks.
    • Integrate akilocale for default copy (title, modal labels) and expose translation keys for host apps.
  2. Schema Intake & Form Rendering

    • Accept an external filterSchema (and optional conditionalFiltersSchema) and translate each field into akiform widgets, honoring component-specific props, labels, and placeholders.
    • Default-render only “primary” filters on first load (configurable count), but respect any persisted selections on subsequent visits.
  3. User Input & Applied State

    • Mirror user edits into an internal state model while emitting JSON payloads to the host via the provided useAppliedFilters tuple so parent apps receive canonical data.
    • Display currently applied filters as removable chips; value formatting should transparently handle primitives, arrays, booleans, and date-like objects.
  4. Filter Visibility Management

    • Provide a modal (using @akinon/ui-modal, @akinon/ui-input, @akinon/ui-pagination) that lists all available filters with search, pagination, and checkbox toggles to control visibility.
    • Persist visibility selections and main-form order to local storage, ensuring a stable experience across sessions.
  5. Persistence Strategy

    • Derive unique local-storage keys using a caller-supplied prefix plus a deterministic suffix (e.g., hash of schema or route) so multiple filter instances cannot collide.
    • Support optional overrides (custom key + custom main path) while always appending our uniqueness salt.
  6. Conditional Filter Surface

    • Render an auxiliary akiform section beneath the main filters when conditionalFiltersSchema exists, reset its form state whenever the schema changes, and merge its values into the emitted applied-filter JSON without persisting to storage.
  7. Bulk Actions & Import Filters

    • Retain the “clear all” action for quick resets, updating UI state, storage, and parent callbacks consistently.
    • Plan dedicated affordances for CSV/XLS import: display buttons in the options toolbar, accept uploads via @akinon/ui-upload, and parse/normalize records into applied filters (details to be implemented in later stages).
  8. Testing & Quality Targets

    • Cover all interactive flows (form entry, chip removal, modal toggles, persistence, conditional schema changes, CSV/XLS hooks) with Vitest and Testing Library, targeting ≥90% coverage without regressing existing suites.
    • Document sample integration snippets so partner apps can verify behavior during development.

Value Persistence (persistValues)

By default, field values and active filter buttons are written to local storage and restored on the next mount. Set persistValues={false} when the filter has to start from values the host supplies (for example filters loaded from an API):

<Akifilter
  filterSchema={filterSchema}
  storageNamespace="saved-filters"
  persistValues={false}
  defaultValues={savedFilters}
  onValuesChange={setFilters}
/>
  • Values and button state are never read or written; the form starts from schema defaults + defaultValues, and buttons start inactive.
  • Values stored before the prop was set are ignored, not deleted.
  • Visible-field (and per-section visibility) persistence is unaffected.
  • Leaving the prop out keeps today's behaviour (true).
  • persistValues and defaultValues are read on mount; changing them later does not reset the form. When the values come from an API, render Akifilter only after they have loaded. Turning persistValues off after mount stops further writes but keeps what was already restored.

Footer

footer renders host content inside the filter card, below every field and section. Pass a node, or a function that receives { values } (the onValuesChange shape, not debounced) to render conditionally. A falsy result (null, undefined, false, '', 0) renders nothing:

<Akifilter
  filterSchema={filterSchema}
  footer={({ values }) =>
    Object.keys(values).length > 0 ? <Button>Save as rule</Button> : null
  }
/>

The footer sits outside the form, so a button there never submits it.


Custom Title

title replaces the default translated "Filters" heading. Pass a string or a node; pass null, false or '' to hide the heading and keep only the toolbar:

<Akifilter filterSchema={filterSchema} title="Order Filters" />

Section Fields

Fields declared with type: 'section' render as collapsible panels below the main filter grid. Every field inside a section is part of the same form state as the top-level filters, so section values are emitted through onValuesChange and persisted to local storage exactly like regular filters.

const filterSchema = [
  { key: 'sku', type: 'text', label: 'SKU', config: { visible: true } },
  {
    key: 'excludeSection',
    type: 'section',
    label: 'Exclude Filters',
    isExcludeSection: true,
    fields: [{ key: 'excludeSku', type: 'text', label: 'Exclude SKU' }]
  }
];

Section flags

| Flag | Effect | | ------------------- | ---------------------------------------------------------------------------------------- | | defaultExpanded | false starts the panel collapsed. | | isExcludeSection | Chips for the section's fields render with the exclude styling (red chip + exclude icon). | | selectableFields | Adds a funnel button that opens a per-section visibility modal. | | enableImportCsv | Adds a CSV import button to the section header. Requires onSectionImportCsv. | | enableImportXls | Adds an XLS/XLSX import button to the section header. Requires onSectionImportXls. |

Only Akifilter reads these flags. isExcludeSection and selectableFields are declared on SectionField in @akinon/akiform-builder for historical reasons and have builder setters of their own; enableImportCsv / enableImportXls are Akifilter's own props and reach a builder-made section through field().extend({ enableImportCsv: true }), which is where any new Akifilter-only prop belongs. See the note on SectionField in that package's types.ts.

Persistence

Section values are stored under the same …:values local-storage entry as the top-level filters and rehydrated on the next mount, so a page refresh keeps exclude filters filled in. Per-section visibility selections (selectableFields) are stored separately under …:section:<sectionKey>.

The storage key is hashed from the top-level field signature only, so editing a section's fields does not invalidate the entry (and existing entries survive an upgrade). Stale values from an older schema are instead rejected on read: a persisted value whose shape cannot fit its field's current type/mode is dropped rather than restored — e.g. an array left behind by a select/multiple field is not fed into a field that is now text. file fields are never restored at all, since a File cannot survive JSON serialisation.

Section field visibility

onVisibleFieldsChange reports the top-level fields only. To follow what a section renders, pass onSectionVisibleFieldsChange — it receives the section's key and the keys of its visible fields, on mount and whenever the list changes.

const handleSectionVisibleFieldsChange = useCallback(
  (sectionKey: string, keys: string[]) =>
    setSectionVisibleKeys(current => ({ ...current, [sectionKey]: keys })),
  []
);

<Akifilter
  filterSchema={filterSchema}
  onSectionVisibleFieldsChange={handleSectionVisibleFieldsChange}
/>;

For a selectableFields section the keys follow the section's visibility modal; any other section renders all of its fields and reports every key. Keep the handler's identity stable, as with onVisibleFieldsChange: a new function on every render re-notifies with an unchanged list.

Section-level CSV / XLS import

A section can expose its own file-import buttons in the collapse header — the same affordance the top-level toolbar offers through enableImportCsv / onImportCsv, and rendered conditionally in the same way as the selectableFields funnel. This is typically used to exclude a large set of records by uploading a file instead of typing values one by one.

A button renders only when both halves are present: the schema flag on the section and the matching handler on the component. The handler receives the section's key first, so a single handler can serve every section.

<Akifilter
  filterSchema={[
    {
      key: 'excludeSection',
      type: 'section',
      label: 'Exclude Filters',
      isExcludeSection: true,
      enableImportCsv: true,
      enableImportXls: true,
      fields: [{ key: 'excludeSku', type: 'text', label: 'Exclude SKU' }]
    }
  ]}
  onSectionImportCsv={(sectionKey, file) => uploadExcludeFile(sectionKey, file)}
  onSectionImportXls={(sectionKey, file) => uploadExcludeFile(sectionKey, file)}
/>

Akifilter never reads or parses the file — it only surfaces the picker and hands the File to the host application. To show the upload as an active filter, feed it back through externalAppliedFilters (set isExclude: true on the chip to match the exclude styling) and clear it from onRemoveExternalFilter.

Import buttons combine freely with selectableFields: the funnel is rendered after the file buttons inside the same header action group. Both mouse clicks and Enter presses are stopped from bubbling out of that group, so activating a header action never toggles the panel underneath it.

With the builder API:

field()
  .key('excludeSection')
  .type('section')
  .label('Exclude Filters')
  .isExcludeSection(true)
  // Akifilter's own flags travel through `extend()`; `isExcludeSection` and
  // `selectableFields` predate it and keep their setters.
  .extend({ enableImportCsv: true, enableImportXls: true })
  .fields([field().key('excludeSku').type('text').label('Exclude SKU').build()])
  .build();

Localization

Section header action copy lives under the section.actions namespace (importCsv, importXls, importCsvTooltip, importXlsTooltip). The tooltip keys interpolate {{section}} with the section's label.


Field Labels

A field's label drives its form input. Two optional props override it for one surface each, so the chip and the picker can say something different from the input:

| Prop | Overrides | Falls back to | | ----------------- | ---------------------------------------------- | ------------- | | appliedLabel | The applied-filter chip (and its remove button's accessible name). | label → placeholder → key | | visibilityLabel | The field's row in the "Select visible filters" modal. | label → placeholder → key |

const filterSchema: AkifilterSchema = [
  {
    key: 'orderNo',
    type: 'text',
    label: 'Order No', // form input
    appliedLabel: 'Order number', // chip: "Order number: 1001"
    visibilityLabel: 'Order reference no' // modal row
  },
  {
    key: 'excludeSection',
    type: 'section',
    label: 'Exclude Filters',
    isExcludeSection: true,
    fields: [
      {
        key: 'excludeStatus',
        type: 'text',
        label: 'Status',
        // The chip is rendered outside the section, so it carries the context
        // the section header would otherwise provide.
        appliedLabel: 'Excluded status'
      }
    ]
  }
];

Notes:

  • Both props are optional. A field that sets neither behaves exactly as before, so adding them to one field never affects the rest of the schema.

  • Section children support them too, including inside exclude sections.

  • The visibility modal's search box matches visibilityLabel, label, placeholder and key, so an overridden field stays findable under either name.

  • Neither prop affects the form input's own label or its accessible name — use label for that.

  • Storage is keyed by field key, never by any label, so renaming or overriding labels does not invalidate persisted values or visibility selections.

  • They are Akifilter's own props, not part of the shared form schema. With the fluent builder they travel through its generic extend() hatch:

    import { field } from '@akinon/akiform-builder';
    
    field()
      .key('orderNo')
      .type('text')
      .label('Order No')
      .extend({ appliedLabel: 'Order number' })
      .build();

    The call is type-checked even though the builder knows nothing about filters: Akifilter declares its props on the builder's FieldExtensions interface, and extend() accepts only declared keys — a typo or a wrong type is a compile error, and an application that wants to hang its own metadata on a field declares it the same way:

    declare module '@akinon/akiform-builder' {
      interface FieldExtensions {
        requestKey?: string;
      }
    }

    AkiformBuilder ignores all of it — it renders the form label from label alone.

See the With Distinct Labels Storybook story for a live example.


Multi-Value Filters and Switchable Filter Types

Tags input out of the box

A select field with mode: 'tags' and no options is a free-form multi-value input: type a value, press Enter to turn it into a removable tag, repeat. The applied chip joins the entries with , .

{
  key: 'keywords',
  type: 'select',
  label: 'Keywords',
  mode: 'tags',
  options: []
}

mode is part of the storage signature, so switching a field between single and multi-value modes invalidates its persisted entry instead of restoring an incompatible shape.

One field, several chips (appliedChips)

By default a field contributes one applied-filter chip. A field that owns several filters at once — a bucket per filter type, a range that reads better as two chips — can produce its own instead:

{
  key: 'attributeName',
  type: 'custom',
  label: 'Attribute Name',
  // Value shape is the field's own business, e.g.
  // { mode: 'exact', buckets: { contains: ['a', 'b'], exact: ['c'] } }
  appliedChips: ({ field, value, formValues }) => [
    { id: 'contains', label: 'Attribute Name (Contains)', value: 'a, b' },
    { id: 'exact', label: 'Attribute Name (Exact match)', value: 'c' }
  ],
  // Return the value the field should hold after the chip is removed,
  // or `null` to clear the field.
  onRemoveAppliedChip: ({ field, value, id }) => dropBucket(value, id),
  render: ({ control }) => <MyInput control={control} />
}

| Prop | Behaviour | | ---- | --------- | | appliedChips | Replaces the field's single chip with one chip per returned entry. Return [] for "no chips right now". Each entry is { id, value, label?, isExclude? }; label defaults to the field's applied label, isExclude to whether the field sits in an exclude section. | | onRemoveAppliedChip | Maps one chip's × back onto a new field value, which Akifilter commits, persists and emits. Without it the chips render but cannot be removed. |

id must be stable for the same logical entry and unique within the field. Chip keys are <field key>::<chip id> internally and are split at the first separator, so an id may contain :: — a field key may not.

appliedChips is only called while the field's own value is non-empty — the emptiness rules that suppress a plain chip (null/undefined, a blank string, an empty array) suppress these too. Use formValues to format the chips (a unit or a mode held by a sibling field), not to derive them from a sibling while this field holds nothing.

With the fluent builder both props travel the same route as the label overrides: field().…​.extend({ appliedChips, onRemoveAppliedChip }).

Because every chip is derived from the field's form value, persistence comes for free: Akifilter stores and rehydrates the composite value like any other one, and Clear all clears the field like any other. Nothing has to be mirrored into host state or written to storage by the application.

Declaring appliedChips also tells the storage layer that the field owns its value shape, so a composite (object) value is rehydrated instead of being rejected as schema drift — the same exemption type: 'custom' gets. Without the prop, only custom fields may hold object values across a reload.

A filter whose type the user can switch

Some filters carry a type next to their value — contains / exact match / starts with — where each type emits under its own request key and several types can be active at the same time. The With Tag Type Selector Storybook story implements the whole flow on top of the pieces above:

| Concern | Mechanism | | ------- | --------- | | The input (tags + the type switcher on its right) | A custom field. Bind it with useController({ control: renderProps.control, name }). | | Where the state lives | One form value holding a bucket per type plus the active type — so Akifilter persists and rehydrates all of it, active type included. | | One chip per type, each with its own label | appliedChips. | | Removing a single type | onRemoveAppliedChip, returning the value without that bucket (null when the last one goes). | | Mapping buckets onto request keys | Host logic, since one logical filter can emit several keys at once. |

The flow the story demonstrates:

  1. With Contains active, entering filter a and filter b produces Attribute Name (Contains): filter a, filter b.
  2. Switching the type leaves that bucket with its own chip and shows the new type's (empty) bucket in the input.
  3. Entering filter c adds Attribute Name (Exact match): filter c, and the payload carries both request keys.
  4. Switching back shows the earlier bucket in the input again.
  5. A page refresh restores every bucket and the active type.