nexa-ui-kit 0.7.11 → 0.8.1

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.
@@ -1,226 +1,226 @@
1
- <script setup>
2
- import { signal, effect, onMounted, onUnmounted } from 'nexa-framework'
3
-
4
- const props = defineProps({
5
- show: { type: Boolean, default: false },
6
- title: { type: String, default: '' },
7
- size: { type: String, default: 'md' },
8
- closable: { type: Boolean, default: true },
9
- closeLeft: { type: Boolean, default: false }
10
- })
11
-
12
- const emit = defineEmits(['close'])
13
-
14
- const isVisible = signal(false)
15
- let modalEl = null
16
- let previousFocus = null
17
- const bodyOverflow = signal('')
18
-
19
- const setModalRef = (el) => { modalEl = el }
20
-
21
- const sizeMap = { sm: '400px', md: '500px', lg: '640px', xl: '800px', full: '96%' }
22
-
23
- effect(() => {
24
- if (props.show) {
25
- previousFocus = document.activeElement
26
- isVisible.value = true
27
- bodyOverflow.value = document.body.style.overflow
28
- document.body.style.overflow = 'hidden'
29
- requestAnimationFrame(() => {
30
- if (modalEl) modalEl.focus()
31
- })
32
- } else {
33
- setTimeout(() => {
34
- isVisible.value = false
35
- document.body.style.overflow = bodyOverflow.value
36
- if (previousFocus && previousFocus.focus) {
37
- try { previousFocus.focus() } catch {}
38
- }
39
- }, 250)
40
- }
41
- })
42
-
43
- const close = () => {
44
- if (!props.closable) return
45
- emit('close')
46
- }
47
-
48
- const handleEsc = (e) => {
49
- if (e.key === 'Escape' && props.show) close()
50
- }
51
-
52
- const handleOverlayClick = (e) => {
53
- if (e.target === e.currentTarget) close()
54
- }
55
-
56
- const focusableSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
57
-
58
- const handleKeydown = (e) => {
59
- if (e.key !== 'Tab' || !modalEl) return
60
- const focusable = modalEl.querySelectorAll(focusableSelector)
61
- if (focusable.length === 0) {
62
- e.preventDefault()
63
- return
64
- }
65
- const first = focusable[0]
66
- const last = focusable[focusable.length - 1]
67
- if (e.shiftKey && document.activeElement === first) {
68
- e.preventDefault()
69
- last.focus()
70
- } else if (!e.shiftKey && document.activeElement === last) {
71
- e.preventDefault()
72
- first.focus()
73
- }
74
- }
75
-
76
- onMounted(() => {
77
- window.addEventListener('keydown', handleEsc)
78
- })
79
-
80
- onUnmounted(() => {
81
- window.removeEventListener('keydown', handleEsc)
82
- document.body.style.overflow = ''
83
- })
84
- </script>
85
-
86
- <template>
87
- <Teleport to="body">
88
- <div v-if="isVisible.value" class="n-modal-root">
89
- <div
90
- class="n-modal-overlay"
91
- :class="{ 'is-active': show }"
92
- @click="handleOverlayClick"
93
- ></div>
94
- <div
95
- :ref="setModalRef"
96
- class="n-modal-container"
97
- :class="{ 'is-active': show }"
98
- :style="{ maxWidth: sizeMap[size] || size }"
99
- tabindex="-1"
100
- @keydown="handleKeydown"
101
- >
102
- <div v-if="title || $slots.header" class="n-modal-header" :class="{ 'close-left': closeLeft }">
103
- <button v-if="closable && closeLeft" class="n-modal-close" @click="close" aria-label="Close">&times;</button>
104
- <slot name="header">
105
- <h3>{{ title }}</h3>
106
- </slot>
107
- <button v-if="closable && !closeLeft" class="n-modal-close" @click="close" aria-label="Close">&times;</button>
108
- </div>
109
- <div class="n-modal-content">
110
- <slot />
111
- </div>
112
- <div v-if="$slots.footer" class="n-modal-footer">
113
- <slot name="footer" />
114
- </div>
115
- </div>
116
- </div>
117
- </Teleport>
118
- </template>
119
-
120
- <style scoped>
121
- .n-modal-root {
122
- position: fixed;
123
- top: 0;
124
- left: 0;
125
- width: 100vw;
126
- height: 100vh;
127
- display: flex;
128
- align-items: center;
129
- justify-content: center;
130
- z-index: var(--n-z-modal);
131
- }
132
-
133
- .n-modal-overlay {
134
- position: absolute;
135
- top: 0;
136
- left: 0;
137
- width: 100%;
138
- height: 100%;
139
- background: var(--n-color-overlay);
140
- backdrop-filter: blur(8px);
141
- opacity: 0;
142
- transition: opacity 0.25s ease;
143
- }
144
-
145
- .n-modal-overlay.is-active {
146
- opacity: 1;
147
- }
148
-
149
- .n-modal-container {
150
- position: relative;
151
- width: 90%;
152
- background: var(--n-color-surface);
153
- border: 1px solid var(--n-color-border);
154
- border-radius: var(--n-radius-2xl);
155
- box-shadow: var(--n-shadow-xl);
156
- transform: scale(0.9) translateY(20px);
157
- opacity: 0;
158
- transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
159
- overflow: hidden;
160
- outline: none;
161
- max-height: 85vh;
162
- display: flex;
163
- flex-direction: column;
164
- }
165
-
166
- .n-modal-container.is-active {
167
- transform: scale(1) translateY(0);
168
- opacity: 1;
169
- }
170
-
171
- .n-modal-header {
172
- padding: var(--n-space-6) var(--n-space-8);
173
- border-bottom: 1px solid var(--n-color-border);
174
- display: flex;
175
- justify-content: space-between;
176
- align-items: center;
177
- flex-shrink: 0;
178
- }
179
-
180
- .n-modal-header h3 {
181
- margin: 0;
182
- font-size: var(--n-text-xl);
183
- font-weight: var(--n-weight-bold);
184
- color: var(--n-color-text);
185
- }
186
-
187
- .n-modal-close {
188
- background: transparent;
189
- border: none;
190
- color: var(--n-color-text-secondary);
191
- font-size: 1.75rem;
192
- cursor: pointer;
193
- transition: color var(--n-transition-fast);
194
- padding: 0;
195
- line-height: 1;
196
- width: 36px;
197
- height: 36px;
198
- display: flex;
199
- align-items: center;
200
- justify-content: center;
201
- border-radius: var(--n-radius-sm);
202
- }
203
-
204
- .n-modal-close:hover {
205
- color: var(--n-color-text);
206
- background: var(--n-color-glass);
207
- }
208
-
209
- .n-modal-content {
210
- padding: var(--n-space-8);
211
- color: var(--n-color-text-secondary);
212
- overflow-y: auto;
213
- overflow-x: hidden;
214
- flex: 1;
215
- }
216
-
217
- .n-modal-footer {
218
- padding: var(--n-space-5) var(--n-space-8);
219
- background: rgba(0, 0, 0, 0.15);
220
- border-top: 1px solid var(--n-color-border);
221
- display: flex;
222
- justify-content: flex-end;
223
- gap: var(--n-space-4);
224
- flex-shrink: 0;
225
- }
226
- </style>
1
+ <script setup>
2
+ import { signal, effect, onMounted, onUnmounted } from 'nexa-framework'
3
+
4
+ const props = defineProps({
5
+ show: { type: Boolean, default: false },
6
+ title: { type: String, default: '' },
7
+ size: { type: String, default: 'md' },
8
+ closable: { type: Boolean, default: true },
9
+ closeLeft: { type: Boolean, default: false }
10
+ })
11
+
12
+ const emit = defineEmits(['close'])
13
+
14
+ const isVisible = signal(false)
15
+ let modalEl = null
16
+ let previousFocus = null
17
+ const bodyOverflow = signal('')
18
+
19
+ const setModalRef = (el) => { modalEl = el }
20
+
21
+ const sizeMap = { sm: '400px', md: '500px', lg: '640px', xl: '800px', full: '96%' }
22
+
23
+ effect(() => {
24
+ if (props.show) {
25
+ previousFocus = document.activeElement
26
+ isVisible.value = true
27
+ bodyOverflow.value = document.body.style.overflow
28
+ document.body.style.overflow = 'hidden'
29
+ requestAnimationFrame(() => {
30
+ if (modalEl) modalEl.focus()
31
+ })
32
+ } else {
33
+ setTimeout(() => {
34
+ isVisible.value = false
35
+ document.body.style.overflow = bodyOverflow.value
36
+ if (previousFocus && previousFocus.focus) {
37
+ try { previousFocus.focus() } catch {}
38
+ }
39
+ }, 250)
40
+ }
41
+ })
42
+
43
+ const close = () => {
44
+ if (!props.closable) return
45
+ emit('close')
46
+ }
47
+
48
+ const handleEsc = (e) => {
49
+ if (e.key === 'Escape' && props.show) close()
50
+ }
51
+
52
+ const handleOverlayClick = (e) => {
53
+ if (e.target === e.currentTarget) close()
54
+ }
55
+
56
+ const focusableSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
57
+
58
+ const handleKeydown = (e) => {
59
+ if (e.key !== 'Tab' || !modalEl) return
60
+ const focusable = modalEl.querySelectorAll(focusableSelector)
61
+ if (focusable.length === 0) {
62
+ e.preventDefault()
63
+ return
64
+ }
65
+ const first = focusable[0]
66
+ const last = focusable[focusable.length - 1]
67
+ if (e.shiftKey && document.activeElement === first) {
68
+ e.preventDefault()
69
+ last.focus()
70
+ } else if (!e.shiftKey && document.activeElement === last) {
71
+ e.preventDefault()
72
+ first.focus()
73
+ }
74
+ }
75
+
76
+ onMounted(() => {
77
+ window.addEventListener('keydown', handleEsc)
78
+ })
79
+
80
+ onUnmounted(() => {
81
+ window.removeEventListener('keydown', handleEsc)
82
+ document.body.style.overflow = ''
83
+ })
84
+ </script>
85
+
86
+ <template>
87
+ <Teleport to="body">
88
+ <div v-if="isVisible.value" class="n-modal-root">
89
+ <div
90
+ class="n-modal-overlay"
91
+ :class="{ 'is-active': show }"
92
+ @click="handleOverlayClick"
93
+ ></div>
94
+ <div
95
+ :ref="setModalRef"
96
+ class="n-modal-container"
97
+ :class="{ 'is-active': show }"
98
+ :style="{ maxWidth: sizeMap[size] || size }"
99
+ tabindex="-1"
100
+ @keydown="handleKeydown"
101
+ >
102
+ <div v-if="title || $slots.header" class="n-modal-header" :class="{ 'close-left': closeLeft }">
103
+ <button v-if="closable && closeLeft" class="n-modal-close" @click="close" aria-label="Close">&times;</button>
104
+ <slot name="header">
105
+ <h3>{{ title }}</h3>
106
+ </slot>
107
+ <button v-if="closable && !closeLeft" class="n-modal-close" @click="close" aria-label="Close">&times;</button>
108
+ </div>
109
+ <div class="n-modal-content">
110
+ <slot />
111
+ </div>
112
+ <div v-if="$slots.footer" class="n-modal-footer">
113
+ <slot name="footer" />
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </Teleport>
118
+ </template>
119
+
120
+ <style scoped>
121
+ .n-modal-root {
122
+ position: fixed;
123
+ top: 0;
124
+ left: 0;
125
+ width: 100vw;
126
+ height: 100vh;
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ z-index: var(--n-z-modal);
131
+ }
132
+
133
+ .n-modal-overlay {
134
+ position: absolute;
135
+ top: 0;
136
+ left: 0;
137
+ width: 100%;
138
+ height: 100%;
139
+ background: var(--n-color-overlay);
140
+ backdrop-filter: blur(8px);
141
+ opacity: 0;
142
+ transition: opacity 0.25s ease;
143
+ }
144
+
145
+ .n-modal-overlay.is-active {
146
+ opacity: 1;
147
+ }
148
+
149
+ .n-modal-container {
150
+ position: relative;
151
+ width: 90%;
152
+ background: var(--n-color-surface);
153
+ border: 1px solid var(--n-color-border);
154
+ border-radius: var(--n-radius-2xl);
155
+ box-shadow: var(--n-shadow-xl);
156
+ transform: scale(0.9) translateY(20px);
157
+ opacity: 0;
158
+ transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
159
+ overflow: hidden;
160
+ outline: none;
161
+ max-height: 85vh;
162
+ display: flex;
163
+ flex-direction: column;
164
+ }
165
+
166
+ .n-modal-container.is-active {
167
+ transform: scale(1) translateY(0);
168
+ opacity: 1;
169
+ }
170
+
171
+ .n-modal-header {
172
+ padding: var(--n-space-6) var(--n-space-8);
173
+ border-bottom: 1px solid var(--n-color-border);
174
+ display: flex;
175
+ justify-content: space-between;
176
+ align-items: center;
177
+ flex-shrink: 0;
178
+ }
179
+
180
+ .n-modal-header h3 {
181
+ margin: 0;
182
+ font-size: var(--n-text-xl);
183
+ font-weight: var(--n-weight-bold);
184
+ color: var(--n-color-text);
185
+ }
186
+
187
+ .n-modal-close {
188
+ background: transparent;
189
+ border: none;
190
+ color: var(--n-color-text-secondary);
191
+ font-size: 1.75rem;
192
+ cursor: pointer;
193
+ transition: color var(--n-transition-fast);
194
+ padding: 0;
195
+ line-height: 1;
196
+ width: 36px;
197
+ height: 36px;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ border-radius: var(--n-radius-sm);
202
+ }
203
+
204
+ .n-modal-close:hover {
205
+ color: var(--n-color-text);
206
+ background: var(--n-color-glass);
207
+ }
208
+
209
+ .n-modal-content {
210
+ padding: var(--n-space-8);
211
+ color: var(--n-color-text-secondary);
212
+ overflow-y: auto;
213
+ overflow-x: hidden;
214
+ flex: 1;
215
+ }
216
+
217
+ .n-modal-footer {
218
+ padding: var(--n-space-5) var(--n-space-8);
219
+ background: rgba(0, 0, 0, 0.15);
220
+ border-top: 1px solid var(--n-color-border);
221
+ display: flex;
222
+ justify-content: flex-end;
223
+ gap: var(--n-space-4);
224
+ flex-shrink: 0;
225
+ }
226
+ </style>