Repository navigation
v0.12.0
foldkit-agent 0.5.0; foldkit-bundle 0.4.0; foldkit-cms 0.3.0;
foldkit-cms-drizzle 0.3.0; foldkit-durable 0.5.0; foldkit-entity 0.5.0;
foldkit-form 0.3.0; foldkit-mirror 0.4.0; foldkit-mixins 0.5.0;
foldkit-mixins-crud 0.5.0; foldkit-mixins-form 0.3.0;
foldkit-mixins-ui 0.5.0; foldkit-primitives 0.4.0; foldkit-remote
0.9.0; foldkit-remote-server 0.9.0; foldkit-richtext 0.2.0;
foldkit-richtext-dom 0.2.0; foldkit-ssr 0.2.0; foldkit-surface 0.5.1;
foldkit-sync 0.7.0; foldkit-remote-drizzle 0.8.1. Republished to re-pin:
foldkit-bundle-surface 0.2.1, foldkit-crud 0.4.1,
foldkit-mixins-richtext 0.1.1, foldkit-mixins-surface 0.4.1, foldkit-react
0.2.1 and the four agent adapters 0.4.1. Three packages are published for the first time at
0.1.0: foldkit-richtext-code, foldkit-richtext-code-shiki and
foldkit-richtext-markdown. foldkit-metadata and foldkit-react-codegen
are unchanged.
A page is assembled, not spread. assembly.runtime builds the runtime
config in one call from an update that already routes every placement, and
derived args come from the parent seed — so a routed page takes its search
text without a second fetch, and assembly.config is gone.
Remote asks once and says when. A query carries its selection and comes
back with the fields (Data.meta says when it arrived and whether it is
stale), and Data.satisfy prepares a Model for a render, which is what the
static build runs per page.
Static sites end to end. The browser entry loads no server renderer, a
takeover is an explicit decision (when/otherwise/fresh), a
determinism check renders every page across zones and locales, and the
staticSite plugin turns vite build into the whole static build.
An editor that replicates. Replicated folds character-identified ops
in any order to the same state, patchTo redraws a remote keystroke without
remounting, and task lists, overlays and decorations round out the canvas.
Views with less machinery. Input.field draws a validated field in one
call, FormView.fields draws per key with overrides, Button.view is one
call, and every adapter has toView; forms validate all without
submitting, and a submit waits or disables explicitly.
Styling without stylesheets. Slots declared by their style, cascade
layers as a value, a theme from a few knobs, Style.install,
Style.usedIn for the first paint, and a foundations plugin that writes
the sheet into the head.
State that survives. The journal pages history, vacuums itself, and
binds replicas to actors across schema 6; Sync exchanges over a socket that
reconnects, persists its outbox in IndexedDB v2, and takes server resets.
New packages
foldkit-richtext-code0.1.0. Total grammars as tokenizers over the
sharedsyntax-*kinds: a JSON lexer that never throws, for
codeDecorations.foldkit-richtext-code-shiki0.1.0. Shiki grammars as tokenizers over
the same kinds (colours discarded, scopes mapped, one stylesheet); the
application owns the highlighter.foldkit-richtext-markdown0.1.0. Markdown round-trips for the editor
document: task items, tight lists, and the words around them.
Upgrading from 0.11
- Replace
assembly.configwithassembly.runtime(compose a narrow
assembly.update(own)first where the parent adds Messages); placement
factories now take the seed without the placement's own field. - Handle
LoadingandFailedfromCms.revisionsOfinstead of an empty
list; pass theRevisionHistory, not rows. - Send mutations with
now, read answers asMutationAnswer, and read
Data.moreas anOption(Data.next/previous/fetchare gone);
entry dependencies are typed, and an overlaid-missing field reads
Failed, notInitial. - Expect Timer/Interval's first tick one interval after
Started; match the
newTimedOut/GoTovariants and the Historygroupfield. - Serve the envelope on the stamped root (not a pre-
</body>script);
SSR.entrytakes notemplate/containerId/headand refusesmeta
plans at construction; thefoldkit-ssr/replayentry is gone. - Open IndexedDB v2 fresh (old code cannot open it) and migrate durable to
schema 6 (legacyReplicaIdrecovers actor-less replicas); the socket
reconnects for as long as its layer lives. - Rebuild hand-built
StyleValues through the brand; move the family
texttoken toon-fill/ink(text['on-accent']is gone). - Upgrade
foldkit-mixins-surfacewithfoldkit-mixins(its peer moved to
^0.5.0), and the agent adapters withfoldkit-agent(^0.5.0). - A hand-written form control now satisfies
Record<FormTag, …>; unknown
FormView.fieldskeys are compile errors. - Stay on Effect
4.0.0-rc.116(rc.117 is admitted untested); the peer range
stops below rc.118, whoseeffect/rpcmove breaks our
effect/unstable/rpcimport. The import moves with the workspace Effect
upgrade, which widens the range again.
Breaking
foldkit-bundle:assembly.configis removed; shared tags reach every
claimant and the parent (first-claimant-wins is gone).foldkit-cms:revisionsOfreturnsRevisionHistory;historyCard
takes it, not rows.foldkit-remote,foldkit-remote-server:MutationSucceededcarries
now;settleSuccess/reconcileMutationtakeMutationAnswer; query
windowing isData.more: OptionwithWindowGrown(next/previous/
fetchremoved); entry dependencies are typed; overlay-missing reads
Failed.foldkit-primitives: Timer/Interval tick late;TimedOut/GoTobreak
exhaustive matches;HistoryModelgainsgroup.foldkit-ssr: the envelope rides the stamped root;SSR.entryanswers
Renderedwith a new contract; thereplayentry is gone.foldkit-sync,foldkit-durable: IndexedDB v2 and durable schema 6 do
not open older stores; the socket reconnects while its layer lives.foldkit-mixins:StyleValueis branded; the familytexttoken is
on-fill/ink.
Added
-
foldkit-mixins-ui, Badge recipe: a status pill with a dot, toned by
an attribute's value —Recipes.Badge({ attribute: 'data-state', tones: { Published: 'success' } }).badge. Unlike the variant recipes every tone is
present at once (one style serves badges in every state), so it takes the
attribute and the value-to-tone map instead of a variant selection; values
the map leaves out keep the base. The CMS demo adopts it for both badges
and keeps only the state-to-tone map. -
foldkit-mixins-crud, Loading/Empty/Failure views: what a read says
before it has an answer, over any status slot —Loading.viewis
role="status"witharia-busy,Empty.viewisrole="status",
Failure.viewisrole="alert", andLoading.shownfades busy text in
late so a quick answer never flashes.ListViewandDetailViewdraw
these for their own states; the CMS demo adopts them for its lists, site
reads, and editor branches. -
foldkit-cms, entry views: the studio cards every CMS draws, on its
own slot builders —Cms.stateBadge,Cms.revisionsOf,Cms.historyCard,
Cms.moreCard, withHistoryCardSlots/MoreCardSlots/RevisionRow
types. Who published and the archive icon stay the application's; words
for states come fromCms.Display. The CMS demo adopts them and keeps
only its slots, styles, and chair names. -
foldkit-cms, entry views fixes:Cms.stateBadgewrites a custom
attribute for a new entry too, andCms.revisionsOf/Cms.historyCard
read aRevisionHistoryso a failed or still-loading history no longer
draws as nothing published. The CMS demo'sstateIscopy is deleted in
favor ofCms.stateIs. -
foldkit-mixins-ui, Touch/Icons mechanisms: no slots, no views —
Touch.target/Touch.targetsfloor controls at 44px where the pointer is
coarse,Icons.glyph(size)draws the icon in--icon, and
Icons.byAttribute(attribute, icons)sets it per value from resolved
url(…)strings. The CMS demo adopts them and keeps only its Lucide
paths. -
foldkit-mixins-ui, Buttonprimary/iconvariants: the main action
in ink (variant: 'primary', tone-independent) and the square icon-only
button (variant: 'icon'withsize: null, words as the accessible name).
The CMS demo adopts them withghostfor its buttons and deletes its
bespokeprimaryButton/iconButton. -
foldkit-mixins-ui, Segmented recipe: a tray of toggle buttons where
pressing selects (role="group"+aria-pressed, notTabs) —tray
picks a muted tray or a plain row,sizethe text density, and the pressed
option rises from the group so icon tiles share the rule. The CMS demo
adopts it for its worklist tabs, viewport switcher, panel tabs, and
inspector choices. -
foldkit-mixins-ui,Input.view/Textarea.view: one call drawing a
field — the value and Messages, a style,type/placeholder(rowsfor a
textarea), and adrawplacing the resolved bundles. The job application's
button fork is deleted in the same spirit: its views call the package
Button.viewinstead of their subset copy. -
foldkit-mixins-ui,Input.field/Textarea.field: a text field in
one call — a field's state drawn with its label, control, and description
placed (first error, or the check while it runs),type/placeholder
(rowsfor a textarea) riding through, and adrawplacing the parts
for layouts the default stack does not own. The waitlist example's
per-kind overrides and the job application's field view adopt them and
keep only their layouts and status marks; both now read the platform
Checking…spelling. -
foldkit-mixins-form, per-key element attrs:FormView.fieldstakes
attrsbesideoverridesandstyles— what one key's element takes
beyond the base field view (type,placeholder, textarearows). The
default renderers forward what fits their element and ignore the rest, and
overrides receive the same map. The waitlist example moves its email type
there and drops the thread-through. -
foldkit-mixins-form, submit gating:FormView.submodeltakes the
submit gate explicitly —canSubmit(a submit waits), a strict predicate
(disabled through checks), or() => true(never pre-disable). The lenient
default is unchanged. -
foldkit-primitives, SSE view parity:SseViewwithisLiveand
viewOfSse(model, wanted), the same shape as the socket'sSocketView—
a page derives what the reader sees instead of keeping its own connection
state. -
foldkit-bundle, derived args: a placement'sargsis a static value
or a factory from the parent seed,args: parent => ({ searchText: searchFromRoute(parent.route) }). The seed is what
assembly.initial(rest)was given, before any placement initialised; the
factory's parameter omits the placement's own field, and every factory sees
the same base seed, so placement order never matters. The result is checked
against the bundle's args Schema and retained forupdate, helpers,
Subscriptions, and resources, never re-run against live state — a factory
runs once per seed it has seen. On a Model
no initialization produced,updatederives per use without retaining. -
Routing example, derived placement args: the People page is a Bundle
placed once with its search text derived from the starting route, so
init(url)isassembly.initial({ route })with no second fetch and no
post-init Message. Route changes after startup still arrive as Messages,
folded through the same placement, and the view keeps itspeopleslot. -
foldkit-primitives, scroll keeping:keepScroll(options)in
foldkit-primitives/dom— the window's scroll across an application's own
navigations (the offset taken when the reader acts, a restore that holds
while the screen settles, entries keyed by the Navigation API with offsets
insessionStorage). A stream sending no Messages, lifted with
Subscription.persistent; the default storage key isfoldkit:scroll.
The CMS example adopts it in its studio and site subscriptions and deletes
its hand-rolledscroll.ts. -
SSR S1, browser entry without the server renderer: the CMS example's
browser entry takesSSRandFOLDKIT_APP_ATTRIBUTEfrom
foldkit-ssr/client— its hand-written root mark is gone, and the takeover
plan rides the bundle instead of a lazy chunk (cheap now that it draws
from the client side, with no server renderer). A static import walk from
the entry fails on reachingfoldkit/experimental/server. -
SSR S6, build determinism: the CMS example renders every page twice
under one time zone, once under zones at both extremes of the date line,
and under two default locales in child processes — any difference fails
the build. Building the check fixed two live leaks:generateSitetakes
the build's clock, threading it into the server and, via
Data.satisfy'snow, into the read stamps the envelope carries. -
SSR S7, static site as a build step:
foldkit-ssr/viterenders every
path after the client bundle is written —generateStaticSite(paths or
a thunk closing over what the site serves, per-path configs, template,
head,flat/directoryfile layout, sitemap, robots) behind thestaticSite
plugin, which evaluates the site module through a server so the config
names a file, never application code.foldkit-mixins/foundationswrites
the sheet module's stylesheet into the head the same way. The CMS example
builds through both and deletes its hand-rolled script and plugin. Empty
paths and two paths naming one file are refused before rendering. -
SSR S5, deciding whether to take a page over:
SSR.hydratetakes
when(asked before anything is adopted),otherwise: 'render'(a
declined page draws afresh where the served page is, replacing its markup
in place; anything else contains it as a resume failure does), andfresh
over the plan's newversion(asked once). The CMS example collapses its
takeover branch into one call: a page for another reader draws afresh
instead of taking stale facts over as live ones. -
foldkit-cms-drizzle,ImportItem<P>: the input ofcms.import,
named and exported — what a seed or a migration passes per entry (type,
values,as,at?,entry?). The package's own tests and the CMS
example'sseed.tsread it instead of re-declaring it, and the README
gains the seeding shape beside it: a fixed clock, named entries imported
dependencies-first,asfixed once. -
foldkit-bundle,assembly.runtime: the runtime config in one call for
an application whoseupdatealready routes every placement.initial
rest becomesinit, or an init function returning
assembly.initial(...)is used asinitwhen the seed needs runtime
input, like the URL. Theupdatepasses through checked (route a narrow
one withassembly.update(own)first, or omit it when the parent adds no
Messages); the ownsubscriptionsandmanagedResourcesmerge with the
items', defaulting to the items', so an application that adds none passes
neither.assembly.configis removed in its favour; the CMS
siteConfigand the entity client use it. Passing an already-wired
record (assembly.subscriptions()) back as own is a type error instead
of a later duplicate-key failure.
Fixed
-
foldkit-bundle, placement typing:PlacedResourceskeeps
onAcquired's parameters, soScene.ManagedResource.acquireon a placed
resource needs no stand-in value; a Link carries its top-level field, so
placements.initialkeeps its exact check through a custom Link, and an
Optionplacement field may be given to startNone. -
foldkit-mixins, a slot named__proto__:forSlotskept its piece
on a plain object, where the name set the prototype instead. -
foldkit-remote, two policies without losing data: two
Data.subscriptionscalls over one domain no longer collect each other's
data. Retention is the domain's: every call'sretainentry roots the
Surfaces andconnectionsof every call, so reads under different policies
can use a call each. -
foldkit-remote, the default clock: it readsDate.nowat each use
instead of capturing it when the domain is made, so fake timers move plans
and stamps. A read understaleWhileRevalidateplanned at the exact
millisecond a value expired no longer stalls. -
foldkit-form, optional andOptionkeys: a key made with
Schema.optionalKeyand left empty is valid and left out of the value, as
it already was forSchema.optional(an optional nested key too). A key
typedOption(Schema.OptionFromNullOr) is edited as the value it holds
and submitsOption.some/Option.none(), instead of throwing "no
control". A validation that changes nothing (a blur on an empty optional
key) returns the Model it was given, so nothing redraws. -
foldkit-mirror, a slow restore no longer loses the stored document: a
key-value or kernel mirror holds each write until the Model has taken in
whatrestoreanswered. Before, the write Subscription started from the
initial Model and, once the throttle passed, replaced or deleted the stored
slice while the restore was still reading it. The hold is per runtime and
applies again on every restore; a failed read counts as an empty store. -
foldkit-mixins,Inert.missingTokensreads inline styles: a
var(--fk-…)in an inline style value is reported when nothing defines it,
as a compiled rule's read already was; a token set inline still counts as
defined. -
foldkit-remote, query payloads in one response:Data.query(Query, input, { select })sends the selection with the query, and the server
returns the selected fields of the page's items with the edges. One
ConnectionMergedmerges the page and writes the fields, so a fresh list
landsReadywithout a second read. Edges-only servers still work: no
selection sent means edges only, and absent entities read as empty. The
shared requirement reader is exported asRemoteServer.readHelper, so a
custom handler answers the same settled-fields payload. -
foldkit-remote,Data.meta(model, projection): when what is shown
was last received (updatedAt, newest server write among it,undefined
when nothing shown was received), and whether any of it is stale or
loading. Pure, for "updated 5s ago" without I/O. -
CMS example, one studio application: posts and pages share a document,
a runtime and an address (examples/cms/src/apps/studioApp.ts); moving
between them swaps no application. Each section keeps its Model, update,
view and subscriptions (its Remote domain folds its own Messages); a
same-document navigation carries the address's query into its target. -
foldkit-mixins-form,FormView.fields(form, { overrides, styles }):
the whole form asdefinedraws it, with per-key overrides and styles, plus
one flat key's control for a layout the caller owns. Keys with no override
render by kind, so a new key of a known kind needs nothing new; unknown
keys are type errors. An override receives the control, field, id, validity,
errors and Messages, with anyh. -
foldkit-primitives/net, the socket reads itself: the WebSocket bundle
tracksopenedbesidestatusandlastError, words its own errors
(Failed to connect to WebSocketbefore it opened,Connection error
after,Connection timeout,Socket unavailable), and derivesSocketView
(Disconnected/Connecting/Connected/Error) from the socket and
whether the page still wants it, withisOpengating sends and frames. A
page keeps onewantConnectionboolean instead of a connection state
machine;foldkit-websocket-chatis the first caller. -
foldkit-mixins-ui,Button.view({ label, style, ... }): a button
drawn as a button in one call (label, style,type,disabled,onClick),
without theUiButton.view→Button.toView→h.buttonceremony.
toViewstays for buttons drawn as something else. -
foldkit-remote,Data.satisfy(model, actives, { passes? }): the Model
with everything the active Surfaces read, for a render that fetches nothing.
It prefetches each Surface that plans a read, cache-first, pass after pass
until a pass plans nothing, so a read that depends on another (a page's
Blocks on its document) is made too. Past the bound (8 passes) it fails with
RemoteUnsatisfied, naming the Surfaces still reading. -
foldkit-ssr,SSR.plan({ meta: model => Meta }): what a page says of
itself to a search engine and a link preview (description,og:*,
twitter:card,article:*,robots, alternates, JSON-LD), from the Model.
The render writes it before</head>, escaped, and checks it like the view;
SSR.hydratereplaces it as the Model changes, so a move to another post
updates its description.SSR.pageandSSR.entrynow refuse a render that
setscanonicalorogUrlinto a template without the tag Foldkit fills,
which Foldkit would otherwise leave out silently. The CMS example's
hand-built head is now its plan'smeta. -
foldkit-ssr,stylesfor the first paint of a served page:
SSR.render,SSR.entryandSSR.handletakestyles: rendered => string, carried as the rendered root's last child where no template head
can take it. Hydration adopts the nodes around it and drops it on its first
patch; styles for a void root are refused withVoidRootWithStyles. The
foldkit-ssrexample ships its stylesheet this way again, asSSR.generate
pages do in the head. -
foldkit-ssr,SSR.sitemap(pages, { origin })and
SSR.robots({ origin, sitemap?, disallow? }): a generated site's sitemap
(each page's full address, itsmodifieddate as the UTC day) and the
robots.txtnaming it. A duplicate path, a relative one, or a dateDate
cannot read is refused. The CMS example drops its ownsitemapOfand
robots. -
foldkit-bundle,onMessage: a placement'sonMessage(and
(message, key)on a collection) observes each child Message as a parent
Step after the child and itsonOut;placements.update(own)typesown's
Message without the placements' wrappers (Bundle.OwnMessage). -
foldkit-bundle, Links forUpdate.foldChild:
Link.wrapper(Message.GotXMessage)builds a wrapper from a variant the union
already declares, andLink.field/Link.optionalwrites return the parent
when the child is unchanged, soUpdate.foldChild({ ...link, update })no
longer redraws for a Message the child ignores.foldkit-ui-showcasefolds
its 38 components this way.Link.foldInit(link, rest)is what
Update.foldChildInittakes: the Link'stoParentMessagewith a
toParentModelthat writes the child into the rest of the parent. -
foldkit-primitives/time,ticks({ intervalMs, onTick }): a clock
entry whose running and interval are functions of the parent Model; a new
interval applies from the next tick without a restart. Timer and Interval
are built on it. -
foldkit-primitives,keyboardEvents({ preventDefault })cancels the
default of the presses a predicate overKeyPresspicks, decided in the
listener. -
foldkit-primitives, websocket:Sentcarries{ data }, and an
optionalconnectTimeoutMscloses a socket still connecting and reports the
newTimedOut. -
foldkit-primitives,History.goTo(model, step)and the bundle's
GoTo { step }jump to any kept step. -
foldkit-mixins, slots declared by their style:Style.slots(pieces)
declares an application's own Slots from their styles and returns
{ slots, style };Style.slot(options, piece)gives one a capability,
events or attributes.forSlotsstays for a contract someone else published. -
foldkit-mixins, short pieces: wherever a piece is taken, a
declarations object meansStyle.selfand a list meansStyle.compose. -
foldkit-mixins/app:AppStyle.make({ palette, colorScheme?, global? })
fixes the theme, theapplayer and the page stylesheet once, givingt,
L,slots,forSlotsandstylesheet. -
foldkit-mixins/utilities:Utilities(p,px,gap,text,
font,rounded,color,bg,flex,truncate, …) are pieces over the
Theme.tokensscales andTheme.oklchnames, so a step the scale lacks is a
type error. -
foldkit-mixins,Style.install(css)puts a stylesheet in the page's
head once and returns the element. -
foldkit-mixins/testing:Inert.drawdraws views that use
h.submodel;Inert.css(nodes)andInert.missingTokens(root, stylesheet)
replace the checks every view test wrote by hand. -
foldkit-mirror,Mirror.bootstrapandmirror.bootstrap(keys): a
first-class pre-init seam. When a store's keys are already in hand at boot
(Flags the server embedded, a synchronous read, a test fixture),
Prefs.bootstrap(keys)folds them into the initial Model before the first
render — the same conservative readreduceapplies to aMirrorRestored,
with no Command and no flash — andMirror.bootstrap(initial, ...steps)
composes store steps with the URL step (model => Filters.reduce(model, url)), keeping URL > store > initial. This is deliberately not part of
Wiring: a bootstrap runs insideinitbefore the Model exists, while
Wiring.initruns startup Commands (such asrestore) after it. -
foldkit-mirror,Mirror.routing:Mirror.routing({ mirrors, urlChanged, init, update, routing })returnsinit,updateandrouting
forRuntime.makeApplication. URL mirrors read the starting URL and each URL
Message before the application's ownupdateroutes it; itsonUrlRequest
andonUrlChangepass through unchanged.foldkit-query-syncuses it. -
foldkit-ssr/client, a browser entry:SSR.plan,resume,
hydrate,static,serving,Resumeand the attribute names (including
FOLDKIT_APP_ATTRIBUTE), importing nothing from
foldkit/experimental/server: about 200 kB minified (60 kB gzip) less on a
hydrating page.foldkit-ssrstill exports everything. -
foldkit-mixins-ui,toViewon every adapter:
X.toView(mixins, { h }, draw)fills a component'stoViewslot and hands
drawthe bundles with the Mixins applied, in a component'sviewor a
Submodel'sviewInputs.resolvestays for bundles already in hand, and its
inputis now optional. Every adapter exports its result type
(ResolvedButton,ResolvedInput,ResolvedRadioGroup, …), and
Textarea'stextareabundle is typed forh.textarea, so no cast. -
foldkit-form, validating without submitting:
Message.ValidatedAll()validates every key and row as a submit would and
submits nothing;form.isValid(model)says whether a submit now would hand
over the value at once (unlikecanSubmit, a running or unrun check is not
valid). A nested row's Message that changes nothing keeps the parent Model
by identity instead of rebuilding its rows, so a no-op in a row renders
nothing. -
foldkit-mixins-form, a check running and a submit in flight: a key
whose check runs shows aChecking…line in the newcheckingSlot
(role="status", named in the control'saria-describedby, words
words.checking), and each field'srootcarriesdata-validationwith
its state. While a submit waits for a check, or the new view input
submittingsays the application's request is in flight, theformis
aria-busywithdata-submittingand the submit button is disabled and
readswords.submitting(defaultSubmitting…). -
foldkit-bundle, a shared tag reaches everyone who shares it: a Message
tag wirings declaresharedis folded by each of them in list order, and
the parent's own update sees it after them. Before, the first claimant took
it: of two URL mirrors only the first read the URL, and an application with
a URL mirror never saw its ownUrlChanged, so it could not route on it. -
foldkit-mixins-crud, loading told from empty: a list's or a detail's
statusline isaria-busywhile the first answer is awaited, as the table
already was while refreshing, so a style can draw a quiet placeholder for
loading and keep its empty state for empty. -
foldkit-agent, correlating by the call: a completion'scorrelate
receives a third argument,{ invocation }(typeCorrelation), so a fact
that carries the invocation's id fromtoMessageis told apart where the
input is not. The todo app and the root README's example completedadd_todo
by matching the trimmed title, which mixed up two todos of one title; they now
passrequestId: invocation.idthroughRequestedTodotoSubmittedTodo. -
CMS example as a static site:
pnpm --filter foldkit-example-cms build:sandboxbuilds the studio and the public site with the same server
running in the page, on SQLite compiled to WebAssembly (sql.js), so it
deploys with no backend. Each visitor's sandbox is kept in the browser's
storage;?resetstarts afresh. The server takes its database
(openServer(clock, sqlite)), and one endpoint module answers requests for
both the HTTP server and the page.
On a phone the studio's sidebar is a top bar (the brand and who is looking,
then the sections in a row), the editor's bar wraps its actions under its
status, the builder's panels scroll within a short height so the page
shows, and a post list's cards fit a narrow canvas. The posts open with what
the demo is and what to try, and the page says it is starting while the
sandbox opens. The address keeps the open post, the search and the Archive
tab, as it kept the open page, so a reload lands where one was, and opening
or closing a post or a page is a step Back returns from. A page has its History
and More (discard, unpublish, archive) under a fold below the editor's bar,
as a post has them in its aside. The History is a timeline: each revision
with its date, time and publisher, the newest marked Live while it is on the
site, and Restore offered only where it would change something. Moving
between the studio's sections and the site no longer paints white: the
foundations' stylesheet is in the HTML. A wait says "Loading…" only once it
is noticeable, an entry being read shows no "New" badge, and the pages list
no longer says "Nothing yet." before it is read. A new screen starts at the
top and Back, Forward or a reload return where it was. A chair's avatar is
centred in its touch target on a phone. The page builder shows its panel
tabs at every width it stacks at: an editor between 52rem and 64rem wide
(a 1280px window's) stacked every panel above the page with no tabs, so a
new page seemed to open without one. The address keeps a post's preview and
the Builder's panel and preview width, so a reload comes back to them, and
the worklist's search and tab go through afoldkit-mirrorURL mirror
(?archive=truewhere it was?archive=1). The blog's eight posts explain
how the demo is made (the CMS, the page builder, Composition, Form, Crud,
Entity and Remote, and the styling), and a post's body can hold headings,
lists and code. The site links to the demo's code on GitHub, and a
published sandbox kept from the earlier seed is replaced by this one. A new
post or page is in the address (new=<id>) from the moment it is started,
so a reload comes back to it; its first save turns that into its own key.
The public site is rendered at build time withfoldkit-ssr: each
published page and post is HTML with its text, styles, description, link
preview, canonical address and article facts, beside a sitemap and
robots.txt. The browser takes a generated page over without drawing or
reading it again, while the visitor's sandbox still holds the seed. The
studio isnoindex. Moving between the studio's Posts and Pages no longer
reloads the page: the two applications share one document and swap in
place, keeping the loaded code and the in-page server, about 60 ms a move
where a load took 220 (and 0.8–1 s on a slowed CPU). -
Dropping a palette tile on the page's own space:
PointerDrag's facts
for a drag ontotargetscarryregion, whether the pointer is inside the
region, soover: nullwithregion: trueis its empty space.
foldkit-buildergainsDraggedOverPage(), which lands a tile where a
press with nothing selected would put it, andfoldkit-mixins-buildersends
it; a tile dropped on an empty page, or below the last node, added nothing. -
foldkit-primitives,FollowTabStop: a Mount keeping focus inside a
container of roving tab stops on its stop, when a transition it did not see
moves the stop or removes the focused descendant.TreeNavigation's
Behavior attaches it, so the Builder's layers keep focus through a
duplicate, a paste or a delete made by key, where focus stayed on the old
row or fell to<body>.foldkit-buildermoveslayers.currentoff a
removed node to the one after it, else before it, else its holder, rather
than leaving it for the first row. -
foldkit-cms:Cms.Display.State's words includewithSchedule
('{state}, {schedule}'), where a state and its schedule were joined by a
comma no application could word. A display renderer's context types its
words asDisplayWords, where it hadany. -
foldkit-builder: its words.Builder.make(name, { words })takes any of
EditWordsovereditWords: what it announces of each edit, its commands'
labels, and the refusals it makes itself, as text with blanks
('Moved {label}{at}');refusalwords thoseapplymakes ({code},
{message}). Announcements name a Block by its label, not its stored name. -
foldkit-mixins-builder: the editor's words. Every word the drawn
Builder shows of its own is one ofBuilderWords(English defaults in
builderWords), given as the view inputwords. Words are text, a value
a blank in them ('Add {label}'), since a view input may hold no nested
function.keysOftakes them, for the named keys (keyNames) and Ctrl,
Alt and Shift, and no longer readsObject's own names for a key called
constructor. A look's blank choice now reads "Default", as its buttons do. -
foldkit-mixins: parts, each drawn again only when what it reads
changed.SlotView.parts(Slots)<Input, Message>()makes parts that name
the input keys they read and are given only those;assembleplaces them.
slots.row.lazy(item, draw, args)draws one repeated item again only when
its arguments or what the Mixins gave it changed. Over 1,000 nodes, a
hover, a selection and a keystroke in the inspector each went from about
50 ms to the next frame. -
foldkit-mixins-builder: the editor as parts.BuilderView.parts(builder)
givesPanels,Palette,Layers,Inspector,Toolbar,Crumbs,
Viewports,Preview,Alert,CanvasandLive, each with its own
Behaviors;BuilderView.assemble(render)places them among an
application's elements, anddefineis the default layout. -
foldkit-builder: the inspector is a form. A node's props, and the
input of the action each event runs, are edited throughfoldkit-form
forms of the Block's settings (settingsOf,inputOf), drawn by
FormView, so a prop gets the control a form would give it, a value that
does not decode shows its error and edits nothing, and a control of the
application's own (Input.bundle, such as a color picker) works with no
Builder code.BuilderView.define(builder, { settings: { field, form, renderers } })styles the forms and draws the application's control kinds. -
foldkit-composition: a Block's words are its own.
Block.words({ label, description, group })names a Block and says what it
is for; the palette, the layers, the inspector,Catalog.describe,
Composition.describeand the agent'soperationSchemaall read it. -
foldkit-primitives/dom:Measure, a Mount that writes where marked
elements are (--fk-<name>-x/-y/-w/-h/-display) for an overlay drawn over
them. The drawn Builder's selection and hover are boxes placed this way. -
foldkit-builder: one table of commands.PageBuilder.commands({ id, label, keys, placement, run }) is every key, node action, toolbar button
and shortcut listed;keyCommandis derived from it andBuilder.make's
commandschanges it. Keys are written for the author's platform
(BuilderView.inputs({ platform: 'mac' })). -
foldkit-builder: a Block dragged from the palette is added where it
is dropped (DragSourceisExistingorNew).foldkit-primitives:
PointerDragtakestargets: { attribute, within }, so a drag may land on
another region's elements. -
foldkit-builder: copy, cut and paste. A node and all it holds is kept
in the Model'sclipboardand on the system clipboard as tagged JSON; a
paste reads it back (readText, new infoldkit-primitives/dom), decodes
it strictly, gives every node a new id and inserts it whole or refuses it. -
foldkit-composition: Patterns.Catalog.make({ patterns })holds
arrangements of Blocks, checked where the Catalog is made;
Op.usePattern({ pattern, ids, at })inserts one, andoperationSchema
offers each with exactly its ids. The drawn palette offers them in a group
of their own. -
Text edited in place. A Block's view draws a text prop with
field(key); the editor edits it on the canvas, frozen at the text it had
when editing began so no redraw moves the caret, one undo step per session
(History.closeandHistory.revert, new infoldkit-primitives/state).
EditableText, new infoldkit-primitives, reads what is typed as text,
once per composition, andRenderer.fieldssays which props a node draws
this way. -
foldkit-mixins: container breakpoints.Style.responsivetakes an
at-rule as well as a media query,Theme.inContainer(name, breakpoints)
writes a theme's breakpoints on a named container, andStyle.at(prelude, piece)puts a selector under an at-rule. The Builder's frame is the page's
container (PAGE_CONTAINER), so a look written this way follows a narrow
preview in a wide window. -
foldkit-mixins-builder: a narrow editor.BuilderView.narrow(width)
shows one panel at a time, chosen by a group of pressed buttons, below that
width of the editor itself. -
foldkit-mixins: a Style's rules arrive with the Slot that draws them.
Compiling a Style records each class with its CSS; in a browser, a class a
Slot draws is appended once to one<style data-foldkit-styles>element
(made only when there is something to add, declaring the standard layer
order unless the page declares its own), unless a stylesheet on the page
already carries it. A Style left out of an application'sStyle.stylesheetnow
draws instead of silently drawing nothing.Style.stylesheetis unchanged. -
foldkit-mixins:Style.usedIn(html), the CSS of every compiled class a
page's markup uses, after the layer order: what a server puts in the head.
foldkit-ssr:SSR.page,SSR.generateandSSR.entrytake
head: rendered => string, put before the template's</head>(a template
without one is refused when there is something to add;SSR.entryrefuses it
when it is made, and answers aheadthat throws500). -
foldkit-composition/foldkit:Renderer.renderdraws a node again only
when what it reads changed. Inside a runtime render, each node's drawing is
memoized on its node object, drawn children, read, visibility and marks, so
an edit redraws the node it touched and its ancestors, and an unrelated Model
change redraws no node. Each node's element is keyed by its id (the edit
wrapper, or the Block's root in view mode unless the Block set a key). -
foldkit-mixins/testing:Inert.draw,unslotted,fixedInlineand
bySlot, which check a package view's customization contract: every
element it draws comes from a Slot, however deeply its views nest, and no
fixed declaration is inline. They found markup outside any Slot in three
packages, now Slots of their own:foldkit-mixins-builder'sselectOption,
foldkit-mixins-form'soptionandchoiceLabel, and
foldkit-mixins-crud'shead,headRowandbody. -
foldkit-mixins/testing:Inert, queries over a view drawn with the
inert builder (all,children,byTag,byRole,byLabel,text,
value,classes,style,pressed), replacing a tree walker seven test
files each defined, and the casts to hand-written node types that came with
them. -
Fixed declarations are rules, not inline styles, so a later layer can
override them:Prose.style's measure and line height (with a newleading
option),Style.grid's template and areas, andStyle.stagger's delay (its
--fk-indexstays inline). The drawn Builder's frame writes
--fk-frame-width, read by a default rule incomponents. -
foldkit-mixins-builder: what the editor calls a Block.
Block.words({ label, description, group })(now infoldkit-composition) names a
Block, says what it is for, and files it in a palette group. The palette is
grouped; each button is named "Add ", shows the description, and is
titled with where it would go ("Adds it inside the Section") or why it
cannot. A layer row shows the label and the node's first text in brief, and
a row that holds others has a toggle. New Slots:paletteGroup,
paletteHeading,paletteLabel,paletteHint,rowToggle,rowLabel,
rowSummary; palette buttons and rows carrydata-block. A palette button's
text is now the label, notAdd <Block>: find it by its accessible name. -
foldkit-mixins-builder: the inspector in parts. It opens with the
selected Block's label, description and actions (now drawn there, each
titled with its shortcut and carryingdata-action), then its settings under
Content, Style, Visibility and Interactions. A look of up to four values is a
row of buttons, one pressed, instead of aselect. Labels are the key spaced
when there is notitle("Text", "Shown when audience is", "On press"), and
viewport buttons read "Wide", "Medium", "Narrow". With nothing selected it
says how to begin and lists the shortcuts. New Slots:inspectorHead,
inspectorTitle,inspectorHint,inspectorSection,
inspectorSectionTitle,choices,choice,shortcuts,shortcutKeys,
shortcutWhat, andlabelandoptionfor a field's name and a
many-choice picker's choice, which were drawn outside any Slot. -
foldkit-builder: Escape deselects inkeyCommand.
foldkit-mixins-buildertakes the shortcuts on the canvas as well as the
layers (the canvas is now focusable), and draws a breadcrumb of where the
selection is (crumbs, onecrumbbutton per node holding it, the page
first, the current onearia-current="location"). -
foldkit-mixins-builder: pointing at a layer row marks its node on the
page (the Builder'sHoveredandUnhovered), and an empty page says how to
begin, in the newemptySlot. -
foldkit-surface:Action, a named capability that ends in a Message.
Action.define({ name, description, input, toMessage })declares one, and
Action.run(action, data)decodes the data as its input before making the
Message, so stored or untrusted data never executes. -
foldkit-agent:Agent.action(action, { available?, authorize? })exposes
an Action as a variant, under the tag of the Message it makes; another tag is
a type error, and a dispatch through one is refused.Agent.variantis
unchanged. -
foldkit-form:Input.bundle, a control with a Model of its own. A key's
draft can now be a Bundle's Model, such as a color picker with a popover, a
page builder, or a rich-text editor.Input.bundle(kind, { bundle, value, fill, settled? })
says how the key's value is read from the Model and written into it. The
Bundle's Messages travel as the form's newControlMessage
(form.control(key).send), its Commands answer asControlMessages, and
its Subscriptions and Resources become the form's, keyed under the key. A
Message is an edit, validated and counted byauthoredChanged, only when it
changes the key's value.model.fields[key].valueis typed as the Bundle's
Model. The Bundle may have no OutMessage and need no services, the key takes
nocheck, and a nested form whose control has Subscriptions or Resources is
refused. -
foldkit-mixins-form: draws a control backed by a Bundle with the
Bundle's own view inside a newcontrolfield slot, unless a renderer names
its kind. A renderer receivesbundle: { model, send }, wrapped for its row
inside a nested form. -
foldkit-cms: the editor runs a form control's Subscriptions and
Resources while an entry is open, and stops them when it closes. -
foldkit-composition(in development, not published): what a page is,
as data.Block.definedeclares what may exist (a props Schema, Regions,
the Content it provides),Catalog.makegathers a context's Blocks and
roots, andComposition.Documentis the tolerant codec of a stored page: any
Block name, props as JSON.Composition.validatereports ten kinds of
finding, from a missing node to a Region that rejects a child, and
Composition.valid(catalog)asks the same as a Schema check for an operation
that publishes.index,describeandCatalog.describeread a Document
and a Catalog.Composition.applyedits a Document by Operations (insert,
insert a tree, remove, move, duplicate, set and unset a prop, the reserved
fields, a batch), refusing what an Operation would cause with no partial
result, and never minting an id itself.Historykeeps undo snapshots,
grouped without a clock.Composition.migratemoves stored pages forward
through named migrations (renameBlock,renameProp,promoteUnknown, or
one of your own), each rewriting a node or declining, and throws when one
breaks the Document's structure.foldkit-composition/foldkitdraws a
Document with one ordinary Foldkit view per Block (a Block without one is a
type error), placeholders for what cannot be drawn, and an edit mode that
marks each node; the same Renderer draws inside afoldkit-ssrstatic
region, so a served page sends none of the Document.Urlrefuses
javascript:and every scheme but http, https, mailto and tel.
foldkit-composition/richtextholds a rich-text body checked against its
Kit. Phases 1 to 4 of the page builder design. -
foldkit-builder(in development, not published): the page builder's
state.Builder.make(name, { catalog, renderer, starters })gives a Bundle
that edits a composition Document by Operations, mints new ids in a Command,
keeps a selection and undo History beside the page, and records why an edit
was refused.builder.inputplaces it as a form key's control, so the page is
the key's value, a selection is not an edit, and a fill starts undo over. Its
plain view (palette, layers, text props, undo, the page in edit mode) is drawn
byfoldkit-mixins-formwith the rest of the form. Phase 5 of the page
builder design. -
The CMS example builds a page too. A Page content type whose
document
key is the page Builder goes through the post's whole story: autosave, a
resumed draft, a preview drawn by the site's own views, publish, revisions,
restore, a schedule and a conflict, with no CMS state added. Phase 6 of the
page builder design. -
foldkit-builder: the keyboard, the layers and announcements. The
Builder placesTreeNavigationandLiveAnnouncein its Model; moving focus
in the layers selects the node.keyCommand(model, key, modifiers)gives the
shortcuts: Alt with the arrows moves the selected node among its siblings,
out of its parent or into the node above; Mod+D duplicates; Delete removes;
Mod+Z, Mod+Shift+Z and Mod+Y undo and redo. Structural edits, undo, redo and
refusals are announced to assistive technology. A selection made anywhere
else, such as an insert or a click on the page, is where the layers' keys
start from.offeredlists the Blocks the palette offers, and
inputWith(view)is the form control drawn by another view. -
foldkit-mixins-builder(in development, not published): the page
Builder, drawn.BuilderView.define(builder)draws a palette, the layers
as an ARIA tree, the selected node's actions and props, undo and redo, a
viewport picker, a refusal as an alert, a live region, and the page in edit
mode through the site's own Renderer. Every element is aBuilderSlots
Slot. The layers takeTreeNavigationand the Builder's shortcuts, and the
canvas takesTargets, so hovering and clicking the page mark and select
its nodes. It adds no state and no Messages. The CMS example's page form
draws its Builder this way. Phase 7 of the page builder design.
The inspector labels a prop with its Schema'stitle, and a Block asks for a
prop's control withBlock.annotate(Builder.controls({ ... })), such as
Input.multiline(), orInput.hidden()to leave it out. A row in the
layers or a node on the page can be dragged onto another with the pointer:
the Builder'sdragsays where a drop would land (dropAt), the target is
marked only where the page allows it, and a drop is one undoable, announced
move. -
foldkit-composition: appearance. A Block offers appearance axes, each a
list of variant values or token names, andvalidateand
Op.setAppearancecheck a node's stored names against them
(composition:invalid-appearance,composition:unknown-token). The new
foldkit-composition/appearancesubpath makes a look from a
foldkit-mixinsslot recipe and theme tokens:Appearance.attach(look)
gives a Block its axes,look.draw({ appearance, h })gives its view the
Slots with the chosen Style attached, every piece compiled once, and
look.stylesis the stylesheet. A Renderer's views receiveappearance,
the choices the Block offers. The drawn Builder's inspector chooses them.
A token axis givenbreakpointsis responsive: a node stores
{ base: 'sm', md: 'lg' }, drawn as rules the widest matching breakpoint
wins.look.draw({ appearance, h, with })attaches a Block's own Behaviors
after the look, and a style property a Behavior owns that a choice also sets
ismixins:style-property-conflict. A layout Block is a Mixins layout this way: the test site's Columns is
Layout.switcher, with its ratio, when it stacks, and its gap as choices.
Phase 8 of the page builder design. -
foldkit-composition:operationSchema(catalog), the Operations a
Catalog's pages take as a Schema, inserts naming only its Blocks with their
stored props, for an agent tool's input. The CMS example's page agent edits
a page through the Builder's ownApplied, undoable like any edit, and a
Block outside the Catalog is refused by the tool's schema. -
foldkit-composition: actions. A Block names its events, a Catalog lists
the actions its pages may reference (foldkit-surfaceActions), and a node
stores which action an event runs with literal input.validateand
Op.setActioncheck them (composition:invalid-action,
composition:unknown-action), and a Renderer's view getson(event), the
Message the action makes from input its Schema decoded first;
Renderer.forMessageschecks it routes every such Message, and a
Renderer.makeview, such as an editor's canvas, gets none. The drawn
Builder's inspector picks each event's action and edits its input. -
foldkit-composition/surface: Surface Blocks.SurfaceBlock.define
places afoldkit-surfaceSurface where an author puts it, with params from
its props;SurfaceBlock.readsandSurfaceBlock.activeread the page's
Surface Blocks as a Query Block's reads are, andvalue(data)reads a node's
typed. -
foldkit-composition: stateful Blocks. A Block markedstatefulis
backed by a Bundle the page's parent places once per node, keyed by its id.
Composition.statefulNodeslists them;Stateful.sync(in/foldkit) keeps
a placed collection in step with the page shown, adding, removing, and
starting again a node whose props changed;Stateful.viewsand
Stateful.htmldraw each node with its own item. -
foldkit-form: a title given before a check names the key. Effect
resolves a checked schema to its last check's annotations, so
Schema.String.annotate({ title }).check(...)lost its label; the form now
falls back to the schema's own annotations. -
foldkit-composition: a Query Block gets its own window, cut to its
firsteven when another read of the same query loaded more rows.
Composition.validdescribes itself withexpected, not atitlea form
would take for the field's label. -
foldkit-cms-drizzle: a publish patches the whole row back, as the
handler left it, so the author's own screen shows what was published without
the handler returning a patch. -
foldkit-mixins-builder: an optional relation prop stored asnulland
decoded to anOption(Schema.OptionFromNullOr) offers its blank: the
inspector asks the stored side. -
foldkit-mixins-ui:Recipes.Buttonon a link is not underlined. -
foldkit-cms:placed.storedEntry(model), the entry the server knows, as
anOption: none while something new is not saved yet, so a link names only
what exists. -
foldkit-mixins-builder: relation pickers for Block props. A prop asks
forInput.relationOne(Entity)orInput.relationMany(Entity)through
Builder.controls, and its choices come inBuilderView.inputs({ options }), keyed'Block.prop'. -
foldkit-form:Input.relationOneandInput.relationMany, a picker for
a key that holds ids without being a relation. -
foldkit-mixins-form: a Bundle control's view takes inputs, from the
form view'scontrols, by key. A key given none draws its view as before. -
foldkit-mixins-builder: the canvas draws the page's data.
BuilderView.submodeltakesBuilderView.inputs({ data }), each node's read,
which the page's parent gives through the form'scontrols, so a Query or
Surface Block on the canvas shows its rows.builder.inputWithaccepts a
view with inputs;QueryBlock.activeandSurfaceBlock.activetype what they
read. -
foldkit-composition/remote: Query Blocks.QueryBlock.definemakes a
Block that names afoldkit-remotequery and derives its input from props;
QueryBlock.reads(Data, catalog, document)is the page's reads as one
Projection keyed by node, fetched and resumed by Remote like any read; the
Renderer hands each node itsdata, and the Block'srows(data)reads it
typed.QueryBlock.active(name, App.owner, Data, …)is the read as an active Surface, so a
server-rendered page resumes it withRemote.resume. The CMS example's pages can list the site's pages. Phase 9 of the
page builder design. -
foldkit-composition: conditions. A Catalog may declare acontext
Schema (an audience, a locale, a flag), and a node'swhenis a list of
conditions over it,eq,isNull,isNotNullandcontains, meant as
foldkit-entity'sExprmeans them.validateandOp.setWhencheck them
(composition:invalid-condition,composition:unknown-context), and
Renderer.rendertakes thecontext: a node whosewhenfails is left out,
or markeddata-composition-hiddenin edit mode, and a page drawn without
its context fails closed.Composition.holds(when, context)is the test.
The Builder keeps what the author previews the page as (preview,
PreviewChosen), and the drawn Builder offers a "Preview as" picker, draws
the canvas for it, and edits a node's conditions in the inspector. -
foldkit-composition/foldkit: the edit wrappers carry the marks.
renderin edit mode takesselected,hoveredanddrop, and puts
data-composition-selected,data-composition-hoveredand
data-composition-dropon those nodes' wrappers, so an editor's CSS draws
the selection and where a drop lands. A node whose view throws is a
placeholder, as a node that cannot be drawn is. -
foldkit-primitives:PointerDrag, dragging one marked element onto
another. A Mount and a Behavior on a container: a press that moves past
4px starts a drag, the element under the pointer is reported with the third
of it the pointer is in (before,inside,after), and a release drops
or Escape cancels. It writes no roles or keys, so it works on a tree's rows
as on a canvas, and it swallows the click a drop ends with. -
foldkit-primitives:TreeNavigation, keyboard navigation of a tree.
After the WAI-ARIA tree pattern: Up and Down through the rows showing, Right
opens or steps in, Left closes or steps out, Home and End. Rows are given in
tree order with their parent; openness is stored as what was toggled away from
openByDefault. Its Behavior writes the tree's ARIA attributes and a roving
tab stop. For a file explorer, a page's layers or a nested menu. -
foldkit-primitives:Targets, which marked descendant is under the
pointer or was pressed. One Mount on a container reportsTargetHovered
once per change andTargetPressedwith its modifiers, for the nearest
descendant carrying a marking attribute, and can stop a press's default. -
foldkit-primitives: an undohistorygroups steps. APushmay name
agroup, and consecutive pushes of the same group are one step, so typing a
word undoes as a whole without a clock. The steps are exported as pure
functions (History.start,push,undo,redo,clear) for a parent
that records an edit in the same transition that makes it. Breaking for
code that builds aHistoryModelby hand: it gainsgroup,nullfor none. -
foldkit-builderkeeps its page in that history: its Model'spageis a
foldkit-primitives/statehistory whose present is the Document
(builder.document(model)reads it), andfoldkit-compositionno longer has
a History of its own. Undo is one mechanism in the repository, not two. -
foldkit-ssr: its build configuration references the packages it builds
from, so another project can reference it. -
foldkit-ssr:SSR.render's result listsunnamed, each element and
event whose handler is a function the page cannot name
({ element: 'button#point', event: 'pointerdown' }). On a page that waits
to boot,SSR.entryandSSR.generatewarn about each once per process. -
foldkit-ssr:pnpm bench:manifestmeasures the resume manifest's size
and decode time at 10, 100 and 1,000 rows; the SSR plan records the table.
pnpm bench:manifest:browsertimes the same page in Chromium. -
foldkit-ssr: the envelope lists the events its markers name, and
Resume.listentakes them asevents, so a deferred page no longer reads
every element's attributes to find them. The page's list of bindings is
checked by hand rather than through a Schema. At a thousand rows, decoding
and listening take 12 ms in Chromium, down from 22. -
foldkit-richtext:Replicated, collaborative rich text without a CRDT. A
document state where every character has an identity:translaterestates an
edit as ops that name characters and blocks,applyOpsfolds them in any
server order to the same state,projectgives the editor aDocument, and
anchor/resolvekeep a selection by its characters.invertgives the ops
that undo an edit and nothing else.translate'scontinuesoption and
coalescemake a burst of typing one op, and aCollectop removes deleted
text, in two phases, through the log.examples/pageswires it to Sync and
Durable. Since:RetypeContainerretypes the caret's block in place,
unionChangeSetunions held IME patches,SetPropsis refused only when
it would make valid props invalid, transactions carry their normalized
transactions, and a cachednodeIndexanswers block lookups. -
foldkit-richtext-dom:patchTo, and anoverlayCommand for
decorations from application state, such as other people's carets.
patchTopatches an externally replaced document without remounting (a
remote keystroke keeps the caret), judging moved runs by both blocks;
empty runs draw addressable spans, string data draws as
data-decoration-*, input rules match on surrounding node kinds, and a
non-moving transition keeps the Model by identity instead of clearing it. -
foldkit-richtext-markdown, task items and tight lists:- [ ]at a
list item's start makes it a task in place, and the printer records
CommonMark spread, so tight items keep nested lists under their text. -
foldkit-richtext-codeandfoldkit-richtext-code-shiki, first
releases: total JSON and Shiki grammars as tokenizers over the shared
syntax-*kinds, forcodeDecorations(the application owns the
highlighter). -
foldkit-sync:Sync.fact, which applies a durable fact in the intent's own transition.onReinstallonSync.mount.Sync.durable(message).- A contract's
coalesce, which merges a burst into one unsent operation. Storage.append, a one-row write per submit, with an IndexedDB outbox
store.- Server push through
{ notify: true }frames andTransport.changes. - Paged exchanges (
more). transport.socket, so presence shares the sync connection.- A presence
throttle. - Server-reset recovery: a server's
epochgoes back asexchange's
optional third argument, and a new one makes the replica rebuild from the
server's answer and resend its outbox.
-
foldkit-durable:Journal.cursor,read(key, after, { limit })andvacuum().- An in-memory snapshot with
snapshotEvery. Journal.epoch(key).- A
replicaIdoption that binds each replica to the actor of its first
commit. Sync'sjournalContract()supplies it.
Changed
-
foldkit-primitives, Timer and Interval's first tick now comes one
interval afterStarted, not at once.History.pushkeeps an already-empty
future, so a view reading it is not redrawn per push. The newTimedOut
andGoTovariants break a match over every variant. -
foldkit-mixins, a brandedStyleValue(breaking for hand-built
values): aStyleValuecarries a brand, so a declarations object is never
mistaken for one;NamedStyle.piecesand arecipeForselection are
StyleValues. -
foldkit-ssr, Foldkit's config types:SSR.hydrate,render,
generate,entryandhandleaccept Foldkit'smakeApplicationconfig
types;routing's callbacks are checked againstupdate's Message, and
resourcesisLayer<never, never, never>(the old type rejected
Foldkit's own Layer). -
foldkit-ssr,SSR.entryanswers OPTIONS (breaking):204with
allow: GET, HEAD[, POST], OPTIONSinstead of405, and a new
headers: request => HeadersInitsets headers on every response it answers. -
examples/foldkit-ssrserves as upstream does: Effect's HTTP server on
@effect/platform-nodeinstead of a hand-writtennode:httphost. -
foldkit-ssr, the envelope rides the stamped root (breaking):
SSR.rendercarries the envelope asdata-foldkit-plus-resumeon the
rendered root, beside Foldkit's own stamps, instead of a script before
</body>; hydration adopts the nodes and drops the attribute on its first
patch.SSR.resumereads it there, refusing a page with no stamped root or
more than one.SSR.entryanswersRenderedand takes notemplate,
containerIdorhead: the host owns the template, throughhandleRequest
or thefoldkitVite plugin's dev server, and a plan withmetais refused
at construction (serve it withSSR.generate, which keeps the template,
headandmeta). The build id is theFOLDKIT_BUILD_IDdeployment both
bundles are compiled with. Thefoldkit-ssgandfoldkit-ssrexamples serve
throughfoldkit({ ssr }), and the CMS example stamps pages with the same
deployment id instead of the entry script's address. -
foldkit-remote, typed entry dependencies:Data.subscriptionsand
Remote.fold(...).subscriptionstype each entry's dependencies
(ReadDependencies, the newLiveDependencies,RetentionRoots) instead of
any; theretainentry requires no services. -
foldkit-cms, drafts: a draft stores the form's Model settled, as it is
shown again, so nothing in flight is saved; a Builder's undo history was,
up to 200 copies of the page, and a page of about 150 blocks could no
longer be saved ("This draft is too large to save"). -
foldkit-mixins-builder, words:selectAHolderisnothingHoldsIt, and
says what it now means: with nothing selected a tile goes anywhere that can
hold it, so a disabled one has nowhere yet. The empty page says to begin
with a block the palette offers. -
foldkit-cms, editor status: a publish or a schedule the form's own
checks stop isIncomplete(a newEditorStatus) until the next edit, and
one waiting for a check readsPublishing; before, the status stayed what
the last save said, "Saved", so a refused publish read as a success.
EditorFormgainsengine.isValidating, whichForm.makeprovides. -
foldkit-builder, dropping: where the node dragged over has no place
for what is dragged, it lands by the nearest node holding it that does,
before or after it, anddrag.overnames that holder; a Section dragged
onto a Section's last Heading landed only on the Section's thin bottom edge.
dropAtagrees. -
foldkit-mixins-builder: a click on a layers row makes it current, as
the keys do (TreeNavigation'sFocused), rather than sendingSelected,
so a narrow editor keeps the layers showing instead of switching to
Settings and hiding the row that had just taken focus. -
CMS example: a Quote's text and a Callout's body keep the lines broken
with Shift+Enter once editing ends, in the editor and on the public page
(white-space: pre-lineon their looks). -
foldkit-primitives,PointerDrag: an Escape that cancels a drag is
caught on the way down and stopped there, so the focused element does not
also act on it; in the Builder, Escape during a canvas drag cancelled the
drag and deselected the node. -
foldkit-mixins-builder: the palette's tiles and the command buttons
are disabled byaria-disabledrather thandisabled, so Undo pressed on
the last step, or a tile whose place fills, keeps focus instead of dropping
it to<body>. Style[aria-disabled="true"]; the CMS example does. -
foldkit-primitives,EditableText: when Enter or Escape ends an edit
and the view then removes the field or makes it no longer editable, focus
comes back to the container instead of falling to<body>, where the next
key (an undo, a Delete) did nothing. The Builder's canvas is that container. -
foldkit-primitives,PointerDrag:targets.withinis looked for
nearest first, under the container's closest ancestor holding a match,
not the first on the page, so the second of two editors dropped its tiles
onto the first's page.foldkit-mixins-builderfinds its canvas by a
data-builder-canvasattribute rather than an id built from the Builder's
name, which a name with a space or a colon broke (no drop landed, or every
move threw). -
foldkit-primitives,Measure: a change in the subtree is measured as
its records arrive, so an editor's selection box moves in the frame the
selection is drawn in rather than one after; and it measures again when an
image or a font inside loads and on each frame of a transition or an
animation inside, which moved a target without resizing it and left the box
where it was. -
foldkit-mixins, per-item memo (slots.x.lazy): a static Style's
contribution is compared too, so two views apart only by a Style no longer
reuse each other's rows (a theme switch kept the old theme's rows); and an
item drawn with no runtime frame (a test, a server) keeps no bookkeeping. -
foldkit-builder, text edited in place: Escape puts the text back even
where another edit came between, a commit writes nothing the page already
holds, a second ask for the field being edited begins nothing, and an undo
or a redo ends editing. A session in which nothing was typed writes nothing
when it ends, so leaving a field untouched keeps an agent's edit made
meanwhile (the Model'seditinggainstyped). A Block named as one ofObject's own names is not
taken for one with starting props, and a pasted tree is checked by its own
ids, so a refusal names what was copied rather than freshly minted ids. -
foldkit-builder, the inspector: an optional prop or input emptied is
taken away (unsetProp, or the input written without it) instead of left
as it was; a value the Block refuses leaves its field showing the node's
value, not one that looks accepted; an action's input is written with only
the keys its Schema names; and a held form Message with a key its variant
lacks is not decoded.Settingsgainskeys. -
foldkit-builder, placing and commands:placeForputs a Block that
is no root, with nothing near the selection to take it, last in the last
Region on the page with room that accepts it, so a node cut and pasted
back comes back rather than being refused with a minted id; a paste with
no place at all is refused before ids are minted, asbuilder:no-place
(a new word,noPlace). Duplicate is not offered where its Region is
full.keyCommandtries each command a key names in turn, so a command an
application adds on a built one's key runs where that one has nothing to
do. -
foldkit-composition:field(key)takes only a prop that is exactly
string; a list of names ('plain' | 'accent') is no longer a field. An
id that is one ofObject's own names (toString,constructor,
__proto__) is an id as any other: a tree naming one it lacks was a
TypeErrorfromapply, and one it held was refused as taken; a stored
Document naming one it lacks was aTypeErrorfromvalidate,indexand
describe, and is nowcomposition:missing-node.
Renderer.fieldsno longer shares the page's memo, which made a view that
called it redraw every node on every change.Composition.treeRefusalis
exported, to check a tree by its own ids before it is rekeyed. -
foldkit-mixins-builder: the default layout is four regions.define
drawsregionsholdingstart(palette, layers),bar(toolbar, crumbs,
viewports, preview),stage(alert, canvas) andend(inspector), where it
drew ten sibling panels;rootis always the containerbuilder, so a
Style may follow the editor's own width. A Style that placed each panel on
the root's grid places the regions instead. -
foldkit-composition/foldkit: absence is anOption.RenderContext.on(event)
returns anOptionof the Message, andRenderer.render's edit options
selected,hovered,dropandeditingtakeOptions, as an editor's
Model holds them, rather thanundefined. -
foldkit-mixins-builder:BuilderView.describeandBuilderView.controls
are gone, forBlock.words(infoldkit-composition) and
Builder.controls(infoldkit-builder). The toolbar'shistory,undo
andredoSlots aretoolbarandtoolbarAction, andACTIONSand
SHORTCUTSare the command table.foldkit-builder:DragStarted
takes asource, anddropAtaDragSource, not an id. -
foldkit-primitives:Measureattaches nothing when it cannot start.
Where there is noResizeObserver(jsdom), it had already attached a
MutationObserver, which then threw on every change to the page. -
A value that may be absent is an
Option, notnullorundefined,
across the read and builder APIs. Breaking:foldkit-surface:Surface.at's params function returns
Option<Params>(none: inactive), andprojectionOfreturns
Option<Projection>.foldkit-remote:Data.active(name, projectionOf)makes an application's
read of the domain an active Surface; it replaces spelling one out from
contract, and throws for a domain on a raw optic, which names no
application.foldkit-crud:detail.at'sidandlist.at'sinputreturn an
Option;more(model)is anOption<Command>.Crud.optionsthrows
without lists.foldkit-cms: the editor'sentry,pageId,resumed,stateand
errorreads returnOptions; an editor domain givesactiveinstead of
contract.foldkit-builder: the Model'sselected,hovered,refusedanddrag
areOptions, stored asnull.Selectedtakes an id andDeselected
clears it; likewiseHovered/Unhovered,DraggedOver({ id, zone })/
DraggedOff, andPreviewChosen/PreviewCleared.placeFor,moveBy,
dropAtandkeyCommandreturnOptions.foldkit-remote: a preview (Data.overlay) of an entity the server has
not seen, missing a field its Selection reads, readsFailedwith an
Overlaiderror naming the fields, notInitialfor good.foldkit-composition:SurfaceBlock.reads,QueryBlock.reads,value
and thedocumentOfof theiractiveareOptions; the reads gain
data(model), each node's value as a Renderer takes it.Block.stored(block, key)is how a prop is stored, andStoredPropsOf<B>its type.
-
foldkit-mixins: a family'stexttoken ison-fill, and each family
gainsink.accent.textread as "accent-colored text" and was the text
on the accent's fill. It is nowaccent['on-fill'](--fk-accent-on-fill),
andaccent.inkis the accent as text on the base surface, for every family.
text['on-accent'], the same value asaccent['on-fill'], is removed.
Prose'smarkdrew its text in the on-fill color over the accent's tint; it
usesink. -
foldkit-remote: a query read shows at most its window, and "load more"
grows the window. Remote keeps one connection per query and input, so a
read offirst: 3now shows three rows even when afirst: 50read of the
same query loaded more, withhasNextset when it cut any. Two windows of one
connection are one query, for the wider; a window wider than what the
connection holds fetches only the rows it lacks (before, it was never
fetched).Data.more(model, projection)returns the Model with the read's
window one page larger, anOption;Data.next,Data.previous,
Data.fetchand the fold'sfetchare removed.Remote.queryand
Remote.queryMessagestill run one page by hand. The store gainsgrown
and theWindowGrownMessage. -
foldkit-bundle:complete'supdateerror names a common cause: an
unannotated parameter on a callback written inline in the config, which keeps
TypeScript from inferring the config at all. -
foldkit-composition:Renderer.renderof a Renderer that sends nothing
takes any application's builder, so a view passes its ownhinstead of
inertHtml. -
foldkit-remote-drizzle:queryis written without type arguments; the
README says how the principal is inferred fromwhere's parameter.
foldkit-cms-drizzledropped its own. -
foldkit-mixins-crud:ListSlotsandDetailSlotsgainbadge, and a
Display renderer's context carries it, so a value drawn as a label of its own
is styled as a Slot.foldkit-cms's state renderer draws in it. -
foldkit-cms-drizzle:cms.import({ type, values, as, at?, entry? })
publishes content that exists already (a seed, or another CMS's) by the
publish path, in one transaction, instead of an application writing the CMS's
tables by hand. -
foldkit-cms: the Entry has a server-derivedmay, the transitions the
reader may ask by the server'sallow; the placed editor gives
may(model, transition), andCms.transitionslists them all.
foldkit-cms-drizzlesupplies it; itsAskedtype is removed for
foldkit-cms'sTransition, which it duplicated. -
foldkit-mixins-form:FormView'ssubmits: falsedraws a form without
its submit button. -
foldkit-primitives:KeepInView({ selector }), a Mount that scrolls
whatever newly matches in its subtree into view.foldkit-mixins-builder
mounts it on the layers panel and the canvas, so a new selection (an inserted
Block, say) is in view.foldkit-compositionexportsMARK_ATTRIBUTE. -
foldkit-form: a number-literal schema resolves to aSelect, whose
optionsmay be text or numbers and whose value is the chosen option itself.
Control.parsenow receives the control'sdata.foldkit-mixins-builder's
inspector draws the select and stores a number as a number. -
foldkit-entity:Selected<typeof selection>, the value a Selection
reads. -
foldkit-cms: the editor's Commands carry the form's requirements, not
any.EditorForm,EditorContentandEditor.makegain aServices
parameter (defaultnever), so an application'supdatetypes its Commands
asRemoteClientand its runner needs no cast. -
foldkit-entity:Words.of(schema), a schema'stitleand
descriptionasOptions, including a title given before a check (which
Effect 4 resolves past).foldkit-form,foldkit-crud's column labels and
foldkit-mixins-builder's inspector read words through it; the inspector and
Crud no longer lose such a title. -
foldkit-remote-drizzle:drizzleWritesandreturning.row.
drizzleWritesis the provided database'sinsert,updateanddelete,
typed by each table's columns, so a handler writes without casting
DrizzleDatabase;returning.row(binding, id)reads back the row a handler
wrote as patches, every column and eachonerelation as its ref.
foldkit-cms-drizzleuses both instead of its own casts. -
foldkit-crud:list.more(model)is the Model showing one page more, an
Option, instead of a Command. -
foldkit-composition:QueryBlock.readsno longer cuts rows itself;
Remote does. -
foldkit-mixins-ui:outlineandghostbuttons read--fk-inkfirst,
so on a band that sets--fk-ink: currentColortheir text is the band's
color instead of the tone's ink, which does not read there. -
foldkit-mixins:Defaults.headingsreads--fk-headingfirst, so a
container drawn in its own color sets--fk-heading: currentColorand its
headings take its color; elsewhere they staytext-overt. -
foldkit-mixins-form:FieldSlotsgainsgroupandaffix, for a
control drawn with text beside it;foldkit-cms's slug renderer draws its
prefix and input in them instead of in bare spans with a
data-cms-slug-prefixattribute. -
foldkit-primitives:TreeNavigationwrites each row's level as
--fk-tree-levelbesidearia-level, so onecalcindents any depth. -
foldkit-composition: the edit marks are one attribute.
data-composition-markisselectedorhoveredand replaces
data-composition-selectedanddata-composition-hovered; a node both
hovered and selected isselected. Restyle[data-composition-selected]as
[data-composition-mark='selected']. -
foldkit-mixins: a recipe'svariantsis optional, so a recipe that is
only its base (Style.recipeFor(Slots)({ base })) no longer writes
variants: {}. -
foldkit-ssr: Messages answered before boot reachupdatebefore any
other Message, instead of being replayed through a Subscription after the
first render. An event dispatched in the task that boots the page, such as
the rest of a burst of typing, reached the runtime before the replay, so
the text ended on its first character. The runtime still starts from the
resumed Model, so its first render is the served markup; itsupdateis
wrapped to run the answered Messages ahead of the first one it processes.
Thefoldkit-ssr.replayentry is gone. -
foldkit-ssr: deferred boot no longer depends onhydraterendering its
first frame before it returns. Until that frame commits the page keeps
answering from its markers, and it then sends the live page the events only
it can answer. -
foldkit-mirror:reducereturns the Model it was given when the store
holds what the Model already does, so the URL change a mirror's own write
causes, a navigation that leaves its keys alone, and a restore that finds
nothing new no longer render the page again. A structured field's text is
remembered by the value's identity, so the write entry no longer serializes
it on every Model change. -
foldkit-bundle: a placement or collection item returns the parent Model
itself when the child'supdatereturns its own Model, so a Message that
changes nothing renders nothing. -
foldkit-primitives: a Message that changes nothing returns the Model it
was given, across state, time, motion, media, interaction, device, network
and event primitives, and the navigation and selection Behaviors describe
their items once per render instead of once per item.Virtualcaches its
row offsets per keys array and heights record.MediaStream'sStarted
while live no longer stays atrequesting, andPresence'sHidewhile
hidden no longer shows the content for the hide duration. -
foldkit-remote: the common Messages that change nothing return the
Model they were given, soData.reduceand the wiring keep the application's root on a
duplicate live event, a repeatedReadStarted, an emptyHydrated, or a
retention pass that collects nothing. An entity's or list row's decoded
value is the same object after a refetch or live patch that brought equal
data to every field of its entity, so a keyed row's lazy view does not
re-run. The live entry no longer closes and reopens its stream on every
event, and a pending request no longer rebuilds the visible store on each
overlay change. A list read now shows a live insert
or an optimistic connection change that wrote no entity; it used to keep
the list from before it. The unusedshouldWakeexport is removed. -
foldkit-remote:MutationSucceededcarriesnow, the clock reading of
the answer, and the entities it writes are dated by it. They were dated 0, so
under a freshness policy every value a mutation wrote was already expired and
read again at once, and one a read had just dated was set back.Data.mutate
andRemote.mutateIntotake anowoption (defaultDate.now), as
ObserveOptionsandLiveOptionsdo.settleSuccessand
reconcileMutationtake the answer as oneMutationAnswerobject
({ entities, deleted?, now }, plusconnections?forsettleSuccess)
instead of positional arguments, so the clock cannot be left out. A
hand-builtMutationSucceededadds the field, and a stored one without it
no longer decodes. -
foldkit-remote: the read entry plans once per Remote state and query. A
Model change the Remote model is not part of, such as typing in a field,
reuses the plan until the next freshness deadline instead of walking every
row again: about 1 ms to 0.02 ms per change for a list of 400 rows. -
foldkit-remote: freshness follows what the server said and when. A
pending optimistic patch keeps the date of the value under it, so under a
freshness policy a request in flight no longer makes a fresh entity read as
expired and refetch it; an entity only a request holds (a temporary id) is
never aged, planned or timed. A field the server settled without a value
now ages out with its entity on the read entry's timer, instead of waiting
for an unrelated Model change.EntityWritetakes an optionalupdatedAt. -
Tag branching the type checker can check. Every package branches on a
_tagit can type: Schema ASTs areSchemaAST.AST, tested with Effect's
guards, instead of a hand-written{ _tag: string }; switches list every
variant or end inabsurd; and casts before a_tagread are gone.
foldkit-entityexportsSchemaShape(isNullish,present,isText),
the one reading of an optional Schema thatcms,crud,form,remote
andremote-drizzleeach had a copy of.foldkit-cms'sEditorForm.field
returns aFieldValidation.Field<unknown>.pnpm tags:checkfinds branching
the checker cannot vouch for. -
foldkit-form:DraftKindgains'model', andNestedFormgains
controlandMessage.Control. A switch overDraftKindhandles the new
case, and a hand-writtenNestedFormadds the two members; forms made by
Form.makehave them. -
foldkit-durableschema 6: schema 5 adds epoch and replica-binding
tables; schema 6 records the replica on each operation. Files from schema 4
or 5 upgrade in place;
retained operations restore their actor bindings. A custom journal with
compacted older operations needslegacyReplicaIdto recover the replica
from an operation id; without it opening is refused.foldkit-syncsupplies
this callback. The snapshot cache also follows the document epoch after a
reset through another handle. Older builds refuse a schema 6 file. -
foldkit-sync: the socket transport reconnects for as long as its layer
lives.maxRetriesnow counts consecutive failures after which queued work
fails fast, instead of ending the transport.Replica.startretries a failed
exchange on a backoff, and every failed exchange setsstatus.lastError. -
foldkit-sync: the IndexedDB database moves to version 2 (the outbox
store); code from before it cannot open a database this version wrote.