@smurfox/proxy-ui 0.3.1 → 0.4.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.
Files changed (30) hide show
  1. package/README.md +91 -6
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Autocomplete.d.vue.ts +0 -0
  4. package/dist/runtime/components/Autocomplete.vue +270 -0
  5. package/dist/runtime/components/Autocomplete.vue.d.ts +0 -0
  6. package/dist/runtime/components/Avatar.d.vue.ts +1 -1
  7. package/dist/runtime/components/Avatar.vue.d.ts +1 -1
  8. package/dist/runtime/components/Button.d.vue.ts +1 -1
  9. package/dist/runtime/components/Button.vue.d.ts +1 -1
  10. package/dist/runtime/components/Chip.d.vue.ts +1 -1
  11. package/dist/runtime/components/Chip.vue.d.ts +1 -1
  12. package/dist/runtime/components/Dropdown.d.vue.ts +11 -13
  13. package/dist/runtime/components/Dropdown.vue +65 -19
  14. package/dist/runtime/components/Dropdown.vue.d.ts +11 -13
  15. package/dist/runtime/components/Input.d.vue.ts +5 -3
  16. package/dist/runtime/components/Input.vue +9 -1
  17. package/dist/runtime/components/Input.vue.d.ts +5 -3
  18. package/dist/runtime/components/Lottie.d.vue.ts +1 -1
  19. package/dist/runtime/components/Lottie.vue.d.ts +1 -1
  20. package/dist/runtime/components/Select.d.vue.ts +0 -37
  21. package/dist/runtime/components/Select.vue +15 -5
  22. package/dist/runtime/components/Select.vue.d.ts +0 -37
  23. package/dist/runtime/components/Tabs.d.vue.ts +1 -1
  24. package/dist/runtime/components/Tabs.vue.d.ts +1 -1
  25. package/dist/runtime/components/TextArea.d.vue.ts +3 -3
  26. package/dist/runtime/components/TextArea.vue.d.ts +3 -3
  27. package/dist/runtime/composables/popoverGroup.d.ts +5 -0
  28. package/dist/runtime/composables/popoverGroup.js +12 -0
  29. package/dist/runtime/types/index.d.ts +17 -0
  30. package/package.json +1 -1
package/README.md CHANGED
@@ -23,14 +23,14 @@ A component library built for **Nuxt 4**, designed with a clean and consistent A
23
23
  ## Installation
24
24
 
25
25
  ```bash
26
- npm install proxy-ui
26
+ npm install @smurfox/proxy-ui
27
27
  ```
28
28
 
29
29
  Add the module to your `nuxt.config.ts`:
30
30
 
31
31
  ```ts
32
32
  export default defineNuxtConfig({
33
- modules: ["proxy-ui"],
33
+ modules: ["@smurfox/proxy-ui"],
34
34
  });
35
35
  ```
36
36
 
@@ -164,6 +164,7 @@ A flexible input component with validation and state management.
164
164
  | `required` | `boolean` | `false` | Shows a red asterisk on the label. |
165
165
  | `error` | `string` | — | Error message to display. Changes styling to danger. |
166
166
  | `disabled` | `boolean` | `false` | Disables the input. |
167
+ | `focus` | `boolean` | `false` | Selects the whole text on focus (useful for pre-filled fields the user is likely to replace). |
167
168
 
168
169
  **Slots**
169
170
 
@@ -178,6 +179,9 @@ A flexible input component with validation and state management.
178
179
  <!-- Basic -->
179
180
  <PUInput label="Email" type="email" placeholder="you@example.com" />
180
181
 
182
+ <!-- Select all on focus (great for pre-filled codes the user wants to replace) -->
183
+ <PUInput v-model="orderCode" label="Order code" focus />
184
+
181
185
  <!-- With helper text -->
182
186
  <PUInput label="Password" type="password" description="At least 8 characters" />
183
187
 
@@ -260,7 +264,7 @@ A multi-line text input. Shares the look and feel of `PUInput`, with extra props
260
264
 
261
265
  ### PUSelect
262
266
 
263
- A custom select with an animated dropdown panel teleported to `body`. Dark-mode aware, supports `v-model`, and emits both `update:modelValue` and `change`.
267
+ A custom select with an animated dropdown panel teleported to `body`. Dark-mode aware, supports `v-model`, and emits both `update:modelValue` and `change`. Only one `PUSelect` can be open at a time across the page — opening a new one automatically closes any other open select.
264
268
 
