dv-web-components 1.0.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.
@@ -0,0 +1,250 @@
1
+ /**
2
+ * ============================================================================
3
+ * Componente: <app-alert>
4
+ *
5
+ * Buenas prácticas aplicadas:
6
+ * 1. Template en memoria a nivel de módulo (se parsea una sola vez, no en cada instancia).
7
+ * 2. Encapsulación completa con Shadow DOM (modo 'open').
8
+ * 3. Ciclo de vida robusto: connectedCallback, disconnectedCallback y attributeChangedCallback.
9
+ * 4. Reflexión entre Atributos HTML y Propiedades JS (getters y setters).
10
+ * 5. Limpieza de Event Listeners al desconectarse para evitar fugas de memoria (memory leaks).
11
+ * 6. Slots con fallback inteligente (icono por defecto o personalizado por el usuario).
12
+ * 7. Comunicación hacia afuera con CustomEvent (bubbles: true, composed: true).
13
+ * 8. Accesibilidad nativa (ARIA roles y soporte de teclado).
14
+ * 9. CSS Theming mediante variables CSS y el pseudo-elemento ::part().
15
+ * ============================================================================
16
+ */
17
+
18
+ // 1. Template estático en memoria (Caché a nivel de módulo)
19
+ const template = document.createElement('template');
20
+ template.innerHTML = `
21
+ <style>
22
+ :host {
23
+ display: block;
24
+ box-sizing: border-box;
25
+ margin-block: 0.75rem;
26
+ font-family: inherit;
27
+ }
28
+
29
+ *, *::before, *::after {
30
+ box-sizing: border-box;
31
+ }
32
+
33
+ .alert {
34
+ display: flex;
35
+ align-items: flex-start;
36
+ gap: 0.875rem;
37
+ padding: 1rem 1.25rem;
38
+ border-radius: 0.75rem;
39
+ border: 1px solid var(--alert-border, rgba(255, 255, 255, 0.1));
40
+ background-color: var(--alert-bg, #1e293b);
41
+ color: var(--alert-text, #f8fafc);
42
+ font-size: 0.925rem;
43
+ line-height: 1.5;
44
+ transition: opacity 0.25s ease, transform 0.25s ease;
45
+ }
46
+
47
+ /* Animación de salida al cerrarse */
48
+ .alert--closing {
49
+ opacity: 0;
50
+ transform: scale(0.96) translateY(-4px);
51
+ }
52
+
53
+ /* Variantes semánticas mediante :host([variant="..."]) */
54
+ :host([variant="info"]) .alert {
55
+ --alert-bg: rgba(14, 165, 233, 0.1);
56
+ --alert-border: rgba(14, 165, 233, 0.3);
57
+ --alert-icon-color: #38bdf8;
58
+ }
59
+
60
+ :host([variant="success"]) .alert {
61
+ --alert-bg: rgba(34, 197, 94, 0.1);
62
+ --alert-border: rgba(34, 197, 94, 0.3);
63
+ --alert-icon-color: #4ade80;
64
+ }
65
+
66
+ :host([variant="warning"]) .alert {
67
+ --alert-bg: rgba(234, 179, 8, 0.1);
68
+ --alert-border: rgba(234, 179, 8, 0.3);
69
+ --alert-icon-color: #facc15;
70
+ }
71
+
72
+ :host([variant="danger"]) .alert {
73
+ --alert-bg: rgba(239, 68, 68, 0.1);
74
+ --alert-border: rgba(239, 68, 68, 0.3);
75
+ --alert-icon-color: #f87171;
76
+ }
77
+
78
+ /* Ranuras de contenido */
79
+ .icon-wrapper {
80
+ display: inline-flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ flex-shrink: 0;
84
+ color: var(--alert-icon-color, #38bdf8);
85
+ padding-top: 0.125rem;
86
+ }
87
+
88
+ .content-wrapper {
89
+ flex: 1;
90
+ min-width: 0;
91
+ }
92
+
93
+ /* Botón de cierre */
94
+ .close-button {
95
+ display: none;
96
+ background: transparent;
97
+ border: none;
98
+ color: inherit;
99
+ opacity: 0.6;
100
+ cursor: pointer;
101
+ padding: 0.25rem;
102
+ margin: -0.25rem -0.25rem -0.25rem auto;
103
+ border-radius: 0.375rem;
104
+ transition: opacity 0.2s ease, background-color 0.2s ease;
105
+ line-height: 1;
106
+ }
107
+
108
+ .close-button:hover,
109
+ .close-button:focus-visible {
110
+ opacity: 1;
111
+ background-color: rgba(255, 255, 255, 0.1);
112
+ outline: none;
113
+ }
114
+
115
+ /* Mostrar botón si tiene el atributo 'dismissible' */
116
+ :host([dismissible]) .close-button {
117
+ display: inline-flex;
118
+ }
119
+ </style>
120
+
121
+ <div class="alert" part="alert" role="alert" aria-live="polite">
122
+ <!-- Slot para icono con un SVG fallback -->
123
+ <span class="icon-wrapper" part="icon">
124
+ <slot name="icon">
125
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
126
+ <circle cx="12" cy="12" r="10"></circle>
127
+ <line x1="12" y1="16" x2="12" y2="12"></line>
128
+ <line x1="12" y1="8" x2="12.01" y2="8"></line>
129
+ </svg>
130
+ </slot>
131
+ </span>
132
+
133
+ <!-- Slot por defecto para el texto del mensaje -->
134
+ <div class="content-wrapper" part="content">
135
+ <slot>Mensaje de alerta por defecto.</slot>
136
+ </div>
137
+
138
+ <!-- Botón de cerrar opcional -->
139
+ <button class="close-button" part="close-button" aria-label="Cerrar alerta">
140
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
141
+ <line x1="18" y1="6" x2="6" y2="18"></line>
142
+ <line x1="6" y1="6" x2="18" y2="18"></line>
143
+ </svg>
144
+ </button>
145
+ </div>
146
+ `;
147
+
148
+ export class AppAlert extends HTMLElement {
149
+ #closeButton = null;
150
+ #alertContainer = null;
151
+
152
+ // 2. Definir los atributos observables
153
+ static get observedAttributes() {
154
+ return ['variant', 'dismissible'];
155
+ }
156
+
157
+ constructor() {
158
+ super();
159
+ // Atachar Shadow DOM
160
+ this.attachShadow({ mode: 'open' });
161
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
162
+
163
+ // Referencias internas cacheadas
164
+ this.#alertContainer = this.shadowRoot.querySelector('.alert');
165
+ this.#closeButton = this.shadowRoot.querySelector('.close-button');
166
+
167
+ // Binding para que el listener conserve el contexto correcto
168
+ this.handleDismiss = this.handleDismiss.bind(this);
169
+ }
170
+
171
+ // 3. Sincronización entre Atributos HTML y Propiedades JS (Reflection)
172
+ get variant() {
173
+ return this.getAttribute('variant') || 'info';
174
+ }
175
+
176
+ set variant(value) {
177
+ if (value) {
178
+ this.setAttribute('variant', value);
179
+ } else {
180
+ this.removeAttribute('variant');
181
+ }
182
+ }
183
+
184
+ get dismissible() {
185
+ return this.hasAttribute('dismissible');
186
+ }
187
+
188
+ set dismissible(value) {
189
+ if (Boolean(value)) {
190
+ this.setAttribute('dismissible', '');
191
+ } else {
192
+ this.removeAttribute('dismissible');
193
+ }
194
+ }
195
+
196
+ // 4. Ciclo de Vida: El elemento se conecta al DOM
197
+ connectedCallback() {
198
+ // Establecer variante por defecto si el usuario no especificó ninguna
199
+ if (!this.hasAttribute('variant')) {
200
+ this.setAttribute('variant', 'info');
201
+ }
202
+
203
+ // Registrar listeners
204
+ this.#closeButton?.addEventListener('click', this.handleDismiss);
205
+ }
206
+
207
+ // 5. Ciclo de Vida: El elemento se retira del DOM (Limpieza)
208
+ disconnectedCallback() {
209
+ this.#closeButton?.removeEventListener('click', this.handleDismiss);
210
+ }
211
+
212
+ // 6. Ciclo de Vida: Reaccionar a cambios de atributos en tiempo real
213
+ attributeChangedCallback(name, oldValue, newValue) {
214
+ if (oldValue === newValue) return;
215
+
216
+ if (name === 'variant') {
217
+ // Opcional: Lógica adicional si se requiere al cambiar la variante
218
+ }
219
+ }
220
+
221
+ // 7. Método público y despacho de Custom Events
222
+ close() {
223
+ this.handleDismiss();
224
+ }
225
+
226
+ handleDismiss() {
227
+ // Despachar evento antes de cerrar (permite ser cancelado con e.preventDefault())
228
+ const closeEvent = new CustomEvent('app-close', {
229
+ bubbles: true,
230
+ composed: true, // 👈 Crucial para que cruce la frontera del Shadow DOM hacia afuera
231
+ cancelable: true,
232
+ detail: { variant: this.variant }
233
+ });
234
+
235
+ const shouldContinue = this.dispatchEvent(closeEvent);
236
+ if (!shouldContinue) return;
237
+
238
+ // Transición suave de salida
239
+ this.#alertContainer.classList.add('alert--closing');
240
+
241
+ this.#alertContainer.addEventListener('transitionend', () => {
242
+ this.remove(); // Se auto-elimina del DOM tras la animación
243
+ }, { once: true });
244
+ }
245
+ }
246
+
247
+ // 8. Registro seguro del Custom Element
248
+ if (!customElements.get('app-alert')) {
249
+ customElements.define('app-alert', AppAlert);
250
+ }
@@ -0,0 +1,62 @@
1
+ const template = document.createElement('template');
2
+ template.innerHTML = `
3
+ <style>
4
+ :host {
5
+ display: block;
6
+ }
7
+ /* Dale estilo a cualquier botón que sea inyectado en un slot */
8
+ ::slotted(button) {
9
+ background-color: #38bdf8;
10
+ color: #0f172a;
11
+ border: none;
12
+ padding: 1rem 2rem;
13
+ border-radius: 6px;
14
+ font-weight: 600;
15
+ cursor: pointer;
16
+ }
17
+ ::slotted(button:hover) {
18
+ opacity: 0.9;
19
+ }
20
+ * {
21
+ box-sizing: border-box;
22
+ margin: 0;
23
+ padding: 0;
24
+ }
25
+ .container {
26
+ background-color: #0f172a;
27
+ padding: 10px;
28
+ border-radius: 10px;
29
+ width: 100%;
30
+ height: 100%;
31
+ }
32
+ </style>
33
+ <div class="container">
34
+ <header>
35
+ <slot name="header">
36
+ <h1>Header</h1>
37
+ </slot>
38
+ </header>
39
+ <main>
40
+ <slot>
41
+ <p>Content</p>
42
+ </slot>
43
+ </main>
44
+ <footer>
45
+ <slot name="footer">
46
+ <p>Footer</p>
47
+ </slot>
48
+ </footer>
49
+ </div>
50
+ `;
51
+
52
+ class AppCard extends HTMLElement {
53
+ constructor() {
54
+ super();
55
+ this.attachShadow({ mode: 'open' });
56
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
57
+ }
58
+ }
59
+
60
+ if (!customElements.get('app-card')) {
61
+ customElements.define('app-card', AppCard);
62
+ }
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { Card } from './card-component/index.js';
2
+ import { Alert } from './alert-component/index.js';
3
+
4
+ export {
5
+ Card,
6
+ Alert
7
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "dv-web-components",
3
+ "version": "1.0.0",
4
+ "description": "Librería de Web Components nativos y accesibles",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "module": "./index.js",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./alert": "./alert-component/index.js",
11
+ "./card": "./card-component/index.js"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "alert-component",
16
+ "card-component"
17
+ ],
18
+ "keywords": [
19
+ "web-components",
20
+ "custom-elements",
21
+ "ui"
22
+ ],
23
+ "author": "Diego Villa",
24
+ "license": "MIT"
25
+ }