cynx-lib 1.3.50 → 1.3.52

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.
@@ -0,0 +1,341 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, useSlots } from "vue";
3
+
4
+ const SIZES: Record<string, { height: number; fontSize: number; paddingH: number }> = {
5
+ xs: { height: 24, fontSize: 11, paddingH: 6 },
6
+ sm: { height: 28, fontSize: 11.5, paddingH: 8 },
7
+ md: { height: 32, fontSize: 13, paddingH: 11 },
8
+ lg: { height: 36, fontSize: 14, paddingH: 14 },
9
+ };
10
+
11
+ const props = withDefaults(
12
+ defineProps<{
13
+ modelValue?: string | number;
14
+ label?: string;
15
+ required?: boolean;
16
+ hint?: string;
17
+ error?: string;
18
+ size?: "xs" | "sm" | "md" | "lg";
19
+ type?: string;
20
+ icon?: any;
21
+ iconRight?: any;
22
+ clearable?: boolean;
23
+ suffix?: string;
24
+ multiline?: boolean;
25
+ disabled?: boolean;
26
+ placeholder?: string;
27
+ className?: string;
28
+ style?: Record<string, any>;
29
+ }>(),
30
+ {
31
+ modelValue: "",
32
+ required: false,
33
+ size: "md",
34
+ type: "text",
35
+ clearable: false,
36
+ multiline: false,
37
+ disabled: false,
38
+ placeholder: "",
39
+ className: "",
40
+ style: () => ({}),
41
+ }
42
+ );
43
+
44
+ const emit = defineEmits<{
45
+ (e: "update:modelValue", value: string): void;
46
+ (e: "clear"): void;
47
+ (e: "focus", event: FocusEvent): void;
48
+ (e: "blur", event: FocusEvent): void;
49
+ }>();
50
+
51
+ const slots = useSlots();
52
+ const inputRef = ref<HTMLInputElement | HTMLTextAreaElement | null>(null);
53
+ const focused = ref(false);
54
+ const hovered = ref(false);
55
+ const s = computed(() => SIZES[props.size] || SIZES.md);
56
+
57
+ const isPasswd = computed(() => props.type === "passwd");
58
+ const resolvedType = computed(() => (isPasswd.value ? "text" : props.type));
59
+ const hasValue = computed(
60
+ () => props.modelValue !== undefined && props.modelValue !== null && props.modelValue !== ""
61
+ );
62
+
63
+ const passwdStyle = computed(() =>
64
+ isPasswd.value
65
+ ? {
66
+ WebkitTextSecurity: "disc",
67
+ textSecurity: "disc",
68
+ fontFamily: hasValue.value ? "monospace" : "var(--f, var(--sans, inherit))",
69
+ fontWeight: hasValue.value ? 700 : "normal",
70
+ letterSpacing: hasValue.value ? "0.15em" : "normal",
71
+ }
72
+ : {}
73
+ );
74
+
75
+ const hasIconLeft = computed(() => Boolean(props.icon) || Boolean(slots.icon));
76
+ const hasIconRight = computed(() => Boolean(props.iconRight) || Boolean(slots.iconRight));
77
+ const showClear = computed(
78
+ () => props.clearable && Boolean(props.modelValue) && !props.disabled && !props.multiline
79
+ );
80
+ const hasSuffix = computed(() => Boolean(props.suffix) && !props.multiline);
81
+ const showRightSlot = computed(() => showClear.value || hasSuffix.value);
82
+
83
+ const handleInput = (e: Event) => {
84
+ emit("update:modelValue", (e.target as HTMLInputElement).value);
85
+ };
86
+
87
+ const handleClear = () => {
88
+ emit("update:modelValue", "");
89
+ emit("clear");
90
+ inputRef.value?.focus();
91
+ };
92
+ </script>
93
+
94
+ <template>
95
+ <div
96
+ class="cynx-input-wrapper"
97
+ style="display: flex; flex-direction: column; gap: 5px; width: 100%;"
98
+ >
99
+ <label
100
+ v-if="label"
101
+ :style="{
102
+ fontSize: size === 'xs' ? '9px' : '11px',
103
+ fontWeight: 600,
104
+ color: 'var(--t2, #797b8d)',
105
+ textTransform: 'uppercase',
106
+ letterSpacing: '.08em',
107
+ userSelect: 'none',
108
+ }"
109
+ >
110
+ {{ label }}
111
+ <span v-if="required" style="color: var(--danger, #f64747); margin-left: 2px;">*</span>
112
+ </label>
113
+
114
+ <div style="position: relative; display: flex; align-items: center; width: 100%;">
115
+ <!-- Left Icon / Text Prefix -->
116
+ <span
117
+ v-if="hasIconLeft"
118
+ style="
119
+ position: absolute;
120
+ left: 10px;
121
+ top: 50%;
122
+ transform: translateY(-50%);
123
+ color: var(--t3, #9b9daa);
124
+ pointer-events: none;
125
+ line-height: 0;
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ z-index: 2;
130
+ "
131
+ >
132
+ <slot name="icon">
133
+ <span
134
+ v-if="typeof icon === 'string'"
135
+ style="font-size: 11px; font-weight: 600; color: var(--t3, #9b9daa); user-select: none;"
136
+ >
137
+ {{ icon }}
138
+ </span>
139
+ <component
140
+ :is="icon"
141
+ v-else
142
+ style="width: 15px; height: 15px; color: var(--t3, #9b9daa);"
143
+ />
144
+ </slot>
145
+ </span>
146
+
147
+ <!-- Right Icon Slot -->
148
+ <span
149
+ v-if="hasIconRight"
150
+ style="
151
+ position: absolute;
152
+ right: 10px;
153
+ top: 50%;
154
+ transform: translateY(-50%);
155
+ color: var(--t3, #9b9daa);
156
+ pointer-events: none;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ z-index: 2;
161
+ "
162
+ >
163
+ <slot name="iconRight">
164
+ <component :is="iconRight" style="width: 15px; height: 15px; color: var(--t3, #9b9daa);" />
165
+ </slot>
166
+ </span>
167
+
168
+ <!-- Textarea Multiline -->
169
+ <textarea
170
+ v-if="multiline"
171
+ ref="inputRef"
172
+ :value="modelValue"
173
+ :placeholder="placeholder"
174
+ :disabled="disabled"
175
+ :class="['cynx-input-control', className]"
176
+ :style="{
177
+ display: 'block',
178
+ width: '100%',
179
+ minHeight: '70px',
180
+ resize: 'vertical',
181
+ whiteSpace: 'pre-wrap',
182
+ overflowWrap: 'break-word',
183
+ background: disabled ? 'var(--grey-1, #f5f5f8)' : 'var(--input-bg, #fff)',
184
+ border: `1px solid ${error ? 'var(--danger, #f64747)' : focused ? 'var(--accent, #12675D)' : hovered ? 'var(--accent, #12675D)' : 'var(--bd, #d7d8e0)'}`,
185
+ borderRadius: 'var(--rads, 8px)',
186
+ color: 'var(--t1, #1a1a2e)',
187
+ fontFamily: 'var(--f, var(--sans, inherit))',
188
+ fontSize: `${s.fontSize}px`,
189
+ paddingTop: '8px',
190
+ paddingBottom: '8px',
191
+ paddingLeft: hasIconLeft ? '34px' : '12px',
192
+ paddingRight: (hasIconRight || showRightSlot) ? '34px' : '12px',
193
+ outline: 'none',
194
+ transition: 'border-color .14s, box-shadow .14s',
195
+ boxShadow: error
196
+ ? 'var(--dd, rgba(246,71,71,.12)), var(--shadow, 0 1px 3px rgba(20,22,41,.1))'
197
+ : focused
198
+ ? 'var(--pd, rgba(18,103,93,.12)), var(--shadow, 0 1px 3px rgba(20,22,41,.1))'
199
+ : 'var(--shadow, 0 1px 3px rgba(20,22,41,.1))',
200
+ opacity: disabled ? 0.5 : 1,
201
+ cursor: disabled ? 'not-allowed' : undefined,
202
+ ...style,
203
+ }"
204
+ @input="handleInput"
205
+ @focus="focused = true; emit('focus', $event)"
206
+ @blur="focused = false; hovered = false; emit('blur', $event)"
207
+ />
208
+
209
+ <!-- Standard Input Control -->
210
+ <input
211
+ v-else
212
+ ref="inputRef"
213
+ :type="resolvedType"
214
+ :value="modelValue"
215
+ :placeholder="placeholder"
216
+ :disabled="disabled"
217
+ :class="['cynx-input-control', className]"
218
+ :style="{
219
+ display: 'block',
220
+ width: '100%',
221
+ background: disabled ? 'var(--grey-1, #f5f5f8)' : 'var(--input-bg, #fff)',
222
+ border: `1px solid ${error ? 'var(--danger, #f64747)' : focused ? 'var(--accent, #12675D)' : hovered ? 'var(--accent, #12675D)' : 'var(--bd, #d7d8e0)'}`,
223
+ borderRadius: 'var(--rads, 8px)',
224
+ color: 'var(--t1, #1a1a2e)',
225
+ fontFamily: 'var(--f, var(--sans, inherit))',
226
+ fontSize: `${s.fontSize}px`,
227
+ height: `${s.height}px`,
228
+ padding: `0 ${s.paddingH}px`,
229
+ paddingLeft: hasIconLeft ? '34px' : `${s.paddingH}px`,
230
+ paddingRight: (hasIconRight || showRightSlot) ? '34px' : `${s.paddingH}px`,
231
+ outline: 'none',
232
+ transition: 'border-color .14s, box-shadow .14s',
233
+ boxShadow: error
234
+ ? 'var(--dd, rgba(246,71,71,.12)), var(--shadow, 0 1px 3px rgba(20,22,41,.1))'
235
+ : focused
236
+ ? 'var(--pd, rgba(18,103,93,.12)), var(--shadow, 0 1px 3px rgba(20,22,41,.1))'
237
+ : 'var(--shadow, 0 1px 3px rgba(20,22,41,.1))',
238
+ opacity: disabled ? 0.5 : 1,
239
+ cursor: disabled ? 'not-allowed' : undefined,
240
+ ...passwdStyle,
241
+ ...style,
242
+ }"
243
+ @input="handleInput"
244
+ @focus="focused = true; emit('focus', $event)"
245
+ @blur="focused = false; hovered = false; emit('blur', $event)"
246
+ />
247
+
248
+ <!-- Clear Button (1-to-1 matching Input.jsx) -->
249
+ <button
250
+ v-if="showClear"
251
+ type="button"
252
+ tabindex="-1"
253
+ style="
254
+ position: absolute;
255
+ right: 8px;
256
+ top: 50%;
257
+ transform: translateY(-50%);
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: center;
261
+ width: 18px;
262
+ height: 18px;
263
+ border-radius: 50%;
264
+ background: transparent;
265
+ border: none;
266
+ cursor: pointer;
267
+ color: var(--t3, #9b9daa);
268
+ padding: 0;
269
+ line-height: 0;
270
+ z-index: 2;
271
+ "
272
+ title="Clear"
273
+ @click="handleClear"
274
+ >
275
+ <svg
276
+ width="10"
277
+ height="10"
278
+ viewBox="0 0 10 10"
279
+ fill="none"
280
+ stroke="currentColor"
281
+ stroke-width="1.8"
282
+ stroke-linecap="round"
283
+ >
284
+ <line x1="1" y1="1" x2="9" y2="9" />
285
+ <line x1="9" y1="1" x2="1" y2="9" />
286
+ </svg>
287
+ </button>
288
+
289
+ <!-- Suffix Text / Element -->
290
+ <span
291
+ v-if="hasSuffix && !showClear"
292
+ style="
293
+ position: absolute;
294
+ right: 8px;
295
+ top: 50%;
296
+ transform: translateY(-50%);
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 4px;
300
+ pointer-events: none;
301
+ z-index: 2;
302
+ "
303
+ >
304
+ {{ suffix }}
305
+ </span>
306
+ </div>
307
+
308
+ <!-- Hint / Error Messages -->
309
+ <span
310
+ v-if="hint && !error"
311
+ style="font-size: 11px; color: var(--t3, #9b9daa);"
312
+ >
313
+ {{ hint }}
314
+ </span>
315
+ <span
316
+ v-if="error"
317
+ style="font-size: 11px; color: var(--danger, #f64747);"
318
+ >
319
+ {{ error }}
320
+ </span>
321
+ </div>
322
+ </template>
323
+
324
+ <style scoped>
325
+ .cynx-input-control::placeholder {
326
+ color: var(--t3, #9b9daa);
327
+ opacity: 1;
328
+ }
329
+
330
+ :root.dark .cynx-input-control,
331
+ :root.dark2 .cynx-input-control {
332
+ --input-bg: #141629;
333
+ --t1: #ffffff;
334
+ --bd: #334155;
335
+ }
336
+
337
+ :root.dark .cynx-input-control::placeholder,
338
+ :root.dark2 .cynx-input-control::placeholder {
339
+ color: #64748b;
340
+ }
341
+ </style>
@@ -0,0 +1,127 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { ChevronLeft, ChevronRight } from "lucide-vue-next";
4
+ import Button from "./Button.vue";
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ page?: number;
9
+ pages?: number;
10
+ total?: number;
11
+ perPage?: number;
12
+ pageSize?: number;
13
+ disabled?: boolean;
14
+ }>(),
15
+ {
16
+ page: 1,
17
+ pages: 1,
18
+ total: 0,
19
+ perPage: 20,
20
+ disabled: false,
21
+ }
22
+ );
23
+
24
+ const emit = defineEmits<{
25
+ (e: "update:page", page: number): void;
26
+ (e: "change", page: number): void;
27
+ }>();
28
+
29
+ const effectivePerPage = computed(() => props.pageSize || props.perPage || 20);
30
+
31
+ const totalPages = computed(() => {
32
+ if (props.pages && props.pages > 1) return props.pages;
33
+ if (props.total && effectivePerPage.value) {
34
+ return Math.ceil(props.total / effectivePerPage.value);
35
+ }
36
+ return 1;
37
+ });
38
+
39
+ const pageItems = computed(() => {
40
+ const totalP = totalPages.value;
41
+ if (!totalP || totalP <= 1) return [];
42
+
43
+ const cur = props.page || 1;
44
+ const items: (number | string)[] = [];
45
+
46
+ for (let i = 1; i <= totalP; i++) {
47
+ if (i === 1 || i === totalP || (i >= cur - 1 && i <= cur + 1)) {
48
+ items.push(i);
49
+ } else if (items[items.length - 1] !== "...") {
50
+ items.push("...");
51
+ }
52
+ }
53
+
54
+ return items;
55
+ });
56
+
57
+ const startItem = computed(() => {
58
+ if (!props.total) return null;
59
+ return (props.page - 1) * effectivePerPage.value + 1;
60
+ });
61
+
62
+ const endItem = computed(() => {
63
+ if (!props.total) return null;
64
+ return Math.min(props.page * effectivePerPage.value, props.total);
65
+ });
66
+
67
+ const handlePageChange = (newPage: number) => {
68
+ if (props.disabled || newPage < 1 || newPage > totalPages.value || newPage === props.page) return;
69
+ emit("update:page", newPage);
70
+ emit("change", newPage);
71
+ };
72
+ </script>
73
+
74
+ <template>
75
+ <div v-if="totalPages > 1 || (total != null && total > 0)" class="flex items-center justify-between w-full py-2 select-none">
76
+ <!-- Info Counter -->
77
+ <div v-if="startItem !== null" class="text-xs text-slate-500 dark:text-slate-400 font-mono">
78
+ {{ startItem }}–{{ endItem }} of {{ total }}
79
+ </div>
80
+ <div v-else />
81
+
82
+ <!-- Page Buttons -->
83
+ <div class="flex items-center gap-1">
84
+ <Button
85
+ variant="ghost"
86
+ size="xs"
87
+ :disabled="disabled || page <= 1"
88
+ title="Previous page"
89
+ @click="handlePageChange(page - 1)"
90
+ >
91
+ <template #icon>
92
+ <ChevronLeft :size="14" />
93
+ </template>
94
+ </Button>
95
+
96
+ <template v-for="(item, i) in pageItems" :key="`p-${i}`">
97
+ <span
98
+ v-if="item === '...'"
99
+ class="px-1 text-xs text-slate-400 dark:text-slate-500"
100
+ >
101
+ …
102
+ </span>
103
+ <Button
104
+ :variant="item === page ? 'primary' : 'ghost'"
105
+ size="xs"
106
+ class="min-w-[28px] h-7 px-2 font-semibold text-xs"
107
+ :disabled="disabled"
108
+ @click="handlePageChange(Number(item))"
109
+ >
110
+ {{ item }}
111
+ </Button>
112
+ </template>
113
+
114
+ <Button
115
+ variant="ghost"
116
+ size="xs"
117
+ :disabled="disabled || page >= totalPages"
118
+ title="Next page"
119
+ @click="handlePageChange(page + 1)"
120
+ >
121
+ <template #icon>
122
+ <ChevronRight :size="14" />
123
+ </template>
124
+ </Button>
125
+ </div>
126
+ </div>
127
+ </template>