265
269
  ```vue
266
270
  <PUSelect
@@ -320,6 +324,85 @@ A custom select with an animated dropdown panel teleported to `body`. Dark-mode
320
324
 
321
325
  ---
322
326
 
327
+ ### PUAutocomplete
328
+
329
+ A searchable variant of `PUSelect` — same look and dropdown behavior, but the trigger is a real `<input>` that filters the options as you type. Includes a clear (`×`) button that resets both the search text and the selected value. Like `PUSelect`, the dropdown is teleported to `body` and only one autocomplete can be open at a time.
330
+
331
+ ```vue
332
+ <PUAutocomplete
333
+ v-model="framework"
334
+ label="Framework"
335
+ placeholder="Search a framework"
336
+ :options="[
337
+ { label: 'Nuxt', value: 'nuxt' },
338
+ { label: 'Vue', value: 'vue' },
339
+ { label: 'React', value: 'react' },
340
+ ]"
341
+ />
342
+ ```
343
+
344
+ Filtering is case-insensitive and matches `label`. When the input text matches the currently-selected option's label exactly, the list is shown unfiltered so the user can browse all options again without clearing first. On close without selection, the input reverts to the selected option's label (or empty if nothing was selected).
345
+
346
+ **Props**
347
+
348
+ | Prop | Type | Default | Description |
349
+ | ------------- | ----------------------------------------------------------- | ------------------------- | ---------------------------------------------------- |
350
+ | `modelValue` | `string \| number \| null` | `null` | Selected value (v-model). |
351
+ | `options` | `{ label: string, value: string \| number }[]` | `[]` | Items shown in the dropdown. |
352
+ | `label` | `string` | — | Label displayed above the input. |
353
+ | `labelClass` | `string` | `'text-sm font-semibold'` | Custom classes for the label. |
354
+ | `placeholder` | `string` | `'Search...'` | Placeholder text shown when the input is empty. |
355
+ | `description` | `string` | — | Helper text displayed below. |
356
+ | `rounded` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl' \| 'full'` | `'xl'` | Border radius. |
357
+ | `variant` | `'default' \| 'secondary'` | `'default'` | Visual style. |
358
+ | `required` | `boolean` | `false` | Shows a red asterisk on the label. |
359
+ | `error` | `string` | `''` | Error message to display. Changes styling to danger. |
360
+ | `disabled` | `boolean` | `false` | Disables the input. |
361
+
362
+ **Events**
363
+
364
+ | Event | Payload | Description |
365
+ | ------------------- | -------------------------------- | ------------------------------------------------------------------------ |
366
+ | `update:modelValue` | `string \| number \| null` | Emitted when an option is picked, or `null` when the clear button is used. |
367
+ | `change` | `string \| number \| null` | Emitted alongside `update:modelValue`. |
368
+ | `search` | `string` | Emitted on every keystroke with the current input text. Useful for remote search. |
369
+
370
+ **Examples**
371
+
372
+ ```vue
373
+ <!-- Basic -->
374
+ <PUAutocomplete v-model="country" :options="countries" label="Country" />
375
+
376
+ <!-- Secondary variant + required -->
377
+ <PUAutocomplete
378
+ v-model="city"
379
+ variant="secondary"
380
+ :options="cities"
381
+ label="City"
382
+ required
383
+ />
384
+
385
+ <!-- With error -->
386
+ <PUAutocomplete
387
+ v-model="status"
388
+ :options="statuses"
389
+ label="Status"
390
+ error="Pick a status"
391
+ required
392
+ />
393
+
394
+ <!-- Remote search via the `search` event -->
395
+ <PUAutocomplete
396
+ v-model="user"
397
+ :options="remoteResults"
398
+ label="User"
399
+ placeholder="Type to search users"
400
+ @search="onSearch"
401
+ />
402
+ ```
403
+
404
+ ---
405
+
323
406
  ### PUCard
324
407
 
325
408
  A flexible card component with customizable styling and borders.
@@ -569,7 +652,7 @@ const activeTab = ref("dashboard");
569
652
 
570
653
  ### PUDropdown
571
654
 
572
- A floating panel anchored to an activator element. Opens on click and closes on outside click. Provides a `closeDropdown` function (via `inject`) so child items can close the panel after acting.
655
+ A floating panel anchored to an activator element. The menu is teleported to `body` and positioned with `position: fixed` relative to the activator, so it escapes any `overflow` ancestor (scroll containers, modals, etc.). The menu re-positions itself on scroll and resize while open. Opens on click and closes on outside click; only one `PUDropdown` can be open at a time across the page. Provides a `closeDropdown` function (via `inject`) so child items can close the panel after acting.
573
656
 
574
657
  ```vue
575
658
  <PUDropdown>
@@ -782,10 +865,12 @@ import type {
782
865
  TableItem,
783
866
  TableRounded,
784
867
  TableItemsSize,
785
- } from "proxy-ui";
868
+ AutocompleteOption,
869
+ AutocompleteProps,
870
+ } from "@smurfox/proxy-ui";
786
871
  ```
787
872
 
788
- > `PUTextArea`, `PUSelect`, and `PUDropdown` define their props inline and do not export dedicated `Props` types. They reuse `InputVariant` and `InputRounded` from the same package.
873
+ > `PUTextArea`, `PUSelect`, and `PUDropdown` define their props inline and do not export dedicated `Props` types. They reuse `InputVariant` and `InputRounded` from the same package. `PUAutocomplete` does export `AutocompleteProps` and `AutocompleteOption`.
789
874
 
790
875
  ---
791
876
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proxy-ui",
3
3
  "configKey": "proxyUI",
