nexa-ui-kit 0.6.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 (114) hide show
  1. package/dist/NBadge.nexa +40 -0
  2. package/dist/NBottomSheet.nexa +124 -0
  3. package/dist/NButton.nexa +123 -0
  4. package/dist/NCard.nexa +74 -0
  5. package/dist/NInput.nexa +116 -0
  6. package/dist/NModal.nexa +165 -0
  7. package/dist/NSelect.nexa +169 -0
  8. package/dist/NToastContainer.nexa +86 -0
  9. package/dist/NTooltip.nexa +115 -0
  10. package/dist/components/NAlert.js +134 -0
  11. package/dist/components/NAlert.nexa +115 -0
  12. package/dist/components/NAutocomplete.js +94 -0
  13. package/dist/components/NAutocomplete.nexa +58 -0
  14. package/dist/components/NAvatar.js +75 -0
  15. package/dist/components/NAvatar.nexa +67 -0
  16. package/dist/components/NBadge.js +74 -0
  17. package/dist/components/NBadge.nexa +61 -0
  18. package/dist/components/NBottomSheet.js +149 -0
  19. package/dist/components/NBottomSheet.nexa +145 -0
  20. package/dist/components/NButton.js +284 -0
  21. package/dist/components/NButton.nexa +275 -0
  22. package/dist/components/NCard.js +117 -0
  23. package/dist/components/NCard.nexa +100 -0
  24. package/dist/components/NCheckbox.js +108 -0
  25. package/dist/components/NCheckbox.nexa +90 -0
  26. package/dist/components/NChips.js +72 -0
  27. package/dist/components/NChips.nexa +57 -0
  28. package/dist/components/NDataTable.js +252 -0
  29. package/dist/components/NDataTable.nexa +186 -0
  30. package/dist/components/NDatepicker.js +379 -0
  31. package/dist/components/NDatepicker.nexa +367 -0
  32. package/dist/components/NForm.js +132 -0
  33. package/dist/components/NForm.nexa +133 -0
  34. package/dist/components/NFormField.js +173 -0
  35. package/dist/components/NFormField.nexa +171 -0
  36. package/dist/components/NInput.js +311 -0
  37. package/dist/components/NInput.nexa +311 -0
  38. package/dist/components/NInputNumber.js +202 -0
  39. package/dist/components/NInputNumber.nexa +199 -0
  40. package/dist/components/NModal.js +221 -0
  41. package/dist/components/NModal.nexa +221 -0
  42. package/dist/components/NMultiSelect.js +156 -0
  43. package/dist/components/NMultiSelect.nexa +77 -0
  44. package/dist/components/NPaginator.js +117 -0
  45. package/dist/components/NPaginator.nexa +77 -0
  46. package/dist/components/NPassword.js +193 -0
  47. package/dist/components/NPassword.nexa +178 -0
  48. package/dist/components/NProgressBar.js +127 -0
  49. package/dist/components/NProgressBar.nexa +111 -0
  50. package/dist/components/NRadio.js +96 -0
  51. package/dist/components/NRadio.nexa +81 -0
  52. package/dist/components/NSelect.js +468 -0
  53. package/dist/components/NSelect.nexa +452 -0
  54. package/dist/components/NSkeleton.js +98 -0
  55. package/dist/components/NSkeleton.nexa +74 -0
  56. package/dist/components/NSwitch.js +92 -0
  57. package/dist/components/NSwitch.nexa +76 -0
  58. package/dist/components/NTabs.js +129 -0
  59. package/dist/components/NTabs.nexa +113 -0
  60. package/dist/components/NTag.js +108 -0
  61. package/dist/components/NTag.nexa +93 -0
  62. package/dist/components/NToastContainer.js +242 -0
  63. package/dist/components/NToastContainer.nexa +221 -0
  64. package/dist/components/NTooltip.js +163 -0
  65. package/dist/components/NTooltip.nexa +166 -0
  66. package/dist/components/NTreeMenu.js +151 -0
  67. package/dist/components/NTreeMenu.nexa +142 -0
  68. package/dist/index.d.ts +32 -0
  69. package/dist/index.js +34 -0
  70. package/dist/services/FloatingOverlay.d.ts +27 -0
  71. package/dist/services/FloatingOverlay.js +98 -0
  72. package/dist/services/FormValidation.d.ts +8 -0
  73. package/dist/services/FormValidation.js +46 -0
  74. package/dist/services/ToastService.d.ts +16 -0
  75. package/dist/services/ToastService.js +26 -0
  76. package/dist/styles/theme.d.ts +1 -0
  77. package/dist/styles/theme.js +144 -0
  78. package/package.json +32 -0
  79. package/src/components/NAlert.nexa +115 -0
  80. package/src/components/NAutocomplete.nexa +58 -0
  81. package/src/components/NAvatar.nexa +67 -0
  82. package/src/components/NBadge.nexa +61 -0
  83. package/src/components/NBottomSheet.nexa +145 -0
  84. package/src/components/NButton.nexa +275 -0
  85. package/src/components/NCard.nexa +100 -0
  86. package/src/components/NCheckbox.nexa +90 -0
  87. package/src/components/NChips.nexa +57 -0
  88. package/src/components/NDataTable.nexa +186 -0
  89. package/src/components/NDatepicker.nexa +367 -0
  90. package/src/components/NForm.nexa +133 -0
  91. package/src/components/NFormField.nexa +171 -0
  92. package/src/components/NInput.nexa +311 -0
  93. package/src/components/NInputNumber.nexa +199 -0
  94. package/src/components/NModal.nexa +221 -0
  95. package/src/components/NMultiSelect.nexa +77 -0
  96. package/src/components/NPaginator.nexa +77 -0
  97. package/src/components/NPassword.nexa +178 -0
  98. package/src/components/NProgressBar.nexa +111 -0
  99. package/src/components/NRadio.nexa +81 -0
  100. package/src/components/NSelect.nexa +452 -0
  101. package/src/components/NSkeleton.nexa +74 -0
  102. package/src/components/NSwitch.nexa +76 -0
  103. package/src/components/NTabs.nexa +113 -0
  104. package/src/components/NTag.nexa +93 -0
  105. package/src/components/NToastContainer.nexa +221 -0
  106. package/src/components/NTooltip.nexa +166 -0
  107. package/src/components/NTreeMenu.nexa +142 -0
  108. package/src/index.ts +36 -0
  109. package/src/services/FloatingOverlay.ts +133 -0
  110. package/src/services/FormValidation.ts +44 -0
  111. package/src/services/ToastService.ts +41 -0
  112. package/src/shims.d.ts +5 -0
  113. package/src/styles/theme.ts +146 -0
  114. package/src/styles/tokens.css +170 -0
