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

foldkit-primitives

v0.15.3

Published

Ready-made primitives for the Elm architecture, packaged with foldkit-bundle: media queries, presence, timers, pagination, history, sockets, observers, clipboard, and interaction Bundles with their Behaviors.

Readme

foldkit-primitives

Ready-made pieces of browser, clock, device, and interaction state for a Foldkit application: a media query, a timer, a socket, an undo stack, a roving tab stop, a resize observer, a clipboard write. Each comes in the shape Foldkit already has, so adding one adds no store, no hook, and no second reducer. The fact lives in your Model, arrives as a Message, and replays like everything else.

import { MediaQuery } from 'foldkit-primitives/media'

The browser reports; the Model owns. A primitive observes something and hands your update a Message. What your application keeps, and what it does about it, stays yours.

Which form a primitive takes

Every primitive is one of five things, chosen by what it has to own:

| It needs to own | Form | Joins the application by | Examples | | --- | --- | --- | --- | | A fact that outlives the moment: a match, a count, a page, a position | bundle | placing it in the parent, as a field and a wrapper Message | MediaQuery, Timer, Pagination, websocket | | Interaction state a view's slots must reflect | bundle + Behavior | placing it, then attaching its Behavior to the view's slots | RovingTabindex, Press, DismissLayer | | Nothing: a stream you react to | entry | lifting it into your Subscriptions, mapped to your Message | keyboardEvents, ticks, broadcastMessages | | Nothing: observation bound to one element | Mount | attaching it in the view with h.OnMount | Resize, Autofocus, KeepInView | | Nothing: work done once that reports back | Command | returning it from update | copyText, share, enterFullscreen | | Nothing: a value computed from what you have | function | calling it | range, matchHotkey, formatRelativeTime |

The lighter form wins when in doubt. Promote to a bundle the day the state needs a name in the Model.

The one dependency is foldkit-bundle, whose placement mechanism every bundle here uses. interaction additionally needs foldkit-mixins, an optional peer, because its Behaviors write to a view's slots.

Install

pnpm add foldkit-primitives foldkit-bundle effect foldkit

effect and foldkit are peer dependencies. Import by subpath (/media, /net, /time, /state, /motion, /interaction, /device, /events, /observers, /dom) so a bundler drops what you never import.

Sixty seconds: follow the color scheme

A bundle is placed in a parent, which gains a field for its state and a wrapper Message for its transitions:

import { Schema } from 'effect'
import type { HtmlBuilder } from 'foldkit/html'
import { Bundle } from 'foldkit-bundle'
import { MediaQuery } from 'foldkit-primitives/media'

const Page = Bundle.compose({ theme: Schema.String }).pipe(
  Bundle.withMessages({ ThemeSet: { theme: Schema.String } }),
  Bundle.withChild('dark', MediaQuery, { args: { query: '(prefers-color-scheme: dark)' } }),
)
type Model = typeof Page.Model.Type
type Message = typeof Page.Message.Type

const { placements } = Page

const config = placements.complete({
  init: () => placements.initial({ theme: 'light' }),
  update: placements.update(model => ({ model })),
  view: (model: Model, h: HtmlBuilder<Message>) =>
    h.div([], [model.dark.matches ? 'Dark mode' : 'Light mode']),
  subscriptions: placements.subscriptions(),
})

What each line does, and does not do:

  • Bundle.compose(...).pipe(withChild('dark', MediaQuery, …)) states the parent: its own field theme, its own Message ThemeSet, and MediaQuery under dark. The Model is { theme, dark: { matches } }; the Message union gains GotDarkMessage. Nothing runs.
  • placements.initial({ theme: 'light' }) gives the fields no placement owns; dark starts at { matches: false }.
  • placements.update(own) routes GotDarkMessage to the bundle and everything else to your own update.
  • placements.subscriptions() is the matchMedia stream, lifted. The browser is touched only when the runtime subscribes, which then corrects matches at once.
  • config is an ordinary Foldkit application config. Hand it to the runtime.

The Solid line this replaces is createMediaQuery('(prefers-color-scheme: dark)'). The difference is where the answer lives: in the Model, where replay, DevTools, an agent, and a server render all see it.

The other three ways in

An entry, lifted into your Subscriptions and mapped to your Message. The parent keeps whatever it wants of the stream:

import { Stream } from 'effect'
import * as Subscription from 'foldkit/subscription'
import { keyboardEvents } from 'foldkit-primitives/events'

const subscriptions = Subscription.make<GameModel, GameMessage>()(() => ({
  keys: Subscription.persistentEntry(
    keyboardEvents({ preventDefault: press => press.key.startsWith('Arrow') }).pipe(
      Stream.filter(event => event._tag === 'Pressed'),
      Stream.map(({ key }) => GameMessage.PressedKey({ key })),
    ),
  ),
}))

A Mount, attached to the element it observes. No ref to thread: the Mount receives its element.

import { Resize } from 'foldkit-primitives/observers'

const panel = (model: PanelModel, h: HtmlBuilder<PanelMessage>) =>
  h.div([h.OnMount(Resize())], [`${model.width} × ${model.height}`])

A Command, returned from update, whose result comes back as a Message your union wraps:

import { mapMessage } from 'foldkit/command'
import { copyText } from 'foldkit-primitives/dom'

const copy = (model: NoteModel): Update.Return<NoteModel, NoteMessage> => ({
  model,
  commands: [mapMessage(copyText(model.text), message => NoteMessage.Clipboard({ message }))],
})

Find one by the job