4
- "version": "0.3.1",
4
+ "version": "0.4.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
File without changes
@@ -0,0 +1,270 @@
1
+ <template>
2
+ <div class="flex flex-col gap-1">
3
+ <div
4
+ v-if="label"
5
+ class="flex items-start gap-1"
6
+ >
7
+ <label
8
+ class="dark:text-white"
9
+ :class="[labelClass]"
10
+ >{{ label }} </label>
11
+ <span
12
+ v-if="props.required"
13
+ class="text-danger"
14
+ >*</span>
15
+ </div>
16
+
17
+ <div
18
+ ref="selectRef"
19
+ class="relative w-full text-left"
20
+ >
21
+ <input
22
+ ref="inputRef"
23
+ type="text"
24
+ :value="searchQuery"
25
+ :placeholder="props.placeholder"
26
+ :disabled="props.disabled"
27
+ class="w-full p-3 pr-10 text-sm transition-colors"
28
+ :class="[
29
+ roundedVariants[props.rounded],
30
+ props.error ? errorVariants[props.variant] : variants[props.variant],
31
+ props.disabled ? 'opacity-70 cursor-not-allowed' : 'cursor-text'
32
+ ]"
33
+ @input="onInput"
34
+ @focus="open"
35
+ @click.stop="open"
36
+ >
37
+
38
+ <button
39
+ v-if="searchQuery && !props.disabled"
40
+ type="button"
41
+ class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center justify-center text-gray-400 hover:text-gray-600 dark:hover:text-white cursor-pointer"
42
+ @click.stop="clear"
43
+ >
44
+ <Icon
45
+ name="mdi:close-circle"
46
+ size="18"
47
+ />
48
+ </button>
49
+ <Icon
50
+ v-else
51
+ name="mdi:chevron-down"
52
+ class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 transition-transform duration-200 pointer-events-none"
53
+ :class="{ 'rotate-180': isOpen && !props.disabled }"
54
+ />
55
+
56
+ <Teleport
57
+ v-if="isOpen && !props.disabled"
58
+ to="body"
59
+ >
60
+ <AnimatePresence>
61
+ <motion.div
62
+ v-if="isOpen && !props.disabled"
63
+ :initial="{ scale: 0.96, opacity: 0, y: -6 }"
64
+ :animate="{ scale: 1, opacity: 1, y: 0 }"
65
+ :exit="{ scale: 0.96, opacity: 0, y: -6 }"
66
+ class="fixed p-2 max-h-56 overflow-y-auto origin-top border rounded-xl shadow-xl"
67
+ :class="
68
+ isDarkMode ? 'bg-[#212123] border-white/10 text-white' : 'bg-white border-gray-100'
69
+ "
70
+ :style="dropdownStyle"
71
+ @click.stop
72
+ >
73
+ <div
74
+ v-if="filteredOptions.length === 0"
75
+ class="px-4 py-2 text-sm text-center"
76
+ :class="isDarkMode ? 'text-white/60' : 'text-black/50'"
77
+ >
78
+ No available options
79
+ </div>
80
+ <template v-else>
81
+ <button
82
+ v-for="option in filteredOptions"
83
+ :key="String(option.value)"
84
+ type="button"
85
+ class="w-full flex items-center justify-between gap-3 px-3 py-2 mb-1 text-left cursor-pointer rounded-lg transition-colors"
86
+ :class="[
87
+ isDarkMode ? 'hover:bg-white/10' : 'hover:bg-gray-100',
88
+ option.value === props.modelValue ? selectedOptionClass : ''
89
+ ]"
90
+ @click.stop="selectOption(option)"
91
+ >
92
+ <span
93
+ class="text-sm truncate"
94
+ :class="
95
+ option.value === props.modelValue ? 'text-primary' : unselectedOptionClass
96
+ "
97
+ >
98
+ {{ option.label }}
99
+ </span>
100
+ <Icon
101
+ v-if="option.value === props.modelValue"
102
+ name="mdi:check"
103
+ class="text-primary text-sm shrink-0"
104
+ />
105
+ </button>
106
+ </template>
107
+ </motion.div>
108
+ </AnimatePresence>
109
+ </Teleport>
110
+ </div>
111
+
112
+ <p
113
+ v-if="description && !props.error"
114
+ class="text-gray-600 dark:text-white/60 text-xs"
115
+ >
116
+ {{ description }}
117
+ </p>
118
+ <p
119
+ v-if="props.error"
120
+ class="text-danger text-xs mt-1"
121
+ >
122
+ {{ props.error }}
123
+ </p>
124
+ </div>
125
+ </template>
126
+
127
+ <script>
128
+ import { AnimatePresence, motion } from "motion-v";
129
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
130
+ import { createPopoverGroup } from "../composables/popoverGroup";
131
+ const popoverGroup = createPopoverGroup();
132
+ </script>
133
+
134
+ <script setup>
135
+ const roundedVariants = {
136
+ "none": "rounded-none",
137
+ "sm": "rounded-sm",
138
+ "md": "rounded-md",
139
+ "lg": "rounded-lg",
140
+ "xl": "rounded-xl",
141
+ "2xl": "rounded-2xl",
142
+ "full": "rounded-full"
143
+ };
144
+ const variants = {
145
+ default: "border border-gray-200 dark:border-white/10 bg-white dark:bg-white/10 enabled:hover:bg-gray-100 dark:enabled:hover:bg-white/20 dark:text-white focus:bg-white dark:focus:bg-white/10 focus:ring-2 focus:ring-primary focus:outline-none",
146
+ secondary: "border border-gray-200 dark:border-white/10 bg-[#EBEBEC] dark:bg-white/20 dark:text-white enabled:hover:bg-[#E0E0E1] dark:enabled:hover:bg-white/30 focus:bg-[#EBEBEC] dark:focus:bg-white/20 focus:ring-2 focus:ring-primary focus:outline-none"
147
+ };
148
+ const errorVariants = {
149
+ default: "border border-danger bg-danger/10 dark:bg-danger/20 text-black dark:text-white enabled:hover:bg-white/20 dark:enabled:hover:bg-white/20 focus:bg-white dark:focus:bg-white/10 focus:ring-2 focus:ring-danger focus:outline-none",
150
+ secondary: "border border-danger bg-danger/22 dark:bg-danger/10 text-black dark:text-white enabled:hover:bg-[#E0E0E1] dark:enabled:hover:bg-white/30 focus:bg-[#EBEBEC] dark:focus:bg-white/20 focus:ring-2 focus:ring-danger focus:outline-none"
151
+ };
152
+ const props = defineProps({
153
+ modelValue: { type: [String, Number, null], required: false, default: null },
154
+ options: { type: Array, required: false, default: () => [] },
155
+ label: { type: String, required: false },
156
+ labelClass: { type: String, required: false, default: "text-sm font-semibold" },
157
+ placeholder: { type: String, required: false, default: "Search..." },
158
+ description: { type: String, required: false },
159
+ rounded: { type: String, required: false, default: "xl" },
160
+ variant: { type: String, required: false, default: "default" },
161
+ required: { type: Boolean, required: false, default: false },
162
+ error: { type: String, required: false, default: "" },
163
+ disabled: { type: Boolean, required: false, default: false }
164
+ });
165
+ const emit = defineEmits(["update:modelValue", "change", "search"]);
166
+ const selectRef = ref(null);
167
+ const inputRef = ref(null);
168
+ const isOpen = ref(false);
169
+ const isDarkMode = ref(false);
170
+ const dropdownPosition = ref({ top: 0, left: 0, width: 0 });
171
+ const searchQuery = ref("");
172
+ const selectedOption = computed(() => {
173
+ return props.options.find((option) => option.value === props.modelValue);
174
+ });
175
+ const filteredOptions = computed(() => {
176
+ const q = searchQuery.value.trim().toLowerCase();
177
+ if (!q) return props.options;
178
+ if (selectedOption.value && searchQuery.value === selectedOption.value.label) {
179
+ return props.options;
180
+ }
181
+ return props.options.filter(
182
+ (option) => option.label.toLowerCase().includes(q)
183
+ );
184
+ });
185
+ const dropdownStyle = computed(() => ({
186
+ top: `${dropdownPosition.value.top}px`,
187
+ left: `${dropdownPosition.value.left}px`,
188
+ width: `${dropdownPosition.value.width}px`,
189
+ zIndex: 9999
190
+ }));
191
+ const selectedOptionClass = computed(() => {
192
+ return isDarkMode.value ? "bg-white/10" : "bg-primary/10";
193
+ });
194
+ const unselectedOptionClass = computed(() => {
195
+ return isDarkMode.value ? "text-white" : "text-black";
196
+ });
197
+ watch(
198
+ () => props.modelValue,
199
+ () => {
200
+ searchQuery.value = selectedOption.value?.label ?? "";
201
+ },
202
+ { immediate: true }
203
+ );
204
+ function syncDarkMode() {
205
+ isDarkMode.value = Boolean(selectRef.value?.closest(".dark"));
206
+ }
207
+ function calculateDropdownPosition() {
208
+ if (!selectRef.value) return;
209
+ syncDarkMode();
210
+ const rect = selectRef.value.getBoundingClientRect();
211
+ dropdownPosition.value = {
212
+ top: rect.bottom + window.scrollY + 8,
213
+ left: rect.left + window.scrollX,
214
+ width: rect.width
215
+ };
216
+ }
217
+ async function open() {
218
+ if (props.disabled || isOpen.value) return;
219
+ popoverGroup.open(close);
220
+ await nextTick();
221
+ calculateDropdownPosition();
222
+ isOpen.value = true;
223
+ }
224
+ function close() {
225
+ if (!isOpen.value) return;
226
+ isOpen.value = false;
227
+ popoverGroup.release(close);
228
+ searchQuery.value = selectedOption.value?.label ?? "";
229
+ }
230
+ function onInput(event) {
231
+ const value = event.target.value;
232
+ searchQuery.value = value;
233
+ emit("search", value);
234
+ if (!isOpen.value) open();
235
+ }
236
+ function selectOption(option) {
237
+ emit("update:modelValue", option.value);
238
+ emit("change", option.value);
239
+ searchQuery.value = option.label;
240
+ isOpen.value = false;
241
+ popoverGroup.release(close);
242
+ }
243
+ function clear() {
244
+ searchQuery.value = "";
245
+ emit("update:modelValue", null);
246
+ emit("change", null);
247
+ emit("search", "");
248
+ inputRef.value?.focus();
249
+ }
250
+ function onClickOutside(event) {
251
+ if (selectRef.value && !selectRef.value.contains(event.target)) {
252
+ close();
253
+ }
254
+ }
255
+ function onScroll() {
256
+ if (isOpen.value) calculateDropdownPosition();
257
+ }
258
+ onMounted(() => {
259
+ syncDarkMode();
260
+ document.addEventListener("click", onClickOutside);
261
+ window.addEventListener("scroll", onScroll, true);
262
+ window.addEventListener("resize", onScroll);
263
+ });
264
+ onUnmounted(() => {
265
+ document.removeEventListener("click", onClickOutside);
266
+ window.removeEventListener("scroll", onScroll, true);
267
+ window.removeEventListener("resize", onScroll);
268
+ popoverGroup.release(close);
269
+ });
270
+ </script>
File without changes
@@ -7,8 +7,8 @@ type __VLS_Props = {
7
7
  rounded?: AvatarRounded;
8
8
  };
