Primitives / Media
Start with one: a media query
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.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(),
})dark starts at { matches: false }. When the runtime subscribes, the
stream emits the current answer at once and then every change; nothing else
performs I/O. A preset places with no args:
import { PrefersDark } from 'foldkit-primitives/media'
const Themed = Bundle.compose({ theme: Schema.String }).pipe(Bundle.withChild('dark', PrefersDark))Two placements of one query open two matchMedia listeners. Share the field
instead.
Breakpoints
Derives a named breakpoint from one resize listener. args.breakpoints maps
names to mobile-first minimum widths; a non-finite width is rejected at
placement. The Model is { width, breakpoint }, with breakpoint the largest
name at or below the width (ties break alphabetically) and null below the
smallest. The server starts at width 0.
const Layout = Bundle.compose({}).pipe(
Bundle.withChild('bp', Breakpoints, { args: { breakpoints: { sm: 640, md: 768, lg: 1024 } } }),
)breakpointFor(width, table) is the same rule as a function. Placing both
Breakpoints and WindowSize (in events) doubles the resize listeners; pick
the one the view reads.
Platform
platformFromUA(ua, hints?) reads mac | windows | linux | android | ios | unknown
from a user-agent string you pass. Mobile checks run first, since Android
contains "Linux" and an iPhone mentions "Mac"; Client Hints platform wins
when recognized, and a multi-touch Mac user agent reads as iOS. It never
throws: unknown input answers unknown.
isBrowser() and isServer() split a server render from the client, for an
init default that differs.
Failure
Without matchMedia (a server, an old browser) the stream is empty and the
slice keeps its initial false; without a window, Breakpoints keeps width 0.