cynx-lib 1.3.54 → 1.3.56

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-lib",
3
- "version": "1.3.54",
3
+ "version": "1.3.56",
4
4
  "type": "module",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -2,19 +2,20 @@
2
2
  import { computed } from "vue";
3
3
 
4
4
  const VARIANTS: Record<string, any> = {
5
- ok: { bg: "rgba(39,174,96,.1)", color: "#16a34a", border: "rgba(39,174,96,.2)", dot: "#16a34a" },
6
- err: { bg: "rgba(246,71,71,.1)", color: "#ef4444", border: "rgba(246,71,71,.2)", dot: "#ef4444" },
7
- warn: { bg: "rgba(249,158,44,.1)", color: "#f59e0b", border: "rgba(249,158,44,.25)", dot: "#f59e0b" },
8
- blue: { bg: "rgba(39,130,228,.1)", color: "#2563eb", border: "rgba(39,130,228,.2)", dot: "#2563eb" },
9
- teal: { bg: "rgba(18,103,93,.1)", color: "#12675D", border: "rgba(18,103,93,.25)", dot: "#12675D" },
10
- dim: { bg: "#f1f5f9", color: "#64748b", border: "#e2e8f0", dot: "#64748b" },
5
+ ok: { bg: "var(--success-bg, rgba(39,174,96,.1))", color: "var(--success, #16a34a)", border: "var(--sd, rgba(39,174,96,.2))", dot: "var(--success, #16a34a)" },
6
+ err: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
7
+ warn: { bg: "var(--accent-bg, rgba(249,158,44,.1))", color: "var(--accent, #f59e0b)", border: "var(--accent-border, rgba(249,158,44,.25))", dot: "var(--accent, #f59e0b)" },
8
+ blue: { bg: "var(--blue-bg, rgba(39,130,228,.1))", color: "var(--blue, #2563eb)", border: "var(--infd, rgba(39,130,228,.2))", dot: "var(--blue, #2563eb)" },
9
+ teal: { bg: "rgba(18,103,93,.1)", color: "var(--accent, #12675D)", border: "rgba(18,103,93,.25)", dot: "var(--accent, #12675D)" },
10
+ dim: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
11
+ ghost: { bg: "transparent", color: "var(--t2, #64748b)", border: "var(--bd, #e2e8f0)", dot: "var(--t3, #94a3b8)" },
11
12
  };
12
13
 
13
14
  const SIZE_MAP: Record<string, any> = {
14
- xs: { padding: "1.5px 6px", fontSize: "9.5px", dotSize: 5 },
15
- sm: { padding: "2px 8px", fontSize: "11px", dotSize: 6 },
16
- md: { padding: "3px 9px", fontSize: "12px", dotSize: 6.5 },
17
- lg: { padding: "4px 12px", fontSize: "13px", dotSize: 8 },
15
+ xs: { padding: "1.5px 6px", plainPadding: "1px 0", fontSize: "9.5px", dotSize: 5 },
16
+ sm: { padding: "2px 8px", plainPadding: "2px 0", fontSize: "11px", dotSize: 6 },
17
+ md: { padding: "3px 9px", plainPadding: "2px 0", fontSize: "12px", dotSize: 6.5 },
18
+ lg: { padding: "4px 12px", plainPadding: "3px 0", fontSize: "13px", dotSize: 8 },
18
19
  };
19
20
 
20
21
  const props = withDefaults(
@@ -23,30 +24,36 @@ const props = withDefaults(
23
24
  size?: "xs" | "sm" | "md" | "lg";
24
25
  dot?: boolean;
25
26
  pill?: boolean;
27
+ plain?: boolean;
28
+ minimal?: boolean;
26
29
  }>(),
27
30
  {
28
31
  variant: "dim",
29
32
  size: "sm",
30
33
  dot: false,
31
34
  pill: true,
35
+ plain: false,
36
+ minimal: false,
32
37
  }
33
38
  );
34
39
 
