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.
Files changed (93) hide show
  1. package/dist/components/pages/security/login/02/ui/LoginCard02.svelte +15 -4
  2. package/dist/components/pages/security/login/03/ui/LoginCard03.svelte +34 -16
  3. package/dist/components/ui/form/Form.svelte +171 -0
  4. package/dist/components/ui/form/Form.svelte.d.ts +4 -0
  5. package/dist/components/ui/form/types.d.ts +78 -0
  6. package/dist/components/ui/form/types.js +1 -0
  7. package/dist/components/ui/form/ui/input-badges.svelte +214 -0
  8. package/dist/components/ui/form/ui/input-badges.svelte.d.ts +14 -0
  9. package/dist/components/ui/form/ui/input-checkbox.svelte +86 -0
  10. package/dist/components/ui/form/ui/input-checkbox.svelte.d.ts +17 -0
  11. package/dist/components/ui/form/ui/input-radio.svelte +82 -0
  12. package/dist/components/ui/form/ui/input-radio.svelte.d.ts +17 -0
  13. package/dist/components/ui/form/ui/input-select.svelte +195 -0
  14. package/dist/components/ui/form/ui/input-select.svelte.d.ts +17 -0
  15. package/dist/components/ui/form/ui/input-textarea.svelte +61 -0
  16. package/dist/components/ui/form/ui/input-textarea.svelte.d.ts +13 -0
  17. package/dist/components/ui/form/ui/input-toggle.svelte +89 -0
  18. package/dist/components/ui/form/ui/input-toggle.svelte.d.ts +11 -0
  19. package/dist/components/ui/modal/badge/index.d.ts +3 -0
  20. package/dist/components/ui/modal/badge/index.js +2 -0
  21. package/dist/components/ui/modal/badge/types.d.ts +35 -0
  22. package/dist/components/ui/modal/badge/types.js +1 -0
  23. package/dist/components/ui/modal/badge/ui/ModalBadgeSelection.svelte +87 -0
  24. package/dist/components/ui/modal/badge/ui/ModalBadgeSelection.svelte.d.ts +4 -0
  25. package/dist/components/ui/modal/core/index.d.ts +3 -0
  26. package/dist/components/ui/modal/core/index.js +2 -0
  27. package/dist/components/ui/modal/core/types.d.ts +82 -0
  28. package/dist/components/ui/modal/core/types.js +1 -0
  29. package/dist/components/ui/modal/core/ui/ModalCore.svelte +129 -0
  30. package/dist/components/ui/modal/core/ui/ModalCore.svelte.d.ts +4 -0
  31. package/dist/components/ui/modal/feedback/index.d.ts +3 -0
  32. package/dist/components/ui/modal/feedback/index.js +2 -0
  33. package/dist/components/ui/modal/feedback/types.d.ts +32 -0
  34. package/dist/components/ui/modal/feedback/types.js +1 -0
  35. package/dist/components/ui/modal/feedback/ui/ModalFeedback.svelte +135 -0
  36. package/dist/components/ui/modal/feedback/ui/ModalFeedback.svelte.d.ts +4 -0
  37. package/dist/components/ui/modal/form/index.d.ts +3 -0
  38. package/dist/components/ui/modal/form/index.js +2 -0
  39. package/dist/components/ui/modal/form/types.d.ts +38 -0
  40. package/dist/components/ui/modal/form/types.js +1 -0
  41. package/dist/components/ui/modal/form/ui/ModalForm.svelte +67 -0
  42. package/dist/components/ui/modal/form/ui/ModalForm.svelte.d.ts +4 -0
  43. package/dist/components/ui/modal/grid/index.d.ts +5 -0
  44. package/dist/components/ui/modal/grid/index.js +4 -0
  45. package/dist/components/ui/modal/grid/types.d.ts +51 -0
  46. package/dist/components/ui/modal/grid/types.js +1 -0
  47. package/dist/components/ui/modal/grid/ui/01/ModalGridSelection01.svelte +83 -0
  48. package/dist/components/ui/modal/grid/ui/01/ModalGridSelection01.svelte.d.ts +4 -0
  49. package/dist/components/ui/modal/grid/ui/02/ModalGridSelection02.svelte +142 -0
  50. package/dist/components/ui/modal/grid/ui/02/ModalGridSelection02.svelte.d.ts +4 -0
  51. package/dist/components/ui/modal/grid/ui/ModalGridSelection.svelte +60 -0
  52. package/dist/components/ui/modal/grid/ui/ModalGridSelection.svelte.d.ts +4 -0
  53. package/dist/components/ui/modal/map/index.d.ts +3 -0
  54. package/dist/components/ui/modal/map/index.js +2 -0
  55. package/dist/components/ui/modal/map/types.d.ts +48 -0
  56. package/dist/components/ui/modal/map/types.js +1 -0
  57. package/dist/components/ui/modal/map/ui/ModalMap.svelte +187 -0
  58. package/dist/components/ui/modal/map/ui/ModalMap.svelte.d.ts +4 -0
  59. package/dist/components/ui/modal/notification/index.d.ts +3 -0
  60. package/dist/components/ui/modal/notification/index.js +2 -0
  61. package/dist/components/ui/modal/notification/types.d.ts +26 -0
  62. package/dist/components/ui/modal/notification/types.js +1 -0
  63. package/dist/components/ui/modal/notification/ui/ModalNotification.svelte +65 -0
  64. package/dist/components/ui/modal/notification/ui/ModalNotification.svelte.d.ts +4 -0
  65. package/dist/components/ui/modal/share/index.d.ts +3 -0
  66. package/dist/components/ui/modal/share/index.js +2 -0
  67. package/dist/components/ui/modal/share/types.d.ts +32 -0
  68. package/dist/components/ui/modal/share/types.js +1 -0
  69. package/dist/components/ui/modal/share/ui/ModalShareSelection.svelte +103 -0
  70. package/dist/components/ui/modal/share/ui/ModalShareSelection.svelte.d.ts +4 -0
  71. package/dist/components/ui/modal/upload/index.d.ts +3 -0
  72. package/dist/components/ui/modal/upload/index.js +2 -0
  73. package/dist/components/ui/modal/upload/types.d.ts +79 -0
  74. package/dist/components/ui/modal/upload/types.js +1 -0
  75. package/dist/components/ui/modal/upload/ui/ModalUpload.svelte +268 -0
  76. package/dist/components/ui/modal/upload/ui/ModalUpload.svelte.d.ts +4 -0
  77. package/dist/components/ui/section/Section01.svelte +90 -0
  78. package/dist/components/ui/section/Section01.svelte.d.ts +4 -0
  79. package/dist/components/ui/section/Section02.svelte +68 -0
  80. package/dist/components/ui/section/Section02.svelte.d.ts +4 -0
  81. package/dist/components/ui/section/Section03.svelte +52 -0
  82. package/dist/components/ui/section/Section03.svelte.d.ts +4 -0
  83. package/dist/components/ui/section/index.d.ts +4 -0
  84. package/dist/components/ui/section/index.js +3 -0
  85. package/dist/components/ui/section/types.d.ts +46 -0
  86. package/dist/components/ui/section/types.js +1 -0
  87. package/dist/docs/components.d.ts +1 -1
  88. package/dist/docs/components.js +882 -0
  89. package/dist/globals.css +443 -0
  90. package/dist/index.d.ts +169 -0
  91. package/dist/index.js +169 -0
  92. package/dist/types/index.d.ts +46 -0
  93. 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 { LoginFormType, LoginRequestDto, LinkProps, SocialLoginItem } from "../../types";
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 bg-white">
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">{$t("text.login.welcome")}</h1>
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 {onSubmit} {forgetPassword} {socialLogins} />
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
- <p class="text-center text-sm text-gray-600">
83
- {$t("text.login.no-account")}
84
- <a
85
- href={register?.url ?? "/register"}
86
- onclick={register?.onclick}
87
- class="font-semibold text-primary hover:underline"
88
- >
89
- {$t("text.login.sign-up")}
90
- </a>
91
- </p>
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 bg-gray-50 rounded-xl">
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 text-gray-900">
127
+ <p class="text-sm font-semibold">
110
128
  {$t("text.login.users.count")}
111
129
  </p>
112
- <p class="text-xs text-gray-500">{$t("text.login.users.worldwide")}</p>
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,4 @@
1
+ import type { FormProps } from "./types";
2
+ declare const Form: import("svelte").Component<FormProps, {}, "">;
3
+ type Form = ReturnType<typeof Form>;
4
+ export default 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;