@bookinglab/booking-ui-react 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 BookingLab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # @bookinglab/booking-ui-react
2
+
3
+ React UI components for BookingLab booking journeys. Build dynamic, accessible booking forms with conditional logic, validation, and full styling customization.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bookinglab/booking-ui-react
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```tsx
14
+ import { BookingForm, Question } from '@bookinglab/booking-ui-react';
15
+
16
+ const questions: Question[] = [
17
+ { id: 1, name: 'Personal Details', detail_type: 'heading' },
18
+ { id: 2, name: 'Full Name', detail_type: 'text_field', required: true },
19
+ { id: 3, name: 'Email', detail_type: 'text_field', required: true },
20
+ ];
21
+
22
+ function App() {
23
+ const handleSubmit = (values) => {
24
+ console.log('Form submitted:', values);
25
+ };
26
+
27
+ return (
28
+ <BookingForm
29
+ questions={questions}
30
+ onSubmit={handleSubmit}
31
+ submitLabel="Book Now"
32
+ />
33
+ );
34
+ }
35
+ ```
36
+
37
+ ## Components
38
+
39
+ ### BookingForm
40
+
41
+ A dynamic form component that renders form fields based on an array of `Question` objects.
42
+
43
+ #### Props
44
+
45
+ | Prop | Type | Default | Description |
46
+ |------|------|---------|-------------|
47
+ | `questions` | `Question[]` | required | Array of question objects defining the form fields |
48
+ | `onSubmit` | `(values: FormValues) => void` | required | Callback fired when form is submitted with valid values |
49
+ | `submitLabel` | `string` | `"Submit"` | Text for the submit button |
50
+ | `className` | `string` | `""` | Class name for the form element |
51
+ | `classNames` | `BookingFormClassNames` | `undefined` | Custom class names for styling individual elements |
52
+
53
+ #### Question Types
54
+
55
+ The `detail_type` property determines how each question is rendered:
56
+
57
+ | Type | Description | Renders |
58
+ |------|-------------|---------|
59
+ | `heading` | Section heading | `<h3>` element |
60
+ | `text_field` | Single-line text | `<input type="text">` |
61
+ | `text_area` | Multi-line text | `<textarea>` |
62
+ | `select` | Dropdown selection | `<select>` with options |
63
+ | `date` | Date picker | `<input type="date">` |
64
+ | `number` | Numeric input | `<input type="number">` |
65
+ | `check` | Checkbox | `<input type="checkbox">` |
66
+
67
+ #### Question Interface
68
+
69
+ ```typescript
70
+ interface Question {
71
+ id: number;
72
+ name: string;
73
+ detail_type: 'heading' | 'text_field' | 'text_area' | 'select' | 'date' | 'number' | 'check';
74
+ required?: boolean;
75
+ help_text?: string;
76
+ options?: QuestionOption[]; // For select type
77
+ settings?: QuestionSettings;
78
+ }
79
+
80
+ interface QuestionOption {
81
+ id: number;
82
+ name: string;
83
+ price?: number;
84
+ is_default?: boolean;
85
+ }
86
+
87
+ interface QuestionSettings {
88
+ conditional_question?: string;
89
+ conditional_answers?: Record<string, string>;
90
+ min?: number;
91
+ max?: number;
92
+ placeholder?: string;
93
+ }
94
+ ```
95
+
96
+ ## Conditional Questions
97
+
98
+ Show or hide questions based on answers to other questions using `conditional_answers`:
99
+
100
+ ```tsx
101
+ const questions: Question[] = [
102
+ {
103
+ id: 1,
104
+ name: 'Service Type',
105
+ detail_type: 'select',
106
+ required: true,
107
+ options: [
108
+ { id: 1, name: 'Consultation' },
109
+ { id: 2, name: 'Follow-up' },
110
+ ],
111
+ },
112
+ {
113
+ id: 2,
114
+ name: 'First time visiting?',
115
+ detail_type: 'check',
116
+ settings: {
117
+ // Only show when "Consultation" (option id: 1) is selected
118
+ conditional_answers: { '1': '1' },
119
+ },
120
+ },
121
+ ];
122
+ ```
123
+
124
+ ## Styling
125
+
126
+ ### Custom Class Names
127
+
128
+ Override default styles using the `classNames` prop:
129
+
130
+ ```tsx
131
+ <BookingForm
132
+ questions={questions}
133
+ onSubmit={handleSubmit}
134
+ classNames={{
135
+ fieldWrapper: 'mb-6',
136
+ label: 'text-white font-bold',
137
+ heading: 'text-2xl text-primary',
138
+ input: 'border-2 border-gray-400 rounded-lg p-3',
139
+ inputError: 'border-red-500',
140
+ checkbox: 'w-5 h-5',
141
+ helpText: 'text-gray-400 text-sm',
142
+ errorText: 'text-red-400',
143
+ button: 'bg-primary text-white py-3 px-6 rounded-lg',
144
+ }}
145
+ />
146
+ ```
147
+
148
+ ### Available Class Name Keys
149
+
150
+ | Key | Description |
151
+ |-----|-------------|
152
+ | `fieldWrapper` | Container for each form field |
153
+ | `label` | All label elements |
154
+ | `heading` | Heading elements (`detail_type: 'heading'`) |
155
+ | `input` | All input elements (text, textarea, select, date, number) |
156
+ | `inputError` | Additional classes for inputs in error state |
157
+ | `checkbox` | Checkbox input specifically |
158
+ | `helpText` | Help text below fields |
159
+ | `errorText` | Error message text |
160
+ | `button` | Submit button |
161
+
162
+ ## Form Values
163
+
164
+ The `onSubmit` callback receives a `FormValues` object mapping question IDs to their values:
165
+
166
+ ```typescript
167
+ type FormValues = Record<number, string | number | boolean>;
168
+
169
+ // Example output:
170
+ {
171
+ 2: "John Doe", // text_field
172
+ 3: "john@example.com", // text_field
173
+ 4: 1, // select (option id)
174
+ 5: true, // check
175
+ }
176
+ ```
177
+
178
+ ## Validation
179
+
180
+ - Required fields are automatically validated
181
+ - Number fields respect `min` and `max` settings
182
+ - Error messages display after field is touched
183
+ - Hidden conditional fields are excluded from validation
184
+
185
+ ## Requirements
186
+
187
+ - React 17.0.0 or higher
188
+ - React DOM 17.0.0 or higher
189
+
190
+ ## TypeScript
191
+
192
+ All types are exported for TypeScript users:
193
+
194
+ ```typescript
195
+ import type {
196
+ Question,
197
+ QuestionOption,
198
+ QuestionSettings,
199
+ FormValues,
200
+ FormErrors,
201
+ BookingFormProps,
202
+ BookingFormClassNames,
203
+ } from '@bookinglab/booking-ui-react';
204
+ ```
205
+
206
+ ## License
207
+
208
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,360 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/components/BookingForm.tsx
7
+ var cx = (...classes) => classes.filter(Boolean).join(" ");
8
+ function FormField({
9
+ question,
10
+ value,
11
+ error,
12
+ onChange,
13
+ classNames
14
+ }) {
15
+ const inputId = `question-${question.id}`;
16
+ const errorId = `${inputId}-error`;
17
+ const hasError = !!error;
18
+ const defaultFieldWrapper = "mb-4";
19
+ const defaultLabel = "block text-sm font-medium mb-1 text-gray-700";
20
+ const defaultHeading = "text-lg font-semibold text-gray-900 mt-4 mb-2 first:mt-0";
21
+ const defaultInput = "w-full px-3 py-2 border rounded-md text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 border-gray-300 focus:ring-blue-500 focus:border-blue-500";
22
+ const defaultInputError = "border-red-500 focus:ring-red-500";
23
+ const defaultCheckbox = "mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500";
24
+ const defaultHelpText = "mt-1 text-xs text-gray-500";
25
+ const defaultErrorText = "mt-1 text-xs text-red-600";
26
+ const inputClasses = hasError ? cx(classNames?.input ?? defaultInput, classNames?.inputError ?? defaultInputError) : cx(classNames?.input ?? defaultInput);
27
+ const labelClasses = classNames?.label ?? defaultLabel;
28
+ const checkboxLabelClasses = cx("text-sm", classNames?.label ?? "text-gray-700");
29
+ const renderLabel = () => {
30
+ if (question.detail_type === "heading") return null;
31
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: inputId, className: labelClasses, children: [
32
+ question.name,
33
+ question.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
34
+ ] });
35
+ };
36
+ const renderHelpText = () => {
37
+ if (!question.help_text) return null;
38
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: classNames?.helpText ?? defaultHelpText, children: question.help_text });
39
+ };
40
+ const renderError = () => {
41
+ if (!error) return null;
42
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { id: errorId, className: classNames?.errorText ?? defaultErrorText, role: "alert", children: error });
43
+ };
44
+ const ariaProps = {
45
+ "aria-invalid": hasError ? true : void 0,
46
+ "aria-describedby": hasError ? errorId : void 0,
47
+ "aria-required": question.required || void 0
48
+ };
49
+ switch (question.detail_type) {
50
+ case "heading":
51
+ return /* @__PURE__ */ jsxRuntime.jsx("h3", { className: classNames?.heading ?? defaultHeading, children: question.name });
52
+ case "text_field":
53
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
54
+ renderLabel(),
55
+ /* @__PURE__ */ jsxRuntime.jsx(
56
+ "input",
57
+ {
58
+ id: inputId,
59
+ type: "text",
60
+ value: value || "",
61
+ onChange: (e) => onChange(e.target.value),
62
+ placeholder: question.settings?.placeholder,
63
+ className: inputClasses,
64
+ ...ariaProps
65
+ }
66
+ ),
67
+ renderHelpText(),
68
+ renderError()
69
+ ] });
70
+ case "text_area":
71
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
72
+ renderLabel(),
73
+ /* @__PURE__ */ jsxRuntime.jsx(
74
+ "textarea",
75
+ {
76
+ id: inputId,
77
+ value: value || "",
78
+ onChange: (e) => onChange(e.target.value),
79
+ placeholder: question.settings?.placeholder,
80
+ rows: 4,
81
+ className: inputClasses,
82
+ ...ariaProps
83
+ }
84
+ ),
85
+ renderHelpText(),
86
+ renderError()
87
+ ] });
88
+ case "select":
89
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
90
+ renderLabel(),
91
+ /* @__PURE__ */ jsxRuntime.jsxs(
92
+ "select",
93
+ {
94
+ id: inputId,
95
+ value: value ?? "",
96
+ onChange: (e) => onChange(e.target.value),
97
+ className: inputClasses,
98
+ ...ariaProps,
99
+ children: [
100
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select an option" }),
101
+ question.options?.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.id, children: option.name }, option.id))
102
+ ]
103
+ }
104
+ ),
105
+ renderHelpText(),
106
+ renderError()
107
+ ] });
108
+ case "date":
109
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
110
+ renderLabel(),
111
+ /* @__PURE__ */ jsxRuntime.jsx(
112
+ "input",
113
+ {
114
+ id: inputId,
115
+ type: "date",
116
+ value: value || "",
117
+ onChange: (e) => onChange(e.target.value),
118
+ min: question.settings?.min?.toString(),
119
+ max: question.settings?.max?.toString(),
120
+ className: inputClasses,
121
+ ...ariaProps
122
+ }
123
+ ),
124
+ renderHelpText(),
125
+ renderError()
126
+ ] });
127
+ case "number":
128
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
129
+ renderLabel(),
130
+ /* @__PURE__ */ jsxRuntime.jsx(
131
+ "input",
132
+ {
133
+ id: inputId,
134
+ type: "number",
135
+ value: value ?? "",
136
+ onChange: (e) => onChange(e.target.value ? Number(e.target.value) : ""),
137
+ min: question.settings?.min,
138
+ max: question.settings?.max,
139
+ placeholder: question.settings?.placeholder,
140
+ className: inputClasses,
141
+ ...ariaProps
142
+ }
143
+ ),
144
+ renderHelpText(),
145
+ renderError()
146
+ ] });
147
+ case "check":
148
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
149
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
150
+ /* @__PURE__ */ jsxRuntime.jsx(
151
+ "input",
152
+ {
153
+ id: inputId,
154
+ type: "checkbox",
155
+ checked: !!value,
156
+ onChange: (e) => onChange(e.target.checked),
157
+ className: classNames?.checkbox ?? defaultCheckbox,
158
+ ...ariaProps
159
+ }
160
+ ),
161
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: inputId, className: checkboxLabelClasses, children: [
162
+ question.name,
163
+ question.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
164
+ ] })
165
+ ] }),
166
+ renderHelpText(),
167
+ renderError()
168
+ ] });
169
+ default:
170
+ return null;
171
+ }
172
+ }
173
+ function BookingForm({
174
+ questions,
175
+ onSubmit,
176
+ submitLabel = "Submit",
177
+ className = "",
178
+ labelClassName,
179
+ classNames: classNamesProp
180
+ }) {
181
+ const classNames = {
182
+ ...classNamesProp,
183
+ label: classNamesProp?.label ?? labelClassName
184
+ };
185
+ const [values, setValues] = react.useState({});
186
+ const [errors, setErrors] = react.useState({});
187
+ const [touched, setTouched] = react.useState({});
188
+ const isQuestionVisible = react.useCallback(
189
+ (question) => {
190
+ const { settings } = question;
191
+ if (!settings?.conditional_answers) {
192
+ return true;
193
+ }
194
+ const conditionEntries = Object.entries(settings.conditional_answers);
195
+ if (conditionEntries.length === 0) {
196
+ return true;
197
+ }
198
+ return conditionEntries.some(([questionIdStr, expectedAnswer]) => {
199
+ const questionId = Number(questionIdStr);
200
+ const currentValue = values[String(questionId)];
201
+ if (currentValue === void 0 || currentValue === "" || currentValue === null) {
202
+ return false;
203
+ }
204
+ const normalizedCurrent = typeof currentValue === "object" && currentValue !== null && "id" in currentValue ? currentValue : currentValue;
205
+ const candidates = /* @__PURE__ */ new Set();
206
+ const addCandidate = (v) => {
207
+ if (v === void 0 || v === null) return;
208
+ const s = String(v).trim();
209
+ if (s) candidates.add(s);
210
+ };
211
+ const sourceQuestion = questions.find((q) => q.id === questionId);
212
+ if (typeof normalizedCurrent === "object" && normalizedCurrent !== null) {
213
+ addCandidate(normalizedCurrent.id);
214
+ addCandidate(normalizedCurrent.name);
215
+ } else {
216
+ addCandidate(normalizedCurrent);
217
+ }
218
+ if (sourceQuestion?.detail_type === "select" && sourceQuestion.options?.length) {
219
+ const currentStr = [...candidates][0] ?? "";
220
+ const optionById = sourceQuestion.options.find((o) => String(o.id) === currentStr);
221
+ const optionByName = sourceQuestion.options.find((o) => String(o.name).trim() === currentStr);
222
+ const opt = optionById ?? optionByName;
223
+ if (opt) {
224
+ addCandidate(opt.id);
225
+ addCandidate(opt.name);
226
+ }
227
+ }
228
+ const matchesScalar = (expected) => {
229
+ const expectedStr = String(expected).trim();
230
+ return candidates.has(expectedStr);
231
+ };
232
+ if (Array.isArray(expectedAnswer)) {
233
+ return expectedAnswer.some((v) => matchesScalar(v));
234
+ }
235
+ if (expectedAnswer && typeof expectedAnswer === "object") {
236
+ const expectedMap = expectedAnswer;
237
+ for (const c of candidates) {
238
+ if (Object.prototype.hasOwnProperty.call(expectedMap, c)) {
239
+ const flag = expectedMap[c];
240
+ return flag === void 0 ? true : !!flag;
241
+ }
242
+ }
243
+ return false;
244
+ }
245
+ return matchesScalar(expectedAnswer);
246
+ });
247
+ },
248
+ [values, questions]
249
+ );
250
+ const visibleQuestions = questions.filter(isQuestionVisible);
251
+ const validateField = react.useCallback((question, value) => {
252
+ if (question.detail_type === "heading") return null;
253
+ if (question.required) {
254
+ if (value === void 0 || value === "" || value === null) {
255
+ return `${question.name} is required`;
256
+ }
257
+ if (question.detail_type === "check" && !value) {
258
+ return `${question.name} must be checked`;
259
+ }
260
+ }
261
+ if (question.detail_type === "number" && value !== "" && value !== void 0) {
262
+ const numValue = Number(value);
263
+ if (question.settings?.min !== void 0 && numValue < question.settings.min) {
264
+ return `Minimum value is ${question.settings.min}`;
265
+ }
266
+ if (question.settings?.max !== void 0 && numValue > question.settings.max) {
267
+ return `Maximum value is ${question.settings.max}`;
268
+ }
269
+ }
270
+ return null;
271
+ }, []);
272
+ const validateAll = react.useCallback(() => {
273
+ const newErrors = {};
274
+ let isValid = true;
275
+ visibleQuestions.forEach((question) => {
276
+ const error = validateField(question, values[question.id]);
277
+ if (error) {
278
+ newErrors[question.id] = error;
279
+ isValid = false;
280
+ }
281
+ });
282
+ setErrors(newErrors);
283
+ return isValid;
284
+ }, [visibleQuestions, values, validateField]);
285
+ react.useEffect(() => {
286
+ const visibleIds = new Set(visibleQuestions.map((q) => q.id));
287
+ setErrors((prev) => {
288
+ let changed = false;
289
+ const next = { ...prev };
290
+ Object.keys(next).forEach((idStr) => {
291
+ const id = Number(idStr);
292
+ if (!visibleIds.has(id)) {
293
+ delete next[id];
294
+ changed = true;
295
+ }
296
+ });
297
+ return changed ? next : prev;
298
+ });
299
+ }, [visibleQuestions]);
300
+ const handleChange = (questionId, value) => {
301
+ setValues((prev) => ({ ...prev, [questionId]: value }));
302
+ setTouched((prev) => ({ ...prev, [questionId]: true }));
303
+ if (touched[questionId]) {
304
+ const question = questions.find((q) => q.id === questionId);
305
+ if (question) {
306
+ const error = validateField(question, value);
307
+ setErrors((prev) => {
308
+ if (error) {
309
+ return { ...prev, [questionId]: error };
310
+ }
311
+ const { [questionId]: _, ...rest } = prev;
312
+ return rest;
313
+ });
314
+ }
315
+ }
316
+ };
317
+ const handleSubmit = (e) => {
318
+ e.preventDefault();
319
+ const allTouched = {};
320
+ visibleQuestions.forEach((q) => {
321
+ allTouched[q.id] = true;
322
+ });
323
+ setTouched(allTouched);
324
+ if (validateAll()) {
325
+ const visibleValues = {};
326
+ visibleQuestions.forEach((q) => {
327
+ if (q.detail_type !== "heading" && values[q.id] !== void 0) {
328
+ visibleValues[q.id] = values[q.id];
329
+ }
330
+ });
331
+ onSubmit(visibleValues);
332
+ }
333
+ };
334
+ const defaultButton = "w-full mt-4 px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors";
335
+ return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className, noValidate: true, children: [
336
+ visibleQuestions.map((question) => /* @__PURE__ */ jsxRuntime.jsx(
337
+ FormField,
338
+ {
339
+ question,
340
+ value: values[question.id],
341
+ error: touched[question.id] ? errors[question.id] : void 0,
342
+ onChange: (value) => handleChange(question.id, value),
343
+ classNames
344
+ },
345
+ question.id
346
+ )),
347
+ /* @__PURE__ */ jsxRuntime.jsx(
348
+ "button",
349
+ {
350
+ type: "submit",
351
+ className: classNames?.button ?? defaultButton,
352
+ children: submitLabel
353
+ }
354
+ )
355
+ ] });
356
+ }
357
+
358
+ exports.BookingForm = BookingForm;
359
+ //# sourceMappingURL=index.cjs.map
360
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/BookingForm.tsx"],"names":["jsxs","jsx","useState","useCallback","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import React, { useState, useEffect, useCallback } from 'react';\nimport type { Question, FormValues, FormErrors, BookingFormProps, BookingFormClassNames } from '../types';\n\n/** Merge base classes with optional custom classes */\nconst cx = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ');\n\n/**\n * Renders a single form field based on question type\n */\nfunction FormField({\n question,\n value,\n error,\n onChange,\n classNames,\n}: {\n question: Question;\n value: string | number | boolean | undefined;\n error?: string;\n onChange: (value: string | number | boolean) => void;\n classNames?: BookingFormClassNames;\n}) {\n const inputId = `question-${question.id}`;\n const errorId = `${inputId}-error`;\n const hasError = !!error;\n\n // Default classes (can be overridden via classNames prop)\n const defaultFieldWrapper = 'mb-4';\n const defaultLabel = 'block text-sm font-medium mb-1 text-gray-700';\n const defaultHeading = 'text-lg font-semibold text-gray-900 mt-4 mb-2 first:mt-0';\n const defaultInput = 'w-full px-3 py-2 border rounded-md text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 border-gray-300 focus:ring-blue-500 focus:border-blue-500';\n const defaultInputError = 'border-red-500 focus:ring-red-500';\n const defaultCheckbox = 'mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500';\n const defaultHelpText = 'mt-1 text-xs text-gray-500';\n const defaultErrorText = 'mt-1 text-xs text-red-600';\n\n const inputClasses = hasError\n ? cx(classNames?.input ?? defaultInput, classNames?.inputError ?? defaultInputError)\n : cx(classNames?.input ?? defaultInput);\n\n const labelClasses = classNames?.label ?? defaultLabel;\n const checkboxLabelClasses = cx('text-sm', classNames?.label ?? 'text-gray-700');\n\n const renderLabel = () => {\n if (question.detail_type === 'heading') return null;\n return (\n <label htmlFor={inputId} className={labelClasses}>\n {question.name}\n {question.required && <span className=\"text-red-500 ml-1\" aria-hidden=\"true\">*</span>}\n </label>\n );\n };\n\n const renderHelpText = () => {\n if (!question.help_text) return null;\n return <p className={classNames?.helpText ?? defaultHelpText}>{question.help_text}</p>;\n };\n\n const renderError = () => {\n if (!error) return null;\n return (\n <p id={errorId} className={classNames?.errorText ?? defaultErrorText} role=\"alert\">\n {error}\n </p>\n );\n };\n\n const ariaProps = {\n 'aria-invalid': hasError ? true : undefined,\n 'aria-describedby': hasError ? errorId : undefined,\n 'aria-required': question.required || undefined,\n };\n\n switch (question.detail_type) {\n case 'heading':\n return (\n <h3 className={classNames?.heading ?? defaultHeading}>\n {question.name}\n </h3>\n );\n\n case 'text_field':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"text\"\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n placeholder={question.settings?.placeholder}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'text_area':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <textarea\n id={inputId}\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n placeholder={question.settings?.placeholder}\n rows={4}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'select':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <select\n id={inputId}\n value={(value as string | number) ?? ''}\n onChange={(e) => onChange(e.target.value)}\n className={inputClasses}\n {...ariaProps}\n >\n <option value=\"\">Select an option</option>\n {question.options?.map((option) => (\n <option key={option.id} value={option.id}>\n {option.name}\n </option>\n ))}\n </select>\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'date':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"date\"\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n min={question.settings?.min?.toString()}\n max={question.settings?.max?.toString()}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'number':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"number\"\n value={(value as number) ?? ''}\n onChange={(e) => onChange(e.target.value ? Number(e.target.value) : '')}\n min={question.settings?.min}\n max={question.settings?.max}\n placeholder={question.settings?.placeholder}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'check':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n <div className=\"flex items-start gap-2\">\n <input\n id={inputId}\n type=\"checkbox\"\n checked={!!value}\n onChange={(e) => onChange(e.target.checked)}\n className={classNames?.checkbox ?? defaultCheckbox}\n {...ariaProps}\n />\n <label htmlFor={inputId} className={checkboxLabelClasses}>\n {question.name}\n {question.required && <span className=\"text-red-500 ml-1\" aria-hidden=\"true\">*</span>}\n </label>\n </div>\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n default:\n return null;\n }\n}\n\n/**\n * BookingForm - A dynamic form component for booking questions\n *\n * @example\n * ```tsx\n * const questions: Question[] = [\n * { id: 1, name: 'Personal Details', detail_type: 'heading' },\n * { id: 2, name: 'Full Name', detail_type: 'text_field', required: true },\n * { id: 3, name: 'Email', detail_type: 'text_field', required: true },\n * ];\n *\n * <BookingForm\n * questions={questions}\n * onSubmit={(values) => console.log(values)}\n * />\n * ```\n */\nexport function BookingForm({\n questions,\n onSubmit,\n submitLabel = 'Submit',\n className = '',\n labelClassName,\n classNames: classNamesProp,\n}: BookingFormProps) {\n // Merge deprecated labelClassName into classNames for backwards compatibility\n const classNames: BookingFormClassNames = {\n ...classNamesProp,\n label: classNamesProp?.label ?? labelClassName,\n };\n const [values, setValues] = useState<FormValues>({});\n const [errors, setErrors] = useState<FormErrors>({});\n const [touched, setTouched] = useState<Record<number, boolean>>({});\n\n\n // Check if a question should be visible based on conditional settings\n const isQuestionVisible = useCallback(\n (question: Question): boolean => {\n const { settings } = question;\n if (!settings?.conditional_answers) {\n return true;\n }\n\n // Check all conditions in conditional_answers\n // Each entry is { questionId: expectedAnswerValue }\n const conditionEntries = Object.entries(settings.conditional_answers as Record<string, unknown>);\n if (conditionEntries.length === 0) {\n return true;\n }\n\n // Question is visible if ANY condition is met\n return conditionEntries.some(([questionIdStr, expectedAnswer]) => {\n const questionId = Number(questionIdStr);\n const currentValue = (values as Record<string, unknown>)[String(questionId)];\n\n if (currentValue === undefined || currentValue === '' || currentValue === null) {\n return false;\n }\n\n // Selects typically store the option id (as string), but allow `{ id, name }` objects too\n const normalizedCurrent =\n typeof currentValue === 'object' && currentValue !== null && 'id' in (currentValue as any)\n ? (currentValue as any)\n : currentValue;\n\n // Build a set of candidate strings for comparison.\n // For select questions we support matching by BOTH option id and option name.\n const candidates = new Set<string>();\n\n const addCandidate = (v: unknown) => {\n if (v === undefined || v === null) return;\n const s = String(v).trim();\n if (s) candidates.add(s);\n };\n\n const sourceQuestion = questions.find((q) => q.id === questionId);\n\n if (typeof normalizedCurrent === 'object' && normalizedCurrent !== null) {\n addCandidate((normalizedCurrent as any).id);\n addCandidate((normalizedCurrent as any).name);\n } else {\n addCandidate(normalizedCurrent);\n }\n\n if (sourceQuestion?.detail_type === 'select' && sourceQuestion.options?.length) {\n // Try to resolve the selected option via id or name.\n const currentStr = [...candidates][0] ?? '';\n const optionById = sourceQuestion.options.find((o) => String(o.id) === currentStr);\n const optionByName = sourceQuestion.options.find((o) => String(o.name).trim() === currentStr);\n const opt = optionById ?? optionByName;\n if (opt) {\n addCandidate(opt.id);\n addCandidate(opt.name);\n }\n }\n\n const matchesScalar = (expected: unknown) => {\n const expectedStr = String(expected).trim();\n return candidates.has(expectedStr);\n };\n\n // Support multiple expected formats:\n // - scalar: \"Consultation\" / \"1\" / 1 / true\n // - array: [\"Consultation\", \"Follow-up\"]\n // - map/object: { \"Consultation\": true }\n if (Array.isArray(expectedAnswer)) {\n return expectedAnswer.some((v) => matchesScalar(v));\n }\n\n if (expectedAnswer && typeof expectedAnswer === 'object') {\n const expectedMap = expectedAnswer as Record<string, unknown>;\n for (const c of candidates) {\n if (Object.prototype.hasOwnProperty.call(expectedMap, c)) {\n const flag = expectedMap[c];\n return flag === undefined ? true : !!flag;\n }\n }\n return false;\n }\n\n return matchesScalar(expectedAnswer);\n });\n },\n [values, questions]\n );\n\n // Get visible questions\n const visibleQuestions = questions.filter(isQuestionVisible);\n\n\n // Validate a single field\n const validateField = useCallback((question: Question, value: FormValues[number]): string | null => {\n if (question.detail_type === 'heading') return null;\n\n if (question.required) {\n if (value === undefined || value === '' || value === null) {\n return `${question.name} is required`;\n }\n if (question.detail_type === 'check' && !value) {\n return `${question.name} must be checked`;\n }\n }\n\n if (question.detail_type === 'number' && value !== '' && value !== undefined) {\n const numValue = Number(value);\n if (question.settings?.min !== undefined && numValue < question.settings.min) {\n return `Minimum value is ${question.settings.min}`;\n }\n if (question.settings?.max !== undefined && numValue > question.settings.max) {\n return `Maximum value is ${question.settings.max}`;\n }\n }\n\n return null;\n }, []);\n\n // Validate all visible fields\n const validateAll = useCallback((): boolean => {\n const newErrors: FormErrors = {};\n let isValid = true;\n\n visibleQuestions.forEach((question) => {\n const error = validateField(question, values[question.id]);\n if (error) {\n newErrors[question.id] = error;\n isValid = false;\n }\n });\n\n setErrors(newErrors);\n return isValid;\n }, [visibleQuestions, values, validateField]);\n\n // Clear errors for hidden questions\n useEffect(() => {\n const visibleIds = new Set(visibleQuestions.map((q) => q.id));\n\n setErrors((prev) => {\n let changed = false;\n const next = { ...prev };\n\n Object.keys(next).forEach((idStr) => {\n const id = Number(idStr);\n if (!visibleIds.has(id)) {\n delete next[id];\n changed = true;\n }\n });\n\n return changed ? next : prev;\n });\n }, [visibleQuestions]);\n\n const handleChange = (questionId: number, value: string | number | boolean) => {\n setValues((prev) => ({ ...prev, [questionId]: value }));\n setTouched((prev) => ({ ...prev, [questionId]: true }));\n\n // Clear error on change if touched\n if (touched[questionId]) {\n const question = questions.find((q) => q.id === questionId);\n if (question) {\n const error = validateField(question, value);\n setErrors((prev) => {\n if (error) {\n return { ...prev, [questionId]: error };\n }\n const { [questionId]: _, ...rest } = prev;\n return rest;\n });\n }\n }\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n\n // Mark all fields as touched\n const allTouched: Record<number, boolean> = {};\n visibleQuestions.forEach((q) => {\n allTouched[q.id] = true;\n });\n setTouched(allTouched);\n\n if (validateAll()) {\n // Only include visible question values\n const visibleValues: FormValues = {};\n visibleQuestions.forEach((q) => {\n if (q.detail_type !== 'heading' && values[q.id] !== undefined) {\n visibleValues[q.id] = values[q.id];\n }\n });\n onSubmit(visibleValues);\n }\n };\n\n const defaultButton = 'w-full mt-4 px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors';\n\n return (\n <form onSubmit={handleSubmit} className={className} noValidate>\n {visibleQuestions.map((question) => (\n <FormField\n key={question.id}\n question={question}\n value={values[question.id]}\n error={touched[question.id] ? errors[question.id] : undefined}\n onChange={(value) => handleChange(question.id, value)}\n classNames={classNames}\n />\n ))}\n <button\n type=\"submit\"\n className={classNames?.button ?? defaultButton}\n >\n {submitLabel}\n </button>\n </form>\n );\n}\n\nexport default BookingForm;\n"]}
@@ -0,0 +1,93 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface QuestionOption {
4
+ id: number;
5
+ name: string;
6
+ price?: number;
7
+ is_default?: boolean;
8
+ }
9
+ interface QuestionSettings {
10
+ conditional_question?: string;
11
+ conditional_answers?: Record<string, string>;
12
+ min?: number;
13
+ max?: number;
14
+ placeholder?: string;
15
+ }
16
+ interface Question {
17
+ id: number;
18
+ name: string;
19
+ detail_type: 'heading' | 'text_field' | 'text_area' | 'select' | 'date' | 'number' | 'check';
20
+ required?: boolean;
21
+ important?: boolean;
22
+ admin_only?: boolean;
23
+ applies_to?: number;
24
+ ask_member?: boolean;
25
+ options?: QuestionOption[];
26
+ settings?: QuestionSettings;
27
+ help_text?: string;
28
+ price?: number;
29
+ price_per_booking?: boolean;
30
+ outcome?: boolean;
31
+ hide_on_customer_journey?: boolean;
32
+ }
33
+ type FormValues = Record<number, string | number | boolean>;
34
+ type FormErrors = Record<number, string>;
35
+ /**
36
+ * Custom class names for styling form elements
37
+ */
38
+ interface BookingFormClassNames {
39
+ /** Wrapper for each form field */
40
+ fieldWrapper?: string;
41
+ /** All labels */
42
+ label?: string;
43
+ /** Heading elements (detail_type: 'heading') */
44
+ heading?: string;
45
+ /** All input elements (text, textarea, select, date, number) */
46
+ input?: string;
47
+ /** Input elements in error state */
48
+ inputError?: string;
49
+ /** Checkbox input specifically */
50
+ checkbox?: string;
51
+ /** Help text below fields */
52
+ helpText?: string;
53
+ /** Error message text */
54
+ errorText?: string;
55
+ /** Submit button */
56
+ button?: string;
57
+ }
58
+ interface BookingFormProps {
59
+ questions: Question[];
60
+ onSubmit: (values: FormValues) => void;
61
+ submitLabel?: string;
62
+ /** Class name for the form element */
63
+ className?: string;
64
+ /** @deprecated Use classNames.label instead */
65
+ labelClassName?: string;
66
+ /** Custom class names for styling individual elements */
67
+ classNames?: BookingFormClassNames;
68
+ }
69
+ interface BookingUIConfig {
70
+ theme?: 'light' | 'dark' | 'system';
71
+ locale?: string;
72
+ }
73
+
74
+ /**
75
+ * BookingForm - A dynamic form component for booking questions
76
+ *
77
+ * @example
78
+ * ```tsx
79
+ * const questions: Question[] = [
80
+ * { id: 1, name: 'Personal Details', detail_type: 'heading' },
81
+ * { id: 2, name: 'Full Name', detail_type: 'text_field', required: true },
82
+ * { id: 3, name: 'Email', detail_type: 'text_field', required: true },
83
+ * ];
84
+ *
85
+ * <BookingForm
86
+ * questions={questions}
87
+ * onSubmit={(values) => console.log(values)}
88
+ * />
89
+ * ```
90
+ */
91
+ declare function BookingForm({ questions, onSubmit, submitLabel, className, labelClassName, classNames: classNamesProp, }: BookingFormProps): react_jsx_runtime.JSX.Element;
92
+
93
+ export { BookingForm, type BookingFormClassNames, type BookingFormProps, type BookingUIConfig, type FormErrors, type FormValues, type Question, type QuestionOption, type QuestionSettings };
@@ -0,0 +1,93 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface QuestionOption {
4
+ id: number;
5
+ name: string;
6
+ price?: number;
7
+ is_default?: boolean;
8
+ }
9
+ interface QuestionSettings {
10
+ conditional_question?: string;
11
+ conditional_answers?: Record<string, string>;
12
+ min?: number;
13
+ max?: number;
14
+ placeholder?: string;
15
+ }
16
+ interface Question {
17
+ id: number;
18
+ name: string;
19
+ detail_type: 'heading' | 'text_field' | 'text_area' | 'select' | 'date' | 'number' | 'check';
20
+ required?: boolean;
21
+ important?: boolean;
22
+ admin_only?: boolean;
23
+ applies_to?: number;
24
+ ask_member?: boolean;
25
+ options?: QuestionOption[];
26
+ settings?: QuestionSettings;
27
+ help_text?: string;
28
+ price?: number;
29
+ price_per_booking?: boolean;
30
+ outcome?: boolean;
31
+ hide_on_customer_journey?: boolean;
32
+ }
33
+ type FormValues = Record<number, string | number | boolean>;
34
+ type FormErrors = Record<number, string>;
35
+ /**
36
+ * Custom class names for styling form elements
37
+ */
38
+ interface BookingFormClassNames {
39
+ /** Wrapper for each form field */
40
+ fieldWrapper?: string;
41
+ /** All labels */
42
+ label?: string;
43
+ /** Heading elements (detail_type: 'heading') */
44
+ heading?: string;
45
+ /** All input elements (text, textarea, select, date, number) */
46
+ input?: string;
47
+ /** Input elements in error state */
48
+ inputError?: string;
49
+ /** Checkbox input specifically */
50
+ checkbox?: string;
51
+ /** Help text below fields */
52
+ helpText?: string;
53
+ /** Error message text */
54
+ errorText?: string;
55
+ /** Submit button */
56
+ button?: string;
57
+ }
58
+ interface BookingFormProps {
59
+ questions: Question[];
60
+ onSubmit: (values: FormValues) => void;
61
+ submitLabel?: string;
62
+ /** Class name for the form element */
63
+ className?: string;
64
+ /** @deprecated Use classNames.label instead */
65
+ labelClassName?: string;
66
+ /** Custom class names for styling individual elements */
67
+ classNames?: BookingFormClassNames;
68
+ }
69
+ interface BookingUIConfig {
70
+ theme?: 'light' | 'dark' | 'system';
71
+ locale?: string;
72
+ }
73
+
74
+ /**
75
+ * BookingForm - A dynamic form component for booking questions
76
+ *
77
+ * @example
78
+ * ```tsx
79
+ * const questions: Question[] = [
80
+ * { id: 1, name: 'Personal Details', detail_type: 'heading' },
81
+ * { id: 2, name: 'Full Name', detail_type: 'text_field', required: true },
82
+ * { id: 3, name: 'Email', detail_type: 'text_field', required: true },
83
+ * ];
84
+ *
85
+ * <BookingForm
86
+ * questions={questions}
87
+ * onSubmit={(values) => console.log(values)}
88
+ * />
89
+ * ```
90
+ */
91
+ declare function BookingForm({ questions, onSubmit, submitLabel, className, labelClassName, classNames: classNamesProp, }: BookingFormProps): react_jsx_runtime.JSX.Element;
92
+
93
+ export { BookingForm, type BookingFormClassNames, type BookingFormProps, type BookingUIConfig, type FormErrors, type FormValues, type Question, type QuestionOption, type QuestionSettings };
package/dist/index.js ADDED
@@ -0,0 +1,358 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+
4
+ // src/components/BookingForm.tsx
5
+ var cx = (...classes) => classes.filter(Boolean).join(" ");
6
+ function FormField({
7
+ question,
8
+ value,
9
+ error,
10
+ onChange,
11
+ classNames
12
+ }) {
13
+ const inputId = `question-${question.id}`;
14
+ const errorId = `${inputId}-error`;
15
+ const hasError = !!error;
16
+ const defaultFieldWrapper = "mb-4";
17
+ const defaultLabel = "block text-sm font-medium mb-1 text-gray-700";
18
+ const defaultHeading = "text-lg font-semibold text-gray-900 mt-4 mb-2 first:mt-0";
19
+ const defaultInput = "w-full px-3 py-2 border rounded-md text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 border-gray-300 focus:ring-blue-500 focus:border-blue-500";
20
+ const defaultInputError = "border-red-500 focus:ring-red-500";
21
+ const defaultCheckbox = "mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500";
22
+ const defaultHelpText = "mt-1 text-xs text-gray-500";
23
+ const defaultErrorText = "mt-1 text-xs text-red-600";
24
+ const inputClasses = hasError ? cx(classNames?.input ?? defaultInput, classNames?.inputError ?? defaultInputError) : cx(classNames?.input ?? defaultInput);
25
+ const labelClasses = classNames?.label ?? defaultLabel;
26
+ const checkboxLabelClasses = cx("text-sm", classNames?.label ?? "text-gray-700");
27
+ const renderLabel = () => {
28
+ if (question.detail_type === "heading") return null;
29
+ return /* @__PURE__ */ jsxs("label", { htmlFor: inputId, className: labelClasses, children: [
30
+ question.name,
31
+ question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
32
+ ] });
33
+ };
34
+ const renderHelpText = () => {
35
+ if (!question.help_text) return null;
36
+ return /* @__PURE__ */ jsx("p", { className: classNames?.helpText ?? defaultHelpText, children: question.help_text });
37
+ };
38
+ const renderError = () => {
39
+ if (!error) return null;
40
+ return /* @__PURE__ */ jsx("p", { id: errorId, className: classNames?.errorText ?? defaultErrorText, role: "alert", children: error });
41
+ };
42
+ const ariaProps = {
43
+ "aria-invalid": hasError ? true : void 0,
44
+ "aria-describedby": hasError ? errorId : void 0,
45
+ "aria-required": question.required || void 0
46
+ };
47
+ switch (question.detail_type) {
48
+ case "heading":
49
+ return /* @__PURE__ */ jsx("h3", { className: classNames?.heading ?? defaultHeading, children: question.name });
50
+ case "text_field":
51
+ return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
52
+ renderLabel(),
53
+ /* @__PURE__ */ jsx(
54
+ "input",
55
+ {
56
+ id: inputId,
57
+ type: "text",
58
+ value: value || "",
59
+ onChange: (e) => onChange(e.target.value),
60
+ placeholder: question.settings?.placeholder,
61
+ className: inputClasses,
62
+ ...ariaProps
63
+ }
64
+ ),
65
+ renderHelpText(),
66
+ renderError()
67
+ ] });
68
+ case "text_area":
69
+ return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
70
+ renderLabel(),
71
+ /* @__PURE__ */ jsx(
72
+ "textarea",
73
+ {
74
+ id: inputId,
75
+ value: value || "",
76
+ onChange: (e) => onChange(e.target.value),
77
+ placeholder: question.settings?.placeholder,
78
+ rows: 4,
79
+ className: inputClasses,
80
+ ...ariaProps
81
+ }
82
+ ),
83
+ renderHelpText(),
84
+ renderError()
85
+ ] });
86
+ case "select":
87
+ return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
88
+ renderLabel(),
89
+ /* @__PURE__ */ jsxs(
90
+ "select",
91
+ {
92
+ id: inputId,
93
+ value: value ?? "",
94
+ onChange: (e) => onChange(e.target.value),
95
+ className: inputClasses,
96
+ ...ariaProps,
97
+ children: [
98
+ /* @__PURE__ */ jsx("option", { value: "", children: "Select an option" }),
99
+ question.options?.map((option) => /* @__PURE__ */ jsx("option", { value: option.id, children: option.name }, option.id))
100
+ ]
101
+ }
102
+ ),
103
+ renderHelpText(),
104
+ renderError()
105
+ ] });
106
+ case "date":
107
+ return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
108
+ renderLabel(),
109
+ /* @__PURE__ */ jsx(
110
+ "input",
111
+ {
112
+ id: inputId,
113
+ type: "date",
114
+ value: value || "",
115
+ onChange: (e) => onChange(e.target.value),
116
+ min: question.settings?.min?.toString(),
117
+ max: question.settings?.max?.toString(),
118
+ className: inputClasses,
119
+ ...ariaProps
120
+ }
121
+ ),
122
+ renderHelpText(),
123
+ renderError()
124
+ ] });
125
+ case "number":
126
+ return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
127
+ renderLabel(),
128
+ /* @__PURE__ */ jsx(
129
+ "input",
130
+ {
131
+ id: inputId,
132
+ type: "number",
133
+ value: value ?? "",
134
+ onChange: (e) => onChange(e.target.value ? Number(e.target.value) : ""),
135
+ min: question.settings?.min,
136
+ max: question.settings?.max,
137
+ placeholder: question.settings?.placeholder,
138
+ className: inputClasses,
139
+ ...ariaProps
140
+ }
141
+ ),
142
+ renderHelpText(),
143
+ renderError()
144
+ ] });
145
+ case "check":
146
+ return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
147
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
148
+ /* @__PURE__ */ jsx(
149
+ "input",
150
+ {
151
+ id: inputId,
152
+ type: "checkbox",
153
+ checked: !!value,
154
+ onChange: (e) => onChange(e.target.checked),
155
+ className: classNames?.checkbox ?? defaultCheckbox,
156
+ ...ariaProps
157
+ }
158
+ ),
159
+ /* @__PURE__ */ jsxs("label", { htmlFor: inputId, className: checkboxLabelClasses, children: [
160
+ question.name,
161
+ question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
162
+ ] })
163
+ ] }),
164
+ renderHelpText(),
165
+ renderError()
166
+ ] });
167
+ default:
168
+ return null;
169
+ }
170
+ }
171
+ function BookingForm({
172
+ questions,
173
+ onSubmit,
174
+ submitLabel = "Submit",
175
+ className = "",
176
+ labelClassName,
177
+ classNames: classNamesProp
178
+ }) {
179
+ const classNames = {
180
+ ...classNamesProp,
181
+ label: classNamesProp?.label ?? labelClassName
182
+ };
183
+ const [values, setValues] = useState({});
184
+ const [errors, setErrors] = useState({});
185
+ const [touched, setTouched] = useState({});
186
+ const isQuestionVisible = useCallback(
187
+ (question) => {
188
+ const { settings } = question;
189
+ if (!settings?.conditional_answers) {
190
+ return true;
191
+ }
192
+ const conditionEntries = Object.entries(settings.conditional_answers);
193
+ if (conditionEntries.length === 0) {
194
+ return true;
195
+ }
196
+ return conditionEntries.some(([questionIdStr, expectedAnswer]) => {
197
+ const questionId = Number(questionIdStr);
198
+ const currentValue = values[String(questionId)];
199
+ if (currentValue === void 0 || currentValue === "" || currentValue === null) {
200
+ return false;
201
+ }
202
+ const normalizedCurrent = typeof currentValue === "object" && currentValue !== null && "id" in currentValue ? currentValue : currentValue;
203
+ const candidates = /* @__PURE__ */ new Set();
204
+ const addCandidate = (v) => {
205
+ if (v === void 0 || v === null) return;
206
+ const s = String(v).trim();
207
+ if (s) candidates.add(s);
208
+ };
209
+ const sourceQuestion = questions.find((q) => q.id === questionId);
210
+ if (typeof normalizedCurrent === "object" && normalizedCurrent !== null) {
211
+ addCandidate(normalizedCurrent.id);
212
+ addCandidate(normalizedCurrent.name);
213
+ } else {
214
+ addCandidate(normalizedCurrent);
215
+ }
216
+ if (sourceQuestion?.detail_type === "select" && sourceQuestion.options?.length) {
217
+ const currentStr = [...candidates][0] ?? "";
218
+ const optionById = sourceQuestion.options.find((o) => String(o.id) === currentStr);
219
+ const optionByName = sourceQuestion.options.find((o) => String(o.name).trim() === currentStr);
220
+ const opt = optionById ?? optionByName;
221
+ if (opt) {
222
+ addCandidate(opt.id);
223
+ addCandidate(opt.name);
224
+ }
225
+ }
226
+ const matchesScalar = (expected) => {
227
+ const expectedStr = String(expected).trim();
228
+ return candidates.has(expectedStr);
229
+ };
230
+ if (Array.isArray(expectedAnswer)) {
231
+ return expectedAnswer.some((v) => matchesScalar(v));
232
+ }
233
+ if (expectedAnswer && typeof expectedAnswer === "object") {
234
+ const expectedMap = expectedAnswer;
235
+ for (const c of candidates) {
236
+ if (Object.prototype.hasOwnProperty.call(expectedMap, c)) {
237
+ const flag = expectedMap[c];
238
+ return flag === void 0 ? true : !!flag;
239
+ }
240
+ }
241
+ return false;
242
+ }
243
+ return matchesScalar(expectedAnswer);
244
+ });
245
+ },
246
+ [values, questions]
247
+ );
248
+ const visibleQuestions = questions.filter(isQuestionVisible);
249
+ const validateField = useCallback((question, value) => {
250
+ if (question.detail_type === "heading") return null;
251
+ if (question.required) {
252
+ if (value === void 0 || value === "" || value === null) {
253
+ return `${question.name} is required`;
254
+ }
255
+ if (question.detail_type === "check" && !value) {
256
+ return `${question.name} must be checked`;
257
+ }
258
+ }
259
+ if (question.detail_type === "number" && value !== "" && value !== void 0) {
260
+ const numValue = Number(value);
261
+ if (question.settings?.min !== void 0 && numValue < question.settings.min) {
262
+ return `Minimum value is ${question.settings.min}`;
263
+ }
264
+ if (question.settings?.max !== void 0 && numValue > question.settings.max) {
265
+ return `Maximum value is ${question.settings.max}`;
266
+ }
267
+ }
268
+ return null;
269
+ }, []);
270
+ const validateAll = useCallback(() => {
271
+ const newErrors = {};
272
+ let isValid = true;
273
+ visibleQuestions.forEach((question) => {
274
+ const error = validateField(question, values[question.id]);
275
+ if (error) {
276
+ newErrors[question.id] = error;
277
+ isValid = false;
278
+ }
279
+ });
280
+ setErrors(newErrors);
281
+ return isValid;
282
+ }, [visibleQuestions, values, validateField]);
283
+ useEffect(() => {
284
+ const visibleIds = new Set(visibleQuestions.map((q) => q.id));
285
+ setErrors((prev) => {
286
+ let changed = false;
287
+ const next = { ...prev };
288
+ Object.keys(next).forEach((idStr) => {
289
+ const id = Number(idStr);
290
+ if (!visibleIds.has(id)) {
291
+ delete next[id];
292
+ changed = true;
293
+ }
294
+ });
295
+ return changed ? next : prev;
296
+ });
297
+ }, [visibleQuestions]);
298
+ const handleChange = (questionId, value) => {
299
+ setValues((prev) => ({ ...prev, [questionId]: value }));
300
+ setTouched((prev) => ({ ...prev, [questionId]: true }));
301
+ if (touched[questionId]) {
302
+ const question = questions.find((q) => q.id === questionId);
303
+ if (question) {
304
+ const error = validateField(question, value);
305
+ setErrors((prev) => {
306
+ if (error) {
307
+ return { ...prev, [questionId]: error };
308
+ }
309
+ const { [questionId]: _, ...rest } = prev;
310
+ return rest;
311
+ });
312
+ }
313
+ }
314
+ };
315
+ const handleSubmit = (e) => {
316
+ e.preventDefault();
317
+ const allTouched = {};
318
+ visibleQuestions.forEach((q) => {
319
+ allTouched[q.id] = true;
320
+ });
321
+ setTouched(allTouched);
322
+ if (validateAll()) {
323
+ const visibleValues = {};
324
+ visibleQuestions.forEach((q) => {
325
+ if (q.detail_type !== "heading" && values[q.id] !== void 0) {
326
+ visibleValues[q.id] = values[q.id];
327
+ }
328
+ });
329
+ onSubmit(visibleValues);
330
+ }
331
+ };
332
+ const defaultButton = "w-full mt-4 px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors";
333
+ return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className, noValidate: true, children: [
334
+ visibleQuestions.map((question) => /* @__PURE__ */ jsx(
335
+ FormField,
336
+ {
337
+ question,
338
+ value: values[question.id],
339
+ error: touched[question.id] ? errors[question.id] : void 0,
340
+ onChange: (value) => handleChange(question.id, value),
341
+ classNames
342
+ },
343
+ question.id
344
+ )),
345
+ /* @__PURE__ */ jsx(
346
+ "button",
347
+ {
348
+ type: "submit",
349
+ className: classNames?.button ?? defaultButton,
350
+ children: submitLabel
351
+ }
352
+ )
353
+ ] });
354
+ }
355
+
356
+ export { BookingForm };
357
+ //# sourceMappingURL=index.js.map
358
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/BookingForm.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import React, { useState, useEffect, useCallback } from 'react';\nimport type { Question, FormValues, FormErrors, BookingFormProps, BookingFormClassNames } from '../types';\n\n/** Merge base classes with optional custom classes */\nconst cx = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ');\n\n/**\n * Renders a single form field based on question type\n */\nfunction FormField({\n question,\n value,\n error,\n onChange,\n classNames,\n}: {\n question: Question;\n value: string | number | boolean | undefined;\n error?: string;\n onChange: (value: string | number | boolean) => void;\n classNames?: BookingFormClassNames;\n}) {\n const inputId = `question-${question.id}`;\n const errorId = `${inputId}-error`;\n const hasError = !!error;\n\n // Default classes (can be overridden via classNames prop)\n const defaultFieldWrapper = 'mb-4';\n const defaultLabel = 'block text-sm font-medium mb-1 text-gray-700';\n const defaultHeading = 'text-lg font-semibold text-gray-900 mt-4 mb-2 first:mt-0';\n const defaultInput = 'w-full px-3 py-2 border rounded-md text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 border-gray-300 focus:ring-blue-500 focus:border-blue-500';\n const defaultInputError = 'border-red-500 focus:ring-red-500';\n const defaultCheckbox = 'mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500';\n const defaultHelpText = 'mt-1 text-xs text-gray-500';\n const defaultErrorText = 'mt-1 text-xs text-red-600';\n\n const inputClasses = hasError\n ? cx(classNames?.input ?? defaultInput, classNames?.inputError ?? defaultInputError)\n : cx(classNames?.input ?? defaultInput);\n\n const labelClasses = classNames?.label ?? defaultLabel;\n const checkboxLabelClasses = cx('text-sm', classNames?.label ?? 'text-gray-700');\n\n const renderLabel = () => {\n if (question.detail_type === 'heading') return null;\n return (\n <label htmlFor={inputId} className={labelClasses}>\n {question.name}\n {question.required && <span className=\"text-red-500 ml-1\" aria-hidden=\"true\">*</span>}\n </label>\n );\n };\n\n const renderHelpText = () => {\n if (!question.help_text) return null;\n return <p className={classNames?.helpText ?? defaultHelpText}>{question.help_text}</p>;\n };\n\n const renderError = () => {\n if (!error) return null;\n return (\n <p id={errorId} className={classNames?.errorText ?? defaultErrorText} role=\"alert\">\n {error}\n </p>\n );\n };\n\n const ariaProps = {\n 'aria-invalid': hasError ? true : undefined,\n 'aria-describedby': hasError ? errorId : undefined,\n 'aria-required': question.required || undefined,\n };\n\n switch (question.detail_type) {\n case 'heading':\n return (\n <h3 className={classNames?.heading ?? defaultHeading}>\n {question.name}\n </h3>\n );\n\n case 'text_field':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"text\"\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n placeholder={question.settings?.placeholder}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'text_area':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <textarea\n id={inputId}\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n placeholder={question.settings?.placeholder}\n rows={4}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'select':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <select\n id={inputId}\n value={(value as string | number) ?? ''}\n onChange={(e) => onChange(e.target.value)}\n className={inputClasses}\n {...ariaProps}\n >\n <option value=\"\">Select an option</option>\n {question.options?.map((option) => (\n <option key={option.id} value={option.id}>\n {option.name}\n </option>\n ))}\n </select>\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'date':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"date\"\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n min={question.settings?.min?.toString()}\n max={question.settings?.max?.toString()}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'number':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"number\"\n value={(value as number) ?? ''}\n onChange={(e) => onChange(e.target.value ? Number(e.target.value) : '')}\n min={question.settings?.min}\n max={question.settings?.max}\n placeholder={question.settings?.placeholder}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'check':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n <div className=\"flex items-start gap-2\">\n <input\n id={inputId}\n type=\"checkbox\"\n checked={!!value}\n onChange={(e) => onChange(e.target.checked)}\n className={classNames?.checkbox ?? defaultCheckbox}\n {...ariaProps}\n />\n <label htmlFor={inputId} className={checkboxLabelClasses}>\n {question.name}\n {question.required && <span className=\"text-red-500 ml-1\" aria-hidden=\"true\">*</span>}\n </label>\n </div>\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n default:\n return null;\n }\n}\n\n/**\n * BookingForm - A dynamic form component for booking questions\n *\n * @example\n * ```tsx\n * const questions: Question[] = [\n * { id: 1, name: 'Personal Details', detail_type: 'heading' },\n * { id: 2, name: 'Full Name', detail_type: 'text_field', required: true },\n * { id: 3, name: 'Email', detail_type: 'text_field', required: true },\n * ];\n *\n * <BookingForm\n * questions={questions}\n * onSubmit={(values) => console.log(values)}\n * />\n * ```\n */\nexport function BookingForm({\n questions,\n onSubmit,\n submitLabel = 'Submit',\n className = '',\n labelClassName,\n classNames: classNamesProp,\n}: BookingFormProps) {\n // Merge deprecated labelClassName into classNames for backwards compatibility\n const classNames: BookingFormClassNames = {\n ...classNamesProp,\n label: classNamesProp?.label ?? labelClassName,\n };\n const [values, setValues] = useState<FormValues>({});\n const [errors, setErrors] = useState<FormErrors>({});\n const [touched, setTouched] = useState<Record<number, boolean>>({});\n\n\n // Check if a question should be visible based on conditional settings\n const isQuestionVisible = useCallback(\n (question: Question): boolean => {\n const { settings } = question;\n if (!settings?.conditional_answers) {\n return true;\n }\n\n // Check all conditions in conditional_answers\n // Each entry is { questionId: expectedAnswerValue }\n const conditionEntries = Object.entries(settings.conditional_answers as Record<string, unknown>);\n if (conditionEntries.length === 0) {\n return true;\n }\n\n // Question is visible if ANY condition is met\n return conditionEntries.some(([questionIdStr, expectedAnswer]) => {\n const questionId = Number(questionIdStr);\n const currentValue = (values as Record<string, unknown>)[String(questionId)];\n\n if (currentValue === undefined || currentValue === '' || currentValue === null) {\n return false;\n }\n\n // Selects typically store the option id (as string), but allow `{ id, name }` objects too\n const normalizedCurrent =\n typeof currentValue === 'object' && currentValue !== null && 'id' in (currentValue as any)\n ? (currentValue as any)\n : currentValue;\n\n // Build a set of candidate strings for comparison.\n // For select questions we support matching by BOTH option id and option name.\n const candidates = new Set<string>();\n\n const addCandidate = (v: unknown) => {\n if (v === undefined || v === null) return;\n const s = String(v).trim();\n if (s) candidates.add(s);\n };\n\n const sourceQuestion = questions.find((q) => q.id === questionId);\n\n if (typeof normalizedCurrent === 'object' && normalizedCurrent !== null) {\n addCandidate((normalizedCurrent as any).id);\n addCandidate((normalizedCurrent as any).name);\n } else {\n addCandidate(normalizedCurrent);\n }\n\n if (sourceQuestion?.detail_type === 'select' && sourceQuestion.options?.length) {\n // Try to resolve the selected option via id or name.\n const currentStr = [...candidates][0] ?? '';\n const optionById = sourceQuestion.options.find((o) => String(o.id) === currentStr);\n const optionByName = sourceQuestion.options.find((o) => String(o.name).trim() === currentStr);\n const opt = optionById ?? optionByName;\n if (opt) {\n addCandidate(opt.id);\n addCandidate(opt.name);\n }\n }\n\n const matchesScalar = (expected: unknown) => {\n const expectedStr = String(expected).trim();\n return candidates.has(expectedStr);\n };\n\n // Support multiple expected formats:\n // - scalar: \"Consultation\" / \"1\" / 1 / true\n // - array: [\"Consultation\", \"Follow-up\"]\n // - map/object: { \"Consultation\": true }\n if (Array.isArray(expectedAnswer)) {\n return expectedAnswer.some((v) => matchesScalar(v));\n }\n\n if (expectedAnswer && typeof expectedAnswer === 'object') {\n const expectedMap = expectedAnswer as Record<string, unknown>;\n for (const c of candidates) {\n if (Object.prototype.hasOwnProperty.call(expectedMap, c)) {\n const flag = expectedMap[c];\n return flag === undefined ? true : !!flag;\n }\n }\n return false;\n }\n\n return matchesScalar(expectedAnswer);\n });\n },\n [values, questions]\n );\n\n // Get visible questions\n const visibleQuestions = questions.filter(isQuestionVisible);\n\n\n // Validate a single field\n const validateField = useCallback((question: Question, value: FormValues[number]): string | null => {\n if (question.detail_type === 'heading') return null;\n\n if (question.required) {\n if (value === undefined || value === '' || value === null) {\n return `${question.name} is required`;\n }\n if (question.detail_type === 'check' && !value) {\n return `${question.name} must be checked`;\n }\n }\n\n if (question.detail_type === 'number' && value !== '' && value !== undefined) {\n const numValue = Number(value);\n if (question.settings?.min !== undefined && numValue < question.settings.min) {\n return `Minimum value is ${question.settings.min}`;\n }\n if (question.settings?.max !== undefined && numValue > question.settings.max) {\n return `Maximum value is ${question.settings.max}`;\n }\n }\n\n return null;\n }, []);\n\n // Validate all visible fields\n const validateAll = useCallback((): boolean => {\n const newErrors: FormErrors = {};\n let isValid = true;\n\n visibleQuestions.forEach((question) => {\n const error = validateField(question, values[question.id]);\n if (error) {\n newErrors[question.id] = error;\n isValid = false;\n }\n });\n\n setErrors(newErrors);\n return isValid;\n }, [visibleQuestions, values, validateField]);\n\n // Clear errors for hidden questions\n useEffect(() => {\n const visibleIds = new Set(visibleQuestions.map((q) => q.id));\n\n setErrors((prev) => {\n let changed = false;\n const next = { ...prev };\n\n Object.keys(next).forEach((idStr) => {\n const id = Number(idStr);\n if (!visibleIds.has(id)) {\n delete next[id];\n changed = true;\n }\n });\n\n return changed ? next : prev;\n });\n }, [visibleQuestions]);\n\n const handleChange = (questionId: number, value: string | number | boolean) => {\n setValues((prev) => ({ ...prev, [questionId]: value }));\n setTouched((prev) => ({ ...prev, [questionId]: true }));\n\n // Clear error on change if touched\n if (touched[questionId]) {\n const question = questions.find((q) => q.id === questionId);\n if (question) {\n const error = validateField(question, value);\n setErrors((prev) => {\n if (error) {\n return { ...prev, [questionId]: error };\n }\n const { [questionId]: _, ...rest } = prev;\n return rest;\n });\n }\n }\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n\n // Mark all fields as touched\n const allTouched: Record<number, boolean> = {};\n visibleQuestions.forEach((q) => {\n allTouched[q.id] = true;\n });\n setTouched(allTouched);\n\n if (validateAll()) {\n // Only include visible question values\n const visibleValues: FormValues = {};\n visibleQuestions.forEach((q) => {\n if (q.detail_type !== 'heading' && values[q.id] !== undefined) {\n visibleValues[q.id] = values[q.id];\n }\n });\n onSubmit(visibleValues);\n }\n };\n\n const defaultButton = 'w-full mt-4 px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors';\n\n return (\n <form onSubmit={handleSubmit} className={className} noValidate>\n {visibleQuestions.map((question) => (\n <FormField\n key={question.id}\n question={question}\n value={values[question.id]}\n error={touched[question.id] ? errors[question.id] : undefined}\n onChange={(value) => handleChange(question.id, value)}\n classNames={classNames}\n />\n ))}\n <button\n type=\"submit\"\n className={classNames?.button ?? defaultButton}\n >\n {submitLabel}\n </button>\n </form>\n );\n}\n\nexport default BookingForm;\n"]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@bookinglab/booking-ui-react",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "React UI components for BookingLab booking journeys",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "sideEffects": false,
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "keywords": [
23
+ "react",
24
+ "ui",
25
+ "components",
26
+ "booking",
27
+ "bookinglab",
28
+ "scheduling",
29
+ "appointments"
30
+ ],
31
+ "author": "BookingLab",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/bookinglab/booking-ui-react"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/bookinglab/booking-ui-react/issues"
39
+ },
40
+ "homepage": "https://github.com/bookinglab/booking-ui-react#readme",
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "dev": "tsup --watch",
44
+ "typecheck": "tsc --noEmit",
45
+ "prepublishOnly": "npm run build"
46
+ },
47
+ "peerDependencies": {
48
+ "react": ">=17.0.0",
49
+ "react-dom": ">=17.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/react": "^18.3.0",
53
+ "@types/react-dom": "^18.3.0",
54
+ "tsup": "^8.0.0",
55
+ "typescript": "^5.8.0"
56
+ }
57
+ }