@fy-/fws-vue 0.3.1 → 0.3.4

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.
@@ -3,7 +3,7 @@ import { LinkIcon } from "@heroicons/vue/24/solid";
3
3
  import { computed, ref, toRef } from "vue";
4
4
  import type { ErrorObject } from "@vuelidate/core";
5
5
  import { useTranslation } from "../../composables/translations";
6
-
6
+ import DefaultTagInput from "./DefaultTagInput.vue";
7
7
  type modelValueType = string | number | string[] | number[] | undefined;
8
8
 
9
9
  type checkboxValueType = any[] | Set<any> | undefined | boolean;
@@ -25,6 +25,7 @@ const props = withDefaults(
25
25
  options?: string[][];
26
26
  help?: string;
27
27
  error?: string;
28
+ color?: string;
28
29
  errorVuelidate?: ErrorObject[];
29
30
  disabled?: boolean;
30
31
  }>(),
@@ -106,7 +107,7 @@ defineExpose({ focus, blur, getInputRef });
106
107
  'select',
107
108
  'phone',
108
109
  'chips',
109
- 'opts',
110
+ 'tags',
110
111
  'mask',
111
112
  ].includes(type)
112
113
  "
@@ -122,10 +123,18 @@ defineExpose({ focus, blur, getInputRef });
122
123
  'search',
123
124
  'url',
124
125
  'mask',
126
+ 'date',
127
+ 'datetime',
125
128
  ].includes(type)
126
129
  "
127
130
  class="relative"
128
131
  >
132
+ <label
133
+ :for="id"
134
+ v-if="label"
135
+ class="block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white"
136
+ >{{ label }}
137
+ </label>
129
138
  <input
130
139
  ref="inputRef"
131
140
  :type="type"
@@ -134,27 +143,41 @@ defineExpose({ focus, blur, getInputRef });
134
143
  :class="{
135
144
  error: checkErrors,
136
145
  }"
146
+ v-model="model"
137
147
  :autocomplete="autocomplete"
138
148
  :disabled="disabled"
139
149
  :aria-describedby="help ? `${id}-help` : id"
140
- class="block px-2.5 pb-2.5 pt-4 w-full text-sm text-fv-neutral-900 bg-transparent rounded-lg border-1 border-fv-neutral-300 appearance-none dark:text-white dark:border-fv-neutral-600 dark:focus:border-fv-primary-500 focus:outline-none focus:ring-0 focus:border-fv-primary-600 peer"
150
+ class="bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-lg focus:ring-fv-primary-500 focus:border-fv-primary-500 block w-full p-2.5 dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-500 dark:focus:border-fv-primary-500"
141
151
  :required="req"
142
152
  @focus="handleFocus"
143
153
  @blur="handleBlur"
144
154
  />
155
+ </div>
156
+ <div v-if="type == 'chips' || type == 'tags'">
145
157
  <label
146
158
  :for="id"
147
159
  v-if="label || placeholder"
148
- class="absolute text-sm text-fv-neutral-500 dark:text-fv-neutral-400 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white dark:bg-fv-neutral-900 px-2 peer-focus:px-2 peer-focus:text-fv-primary-600 peer-focus:dark:text-fv-primary-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto start-1"
160
+ class="block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white"
149
161
  >{{ label ? label : placeholder }}
150
162
  </label>
163
+ <!-- @vue-skip -->
164
+ <DefaultTagInput
165
+ v-model="model"
166
+ :id="id"
167
+ :disabled="disabled"
168
+ :color="color"
169
+ :error="checkErrors"
170
+ :help="help"
171
+ />
151
172
  </div>
152
173
  <div class="group relative" v-else-if="type == 'textarea'">
153
174
  <label
175
+ v-if="label"
154
176
  :for="id"
155
177
  class="block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white"
156
- >Your message</label
178
+ >{{ label }}</label
157
179
  >
180
+ <!-- @vue-skip -->
158
181
  <textarea
159
182
  :id="id"
160
183
  :name="id"
