@functionalcms/svelte-components 4.30.0 → 4.31.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/dist/components/form/Dropzone.svelte +165 -140
- package/dist/components/form/Dropzone.svelte.d.ts +2 -10
- package/dist/components/form/SmartForm.svelte +12 -70
- package/dist/components/form/SmartForm.svelte.d.ts +2 -6
- package/dist/components/form/form.d.ts +1 -8
- package/dist/components/form/form.js +0 -15
- package/dist/components/layouts/TwoColumnsLayout.svelte +155 -25
- package/dist/components/layouts/TwoColumnsLayout.svelte.d.ts +7 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -1,158 +1,183 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
label: string;
|
|
6
|
-
buttonLabel: string
|
|
7
|
-
css: string;
|
|
8
|
-
accept: string;
|
|
9
|
-
multiple: boolean;
|
|
10
|
-
maxSize: number;
|
|
11
|
-
renderSelctedFilesPreview?: Snippet<[File[]]>;
|
|
12
|
-
}
|
|
2
|
+
import { displaySize, type FileDropZoneProps, type FileRejectedReason } from './dropzone.js';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
13
4
|
|
|
14
5
|
let {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
function onInputChange(e) {
|
|
29
|
-
const picked = Array.from<File>(e.currentTarget.files ?? []);
|
|
30
|
-
setFiles(picked);
|
|
31
|
-
}
|
|
6
|
+
id,
|
|
7
|
+
children,
|
|
8
|
+
maxFiles,
|
|
9
|
+
maxFileSize,
|
|
10
|
+
fileCount,
|
|
11
|
+
disabled = false,
|
|
12
|
+
onUpload,
|
|
13
|
+
onFileRejected,
|
|
14
|
+
accept,
|
|
15
|
+
class: css,
|
|
16
|
+
...rest
|
|
17
|
+
}: FileDropZoneProps = $props();
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
19
|
+
if (maxFiles !== undefined && fileCount === undefined) {
|
|
20
|
+
console.warn(
|
|
21
|
+
'Make sure to provide FileDropZone with `fileCount` when using the `maxFiles` prompt'
|
|
36
22
|
);
|
|
37
|
-
files = [...files, ...filtered];
|
|
38
23
|
}
|
|
39
24
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
25
|
+
let uploading = $state(false);
|
|
26
|
+
|
|
27
|
+
const drop = async (
|
|
28
|
+
e: DragEvent & {
|
|
29
|
+
currentTarget: EventTarget & HTMLLabelElement;
|
|
30
|
+
}
|
|
31
|
+
) => {
|
|
32
|
+
if (disabled || !canUploadFiles) return;
|
|
33
|
+
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
|
|
36
|
+
const droppedFiles = Array.from(e.dataTransfer?.files ?? []);
|
|
37
|
+
|
|
38
|
+
await upload(droppedFiles);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const change = async (
|
|
42
|
+
e: Event & {
|
|
43
|
+
currentTarget: EventTarget & HTMLInputElement;
|
|
44
|
+
}
|
|
45
|
+
) => {
|
|
46
|
+
if (disabled) return;
|
|
47
|
+
|
|
48
|
+
const selectedFiles = e.currentTarget.files;
|
|
49
|
+
|
|
50
|
+
if (!selectedFiles) return;
|
|
51
|
+
|
|
52
|
+
await upload(Array.from(selectedFiles));
|
|
53
|
+
|
|
54
|
+
// this if a file fails and we upload the same file again we still get feedback
|
|
55
|
+
(e.target as HTMLInputElement).value = '';
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const shouldAcceptFile = (file: File, fileNumber: number): FileRejectedReason | undefined => {
|
|
59
|
+
if (maxFileSize !== undefined && file.size > maxFileSize) return 'Maximum file size exceeded';
|
|
60
|
+
|
|
61
|
+
if (maxFiles !== undefined && fileNumber > maxFiles) return 'Maximum files uploaded';
|
|
62
|
+
|
|
63
|
+
if (!accept) return undefined;
|
|
64
|
+
|
|
65
|
+
const acceptedTypes = accept.split(',').map((a) => a.trim().toLowerCase());
|
|
66
|
+
const fileType = file.type.toLowerCase();
|
|
67
|
+
const fileName = file.name.toLowerCase();
|
|
68
|
+
|
|
69
|
+
const isAcceptable = acceptedTypes.some((pattern) => {
|
|
70
|
+
// check extension like .mp4
|
|
71
|
+
if (fileType.startsWith('.')) {
|
|
72
|
+
return fileName.endsWith(pattern);
|
|
50
73
|
}
|
|
51
|
-
|
|
74
|
+
|
|
75
|
+
// if pattern has wild card like video/*
|
|
76
|
+
if (pattern.endsWith('/*')) {
|
|
77
|
+
const baseType = pattern.slice(0, pattern.indexOf('/*'));
|
|
78
|
+
return fileType.startsWith(baseType + '/');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// otherwise it must be a specific type like video/mp4
|
|
82
|
+
return fileType === pattern;
|
|
52
83
|
});
|
|
53
|
-
}
|
|
54
84
|
|
|
55
|
-
|
|
56
|
-
e.preventDefault();
|
|
57
|
-
e.stopPropagation();
|
|
58
|
-
const dropped = Array.from(e.dataTransfer?.files ?? []);
|
|
59
|
-
setFiles(dropped);
|
|
60
|
-
isDragging = false;
|
|
61
|
-
}
|
|
85
|
+
if (!isAcceptable) return 'File type not allowed';
|
|
62
86
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
e.dataTransfer.dropEffect = "copy";
|
|
66
|
-
isDragging = true;
|
|
67
|
-
}
|
|
87
|
+
return undefined;
|
|
88
|
+
};
|
|
68
89
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
const upload = async (uploadFiles: File[]) => {
|
|
91
|
+
uploading = true;
|
|
92
|
+
|
|
93
|
+
const validFiles: File[] = [];
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < uploadFiles.length; i++) {
|
|
96
|
+
const file = uploadFiles[i];
|
|
74
97
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
84
|
-
event.preventDefault();
|
|
85
|
-
fileInput?.click();
|
|
98
|
+
const rejectedReason = shouldAcceptFile(file, (fileCount ?? 0) + i + 1);
|
|
99
|
+
|
|
100
|
+
if (rejectedReason) {
|
|
101
|
+
onFileRejected?.({ file, reason: rejectedReason });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
validFiles.push(file);
|
|
86
106
|
}
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
|
|
108
|
+
await onUpload(validFiles);
|
|
109
|
+
|
|
110
|
+
uploading = false;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const canUploadFiles = $derived(
|
|
114
|
+
!disabled &&
|
|
115
|
+
!uploading &&
|
|
116
|
+
!(maxFiles !== undefined && fileCount !== undefined && fileCount >= maxFiles)
|
|
117
|
+
);
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<label
|
|
121
|
+
ondragover={(e) => e.preventDefault()}
|
|
122
|
+
ondrop={drop}
|
|
123
|
+
for={id}
|
|
124
|
+
aria-disabled={!canUploadFiles}
|
|
125
|
+
class={cn(
|
|
126
|
+
'flex h-48 w-full place-items-center justify-center rounded-lg border-2 border-dashed border-border p-6 transition-all hover:cursor-pointer hover:bg-accent/25 aria-disabled:opacity-50 aria-disabled:hover:cursor-not-allowed',
|
|
127
|
+
className
|
|
128
|
+
)}
|
|
89
129
|
>
|
|
130
|
+
{#if children}
|
|
131
|
+
{@render children()}
|
|
132
|
+
{:else}
|
|
133
|
+
<div class="flex flex-col place-items-center justify-center gap-2">
|
|
134
|
+
<div
|
|
135
|
+
class="flex size-14 place-items-center justify-center rounded-full border border-dashed border-border text-muted-foreground"
|
|
136
|
+
>
|
|
137
|
+
<svg
|
|
138
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
139
|
+
width="24"
|
|
140
|
+
height="24"
|
|
141
|
+
viewBox="0 0 24 24"
|
|
142
|
+
fill="none"
|
|
143
|
+
stroke="currentColor"
|
|
144
|
+
stroke-width="2"
|
|
145
|
+
stroke-linecap="round"
|
|
146
|
+
stroke-linejoin="round"
|
|
147
|
+
class="lucide lucide-upload-icon lucide-upload"
|
|
148
|
+
><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline
|
|
149
|
+
points="17 8 12 3 7 8"
|
|
150
|
+
/><line x1="12" x2="12" y1="3" y2="15" /></svg
|
|
151
|
+
>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="flex flex-col gap-0.5 text-center">
|
|
154
|
+
<span class="font-medium text-muted-foreground">
|
|
155
|
+
Drag 'n' drop files here, or click to select files
|
|
156
|
+
</span>
|
|
157
|
+
{#if maxFiles || maxFileSize}
|
|
158
|
+
<span class="text-sm text-muted-foreground/75">
|
|
159
|
+
{#if maxFiles}
|
|
160
|
+
<span>You can upload {maxFiles} files</span>
|
|
161
|
+
{/if}
|
|
162
|
+
{#if maxFiles && maxFileSize}
|
|
163
|
+
<span>(up to {displaySize(maxFileSize)} each)</span>
|
|
164
|
+
{/if}
|
|
165
|
+
{#if maxFileSize && !maxFiles}
|
|
166
|
+
<span>Maximum size {displaySize(maxFileSize)}</span>
|
|
167
|
+
{/if}
|
|
168
|
+
</span>
|
|
169
|
+
{/if}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
{/if}
|
|
90
173
|
<input
|
|
91
|
-
|
|
92
|
-
|
|
174
|
+
{...rest}
|
|
175
|
+
disabled={!canUploadFiles}
|
|
176
|
+
{id}
|
|
93
177
|
{accept}
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
178
|
+
multiple={maxFiles === undefined || maxFiles - (fileCount ?? 0) > 1}
|
|
179
|
+
type="file"
|
|
180
|
+
onchange={change}
|
|
181
|
+
class="hidden"
|
|
98
182
|
/>
|
|
99
|
-
|
|
100
|
-
<span class="title">{@html label}</span>
|
|
101
|
-
<button type="button" class="browse">{buttonLabel}</button>
|
|
102
|
-
</label>
|
|
103
|
-
|
|
104
|
-
{#if files.length}
|
|
105
|
-
{#if renderSelctedFilesPreview}
|
|
106
|
-
{@render renderSelctedFilesPreview(files)}
|
|
107
|
-
{:else}
|
|
108
|
-
<ul class="file-list">
|
|
109
|
-
{#each files as f}
|
|
110
|
-
<li>{f.name} — {(f.size / 1024).toFixed(1)} KB</li>
|
|
111
|
-
{/each}
|
|
112
|
-
</ul>
|
|
113
|
-
{/if}
|
|
114
|
-
{/if}
|
|
115
|
-
</div>
|
|
116
|
-
|
|
117
|
-
<style>
|
|
118
|
-
.dropzone {
|
|
119
|
-
border: 2px dashed #9aa0a6;
|
|
120
|
-
border-radius: 10px;
|
|
121
|
-
padding: 24px;
|
|
122
|
-
text-align: center;
|
|
123
|
-
transition:
|
|
124
|
-
border-color 0.15s,
|
|
125
|
-
background-color 0.15s;
|
|
126
|
-
position: relative;
|
|
127
|
-
cursor: pointer;
|
|
128
|
-
}
|
|
129
|
-
.dropzone.dragging {
|
|
130
|
-
border-color: #1a73e8;
|
|
131
|
-
background-color: #f0f7ff;
|
|
132
|
-
}
|
|
133
|
-
/* Hide native input but keep it accessible via label */
|
|
134
|
-
input[type="file"] {
|
|
135
|
-
position: absolute;
|
|
136
|
-
inset: 0;
|
|
137
|
-
opacity: 0;
|
|
138
|
-
width: 100%;
|
|
139
|
-
height: 100%;
|
|
140
|
-
cursor: pointer;
|
|
141
|
-
}
|
|
142
|
-
.prompt {
|
|
143
|
-
display: grid;
|
|
144
|
-
gap: 8px;
|
|
145
|
-
justify-items: center;
|
|
146
|
-
}
|
|
147
|
-
.browse {
|
|
148
|
-
padding: 6px 12px;
|
|
149
|
-
border: 1px solid #1a73e8;
|
|
150
|
-
background: #1a73e8;
|
|
151
|
-
color: white;
|
|
152
|
-
border-radius: 6px;
|
|
153
|
-
}
|
|
154
|
-
.file-list {
|
|
155
|
-
margin-top: 12px;
|
|
156
|
-
text-align: left;
|
|
157
|
-
}
|
|
158
|
-
</style>
|
|
183
|
+
</label>
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
declare const Dropzone: import("svelte").Component<
|
|
3
|
-
label: string;
|
|
4
|
-
buttonLabel: string;
|
|
5
|
-
css: string;
|
|
6
|
-
accept: string;
|
|
7
|
-
multiple: boolean;
|
|
8
|
-
maxSize: number;
|
|
9
|
-
renderSelctedFilesPreview?: Snippet<[File[]]>;
|
|
10
|
-
}>, {}, "">;
|
|
1
|
+
import { type FileDropZoneProps } from './dropzone.js';
|
|
2
|
+
declare const Dropzone: import("svelte").Component<FileDropZoneProps, {}, "">;
|
|
11
3
|
type Dropzone = ReturnType<typeof Dropzone>;
|
|
12
4
|
export default Dropzone;
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import Dropzone from './Dropzone.svelte';
|
|
3
2
|
import Input from './Input.svelte';
|
|
4
|
-
import { type Field, FieldType, InputType, SubmitResult
|
|
3
|
+
import { type Field, FieldType, InputType, SubmitResult} from './form.js';
|
|
5
4
|
import { fade } from 'svelte/transition';
|
|
6
5
|
|
|
7
|
-
interface Css {
|
|
8
|
-
form?: string;
|
|
9
|
-
error?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
6
|
interface Props {
|
|
13
|
-
action
|
|
14
|
-
css?:
|
|
7
|
+
action: string;
|
|
8
|
+
css?: string;
|
|
15
9
|
successMessage?: string;
|
|
16
10
|
submitButonText?: string;
|
|
17
11
|
fields?: Array<Field>;
|
|
@@ -21,10 +15,10 @@
|
|
|
21
15
|
let {
|
|
22
16
|
action,
|
|
23
17
|
fields = [],
|
|
24
|
-
css =
|
|
18
|
+
css = '',
|
|
25
19
|
submitButonText = 'Submit',
|
|
26
20
|
successMessage,
|
|
27
|
-
onMessageSubmitted
|
|
21
|
+
onMessageSubmitted,
|
|
28
22
|
}: Props = $props();
|
|
29
23
|
|
|
30
24
|
let form: any;
|
|
@@ -39,8 +33,7 @@
|
|
|
39
33
|
isSendingMessage = true;
|
|
40
34
|
event.preventDefault();
|
|
41
35
|
const formData = new FormData(form);
|
|
42
|
-
const
|
|
43
|
-
const response = await fetch(formAction, {
|
|
36
|
+
const response = await fetch(action, {
|
|
44
37
|
method: 'POST',
|
|
45
38
|
body: formData
|
|
46
39
|
});
|
|
@@ -51,17 +44,6 @@
|
|
|
51
44
|
console.error('Error submitting form:', error);
|
|
52
45
|
}
|
|
53
46
|
};
|
|
54
|
-
const handleFilesSelect = (field: Field) => {
|
|
55
|
-
const processNewUpload = async (files: File[]) => {
|
|
56
|
-
if (!Array.isArray(field.value)) {
|
|
57
|
-
field.value = [];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
field.value = [...field.value, ...files];
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
return processNewUpload;
|
|
64
|
-
};
|
|
65
47
|
</script>
|
|
66
48
|
|
|
67
49
|
{#if showMessage}
|
|
@@ -69,7 +51,7 @@
|
|
|
69
51
|
{successMessage}
|
|
70
52
|
</div>
|
|
71
53
|
{:else}
|
|
72
|
-
<form method="POST" bind:this={form} class={css
|
|
54
|
+
<form method="POST" bind:this={form} class={css}>
|
|
73
55
|
<fieldset>
|
|
74
56
|
{#each fields as field}
|
|
75
57
|
{#if field.type === FieldType.Input}
|
|
@@ -80,8 +62,6 @@
|
|
|
80
62
|
placeholder={field.placeholder}
|
|
81
63
|
type={field.subType}
|
|
82
64
|
isRequired={field.isRequired || false}
|
|
83
|
-
value={field.value}
|
|
84
|
-
{...field.props}
|
|
85
65
|
/>
|
|
86
66
|
{:else if field.type === FieldType.Textarea}
|
|
87
67
|
<Input
|
|
@@ -91,67 +71,29 @@
|
|
|
91
71
|
type={InputType.Textarea}
|
|
92
72
|
placeholder={field.placeholder}
|
|
93
73
|
isRequired={field.isRequired || false}
|
|
94
|
-
value={field.value}
|
|
95
|
-
/>
|
|
96
|
-
{:else if field.type === FieldType.Hidden}
|
|
97
|
-
<input
|
|
98
|
-
id={field.name}
|
|
99
|
-
name={field.name}
|
|
100
|
-
value={field.value}
|
|
101
|
-
type="hidden"
|
|
102
|
-
required={field.isRequired || false}
|
|
103
74
|
/>
|
|
104
75
|
{:else if field.type === FieldType.Radio}
|
|
105
76
|
{#each field.options as option}
|
|
106
77
|
<label>
|
|
107
78
|
{#if option.checked}
|
|
108
|
-
<input
|
|
109
|
-
type="radio"
|
|
110
|
-
name={field.name}
|
|
111
|
-
value={option.value}
|
|
112
|
-
required={field.isRequired}
|
|
113
|
-
checked
|
|
114
|
-
/>
|
|
79
|
+
<input type="radio" name={field.name} value={option.value} required={field.isRequired} checked/>
|
|
115
80
|
{:else}
|
|
116
|
-
<input
|
|
117
|
-
type="radio"
|
|
118
|
-
name={field.name}
|
|
119
|
-
value={option.value}
|
|
120
|
-
required={field.isRequired}
|
|
121
|
-
/>
|
|
81
|
+
<input type="radio" name={field.name} value={option.value} required={field.isRequired}/>
|
|
122
82
|
{/if}
|
|
123
83
|
{option.label}
|
|
124
|
-
</label>
|
|
84
|
+
</label>
|
|
125
85
|
{/each}
|
|
126
86
|
{:else if field.type === FieldType.Checkbox}
|
|
127
87
|
{#each field.options as option}
|
|
128
88
|
<label>
|
|
129
89
|
{#if option.checked}
|
|
130
|
-
|
|
131
|
-
type="checkbox"
|
|
132
|
-
name={field.name}
|
|
133
|
-
value={option.value}
|
|
134
|
-
required={field.isRequired}
|
|
135
|
-
checked
|
|
136
|
-
/>
|
|
90
|
+
<input type="checkbox" name={field.name} value={option.value} required={field.isRequired} checked/>
|
|
137
91
|
{:else}
|
|
138
|
-
|
|
139
|
-
type="checkbox"
|
|
140
|
-
name={field.name}
|
|
141
|
-
value={option.value}
|
|
142
|
-
required={field.isRequired}
|
|
143
|
-
/>
|
|
92
|
+
<input type="checkbox" name={field.name} value={option.value} required={field.isRequired} />
|
|
144
93
|
{/if}
|
|
145
94
|
{option.label}
|
|
146
95
|
</label>
|
|
147
96
|
{/each}
|
|
148
|
-
{:else if field.type === FieldType.DropZone}
|
|
149
|
-
<Dropzone ondrop={handleFilesSelect(field)}>
|
|
150
|
-
{field.label}
|
|
151
|
-
</Dropzone>
|
|
152
|
-
{/if}
|
|
153
|
-
{#if field.error}
|
|
154
|
-
<div class={css.error}>{field.error}</div>
|
|
155
97
|
{/if}
|
|
156
98
|
{/each}
|
|
157
99
|
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { type Field, SubmitResult } from './form.js';
|
|
2
|
-
interface Css {
|
|
3
|
-
form?: string;
|
|
4
|
-
error?: string;
|
|
5
|
-
}
|
|
6
2
|
interface Props {
|
|
7
|
-
action
|
|
8
|
-
css?:
|
|
3
|
+
action: string;
|
|
4
|
+
css?: string;
|
|
9
5
|
successMessage?: string;
|
|
10
6
|
submitButonText?: string;
|
|
11
7
|
fields?: Array<Field>;
|
|
@@ -2,9 +2,7 @@ export declare enum FieldType {
|
|
|
2
2
|
Input = "input",
|
|
3
3
|
Textarea = "textarea",
|
|
4
4
|
Radio = "radio",
|
|
5
|
-
Checkbox = "checkbox"
|
|
6
|
-
Hidden = "hidden",
|
|
7
|
-
DropZone = "dropzone"
|
|
5
|
+
Checkbox = "checkbox"
|
|
8
6
|
}
|
|
9
7
|
export declare enum InputType {
|
|
10
8
|
Text = "text",
|
|
@@ -40,13 +38,8 @@ export interface Field {
|
|
|
40
38
|
checked?: boolean;
|
|
41
39
|
}>;
|
|
42
40
|
checked?: boolean;
|
|
43
|
-
value?: string | number | boolean | File[];
|
|
44
|
-
props?: any;
|
|
45
|
-
error?: string;
|
|
46
41
|
}
|
|
47
42
|
export declare enum SubmitResult {
|
|
48
43
|
Success = "success",
|
|
49
44
|
Error = "error"
|
|
50
45
|
}
|
|
51
|
-
export declare const readForm: (request: Request, fields: Array<Field>) => Promise<any>;
|
|
52
|
-
export declare const refreshPage: () => void;
|
|
@@ -4,8 +4,6 @@ export var FieldType;
|
|
|
4
4
|
FieldType["Textarea"] = "textarea";
|
|
5
5
|
FieldType["Radio"] = "radio";
|
|
6
6
|
FieldType["Checkbox"] = "checkbox";
|
|
7
|
-
FieldType["Hidden"] = "hidden";
|
|
8
|
-
FieldType["DropZone"] = "dropzone";
|
|
9
7
|
})(FieldType || (FieldType = {}));
|
|
10
8
|
export var InputType;
|
|
11
9
|
(function (InputType) {
|
|
@@ -35,16 +33,3 @@ export var SubmitResult;
|
|
|
35
33
|
SubmitResult["Success"] = "success";
|
|
36
34
|
SubmitResult["Error"] = "error";
|
|
37
35
|
})(SubmitResult || (SubmitResult = {}));
|
|
38
|
-
export const readForm = async (request, fields) => {
|
|
39
|
-
const formData = await request.formData();
|
|
40
|
-
let response = {};
|
|
41
|
-
fields.forEach(field => {
|
|
42
|
-
response[field.name] = formData.get(field.name);
|
|
43
|
-
});
|
|
44
|
-
return response;
|
|
45
|
-
};
|
|
46
|
-
export const refreshPage = () => {
|
|
47
|
-
if (window) {
|
|
48
|
-
window?.location?.reload();
|
|
49
|
-
}
|
|
50
|
-
};
|
|
@@ -1,44 +1,174 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import type { Snippet } from 'svelte';
|
|
4
|
-
|
|
5
|
-
interface Css {}
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
6
3
|
|
|
7
4
|
interface TwoColumnLayoutProps {
|
|
8
5
|
leftRender: Snippet;
|
|
9
6
|
rightRender: Snippet;
|
|
10
7
|
containerCss: string;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
spitPercentage: number;
|
|
9
|
+
isResizable: boolean;
|
|
10
|
+
isCollapsible: boolean;
|
|
11
|
+
collapsedWidthPx: number;
|
|
12
|
+
minWidthPercent: number;
|
|
13
|
+
maxWidthPercent: number;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
let {
|
|
18
17
|
leftRender,
|
|
19
18
|
rightRender,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
containerCss = "",
|
|
20
|
+
spitPercentage = 30,
|
|
21
|
+
isResizable = true,
|
|
22
|
+
isCollapsible = true,
|
|
23
|
+
collapsedWidthPx = 60,
|
|
24
|
+
minWidthPercent = 10,
|
|
25
|
+
maxWidthPercent = 80,
|
|
25
26
|
}: Partial<TwoColumnLayoutProps> = $props();
|
|
26
27
|
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
28
|
+
let container: HTMLElement | undefined = undefined;
|
|
29
|
+
let leftWidthPx = $state(0);
|
|
30
|
+
let isCollapsed = $state(false);
|
|
31
|
+
let isDragging = $state(false);
|
|
32
|
+
|
|
33
|
+
$effect(() => {
|
|
34
|
+
if (container) {
|
|
35
|
+
const containerWidth = container.getBoundingClientRect().width;
|
|
36
|
+
leftWidthPx = containerWidth * (spitPercentage / 100);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const toggleCollapse = () => {
|
|
41
|
+
isCollapsed = !isCollapsed;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let startX = 0;
|
|
45
|
+
|
|
46
|
+
const handleMouseDown = (event: MouseEvent) => {
|
|
47
|
+
isDragging = true;
|
|
48
|
+
startX = event.clientX;
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
53
|
+
if (!isDragging) return;
|
|
54
|
+
|
|
55
|
+
const deltaX = event.clientX - startX;
|
|
56
|
+
const containerWidth = container?.getBoundingClientRect().width || 1;
|
|
57
|
+
const newWidth = leftWidthPx + deltaX;
|
|
58
|
+
const minWidthPx = containerWidth * (minWidthPercent / 100);
|
|
59
|
+
const maxWidthPx = containerWidth * (maxWidthPercent / 100);
|
|
60
|
+
|
|
61
|
+
if (newWidth >= minWidthPx && newWidth <= maxWidthPx) {
|
|
62
|
+
leftWidthPx = newWidth;
|
|
63
|
+
startX = event.clientX;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleMouseUp = () => {
|
|
68
|
+
isDragging = false;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
$effect(() => {
|
|
72
|
+
if (isDragging) {
|
|
73
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
74
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
78
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
33
82
|
</script>
|
|
34
83
|
|
|
35
|
-
<section class={
|
|
36
|
-
<div class="left {
|
|
37
|
-
{
|
|
84
|
+
<section class="container {containerCss}" bind:this={container}>
|
|
85
|
+
<div class="left" style:width={isCollapsed ? `${collapsedWidthPx}px` : `${leftWidthPx}px`}>
|
|
86
|
+
{#if isCollapsible}
|
|
87
|
+
<div class="toggle-button-container">
|
|
88
|
+
<button class="toggle-button" onclick={toggleCollapse} aria-label={isCollapsed ? "Expand" : "Collapse"}>
|
|
89
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
90
|
+
{#if isCollapsed}
|
|
91
|
+
<path d="M6 12L10 8L6 4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
92
|
+
{:else}
|
|
93
|
+
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
94
|
+
{/if}
|
|
95
|
+
</svg>
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
{/if}
|
|
99
|
+
{@render leftRender?.(isCollapsed)}
|
|
38
100
|
</div>
|
|
39
|
-
|
|
40
|
-
|
|
101
|
+
|
|
102
|
+
{#if isResizable && !isCollapsed}
|
|
103
|
+
<div
|
|
104
|
+
class="isResizable"
|
|
105
|
+
onmousedown={handleMouseDown}
|
|
106
|
+
role="separator"
|
|
107
|
+
aria-label="Resizer"
|
|
108
|
+
></div>
|
|
109
|
+
{/if}
|
|
110
|
+
|
|
111
|
+
<div class="right" style:flex="1">
|
|
112
|
+
{@render rightRender?.()}
|
|
41
113
|
</div>
|
|
42
114
|
</section>
|
|
43
115
|
|
|
44
|
-
<style
|
|
116
|
+
<style>
|
|
117
|
+
.container {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: row;
|
|
120
|
+
width: 100%;
|
|
121
|
+
height: 100%;
|
|
122
|
+
position: relative;
|
|
123
|
+
}
|
|
124
|
+
.left {
|
|
125
|
+
transition: width 0.3s ease;
|
|
126
|
+
position: relative;
|
|
127
|
+
overflow-x: hidden;
|
|
128
|
+
}
|
|
129
|
+
.right {
|
|
130
|
+
}
|
|
131
|
+
.isResizable {
|
|
132
|
+
width: 4px;
|
|
133
|
+
background-color: #e0e0e0;
|
|
134
|
+
cursor: col-resize;
|
|
135
|
+
transition: background-color 0.2s;
|
|
136
|
+
flex-shrink: 0;
|
|
137
|
+
}
|
|
138
|
+
.isResizable:hover {
|
|
139
|
+
background-color: #0078d4;
|
|
140
|
+
}
|
|
141
|
+
.toggle-button-container {
|
|
142
|
+
position: sticky;
|
|
143
|
+
top: 0;
|
|
144
|
+
left: 0;
|
|
145
|
+
width: 100%;
|
|
146
|
+
height: 48px;
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: flex-start;
|
|
150
|
+
padding: 0 8px;
|
|
151
|
+
background-color: transparent;
|
|
152
|
+
z-index: 100;
|
|
153
|
+
}
|
|
154
|
+
.toggle-button {
|
|
155
|
+
background-color: transparent;
|
|
156
|
+
border: none;
|
|
157
|
+
width: 32px;
|
|
158
|
+
height: 32px;
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
justify-content: center;
|
|
162
|
+
cursor: pointer;
|
|
163
|
+
border-radius: 2px;
|
|
164
|
+
color: #323130;
|
|
165
|
+
transition: background-color 0.1s;
|
|
166
|
+
padding: 0;
|
|
167
|
+
}
|
|
168
|
+
.toggle-button:hover {
|
|
169
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
170
|
+
}
|
|
171
|
+
.toggle-button:active {
|
|
172
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import type { Snippet } from
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
2
|
declare const TwoColumnsLayout: import("svelte").Component<Partial<{
|
|
3
3
|
leftRender: Snippet;
|
|
4
4
|
rightRender: Snippet;
|
|
5
5
|
containerCss: string;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
spitPercentage: number;
|
|
7
|
+
isResizable: boolean;
|
|
8
|
+
isCollapsible: boolean;
|
|
9
|
+
collapsedWidthPx: number;
|
|
10
|
+
minWidthPercent: number;
|
|
11
|
+
maxWidthPercent: number;
|
|
10
12
|
}>, {}, "">;
|
|
11
13
|
type TwoColumnsLayout = ReturnType<typeof TwoColumnsLayout>;
|
|
12
14
|
export default TwoColumnsLayout;
|
package/dist/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { default as Button } from './components/form/Button.svelte';
|
|
|
29
29
|
export { default as Input } from './components/form/Input.svelte';
|
|
30
30
|
export { default as Switch } from './components/form/Switch.svelte';
|
|
31
31
|
export { default as ChoiceInput } from './components/form/ChoiceInput.svelte';
|
|
32
|
-
export { InputType, FieldType, LabelSize, type Field, SubmitResult
|
|
32
|
+
export { InputType, FieldType, LabelSize, type Field, SubmitResult } from './components/form/form.js';
|
|
33
33
|
export { default as AntiBot } from './components/form/AntiBot.svelte';
|
|
34
34
|
export { default as Dropzone } from './components/form/Dropzone.svelte';
|
|
35
35
|
export { default as Select } from './components/form/Select.svelte';
|
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ export { default as Button } from './components/form/Button.svelte';
|
|
|
42
42
|
export { default as Input } from './components/form/Input.svelte';
|
|
43
43
|
export { default as Switch } from './components/form/Switch.svelte';
|
|
44
44
|
export { default as ChoiceInput } from './components/form/ChoiceInput.svelte';
|
|
45
|
-
export { InputType, FieldType, LabelSize, SubmitResult
|
|
45
|
+
export { InputType, FieldType, LabelSize, SubmitResult } from './components/form/form.js';
|
|
46
46
|
export { default as AntiBot } from './components/form/AntiBot.svelte';
|
|
47
47
|
export { default as Dropzone } from './components/form/Dropzone.svelte';
|
|
48
48
|
export { default as Select } from './components/form/Select.svelte';
|