@welshare/questionnaire 0.2.4 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +191 -2
  2. package/dist/esm/components/question-renderer.d.ts +8 -1
  3. package/dist/esm/components/question-renderer.d.ts.map +1 -1
  4. package/dist/esm/components/question-renderer.js +70 -6
  5. package/dist/esm/components/questions/boolean-question.d.ts +3 -2
  6. package/dist/esm/components/questions/boolean-question.d.ts.map +1 -1
  7. package/dist/esm/components/questions/boolean-question.js +2 -2
  8. package/dist/esm/components/questions/choice-question.d.ts +10 -3
  9. package/dist/esm/components/questions/choice-question.d.ts.map +1 -1
  10. package/dist/esm/components/questions/choice-question.js +31 -23
  11. package/dist/esm/components/questions/multiple-choice-question.d.ts +11 -4
  12. package/dist/esm/components/questions/multiple-choice-question.d.ts.map +1 -1
  13. package/dist/esm/components/questions/multiple-choice-question.js +20 -6
  14. package/dist/esm/components/questions/other-text-input.d.ts +39 -0
  15. package/dist/esm/components/questions/other-text-input.d.ts.map +1 -0
  16. package/dist/esm/components/questions/other-text-input.js +35 -0
  17. package/dist/esm/components/questions/quantity-question.d.ts +13 -0
  18. package/dist/esm/components/questions/quantity-question.d.ts.map +1 -0
  19. package/dist/esm/components/questions/quantity-question.js +79 -0
  20. package/dist/esm/index.d.ts +3 -1
  21. package/dist/esm/index.d.ts.map +1 -1
  22. package/dist/esm/index.js +1 -0
  23. package/dist/esm/lib/constants.d.ts +7 -0
  24. package/dist/esm/lib/constants.d.ts.map +1 -1
  25. package/dist/esm/lib/constants.js +7 -0
  26. package/dist/esm/types/index.d.ts +6 -0
  27. package/dist/esm/types/index.d.ts.map +1 -1
  28. package/dist/styles.css +117 -0
  29. package/dist/tokens.css +6 -0
  30. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @welshare/questionnaire
2
2
 
3
- FHIR R4 Questionnaire components for React with state management, validation, and theming.
3
+ FHIR R5 Questionnaire components for React with state management, validation, and theming.
4
4
 
5
5
  ## Installation
6
6
 
@@ -104,10 +104,30 @@ const {
104
104
  - `className?: string` - Container CSS classes
105
105
  - `inputClassName?: string` - Input CSS classes
106
106
  - `choiceClassName?: string` - Choice option CSS classes
107
+ - `choiceLayout?: ChoiceLayout` - `"stacked"` (default) or `"inline-wrap"` (horizontal chip layout)
107
108
  - `renderRadioInput?: (props: RadioInputProps) => ReactNode` - Custom radio renderer
108
109
  - `renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode` - Custom checkbox renderer
109
110
 
110
- **Supported Types:** `choice`, `boolean`, `integer`, `decimal`, `string`, `text`
111
+ **Supported Types:** `coding`, `boolean`, `integer`, `decimal`, `string`, `text`, `quantity`
112
+
113
+ ### Choice Questions with Custom Text Answers
114
+
115
+ Choice fields **can** optionally support custom user text input alongside coded options when you set `answerConstraint: "optionsOrString"` in the FHIR questionnaire definition:
116
+
117
+ ```json
118
+ {
119
+ "linkId": "referral-source",
120
+ "text": "How did you hear about us?",
121
+ "type": "coding",
122
+ "answerConstraint": "optionsOrString",
123
+ "answerOption": [
124
+ { "valueCoding": { "code": "search", "display": "Search engine" } },
125
+ { "valueCoding": { "code": "social", "display": "Social media" } }
126
+ ]
127
+ }
128
+ ```
129
+
130
+ This renders coded options with an "Other" text field. For single-select (`repeats: false`), selecting a coded option or entering text is mutually exclusive. For multi-select (`repeats: true`), coded selections and free text coexist in the answer array.
111
131
 
112
132
  ### BmiForm
113
133
 
@@ -168,6 +188,69 @@ function MyComponent() {
168
188
  }
169
189
  ```
170
190
 
191
+ ### Quantity Questions with Unit Selection
192
+
193
+ Quantity questions allow users to enter numeric values with unit selection. Use the standard FHIR `questionnaire-unitOption` extension to define available units:
194
+
195
+ ```tsx
196
+ const waistCircumferenceItem = {
197
+ linkId: "waist",
198
+ type: "quantity",
199
+ text: "What is your waist circumference?",
200
+ required: false,
201
+ code: [
202
+ {
203
+ system: "http://loinc.org",
204
+ code: "8280-0",
205
+ display: "Waist circumference at umbilicus by tape measure",
206
+ },
207
+ ],
208
+ extension: [
209
+ {
210
+ url: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
211
+ valueCoding: {
212
+ system: "http://unitsofmeasure.org",
213
+ code: "cm",
214
+ display: "cm",
215
+ },
216
+ },
217
+ {
218
+ url: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
219
+ valueCoding: {
220
+ system: "http://unitsofmeasure.org",
221
+ code: "[in_i]",
222
+ display: "in",
223
+ },
224
+ },
225
+ ],
226
+ };
227
+ ```
228
+
229
+ **Features:**
230
+
231
+ - Automatic unit toggle when multiple units are defined
232
+ - Single unit display when only one unit is provided
233
+ - Fallback to simple decimal input when no units are configured
234
+ - Value automatically clears when switching units
235
+
236
+ **Response Format:**
237
+
238
+ ```json
239
+ {
240
+ "linkId": "waist",
241
+ "answer": [
242
+ {
243
+ "valueQuantity": {
244
+ "value": 85,
245
+ "unit": "cm",
246
+ "system": "http://unitsofmeasure.org",
247
+ "code": "cm"
248
+ }
249
+ }
250
+ ]
251
+ }
252
+ ```
253
+
171
254
  ### LegalConsentForm
172
255
 
173
256
  A self-contained form component for collecting user consent before data submission. This component handles:
@@ -311,6 +394,112 @@ Override CSS custom properties:
311
394
  }
312
395
  ```
313
396
 
397
+ ### Choice Layout
398
+
399
+ Choice-based questions (`coding`, `choice`, `boolean`) support two layout modes via the `choiceLayout` prop:
400
+
401
+ | Mode | Class applied | Behavior |
402
+ |------|---------------|----------|
403
+ | `"stacked"` (default) | `.wq-choice-layout-stacked` | Vertical column, one option per row |
404
+ | `"inline-wrap"` | `.wq-choice-layout-inline-wrap` | Horizontal chip layout that wraps |
405
+
406
+ ```tsx
407
+ <QuestionRenderer
408
+ item={item}
409
+ choiceLayout="inline-wrap"
410
+ />
411
+ ```
412
+
413
+ Both modes apply to single-select, multi-select (`repeats: true`), and boolean questions. Existing consumers that don't set `choiceLayout` see no change (defaults to `"stacked"`).
414
+
415
+ #### Chip Design Tokens
416
+
417
+ When using `inline-wrap`, these tokens control chip appearance:
418
+
419
+ | Token | Default | Purpose |
420
+ |-------|---------|---------|
421
+ | `--wq-choice-chip-radius` | `9999px` (pill) | Border radius |
422
+ | `--wq-choice-chip-padding-x` | `1rem` | Horizontal padding |
423
+ | `--wq-choice-chip-padding-y` | `0.5rem` | Vertical padding |
424
+ | `--wq-choice-chip-gap` | `0.5rem` | Gap between chips |
425
+
426
+ #### CSS Class Contract
427
+
428
+ The choice container always emits:
429
+
430
+ - `.wq-question-choice` — base container
431
+ - `.wq-choice-layout-stacked` or `.wq-choice-layout-inline-wrap` — layout variant
432
+ - Your `choiceClassName` value (if provided)
433
+
434
+ State classes on individual options remain unchanged: `.wq-selected`, `.wq-disabled`.
435
+
436
+ #### Quick Setup: Dark Chip Theme
437
+
438
+ To get a dark-themed chip-style UI (horizontal wrapping options with strong selected state), combine the layout prop with token overrides and a small scoped CSS file.
439
+
440
+ **1) Render with layout and a shared choice class:**
441
+
442
+ ```tsx
443
+ <QuestionRenderer
444
+ item={item}
445
+ choiceLayout="inline-wrap"
446
+ choiceClassName="questionnaire-choice"
447
+ />
448
+ ```
449
+
450
+ **2) Set theme tokens (global CSS):**
451
+
452
+ ```css
453
+ :root {
454
+ --wq-color-surface: hsl(246 65% 10%);
455
+ --wq-color-border: hsl(234 50% 20%);
456
+ --wq-color-border-hover: hsl(214 98% 52%);
457
+ --wq-color-border-focus: hsl(214 98% 52%);
458
+ --wq-color-text-primary: hsl(220 20% 95%);
459
+ --wq-color-text-secondary: hsl(220 15% 60%);
460
+ --wq-color-selected: hsl(214 98% 52%);
461
+ --wq-color-selected-border: hsl(214 98% 52%);
462
+
463
+ --wq-choice-chip-radius: 14px;
464
+ --wq-choice-chip-padding-x: 1rem;
465
+ --wq-choice-chip-padding-y: 0.625rem;
466
+ --wq-choice-chip-gap: 0.5rem;
467
+ }
468
+ ```
469
+
470
+ **3) Add scoped brand overrides (e.g. `CustomInputs.css`):**
471
+
472
+ ```css
473
+ .wq-question-choice.questionnaire-choice .wq-choice-option.wq-selected {
474
+ background: var(--wq-color-selected);
475
+ border-color: var(--wq-color-selected-border);
476
+ }
477
+
478
+ .wq-question-choice.questionnaire-choice .wq-choice-label {
479
+ overflow-wrap: anywhere;
480
+ }
481
+ ```
482
+
483
+ **4) (Optional) Hide native radio/checkbox visuals:**
484
+
485
+ Keep inputs in the DOM for accessibility; hide only the visual markers for pure chips:
486
+
487
+ ```css
488
+ .wq-question-choice.questionnaire-choice input[type="radio"],
489
+ .wq-question-choice.questionnaire-choice input[type="checkbox"] {
490
+ position: absolute;
491
+ inline-size: 1px;
492
+ block-size: 1px;
493
+ opacity: 0;
494
+ pointer-events: none;
495
+ }
496
+ ```
497
+
498
+ **Notes:**
499
+ - Keep overrides scoped to your `choiceClassName` to avoid unintended global changes.
500
+ - Use package state classes (`.wq-selected`, `.wq-disabled`) instead of custom state class names.
501
+ - This setup works consistently for single-choice, multi-select, and boolean question types.
502
+
314
503
  ### Custom Input Renderers
