@welshare/questionnaire 0.1.2 → 0.2.1

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 (106) hide show
  1. package/README.md +308 -30
  2. package/dist/esm/components/bmi-form.d.ts +68 -0
  3. package/dist/esm/components/bmi-form.d.ts.map +1 -0
  4. package/dist/esm/components/bmi-form.js +138 -0
  5. package/dist/esm/components/media-attachment.d.ts +21 -0
  6. package/dist/esm/components/media-attachment.d.ts.map +1 -0
  7. package/dist/esm/components/media-attachment.js +22 -0
  8. package/dist/esm/components/question-renderer.d.ts +6 -1
  9. package/dist/esm/components/question-renderer.d.ts.map +1 -1
  10. package/dist/esm/components/question-renderer.js +30 -14
  11. package/dist/esm/components/questions/boolean-question.d.ts +4 -4
  12. package/dist/esm/components/questions/boolean-question.d.ts.map +1 -1
  13. package/dist/esm/components/questions/boolean-question.js +6 -6
  14. package/dist/esm/components/questions/choice-question.d.ts +4 -4
  15. package/dist/esm/components/questions/choice-question.d.ts.map +1 -1
  16. package/dist/esm/components/questions/choice-question.js +19 -11
  17. package/dist/esm/components/questions/decimal-question.d.ts +8 -1
  18. package/dist/esm/components/questions/decimal-question.d.ts.map +1 -1
  19. package/dist/esm/components/questions/decimal-question.js +19 -1
  20. package/dist/esm/components/questions/multiple-choice-question.d.ts.map +1 -1
  21. package/dist/esm/components/questions/multiple-choice-question.js +21 -13
  22. package/dist/esm/contexts/questionnaire-context.d.ts.map +1 -1
  23. package/dist/esm/contexts/questionnaire-context.js +7 -24
  24. package/dist/esm/index.d.ts +7 -3
  25. package/dist/esm/index.d.ts.map +1 -1
  26. package/dist/esm/index.js +5 -1
  27. package/dist/esm/lib/bmi-helpers.d.ts +50 -0
  28. package/dist/esm/lib/bmi-helpers.d.ts.map +1 -0
  29. package/dist/esm/lib/bmi-helpers.js +69 -0
  30. package/dist/esm/lib/constants.d.ts +106 -0
  31. package/dist/esm/lib/constants.d.ts.map +1 -0
  32. package/dist/esm/lib/constants.js +105 -0
  33. package/dist/esm/lib/questionnaire-utils.d.ts +35 -1
  34. package/dist/esm/lib/questionnaire-utils.d.ts.map +1 -1
  35. package/dist/esm/lib/questionnaire-utils.js +111 -4
  36. package/dist/esm/types/fhir.d.ts +26 -5
  37. package/dist/esm/types/fhir.d.ts.map +1 -1
  38. package/dist/esm/types/fhir.js +3 -1
  39. package/dist/esm/types/index.d.ts +25 -0
  40. package/dist/esm/types/index.d.ts.map +1 -1
  41. package/dist/styles.css +108 -0
  42. package/package.json +17 -6
  43. package/dist/node_modules/@welshare/questionnaire/.tshy/build.json +0 -8
  44. package/dist/node_modules/@welshare/questionnaire/.tshy/esm.json +0 -16
  45. package/dist/node_modules/@welshare/questionnaire/LICENSE +0 -7
  46. package/dist/node_modules/@welshare/questionnaire/README.md +0 -173
  47. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.d.ts +0 -44
  48. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.d.ts.map +0 -1
  49. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.js +0 -28
  50. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.d.ts +0 -80
  51. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.d.ts.map +0 -1
  52. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.js +0 -183
  53. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.d.ts +0 -15
  54. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.d.ts.map +0 -1
  55. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.js +0 -19
  56. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.d.ts +0 -19
  57. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.d.ts.map +0 -1
  58. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.js +0 -23
  59. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.d.ts +0 -12
  60. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.d.ts.map +0 -1
  61. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.js +0 -7
  62. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.d.ts +0 -18
  63. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.d.ts.map +0 -1
  64. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.js +0 -24
  65. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.d.ts +0 -20
  66. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.d.ts.map +0 -1
  67. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.js +0 -39
  68. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.d.ts +0 -12
  69. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.d.ts.map +0 -1
  70. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.js +0 -7
  71. package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.d.ts +0 -41
  72. package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.d.ts.map +0 -1
  73. package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.js +0 -350
  74. package/dist/node_modules/@welshare/questionnaire/dist/esm/index.d.ts +0 -7
  75. package/dist/node_modules/@welshare/questionnaire/dist/esm/index.d.ts.map +0 -1
  76. package/dist/node_modules/@welshare/questionnaire/dist/esm/index.js +0 -6
  77. package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.d.ts +0 -33
  78. package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.d.ts.map +0 -1
  79. package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.js +0 -99
  80. package/dist/node_modules/@welshare/questionnaire/dist/esm/package.json +0 -3
  81. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.d.ts +0 -117
  82. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.d.ts.map +0 -1
  83. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.js +0 -3
  84. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.d.ts +0 -51
  85. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.d.ts.map +0 -1
  86. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.js +0 -1
  87. package/dist/node_modules/@welshare/questionnaire/dist/styles.css +0 -467
  88. package/dist/node_modules/@welshare/questionnaire/dist/tokens.css +0 -130
  89. package/dist/node_modules/@welshare/questionnaire/package.json +0 -85
  90. package/dist/node_modules/@welshare/questionnaire/src/components/debug-section.tsx +0 -116
  91. package/dist/node_modules/@welshare/questionnaire/src/components/question-renderer.tsx +0 -391
  92. package/dist/node_modules/@welshare/questionnaire/src/components/questionnaire-styles.css +0 -467
  93. package/dist/node_modules/@welshare/questionnaire/src/components/questionnaire-tokens.css +0 -130
  94. package/dist/node_modules/@welshare/questionnaire/src/components/questions/boolean-question.tsx +0 -72
  95. package/dist/node_modules/@welshare/questionnaire/src/components/questions/choice-question.tsx +0 -68
  96. package/dist/node_modules/@welshare/questionnaire/src/components/questions/decimal-question.tsx +0 -32
  97. package/dist/node_modules/@welshare/questionnaire/src/components/questions/integer-question.tsx +0 -87
  98. package/dist/node_modules/@welshare/questionnaire/src/components/questions/multiple-choice-question.tsx +0 -119
  99. package/dist/node_modules/@welshare/questionnaire/src/components/questions/string-question.tsx +0 -31
  100. package/dist/node_modules/@welshare/questionnaire/src/contexts/questionnaire-context.tsx +0 -499
  101. package/dist/node_modules/@welshare/questionnaire/src/index.ts +0 -41
  102. package/dist/node_modules/@welshare/questionnaire/src/lib/__tests__/questionnaire-utils.test.ts +0 -578
  103. package/dist/node_modules/@welshare/questionnaire/src/lib/questionnaire-utils.ts +0 -122
  104. package/dist/node_modules/@welshare/questionnaire/src/types/fhir.ts +0 -126
  105. package/dist/node_modules/@welshare/questionnaire/src/types/index.ts +0 -44
  106. package/dist/node_modules/@welshare/questionnaire/tsconfig.json +0 -16
