@syntrologie/adapt-feedback 2.8.0-canary.239

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.
@@ -0,0 +1,880 @@
1
+ // src/FeedbackWidget.ts
2
+ import { css as css7, html as html7, LitElement as LitElement7, nothing } from "lit";
3
+
4
+ // src/engine/FlowEngine.ts
5
+ var FlowEngineValidationError = class extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "FlowEngineValidationError";
9
+ }
10
+ };
11
+ var FlowEngine = class {
12
+ constructor(steps) {
13
+ this._answers = /* @__PURE__ */ new Map();
14
+ this._validate(steps);
15
+ this._steps = steps;
16
+ this._stepMap = new Map(steps.map((s) => [s.id, s]));
17
+ this._path = [steps[0].id];
18
+ }
19
+ get currentStep() {
20
+ const id = this._path[this._path.length - 1];
21
+ return this._stepMap.get(id);
22
+ }
23
+ get answers() {
24
+ return this._answers;
25
+ }
26
+ get path() {
27
+ return this._path;
28
+ }
29
+ get isComplete() {
30
+ return this.currentStep.type === "end";
31
+ }
32
+ get stepsCompleted() {
33
+ return this._path.filter((id) => this._answers.has(id)).length;
34
+ }
35
+ get stepIndex() {
36
+ return this._path.length - 1;
37
+ }
38
+ getStep(id) {
39
+ return this._stepMap.get(id);
40
+ }
41
+ answer(value) {
42
+ const step = this.currentStep;
43
+ if (step.type === "end") return;
44
+ this._answers.set(step.id, value);
45
+ const nextId = this._resolveNext(step, value);
46
+ const currentIdx = this._path.length - 1;
47
+ this._path = this._path.slice(0, currentIdx + 1);
48
+ this._path.push(nextId);
49
+ }
50
+ goBack() {
51
+ if (this._path.length <= 1) return;
52
+ this._path = this._path.slice(0, -1);
53
+ }
54
+ _resolveNext(step, value) {
55
+ if (step.next === void 0) {
56
+ return this._nextInArray(step.id);
57
+ }
58
+ if (typeof step.next === "string") {
59
+ return step.next;
60
+ }
61
+ for (const rule of step.next) {
62
+ if (this._evaluateRule(rule, value)) {
63
+ return rule.goto;
64
+ }
65
+ }
66
+ return this._nextInArray(step.id);
67
+ }
68
+ _nextInArray(currentId) {
69
+ const idx = this._steps.findIndex((s) => s.id === currentId);
70
+ return this._steps[idx + 1].id;
71
+ }
72
+ _evaluateRule(rule, value) {
73
+ const { op, value: ruleValue } = rule.if;
74
+ switch (op) {
75
+ case "eq":
76
+ return value === ruleValue;
77
+ case "neq":
78
+ return value !== ruleValue;
79
+ case "lt":
80
+ return Number(value) < Number(ruleValue);
81
+ case "lte":
82
+ return Number(value) <= Number(ruleValue);
83
+ case "gt":
84
+ return Number(value) > Number(ruleValue);
85
+ case "gte":
86
+ return Number(value) >= Number(ruleValue);
87
+ case "in":
88
+ return Array.isArray(ruleValue) && ruleValue.includes(String(value));
89
+ default:
90
+ return false;
91
+ }
92
+ }
93
+ _validate(steps) {
94
+ if (steps.length === 0) {
95
+ throw new FlowEngineValidationError("Steps array must not be empty");
96
+ }
97
+ const ids = /* @__PURE__ */ new Set();
98
+ for (const step of steps) {
99
+ if (ids.has(step.id)) {
100
+ throw new FlowEngineValidationError(`Duplicate step ID: "${step.id}"`);
101
+ }
102
+ ids.add(step.id);
103
+ }
104
+ const hasEnd = steps.some((s) => s.type === "end");
105
+ if (!hasEnd) {
106
+ throw new FlowEngineValidationError('At least one step must have type "end"');
107
+ }
108
+ for (let i = 0; i < steps.length; i++) {
109
+ const step = steps[i];
110
+ if (typeof step.next === "string") {
111
+ if (!ids.has(step.next)) {
112
+ throw new FlowEngineValidationError(
113
+ `Step "${step.id}" has next target "${step.next}" which does not exist`
114
+ );
115
+ }
116
+ } else if (Array.isArray(step.next)) {
117
+ for (const rule of step.next) {
118
+ if (!ids.has(rule.goto)) {
119
+ throw new FlowEngineValidationError(
120
+ `Step "${step.id}" has branch rule targeting "${rule.goto}" which does not exist`
121
+ );
122
+ }
123
+ }
124
+ }
125
+ if (step.type !== "end" && step.next === void 0 && i === steps.length - 1) {
126
+ throw new FlowEngineValidationError(
127
+ `Step "${step.id}" is last in the array with no explicit "next" and is not an end step`
128
+ );
129
+ }
130
+ }
131
+ }
132
+ };
133
+
134
+ // src/rendering/ChoiceInput.ts
135
+ import { css, html, LitElement } from "lit";
136
+ var ChoiceInput = class extends LitElement {
137
+ constructor() {
138
+ super(...arguments);
139
+ this.options = [];
140
+ }
141
+ _select(opt) {
142
+ this.value = opt;
143
+ this.dispatchEvent(
144
+ new CustomEvent("step-answer", {
145
+ detail: { value: opt },
146
+ bubbles: true,
147
+ composed: true
148
+ })
149
+ );
150
+ }
151
+ render() {
152
+ return html`
153
+ <div class="choices" role="radiogroup">
154
+ ${(this.options ?? []).map(
155
+ (opt) => html`
156
+ <button
157
+ type="button"
158
+ role="radio"
159
+ aria-pressed=${this.value === opt ? "true" : "false"}
160
+ @click=${() => this._select(opt)}
161
+ >
162
+ <span class="radio"><span class="radio-dot"></span></span>
163
+ ${opt}
164
+ </button>
165
+ `
166
+ )}
167
+ </div>
168
+ `;
169
+ }
170
+ };
171
+ ChoiceInput.properties = {
172
+ options: { attribute: false },
173
+ value: { type: String }
174
+ };
175
+ ChoiceInput.styles = css`
176
+ :host { display: block; }
177
+ .choices {
178
+ display: flex;
179
+ flex-direction: column;
180
+ gap: 8px;
181
+ padding: 8px 0;
182
+ }
183
+ button {
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 10px;
187
+ width: 100%;
188
+ padding: 10px 14px;
189
+ border-radius: 8px;
190
+ border: 2px solid var(--sc-content-border-color, #e5e7eb);
191
+ background: var(--sc-content-bg-color, #fff);
192
+ color: var(--sc-content-text-color, #111827);
193
+ font-size: 14px;
194
+ text-align: left;
195
+ cursor: pointer;
196
+ transition: border-color 0.15s, background 0.15s;
197
+ }
198
+ button:hover {
199
+ border-color: var(--sc-color-primary, #4f46e5);
200
+ }
201
+ button[aria-pressed="true"] {
202
+ border-color: var(--sc-color-primary, #4f46e5);
203
+ background: var(--sc-color-primary-light, #eef2ff);
204
+ }
205
+ .radio {
206
+ width: 18px;
207
+ height: 18px;
208
+ border-radius: 50%;
209
+ border: 2px solid var(--sc-content-border-color, #e5e7eb);
210
+ flex-shrink: 0;
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ }
215
+ button[aria-pressed="true"] .radio {
216
+ border-color: var(--sc-color-primary, #4f46e5);
217
+ }
218
+ .radio-dot {
219
+ width: 8px;
220
+ height: 8px;
221
+ border-radius: 50%;
222
+ background: transparent;
223
+ }
224
+ button[aria-pressed="true"] .radio-dot {
225
+ background: var(--sc-color-primary, #4f46e5);
226
+ }
227
+ `;
228
+ customElements.define("sf-choice-input", ChoiceInput);
229
+
230
+ // src/rendering/EndScreen.ts
231
+ import { css as css2, html as html2, LitElement as LitElement2 } from "lit";
232
+ var EndScreen = class extends LitElement2 {
233
+ constructor() {
234
+ super(...arguments);
235
+ this.message = "Thank you!";
236
+ }
237
+ render() {
238
+ return html2`
239
+ <div class="end-wrap">
240
+ <div class="checkmark">✓</div>
241
+ <div class="message">${this.message}</div>
242
+ </div>
243
+ `;
244
+ }
245
+ };
246
+ EndScreen.properties = {
247
+ message: { type: String }
248
+ };
249
+ EndScreen.styles = css2`
250
+ :host { display: block; }
251
+ .end-wrap {
252
+ display: flex;
253
+ flex-direction: column;
254
+ align-items: center;
255
+ justify-content: center;
256
+ padding: 24px 16px;
257
+ text-align: center;
258
+ gap: 12px;
259
+ }
260
+ .checkmark {
261
+ width: 48px;
262
+ height: 48px;
263
+ border-radius: 50%;
264
+ background: var(--sc-color-primary-light, #eef2ff);
265
+ display: flex;
266
+ align-items: center;
267
+ justify-content: center;
268
+ font-size: 24px;
269
+ }
270
+ .message {
271
+ color: var(--sc-content-text-color, #111827);
272
+ font-size: 16px;
273
+ font-weight: 500;
274
+ line-height: 1.5;
275
+ }
276
+ `;
277
+ customElements.define("sf-end-screen", EndScreen);
278
+
279
+ // src/rendering/NpsInput.ts
280
+ import { css as css3, html as html3, LitElement as LitElement3 } from "lit";
281
+ var NpsInput = class extends LitElement3 {
282
+ constructor() {
283
+ super(...arguments);
284
+ this.lowLabel = "Not likely";
285
+ this.highLabel = "Very likely";
286
+ }
287
+ _select(val) {
288
+ this.value = val;
289
+ this.dispatchEvent(
290
+ new CustomEvent("step-answer", {
291
+ detail: { value: val },
292
+ bubbles: true,
293
+ composed: true
294
+ })
295
+ );
296
+ }
297
+ render() {
298
+ const items = Array.from({ length: 11 }, (_, i) => i);
299
+ return html3`
300
+ <div>
301
+ <div class="nps-scale">
302
+ ${items.map(
303
+ (n) => html3`
304
+ <button
305
+ type="button"
306
+ aria-label="Score ${n}"
307
+ aria-pressed=${this.value === n ? "true" : "false"}
308
+ @click=${() => this._select(n)}
309
+ >${n}</button>
310
+ `
311
+ )}
312
+ </div>
313
+ <div class="labels">
314
+ <span>${this.lowLabel}</span>
315
+ <span>${this.highLabel}</span>
316
+ </div>
317
+ </div>
318
+ `;
319
+ }
320
+ };
321
+ NpsInput.properties = {
322
+ lowLabel: { type: String },
323
+ highLabel: { type: String },
324
+ value: { type: Number }
325
+ };
326
+ NpsInput.styles = css3`
327
+ :host { display: block; }
328
+ .nps-scale {
329
+ display: flex;
330
+ gap: 4px;
331
+ justify-content: center;
332
+ padding: 12px 0 4px;
333
+ }
334
+ button {
335
+ min-width: 32px;
336
+ height: 36px;
337
+ border-radius: 6px;
338
+ border: 2px solid var(--sc-content-border-color, #e5e7eb);
339
+ background: var(--sc-content-bg-color, #fff);
340
+ color: var(--sc-content-text-color, #111827);
341
+ font-size: 14px;
342
+ font-weight: 600;
343
+ cursor: pointer;
344
+ transition: border-color 0.15s, background 0.15s;
345
+ padding: 0 4px;
346
+ }
347
+ button:hover {
348
+ border-color: var(--sc-color-primary, #4f46e5);
349
+ }
350
+ button[aria-pressed="true"] {
351
+ border-color: var(--sc-color-primary, #4f46e5);
352
+ background: var(--sc-color-primary, #4f46e5);
353
+ color: #fff;
354
+ }
355
+ .labels {
356
+ display: flex;
357
+ justify-content: space-between;
358
+ padding: 4px 2px 0;
359
+ font-size: 12px;
360
+ color: var(--sc-content-text-secondary-color, #6b7280);
361
+ }
362
+ `;
363
+ customElements.define("sf-nps-input", NpsInput);
364
+
365
+ // src/rendering/RatingInput.ts
366
+ import { css as css4, html as html4, LitElement as LitElement4 } from "lit";
367
+ var RatingInput = class extends LitElement4 {
368
+ constructor() {
369
+ super(...arguments);
370
+ this.max = 5;
371
+ }
372
+ _select(val) {
373
+ this.value = val;
374
+ this.dispatchEvent(
375
+ new CustomEvent("step-answer", {
376
+ detail: { value: val },
377
+ bubbles: true,
378
+ composed: true
379
+ })
380
+ );
381
+ }
382
+ render() {
383
+ const count = this.max || 5;
384
+ const items = Array.from({ length: count }, (_, i) => i + 1);
385
+ return html4`
386
+ <div class="rating-row">
387
+ ${items.map(
388
+ (n) => html4`
389
+ <button
390
+ type="button"
391
+ aria-label="Rate ${n} of ${count}"
392
+ aria-pressed=${this.value === n ? "true" : "false"}
393
+ class=${this.value === n ? "selected" : ""}
394
+ @click=${() => this._select(n)}
395
+ >${n}</button>
396
+ `
397
+ )}
398
+ </div>
399
+ `;
400
+ }
401
+ };
402
+ RatingInput.properties = {
403
+ max: { type: Number },
404
+ value: { type: Number }
405
+ };
406
+ RatingInput.styles = css4`
407
+ :host { display: block; }
408
+ .rating-row {
409
+ display: flex;
410
+ gap: 8px;
411
+ justify-content: center;
412
+ padding: 12px 0;
413
+ }
414
+ button {
415
+ width: 40px;
416
+ height: 40px;
417
+ border-radius: 8px;
418
+ border: 2px solid var(--sc-content-border-color, #e5e7eb);
419
+ background: var(--sc-content-bg-color, #fff);
420
+ color: var(--sc-content-text-color, #111827);
421
+ font-size: 16px;
422
+ font-weight: 600;
423
+ cursor: pointer;
424
+ transition: transform 0.1s, border-color 0.15s, background 0.15s;
425
+ }
426
+ button:hover {
427
+ border-color: var(--sc-color-primary, #4f46e5);
428
+ }
429
+ button[aria-pressed="true"],
430
+ button.selected {
431
+ border-color: var(--sc-color-primary, #4f46e5);
432
+ background: var(--sc-color-primary, #4f46e5);
433
+ color: #fff;
434
+ }
435
+ `;
436
+ customElements.define("sf-rating-input", RatingInput);
437
+
438
+ // src/rendering/TextInput.ts
439
+ import { css as css5, html as html5, LitElement as LitElement5 } from "lit";
440
+ var TextInput = class extends LitElement5 {
441
+ constructor() {
442
+ super(...arguments);
443
+ this.placeholder = "";
444
+ this.value = "";
445
+ this.required = true;
446
+ this._currentText = "";
447
+ this._handleInput = (e) => {
448
+ this._currentText = e.target.value;
449
+ this.requestUpdate();
450
+ };
451
+ this._submit = () => {
452
+ if (this.required && !this._currentText.trim()) return;
453
+ this.dispatchEvent(
454
+ new CustomEvent("step-answer", {
455
+ detail: { value: this._currentText },
456
+ bubbles: true,
457
+ composed: true
458
+ })
459
+ );
460
+ };
461
+ }
462
+ connectedCallback() {
463
+ super.connectedCallback();
464
+ this._currentText = this.value || "";
465
+ }
466
+ render() {
467
+ const canSubmit = !this.required || this._currentText.trim().length > 0;
468
+ return html5`
469
+ <div class="text-wrap">
470
+ <textarea
471
+ .value=${this._currentText}
472
+ placeholder=${this.placeholder || "Type your answer..."}
473
+ @input=${this._handleInput}
474
+ ></textarea>
475
+ <div class="submit-row">
476
+ <button type="button" ?disabled=${!canSubmit} @click=${this._submit}>
477
+ Submit
478
+ </button>
479
+ </div>
480
+ </div>
481
+ `;
482
+ }
483
+ };
484
+ TextInput.properties = {
485
+ placeholder: { type: String },
486
+ value: { type: String },
487
+ required: { type: Boolean }
488
+ };
489
+ TextInput.styles = css5`
490
+ :host { display: block; }
491
+ .text-wrap {
492
+ padding: 8px 0;
493
+ }
494
+ textarea {
495
+ width: 100%;
496
+ min-height: 80px;
497
+ padding: 10px 12px;
498
+ border-radius: 8px;
499
+ border: 2px solid var(--sc-content-border-color, #e5e7eb);
500
+ background: var(--sc-content-bg-color, #fff);
501
+ color: var(--sc-content-text-color, #111827);
502
+ font-family: inherit;
503
+ font-size: 14px;
504
+ resize: vertical;
505
+ box-sizing: border-box;
506
+ transition: border-color 0.15s;
507
+ }
508
+ textarea:focus {
509
+ outline: none;
510
+ border-color: var(--sc-color-primary, #4f46e5);
511
+ }
512
+ textarea::placeholder {
513
+ color: var(--sc-content-text-secondary-color, #9ca3af);
514
+ }
515
+ .submit-row {
516
+ display: flex;
517
+ justify-content: flex-end;
518
+ padding-top: 8px;
519
+ }
520
+ button {
521
+ padding: 8px 20px;
522
+ border-radius: 8px;
523
+ border: none;
524
+ background: var(--sc-color-primary, #4f46e5);
525
+ color: #fff;
526
+ font-size: 14px;
527
+ font-weight: 600;
528
+ cursor: pointer;
529
+ transition: opacity 0.15s;
530
+ }
531
+ button:hover { opacity: 0.9; }
532
+ button:disabled {
533
+ opacity: 0.4;
534
+ cursor: not-allowed;
535
+ }
536
+ `;
537
+ customElements.define("sf-text-input", TextInput);
538
+
539
+ // src/rendering/ThumbsInput.ts
540
+ import { css as css6, html as html6, LitElement as LitElement6 } from "lit";
541
+ var ThumbsInput = class extends LitElement6 {
542
+ _select(val) {
543
+ this.value = val;
544
+ this.dispatchEvent(
545
+ new CustomEvent("step-answer", {
546
+ detail: { value: val },
547
+ bubbles: true,
548
+ composed: true
549
+ })
550
+ );
551
+ }
552
+ render() {
553
+ return html6`
554
+ <div class="thumbs-row">
555
+ <button
556
+ type="button"
557
+ aria-label="Thumbs up"
558
+ aria-pressed=${this.value === 1 ? "true" : "false"}
559
+ @click=${() => this._select(1)}
560
+ >\u{1F44D}</button>
561
+ <button
562
+ type="button"
563
+ aria-label="Thumbs down"
564
+ aria-pressed=${this.value === 0 ? "true" : "false"}
565
+ @click=${() => this._select(0)}
566
+ >\u{1F44E}</button>
567
+ </div>
568
+ `;
569
+ }
570
+ };
571
+ ThumbsInput.properties = {
572
+ value: { type: Number }
573
+ };
574
+ ThumbsInput.styles = css6`
575
+ :host { display: block; }
576
+ .thumbs-row {
577
+ display: flex;
578
+ gap: 16px;
579
+ justify-content: center;
580
+ padding: 12px 0;
581
+ }
582
+ button {
583
+ width: 64px;
584
+ height: 64px;
585
+ border-radius: 50%;
586
+ border: 2px solid var(--sc-content-border-color, #e5e7eb);
587
+ background: var(--sc-content-bg-color, #fff);
588
+ font-size: 28px;
589
+ cursor: pointer;
590
+ transition: transform 0.15s, border-color 0.15s, background 0.15s;
591
+ display: flex;
592
+ align-items: center;
593
+ justify-content: center;
594
+ }
595
+ button:hover {
596
+ transform: scale(1.1);
597
+ border-color: var(--sc-color-primary, #4f46e5);
598
+ }
599
+ button[aria-pressed="true"] {
600
+ border-color: var(--sc-color-primary, #4f46e5);
601
+ background: var(--sc-color-primary-light, #eef2ff);
602
+ }
603
+ `;
604
+ customElements.define("sf-thumbs-input", ThumbsInput);
605
+
606
+ // src/FeedbackWidget.ts
607
+ var SyntroFeedbackElement = class extends LitElement7 {
608
+ constructor() {
609
+ super(...arguments);
610
+ this.tileId = "feedback-widget";
611
+ this._direction = "forward";
612
+ this._onAnswer = (e) => {
613
+ if (!this._engine || !this.config) return;
614
+ const step = this._engine.currentStep;
615
+ const { value } = e.detail;
616
+ this.runtime?.events.publish("feedback:response", {
617
+ surveyId: this.tileId,
618
+ stepId: step.id,
619
+ stepType: step.type,
620
+ value,
621
+ stepIndex: this._engine.stepIndex
622
+ });
623
+ this._direction = "forward";
624
+ this._engine.answer(value);
625
+ const updated = Object.create(
626
+ Object.getPrototypeOf(this._engine),
627
+ Object.getOwnPropertyDescriptors(this._engine)
628
+ );
629
+ this._engine = updated;
630
+ if (updated.isComplete) {
631
+ this.runtime?.events.publish("feedback:completed", {
632
+ surveyId: this.tileId,
633
+ stepsCompleted: updated.stepsCompleted,
634
+ abandoned: false
635
+ });
636
+ }
637
+ };
638
+ this._goBack = () => {
639
+ if (!this._engine) return;
640
+ this._direction = "back";
641
+ this._engine.goBack();
642
+ this._engine = Object.create(
643
+ Object.getPrototypeOf(this._engine),
644
+ Object.getOwnPropertyDescriptors(this._engine)
645
+ );
646
+ };
647
+ }
648
+ willUpdate(changed) {
649
+ if (changed.has("config") && this.config?.steps?.length) {
650
+ try {
651
+ this._engine = new FlowEngine(this.config.steps);
652
+ } catch {
653
+ this._engine = void 0;
654
+ }
655
+ }
656
+ }
657
+ _renderStepInput(step) {
658
+ const prevAnswer = this._engine?.answers.get(step.id);
659
+ switch (step.type) {
660
+ case "thumbs":
661
+ return html7`<sf-thumbs-input
662
+ .value=${prevAnswer}
663
+ @step-answer=${this._onAnswer}
664
+ ></sf-thumbs-input>`;
665
+ case "rating":
666
+ return html7`<sf-rating-input
667
+ .max=${step.ratingMax ?? 5}
668
+ .value=${prevAnswer}
669
+ @step-answer=${this._onAnswer}
670
+ ></sf-rating-input>`;
671
+ case "nps":
672
+ return html7`<sf-nps-input
673
+ .lowLabel=${step.npsLowLabel ?? "Not likely"}
674
+ .highLabel=${step.npsHighLabel ?? "Very likely"}
675
+ .value=${prevAnswer}
676
+ @step-answer=${this._onAnswer}
677
+ ></sf-nps-input>`;
678
+ case "choice":
679
+ return html7`<sf-choice-input
680
+ .options=${step.options ?? []}
681
+ .value=${prevAnswer}
682
+ @step-answer=${this._onAnswer}
683
+ ></sf-choice-input>`;
684
+ case "text":
685
+ return html7`<sf-text-input
686
+ .placeholder=${step.placeholder ?? ""}
687
+ .value=${prevAnswer ?? ""}
688
+ .required=${step.required !== false}
689
+ @step-answer=${this._onAnswer}
690
+ ></sf-text-input>`;
691
+ case "end":
692
+ return html7`<sf-end-screen .message=${step.prompt}></sf-end-screen>`;
693
+ default:
694
+ return nothing;
695
+ }
696
+ }
697
+ render() {
698
+ if (!this._engine || !this.config) {
699
+ return html7`<div style="padding:16px;color:var(--sc-content-text-secondary-color,#87919f)">
700
+ Survey widget requires config.
701
+ </div>`;
702
+ }
703
+ const step = this._engine.currentStep;
704
+ const showProgress = this.config.showProgress !== false;
705
+ const allowBack = this.config.allowBack === true;
706
+ const canGoBack = allowBack && this._engine.path.length > 1 && step.type !== "end";
707
+ const totalSteps = this.config.steps.filter((s) => s.type !== "end").length;
708
+ const progressPct = totalSteps > 0 ? this._engine.stepsCompleted / totalSteps * 100 : 0;
709
+ const animClass = this._direction === "forward" ? "slide-in-right" : "slide-in-left";
710
+ return html7`
711
+ ${this.config.title ? html7`<div class="header">${this.config.title}</div>` : nothing}
712
+ ${showProgress && step.type !== "end" ? html7`<div class="progress-bar">
713
+ <div class="progress-fill" style="width:${progressPct}%"></div>
714
+ </div>` : nothing}
715
+ <div class="step-container">
716
+ <div class="step-content ${animClass}" key=${step.id}>
717
+ ${step.type !== "end" ? html7`
718
+ <div class="prompt">${step.prompt}</div>
719
+ ${step.description ? html7`<div class="description">${step.description}</div>` : nothing}
720
+ ` : nothing}
721
+ ${this._renderStepInput(step)}
722
+ </div>
723
+ </div>
724
+ ${canGoBack || step.type !== "end" && showProgress ? html7`<div class="footer">
725
+ ${canGoBack ? html7`<button type="button" class="back-btn" @click=${this._goBack}>← Back</button>` : html7`<span></span>`}
726
+ ${step.type !== "end" ? html7`<span class="step-count">${this._engine.stepsCompleted + 1} of ${totalSteps}</span>` : nothing}
727
+ </div>` : nothing}
728
+ `;
729
+ }
730
+ };
731
+ SyntroFeedbackElement.properties = {
732
+ config: { attribute: false },
733
+ runtime: { attribute: false },
734
+ tileId: { type: String },
735
+ _engine: { state: true },
736
+ _direction: { state: true }
737
+ };
738
+ SyntroFeedbackElement.styles = css7`
739
+ :host {
740
+ display: flex;
741
+ flex-direction: column;
742
+ height: 100%;
743
+ font-family: var(--sc-font-family, system-ui, -apple-system, sans-serif);
744
+ color: var(--sc-content-text-color, #111827);
745
+ }
746
+ .header {
747
+ padding: 12px 16px 8px;
748
+ font-size: 15px;
749
+ font-weight: 600;
750
+ color: var(--sc-content-text-color, #111827);
751
+ }
752
+ .progress-bar {
753
+ height: 3px;
754
+ background: var(--sc-content-border-color, #e5e7eb);
755
+ margin: 0 16px;
756
+ border-radius: 2px;
757
+ overflow: hidden;
758
+ }
759
+ .progress-fill {
760
+ height: 100%;
761
+ background: var(--sc-color-primary, #4f46e5);
762
+ border-radius: 2px;
763
+ transition: width 0.3s ease;
764
+ }
765
+ .step-container {
766
+ flex: 1;
767
+ overflow: hidden;
768
+ position: relative;
769
+ }
770
+ .step-content {
771
+ padding: 16px;
772
+ animation-duration: 0.25s;
773
+ animation-fill-mode: both;
774
+ animation-timing-function: ease-out;
775
+ }
776
+ .step-content.slide-in-right {
777
+ animation-name: slideInRight;
778
+ }
779
+ .step-content.slide-in-left {
780
+ animation-name: slideInLeft;
781
+ }
782
+ @keyframes slideInRight {
783
+ from { transform: translateX(30px); opacity: 0; }
784
+ to { transform: translateX(0); opacity: 1; }
785
+ }
786
+ @keyframes slideInLeft {
787
+ from { transform: translateX(-30px); opacity: 0; }
788
+ to { transform: translateX(0); opacity: 1; }
789
+ }
790
+ .prompt {
791
+ font-size: 15px;
792
+ font-weight: 500;
793
+ line-height: 1.4;
794
+ margin-bottom: 4px;
795
+ }
796
+ .description {
797
+ font-size: 13px;
798
+ color: var(--sc-content-text-secondary-color, #6b7280);
799
+ margin-bottom: 8px;
800
+ line-height: 1.4;
801
+ }
802
+ .footer {
803
+ display: flex;
804
+ align-items: center;
805
+ justify-content: space-between;
806
+ padding: 8px 16px 12px;
807
+ gap: 8px;
808
+ }
809
+ .back-btn {
810
+ padding: 6px 14px;
811
+ border-radius: 6px;
812
+ border: 1px solid var(--sc-content-border-color, #e5e7eb);
813
+ background: transparent;
814
+ color: var(--sc-content-text-secondary-color, #6b7280);
815
+ font-size: 13px;
816
+ cursor: pointer;
817
+ transition: border-color 0.15s;
818
+ }
819
+ .back-btn:hover {
820
+ border-color: var(--sc-content-text-color, #111827);
821
+ }
822
+ .step-count {
823
+ font-size: 12px;
824
+ color: var(--sc-content-text-secondary-color, #9ca3af);
825
+ }
826
+ `;
827
+ customElements.define("syntro-feedback", SyntroFeedbackElement);
828
+ var FeedbackWidgetLitMountable = {
829
+ mount(container, mountConfig) {
830
+ const {
831
+ config,
832
+ runtime,
833
+ tileId = "feedback-widget"
834
+ } = mountConfig ?? {};
835
+ if (!config) {
836
+ container.innerHTML = '<div style="padding:16px;color:var(--sc-content-text-secondary-color,#87919f)">Feedback widget requires config.</div>';
837
+ return () => {
838
+ container.innerHTML = "";
839
+ };
840
+ }
841
+ let tracker;
842
+ try {
843
+ tracker = new FlowEngine(config.steps);
844
+ } catch {
845
+ }
846
+ const el = document.createElement("syntro-feedback");
847
+ el.config = config;
848
+ el.runtime = runtime;
849
+ el.tileId = tileId;
850
+ el.style.cssText = "display:flex;flex-direction:column;height:100%;width:100%;";
851
+ container.appendChild(el);
852
+ let completed = false;
853
+ const onAnswer = (e) => {
854
+ const detail = e.detail;
855
+ if (tracker && !tracker.isComplete) {
856
+ tracker.answer(detail.value);
857
+ if (tracker.isComplete) completed = true;
858
+ }
859
+ };
860
+ el.addEventListener("step-answer", onAnswer);
861
+ return () => {
862
+ el.removeEventListener("step-answer", onAnswer);
863
+ if (!completed && runtime && tracker) {
864
+ runtime.events.publish("feedback:abandoned", {
865
+ surveyId: tileId,
866
+ lastStepId: tracker.currentStep.id,
867
+ stepsCompleted: tracker.stepsCompleted,
868
+ abandoned: true
869
+ });
870
+ }
871
+ el.remove();
872
+ };
873
+ }
874
+ };
875
+
876
+ export {
877
+ SyntroFeedbackElement,
878
+ FeedbackWidgetLitMountable
879
+ };
880
+ //# sourceMappingURL=chunk-3TUS6XVJ.js.map