Repository navigation
v0.15.0
foldkit-sync 0.9.0; foldkit-durable 0.7.0; foldkit-remote 0.12.0;
foldkit-remote-server 0.12.0; foldkit-remote-drizzle 0.10.0;
foldkit-cms-drizzle 0.5.0; foldkit-crud 0.6.0; foldkit-mixins 0.7.0;
foldkit-primitives 0.6.0; foldkit-surface 0.7.0; foldkit-mixins-ui
0.6.1. New: foldkit-data-grid 0.1.0 and foldkit-mixins-data-grid 0.1.0,
experimental. Republished to re-pin: foldkit-agent 0.6.1,
foldkit-agent-a2a, foldkit-agent-mcp, foldkit-agent-native and
foldkit-agent-webmcp 0.5.1, foldkit-bundle-surface 0.3.1, foldkit-cms
0.4.2, foldkit-mirror 0.5.1, foldkit-mixins-crud 0.6.2,
foldkit-mixins-form 0.4.2, foldkit-mixins-richtext 0.2.1,
foldkit-mixins-surface 0.5.1 and foldkit-ssr 0.3.1, each of which pins a
bumped package exactly. Every other package is unchanged.
Editing server data, and a data grid to do it in. A table a server owns
can now be edited offline as durable operations: foldkit-sync/entity keeps
the edits per cell and lays them over Remote's rows, and
foldkit-sync/journal's editsJournal applies each committed edit to the
table as the journal's read model, recovering from crashes and refusing a
table ahead of its journal. The data grid that edits it is new: an
accessible, virtualized grid over 100,000 rows with focus, selection, column
state, typed editors, the clipboard, fill, and undo as the application's.
Servers also run in the browser now: SharedHost in foldkit-primitives/net
hosts one for every tab, reached over MessagePorts by Remote and Sync, which
is how the demos at foldkit-plus.pages.dev
run.
Upgrading from 0.14
ReplicaStatus.rejectedis a list of rejections (foldkit-sync):
ReadonlyArray<Rejection>, each{ opId, reason: Option<string> }, where it
was a list of operation ids.Sync.transport.sockettakes aurlor amakeSocket, not both
(foldkit-sync), andmakeSockettakes no argument.editsJournalrequirestableRevision(foldkit-sync/journal), and
fails withTableAheadOfJournalErrorwhile the table holds a revision past
the journal's.- Install
drizzle-ormyourself besidefoldkit-remote-drizzleand
foldkit-cms-drizzle: it is a peer dependency now, so the application's
copy is the one they use. containson Postgres folds ASCII only (foldkit-remote-drizzle), as it
already did in SQLite and in memory:éno longer findsÉthere.
Added
-
foldkit-mixins-data-grid: Delete and Backspace clear the range's
editable cells, or the focused cell, as a cut does without copying, so they
reportOut.Pastedwith empty text and a column that refuses it says so
(#144). -
examples/registrysearches on the server. A search box sets the
Products query's input; the query is now aQuery.definewhose body,
Expr.containson the description, the Drizzle binding compiles (#144). -
examples/data-gridkeeps its column layout in the browser: saved with
a Command after any transition that changes the column state, and read
back withcolumnState.restoreas the page opens (#144). -
foldkit-data-gridandfoldkit-mixins-data-gridare published, at
an experimental0.1.0: the grid's state, geometry and keyboard, and the
grid drawn as an accessible, virtualized WAI-ARIA grid through slots. Their
entries below are the first release's (#158). -
Fill in the data grid.
foldkit-data-grid'sFillis a spreadsheet's
fill as pure functions:plan(the source's box and the box beyond it
toward a cell, along one axis),cells(what each lane writes there) and
series(numbers a constant step apart continue it; anything else
repeats). In the grid, Ctrl or Meta with D or R, or a drag of the range's
fill handle (thefillHandleSlot, its target drawndata-fill; held at
the grid's edge, the drag scrolls it), reports
Out.Filled({ source, to }), soGrid.Out.matchneeds that arm; the
application writesGrid.fill(rows, model.grid, request), judged as a
paste is. The registry undoes a fill as one step.Columns.textOfis a
value as a cell shows it (#144). -
foldkit-data-grid:Grid.window(input), the rows and columns a
placed grid's view draws, worked out as the view does (the view now calls
it), so an application can count what is drawn without estimating. The
registry shows it in a strip under its title, beside a How this page
works panel that lists the ownersModule.manifestreads from its
contracts and what each field holds now (#163). -
Undo and redo in the data grid, and in the registry as new edits. Ctrl
or Meta with Z on afoldkit-data-gridgrid sendsUndoRequested, with
Shift (or Ctrl+Y)RedoRequested, each reported as the OutMessage of the
same name. They carry nothing, since the history is the application's; an
open editor keeps the keys.Grid.Out.matchnow needs the two arms. The
registry keeps a per-tab stack: an undo is anEditedProductsof what each
cell held, so it goes through the outbox like any edit, and a cell changed
since is left alone and said.foldkit-sync/entity'sEditableEntity
gainschangeAt(row, member), a cell's value as aChange(#164). -
foldkit-remote-server:RemoteServer.memoryauthorizes per entity.
It takesauthorize, per entity name, the functionRemoteServer.entity
takes, and theprincipalitslayeranswers as;serveranswers as any
principal throughhandlers. The principal's type is inferred from them, and
principalis required unless that type admitsundefined. A name the
domain does not declare is refused when the backend is made. A memory-backed
test or demo no longer copies the read path to try authorization (#147). -
foldkit-sync/journal:TableAheadOfJournalError.editsJournal's
settlerefuses to start while the table holds a revision past the
journal's cursor, as a table that outlived a reset of its journal does:
every new edit would otherwise be skipped byapplyand counted absorbed by
the replicas, and lost with nothing said. It refuses on every exchange until
the table is rebuilt with the journal (#176). -
foldkit-primitives/net:SharedHost, one in-browser server for every
tab and frame.SharedHost.define({ name, opening })givesserve, a
SharedWorker's entry, andconnect({ worker, inPage }), whoseopen(opening)
returns aMessagePortfor one conversation. The host decodes each opening
first and ignores one that does not decode or has no port, and its
signalaborts when the page's Web Lock is released. Without SharedWorker
the top document hosts, and same-origin frames send their openings there
(#167). -
foldkit-sync:Sync.transport.portSocket(port, { signal? }), a socket
over aMessagePort. Its frames are the port's strings, anything else is
ignored, andonClosefires once, onclose()or whensignalaborts, so
a host that serves a pane stops serving it when the pane goes. The registry
sandbox's own copy, whoseonClosenever fired, is gone (#167). -
foldkit-remote/portandfoldkit-remote-server/port: Remote over a
MessagePort.port(open)is Remote's client andservePort(handlers, port)its server, over Effect's own RPC worker protocol, driven through
effect/workerswith no browser package: reads, queries, mutations and
live streams cross it with their typed errors, an interrupted call
interrupts its handler, and the serving ends when the client closes its
end.portProtocol(open)is the protocol alone, for a hand-made RPC
client (#167). -
Docs: Editing server data through a
journal. When rows are too many to
replicate but edits to them must survive offline: who owns what, the one
rule (a cell shows its edit while pending or committed after the row's
revision), a one-column path throughfoldkit-sync/entityand
editsJournal, conflicts, refusals, absorbing, and each failure case,
with the registry as the worked example. Its snippets compile in
examples/registry/test/guide.test-d.ts; the registry README keeps what is
its own (#171). -
foldkit-sync/journal:editsJournal, the table as the journal's read
model. Itssettleapplies each committed change through the
application'sapplyas a recovery intent and itsabsorbrecords what
the table holds and compacts behind it. Recovery starts at the journal's
floor, so a server restarted over a compacted journal no longer fails
every exchange asking for history that is gone; intents are keyed by the
journal's epoch, so an operation sent again after a reset is applied
again. The registry's journal is built on it (#166). -
foldkit-sync/entity: edits to rows a server owns.EditableEntity.make( entity, { members })keeps edits one per cell, each value typed by its
member, committedata sequencebyan author ({ actor, replica }),
and lays them over Remote's rows by each row'srevision. Pure functions
merge and absorb them, hold an absorbed edit while its cached row is
behind it and settle it when a read reaches it, report a cell another
tab's later commit replaced, and name a refusal's cells.examples/registry
is built on it: a change is one cell and the server writes the one column
it names. Another editable column is an entry inmembers, its grid
column, its arm inchangeOfand its table column; none of the edit rules
is written per field any more (#166). -
foldkit-sync: a reinstall says when the server was reset.
ReplicaStatus.epochis the server history the cursor points into, and
Sync.mount'sonReinstall(next, previous, { reset })is told when it
changed.examples/registryreads its rows again on a reset: their
revisions counted the old history, so an edit committed since at a low
sequence looked as if the rows already held it (#166). -
foldkit-mixins-data-grid: a choice is a list the grid draws. A
column of literals edits as arole="combobox"beside itsrole="listbox"
(newchoiceListSlot), styled with the page, opening below the cell or
above it near the bottom of the view; arrows, Home, End and the page keys
walk it (EditSteppedinfoldkit-data-grid), a letter finds an option
(EditTypedon a choice), and a press chooses one (EditChosen) without
the blur committing the old draft first.choiceEditor: 'native'keeps
the platform'sselect(#168). -
foldkit-mixins-data-grid: shared cell marks and their legend.
GridMarksnames five marks (pending,saved,refused,replaced,
peer);GridMarkStyle, attached afterGridStyle, draws each on the
theme's tokens with a shape of its own and an outline under forced
colours;GridLegenddraws what they mean beside swatches
GridLegendStylepaints with the cells' rules.examples/registryuses
them in place of its hex colours and hand-drawn legend (#168). -
foldkit-mixins/theme: shadow tokens.Theme.tokens.shadowis a
scale (xs,sm,md,lg,xl,inset) drawn in one color,
--fk-shadow-color, falling back to a faint black;Theme.oklchsets it,
a near-black of the neutral hue that is denser in a dark scheme, where a
shadow mixed from the text glowed.GridStyle's editing lift and error
bubble and the dialog and segmented recipes use the scale (#168). -
foldkit-data-grid: an unchanged edit reports nothing. The edit keeps
the text it began from (editing.from); a commit of that text, or of text
that decodes to the same value (Equal.equals), closes the edit and moves
focus with noOut.Edited. Leaving an editor as it opened, after another
device changed the cell beneath it, no longer sends the old value over the
new one (#174).EditTypedcarriesfrom, the text the cell showed.
A paste does the same per cell:Clipboard.pasteAttakes{ editable, from }(was a positionaleditable), eachPastedcell carriesfrom,
unchanged cells are dropped, and a paste or cut that changes nothing
reports nothing.examples/registrydrops its ownholdscheck. -
foldkit-mixins-data-grid: clicking away saves, and a refused draft
says why. Focus leaving the editor commits it, as Enter does without
moving; a draft the column refuses stays open, its field in the error
colour, with the column's message shown below it in a neweditorError
Slot (role="alert", named by the field'saria-describedby, which
replaces itsaria-description).GridStyledraws the sort direction as
a small chevron whose place is kept while unsorted, so sorting no longer
wraps a narrow header or moves its label, keeps header labels on one
line, and parts columns with a faint line that is, on a header, the
resize handle. The registry's page is in neutral greys. -
A mark says what it means, and an unchanged edit is none. A cell's
mark is also itstitle, shown on hover (the description a screen
reader hears was there already). The registry draws a legend of its marks
above the grid, presence in violet so it is not taken for a replacement,
and a cell committed with the value it already shows (the same text, or
01.73for 1.73) makes no edit: nothing is sent and nothing marked. -
foldkit-mixins-data-grid: a column that does not edit says so. In a
grid where some columns edit, the others' headers and cells carry
aria-readonly, andGridStyledraws a padlock before the header's
label (with a Read-only title) and the cells' text muted, with the
ordinary pointer. A grid where nothing edits marks nothing. -
foldkit-mixins-data-grid: a choice opens its list. Theselecta
column of literals edits in opens its list as it opens (showPicker,
where the browser allows), so a double-click shows the choices without
another click. The registry's status column is wide enough for its
longest. -
foldkit-mixins-data-grid: an edit looks like one, and a pin shows.
A double-click opens an editable cell's editor, as Enter does. The cell
being edited carriesdata-editing, andGridStylelifts it with a
shadow and gives the field the whole cell, outlined and with a caret in
the focus colour, where before it was the browser's bare input inside the
cell's padding; an editable cell carriesdata-editableand shows a text
cursor. The pinned column next to the scrolling ones carries
data-pinned-edge, drawn as a divider, so a pinned column is seen to be.
The registry sorts every column the server can order, UPC, line and
status too, and edits the line and the status as well (a choice of the
Product's statuses), so a double-click on any column but the UPC edits. -
foldkit-durable/core: the journal without Node.Journal.layer(options)
andJournal.define(key).layer(options)give the journal as a layer that
needs aSqlClient, anyeffect/sqlSQLite client, such as
@effect/sql-sqlite-wasmin a browser, where a browser test runs append,
retry, refusal, recovery, compaction and reset over it. Provide the driver
to the layer, and the database opens and closes with the journal; a
journal handed out as a value could keep a connection already closed
(#169). Payloads are hashed with@noble/hashesinstead of
node:crypto, to the same digest, and epochs come from
globalThis.crypto.randomUUID. The main entry'sJournal.makeover a
node:sqlitefile is unchanged (#162). -
examples/registrysays where each edit is. A cell's mark shows an
edit not yet sent, one the journal has and the table does not yet, and one
the server refused, edged until its line (naming the cells and the
server's reason) is dismissed. A Work offline switch pauses the
transport, and switching back exchanges at once (#161). -
examples/registrysays when another device's edit won. The journal
stamps who committed each edit (by); a field this device last wrote that
another device's later commit replaced is edged amber, with a line naming
the device and the value it had, until dismissed. A connection names its
device (?device=on the socket). The registry's server and journal no
longer import Node (an SQLite seam, andfoldkit-durable/core), so a
sandbox can run them in the page (#162). -
examples/registryruns in the browser: two devices, one page.
build:sandboxbuilds Device A and Device B side by side, each a page in a
frame with its own replica, against one server in a SharedWorker: the
products table on sql.js and the journal on SQLite compiled to WebAssembly,
overMessagePorts. An e2e test builds it, serves the files, and drives a
shared edit, a conflict the losing device is told of, and an offline edit
that survives a reload (#162). -
examples/registry: presence, and each device's status on its card.
A device announces the cell it has focused over Sync's presence, on the
same socket the journal is served on, and the other draws it outlined in
violet; a device working offline leaves. In the sandbox, each card's dot
and badge say where that device's edits stand, from a status its frame
posts to the page. -
foldkit-sync: why an edit was refused. An exchange may carry
reasons: [{ opId, reason }]besiderejected(each at most 500
characters; a reason for an operation not rejected fails the exchange),
andjournalExchangegives each rejection one: anauthorizerule's own,
now that a rule may return{ allowed: false, reason }as Durable's does,
or a fixed sentence ("Not a valid operation", "This connection may not
make changes"), never an error's message. Breaking:
ReplicaStatus.rejectedisReadonlyArray<Rejection>,{ opId, reason: Option<string>, operation }, in place of the ids: the refused operation
comes with its refusal, so what it changed is read from it rather than
looked up while it was pending, which a page that heard late could miss. The todo-app's rules give their
reasons (#161). -
foldkit-mixins-data-grid: cell marks.marks: address => Option.some({ name, description })gives a cell a state of the
application's:data-markon its Slot, which the default style shows as a
dot in the corner and an application styles by name, and the description
as itsaria-description. Asked only for the cells drawn (#161). -
foldkit-durable: a commit stamp.JournalOptions.stamp(operation, { sequence, actorId })writes what the commit decided into the operation,
aftervalidateandauthorizeand beforereduce. The stamped operation
is what is stored, read, loaded and recovered; a retry is still recognized
by what was sent, since a retransmission is now compared by the hash of
the operation as sent rather than by the stored text. A stamp that changes
theopIdis refused. For a read model that must know which commits it
holds (#159). -
foldkit-sync: commit stamps on the contract.make({ stamp: { Variant: (message, { sequence, actorId, replicaId }) => … } })writes what the
server's commit decided into a durable Message, per variant, and returns
that variant (another is rejected by id; a non-durable key is a type
error). It rides injournalContract()as Durable'sstamp, so replicas
replay the stamped Message in place of what they sent.defineSynctakes
the same asstamp(message, commit). New typesCommitStampand
StampPolicy. A replica exposes itsreplicaId, the one its commits
are stamped with. -
foldkit-data-grid, the grid's pure model. Phase 0
of the DataGrid design: typedColumns
by stable id, aRowModelwith aKnownorUnknownRowCount, a
saveableColumnLayoutof start, center and end regions with hidden
columns, and aGridProjectionthat answers moves, edges and rectangular
ranges in display order, each as anOption. A cell is named by row key
and column id, so it survives a re-sort or a reorder. -
foldkit-data-grid, focus (Phase 1).GridFocus.make(columns)is a
Bundle holding the focused cell as anOption, stored asnull, and
decoded only against the grid's own column ids.GridFocus.targetis the
cell a key moves to (arrows, Home and End, Ctrl for the corners, PageUp and
PageDown, RTL),tabStopthe cell holding the one tab stop, andcellIda
DOM id foraria-activedescendantthat no two cells share. -
foldkit-data-grid, virtualization (Phase 2).VirtualGrid.windowis
the rows and center columns a viewport shows, with overscan and spacers;
rows are one height, so it never walks them, and pinned columns are always
drawn.VirtualGrid.revealis the least scroll that shows a cell.
GridViewportkeeps the container's offsets and size in the Model, its
MeasureMount reports them on mount, scroll and resize, andscrollTois
the Command that applies a reveal. A benchmark at 100,000 rows is in
docs/benchmarks.md. -
foldkit-data-grid,DataGrid.make({ id, columns }). Focus and the
viewport as the one Bundle a grid places. A key'sMovedcarries the
reveal the view worked out, and the update issues the scroll for the
container namedid. -
foldkit-mixins-data-grid, the grid drawn (Phase 3).
DataGridView<Message>().define(Grid)draws aDataGridas a WAI-ARIA
grid of the viewport's window of cells:aria-rowcount(or-1when the
count is unknown), each cell's logicalaria-rowindexandaria-colindex,
one tab stop witharia-activedescendantwhile the focused cell is drawn,
sticky pinned columns, a placeholder for rows not loaded, and the keyboard
throughGridFocus.target. Every element is a Slot; a Style that sets the
geometry the window depends on is refused.GridStyleis a default look in
thecomponentslayer. It renders on a server as markup a parser reads
back the same. -
foldkit-data-grid, column state (Phase 4). A column spec takes
width,minWidth,maxWidth,resizableandhideable. The
DataGridModel holds the column state, aColumnLayoutplus the widths
resized, withColumnResized,ColumnHidden,ColumnShownand
ColumnMoved(a reorder, a pin or an unpin) to change it; each returns the
Model it was given when it changes nothing, and the last column shown
stays.columnState.restorereads a saved state back leniently and lists
the ids it dropped.Grid.project(rows, state)is the projection for a
column state, built once per rows model and state. The view takesrows
in place ofprojectionandwidth, and draws the column state. -
foldkit-mixins-data-grid, resize handles. Each resizable column has a
role="separator"handle with its width inaria-valuenow: a pointer drag
resizes from where it began (ResizeStarted,ResizeMoved,
ResizeEnded; a cancelled drag puts the width back), and a focused handle
steps with the arrow keys, mirrored in right-to-left text. The grid now
takes only keys aimed at itself, so a handle's keys do not move focus. -
foldkit-data-grid, selection (Phase 5, the state). Opt-in with
rowSelection: 'single' | 'multiple'andcellSelection: true. Rows are a
RowSelectionofKeysorAllExcept, so select-all holds rows not
loaded;RowSelected,RowsExtended,AllRowsSelectedand
RowsClearedchange it, with an anchor for Shift ranges. One rectangle of
cells is held by its corners (CellsSelected,CellsCleared); a plain
click or key lets it go.GridSelection.extendis the range a Shift key
makes androwsBetweena Shift range of rows. -
foldkit-mixins-data-grid, selection drawn and driven. Rows say
aria-selectedand cells in the range say it; the grid is
aria-multiselectable. One click listener on the body (CellPressed,
parsed and checked in the grid's update) focuses a cell, spans a range
with Shift and toggles a row with Ctrl or Meta. Shift with a move extends
a range and scrolls its far corner in, Space and Shift+Space select rows,
Ctrl or Meta with A selects all, and Escape lets a range go. The per-cell
mousedownhandlers are gone. -
Over Remote and CRUD (Phase 7).
foldkit-data-grid/crud(optional
peersfoldkit-crudandfoldkit-remote):GridCrud.columns(list, { columns, words })from aCrud.list's members and Displays, with the
grid's own per-member options (pinning, widths,edit, a header),rows(page, key)a row model of a
Remote page (unknown while there are more; the rows a failed read had),
andstatus(page)aRowStatus. The view takesstatus(busy while
loading or refreshing, a failure withonRetry),onMorewhile the count
is unknown, andsortforaria-sortand a sort button, Enter on a
header sending it too. -
The clipboard (Phase 8).
Clipboard.toTsvandparseTsvwrite and
read spreadsheet text (quoted cells, any line end);copyis a box's cells
andpasteAtwhere pasted text lands. The grid's OutMessage is now a union,
Out.Edited | Out.Pasted: a paste is checked cell by cell against each
column and reported once, with what was refused. The view copies the range
(or the focused cell), pastes from the range's corner, and cuts by copying
and clearing, leaving the clipboard to a cell being edited. -
pnpm e2e, the examples with a server end to end. A Vitest project
of its own (vitest.e2e.config.ts), kept out ofpnpm test, and a CI job:
each test starts the example's server and Vite and drives Chromium with
Playwright. The registry's covers reading on scroll, a server sort, an
edit read back from the table and kept across a reload, and the column
menu. The todo app's covers a todo reaching a second person's page without
a reload, written to the journal, and kept across one. The CMS's covers a
writer's draft that the site does not show, and an editor's publish that
it does. The entity example's covers a title saved from the form, read
back from the table and kept across a reload; the pages example's, two
tabs typing into one page, read back by a browser with nothing stored.
examples/pagesgainsstartPagesServer(run byserverMain.ts) and a
Vite config: the page connects to/syncon its own origin, proxied to
the server, and the workspace packages resolve to their source, so it no
longer needspnpm build. -
Messages carry intent: the guide, and
Frames.trackto test it.
docs/state-model.mdsays why a Message should carry what the user did
andupdatework out what it means, with the cases that were bugs.
Frames.track()(foldkit-mixins/testing) tracks the page's frames:
hold()keeps them back so a test's events meet the view as last drawn,
andsettle()waits until everything asked for is drawn, instead of a
sleep.Sync.mount's handle gainssettled(), which waits until the Model
has caught up with the replica (#170).Sort.inputs's callback also
gets the column clicked (foldkit-crud), so a sort header sends it and
updatetoggles; the entity and registry examples do, and two clicks
inside one frame now sort descending. -
App.runnable({ initial, update })(foldkit-surface). An
application made from its Model and Messages is made runnable afterwards,
with the same references and owner, for an update built over those
references (a Remote domain, a placed Bundle) that Sync replays. The
registry had two applications with two owners for this; it has one. -
foldkit-sync/journal, the server's exchange over a journal.
serveJournal(socket, options)answers one socket's exchanges over a
foldkit-durablejournal (an optional peer) and notifies it of each
commit;journalExchange(options)is the same as aTransportClient, and
journalChangesthe notice alone. An operation that does not decode,
names another document, is refused byauthorizeorvalidate, or reuses
an id is rejected by its id, so one bad operation no longer blocks the
outbox; a cursor ahead of the journal's fails before anything is appended,
and a replica of another epoch is answered from the start.refuse
rejects by what the transport established, andsettleapplies commits
elsewhere after each exchange. The todo-app, registry and kitchen-sink
servers use it; todo-app's replicas now hear of each other's commits. -
Remote over plain JSON (
foldkit-remote,foldkit-remote-server).
Remote.http(url, { headers })is Remote's client overPOSTed JSON,
Remote.json(send)the same over any way of sending it (a worker, a
server in the page), andRemoteServer.answer(handlers, body)the server's
side: it decodes the request and payload by the protocol's own schemas
before a handler sees them, and answers 200, 400 for a body that is not a
request (an operation namedconstructorincluded), or 500, a defect
saying onlyInternal error.RemoteJsonRequestandRemoteJsonAnswer
are the wire. The entity, CMS and registry examples use them in place of
their own copies. -
Typed editing from Schema (
foldkit-data-grid). A column'sedit
takes aschemafrom the text typed to its value, in place of
validate: a draft is committed only when it decodes, and the failure's
message is the cell's error.Grid.matchEdit(cell, handlers)reads a
reported cell's value back, one handler per editable column, each typed
by its column's schema.GridCrud.columnskeeps each member's options in
its type, andColumnscarries its specs type-only, so a grid infers a
column's schema exactly. The editor is read from the schema
(CellEditor,Grid.editorFor): a union of string literals is aselect
of them (newchoiceandchoiceOptionslots), a number a text field
withinputmode="decimal", anything else text. -
RowModel.map(rows, input, make)(foldkit-data-grid). Transforms
each row as it is read, with the per-row function made frominputonce
perrowsandinput, and the same model while both are. The registry's
edits over Remote's rows are one call. -
examples/registry, the registry over Remote and Sync. 100,000
products in SQLite behind a DrizzleRemoteServer, read a page at a time
through aCrud.list, sorted on the server, loaded more on scroll. Edits
are a Sync document kept on the device until the server's journal commits
them; the journal applies each to the table throughrecover, and the
grid draws a row with the replica's edits over it, so an edit survives a
reload while offline and another device's shows on the next exchange. -
A sorted header shows its direction (
foldkit-mixins-data-grid). The
sort button carriesdata-sort(ascordesc), andGridStylenow
styles thesortslot as the header's text with an arrow after it; it
had been left a native button. -
Fast typing on a grid keeps every key (
foldkit-data-grid). A key
typed on the grid itself is nowEditTyped({ address, text }): it starts
the edit, or adds to the one open on that cell. Typing "Dowel" faster than
a frame used to restart the edit on each key that reached the grid before
its editor took focus, leaving "l"; Escape in that gap now cancels. -
A column menu (
foldkit-mixins-data-grid).columnMenu: trueputs a
menu button on each header (menuButton) opening arole="menu"
(menu,menuItem) ofGrid.menuItems; Alt+ArrowDown, Shift+F10 or the
menu key opens it from a focused header. It takes focus, walks with the
arrows, Home and End, runs an item on Enter, Space or a click, and closes
on Escape, a choice or focus leaving it. Words:menu,pinStart,
pinEnd,unpin,hide,show. -
A column menu's state (
foldkit-data-grid).menuin the Model
holds the column whose menu is open and its active item;MenuOpened,
MenuMoved,MenuChosenandMenuCloseddrive it.Grid.menuItems
lists pin to another region, hide, and show each hidden column, each only
when it changes something, asGrid.MenuItems; a chosen item runs the
column Message it stands for. Hiding a column now also takes its header's
focus and closes its menu, as it already ended its edit. -
More rows as the end comes into view (
foldkit-mixins-data-grid).
moreOnScroll: truebesideonMoresends it when the More button comes
within 200px of the grid's visible box (an IntersectionObserver rooted at
the grid), and again after each load while the button stays in view; it
asks nothing whilestatusis busy. -
A column header dragged to reorder it (
foldkit-data-grid). The grid
holds a drag as its column and the pointer's delta (dragging), from
ColumnDragStartedwith the header's DOM id (GridFocus.headerOfreads
it back),ColumnDraggedandColumnDragEnded. Let go, the column moves
within its region past the middle of each shown neighbour it crossed,
worked out then byColumnState.dropAtfrom the columns as they stand; a
cancelled drag moves nothing.ColumnState.regionOfsays which region a
column is in.foldkit-mixins-data-griddraws it: a press on a header
moved past 4px drags it (a click, on a sort button too, stays a click),
the header follows the pointer withdata-dragging, the neighbour it
would land beside hasdata-drop, and Escape lets it go back. -
examples/data-grid, a 100,000-row product registry. The UPC
pinned, descriptions and prices edited with validation, rows and ranges
selected, and copy and paste; the products are the application's, written
byonOutfrom the grid'sEditedandPasted. Tested in jsdom and, at
full size, in Chromium. -
Editing (Phase 6). A column with
edit: { draft?, schema? }is
editable as text. TheDataGridModel holds the session (editing), with
EditStarted,EditChanged,EditCommittedandEditCancelled; a commit
the column accepts moves focus and returns the OutMessageEdited({ row, column, text }), so every grid is now placed withonOut
(Bundle.ignorefor one that edits nothing). A refused draft stays with
its error; a click elsewhere commits first; hiding the column ends it. The
view opens a focused field on Enter, F2 or a typed key, commits with Enter
and Tab, cancels with Escape, marks a refused draftaria-invalid, and
hands focus back to the grid. -
The header row by keyboard. Focus holds a
headerbeside the current
cell (HeaderFocused, revealing a header scrolled away): ArrowUp from the
first row reaches it, the arrows and Home and End walk it, and ArrowDown or
Escape return. On a header, Shift with an arrow resizes the column and Ctrl
or Meta with Shift and an arrow reorders it.GridFocus.headerIdnames a
header cell. -
foldkit-data-grid,GridFocus.cellOfreads a cell id back into an
address; an id from another grid, or with a malformed escape, is none. -
foldkit-data-grid,Columns.definewritten inline keeps its row type.
InsideDataGrid.make({ columns: Columns.define<Row>()({...}) })every
rowwasunknown; the return type isNoInfernow. -
foldkit-data-grid,GridViewport.scrollToon a container that cannot
scroll still reportsRevealed: a DOM with no layout has noscrollTo,
and the Command threw there.
Changed
foldkit-mixins-data-grid: more on scroll is theMoreOnScroll
Behavior, not aGridInputflag.moreOnScroll: trueis gone: attach it
to the view,DataGridView<Message>().define(Grid).pipe(MoreOnScroll). The
More button is now keyed by the rows loaded and whether a load is in
flight, whatever is attached (#157).foldkit-sync/journal:editsJournaltakestableRevision, the
highest revision the table holds, required, for the check under Added. An edit to
a row the table lacks is documented as the journal'svalidateto refuse,
since recovery cannot refuse what has committed; the registry does so (#176).foldkit-sync:Sync.transport.sockettakes aurlor amakeSocket,
not both.makeSockettakes no argument now, and a socket opened by one
needs no placeholderurl; passing both is a type error (#167).
Fixed
foldkit-remote-drizzle:containsfolds ASCII alone on Postgres too.
Postgres'slowerfollows the database collation, so under a UTF-8 one a
search foréfoundÉlan, which SQLite and the reference interpreter do
not. The fold now runs under theCcollation on a Postgres table, and the
conformance suite runs against Postgres (PGlite) as well as SQLite (#145).foldkit-remote-drizzle: a search the SQL cannot hold is the query's own
error. A query input holding NUL threwQueryCompileErrorfrom inside the
request, which the RPC server answered as a protocol-level defect, so any
client could turn a query into a server defect and the two backends told the
same input apart. It now fails withRemoteServerError, which the client
receives asRemoteQueryError, as the memory backend's does (#146).foldkit-sync:Sync.presence.servestops when its socket closes, as
Sync.transport.servedoes. A peer whose socket had closed stayed in the
hub and was sent every other peer's presence for the life of the server.foldkit-sync:statusChangesandchangesmiss no change. Each
emitted the current value and subscribed to later changes only once that
value was taken, so a subscriber slow to take it, and any change between
the read and the subscription, went untold. They subscribe first now.examples/registry: an edit lost while offline is said. When the
journal absorbed another device's later edit of a cell before this device
heard of it, the page kept showing its own value as saved until some read,
then changed it silently. A newly retired edit now asks for its rows again,
and a read at or past an edit's revision holding another value says "a
later edit replaced yours" (Replacement.byis anOption). The page names
its device in the Model (DeviceNamed), soreplacedOfreads it there (#173).foldkit-sync/journalno longer reaches Node. It imported
foldkit-durable's main entry, which bringsnode:sqlite, so a browser
bundle of it failed; it importsfoldkit-durable/core.foldkit-remote-drizzleandfoldkit-cms-drizzletakedrizzle-ormas
a peer. As a dependency, an application whose Drizzle resolved with other
optional peers got a second copy, and its tables did not type-check against
the adapter's. Installdrizzle-ormbeside them.examples/registry: a committed edit no longer hides the table for good.
Every edit a device knew of was laid over every row Remote read, with
nothing to say when the table had it, so a row changed afterwards by any
other commit kept showing the old edit. The table is the journal's read
model: each row carries therevisionit has read the journal to, written
with the change in one statement that never moves a row back; the journal
stamps each edit with the sequence it committed at; and a row shows an edit
only while it is pending or committed after the row's revision. A table
write that throws now fails its recovery intent and is retried on the next
exchange, where it died before; an operation that says when it committed
is refused. Every few seconds the server records, as its own operation
(AbsorbedEdits, refused from a client), that the table holds every edit
through its recovery cursor: replicas drop those edits and the log is
compacted, so the replicated slice is what the table lacks. A page keeps an
absorbed edit inretired, set byonReinstall, until its cached row is
read at the edit's revision. The document isregistry-edits-2, since
editschanged shape, so a replica stored under the old one is not opened
(#159).examples/cmsstarts its studio again. Since the studio's sections
share one runtime, the entry drew it into a new element in place of#app
that had no id, which the runtime refuses before drawing, sopnpm devand
the sandbox build showed a blank studio. The element takes#app's id.examples/todo-appadds what was typed, and keeps edits from a second
browser. The composer sentRequestedTodo({ title: model.draft }), the
draft of the frame last drawn, so Enter pressed before the next frame
added nothing; it sendsDraftSubmittedandupdatereads the draft, and
an agent'sRequestedTodono longer clears it. The replica was named by
the token, so a second browser signed in as the same person restarted its
operation ids and the journal refused each new one as reused; each tab is
its own replica now. The end-to-end test found both.foldkit-sync,Mounted.dispatchtakes a Message as its type is. It
handed the value to a Foldkit inbound Port, which decodes what it is sent,
so a Message with a transforming field (Schema.OptionFromNullOr, a
NumberFromString) failed with aSchemaErrorand never reached
update. It encodes the Message first now.