@@ -0,0 +1,199 @@
1
+ <script setup>
2
+ import { signal, computed, inject, effect, batch } from 'nexa-framework'
3
+
4
+ const props = defineProps({
5
+ modelValue: { type: Number, default: 0 },
6
+ min: { type: Number, default: null },
7
+ max: { type: Number, default: null },
8
+ step: { type: Number, default: 1 },
9
+ bindField: { type: Boolean, default: false },
10
+ disabled: { type: Boolean, default: false },
11
+ readonly: { type: Boolean, default: false },
12
+ label: { type: String, default: '' },
13
+ placeholder: { type: String, default: '' }
14
+ })
15
+
16
+ const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
17
+
18
+ const field = inject('nexa-ui:form-field', undefined)
19
+
20
+ const effectiveValue = computed(() => {
21
+ if (props.bindField && field?.value) {
22
+ const v = field.value.value
23
+ return typeof v === 'number' ? v : Number(v)
24
+ }
25
+ return props.modelValue
26
+ })
27
+
28
+ const effectiveDisabled = computed(() => {
29
+ if (props.disabled) return true
30
+ if (props.bindField && field?.disabled) return !!field.disabled.value
31
+ return false
32
+ })
33
+
34
+ const text = signal(String(effectiveValue.value ?? ''))
35
+
36
+ effect(() => {
37
+ const next = effectiveValue.value
38
+ const nextText = next === null || next === undefined ? '' : String(next)
39
+ if (text.value === nextText) return
40
+ text.value = nextText
41
+ })
42
+
43
+ const canEdit = computed(() => !effectiveDisabled.value && !props.readonly)
44
+
45
+ const sanitize = (raw) => {
46
+ const input = String(raw ?? '')
47
+ if (!input) return ''
48
+ const allowMinus = typeof props.min === 'number' ? props.min < 0 : true
49
+ let out = ''
50
+ let hasDot = false
51
+ let hasMinus = false
52
+ for (let i = 0; i < input.length; i++) {
53
+ const ch = input[i] === ',' ? '.' : input[i]
54
+ const isDigit = ch >= '0' && ch <= '9'
55
+ if (isDigit) {
56
+ out += ch
57
+ continue
58
+ }
59
+ if (ch === '.' && !hasDot) {
60
+ hasDot = true
61
+ out += ch
62
+ continue
63
+ }
64
+ if (ch === '-' && allowMinus && !hasMinus && out.length === 0) {
65
+ hasMinus = true
66
+ out += ch
67
+ }
68
+ }
69
+ return out
70
+ }
71
+
72
+ const clamp = (n) => {
73
+ if (n == null || Number.isNaN(n)) return null
74
+ let out = n
75
+ if (typeof props.min === 'number' && !Number.isNaN(props.min)) out = Math.max(out, props.min)
76
+ if (typeof props.max === 'number' && !Number.isNaN(props.max)) out = Math.min(out, props.max)
77
+ return out
78
+ }
79
+
80
+ const parse = (raw) => {
81
+ const s = sanitize(raw).trim()
82
+ if (!s) return null
83
+ const n = Number(s)
84
+ if (Number.isNaN(n)) return null
85
+ return n
86
+ }
87
+
88
+ const setValue = (next) => {
89
+ if (!canEdit.value) return
90
+ const clamped = clamp(next)
91
+ if (clamped == null) return
92
+ batch(() => {
93
+ if (props.bindField && field?.setValue) field.setValue(clamped)
94
+ else emit('update:modelValue', clamped)
95
+ text.value = String(clamped)
96
+ })
97
+ }
98
+
99
+ const inc = () => {
100
+ if (!canEdit.value) return
101
+ const base = typeof effectiveValue.value === 'number' && !Number.isNaN(effectiveValue.value) ? effectiveValue.value : 0
102
+ setValue(base + (props.step || 1))
103
+ }
104
+
105
+ const dec = () => {
106
+ if (!canEdit.value) return
107
+ const base = typeof effectiveValue.value === 'number' && !Number.isNaN(effectiveValue.value) ? effectiveValue.value : 0
108
+ setValue(base - (props.step || 1))
109
+ }
110
+
111
+ const onInput = (e) => {
112
+ if (!canEdit.value) return
113
+ const nextRaw = e.target.value
114
+ const nextText = sanitize(nextRaw)
115
+ if (e?.target && e.target.value !== nextText) {
116
+ e.target.value = nextText
117
+ }
118
+ batch(() => {
119
+ text.value = nextText
120
+ const n = parse(nextText)
121
+ const clamped = clamp(n)
122
+ if (clamped == null) return
123
+ if (props.bindField && field?.setValue) field.setValue(clamped)
124
+ else emit('update:modelValue', clamped)
125
+ })
126
+ }
127
+
128
+ const onBeforeInput = (e) => {
129
+ if (!canEdit.value) return
130
+ const inputType = e?.inputType
131
+ if (typeof inputType === 'string' && inputType.includes('Composition')) return
132
+ if (typeof inputType === 'string' && !inputType.startsWith('insert')) return
133
+ const data = e?.data
134
+ if (typeof data !== 'string' || !data) return
135
+ for (let i = 0; i < data.length; i++) {
136
+ const ch = data[i] === ',' ? '.' : data[i]
137
+ const isDigit = ch >= '0' && ch <= '9'
138
+ if (isDigit || ch === '.' || ch === '-') continue
139
+ e.preventDefault()
140
+ return
141
+ }
142
+ }
143
+
144
+ const onPaste = (e) => {
145
+ if (!canEdit.value) return
146
+ const pasted = e?.clipboardData?.getData?.('text')
147
+ if (typeof pasted !== 'string') return
148
+ e.preventDefault()
149
+ const nextText = sanitize(pasted)
150
+ const target = e?.target
151
+ if (!target) return
152
+ target.value = nextText
153
+ onInput({ target })
154
+ }
155
+
156
+ const onKeydown = (e) => {
157
+ if (!canEdit.value) return
158
+ if (e.ctrlKey || e.metaKey || e.altKey) return
159
+ const key = e.key
160
+ if (typeof key !== 'string' || key.length !== 1) return
161
+ const ch = key === ',' ? '.' : key
162
+ const isDigit = ch >= '0' && ch <= '9'
163
+ if (isDigit || ch === '.' || ch === '-') return
164
+ e.preventDefault()
165
+ }
166
+
167
+ const onBlur = () => {
168
+ emit('blur')
169
+ if (props.bindField && field?.onBlur) field.onBlur()
170
+ const n = parse(text.value)
171
+ const clamped = clamp(n)
172
+ if (clamped == null) {
173
+ text.value = String(effectiveValue.value ?? '')
174
+ return
175
+ }
176
+ batch(() => {
177
+ text.value = String(clamped)
178
+ if (props.bindField && field?.setValue) field.setValue(clamped)
179
+ else emit('update:modelValue', clamped)
180
+ })
181
+ }
182
+
183
+ const onFocus = () => emit('focus')
184
+ </script>
185
+
186
+ <template>
187
+ <div class="n-inum">
188
+ <label v-if="label" class="n-inum-label">{{ label }}</label>
189
+ <div class="n-inum-wrap" :class="{ 'is-disabled': effectiveDisabled.value }">
190
+ <button type="button" class="n-inum-btn n-inum-dec" :disabled="effectiveDisabled.value || readonly" aria-label="Decrement" @click="dec">−</button>
191
+ <input class="n-inum-input" type="text" :value="text.value" :placeholder="placeholder" :disabled="effectiveDisabled.value" :readonly="readonly" inputmode="decimal" autocomplete="off" @beforeinput="onBeforeInput" @keydown="onKeydown" @paste="onPaste" @input="onInput" @focus="onFocus" @blur="onBlur" />
192
+ <button type="button" class="n-inum-btn n-inum-inc" :disabled="effectiveDisabled.value || readonly" aria-label="Increment" @click="inc">+</button>
193
+ </div>
194
+ </div>
195
+ </template>
196
+
197
+ <style scoped>
198
+ .n-inum{display:flex;flex-direction:column;gap:var(--n-space-2);width:100%;font-family:var(--n-font-sans)}.n-inum-label{display:block;font-size:var(--n-text-sm);font-weight:var(--n-weight-medium);color:var(--n-color-text-secondary);margin-bottom:var(--n-space-2)}.n-inum-wrap{display:flex;align-items:stretch;min-height:44px;background:var(--n-color-surface);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);overflow:hidden;transition:all var(--n-transition-fast)}.n-inum-wrap:focus-within{border-color:var(--n-color-primary);box-shadow:0 0 0 3px var(--n-color-primary-light)}.n-inum-input{flex:1;background:transparent;border:none;outline:none;padding:0.75rem 0.75rem;color:var(--n-color-text);font-size:var(--n-text-base);font-family:inherit;text-align:center;line-height:1.2;box-sizing:border-box}.n-inum-btn{width:2.5rem;background:transparent;border:none;color:var(--n-color-text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:var(--n-text-base);transition:all var(--n-transition-fast)}.n-inum-btn:hover:not(:disabled){background:var(--n-color-glass);color:var(--n-color-text)}.n-inum-btn:disabled{opacity:0.5;cursor:not-allowed}.is-disabled{opacity:0.6;cursor:not-allowed;background:var(--n-color-surface-alt)}
199
+ </style>
@@ -0,0 +1,221 @@
1
+ import { signal, effect, onMounted, onUnmounted, h, hText, defineComponent, registerComponent, reloadComponent, injectStyle, Teleport } from 'nexa-framework'
2
+
3
+ const _sfc_main = defineComponent({
4
+ __scopeId: 'data-v-42ba3820',
5
+ __hmrId: 'NModal_nexa',
6
+ props: {
7
+ show: { type: Boolean, default: false },
8
+ title: { type: String, default: '' },
9
+ size: { type: String, default: 'md' },
10
+ closable: { type: Boolean, default: true }
11
+ },
12
+ emits: ['close'],
13
+ setup(props, setupContext) {
14
+ const { emit, slots, slots: $slots } = setupContext
15
+ const isVisible = signal(false)
16
+ let modalEl = null
17
+ let previousFocus = null
18
+ const bodyOverflow = signal('')
19
+ const sizeMap = { sm: '400px', md: '500px', lg: '640px', xl: '800px', full: '96%' }
20
+ effect(() => {
21
+ if (props.show) {
22
+ previousFocus = document.activeElement
23
+ isVisible.value = true
24
+ bodyOverflow.value = document.body.style.overflow
25
+ document.body.style.overflow = 'hidden'
26
+ requestAnimationFrame(() => {
27
+ if (modalEl) modalEl.focus()
28
+ })
29
+ } else {
30
+ setTimeout(() => {
31
+ isVisible.value = false
32
+ document.body.style.overflow = bodyOverflow.value
33
+ if (previousFocus && previousFocus.focus) {
34
+ try { previousFocus.focus() } catch {}
35
+ }
36
+ }, 250)
37
+ }
38
+ })
39
+ const close = () => {
40
+ if (!props.closable) return
41
+ emit('close')
42
+ }
43
+ const handleEsc = (e) => {
44
+ if (e.key === 'Escape' && props.show) close()
45
+ }
46
+ const handleOverlayClick = (e) => {
47
+ if (e.target === e.currentTarget) close()
48
+ }
49
+ const focusableSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
50
+ const handleKeydown = (e) => {
51
+ if (e.key !== 'Tab' || !modalEl) return
52
+ const focusable = modalEl.querySelectorAll(focusableSelector)
53
+ if (focusable.length === 0) {
54
+ e.preventDefault()
55
+ return
56
+ }
57
+ const first = focusable[0]
58
+ const last = focusable[focusable.length - 1]
59
+ if (e.shiftKey && document.activeElement === first) {
60
+ e.preventDefault()
61
+ last.focus()
62
+ } else if (!e.shiftKey && document.activeElement === last) {
63
+ e.preventDefault()
64
+ first.focus()
65
+ }
66
+ }
67
+ onMounted(() => {
68
+ window.addEventListener('keydown', handleEsc)
69
+ })
70
+ onUnmounted(() => {
71
+ window.removeEventListener('keydown', handleEsc)
72
+ document.body.style.overflow = ''
73
+ })
74
+ return { isVisible, modalEl, previousFocus, bodyOverflow, sizeMap, close, handleEsc, handleOverlayClick, focusableSelector, handleKeydown, $slots, emit }
75
+ }
76
+ })
77
+ // Injected render function
78
+ _sfc_main.render = function(ctx) {
79
+ const { isVisible, modalEl, previousFocus, bodyOverflow, sizeMap, close, handleEsc, handleOverlayClick, focusableSelector, handleKeydown, $slots, emit, show, title, size, closable, Fragment: _ntc_Fragment, Teleport: _ntc_Teleport } = ctx
80
+ return h(_ntc_Teleport, { to: "body", "data-v-42ba3820": "" }, [
81
+ "\n ",
82
+ (isVisible.value) ? h('div', { class: "n-modal-root", "data-v-42ba3820": "" }, [
83
+ "\n ",
84
+ h('div', { class: ["n-modal-overlay", { 'is-active': show }], onClick: handleOverlayClick, "data-v-42ba3820": "" }),
85
+ "\n ",
86
+ h('div', { class: ["n-modal-container", { 'is-active': show }], ref: "modalEl", style: { maxWidth: sizeMap[size] || size }, tabindex: "-1", onKeydown: handleKeydown, "data-v-42ba3820": "" }, [
87
+ "\n ",
88
+ (title) ? h('div', { class: "n-modal-header", "data-v-42ba3820": "" }, [
89
+ "\n ",
90
+ ctx.$slots.header ? ctx.$slots.header() : null,
91
+ "\n ",
92
+ (closable) ? h('button', { class: "n-modal-close", onClick: close, "aria-label": "Close", "data-v-42ba3820": "" }, [
93
+ "×"
94
+ ]) : null
95
+ ]) : null,
96
+ h('div', { class: "n-modal-content", "data-v-42ba3820": "" }, [
97
+ "\n ",
98
+ ctx.$slots.default ? ctx.$slots.default() : null,
99
+ "\n "
100
+ ]),
101
+ "\n ",
102
+ ($slots.footer) ? h('div', { class: "n-modal-footer", "data-v-42ba3820": "" }, [
103
+ "\n ",
104
+ ctx.$slots.footer ? ctx.$slots.footer() : null,
105
+ "\n "
106
+ ]) : null
107
+ ]),
108
+ "\n "
109
+ ]) : null
110
+ ])
111
+ }
112
+ _sfc_main.__scopeId = 'data-v-42ba3820'
113
+ _sfc_main.__hmrId = 'NModal_nexa'
114
+
115
+ export default _sfc_main
116
+
117
+ const __style = `.n-modal-root[data-v-42ba3820]{
118
+ position: fixed;
119
+ top: 0;
120
+ left: 0;
121
+ width: 100vw;
122
+ height: 100vh;
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ z-index: var(--n-z-modal);
127
+ }
128
+
129
+ .n-modal-overlay[data-v-42ba3820]{
130
+ position: absolute;
131
+ top: 0;
132
+ left: 0;
133
+ width: 100%;
134
+ height: 100%;
135
+ background: var(--n-color-overlay);
136
+ backdrop-filter: blur(8px);
137
+ opacity: 0;
138
+ transition: opacity 0.25s ease;
139
+ }
140
+
141
+ .n-modal-overlay.is-active[data-v-42ba3820]{
142
+ opacity: 1;
143
+ }
144
+
145
+ .n-modal-container[data-v-42ba3820]{
146
+ position: relative;
147
+ width: 90%;
148
+ background: var(--n-color-surface);
149
+ border: 1px solid var(--n-color-border);
150
+ border-radius: var(--n-radius-2xl);
151
+ box-shadow: var(--n-shadow-xl);
152
+ transform: scale(0.9) translateY(20px);
153
+ opacity: 0;
154
+ transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
155
+ overflow: hidden;
156
+ outline: none;
157
+ max-height: 85vh;
158
+ display: flex;
159
+ flex-direction: column;
160
+ }
161
+
162
+ .n-modal-container.is-active[data-v-42ba3820]{
163
+ transform: scale(1) translateY(0);
164
+ opacity: 1;
165
+ }
166
+
167
+ .n-modal-header[data-v-42ba3820]{
168
+ padding: var(--n-space-6) var(--n-space-8);
169
+ border-bottom: 1px solid var(--n-color-border);
170
+ display: flex;
171
+ justify-content: space-between;
172
+ align-items: center;
173
+ flex-shrink: 0;
174
+ }
175
+
176
+ .n-modal-header h3[data-v-42ba3820]{
177
+ margin: 0;
178
+ font-size: var(--n-text-xl);
179
+ font-weight: var(--n-weight-bold);
180
+ color: var(--n-color-text);
181
+ }
182
+
183
+ .n-modal-close[data-v-42ba3820]{
184
+ background: transparent;
185
+ border: none;
186
+ color: var(--n-color-text-secondary);
187
+ font-size: 1.75rem;
188
+ cursor: pointer;
189
+ transition: color var(--n-transition-fast);
190
+ padding: 0;
191
+ line-height: 1;
192
+ width: 36px;
193
+ height: 36px;
194
+ display: flex;
195
+ align-items: center;
196
+ justify-content: center;
197
+ border-radius: var(--n-radius-sm);
198
+ }
199
+
200
+ .n-modal-close[data-v-42ba3820]:hover{
201
+ color: var(--n-color-text);
202
+ background: var(--n-color-glass);
203
+ }
204
+
205
+ .n-modal-content[data-v-42ba3820]{
206
+ padding: var(--n-space-8);
207
+ color: var(--n-color-text-secondary);
208
+ overflow-y: auto;
209
+ flex: 1;
210
+ }
211
+
212
+ .n-modal-footer[data-v-42ba3820]{
213
+ padding: var(--n-space-5) var(--n-space-8);
214
+ background: rgba(0, 0, 0, 0.15);
215
+ border-top: 1px solid var(--n-color-border);
216
+ display: flex;
217
+ justify-content: flex-end;
218
+ gap: var(--n-space-4);
219
+ flex-shrink: 0;
220
+ }`
221
+ injectStyle('data-v-42ba3820', __style)
@@ -0,0 +1,221 @@
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
+ })
10
+
11
+ const emit = defineEmits(['close'])
12
+
13
+ const isVisible = signal(false)
14
+ let modalEl = null
15
+ let previousFocus = null
16
+ const bodyOverflow = signal('')
17
+
18
+ const sizeMap = { sm: '400px', md: '500px', lg: '640px', xl: '800px', full: '96%' }
19
+
20
+ effect(() => {
21
+ if (props.show) {
22
+ previousFocus = document.activeElement
23
+ isVisible.value = true
24
+ bodyOverflow.value = document.body.style.overflow
25
+ document.body.style.overflow = 'hidden'
26
+ requestAnimationFrame(() => {
27
+ if (modalEl) modalEl.focus()
28
+ })
29
+ } else {
30
+ setTimeout(() => {
31
+ isVisible.value = false
32
+ document.body.style.overflow = bodyOverflow.value
33
+ if (previousFocus && previousFocus.focus) {
34
+ try { previousFocus.focus() } catch {}
35
+ }
36
+ }, 250)
37
+ }
38
+ })
39
+
40
+ const close = () => {
41
+ if (!props.closable) return
42
+ emit('close')
43
+ }
44
+
45
+ const handleEsc = (e) => {
46
+ if (e.key === 'Escape' && props.show) close()
47
+ }
48
+
49
+ const handleOverlayClick = (e) => {
50
+ if (e.target === e.currentTarget) close()
51
+ }
52
+
53
+ const focusableSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
54
+
55
+ const handleKeydown = (e) => {
56
+ if (e.key !== 'Tab' || !modalEl) return
57
+ const focusable = modalEl.querySelectorAll(focusableSelector)
58
+ if (focusable.length === 0) {
59
+ e.preventDefault()
60
+ return
61
+ }
62
+ const first = focusable[0]
63
+ const last = focusable[focusable.length - 1]
64
+ if (e.shiftKey && document.activeElement === first) {
65
+ e.preventDefault()
66
+ last.focus()
67
+ } else if (!e.shiftKey && document.activeElement === last) {
68
+ e.preventDefault()
69
+ first.focus()
70
+ }
71
+ }
72
+
73
+ onMounted(() => {
74
+ window.addEventListener('keydown', handleEsc)
75
+ })
76
+
77
+ onUnmounted(() => {
78
+ window.removeEventListener('keydown', handleEsc)
79
+ document.body.style.overflow = ''
80
+ })
81
+ </script>
82
+
83
+ <template>
84
+ <Teleport to="body">
85
+ <div v-if="isVisible.value" class="n-modal-root">
86
+ <div
87
+ class="n-modal-overlay"
88
+ :class="{ 'is-active': show }"
89
+ @click="handleOverlayClick"
90
+ ></div>
91
+ <div
92
+ ref="modalEl"
93
+ class="n-modal-container"
94
+ :class="{ 'is-active': show }"
95
+ :style="{ maxWidth: sizeMap[size] || size }"
96
+ tabindex="-1"
97
+ @keydown="handleKeydown"
98
+ >
99
+ <div v-if="title" class="n-modal-header">
100
+ <slot name="header">
101
+ <h3>{{ title }}</h3>
102
+ </slot>
103
+ <button v-if="closable" class="n-modal-close" @click="close" aria-label="Close">&times;</button>
104
+ </div>
105
+ <div class="n-modal-content">
106
+ <slot />
107
+ </div>
108
+ <div v-if="$slots.footer" class="n-modal-footer">
109
+ <slot name="footer" />
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </Teleport>
114
+ </template>
115
+
116
+ <style scoped>
117
+ .n-modal-root {
118
+ position: fixed;
119
+ top: 0;
120
+ left: 0;
121
+ width: 100vw;
122
+ height: 100vh;
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ z-index: var(--n-z-modal);
127
+ }
128
+
129
+ .n-modal-overlay {
130
+ position: absolute;
131
+ top: 0;
132
+ left: 0;
133
+ width: 100%;
134
+ height: 100%;
135
+ background: var(--n-color-overlay);
136
+ backdrop-filter: blur(8px);
137
+ opacity: 0;
138
+ transition: opacity 0.25s ease;
139
+ }
140
+
141
+ .n-modal-overlay.is-active {
142
+ opacity: 1;
143
+ }
144
+
145
+ .n-modal-container {
146
+ position: relative;
147
+ width: 90%;
148
+ background: var(--n-color-surface);
149
+ border: 1px solid var(--n-color-border);
150
+ border-radius: var(--n-radius-2xl);
151
+ box-shadow: var(--n-shadow-xl);
152
+ transform: scale(0.9) translateY(20px);
153
+ opacity: 0;
154
+ transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
155
+ overflow: hidden;
156
+ outline: none;
157
+ max-height: 85vh;
158
+ display: flex;
159
+ flex-direction: column;
160
+ }
161
+
162
+ .n-modal-container.is-active {
163
+ transform: scale(1) translateY(0);
164
+ opacity: 1;
165
+ }
166
+
167
+ .n-modal-header {
168
+ padding: var(--n-space-6) var(--n-space-8);
169
+ border-bottom: 1px solid var(--n-color-border);
170
+ display: flex;
171
+ justify-content: space-between;
172
+ align-items: center;
173
+ flex-shrink: 0;
174
+ }
175
+
176
+ .n-modal-header h3 {
177
+ margin: 0;
178
+ font-size: var(--n-text-xl);
179
+ font-weight: var(--n-weight-bold);
180
+ color: var(--n-color-text);
181
+ }
182
+
183
+ .n-modal-close {
184
+ background: transparent;
185
+ border: none;
186
+ color: var(--n-color-text-secondary);
187
+ font-size: 1.75rem;
188
+ cursor: pointer;
189
+ transition: color var(--n-transition-fast);
190
+ padding: 0;
191
+ line-height: 1;
192
+ width: 36px;
193
+ height: 36px;
194
+ display: flex;
195
+ align-items: center;
196
+ justify-content: center;
197
+ border-radius: var(--n-radius-sm);
198
+ }
199
+
200
+ .n-modal-close:hover {
201
+ color: var(--n-color-text);
202
+ background: var(--n-color-glass);
203
+ }
204
+
205
+ .n-modal-content {
206
+ padding: var(--n-space-8);
207
+ color: var(--n-color-text-secondary);
208
+ overflow-y: auto;
209
+ flex: 1;
210
+ }
211
+
212
+ .n-modal-footer {
213
+ padding: var(--n-space-5) var(--n-space-8);
214
+ background: rgba(0, 0, 0, 0.15);
215
+ border-top: 1px solid var(--n-color-border);
216
+ display: flex;
217
+ justify-content: flex-end;
218
+ gap: var(--n-space-4);
219
+ flex-shrink: 0;
220
+ }
221
+ </style>