9
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
10
- size: AvatarSize;
11
10
  rounded: AvatarRounded;
11
+ size: AvatarSize;
12
12
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
13
  declare const _default: typeof __VLS_export;
14
14
  export default _default;
@@ -7,8 +7,8 @@ type __VLS_Props = {
7
7
  rounded?: AvatarRounded;
8
8
  };
9
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
10
- size: AvatarSize;
11
10
  rounded: AvatarRounded;
11
+ size: AvatarSize;
12
12
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
13
  declare const _default: typeof __VLS_export;
14
14
  export default _default;
@@ -24,10 +24,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
24
24
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
25
25
  onClick?: ((event: MouseEvent) => any) | undefined;
26
26
  }>, {
27
- size: ButtonSize;
28
27
  rounded: ButtonRounded;
29
28
  variant: ButtonVariant;
30
29
  color: ButtonColor;
30
+ size: ButtonSize;
31
31
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
32
32
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
33
33
  declare const _default: typeof __VLS_export;
@@ -24,10 +24,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
24
24
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
25
25
  onClick?: ((event: MouseEvent) => any) | undefined;
26
26
  }>, {
27
- size: ButtonSize;
28
27
  rounded: ButtonRounded;
29
28
  variant: ButtonVariant;
30
29
  color: ButtonColor;
30
+ size: ButtonSize;
31
31
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
32
32
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
33
33
  declare const _default: typeof __VLS_export;
@@ -15,10 +15,10 @@ type __VLS_Slots = {} & {
15
15
  default?: (props: typeof __VLS_6) => any;
16
16
  };
17
17
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
18
- size: ChipSize;
19
18
  rounded: ChipRounded;
20
19
  variant: ChipVariant;
21
20
  color: ChipColor;
21
+ size: ChipSize;
22
22
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
23
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
24
24
  declare const _default: typeof __VLS_export;
@@ -15,10 +15,10 @@ type __VLS_Slots = {} & {
15
15
  default?: (props: typeof __VLS_6) => any;
16
16
  };
17
17
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
18
- size: ChipSize;
19
18
  rounded: ChipRounded;
20
19
  variant: ChipVariant;
21
20
  color: ChipColor;
21
+ size: ChipSize;
22
22
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
23
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
24
24
  declare const _default: typeof __VLS_export;
@@ -1,18 +1,16 @@
1
- type __VLS_Props = {
2
- menuMinWidth?: string;
3
- };
4
- declare var __VLS_1: {}, __VLS_17: {};
5
- type __VLS_Slots = {} & {
6
- activator?: (props: typeof __VLS_1) => any;
7
- } & {
8
- content?: (props: typeof __VLS_17) => any;
9
- };
10
- declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
- menuMinWidth: string;
12
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
- declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
14
1
  declare const _default: typeof __VLS_export;
15
2
  export default _default;
3
+ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<{
4
+ menuMinWidth?: string;
5
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{
6
+ menuMinWidth?: string;
7
+ }> & Readonly<{}>, {
8
+ menuMinWidth: string;
9
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>, {
10
+ activator?: (props: {}) => any;
11
+ } & {
12
+ content?: (props: {}) => any;
13
+ }>;
16
14
  type __VLS_WithSlots<T, S> = T & {
17
15
  new (): {
18
16
  $slots: S;
@@ -3,55 +3,101 @@
3
3
  ref="dropdownRef"
4
4
  class="relative inline-block text-left"
5
5
  >
6
- <div @click="toggle">
6
+ <div
7
+ ref="activatorRef"
8
+ @click="toggle"
9
+ >
7
10
  <slot name="activator" />
8
11
  </div>
9
12
 
10
- <AnimatePresence>
11
- <motion.div
12
- v-if="isOpen"
13
- :initial="{ scale: 0.9, opacity: 0, y: -10 }"
14
- :exit="{ opacity: 0 }"
15
- :animate="{ scale: 1, opacity: 1, y: 0 }"
16
- :class="[
17
- 'absolute right-0 z-50 mt-2 origin-top-right bg-white border border-gray-100 rounded-xl shadow-xl dark:bg-[#18181B] dark:border-black/40',
13
+ <Teleport to="body">
14
+ <AnimatePresence>
15
+ <motion.div
16
+ v-if="isOpen"
17
+ ref="menuRef"
18
+ :initial="{ scale: 0.9, opacity: 0, y: -10 }"
19
+ :exit="{ opacity: 0 }"
20
+ :animate="{ scale: 1, opacity: 1, y: 0 }"
21
+ :style="menuStyle"
22
+ :class="[
23
+ 'fixed z-50 origin-top-right bg-white border border-gray-100 rounded-xl shadow-xl dark:bg-[#18181B] dark:border-black/40',
18
24
  props.menuMinWidth
19
25
  ]"
20
- @click="handleContentClick"
21
- >
22
- <slot name="content" />
23
- </motion.div>
24
- </AnimatePresence>
26
+ @click="handleContentClick"
27
+ >
28
+ <slot name="content" />
29
+ </motion.div>
30
+ </AnimatePresence>
31
+ </Teleport>
25
32
  </div>
26
33
  </template>
27
34
 
28
- <script setup>
35
+ <script>
29
36
  import { AnimatePresence, motion } from "motion-v";
30
- import { ref, onMounted, onUnmounted, provide } from "vue";
37
+ import { ref, reactive, onMounted, onUnmounted, provide, nextTick, watch } from "vue";
38
+ import { createPopoverGroup } from "../composables/popoverGroup";
39
+ const popoverGroup = createPopoverGroup();
40
+ </script>
41
+
42
+ <script setup>
31
43
  const props = defineProps({
32
44
  menuMinWidth: { type: String, required: false, default: "min-w-52" }
33
45
  });
34
46
  const isOpen = ref(false);
35
47
  const dropdownRef = ref(null);
36
- const toggle = () => {
37
- isOpen.value = !isOpen.value;
48
+ const activatorRef = ref(null);
49
+ const menuRef = ref(null);
50
+ const menuStyle = reactive({
51
+ top: "0px",
52
+ right: "0px"
53
+ });
54
+ const updatePosition = () => {
55
+ if (!activatorRef.value) return;
56
+ const rect = activatorRef.value.getBoundingClientRect();
57
+ menuStyle.top = `${rect.bottom + 8}px`;
58
+ menuStyle.right = `${window.innerWidth - rect.right}px`;
38
59
  };
39
60
  const close = () => {
40
61
  isOpen.value = false;
62
+ popoverGroup.release(close);
63
+ };
64
+ const toggle = () => {
65
+ if (isOpen.value) {
66
+ close();
67
+ return;
68
+ }
69
+ popoverGroup.open(close);
70
+ isOpen.value = true;
71
+ nextTick(updatePosition);
41
72
  };
42
73
  provide("closeDropdown", close);
43
74
  const onClickOutside = (event) => {
44
- if (dropdownRef.value && !dropdownRef.value.contains(event.target)) {
75
+ const target = event.target;
76
+ const insideActivator = dropdownRef.value?.contains(target);
77
+ const insideMenu = menuRef.value?.contains(target);
78
+ if (!insideActivator && !insideMenu) {
45
79
  setTimeout(() => close(), 10);
46
80
  }
47
81
  };
48
82
  const handleContentClick = (event) => {
49
83
  event.stopPropagation();
50
84
  };
85
+ watch(isOpen, (open) => {
86
+ if (open) {
87
+ window.addEventListener("scroll", updatePosition, true);
88
+ window.addEventListener("resize", updatePosition);
89
+ } else {
90
+ window.removeEventListener("scroll", updatePosition, true);
91
+ window.removeEventListener("resize", updatePosition);
92
+ }
93
+ });
51
94
  onMounted(() => {
52
95
  document.addEventListener("click", onClickOutside);
53
96
  });
54
97
  onUnmounted(() => {
55
98
  document.removeEventListener("click", onClickOutside);
99
+ window.removeEventListener("scroll", updatePosition, true);
100
+ window.removeEventListener("resize", updatePosition);
101
+ popoverGroup.release(close);
56
102
  });
57
103
  </script>
@@ -1,18 +1,16 @@
1
- type __VLS_Props = {
2
- menuMinWidth?: string;
3
- };
4
- declare var __VLS_1: {}, __VLS_17: {};
5
- type __VLS_Slots = {} & {
6
- activator?: (props: typeof __VLS_1) => any;
7
- } & {
8
- content?: (props: typeof __VLS_17) => any;
9
- };
10
- declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
- menuMinWidth: string;
12
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
- declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
14
1
  declare const _default: typeof __VLS_export;
15
2
  export default _default;
3
+ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<{
4
+ menuMinWidth?: string;
5
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{
6
+ menuMinWidth?: string;
7
+ }> & Readonly<{}>, {
8
+ menuMinWidth: string;
9
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>, {
10
+ activator?: (props: {}) => any;
11
+ } & {
12
+ content?: (props: {}) => any;
13
+ }>;
16
14
  type __VLS_WithSlots<T, S> = T & {
17
15
  new (): {
18
16
  $slots: S;
@@ -11,6 +11,7 @@ type __VLS_Props = {
11
11
  required?: boolean;
12
12
  error?: string;
13
13
  disabled?: boolean;
14
+ focus?: boolean;
14
15
  };
15
16
  declare var __VLS_1: {}, __VLS_3: {};
16
17
  type __VLS_Slots = {} & {
@@ -23,12 +24,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
23
24
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
24
25
  "onUpdate:modelValue"?: ((value: string | number) => any) | undefined;
25
26
  }>, {
27
+ focus: boolean;
28
+ labelClass: string;
26
29
  rounded: InputRounded;
27
- type: string;
28
30
  variant: InputVariant;
29
- disabled: boolean;
30
31
  required: boolean;
31
- labelClass: string;
32
+ disabled: boolean;
33
+ type: string;
32
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
33
35
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
34
36
  declare const _default: typeof __VLS_export;
@@ -38,6 +38,7 @@
38
38
  @input="
39
39
  emit('update:modelValue', $event.target.value)
40
40
  "
41
+ @focus="handleFocus"
41
42
  >
42
43
  <!-- endContent -->
43
44
  <div
@@ -92,9 +93,16 @@ const props = defineProps({
92
93
  variant: { type: String, required: false, default: "default" },
93
94
  required: { type: Boolean, required: false, default: false },
94
95
  error: { type: String, required: false },
95
- disabled: { type: Boolean, required: false, default: false }
96
+ disabled: { type: Boolean, required: false, default: false },
97
+ focus: { type: Boolean, required: false, default: false }
96
98
  });
97
99
  const emit = defineEmits(["update:modelValue"]);
100
+ const handleFocus = (event) => {
101
+ if (props.focus) {
102
+ const target = event.target;
103
+ target.select();
104
+ }
105
+ };
98
106
  </script>
99
107
 
100
108
  <style scoped>
@@ -11,6 +11,7 @@ type __VLS_Props = {
11
11
  required?: boolean;
12
12
  error?: string;
13
13
  disabled?: boolean;
14
+ focus?: boolean;
14
15
  };
15
16
  declare var __VLS_1: {}, __VLS_3: {};
16
17
  type __VLS_Slots = {} & {
@@ -23,12 +24,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
23
24
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
24
25
  "onUpdate:modelValue"?: ((value: string | number) => any) | undefined;
25
26
  }>, {
27
+ focus: boolean;
28
+ labelClass: string;
26
29
  rounded: InputRounded;
27
- type: string;
28
30
  variant: InputVariant;
29
- disabled: boolean;
30
31
  required: boolean;
31
- labelClass: string;
32
+ disabled: boolean;
33
+ type: string;
32
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
33
35
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
34
36
  declare const _default: typeof __VLS_export;
@@ -5,8 +5,8 @@ type __VLS_Props = {
5
5
  width?: number;
6
6
  };
7
7
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
8
- height: number;
9
8
  width: number;
9
+ height: number;
10
10
  loop: boolean;
11
11
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
12
  declare const _default: typeof __VLS_export;
@@ -5,8 +5,8 @@ type __VLS_Props = {
5
5
  width?: number;
6
6
  };
7
7
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
8
- height: number;
9
8
  width: number;
9
+ height: number;
10
10
  loop: boolean;
11
11
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
12
  declare const _default: typeof __VLS_export;
@@ -1,37 +0,0 @@
1
- import type { InputRounded, InputVariant } from '../types/index.js';
2
- interface SelectOption {
3
- label: string;
4
- value: string | number;
5
- }
6
- type __VLS_Props = {
7
- modelValue?: string | number | null;
8
- options?: SelectOption[];
9
- label?: string;
10
- labelClass?: string;
11
- placeholder?: string;
12
- description?: string;
13
- rounded?: InputRounded;
14
- variant?: InputVariant;
15
- required?: boolean;
16
- error?: string;
17
- disabled?: boolean;
18
- };
19
- declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
20
- change: (value: string | number) => any;
21
- "update:modelValue": (value: string | number) => any;
22
- }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
23
- onChange?: ((value: string | number) => any) | undefined;
24
- "onUpdate:modelValue"?: ((value: string | number) => any) | undefined;
25
- }>, {
26
- rounded: InputRounded;
27
- error: string;
28
- variant: InputVariant;
29
- disabled: boolean;
30
- placeholder: string;
31
- required: boolean;
32
- modelValue: string | number | null;
33
- labelClass: string;
34
- options: SelectOption[];
35
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
- declare const _default: typeof __VLS_export;
37
- export default _default;
@@ -111,9 +111,14 @@
111
111
  </div>
112
112
  </template>
113
113
 
114
- <script setup>
114
+ <script>
115
115
  import { AnimatePresence, motion } from "motion-v";
116
116
  import { computed, nextTick, onMounted, onUnmounted, ref } from "vue";
117
+ import { createPopoverGroup } from "../composables/popoverGroup";
118
+ const popoverGroup = createPopoverGroup();
119
+ </script>
120
+
121
+ <script setup>
117
122
  const roundedVariants = {
118
123
  "none": "rounded-none",
119
124
  "sm": "rounded-sm",
@@ -186,14 +191,18 @@ async function toggle() {
186
191
  if (props.disabled) {
187
192
  return;
188
193
  }
189
- if (!isOpen.value) {
190
- await nextTick();
191
- calculateDropdownPosition();
194
+ if (isOpen.value) {
195
+ close();
196
+ return;
192
197
  }
193
- isOpen.value = !isOpen.value;
198
+ popoverGroup.open(close);
199
+ await nextTick();
200
+ calculateDropdownPosition();
201
+ isOpen.value = true;
194
202
  }
195
203
  function close() {
196
204
  isOpen.value = false;
205
+ popoverGroup.release(close);
197
206
  }
198
207
  function selectOption(option) {
199
208
  emit("update:modelValue", option.value);
@@ -220,5 +229,6 @@ onUnmounted(() => {
220
229
  document.removeEventListener("click", onClickOutside);
221
230
  window.removeEventListener("scroll", onScroll, true);
222
231
  window.removeEventListener("resize", onScroll);
232
+ popoverGroup.release(close);
223
233
  });
224
234
  </script>
@@ -1,37 +0,0 @@
1
- import type { InputRounded, InputVariant } from '../types/index.js';
2
- interface SelectOption {
3
- label: string;
4
- value: string | number;
5
- }
6
- type __VLS_Props = {
7
- modelValue?: string | number | null;
8
- options?: SelectOption[];
9
- label?: string;
10
- labelClass?: string;
11
- placeholder?: string;
12
- description?: string;
13
- rounded?: InputRounded;
14
- variant?: InputVariant;
15
- required?: boolean;
16
- error?: string;
17
- disabled?: boolean;
18
- };
19
- declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
20
- change: (value: string | number) => any;
21
- "update:modelValue": (value: string | number) => any;
22
- }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
23
- onChange?: ((value: string | number) => any) | undefined;
24
- "onUpdate:modelValue"?: ((value: string | number) => any) | undefined;
25
- }>, {
26
- rounded: InputRounded;
27
- error: string;
28
- variant: InputVariant;
29
- disabled: boolean;
30
- placeholder: string;
31
- required: boolean;
32
- modelValue: string | number | null;
33
- labelClass: string;
34
- options: SelectOption[];
35
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
- declare const _default: typeof __VLS_export;
37
- export default _default;
@@ -4,9 +4,9 @@ declare const __VLS_export: import("vue").DefineComponent<TabsProps, {}, {}, {},
4
4
  }, string, import("vue").PublicProps, Readonly<TabsProps> & Readonly<{
5
5
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
6
6
  }>, {
7
+ modelValue: string;
7
8
  rounded: import("../types/index.js").TabsRounded;
8
9
  iconSize: number;
9
- modelValue: string;
10
10
  bgColor: string;
11
11
  btnColor: string;
12
12
  activeTextColor: string;
@@ -4,9 +4,9 @@ declare const __VLS_export: import("vue").DefineComponent<TabsProps, {}, {}, {},
4
4
  }, string, import("vue").PublicProps, Readonly<TabsProps> & Readonly<{
5
5
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
6
6
  }>, {
7
+ modelValue: string;
7
8
  rounded: import("../types/index.js").TabsRounded;
8
9
  iconSize: number;
9
- modelValue: string;
10
10
  bgColor: string;
11
11
  btnColor: string;
12
12
  activeTextColor: string;
@@ -25,13 +25,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
25
25
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
26
26
  "onUpdate:modelValue"?: ((value: string | number) => any) | undefined;
27
27
  }>, {
28
- rounded: InputRounded;
29
28
  resize: TextAreaResize;
29
+ labelClass: string;
30
+ rounded: InputRounded;
30
31
  variant: InputVariant;
31
- disabled: boolean;
32
32
  required: boolean;
33
+ disabled: boolean;
33
34
  rows: number | string;
34
- labelClass: string;
35
35
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
36
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
37
37
  declare const _default: typeof __VLS_export;
@@ -25,13 +25,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
25
25
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
26
26
  "onUpdate:modelValue"?: ((value: string | number) => any) | undefined;
27
27
  }>, {
28
- rounded: InputRounded;
29
28
  resize: TextAreaResize;
29
+ labelClass: string;
30
+ rounded: InputRounded;
30
31
  variant: InputVariant;
31
- disabled: boolean;
32
32
  required: boolean;
33
+ disabled: boolean;
33
34
  rows: number | string;
34
- labelClass: string;
35
35
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
36
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
37
37
  declare const _default: typeof __VLS_export;
@@ -0,0 +1,5 @@
1
+ export interface PopoverGroup {
2
+ open: (close: () => void) => void;
3
+ release: (close: () => void) => void;
4
+ }
5
+ export declare function createPopoverGroup(): PopoverGroup;
@@ -0,0 +1,12 @@
1
+ export function createPopoverGroup() {
2
+ let activeClose = null;
3
+ return {
4
+ open(close) {
5
+ if (activeClose && activeClose !== close) activeClose();
6
+ activeClose = close;
7
+ },
8
+ release(close) {
9
+ if (activeClose === close) activeClose = null;
10
+ }
11
+ };
12
+ }
@@ -53,6 +53,23 @@ export interface InputProps {
53
53
  error?: string;
54
54
  disabled?: boolean;
55
55
  }
56
+ export interface AutocompleteOption {
57
+ label: string;
58
+ value: string | number;
59
+ }
60
+ export interface AutocompleteProps {
61
+ modelValue?: string | number | null;
62
+ options?: AutocompleteOption[];
63
+ label?: string;
64
+ labelClass?: string;
65
+ placeholder?: string;
66
+ description?: string;
67
+ rounded?: InputRounded;
68
+ variant?: InputVariant;
69
+ required?: boolean;
70
+ error?: string;
71
+ disabled?: boolean;
72
+ }
56
73
  export type TabsRounded = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
57
74
  export interface TabItem {
58
75
  label: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smurfox/proxy-ui",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "A UI component library built for Nuxt 4",
5
5
  "repository": {
6
6
  "type": "git",