35
40
  const v = computed(() => VARIANTS[props.variant] || VARIANTS.dim);
36
41
  const s = computed(() => SIZE_MAP[props.size] || SIZE_MAP.sm);
42
+ const isPlain = computed(() => props.plain || props.minimal);
37
43
  </script>
38
44
 
39
45
  <template>
40
46
  <span
41
47
  :class="[
42
- 'inline-flex items-center gap-1.5 font-bold leading-none select-none',
43
- pill ? 'rounded-full' : 'rounded-md'
48
+ 'inline-flex items-center gap-1.5 select-none',
49
+ isPlain ? 'font-semibold' : 'font-bold leading-none',
50
+ isPlain ? '' : (pill ? 'rounded-full' : 'rounded-md')
44
51
  ]"
45
52
  :style="{
46
- background: v.bg,
53
+ background: isPlain ? 'transparent' : v.bg,
47
54
  color: v.color,
48
- border: `1px solid ${v.border}`,
49
- padding: s.padding,
55
+ border: isPlain ? 'none' : `1px solid ${v.border}`,
56
+ padding: isPlain ? s.plainPadding : s.padding,
50
57
  fontSize: s.fontSize,
51
58
  }"
52
59
  >
@@ -0,0 +1,226 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, onUnmounted, computed } from "vue";
3
+ import { X } from "lucide-vue-next";
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ modelValue?: boolean;
8
+ open?: boolean;
9
+ title?: string;
10
+ maxWidth?: number | string;
11
+ maxHeight?: number | string;
12
+ showClose?: boolean;
13
+ bodyStyle?: Record<string, any>;
14
+ style?: Record<string, any>;
15
+ }>(),
16
+ {
17
+ modelValue: false,
18
+ open: false,
19
+ title: "",
20
+ maxWidth: 520,
21
+ showClose: true,
22
+ bodyStyle: () => ({}),
23
+ style: () => ({}),
24
+ }
25
+ );
26
+
27
+ const emit = defineEmits<{
28
+ (e: "update:modelValue", value: boolean): void;
29
+ (e: "close"): void;
30
+ }>();
31
+
32
+ const isOpen = computed(() => props.modelValue || props.open);
33
+ const closing = ref(false);
34
+
35
+ const STYLES = `
36
+ @keyframes cynxModalFadeIn { from { opacity: 0; } to { opacity: 1; } }
37
+ @keyframes cynxModalFadeOut { from { opacity: 1; } to { opacity: 0; } }
38
+ @keyframes cynxModalCardIn { from { opacity: 0; transform: scale(.96) translateY(-8px); } to { opacity: 1; transform: scale(1) translateY(0); } }
39
+ @keyframes cynxModalCardOut { from { opacity: 1; transform: scale(1) translateY(0); } to { opacity: 0; transform: scale(.96) translateY(-8px); } }
40
+ `;
41
+
42
+ let _id = "cynx-modal-styles";
43
+ function ensureStyles() {
44
+ if (typeof document === "undefined") return;
45
+ if (document.getElementById(_id)) return;
46
+ const tag = document.createElement("style");
47
+ tag.id = _id;
48
+ tag.textContent = STYLES;
49
+ document.head.appendChild(tag);
50
+ }
51
+
52
+ if (typeof document !== "undefined") ensureStyles();
53
+
54
+ function close() {
55
+ if (closing.value) return;
56
+ closing.value = true;
57
+ setTimeout(() => {
58
+ closing.value = false;
59
+ emit("update:modelValue", false);
60
+ emit("close");
61
+ }, 260);
62
+ }
63
+
64
+ function handleKeydown(e: KeyboardEvent) {
65
+ if (e.key === "Escape" && isOpen.value) {
66
+ close();
67
+ }
68
+ }
69
+
70
+ watch(
71
+ isOpen,
72
+ (val) => {
73
+ if (typeof document === "undefined") return;
74
+ if (val) {
75
+ document.body.style.overflow = "hidden";
76
+ window.addEventListener("keydown", handleKeydown);
77
+ } else {
78
+ document.body.style.overflow = "";
79
+ window.removeEventListener("keydown", handleKeydown);
80
+ }
81
+ },
82
+ { immediate: true }
83
+ );
84
+
85
+ onUnmounted(() => {
86
+ if (typeof document !== "undefined") {
87
+ document.body.style.overflow = "";
88
+ window.removeEventListener("keydown", handleKeydown);
89
+ }
90
+ });
91
+
92
+ const resolvedMaxWidth = computed(() => {
93
+ if (typeof props.maxWidth === "number") return `${props.maxWidth}px`;
94
+ return props.maxWidth;
95
+ });
96
+
97
+ const resolvedMaxHeight = computed(() => {
98
+ if (props.maxHeight) {
99
+ return typeof props.maxHeight === "number" ? `${props.maxHeight}px` : props.maxHeight;
100
+ }
101
+ return "85vh";
102
+ });
103
+ </script>
104
+
105
+ <template>
106
+ <Teleport to="body">
107
+ <div
108
+ v-if="isOpen || closing"
109
+ class="cynx-modal-backdrop"
110
+ :style="{
111
+ position: 'fixed',
112
+ inset: 0,
113
+ background: 'rgba(20, 22, 41, .45)',
114
+ display: 'flex',
115
+ alignItems: 'center',
116
+ justifyContent: 'center',
117
+ zIndex: 8000,
118
+ padding: '20px',
119
+ animation: `${closing ? 'cynxModalFadeOut' : 'cynxModalFadeIn'} .26s ease forwards`,
120
+ }"
121
+ @mousedown.self="close"
122
+ >
123
+ <div
124
+ class="cynx-modal-card"
125
+ :style="{
126
+ background: 'var(--surface, #ffffff)',
127
+ border: '1px solid var(--border, #e2e8f0)',
128
+ borderRadius: '14px',
129
+ width: '100%',
130
+ maxWidth: resolvedMaxWidth,
131
+ maxHeight: resolvedMaxHeight,
132
+ boxShadow: 'var(--shadow-lg, 0 20px 60px rgba(0, 0, 0, .25))',
133
+ overflow: 'hidden',
134
+ display: 'flex',
135
+ flexDirection: 'column',
136
+ animation: `${closing ? 'cynxModalCardOut' : 'cynxModalCardIn'} .26s cubic-bezier(.4, 0, .2, 1) forwards`,
137
+ ...style,
138
+ }"
139
+ @mousedown.stop
140
+ >
141
+ <!-- Modal Header -->
142
+ <div
143
+ class="cynx-modal-hdr"
144
+ :style="{
145
+ display: 'flex',
146
+ alignItems: 'center',
147
+ justifyContent: 'space-between',
148
+ padding: '15px 20px',
149
+ borderBottom: '1px solid var(--border-light, #ebebf0)',
150
+ borderRadius: '14px 14px 0 0',
151
+ flexShrink: 0,
152
+ }"
153
+ >
154
+ <slot name="header">
155
+ <span
156
+ class="cynx-modal-title"
157
+ :style="{
158
+ fontSize: '.95rem',
159
+ fontWeight: 700,
160
+ letterSpacing: '-.01em',
161
+ color: 'var(--t, var(--primary-text, #0f111c))',
162
+ }"
163
+ >
164
+ {{ title }}
165
+ </span>
166
+ </slot>
167
+
168
+ <button
169
+ v-if="showClose"
170
+ type="button"
171
+ class="cynx-modal-close"
172
+ :style="{
173
+ background: 'none',
174
+ border: 'none',
175
+ cursor: 'pointer',
176
+ padding: '4px',
177
+ borderRadius: '6px',
178
+ color: 'var(--muted, #797b8d)',
179
+ display: 'flex',
180
+ alignItems: 'center',
181
+ justifyContent: 'center',
182
+ transition: 'all .14s',
183
+ }"
184
+ @click="close"
185
+ >
186
+ <X :size="16" />
187
+ </button>
188
+ </div>
189
+
190
+ <!-- Modal Body -->
191
+ <div
192
+ class="cynx-modal-body custom-scrollbar"
193
+ :style="{
194
+ padding: '20px',
195
+ overflowX: 'hidden',
196
+ overflowY: 'auto',
197
+ flex: '1 1 auto',
198
+ minHeight: 0,
199
+ color: 'var(--t, var(--primary-text, #0f111c))',
200
+ ...bodyStyle,
201
+ }"
202
+ >
203
+ <slot />
204
+ </div>
205
+
206
+ <!-- Modal Footer -->
207
+ <div
208
+ v-if="$slots.footer"
209
+ class="cynx-modal-footer"
210
+ :style="{
211
+ display: 'flex',
212
+ justifyContent: 'flex-end',
213
+ gap: '8px',
214
+ padding: '13px 20px',
215
+ borderTop: '1px solid var(--border-light, #ebebf0)',
216
+ background: 'var(--grey-1, #f5f5f8)',
217
+ borderRadius: '0 0 14px 14px',
218
+ flexShrink: 0,
219
+ }"
220
+ >
221
+ <slot name="footer" />
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </Teleport>
226
+ </template>
@@ -1,6 +1,20 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from "vue";
3
3
 
