Primitives / foldkit-primitives

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:

ts
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.