@welshare/questionnaire 0.2.4 → 0.2.6

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.
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
 
@@ -107,7 +107,26 @@ const {
107
107
  - `renderRadioInput?: (props: RadioInputProps) => ReactNode` - Custom radio renderer
108
108
  - `renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode` - Custom checkbox renderer
109
109
 
110
- **Supported Types:** `choice`, `boolean`, `integer`, `decimal`, `string`, `text`
110
+ **Supported Types:** `coding`, `boolean`, `integer`, `decimal`, `string`, `text`, `quantity`
111
+
112
+ ### Choice Questions with Custom Text Answers
113
+
114
+ Choice fields **can** optionally support custom user text input alongside coded options when you set `answerConstraint: "optionsOrString"` in the FHIR questionnaire definition:
115
+
116
+ ```json
117
+ {
118
+ "linkId": "referral-source",
119
+ "text": "How did you hear about us?",
120
+ "type": "coding",
121
+ "answerConstraint": "optionsOrString",
122
+ "answerOption": [
123
+ { "valueCoding": { "code": "search", "display": "Search engine" } },
124
+ { "valueCoding": { "code": "social", "display": "Social media" } }
125
+ ]
126
+ }
127
+ ```
128
+
129
+ 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
130
 
112
131
  ### BmiForm
113
132
 
@@ -168,6 +187,69 @@ function MyComponent() {
168
187
  }
169
188
  ```
170
189
 
190
+ ### Quantity Questions with Unit Selection
191
+
192
+ Quantity questions allow users to enter numeric values with unit selection. Use the standard FHIR `questionnaire-unitOption` extension to define available units:
193
+
194
+ ```tsx
195
+ const waistCircumferenceItem = {
196
+ linkId: "waist",
197
+ type: "quantity",
198
+ text: "What is your waist circumference?",
199
+ required: false,
200
+ code: [
201
+ {
202
+ system: "http://loinc.org",
203
+ code: "8280-0",
204
+ display: "Waist circumference at umbilicus by tape measure",
205
+ },
206
+ ],
207
+ extension: [
208
+ {
209
+ url: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
210
+ valueCoding: {
211
+ system: "http://unitsofmeasure.org",
212
+ code: "cm",
213
+ display: "cm",
214
+ },
215
+ },
216
+ {
217
+ url: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
218
+ valueCoding: {
219
+ system: "http://unitsofmeasure.org",
220
+ code: "[in_i]",
221
+ display: "in",
222
+ },
223
+ },
224
+ ],
225
+ };
226
+ ```
227
+
228
+ **Features:**
229
+
230
+ - Automatic unit toggle when multiple units are defined
231
+ - Single unit display when only one unit is provided
232
+ - Fallback to simple decimal input when no units are configured
233
+ - Value automatically clears when switching units
234
+
235
+ **Response Format:**
236
+
237
+ ```json
238
+ {
239
+ "linkId": "waist",
240
+ "answer": [
241
+ {
242
+ "valueQuantity": {
243
+ "value": 85,
244
+ "unit": "cm",
245
+ "system": "http://unitsofmeasure.org",
246
+ "code": "cm"
247
+ }
248
+ }
249
+ ]
250
+ }
251
+ ```
252
+
171
253
  ### LegalConsentForm
172
254
 
173
255
  A self-contained form component for collecting user consent before data submission. This component handles:
@@ -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,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"}
@@ -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
  *
@@ -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,21 +184,66 @@ 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, 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, 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 }));
@@ -10,10 +10,16 @@ export interface ChoiceQuestionProps {
10
10
  display?: string;
11
11
  }, valueInteger?: number) => void;
12
12
  choiceClassName?: string;
13
+ inputClassName?: string;
13
14
  renderRadioInput?: (props: RadioInputProps) => ReactNode;
15
+ onOtherTextChange?: (value: string) => void;
14
16
  }
15
17
  /**
16
- * Renders a single-select choice question with radio buttons
18
+ * Renders a single-select choice question with radio buttons.
19
+ *
20
+ * FHIR R5: When answerConstraint="optionsOrString", displays an optional
21
+ * "Other" text field alongside coded options. Single-select means selecting
22
+ * a coded option or entering text is mutually exclusive (per FHIR spec).
17
23
  */
18
- export declare const ChoiceQuestion: ({ item, currentAnswer, onChoiceChange, choiceClassName, renderRadioInput, }: ChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
24
+ export declare const ChoiceQuestion: ({ item, currentAnswer, onChoiceChange, choiceClassName, inputClassName, renderRadioInput, onOtherTextChange, }: ChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
19
25
  //# 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,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGnD,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,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,gHAQ5B,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 = "", 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 ${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
  };
@@ -10,11 +10,17 @@ export interface MultipleChoiceQuestionProps {
10
10
  display?: string;
11
11
  }, valueInteger?: number) => void;
12
12
  choiceClassName?: string;
13
+ inputClassName?: string;
13
14
  renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode;
15
+ onOtherTextChange?: (value: string) => void;
14
16
  }
15
17
  /**
16
- * Renders a multi-select choice question with checkboxes
17
- * Supports exclusive options and maxAnswers limits
18
+ * Renders a multi-select choice question with checkboxes.
19
+ * Supports exclusive options and maxAnswers limits.
20
+ *
21
+ * FHIR R5: When answerConstraint="optionsOrString", displays an optional
22
+ * "Other" text field alongside coded options. Multi-select means both
23
+ * coded options and free-text can coexist in the answers array.
18
24
  */
19
- export declare const MultipleChoiceQuestion: ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName, renderCheckboxInput, }: MultipleChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
25
+ export declare const MultipleChoiceQuestion: ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName, inputClassName, renderCheckboxInput, onOtherTextChange, }: MultipleChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
20
26
  //# 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,MAAM,sBAAsB,CAAC;AAG1D,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,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,4HAQpC,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 = "", 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;
@@ -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,5 +1,7 @@
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";
@@ -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,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
package/dist/styles.css CHANGED
@@ -106,6 +106,65 @@
106
106
  font-weight: var(--wq-font-weight-normal);
107
107
  }
108
108
 
109
+ /* === Quantity Inputs === */
110
+ .wq-quantity-question {
111
+ display: flex;
112
+ flex-direction: column;
113
+ gap: var(--wq-space-md);
114
+ }
115
+
116
+ .wq-quantity-input-row {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: var(--wq-space-sm);
120
+ }
121
+
122
+ .wq-quantity-input-field {
123
+ flex: 1;
124
+ }
125
+
126
+ .wq-quantity-unit-label {
127
+ font-size: var(--wq-font-size-base);
128
+ font-weight: var(--wq-font-weight-medium);
129
+ color: var(--wq-color-text-primary);
130
+ opacity: 0.7;
131
+ min-width: 2rem;
132
+ }
133
+
134
+ .wq-quantity-unit-toggle {
135
+ display: flex;
136
+ gap: 0;
137
+ border: var(--wq-border-width) solid var(--wq-color-border);
138
+ border-radius: var(--wq-radius-md);
139
+ overflow: hidden;
140
+ width: fit-content;
141
+ }
142
+
143
+ .wq-quantity-unit-option {
144
+ padding: var(--wq-space-sm) var(--wq-space-lg);
145
+ font-size: var(--wq-font-size-sm);
146
+ font-family: inherit;
147
+ background: var(--wq-color-surface);
148
+ color: var(--wq-color-text-primary);
149
+ border: none;
150
+ cursor: pointer;
151
+ transition: all var(--wq-transition-fast);
152
+ }
153
+
154
+ .wq-quantity-unit-option:not(:last-child) {
155
+ border-right: var(--wq-border-width) solid var(--wq-color-border);
156
+ }
157
+
158
+ .wq-quantity-unit-option.wq-selected {
159
+ background: var(--wq-color-primary);
160
+ color: white;
161
+ font-weight: var(--wq-font-weight-semibold);
162
+ }
163
+
164
+ .wq-quantity-unit-option:hover:not(.wq-selected) {
165
+ background-color: var(--wq-color-background);
166
+ }
167
+
109
168
  /* === Text Inputs === */
110
169
  .wq-question-input {
111
170
  width: 100%;
@@ -248,6 +307,34 @@
248
307
  font-weight: var(--wq-font-weight-medium);
249
308
  }
250
309
 
310
+ /* === Open-Choice (coded options + free-text "Other") === */
311
+ .wq-open-choice {
312
+ display: flex;
313
+ flex-direction: column;
314
+ gap: var(--wq-space-md);
315
+ }
316
+
317
+ .wq-open-choice-other {
318
+ display: flex;
319
+ flex-direction: column;
320
+ gap: var(--wq-space-sm);
321
+ margin-top: var(--wq-space-sm);
322
+ }
323
+
324
+ .wq-open-choice-other-label {
325
+ font-size: var(--wq-font-size-sm);
326
+ font-weight: var(--wq-font-weight-medium);
327
+ color: var(--wq-color-text-secondary);
328
+ }
329
+
330
+ .wq-open-choice-other-active .wq-open-choice-other-label {
331
+ color: var(--wq-color-text-primary);
332
+ }
333
+
334
+ .wq-open-choice-other-input {
335
+ max-width: 24rem;
336
+ }
337
+
251
338
  /* === Unsupported Type === */
252
339
  .wq-unsupported-type {
253
340
  padding: var(--wq-space-lg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@welshare/questionnaire",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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
  },