4
+ const STYLES = `.spin{animation:cx-spin .7s linear infinite}@keyframes cx-spin{to{transform:rotate(360deg)}}.v-progress-circular--indeterminate>svg{animation:progress-circular-rotate .8s linear infinite;transform-origin:center center;transition:all .2s ease-in-out}.v-progress-circular>svg{width:100%;height:100%;margin:auto;position:absolute;inset:0;z-index:0}@keyframes progress-circular-rotate{100%{transform:rotate(270deg)}}@keyframes progress-circular-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:89,200;stroke-dashoffset:-35px}100%{stroke-dasharray:89,200;stroke-dashoffset:-124px}}.v-progress-circular--indeterminate .v-progress-circular__overlay{animation:progress-circular-dash 1.4s ease-in-out infinite;stroke-linecap:round}`;
5
+
6
+ const _id = "cynx-spinner-styles";
7
+ function ensureStyles() {
8
+ if (typeof document === "undefined") return;
9
+ if (document.getElementById(_id)) return;
10
+ const tag = document.createElement("style");
11
+ tag.id = _id;
12
+ tag.textContent = STYLES;
13
+ document.head.appendChild(tag);
14
+ }
15
+
16
+ if (typeof document !== "undefined") ensureStyles();
17
+
4
18
  const props = withDefaults(
5
19
  defineProps<{
6
20
  size?: number;
@@ -10,7 +24,7 @@ const props = withDefaults(
10
24
  variant?: "default" | "classic";
11
25
  }>(),
12
26
  {
13
- size: 20,
27
+ size: undefined,
14
28
  inline: false,
15
29
  color: "currentColor",
16
30
  fullPage: false,
@@ -18,56 +32,61 @@ const props = withDefaults(
18
32
  }
19
33
  );
20
34
 
21
- const s = computed(() => (props.inline ? 14 : props.size));
35
+ ensureStyles();
36
+
37
+ const s = computed(() => {
38
+ if (props.size !== undefined && props.size !== null) {
39
+ return props.size;
40
+ }
41
+ return props.inline ? 14 : 20;
42
+ });
22
43
  </script>
23
44
 
24
45
  <template>
25
- <!-- Fullpage Loading Container -->
26
- <div
27
- v-if="fullPage"
28
- class="fixed inset-0 bg-[var(--bg,#f8fafc)] flex flex-col items-center justify-center gap-4 z-[9999]"
29
- >
46
+ <!-- Classic Variant -->
47
+ <template v-if="variant === 'classic'">
30
48
  <div
31
- class="v-progress-circular v-progress-circular--indeterminate relative"
32
- :style="{ width: `${s}px`, height: `${s}px` }"
49
+ v-if="fullPage"
50
+ :style="{
51
+ position: 'fixed',
52
+ inset: 0,
53
+ background: 'var(--bg, #f8fafc)',
54
+ display: 'flex',
55
+ flexDirection: 'column',
56
+ alignItems: 'center',
57
+ justifyContent: 'center',
58
+ gap: '16px',
59
+ zIndex: 9999,
60
+ }"
33
61
  >
34
62
  <svg
35
- viewBox="0 0 42.42 42.42"
36
- class="w-full h-full absolute inset-0 -rotate-90"
63
+ class="spin"
64
+ :width="s"
65
+ :height="s"
66
+ viewBox="0 0 24 24"
67
+ fill="none"
68
+ :stroke="color"
69
+ stroke-width="2.5"
70
+ stroke-linecap="round"
37
71
  >
38
- <circle
39
- fill="transparent"
40
- cx="50%"
41
- cy="50%"
42
- r="20"
43
- stroke="var(--grey-2,#e0e0e0)"
44
- stroke-width="2.42"
45
- stroke-dasharray="125.66"
46
- stroke-dashoffset="0"
47
- />
48
- <circle
49
- fill="transparent"
50
- cx="50%"
51
- cy="50%"
52
- r="20"
53
- :stroke="color === 'currentColor' ? 'var(--accent,#12675D)' : color"
54
- stroke-width="2.42"
55
- class="v-progress-circular__overlay"
72
+ <path
73
+ d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"
56
74
  />
57
75
  </svg>
76
+ <div
77
+ :style="{
78
+ fontSize: '.75rem',
79
+ fontFamily: 'var(--mono, ui-monospace, monospace)',
80
+ color: 'var(--muted, #797b8d)',
81
+ }"
82
+ >
83
+ Loading...
84
+ </div>
58
85
  </div>