package/README.md CHANGED
@@ -11,16 +11,16 @@ npm install @welshare/questionnaire
11
11
  ## Quick Start
12
12
 
13
13
  ```tsx
14
- import { useState, useEffect } from 'react';
14
+ import { useState, useEffect } from "react";
15
15
  import {
16
16
  QuestionnaireProvider,
17
17
  useQuestionnaire,
18
18
  QuestionRenderer,
19
19
  getVisiblePages,
20
20
  type Questionnaire,
21
- } from '@welshare/questionnaire';
22
- import '@welshare/questionnaire/tokens.css';
23
- import '@welshare/questionnaire/styles.css';
21
+ } from "@welshare/questionnaire";
22
+ import "@welshare/questionnaire/tokens.css";
23
+ import "@welshare/questionnaire/styles.css";
24
24
 
25
25
  function QuestionnairePage() {
26
26
  const { questionnaire, isPageValid } = useQuestionnaire();
@@ -45,11 +45,13 @@ function QuestionnairePage() {
45
45
  }
46
46
 
47
47
  function App() {
48
- const [questionnaire, setQuestionnaire] = useState<Questionnaire | null>(null);
48
+ const [questionnaire, setQuestionnaire] = useState<Questionnaire | null>(
49
+ null
50
+ );
49
51
 
50
52
  useEffect(() => {
51
- fetch('/api/questionnaire/your-id')
52
- .then(res => res.json())
53
+ fetch("/api/questionnaire/your-id")
54
+ .then((res) => res.json())
53
55
  .then(setQuestionnaire);
54
56
  }, []);
55
57
 
@@ -68,6 +70,7 @@ function App() {
68
70
  ### QuestionnaireProvider
69
71
 
70
72
  **Props:**
73
+
71
74
  - `questionnaire: Questionnaire` - FHIR Questionnaire object
72
75
  - `questionnaireId?: string` - Optional ID override (defaults to `questionnaire.id`)
73
76
  - `useNestedStructure?: boolean` - Nested or flat response structure (default: `true`)
@@ -76,18 +79,27 @@ function App() {
76
79
 
77
80
  ```tsx
