@bl33dz/fa814698dcde12f86a37ac31dd3aedf9 1.0.19 → 1.0.21

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.0.19",
6
+ "version": "1.0.21",
7
7
  "main": "dist/perisai-ui.umd.js",
8
8
  "module": "dist/perisai-ui.es.js",
9
9
  "scripts": {
@@ -28,7 +28,6 @@
28
28
  "esbuild": "^0.25.12",
29
29
  "estree-walker": "^2.0.2",
30
30
  "fdir": "^6.5.0",
31
- "input-otp": "^1.4.2",
32
31
  "lucide-vue-next": "^0.554.0",
33
32
  "magic-string": "^0.30.21",
34
33
  "nanoid": "^3.3.11",
@@ -39,7 +38,8 @@
39
38
  "source-map-js": "^1.2.1",
40
39
  "tailwind-merge": "^3.4.0",
41
40
  "tinyglobby": "^0.2.15",
42
- "vaul-vue": "^0.4.1"
41
+ "vaul-vue": "^0.4.1",
42
+ "vue-input-otp": "^0.3.2"
43
43
  },
44
44
  "description": ""
45
45
  }
package/src/index.ts CHANGED
@@ -10,12 +10,10 @@ export * from './shadcn/tooltip';
10
10
  export * from './shadcn/command';
11
11
  export * from './shadcn/switch';
12
12
  export * from './shadcn/collapsible';
13
+ export * from './shadcn/navigation-menu';
14
+ export * from './shadcn/input-otp';
13
15
 
14
16
  export * from './ui/tree';
15
- export { default as InputOTP } from './ui/InputOTP.vue';
16
- export { default as InputOTPGroup } from './ui/InputOTPGroup.vue';
17
- export { default as InputOTPSeparator } from './ui/InputOTPSeparator.vue';
18
- export { default as InputOTPSlot } from './ui/InputOTPSlot.vue';
19
17
  export { default as PopoverContent } from './ui/PopoverContent.vue';
20
18
  export { default as PopoverTrigger } from './ui/PopoverTrigger.vue';
21
19
  export { default as SelectContent } from './ui/SelectContent.vue';
@@ -112,7 +110,6 @@ export { default as Slash } from './ui/icons/Slash.vue';
112
110
  export { default as Trash2 } from './ui/icons/Trash2.vue';
113
111
  export { default as Type } from './ui/icons/Type.vue';
114
112
  export { default as User } from './ui/icons/User.vue';
115
- export { default as InputOtp } from './ui/input-otp.vue';
116
113
  export { default as Input } from './ui/input.vue';
117
114
  export { default as Label } from './ui/label.vue';