59
- <div class="text-xs font-mono text-[var(--muted,#797b8d)]">Loading...</div>
60
- </div>
61
86
 
62
- <!-- Inline / Normal Spinner -->
63
- <div
64
- v-else
65
- :class="[inline ? 'inline-flex items-center' : 'flex items-center justify-center py-12']"
66
- >
67
- <!-- Classic spinner variant -->
68
87
  <svg
69
- v-if="variant === 'classic'"
70
- class="animate-spin"
88
+ v-else-if="inline"
89
+ class="spin"
71
90
  :width="s"
72
91
  :height="s"
73
92
  viewBox="0 0 24 24"
@@ -81,66 +100,164 @@ const s = computed(() => (props.inline ? 14 : props.size));
81
100
  />
82
101
  </svg>
83
102
 
84
- <!-- Circular spinner variant (default) -->
85
103
  <div
86
104
  v-else
87
- class="v-progress-circular v-progress-circular--indeterminate relative"
88
- :style="{ width: `${s}px`, height: `${s}px` }"
105
+ :style="{
106
+ display: 'flex',
107
+ alignItems: 'center',
108
+ justifyContent: 'center',
109
+ padding: '48px 0',
110
+ }"
111
+ >
112
+ <svg
113
+ class="spin"
114
+ :width="s"
115
+ :height="s"
116
+ viewBox="0 0 24 24"
117
+ fill="none"
118
+ :stroke="color"
119
+ stroke-width="2.5"
120
+ stroke-linecap="round"
121
+ >
122
+ <path
123
+ d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"
124
+ />
125
+ </svg>
126
+ </div>
127
+ </template>
128
+
129
+ <!-- Circular Variant (Default) -->
130
+ <template v-else>
131
+ <!-- FullPage Mode -->
132
+ <div
133
+ v-if="fullPage"
134
+ :style="{
135
+ position: 'fixed',
136
+ inset: 0,
137
+ background: 'var(--bg, #f8fafc)',
138
+ display: 'flex',
139
+ flexDirection: 'column',
140
+ alignItems: 'center',
141
+ justifyContent: 'center',
142
+ gap: '16px',
143
+ zIndex: 9999,
144
+ }"
145
+ >
146
+ <div
147
+ class="v-progress-circular v-progress-circular--indeterminate"
148
+ :style="{ width: `${s}px`, height: `${s}px` }"
149
+ >
150
+ <svg
151
+ xmlns="http://www.w3.org/2000/svg"
152
+ viewBox="0 0 42.42 42.42"
153
+ :style="{ width: `${s}px`, height: `${s}px`, transform: 'rotate(-90deg)' }"
154
+ >
155
+ <circle
156
+ fill="transparent"
157
+ cx="50%"
158
+ cy="50%"
159
+ r="20"
160
+ stroke="var(--grey-2, #e0e0e0)"
161
+ stroke-width="2.42"
162
+ stroke-dasharray="125.66"
163
+ stroke-dashoffset="0"
164
+ />
165
+ <circle
166
+ class="v-progress-circular__overlay"
167
+ fill="transparent"
168
+ cx="50%"
169
+ cy="50%"
170
+ r="20"
171
+ :stroke="color"
172
+ stroke-width="2.42"
173
+ stroke-dasharray="125.66"
174
+ stroke-dashoffset="125.66"
175
+ />
176
+ </svg>
177
+ </div>
178
+ <div
179
+ :style="{
180
+ fontSize: '.75rem',
181
+ fontFamily: 'var(--mono, ui-monospace, monospace)',
182
+ color: 'var(--muted, #797b8d)',
183
+ }"
184
+ >
185
+ Loading...
186
+ </div>
187
+ </div>
188
+
189
+ <!-- Inline Mode -->
190
+ <div
191
+ v-else-if="inline"
192
+ class="v-progress-circular v-progress-circular--indeterminate"
193
+ :style="{
194
+ width: `${s}px`,
195
+ height: `${s}px`,
196
+ position: 'relative',
197
+ display: 'inline-flex',
198
+ flexShrink: 0,
199
+ }"
89
200
  >
