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,46 @@
1
+ export const required = (message = 'Required') => (value) => {
2
+ if (value === null || value === undefined)
3
+ return message;
4
+ if (typeof value === 'string' && value.trim() === '')
5
+ return message;
6
+ if (Array.isArray(value) && value.length === 0)
7
+ return message;
8
+ return null;
9
+ };
10
+ export const minLength = (length, message) => (value) => {
11
+ const str = value === null || value === undefined ? '' : String(value);
12
+ if (str.length < length)
13
+ return message || `Must be at least ${length} characters`;
14
+ return null;
15
+ };
16
+ export const maxLength = (length, message) => (value) => {
17
+ const str = value === null || value === undefined ? '' : String(value);
18
+ if (str.length > length)
19
+ return message || `Must be at most ${length} characters`;
20
+ return null;
21
+ };
22
+ export const pattern = (re, message = 'Invalid format') => (value) => {
23
+ const str = value === null || value === undefined ? '' : String(value);
24
+ if (!re.test(str))
25
+ return message;
26
+ return null;
27
+ };
28
+ export const email = (message = 'Invalid email') => (value) => {
29
+ const str = value === null || value === undefined ? '' : String(value).trim();
30
+ if (!str)
31
+ return null;
32
+ const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
33
+ if (!re.test(str))
34
+ return message;
35
+ return null;
36
+ };
37
+ export const sameAs = (otherField, message) => (value, values) => {
38
+ if (value !== values[otherField])
39
+ return message || `Must match ${otherField}`;
40
+ return null;
41
+ };
42
+ export const asyncRule = (rule, delayMs = 0) => async (value, values) => {
43
+ if (delayMs > 0)
44
+ await new Promise(r => setTimeout(r, delayMs));
45
+ return rule(value, values);
46
+ };
@@ -0,0 +1,16 @@
1
+ export type ToastType = 'success' | 'info' | 'warning' | 'error';
2
+ export interface Toast {
3
+ id: number;
4
+ message: string;
5
+ type: ToastType;
6
+ duration?: number;
7
+ }
8
+ export declare const useToast: () => {
9
+ toasts: import("nexa-reactivity").Signal<Toast[]>;
10
+ add: (message: string, type?: ToastType, duration?: number) => number;
11
+ remove: (id: number) => void;
12
+ success: (msg: string, dur?: number) => number;
13
+ error: (msg: string, dur?: number) => number;
14
+ info: (msg: string, dur?: number) => number;
15
+ warning: (msg: string, dur?: number) => number;
16
+ };
@@ -0,0 +1,26 @@
1
+ import { signal } from 'nexa-framework';
2
+ const toasts = signal([]);
3
+ let nextId = 1;
4
+ export const useToast = () => {
5
+ const add = (message, type = 'info', duration = 3000) => {
6
+ const id = nextId++;
7
+ const toast = { id, message, type, duration };
8
+ toasts.value = [...toasts.value, toast];
9
+ if (duration > 0) {
10
+ setTimeout(() => remove(id), duration);
11
+ }
12
+ return id;
13
+ };
14
+ const remove = (id) => {
15
+ toasts.value = toasts.value.filter((t) => t.id !== id);
16
+ };
17
+ return {
18
+ toasts,
19
+ add,
20
+ remove,
21
+ success: (msg, dur) => add(msg, 'success', dur),
22
+ error: (msg, dur) => add(msg, 'error', dur),
23
+ info: (msg, dur) => add(msg, 'info', dur),
24
+ warning: (msg, dur) => add(msg, 'warning', dur)
25
+ };
26
+ };
@@ -0,0 +1 @@
1
+ export declare function installTheme(): void;
@@ -0,0 +1,144 @@
1
+ let installed = false;
2
+ export function installTheme() {
3
+ if (installed)
4
+ return;
5
+ installed = true;
6
+ const tokenCSS = `
7
+ :root {
8
+ --n-color-primary: #3b82f6;
9
+ --n-color-primary-hover: #2563eb;
10
+ --n-color-primary-active: #1d4ed8;
11
+ --n-color-primary-light: rgba(59, 130, 246, 0.12);
12
+ --n-color-primary-glow: rgba(59, 130, 246, 0.3);
13
+ --n-color-success: #10b981;
14
+ --n-color-success-hover: #059669;
15
+ --n-color-success-light: rgba(16, 185, 129, 0.12);
16
+ --n-color-warning: #f59e0b;
17
+ --n-color-warning-hover: #d97706;
18
+ --n-color-warning-light: rgba(245, 158, 11, 0.12);
19
+ --n-color-danger: #ef4444;
20
+ --n-color-danger-hover: #dc2626;
21
+ --n-color-danger-light: rgba(239, 68, 68, 0.12);
22
+ --n-color-info: #06b6d4;
23
+ --n-color-info-hover: #0891b2;
24
+ --n-color-info-light: rgba(6, 182, 212, 0.12);
25
+ --n-color-surface: #0f172a;
26
+ --n-color-surface-alt: #1e293b;
27
+ --n-color-surface-hover: #334155;
28
+ --n-color-surface-elevated: #1e293b;
29
+ --n-color-bg: #020617;
30
+ --n-color-bg-alt: #0f172a;
31
+ --n-color-text: #f8fafc;
32
+ --n-color-text-secondary: #94a3b8;
33
+ --n-color-text-muted: #64748b;
34
+ --n-color-text-inverse: #0f172a;
35
+ --n-color-border: rgba(255, 255, 255, 0.08);
36
+ --n-color-border-hover: rgba(255, 255, 255, 0.15);
37
+ --n-color-border-active: rgba(255, 255, 255, 0.25);
38
+ --n-color-overlay: rgba(2, 6, 17, 0.7);
39
+ --n-color-glass: rgba(255, 255, 255, 0.04);
40
+ --n-color-glass-border: rgba(255, 255, 255, 0.08);
41
+ --n-color-glass-hover: rgba(255, 255, 255, 0.08);
42
+ --n-font-sans: 'Outfit', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
43
+ --n-font-mono: 'JetBrains Mono', 'Fira Code', monospace;
44
+ --n-text-xs: 0.75rem;
45
+ --n-text-sm: 0.875rem;
46
+ --n-text-base: 1rem;
47
+ --n-text-lg: 1.125rem;
48
+ --n-text-xl: 1.25rem;
49
+ --n-text-2xl: 1.5rem;
50
+ --n-text-3xl: 2rem;
51
+ --n-weight-normal: 400;
52
+ --n-weight-medium: 500;
53
+ --n-weight-semibold: 600;
54
+ --n-weight-bold: 700;
55
+ --n-weight-extrabold: 800;
56
+ --n-leading-tight: 1.25;
57
+ --n-leading-normal: 1.5;
58
+ --n-leading-relaxed: 1.75;
59
+ --n-tracking-tight: -0.025em;
60
+ --n-tracking-normal: 0;
61
+ --n-tracking-wide: 0.05em;
62
+ --n-space-1: 0.25rem;
63
+ --n-space-2: 0.5rem;
64
+ --n-space-3: 0.75rem;
65
+ --n-space-4: 1rem;
66
+ --n-space-5: 1.25rem;
67
+ --n-space-6: 1.5rem;
68
+ --n-space-8: 2rem;
69
+ --n-space-10: 2.5rem;
70
+ --n-space-12: 3rem;
71
+ --n-space-16: 4rem;
72
+ --n-radius-sm: 6px;
73
+ --n-radius-md: 10px;
74
+ --n-radius-lg: 14px;
75
+ --n-radius-xl: 20px;
76
+ --n-radius-2xl: 28px;
77
+ --n-radius-full: 9999px;
78
+ --n-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
79
+ --n-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
80
+ --n-shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
81
+ --n-shadow-xl: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
82
+ --n-shadow-glow-primary: 0 4px 15px -3px rgba(37, 99, 235, 0.3);
83
+ --n-shadow-glow-danger: 0 4px 15px -3px rgba(220, 38, 38, 0.3);
84
+ --n-transition-fast: 0.15s ease;
85
+ --n-transition-normal: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
86
+ --n-transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
87
+ --n-transition-spring: 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
88
+ --n-z-dropdown: 100;
89
+ --n-z-sticky: 200;
90
+ --n-z-overlay: 500;
91
+ --n-z-modal: 2000;
92
+ --n-z-toast: 3000;
93
+ --n-z-tooltip: 1000;
94
+ }
95
+ [data-theme="light"] {
96
+ --n-color-primary: #2563eb;
97
+ --n-color-primary-hover: #1d4ed8;
98
+ --n-color-primary-active: #1e40af;
99
+ --n-color-primary-light: rgba(37, 99, 235, 0.08);
100
+ --n-color-primary-glow: rgba(37, 99, 235, 0.2);
101
+ --n-color-success: #059669;
102
+ --n-color-success-light: rgba(5, 150, 105, 0.08);
103
+ --n-color-warning: #d97706;
104
+ --n-color-warning-light: rgba(217, 119, 6, 0.08);
105
+ --n-color-danger: #dc2626;
106
+ --n-color-danger-light: rgba(220, 38, 38, 0.08);
107
+ --n-color-info: #0891b2;
108
+ --n-color-info-light: rgba(8, 145, 178, 0.08);
109
+ --n-color-surface: #ffffff;
110
+ --n-color-surface-alt: #f8fafc;
111
+ --n-color-surface-hover: #f1f5f9;
112
+ --n-color-surface-elevated: #ffffff;
113
+ --n-color-bg: #f8fafc;
114
+ --n-color-bg-alt: #f1f5f9;
115
+ --n-color-text: #0f172a;
116
+ --n-color-text-secondary: #475569;
117
+ --n-color-text-muted: #94a3b8;
118
+ --n-color-text-inverse: #f8fafc;
119
+ --n-color-border: rgba(0, 0, 0, 0.08);
120
+ --n-color-border-hover: rgba(0, 0, 0, 0.15);
121
+ --n-color-border-active: rgba(0, 0, 0, 0.25);
122
+ --n-color-overlay: rgba(15, 23, 42, 0.5);
123
+ --n-color-glass: rgba(255, 255, 255, 0.6);
124
+ --n-color-glass-border: rgba(0, 0, 0, 0.06);
125
+ --n-color-glass-hover: rgba(255, 255, 255, 0.8);
126
+ --n-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
127
+ --n-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.06);
128
+ --n-shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.08);
129
+ --n-shadow-xl: 0 25px 50px -12px rgba(0, 0, 0, 0.12);
130
+ --n-shadow-glow-primary: 0 4px 15px -3px rgba(37, 99, 235, 0.2);
131
+ --n-shadow-glow-danger: 0 4px 15px -3px rgba(220, 38, 38, 0.2);
132
+ }
133
+ `;
134
+ if (typeof document !== 'undefined') {
135
+ const styleId = 'nexa-ui-tokens';
136
+ let el = document.getElementById(styleId);
137
+ if (!el) {
138
+ el = document.createElement('style');
139
+ el.id = styleId;
140
+ document.head.appendChild(el);
141
+ }
142
+ el.textContent = tokenCSS;
143
+ }
144
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "nexa-ui-kit",
3
+ "version": "0.6.0",
4
+ "description": "Premium component library for Nexa Framework",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src"
19
+ ],
20
+ "dependencies": {
21
+ "nexa-framework": "0.6.0",
22
+ "nexa-mobile": "0.6.0"
23
+ },
24
+ "devDependencies": {
25
+ "cpx": "^1.5.0",
26
+ "nexa-compiler": "0.6.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc && node scripts/compile-nexa.js && node scripts/patch-imports.js && cpx \"src/**/*.nexa\" dist",
30
+ "dev": "tsc --watch"
31
+ }
32
+ }
@@ -0,0 +1,115 @@
1
+ <script setup>
2
+ import { signal } from 'nexa-framework'
3
+
4
+ const props = defineProps({
5
+ variant: { type: String, default: 'info' },
6
+ title: { type: String, default: '' },
7
+ closable: { type: Boolean, default: false },
8
+ icon: { type: String, default: '' }
9
+ })
10
+
11
+ const emit = defineEmits(['close'])
12
+
13
+ const visible = signal(true)
14
+
15
+ const dismiss = () => {
16
+ visible.value = false
17
+ emit('close')
18
+ }
19
+
20
+ const icons = { success: '✓', error: '✕', warning: '⚡', info: 'ℹ' }
21
+ </script>
22
+
23
+ <template>
24
+ <div v-if="visible.value" class="n-alert" :class="`is-${variant}`">
25
+ <span class="n-alert-icon">{{ icon || icons[variant] || 'ℹ' }}</span>
26
+ <div class="n-alert-body">
27
+ <span v-if="title" class="n-alert-title">{{ title }}</span>
28
+ <span class="n-alert-text"><slot /></span>
29
+ </div>
30
+ <button v-if="closable" class="n-alert-close" @click="dismiss">&times;</button>
31
+ </div>
32
+ </template>
33
+
34
+ <style scoped>
35
+ .n-alert {
36
+ display: flex;
37
+ align-items: flex-start;
38
+ gap: var(--n-space-3);
39
+ padding: var(--n-space-4) var(--n-space-5);
40
+ border-radius: var(--n-radius-md);
41
+ border: 1px solid transparent;
42
+ font-size: var(--n-text-sm);
43
+ line-height: var(--n-leading-normal);
44
+ animation: n-alert-in 0.25s ease-out;
45
+ }
46
+
47
+ @keyframes n-alert-in {
48
+ from { opacity: 0; transform: translateY(-8px); }
49
+ to { opacity: 1; transform: translateY(0); }
50
+ }
51
+
52
+ .is-info {
53
+ background: var(--n-color-primary-light);
54
+ border-color: rgba(59, 130, 246, 0.2);
55
+ color: var(--n-color-primary);
56
+ }
57
+
58
+ .is-success {
59
+ background: var(--n-color-success-light);
60
+ border-color: rgba(16, 185, 129, 0.2);
61
+ color: var(--n-color-success);
62
+ }
63
+
64
+ .is-warning {
65
+ background: var(--n-color-warning-light);
66
+ border-color: rgba(245, 158, 11, 0.2);
67
+ color: var(--n-color-warning);
68
+ }
69
+
70
+ .is-error {
71
+ background: var(--n-color-danger-light);
72
+ border-color: rgba(239, 68, 68, 0.2);
73
+ color: var(--n-color-danger);
74
+ }
75
+
76
+ .n-alert-icon {
77
+ font-size: var(--n-text-base);
78
+ font-weight: var(--n-weight-bold);
79
+ flex-shrink: 0;
80
+ line-height: 1.4;
81
+ }
82
+
83
+ .n-alert-body {
84
+ flex: 1;
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: var(--n-space-1);
88
+ color: var(--n-color-text);
89
+ }
90
+
91
+ .n-alert-title {
92
+ font-weight: var(--n-weight-semibold);
93
+ font-size: var(--n-text-sm);
94
+ }
95
+
96
+ .n-alert-text {
97
+ color: var(--n-color-text-secondary);
98
+ }
99
+
100
+ .n-alert-close {
101
+ background: transparent;
102
+ border: none;
103
+ color: var(--n-color-text-muted);
104
+ font-size: var(--n-text-lg);
105
+ cursor: pointer;
106
+ padding: 0;
107
+ line-height: 1;
108
+ flex-shrink: 0;
109
+ transition: color var(--n-transition-fast);
110
+ }
111
+
112
+ .n-alert-close:hover {
113
+ color: var(--n-color-text);
114
+ }
115
+ </style>
@@ -0,0 +1,58 @@
1
+ <script setup>
2
+ import { signal, computed, effect, onBeforeUnmount } from 'nexa-framework'
3
+ import { trackFloatingOverlay } from '../services/FloatingOverlay.js'
4
+ const props = defineProps({ modelValue: { type: String, default: '' }, options: { type: Array, default: () => [] }, placeholder: { type: String, default: '' }, label: { type: String, default: '' }, disabled: { type: Boolean, default: false }, readonly: { type: Boolean, default: false }, clearable: { type: Boolean, default: false }, minLength: { type: Number, default: 1 }, delay: { type: Number, default: 200 }, loading: { type: Boolean, default: false }, dropdown: { type: Boolean, default: false }, emptyMessage: { type: String, default: 'No results' }, placement: { type: String, default: 'auto' } })
5
+ const emit = defineEmits(['update:modelValue', 'select', 'complete', 'clear'])
6
+ const instanceId = `n-ac-${Math.random().toString(16).slice(2)}`
7
+ const listboxId = `${instanceId}-listbox`
8
+ const inputId = `${instanceId}-input`
9
+ const isOpen = signal(false)
10
+ const query = signal('')
11
+ const focusedIndex = signal(-1)
12
+ const rootEl = signal(null)
13
+ const popupStyle = signal({})
14
+ const resolvedPlacement = signal('bottom')
15
+ let stopTracking = null
16
+ let completeTimer = null
17
+ const normalizeOption = (opt) => { if (opt == null) return { label: '', value: null, raw: opt }; if (typeof opt === 'string' || typeof opt === 'number') return { label: String(opt), value: opt, raw: opt }; if (typeof opt === 'object') { const label = 'label' in opt ? String(opt.label) : String(opt.value ?? ''); return { label, value: 'value' in opt ? opt.value : label, raw: opt } } return { label: String(opt), value: opt, raw: opt } }
18
+ const normalizedOptions = computed(() => props.options.map(normalizeOption))
19
+ const filteredOptions = computed(() => { const q = query.value.trim().toLowerCase(); if (!q) return normalizedOptions.value; return normalizedOptions.value.filter(opt => opt.label.toLowerCase().includes(q)) })
20
+ const activeId = computed(() => { const idx = focusedIndex.value; if (idx < 0) return ''; const opt = filteredOptions.value[idx]; return opt ? `${instanceId}-opt-${idx}` : '' })
21
+ const close = () => { if (!isOpen.value) return; isOpen.value = false; focusedIndex.value = -1; if (stopTracking) { stopTracking(); stopTracking = null } }
22
+ const open = (e) => { if (props.disabled || props.readonly || isOpen.value) return; isOpen.value = true; const t = e?.currentTarget || e?.target; rootEl.value = t?.closest ? t.closest(`[data-autocomplete-root="${instanceId}"]`) : null; stopTracking = trackFloatingOverlay({ isOpen: () => isOpen.value, getAnchor: () => { const root = rootEl.value; return root ? root.querySelector('.n-ac-input-wrap') : null }, getPopup: () => document.querySelector(`[data-autocomplete-popup="${instanceId}"]`), placement: props.placement, align: 'start', matchWidth: true, minWidth: 240, gap: 8, margin: 8, zIndex: 9999, onUpdate: (r) => { popupStyle.value = r.style; resolvedPlacement.value = r.placement }, isEventInside: (ev) => { const el = ev.target; if (!el || typeof el.closest !== 'function') return false; return !!(el.closest(`[data-autocomplete-root="${instanceId}"]`) || el.closest(`[data-autocomplete-popup="${instanceId}"]`)) }, onOutside: () => close() }) }
23
+ const requestComplete = () => { clearTimeout(completeTimer); completeTimer = setTimeout(() => emit('complete', query.value), props.delay) }
24
+ const onInput = (e) => { const value = e.target.value; query.value = value; emit('update:modelValue', value); if (value.trim().length >= props.minLength) { open(e); requestComplete() } else close() }
25
+ const onFocus = (e) => { query.value = props.modelValue || ''; if ((props.modelValue || '').trim().length >= props.minLength) { open(e); requestComplete() } }
26
+ const onKeydown = (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); return } if (!isOpen.value) { if (e.key === 'ArrowDown') { e.preventDefault(); open(e); focusedIndex.value = 0 } return } const items = filteredOptions.value; if (e.key === 'ArrowDown') { e.preventDefault(); focusedIndex.value = Math.min(focusedIndex.value + 1, items.length - 1); return } if (e.key === 'ArrowUp') { e.preventDefault(); focusedIndex.value = Math.max(focusedIndex.value - 1, 0); return } if (e.key === 'Enter') { e.preventDefault(); const opt = items[focusedIndex.value]; if (!opt) return; emit('update:modelValue', opt.label); emit('select', opt.raw); query.value = opt.label; close() } }
27
+ const selectOption = (opt) => { emit('update:modelValue', opt.label); emit('select', opt.raw); query.value = opt.label; close() }
28
+ const clearValue = () => { if (props.disabled || props.readonly) return; emit('update:modelValue', ''); emit('clear'); query.value = ''; close() }
29
+ const openAll = (e) => { if (props.disabled || props.readonly) return; query.value = props.modelValue || ''; open(e); requestComplete() }
30
+ effect(() => { if (!isOpen.value) return; if (focusedIndex.value < 0 && filteredOptions.value.length > 0) focusedIndex.value = 0 })
31
+ onBeforeUnmount(() => { clearTimeout(completeTimer); close() })
32
+ </script>
33
+
34
+ <template>
35
+ <div class="n-ac" :data-autocomplete-root="instanceId">
36
+ <label v-if="label" class="n-ac-label" :for="inputId">{{ label }}</label>
37
+ <div class="n-ac-input-wrap" :class="{ 'is-disabled': disabled }">
38
+ <input class="n-ac-input" :id="inputId" :value="modelValue" :placeholder="placeholder" :disabled="disabled" :readonly="readonly" role="combobox" aria-autocomplete="list" :aria-expanded="isOpen.value" :aria-controls="listboxId" :aria-activedescendant="activeId.value || undefined" @input="onInput" @focus="onFocus" @keydown="onKeydown" />
39
+ <div class="n-ac-actions">
40
+ <button v-if="clearable && modelValue" type="button" class="n-ac-action" :disabled="disabled || readonly" aria-label="Limpiar" @click="clearValue">✕</button>
41
+ <button v-if="dropdown" type="button" class="n-ac-action" :disabled="disabled || readonly" aria-label="Abrir" @click="openAll">▾</button>
42
+ </div>
43
+ </div>
44
+ <Teleport to="body">
45
+ <div v-if="isOpen.value" class="n-ac-popup" :class="{ 'is-top': resolvedPlacement.value === 'top' }" :data-autocomplete-popup="instanceId" :style="popupStyle.value">
46
+ <div v-if="loading" class="n-ac-loading">Loading...</div>
47
+ <div v-else class="n-ac-list" role="listbox" :id="listboxId">
48
+ <button v-for="(opt, i) in filteredOptions.value" :key="opt.value" type="button" class="n-ac-option" role="option" :id="instanceId + '-opt-' + i" :aria-selected="(i === focusedIndex.value).toString()" :class="{ 'is-focused': i === focusedIndex.value }" @mouseenter="focusedIndex.value = i" @click="selectOption(opt)">{{ opt.label }}</button>
49
+ <div v-if="filteredOptions.value.length === 0" class="n-ac-empty">{{ emptyMessage }}</div>
50
+ </div>
51
+ </div>
52
+ </Teleport>
53
+ </div>
54
+ </template>
55
+
56
+ <style scoped>
57
+ .n-ac{display:flex;flex-direction:column;gap:var(--n-space-2);width:100%;font-family:var(--n-font-sans)}.n-ac-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-ac-input-wrap{position:relative;display:flex;align-items:center;background:var(--n-color-surface);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);transition:all var(--n-transition-fast)}.n-ac-input-wrap:focus-within{border-color:var(--n-color-primary);box-shadow:0 0 0 3px var(--n-color-primary-light)}.n-ac-input{width:100%;background:transparent;border:none;outline:none;padding:0.75rem 2.75rem 0.75rem 1rem;color:var(--n-color-text);font-size:var(--n-text-base);font-family:inherit}.n-ac-input::placeholder{color:var(--n-color-text-muted)}.n-ac-actions{position:absolute;right:0.5rem;display:flex;align-items:center;gap:0.15rem}.n-ac-action{background:transparent;border:none;color:var(--n-color-text-muted);cursor:pointer;padding:0.25rem;border-radius:var(--n-radius-sm);transition:all var(--n-transition-fast);line-height:1;display:flex;align-items:center}.n-ac-action:hover:not(:disabled){color:var(--n-color-text);background:var(--n-color-glass)}.n-ac-action:disabled{opacity:0.5;cursor:not-allowed}.n-ac-input-wrap.is-disabled{opacity:0.5;cursor:not-allowed;background:var(--n-color-surface-alt)}.n-ac-popup{background:var(--n-color-surface);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);box-shadow:var(--n-shadow-lg);overflow:hidden;animation:n-ac-in .2s cubic-bezier(0,1,0,1)}.n-ac-popup.is-top{animation:n-ac-in-top .2s cubic-bezier(0,1,0,1)}@keyframes n-ac-in{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes n-ac-in-top{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.n-ac-loading{padding:var(--n-space-3) var(--n-space-4);color:var(--n-color-text-muted);font-size:var(--n-text-sm)}.n-ac-list{max-height:260px;overflow:auto;display:flex;flex-direction:column}.n-ac-option{text-align:left;padding:0.7rem 1rem;color:var(--n-color-text-secondary);background:transparent;border:none;cursor:pointer;transition:all var(--n-transition-fast)}.n-ac-option:hover,.n-ac-option.is-focused{background:var(--n-color-glass);color:var(--n-color-text)}.n-ac-empty{padding:var(--n-space-4);color:var(--n-color-text-muted);text-align:center;font-size:var(--n-text-sm)}
58
+ </style>
@@ -0,0 +1,67 @@
1
+ <script setup>
2
+ import { computed } from 'nexa-framework'
3
+
4
+ const props = defineProps({
5
+ label: { type: String, default: '' },
6
+ size: { type: String, default: 'md' },
7
+ variant: { type: String, default: 'primary' },
8
+ image: { type: String, default: '' },
9
+ shape: { type: String, default: 'rounded' } // rounded, circle, square
10
+ })
11
+
12
+ const initials = computed(() => {
13
+ if (!props.label) return '?'
14
+ return props.label
15
+ .split(' ')
16
+ .map(w => w[0])
17
+ .join('')
18
+ .toUpperCase()
19
+ .slice(0, 2)
20
+ })
21
+ </script>
22
+
23
+ <template>
24
+ <div
25
+ :class="['n-avatar', `is-${size}`, `is-${variant}`, `is-${shape}`]"
26
+ v-bind:style="image ? { backgroundImage: `url(${image})` } : {}"
27
+ :aria-label="label"
28
+ >
29
+ <span v-if="!image" class="n-avatar-initials">{{ initials.value }}</span>
30
+ </div>
31
+ </template>
32
+
33
+ <style scoped>
34
+ .n-avatar {
35
+ display: inline-flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ flex-shrink: 0;
39
+ background-size: cover;
40
+ background-position: center;
41
+ background-color: var(--n-color-surface-hover);
42
+ font-weight: var(--n-weight-bold);
43
+ color: var(--n-color-text);
44
+ user-select: none;
45
+ transition: all var(--n-transition-fast);
46
+ }
47
+
48
+ .is-sm { width: 32px; height: 32px; font-size: var(--n-text-xs); }
49
+ .is-md { width: 40px; height: 40px; font-size: var(--n-text-sm); }
50
+ .is-lg { width: 56px; height: 56px; font-size: var(--n-text-lg); }
51
+ .is-xl { width: 72px; height: 72px; font-size: var(--n-text-2xl); }
52
+
53
+ .is-circle { border-radius: var(--n-radius-full); }
54
+ .is-rounded { border-radius: var(--n-radius-md); }
55
+ .is-square { border-radius: 0; }
56
+
57
+ .is-primary { background-color: var(--n-color-primary); color: white; }
58
+ .is-success { background-color: var(--n-color-success); color: white; }
59
+ .is-danger { background-color: var(--n-color-danger); color: white; }
60
+ .is-warning { background-color: var(--n-color-warning); color: white; }
61
+ .is-info { background-color: var(--n-color-info); color: white; }
62
+
63
+ .n-avatar-initials {
64
+ line-height: 1;
65
+ font-weight: var(--n-weight-bold);
66
+ }
67
+ </style>
@@ -0,0 +1,61 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ variant: { type: String, default: 'primary' },
4
+ size: { type: String, default: 'md' },
5
+ rounded: { type: Boolean, default: false },
6
+ dot: { type: Boolean, default: false },
7
+ position: { type: String, default: '' }
8
+ })
9
+ </script>
10
+
11
+ <template>
12
+ <span v-if="dot" class="n-badge-dot" :class="[`is-${variant}`, position ? `is-${position}` : '']"></span>
13
+ <span v-else :class="['n-badge', `n-badge-${variant}`, `n-badge-${size}`, rounded ? 'is-rounded' : '']">
14
+ <slot />
15
+ </span>
16
+ </template>
17
+
18
+ <style scoped>
19
+ .n-badge {
20
+ display: inline-flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ padding: 0.25rem 0.6rem;
24
+ font-size: var(--n-text-xs);
25
+ font-weight: var(--n-weight-bold);
26
+ border-radius: var(--n-radius-sm);
27
+ line-height: 1;
28
+ font-family: var(--n-font-sans);
29
+ text-transform: uppercase;
30
+ letter-spacing: var(--n-tracking-wide);
31
+ transition: all var(--n-transition-fast);
32
+ }
33
+
34
+ .n-badge-primary { background: var(--n-color-primary); color: white; }
35
+ .n-badge-success { background: var(--n-color-success); color: white; }
36
+ .n-badge-danger { background: var(--n-color-danger); color: white; }
37
+ .n-badge-warning { background: var(--n-color-warning); color: white; }
38
+ .n-badge-info { background: var(--n-color-info); color: white; }
39
+ .n-badge-secondary { background: var(--n-color-surface-hover); color: var(--n-color-text-secondary); }
40
+
41
+ .n-badge-sm { padding: 0.15rem 0.4rem; font-size: 0.65rem; }
42
+ .n-badge-lg { padding: 0.4rem 0.8rem; font-size: var(--n-text-sm); }
43
+
44
+ .is-rounded { border-radius: var(--n-radius-full); }
45
+
46
+ .n-badge-dot {
47
+ display: inline-block;
48
+ width: 8px;
49
+ height: 8px;
50
+ border-radius: var(--n-radius-full);
51
+ vertical-align: middle;
52
+ flex-shrink: 0;
53
+ }
54
+
55
+ .n-badge-dot.is-primary { background: var(--n-color-primary); }
56
+ .n-badge-dot.is-success { background: var(--n-color-success); }
57
+ .n-badge-dot.is-danger { background: var(--n-color-danger); }
58
+ .n-badge-dot.is-warning { background: var(--n-color-warning); }
59
+ .n-badge-dot.is-info { background: var(--n-color-info); }
60
+ .n-badge-dot.is-secondary { background: var(--n-color-text-muted); }
61
+ </style>