@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.
- package/README.md +63 -25
- package/dist/form-fields/Autocomplete.svelte +261 -0
- package/dist/form-fields/Autocomplete.svelte.d.ts +43 -0
- package/dist/form-fields/CollapsibleSection.svelte +127 -0
- package/dist/form-fields/CollapsibleSection.svelte.d.ts +26 -0
- package/dist/form-fields/FieldLabel.svelte +18 -3
- package/dist/form-fields/FieldLabel.svelte.d.ts +1 -0
- package/dist/form-fields/FieldMessage.svelte +20 -9
- package/dist/form-fields/Form.svelte +19 -2
- package/dist/form-fields/Form.svelte.d.ts +2 -0
- package/dist/form-fields/UploadStatus.svelte +76 -0
- package/dist/form-fields/UploadStatus.svelte.d.ts +18 -0
- package/dist/form-fields/fields/_field-builder-reorder.svelte +35 -0
- package/dist/form-fields/fields/_field-builder-reorder.svelte.d.ts +22 -0
- package/dist/form-fields/{sub → fields}/array-builder-field.svelte +104 -38
- package/dist/form-fields/fields/checkbox-autocomplete.svelte +153 -0
- package/dist/form-fields/fields/checkbox-field.svelte +69 -0
- package/dist/form-fields/{sub → fields}/contact-selector-field.svelte +9 -6
- package/dist/form-fields/{sub → fields}/currency-field.svelte +108 -70
- package/dist/form-fields/fields/data-indication-field.svelte +39 -0
- package/dist/form-fields/fields/date-field.svelte +31 -0
- package/dist/form-fields/fields/date-time-field.svelte +31 -0
- package/dist/form-fields/{sub → fields}/email-field.svelte +25 -11
- package/dist/form-fields/fields/field-builder-field.svelte +568 -0
- package/dist/form-fields/fields/file-field.svelte +284 -0
- package/dist/form-fields/{sub → fields}/file-field.svelte.d.ts +2 -0
- package/dist/form-fields/fields/grid-field.svelte +152 -0
- package/dist/form-fields/{sub → fields}/heading-field.svelte +7 -4
- package/dist/form-fields/{sub → fields}/html-field.svelte +6 -4
- package/dist/form-fields/{sub → fields}/map-field.svelte +121 -78
- package/dist/form-fields/{sub → fields}/multi-checkbox-field.svelte +35 -20
- package/dist/form-fields/fields/multistep-field.svelte +135 -0
- package/dist/form-fields/fields/note-field.svelte +35 -0
- package/dist/form-fields/{sub → fields}/number-field.svelte +29 -14
- package/dist/form-fields/fields/object-builder-field.svelte +176 -0
- package/dist/form-fields/fields/qr-code-scanner-field.svelte +203 -0
- package/dist/form-fields/fields/radio-field.svelte +82 -0
- package/dist/form-fields/{sub → fields}/radio-field.svelte.d.ts +1 -1
- package/dist/form-fields/fields/screenrecorder-field.svelte +255 -0
- package/dist/form-fields/{sub → fields}/screenshot-field.svelte +101 -40
- package/dist/form-fields/{sub → fields}/scroll-and-read-display-field.svelte +43 -27
- package/dist/form-fields/fields/section-field.svelte +73 -0
- package/dist/form-fields/fields/select-field.svelte +104 -0
- package/dist/form-fields/{sub → fields}/signature-field.svelte +78 -22
- package/dist/form-fields/fields/slider-field.svelte +53 -0
- package/dist/form-fields/{sub → fields}/smart-text-field.svelte +62 -82
- package/dist/form-fields/fields/telephone-field.svelte +108 -0
- package/dist/form-fields/{sub → fields}/telephone-field.svelte.d.ts +1 -1
- package/dist/form-fields/{sub → fields}/text-field.svelte +28 -11
- package/dist/form-fields/{sub → fields}/voicenote-field.svelte +117 -46
- package/dist/form-fields/file_creation_options.d.ts +1 -1
- package/dist/form-fields/form-field.svelte +8 -1
- package/dist/form-fields/icons/ChevronDownIcon.svelte +21 -0
- package/dist/form-fields/icons/ChevronDownIcon.svelte.d.ts +20 -0
- package/dist/form-fields/icons/QrCodeScannerIcon.svelte +25 -0
- package/dist/form-fields/icons/QrCodeScannerIcon.svelte.d.ts +19 -0
- package/dist/form-fields/icons/ScreenRecordingIcon.svelte +21 -0
- package/dist/form-fields/icons/ScreenRecordingIcon.svelte.d.ts +19 -0
- package/dist/form-fields/icons/ScreenshotIcon.svelte +22 -0
- package/dist/form-fields/icons/ScreenshotIcon.svelte.d.ts +19 -0
- package/dist/form-fields/index.d.ts +5 -1
- package/dist/form-fields/index.js +3 -1
- package/dist/form-fields/interfaces.d.ts +63 -63
- package/dist/form-fields/interfaces.js +42 -39
- package/dist/form-fields/intl-tel-input.css +1 -0
- package/dist/form-fields/styles.d.ts +17 -0
- package/dist/form-fields/styles.js +4 -0
- package/package.json +2 -4
- package/dist/form-fields/sub/checkbox-autocomplete.svelte +0 -56
- package/dist/form-fields/sub/checkbox-field.svelte +0 -42
- package/dist/form-fields/sub/data-indication-field.svelte +0 -19
- package/dist/form-fields/sub/date-field.svelte +0 -31
- package/dist/form-fields/sub/date-time-field.svelte +0 -31
- package/dist/form-fields/sub/field-builder-field.svelte +0 -657
- package/dist/form-fields/sub/file-field.svelte +0 -159
- package/dist/form-fields/sub/grid-field.svelte +0 -69
- package/dist/form-fields/sub/multistep-field.svelte +0 -70
- package/dist/form-fields/sub/note-field.svelte +0 -18
- package/dist/form-fields/sub/object-builder-field.svelte +0 -134
- package/dist/form-fields/sub/qr-code-scanner-field.svelte +0 -84
- package/dist/form-fields/sub/radio-field.svelte +0 -69
- package/dist/form-fields/sub/screenrecorder-field.svelte +0 -178
- package/dist/form-fields/sub/section-field.svelte +0 -50
- package/dist/form-fields/sub/select-field.svelte +0 -150
- package/dist/form-fields/sub/slider-field.svelte +0 -28
- package/dist/form-fields/sub/telephone-field.svelte +0 -64
- /package/dist/form-fields/{sub → fields}/array-builder-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/checkbox-autocomplete.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/checkbox-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/code-field.svelte +0 -0
- /package/dist/form-fields/{sub → fields}/code-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/contact-selector-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/currency-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/data-indication-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/date-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/date-time-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/email-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/field-builder-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/grid-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/heading-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/hidden-field.svelte +0 -0
- /package/dist/form-fields/{sub → fields}/hidden-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/hidden-location-field.svelte +0 -0
- /package/dist/form-fields/{sub → fields}/hidden-location-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/html-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/jsoneditor-field.svelte +0 -0
- /package/dist/form-fields/{sub → fields}/jsoneditor-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/map-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/multi-checkbox-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/multistep-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/note-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/number-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/object-builder-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/qr-code-scanner-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/screenrecorder-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/screenshot-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/scroll-and-read-display-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/section-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/select-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/selectresource-field.svelte +0 -0
- /package/dist/form-fields/{sub → fields}/selectresource-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/signature-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/slider-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/smart-text-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/text-field.svelte.d.ts +0 -0
- /package/dist/form-fields/{sub → fields}/voicenote-field.svelte.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,38 +1,76 @@
|
|
|
1
1
|
# stubber-form-fields-pkg
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package provides a set of Svelte components for building accessible, composable form fields from json definitions.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
git clone git@github.com:stubber/stubber-public-form-fields-pkg.git form-fields-pkg
|
|
7
|
-
cd form-fields-pkg
|
|
8
|
-
cp .env_staging .env
|
|
9
|
-
pnpm install
|
|
10
|
-
pnpm run dev
|
|
11
|
-
```
|
|
5
|
+
## Consumer styling contract
|
|
12
6
|
|
|
13
|
-
|
|
7
|
+
The package owns the structural CSS that fields need to function, including layout, spacing,
|
|
8
|
+
sizing, alignment, overlays, and disclosure movement. Consumers own the visual theme and provide
|
|
9
|
+
classes for semantic roles through the `Form` component's `styles` prop.
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
`FormFieldStyles` is a flat set of class hooks. A class string is added directly to every element
|
|
12
|
+
with that role, alongside the package's internal structural class. All properties are optional;
|
|
13
|
+
the package does not supply or merge a default visual theme, and an omitted role receives no
|
|
14
|
+
consumer class. Pass the styles once at the outer form boundary; nested forms inherit them.
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
```svelte
|
|
17
|
+
<script lang="ts">
|
|
18
|
+
import { Form, type FormFieldStyles } from "@stubber/form-fields";
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
const styles: FormFieldStyles = {
|
|
21
|
+
label: "text-sm font-medium",
|
|
22
|
+
message: "text-xs text-slate-500 data-[variant=error]:text-red-600",
|
|
23
|
+
input: "w-full rounded border px-3 py-2",
|
|
24
|
+
choice: "size-4 accent-blue-600",
|
|
25
|
+
range: "w-full accent-blue-600",
|
|
26
|
+
group: "rounded border p-3 data-[invalid]:border-red-600",
|
|
27
|
+
dropzone: "rounded border-2 border-dashed p-4 data-[dragging]:border-blue-600",
|
|
28
|
+
uploadItem: "rounded border bg-slate-50 p-2",
|
|
29
|
+
popover: "rounded border bg-white p-1 shadow-md",
|
|
30
|
+
option:
|
|
31
|
+
"px-2 py-1 data-[highlighted]:bg-blue-600 data-[highlighted]:text-white aria-[selected=true]:font-semibold",
|
|
32
|
+
button:
|
|
33
|
+
"rounded px-2 py-1 data-[variant=primary]:bg-blue-600 data-[variant=primary]:text-white data-[variant=destructive]:bg-red-600 data-[variant=destructive]:text-white data-[size=icon]:size-8 data-[size=icon]:p-0",
|
|
34
|
+
};
|
|
35
|
+
</script>
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
breaking: change button color to red
|
|
24
|
-
feat!: add new button component
|
|
37
|
+
<Form {styles} {form} {initial_form} />
|
|
38
|
+
```
|
|
25
39
|
|
|
26
|
-
|
|
27
|
-
feat: add new button component
|
|
40
|
+
The supported roles are:
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
| Role | Applied to |
|
|
43
|
+
| ------------ | ------------------------------------------------------------------------------------------------------ |
|
|
44
|
+
| `label` | Field labels and section/group legends |
|
|
45
|
+
| `input` | Text-like controls, including text areas, number/date inputs, autocomplete inputs, and editor surfaces |
|
|
46
|
+
| `choice` | Checkboxes, radio buttons, and other binary or multiple-choice controls |
|
|
47
|
+
| `range` | Range/slider inputs |
|
|
48
|
+
| `message` | Help and validation messages |
|
|
49
|
+
| `group` | Section and grouped-field containers |
|
|
50
|
+
| `dropzone` | File upload dropzones |
|
|
51
|
+
| `uploadItem` | Uploaded, recorded, or captured file items |
|
|
52
|
+
| `popover` | Option lists, popovers, and dialogs |
|
|
53
|
+
| `option` | Individual options in a choice list |
|
|
54
|
+
| `button` | Form actions, including add, remove, navigation, capture, and recording controls |
|
|
55
|
+
|
|
56
|
+
### State selectors
|
|
32
57
|
|
|
33
|
-
|
|
58
|
+
Use native selectors where the state has an HTML or ARIA equivalent: `:disabled`,
|
|
59
|
+
`[aria-invalid="true"]`, and `[aria-selected="true"]`. The package exposes the remaining visual
|
|
60
|
+
states as attributes:
|
|
34
61
|
|
|
35
|
-
|
|
62
|
+
| Role | Attribute | Values/presence |
|
|
63
|
+
| ---------- | ------------------ | ------------------------------------------ |
|
|
64
|
+
| `message` | `data-variant` | `help`, `error` |
|
|
65
|
+
| `group` | `data-invalid` | Present when invalid |
|
|
66
|
+
| `dropzone` | `data-dragging` | Present while dragging over the target |
|
|
67
|
+
| `dropzone` | `data-disabled` | Present when the upload limit is reached |
|
|
68
|
+
| `dropzone` | `data-invalid` | Present when invalid |
|
|
69
|
+
| `option` | `data-highlighted` | Present on the keyboard-highlighted option |
|
|
70
|
+
| `button` | `data-variant` | `primary`, `destructive` |
|
|
71
|
+
| `button` | `data-size` | `icon` |
|
|
36
72
|
|
|
37
|
-
|
|
38
|
-
|
|
73
|
+
Button variant and size are independent, so a button may be both
|
|
74
|
+
`data-variant="destructive"` and `data-size="icon"`. Treat the role names and state attributes above
|
|
75
|
+
as the public styling API; internal package class names and DOM structure are implementation
|
|
76
|
+
details and should not be targeted by consumer styles.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
<script context="module"></script>
|
|
2
|
+
|
|
3
|
+
<script>import { debounce, isEqual } from "lodash-es";
|
|
4
|
+
import { createEventDispatcher, getContext, onDestroy, tick } from "svelte";
|
|
5
|
+
import ChevronDownIcon from "./icons/ChevronDownIcon.svelte";
|
|
6
|
+
import { STYLES_CONTEXT, classNames } from "./styles";
|
|
7
|
+
export let id;
|
|
8
|
+
export let options = [];
|
|
9
|
+
export let value = void 0;
|
|
10
|
+
export let placeholder = "Select...";
|
|
11
|
+
export let emptyMessage = "No results found.";
|
|
12
|
+
export let loadingMessage = "Loading\u2026";
|
|
13
|
+
export let ariaLabel = void 0;
|
|
14
|
+
export let ariaDescribedby = void 0;
|
|
15
|
+
export let invalid = false;
|
|
16
|
+
export let inputClass = void 0;
|
|
17
|
+
export let variant = "default";
|
|
18
|
+
export let loadOptions = void 0;
|
|
19
|
+
export let selected = (option, value2) => isEqual(option.value, value2);
|
|
20
|
+
const styles = getContext(STYLES_CONTEXT);
|
|
21
|
+
const dispatch = createEventDispatcher();
|
|
22
|
+
let open = false;
|
|
23
|
+
let loading = false;
|
|
24
|
+
let query = "";
|
|
25
|
+
let highlightedIndex = 0;
|
|
26
|
+
let inputElement;
|
|
27
|
+
let listElement;
|
|
28
|
+
let availableOptions = options;
|
|
29
|
+
let requestId = 0;
|
|
30
|
+
$: if (!loadOptions) availableOptions = options;
|
|
31
|
+
$: selectedLabel = availableOptions.find((option) => selected(option, value))?.label ?? placeholder;
|
|
32
|
+
$: if (!open) query = selectedLabel;
|
|
33
|
+
$: visibleOptions = loadOptions ? availableOptions : availableOptions.filter(
|
|
34
|
+
(option) => (option.searchText ?? option.label).toLowerCase().includes(query.trim().toLowerCase())
|
|
35
|
+
);
|
|
36
|
+
$: if (highlightedIndex >= visibleOptions.length) {
|
|
37
|
+
highlightedIndex = Math.max(visibleOptions.length - 1, 0);
|
|
38
|
+
}
|
|
39
|
+
$: if (open && listElement && visibleOptions[highlightedIndex]) {
|
|
40
|
+
const index = highlightedIndex;
|
|
41
|
+
void tick().then(() => {
|
|
42
|
+
if (open && index === highlightedIndex) {
|
|
43
|
+
listElement.children[index]?.scrollIntoView({
|
|
44
|
+
block: "nearest",
|
|
45
|
+
inline: "nearest"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const search = async (searchTerm) => {
|
|
51
|
+
if (!loadOptions) return;
|
|
52
|
+
const currentRequest = ++requestId;
|
|
53
|
+
loading = true;
|
|
54
|
+
try {
|
|
55
|
+
const results = await loadOptions(searchTerm);
|
|
56
|
+
if (currentRequest !== requestId) return;
|
|
57
|
+
availableOptions = results;
|
|
58
|
+
highlightedIndex = 0;
|
|
59
|
+
} finally {
|
|
60
|
+
if (currentRequest === requestId) loading = false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const debouncedSearch = debounce(search, 300);
|
|
64
|
+
onDestroy(() => debouncedSearch.cancel());
|
|
65
|
+
function openList() {
|
|
66
|
+
if (open) return;
|
|
67
|
+
open = true;
|
|
68
|
+
query = "";
|
|
69
|
+
highlightedIndex = Math.max(
|
|
70
|
+
availableOptions.findIndex((option) => selected(option, value)),
|
|
71
|
+
0
|
|
72
|
+
);
|
|
73
|
+
if (loadOptions) void search("");
|
|
74
|
+
}
|
|
75
|
+
function closeList() {
|
|
76
|
+
open = false;
|
|
77
|
+
query = selectedLabel;
|
|
78
|
+
}
|
|
79
|
+
function choose(option) {
|
|
80
|
+
dispatch("select", option);
|
|
81
|
+
inputElement.focus();
|
|
82
|
+
closeList();
|
|
83
|
+
}
|
|
84
|
+
function handleInput(event) {
|
|
85
|
+
query = event.currentTarget.value;
|
|
86
|
+
open = true;
|
|
87
|
+
highlightedIndex = 0;
|
|
88
|
+
if (loadOptions) debouncedSearch(query);
|
|
89
|
+
}
|
|
90
|
+
function handleKeydown(event) {
|
|
91
|
+
if (event.key === "ArrowDown") {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
openList();
|
|
94
|
+
highlightedIndex = Math.min(highlightedIndex + 1, visibleOptions.length - 1);
|
|
95
|
+
} else if (event.key === "ArrowUp") {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
openList();
|
|
98
|
+
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
|
99
|
+
} else if (event.key === "Enter" && open && visibleOptions[highlightedIndex]) {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
choose(visibleOptions[highlightedIndex]);
|
|
102
|
+
} else if (event.key === "Escape" && open) {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
closeList();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function handleFocusout(event) {
|
|
108
|
+
const nextTarget = event.relatedTarget;
|
|
109
|
+
const currentTarget = event.currentTarget;
|
|
110
|
+
if (!(nextTarget instanceof Node) || !currentTarget.contains(nextTarget)) closeList();
|
|
111
|
+
}
|
|
112
|
+
function optionKey(option, index) {
|
|
113
|
+
return option.key ?? `${option.label}-${index}`;
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<div class="autocomplete" data-variant={variant} on:focusout={handleFocusout}>
|
|
118
|
+
<div class="autocomplete-input-wrapper" aria-invalid={invalid ? true : undefined}>
|
|
119
|
+
<input
|
|
120
|
+
bind:this={inputElement}
|
|
121
|
+
{id}
|
|
122
|
+
class={classNames("autocomplete-input", inputClass, variant === "default" && $styles.input)}
|
|
123
|
+
data-variant={variant === "embedded" ? "embedded" : undefined}
|
|
124
|
+
type="text"
|
|
125
|
+
role="combobox"
|
|
126
|
+
autocomplete="off"
|
|
127
|
+
value={query}
|
|
128
|
+
{placeholder}
|
|
129
|
+
aria-label={ariaLabel}
|
|
130
|
+
aria-autocomplete="list"
|
|
131
|
+
aria-expanded={open}
|
|
132
|
+
aria-controls="autocomplete-listbox-{id}"
|
|
133
|
+
aria-activedescendant={open && visibleOptions[highlightedIndex]
|
|
134
|
+
? `autocomplete-option-${id}-${highlightedIndex}`
|
|
135
|
+
: undefined}
|
|
136
|
+
aria-invalid={invalid ? true : undefined}
|
|
137
|
+
aria-describedby={ariaDescribedby}
|
|
138
|
+
on:focus={openList}
|
|
139
|
+
on:click={openList}
|
|
140
|
+
on:input={handleInput}
|
|
141
|
+
on:keydown={handleKeydown}
|
|
142
|
+
/>
|
|
143
|
+
<span class:open class="autocomplete-indicator" aria-hidden="true">
|
|
144
|
+
<ChevronDownIcon />
|
|
145
|
+
</span>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{#if open}
|
|
149
|
+
<ul
|
|
150
|
+
bind:this={listElement}
|
|
151
|
+
id="autocomplete-listbox-{id}"
|
|
152
|
+
class={classNames("autocomplete-options", $styles.popover)}
|
|
153
|
+
role="listbox"
|
|
154
|
+
>
|
|
155
|
+
{#if loading}
|
|
156
|
+
<li class="autocomplete-status" role="status">{loadingMessage}</li>
|
|
157
|
+
{:else if visibleOptions.length === 0}
|
|
158
|
+
<li class="autocomplete-status" role="status">{emptyMessage}</li>
|
|
159
|
+
{:else}
|
|
160
|
+
{#each visibleOptions as option, index (optionKey(option, index))}
|
|
161
|
+
<li role="presentation">
|
|
162
|
+
<button
|
|
163
|
+
id="autocomplete-option-{id}-{index}"
|
|
164
|
+
type="button"
|
|
165
|
+
role="option"
|
|
166
|
+
tabindex="-1"
|
|
167
|
+
aria-selected={selected(option, value)}
|
|
168
|
+
data-highlighted={index === highlightedIndex ? true : undefined}
|
|
169
|
+
class={classNames("autocomplete-option", $styles.option)}
|
|
170
|
+
on:mousedown={(event) => event.preventDefault()}
|
|
171
|
+
on:mouseenter={() => (highlightedIndex = index)}
|
|
172
|
+
on:click={() => choose(option)}
|
|
173
|
+
>
|
|
174
|
+
<slot name="option" {option}>{option.label}</slot>
|
|
175
|
+
</button>
|
|
176
|
+
</li>
|
|
177
|
+
{/each}
|
|
178
|
+
{/if}
|
|
179
|
+
</ul>
|
|
180
|
+
{/if}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<style>
|
|
184
|
+
.autocomplete {
|
|
185
|
+
position: relative;
|
|
186
|
+
box-sizing: border-box;
|
|
187
|
+
width: 100%;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.autocomplete-input-wrapper {
|
|
191
|
+
position: relative;
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.autocomplete-input {
|
|
197
|
+
box-sizing: border-box;
|
|
198
|
+
width: 100%;
|
|
199
|
+
padding-inline-end: 2rem;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.autocomplete-indicator {
|
|
203
|
+
position: absolute;
|
|
204
|
+
inset-inline-end: 0.5rem;
|
|
205
|
+
display: flex;
|
|
206
|
+
pointer-events: none;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.autocomplete-options {
|
|
210
|
+
position: absolute;
|
|
211
|
+
z-index: 20;
|
|
212
|
+
top: 100%;
|
|
213
|
+
box-sizing: border-box;
|
|
214
|
+
width: 100%;
|
|
215
|
+
max-height: 15rem;
|
|
216
|
+
overflow-y: auto;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
:global(:where(.autocomplete-option)) {
|
|
220
|
+
-webkit-appearance: none;
|
|
221
|
+
-moz-appearance: none;
|
|
222
|
+
appearance: none;
|
|
223
|
+
box-sizing: border-box;
|
|
224
|
+
display: block;
|
|
225
|
+
width: 100%;
|
|
226
|
+
padding: 0;
|
|
227
|
+
border: 0;
|
|
228
|
+
background: transparent;
|
|
229
|
+
color: inherit;
|
|
230
|
+
font: inherit;
|
|
231
|
+
text-align: start;
|
|
232
|
+
cursor: pointer;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.autocomplete[data-variant="embedded"] {
|
|
236
|
+
width: 5.5rem;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.autocomplete[data-variant="embedded"] .autocomplete-input {
|
|
240
|
+
padding-inline-end: 0.5rem;
|
|
241
|
+
background: transparent;
|
|
242
|
+
color: inherit;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.autocomplete[data-variant="embedded"] .autocomplete-input:focus {
|
|
246
|
+
outline: none;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.autocomplete[data-variant="embedded"] .autocomplete-indicator {
|
|
250
|
+
inset-block: 0;
|
|
251
|
+
inset-inline-end: 0;
|
|
252
|
+
align-items: center;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.autocomplete[data-variant="embedded"] .autocomplete-options {
|
|
256
|
+
right: 0;
|
|
257
|
+
left: auto;
|
|
258
|
+
width: min(18rem, calc(100vw - 2rem));
|
|
259
|
+
max-height: 16rem;
|
|
260
|
+
}
|
|
261
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
|
2
|
+
export interface AutocompleteOption<T = any> {
|
|
3
|
+
label: string;
|
|
4
|
+
value: T;
|
|
5
|
+
key?: string;
|
|
6
|
+
searchText?: string;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
}
|
|
9
|
+
declare const __propDef: {
|
|
10
|
+
props: {
|
|
11
|
+
id: string;
|
|
12
|
+
options?: AutocompleteOption[];
|
|
13
|
+
value?: any;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
emptyMessage?: string;
|
|
16
|
+
loadingMessage?: string;
|
|
17
|
+
ariaLabel?: string | undefined;
|
|
18
|
+
ariaDescribedby?: string | undefined;
|
|
19
|
+
invalid?: boolean;
|
|
20
|
+
inputClass?: string | undefined;
|
|
21
|
+
variant?: "default" | "embedded";
|
|
22
|
+
loadOptions?: ((searchTerm: string) => Promise<AutocompleteOption[]>) | undefined;
|
|
23
|
+
selected?: (option: AutocompleteOption, value: any) => boolean;
|
|
24
|
+
};
|
|
25
|
+
events: {
|
|
26
|
+
select: CustomEvent<AutocompleteOption<any>>;
|
|
27
|
+
} & {
|
|
28
|
+
[evt: string]: CustomEvent<any>;
|
|
29
|
+
};
|
|
30
|
+
slots: {
|
|
31
|
+
option: {
|
|
32
|
+
option: AutocompleteOption<any>;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
exports?: {} | undefined;
|
|
36
|
+
bindings?: string | undefined;
|
|
37
|
+
};
|
|
38
|
+
export type AutocompleteProps = typeof __propDef.props;
|
|
39
|
+
export type AutocompleteEvents = typeof __propDef.events;
|
|
40
|
+
export type AutocompleteSlots = typeof __propDef.slots;
|
|
41
|
+
export default class Autocomplete extends SvelteComponent<AutocompleteProps, AutocompleteEvents, AutocompleteSlots> {
|
|
42
|
+
}
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script>import ChevronDownIcon from "./icons/ChevronDownIcon.svelte";
|
|
2
|
+
export let title;
|
|
3
|
+
export let open = false;
|
|
4
|
+
export let contentId = void 0;
|
|
5
|
+
export let labelledBy = void 0;
|
|
6
|
+
export let onToggle = void 0;
|
|
7
|
+
function toggle() {
|
|
8
|
+
open = !open;
|
|
9
|
+
onToggle?.(open);
|
|
10
|
+
}
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div class="collapsible-section">
|
|
14
|
+
<div class="collapsible-section-header">
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
class="collapsible-section-toggle"
|
|
18
|
+
aria-expanded={open}
|
|
19
|
+
aria-controls={contentId}
|
|
20
|
+
aria-label={open ? `Collapse ${title}` : `Expand ${title}`}
|
|
21
|
+
on:click={toggle}
|
|
22
|
+
>
|
|
23
|
+
<span class="collapsible-section-heading">
|
|
24
|
+
<slot name="heading">{title}</slot>
|
|
25
|
+
</span>
|
|
26
|
+
<span
|
|
27
|
+
class="collapsible-section-indicator"
|
|
28
|
+
class:collapsible-section-indicator-open={open}
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
>
|
|
31
|
+
<ChevronDownIcon />
|
|
32
|
+
</span>
|
|
33
|
+
</button>
|
|
34
|
+
<slot name="actions" />
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{#if open}
|
|
38
|
+
<div id={contentId} class="collapsible-section-content" aria-labelledby={labelledBy}>
|
|
39
|
+
<slot />
|
|
40
|
+
</div>
|
|
41
|
+
{/if}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
.collapsible-section,
|
|
46
|
+
.collapsible-section-header {
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
width: 100%;
|
|
49
|
+
min-width: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.collapsible-section-header {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 0.25rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.collapsible-section-toggle {
|
|
59
|
+
-webkit-appearance: none;
|
|
60
|
+
-moz-appearance: none;
|
|
61
|
+
appearance: none;
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: 0.75rem;
|
|
65
|
+
flex: 1;
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
width: 100%;
|
|
68
|
+
min-width: 0;
|
|
69
|
+
padding: 0.5rem;
|
|
70
|
+
border: 0;
|
|
71
|
+
border-radius: 0.375rem;
|
|
72
|
+
background: transparent;
|
|
73
|
+
color: inherit;
|
|
74
|
+
font: inherit;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
text-align: start;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.collapsible-section-toggle:hover {
|
|
81
|
+
background: rgba(127, 127, 127, 0.08);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.collapsible-section-toggle:focus {
|
|
85
|
+
outline: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.collapsible-section-toggle:focus-visible {
|
|
89
|
+
outline: 2px solid Highlight;
|
|
90
|
+
outline-offset: 2px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.collapsible-section-heading {
|
|
94
|
+
flex: 1;
|
|
95
|
+
min-width: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.collapsible-section-indicator {
|
|
99
|
+
display: inline-flex;
|
|
100
|
+
flex: none;
|
|
101
|
+
transform: rotate(-90deg);
|
|
102
|
+
transition: transform 150ms ease;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.collapsible-section-indicator-open {
|
|
106
|
+
transform: rotate(0deg);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.collapsible-section-content {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
gap: 0.5rem;
|
|
113
|
+
box-sizing: border-box;
|
|
114
|
+
width: auto;
|
|
115
|
+
min-width: 0;
|
|
116
|
+
margin-top: 0.5rem;
|
|
117
|
+
margin-inline-start: 0.5rem;
|
|
118
|
+
padding-inline-start: 0.75rem;
|
|
119
|
+
/* border-inline-start: 2px solid rgba(127, 127, 127, 0.25); */
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@media (prefers-reduced-motion: reduce) {
|
|
123
|
+
.collapsible-section-indicator {
|
|
124
|
+
transition: none;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: {
|
|
4
|
+
title: string;
|
|
5
|
+
open?: boolean;
|
|
6
|
+
contentId?: string | undefined;
|
|
7
|
+
labelledBy?: string | undefined;
|
|
8
|
+
onToggle?: ((open: boolean) => void) | undefined;
|
|
9
|
+
};
|
|
10
|
+
events: {
|
|
11
|
+
[evt: string]: CustomEvent<any>;
|
|
12
|
+
};
|
|
13
|
+
slots: {
|
|
14
|
+
heading: {};
|
|
15
|
+
actions: {};
|
|
16
|
+
default: {};
|
|
17
|
+
};
|
|
18
|
+
exports?: {} | undefined;
|
|
19
|
+
bindings?: string | undefined;
|
|
20
|
+
};
|
|
21
|
+
export type CollapsibleSectionProps = typeof __propDef.props;
|
|
22
|
+
export type CollapsibleSectionEvents = typeof __propDef.events;
|
|
23
|
+
export type CollapsibleSectionSlots = typeof __propDef.slots;
|
|
24
|
+
export default class CollapsibleSection extends SvelteComponent<CollapsibleSectionProps, CollapsibleSectionEvents, CollapsibleSectionSlots> {
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
<script>import {
|
|
1
|
+
<script>import { getContext } from "svelte";
|
|
2
2
|
import FieldExprToggle from "./FieldExprToggle.svelte";
|
|
3
|
+
import { STYLES_CONTEXT, classNames } from "./styles";
|
|
3
4
|
export let fieldStore;
|
|
5
|
+
export let with_margin = true;
|
|
6
|
+
const styles = getContext(STYLES_CONTEXT);
|
|
4
7
|
$: hide_label = $fieldStore.hide_label;
|
|
5
8
|
</script>
|
|
6
9
|
|
|
7
|
-
<div class="
|
|
10
|
+
<div class="field-label-wrapper" class:field-label-wrapper-spacing={with_margin}>
|
|
8
11
|
{#if !hide_label}
|
|
9
|
-
<
|
|
12
|
+
<label class={$styles.label} for={$fieldStore.id}>{$fieldStore.label}</label>
|
|
10
13
|
{/if}
|
|
11
14
|
<FieldExprToggle {fieldStore} />
|
|
12
15
|
</div>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
.field-label-wrapper {
|
|
19
|
+
display: flex;
|
|
20
|
+
justify-content: space-between;
|
|
21
|
+
align-items: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.field-label-wrapper-spacing {
|
|
25
|
+
margin-bottom: 0.25rem;
|
|
26
|
+
}
|
|
27
|
+
</style>
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
<script>import {
|
|
1
|
+
<script>import { getContext } from "svelte";
|
|
2
|
+
import { STYLES_CONTEXT, classNames } from "./styles";
|
|
2
3
|
export let fieldStore;
|
|
4
|
+
const styles = getContext(STYLES_CONTEXT);
|
|
3
5
|
$: validation_result = $fieldStore.validation_result;
|
|
4
|
-
$: type = validation_result?.type;
|
|
5
6
|
</script>
|
|
6
7
|
|
|
7
|
-
<
|
|
8
|
+
<p
|
|
8
9
|
id="message-{$fieldStore.id}"
|
|
9
|
-
|
|
10
|
-
class="
|
|
11
|
-
|
|
12
|
-
: 'text-muted-foreground'}"
|
|
10
|
+
aria-hidden={!validation_result}
|
|
11
|
+
class={classNames("field-message", $styles.message)}
|
|
12
|
+
data-variant={validation_result?.type === "error" ? "error" : "help"}
|
|
13
13
|
>
|
|
14
|
-
{
|
|
15
|
-
</
|
|
14
|
+
{validation_result?.message ?? "\u00a0"}
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
.field-message {
|
|
19
|
+
margin: 0;
|
|
20
|
+
min-height: 1em;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.field-message[aria-hidden="true"] {
|
|
24
|
+
visibility: hidden;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
<script>import { setContext } from "svelte";
|
|
1
|
+
<script>import { getContext, setContext } from "svelte";
|
|
2
2
|
import { get as getStoreValue, writable } from "svelte/store";
|
|
3
3
|
import FormField from "./form-field.svelte";
|
|
4
|
+
import { STYLES_CONTEXT } from "./styles";
|
|
4
5
|
import { build_fields } from "./utils";
|
|
5
6
|
import { validate_field } from "./validations/validate_field";
|
|
6
7
|
export let initial_form;
|
|
@@ -9,7 +10,14 @@ export let attachments = writable([]);
|
|
|
9
10
|
export let dependencies = void 0;
|
|
10
11
|
export let form_valid = false;
|
|
11
12
|
export let dynamic_field_type = false;
|
|
13
|
+
export let styles = void 0;
|
|
12
14
|
setContext("dynamic_field_type", dynamic_field_type);
|
|
15
|
+
const parentStyles = getContext(STYLES_CONTEXT);
|
|
16
|
+
const stylesContext = writable(
|
|
17
|
+
styles ?? (parentStyles ? getStoreValue(parentStyles) : {})
|
|
18
|
+
);
|
|
19
|
+
setContext(STYLES_CONTEXT, stylesContext);
|
|
20
|
+
$: stylesContext.set(styles ?? (parentStyles ? getStoreValue(parentStyles) : {}));
|
|
13
21
|
let fields = [];
|
|
14
22
|
let form_errors = {};
|
|
15
23
|
const rebuild_fields = (initial_form2, form2) => {
|
|
@@ -42,8 +50,17 @@ const validate_form = async (_) => {
|
|
|
42
50
|
};
|
|
43
51
|
</script>
|
|
44
52
|
|
|
45
|
-
<div class="
|
|
53
|
+
<div class="form">
|
|
46
54
|
{#each fields as fieldStore (getStoreValue(fieldStore).id)}
|
|
47
55
|
<FormField {fieldStore} />
|
|
48
56
|
{/each}
|
|
49
57
|
</div>
|
|
58
|
+
|
|
59
|
+
<style>
|
|
60
|
+
.form {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 0.5rem;
|
|
64
|
+
width: 100%;
|
|
65
|
+
}
|
|
66
|
+
</style>
|