90
201
  <svg
91
202
  viewBox="0 0 42.42 42.42"
92
- class="w-full h-full absolute inset-0 -rotate-90"
203
+ :style="{ width: '100%', height: '100%', transform: 'rotate(-90deg)' }"
93
204
  >
94
205
  <circle
206
+ class="v-progress-circular__overlay"
95
207
  fill="transparent"
96
208
  cx="50%"
97
209
  cy="50%"
98
210
  r="20"
99
- stroke="var(--grey-2,#e0e0e0)"
211
+ :stroke="color"
100
212
  stroke-width="2.42"
101
213
  stroke-dasharray="125.66"
102
- stroke-dashoffset="0"
103
- />
104
- <circle
105
- fill="transparent"
106
- cx="50%"
107
- cy="50%"
108
- r="20"
109
- :stroke="color === 'currentColor' ? 'var(--accent,#12675D)' : color"
110
- stroke-width="2.42"
111
- class="v-progress-circular__overlay"
214
+ stroke-dashoffset="125.66"
112
215
  />
113
216
  </svg>
114
217
  </div>
115
- </div>
116
- </template>
117
218
 
118
- <style scoped>
119
- .v-progress-circular--indeterminate > svg {
120
- animation: progress-circular-rotate 0.8s linear infinite;
121
- transform-origin: center center;
122
- }
123
- @keyframes progress-circular-rotate {
124
- 100% {
125
- transform: rotate(270deg);
126
- }
127
- }
128
- @keyframes progress-circular-dash {
129
- 0% {
130
- stroke-dasharray: 1, 200;
131
- stroke-dashoffset: 0;
132
- }
133
- 50% {
134
- stroke-dasharray: 89, 200;
135
- stroke-dashoffset: -35px;
136
- }
137
- 100% {
138
- stroke-dasharray: 89, 200;
139
- stroke-dashoffset: -124px;
140
- }
141
- }
142
- .v-progress-circular--indeterminate .v-progress-circular__overlay {
143
- animation: progress-circular-dash 1.4s ease-in-out infinite;
144
- stroke-linecap: round;
145
- }
146
- </style>
219
+ <!-- Normal Centered Mode -->
220
+ <div
221
+ v-else
222
+ :style="{
223
+ display: 'flex',
224
+ alignItems: 'center',
225
+ justifyContent: 'center',
226
+ padding: '48px 0',
227
+ }"
228
+ >
229
+ <div
230
+ class="v-progress-circular v-progress-circular--indeterminate"
231
+ :style="{ width: `${s}px`, height: `${s}px` }"
232
+ >
233
+ <svg
234
+ xmlns="http://www.w3.org/2000/svg"
235
+ viewBox="0 0 42.42 42.42"
236
+ :style="{ width: `${s}px`, height: `${s}px`, transform: 'rotate(-90deg)' }"
237
+ >
238
+ <circle
239
+ fill="transparent"
240
+ cx="50%"
241
+ cy="50%"
242
+ r="20"
243
+ stroke="var(--grey-2, #e0e0e0)"
244
+ stroke-width="2.42"
245
+ stroke-dasharray="125.66"
246
+ stroke-dashoffset="0"
247
+ />
248
+ <circle
249
+ class="v-progress-circular__overlay"
250
+ fill="transparent"
251
+ cx="50%"
252
+ cy="50%"
253
+ r="20"
254
+ :stroke="color"
255
+ stroke-width="2.42"
256
+ stroke-dasharray="125.66"
257
+ stroke-dashoffset="125.66"
258
+ />
259
+ </svg>
260
+ </div>
261
+ </div>
262
+ </template>
263
+ </template>
@@ -0,0 +1,115 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
4
+ const SIZES: Record<string, { w: number; h: number; r: number; dot: number; off: number; on: number }> = {
5
+ sm: { w: 28, h: 16, r: 8, dot: 12, off: 2, on: 14 },
6
+ md: { w: 34, h: 18, r: 9, dot: 14, off: 2, on: 18 },
7
+ lg: { w: 44, h: 24, r: 12, dot: 18, off: 3, on: 23 },
8
+ };
9
+
10
+ const COLORS: Record<string, string> = {
11
+ primary: "var(--accent, #12675D)",
12
+ success: "var(--success, var(--s, #27ae60))",
13
+ warning: "var(--warning, var(--w, #f59e0b))",
14
+ danger: "var(--danger, var(--d, #f64747))",
15
+ };
16
+
17
+ const OFF_COLOR = "var(--border, var(--bd, #d7d8e0))";
18
+
19
+ const props = withDefaults(
20
+ defineProps<{
21
+ modelValue?: boolean;
22
+ checked?: boolean;
23
+ disabled?: boolean;
24
+ size?: "sm" | "md" | "lg";
25
+ color?: "primary" | "success" | "warning" | "danger";
26
+ label?: string;
27
+ labelStyle?: Record<string, any>;
28
+ className?: string;
29
+ style?: Record<string, any>;
30
+ }>(),
31
+ {
32
+ modelValue: false,
33
+ checked: false,
34
+ disabled: false,
35
+ size: "md",
36
+ color: "primary",
37
+ label: "",
38
+ labelStyle: () => ({}),
39
+ className: "",
40
+ style: () => ({}),
41
+ }
42
+ );
43
+
44
+ const emit = defineEmits<{
45
+ (e: "update:modelValue", value: boolean): void;
46
+ (e: "change", value: boolean): void;
47
+ }>();
48
+
49
+ const isChecked = computed(() => (props.modelValue !== undefined ? props.modelValue : props.checked));
50
+ const s = computed(() => SIZES[props.size] || SIZES.md);
51
+ const trackBg = computed(() => (isChecked.value ? (COLORS[props.color] || COLORS.primary) : OFF_COLOR));
52
+
53
+ const toggle = () => {
54
+ if (props.disabled) return;
55
+ const next = !isChecked.value;
56
+ emit("update:modelValue", next);
57
+ emit("change", next);
58
+ };
59
+ </script>
60
+
61
+ <template>
62
+ <label
63
+ :class="['cynx-toggle-label', className]"
64
+ :style="{
65
+ display: 'inline-flex',
66
+ alignItems: 'center',
67
+ gap: '8px',
68
+ cursor: disabled ? 'not-allowed' : 'pointer',
69
+ userSelect: 'none',
70
+ opacity: disabled ? 0.4 : 1,
71
+ ...style,
72
+ }"
73
+ @click.prevent="toggle"
74
+ >
75
+ <input
76
+ type="checkbox"
77
+ :checked="isChecked"
78
+ :disabled="disabled"
79
+ style="position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;"
80
+ />
81
+ <span
82
+ class="cynx-toggle-track"
83
+ :style="{
84
+ position: 'relative',
85
+ width: `${s.w}px`,
86
+ height: `${s.h}px`,
87
+ borderRadius: `${s.r}px`,
88
+ background: trackBg,
89
+ flexShrink: 0,
90
+ transition: 'background .25s ease',
91
+ boxShadow: 'var(--shadow-sm, 0 1px 3px rgba(20,22,41,.1))',
92
+ }"
93
+ >
94
+ <span
95
+ class="cynx-toggle-dot"
96
+ :style="{
97
+ position: 'absolute',
98
+ top: '50%',
99
+ transform: 'translateY(-50%)',
100
+ left: `${isChecked ? s.on : s.off}px`,
101
+ width: `${s.dot}px`,
102
+ height: `${s.dot}px`,
103
+ background: 'var(--surface, #ffffff)',
104
+ borderRadius: '50%',
105
+ boxShadow: '0 1px 3px rgba(0,0,0,.25)',
106
+ transition: 'left .3s cubic-bezier(.34,1.56,.64,1), transform .2s ease',
107
+ willChange: 'left',
108
+ }"
109
+ />
110
+ </span>
111
+ <span v-if="label || $slots.default" :style="labelStyle" class="cynx-toggle-text">
112
+ <slot>{{ label }}</slot>
113
+ </span>
114
+ </label>
115
+ </template>
package/vue/index.d.ts CHANGED
@@ -50,6 +50,8 @@ export declare const Sidebar: DefineComponent<any, any, any>;
50
50
  export declare const Table: DefineComponent<any, any, any>;