78
81
  const {
79
- questionnaire, response,
80
- updateAnswer, updateMultipleAnswers,
81
- getAnswer, getAnswers,
82
- isPageValid, getRequiredQuestions, getUnansweredRequiredQuestions,
83
- markValidationErrors, clearValidationErrors, hasValidationError,
84
- debugMode, toggleDebugMode,
82
+ questionnaire,
83
+ response,
84
+ updateAnswer,
85
+ updateMultipleAnswers,
86
+ getAnswer,
87
+ getAnswers,
88
+ isPageValid,
89
+ getRequiredQuestions,
90
+ getUnansweredRequiredQuestions,
91
+ markValidationErrors,
92
+ clearValidationErrors,
93
+ hasValidationError,
94
+ debugMode,
95
+ toggleDebugMode,
85
96
  } = useQuestionnaire();
86
97
  ```
87
98
 
88
99
  ### QuestionRenderer
89
100
 
90
101
  **Props:**
102
+
91
103
  - `item: QuestionnaireItem` - Questionnaire item to render
92
104
  - `className?: string` - Container CSS classes
93
105
  - `inputClassName?: string` - Input CSS classes
@@ -97,12 +109,78 @@ const {
97
109
 
98
110
  **Supported Types:** `choice`, `boolean`, `integer`, `decimal`, `string`, `text`
99
111
 
112
+ ### BmiForm
113
+
114
+ A controlled component for collecting height, weight, and calculating BMI. The component acts as an input helper that manages BMI calculation internally and reports changes to the parent via callbacks. Unit system (metric/imperial) is managed internally and automatically clears all fields when switched.
115
+
116
+ **Props:**
117
+
118
+ - `height: number` - Current height value (0 when empty)
119
+ - `weight: number` - Current weight value (0 when empty)
120
+ - `bmi: number` - Current BMI value (calculated and set by the component, 0 when not calculated)
121
+ - `onHeightChange: (value: number, unit: "cm" | "in") => void` - Called when height changes, includes current unit
122
+ - `onWeightChange: (value: number, unit: "kg" | "lb") => void` - Called when weight changes, includes current unit
123
+ - `onBmiChange: (value: number) => void` - Called when BMI is calculated or cleared
124
+ - `className?: string` - Optional CSS classes
125
+
126
+ **Features:**
127
+
128
+ - Controlled numeric values (height, weight, bmi) managed by parent
129
+ - BMI calculation handled internally by the component
130
+ - Unit system managed internally (defaults to metric)
131
+ - Automatically clears all fields (sets to 0) when switching unit systems
132
+ - No unit conversion - values are cleared on unit system change
133
+ - Uses consistent styling with questionnaire components
134
+
135
+ **Example:**
136
+
137
+ ```tsx
138
+ import { useState } from "react";
139
+ import { BmiForm, getBmiCategory } from "@welshare/questionnaire";
140
+ import "@welshare/questionnaire/tokens.css";
141
+ import "@welshare/questionnaire/styles.css";
142
+
143
+ function MyComponent() {
144
+ const [height, setHeight] = useState(0);
145
+ const [weight, setWeight] = useState(0);
146
+ const [bmi, setBmi] = useState(0);
147
+
148
+ const handleBmiChange = (value: number) => {
149
+ setBmi(value);
150
+
151
+ // Optional: Get BMI category
152
+ if (value) {
153
+ const category = getBmiCategory(value);
154
+ console.log(`BMI Category: ${category}`);
155
+ }
156
+ };
157
+
158
+ return (
159
+ <BmiForm
160
+ height={height}
161
+ weight={weight}
162
+ bmi={bmi}
163
+ onHeightChange={(value, unit) => setHeight(value)}
164
+ onWeightChange={(value, unit) => setWeight(value)}
165
+ onBmiChange={handleBmiChange}
166
+ />
167
+ );
168
+ }
169
+ ```
170
+
100
171
  ### Utilities
101
172
 
173
+ **Questionnaire Utilities:**
174
+
102
175
  - `getVisiblePages(questionnaire)` - Get visible page groups
103
176
  - `calculateProgress(currentIndex, total)` - Calculate progress percentage
104
177
  - `getAllQuestionsFromPage(pageItem)` - Get all questions from a page
105
178
 
179
+ **BMI Helper Functions:**
180
+
181
+ - `calculateBmi(height, weight, unitSystem)` - Calculate BMI from height and weight
182
+ - `getBmiCategory(bmi)` - Get WHO BMI category (Underweight, Normal weight, Overweight, Obese)
183
+
106
184
  ## Theming
107
185
 
108
186
  Override CSS custom properties:
@@ -134,40 +212,240 @@ Override CSS custom properties:
134
212
  ## FHIR Extensions
135
213
 
136
214
  ### Hidden Questions
215
+
137
216
  ```json
