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.
- package/alert-component/index.js +250 -0
- package/card-component/index.js +62 -0
- package/index.js +7 -0
- package/package.json +25 -0
|
@@ -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
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
|
+
}
|