Widget catalogue
Every input the widget renders and the declaration that produces it.
An input is derived from the field declaration in submission.properties. A step lists field names; it never names an input kind.
| Kind | Declaration | Renders as | Checked before advancing |
|---|---|---|---|
| Choice | oneOf of const + title | buttons, or select in a form | value is one of oneOf |
| Text | type: "string" | input type="text" | minLength, maxLength |
| Phone | format: "phone" | input type="tel" | 7–15 digits |
format: "email" | input type="email" | local@domain.tld | |
| Date | format: "date" | input type="date" | YYYY-MM-DD |
| Appointment | format: "date-time" + schedule | grid of slots by day | slot came from the grid |
Applies to every kind:
| In the declaration | Effect |
|---|---|
name in submission.required | asterisk on the label; step will not advance while empty |
placeholder | hint inside the empty input |
title | the label, and the row label in summary |
theme.primaryColor | accent of buttons, launcher and selected slot |
meta.locale | language of formatted dates and times |
Choice
{
"title": "Service",
"oneOf": [
{
"const": "haircut",
"title": "Haircut"
},
{
"const": "beard",
"title": "Beard trim"
},
{
"const": "both",
"title": "Haircut and beard"
}
]
}
One declaration, two renderings, selected by the step:
| Step | Rendering |
|---|---|
"fields": ["service"] | a row of buttons, one per option; the click advances the step |
"fields": ["property_type", "rooms", "address"] | select inside a form with a submit button |
submitLabel sets the button text of the form rendering; default is Send.
Text
{
"title": "Anything we should know",
"type": "string",
"maxLength": 200,
"placeholder": "Optional"
}
Single line. maxLength is enforced as the visitor types; minLength on advance.
Phone
format: "phone": 7 to 15 digits after stripping spaces, +, -, (, ). Anything else is rejected.
{
"title": "Phone",
"type": "string",
"format": "phone",
"placeholder": "+44 7700 900000"
}
format: "email": one @, a dot in the domain, no spaces.
{
"title": "Email",
"type": "string",
"format": "email"
}
Date
format: "date": a calendar date without time or zone, YYYY-MM-DD.
{
"title": "Preferred date",
"type": "string",
"format": "date"
}
Choose between date and appointment by what the business does with it: date states a preference the owner confirms later, appointment takes a slot.
Appointment
{
"title": "Appointment time",
"type": "string",
"format": "date-time",
"durationMinutes": 45,
"timeZone": "Europe/London",
"schedule": [
{
"days": [
"mon",
"wed",
"thu"
],
"from": "10:00",
"to": "19:00"
},
{
"days": [
"fri"
],
"from": "10:00",
"to": "21:00"
},
{
"days": [
"tue"
],
"from": "15:00",
"to": "19:00"
},
{
"days": [
"sat"
],
"from": "11:00",
"to": "14:00"
},
{
"days": [
"sat"
],
"from": "15:00",
"to": "18:00"
}
]
}
format: "date-time" with three required annotations: durationMinutes, schedule, timeZone. Slots are durationMinutes long and cover the next 14 days.
schedule rules:
| Rule | Result |
|---|---|
{ "days": ["mon","wed"], "from": "10:00", "to": "19:00" } | same hours on both days |
two rules for sat, 11:00–14:00 and 15:00–18:00 | a break between them |
| a day absent from every rule | closed that day |
| two overlapping rules for one day | rejected by flow-lint |
from later than to | rejected by flow-lint |
remaining window shorter than durationMinutes | no slot offered in it |
from and to are wall-clock time in timeZone; an offset such as 10:00+01:00 is rejected.
Picking a slot does not hold it: free time is the schedule minus the owner's calendar at the moment of asking, and the owner confirms the request.
Screens
Screens produce no values.
| Screen | Declaration | Behaviour |
|---|---|---|
message | text, next | a bot line |
summary | title, fields, actions | rows of collected values, then the actions below |
status_tracker | entity, title | the request status: new, confirmed or rejected |
summary actions:
| Action | Effect |
|---|---|
confirm | sends the request and goes to next; every required field must be filled by now |
edit | returns to the step in target, keeping every entered value |
restart | clears the dialogue and returns to flow.start |
{
"type": "summary",
"title": "Check your booking:",
"fields": [
"service",
"appointment",
"name",
"phone"
],
"actions": [
{
"type": "confirm",
"label": "Book it",
"next": "status"
},
{
"type": "edit",
"label": "Change time",
"target": "appointment"
},
{
"type": "restart",
"label": "Start over"
}
]
}