138
217
  {
139
- "extension": [{
140
- "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden",
141
- "valueBoolean": true
142
- }]
218
+ "extension": [
219
+ {
220
+ "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden",
221
+ "valueBoolean": true
222
+ }
223
+ ]
224
+ }
225
+ ```
226
+
227
+ ### Media Attachments (Images)
228
+
229
+ Display images or other media content with question items using the FHIR SDC (Structured Data Capture) extensions.
230
+
231
+ **Item Media (images on questions):**
232
+
233
+ ```json
234
+ {
235
+ "linkId": "body-diagram",
236
+ "text": "Where is the pain located?",
237
+ "type": "choice",
238
+ "extension": [
239
+ {
240
+ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemMedia",
241
+ "valueAttachment": {
242
+ "contentType": "image/png",
243
+ "url": "https://example.com/images/body-diagram.png",
244
+ "title": "Body diagram"
245
+ }
246
+ }
247
+ ]
248
+ }
249
+ ```
250
+
251
+ **Answer Option Media (images on answer options):**
252
+
253
+ ```json
254
+ {
255
+ "linkId": "skin-condition",
256
+ "text": "Which image best matches your condition?",
257
+ "type": "choice",
258
+ "answerOption": [
259
+ {
260
+ "valueCoding": {
261
+ "code": "mild",
262
+ "display": "Mild"
263
+ },
264
+ "extension": [
265
+ {
266
+ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia",
267
+ "valueAttachment": {
268
+ "contentType": "image/jpeg",
269
+ "url": "https://example.com/images/mild.jpg",
270
+ "title": "Mild condition"
271
+ }
272
+ }
273
+ ]
274
+ },
275
+ {
276
+ "valueCoding": {
277
+ "code": "severe",
278
+ "display": "Severe"
279
+ },
280
+ "extension": [
281
+ {
282
+ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia",
283
+ "valueAttachment": {
284
+ "contentType": "image/jpeg",
285
+ "url": "https://example.com/images/severe.jpg",
286
+ "title": "Severe condition"
287
+ }
288
+ }
289
+ ]
290
+ }
291
+ ]
292
+ }
293
+ ```
294
+
295
+ **CSS Classes for Styling:**
296
+
297
+ - `.wq-question-media` - Container for question-level images
298
+ - `.wq-question-image` - Individual question image
299
+ - `.wq-choice-option-wrapper` - Wrapper around answer option with image
300
+ - `.wq-choice-option-image` - Individual answer option image
301
+
302
+ **Example Styling:**
303
+
304
+ ```css
305
+ .wq-question-image {
306
+ max-width: 100%;
307
+ height: auto;
308
+ margin-bottom: 1rem;
309
+ border-radius: 0.5rem;
310
+ }
311
+
312
+ .wq-choice-option-image {
313
+ max-width: 200px;
314
+ height: auto;
315
+ margin-bottom: 0.5rem;
316
+ border-radius: 0.25rem;
143
317
  }
144
318
  ```
145
319
 
146
320
  ### Slider Controls
321
+
147
322
  ```json
148
323
  {
149
- "extension": [{
150
- "url": "http://codes.welshare.app/StructureDefinition/questionnaire-slider-control",
151
- "extension": [
152
- { "url": "minValue", "valueInteger": 0 },
153
- { "url": "maxValue", "valueInteger": 100 },
154
- { "url": "step", "valueInteger": 1 },
155
- { "url": "unit", "valueString": "minutes" }
156
- ]
157
- }]
324
+ "extension": [
325
+ {
326
+ "url": "http://codes.welshare.app/StructureDefinition/questionnaire-slider-control",
327
+ "extension": [
328
+ { "url": "minValue", "valueInteger": 0 },
329
+ { "url": "maxValue", "valueInteger": 100 },
330
+ { "url": "step", "valueInteger": 1 },
331
+ { "url": "unit", "valueString": "minutes" }
332
+ ]
333
+ }
334
+ ]
158
335
  }
159
336
  ```
160
337
 
161
338
  ### Exclusive Options
339
+
340
+ ```json
341
+ {
342
+ "extension": [
343
+ {
344
+ "url": "http://codes.welshare.app/StructureDefinition/questionnaire-exclusive-option",
345
+ "valueString": "none-of-the-above-code"
346
+ }
347
+ ]
348
+ }
349
+ ```
350
+
351
+ ### Input Helpers
352
+
353
+ Input helpers allow you to provide auxiliary UI (like calculators or lookup dialogs) to assist users in filling out form fields. The library detects the extension and calls your `renderHelperTrigger` function, but you control the dialog/modal implementation.
354
+
355
+ **FHIR Extension:**
356
+
162
357
  ```json
163
358
  {
164
- "extension": [{
165
- "url": "http://codes.welshare.app/StructureDefinition/questionnaire-exclusive-option",
166
- "valueString": "none-of-the-above-code"
167
- }]
359
+ "linkId": "bmi",
360
+ "text": "Body Mass Index",
361
+ "type": "decimal",
362
+ "extension": [
363
+ {
364
+ "url": "http://codes.welshare.app/StructureDefinition/questionnaire-inputHelper",
365
+ "valueCodeableConcept": {
366
+ "coding": [
367
+ {
368
+ "system": "http://codes.welshare.app/input-helper-type",
369
+ "code": "bmi-calculator",
370
+ "display": "BMI Calculator"
371
+ }
372
+ ],
373
+ "text": "Calculate BMI from height and weight"
374
+ }
375
+ }
376
+ ]
168
377
  }