51
51
  export declare const Pagination: DefineComponent<any, any, any>;
52
52
  export declare const Skeleton: DefineComponent<any, any, any>;
53
+ export declare const Modal: DefineComponent<any, any, any>;
54
+ export declare const Toggle: DefineComponent<any, any, any>;
53
55
  export declare const XTable: DefineComponent<any, any, any>;
54
56
  export declare const XThead: DefineComponent<any, any, any>;
55
57
  export declare const XTbody: DefineComponent<any, any, any>;
package/vue/index.js CHANGED
@@ -10,6 +10,8 @@ import Sidebar from "./components/Sidebar.vue";
10
10
  import Table from "./components/Table.vue";
11
11
  import Pagination from "./components/Pagination.vue";
12
12
  import Skeleton from "./components/Skeleton.vue";
13
+ import Modal from "./components/Modal.vue";
14
+ import Toggle from "./components/Toggle.vue";
13
15
  import { XTable, XThead, XTbody, XTr, XTh, XTd } from "./components/XTable.vue";
14
16
  import { useAutoSkeletonRows } from "./composables/useAutoSkeletonRows";
15
17
 
@@ -25,6 +27,8 @@ export {
25
27
  Table,
26
28
  Pagination,
27
29
  Skeleton,
30
+ Modal,
31
+ Toggle,
28
32
  XTable,
29
33
  XThead,
30
34
  XTbody,
@@ -46,6 +50,8 @@ export default {
46
50
  Table,
47
51
  Pagination,
48
52
  Skeleton,
53
+ Modal,
54
+ Toggle,
49
55
  XTable,
50
56
  XThead,
51
57
  XTbody,