@@ -162,6 +185,7 @@ defineExpose({ focus, blur, getInputRef });
162
185
  :class="{
163
186
  error: checkErrors,
164
187
  }"
188
+ v-model="model"
165
189
  :placeholder="placeholder"
166
190
  :disabled="disabled"
167
191
  :aria-describedby="help ? `${id}-help` : id"
@@ -171,50 +195,6 @@ defineExpose({ focus, blur, getInputRef });
171
195
  class="block p-2.5 w-full text-sm text-fv-neutral-900 bg-fv-neutral-50 rounded-lg border border-fv-neutral-300 focus:ring-fv-primary-500 focus:border-fv-primary-500 dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-500 dark:focus:border-fv-primary-500"
172
196
  ></textarea>
173
197
  </div>
174
- <div
175
- class="relative max-w-sm"
176
- v-else-if="type == 'datetime' || type == 'date'"
177
- >
178
- <div
179
- class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none"
180
- >
181
- <svg
182
- class="w-4 h-4 text-fv-neutral-500 dark:text-fv-neutral-400"
183
- aria-hidden="true"
184
- xmlns="http://www.w3.org/2000/svg"
185
- fill="currentColor"
186
- viewBox="0 0 20 20"
187
- >
188
- <path
189
- d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z"
190
- />
191
- </svg>
192
- </div>
193
- <label
194
- :for="id"
195
- v-if="label"
196
- class="block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white"
197
- >{{ label }}</label
198
- >
199
-
200
- <input
201
- datepicker
202
- ref="inputRef"
203
- type="text"
204
- :class="{
205
- error: checkErrors,
206
- }"
207
- class="bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-lg focus:ring-fv-primary-500 focus:border-fv-primary-500 block w-full ps-10 p-2.5 dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-500 dark:focus:border-fv-primary-500"
208
- :id="id"
209
- :name="id"
210
- :disabled="disabled"
211
- :aria-describedby="help ? `${id}-help` : id"
212
- :required="req"
213
- @focus="handleFocus"
214
- @blur="handleBlur"
215
- :placeholder="placeholder"
216
- />
217
- </div>
218
198
  <div class="relative" v-else-if="type == 'select'">
219
199
  <label
220
200
  :for="id"
@@ -226,6 +206,7 @@ defineExpose({ focus, blur, getInputRef });
226
206
  :id="id"
227
207
  :name="id"
228
208
  ref="inputRef"
209
+ v-model="model"
229
210
  :disabled="disabled"
230
211
  :aria-describedby="help ? `${id}-help` : id"
231
212
  :required="req"
@@ -257,12 +238,14 @@ defineExpose({ focus, blur, getInputRef });
257
238
  <input
258
239
  type="checkbox"
259
240
  v-model="modelCheckbox"
241
+ :true-value="checkboxTrueValue"
242
+ :false-value="checkboxFalseValue"
260
243
  class="sr-only peer"
261
244
  @focus="handleFocus"
262
245
  @blur="handleBlur"
263
246
  />
264
247
  <div
265
- class="relative w-11 h-6 bg-fv-neutral-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-fv-primary-300 dark:peer-focus:ring-fv-primary-800 rounded-full peer dark:bg-fv-neutral-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-fv-neutral-300 after:border after:rounded-full after:w-5 after:h-5 after:transition-all dark:border-fv-neutral-600 peer-checked:bg-fv-primary-600"
248
+ class="relative flex-0 flex-shrink-0 w-11 h-6 bg-fv-neutral-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-fv-primary-300 dark:peer-focus:ring-fv-primary-800 rounded-full peer dark:bg-fv-neutral-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-fv-neutral-300 after:border after:rounded-full after:w-5 after:h-5 after:transition-all dark:border-fv-neutral-600 peer-checked:bg-fv-primary-600"
266
249
  ></div>
267
250
  <span
268
251
  class="ms-3 text-sm font-medium text-fv-neutral-900 dark:text-fv-neutral-300"