315
504
 
316
505
  ```tsx
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { QuestionnaireItem } from "../types/fhir.js";
3
- import { RadioInputProps, CheckboxInputProps, HelperTriggerProps } from "../types/index.js";
3
+ import { ChoiceLayout, RadioInputProps, CheckboxInputProps, HelperTriggerProps } from "../types/index.js";
4
4
  export interface QuestionRendererProps {
5
5
  item: QuestionnaireItem;
6
6
  /**
@@ -18,6 +18,13 @@ export interface QuestionRendererProps {
18
18
  * Will be appended to the default wq-choice-option class
19
19
  */
20
20
  choiceClassName?: string;
21
+ /**
22
+ * Layout mode for choice-based questions (coding, choice,
23
+ * boolean).
24
+ * - `"stacked"` (default): vertical column, one option per row.
25
+ * - `"inline-wrap"`: horizontal chip layout that wraps.
26
+ */
27
+ choiceLayout?: ChoiceLayout;
21
28
  /**
22
29
  * Custom renderer for radio button inputs.
23
30
  * When provided, this function will be called to render each radio option,
@@ -1 +1 @@
1
- {"version":3,"file":"question-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/question-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAkB1D,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,iBAAiB,CAAC;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;IACzD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;IAC/D;;;OAGG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;CAChE;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,eAAe,qBAAqB,4CAyBpE,CAAC"}
1
+ {"version":3,"file":"question-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/question-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAmB1D,OAAO,EACL,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,iBAAiB,CAAC;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;IACzD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;IAC/D;;;OAGG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;CAChE;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,eAAe,qBAAqB,4CAyBpE,CAAC"}
@@ -10,6 +10,7 @@ import { IntegerQuestion } from "./questions/integer-question.js";
10
10
  import { DecimalQuestion, getDecimalHelperTriggerProps, } from "./questions/decimal-question.js";
11
11
  import { StringQuestion } from "./questions/string-question.js";
12
12
  import { BooleanQuestion } from "./questions/boolean-question.js";
13
+ import { QuantityQuestion } from "./questions/quantity-question.js";
13
14
  /**
14
15
  * Wrapper component that combines the library's QuestionRenderer with debug functionality.
15
16
  *
@@ -38,7 +39,7 @@ export const QuestionRenderer = (rendererProps) => {
38
39
  * - Auto-populated (hidden) fields
39
40
  * - Validation error display
40
41
  */
41
- const QuestionRendererInternal = ({ item, className = "", inputClassName = "", choiceClassName = "", renderRadioInput, renderCheckboxInput, renderHelperTrigger, }) => {
42
+ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", choiceClassName = "", choiceLayout = "stacked", renderRadioInput, renderCheckboxInput, renderHelperTrigger, }) => {
42
43
  const { updateAnswer, updateMultipleAnswers, getAnswer, getAnswers, hasValidationError, } = useQuestionnaire();
43
44
  const currentAnswer = getAnswer(item.linkId);
44
45
  const currentAnswers = getAnswers(item.linkId);
@@ -146,6 +147,24 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
146
147
  updateAnswer(item.linkId, { valueDecimal: numValue });
147
148
  }
148
149
  };
150
+ const handleQuantityChange = (value, unit, system, code) => {
151
+ // Allow clearing the controlled input
152
+ if (value === "") {
153
+ updateAnswer(item.linkId, {});
154
+ return;
155
+ }
156
+ const numValue = parseFloat(value);
157
+ if (!isNaN(numValue)) {
158
+ updateAnswer(item.linkId, {
159
+ valueQuantity: {
160
+ value: numValue,
161
+ unit,
162
+ system,
163
+ code,
164
+ },
165
+ });
166
+ }
167
+ };
149
168
  const handleStringChange = (value) => {
150
169
  updateAnswer(item.linkId, { valueString: value });
151
170
  };
@@ -165,26 +184,71 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
165
184
  return { minValue, maxValue, step, unit };
166
185
  };
167
186
  const sliderConfig = getSliderConfig();
187
+ /**
188
+ * Handle free-text "Other" input for coding questions with answerConstraint.
189
+ *
190
+ * Single-select (repeats=false): writing text clears any coded selection
191
+ * (valueString and valueCoding are mutually exclusive per FHIR spec).
192
+ *
193
+ * Multi-select (repeats=true): the free-text answer coexists with coded answers.
194
+ */
195
+ const handleOtherTextChange = (value) => {
196
+ if (item.repeats) {
197
+ // Multi-select: keep all coded answers, replace free-text
198
+ const codedAnswers = currentAnswers.filter((a) => a.valueCoding !== undefined);
199
+ if (value) {
200
+ updateMultipleAnswers(item.linkId, [
201
+ ...codedAnswers,
202
+ { valueString: value },
203
+ ]);
204
+ }
205
+ else {
206
+ updateMultipleAnswers(item.linkId, codedAnswers);
207
+ }
208
+ }
209
+ else {
210
+ // Single-select: free-text replaces any coded answer
211
+ if (value) {
212
+ updateAnswer(item.linkId, { valueString: value });
213
+ }
214
+ else {
215
+ updateAnswer(item.linkId, {});
216
+ }
217
+ }
218
+ };
168
219
  const renderQuestion = () => {
169
220
  switch (item.type) {
170
- // FHIR R4 uses 'choice', R5 uses 'coding' - both render the same way
171
- case "choice":
221
+ // FHIR R5 'coding' type for coded answers
222
+ // When answerConstraint="optionsOrString", allows free-text "other" input
172
223
  case "coding":
224
+ // @deprecated Legacy FHIR R4 types — use "coding" instead
225
+ // "open-choice" is equivalent to "coding" with answerConstraint="optionsOrString"
226
+ // eslint-disable-next-line no-fallthrough
227
+ case "choice":
228
+ case "open-choice": {
229
+ // Normalize R4 "open-choice" → answerConstraint="optionsOrString"
230
+ const normalizedItem = item.type === "open-choice" &&
231
+ item.answerConstraint !== "optionsOrString"
232
+ ? { ...item, answerConstraint: "optionsOrString" }
233
+ : item;
173
234
  // Multi-select with checkboxes when repeats is true
174
235
  if (item.repeats) {
175
- return (_jsx(MultipleChoiceQuestion, { item: item, currentAnswers: currentAnswers, onMultipleChoiceToggle: handleMultipleChoiceToggle, choiceClassName: choiceClassName, renderCheckboxInput: renderCheckboxInput }));
236
+ return (_jsx(MultipleChoiceQuestion, { item: normalizedItem, currentAnswers: currentAnswers, onMultipleChoiceToggle: handleMultipleChoiceToggle, onOtherTextChange: handleOtherTextChange, choiceClassName: choiceClassName, choiceLayout: choiceLayout, inputClassName: inputClassName, renderCheckboxInput: renderCheckboxInput }));
176
237
  }
177
238
  // Single-select with radio buttons (default)
178
- return (_jsx(ChoiceQuestion, { item: item, currentAnswer: currentAnswer, onChoiceChange: handleChoiceChange, choiceClassName: choiceClassName, renderRadioInput: renderRadioInput }));
239
+ return (_jsx(ChoiceQuestion, { item: normalizedItem, currentAnswer: currentAnswer, onChoiceChange: handleChoiceChange, onOtherTextChange: handleOtherTextChange, choiceClassName: choiceClassName, choiceLayout: choiceLayout, inputClassName: inputClassName, renderRadioInput: renderRadioInput }));
240
+ }
179
241
  case "integer":
180
242
  return (_jsx(IntegerQuestion, { item: item, currentAnswer: currentAnswer, onIntegerChange: handleIntegerChange, inputClassName: inputClassName, sliderConfig: sliderConfig }));
181
243
  case "decimal":
182
244
  return (_jsx(DecimalQuestion, { item: item, currentAnswer: currentAnswer, onDecimalChange: handleDecimalChange, inputClassName: inputClassName }));
245
+ case "quantity":
246
+ return (_jsx(QuantityQuestion, { item: item, currentAnswer: currentAnswer, onQuantityChange: handleQuantityChange, inputClassName: inputClassName }));
183
247
  case "string":
184
248
  case "text":
185
249
  return (_jsx(StringQuestion, { item: item, currentAnswer: currentAnswer, onStringChange: handleStringChange, inputClassName: inputClassName }));
186
250
  case "boolean":
187
- return (_jsx(BooleanQuestion, { item: item, currentAnswer: currentAnswer, onBooleanChange: handleBooleanChange, choiceClassName: choiceClassName, renderRadioInput: renderRadioInput }));
251
+ return (_jsx(BooleanQuestion, { item: item, currentAnswer: currentAnswer, onBooleanChange: handleBooleanChange, choiceClassName: choiceClassName, choiceLayout: choiceLayout, renderRadioInput: renderRadioInput }));
188
252
  default:
189
253
  return (_jsxs("div", { className: "wq-unsupported-type", children: ["Unsupported question type: ", item.type] }));
190
254
  }
@@ -1,15 +1,16 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { QuestionnaireItem, QuestionnaireResponseAnswer } from "../../types/fhir.js";
3
- import { RadioInputProps } from "@/types/index.js";
3
+ import { ChoiceLayout, RadioInputProps } from "@/types/index.js";
4
4
  export interface BooleanQuestionProps {
5
5
  item: QuestionnaireItem;
6
6
  currentAnswer?: QuestionnaireResponseAnswer;
7
7
  onBooleanChange: (value: boolean) => void;
8
8
  choiceClassName?: string;
9
+ choiceLayout?: ChoiceLayout;
9
10
  renderRadioInput?: (props: RadioInputProps) => ReactNode;
10
11
  }
11
12
  /**
12
13
  * Renders a boolean question with Yes/No radio buttons
13
14
  */
14
- export declare const BooleanQuestion: ({ item, currentAnswer, onBooleanChange, choiceClassName, renderRadioInput, }: BooleanQuestionProps) => import("react/jsx-runtime").JSX.Element;
15
+ export declare const BooleanQuestion: ({ item, currentAnswer, onBooleanChange, choiceClassName, choiceLayout, renderRadioInput, }: BooleanQuestionProps) => import("react/jsx-runtime").JSX.Element;
15
16
  //# sourceMappingURL=boolean-question.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"boolean-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/boolean-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,eAAe,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;CAC1D;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,8EAM7B,oBAAoB,4CAoDtB,CAAC"}
1
+ {"version":3,"file":"boolean-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/boolean-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEjE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,eAAe,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;CAC1D;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,4FAO7B,oBAAoB,4CAoDtB,CAAC"}
@@ -2,8 +2,8 @@ import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-run
2
2
  /**
3
3
  * Renders a boolean question with Yes/No radio buttons
4
4
  */
5
- export const BooleanQuestion = ({ item, currentAnswer, onBooleanChange, choiceClassName = "", renderRadioInput, }) => {
6
- return (_jsx("div", { className: `wq-question-choice ${choiceClassName}`, children: renderRadioInput ? (_jsxs(_Fragment, { children: [renderRadioInput({
5
+ export const BooleanQuestion = ({ item, currentAnswer, onBooleanChange, choiceClassName = "", choiceLayout = "stacked", renderRadioInput, }) => {
6
+ return (_jsx("div", { className: `wq-question-choice wq-choice-layout-${choiceLayout} ${choiceClassName}`, children: renderRadioInput ? (_jsxs(_Fragment, { children: [renderRadioInput({
7
7
  linkId: item.linkId,
8
8
  checked: currentAnswer?.valueBoolean === true,
9
9
  onChange: () => onBooleanChange(true),
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { QuestionnaireItem, QuestionnaireResponseAnswer } from "../../types/fhir.js";
3
- import { RadioInputProps } from "@/types/index.js";
3
+ import { ChoiceLayout, RadioInputProps } from "@/types/index.js";
4
4
  export interface ChoiceQuestionProps {
5
5
  item: QuestionnaireItem;
6
6
  currentAnswer?: QuestionnaireResponseAnswer;
@@ -10,10 +10,17 @@ export interface ChoiceQuestionProps {
10
10
  display?: string;
11
11
  }, valueInteger?: number) => void;
12
12
  choiceClassName?: string;
13
+ choiceLayout?: ChoiceLayout;
14
+ inputClassName?: string;
13
15
  renderRadioInput?: (props: RadioInputProps) => ReactNode;
16
+ onOtherTextChange?: (value: string) => void;
14
17
  }
15
18
  /**
16
- * Renders a single-select choice question with radio buttons
19
+ * Renders a single-select choice question with radio buttons.
20
+ *
21
+ * FHIR R5: When answerConstraint="optionsOrString", displays an optional
22
+ * "Other" text field alongside coded options. Single-select means selecting
23
+ * a coded option or entering text is mutually exclusive (per FHIR spec).
17
24
  */
18
- export declare const ChoiceQuestion: ({ item, currentAnswer, onChoiceChange, choiceClassName, renderRadioInput, }: ChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
25
+ export declare const ChoiceQuestion: ({ item, currentAnswer, onChoiceChange, choiceClassName, choiceLayout, inputClassName, renderRadioInput, onOtherTextChange, }: ChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
19
26
  //# sourceMappingURL=choice-question.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"choice-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/choice-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,cAAc,EAAE,CACd,WAAW,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EACjE,YAAY,CAAC,EAAE,MAAM,KAClB,IAAI,CAAC;IACV,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;CAC1D;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,GAAI,6EAM5B,mBAAmB,4CA4ErB,CAAC"}
1
+ {"version":3,"file":"choice-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/choice-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGjE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,cAAc,EAAE,CACd,WAAW,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EACjE,YAAY,CAAC,EAAE,MAAM,KAClB,IAAI,CAAC;IACV,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;IACzD,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,8HAS5B,mBAAmB,4CAyFrB,CAAC"}
@@ -1,31 +1,39 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { getAnswerOptionMedia } from "../../lib/questionnaire-utils.js";
3
3
  import { MediaAttachment } from "../media-attachment.js";
4
+ import { OtherTextInput } from "./other-text-input.js";
4
5
  /**
5
- * Renders a single-select choice question with radio buttons
6
+ * Renders a single-select choice question with radio buttons.
7
+ *
8
+ * FHIR R5: When answerConstraint="optionsOrString", displays an optional
9
+ * "Other" text field alongside coded options. Single-select means selecting
10
+ * a coded option or entering text is mutually exclusive (per FHIR spec).
6
11
  */
7
- export const ChoiceQuestion = ({ item, currentAnswer, onChoiceChange, choiceClassName = "", renderRadioInput, }) => {
8
- return (_jsx("div", { className: `wq-question-choice ${choiceClassName}`, children: item.answerOption?.map((option, index) => {
9
- const isSelected = currentAnswer?.valueCoding?.code === option.valueCoding?.code;
10
- // Get media attachment for this answer option
11
- const mediaAttachment = getAnswerOptionMedia(option);
12
- // Use custom renderer if provided
13
- if (renderRadioInput) {
12
+ export const ChoiceQuestion = ({ item, currentAnswer, onChoiceChange, choiceClassName = "", choiceLayout = "stacked", inputClassName = "", renderRadioInput, onOtherTextChange, }) => {
13
+ // Check if this item allows free-text "other" answers (FHIR R5)
14
+ // https://hl7.org/fhir/valueset-questionnaire-answer-constraint.html#expansion
15
+ const allowsOtherText = item.answerConstraint === "optionsOrString";
16
+ return (_jsxs("div", { className: `wq-question-choice wq-choice-layout-${choiceLayout} ${choiceClassName}`, children: [item.answerOption?.map((option, index) => {
17
+ const isSelected = currentAnswer?.valueCoding?.code === option.valueCoding?.code;
18
+ // Get media attachment for this answer option
19
+ const mediaAttachment = getAnswerOptionMedia(option);
20
+ // Use custom renderer if provided
21
+ if (renderRadioInput) {
22
+ return (_jsxs("div", { className: "wq-choice-option-wrapper", children: [mediaAttachment && (_jsx(MediaAttachment, { attachment: mediaAttachment, alt: mediaAttachment.title ||
23
+ option.valueCoding?.display ||
24
+ `Option ${index + 1}`, className: "wq-choice-option-image" })), renderRadioInput({
25
+ linkId: item.linkId,
26
+ valueCoding: option.valueCoding,
27
+ valueInteger: option.valueInteger,
28
+ checked: isSelected,
29
+ onChange: () => onChoiceChange(option.valueCoding || {}, option.valueInteger),
30
+ label: option.valueCoding?.display || "",
31
+ index,
32
+ })] }, index));
33
+ }
34
+ // Default rendering
14
35
  return (_jsxs("div", { className: "wq-choice-option-wrapper", children: [mediaAttachment && (_jsx(MediaAttachment, { attachment: mediaAttachment, alt: mediaAttachment.title ||
15
36
  option.valueCoding?.display ||
16
- `Option ${index + 1}`, className: "wq-choice-option-image" })), renderRadioInput({
17
- linkId: item.linkId,
18
- valueCoding: option.valueCoding,
19
- valueInteger: option.valueInteger,
20
- checked: isSelected,
21
- onChange: () => onChoiceChange(option.valueCoding || {}, option.valueInteger),
22
- label: option.valueCoding?.display || "",
23
- index,
24
- })] }, index));
25
- }
26
- // Default rendering
27
- return (_jsxs("div", { className: "wq-choice-option-wrapper", children: [mediaAttachment && (_jsx(MediaAttachment, { attachment: mediaAttachment, alt: mediaAttachment.title ||
28
- option.valueCoding?.display ||
29
- `Option ${index + 1}`, className: "wq-choice-option-image" })), _jsxs("label", { className: `wq-choice-option ${isSelected ? "wq-selected" : ""}`, children: [_jsx("input", { type: "radio", name: item.linkId, value: option.valueCoding?.code, checked: isSelected, onChange: () => onChoiceChange(option.valueCoding || {}, option.valueInteger), "data-wq-input": "radio", "data-wq-selected": isSelected }), _jsx("span", { className: "wq-choice-label", children: option.valueCoding?.display })] })] }, index));
30
- }) }));
37
+ `Option ${index + 1}`, className: "wq-choice-option-image" })), _jsxs("label", { className: `wq-choice-option ${isSelected ? "wq-selected" : ""}`, children: [_jsx("input", { type: "radio", name: item.linkId, value: option.valueCoding?.code, checked: isSelected, onChange: () => onChoiceChange(option.valueCoding || {}, option.valueInteger), "data-wq-input": "radio", "data-wq-selected": isSelected }), _jsx("span", { className: "wq-choice-label", children: option.valueCoding?.display })] })] }, index));
38
+ }), allowsOtherText && onOtherTextChange && (_jsx(OtherTextInput, { currentAnswer: currentAnswer, onTextChange: onOtherTextChange, inputClassName: inputClassName, isMultiSelect: false }))] }));
31
39
  };
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { QuestionnaireItem, QuestionnaireResponseAnswer } from "../../types/fhir.js";
3
- import { CheckboxInputProps } from "../../types/index.js";
3
+ import { CheckboxInputProps, ChoiceLayout } from "../../types/index.js";
4
4
  export interface MultipleChoiceQuestionProps {
5
5
  item: QuestionnaireItem;
6
6
  currentAnswers: QuestionnaireResponseAnswer[];
@@ -10,11 +10,18 @@ export interface MultipleChoiceQuestionProps {
10
10
  display?: string;
11
11
  }, valueInteger?: number) => void;
12
12
  choiceClassName?: string;
13
+ choiceLayout?: ChoiceLayout;
14
+ inputClassName?: string;
13
15
  renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode;
16
+ onOtherTextChange?: (value: string) => void;
14
17
  }
15
18
  /**
16
- * Renders a multi-select choice question with checkboxes
17
- * Supports exclusive options and maxAnswers limits
19
+ * Renders a multi-select choice question with checkboxes.
20
+ * Supports exclusive options and maxAnswers limits.
21
+ *
22
+ * FHIR R5: When answerConstraint="optionsOrString", displays an optional
23
+ * "Other" text field alongside coded options. Multi-select means both
24
+ * coded options and free-text can coexist in the answers array.
18
25
  */
19
- export declare const MultipleChoiceQuestion: ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName, renderCheckboxInput, }: MultipleChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
26
+ export declare const MultipleChoiceQuestion: ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName, choiceLayout, inputClassName, renderCheckboxInput, onOtherTextChange, }: MultipleChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
20
27
  //# sourceMappingURL=multiple-choice-question.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"multiple-choice-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/multiple-choice-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,iBAAiB,CAAC;IACxB,cAAc,EAAE,2BAA2B,EAAE,CAAC;IAC9C,sBAAsB,EAAE,CACtB,WAAW,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EACjE,YAAY,CAAC,EAAE,MAAM,KAClB,IAAI,CAAC;IACV,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;CAChE;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,yFAMpC,2BAA2B,4CAkH7B,CAAC"}
1
+ {"version":3,"file":"multiple-choice-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/multiple-choice-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGxE,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,iBAAiB,CAAC;IACxB,cAAc,EAAE,2BAA2B,EAAE,CAAC;IAC9C,sBAAsB,EAAE,CACtB,WAAW,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EACjE,YAAY,CAAC,EAAE,MAAM,KAClB,IAAI,CAAC;IACV,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;IAC/D,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAAI,0IASpC,2BAA2B,4CA0I7B,CAAC"}
@@ -2,13 +2,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { WELSHARE_EXTENSIONS } from "../../lib/constants.js";
3
3
  import { getAnswerOptionMedia } from "../../lib/questionnaire-utils.js";
4
4
  import { MediaAttachment } from "../media-attachment.js";
5
+ import { OtherTextInput } from "./other-text-input.js";
5
6
  /**
6
- * Renders a multi-select choice question with checkboxes
7
- * Supports exclusive options and maxAnswers limits
7
+ * Renders a multi-select choice question with checkboxes.
8
+ * Supports exclusive options and maxAnswers limits.
9
+ *
10
+ * FHIR R5: When answerConstraint="optionsOrString", displays an optional
11
+ * "Other" text field alongside coded options. Multi-select means both
12
+ * coded options and free-text can coexist in the answers array.
8
13
  */
9
- export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName = "", renderCheckboxInput, }) => {
14
+ export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName = "", choiceLayout = "stacked", inputClassName = "", renderCheckboxInput, onOtherTextChange, }) => {
15
+ // Check if this item allows free-text "other" answers (FHIR R5)
16
+ // https://hl7.org/fhir/valueset-questionnaire-answer-constraint.html#expansion
17
+ const allowsOtherText = item.answerConstraint === "optionsOrString";
18
+ // Find the free-text answer for maxAnswers logic
19
+ const freeTextAnswer = currentAnswers.find((a) => a.valueString !== undefined && !a.valueCoding);
20
+ // Count only coded answers for maxAnswers logic
21
+ const codedAnswersCount = currentAnswers.filter((a) => a.valueCoding !== undefined).length;
10
22
  const maxAnswers = item.maxAnswers || Number.MAX_SAFE_INTEGER;
11
- const atMaxAnswers = currentAnswers.length >= maxAnswers;
23
+ // Account for free-text answer in max count
24
+ const totalCount = codedAnswersCount + (freeTextAnswer ? 1 : 0);
25
+ const atMaxAnswers = totalCount >= maxAnswers;
12
26
  // Check if there's an exclusive option extension
13
27
  const exclusiveOptionExt = item.extension?.find((ext) => ext.url === WELSHARE_EXTENSIONS.EXCLUSIVE_OPTION);
14
28
  const exclusiveOptionCode = exclusiveOptionExt?.valueString;
@@ -19,7 +33,7 @@ export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceT
19
33
  const optionsToShow = exclusiveSelected
20
34
  ? item.answerOption?.filter((opt) => opt.valueCoding?.code === exclusiveOptionCode)
21
35
  : item.answerOption;
22
- return (_jsxs("div", { className: `wq-question-choice ${choiceClassName}`, children: [optionsToShow?.map((option, index) => {
36
+ return (_jsxs("div", { className: `wq-question-choice wq-choice-layout-${choiceLayout} ${choiceClassName}`, children: [optionsToShow?.map((option, index) => {
23
37
  const isSelected = currentAnswers.some((answer) => answer.valueCoding?.code === option.valueCoding?.code);
24
38
  const isDisabled = !isSelected && atMaxAnswers;
25
39
  // Get media attachment for this answer option
@@ -43,5 +57,5 @@ export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceT
43
57
  return (_jsxs("div", { className: "wq-choice-option-wrapper", children: [mediaAttachment && (_jsx(MediaAttachment, { attachment: mediaAttachment, alt: mediaAttachment.title ||
44
58
  option.valueCoding?.display ||
45
59
  `Option ${index + 1}`, className: "wq-choice-option-image" })), _jsxs("label", { className: `wq-choice-option ${isSelected ? "wq-selected" : ""} ${isDisabled ? "wq-disabled" : ""}`, children: [_jsx("input", { type: "checkbox", name: item.linkId, value: option.valueCoding?.code, checked: isSelected, disabled: isDisabled, onChange: () => onMultipleChoiceToggle(option.valueCoding || {}, option.valueInteger), "data-wq-input": "checkbox", "data-wq-selected": isSelected }), _jsx("span", { className: "wq-choice-label", children: option.valueCoding?.display })] })] }, index));
46
- }), item.maxAnswers && (_jsxs("div", { className: "wq-max-answers-hint", children: ["Selected: ", currentAnswers.length, " / ", item.maxAnswers] }))] }));
60
+ }), item.maxAnswers && (_jsxs("div", { className: "wq-max-answers-hint", children: ["Selected: ", totalCount, " / ", item.maxAnswers] })), allowsOtherText && onOtherTextChange && (_jsx(OtherTextInput, { currentAnswers: currentAnswers, onTextChange: onOtherTextChange, inputClassName: inputClassName, isMultiSelect: true }))] }));
47
61
  };
@@ -0,0 +1,39 @@
1
+ import type { QuestionnaireResponseAnswer } from "../../types/fhir.js";
2
+ export interface OtherTextInputProps {
3
+ /**
4
+ * Current answer (for single-select) or answers array (for multi-select).
5
+ * Used to determine if free-text is active and what value to display.
6
+ */
7
+ currentAnswer?: QuestionnaireResponseAnswer;
8
+ currentAnswers?: QuestionnaireResponseAnswer[];
9
+ /**
10
+ * Callback fired when the user types in the "Other" field.
11
+ */
12
+ onTextChange: (value: string) => void;
13
+ /**
14
+ * Additional CSS class name for the input element.
15
+ */
16
+ inputClassName?: string;
17
+ /**
18
+ * Whether this is used in a multi-select context (repeats=true).
19
+ * Affects how we extract the free-text value.
20
+ */
21
+ isMultiSelect?: boolean;
22
+ }
23
+ /**
24
+ * Shared "Other" text input component for FHIR R5 coding questions
25
+ * with answerConstraint="optionsOrString".
26
+ *
27
+ * This component renders a labeled text field that allows users to enter
28
+ * free-text responses alongside coded options. It follows FHIR R5 semantics:
29
+ * - Single-select: free-text and coded options are mutually exclusive
30
+ * - Multi-select: free-text coexists with coded options
31
+ *
32
+ * Uses unified CSS classes from questionnaire-styles.css:
33
+ * - .wq-open-choice-other (container)
34
+ * - .wq-open-choice-other-label (label)
35
+ * - .wq-open-choice-other-input (input field)
36
+ * - .wq-open-choice-other-active (active state modifier)
37
+ */
38
+ export declare const OtherTextInput: ({ currentAnswer, currentAnswers, onTextChange, inputClassName, isMultiSelect, }: OtherTextInputProps) => import("react/jsx-runtime").JSX.Element;
39
+ //# sourceMappingURL=other-text-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"other-text-input.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/other-text-input.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAEvE,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,cAAc,CAAC,EAAE,2BAA2B,EAAE,CAAC;IAC/C;;OAEG;IACH,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,GAAI,iFAM5B,mBAAmB,4CAoCrB,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Shared "Other" text input component for FHIR R5 coding questions
4
+ * with answerConstraint="optionsOrString".
5
+ *
6
+ * This component renders a labeled text field that allows users to enter
7
+ * free-text responses alongside coded options. It follows FHIR R5 semantics:
8
+ * - Single-select: free-text and coded options are mutually exclusive
9
+ * - Multi-select: free-text coexists with coded options
10
+ *
11
+ * Uses unified CSS classes from questionnaire-styles.css:
12
+ * - .wq-open-choice-other (container)
13
+ * - .wq-open-choice-other-label (label)
14
+ * - .wq-open-choice-other-input (input field)
15
+ * - .wq-open-choice-other-active (active state modifier)
16
+ */
17
+ export const OtherTextInput = ({ currentAnswer, currentAnswers = [], onTextChange, inputClassName = "", isMultiSelect = false, }) => {
18
+ // Extract free-text value based on context
19
+ let otherTextValue = "";
20
+ let hasCodedSelection = false;
21
+ if (isMultiSelect) {
22
+ // Multi-select: find free-text answer in array
23
+ const freeTextAnswer = currentAnswers.find((a) => a.valueString !== undefined && !a.valueCoding);
24
+ otherTextValue = freeTextAnswer?.valueString ?? "";
25
+ hasCodedSelection = currentAnswers.some((a) => a.valueCoding !== undefined);
26
+ }
27
+ else {
28
+ // Single-select: check current answer
29
+ otherTextValue = currentAnswer?.valueString ?? "";
30
+ hasCodedSelection = !!currentAnswer?.valueCoding;
31
+ }
32
+ // In single-select, the input is "active" when free-text is entered and no coded option is selected
33
+ const isActive = !isMultiSelect && !hasCodedSelection && !!otherTextValue;
34
+ return (_jsxs("div", { className: `wq-open-choice-other ${isActive ? "wq-open-choice-other-active" : ""}`, children: [_jsx("label", { className: "wq-open-choice-other-label", children: "Other" }), _jsx("input", { type: "text", className: `wq-question-input wq-open-choice-other-input ${inputClassName}`, value: otherTextValue, onChange: (e) => onTextChange(e.target.value), placeholder: "Enter other answer", "data-wq-input": "open-choice-text" })] }));
35
+ };
@@ -0,0 +1,13 @@
1
+ import type { QuestionnaireItem, QuestionnaireResponseAnswer } from "../../types/fhir.js";
2
+ export interface QuantityQuestionProps {
3
+ item: QuestionnaireItem;
4
+ currentAnswer?: QuestionnaireResponseAnswer;
5
+ onQuantityChange: (value: string, unit: string, system: string, code: string) => void;
6
+ inputClassName?: string;
7
+ }
8
+ /**
9
+ * Renders a quantity question with unit selection
10
+ * Supports the FHIR questionnaire-unitOption extension for defining available units
11
+ */
12
+ export declare const QuantityQuestion: ({ item, currentAnswer, onQuantityChange, inputClassName, }: QuantityQuestionProps) => import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=quantity-question.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quantity-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/quantity-question.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,gBAAgB,EAAE,CAChB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,KACT,IAAI,CAAC;IACV,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAgCD;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,4DAK9B,qBAAqB,4CA8GvB,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { FHIR_EXTENSIONS } from "../../lib/constants.js";
4
+ /**
5
+ * Extract unit options from FHIR questionnaire-unitOption extensions
6
+ */
7
+ const getUnitOptions = (item) => {
8
+ if (!item.extension)
9
+ return [];
10
+ const unitOptions = [];
11
+ for (const ext of item.extension) {
12
+ if (ext.url === FHIR_EXTENSIONS.QUESTIONNAIRE_UNIT_OPTION &&
13
+ ext.valueCoding) {
14
+ unitOptions.push({
15
+ display: ext.valueCoding.display || ext.valueCoding.code || "",
16
+ code: ext.valueCoding.code || "",
17
+ system: ext.valueCoding.system || "",
18
+ });
19
+ }
20
+ }
21
+ return unitOptions;
22
+ };
23
+ /**
24
+ * Renders a quantity question with unit selection
25
+ * Supports the FHIR questionnaire-unitOption extension for defining available units
26
+ */
27
+ export const QuantityQuestion = ({ item, currentAnswer, onQuantityChange, inputClassName = "", }) => {
28
+ const unitOptions = getUnitOptions(item);
29
+ const [selectedUnitIndex, setSelectedUnitIndex] = useState(0);
30
+ // Sync selected unit from existing answer
31
+ useEffect(() => {
32
+ if (currentAnswer?.valueQuantity?.code && unitOptions.length > 0) {
33
+ const matchingIndex = unitOptions.findIndex((opt) => opt.code === currentAnswer.valueQuantity?.code);
34
+ if (matchingIndex !== -1) {
35
+ setSelectedUnitIndex(matchingIndex);
36
+ }
37
+ }
38
+ }, [currentAnswer?.valueQuantity?.code, unitOptions]);
39
+ // Default unit with proper bounds checking
40
+ const defaultUnit = { display: "", code: "", system: "" };
41
+ const currentUnit = unitOptions.length > 0 && selectedUnitIndex < unitOptions.length
42
+ ? (unitOptions[selectedUnitIndex] ?? defaultUnit)
43
+ : defaultUnit;
44
+ // Get input value from current answer
45
+ const [inputValue, setInputValue] = useState(() => {
46
+ const qty = currentAnswer?.valueQuantity;
47
+ if (qty?.value !== undefined)
48
+ return String(qty.value);
49
+ return "";
50
+ });
51
+ const handleChange = (raw) => {
52
+ setInputValue(raw);
53
+ if (raw === "") {
54
+ onQuantityChange("", currentUnit?.display ?? "", currentUnit?.system ?? "", currentUnit?.code ?? "");
55
+ return;
56
+ }
57
+ const num = parseFloat(raw);
58
+ if (!isNaN(num)) {
59
+ onQuantityChange(String(num), currentUnit?.display ?? "", currentUnit?.system ?? "", currentUnit?.code ?? "");
60
+ }
61
+ };
62
+ const handleUnitChange = (index) => {
63
+ // Validate index bounds
64
+ if (index < 0 || index >= unitOptions.length)
65
+ return;
66
+ setSelectedUnitIndex(index);
67
+ // Clear the input when unit changes since the value is in different units
68
+ setInputValue("");
69
+ const newUnit = unitOptions[index];
70
+ if (newUnit) {
71
+ onQuantityChange("", newUnit.display, newUnit.system, newUnit.code);
72
+ }
73
+ };
74
+ // If no unit options are defined, render as a simple decimal input
75
+ if (unitOptions.length === 0) {
76
+ return (_jsx("input", { type: "number", className: `wq-question-input ${inputClassName}`, value: currentAnswer?.valueQuantity?.value ?? "", onChange: (e) => onQuantityChange(e.target.value, "", "", ""), placeholder: "Enter a value", step: "0.1" }));
77
+ }
78
+ return (_jsxs("div", { className: "wq-quantity-question", children: [_jsxs("div", { className: "wq-quantity-input-row", children: [_jsx("input", { type: "number", className: `wq-question-input wq-quantity-input-field ${inputClassName}`, value: inputValue, onChange: (e) => handleChange(e.target.value), placeholder: `Enter value${currentUnit?.display ? ` in ${currentUnit.display}` : ""}`, step: "0.1" }), _jsx("span", { className: "wq-quantity-unit-label", children: currentUnit?.display ?? "" })] }), unitOptions.length > 1 && (_jsx("div", { className: "wq-quantity-unit-toggle", children: unitOptions.map((unit, i) => (_jsx("button", { type: "button", className: `wq-quantity-unit-option ${i === selectedUnitIndex ? "wq-selected" : ""}`, onClick: () => handleUnitChange(i), children: unit.display }, unit.code))) }))] }));
79
+ };
@@ -1,12 +1,14 @@
1
1
  export { QuestionRenderer } from "./components/question-renderer.js";
2
2
  export type { QuestionRendererProps } from "./components/question-renderer.js";
3
+ export { QuantityQuestion } from "./components/questions/quantity-question.js";
4
+ export type { QuantityQuestionProps } from "./components/questions/quantity-question.js";
3
5
  export { BmiForm } from "./components/bmi-form.js";
4
6
  export type { BmiFormProps } from "./components/bmi-form.js";
5
7
  export { LegalConsentForm } from "./components/legal-consent-form.js";
6
8
  export type { LegalConsentFormProps, LegalConsentResult, LegalCheckboxProps, LegalDocumentLinks, } from "./components/legal-consent-form.js";
7
9
  export { QuestionnaireProvider, useQuestionnaire, type QuestionnaireContextType, type QuestionnaireProviderProps, } from "./contexts/questionnaire-context.js";
8
10
  export type { Attachment, Coding, Extension, Questionnaire, QuestionnaireItem, QuestionnaireItemAnswerOption, QuestionnaireResponse, QuestionnaireResponseAnswer, QuestionnaireResponseItem, } from "./types/fhir.js";
9
- export type { RadioInputProps, CheckboxInputProps, InputHelperConfig, HelperTriggerProps, } from "./types/index.js";
11
+ export type { ChoiceLayout, RadioInputProps, CheckboxInputProps, InputHelperConfig, HelperTriggerProps, } from "./types/index.js";
10
12
  export { calculateProgress, findQuestionnaireItem, getAllQuestionsFromPage, getAnswerOptionMedia, getExclusiveOptionCode, getInputHelper, getItemMedia, getVisiblePages, hasAnswerValue, isQuestionHidden, } from "./lib/questionnaire-utils.js";
11
13
  export { calculateBmi } from "./lib/bmi-helpers.js";
12
14
  export { FHIR_EXTENSIONS, WELSHARE_EXTENSIONS, WELSHARE_CODE_SYSTEMS, FHIR_CODE_SYSTEMS, } from "./lib/constants.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAE/E,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,YAAY,EACV,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,GAChC,MAAM,qCAAqC,CAAC;AAG7C,YAAY,EACV,UAAU,EACV,MAAM,EACN,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,6BAA6B,EAC7B,qBAAqB,EACrB,2BAA2B,EAC3B,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGpD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAE/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AAC/E,YAAY,EAAE,qBAAqB,EAAE,MAAM,6CAA6C,CAAC;AAEzF,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,YAAY,EACV,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,GAChC,MAAM,qCAAqC,CAAC;AAG7C,YAAY,EACV,UAAU,EACV,MAAM,EACN,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,6BAA6B,EAC7B,qBAAqB,EACrB,2BAA2B,EAC3B,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGpD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC"}
package/dist/esm/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Components
2
2
  export { QuestionRenderer } from "./components/question-renderer.js";
3
+ export { QuantityQuestion } from "./components/questions/quantity-question.js";
3
4
  export { BmiForm } from "./components/bmi-form.js";
4
5
  export { LegalConsentForm } from "./components/legal-consent-form.js";
5
6
  // Contexts
@@ -21,6 +21,13 @@ export declare const FHIR_EXTENSIONS: {
21
21
  * @see http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia
22
22
  */
23
23
  readonly ITEM_ANSWER_MEDIA: "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia";
24
+ /**
25
+ * Standard FHIR extension for defining unit options for quantity questions
26
+ * Allows users to select from predefined units of measurement
27
+ * Type: valueCoding with system, code, and display for the unit
28
+ * @see http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption
29
+ */
30
+ readonly QUESTIONNAIRE_UNIT_OPTION: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption";
24
31
  };
25
32
  /**
26
33
  * Welshare Custom Extension URLs
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,eAAe;IAC1B;;;;OAIG;;IAIH;;;;OAIG;;IAIH;;;;OAIG;;CAGK,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;IAC9B;;;;;;;;;;;OAWG;;IAIH;;;;;;;;;;;;;;;;;OAiBG;;IAIH;;;;;;;;;;;;;;;;OAgBG;;CAGK,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,qBAAqB;IAChC;;;;OAIG;;CAEK,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,iBAAiB;IAC5B;;;;OAIG;;CAEK,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,eAAe;IAC1B;;;;OAIG;;IAIH;;;;OAIG;;IAIH;;;;OAIG;;IAIH;;;;;OAKG;;CAGK,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;IAC9B;;;;;;;;;;;OAWG;;IAIH;;;;;;;;;;;;;;;;;OAiBG;;IAIH;;;;;;;;;;;;;;;;OAgBG;;CAGK,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,qBAAqB;IAChC;;;;OAIG;;CAEK,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,iBAAiB;IAC5B;;;;OAIG;;CAEK,CAAC"}
@@ -21,6 +21,13 @@ export const FHIR_EXTENSIONS = {
21
21
  * @see http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia
22
22
  */
23
23
  ITEM_ANSWER_MEDIA: "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia",
24
+ /**
25
+ * Standard FHIR extension for defining unit options for quantity questions
26
+ * Allows users to select from predefined units of measurement
27
+ * Type: valueCoding with system, code, and display for the unit
28
+ * @see http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption
29
+ */
30
+ QUESTIONNAIRE_UNIT_OPTION: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
24
31
  };
25
32
  /**
26
33
  * Welshare Custom Extension URLs
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Layout mode for choice-based questions.
3
+ * - `"stacked"`: Default column layout (one option per row).
4
+ * - `"inline-wrap"`: Horizontal chip layout that wraps to next line.
5
+ */
6
+ export type ChoiceLayout = "stacked" | "inline-wrap";
1
7
  /**
2
8
  * Props for rendering a radio button option
3
9
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,MAAM,GAAG,MAAM;IACrD,kDAAkD;IAClD,MAAM,EAAE,iBAAiB,CAAC;IAC1B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,YAAY,CAAC,EAAE,CAAC,CAAC;IACjB,0DAA0D;IAC1D,eAAe,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CACrC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,aAAa,CAAC;AAErD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,MAAM,GAAG,MAAM;IACrD,kDAAkD;IAClD,MAAM,EAAE,iBAAiB,CAAC;IAC1B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,YAAY,CAAC,EAAE,CAAC,CAAC;IACjB,0DAA0D;IAC1D,eAAe,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CACrC"}
package/dist/styles.css CHANGED
@@ -99,6 +99,30 @@
99
99
  font-weight: var(--wq-font-weight-medium);
100
100
  }
101
101
 
102
+ /* === Inline-Wrap (Chip) Layout === */
103
+ .wq-question-choice.wq-choice-layout-inline-wrap {
104
+ flex-direction: row;
105
+ flex-wrap: wrap;
106
+ gap: var(--wq-choice-chip-gap);
107
+ }
108
+
109
+ .wq-question-choice.wq-choice-layout-inline-wrap
110
+ .wq-choice-option-wrapper {
111
+ max-width: 100%;
112
+ }
113
+
114
+ .wq-question-choice.wq-choice-layout-inline-wrap .wq-choice-option {
115
+ border-radius: var(--wq-choice-chip-radius);
116
+ padding: var(--wq-choice-chip-padding-y)
117
+ var(--wq-choice-chip-padding-x);
118
+ width: auto;
119
+ }
120
+
121
+ .wq-question-choice.wq-choice-layout-inline-wrap .wq-choice-label {
122
+ white-space: normal;
123
+ word-break: break-word;
124
+ }
125
+
102
126
  .wq-max-answers-hint {
103
127
  font-size: var(--wq-font-size-sm);
104
128
  color: var(--wq-color-text-tertiary);
@@ -106,6 +130,65 @@
106
130
  font-weight: var(--wq-font-weight-normal);
107
131
  }
108
132
 
133
+ /* === Quantity Inputs === */
134
+ .wq-quantity-question {
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: var(--wq-space-md);
138
+ }
139
+
140
+ .wq-quantity-input-row {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: var(--wq-space-sm);
144
+ }
145
+
146
+ .wq-quantity-input-field {
147
+ flex: 1;
148
+ }
149
+
150
+ .wq-quantity-unit-label {
151
+ font-size: var(--wq-font-size-base);
152
+ font-weight: var(--wq-font-weight-medium);
153
+ color: var(--wq-color-text-primary);
154
+ opacity: 0.7;
155
+ min-width: 2rem;
156
+ }
157
+
158
+ .wq-quantity-unit-toggle {
159
+ display: flex;
160
+ gap: 0;
161
+ border: var(--wq-border-width) solid var(--wq-color-border);
162
+ border-radius: var(--wq-radius-md);
163
+ overflow: hidden;
164
+ width: fit-content;
165
+ }
166
+
167
+ .wq-quantity-unit-option {
168
+ padding: var(--wq-space-sm) var(--wq-space-lg);
169
+ font-size: var(--wq-font-size-sm);
170
+ font-family: inherit;
171
+ background: var(--wq-color-surface);
172
+ color: var(--wq-color-text-primary);
173
+ border: none;
174
+ cursor: pointer;
175
+ transition: all var(--wq-transition-fast);
176
+ }
177
+
178
+ .wq-quantity-unit-option:not(:last-child) {
179
+ border-right: var(--wq-border-width) solid var(--wq-color-border);
180
+ }
181
+
182
+ .wq-quantity-unit-option.wq-selected {
183
+ background: var(--wq-color-primary);
184
+ color: white;
185
+ font-weight: var(--wq-font-weight-semibold);
186
+ }
187
+
188
+ .wq-quantity-unit-option:hover:not(.wq-selected) {
189
+ background-color: var(--wq-color-background);
190
+ }
191
+
109
192
  /* === Text Inputs === */
110
193
  .wq-question-input {
111
194
  width: 100%;
@@ -248,6 +331,34 @@
248
331
  font-weight: var(--wq-font-weight-medium);
249
332
  }
250
333
 
334
+ /* === Open-Choice (coded options + free-text "Other") === */
335
+ .wq-open-choice {
336
+ display: flex;
337
+ flex-direction: column;
338
+ gap: var(--wq-space-md);
339
+ }
340
+
341
+ .wq-open-choice-other {
342
+ display: flex;
343
+ flex-direction: column;
344
+ gap: var(--wq-space-sm);
345
+ margin-top: var(--wq-space-sm);
346
+ }
347
+
348
+ .wq-open-choice-other-label {
349
+ font-size: var(--wq-font-size-sm);
350
+ font-weight: var(--wq-font-weight-medium);
351
+ color: var(--wq-color-text-secondary);
352
+ }
353
+
354
+ .wq-open-choice-other-active .wq-open-choice-other-label {
355
+ color: var(--wq-color-text-primary);
356
+ }
357
+
358
+ .wq-open-choice-other-input {
359
+ max-width: 24rem;
360
+ }
361
+
251
362
  /* === Unsupported Type === */
252
363
  .wq-unsupported-type {
253
364
  padding: var(--wq-space-lg);
@@ -315,6 +426,12 @@
315
426
  .wq-slider-value-display {
316
427
  font-size: var(--wq-font-size-lg);
317
428
  }
429
+
430
+ .wq-question-choice.wq-choice-layout-inline-wrap
431
+ .wq-choice-option {
432
+ padding: var(--wq-choice-chip-padding-y)
433
+ var(--wq-space-md);
434
+ }
318
435
  }
319
436
 
320
437
  /* === Print Styles === */
package/dist/tokens.css CHANGED
@@ -107,6 +107,12 @@
107
107
  --wq-choice-padding-x: var(--wq-space-lg);
108
108
  --wq-choice-padding-y: var(--wq-space-md);
109
109
 
110
+ /* Chip tokens (used by inline-wrap layout) */
111
+ --wq-choice-chip-radius: var(--wq-radius-full);
112
+ --wq-choice-chip-padding-x: var(--wq-space-lg);
113
+ --wq-choice-chip-padding-y: var(--wq-space-sm);
114
+ --wq-choice-chip-gap: var(--wq-space-sm);
115
+
110
116
  --wq-question-gap: var(--wq-space-xl);
111
117
 
112
118
  --wq-slider-height: 0.5rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@welshare/questionnaire",
3
- "version": "0.2.4",
3
+ "version": "0.2.7",
4
4
  "description": "FHIR Questionnaire components for React with state management and validation",
5
5
  "keywords": [
6
6
  "react",
@@ -38,7 +38,7 @@
38
38
  "storybook": "^10.1.11",
39
39
  "@storybook/react": "^10.1.11",
40
40
  "@storybook/react-vite": "^10.1.11",
41
- "@welshare/sdk": "0.3.4",
41
+ "@welshare/sdk": "0.3.5",
42
42
  "@workspace/eslint-config": "0.0.0",
43
43
  "@workspace/typescript-config": "0.0.0"
44
44
  },