negodesign 0.0.74 → 0.0.75
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/pages/security/login/02/ui/LoginCard02.svelte +15 -4
- package/dist/components/pages/security/login/03/ui/LoginCard03.svelte +34 -16
- package/dist/components/ui/form/Form.svelte +171 -0
- package/dist/components/ui/form/Form.svelte.d.ts +4 -0
- package/dist/components/ui/form/types.d.ts +78 -0
- package/dist/components/ui/form/types.js +1 -0
- package/dist/components/ui/form/ui/input-badges.svelte +214 -0
- package/dist/components/ui/form/ui/input-badges.svelte.d.ts +14 -0
- package/dist/components/ui/form/ui/input-checkbox.svelte +86 -0
- package/dist/components/ui/form/ui/input-checkbox.svelte.d.ts +17 -0
- package/dist/components/ui/form/ui/input-radio.svelte +82 -0
- package/dist/components/ui/form/ui/input-radio.svelte.d.ts +17 -0
- package/dist/components/ui/form/ui/input-select.svelte +195 -0
- package/dist/components/ui/form/ui/input-select.svelte.d.ts +17 -0
- package/dist/components/ui/form/ui/input-textarea.svelte +61 -0
- package/dist/components/ui/form/ui/input-textarea.svelte.d.ts +13 -0
- package/dist/components/ui/form/ui/input-toggle.svelte +89 -0
- package/dist/components/ui/form/ui/input-toggle.svelte.d.ts +11 -0
- package/dist/components/ui/modal/badge/index.d.ts +3 -0
- package/dist/components/ui/modal/badge/index.js +2 -0
- package/dist/components/ui/modal/badge/types.d.ts +35 -0
- package/dist/components/ui/modal/badge/types.js +1 -0
- package/dist/components/ui/modal/badge/ui/ModalBadgeSelection.svelte +87 -0
- package/dist/components/ui/modal/badge/ui/ModalBadgeSelection.svelte.d.ts +4 -0
- package/dist/components/ui/modal/core/index.d.ts +3 -0
- package/dist/components/ui/modal/core/index.js +2 -0
- package/dist/components/ui/modal/core/types.d.ts +82 -0
- package/dist/components/ui/modal/core/types.js +1 -0
- package/dist/components/ui/modal/core/ui/ModalCore.svelte +129 -0
- package/dist/components/ui/modal/core/ui/ModalCore.svelte.d.ts +4 -0
- package/dist/components/ui/modal/feedback/index.d.ts +3 -0
- package/dist/components/ui/modal/feedback/index.js +2 -0
- package/dist/components/ui/modal/feedback/types.d.ts +32 -0
- package/dist/components/ui/modal/feedback/types.js +1 -0
- package/dist/components/ui/modal/feedback/ui/ModalFeedback.svelte +135 -0
- package/dist/components/ui/modal/feedback/ui/ModalFeedback.svelte.d.ts +4 -0
- package/dist/components/ui/modal/form/index.d.ts +3 -0
- package/dist/components/ui/modal/form/index.js +2 -0
- package/dist/components/ui/modal/form/types.d.ts +38 -0
- package/dist/components/ui/modal/form/types.js +1 -0
- package/dist/components/ui/modal/form/ui/ModalForm.svelte +67 -0
- package/dist/components/ui/modal/form/ui/ModalForm.svelte.d.ts +4 -0
- package/dist/components/ui/modal/grid/index.d.ts +5 -0
- package/dist/components/ui/modal/grid/index.js +4 -0
- package/dist/components/ui/modal/grid/types.d.ts +51 -0
- package/dist/components/ui/modal/grid/types.js +1 -0
- package/dist/components/ui/modal/grid/ui/01/ModalGridSelection01.svelte +83 -0
- package/dist/components/ui/modal/grid/ui/01/ModalGridSelection01.svelte.d.ts +4 -0
- package/dist/components/ui/modal/grid/ui/02/ModalGridSelection02.svelte +142 -0
- package/dist/components/ui/modal/grid/ui/02/ModalGridSelection02.svelte.d.ts +4 -0
- package/dist/components/ui/modal/grid/ui/ModalGridSelection.svelte +60 -0
- package/dist/components/ui/modal/grid/ui/ModalGridSelection.svelte.d.ts +4 -0
- package/dist/components/ui/modal/map/index.d.ts +3 -0
- package/dist/components/ui/modal/map/index.js +2 -0
- package/dist/components/ui/modal/map/types.d.ts +48 -0
- package/dist/components/ui/modal/map/types.js +1 -0
- package/dist/components/ui/modal/map/ui/ModalMap.svelte +187 -0
- package/dist/components/ui/modal/map/ui/ModalMap.svelte.d.ts +4 -0
- package/dist/components/ui/modal/notification/index.d.ts +3 -0
- package/dist/components/ui/modal/notification/index.js +2 -0
- package/dist/components/ui/modal/notification/types.d.ts +26 -0
- package/dist/components/ui/modal/notification/types.js +1 -0
- package/dist/components/ui/modal/notification/ui/ModalNotification.svelte +65 -0
- package/dist/components/ui/modal/notification/ui/ModalNotification.svelte.d.ts +4 -0
- package/dist/components/ui/modal/share/index.d.ts +3 -0
- package/dist/components/ui/modal/share/index.js +2 -0
- package/dist/components/ui/modal/share/types.d.ts +32 -0
- package/dist/components/ui/modal/share/types.js +1 -0
- package/dist/components/ui/modal/share/ui/ModalShareSelection.svelte +103 -0
- package/dist/components/ui/modal/share/ui/ModalShareSelection.svelte.d.ts +4 -0
- package/dist/components/ui/modal/upload/index.d.ts +3 -0
- package/dist/components/ui/modal/upload/index.js +2 -0
- package/dist/components/ui/modal/upload/types.d.ts +79 -0
- package/dist/components/ui/modal/upload/types.js +1 -0
- package/dist/components/ui/modal/upload/ui/ModalUpload.svelte +268 -0
- package/dist/components/ui/modal/upload/ui/ModalUpload.svelte.d.ts +4 -0
- package/dist/components/ui/section/Section01.svelte +90 -0
- package/dist/components/ui/section/Section01.svelte.d.ts +4 -0
- package/dist/components/ui/section/Section02.svelte +68 -0
- package/dist/components/ui/section/Section02.svelte.d.ts +4 -0
- package/dist/components/ui/section/Section03.svelte +52 -0
- package/dist/components/ui/section/Section03.svelte.d.ts +4 -0
- package/dist/components/ui/section/index.d.ts +4 -0
- package/dist/components/ui/section/index.js +3 -0
- package/dist/components/ui/section/types.d.ts +46 -0
- package/dist/components/ui/section/types.js +1 -0
- package/dist/docs/components.d.ts +1 -1
- package/dist/docs/components.js +882 -0
- package/dist/globals.css +443 -0
- package/dist/index.d.ts +169 -0
- package/dist/index.js +169 -0
- package/dist/types/index.d.ts +46 -0
- package/package.json +1 -1
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
import LanguageSwitcher from "../../../../../ui/language-switcher/language-switcher.svelte";
|
|
13
13
|
import { LightSwitch } from "../../../../../ui/light-switch";
|
|
14
14
|
import { t } from "../../../../../../i18n";
|
|
15
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
LoginFormType,
|
|
17
|
+
LoginRequestDto,
|
|
18
|
+
LinkProps,
|
|
19
|
+
SocialLoginItem,
|
|
20
|
+
} from "../../types";
|
|
16
21
|
import FormLoginUsernamePassword02 from "./FormLoginUsernamePassword02.svelte";
|
|
17
22
|
import FormLoginEmailPassword02 from "./FormLoginEmailPassword02.svelte";
|
|
18
23
|
import FormLoginPhonePassword02 from "./FormLoginPhonePassword02.svelte";
|
|
@@ -38,7 +43,7 @@
|
|
|
38
43
|
}: Props = $props();
|
|
39
44
|
</script>
|
|
40
45
|
|
|
41
|
-
<div class="flex-1 flex flex-col justify-between h-full
|
|
46
|
+
<div class="flex-1 flex flex-col justify-between h-full">
|
|
42
47
|
<nav class="flex justify-end p-5">
|
|
43
48
|
<div class="flex gap-2">
|
|
44
49
|
<LightSwitch />
|
|
@@ -49,7 +54,9 @@
|
|
|
49
54
|
<div class="flex-1 flex flex-col justify-center px-8 md:px-12 pb-8">
|
|
50
55
|
<div class="w-full max-w-sm mx-auto space-y-6">
|
|
51
56
|
<div class="text-center space-y-2">
|
|
52
|
-
<h1 class="text-2xl font-bold text-gray-900">
|
|
57
|
+
<h1 class="text-2xl font-bold text-gray-900">
|
|
58
|
+
{$t("text.login.welcome")}
|
|
59
|
+
</h1>
|
|
53
60
|
<p class="text-sm text-gray-500">
|
|
54
61
|
{$t("text.login.continue")}
|
|
55
62
|
</p>
|
|
@@ -58,7 +65,11 @@
|
|
|
58
65
|
{#if formType == "EMAIL_PASSWORD"}
|
|
59
66
|
<FormLoginEmailPassword02 {onSubmit} {forgetPassword} {socialLogins} />
|
|
60
67
|
{:else if formType == "USERNAME_PASSWORD"}
|
|
61
|
-
<FormLoginUsernamePassword02
|
|
68
|
+
<FormLoginUsernamePassword02
|
|
69
|
+
{onSubmit}
|
|
70
|
+
{forgetPassword}
|
|
71
|
+
{socialLogins}
|
|
72
|
+
/>
|
|
62
73
|
{:else if formType == "PHONE_PASSWORD"}
|
|
63
74
|
<FormLoginPhonePassword02 {onSubmit} {forgetPassword} {socialLogins} />
|
|
64
75
|
{/if}
|
|
@@ -54,9 +54,7 @@
|
|
|
54
54
|
<div class="flex-1 flex flex-col justify-center px-8 md:px-12 pb-8">
|
|
55
55
|
<div class="w-full max-w-sm mx-auto space-y-6">
|
|
56
56
|
<div class="space-y-2">
|
|
57
|
-
<div
|
|
58
|
-
class="w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center"
|
|
59
|
-
>
|
|
57
|
+
<div class="w-12 h-12 rounded-xl flex items-center justify-center">
|
|
60
58
|
<span class="text-xl font-bold text-gray-600">G</span>
|
|
61
59
|
</div>
|
|
62
60
|
<h1 class="text-2xl font-bold text-gray-900">
|
|
@@ -79,21 +77,41 @@
|
|
|
79
77
|
<FormLoginPhonePassword03 {onSubmit} {forgetPassword} {socialLogins} />
|
|
80
78
|
{/if}
|
|
81
79
|
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
<div class="flex flex-col space-y-2">
|
|
81
|
+
<div class="text-center text-xs space-x-2">
|
|
82
|
+
<a
|
|
83
|
+
href={privacyPolicy?.url ?? "/privacy-policy"}
|
|
84
|
+
onclick={privacyPolicy?.onclick}
|
|
85
|
+
class="hover:underline"
|
|
86
|
+
>
|
|
87
|
+
{$t("link.privacy-policy.label")}
|
|
88
|
+
</a>
|
|
89
|
+
<span>|</span>
|
|
90
|
+
<a
|
|
91
|
+
href={termsOfService?.url ?? "/terms-of-service"}
|
|
92
|
+
onclick={termsOfService?.onclick}
|
|
93
|
+
class="hover:underline"
|
|
94
|
+
>
|
|
95
|
+
{$t("link.terms-of-service.label")}
|
|
96
|
+
</a>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<p class="text-center text-sm">
|
|
100
|
+
{$t("text.login.no-account")}
|
|
101
|
+
<a
|
|
102
|
+
href={register?.url ?? "/register"}
|
|
103
|
+
onclick={register?.onclick}
|
|
104
|
+
class="font-semibold text-primary hover:underline"
|
|
105
|
+
>
|
|
106
|
+
{$t("text.login.sign-up")}
|
|
107
|
+
</a>
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
92
110
|
</div>
|
|
93
111
|
</div>
|
|
94
112
|
|
|
95
113
|
<div class="px-8 pb-8">
|
|
96
|
-
<div class="flex items-center gap-3 p-4
|
|
114
|
+
<div class="flex items-center gap-3 p-4 rounded-xl">
|
|
97
115
|
<div class="flex -space-x-2">
|
|
98
116
|
<div
|
|
99
117
|
class="w-8 h-8 rounded-full bg-linear-to-br from-blue-400 to-blue-600 border-2 border-white"
|
|
@@ -106,10 +124,10 @@
|
|
|
106
124
|
></div>
|
|
107
125
|
</div>
|
|
108
126
|
<div>
|
|
109
|
-
<p class="text-sm font-semibold
|
|
127
|
+
<p class="text-sm font-semibold">
|
|
110
128
|
{$t("text.login.users.count")}
|
|
111
129
|
</p>
|
|
112
|
-
<p class="text-xs
|
|
130
|
+
<p class="text-xs">{$t("text.login.users.worldwide")}</p>
|
|
113
131
|
</div>
|
|
114
132
|
</div>
|
|
115
133
|
</div>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from "../../../utils";
|
|
3
|
+
import InputBase from "./ui/input-base.svelte";
|
|
4
|
+
import InputSelect from "./ui/input-select.svelte";
|
|
5
|
+
import InputRadio from "./ui/input-radio.svelte";
|
|
6
|
+
import InputCheckbox from "./ui/input-checkbox.svelte";
|
|
7
|
+
import InputTextarea from "./ui/input-textarea.svelte";
|
|
8
|
+
import InputToggle from "./ui/input-toggle.svelte";
|
|
9
|
+
import InputBadges from "./ui/input-badges.svelte";
|
|
10
|
+
import type { FormProps, FormInputConfig } from "./types";
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
inputs = [],
|
|
14
|
+
onSubmit,
|
|
15
|
+
submitText = "Submit",
|
|
16
|
+
submitLoadingText = "Submitting...",
|
|
17
|
+
isLoading = false,
|
|
18
|
+
columns = 2,
|
|
19
|
+
gap = "gap-4",
|
|
20
|
+
class: className,
|
|
21
|
+
}: FormProps = $props();
|
|
22
|
+
|
|
23
|
+
let formValues = $state<Record<string, string | string[] | boolean>>(getInitialValues());
|
|
24
|
+
|
|
25
|
+
function getInitialValues(): Record<string, string | string[] | boolean> {
|
|
26
|
+
const values: Record<string, string | string[] | boolean> = {};
|
|
27
|
+
for (const input of inputs) {
|
|
28
|
+
if (input.type === "toggle") {
|
|
29
|
+
values[input.name] = (input.value as boolean) ?? false;
|
|
30
|
+
} else if (input.type === "checkbox" || input.type === "badges" || input.multiple) {
|
|
31
|
+
values[input.name] = (input.value as string[]) ?? [];
|
|
32
|
+
} else {
|
|
33
|
+
values[input.name] = (input.value as string) ?? "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return values;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleTextChange(name: string, e: Event) {
|
|
40
|
+
const val = (e.target as HTMLInputElement | HTMLTextAreaElement).value;
|
|
41
|
+
formValues[name] = val;
|
|
42
|
+
const config = inputs.find((i) => i.name === name);
|
|
43
|
+
if (config?.onChange) {
|
|
44
|
+
(config.onChange as (v: string | string[] | boolean) => void)(val);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function handleSelectChange(name: string, val: string | string[]) {
|
|
49
|
+
formValues[name] = val;
|
|
50
|
+
const config = inputs.find((i) => i.name === name);
|
|
51
|
+
if (config?.onChange) {
|
|
52
|
+
(config.onChange as (v: string | string[] | boolean) => void)(val);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleToggleChange(name: string, val: boolean) {
|
|
57
|
+
formValues[name] = val;
|
|
58
|
+
const config = inputs.find((i) => i.name === name);
|
|
59
|
+
if (config?.onChange) {
|
|
60
|
+
(config.onChange as (v: string | string[] | boolean) => void)(val);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleSubmit(e: Event) {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
if (isLoading) return;
|
|
67
|
+
onSubmit?.(formValues);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getColSpan(input: FormInputConfig): string {
|
|
71
|
+
if (input.fill) return "col-span-1 md:col-span-2";
|
|
72
|
+
return "col-span-1";
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<form
|
|
77
|
+
onsubmit={handleSubmit}
|
|
78
|
+
class={cn(
|
|
79
|
+
"grid w-full",
|
|
80
|
+
columns === 2 ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1",
|
|
81
|
+
gap,
|
|
82
|
+
className
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{#each inputs as input (input.name)}
|
|
86
|
+
<div class={getColSpan(input)}>
|
|
87
|
+
{#if input.type === "select"}
|
|
88
|
+
<InputSelect
|
|
89
|
+
label={input.label}
|
|
90
|
+
isLabel
|
|
91
|
+
placeholder={input.placeholder}
|
|
92
|
+
options={input.options ?? []}
|
|
93
|
+
multiple={input.multiple ?? false}
|
|
94
|
+
disabled={input.disabled ?? false}
|
|
95
|
+
required={input.required ?? false}
|
|
96
|
+
bind:value={() => formValues[input.name] as string, (v) => handleSelectChange(input.name, v)}
|
|
97
|
+
/>
|
|
98
|
+
{:else if input.type === "radio"}
|
|
99
|
+
<InputRadio
|
|
100
|
+
label={input.label}
|
|
101
|
+
isLabel
|
|
102
|
+
name={input.name}
|
|
103
|
+
options={input.options ?? []}
|
|
104
|
+
disabled={input.disabled ?? false}
|
|
105
|
+
required={input.required ?? false}
|
|
106
|
+
bind:value={() => formValues[input.name] as string, (v) => handleSelectChange(input.name, v)}
|
|
107
|
+
/>
|
|
108
|
+
{:else if input.type === "checkbox"}
|
|
109
|
+
<InputCheckbox
|
|
110
|
+
label={input.label}
|
|
111
|
+
isLabel
|
|
112
|
+
name={input.name}
|
|
113
|
+
options={input.options ?? []}
|
|
114
|
+
disabled={input.disabled ?? false}
|
|
115
|
+
required={input.required ?? false}
|
|
116
|
+
bind:value={() => formValues[input.name] as string[], (v) => handleSelectChange(input.name, v)}
|
|
117
|
+
/>
|
|
118
|
+
{:else if input.type === "textarea"}
|
|
119
|
+
<InputTextarea
|
|
120
|
+
label={input.label}
|
|
121
|
+
isLabel
|
|
122
|
+
placeholder={input.placeholder}
|
|
123
|
+
disabled={input.disabled ?? false}
|
|
124
|
+
required={input.required ?? false}
|
|
125
|
+
rows={input.rows ?? 4}
|
|
126
|
+
bind:value={() => formValues[input.name] as string, (v) => handleSelectChange(input.name, v)}
|
|
127
|
+
/>
|
|
128
|
+
{:else if input.type === "toggle"}
|
|
129
|
+
<InputToggle
|
|
130
|
+
label={input.label}
|
|
131
|
+
isLabel
|
|
132
|
+
description={input.description}
|
|
133
|
+
disabled={input.disabled ?? false}
|
|
134
|
+
bind:value={() => formValues[input.name] as boolean, (v) => handleToggleChange(input.name, v)}
|
|
135
|
+
/>
|
|
136
|
+
{:else if input.type === "badges"}
|
|
137
|
+
<InputBadges
|
|
138
|
+
label={input.label}
|
|
139
|
+
isLabel
|
|
140
|
+
placeholder={input.placeholder}
|
|
141
|
+
suggestions={input.suggestions ?? []}
|
|
142
|
+
maxTags={input.maxTags ?? 0}
|
|
143
|
+
disabled={input.disabled ?? false}
|
|
144
|
+
required={input.required ?? false}
|
|
145
|
+
bind:value={() => formValues[input.name] as string[], (v) => handleSelectChange(input.name, v)}
|
|
146
|
+
/>
|
|
147
|
+
{:else}
|
|
148
|
+
<InputBase
|
|
149
|
+
type={input.type === "phone" ? "tel" : input.type}
|
|
150
|
+
label={input.label}
|
|
151
|
+
isLabel
|
|
152
|
+
isIcon
|
|
153
|
+
placeholder={input.placeholder}
|
|
154
|
+
disabled={input.disabled ?? false}
|
|
155
|
+
required={input.required ?? false}
|
|
156
|
+
bind:value={() => formValues[input.name] as string, (v) => handleSelectChange(input.name, v)}
|
|
157
|
+
/>
|
|
158
|
+
{/if}
|
|
159
|
+
</div>
|
|
160
|
+
{/each}
|
|
161
|
+
|
|
162
|
+
<div class={cn("flex justify-end", columns === 2 ? "col-span-1 md:col-span-2" : "col-span-1")}>
|
|
163
|
+
<button
|
|
164
|
+
type="submit"
|
|
165
|
+
disabled={isLoading}
|
|
166
|
+
class="inline-flex items-center gap-2 rounded-lg bg-primary px-6 py-2.5 text-sm font-semibold text-primary-foreground transition hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
167
|
+
>
|
|
168
|
+
{isLoading ? submitLoadingText : submitText}
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</form>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opção para inputs do tipo select, radio ou checkbox.
|
|
3
|
+
* @property value - Valor identificador da opção.
|
|
4
|
+
* @property label - Texto visível da opção.
|
|
5
|
+
*/
|
|
6
|
+
export type FormOption = {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Configuração de um input dentro do Form.
|
|
12
|
+
* Cada item define um campo do formulário com o seu tipo e propriedades.
|
|
13
|
+
*
|
|
14
|
+
* Tipos suportados:
|
|
15
|
+
* - `'text'` | `'email'` | `'password'` | `'date'` | `'phone'` — inputs de texto
|
|
16
|
+
* - `'select'` — dropdown/select com opções
|
|
17
|
+
* - `'radio'` — grupo de radio buttons
|
|
18
|
+
* - `'checkbox'` — grupo de checkboxes
|
|
19
|
+
* - `'textarea'` — área de texto
|
|
20
|
+
* - `'toggle'` — interruptor on/off
|
|
21
|
+
* - `'badges'` — input de tags/badges com sugestões
|
|
22
|
+
*
|
|
23
|
+
* @property type - Tipo do input.
|
|
24
|
+
* @property name - Nome do campo (usado como chave no objeto de dados).
|
|
25
|
+
* @property value - Valor inicial do campo.
|
|
26
|
+
* @property label - Label/rótulo do campo.
|
|
27
|
+
* @property placeholder - Texto placeholder.
|
|
28
|
+
* @property onChange - Callback ao alterar o valor.
|
|
29
|
+
* @property disabled - Desativa o campo.
|
|
30
|
+
* @property required - Marca como obrigatório.
|
|
31
|
+
* @property fill - Ocupa 2 colunas (true) ou 1 coluna (false/default).
|
|
32
|
+
* @property options - Opções para select, radio ou checkbox.
|
|
33
|
+
* @property multiple - Permite seleção múltipla (apenas select).
|
|
34
|
+
* @property rows - Número de linhas (apenas textarea).
|
|
35
|
+
* @property description - Texto descritivo (apenas toggle).
|
|
36
|
+
* @property suggestions - Sugestões de tags (apenas badges).
|
|
37
|
+
* @property maxTags - Número máximo de tags (apenas badges, 0 = ilimitado).
|
|
38
|
+
*/
|
|
39
|
+
export type FormInputConfig = {
|
|
40
|
+
type: "text" | "email" | "password" | "date" | "phone" | "select" | "radio" | "checkbox" | "textarea" | "toggle" | "badges";
|
|
41
|
+
name: string;
|
|
42
|
+
value?: string | string[] | boolean;
|
|
43
|
+
label?: string;
|
|
44
|
+
placeholder?: string;
|
|
45
|
+
onChange?: (value: string | string[] | boolean) => void;
|
|
46
|
+
disabled?: boolean;
|
|
47
|
+
required?: boolean;
|
|
48
|
+
fill?: boolean;
|
|
49
|
+
options?: FormOption[];
|
|
50
|
+
multiple?: boolean;
|
|
51
|
+
rows?: number;
|
|
52
|
+
description?: string;
|
|
53
|
+
suggestions?: string[];
|
|
54
|
+
maxTags?: number;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Props do componente Form — formulário com grid responsivo de 2 colunas.
|
|
58
|
+
* Organiza os inputs numa grid, onde cada campo pode ocupar 1 ou 2 colunas via `fill`.
|
|
59
|
+
*
|
|
60
|
+
* @property inputs - Array de configurações dos inputs do formulário.
|
|
61
|
+
* @property onSubmit - Callback ao submeter o formulário (objeto com name:value).
|
|
62
|
+
* @property submitText - Texto do botão de submissão.
|
|
63
|
+
* @property submitLoadingText - Texto durante carregamento.
|
|
64
|
+
* @property isLoading - Estado de carregamento.
|
|
65
|
+
* @property columns - Número de colunas da grid (padrão: 2).
|
|
66
|
+
* @property gap - Espaçamento entre os campos.
|
|
67
|
+
* @property class - Classe CSS extra no container.
|
|
68
|
+
*/
|
|
69
|
+
export type FormProps = {
|
|
70
|
+
inputs: FormInputConfig[];
|
|
71
|
+
onSubmit?: (data: Record<string, string | string[] | boolean>) => void;
|
|
72
|
+
submitText?: string;
|
|
73
|
+
submitLoadingText?: string;
|
|
74
|
+
isLoading?: boolean;
|
|
75
|
+
columns?: 1 | 2;
|
|
76
|
+
gap?: string;
|
|
77
|
+
class?: string;
|
|
78
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Label } from "../../label";
|
|
3
|
+
import { cn } from "../../../../utils";
|
|
4
|
+
import { Cancel01Icon } from "@hugeicons/core-free-icons";
|
|
5
|
+
import { HugeiconsIcon } from "@hugeicons/svelte";
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
label,
|
|
9
|
+
isLabel = false,
|
|
10
|
+
value = $bindable([]),
|
|
11
|
+
disabled = false,
|
|
12
|
+
required = false,
|
|
13
|
+
placeholder = "Add a tag...",
|
|
14
|
+
suggestions = [],
|
|
15
|
+
maxTags = 0,
|
|
16
|
+
class: className,
|
|
17
|
+
}: {
|
|
18
|
+
label?: string;
|
|
19
|
+
isLabel?: boolean;
|
|
20
|
+
value?: string[];
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
required?: boolean;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
suggestions?: string[];
|
|
25
|
+
maxTags?: number;
|
|
26
|
+
class?: string;
|
|
27
|
+
} = $props();
|
|
28
|
+
|
|
29
|
+
let inputValue = $state("");
|
|
30
|
+
let isInputFocused = $state(false);
|
|
31
|
+
let showSuggestions = $state(false);
|
|
32
|
+
|
|
33
|
+
const filteredSuggestions = $derived(
|
|
34
|
+
suggestions.filter(
|
|
35
|
+
(s) =>
|
|
36
|
+
!value.includes(s) &&
|
|
37
|
+
s.toLowerCase().includes(inputValue.toLowerCase())
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
function addTag(tag: string) {
|
|
42
|
+
const trimmed = tag.trim();
|
|
43
|
+
if (!trimmed || disabled) return;
|
|
44
|
+
if (value.includes(trimmed)) return;
|
|
45
|
+
if (maxTags > 0 && value.length >= maxTags) return;
|
|
46
|
+
value = [...value, trimmed];
|
|
47
|
+
inputValue = "";
|
|
48
|
+
showSuggestions = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function removeTag(tag: string) {
|
|
52
|
+
if (disabled) return;
|
|
53
|
+
value = value.filter((t) => t !== tag);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
57
|
+
if (e.key === "Enter") {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
addTag(inputValue);
|
|
60
|
+
} else if (e.key === "Backspace" && !inputValue && value.length > 0) {
|
|
61
|
+
value = value.slice(0, -1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleInput(e: Event) {
|
|
66
|
+
inputValue = (e.target as HTMLInputElement).value;
|
|
67
|
+
showSuggestions = inputValue.length > 0 && filteredSuggestions.length > 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function handleFocus() {
|
|
71
|
+
isInputFocused = true;
|
|
72
|
+
if (inputValue.length > 0 && filteredSuggestions.length > 0) {
|
|
73
|
+
showSuggestions = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleBlur() {
|
|
78
|
+
isInputFocused = false;
|
|
79
|
+
setTimeout(() => (showSuggestions = false), 150);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function selectSuggestion(suggestion: string) {
|
|
83
|
+
addTag(suggestion);
|
|
84
|
+
inputValue = "";
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
{#if isLabel}
|
|
89
|
+
<div class={cn("flex w-full flex-col gap-2", className)}>
|
|
90
|
+
{#if label}
|
|
91
|
+
<Label>{label}</Label>
|
|
92
|
+
{/if}
|
|
93
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
94
|
+
<div
|
|
95
|
+
class={cn(
|
|
96
|
+
"flex min-h-[42px] flex-wrap items-center gap-1.5 rounded-md border border-input bg-card px-3 py-1.5 transition-colors",
|
|
97
|
+
isInputFocused && "ring-2 ring-ring ring-offset-2 ring-offset-background",
|
|
98
|
+
disabled && "cursor-not-allowed opacity-50"
|
|
99
|
+
)}
|
|
100
|
+
onclick={() => !disabled && document.getElementById(`badges-input-${label}`)?.focus()}
|
|
101
|
+
>
|
|
102
|
+
{#each value as tag (tag)}
|
|
103
|
+
<span class="inline-flex items-center gap-1 rounded-full border border-border bg-muted px-2.5 py-0.5 text-xs font-medium text-foreground">
|
|
104
|
+
{tag}
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
{disabled}
|
|
108
|
+
class="ml-0.5 rounded-full p-0.5 transition-colors hover:bg-destructive/10 hover:text-destructive"
|
|
109
|
+
onclick={(e) => { e.stopPropagation(); removeTag(tag); }}
|
|
110
|
+
>
|
|
111
|
+
<HugeiconsIcon icon={Cancel01Icon} class="size-3" />
|
|
112
|
+
</button>
|
|
113
|
+
</span>
|
|
114
|
+
{/each}
|
|
115
|
+
|
|
116
|
+
{#if maxTags === 0 || value.length < maxTags}
|
|
117
|
+
<input
|
|
118
|
+
id="badges-input-{label}"
|
|
119
|
+
type="text"
|
|
120
|
+
bind:value={inputValue}
|
|
121
|
+
{placeholder}
|
|
122
|
+
{disabled}
|
|
123
|
+
{required}
|
|
124
|
+
class="min-w-[120px] flex-1 bg-transparent py-1 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
|
|
125
|
+
onkeydown={handleKeydown}
|
|
126
|
+
oninput={handleInput}
|
|
127
|
+
onfocus={handleFocus}
|
|
128
|
+
onblur={handleBlur}
|
|
129
|
+
/>
|
|
130
|
+
{/if}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{#if showSuggestions && filteredSuggestions.length > 0}
|
|
134
|
+
<div class="max-h-40 overflow-auto rounded-md border border-border bg-card shadow-md">
|
|
135
|
+
{#each filteredSuggestions as suggestion (suggestion)}
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
class="flex w-full items-center px-3 py-2 text-sm transition-colors hover:bg-muted"
|
|
139
|
+
onmousedown={(e) => { e.preventDefault(); selectSuggestion(suggestion); }}
|
|
140
|
+
>
|
|
141
|
+
{suggestion}
|
|
142
|
+
</button>
|
|
143
|
+
{/each}
|
|
144
|
+
</div>
|
|
145
|
+
{/if}
|
|
146
|
+
|
|
147
|
+
{#if maxTags > 0}
|
|
148
|
+
<p class="text-xs text-muted-foreground">
|
|
149
|
+
{value.length}/{maxTags} tags
|
|
150
|
+
</p>
|
|
151
|
+
{/if}
|
|
152
|
+
</div>
|
|
153
|
+
{:else}
|
|
154
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
155
|
+
<div
|
|
156
|
+
class={cn(
|
|
157
|
+
"flex min-h-[42px] flex-wrap items-center gap-1.5 rounded-md border border-input bg-card px-3 py-1.5 transition-colors",
|
|
158
|
+
isInputFocused && "ring-2 ring-ring ring-offset-2 ring-offset-background",
|
|
159
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
160
|
+
className
|
|
161
|
+
)}
|
|
162
|
+
onclick={() => !disabled && document.getElementById(`badges-input-${label}`)?.focus()}
|
|
163
|
+
>
|
|
164
|
+
{#each value as tag (tag)}
|
|
165
|
+
<span class="inline-flex items-center gap-1 rounded-full border border-border bg-muted px-2.5 py-0.5 text-xs font-medium text-foreground">
|
|
166
|
+
{tag}
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
{disabled}
|
|
170
|
+
class="ml-0.5 rounded-full p-0.5 transition-colors hover:bg-destructive/10 hover:text-destructive"
|
|
171
|
+
onclick={(e) => { e.stopPropagation(); removeTag(tag); }}
|
|
172
|
+
>
|
|
173
|
+
<HugeiconsIcon icon={Cancel01Icon} class="size-3" />
|
|
174
|
+
</button>
|
|
175
|
+
</span>
|
|
176
|
+
{/each}
|
|
177
|
+
|
|
178
|
+
{#if maxTags === 0 || value.length < maxTags}
|
|
179
|
+
<input
|
|
180
|
+
id="badges-input-{label}"
|
|
181
|
+
type="text"
|
|
182
|
+
bind:value={inputValue}
|
|
183
|
+
{placeholder}
|
|
184
|
+
{disabled}
|
|
185
|
+
{required}
|
|
186
|
+
class="min-w-[120px] flex-1 bg-transparent py-1 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
|
|
187
|
+
onkeydown={handleKeydown}
|
|
188
|
+
oninput={handleInput}
|
|
189
|
+
onfocus={handleFocus}
|
|
190
|
+
onblur={handleBlur}
|
|
191
|
+
/>
|
|
192
|
+
{/if}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{#if showSuggestions && filteredSuggestions.length > 0}
|
|
196
|
+
<div class="max-h-40 overflow-auto rounded-md border border-border bg-card shadow-md">
|
|
197
|
+
{#each filteredSuggestions as suggestion (suggestion)}
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
class="flex w-full items-center px-3 py-2 text-sm transition-colors hover:bg-muted"
|
|
201
|
+
onmousedown={(e) => { e.preventDefault(); selectSuggestion(suggestion); }}
|
|
202
|
+
>
|
|
203
|
+
{suggestion}
|
|
204
|
+
</button>
|
|
205
|
+
{/each}
|
|
206
|
+
</div>
|
|
207
|
+
{/if}
|
|
208
|
+
|
|
209
|
+
{#if maxTags > 0}
|
|
210
|
+
<p class="text-xs text-muted-foreground">
|
|
211
|
+
{value.length}/{maxTags} tags
|
|
212
|
+
</p>
|
|
213
|
+
{/if}
|
|
214
|
+
{/if}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
label?: string;
|
|
3
|
+
isLabel?: boolean;
|
|
4
|
+
value?: string[];
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
suggestions?: string[];
|
|
9
|
+
maxTags?: number;
|
|
10
|
+
class?: string;
|
|
11
|
+
};
|
|
12
|
+
declare const InputBadges: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
13
|
+
type InputBadges = ReturnType<typeof InputBadges>;
|
|
14
|
+
export default InputBadges;
|