@@ -288,6 +271,8 @@ defineExpose({ focus, blur, getInputRef });
288
271
  :type="type"
289
272
  @focus="handleFocus"
290
273
  @blur="handleBlur"
274
+ :true-value="checkboxTrueValue"
275
+ :false-value="checkboxFalseValue"
291
276
  v-model="modelCheckbox"
292
277
  class="w-4 h-4 text-fv-primary-600 bg-fv-neutral-100 border-fv-neutral-300 rounded focus:ring-fv-primary-500 dark:focus:ring-fv-primary-600 dark:ring-offset-fv-neutral-800 dark:focus:ring-offset-fv-neutral-800 focus:ring-2 dark:bg-fv-neutral-700 dark:border-fv-neutral-600"
293
278
  />
@@ -295,13 +280,13 @@ defineExpose({ focus, blur, getInputRef });
295
280
  <div class="ms-2 text-sm">
296
281
  <label
297
282
  :for="id"
298
- class="font-medium text-gray-900 dark:text-gray-300"
283
+ class="font-medium text-fv-neutral-900 dark:text-fv-neutral-300"
299
284
  >{{ label }}</label
300
285
  >
301
286
  <p
302
287
  :id="`${id}-help`"
303
288
  v-if="help"
304
- class="text-xs font-normal text-gray-500 dark:text-gray-400"
289
+ class="text-xs font-normal text-fv-neutral-500 dark:text-fv-neutral-400"
305
290
  >
306
291
  {{ help }}
307
292
  </p>
