@stubber/form-fields 2.2.6 → 3.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.
Files changed (126) hide show
  1. package/README.md +63 -25
  2. package/dist/form-fields/Autocomplete.svelte +261 -0
  3. package/dist/form-fields/Autocomplete.svelte.d.ts +43 -0
  4. package/dist/form-fields/CollapsibleSection.svelte +127 -0
  5. package/dist/form-fields/CollapsibleSection.svelte.d.ts +26 -0
  6. package/dist/form-fields/FieldLabel.svelte +18 -3
  7. package/dist/form-fields/FieldLabel.svelte.d.ts +1 -0
  8. package/dist/form-fields/FieldMessage.svelte +20 -9
  9. package/dist/form-fields/Form.svelte +19 -2
  10. package/dist/form-fields/Form.svelte.d.ts +2 -0
  11. package/dist/form-fields/UploadStatus.svelte +76 -0
  12. package/dist/form-fields/UploadStatus.svelte.d.ts +18 -0
  13. package/dist/form-fields/fields/_field-builder-reorder.svelte +35 -0
  14. package/dist/form-fields/fields/_field-builder-reorder.svelte.d.ts +22 -0
  15. package/dist/form-fields/{sub → fields}/array-builder-field.svelte +104 -38
  16. package/dist/form-fields/fields/checkbox-autocomplete.svelte +153 -0
  17. package/dist/form-fields/fields/checkbox-field.svelte +69 -0
  18. package/dist/form-fields/{sub → fields}/contact-selector-field.svelte +9 -6
  19. package/dist/form-fields/{sub → fields}/currency-field.svelte +108 -70
  20. package/dist/form-fields/fields/data-indication-field.svelte +39 -0
  21. package/dist/form-fields/fields/date-field.svelte +31 -0
  22. package/dist/form-fields/fields/date-time-field.svelte +31 -0
  23. package/dist/form-fields/{sub → fields}/email-field.svelte +25 -11
  24. package/dist/form-fields/fields/field-builder-field.svelte +568 -0
  25. package/dist/form-fields/fields/file-field.svelte +284 -0
  26. package/dist/form-fields/{sub → fields}/file-field.svelte.d.ts +2 -0
  27. package/dist/form-fields/fields/grid-field.svelte +152 -0
  28. package/dist/form-fields/{sub → fields}/heading-field.svelte +7 -4
  29. package/dist/form-fields/{sub → fields}/html-field.svelte +6 -4
  30. package/dist/form-fields/{sub → fields}/map-field.svelte +121 -78
  31. package/dist/form-fields/{sub → fields}/multi-checkbox-field.svelte +35 -20
  32. package/dist/form-fields/fields/multistep-field.svelte +135 -0
  33. package/dist/form-fields/fields/note-field.svelte +35 -0
  34. package/dist/form-fields/{sub → fields}/number-field.svelte +29 -14
  35. package/dist/form-fields/fields/object-builder-field.svelte +176 -0
  36. package/dist/form-fields/fields/qr-code-scanner-field.svelte +203 -0
  37. package/dist/form-fields/fields/radio-field.svelte +82 -0
  38. package/dist/form-fields/{sub → fields}/radio-field.svelte.d.ts +1 -1
  39. package/dist/form-fields/fields/screenrecorder-field.svelte +255 -0
  40. package/dist/form-fields/{sub → fields}/screenshot-field.svelte +101 -40
  41. package/dist/form-fields/{sub → fields}/scroll-and-read-display-field.svelte +43 -27
  42. package/dist/form-fields/fields/section-field.svelte +73 -0
  43. package/dist/form-fields/fields/select-field.svelte +104 -0
  44. package/dist/form-fields/{sub → fields}/signature-field.svelte +78 -22
  45. package/dist/form-fields/fields/slider-field.svelte +53 -0
  46. package/dist/form-fields/{sub → fields}/smart-text-field.svelte +62 -82
  47. package/dist/form-fields/fields/telephone-field.svelte +108 -0
  48. package/dist/form-fields/{sub → fields}/telephone-field.svelte.d.ts +1 -1
  49. package/dist/form-fields/{sub → fields}/text-field.svelte +28 -11
  50. package/dist/form-fields/{sub → fields}/voicenote-field.svelte +117 -46
  51. package/dist/form-fields/file_creation_options.d.ts +1 -1
  52. package/dist/form-fields/form-field.svelte +8 -1
  53. package/dist/form-fields/icons/ChevronDownIcon.svelte +21 -0
  54. package/dist/form-fields/icons/ChevronDownIcon.svelte.d.ts +20 -0
  55. package/dist/form-fields/icons/QrCodeScannerIcon.svelte +25 -0
  56. package/dist/form-fields/icons/QrCodeScannerIcon.svelte.d.ts +19 -0
  57. package/dist/form-fields/icons/ScreenRecordingIcon.svelte +21 -0
  58. package/dist/form-fields/icons/ScreenRecordingIcon.svelte.d.ts +19 -0
  59. package/dist/form-fields/icons/ScreenshotIcon.svelte +22 -0
  60. package/dist/form-fields/icons/ScreenshotIcon.svelte.d.ts +19 -0
  61. package/dist/form-fields/index.d.ts +5 -1
  62. package/dist/form-fields/index.js +3 -1
  63. package/dist/form-fields/interfaces.d.ts +63 -63
  64. package/dist/form-fields/interfaces.js +42 -39
  65. package/dist/form-fields/intl-tel-input.css +1 -0
  66. package/dist/form-fields/styles.d.ts +17 -0
  67. package/dist/form-fields/styles.js +4 -0
  68. package/package.json +2 -4
  69. package/dist/form-fields/sub/checkbox-autocomplete.svelte +0 -56
  70. package/dist/form-fields/sub/checkbox-field.svelte +0 -42
  71. package/dist/form-fields/sub/data-indication-field.svelte +0 -19
  72. package/dist/form-fields/sub/date-field.svelte +0 -31
  73. package/dist/form-fields/sub/date-time-field.svelte +0 -31
  74. package/dist/form-fields/sub/field-builder-field.svelte +0 -657
  75. package/dist/form-fields/sub/file-field.svelte +0 -159
  76. package/dist/form-fields/sub/grid-field.svelte +0 -69
  77. package/dist/form-fields/sub/multistep-field.svelte +0 -70
  78. package/dist/form-fields/sub/note-field.svelte +0 -18
  79. package/dist/form-fields/sub/object-builder-field.svelte +0 -134
  80. package/dist/form-fields/sub/qr-code-scanner-field.svelte +0 -84
  81. package/dist/form-fields/sub/radio-field.svelte +0 -69
  82. package/dist/form-fields/sub/screenrecorder-field.svelte +0 -178
  83. package/dist/form-fields/sub/section-field.svelte +0 -50
  84. package/dist/form-fields/sub/select-field.svelte +0 -150
  85. package/dist/form-fields/sub/slider-field.svelte +0 -28
  86. package/dist/form-fields/sub/telephone-field.svelte +0 -64
  87. /package/dist/form-fields/{sub → fields}/array-builder-field.svelte.d.ts +0 -0
  88. /package/dist/form-fields/{sub → fields}/checkbox-autocomplete.svelte.d.ts +0 -0
  89. /package/dist/form-fields/{sub → fields}/checkbox-field.svelte.d.ts +0 -0
  90. /package/dist/form-fields/{sub → fields}/code-field.svelte +0 -0
  91. /package/dist/form-fields/{sub → fields}/code-field.svelte.d.ts +0 -0
  92. /package/dist/form-fields/{sub → fields}/contact-selector-field.svelte.d.ts +0 -0
  93. /package/dist/form-fields/{sub → fields}/currency-field.svelte.d.ts +0 -0
  94. /package/dist/form-fields/{sub → fields}/data-indication-field.svelte.d.ts +0 -0
  95. /package/dist/form-fields/{sub → fields}/date-field.svelte.d.ts +0 -0
  96. /package/dist/form-fields/{sub → fields}/date-time-field.svelte.d.ts +0 -0
  97. /package/dist/form-fields/{sub → fields}/email-field.svelte.d.ts +0 -0
  98. /package/dist/form-fields/{sub → fields}/field-builder-field.svelte.d.ts +0 -0
  99. /package/dist/form-fields/{sub → fields}/grid-field.svelte.d.ts +0 -0
  100. /package/dist/form-fields/{sub → fields}/heading-field.svelte.d.ts +0 -0
  101. /package/dist/form-fields/{sub → fields}/hidden-field.svelte +0 -0
  102. /package/dist/form-fields/{sub → fields}/hidden-field.svelte.d.ts +0 -0
  103. /package/dist/form-fields/{sub → fields}/hidden-location-field.svelte +0 -0
  104. /package/dist/form-fields/{sub → fields}/hidden-location-field.svelte.d.ts +0 -0
  105. /package/dist/form-fields/{sub → fields}/html-field.svelte.d.ts +0 -0
  106. /package/dist/form-fields/{sub → fields}/jsoneditor-field.svelte +0 -0
  107. /package/dist/form-fields/{sub → fields}/jsoneditor-field.svelte.d.ts +0 -0
  108. /package/dist/form-fields/{sub → fields}/map-field.svelte.d.ts +0 -0
  109. /package/dist/form-fields/{sub → fields}/multi-checkbox-field.svelte.d.ts +0 -0
  110. /package/dist/form-fields/{sub → fields}/multistep-field.svelte.d.ts +0 -0
  111. /package/dist/form-fields/{sub → fields}/note-field.svelte.d.ts +0 -0
  112. /package/dist/form-fields/{sub → fields}/number-field.svelte.d.ts +0 -0
  113. /package/dist/form-fields/{sub → fields}/object-builder-field.svelte.d.ts +0 -0
  114. /package/dist/form-fields/{sub → fields}/qr-code-scanner-field.svelte.d.ts +0 -0
  115. /package/dist/form-fields/{sub → fields}/screenrecorder-field.svelte.d.ts +0 -0
  116. /package/dist/form-fields/{sub → fields}/screenshot-field.svelte.d.ts +0 -0
  117. /package/dist/form-fields/{sub → fields}/scroll-and-read-display-field.svelte.d.ts +0 -0
  118. /package/dist/form-fields/{sub → fields}/section-field.svelte.d.ts +0 -0
  119. /package/dist/form-fields/{sub → fields}/select-field.svelte.d.ts +0 -0
  120. /package/dist/form-fields/{sub → fields}/selectresource-field.svelte +0 -0
  121. /package/dist/form-fields/{sub → fields}/selectresource-field.svelte.d.ts +0 -0
  122. /package/dist/form-fields/{sub → fields}/signature-field.svelte.d.ts +0 -0
  123. /package/dist/form-fields/{sub → fields}/slider-field.svelte.d.ts +0 -0
  124. /package/dist/form-fields/{sub → fields}/smart-text-field.svelte.d.ts +0 -0
  125. /package/dist/form-fields/{sub → fields}/text-field.svelte.d.ts +0 -0
  126. /package/dist/form-fields/{sub → fields}/voicenote-field.svelte.d.ts +0 -0
