From a1108933e078e064be8ef11ce2761f365a93dadf Mon Sep 17 00:00:00 2001 From: Najuna Brian Date: Thu, 2 Jul 2026 12:16:18 +0300 Subject: [PATCH 1/5] docs: document Likert and duration question types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add form-author reference for the built-in `likert` and `duration` question types under Reference → Form Specifications → Question Types, covering display modes, colour, presets, layout, N/A, and stored values. --- docs/reference/form-specifications.md | 117 ++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/docs/reference/form-specifications.md b/docs/reference/form-specifications.md index f9a3c6f..c254b4b 100644 --- a/docs/reference/form-specifications.md +++ b/docs/reference/form-specifications.md @@ -294,6 +294,123 @@ Generic attachment via document picker. Use **`type: object`** with **`format: s } ``` +### Rating Scales (Likert) + +Use **`format: "likert"`** for agreement, satisfaction, frequency, importance, likelihood, and numeric rating scales. The stored value is the selected `oneOf[].const` (typically an integer, or `null` when "Not applicable" is chosen). Labels come from each `oneOf[].title` and are fully translatable. + +```json +{ + "type": "integer", + "format": "likert", + "title": "How satisfied are you with the service?", + "oneOf": [ + { "const": 1, "title": "Very dissatisfied" }, + { "const": 2, "title": "Dissatisfied" }, + { "const": 3, "title": "Neutral" }, + { "const": 4, "title": "Satisfied" }, + { "const": 5, "title": "Very satisfied" } + ], + "likert": { + "display": "buttons", + "colorMode": "spectrum", + "allowClear": true + } +} +``` + +All variants share one clean look: neutral outlined option cells, with an accent border and tint on the selected option only. Every option is a touch-friendly target and the layout adapts to phone and tablet widths. + +#### Display modes (`likert.display`) + +| Value | Presentation | +| --------- | -------------------------------------------------------- | +| `buttons` | Equal-width labelled option cells (default) | +| `radio` | Radio row with labels below (classic survey style) | +| `slider` | Slider with tick marks, endpoint anchors, and value badge| +| `numeric` | Compact number cells (NPS style) | +| `stars` | Star rating with the selected label alongside | +| `emoji` | One emoji per option (`emoji` on each `oneOf` entry) | + +#### Colour (`likert.colorMode`) + +| Value | Selected-option accent | +| ---------- | ----------------------------------------------- | +| `neutral` | Theme primary (default) | +| `spectrum` | Semantic red → yellow → green by scale position | +| `stars` | Standard rating gold (used with `display: "stars"`) | + +Colour is only ever a secondary cue — selection is always conveyed by the border, tint, and weight as well, so scales remain readable for colour-blind respondents. + +#### Presets + +When you omit `oneOf`, set `likert.preset` to generate standard options: `agreement`, `frequency`, `satisfaction`, `importance`, `likelihood`, `numeric_0_10`, `numeric_1_5`, `numeric_1_7`. + +#### Choosing a scale + +| Display | Best for | Label pattern | +| -------------------------------------- | ------------------------------- | ---------------------------------------------------------------- | +| `buttons` / `radio` | Opinion scales (3–5 options) | Full label per option (`oneOf[].title`) | +| `numeric` | NPS, pain, rating (5+ points) | Numbers in cells; word labels on the first/last `oneOf` entries | +| `buttons` + `endpointLabelsOnly: true` | NPS 0–10 in button form | Digits in cells; endpoint words below | +| `slider` | Continuous 0–10 ranges | Endpoint word anchors below; value badge always visible | +| `emoji` | Optional sentiment (low-stakes) | Emoji **and** text label on every option | +| `stars` | 5-point satisfaction | Star count with the selected label beside it | + +Research on survey scales favours **numeric scales with verbal endpoint anchors** (highest reliability) and **fully-labelled word buttons** (fastest to answer). Emoji are engaging but can cluster toward the middle and vary by culture, so they always render with a text label and are best reserved for informal contexts. + +#### Additional options + +| Option | Description | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | +| `likert.allowClear` | Tapping the selected option again clears it (default `true`). | +| `likert.endpointLabelsOnly` | Show word labels only at the endpoints of long numeric scales; omit for 3–4 option scales. | +| `likert.allowNotApplicable` | Adds a "Not applicable" choice. Use `type: ["integer", "null"]`; stores the `notApplicableValue`. | +| `likert.notApplicableLabel` | Custom label for the N/A option (default "Not applicable"). | +| `likert.notApplicableValue` | Stored value for N/A (default `null`). | + +#### Layout (UI schema) + +Control the arrangement from the UI schema `options`: + +```json +{ + "type": "Control", + "scope": "#/properties/satisfaction", + "options": { "orientation": "cols-2" } +} +``` + +`options.orientation` accepts `horizontal` (default), `vertical` (stacked), `flow` (wrap), or `cols-2` … `cols-5` (a fixed multi-column grid, useful on tablets). Word and radio scales automatically stack to one option per row on narrow phones. `options.display` overrides `likert.display` per placement. + +In review/read-only mode the selected answer stays prominent while the other options are de-emphasised, and the finalize summary shows the option's `title` (or "Not applicable"). + +### Duration / Timer + +Use **`format: "duration"`** to capture an elapsed time. The stored value is always a number of **seconds**. + +```json +{ + "type": "number", + "format": "duration", + "title": "Time to complete the task", + "minimum": 0, + "duration": { + "mode": "stopwatch", + "unit": "seconds", + "precision": 1, + "allowManualEntry": true + } +} +``` + +| `duration.mode` | Presentation | +| --------------- | --------------------------------------------------- | +| `stopwatch` | Start / Pause / Resume / Reset, then **Save** to commit | +| `countdown` | Counts down from `duration.countdownFrom` seconds | +| `manual` | A plain numeric seconds field | + +Other options: `unit` (display unit), `precision` (decimal places), `allowManualEntry` (permit typing a value alongside the timer), and `countdownFrom` (starting seconds for `countdown`). The stopwatch commits only when the collector presses **Save**, so an unsaved running timer never writes a partial value. The finalize summary renders the value in a human-readable form (e.g. `1m 30s`). + ## Form Versioning Forms support versioning to allow updates while maintaining compatibility: From 2263d7fba4f291113ace68635289204e490043e8 Mon Sep 17 00:00:00 2001 From: Najuna Brian Date: Thu, 2 Jul 2026 13:02:40 +0300 Subject: [PATCH 2/5] docs: add Likert and duration quick-start recipes Expand form-specifications with end-to-end schema/ui.json examples, stored-value tables, preset reference, copy-paste recipes for common variants, and duration mode examples so new form authors can use them directly. --- docs/reference/form-specifications.md | 370 +++++++++++++++++++++++--- 1 file changed, 330 insertions(+), 40 deletions(-) diff --git a/docs/reference/form-specifications.md b/docs/reference/form-specifications.md index c254b4b..c7e9ac7 100644 --- a/docs/reference/form-specifications.md +++ b/docs/reference/form-specifications.md @@ -296,54 +296,125 @@ Generic attachment via document picker. Use **`type: object`** with **`format: s ### Rating Scales (Likert) -Use **`format: "likert"`** for agreement, satisfaction, frequency, importance, likelihood, and numeric rating scales. The stored value is the selected `oneOf[].const` (typically an integer, or `null` when "Not applicable" is chosen). Labels come from each `oneOf[].title` and are fully translatable. +Use **`format: "likert"`** for agreement, satisfaction, frequency, importance, likelihood, and numeric rating scales. + +#### Quick start + +Add a field to **`schema.json`**, then wire it in **`ui.json`** with a standard `Control`: + +**schema.json** ```json { - "type": "integer", - "format": "likert", - "title": "How satisfied are you with the service?", - "oneOf": [ - { "const": 1, "title": "Very dissatisfied" }, - { "const": 2, "title": "Dissatisfied" }, - { "const": 3, "title": "Neutral" }, - { "const": 4, "title": "Satisfied" }, - { "const": 5, "title": "Very satisfied" } - ], - "likert": { - "display": "buttons", - "colorMode": "spectrum", - "allowClear": true - } + "type": "object", + "properties": { + "satisfaction": { + "type": "integer", + "format": "likert", + "title": "How satisfied are you with the service?", + "oneOf": [ + { "const": 1, "title": "Very dissatisfied" }, + { "const": 2, "title": "Dissatisfied" }, + { "const": 3, "title": "Neutral" }, + { "const": 4, "title": "Satisfied" }, + { "const": 5, "title": "Very satisfied" } + ], + "likert": { + "display": "buttons", + "colorMode": "spectrum", + "allowClear": true + } + } + }, + "required": ["satisfaction"] } ``` -All variants share one clean look: neutral outlined option cells, with an accent border and tint on the selected option only. Every option is a touch-friendly target and the layout adapts to phone and tablet widths. +**ui.json** + +```json +{ + "type": "Control", + "scope": "#/properties/satisfaction" +} +``` + +That is enough to render the question. Put display options (`likert.display`, `colorMode`, etc.) in **schema.json**. Use **ui.json** only when you need per-placement overrides such as layout (`orientation`) or a different display for the same field on another page. + +For multi-locale forms, put the visible question text on `Control.label` in `ui.json` (with optional `translations`) rather than relying on `schema.title` alone — see [Form translations](/guides/form-translations). + +#### What gets stored + +The observation stores the selected option's **`oneOf[].const`** — not the label text. + +| User selects | Stored value (`satisfaction`) | +| ------------ | ----------------------------- | +| "Satisfied" | `4` | +| Clears answer (when `allowClear: true`) | field omitted or `null` depending on schema | +| "Not applicable" (when configured) | `null` (or `notApplicableValue`) | + +Example observation payload: + +```json +{ + "satisfaction": 4 +} +``` + +In review/finalize mode, Formplayer shows the matching `oneOf[].title` (e.g. "Satisfied"), or the N/A label when the stored value is null. #### Display modes (`likert.display`) -| Value | Presentation | -| --------- | -------------------------------------------------------- | -| `buttons` | Equal-width labelled option cells (default) | -| `radio` | Radio row with labels below (classic survey style) | -| `slider` | Slider with tick marks, endpoint anchors, and value badge| -| `numeric` | Compact number cells (NPS style) | -| `stars` | Star rating with the selected label alongside | -| `emoji` | One emoji per option (`emoji` on each `oneOf` entry) | +| Value | Presentation | +| --------- | --------------------------------------------------------- | +| `buttons` | Equal-width labelled option cells (**default**) | +| `radio` | Radio row with labels below (classic survey style) | +| `slider` | Slider with tick marks, endpoint anchors, and value badge | +| `numeric` | Compact number cells (NPS style) | +| `stars` | Star rating with the selected label alongside | +| `emoji` | One emoji per option (`emoji` on each `oneOf` entry) | + +All variants share one clean look: neutral outlined option cells, with an accent border and tint on the selected option only. Every option is a touch-friendly target and the layout adapts to phone and tablet widths. #### Colour (`likert.colorMode`) -| Value | Selected-option accent | -| ---------- | ----------------------------------------------- | -| `neutral` | Theme primary (default) | -| `spectrum` | Semantic red → yellow → green by scale position | -| `stars` | Standard rating gold (used with `display: "stars"`) | +| Value | Selected-option accent | +| ---------- | ----------------------------------------------------- | +| `neutral` | Theme primary (**default**) | +| `spectrum` | Semantic red → yellow → green by scale position | +| `stars` | Standard rating gold (used with `display: "stars"`) | Colour is only ever a secondary cue — selection is always conveyed by the border, tint, and weight as well, so scales remain readable for colour-blind respondents. #### Presets -When you omit `oneOf`, set `likert.preset` to generate standard options: `agreement`, `frequency`, `satisfaction`, `importance`, `likelihood`, `numeric_0_10`, `numeric_1_5`, `numeric_1_7`. +When you omit `oneOf`, set **`likert.preset`** to generate standard options automatically: + +```json +{ + "type": "integer", + "format": "likert", + "title": "How often do you exercise?", + "likert": { + "preset": "frequency", + "display": "buttons", + "colorMode": "neutral" + } +} +``` + +| Preset | Stored values | Options (labels) | +| -------------- | ------------- | ---------------- | +| `agreement` | 1–5 | Strongly disagree → Strongly agree | +| `frequency` | 1–5 | Never → Always | +| `satisfaction` | 1–5 | Very dissatisfied → Very satisfied | +| `importance` | 1–5 | Not important → Very important | +| `likelihood` | 1–5 | Very unlikely → Very likely | +| `numeric_0_10` | 0–10 | `0`, `1`, … `10` | +| `numeric_1_5` | 1–5 | `1`, `2`, … `5` | +| `numeric_1_7` | 1–7 | `1`, `2`, … `7` | + +Presets are a shortcut for common scales. For custom wording or non-standard value ranges, define your own `oneOf` instead. #### Choosing a scale @@ -358,6 +429,124 @@ When you omit `oneOf`, set `likert.preset` to generate standard options: `agreem Research on survey scales favours **numeric scales with verbal endpoint anchors** (highest reliability) and **fully-labelled word buttons** (fastest to answer). Emoji are engaging but can cluster toward the middle and vary by culture, so they always render with a text label and are best reserved for informal contexts. +#### Copy-paste recipes + +**Pain slider (0–10 with word anchors)** — stores `0` … `10`: + +```json +{ + "type": "integer", + "format": "likert", + "title": "Rate your pain level", + "oneOf": [ + { "const": 0, "title": "No pain" }, + { "const": 1, "title": "1" }, + { "const": 2, "title": "2" }, + { "const": 3, "title": "3" }, + { "const": 4, "title": "4" }, + { "const": 5, "title": "5" }, + { "const": 6, "title": "6" }, + { "const": 7, "title": "7" }, + { "const": 8, "title": "8" }, + { "const": 9, "title": "9" }, + { "const": 10, "title": "Worst pain" } + ], + "likert": { + "display": "slider", + "colorMode": "spectrum" + } +} +``` + +**NPS-style 0–10 (endpoint labels only in button form)** — stores `0` … `10`: + +```json +{ + "type": "integer", + "format": "likert", + "title": "How likely are you to recommend us?", + "oneOf": [ + { "const": 0, "title": "Not at all likely" }, + { "const": 1, "title": "1" }, + { "const": 2, "title": "2" }, + { "const": 3, "title": "3" }, + { "const": 4, "title": "4" }, + { "const": 5, "title": "5" }, + { "const": 6, "title": "6" }, + { "const": 7, "title": "7" }, + { "const": 8, "title": "8" }, + { "const": 9, "title": "9" }, + { "const": 10, "title": "Extremely likely" } + ], + "likert": { + "display": "buttons", + "colorMode": "spectrum", + "endpointLabelsOnly": true, + "allowClear": true + } +} +``` + +**Emoji sentiment** — stores `1` … `5`; `emoji` is display-only metadata: + +```json +{ + "type": "integer", + "format": "likert", + "title": "How do you feel about your visit today?", + "oneOf": [ + { "const": 1, "title": "Very bad", "emoji": "😞" }, + { "const": 2, "title": "Bad", "emoji": "😕" }, + { "const": 3, "title": "Okay", "emoji": "😐" }, + { "const": 4, "title": "Good", "emoji": "🙂" }, + { "const": 5, "title": "Great", "emoji": "😄" } + ], + "likert": { + "display": "emoji", + "colorMode": "spectrum", + "allowClear": true + } +} +``` + +**With "Not applicable"** — stores `null` when N/A is chosen; use `type: ["integer", "null"]`: + +```json +{ + "type": ["integer", "null"], + "format": "likert", + "title": "How important is this feature to you?", + "oneOf": [ + { "const": 1, "title": "Not important" }, + { "const": 2, "title": "Slightly important" }, + { "const": 3, "title": "Moderately important" }, + { "const": 4, "title": "Important" }, + { "const": 5, "title": "Very important" } + ], + "likert": { + "display": "buttons", + "allowClear": true, + "allowNotApplicable": true, + "notApplicableLabel": "Not applicable", + "notApplicableValue": null + } +} +``` + +**Required field** — add the property name to the schema `required` array: + +```json +{ + "type": "object", + "properties": { + "satisfaction": { "type": "integer", "format": "likert", "likert": { "preset": "satisfaction" } } + }, + "required": ["satisfaction"] +} +``` + +You can also mark a control required in `ui.json` with `"options": { "required": true }` on that `Control`. + #### Additional options | Option | Description | @@ -380,13 +569,78 @@ Control the arrangement from the UI schema `options`: } ``` -`options.orientation` accepts `horizontal` (default), `vertical` (stacked), `flow` (wrap), or `cols-2` … `cols-5` (a fixed multi-column grid, useful on tablets). Word and radio scales automatically stack to one option per row on narrow phones. `options.display` overrides `likert.display` per placement. +`options.orientation` accepts `horizontal` (default), `vertical` (stacked), `flow` (wrap), or `cols-2` … `cols-5` (a fixed multi-column grid, useful on tablets). Word and radio scales automatically stack to one option per row on narrow phones. `options.display` overrides `likert.display` for that control only. -In review/read-only mode the selected answer stays prominent while the other options are de-emphasised, and the finalize summary shows the option's `title` (or "Not applicable"). +In review/read-only mode the selected answer stays prominent while the other options are de-emphasised. ### Duration / Timer -Use **`format: "duration"`** to capture an elapsed time. The stored value is always a number of **seconds**. +Use **`format: "duration"`** to capture elapsed time. The stored value is always a JSON **number of seconds** (e.g. `90.5` for one minute thirty-and-a-half seconds). + +#### Quick start + +**schema.json** + +```json +{ + "type": "object", + "properties": { + "task_duration": { + "type": "number", + "format": "duration", + "title": "Time to complete the task", + "minimum": 0, + "duration": { + "mode": "stopwatch", + "unit": "seconds", + "precision": 1, + "allowManualEntry": true + } + } + } +} +``` + +**ui.json** + +```json +{ + "type": "Control", + "scope": "#/properties/task_duration" +} +``` + +#### What gets stored + +| Mode | When value is written | Example stored value | +| ----------- | ---------------------------------- | -------------------- | +| `stopwatch` | Collector taps **Save** after timing | `125.3` (seconds) | +| `countdown` | When countdown completes or is saved | `42.0` | +| `manual` | On blur / field commit | `300` | + +Example observation payload: + +```json +{ + "task_duration": 125.3 +} +``` + +The finalize summary shows a human-readable form (e.g. `2 min 5.3 sec`). Use `minimum: 0` to reject negative durations. + +#### Modes + +| `duration.mode` | Presentation | +| --------------- | ----------------------------------------------------- | +| `stopwatch` | Start / Pause / Resume / Reset, then **Save** to commit | +| `countdown` | Counts down from `duration.countdownFrom` seconds | +| `manual` | A plain numeric seconds field only | + +**Stopwatch** does not write a value while the timer is running. The collector must pause and tap **Save** — this prevents partial or accidental commits. + +#### Copy-paste recipes + +**Stopwatch with optional manual entry** (default pattern): ```json { @@ -403,13 +657,49 @@ Use **`format: "duration"`** to capture an elapsed time. The stored value is alw } ``` -| `duration.mode` | Presentation | -| --------------- | --------------------------------------------------- | -| `stopwatch` | Start / Pause / Resume / Reset, then **Save** to commit | -| `countdown` | Counts down from `duration.countdownFrom` seconds | -| `manual` | A plain numeric seconds field | +**Countdown from 60 seconds** (e.g. breath-hold test): + +```json +{ + "type": "number", + "format": "duration", + "title": "Hold breath duration", + "minimum": 0, + "duration": { + "mode": "countdown", + "unit": "seconds", + "precision": 1, + "countdownFrom": 60, + "allowManualEntry": false + } +} +``` + +**Manual entry only** (no timer UI): + +```json +{ + "type": "number", + "format": "duration", + "title": "Enter elapsed time (seconds)", + "minimum": 0, + "duration": { + "mode": "manual", + "unit": "seconds", + "precision": 1 + } +} +``` + +#### Duration options -Other options: `unit` (display unit), `precision` (decimal places), `allowManualEntry` (permit typing a value alongside the timer), and `countdownFrom` (starting seconds for `countdown`). The stopwatch commits only when the collector presses **Save**, so an unsaved running timer never writes a partial value. The finalize summary renders the value in a human-readable form (e.g. `1m 30s`). +| Option | Description | +| -------------------- | ----------- | +| `duration.mode` | `stopwatch`, `countdown`, or `manual` (default `stopwatch`) | +| `duration.unit` | Display unit; currently only `"seconds"` | +| `duration.precision` | Decimal places shown (default `1`) | +| `duration.allowManualEntry` | When `true`, shows a seconds input alongside stopwatch/countdown | +| `duration.countdownFrom` | Starting seconds for `countdown` mode (required for countdown) | ## Form Versioning From c5539a364881b06509cf86e3dcbae56a1d766172 Mon Sep 17 00:00:00 2001 From: Najuna Brian Date: Thu, 2 Jul 2026 13:23:36 +0300 Subject: [PATCH 3/5] docs: clarify Likert display is independent of scale content Note that any display works with any oneOf labels, rename the pain-slider recipe to a generic 0-10 slider pattern, and add a task-difficulty example. --- docs/reference/form-specifications.md | 35 +++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/reference/form-specifications.md b/docs/reference/form-specifications.md index c7e9ac7..0e069df 100644 --- a/docs/reference/form-specifications.md +++ b/docs/reference/form-specifications.md @@ -298,6 +298,8 @@ Generic attachment via document picker. Use **`type: object`** with **`format: s Use **`format: "likert"`** for agreement, satisfaction, frequency, importance, likelihood, and numeric rating scales. +**Display and content are independent.** There are no topic-specific Likert types (no separate "pain slider" or "satisfaction buttons"). Pick a **`likert.display`** for *how* the scale looks, and set **`title`** plus **`oneOf`** for *what* it measures — any display works with any values and labels. Presets are optional shortcuts for common label sets on standard numeric ranges. + #### Quick start Add a field to **`schema.json`**, then wire it in **`ui.json`** with a standard `Control`: @@ -421,7 +423,7 @@ Presets are a shortcut for common scales. For custom wording or non-standard val | Display | Best for | Label pattern | | -------------------------------------- | ------------------------------- | ---------------------------------------------------------------- | | `buttons` / `radio` | Opinion scales (3–5 options) | Full label per option (`oneOf[].title`) | -| `numeric` | NPS, pain, rating (5+ points) | Numbers in cells; word labels on the first/last `oneOf` entries | +| `numeric` | NPS, intensity, rating (5+ points) | Numbers in cells; word labels on the first/last `oneOf` entries | | `buttons` + `endpointLabelsOnly: true` | NPS 0–10 in button form | Digits in cells; endpoint words below | | `slider` | Continuous 0–10 ranges | Endpoint word anchors below; value badge always visible | | `emoji` | Optional sentiment (low-stakes) | Emoji **and** text label on every option | @@ -431,7 +433,9 @@ Research on survey scales favours **numeric scales with verbal endpoint anchors* #### Copy-paste recipes -**Pain slider (0–10 with word anchors)** — stores `0` … `10`: +**0–10 slider with endpoint anchors** — stores `0` … `10`. Change `title` and the first/last `oneOf[].title` values to rate anything; the slider UI is the same. + +*Example — pain intensity:* ```json { @@ -458,6 +462,33 @@ Research on survey scales favours **numeric scales with verbal endpoint anchors* } ``` +*Example — task difficulty (same display, different labels):* + +```json +{ + "type": "integer", + "format": "likert", + "title": "How difficult was this task?", + "oneOf": [ + { "const": 0, "title": "Very easy" }, + { "const": 1, "title": "1" }, + { "const": 2, "title": "2" }, + { "const": 3, "title": "3" }, + { "const": 4, "title": "4" }, + { "const": 5, "title": "5" }, + { "const": 6, "title": "6" }, + { "const": 7, "title": "7" }, + { "const": 8, "title": "8" }, + { "const": 9, "title": "9" }, + { "const": 10, "title": "Extremely hard" } + ], + "likert": { + "display": "slider", + "colorMode": "spectrum" + } +} +``` + **NPS-style 0–10 (endpoint labels only in button form)** — stores `0` … `10`: ```json From 4fba0f5faec683a1dc3f323a789c9569f2af0627 Mon Sep 17 00:00:00 2001 From: Najuna Brian Date: Thu, 2 Jul 2026 13:26:02 +0300 Subject: [PATCH 4/5] docs: present Likert display/content note as a tip admonition --- docs/reference/form-specifications.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/reference/form-specifications.md b/docs/reference/form-specifications.md index 0e069df..1608356 100644 --- a/docs/reference/form-specifications.md +++ b/docs/reference/form-specifications.md @@ -298,7 +298,11 @@ Generic attachment via document picker. Use **`type: object`** with **`format: s Use **`format: "likert"`** for agreement, satisfaction, frequency, importance, likelihood, and numeric rating scales. -**Display and content are independent.** There are no topic-specific Likert types (no separate "pain slider" or "satisfaction buttons"). Pick a **`likert.display`** for *how* the scale looks, and set **`title`** plus **`oneOf`** for *what* it measures — any display works with any values and labels. Presets are optional shortcuts for common label sets on standard numeric ranges. +:::tip Display and content are independent + +There are no topic-specific Likert types (no separate "pain slider" or "satisfaction buttons"). Pick a **`likert.display`** for *how* the scale looks, and set **`title`** plus **`oneOf`** for *what* it measures — any display works with any values and labels. Presets are optional shortcuts for common label sets on standard numeric ranges. + +::: #### Quick start From 75c6a4f7c312bb78c16495d8280f0ca82121f167 Mon Sep 17 00:00:00 2001 From: Najuna Brian Date: Thu, 2 Jul 2026 19:08:03 +0300 Subject: [PATCH 5/5] docs: add Likert N/A validation, layout, and i18n guidance Document automatic Not applicable schema handling, equal-width grid layout on tablets, and translating scale option labels via ui.json options.oneOf. --- docs/reference/form-specifications.md | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/reference/form-specifications.md b/docs/reference/form-specifications.md index 1608356..e80048b 100644 --- a/docs/reference/form-specifications.md +++ b/docs/reference/form-specifications.md @@ -568,6 +568,12 @@ Research on survey scales favours **numeric scales with verbal endpoint anchors* } ``` +:::note + +You only need `type: ["integer", "null"]` (so the field can hold the N/A value). Formplayer automatically allows the `notApplicableValue` during validation — you do **not** need to add a `null` branch to `oneOf` yourself, and the N/A choice is never shown twice. + +::: + **Required field** — add the property name to the schema `required` array: ```json @@ -606,8 +612,41 @@ Control the arrangement from the UI schema `options`: `options.orientation` accepts `horizontal` (default), `vertical` (stacked), `flow` (wrap), or `cols-2` … `cols-5` (a fixed multi-column grid, useful on tablets). Word and radio scales automatically stack to one option per row on narrow phones. `options.display` overrides `likert.display` for that control only. +On tablets and desktop, word-label scales render as **equal-width cells** in an even grid, so long labels (e.g. "Strongly agree", "Always") never stretch to a full-width row. + In review/read-only mode the selected answer stays prominent while the other options are de-emphasised. +#### Translating option labels + +Question text is translated via `Control.label` + `translations` (see [Form translations](/guides/form-translations)). To translate the **scale option labels**, mirror the values in the UI schema `options.oneOf` and provide per-locale overrides — they are matched to the schema options by `const`: + +```json +{ + "type": "Control", + "scope": "#/properties/satisfaction", + "label": "How satisfied are you?", + "options": { + "oneOf": [ + { "const": 1, "title": "Very dissatisfied" }, + { "const": 5, "title": "Very satisfied" } + ] + }, + "translations": { + "pt": { + "label": "Quão satisfeito está?", + "options": { + "oneOf": [ + { "const": 1, "title": "Muito insatisfeito" }, + { "const": 5, "title": "Muito satisfeito" } + ] + } + } + } +} +``` + +The stored value is unchanged (`oneOf[].const`). Any option not listed in a locale keeps its `schema.json` `oneOf[].title`. The finalize summary uses the `schema.json` titles. + ### Duration / Timer Use **`format: "duration"`** to capture elapsed time. The stored value is always a JSON **number of seconds** (e.g. `90.5` for one minute thirty-and-a-half seconds).