@qaiddev/quests-embed 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 +21 -0
- package/README.md +462 -0
- package/dist/embed.d.ts +71 -0
- package/dist/index.d.ts +36 -0
- package/dist/inputs.d.ts +34 -0
- package/dist/qaid-quests.js +752 -0
- package/dist/qaid-quests.js.map +1 -0
- package/dist/qaid-quests.umd.cjs +2 -0
- package/dist/qaid-quests.umd.cjs.map +1 -0
- package/dist/styles.d.ts +22 -0
- package/dist/types.d.ts +225 -0
- package/dist/visibility.d.ts +15 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QAid
|
|
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,462 @@
|
|
|
1
|
+
# @qaiddev/quests-embed
|
|
2
|
+
|
|
3
|
+
A zero-dependency, lightweight questionnaire embed that renders step-by-step forms from a JSON definition. Drop it inline on a page or open it as a modal — answers are autosaved as the user types and submitted to your API endpoint or the [QAid.dev hosted dashboard](https://qaid.dev).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
### npm
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @qaiddev/quests-embed
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { QaidQuests } from '@qaiddev/quests-embed';
|
|
15
|
+
|
|
16
|
+
new QaidQuests({
|
|
17
|
+
endpoint: 'https://qaid.dev/api/responses',
|
|
18
|
+
apiKey: 'YOUR_API_KEY',
|
|
19
|
+
configUrl: '/forms/intake.json',
|
|
20
|
+
container: '#form',
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### CDN / Script Tag
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<div id="form"></div>
|
|
28
|
+
|
|
29
|
+
<script
|
|
30
|
+
src="https://unpkg.com/@qaiddev/quests-embed/dist/qaid-quests.umd.cjs"
|
|
31
|
+
data-endpoint="https://qaid.dev/api/responses"
|
|
32
|
+
data-api-key="YOUR_API_KEY"
|
|
33
|
+
data-config-url="/forms/intake.json"
|
|
34
|
+
data-container="#form"
|
|
35
|
+
></script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The embed auto-initializes when it detects a `data-endpoint` attribute on its script tag and either `data-config-url` or an inline JSON config.
|
|
39
|
+
|
|
40
|
+
### JSON Config (Script Tag)
|
|
41
|
+
|
|
42
|
+
For complex configurations or inline questionnaires, use a separate JSON config element:
|
|
43
|
+
|
|
44
|
+
```html
|
|
45
|
+
<script type="application/json" data-quests-config>
|
|
46
|
+
{
|
|
47
|
+
"endpoint": "https://qaid.dev/api/responses",
|
|
48
|
+
"apiKey": "YOUR_API_KEY",
|
|
49
|
+
"container": "#form",
|
|
50
|
+
"questionnaire": {
|
|
51
|
+
"title": "Tell us about your project",
|
|
52
|
+
"questions": [
|
|
53
|
+
{ "id": "name", "type": "text", "label": "Your name", "required": true },
|
|
54
|
+
{ "id": "budget", "type": "currency", "label": "Project budget", "currency": "USD" },
|
|
55
|
+
{
|
|
56
|
+
"id": "stack",
|
|
57
|
+
"type": "multiple-choice",
|
|
58
|
+
"label": "Which stack are you using?",
|
|
59
|
+
"options": [
|
|
60
|
+
{ "value": "react", "label": "React" },
|
|
61
|
+
{ "value": "vue", "label": "Vue" },
|
|
62
|
+
{ "value": "svelte", "label": "Svelte" }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
<script src="https://unpkg.com/@qaiddev/quests-embed/dist/qaid-quests.umd.cjs"></script>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## How It Works
|
|
73
|
+
|
|
74
|
+
1. The embed loads the questionnaire — either inline via `questionnaire`, or fetched from `configUrl`
|
|
75
|
+
2. A response record is created on your server (`POST` to `endpoint`)
|
|
76
|
+
3. Questions render one at a time with a progress bar and Back/Next navigation
|
|
77
|
+
4. Answers autosave as the user moves between steps (`PATCH` per question). Text and currency fields debounce by `saveDebounceMs` (default 500ms)
|
|
78
|
+
5. On the final step, all answers are submitted in a single batch (`POST` to `{endpoint}/{id}/submit`)
|
|
79
|
+
6. A "thank you" screen appears when finished
|
|
80
|
+
|
|
81
|
+
If `container` is set, the form renders inline inside that element. If not, it opens as a centered modal with a backdrop.
|
|
82
|
+
|
|
83
|
+
## Questionnaire Definition
|
|
84
|
+
|
|
85
|
+
A `Questionnaire` is the JSON document that describes the form. It can be loaded inline via the `questionnaire` config option, or fetched from a URL via `configUrl`.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
interface Questionnaire {
|
|
89
|
+
id?: string; // Stable identifier sent with createResponse
|
|
90
|
+
title?: string; // Shown at the top of the form
|
|
91
|
+
description?: string; // Shown under the title
|
|
92
|
+
thankYouTitle?: string; // Default "Thank you!"
|
|
93
|
+
thankYouMessage?: string; // Optional subtitle on the done screen
|
|
94
|
+
submitLabel?: string; // Default "Submit"
|
|
95
|
+
nextLabel?: string; // Default "Next"
|
|
96
|
+
backLabel?: string; // Default "Back"
|
|
97
|
+
questions: Question[];
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Question Types
|
|
102
|
+
|
|
103
|
+
All questions share these base fields:
|
|
104
|
+
|
|
105
|
+
| Field | Type | Description |
|
|
106
|
+
|-------|------|-------------|
|
|
107
|
+
| `id` | `string` | **(required)** Stable answer key |
|
|
108
|
+
| `label` | `string` | **(required)** Question label |
|
|
109
|
+
| `description` | `string` | Optional helper text |
|
|
110
|
+
| `required` | `boolean` | Whether an answer is required to advance |
|
|
111
|
+
|
|
112
|
+
Plus a `type` discriminator that selects one of:
|
|
113
|
+
|
|
114
|
+
#### Text — `type: "text"`
|
|
115
|
+
|
|
116
|
+
| Field | Type | Description |
|
|
117
|
+
|-------|------|-------------|
|
|
118
|
+
| `placeholder` | `string` | Input placeholder |
|
|
119
|
+
| `multiline` | `boolean` | Use a textarea instead of a single-line input |
|
|
120
|
+
| `maxLength` | `number` | Max length |
|
|
121
|
+
| `minLength` | `number` | Min length (only enforced when `required`) |
|
|
122
|
+
| `inputType` | `"text" \| "email" \| "tel" \| "url"` | Native input type for short fields. Default `"text"` |
|
|
123
|
+
|
|
124
|
+
#### Currency — `type: "currency"`
|
|
125
|
+
|
|
126
|
+
| Field | Type | Description |
|
|
127
|
+
|-------|------|-------------|
|
|
128
|
+
| `currency` | `string` | ISO currency code, e.g. `"USD"`. Default `"USD"` |
|
|
129
|
+
| `min` | `number` | Minimum value |
|
|
130
|
+
| `max` | `number` | Maximum value |
|
|
131
|
+
| `locale` | `string` | Locale for number formatting. Default browser locale |
|
|
132
|
+
| `placeholder` | `string` | Input placeholder |
|
|
133
|
+
|
|
134
|
+
#### Range — `type: "range"`
|
|
135
|
+
|
|
136
|
+
| Field | Type | Description |
|
|
137
|
+
|-------|------|-------------|
|
|
138
|
+
| `min` | `number` | **(required)** Minimum value |
|
|
139
|
+
| `max` | `number` | **(required)** Maximum value |
|
|
140
|
+
| `step` | `number` | Step size. Default `1` |
|
|
141
|
+
| `defaultValue` | `number` | Initial value when no answer yet. Default `min` |
|
|
142
|
+
| `unit` | `string` | Suffix shown next to the value (e.g. `"/10"`, `"%"`) |
|
|
143
|
+
|
|
144
|
+
#### Date — `type: "date"`
|
|
145
|
+
|
|
146
|
+
| Field | Type | Description |
|
|
147
|
+
|-------|------|-------------|
|
|
148
|
+
| `min` | `string` | ISO date string for the earliest allowed date |
|
|
149
|
+
| `max` | `string` | ISO date string for the latest allowed date |
|
|
150
|
+
|
|
151
|
+
#### Multiple Choice — `type: "multiple-choice"`
|
|
152
|
+
|
|
153
|
+
| Field | Type | Description |
|
|
154
|
+
|-------|------|-------------|
|
|
155
|
+
| `options` | `MultipleChoiceOption[]` | **(required)** List of options |
|
|
156
|
+
| `multiple` | `boolean` | Allow multiple selections. Answer becomes `string[]`. Default `false` |
|
|
157
|
+
|
|
158
|
+
Each option has `value`, `label`, and optional `description`.
|
|
159
|
+
|
|
160
|
+
### Example questionnaire JSON
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"id": "intake-2026",
|
|
165
|
+
"title": "Project intake",
|
|
166
|
+
"description": "Tell us a bit about your project. Takes about 2 minutes.",
|
|
167
|
+
"submitLabel": "Submit",
|
|
168
|
+
"thankYouTitle": "Got it!",
|
|
169
|
+
"thankYouMessage": "We'll be in touch within one business day.",
|
|
170
|
+
"questions": [
|
|
171
|
+
{
|
|
172
|
+
"id": "name",
|
|
173
|
+
"type": "text",
|
|
174
|
+
"label": "Your name",
|
|
175
|
+
"required": true,
|
|
176
|
+
"placeholder": "Jane Smith"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"id": "email",
|
|
180
|
+
"type": "text",
|
|
181
|
+
"label": "Best email to reach you",
|
|
182
|
+
"inputType": "email",
|
|
183
|
+
"required": true
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"id": "budget",
|
|
187
|
+
"type": "currency",
|
|
188
|
+
"label": "What's your budget?",
|
|
189
|
+
"currency": "USD",
|
|
190
|
+
"min": 0
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"id": "timeline",
|
|
194
|
+
"type": "range",
|
|
195
|
+
"label": "How urgent is this?",
|
|
196
|
+
"min": 1,
|
|
197
|
+
"max": 10,
|
|
198
|
+
"defaultValue": 5,
|
|
199
|
+
"unit": "/10"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"id": "start",
|
|
203
|
+
"type": "date",
|
|
204
|
+
"label": "When would you like to start?"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"id": "needs",
|
|
208
|
+
"type": "multiple-choice",
|
|
209
|
+
"label": "What do you need help with?",
|
|
210
|
+
"multiple": true,
|
|
211
|
+
"options": [
|
|
212
|
+
{ "value": "design", "label": "Design" },
|
|
213
|
+
{ "value": "engineering", "label": "Engineering" },
|
|
214
|
+
{ "value": "strategy", "label": "Strategy" }
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Configuration
|
|
222
|
+
|
|
223
|
+
All options are optional except `endpoint` and one of `questionnaire` / `configUrl`.
|
|
224
|
+
|
|
225
|
+
We offer a Free Plan that hosts both the endpoint and a dashboard for managing your form responses.
|
|
226
|
+
|
|
227
|
+
### Core Options
|
|
228
|
+
|
|
229
|
+
| Option | Type | Default | Description |
|
|
230
|
+
|--------|------|---------|-------------|
|
|
231
|
+
| `endpoint` | `string` | **(required)** | API endpoint URL for storing answers |
|
|
232
|
+
| `questionnaire` | `Questionnaire` | — | Inline questionnaire definition (takes precedence over `configUrl`) |
|
|
233
|
+
| `configUrl` | `string` | — | URL to fetch the questionnaire JSON from |
|
|
234
|
+
| `apiKey` | `string` | `""` | API key for authenticating with the service |
|
|
235
|
+
| `container` | `string` | `""` | CSS selector for a host element. If absent, the form opens as a modal |
|
|
236
|
+
| `zIndex` | `number` | `50` | z-index for modal mode |
|
|
237
|
+
|
|
238
|
+
### Behavior
|
|
239
|
+
|
|
240
|
+
| Option | Type | Default | Description |
|
|
241
|
+
|--------|------|---------|-------------|
|
|
242
|
+
| `autoAdvance` | `boolean` | `false` | Advance automatically on selection for single-choice multiple-choice and range questions |
|
|
243
|
+
| `saveDebounceMs` | `number` | `500` | Debounce in ms for autosave on text/currency/range |
|
|
244
|
+
| `autoFocus` | `boolean` | `true` | Auto-focus the input on each step. Set `false` in preview/embedded contexts that shouldn't steal focus |
|
|
245
|
+
|
|
246
|
+
### Appearance
|
|
247
|
+
|
|
248
|
+
| Option | Type | Default | Description |
|
|
249
|
+
|--------|------|---------|-------------|
|
|
250
|
+
| `modalWidth` | `number` | `480` | Width of the modal in pixels |
|
|
251
|
+
| `backdropOpacity` | `number` | `0.4` | Opacity of the dark backdrop in modal mode (0-1) |
|
|
252
|
+
| `fontFamily` | `string` | `"system-ui, -apple-system, sans-serif"` | Font family for all text |
|
|
253
|
+
| `fontSize` | `number` | `16` | Base font size in pixels |
|
|
254
|
+
| `css` | `string` | `""` | Custom CSS injected into the shadow root for theming |
|
|
255
|
+
|
|
256
|
+
### Colors
|
|
257
|
+
|
|
258
|
+
Pass a `colors` object to customize the color scheme. Variable names are shared with `@qaiddev/thumbs-embed`, so themes written for one embed render identically in the other:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
new QaidQuests({
|
|
262
|
+
endpoint: '/api/responses',
|
|
263
|
+
configUrl: '/forms/intake.json',
|
|
264
|
+
colors: {
|
|
265
|
+
positive: 'rgb(0, 200, 83)', // Progress bar, focus rings, submit button
|
|
266
|
+
negative: 'rgb(255, 0, 0)', // Validation errors
|
|
267
|
+
marker: '#6366f1', // Selection / highlight
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Colors accept hex (`#ABC`, `#AABBCC`) or `rgb(r, g, b)` format. The text color used on top of `positive` and `marker` is computed automatically based on luminance.
|
|
273
|
+
|
|
274
|
+
## Script Tag Data Attributes
|
|
275
|
+
|
|
276
|
+
When using the script tag method, all config options are available as `data-*` attributes:
|
|
277
|
+
|
|
278
|
+
| Attribute | Maps To |
|
|
279
|
+
|-----------|---------|
|
|
280
|
+
| `data-endpoint` | `endpoint` |
|
|
281
|
+
| `data-config-url` | `configUrl` |
|
|
282
|
+
| `data-api-key` | `apiKey` |
|
|
283
|
+
| `data-container` | `container` |
|
|
284
|
+
| `data-zindex` | `zIndex` |
|
|
285
|
+
| `data-positive-color` | `colors.positive` |
|
|
286
|
+
| `data-negative-color` | `colors.negative` |
|
|
287
|
+
| `data-marker-color` | `colors.marker` |
|
|
288
|
+
| `data-modal-width` | `modalWidth` |
|
|
289
|
+
| `data-backdrop-opacity` | `backdropOpacity` |
|
|
290
|
+
| `data-font-family` | `fontFamily` |
|
|
291
|
+
| `data-font-size` | `fontSize` |
|
|
292
|
+
| `data-auto-advance` | `autoAdvance` (`"true"` to enable) |
|
|
293
|
+
| `data-save-debounce-ms` | `saveDebounceMs` |
|
|
294
|
+
| `data-css-selector` | CSS selector for an element whose `textContent` is used as `css` |
|
|
295
|
+
|
|
296
|
+
## Modal vs. Inline
|
|
297
|
+
|
|
298
|
+
By default the embed opens as a fixed-position modal centered on the viewport. To render the form inline inside your own element, pass `container`:
|
|
299
|
+
|
|
300
|
+
```html
|
|
301
|
+
<div id="my-form-spot"></div>
|
|
302
|
+
|
|
303
|
+
<script>
|
|
304
|
+
new QaidQuests({
|
|
305
|
+
endpoint: '/api/responses',
|
|
306
|
+
configUrl: '/forms/intake.json',
|
|
307
|
+
container: '#my-form-spot',
|
|
308
|
+
});
|
|
309
|
+
</script>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
When `container` is set, `zIndex` is ignored and the backdrop / close button are not rendered. You control the layout.
|
|
313
|
+
|
|
314
|
+
## API
|
|
315
|
+
|
|
316
|
+
### Constructor
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
const quests = new QaidQuests(config: QuestsConfig);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Methods
|
|
323
|
+
|
|
324
|
+
| Method | Description |
|
|
325
|
+
|--------|-------------|
|
|
326
|
+
| `getAnswers()` | Read-only snapshot of the answers collected so far, keyed by question id |
|
|
327
|
+
| `destroy()` | Remove all DOM elements, event listeners, and injected styles. Safe to call multiple times |
|
|
328
|
+
|
|
329
|
+
### Server Protocol
|
|
330
|
+
|
|
331
|
+
The embed talks to your endpoint in three steps:
|
|
332
|
+
|
|
333
|
+
**1. Create response** — `POST {endpoint}`:
|
|
334
|
+
|
|
335
|
+
```json
|
|
336
|
+
{
|
|
337
|
+
"apiKey": "YOUR_API_KEY",
|
|
338
|
+
"questId": "intake-2026",
|
|
339
|
+
"pageUrl": "https://example.com/contact",
|
|
340
|
+
"visitorId": "a1b2c3d4-...",
|
|
341
|
+
"userAgent": "Mozilla/5.0 ..."
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Your endpoint should return `{ "id": "..." }` (string or number). The form is usable immediately while creation is in flight — saves queue until the id arrives.
|
|
346
|
+
|
|
347
|
+
**2. Save answer** — `PATCH {endpoint}/{id}` (one request per question, debounced for text/currency/range):
|
|
348
|
+
|
|
349
|
+
```json
|
|
350
|
+
{
|
|
351
|
+
"questionId": "budget",
|
|
352
|
+
"value": 5000
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`value` is `string | number | string[] | null` depending on the question type.
|
|
357
|
+
|
|
358
|
+
**3. Submit** — `POST {endpoint}/{id}/submit`:
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
{
|
|
362
|
+
"answers": {
|
|
363
|
+
"name": "Jane Smith",
|
|
364
|
+
"budget": 5000,
|
|
365
|
+
"needs": ["design", "engineering"]
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
When an `apiKey` is configured, it's also sent as an `X-API-Key` header on every request.
|
|
371
|
+
|
|
372
|
+
## Answer Types
|
|
373
|
+
|
|
374
|
+
| Question type | Answer value type |
|
|
375
|
+
|---------------|-------------------|
|
|
376
|
+
| `text` | `string` |
|
|
377
|
+
| `currency` | `number` |
|
|
378
|
+
| `range` | `number` |
|
|
379
|
+
| `date` | `string` (ISO date) |
|
|
380
|
+
| `multiple-choice` (single) | `string` |
|
|
381
|
+
| `multiple-choice` (multiple) | `string[]` |
|
|
382
|
+
|
|
383
|
+
Unanswered questions are stored as `null`.
|
|
384
|
+
|
|
385
|
+
## Visitor IDs
|
|
386
|
+
|
|
387
|
+
The embed generates a stable `visitorId` (UUID) per browser, stored in `localStorage` under the `qaid_visitor_id` key. It's sent with every `createResponse` call so you can correlate multiple responses from the same visitor. If `localStorage` is unavailable, a fresh UUID is generated per session.
|
|
388
|
+
|
|
389
|
+
## Keyboard Behavior
|
|
390
|
+
|
|
391
|
+
- **Enter** — advance to the next question, or submit on the final step
|
|
392
|
+
- **Escape** — close the form (modal mode only; ignored when `container` is set)
|
|
393
|
+
- **Tab / Shift+Tab** — move focus between inputs and buttons
|
|
394
|
+
|
|
395
|
+
Each step auto-focuses its input on render so keyboard and screen-reader users land on the active question. Set `autoFocus: false` on the initial mount to opt out.
|
|
396
|
+
|
|
397
|
+
## Theming
|
|
398
|
+
|
|
399
|
+
The embed renders inside a Shadow DOM, so your page's CSS can't leak in. To theme it, pass a `css` string that's injected into the shadow root, or override the CSS custom properties:
|
|
400
|
+
|
|
401
|
+
```css
|
|
402
|
+
:root {
|
|
403
|
+
--qaid-positive: rgb(0, 200, 83);
|
|
404
|
+
--qaid-negative: rgb(255, 0, 0);
|
|
405
|
+
--qaid-marker: #6366f1;
|
|
406
|
+
--qaid-modal-width: 480px;
|
|
407
|
+
--qaid-backdrop-opacity: 0.4;
|
|
408
|
+
--qaid-font-family: system-ui, -apple-system, sans-serif;
|
|
409
|
+
--qaid-font-size: 16px;
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
The variable names are shared with `@qaiddev/thumbs-embed`, so a single theme can drive both embeds.
|
|
414
|
+
|
|
415
|
+
## Hosted Dashboard
|
|
416
|
+
|
|
417
|
+
[Qaid](https://qaid.dev) provides a hosted dashboard for managing responses collected by this embed:
|
|
418
|
+
|
|
419
|
+
- **Response inbox** with answers, timestamps, and visitor history
|
|
420
|
+
- **Form builder** for editing questionnaire JSON without redeploying
|
|
421
|
+
- **Project management** with multiple API keys
|
|
422
|
+
- **Team collaboration** with owners, members, and invitations
|
|
423
|
+
- **Analytics** and data retention
|
|
424
|
+
|
|
425
|
+
### Plans
|
|
426
|
+
|
|
427
|
+
| | Cadet (Free) | Commander ($9/mo) | Admiral (Enterprise) |
|
|
428
|
+
|---|---|---|---|
|
|
429
|
+
| Projects | 1 | Unlimited | Unlimited |
|
|
430
|
+
| Responses | 100/mo | Unlimited | Unlimited |
|
|
431
|
+
| Data retention | 7 days | 1 year | Unlimited |
|
|
432
|
+
| Notifications | - | Email/Text | Email/Text |
|
|
433
|
+
| Integrations | - | GitHub + more | Custom |
|
|
434
|
+
|
|
435
|
+
Sign up at [qaid.dev](https://qaid.dev). You can also self-host — the embed works with any endpoint that accepts the protocol described above.
|
|
436
|
+
|
|
437
|
+
## Self-Hosting
|
|
438
|
+
|
|
439
|
+
The embed is endpoint-agnostic. Point it at your own server:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
new QaidQuests({
|
|
443
|
+
endpoint: 'https://your-server.com/api/responses',
|
|
444
|
+
configUrl: '/forms/intake.json',
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Your server needs to handle:
|
|
449
|
+
|
|
450
|
+
1. `POST /api/responses` — Accept the create-response payload, return `{ "id": string | number }`
|
|
451
|
+
2. `PATCH /api/responses/:id` — Accept `{ "questionId": string, "value": ... }` to save a single answer
|
|
452
|
+
3. `POST /api/responses/:id/submit` — Accept `{ "answers": Record<string, ...> }` for the final batch
|
|
453
|
+
|
|
454
|
+
## Project Badges
|
|
455
|
+
|
|
456
|
+
<a href="https://www.npmjs.com/package/@qaiddev/quests-embed">
|
|
457
|
+
<img alt="npm version" src="https://img.shields.io/npm/v/@qaiddev/quests-embed" />
|
|
458
|
+
</a>
|
|
459
|
+
|
|
460
|
+
<a href="https://github.com/qaiddev/quests-embed/blob/prod/LICENSE">
|
|
461
|
+
<img alt="license" src="https://img.shields.io/npm/l/@qaiddev/quests-embed"/>
|
|
462
|
+
</a>
|
package/dist/embed.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Answers, QuestsConfig } from "./types";
|
|
2
|
+
export declare class QaidQuests {
|
|
3
|
+
private config;
|
|
4
|
+
private questionnaire;
|
|
5
|
+
private inlineQuestionnaire;
|
|
6
|
+
private configUrl;
|
|
7
|
+
private state;
|
|
8
|
+
private stepIndex;
|
|
9
|
+
private visibleQuestions;
|
|
10
|
+
private pendingGoToStep;
|
|
11
|
+
private hasRenderedStep;
|
|
12
|
+
private answers;
|
|
13
|
+
private responseId;
|
|
14
|
+
private visitorId;
|
|
15
|
+
private pendingSaveTimer;
|
|
16
|
+
private pendingSaveQuestionId;
|
|
17
|
+
private pendingSaveValue;
|
|
18
|
+
private inflightSaves;
|
|
19
|
+
private shadowHost;
|
|
20
|
+
private shadowRoot;
|
|
21
|
+
private isUserContainer;
|
|
22
|
+
private rootEl;
|
|
23
|
+
private cardEl;
|
|
24
|
+
private titleEl;
|
|
25
|
+
private stepCounterEl;
|
|
26
|
+
private progressEl;
|
|
27
|
+
private progressFillEl;
|
|
28
|
+
private bodyEl;
|
|
29
|
+
private footerEl;
|
|
30
|
+
private savingEl;
|
|
31
|
+
private backdropEl;
|
|
32
|
+
private currentInput;
|
|
33
|
+
private boundKeyDown;
|
|
34
|
+
private cssVars;
|
|
35
|
+
constructor(config: QuestsConfig);
|
|
36
|
+
private init;
|
|
37
|
+
private loadQuestionnaire;
|
|
38
|
+
private mountShell;
|
|
39
|
+
private renderLoading;
|
|
40
|
+
private renderError;
|
|
41
|
+
private renderHeader;
|
|
42
|
+
private renderStep;
|
|
43
|
+
private renderDone;
|
|
44
|
+
private advance;
|
|
45
|
+
private back;
|
|
46
|
+
private recomputeVisible;
|
|
47
|
+
private handleAnswerChange;
|
|
48
|
+
private flushPendingSave;
|
|
49
|
+
private saveAnswer;
|
|
50
|
+
private doSave;
|
|
51
|
+
private createResponse;
|
|
52
|
+
private submit;
|
|
53
|
+
private jsonHeaders;
|
|
54
|
+
private showSaving;
|
|
55
|
+
private handleKeyDown;
|
|
56
|
+
private close;
|
|
57
|
+
/** Destroy the embed and clean up all resources */
|
|
58
|
+
destroy(): void;
|
|
59
|
+
/** Read-only snapshot of current answers */
|
|
60
|
+
getAnswers(): Answers;
|
|
61
|
+
/**
|
|
62
|
+
* Jump to the visible question with the given id. Returns true if
|
|
63
|
+
* the question is currently visible (and the embed navigated to
|
|
64
|
+
* it), false if it's hidden by an unmet `visibleIf` predicate or
|
|
65
|
+
* unknown. If the embed is still initializing, the request is
|
|
66
|
+
* latched and applied as soon as the questionnaire is ready.
|
|
67
|
+
*
|
|
68
|
+
* Intended for editor previews and other host-driven step control.
|
|
69
|
+
*/
|
|
70
|
+
goToStep(questionId: string): boolean;
|
|
71
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @qaiddev/quests-embed
|
|
3
|
+
*
|
|
4
|
+
* Step-by-step JSON-defined form embed.
|
|
5
|
+
*
|
|
6
|
+
* Usage as ES module:
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { QaidQuests } from "@qaiddev/quests-embed";
|
|
9
|
+
*
|
|
10
|
+
* new QaidQuests({
|
|
11
|
+
* endpoint: "/api/responses",
|
|
12
|
+
* configUrl: "/forms/intake.json",
|
|
13
|
+
* container: "#form",
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Usage via script tag (auto-init):
|
|
18
|
+
* ```html
|
|
19
|
+
* <script src="qaid-quests.umd.cjs"
|
|
20
|
+
* data-endpoint="/api/responses"
|
|
21
|
+
* data-config-url="/forms/intake.json"
|
|
22
|
+
* data-container="#form"></script>
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Or with a JSON config tag:
|
|
26
|
+
* ```html
|
|
27
|
+
* <script type="application/json" data-quests-config>
|
|
28
|
+
* { "endpoint": "/api/responses", "configUrl": "/forms/intake.json" }
|
|
29
|
+
* </script>
|
|
30
|
+
* <script src="qaid-quests.umd.cjs"></script>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import { QaidQuests } from "./embed";
|
|
34
|
+
export { QaidQuests };
|
|
35
|
+
export { getVisibleQuestions, evaluateRule } from "./visibility";
|
|
36
|
+
export type { QuestsConfig, ResolvedQuestsConfig, Questionnaire, Question, TextQuestion, CurrencyQuestion, RangeQuestion, DateQuestion, MultipleChoiceQuestion, MultipleChoiceOption, Answers, AnswerValue, VisibilityRule, CreateResponsePayload, CreateResponseResult, UpdateAnswerPayload, SubmitPayload, } from "./types";
|
package/dist/inputs.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input renderers for each question type.
|
|
3
|
+
*
|
|
4
|
+
* Each renderer returns a small object with an element to mount, a
|
|
5
|
+
* focus helper, a synchronous current-value reader, and a validator.
|
|
6
|
+
*
|
|
7
|
+
* Per-keystroke autosave is wired by the embed: the renderer just
|
|
8
|
+
* calls back into onChange whenever its underlying value changes.
|
|
9
|
+
* Renderers also call onSubmit when the user presses Enter (or
|
|
10
|
+
* onAutoAdvance after a discrete selection that should auto-advance).
|
|
11
|
+
*/
|
|
12
|
+
import type { AnswerValue, Question } from "./types";
|
|
13
|
+
export interface QuestionInput {
|
|
14
|
+
element: HTMLElement;
|
|
15
|
+
focus(): void;
|
|
16
|
+
getValue(): AnswerValue;
|
|
17
|
+
isValid(): boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface QuestionInputOptions {
|
|
20
|
+
question: Question;
|
|
21
|
+
initialValue: AnswerValue;
|
|
22
|
+
/** Called any time the value changes (every keystroke for text). */
|
|
23
|
+
onChange: (value: AnswerValue) => void;
|
|
24
|
+
/** Called when the user presses Enter to advance. */
|
|
25
|
+
onSubmit: () => void;
|
|
26
|
+
/**
|
|
27
|
+
* Called after a discrete selection that should auto-advance
|
|
28
|
+
* (e.g. picking a single-choice option when autoAdvance is on).
|
|
29
|
+
*/
|
|
30
|
+
onAutoAdvance: () => void;
|
|
31
|
+
/** Whether the embed is configured to auto-advance after discrete selections */
|
|
32
|
+
autoAdvance: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function createInput(options: QuestionInputOptions): QuestionInput;
|