169
378
  ```
170
379
 
380
+ **Implementation:**
381
+
382
+ ```tsx
383
+ import { useState } from "react";
384
+ import {
385
+ QuestionRenderer,
386
+ BmiForm,
387
+ type HelperTriggerProps,
388
+ } from "@welshare/questionnaire";
389
+
390
+ function MyQuestionnaireRenderer({ item }) {
391
+ const [showBmiDialog, setShowBmiDialog] = useState(false);
392
+ const [helperCallback, setHelperCallback] = useState<
393
+ ((v: string | number) => void) | null
394
+ >(null);
395
+
396
+ const handleHelperTrigger = ({
397
+ helper,
398
+ onValueSelected,
399
+ }: HelperTriggerProps) => {
400
+ if (helper.type === "bmi-calculator") {
401
+ return (
402
+ <button
403
+ type="button"
404
+ onClick={() => {
405
+ setHelperCallback(() => onValueSelected);
406
+ setShowBmiDialog(true);
407
+ }}
408
+ >
409
+ {helper.display || "Calculate"}
410
+ </button>
411
+ );
412
+ }
413
+ return null;
414
+ };
415
+
416
+ return (
417
+ <>
418
+ <QuestionRenderer item={item} renderHelperTrigger={handleHelperTrigger} />
419
+
420
+ {showBmiDialog && (
421
+ <Dialog onClose={() => setShowBmiDialog(false)}>
422
+ <BmiForm
423
+ onSubmit={({ bmi }) => {
424
+ helperCallback?.(bmi);
425
+ setShowBmiDialog(false);
426
+ }}
427
+ />
428
+ </Dialog>
429
+ )}
430
+ </>
431
+ );
432
+ }
433
+ ```
434
+
435
+ **Helper Trigger Props:**
436
+
437
+ - `helper: InputHelperConfig` - Configuration from the extension
438
+ - `type: string` - Helper identifier (e.g., "bmi-calculator")
439
+ - `display?: string` - Display name from the extension
440
+ - `description?: string` - Description/tooltip text
441
+ - `linkId: string` - The question's linkId
442
+ - `currentValue?: T` - Current field value (if any), where T defaults to `string | number`
443
+ - `onValueSelected: (value: T) => void` - Callback to update the field
444
+
445
+ **Note:** `HelperTriggerProps<T>` is a generic interface. For decimal questions, it's specialized as `HelperTriggerProps<number>`, but you can use different types for other question types (e.g., `HelperTriggerProps<string>` for text inputs).
446
+
447
+ **Note:** The library only renders the trigger element you provide. You are responsible for implementing the dialog/modal UI, as this allows you to use your preferred dialog system (e.g., shadcn/ui, Radix UI, MUI, etc.).
448
+
171
449
  ## License
172
450
 
173
451
  MIT © Welshare UG (haftungsbeschränkt)
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Result object returned on submission - all values in metric units
3
+ */
4
+ export interface BmiSubmissionResult {
5
+ /** Height in centimeters */
6
+ heightCm: number;
7
+ /** Weight in kilograms */
8
+ weightKg: number;
9
+ /** Calculated BMI (kg/m²) */
10
+ bmi: number;
11
+ }
12
+ /**
13
+ * Initial values for the BMI form - always in metric units
14
+ */
15
+ export interface BmiInitialValues {
16
+ /** Height in centimeters */
17
+ heightCm?: number;
18
+ /** Weight in kilograms */
19
+ weightKg?: number;
20
+ }
21
+ export interface BmiFormProps {
22
+ /**
23
+ * Initial values in metric units (cm, kg).
24
+ * These are used to populate the form on mount.
25
+ * If unitPreference is "imperial", they will be converted for display.
26
+ */
27
+ initialValues?: BmiInitialValues;
28
+ /**
29
+ * Preferred unit system for initial display.
30
+ * When "imperial", initial metric values are converted for display.
31
+ * Defaults to "metric".
32
+ */
33
+ unitPreference?: "metric" | "imperial";
34
+ /**
35
+ * Callback fired when user submits/confirms the BMI values.
36
+ * Returns all values in metric units (cm, kg).
37
+ */
38
+ onSubmit?: (result: BmiSubmissionResult) => void;
39
+ /**
40
+ * Callback fired whenever any value changes.
41
+ * Returns all values in metric units (cm, kg).
42
+ * Use this for real-time updates without requiring explicit submission.
43
+ */
44
+ onChange?: (result: BmiSubmissionResult) => void;
45
+ /**
46
+ * Additional CSS class names
47
+ */
48
+ className?: string;
49
+ }
50
+ /**
51
+ * BMI Form Component
52
+ *
53
+ * A self-contained form component for entering height and weight measurements.
54
+ * Manages all state internally and supports both metric (cm/kg) and imperial (ft+in/lb) input.
55
+ *
56
+ * **Clinical Standard Compliance:**
57
+ * - All external communication uses metric units (cm, kg)
58
+ * - Imperial inputs are UI conveniences that convert to metric internally
59
+ * - Initial values must always be provided in metric
60
+ * - BMI is always calculated using the metric formula: kg / m²
61
+ *
62
+ * **Unit System Behavior:**
63
+ * - Switching unit systems clears all input fields (no automatic conversion)
64
+ * - Initial values provided in metric are converted to imperial for display
65
+ * only when unitPreference is "imperial"
66
+ */
67
+ export declare const BmiForm: ({ initialValues, unitPreference, onSubmit, onChange, className, }: BmiFormProps) => import("react/jsx-runtime").JSX.Element;
68
+ //# sourceMappingURL=bmi-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bmi-form.d.ts","sourceRoot":"","sources":["../../../src/components/bmi-form.tsx"],"names":[],"mappings":"AASA;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4BAA4B;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,gBAAgB,CAAC;IAEjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IAEvC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAEjD;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAEjD;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,OAAO,GAAI,mEAMrB,YAAY,4CA8Sd,CAAC"}
@@ -0,0 +1,138 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect, useCallback, useRef } from "react";
3
+ import { calculateBmi, cmToFeetAndInches, feetAndInchesToCm, kgToLbs, lbsToKg, } from "../lib/bmi-helpers.js";
4
+ /**
5
+ * BMI Form Component
6
+ *
7
+ * A self-contained form component for entering height and weight measurements.
8
+ * Manages all state internally and supports both metric (cm/kg) and imperial (ft+in/lb) input.
9
+ *
10
+ * **Clinical Standard Compliance:**
11
+ * - All external communication uses metric units (cm, kg)
12
+ * - Imperial inputs are UI conveniences that convert to metric internally
13
+ * - Initial values must always be provided in metric
14
+ * - BMI is always calculated using the metric formula: kg / m²
15
+ *
16
+ * **Unit System Behavior:**
17
+ * - Switching unit systems clears all input fields (no automatic conversion)
18
+ * - Initial values provided in metric are converted to imperial for display
19
+ * only when unitPreference is "imperial"
20
+ */
21
+ export const BmiForm = ({ initialValues, unitPreference = "metric", onSubmit, onChange, className = "", }) => {
22
+ // Track if this is the initial render to handle initial value conversion
23
+ const isInitialRender = useRef(true);
24
+ // Unit system state - starts with preference
25
+ const [unitSystem, setUnitSystem] = useState(unitPreference);
26
+ // Internal state for metric display (cm)
27
+ const [heightCmInput, setHeightCmInput] = useState("");
28
+ // Internal state for imperial height display (ft + in)
29
+ const [feetInput, setFeetInput] = useState("");
30
+ const [inchesInput, setInchesInput] = useState("");
31
+ // Internal state for metric weight display (kg)
32
+ const [weightKgInput, setWeightKgInput] = useState("");
33
+ // Internal state for imperial weight display (lb)
34
+ const [weightLbInput, setWeightLbInput] = useState("");
35
+ // The canonical metric values (source of truth for calculations)
36
+ const [heightCm, setHeightCm] = useState(0);
37
+ const [weightKg, setWeightKg] = useState(0);
38
+ // Calculated BMI (always from metric values)
39
+ const [bmi, setBmi] = useState(0);
40
+ // Initialize form with provided values (converted to display units if needed)
41
+ useEffect(() => {
42
+ if (!isInitialRender.current)
43
+ return;
44
+ isInitialRender.current = false;
45
+ const initialHeightCm = initialValues?.heightCm ?? 0;
46
+ const initialWeightKg = initialValues?.weightKg ?? 0;
47
+ // Set canonical metric values
48
+ setHeightCm(initialHeightCm);
49
+ setWeightKg(initialWeightKg);
50
+ if (unitPreference === "metric") {
51
+ // Display as metric
52
+ if (initialHeightCm > 0) {
53
+ setHeightCmInput(String(initialHeightCm));
54
+ }
55
+ if (initialWeightKg > 0) {
56
+ setWeightKgInput(String(initialWeightKg));
57
+ }
58
+ }
59
+ else {
60
+ // Convert metric to imperial for display
61
+ if (initialHeightCm > 0) {
62
+ const { feet, inches } = cmToFeetAndInches(initialHeightCm);
63
+ setFeetInput(String(feet));
64
+ setInchesInput(String(inches));
65
+ }
66
+ if (initialWeightKg > 0) {
67
+ const lbs = Math.round(kgToLbs(initialWeightKg) * 10) / 10;
68
+ setWeightLbInput(String(lbs));
69
+ }
70
+ }
71
+ }, [initialValues, unitPreference]);
72
+ // Calculate BMI whenever canonical metric values change
73
+ useEffect(() => {
74
+ const calculatedBmi = calculateBmi(heightCm, weightKg);
75
+ setBmi(calculatedBmi);
76
+ }, [heightCm, weightKg]);
77
+ // Notify parent of changes
78
+ useEffect(() => {
79
+ if (onChange && !isInitialRender.current) {
80
+ onChange({ heightCm, weightKg, bmi });
81
+ }
82
+ }, [heightCm, weightKg, bmi, onChange]);
83
+ // Handle unit system change - clear all fields
84
+ const handleUnitSystemChange = useCallback((newSystem) => {
85
+ if (newSystem === unitSystem)
86
+ return;
87
+ // Clear all inputs
88
+ setHeightCmInput("");
89
+ setFeetInput("");
90
+ setInchesInput("");
91
+ setWeightKgInput("");
92
+ setWeightLbInput("");
93
+ // Clear canonical values
94
+ setHeightCm(0);
95
+ setWeightKg(0);
96
+ setBmi(0);
97
+ setUnitSystem(newSystem);
98
+ }, [unitSystem]);
99
+ // Metric height change (cm)
100
+ const handleHeightCmChange = useCallback((value) => {
101
+ setHeightCmInput(value);
102
+ const numValue = parseFloat(value);
103
+ setHeightCm(isNaN(numValue) ? 0 : numValue);
104
+ }, []);
105
+ // Imperial height change (feet)
106
+ const handleFeetChange = useCallback((value) => {
107
+ setFeetInput(value);
108
+ const feet = parseFloat(value) || 0;
109
+ const inches = parseFloat(inchesInput) || 0;
110
+ setHeightCm(feetAndInchesToCm(feet, inches));
111
+ }, [inchesInput]);
112
+ // Imperial height change (inches)
113
+ const handleInchesChange = useCallback((value) => {
114
+ setInchesInput(value);
115
+ const feet = parseFloat(feetInput) || 0;
116
+ const inches = parseFloat(value) || 0;
117
+ setHeightCm(feetAndInchesToCm(feet, inches));
118
+ }, [feetInput]);
119
+ // Metric weight change (kg)
120
+ const handleWeightKgChange = useCallback((value) => {
121
+ setWeightKgInput(value);
122
+ const numValue = parseFloat(value);
123
+ setWeightKg(isNaN(numValue) ? 0 : numValue);
124
+ }, []);
125
+ // Imperial weight change (lb)
126
+ const handleWeightLbChange = useCallback((value) => {
127
+ setWeightLbInput(value);
128
+ const numValue = parseFloat(value);
129
+ setWeightKg(isNaN(numValue) ? 0 : lbsToKg(numValue));
130
+ }, []);
131
+ // Handle form submission
132
+ const handleSubmit = useCallback(() => {
133
+ if (onSubmit) {
134
+ onSubmit({ heightCm, weightKg, bmi });
135
+ }
136
+ }, [onSubmit, heightCm, weightKg, bmi]);
137
+ return (_jsxs("div", { className: `wq-bmi-form ${className}`, children: [_jsxs("div", { className: "wq-bmi-unit-toggle", children: [_jsx("button", { type: "button", className: `wq-bmi-unit-option ${unitSystem === "metric" ? "wq-selected" : ""}`, onClick: () => handleUnitSystemChange("metric"), "aria-pressed": unitSystem === "metric", children: "Metric" }), _jsx("button", { type: "button", className: `wq-bmi-unit-option ${unitSystem === "imperial" ? "wq-selected" : ""}`, onClick: () => handleUnitSystemChange("imperial"), "aria-pressed": unitSystem === "imperial", children: "Imperial" })] }), _jsxs("div", { className: "wq-bmi-field", children: [_jsx("label", { htmlFor: "bmi-height", className: "wq-bmi-label", children: unitSystem === "metric" ? "Height (cm)" : "Height (ft + in)" }), unitSystem === "metric" ? (_jsxs("div", { className: "wq-bmi-input-group", children: [_jsx("input", { id: "bmi-height", type: "number", className: "wq-question-input", value: heightCmInput, onChange: (e) => handleHeightCmChange(e.target.value), placeholder: "Enter height in cm", step: "0.1", min: "0" }), _jsx("span", { className: "wq-bmi-unit", children: "cm" })] })) : (_jsxs("div", { className: "wq-bmi-imperial-height", children: [_jsxs("div", { className: "wq-bmi-input-group", children: [_jsx("input", { id: "bmi-height-feet", type: "number", className: "wq-question-input", value: feetInput, onChange: (e) => handleFeetChange(e.target.value), placeholder: "Feet", step: "1", min: "0" }), _jsx("span", { className: "wq-bmi-unit", children: "ft" })] }), _jsxs("div", { className: "wq-bmi-input-group", children: [_jsx("input", { id: "bmi-height-inches", type: "number", className: "wq-question-input", value: inchesInput, onChange: (e) => handleInchesChange(e.target.value), placeholder: "Inches", step: "0.1", min: "0", max: "11.9" }), _jsx("span", { className: "wq-bmi-unit", children: "in" })] })] }))] }), _jsxs("div", { className: "wq-bmi-field", children: [_jsx("label", { htmlFor: "bmi-weight", className: "wq-bmi-label", children: unitSystem === "metric" ? "Weight (kg)" : "Weight (lbs)" }), _jsx("div", { className: "wq-bmi-input-group", children: unitSystem === "metric" ? (_jsxs(_Fragment, { children: [_jsx("input", { id: "bmi-weight", type: "number", className: "wq-question-input", value: weightKgInput, onChange: (e) => handleWeightKgChange(e.target.value), placeholder: "Enter weight in kg", step: "0.1", min: "0" }), _jsx("span", { className: "wq-bmi-unit", children: "kg" })] })) : (_jsxs(_Fragment, { children: [_jsx("input", { id: "bmi-weight", type: "number", className: "wq-question-input", value: weightLbInput, onChange: (e) => handleWeightLbChange(e.target.value), placeholder: "Enter weight in lbs", step: "0.1", min: "0" }), _jsx("span", { className: "wq-bmi-unit", children: "lbs" })] })) })] }), _jsxs("div", { className: "wq-bmi-field", children: [_jsx("label", { htmlFor: "bmi-result", className: "wq-bmi-label", children: "BMI" }), _jsxs("div", { className: "wq-bmi-input-group", children: [_jsx("input", { id: "bmi-result", type: "text", className: "wq-question-input wq-bmi-result", value: bmi ? bmi.toFixed(1) : "", placeholder: "Calculated automatically", readOnly: true, disabled: true }), _jsx("span", { className: "wq-bmi-unit", children: "kg/m\u00B2" })] })] }), bmi > 0 && (_jsx("div", { className: "wq-bmi-info", children: _jsx("p", { className: "wq-text-secondary", children: "BMI is calculated automatically from height and weight measurements." }) })), onSubmit && (_jsx("div", { className: "wq-bmi-actions", children: _jsx("button", { type: "button", className: "wq-button wq-button-primary", onClick: handleSubmit, disabled: !bmi, children: "Confirm Measurements" }) }))] }));
138
+ };
@@ -0,0 +1,21 @@
1
+ import type { Attachment } from "../types/fhir.js";
2
+ export interface MediaAttachmentProps {
3
+ attachment: Attachment;
4
+ alt: string;
5
+ className?: string;
6
+ }
7
+ /**
8
+ * Renders a single media attachment (image) if it has a URL and is an image type
9
+ */
10
+ export declare const MediaAttachment: ({ attachment, alt, className, }: MediaAttachmentProps) => import("react/jsx-runtime").JSX.Element | null;
11
+ export interface MediaAttachmentsProps {
12
+ attachments: Attachment[];
13
+ baseAlt?: string;
14
+ className?: string;
15
+ containerClassName?: string;
16
+ }
17
+ /**
18
+ * Renders multiple media attachments in a container
19
+ */
20
+ export declare const MediaAttachments: ({ attachments, baseAlt, className, containerClassName, }: MediaAttachmentsProps) => import("react/jsx-runtime").JSX.Element | null;
21
+ //# sourceMappingURL=media-attachment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-attachment.d.ts","sourceRoot":"","sources":["../../../src/components/media-attachment.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,iCAI7B,oBAAoB,mDAkBtB,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,GAAI,0DAK9B,qBAAqB,mDAevB,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Renders a single media attachment (image) if it has a URL and is an image type
4
+ */
5
+ export const MediaAttachment = ({ attachment, alt, className = "", }) => {
6
+ // Only render if URL exists
7
+ if (!attachment.url)
8
+ return null;
9
+ // Check if it's an image (or assume image if no contentType)
10
+ const isImage = !attachment.contentType || attachment.contentType.startsWith("image/");
11
+ if (!isImage)
12
+ return null;
13
+ return (_jsx("img", { src: attachment.url, alt: alt, className: className, title: attachment.title }));
14
+ };
15
+ /**
16
+ * Renders multiple media attachments in a container
17
+ */
18
+ export const MediaAttachments = ({ attachments, baseAlt = "Image", className = "", containerClassName = "", }) => {
19
+ if (attachments.length === 0)
20
+ return null;
21
+ return (_jsx("div", { className: containerClassName, children: attachments.map((attachment, index) => (_jsx(MediaAttachment, { attachment: attachment, alt: attachment.title || `${baseAlt} ${index + 1}`, className: className }, index))) }));
22
+ };