Surface / foldkit-surface

Quick start

ts
import { Option, Schema } from 'effect'
import { defineMessageUnion } from 'foldkit/message'
import { Projection, Surface } from 'foldkit-surface'

const Todo = Schema.Struct({ id: Schema.String, title: Schema.String, done: Schema.Boolean })
const Model = Schema.Struct({
  todos: Schema.Array(Todo),
  todosById: Schema.Record(Schema.String, Todo),
  selectedTodoId: Schema.Option(Schema.String),
})
const Message = defineMessageUnion({
  CreatedTodo: { id: Schema.String, title: Schema.String },
  ToggledTodo: { id: Schema.String },
  SelectedTodo: { id: Schema.String },
})

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

const TodoList = App.surface('TodoList', {
  model: ({ model }) => ({ todos: model.todos, selectedTodoId: model.selectedTodoId }),
  messages: [Message.ToggledTodo, Message.SelectedTodo],
})

const model: typeof Model.Type = {
  todos: [{ id: 't1', title: 'Read the guide', done: false }],
  todosById: {},
  selectedTodoId: Option.some('t1'),
}
Surface.read(TodoList, model)
// { todos: [{ id: 't1', title: 'Read the guide', done: false }], selectedTodoId: Option.some('t1') }

TodoList is a named contract: the projection a feature reads (here the two fields, as Projection.struct lifts them) and the Messages it may send. It reads purely (Surface.read(TodoList, model)), binds a renderer (Surface.view), and is what foldkit-remote, foldkit-sync, and foldkit-agent derive their work from. Projection.pick(App.model.todos) is a writable projection of the same field for a replicator. The extra todosById field illustrates dynamic record lookups later; this read neither uses it nor synchronizes it with todos. Choose one authoritative collection in a real application.