118
115
  export { default as Pagination } from './ui/pagination.vue';
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue"
3
+ import type { OTPInputEmits, OTPInputProps } from "vue-input-otp"
4
+ import { reactiveOmit } from "@vueuse/core"
5
+ import { useForwardPropsEmits } from "reka-ui"
6
+ import { OTPInput } from "vue-input-otp"
7
+ import { cn } from "../../lib/utils"
8
+
9
+ const props = defineProps<OTPInputProps & { class?: HTMLAttributes["class"] }>()
10
+
11
+ const emits = defineEmits<OTPInputEmits>()
12
+
13
+ const delegatedProps = reactiveOmit(props, "class")
14
+
15
+ const forwarded = useForwardPropsEmits(delegatedProps, emits)
16
+ </script>
17
+
18
+ <template>
19
+ <OTPInput
20
+ v-slot="slotProps"
21
+ v-bind="forwarded"
22
+ :container-class="cn('flex items-center gap-2 has-disabled:opacity-50', props.class)"
23
+ data-slot="input-otp"
24
+ class="disabled:cursor-not-allowed"
25
+ >
26
+ <slot v-bind="slotProps" />
27
+ </OTPInput>
28
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue"
3
+ import { reactiveOmit } from "@vueuse/core"
4
+ import { useForwardProps } from "reka-ui"
5
+ import { cn } from "../../lib/utils"
6
+
7
+ const props = defineProps<{ class?: HTMLAttributes["class"] }>()
8
+
9
+ const delegatedProps = reactiveOmit(props, "class")
10
+
11
+ const forwarded = useForwardProps(delegatedProps)
12
+ </script>
13
+
14
+ <template>
15
+ <div
16
+ data-slot="input-otp-group"
17
+ v-bind="forwarded"
18
+ :class="cn('flex items-center', props.class)"
19
+ >
20
+ <slot />
21
+ </div>
22
+ </template>
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue"
3
+ import { MinusIcon } from "lucide-vue-next"
4
+ import { useForwardProps } from "reka-ui"
5
+
6
+ const props = defineProps<{ class?: HTMLAttributes["class"] }>()
7
+
8
+ const forwarded = useForwardProps(props)
9
+ </script>
10
+
11
+ <template>
12
+ <div
13
+ data-slot="input-otp-separator"
14
+ role="separator"
15
+ v-bind="forwarded"
16
+ >
17
+ <slot>
18
+ <MinusIcon />
19
+ </slot>
20
+ </div>
21
+ </template>
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue"
3
+ import { reactiveOmit } from "@vueuse/core"
4
+ import { useForwardProps } from "reka-ui"
5
+ import { computed } from "vue"
6
+ import { useVueOTPContext } from "vue-input-otp"
7
+ import { cn } from "../../lib/utils"
8
+
9
+ const props = defineProps<{ index: number, class?: HTMLAttributes["class"] }>()
10
+
11
+ const delegatedProps = reactiveOmit(props, "class")
12
+
13
+ const forwarded = useForwardProps(delegatedProps)
14
+
15
+ const context = useVueOTPContext()
16
+
17
+ const slot = computed(() => context?.value.slots[props.index])
18
+ </script>
19
+
20
+ <template>
21
+ <div
22
+ v-bind="forwarded"
23
+ data-slot="input-otp-slot"
24
+ :data-active="slot?.isActive"
25
+ :class="cn('data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm bg-input-background transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]', props.class)"
26
+ >
27
+ {{ slot?.char }}
28
+ <div v-if="slot?.hasFakeCaret" class="pointer-events-none absolute inset-0 flex items-center justify-center">
29
+ <div class="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
30
+ </div>
31
+ </div>
32
+ </template>
@@ -0,0 +1,4 @@
1
+ export { default as InputOTP } from "./InputOTP.vue"
2
+ export { default as InputOTPGroup } from "./InputOTPGroup.vue"
3
+ export { default as InputOTPSeparator } from "./InputOTPSeparator.vue"
4
+ export { default as InputOTPSlot } from "./InputOTPSlot.vue"
@@ -1,24 +1,29 @@
1
1
  <script setup lang="ts">
2
+ import type { NavigationMenuTriggerProps } from "reka-ui"
3
+ import type { HTMLAttributes } from "vue"
4
+ import { reactiveOmit } from "@vueuse/core"
5
+ import { NavigationMenuTrigger, useForwardProps } from "reka-ui"
2
6
  import { cn } from '@/lib/utils';
3
7
  import { ChevronDown } from 'lucide-vue-next';
4
- const props = defineProps({
5
- class: { type: String, default: '' },
6
- open: { type: Boolean, default: false },
7
- });
8
+
9
+ const props = defineProps<NavigationMenuTriggerProps & { class?: HTMLAttributes["class"] }>()
10
+
11
+ const delegatedProps = reactiveOmit(props, "class")
12
+ const forwardedProps = useForwardProps(delegatedProps)
8
13
  </script>
9
14
  <template>
10
- <button
11
- :class="cn('group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 transition-[color,box-shadow] outline-none',
12
- { 'data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-1': props.open },
13
- props.class)
14
- "
15
- :aria-expanded="props.open ? 'true' : 'false'"
15
+ <NavigationMenuTrigger
16
+ data-slot="navigation-menu-trigger"
17
+ v-bind="forwardedProps"
18
+ :class="cn(
19
+ 'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 transition-[color,box-shadow] outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-1',
20
+ props.class
21
+ )"
16
22
  >