@@ -0,0 +1,170 @@
1
+ <template>
2
+ <div
3
+ :class="`tags-input ${$props.error ? 'error' : ''}`"
4
+ @click="focusInput"
5
+ @keydown.delete.prevent="removeLastTag"
6
+ @keydown.enter.prevent="addTag"
7
+ >
8
+ <span v-for="(tag, index) in tags" :key="index" :class="`tag ${color}`">
9
+ {{ tag }}
10
+ <button type="button" @click.prevent="removeTag(index)">
11
+ <svg
12
+ class="w-4 h-4"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ fill="none"
15
+ viewBox="0 0 24 24"
16
+ stroke-width="1.5"
17
+ stroke="currentColor"
18
+ >
19
+ <path
20
+ stroke-linecap="round"
21
+ stroke-linejoin="round"
22
+ d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
23
+ />
24
+ </svg>
25
+ </button>
26
+ </span>
27
+ <div
28
+ contenteditable
29
+ class="input"
30
+ :id="`tags_${id}`"
31
+ ref="textInput"
32
+ @input="handleInput"
33
+ @paste.prevent="handlePaste"
34
+ placeholder="Add a tag..."
35
+ ></div>
36
+ </div>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import { ref, watch, onMounted } from "vue";
41
+ type colorType = "blue" | "red" | "green" | "purple" | "orange" | "neutral";
42
+
43
+ const props = withDefaults(
44
+ defineProps<{
45
+ modelValue: string[];
46
+ color?: colorType;
47
+ label?: string;
48
+ id: string;
49
+ separators?: string[];
50
+ autofocus?: boolean;
51
+ help?: string;
52
+ error?: string;
53
+ }>(),
54
+ {
55
+ color: "blue",
56
+ label: "Tags",
57
+ separators: () => [","],
58
+ autofocus: false,
59
+ },
60
+ );
61
+
62
+ const emit = defineEmits(["update:modelValue"]);
63
+ const tags = ref([...props.modelValue]);
64
+ const textInput = ref<HTMLElement>();
65
+
66
+ watch(
67
+ tags,
68
+ (newTags) => {
69
+ emit("update:modelValue", newTags);
70
+ },
71
+ { deep: true },
72
+ );
73
+
74
+ onMounted(() => {
75
+ if (props.autofocus) {
76
+ focusInput();
77
+ }
78
+ });
79
+
80
+ const handleInput = (event: any) => {
81
+ const separatorsRegex = new RegExp(props.separators.join("|"));
82
+ if (separatorsRegex.test(event.data)) {
83
+ addTag();
84
+ }
85
+ };
86
+
87
+ const addTag = () => {
88
+ if (!textInput.value) return;
89
+
90
+ const separatorsRegex = new RegExp(props.separators.join("|"));
91
+ const newTags = textInput.value.innerText
92
+ .split(separatorsRegex)
93
+ .map((tag: string) => tag.trim())
94
+ .filter((tag: string) => tag.length > 0);
95
+ tags.value.push(...newTags);
96
+ textInput.value.innerText = "";
97
+ };
98
+
99
+ const removeTag = (index: number) => {
100
+ tags.value.splice(index, 1);
101
+ focusInput();
102
+ };
103
+
104
+ const removeLastTag = () => {
105
+ if (!textInput.value) return;
106
+
107
+ if (textInput.value.innerText === "") {
108
+ tags.value.pop();
109
+ }
110
+ };
111
+
112
+ const focusInput = () => {
113
+ if (!textInput.value) return;
114
+
115
+ textInput.value.focus();
116
+ };
117
+
118
+ const handlePaste = (e: any) => {
119
+ if (!textInput.value) return;
120
+
121
+ // @ts-ignore
122
+ const text = (e.clipboardData || window.clipboardData).getData("text");
123
+ const separatorsRegex = new RegExp(props.separators.join("|"), "g");
124
+ const pasteText = text.replace(separatorsRegex, ",");
125
+ textInput.value.innerText += pasteText;
126
+ e.preventDefault();
127
+ addTag();
128
+ };
129
+ </script>
130
+
131
+ <style scoped>
132
+ .tags-input {
133
+ cursor: text;
134
+ @apply flex flex-wrap gap-2 items-center shadow-sm bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-sm focus:ring-fv-primary-500 focus:border-fv-primary-500 w-full p-2.5 dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-500 dark:focus:border-fv-primary-500;
135
+ &.error {
136
+ @apply border-red-500 dark:border-red-400 border !important;
137
+ }
138
+ }
139
+ .tag-label {
140
+ @apply block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white;
141
+ }
142
+ .tag {
143
+ @apply inline-flex gap-1 font-medium px-2.5 py-0.5 rounded text-black dark:text-white;
144
+ &.blue {
145
+ @apply bg-blue-400 dark:bg-blue-900;
146
+ }
147
+ &.red {
148
+ @apply bg-red-400 dark:bg-red-900;
149
+ }
150
+ &.green {
151
+ @apply bg-green-400 dark:bg-green-900;
152
+ }
153
+ &.purple {
154
+ @apply bg-purple-400 dark:bg-purple-900;
155
+ }
156
+ &.orange {
157
+ @apply bg-orange-400 dark:bg-orange-900;
158
+ }
159
+ &.neutral {
160
+ @apply bg-fv-neutral-400 dark:bg-fv-neutral-900;
161
+ }
162
+ }
163
+
164
+ .input {
165
+ flex-grow: 1;
166
+ min-width: 100px;
167
+ outline: none;
168
+ border: none;
169
+ }
170
+ </style>
package/index.ts CHANGED
@@ -41,6 +41,7 @@ import DefaultSidebar from "./components/ui/DefaultSidebar.vue";
41
41
  import DefaultGallery from "./components/ui/DefaultGallery.vue";
42
42
  import DefaultDropdown from "./components/ui/DefaultDropdown.vue";
43
43
  import DefaultDropdownLink from "./components/ui/DefaultDropdownLink.vue";
44
+ import DefaultTagInput from "./components/ui/DefaultTagInput.vue";
44
45
  // Components/FWS
45
46
  import UserFlow from "./components/fws/UserFlow.vue";
46
47
  import DataTable from "./components/fws/DataTable.vue";
@@ -125,6 +126,7 @@ export {
125
126
  DefaultGallery,
126
127
  DefaultDropdown,
127
128
  DefaultDropdownLink,
129
+ DefaultTagInput,
128
130
 
129
131
  // FWS
130
132
  UserFlow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "repository": {