Composition / foldkit-mixins-builder

What the inspector draws

The selected node's props are its Block's settings form, a foldkit-form form that foldkit-builder makes (see its README), drawn by foldkit-mixins-form's FormView. So each prop gets the control a form would give it, and its fields are styled through FieldSlots and FormSlots, as any form's are:

Prop SchemaControl
Schema.Booleaninput type="checkbox"
Schema.Literals([...])select of the literals, text or numbers; a number is stored as a number
Schema.Numberinput type="text", so "1." is kept while it is typed
Schema.String, and a brand of it such as Urlinput type="text"
anything elseits JSON, shown and not edited

A control of the application's own, such as a color picker given with Builder.controls({ tint: Input.bundle(...) }), is drawn with its Bundle's view, as any form draws it. A field is labelled with its Schema's title, else its prop key, spaced. A change that decodes is one setProp, and typing a word is one undo step. One that does not (abc for a number, an address its brand refuses) shows its error at its field and changes nothing.

Where the Schema alone does not say, the Block asks for a control through metadata. Input.multiline() draws a textarea, and Input.hidden() leaves the prop out:

ts
import { Schema } from 'effect'
import { Block, Content } from 'foldkit-composition'
import { Builder } from 'foldkit-builder'
import { Input } from 'foldkit-form'

const Quote = Block.define('Quote', {
  Props: Schema.Struct({
    text: Schema.String.annotate({ title: 'Quotation' }),
    ref: Schema.String,
  }),
  provides: [Content.Flow],
}).pipe(Block.annotate(Builder.controls({ text: Input.multiline(), ref: Input.hidden() })))

The hint is foldkit-builder's, kept on the Block beside any other package's metadata; foldkit-composition does not read it. A later annotation's prop replaces an earlier one's.

What the editor calls a Block

A Block's label, description and palette group are its words, from foldkit-composition, which an agent's tool reads too:

ts
const Described = Quote.pipe(Block.words({ group: 'Text', description: 'Words someone said' }))

The label defaults to the Block's name spaced (PostList is "Post list"), and Blocks given no group share one, "Blocks"; a group is headed only when there is more than one. The palette shows the label and the description, and a layer row the label and the node's first text prop, cut to forty characters. Palette buttons and layer rows carry data-block with the Block's name, so a Style can give each an icon; a pattern's button carries data-pattern with its name.

A prop that points at the application's things

A prop holding an id, such as the category a list shows, asks for a picker: Input.relationOne(Category) draws a select, and Input.relationMany(Tag) a group of checkboxes over a prop that is an array of ids. What they choose from is the application's rows, loaded by its own query as a form's picker options are; the page's parent gives them in the Builder's view inputs, keyed 'Block.prop':

ts
const Featured = Block.define('Featured', {
  Props: Schema.Struct({ category: Schema.NullOr(Schema.String) }),
  provides: [Content.Flow],
}).pipe(Block.annotate(Builder.controls({ category: Input.relationOne(Category) })))

EditorSlot.view(model, h, {
  controls: {
    document: BuilderView.inputs({
      options: { 'Featured.category': categories.map(c => ({ value: c.id, label: c.name })) },
    }),
  },
})

A relationOne always offers "none", stored as null where the Schema admits it; choosing it for a prop that needs an id says so at the field. A chosen id the choices lack (a row since deleted, or choices not loaded yet) is shown as ? id; in a relationMany it stays chosen until it is unchecked. The inspector lists the choices it is given and has no search box.

A node whose Block the Catalog does not know is shown, with its props, but not edited. A stored value a select does not offer, such as a choice an older version made, is shown as ? value and chosen, with its error, rather than as the blank.

The props are under Content, labelled by their Schema's title, else their key spaced (text is "Text"). Under Style, each appearance axis the Block offers: one of up to four values is a row of buttons, "Default" and each value, the chosen one pressed (a stored value the axis lacks is shown pressed as ? value); one with more values is a select, with a blank for the default. A choice is one setAppearance, and choosing the default for the last one removes the node's appearance. A responsive token axis is a select per breakpoint (Space at md), blank for unchanged; with only the base chosen, one name is stored.

Then, under Visibility, when the Catalog declares a context, one field per context key says when the node shows: Shown when audience is is a select of the key's literals (a flag's is true and false, other keys are typed in), with a blank for always. A choice is one setWhen holding an eq condition for that key; conditions of other kinds are kept as they are, and clearing the last removes the node's when.

Last, under Interactions, each event the Block names says what it runs: On press is a select of the Catalog's actions, blank for nothing, and under it the chosen action's input as a form of its own, drawn and styled as the props are; its ids are <Block>-<event>-<action>-<key>. Choosing an action starts its input from each field's empty value as stored ('', null, false), else zero or a select's first choice (inputOf(block, event, action).seed() in foldkit-builder); a start the action's Schema still refuses is refused and shown in the alert. Each change to the input is one setAction with the changed keys over the rest, and choosing nothing removes it.

The looks and the conditions stay the Builder's own controls, drawn through BuilderSlots: each is a choice from a fixed list, which a form would draw no better.