17
23
  <slot />
18
24
  <ChevronDown
19
25
  class="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
20
26
  aria-hidden="true"
21
- :class="{ 'rotate-180': props.open }"
22
27
  />
23
- </button>
28
+ </NavigationMenuTrigger>
24
29
  </template>
@@ -1,21 +1,37 @@
1
1
  <template>
2
- <nav class="flex items-center justify-center gap-2" role="navigation" aria-label="Pagination">
2
+ <nav class="flex items-center justify-center gap-1" role="navigation" aria-label="Pagination">
3
3
  <button
4
- class="h-8 w-8 flex items-center justify-center rounded border border-input bg-background px-0 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
4
+ class="px-3 py-2 text-sm font-medium rounded-md transition-colors hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:cursor-not-allowed"
5
5
  :disabled="page <= 1"
6
6
  @click="$emit('update:page', page - 1)"
7
7
  aria-label="Previous page"
8
8
  >
9
- &lt;
9
+ Previous
10
10
  </button>
11
- <span class="px-2 text-sm">{{ page }} / {{ totalPages }}</span>
11
+
12
+ <template v-for="(pageNum, index) in visiblePages" :key="pageNum">
13
+ <span v-if="pageNum === '...'" class="px-3 py-2 text-sm">...</span>
14
+ <button
15
+ v-else
16
+ class="px-3 py-2 text-sm font-medium rounded-md transition-colors"
17
+ :class="pageNum === page
18
+ ? 'border border-input bg-background'
19
+ : 'hover:bg-accent hover:text-accent-foreground'"
20
+ @click="$emit('update:page', pageNum)"
21
+ :aria-label="`Page ${pageNum}`"
22
+ :aria-current="pageNum === page ? 'page' : undefined"
23
+ >
24
+ {{ pageNum }}
25
+ </button>
26
+ </template>
27
+
12
28
  <button
13
- class="h-8 w-8 flex items-center justify-center rounded border border-input bg-background px-0 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
29
+ class="px-3 py-2 text-sm font-medium rounded-md transition-colors hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:cursor-not-allowed"
14
30
  :disabled="page >= totalPages"
15
31
  @click="$emit('update:page', page + 1)"
16
32
  aria-label="Next page"
17
33
  >
18
- &gt;
34
+ Next
19
35
  </button>
20
36
  </nav>
21
37
  </template>
@@ -38,4 +54,45 @@ const props = defineProps({
38
54
  });
39
55
  const emit = defineEmits(['update:page']);
40
56
  const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.perPage)));
57
+
58
+ const visiblePages = computed(() => {
59
+ const pages = [];
60
+ const current = props.page;
61
+ const total = totalPages.value;
62
+
63
+ if (total <= 7) {
64
+ // Show all pages if total is 7 or less
65
+ for (let i = 1; i <= total; i++) {
66
+ pages.push(i);
67
+ }
68
+ } else {
69
+ // Always show first page
70
+ pages.push(1);
71
+
72
+ if (current <= 4) {
73
+ // Show pages 2, 3, 4, 5, then ellipsis, then last page
74
+ for (let i = 2; i <= 5; i++) {
75
+ pages.push(i);
76
+ }
77
+ pages.push('...');
78
+ pages.push(total);
79
+ } else if (current >= total - 3) {
80
+ // Show first page, ellipsis, then last 4 pages
81
+ pages.push('...');
82
+ for (let i = total - 4; i <= total; i++) {
83
+ pages.push(i);
84
+ }
85
+ } else {
86
+ // Show first page, ellipsis, current-1, current, current+1, ellipsis, last page
87
+ pages.push('...');
88
+ pages.push(current - 1);
89
+ pages.push(current);
90
+ pages.push(current + 1);
91
+ pages.push('...');
92
+ pages.push(total);
93
+ }
94
+ }
95
+
96
+ return pages;
97
+ });
41
98
  </script>
