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-mixins-surface

v0.4.1

Published

Bridge foldkit-surface's projected Model and Message subset to foldkit-mixins SlotViews.

Readme

foldkit-mixins-surface

Connects a foldkit-surface feature boundary to a foldkit-mixins SlotView without repeating that boundary in the view layer.

The two packages answer different questions:

Surface   -> what may this feature observe and which Messages may it emit?
SlotView  -> where may this view be customized with Style and Behavior?

SurfaceView.define joins them. The SlotView's input becomes exactly the Surface's projected value, and its Message universe becomes exactly the Surface's exposed subset. A Behavior cannot quietly read a root-Model field the Surface omitted or emit a Message the feature did not expose; both mistakes are type errors where the view is defined.

The bridge creates no state, no renderer, and no second runtime. The result is an ordinary SlotView with the Surface's boundary reflected in its types.

Use it when

You already render a Surface and want to style or decorate that feature through typed Mixins extension points. If you are not using foldkit-surface, use SlotView.define / SlotView.forMessages from foldkit-mixins directly — this package adds nothing else.

For the Mixins mental model itself (Slot, Style, Behavior, SlotView), read Inside-out view composition first.

Install

pnpm add foldkit-mixins foldkit-surface foldkit-mixins-surface

foldkit, effect, foldkit-mixins, and foldkit-surface are peer dependencies.

Usage

import { Option, Schema } from 'effect'
import { defineMessageUnion } from 'foldkit/message'
import { Behavior, Capability, Event, Slot, Slots, Style } from 'foldkit-mixins'
import { SurfaceView } from 'foldkit-mixins-surface'
import { Surface } from 'foldkit-surface'

const Model = Schema.Struct({
  todos: Schema.Array(Schema.Struct({ id: Schema.String, title: Schema.String })),
  selectedId: Schema.Option(Schema.String),
  secret: Schema.String,
})

const Message = defineMessageUnion({
  SelectedTodo: { id: Schema.String },
  ArchivedTodo: { id: Schema.String },
  ClearedSecret: {},
})

const App = Surface.application({ Model, Message })

// Projects two of the three fields, exposes two of the three Messages.
const TodoList = App.surface('TodoList', {
  model: ({ model }) => ({ todos: model.todos, selectedId: model.selectedId }),
  messages: [Message.SelectedTodo, Message.ArchivedTodo],
})

const TodoSlots = Slots.define({
  root: Slot.make({ capability: Capability.Container }),
  archive: Slot.make({ capability: Capability.Interactive, events: [Event.Click] }),
})

const TodoStyle = Style.forSlots(TodoSlots)({ root: Style.class('todo-list') })

/** What the Surface projects — the input every attachment sees. */
type ProjectedTodos = {
  readonly todos: ReadonlyArray<{ readonly id: string; readonly title: string }>
  readonly selectedId: Option.Option<string>
}

// `input` is the projection, so `input.secret` does not type-check; `h` builds
// only `SelectedTodo` and `ArchivedTodo`.
const TodoBehavior = Behavior.forSlots(TodoSlots)<
  ProjectedTodos,
  typeof Message.SelectedTodo.Type | typeof Message.ArchivedTodo.Type
>({
  archive: Behavior.slot({
    requires: { events: [Event.Click] },
    attributes: ({ input, h }) =>
      Option.match(input.selectedId, {
        onNone: () => [],
        onSome: id => [h.OnClick(Message.ArchivedTodo({ id }))],
      }),
  }),
})

const TodoListView = SurfaceView.define(TodoList, TodoSlots, (model, slots, h) =>
  h.section(slots.root.attrs(), [
    h.ul([], model.todos.map(todo => h.li([], [todo.title]))),
    h.button(
      slots.archive.attrs([h.Disabled(Option.isNone(model.selectedId))]),
      ['Archive selected'],
    ),
  ]),
).pipe(Style.attach(TodoStyle), Behavior.attach(TodoBehavior))

// `toRenderer` adapts a SlotView to what `Surface.view`/`Surface.rootView` take.
const renderer = Surface.view(TodoList, SurfaceView.toRenderer(TodoListView))

slots.archive.attrs(...) is where the attached click behavior reaches a real element. Declaring a slot or attaching a Behavior alone renders nothing. With no selection the button is disabled; with a selection its click emits ArchivedTodo, and the application's reducer performs the transition.

The useful part is what the render callback does not repeat: no type annotation on model or h, no second Message union, and no root-Model input type. SurfaceView.define gets those from the Surface.

A Behavior's Input parameter still has to be exactly the projected type — a SlotView is invariant in it, so a narrower "just the fields I happen to read" type is rejected by Behavior.attach.

toRenderer is the single boundary adaptation. Surface.view hands a renderer a ViewBuilder with its MessageUniverse phantom removed; the SlotView is sound there because SurfaceView.define already restricted what it can construct to the Surface's Message subset.

What the bridge buys you

Without this package, Surface and SlotView are both type-safe independently, but you can accidentally describe the same feature boundary twice:

Surface projection / Message subset
             X
SlotView input / Message universe

With SurfaceView.define, there is one declaration:

Surface
  │
  ├─ projected value ──────────────┐
  └─ Message subset ────────────┐  │
                                ▼  ▼
                             SlotView
                                │
                         Style + Behavior

That is the whole package.

Rendering without a runtime

SurfaceView.render(view, surface, params, root) projects root through the Surface and renders the view with SlotView.inertBuilder. It returns the Html, but nothing mounts and handlers are built without ever being dispatched. It is for tests, demos, and static output, not a replacement for Surface.view in a running app.

const html = SurfaceView.render(TodoListView, TodoList, undefined, {
  todos: [{ id: 't1', title: 'Write docs' }],
  selectedId: Option.some('t1'),
  secret: 'not projected',
})

Introspection

SurfaceView.inspect(view) returns serializable { name, slots, mixins } with no functions. SurfaceView.describe(surface, params, view) merges that with Surface.inspect — what the Surface observes, its projection metadata, and what it may emit, with emitted Messages as tags — into one value. SurfaceView.toMarkdown renders it deterministically for docs or a CI drift check.

Each of define, toRenderer, render, inspect, describe, and toMarkdown is also a direct named export if you prefer that to the SurfaceView namespace.

Status

The API is still settling in the 0.x series. This is deliberately a small bridge rather than a second view system; changes should keep it that way. See the design note for the underlying Mixins decisions.

See also