@@ -0,0 +1,255 @@
1
+ <script context="module">import {} from "../interfaces";
2
+ </script>
3
+
4
+ <script>import { getContext, onDestroy } from "svelte";
5
+ import FieldLabel from "../FieldLabel.svelte";
6
+ import FieldMessage from "../FieldMessage.svelte";
7
+ import UploadStatus from "../UploadStatus.svelte";
8
+ import {
9
+ append_attachment,
10
+ remove_attachment,
11
+ uploadFiles
12
+ } from "../fileserver";
13
+ import ScreenRecordingIcon from "../icons/ScreenRecordingIcon.svelte";
14
+ import { STYLES_CONTEXT, classNames } from "../styles";
15
+ export let fieldStore;
16
+ const styles = getContext(STYLES_CONTEXT);
17
+ let fileList = [];
18
+ let mediaRecorder = null;
19
+ let captureStreams = [];
20
+ $: isRecording = mediaRecorder?.state === "recording";
21
+ $: buttonLabel = isRecording ? "Stop Recording" : "Start Recording";
22
+ let max_files_param = $fieldStore.params?.max_files;
23
+ let file_creation_options = $fieldStore.params?.file_creation_options;
24
+ $: max_files = isNaN(parseInt(max_files_param)) ? Infinity : parseInt(max_files_param);
25
+ $: limit_remaining = max_files - fileList.length;
26
+ function stopCaptureStreams() {
27
+ captureStreams.forEach((stream) => stream.getTracks().forEach((track) => track.stop()));
28
+ captureStreams = [];
29
+ }
30
+ onDestroy(() => {
31
+ if (mediaRecorder?.state === "recording") {
32
+ mediaRecorder.onstop = null;
33
+ mediaRecorder.stop();
34
+ }
35
+ stopCaptureStreams();
36
+ fileList.forEach((item) => item.previewUrl && URL.revokeObjectURL(item.previewUrl));
37
+ });
38
+ const toggleRecording = () => {
39
+ if (mediaRecorder?.state === "recording") {
40
+ stopRecording();
41
+ } else {
42
+ startRecording();
43
+ }
44
+ };
45
+ async function startRecording() {
46
+ if (limit_remaining <= 0) {
47
+ alert("You have reached the maximum number of files allowed");
48
+ return;
49
+ }
50
+ if (!navigator.mediaDevices.getDisplayMedia) {
51
+ alert("Screen capture is not supported in this browser.");
52
+ return;
53
+ }
54
+ const stream = await navigator.mediaDevices.getDisplayMedia({
55
+ audio: true,
56
+ video: true
57
+ });
58
+ const audiostream = await navigator.mediaDevices.getUserMedia({ audio: true });
59
+ captureStreams = [stream, audiostream];
60
+ const combine = new MediaStream([...stream.getVideoTracks(), ...audiostream.getAudioTracks()]);
61
+ const recorder = new MediaRecorder(combine, { mimeType: "video/webm;codecs=vp9" });
62
+ mediaRecorder = recorder;
63
+ const media = [];
64
+ recorder.ondataavailable = (e) => media.push(e.data);
65
+ recorder.onstop = function() {
66
+ let id = Math.random().toString(36).substring(7);
67
+ let filename = `${$fieldStore.label}_${id}.webm`;
68
+ let blob = new Blob(media, { type: "video/webm;codecs=vp9" });
69
+ let file = new File([blob], filename, { type: "video/webm;codecs=vp9" });
70
+ uploadFile(file, blob, filename, URL.createObjectURL(blob));
71
+ stopCaptureStreams();
72
+ mediaRecorder = null;
73
+ };
74
+ recorder.start();
75
+ }
76
+ function stopRecording() {
77
+ mediaRecorder?.stop();
78
+ }
79
+ async function uploadFile(file, blob, filename, previewUrl) {
80
+ fileList = [
81
+ ...fileList,
82
+ {
83
+ file,
84
+ blob,
85
+ filename,
86
+ previewUrl,
87
+ is_uploaded: false,
88
+ is_failed: false
89
+ }
90
+ ];
91
+ const upload_res = await uploadFiles(
92
+ [file],
93
+ $fieldStore.formDependencies,
94
+ file_creation_options
95
+ );
96
+ const { uploaded_files = [], failed_files = [] } = upload_res || {};
97
+ const newly_uploaded = uploaded_files.find((f) => f.filename === filename) || {};
98
+ const is_uploaded = uploaded_files.length > 0;
99
+ const is_failed = failed_files.length > 0;
100
+ fileList = fileList.map((item) => {
101
+ if (item.filename === filename) {
102
+ return {
103
+ ...item,
104
+ ...newly_uploaded,
105
+ is_uploaded,
106
+ is_failed
107
+ };
108
+ }
109
+ return item;
110
+ });
111
+ if (newly_uploaded.fileuuid) {
112
+ append_attachment(newly_uploaded, $fieldStore.attachmentsStore);
113
+ }
114
+ }
115
+ const update_file_list = (field_value) => {
116
+ if (Array.isArray(field_value)) {
117
+ fileList = field_value.map((f) => ({
118
+ file: f.file,
119
+ blob: f.blob,
120
+ fileuuid: f.fileuuid,
121
+ contentType: f.contentType,
122
+ originalname: f.originalname,
123
+ filename: f.filename,
124
+ previewUrl: f.previewUrl || (f.blob ? URL.createObjectURL(f.blob) : void 0),
125
+ is_uploaded: f.is_uploaded || f.fileuuid ? true : false,
126
+ is_failed: f.is_failed || false
127
+ }));
128
+ }
129
+ };
130
+ update_file_list($fieldStore.value);
131
+ $: update_field_value(fileList);
132
+ const update_field_value = (files) => {
133
+ const uploaded_files = files.filter((f) => f.is_uploaded && f.fileuuid);
134
+ $fieldStore.value = uploaded_files.map((f) => ({
135
+ filename: f.filename,
136
+ fileuuid: f.fileuuid,
137
+ contentType: f.contentType,
138
+ originalname: f.originalname
139
+ }));
140
+ };
141
+ function removeFile(item) {
142
+ fileList = fileList.filter((f) => f !== item);
143
+ if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
144
+ remove_attachment(item.fileuuid, $fieldStore.attachmentsStore);
145
+ }
146
+ </script>
147
+
148
+ <div class="screenrecorder-field">
149
+ <FieldLabel {fieldStore} />
150
+ <div
151
+ class="screenrecorder-control"
152
+ class:screenrecorder-control-invalid={$fieldStore.validation_result?.type === "error"}
153
+ aria-invalid={$fieldStore.validation_result?.type === "error" ? true : undefined}
154
+ >
155
+ <div class="screenrecorder-list" aria-live="polite">
156
+ {#each fileList as item}
157
+ <div class={classNames("screenrecorder-item", $styles.uploadItem)}>
158
+ <UploadStatus
159
+ status={item.is_uploaded ? "uploaded" : item.is_failed ? "failed" : "uploading"}
160
+ />
161
+ {#if item.previewUrl}
162
+ <video class="screenrecorder-preview" controls src={item.previewUrl}>
163
+ <track kind="captions" />
164
+ </video>
165
+ {:else}
166
+ <p class="screenrecorder-filename">{item?.filename}</p>
167
+ {/if}
168
+ <button
169
+ type="button"
170
+ class={classNames("screenrecorder-remove", $styles.button)}
171
+ data-variant="destructive"
172
+ data-size="icon"
173
+ aria-label="Remove {item.filename}"
174
+ on:click={() => removeFile(item)}
175
+ >
176
+ <span aria-hidden="true">×</span>
177
+ </button>
178
+ </div>
179
+ {/each}
180
+ </div>
181
+ <button
182
+ id={$fieldStore.id}
183
+ type="button"
184
+ class={classNames("screenrecorder-record", $styles.button)}
185
+ data-variant="primary"
186
+ class:screenrecorder-record-active={isRecording}
187
+ aria-pressed={isRecording}
188
+ aria-describedby="message-{$fieldStore.id}"
189
+ on:click={toggleRecording}
190
+ >
191
+ <span class="screenrecorder-icon" aria-hidden="true">
192
+ {#if isRecording}
193
+
194
+ {:else}
195
+ <ScreenRecordingIcon />
196
+ {/if}
197
+ </span>
198
+ {buttonLabel}
199
+ </button>
200
+ </div>
201
+ <FieldMessage {fieldStore} />
202
+ </div>
203
+
204
+ <style>
205
+ .screenrecorder-field,
206
+ .screenrecorder-control,
207
+ .screenrecorder-list {
208
+ box-sizing: border-box;
209
+ width: 100%;
210
+ min-width: 0;
211
+ }
212
+
213
+ .screenrecorder-list {
214
+ display: flex;
215
+ flex-direction: column;
216
+ gap: 0.5rem;
217
+ }
218
+
219
+ .screenrecorder-control {
220
+ display: flex;
221
+ flex-direction: column;
222
+ gap: 0.5rem;
223
+ }
224
+
225
+ :where(.screenrecorder-item) {
226
+ display: flex;
227
+ align-items: center;
228
+ gap: 0.5rem;
229
+ min-width: 0;
230
+ padding: var(--form-upload-item-padding, 0.625rem 0.75rem);
231
+ border: var(--form-upload-item-border, 1px solid #e2e8f0);
232
+ border-radius: var(--form-upload-item-radius, 0.5rem);
233
+ background: var(--form-upload-item-background, #fff);
234
+ }
235
+
236
+ .screenrecorder-preview,
237
+ .screenrecorder-filename {
238
+ flex: 1;
239
+ min-width: 0;
240
+ max-width: 100%;
241
+ }
242
+
243
+ .screenrecorder-filename {
244
+ margin: 0;
245
+ overflow-wrap: anywhere;
246
+ }
247
+
248
+ :where(.screenrecorder-record) {
249
+ align-self: flex-start;
250
+ }
251
+
252
+ :where(.screenrecorder-remove) {
253
+ flex: none;
254
+ }
255
+ </style>
@@ -1,16 +1,20 @@
1
1
  <script context="module">import {} from "../interfaces";
2
2
  </script>
3
3
 
4
- <script>import { Button } from "@stubber/ui/button";
5
- import { snakeCase } from "lodash-es";
4
+ <script>import { snakeCase } from "lodash-es";
5
+ import { getContext } from "svelte";
6
6
  import FieldLabel from "../FieldLabel.svelte";
7
7
  import FieldMessage from "../FieldMessage.svelte";
8
+ import UploadStatus from "../UploadStatus.svelte";
8
9
  import {
9
10
  append_attachment,
10
11
  remove_attachment,
11
12
  uploadFiles
12
13
  } from "../fileserver";
14
+ import ScreenshotIcon from "../icons/ScreenshotIcon.svelte";
15
+ import { STYLES_CONTEXT, classNames } from "../styles";
13
16
  export let fieldStore;
17
+ const styles = getContext(STYLES_CONTEXT);
14
18
  const file_name_prefix = snakeCase($fieldStore.label || "Screenshot");
15
19
  let fileList = [];
16
20
  let isRecording = false;
@@ -39,7 +43,7 @@ async function startRecording() {
39
43
  return;
40
44
  }
41
45
  const stream = await navigator.mediaDevices.getDisplayMedia({
42
- video: { mediaSource: "screen" }
46
+ video: true
43
47
  });
44
48
  const track = stream.getVideoTracks()[0];
45
49
  const imageCapture = new ImageCapture(track);
@@ -128,43 +132,100 @@ function removeFile(item) {
128
132
  }
129
133
  </script>
130
134
 
131
- <FieldLabel {fieldStore} />
132
- <div class="flex flex-col items-start gap-2">
133
- <Button variant={isRecording ? "destructive" : "default"} on:click={toggleRecording}>
134
- {#if isRecording}
135
- <i class="fa-solid fa-camera-slash" />
136
- {:else}
137
- <i class="fa-solid fa-camera" />
138
- {/if}
139
- {buttonLabel}
140
- </Button>
141
- <div class="flex w-full flex-col gap-1">
142
- {#each fileList as item}
143
- <div class="flex w-full flex-row items-center gap-1">
144
- <div class="flex h-6 w-6 shrink-0 items-center justify-center">
145
- {#if item.is_uploaded}
146
- <i class="fa fa-check text-success-500" />
147
- {:else if item.is_failed}
148
- <i class="fa-regular fa-triangle-exclamation text-danger-400" />
149
- {:else}
150
- <i class="fa fa-pulse fa-spinner text-surface-500" />
151
- {/if}
135
+ <div class="screenshot-field">
136
+ <FieldLabel {fieldStore} />
137
+ <div
138
+ class="screenshot-control"
139
+ class:screenshot-control-invalid={$fieldStore.validation_result?.type === "error"}
140
+ aria-invalid={$fieldStore.validation_result?.type === "error" ? true : undefined}
141
+ >
142
+ <div class="screenshot-list" aria-live="polite">
143
+ {#each fileList as item}
144
+ <div class={classNames("screenshot-item", $styles.uploadItem)}>
145
+ <UploadStatus
146
+ status={item.is_uploaded ? "uploaded" : item.is_failed ? "failed" : "uploading"}
147
+ />
148
+ <p class="screenshot-filename">{item?.filename}</p>
149
+ <button
150
+ type="button"
151
+ class={classNames("screenshot-remove", $styles.button)}
152
+ data-variant="destructive"
153
+ data-size="icon"
154
+ aria-label="Remove {item.filename}"
155
+ on:click={() => removeFile(item)}
156
+ >
157
+ <span aria-hidden="true">×</span>
158
+ </button>
152
159
  </div>
153
-
154
- <div class="w-full shrink truncate rounded-sm border border-surface-200 py-1 pl-2">
155
- <p class="text-fluid-md text-surface-800">
156
- {item?.filename}
157
- </p>
158
- </div>
159
- <Button
160
- variant="destructive"
161
- class="h-6 w-6 shrink-0 p-0"
162
- on:click={() => removeFile(item)}
163
- >
164
- <i class="fa-solid fa-2xs fa-x" />
165
- </Button>
166
- </div>
167
- {/each}
160
+ {/each}
161
+ </div>
162
+ <button
163
+ id={$fieldStore.id}
164
+ type="button"
165
+ class={classNames("screenshot-capture", $styles.button)}
166
+ data-variant="primary"
167
+ class:screenshot-capture-active={isRecording}
168
+ aria-pressed={isRecording}
169
+ aria-describedby="message-{$fieldStore.id}"
170
+ on:click={toggleRecording}
171
+ >
172
+ <span class="screenshot-icon" aria-hidden="true">
173
+ {#if isRecording}
174
+ ×
175
+ {:else}
176
+ <ScreenshotIcon />
177
+ {/if}
178
+ </span>
179
+ {buttonLabel}
180
+ </button>
168
181
  </div>
182
+ <FieldMessage {fieldStore} />
169
183
  </div>
170
- <FieldMessage {fieldStore} />
184
+
185
+ <style>
186
+ .screenshot-field,
187
+ .screenshot-control,
188
+ .screenshot-list {
189
+ box-sizing: border-box;
190
+ width: 100%;
191
+ min-width: 0;
192
+ }
193
+
194
+ .screenshot-list {
195
+ display: flex;
196
+ flex-direction: column;
197
+ gap: 0.5rem;
198
+ }
199
+
200
+ .screenshot-control {
201
+ display: flex;
202
+ flex-direction: column;
203
+ gap: 0.5rem;
204
+ }
205
+
206
+ :where(.screenshot-item) {
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 0.5rem;
210
+ min-width: 0;
211
+ padding: var(--form-upload-item-padding, 0.625rem 0.75rem);
212
+ border: var(--form-upload-item-border, 1px solid #e2e8f0);
213
+ border-radius: var(--form-upload-item-radius, 0.5rem);
214
+ background: var(--form-upload-item-background, #fff);
215
+ }
216
+
217
+ .screenshot-filename {
218
+ flex: 1;
219
+ min-width: 0;
220
+ margin: 0;
221
+ overflow-wrap: anywhere;
222
+ }
223
+
224
+ :where(.screenshot-capture) {
225
+ align-self: flex-start;
226
+ }
227
+
228
+ :where(.screenshot-remove) {
229
+ flex: none;
230
+ }
231
+ </style>
@@ -17,26 +17,23 @@ export const scroll_and_read_display_field_param_spec = {
17
17
  };
18
18
  </script>
19
19
 
20
- <script>import { Checkbox } from "@stubber/ui/checkbox";
21
- import { Label } from "@stubber/ui/label";
22
- import { isEqual } from "lodash-es";
20
+ <script>import { isEqual } from "lodash-es";
21
+ import { getContext, onMount } from "svelte";
22
+ import FieldLabel from "../FieldLabel.svelte";
23
23
  import FieldMessage from "../FieldMessage.svelte";
24
+ import { STYLES_CONTEXT, classNames } from "../styles";
24
25
  import { set_value_details } from "../utils";
25
- import { onMount } from "svelte";
26
- import FieldExprToggle from "../FieldExprToggle.svelte";
27
26
  export let fieldStore;
27
+ const styles = getContext(STYLES_CONTEXT);
28
28
  let params = $fieldStore.params;
29
29
  let yes_value = params?.checkedvalue ?? true;
30
30
  let no_value = params?.uncheckedvalue ?? false;
31
31
  let displaytext = params?.displaytext || "";
32
- $: validation_result = $fieldStore.validation_result;
33
- $: type = validation_result?.type;
34
- $: is_error = type === "error";
35
32
  let scrolled_to_bottom = false;
36
33
  $: checked = isEqual($fieldStore.value, yes_value);
37
- const handle_checked_change = (new_checked) => {
38
- let new_value = new_checked ? yes_value : no_value;
39
- $fieldStore.value = new_value;
34
+ const handle_checked_change = (event) => {
35
+ const new_checked = event.currentTarget.checked;
36
+ $fieldStore.value = new_checked ? yes_value : no_value;
40
37
  };
41
38
  let outer;
42
39
  const handle_scroll = (event) => {
@@ -60,33 +57,52 @@ const update_value_details = (new_value) => {
60
57
  };
61
58
  </script>
62
59
 
63
- <div class="my-2 flex w-full flex-col text-surface-900">
64
- <!-- div with inner shadow -->
60
+ <div class="scroll-and-read-field">
61
+ <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
65
62
  <div
66
63
  bind:this={outer}
67
- class="max-h-[300px] overflow-y-scroll rounded-md border shadow-inner"
64
+ class="scroll-and-read-content"
65
+ role="region"
66
+ aria-label="{$fieldStore.label} content"
67
+ tabindex="0"
68
68
  on:scroll={handle_scroll}
69
69
  >
70
- <div class="p-6 px-2">
71
- {@html displaytext}
72
- </div>
70
+ {@html displaytext}
73
71
  </div>
74
72
  <div
75
- class="{!scrolled_to_bottom
76
- ? 'opacity-20'
77
- : ''} relative mt-2 flex items-center gap-x-2 {is_error ? 'border-b border-warning-500' : ''}"
73
+ class="scroll-and-read-confirmation"
74
+ aria-invalid={$fieldStore.validation_result?.type === "error" ? true : undefined}
78
75
  >
79
- <Checkbox
76
+ <input
77
+ class={classNames("scroll-and-read-input", $styles.choice)}
80
78
  disabled={!scrolled_to_bottom}
81
- id="input_{$fieldStore.id}"
79
+ id={$fieldStore.id}
82
80
  name={$fieldStore.id}
81
+ type="checkbox"
83
82
  {checked}
84
- onCheckedChange={handle_checked_change}
83
+ aria-invalid={$fieldStore.validation_result?.type === "error" ? true : undefined}
84
+ aria-describedby="message-{$fieldStore.id}"
85
+ on:change={handle_checked_change}
85
86
  />
86
- <Label for="input_{$fieldStore.id}" class="block">
87
- {$fieldStore.label}
88
- </Label>
89
- <FieldExprToggle {fieldStore} />
87
+ <FieldLabel {fieldStore} with_margin={false} />
90
88
  </div>
91
89
  <FieldMessage {fieldStore} />
92
90
  </div>
91
+
92
+ <style>
93
+ .scroll-and-read-field {
94
+ min-width: 0;
95
+ width: 100%;
96
+ }
97
+
98
+ .scroll-and-read-content {
99
+ max-height: 20rem;
100
+ overflow-y: auto;
101
+ }
102
+
103
+ .scroll-and-read-confirmation {
104
+ display: flex;
105
+ gap: 0.5rem;
106
+ align-items: center;
107
+ }
108
+ </style>
@@ -0,0 +1,73 @@
1
+ <script context="module">import { get as get_store_value } from "svelte/store";
2
+ </script>
3
+
4
+ <script>import { getContext } from "svelte";
5
+ import FieldMessage from "../FieldMessage.svelte";
6
+ import FormField from "../form-field.svelte";
7
+ import FieldExprToggle from "../FieldExprToggle.svelte";
8
+ import CollapsibleSection from "../CollapsibleSection.svelte";
9
+ import { STYLES_CONTEXT, classNames } from "../styles";
10
+ import { validate_field } from "../validations/validate_field";
11
+ export let fieldStore;
12
+ const styles = getContext(STYLES_CONTEXT);
13
+ let open = true;
14
+ $: validate_sub_fields($fieldStore);
15
+ let section_has_errors = false;
16
+ const validate_sub_fields = async (field) => {
17
+ section_has_errors = false;
18
+ if (field.sub_fields && field.sub_fields.length > 0) {
19
+ for (const sub_field of field.sub_fields || []) {
20
+ const { errors } = await validate_field(field.formStore, sub_field);
21
+ if (errors.length > 0) {
22
+ section_has_errors = true;
23
+ }
24
+ }
25
+ }
26
+ };
27
+ </script>
28
+
29
+ <section
30
+ class={classNames("section-field", $styles.group)}
31
+ class:section-field-invalid={section_has_errors}
32
+ data-invalid={section_has_errors ? true : undefined}
33
+ aria-labelledby="label-{$fieldStore.id}"
34
+ >
35
+ <CollapsibleSection
36
+ title={$fieldStore.label}
37
+ bind:open
38
+ contentId="content-{$fieldStore.id}"
39
+ labelledBy="label-{$fieldStore.id}"
40
+ >
41
+ <div slot="heading" class="section-heading">
42
+ <span id="label-{$fieldStore.id}" class={classNames("section-label", $styles.label)}
43
+ >{$fieldStore.label}</span
44
+ >
45
+ <FieldMessage {fieldStore} />
46
+ </div>
47
+ <div slot="actions" class="section-actions">
48
+ <FieldExprToggle {fieldStore} />
49
+ </div>
50
+ {#each $fieldStore.sub_fields || [] as sub_field (get_store_value(sub_field).id)}
51
+ <FormField fieldStore={sub_field} />
52
+ {/each}
53
+ </CollapsibleSection>
54
+ </section>
55
+
56
+ <style>
57
+ .section-field {
58
+ box-sizing: border-box;
59
+ width: 100%;
60
+ min-width: 0;
61
+ }
62
+
63
+ .section-heading {
64
+ min-width: 0;
65
+ }
66
+
67
+ .section-actions {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 0.25rem;
71
+ flex: none;
72
+ }
73
+ </style>