@welshare/questionnaire 0.2.6 → 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.
package/README.md CHANGED
@@ -104,6 +104,7 @@ 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
 
@@ -393,6 +394,112 @@ Override CSS custom properties:
393
394
  }
394
395
  ```
395
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
+
396
503
  ### Custom Input Renderers
397
504
 
398
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;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"}
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"}
@@ -39,7 +39,7 @@ export const QuestionRenderer = (rendererProps) => {
39
39
  * - Auto-populated (hidden) fields
40
40
  * - Validation error display
41
41
  */
42
- const QuestionRendererInternal = ({ item, className = "", inputClassName = "", choiceClassName = "", renderRadioInput, renderCheckboxInput, renderHelperTrigger, }) => {
42
+ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", choiceClassName = "", choiceLayout = "stacked", renderRadioInput, renderCheckboxInput, renderHelperTrigger, }) => {
43
43
  const { updateAnswer, updateMultipleAnswers, getAnswer, getAnswers, hasValidationError, } = useQuestionnaire();
44
44
  const currentAnswer = getAnswer(item.linkId);
45
45
  const currentAnswers = getAnswers(item.linkId);
@@ -233,10 +233,10 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
233
233
  : item;
234
234
  // Multi-select with checkboxes when repeats is true
235
235
  if (item.repeats) {
236
- return (_jsx(MultipleChoiceQuestion, { item: normalizedItem, currentAnswers: currentAnswers, onMultipleChoiceToggle: handleMultipleChoiceToggle, onOtherTextChange: handleOtherTextChange, choiceClassName: choiceClassName, inputClassName: inputClassName, renderCheckboxInput: renderCheckboxInput }));
236
+ return (_jsx(MultipleChoiceQuestion, { item: normalizedItem, currentAnswers: currentAnswers, onMultipleChoiceToggle: handleMultipleChoiceToggle, onOtherTextChange: handleOtherTextChange, choiceClassName: choiceClassName, choiceLayout: choiceLayout, inputClassName: inputClassName, renderCheckboxInput: renderCheckboxInput }));
237
237
  }
238
238
  // Single-select with radio buttons (default)
239
- return (_jsx(ChoiceQuestion, { item: normalizedItem, currentAnswer: currentAnswer, onChoiceChange: handleChoiceChange, onOtherTextChange: handleOtherTextChange, choiceClassName: choiceClassName, inputClassName: inputClassName, renderRadioInput: renderRadioInput }));
239
+ return (_jsx(ChoiceQuestion, { item: normalizedItem, currentAnswer: currentAnswer, onChoiceChange: handleChoiceChange, onOtherTextChange: handleOtherTextChange, choiceClassName: choiceClassName, choiceLayout: choiceLayout, inputClassName: inputClassName, renderRadioInput: renderRadioInput }));
240
240
  }
241
241
  case "integer":
242
242
  return (_jsx(IntegerQuestion, { item: item, currentAnswer: currentAnswer, onIntegerChange: handleIntegerChange, inputClassName: inputClassName, sliderConfig: sliderConfig }));
@@ -248,7 +248,7 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
248
248
  case "text":
249
249
  return (_jsx(StringQuestion, { item: item, currentAnswer: currentAnswer, onStringChange: handleStringChange, inputClassName: inputClassName }));
250
250
  case "boolean":
251
- 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 }));
252
252
  default:
253
253
  return (_jsxs("div", { className: "wq-unsupported-type", children: ["Unsupported question type: ", item.type] }));
254
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,6 +10,7 @@ export interface ChoiceQuestionProps {
10
10
  display?: string;
11
11
  }, valueInteger?: number) => void;
12
12
  choiceClassName?: string;
13
+ choiceLayout?: ChoiceLayout;
13
14
  inputClassName?: string;
14
15
  renderRadioInput?: (props: RadioInputProps) => ReactNode;
15
16
  onOtherTextChange?: (value: string) => void;
@@ -21,5 +22,5 @@ export interface ChoiceQuestionProps {
21
22
  * "Other" text field alongside coded options. Single-select means selecting
22
23
  * a coded option or entering text is mutually exclusive (per FHIR spec).
23
24
  */
24
- export declare const ChoiceQuestion: ({ item, currentAnswer, onChoiceChange, choiceClassName, inputClassName, renderRadioInput, onOtherTextChange, }: 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;
25
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;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
+ {"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"}
@@ -9,11 +9,11 @@ import { OtherTextInput } from "./other-text-input.js";
9
9
  * "Other" text field alongside coded options. Single-select means selecting
10
10
  * a coded option or entering text is mutually exclusive (per FHIR spec).
11
11
  */
12
- export const ChoiceQuestion = ({ item, currentAnswer, onChoiceChange, choiceClassName = "", inputClassName = "", renderRadioInput, onOtherTextChange, }) => {
12
+ export const ChoiceQuestion = ({ item, currentAnswer, onChoiceChange, choiceClassName = "", choiceLayout = "stacked", inputClassName = "", renderRadioInput, onOtherTextChange, }) => {
13
13
  // Check if this item allows free-text "other" answers (FHIR R5)
14
14
  // https://hl7.org/fhir/valueset-questionnaire-answer-constraint.html#expansion
15
15
  const allowsOtherText = item.answerConstraint === "optionsOrString";
16
- return (_jsxs("div", { className: `wq-question-choice ${choiceClassName}`, children: [item.answerOption?.map((option, index) => {
16
+ return (_jsxs("div", { className: `wq-question-choice wq-choice-layout-${choiceLayout} ${choiceClassName}`, children: [item.answerOption?.map((option, index) => {
17
17
  const isSelected = currentAnswer?.valueCoding?.code === option.valueCoding?.code;
18
18
  // Get media attachment for this answer option
19
19
  const mediaAttachment = getAnswerOptionMedia(option);
@@ -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,6 +10,7 @@ export interface MultipleChoiceQuestionProps {
10
10
  display?: string;
11
11
  }, valueInteger?: number) => void;
12
12
  choiceClassName?: string;
13
+ choiceLayout?: ChoiceLayout;
13
14
  inputClassName?: string;
14
15
  renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode;
15
16
  onOtherTextChange?: (value: string) => void;
@@ -22,5 +23,5 @@ export interface MultipleChoiceQuestionProps {
22
23
  * "Other" text field alongside coded options. Multi-select means both
23
24
  * coded options and free-text can coexist in the answers array.
24
25
  */
25
- export declare const MultipleChoiceQuestion: ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName, inputClassName, renderCheckboxInput, onOtherTextChange, }: 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;
26
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;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"}
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"}
@@ -11,7 +11,7 @@ import { OtherTextInput } from "./other-text-input.js";
11
11
  * "Other" text field alongside coded options. Multi-select means both
12
12
  * coded options and free-text can coexist in the answers array.
13
13
  */
14
- export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName = "", inputClassName = "", renderCheckboxInput, onOtherTextChange, }) => {
14
+ export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName = "", choiceLayout = "stacked", inputClassName = "", renderCheckboxInput, onOtherTextChange, }) => {
15
15
  // Check if this item allows free-text "other" answers (FHIR R5)
16
16
  // https://hl7.org/fhir/valueset-questionnaire-answer-constraint.html#expansion
17
17
  const allowsOtherText = item.answerConstraint === "optionsOrString";
@@ -33,7 +33,7 @@ export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceT
33
33
  const optionsToShow = exclusiveSelected
34
34
  ? item.answerOption?.filter((opt) => opt.valueCoding?.code === exclusiveOptionCode)
35
35
  : item.answerOption;
36
- 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) => {
37
37
  const isSelected = currentAnswers.some((answer) => answer.valueCoding?.code === option.valueCoding?.code);
38
38
  const isDisabled = !isSelected && atMaxAnswers;
39
39
  // Get media attachment for this answer option
@@ -8,7 +8,7 @@ export { LegalConsentForm } from "./components/legal-consent-form.js";
8
8
  export type { LegalConsentFormProps, LegalConsentResult, LegalCheckboxProps, LegalDocumentLinks, } from "./components/legal-consent-form.js";
9
9
  export { QuestionnaireProvider, useQuestionnaire, type QuestionnaireContextType, type QuestionnaireProviderProps, } from "./contexts/questionnaire-context.js";
10
10
  export type { Attachment, Coding, Extension, Questionnaire, QuestionnaireItem, QuestionnaireItemAnswerOption, QuestionnaireResponse, QuestionnaireResponseAnswer, QuestionnaireResponseItem, } from "./types/fhir.js";
11
- export type { RadioInputProps, CheckboxInputProps, InputHelperConfig, HelperTriggerProps, } from "./types/index.js";
11
+ export type { ChoiceLayout, RadioInputProps, CheckboxInputProps, InputHelperConfig, HelperTriggerProps, } from "./types/index.js";
12
12
  export { calculateProgress, findQuestionnaireItem, getAllQuestionsFromPage, getAnswerOptionMedia, getExclusiveOptionCode, getInputHelper, getItemMedia, getVisiblePages, hasAnswerValue, isQuestionHidden, } from "./lib/questionnaire-utils.js";
13
13
  export { calculateBmi } from "./lib/bmi-helpers.js";
14
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,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"}
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"}
@@ -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);
@@ -402,6 +426,12 @@
402
426
  .wq-slider-value-display {
403
427
  font-size: var(--wq-font-size-lg);
404
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
+ }
405
435
  }
406
436
 
407
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.6",
3
+ "version": "0.2.7",
4
4
  "description": "FHIR Questionnaire components for React with state management and validation",
5
5
  "keywords": [
6
6
  "react",