package/src/ui/select.vue CHANGED
@@ -20,7 +20,7 @@
20
20
  :disabled="disabled"
21
21
  :class="mergedClass"
22
22
  >
23
- <SelectTrigger class="w-full">
23
+ <SelectTrigger :class="cn('w-full', triggerClass)">
24
24
  <slot name="trigger">
25
25
  <SelectValue :placeholder="placeholder" />
26
26
  </slot>
@@ -59,6 +59,7 @@ const props = defineProps({
59
59
  disabled: Boolean,
60
60
  customClass: String,
61
61
  contentClass: String,
62
+ triggerClass: String,
62
63
  title: String,
63
64
  maxDisplay: { type: Number, default: 3 },
64
65
  size: { type: String, default: 'default' },
package/vite.config.ts CHANGED
@@ -25,7 +25,6 @@ export default defineConfig({
25
25
  "vaul-vue",
26
26
  "clsx",
27
27
  "tailwind-merge",
28
- "input-otp",
29
28
  "class-variance-authority" // keep dynamic async imports unbundled
30
29
  ],
31
30
  output: {
@@ -1,69 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, provide, h, defineAsyncComponent, useSlots } from 'vue';
3
- import { cn } from './utils';
4
- import { InputOTPContext, InputOTPMaskContext } from './InputOTPContext';
5
-
6
- const props = defineProps({
7
- modelValue: { type: String, default: '' },
8
- maxLength: { type: Number, required: true },
9
- disabled: { type: Boolean, default: false },
10
- masked: { type: Boolean, default: false },
11
- class: String,
12
- containerClass: String,
13
- });
14
- const emit = defineEmits(['update:modelValue']);
15
- const slots = useSlots();
16
-
17
- const handleChange = (newValue: string) => {
18
- if (newValue.length <= props.maxLength) {
19
- emit('update:modelValue', newValue);
20
- }
21
- };
22
-
23
- // Try to use external input-otp package if available
24
- let OTPInput: any = null;
25
- try {
26
- OTPInput = defineAsyncComponent(() => import('input-otp'));
27
- } catch (e) {
28
- OTPInput = null;
29
- }
30
-
31
- if (OTPInput) {
32
- provide(InputOTPMaskContext, { masked: props.masked });
33
- }
34
-
35
- const contextValue = computed(() => ({
36
- value: props.modelValue,
37
- onChange: handleChange,
38
- maxLength: props.maxLength,
39
- disabled: props.disabled,
40
- masked: props.masked,
41
- }));
42
-
43
- if (!OTPInput) {
44
- provide(InputOTPContext, contextValue.value);
45
- provide(InputOTPMaskContext, { masked: props.masked });
46
- }
47
- </script>
48
- <template>
49
- <component
50
- v-if="OTPInput"
51
- :is="OTPInput"
52
- data-slot="input-otp"
53
- :container-class="cn('flex items-center gap-2 has-disabled:opacity-50', props.containerClass)"
54
- :class="cn('disabled:cursor-not-allowed', props.class)"
55
- :max-length="props.maxLength"
56
- :value="props.modelValue"
57
- :onChange="(val: string) => emit('update:modelValue', val)"
58
- :disabled="props.disabled"
59
- >
60
- <slot />
61
- </component>
62
- <div
63
- v-else
64
- data-slot="input-otp"
65
- :class="cn('flex items-center gap-2 has-disabled:opacity-50', props.containerClass)"
66
- >
67
- <slot />
68
- </div>
69
- </template>
@@ -1,7 +0,0 @@
1
- <script setup lang="ts">
2
- import { cn } from './utils';
3
- const props = defineProps({ class: String });
4
- </script>
5
- <template>
6
- <div data-slot="input-otp-group" :class="cn('flex items-center gap-1', props.class)"><slot /></div>
7
- </template>
@@ -1,8 +0,0 @@
1
- <script setup lang="ts">
2
- import { Minus } from 'lucide-vue-next';
3
- </script>
4
- <template>
5
- <div data-slot="input-otp-separator" role="separator">
6
- <Minus class="h-4 w-4" />
7
- </div>
8
- </template>
@@ -1,61 +0,0 @@
1
- <script setup>
2
- import { computed } from 'vue';
3
- import { cn } from '@/lib/utils';
4
-
5
- const props = defineProps({
6
- index: Number,
7
- char: String,
8
- class: String,
9
- disabled: Boolean,
10
- });
11
-
12
- const extDisplayChar = computed(() => {
13
- return '';
14
- });
15
-
16
- const extIsActive = computed(() => {
17
- });
18
-
19
- const isActive = computed(() => {
20
- });
21
-
22
- const handleClick = () => {
23
- };
24
-
25
- const handleInput = (e) => {
26
- const value = e.target.value;
27
- };
28
-
29
- </script>
30
-
31
- <template>
32
- <!-- Fallback context: input field mode -->
33
- <div
34
- data-slot="input-otp-slot"
35
- :data-index="props.index"
36
- :data-active="isActive"
37
- :class="cn('data-[active=true]:border-ring data-[active=true]:ring-ring/50 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm bg-input-background transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px] cursor-text', props.class)"
38
- @click="handleClick"
39
- >
40
- <input
41
- ref="inputRef"
42
- type="text"
43
- inputmode="numeric"
44
- pattern="[0-9]*"
45
- maxlength="1"
46
- :value="char"
47
- @input="handleInput"
48
- @keydown="handleKeydown"
49
- @focus="isFocused = true"
50
- @blur="isFocused = false"
51
- class="absolute inset-0 w-full h-full bg-transparent border-0 outline-none text-center opacity-0 caret-transparent"
52
- style="caret-color: transparent;"
53
- />
54
- <div class="pointer-events-none select-none relative z-10 flex items-center justify-center">
55
- <span v-if="char" class="text-foreground">{{ displayChar }}</span>
56
- </div>
57
- <div v-if="isActive && !char" class="pointer-events-none absolute inset-0 flex items-center justify-center z-20">
58
- <div class="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
59
- </div>
60
- </div>
61
- </template>
@@ -1,52 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, provide, defineAsyncComponent, useSlots } from 'vue';
3
- import { cn } from '@/lib/utils';
4
- // These would be your context keys (provide/inject)
5
- const InputOTPMaskContext = Symbol('InputOTPMaskContext');
6
- const FallbackInputOTPContext = Symbol('FallbackInputOTPContext');
7
-
8
- const props = defineProps({
9
- class: String,
10
- containerClass: String,
11
- maxLength: { type: Number, required: true },
12
- modelValue: { type: String, default: '' },
13
- disabled: { type: Boolean, default: false },
14
- masked: { type: Boolean, default: false },
15
- });
16
- const emit = defineEmits(['update:modelValue']);
17
- const slots = useSlots();
18
-
19
- const handleChange = (newValue: string) => {
20
- if (newValue.length <= props.maxLength) {
21
- emit('update:modelValue', newValue);
22
- }
23
- };
24
-
25
- import { OTPInput } from "input-otp";
26
-
27
- const contextValue = computed(() => ({
28
- value: props.modelValue,
29
- onChange: handleChange,
30
- maxLength: props.maxLength,
31
- disabled: props.disabled,
32
- masked: props.masked,
33
- }));
34
-
35
- </script>
36
- <template>
37
- <OTPInput
38
- data-slot="input-otp"
39
- :container-class="cn('flex items-center gap-2 has-disabled:opacity-50', props.containerClass)"
40
- :class-name="cn('disabled:cursor-not-allowed', props.class)"
41
- :maxlength="props.maxLength"
42
- :value="props.modelValue"
43
- :on-change="(val) => emit('update:modelValue', val)"
44
- :disabled="props.disabled"
45
- v-bind="props"
46
- style="display: flex;"
47
- >
48
- <div class="flex items-center gap-2">
49
- <slot />
50
- </div>
51
- </OTPInput>
52
- </template>