| You want to | Reach for | In | | --- | --- | --- | | Follow the color scheme, reduced motion or any media query | MediaQuery, PrefersDark, PrefersReducedMotion | media | | Change the layout at named widths | Breakpoints | media | | Know whether the tab is visible, the window's size, or the user idle | Visibility, WindowSize, Idle | events | | Answer keyboard shortcuts | keyboardEvents with matchHotkey | events | | Count ticks, or run something on an interval | Timer, Interval, ticks | time | | Wait until typing stops, or limit how often something runs | debounce, Throttle | time | | Say "3 minutes ago" | formatRelativeTime | time | | Page through a list | Pagination, range | state | | Undo and redo | history | state | | Draw only the visible rows of a long list | Virtual | state | | Animate a value, or let an element leave before it is removed | Tween, Spring, Presence | motion | | Move with arrow keys through a toolbar, list, grid or tree | RovingTabindex, ListNavigation, GridNavigation, TreeNavigation | interaction | | Close a popup on Escape or an outside press | DismissLayer, Overlay | interaction | | Press, long-press or drag | Press, LongPress, Move, PointerDrag | interaction | | Read where the pointer is inside an element (a color area, a dial, a pad drawn on) | PointerPosition | dom | | Edit text in place | EditableText | interaction | | Tell a screen reader what changed | LiveAnnounce | interaction | | Know whether the browser is online, or talk over a socket | Online, websocket, sse | net | | Talk between tabs | broadcastMessages, SharedHost | net | | Use the camera, the location or a permission | mediaStream, Geolocation, permissions | device | | Track one element's size or visibility | Resize, Intersection, Bounds | observers | | Focus, mask or keep an element in view | Autofocus, InputMask, KeepInView | dom | | Copy, share, load a script or restore scroll | copyText, share, loadScript, keepScroll | dom |

Working widgets built from the interaction primitives are listed under In the examples.

The map

Each subpath is one concern and one import. Its README has the full reference: every Model shape, Message, argument, and failure rule.

| Subpath | What it covers | Primitives | | --- | --- | --- | | media | the environment | MediaQuery (+ PrefersDark, PrefersReducedMotion), Breakpoints, platformFromUA, isBrowser | | net | the network | Online, websocket, sse, broadcastMessages, postBroadcast, SharedHost | | time | the clock | Timer, Interval, ticks, debounce, Throttle, formatRelativeTime | | state | UI state with nowhere else to live | Pagination, history, Locale, SelectionSet, Virtual, range, layout math | | motion | animation | Tween, Spring, Presence, the Motion service | | interaction | keyboard, pointer, and focus patterns, with their Behaviors | RovingTabindex, Typeahead, ListNavigation, GridNavigation, TreeNavigation, FocusScope, Press, LongPress, Move, Targets, PointerDrag, EditableText, FocusVisible, DismissLayer, Layers, Selection, LiveAnnounce | | device | hardware | Geolocation, mediaDevices, mediaStream, permissions, fullscreen | | events | raw window events | Visibility, WindowSize, Idle, InputModality, keyboardEvents, pointerEvents, scrollEvents, activeElementEvents, matchHotkey | | observers | element observers | Resize, Intersection, Mutation, Bounds | | dom | element Mounts and one-shot Commands | Autofocus, InputMask, KeepInView, Measure, FluidHover, ScrollBars, PointerPosition, FocusScope, FollowTabStop, ScrollLock, HideOutside, copyText, readText, share, loadScript, decodeImage, keepScroll, pointedLinks |

Rules every primitive keeps

These hold across the package, so the subpath pages do not repeat them.

  • A fact that changed nothing returns the Model it was given. A repeated resize, Started while running, a total already held: the placement then returns the parent unchanged, Foldkit renders nothing, and a createLazy view over the slice is skipped.
  • Init is a safe default, not a read. matches: false, online: true, a count of zero. A server render shows these; the subscription corrects them on the client. Do not infer readiness from the default.
  • Without the platform API, nothing throws. A stream is empty and the slice keeps its default; a Command yields its failure Message (CopyFailed, ShareFailed); a Mount emits nothing.
  • One placement observes once. Two placements of one query open two listeners; share the field. One assembly holds one socket, stream, camera, or permission watch: their resource tags are per module, and assemble refuses the second.
  • An OutMessage cannot be dropped by omission. debounce, Throttle, Press, DismissLayer, and the rest that surface a settled value require onOut at placement. Bundle.ignore drops one on purpose.
  • Everything timed runs on Effect's clock. Timers, tweens, debounces, presence, idle, typeahead. Tests advance them with TestClock.adjust instead of waiting.
  • Reduced motion is a service. Provide Motion.live through the assembly's resources, or Motion.reduced and Motion.full in a test; absent, motion is full.
  • Elements never cross into the Model. A focus report is { tag, id }, a mutation names nodes, a target is its marked id.

Testing a placement

Three ingredients, each shown in this package's test/ directory under the primitive's name:

  1. Substitute the environment. Factories take a create (streams, devices, permissions, sockets) or request (camera) double, so a test passes a fake instead of stubbing a global.
  2. Advance time instead of waiting. Anything on Effect's clock runs under TestClock.adjust. Yield after forking before the first adjust, or dispatched events reach listeners not yet registered.
  3. Never hang on a quiet stream. Collect with a bounded take that fails fast naming the stall.

With Surface and Mirror

Placed state is ordinary Model, so nothing here has a Surface or Mirror API of its own. Declare a Surface over the placed fields to render them or expose them to an agent; point Mirror.url at them to link them; spread the bundle's cases into the same unions. A slice that must survive a reload persists through Mirror.kv; this package owns live facts only.

Limits

  • Presets cover the common media queries; anything else passes args.
  • Virtual windows one axis of a list; windowed grids stay out by design.
  • Sync.lww-style merging, collaborative text, and other replicated state are foldkit-sync's, not a primitive.